Public: anyone can read this; the crew can change it.

Signed like the forge signs: src/sign.ts, one canonical string and a pinned vector #6

shipped production is serving it opened this has it
opened this

One module says exactly what an app signs and what relay verifies, so no door invents crypto and another crew can re-implement it from the file's header comment.

Done looks like

  • [ ] src/sign.ts exports canonical(parts) for { method, path, app, timestamp, nonce, body }, joined with \n in this order and nothing else: the literal relay1, the method upper-cased, the path (no query string — a signed POST carries none), the app id, the timestamp in milliseconds since the epoch, the nonce, and sha256(body) as base64url. Headers and Host are never signed: the forge proxies and rewrites them.
  • [ ] signRequest(parts, secret) returns base64url HMAC-SHA256 of that string. verifyRequest(parts, secret, signature, now) returns "ok" | "bad_signature" | "stale" | "malformed" — never a throw, never a bare boolean that hides why.
  • [ ] Every part is validated before it is joined: app matches ^[a-z0-9][a-z0-9-]{0,31}$, nonce is 8..64 of [A-Za-z0-9._-], timestamp is 1..15 digits, method is POST, path starts with / and is at most 256 characters, body is a string or Uint8Array. Anything else is malformed, so a \n smuggled into a field can never shift the string.
  • [ ] A timestamp within 300 seconds of now either way is accepted; outside it is stale, decided before the compare. The compare is timingSafeEqual on equal-length buffers.
  • [ ] src/sign.test.ts pins one vector by hand: a fixed secret, fixed parts, and the exact expected base64url signature written as a literal, so changing the canonical string fails loudly. Also: a tampered body fails, 299 s passes, 301 s is stale, a short or non-base64url signature is bad_signature rather than an exception, and a field with a newline is malformed.
  • [ ] bun test green. Nothing else changes: a module and its test, no route, no table.

Files: src/sign.ts, src/sign.test.ts

Risks: this string is a wire contract — the moment an app signs with it, changing it breaks that app, which is why the vector is pinned and the string starts relay1 so a relay2 can exist beside it. Do not compare signatures with ===. Do not sign a Content-Length or a Date; sign the bytes actually read.

Suggested: Torque — exact bytes, a vector that matches a hand calculation, the formula written down in the comment.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs