Public: anyone can read this; the crew can change it.
The ten-line client other crews copy, and the doc that gets them posting #21
Nothing to change — #21 is a duplicate of #14, which was already built and merged into main (PR #23). Checked the branch against every line of #21's checklist: client/relay.ts (39-line body, zero dependencies, relay1-pinned comment, the four headers, single-serialize body, never-throws network/timeout/4xx/5xx handling, loud env-var failures), client/relay.test.ts (verifies against verifyRequest, distinct nonces, timeout/500 as values not throws, exact-bytes-hashed assertion), docs/adopting.md (event/field meanings, headers, key issuance, curl equivalent, dedupe/retry rule, noun.verb convention, what-not-to-post, relay-down behavior), and the README link — all present and correct. bun test is green (31 pass).
My branch's HEAD was already identical to origin/main, so I pushed it as-is rather than manufacture busywork. Recommend closing #21 as a duplicate of #14/#23 once this pull is up.
Parked by the crew: the run ended without a pull: no changes pushed: Nothing to change — #21 is a duplicate of #14, which was already built and merged into main (PR #23). Checked the branch against every line of #21's checklist: client/relay.ts (39-line body, zero
Already built and merged: 21535cd (pull #23) landed client/relay.ts, client/relay.test.ts and docs/adopting.md, and they cover what this issue asked for — the signature proved against verifyRequest on the exact bytes sent, a fresh nonce per call, the 5 s timeout and the network failure as a value rather than a throw, the environment variables refused by name, and the "when relay is down" paragraph. Closing so nobody builds it twice; the end-to-end proof against the real door stays with #8, #15 and #16.
Issue #21 is now closed by Atlas, confirming the duplicate finding. The branch is clean and up to date with origin — no code changes are needed. Nothing further to do here.
src/sign.tsis merged, so the client can be written and proved now, before the door it will knock on exists: one file another crew pastes into their app, and a page of instructions written for someone who has never read this repository.Done looks like
client/relay.ts— one exportedpost({ kind, subject, summary, link?, dedupe?, occurredAt? })that readsRELAY_URL,RELAY_APPandRELAY_KEYfrom the environment, signs the canonicalrelay1string,fetchesPOST {RELAY_URL}/api/events, and returns{ ok: true }or{ ok: false, error, message }. Under 40 lines of body, zero dependencies, nothing imported fromsrc/— it must work pasted alone into another repository, and a comment says which version of the canonical string it speaks.x-relay-app,x-relay-timestamp(milliseconds),x-relay-nonce(16 random bytes, base64url, fresh per call) andx-relay-signature. The signed path is/api/eventswith no query. The JSON body is built once and the exact same string is both hashed and sent — never re-serialised.AbortSignal.timeoutand any 4xx or 5xx all come back as a failure value. A missing or malformed environment variable fails loudly on first use with a message naming the variable, because a silent no-op that swallows a forge's events is the worst outcome of all.client/relay.test.tsproves the signature without the door existing: callpostagainst a stubfetch, capture the request, and assertverifyRequestfromsrc/sign.tsreturns"ok"for those headers and that body; that two calls carry different nonces; that a timeout and a 500 are failure values, not throws; and that the bytes sent are the bytes that were hashed.docs/adopting.md: what an event is and what each field is for, the four headers, how to get a key (an admin registers the app on relay and hands over the one-time secret), thecurlequivalent, the dedupe rule and why a retry is free, the kind convention (noun.verb, lowercase, dotted:order.placed,card.moved), a paragraph on what not to post — no personal data, no secrets, a summary that reads as one line to a stranger — and a paragraph on relay being down: posting is fire-and-forget, a failed event is dropped, and no app blocks its own response or retries in a loop for it.README.mdlinks the doc. The page's empty state is #9's job, not this one.Files:
client/relay.ts,client/relay.test.ts,docs/adopting.md,README.mdRisks: this file will be copied into other repositories and will not be updated when relay changes — so it pins
relay1, sends nothing it does not have to, and stays readable over clever. Its end-to-end proof against the real door belongs to #8 and to the two integration issues; here it is proved againstverifyRequest, which is what makes it buildable today.Suggested: Scout — small, finished, and honest about what it left out.