Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions netlify/functions/execution-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connectLambda, getStore } from "@netlify/blobs";
import { Redis } from "@upstash/redis";
import { timingSafeEqual } from "node:crypto";

type NetlifyEvent = {
blobs?: string;
Expand Down Expand Up @@ -58,6 +59,29 @@ function normalizeLambdaHeaders(headers: NetlifyEvent["headers"]) {
);
}

function getHeader(headers: NetlifyEvent["headers"], name: string) {
const direct = headers[name];
const lower = headers[name.toLowerCase()];
return direct ?? lower;
}

function safeTokenEquals(actual: string, expected: string) {
const actualBuffer = Buffer.from(actual);
const expectedBuffer = Buffer.from(expected);
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
}

function isWriteAuthorized(event: NetlifyEvent) {
const expectedToken = process.env.EXECUTION_LEDGER_WRITE_TOKEN;
if (!expectedToken) return false;

const authHeader = getHeader(event.headers, "authorization");
const bearerToken = authHeader?.match(/^Bearer\s+(.+)$/i)?.[1];
const token = bearerToken ?? getHeader(event.headers, "x-execution-ledger-token");

return Boolean(token && safeTokenEquals(token, expectedToken));
}

function isLedgerRows(value: unknown): value is HistoricalExecutionLedgerEntry[] {
return Array.isArray(value);
}
Expand Down Expand Up @@ -177,6 +201,14 @@ function parseBody(event: NetlifyEvent) {
}

export const handler = async (event: NetlifyEvent) => {
if (event.httpMethod === "POST" && !isWriteAuthorized(event)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore a server-side path for ledger writes

On Netlify, /api/* is redirected to this function, and the only in-repo ledger writer I found is the browser call in src/lib/executionLedgerStore.ts, which sends only Content-Type. With this new check, normal board syncs now always get 403 because the server-only EXECUTION_LEDGER_WRITE_TOKEN cannot be safely attached from the browser, so the shared proof ledger stops accumulating rows and silently falls back to per-browser storage. Add a trusted server-side writer/proxy before enforcing the token on this endpoint.

Useful? React with 👍 / 👎.

return response(403, {
configured: true,
rows: [],
message: "Shared execution ledger writes require a server-side write token.",
});
}

const store = getLedgerStore(event);

if (!store) {
Expand Down
Loading