@facet-llc/origin-hmac
v0.1.0
Published
HMAC-SHA256 origin trust header for Facet enforcement. Sign outbound origin requests at the edge or Terminal; verify on the backing origin to refuse direct-bypass traffic. Workers, Deno, and Node compatible (Web Crypto only).
Readme
@facet-llc/origin-hmac
HMAC-SHA256 origin trust header for Facet enforcement.
When the Facet Terminal (or any Facet Worker / Edge Function) forwards a request to a backing dynamic origin, the origin needs a way to verify the request actually transited the Facet edge — and isn't a direct attacker bypassing whatever Cloudflare/Netlify rules sit in front. This package signs the outbound request with a shared HMAC secret; the origin verifies on receipt.
Pure Web Crypto, no Node built-ins. Workers, Deno, and Node 20+ all supported.
Wire format
X-Facet-Origin-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>Signature payload:
${timestamp}.${method}.${path}.${body_sha256_hex}body_sha256_hex is the lowercase-hex SHA-256 of the request body. For GET / HEAD it's a fixed constant (the SHA-256 of empty bytes).
Install
npm install @facet-llc/origin-hmac
# or pnpm add @facet-llc/origin-hmacSign at the edge
import { signRequest } from "@facet-llc/origin-hmac";
export default {
async fetch(request, env, ctx) {
const signed = await signRequest(request, { secret: env.FACET_ORIGIN_HMAC_SECRET });
return fetch(signed);
},
};Verify at the origin
import { verifyRequest, OriginHmacError } from "@facet-llc/origin-hmac";
export async function handler(request: Request): Promise<Response> {
try {
await verifyRequest(request, { secret: process.env.FACET_ORIGIN_HMAC_SECRET! });
} catch (e) {
if (e instanceof OriginHmacError) {
return new Response(`origin trust failed: ${e.code}`, { status: 401 });
}
throw e;
}
return doActualWork(request);
}Replay protection
The verifier rejects timestamps outside ±300 seconds of its own clock by default. Pass toleranceSeconds to widen or tighten:
await verifyRequest(request, {
secret: env.FACET_ORIGIN_HMAC_SECRET,
toleranceSeconds: 60, // strict 1-minute window
});Error codes
| Code | When |
| -------------------- | ------------------------------------------------------------------------------ |
| SECRET_MISSING | The secret is empty or undefined. |
| MISSING_HEADER | No X-Facet-Origin-Signature header on the request. |
| MALFORMED_HEADER | Header present but doesn't parse to t=<int>,v1=<64-hex>. |
| STALE_TIMESTAMP | t is older than now - toleranceSeconds. |
| FUTURE_TIMESTAMP | t is newer than now + toleranceSeconds (clock skew or attacker). |
| BAD_SIGNATURE | HMAC mismatch. Wrong secret, tampered body, tampered path, or tampered method. |
| BODY_READ_FAILED | Couldn't read the request body for hashing. |
| CRYPTO_UNAVAILABLE | Web Crypto unavailable. (Should never fire on Workers / Deno / Node 20+.) |
Threat model
This package defends against direct-origin bypass: an attacker who has discovered the origin URL and connects to it directly, skipping the Facet edge. With this header in place, the origin refuses any request that didn't come through a signer holding the shared secret.
It does not defend against:
- A compromised secret (rotate it)
- An attacker who can hit the Facet edge (the edge's classifier + WAF rules are the boundary there)
- Replay within the tolerance window (use idempotency keys at the origin if this matters)
Build
pnpm install
pnpm --filter @facet-llc/origin-hmac build
pnpm --filter @facet-llc/origin-hmac testLicense
Apache-2.0.
