@growth-labs/foundry-signing
v0.1.0
Published
The Foundry route-family HMAC: the legacy dot string and the host-bound foundry-cms-v2 string, one verifier, and the shared golden vectors.
Readme
@growth-labs/foundry-signing
The Foundry route-family HMAC, in one place. Foundry (fulcrum-labs/foundry
articlechain/publishauth.go) signs every request to a site's Foundry routes
with one estate-wide secret; every Workers-side verifier imports this package
instead of keeping its own copy.
| Form | Signed string | Used by |
| --- | --- | --- |
| legacy | METHOD.PATH.TIMESTAMP.sha256hex(body) | cms Foundry routes on publications that have not opted in |
| foundry-cms-v2 | "foundry-cms-v2\nMETHOD\nHOST\nPATH\nTIMESTAMP\nsha256hex(body)" (no trailing newline) | cms Foundry routes on opted-in publications; the video import route |
HOST is the site's configured canonical host (lowercase, no scheme or
port), never the request's Host header. The secret is shared by every site, so
binding the host is what stops a request signed for one site verifying on
another. The signature is lowercase hex HMAC-SHA256 in X-Foundry-Signature
(an optional sha256= prefix is accepted); X-Foundry-Timestamp is unix
seconds within ±300 s, and the string is verified over the timestamp text the
request carried.
import { isCanonicalHost, verifyFoundryRequest } from '@growth-labs/foundry-signing'
const ok = await verifyFoundryRequest(secret, {
method: request.method,
host: isCanonicalHost(configuredHost) ? configuredHost : null, // null = legacy string
path: new URL(request.url).pathname,
timestampHeader: request.headers.get('X-Foundry-Timestamp'),
signatureHeader: request.headers.get('X-Foundry-Signature'),
body: rawBody,
})Also exported: signFoundryRequest (the producer side, for tests and tools),
foundrySignedString, sha256Hex, isCanonicalHost, FOUNDRY_CMS_V2 and
FOUNDRY_SIGNATURE_MAX_SKEW_SECONDS.
Golden vectors
@growth-labs/foundry-signing/fixtures exports GOLDEN_SECRET,
GOLDEN_TIMESTAMP and GOLDEN_VECTORS: the foundry-cms-v2 vectors agreed
with Foundry's Go signer (SignPublishRequestV2). Every consumer's tests import
them, so the TypeScript and Go halves are proven byte-equal and cannot drift.
Zero runtime dependencies; WebCrypto only, so it runs unchanged in Workers and Node.
