@byollm/server 0.1.0-alpha.87 → 0.1.0-alpha.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.87`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.89`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npm install @byollm/server@alpha`.
5
5
  >
@@ -38,10 +38,31 @@ function secretsMatch(aHex, bHex) {
38
38
  }
39
39
  var generateLeaseId = () => randomUUID();
40
40
 
41
+ // src/sealed-outcome.ts
42
+ import { SealedOutcome } from "@byollm/protocol";
43
+ function openSealedOutcome(input) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(input.plaintext);
47
+ } catch {
48
+ return { ok: false, why: "the sealed result was not valid JSON" };
49
+ }
50
+ const sealed = SealedOutcome.safeParse(parsed);
51
+ if (!sealed.success) {
52
+ return { ok: false, why: "the sealed result was not an outcome" };
53
+ }
54
+ if (sealed.data.outcome.outcome !== input.disposition) {
55
+ return {
56
+ ok: false,
57
+ why: "the declared disposition is not the one that was sealed"
58
+ };
59
+ }
60
+ return { ok: true, value: sealed.data };
61
+ }
62
+
41
63
  // src/handlers.ts
42
64
  import {
43
65
  FetchRequest,
44
- SealedOutcome,
45
66
  keyId as keyId2,
46
67
  open as open2,
47
68
  publicIdentityOf as publicIdentityOf2,
@@ -490,18 +511,12 @@ var ByollmHandlers = class {
490
511
  if (!opened.ok) {
491
512
  return refuse("the result did not verify as coming from this device");
492
513
  }
493
- let parsed;
494
- try {
495
- parsed = JSON.parse(opened.plaintext);
496
- } catch {
497
- return refuse("the sealed result was not valid JSON");
498
- }
499
- const sealed = SealedOutcome.safeParse(parsed);
500
- if (!sealed.success) return refuse("the sealed result was not an outcome");
501
- if (sealed.data.outcome.outcome !== request.disposition) {
502
- return refuse("the declared disposition is not the one that was sealed");
503
- }
504
- return { ok: true, value: sealed.data };
514
+ const outcome = openSealedOutcome({
515
+ plaintext: opened.plaintext,
516
+ disposition: request.disposition
517
+ });
518
+ if (!outcome.ok) return refuse(outcome.why);
519
+ return { ok: true, value: outcome.value };
505
520
  }
506
521
  // -- 5. release -----------------------------------------------------------
507
522
  async #release(request, runner) {
@@ -651,6 +666,7 @@ export {
651
666
  hashSecret,
652
667
  secretsMatch,
653
668
  generateLeaseId,
669
+ openSealedOutcome,
654
670
  deadlineFor,
655
671
  resealForDevice,
656
672
  ByollmHandlers,
@@ -659,4 +675,4 @@ export {
659
675
  signatureFrom,
660
676
  createFetchHandler
661
677
  };
662
- //# sourceMappingURL=chunk-5WS55FRU.js.map
678
+ //# sourceMappingURL=chunk-36Y77FUD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ids.ts","../src/sealed-outcome.ts","../src/handlers.ts","../src/reseal.ts","../src/records.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n // A bare UUID, not a prefixed one.\n //\n // The app mints this now, because byollm_009 §6 binds the job id into the\n // envelope's signature — so the id must exist before the row does. A\n // `job_`-prefixed string is not a `uuid`, and the Supabase adapter's column\n // is, so the prefix would have made every enqueue fail there while passing\n // in memory. Ids are opaque to the protocol; the prefix was only ever\n // decoration.\n return randomUUID();\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n\n/**\n * A fresh id for one lease grant.\n *\n * Not a secret and not guessed at — a daemon is told its lease id in the claim\n * response. It exists to distinguish *this* grant from the next one over the\n * same job by the same runner, which is what stops a replayed release landing\n * on a lease the sender never meant.\n */\nexport const generateLeaseId = (): string => randomUUID();\n","import { SealedOutcome, type JobOutcome } from \"@byollm/protocol\";\n\n/**\n * Open a sealed outcome, once, for both lanes — B064 step 4's prerequisite.\n *\n * `handlers.ts` and `cloud.ts` each did the same three things to a decrypted\n * envelope: parse the JSON, validate it as a {@link SealedOutcome}, and check\n * the clear-text disposition against what was sealed. Two copies of one\n * decision, and the only difference between them was how they reported a\n * failure — a refusal message on the direct lane, `null` on the cloud lane.\n *\n * **Extracted BEFORE the shape changes rather than after.** Step 4 adds a\n * field to what the daemon seals, and a field added to two independent\n * readers is a field added correctly to one of them: the direct lane and the\n * cloud lane would agree until the day they did not, and the lane that broke\n * is the one Kevin is on. Instruction 9 — one definition, both ends — and\n * here both ends are two files in the same package.\n *\n * The disposition check has to live inside this rather than beside it. It is\n * the reason the function exists at all: byollm_009 §6.1 puts it here because\n * **this is the only party that can open the envelope**, so it is the only\n * place the relay's clear-text routing hint can be checked against the truth.\n * Left to the callers it would be a step somebody forgets in the third lane.\n */\nexport type OpenedOutcome =\n | { readonly ok: true; readonly value: SealedOutcome }\n | { readonly ok: false; readonly why: string };\n\nexport function openSealedOutcome(input: {\n /** The decrypted envelope body. */\n readonly plaintext: string;\n /**\n * What the relay was told this job became.\n *\n * Checked, never trusted: it travelled in the clear and the sealed copy is\n * the one the device signed.\n */\n readonly disposition: JobOutcome[\"outcome\"];\n}): OpenedOutcome {\n let parsed: unknown;\n try {\n parsed = JSON.parse(input.plaintext);\n } catch {\n return { ok: false, why: \"the sealed result was not valid JSON\" };\n }\n\n const sealed = SealedOutcome.safeParse(parsed);\n if (!sealed.success) {\n return { ok: false, why: \"the sealed result was not an outcome\" };\n }\n\n if (sealed.data.outcome.outcome !== input.disposition) {\n return {\n ok: false,\n why: \"the declared disposition is not the one that was sealed\",\n };\n }\n\n return { ok: true, value: sealed.data };\n}\n","import { openSealedOutcome } from \"./sealed-outcome.js\";\nimport {\n FetchRequest,\n type SealedOutcome,\n keyId,\n open,\n publicIdentityOf,\n type FetchResponse,\n RequestSignature,\n verifyRequest,\n verifyPublicIdentity,\n type StoredKeys,\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\n/**\n * What a transport must hand the handler to authenticate a call.\n *\n * `rawBody` is the exact bytes received, not a re-serialisation of the parsed\n * object: JSON.stringify does not round-trip byte-for-byte, and a signature\n * over re-serialised input verifies something the sender never signed.\n */\nexport interface AuthContext {\n readonly endpoint: string;\n readonly rawBody: string;\n readonly signature: unknown;\n}\n\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n /**\n * This site's keypairs (byollm_009 §5) — **supplied, never generated here.**\n *\n * A site is usually more than one process. Generating keys at startup would\n * work perfectly in development and fail only in production, silently: each\n * instance would have a different identity, a daemon would pin whichever\n * one approved its pairing, and every request routed to a different\n * instance would fail a signature check it had no way to explain. So this\n * is a required input, and there is a `keygen` script that produces one.\n */\n readonly siteKeys: StoredKeys;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n readonly #siteKeys: StoredKeys;\n /** This site's identity key id — Amendment A's `stub.site`. Derived once. */\n readonly #siteKeyId: string;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n // Fail at construction, not at the first pairing. A site whose keys are\n // malformed should not start and then refuse its users one at a time.\n if (!verifyPublicIdentity(publicIdentityOf(config.siteKeys))) {\n throw new Error(\n \"siteKeys are not internally consistent: the encryption key is not \" +\n \"signed by the identity key. Generate a fresh pair with \" +\n \"`npx @byollm/server keygen`.\",\n );\n }\n this.#siteKeys = config.siteKeys;\n this.#siteKeyId = keyId(publicIdentityOf(config.siteKeys).identity);\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param auth - the signature and the exact bytes it covers\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n auth: AuthContext,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(auth, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on — and since V1-2 it\n // travels as the refusal itself ({@link MUSTS.REVOCATION_HONORED}).\n //\n // It used to be answered with an empty site set, which the daemon\n // read as \"revoked\". That reading is gone: an empty set now means\n // \"nothing is consented right now\", because a projection can arrive\n // empty by accident and the daemon's response to revocation is to\n // delete its pairing. So the one call every daemon always makes — a\n // daemon with no working backend never claims — carries the\n // unambiguous version: 403 with `revoked`, which is a code and not an\n // inference.\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n );\n case \"fetch\":\n return this.#authed(auth, body, FetchRequest, this.#fetch.bind(this));\n case \"result\":\n return this.#authed(auth, body, ResultRequest, this.#result.bind(this));\n case \"release\":\n return this.#authed(\n auth,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: verify the\n * signature, reject a revoked runner, and parse the body.\n *\n * Authentication happens before schema validation so a stranger probing the\n * endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n auth: AuthContext,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(\"unauthorized\", \"this request is not signed\");\n }\n\n const runner = await this.#store.getRunner(signature.data.runnerId);\n if (!runner) {\n return fail(\"unauthorized\", \"this runner is not recognised\");\n }\n\n // Verified against the identity pinned when the user approved this\n // machine — not against anything the request carries. A signature that\n // authenticates itself authenticates nothing.\n const failure = verifyRequest({\n identityPublic: runner.device.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#now(),\n });\n if (failure !== null) {\n // Deliberately one message for both causes. Telling a caller whether\n // their clock or their key is wrong tells an attacker which half of a\n // forgery already works.\n return fail(\"unauthorized\", \"this request's signature is not valid\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n /**\n * Hand over the payload for a lease this runner holds — byollm_009 §6.\n *\n * The second half of claim-then-fetch. A claim answers with a stub, and the\n * work itself is collected separately by the device that took it, because a\n * payload can only be sealed once its recipient is known.\n *\n * Scoped to the lease, not the job: answering for whatever lease happens to\n * exist would hand the work to a runner whose grant had already been\n * superseded.\n */\n async #fetch(\n request: FetchRequest,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n const job = await this.#store.get(request.jobId);\n if (\n !job ||\n job.lease?.runnerId !== runner.id ||\n job.lease.id !== request.leaseId\n ) {\n // One answer for \"no such job\", \"not yours\" and \"a lease you no longer\n // hold\". A caller who is allowed to know already knows which.\n return fail(\"not-found\", \"no such lease on this job\");\n }\n // One implementation of open-and-reseal, shared with the cloud lane: the\n // deadline and key ids are bound into a signature, and two copies of a\n // bound value is the bug this codebase keeps finding.\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: { id: job.id, envelope: job.envelope, createdAt: job.createdAt },\n device: runner.device,\n });\n if (!resealed.ok) {\n return fail(\"server-error\", \"this job's payload could not be opened\");\n }\n return ok({ envelope: resealed.envelope } satisfies FetchResponse);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n // The machine must prove its encryption key belongs to the identity it\n // is presenting, before either is stored. Otherwise a caller could pair\n // a real identity with an encryption key it holds the secret for, and\n // read everything later sealed to that runner.\n if (!verifyPublicIdentity(request.device)) {\n return fail(\n \"bad-request\",\n \"the device's encryption key is not signed by the identity it was presented with\",\n );\n }\n\n await this.#store.createPairing({\n device: request.device,\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n collected: false,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n !pairing.collected &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n // Only on approval: a pending or denied poll learns nothing, so an\n // unapproved code cannot be used to enumerate a site's keys.\n //\n // One entry, because a direct site *is* one site — the same shape a\n // hub answers with rather than a special case (cloud_009 §5). The\n // daemon's lookup is one map read on every lane, which is what keeps\n // the two lanes one protocol.\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n audience: job.audience,\n owner: job.owner,\n // This site, named by its identity key id — Amendment A §A.3. The\n // daemon pinned this exact value at pairing, so it can check the stub\n // against the envelope it later opens rather than taking our word for\n // which site sent it. On this plane that is redundant, which is the\n // point: the direct and relayed stubs are the same shape, and a daemon\n // serving both cannot tell which upstream it is talking to.\n site: this.#siteKeyId,\n // byollm_016 Phase B. Present only when the site named one, and\n // omitted rather than sent as undefined — the stub is `.strict()` and\n // an explicit undefined is not the same as an absent key.\n ...(job.purpose === undefined ? {} : { purpose: job.purpose }),\n // Bucketed, not measured: an exact size is a stronger fingerprint\n // than routing needs (byollm_009 §6).\n sizeClass: job.sizeClass,\n // Reserved for byollm_006; no job declares it yet.\n streaming: false,\n // The stub's deadline bounds how long a captured envelope is worth\n // keeping, so it is always present — falling back to the TTL window\n // when the app named no absolute one.\n deadlineAt: deadlineFor(job, now),\n // `audienceAllow` is not sent — cloud_008 §0.2. The list stays on\n // `JobRecord`, where `claim` already filtered candidates with it; the\n // daemon's own allowlist is what decides `named` (byollm_001 Rev 1\n // §B) and always was.\n //\n // Removing it from `JobStub` did **not** make this line a type error.\n // A conditional spread is not excess-property-checked, so the field\n // would have gone on being sent to a daemon whose `.strict()` parse\n // now rejects the entire claim response — every daemon on the version\n // pair, refusing all work, for a field nobody read. Worth stating\n // where it happened: the schema is the contract, and the compiler\n // does not enforce it through a spread.\n // No fallback. A job returned from `claim` holds a lease by\n // definition, and synthesising one here would hand the daemon a lease\n // id the store has never heard of — every later release naming it\n // would silently match nothing. A store that returns an unleased job\n // has broken its contract, and this says so.\n lease: leaseOf(job),\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n // No revoked branch here any more — V1-2. A revoked runner is refused by\n // `#authed` before this handler is reached, on heartbeat as on every\n // other endpoint, because \"revoked\" and \"nothing consented right now\"\n // must not arrive as the same empty body.\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n // `renewed` is not reported back — cloud_008 §1.4b. The grants are still\n // extended; the daemon simply never read the list, and `lost` is the\n // signal it acts on.\n const { lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n leases: request.activeLeases,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n // A direct site has no disclosure of its own to go stale: consent to it\n // *is* the pairing, and withdrawing it empties the set above.\n awaitingConsent: [],\n cancel: [...cancel],\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n const outcome = await this.#openResult(request, runner);\n if (!outcome.ok) return outcome.failure;\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n // From the envelope the device signed, not from the request beside it\n // — cloud_008 §2.5. A daemon can no longer seal one answer and declare\n // it came from a different model.\n backendClass: outcome.value.ran.backendClass,\n model: outcome.value.ran.model,\n });\n\n const {\n accepted,\n duplicate,\n job: updated,\n } = await this.#store.complete({\n jobId: request.jobId,\n // Who is asking, for the duplicate answer only — §3.6. Authorisation\n // is `holder`, below, and still is.\n runnerId: runner.id,\n // The grant, not the runner — cloud_008 §1.4a. `CompleteHolder`'s own\n // docstring already called the lease \"the more exact check anyway\";\n // this plane simply had no lease id to give it until now.\n holder: { by: \"lease\", leaseId: request.leaseId },\n outcome: outcome.value.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n // Only when true — cloud_008 §3.6. Absent means \"not a duplicate\", and\n // an optional field that is always present is a required one wearing a\n // question mark.\n ...(duplicate === true ? { duplicate: true } : {}),\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n /**\n * Open a sealed result, or refuse it.\n *\n * The mirror of the daemon's `#openPayload`, and refuses for the same\n * reason: an outcome that does not verify against the device's pinned key is\n * an assertion by whoever relayed it, and storing it would let an\n * intermediary write answers into the app.\n *\n * The clear-text `disposition` is checked here rather than trusted. It is on\n * the wire so a relay can route without opening anything, which means the\n * one thing it must not be is authoritative — a daemon that sealed an error\n * and declared `ok` would otherwise have its declaration believed by\n * everything upstream of this line.\n */\n async #openResult(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<\n { ok: true; value: SealedOutcome } | { ok: false; failure: HandlerResult }\n > {\n const refuse = (why: string) =>\n ({ ok: false as const, failure: fail(\"bad-request\", why) }) as const;\n\n const opened = await open({\n envelope: request.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: runner.device.identity,\n expected: {\n jobId: request.jobId,\n senderKeyId: keyId(runner.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) {\n return refuse(\"the result did not verify as coming from this device\");\n }\n\n /* One function, both lanes — see `sealed-outcome.ts`. The refusal\n messages are the ones this lane already returned, now stated once. */\n const outcome = openSealedOutcome({\n plaintext: opened.plaintext,\n disposition: request.disposition,\n });\n if (!outcome.ok) return refuse(outcome.why);\n return { ok: true, value: outcome.value };\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n leases: request.leases,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n\n/** The lease a claimed job must have, or a loud failure. */\nfunction leaseOf(job: JobRecord): NonNullable<JobRecord[\"lease\"]> {\n if (!job.lease) {\n throw new Error(\n `store returned job ${job.id} from claim with no lease — the store ` +\n `contract requires a claimed job to hold one`,\n );\n }\n return job.lease;\n}\n","import {\n ENVELOPE_MAX_AGE_MS,\n keyId,\n open,\n publicIdentityOf,\n seal,\n type PublicIdentity,\n type SealedEnvelope,\n type StoredKeys,\n} from \"@byollm/protocol\";\n\n/**\n * Open this site's own at-rest envelope and re-seal it to a claiming device.\n *\n * The single operation that makes byollm_009 §6 work, and it now has two\n * callers: {@link ByollmHandlers} answering `fetch` on the direct plane, and\n * the cloud lane answering the relay's \"who claimed it\" poll. Both do exactly\n * this, and the reason it lives in one file is the reason everything else in\n * this codebase does: the deadline, the key ids and the direction are all\n * bound into a signature, and two implementations of a bound value is the same\n * bug as two clock readings — it works until they disagree, and then nothing\n * opens.\n *\n * The plaintext exists for one statement and never reaches a wire, a store, or\n * a log. That is the whole guarantee: the site is an endpoint, so it is\n * entitled to read its own work, and it is the only party between the app and\n * the device that is.\n */\n\ntype ResealFailure = \"unopenable\";\n\nexport type ResealResult =\n | { readonly ok: true; readonly envelope: SealedEnvelope }\n | { readonly ok: false; readonly reason: ResealFailure };\n\nexport async function resealForDevice(input: {\n siteKeys: StoredKeys;\n /** The job's identity and its at-rest ciphertext. */\n job: {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly createdAt: number;\n };\n /** The device that claimed it, as the upstream reported. */\n device: PublicIdentity;\n}): Promise<ResealResult> {\n const senderKeyId = keyId(publicIdentityOf(input.siteKeys).identity);\n\n const opened = await open({\n envelope: input.job.envelope,\n recipientKeys: input.siteKeys,\n senderIdentityPublic: input.siteKeys.identityPublic,\n expected: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: senderKeyId,\n direction: \"payload\",\n },\n });\n if (!opened.ok) {\n // The store holds something this site cannot open: rotated keys, a\n // corrupted row, or someone else's envelope. Not the device's problem and\n // not something a retry fixes.\n return { ok: false, reason: \"unopenable\" };\n }\n\n const envelope = await seal({\n plaintext: opened.plaintext,\n senderKeys: input.siteKeys,\n recipientEncryptionPublic: input.device.encryption,\n context: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: keyId(input.device.identity),\n // From the record, never recomputed from a fresh clock read — the\n // envelope's own deadline is what the signature bound.\n deadlineAt: input.job.createdAt + ENVELOPE_MAX_AGE_MS,\n direction: \"payload\",\n },\n });\n return { ok: true, envelope };\n}\n","import type {\n PayloadFor,\n PublicIdentity,\n Audience,\n Capability,\n JobKind,\n JobOutcome,\n SealedEnvelope,\n SizeClass,\n JobState,\n Lease,\n ResultProvenance,\n} from \"@byollm/protocol\";\n\n/**\n * A job as the server stores it.\n *\n * Adapters map this shape onto their own storage; the field meanings are\n * normative because the conformance kit asserts behaviour that depends on\n * them (TTL clock start, dependency gating, refusal tracking).\n */\nexport interface JobRecord {\n readonly id: string;\n readonly kind: JobKind;\n /**\n * The work, sealed to this site's own encryption key (byollm_009 §10).\n *\n * The store never holds plaintext. The app sees plaintext at enqueue and at\n * result because the app *is* the endpoint; everything in between —\n * database, backups, log aggregators, a support engineer with read access —\n * sees ciphertext.\n *\n * This is not protection from the application the user deliberately sent\n * their work to. It is protection from everything the application's storage\n * touches, which is a longer list than most people picture.\n */\n readonly envelope: SealedEnvelope;\n /** Fixed at enqueue, where the plaintext is. */\n readonly sizeClass: SizeClass;\n readonly audience: Audience;\n /**\n * The service the site named, if it named one — byollm_016 Phase B.\n *\n * Stored rather than derived, because the stub carries it to the router and\n * the router matches on it. `undefined` means the owner's default answers,\n * which is every job written before this field existed.\n */\n readonly purpose: string | undefined;\n /** The app's id for the user who enqueued it. */\n readonly owner: string;\n /** Server-side restriction on which runner owners may take a `named` job. */\n readonly audienceAllow: readonly string[] | undefined;\n /** Job ids that must all be `ok` before this becomes claimable. */\n readonly dependsOn: readonly string[];\n readonly state: JobState;\n readonly lease: Lease | null;\n /**\n * The grant that recorded this job's result — cloud_008 §3.6.\n *\n * Kept after `lease` is nulled, because \"who finished this\" outlives \"who\n * holds this\" and the two are asked for different reasons. It is what lets\n * a replay from the device that finished the job be answered *as a\n * duplicate* rather than as a stale lease — and lets a replay from any\n * other device be refused exactly as it would be for a job that is not\n * terminal, so a job id is not a terminality probe.\n */\n readonly completedByLeaseId: string | null;\n readonly createdAt: number;\n /**\n * When the job became claimable — enqueue time for a job with no\n * dependencies, or the moment its last dependency reached `ok`.\n *\n * **The TTL clock starts here, not at `createdAt`.** Starting it at enqueue\n * would expire a dependent job for the crime of waiting on a slow\n * dependency (byollm_001 Rev 1 §D, TTL clock resolved in build review).\n * `null` means still blocked.\n */\n readonly claimableAt: number | null;\n /** How long an unclaimed job may wait once claimable. */\n readonly ttlMs: number;\n /** Optional absolute deadline, independent of the TTL. */\n readonly deadlineAt: number | null;\n /**\n * Runners that released this job with reason `refused` — their local\n * allowlist declined it. Never offered to them again\n * ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n */\n readonly refusedBy: readonly string[];\n /** How many times this job has been claimed, including lease-expiry retries. */\n readonly attempts: number;\n readonly outcome: JobOutcome | null;\n readonly provenance: ResultProvenance | null;\n readonly updatedAt: number;\n}\n\n/** A paired daemon as the server stores it. */\nexport interface RunnerRecord {\n readonly id: string;\n /** The app's id for the user this runner is bound to — exactly one. */\n readonly owner: string;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n readonly paused: boolean;\n /** Set once; a revoked runner never un-revokes. */\n readonly revokedAt: number | null;\n readonly lastHeartbeatAt: number;\n readonly createdAt: number;\n /**\n * The device's pinned public keys. What later signatures verify against —\n * a runner id names a machine, this proves it.\n */\n readonly device: PublicIdentity;\n}\n\n/** An in-flight device-code pairing. */\nexport interface PairingRecord {\n /** SHA-256 of the device code. The code itself is never stored. */\n readonly deviceCodeHash: string;\n /** The short code the user reads. Unique among live pairings. */\n readonly userCode: string;\n readonly state: \"pending\" | \"approved\" | \"denied\";\n /** Set when approved — learned from the approving user's own session. */\n readonly owner: string | null;\n readonly runnerId: string | null;\n /**\n * Whether this approval has already been collected — cloud_008 §2.4.\n *\n * This was `runnerTokenOnce`, a bearer token held until the daemon's next\n * poll and then nulled. The token is gone (finding 37: minted, hashed,\n * written to two disks, never sent or compared), but the *deliver-once*\n * property it carried is real and separate: a replayed device code must get\n * nothing, or a code seen in a shell history is a second pairing.\n *\n * So the flag stays and the secret does not. Nulling a token to mean\n * \"collected\" was one field doing two jobs, and only one of them was load\n * bearing.\n */\n readonly collected: boolean;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n /**\n * The device's public keys, presented at pair start (byollm_009 §5).\n *\n * Kept on the pairing so the approving user is approving a *specific\n * machine*, not a code that any machine could later redeem. It is copied\n * onto the runner at approval.\n */\n readonly device: PublicIdentity;\n readonly expiresAt: number;\n readonly createdAt: number;\n}\n\n/**\n * What the app supplies to enqueue a job.\n *\n * Generic over the kind, so the payload has to be the payload *for* that kind.\n * These were independent — `kind: JobKind` beside `payload: JobPayload`, the\n * union of both shapes — and the pairing was left to the author's memory. A\n * chat job carrying a generate payload typechecked, built, shipped, and was\n * refused at the relay's ingress with a precise sentence nobody sees until\n * somebody clicks.\n *\n * `PayloadFor<K>` was already exported by the protocol when that happened, and\n * `enqueue` did not use it. A wrong pairing is now a compile error at the call\n * site, which is the only place that knows what it meant.\n *\n * A caller whose `kind` is a variable rather than a literal still gets the old\n * permissive union — the conditional distributes — so nothing that was legal\n * and correct stops compiling.\n */\nexport interface EnqueueInput<K extends JobKind = JobKind> {\n readonly kind: K;\n /** The work, in plaintext. The server seals it before it is stored. */\n readonly payload: PayloadFor<K>;\n readonly owner: string;\n /**\n * Direct lane only. Refused on the cloud lane, where it is derived.\n *\n * On the cloud lane, who may serve a job comes from the person's own\n * mapping — the service they chose, its owner, and that owner's offer scope\n * — none of which a site is told, and all of which the hub holds at claim.\n * A site declaring an audience there was a third vote cast by the one party\n * the disclosure fence forbids from knowing the answer, and its `private`\n * default silently disabled team sharing for every user who had a team.\n *\n * On the direct lane it still selects something real, which is why it stays\n * rather than going in the same release: it is the switch that turns\n * {@link EnqueueInput.audienceAllow} on. `private` is own-devices-only;\n * `team` hands the decision to the allowlist. Without it there is no way to\n * say \"these runner owners, and no others\", and supplier trust needs one.\n *\n * Defaults to `private` — the safe direction, and on this lane a direction\n * a caller can meaningfully choose.\n */\n readonly audience?: Audience;\n /**\n * Which of *your site's* declared purposes this job serves — Amendment L.\n *\n * **A need, never a name.** You declare purposes at registration —\n * `\"revenue\"`, `\"writing-assistant\"` — and each of your users maps them to\n * one of their own services on the consent screen. This field names the\n * purpose; the mapping does the rest.\n *\n * There is no model field, no base URL, no flags, and — since Amendment L —\n * no way to name a service either. Your vocabulary is your purposes; theirs\n * is their services; the two never meet. You learn whether a slot was\n * satisfiable and nothing else.\n *\n * Use the purpose **key**, not its label. Labels are prose for the consent\n * screen and may change; a key travels on every job and is what mappings\n * are stored against.\n *\n * Leave it out only in direct mode, which has no control plane to hold a\n * mapping and answers by kind alone.\n */\n readonly purpose?: string;\n readonly audienceAllow?: readonly string[];\n readonly dependsOn?: readonly string[];\n /** Defaults to the server config's `defaultTtlMs`. */\n readonly ttlMs?: number;\n readonly deadlineAt?: number;\n /** Caller-supplied id, for idempotent enqueue. */\n readonly id?: string;\n}\n\n/**\n * What the *store* is given — the sealed form.\n *\n * Distinct from {@link EnqueueInput} because the two are genuinely different\n * things: an app hands over work in plaintext, and what gets written down is\n * sealed. Collapsing them into one type would mean a field that is sometimes\n * readable and sometimes not, which is the kind of ambiguity that ends with\n * plaintext in a database.\n */\nexport interface StoredJobInput extends Omit<EnqueueInput, \"payload\" | \"id\"> {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly sizeClass: SizeClass;\n}\n\n/**\n * When a job's ciphertext stops being worth carrying — cloud_008 §31.\n *\n * One function because it was two expressions. The direct plane computed\n * `job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs`; the cloud lane\n * computed `record.deadlineAt ?? record.createdAt + <a local constant>`. The\n * first branch agreed and the fallback did not, so a job with no explicit\n * deadline got two different ones depending on which lane published it — and\n * the difference is largest exactly where it matters, for a job blocked on a\n * dependency, whose `claimableAt` may be hours after `createdAt`.\n *\n * The TTL clock starts when a job becomes *claimable*, which is the rule\n * `DEPENDS_ON_GATING` and `TTL_EXPIRY` already share: a dependent job must not\n * spend its life waiting for its dependency.\n */\nexport function deadlineFor(\n job: Pick<JobRecord, \"deadlineAt\" | \"claimableAt\" | \"ttlMs\">,\n now: number,\n): number {\n return job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs;\n}\n","import {\n ENDPOINTS,\n ERROR_STATUS,\n MAX_ENVELOPE_BYTES,\n tooLargeMessage,\n PROTOCOL_PREFIX,\n checkProtocolVersion,\n type Endpoint,\n} from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * Derived, not chosen. This was `8 * 1024 * 1024` beside a comment saying the\n * protocol caps a payload at 4 MB — true when it was written, and the cap has\n * since moved to {@link MAX_ENVELOPE_BYTES}, which is 6 MiB. So the direct\n * lane refused envelopes the protocol permits, and the hub — which derives\n * its own limit the same way this now does — accepted them.\n *\n * That is the failure this codebase keeps finding in other clothes: one rule\n * with two implementations, and only one of them moved. A site self-hosting\n * the SDK and a site on the hub must not disagree about whether a job is too\n * big, so neither of them gets to hold the number.\n *\n * The 512 KiB of headroom is for JSON overhead and a batch of results around\n * the envelope, matching the hub's `MAX_ENVELOPE_BYTES + 512 * 1024`.\n */\nconst MAX_BODY_BYTES = MAX_ENVELOPE_BYTES + 512 * 1024;\n\n/**\n * What a message that is too big is told — B061, Kevin's bisection.\n *\n * This said \"request body too large\" and nothing else, at both call sites,\n * with both numbers already in scope. Somebody who hits it learns that\n * something was too big and not what, not by how much, not whether the limit\n * is per-message or per-account, and not what to do — so the only way\n * forward is to bisect, which is exactly what Kevin did.\n *\n * The sentence and its rounding come from the protocol now — B072. The first\n * version of this copied the relay's WORDS and rediscovered the relay's bug\n * with them: `toFixed` rounds to nearest, so one byte over printed \"this\n * message is 10.5 MB and the limit is 10.5 MB\". The relay had already found\n * that, fixed it, and written the reasoning beside the fix. Copying a\n * sentence copies everything about it except the part that was learned.\n */\nconst tooLarge = (bytes: number): string =>\n tooLargeMessage({ bytes, limit: MAX_BODY_BYTES });\n\n/**\n * Where the protocol endpoints are mounted.\n *\n * Defaults to {@link PROTOCOL_PREFIX}. Pass the real mount point when it is\n * anything else — a Next.js route at `app/api/byollm/[...route]/route.ts`\n * serves `/api/byollm/...`, so it needs `basePath: \"/api/byollm\"`.\n *\n * @throws if the path is not an absolute, single-segment-per-slash path. A\n * mount point is configuration, and a malformed one should fail at startup\n * rather than silently match nothing.\n */\nfunction normalizeBasePath(basePath: string): string {\n const trimmed = basePath.endsWith(\"/\") ? basePath.slice(0, -1) : basePath;\n if (!trimmed.startsWith(\"/\")) {\n throw new Error(`basePath must start with \"/\": got ${basePath}`);\n }\n if (trimmed.includes(\"//\") || /[?#*]/.test(trimmed)) {\n throw new Error(`basePath must be a plain path: got ${basePath}`);\n }\n return trimmed;\n}\n\n/**\n * Pull the endpoint name out of a URL path, or null if it isn't ours.\n *\n * The full path must match `<basePath>/<endpoint>` exactly. This used to\n * compare only the *last* segment, which meant `/anything/at/all/claim`\n * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it\n * appeared in a 404 message and was never matched against. For the handler\n * that serves claim, result and heartbeat, dispatching on a suffix is a\n * looser rule than anyone reading the constant would assume, and loose\n * matching in a security surface should at least be a decision.\n *\n * The cost is that the mount point is now something a deployment has to state\n * rather than something that works by accident. That is the intended trade:\n * a 404 at startup naming the mount point beats a handler answering on paths\n * nobody meant to expose.\n */\nexport function routeEndpoint(\n pathname: string,\n basePath: string = PROTOCOL_PREFIX,\n): Endpoint | null {\n const base = normalizeBasePath(basePath);\n const path = pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n if (!path.startsWith(`${base}/`)) return null;\n const rest = path.slice(base.length + 1);\n return (ENDPOINTS as readonly string[]).includes(rest)\n ? (rest as Endpoint)\n : null;\n}\n\n/**\n * Read the request signature from headers (byollm_009 §4.2).\n *\n * In headers rather than the body so the signature covers the body whole,\n * with no field to exclude from its own hash — a scheme that signs a body\n * minus one field has to agree, byte for byte, on how that field is removed.\n */\nexport function signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const rawIssuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n if (runnerId === null || signature === null || rawIssuedAt === null) {\n return undefined;\n }\n // Checked against null *before* Number(), because `Number(null)` is 0 —\n // finite, plausible-looking, and wrong. A missing timestamp would have\n // become a timestamp of the epoch, which the freshness check would then\n // reject for the wrong reason.\n const issuedAt = Number(rawIssuedAt);\n if (!Number.isFinite(issuedAt)) return undefined;\n return { runnerId, issuedAt, signature };\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig & {\n /**\n * Where these endpoints are mounted. Defaults to\n * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere.\n */\n readonly basePath?: string;\n },\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n // Validate once, at construction: a bad mount point is a deployment bug and\n // should surface when the server starts, not as a silent 404 per request.\n const basePath = normalizeBasePath(config.basePath ?? PROTOCOL_PREFIX);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname, basePath);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${basePath} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: tooLarge(Number(declared)),\n });\n }\n\n let body: unknown;\n let rawBody: string;\n try {\n rawBody = await request.text();\n const text = rawBody;\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: tooLarge(text.length),\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n // byollm_009 §4: version before anything else. A mismatch must name the\n // disagreement and the fix, not surface as a generic bad-request from a\n // schema literal buried in an endpoint — which is what happened before,\n // and is why \"the connection is versionless\" was listed as a defect.\n const refusal = checkProtocolVersion(body);\n if (refusal) {\n return json(ERROR_STATUS[refusal.error], refusal);\n }\n\n const result = await handlers.handle(endpoint, body, {\n endpoint,\n // The bytes as received. Re-serialising the parsed object would verify\n // a signature over something the sender never sent.\n rawBody,\n signature: signatureFrom(request.headers),\n });\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["keyId","open","publicIdentityOf","publicIdentityOf","keyId","open","ERROR_STATUS","ERROR_STATUS"]}
package/dist/index.js CHANGED
@@ -9,11 +9,12 @@ import {
9
9
  generateRunnerId,
10
10
  generateUserCode,
11
11
  hashSecret,
12
+ openSealedOutcome,
12
13
  resealForDevice,
13
14
  routeEndpoint,
14
15
  secretsMatch,
15
16
  signatureFrom
16
- } from "./chunk-5WS55FRU.js";
17
+ } from "./chunk-36Y77FUD.js";
17
18
  import {
18
19
  NoRunnerAvailableError,
19
20
  PollingDelivery,
@@ -36,7 +37,6 @@ import {
36
37
  // src/cloud.ts
37
38
  import {
38
39
  PROTOCOL_VERSION,
39
- SealedOutcome,
40
40
  keyId,
41
41
  open,
42
42
  publicIdentityOf,
@@ -269,16 +269,11 @@ var CloudLane = class {
269
269
  }
270
270
  });
271
271
  if (!opened.ok) return null;
272
- let parsed;
273
- try {
274
- parsed = JSON.parse(opened.plaintext);
275
- } catch {
276
- return null;
277
- }
278
- const sealed = SealedOutcome.safeParse(parsed);
279
- if (!sealed.success) return null;
280
- if (sealed.data.outcome.outcome !== done.disposition) return null;
281
- return sealed.data;
272
+ const outcome = openSealedOutcome({
273
+ plaintext: opened.plaintext,
274
+ disposition: done.disposition
275
+ });
276
+ return outcome.ok ? outcome.value : null;
282
277
  }
283
278
  /**
284
279
  * Sign a site-plane call with this site's identity key.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/app.ts","../src/cloud.ts","../src/keys.ts","../src/memory.ts"],"sourcesContent":["import {\n ENVELOPE_MAX_AGE_MS,\n KindedPayload,\n keyId,\n payloadTextLength,\n publicIdentityOf,\n seal,\n sizeClassOf,\n type StoredKeys,\n backendDescriptor,\n matchAudience,\n type Audience,\n type DeliveredResult,\n type JobKind,\n type MatchRefusal,\n} from \"@byollm/protocol\";\nimport {\n PollingDelivery,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n} from \"./delivery.js\";\nimport { generateJobId, generateRunnerId } from \"./ids.js\";\nimport { CloudLane, type CloudLaneOptions } from \"./cloud.js\";\nimport type { EnqueueInput, JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/**\n * How long since a runner's last heartbeat before it stops counting as live.\n * Three heartbeats of slack at the daemon's ~10s cadence.\n */\nconst DEFAULT_LIVENESS_MS = 35_000;\n\n/** Why a job cannot presently run. */\nexport type NoRunnerReason =\n | \"no-runner-paired\"\n | \"no-runner-online\"\n | \"no-matching-capability\"\n | \"audience-admits-nobody\"\n /**\n * The owner's default for this kind can never serve *this* requester —\n * byollm_016's defaults-meet-audiences corner.\n *\n * The specimen: a default of `claude-cli`, self-locked by\n * `SUBSCRIPTION_SELF_LOCK`, and a team member's unselected job. It resolves\n * to something that will never run it. Reported rather than left to time\n * out, because a wait that can never end is indistinguishable from one that\n * has not ended yet, and only one of them is worth waiting through.\n */\n | \"default-unusable\";\n\n/**\n * The no-runner signal (byollm_001 Rev 1 §D).\n *\n * `available: false` means an app should fall back — hosted model, \"start\n * your runner\" prompt — rather than awaiting something that will never\n * resolve. A job still blocked on dependencies is **not** unavailable; it is\n * waiting, and saying otherwise would make every multi-job flow look broken\n * ({@link MUSTS.NO_RUNNER_SIGNAL}).\n */\nexport interface RunnerAvailability {\n readonly available: boolean;\n readonly reason?: NoRunnerReason;\n /** Live runners that could take work of this shape. */\n readonly candidates: number;\n}\n\nexport interface AvailabilityQuery {\n readonly kind: JobKind;\n readonly owner: string;\n readonly audience?: Audience;\n readonly audienceAllow?: readonly string[];\n}\n\nexport interface ByollmAppOptions {\n readonly store: ByollmStore;\n /** Injectable clock. */\n readonly now?: () => number;\n /** Liveness window for the no-runner signal. */\n readonly livenessMs?: number;\n /**\n * How the app learns a job finished. Defaults to polling the store, which\n * is correct everywhere; the Supabase adapter substitutes Realtime.\n */\n readonly delivery?: (deps: PollingDeliveryDeps) => ResultDelivery;\n /**\n * How long a sustained no-runner signal must persist before `result()`\n * gives up. Longer tolerates a daemon restarting; shorter fails faster.\n */\n readonly noRunnerGraceMs?: number;\n /**\n * This site's keypairs — the same ones the handlers use.\n *\n * The app needs them because it is the *endpoint*: it seals work on the way\n * in and opens results on the way out. Nothing between those two points\n * holds plaintext (byollm_009 §10).\n */\n readonly siteKeys: StoredKeys;\n /**\n * Which connection plane this site uses — cloud_004 §9.4.\n *\n * Omitted means `direct`: a daemon reaches this site's own handlers, and\n * everything works as it always has. Supplying a relay switches the plane\n * and nothing else — `enqueue` is identical in every lane, which is the\n * property that lets the same app move between them by config.\n */\n readonly lane?: CloudLaneOptions;\n}\n\n/**\n * An enqueued job, with the delivery channel attached.\n *\n * `result()` is sugar over the channel — with a timeout and a\n * `noRunnerAvailable` path — never a bare promise that can hang forever\n * (byollm_003 Rev 1).\n */\nexport interface JobHandle {\n readonly id: string;\n /** The job as stored at enqueue time. */\n readonly record: JobRecord;\n /** Wait for a terminal outcome. */\n result(options?: WaitOptions): Promise<DeliveredResult>;\n /** Ask the runner to stop. */\n cancel(): Promise<void>;\n}\n\n/**\n * The app-facing half of `@byollm/server`.\n *\n * The daemon talks to {@link ByollmHandlers}; the app talks to this. Keeping\n * them separate is what makes \"one door per state write\" hold — an app\n * enqueues and cancels through these methods and never writes job rows by\n * hand.\n */\n/**\n * Every option `enqueue` accepts, as data.\n *\n * `Record<keyof EnqueueInput, true>` rather than a hand-kept array, so the\n * compiler refuses this file when a field is added to `EnqueueInput` and not\n * to this list. An allowlist that silently falls behind the type it guards is\n * worse than none: it would start rejecting the very field somebody just\n * added, in the name of catching typos.\n */\nconst ENQUEUE_OPTIONS: Readonly<Record<keyof EnqueueInput, true>> =\n Object.freeze({\n kind: true,\n payload: true,\n owner: true,\n audience: true,\n purpose: true,\n audienceAllow: true,\n dependsOn: true,\n ttlMs: true,\n deadlineAt: true,\n id: true,\n });\n\nexport class ByollmApp {\n readonly #store: ByollmStore;\n readonly #siteKeys: StoredKeys;\n readonly #now: () => number;\n readonly #livenessMs: number;\n readonly #delivery: ResultDelivery;\n /** Present only in the cloud lane; the site's side of the relay. */\n readonly cloud: CloudLane | undefined;\n\n constructor(options: ByollmAppOptions) {\n this.#store = options.store;\n this.#siteKeys = options.siteKeys;\n this.#now = options.now ?? Date.now;\n this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;\n this.cloud =\n options.lane === undefined\n ? undefined\n : new CloudLane({\n options: options.lane,\n store: options.store,\n siteKeys: options.siteKeys,\n now: this.#now,\n });\n\n /**\n * The delivery's dependencies — and on the cloud lane, one fewer.\n *\n * `runnerAvailability` refuses on the cloud lane, deliberately: it counts\n * runners in this site's own store, devices there pair with the relay\n * instead, and it spent a release reporting `no-runner-paired` with\n * confidence for every cloud-lane app that asked.\n *\n * The refusal shipped and this wrapper kept calling it. Delivery asks\n * every 500ms, so `job.result()` threw on its first poll for every\n * cloud-lane site — found by Kevin, on the ordinary consumer loop that\n * none of our own proofs ran.\n *\n * **The law it earned: when you make a function refuse, grep its callers\n * first.** We audited what branched on the untrusted flag and never\n * audited this method's internal callers. A refusal aimed at outsiders\n * that your own loop trips over is a crash wearing a principle.\n *\n * So the question is not asked. Delivery gets no availability instrument\n * on a lane where nothing can answer, rather than an instrument that\n * throws and a `catch` upstream pretending that means \"keep waiting\".\n */\n const deps: PollingDeliveryDeps = {\n ...(options.noRunnerGraceMs === undefined\n ? {}\n : { graceMs: options.noRunnerGraceMs }),\n read: (jobId) => this.result(jobId),\n ...(this.cloud !== undefined\n ? {}\n : { availability: this.#availabilityFor() }),\n };\n this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);\n }\n\n /**\n * The no-runner instrument, for a lane that can actually see runners.\n *\n * A method rather than an inline closure so the branch above reads as one\n * decision — whether this deployment has the instrument at all — instead of\n * a conditional wrapped around thirty lines of body.\n */\n #availabilityFor() {\n return async (jobId: string) => {\n const job = await this.#store.get(jobId);\n if (!job)\n return { available: false, reason: \"unknown-job\", blocked: false };\n // A job waiting on a dependency is waiting, not unavailable.\n if (job.claimableAt === null) {\n return { available: true, blocked: true };\n }\n const availability = await this.runnerAvailability({\n kind: job.kind,\n owner: job.owner,\n audience: job.audience,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: job.audienceAllow }),\n });\n return {\n available: availability.available,\n ...(availability.reason === undefined\n ? {}\n : { reason: availability.reason }),\n blocked: false,\n };\n };\n }\n\n /**\n * Enqueue a job.\n *\n * `audience` defaults to `self` — the safe direction. Widening it means the\n * result comes back marked untrusted (see {@link ByollmApp.result}), and\n * the app is obliged to disclose that to whoever reads it.\n */\n async enqueue<K extends JobKind>(input: EnqueueInput<K>): Promise<JobHandle> {\n // An option this SDK does not know is refused, never ignored.\n //\n // A caller newer than its SDK is the ordinary way this happens, and the\n // case that produced the rule: a site called `enqueue({ service })`\n // against a version that predated the field, the key went nowhere, and\n // nothing said so. The app believed it was selecting a service, was not,\n // and the only symptom was work running on one nobody chose. Silence is\n // the hazard rather than the missing feature.\n const unknown = Object.keys(input).filter(\n (key) => !(key in ENQUEUE_OPTIONS),\n );\n if (unknown.length > 0) {\n throw new Error(\n `enqueue does not understand ${unknown.map((k) => `\\`${k}\\``).join(\", \")}. ` +\n `An option this @byollm/server does not know is refused rather than ` +\n `ignored, because an ignored option is a job that runs differently ` +\n `than you asked with nothing to see — most often an SDK older than ` +\n `the code calling it. Upgrade @byollm/server, or remove the option.`,\n );\n }\n\n /**\n * `audience` is not a fact a cloud-lane site holds — so it may not state\n * one.\n *\n * Who may serve a job is decided by the person: their mapping names a\n * service and its owner, that owner's offer scope says who the service\n * serves, and the hub holds both at claim. The site's declaration was a\n * third vote cast by the one party the disclosure fence forbids from\n * knowing the answer.\n *\n * Which is exactly how its default came to disable the headline feature\n * in silence. It defaults to `private` — own devices only — so a site that\n * simply never mentioned it broke team sharing for every user who had a\n * team, while working perfectly for everyone testing alone. **A\n * declaration required from the party that cannot know is a default in\n * disguise.**\n *\n * Refused rather than ignored, by this method's own rule two paragraphs\n * up: an ignored option is a job that runs differently than asked with\n * nothing to see. The remedy travels with the refusal, because a caller\n * who set it was trying to express something real and deserves to know\n * where that decision now lives.\n */\n if (this.cloud !== undefined && input.audience !== undefined) {\n throw new Error(\n \"enqueue does not take `audience` on the cloud lane. Who may serve a \" +\n \"job is derived from the person's own mapping — the service they \" +\n \"chose, its owner, and that owner's sharing — which your site is \" +\n \"not told and cannot compute. Remove `audience`; ask for the kind \" +\n \"and the purpose, and their decision does the rest.\",\n );\n }\n\n // Validate the payload against its kind before anything stores it.\n //\n // The schemas are `.strict()`, so this drops a payload carrying fields\n // the kind does not define — `command`, `argv`, `model`, `baseUrl`. Types\n // do not survive a JSON boundary, and an app assembling a payload from\n // user input is the ordinary case, so \"the caller is typed\" is not a\n // check ({@link MUSTS.KIND_NO_CODE}, {@link MUSTS.NO_PAYLOAD_ROUTING}).\n //\n // Refusing here rather than relying on the daemon is deliberate. The\n // daemon does re-validate and would reject this — but it parses a whole\n // claim response at once, so one malformed job would fail the batch it\n // arrived in and stall unrelated work. Rejecting at enqueue puts the\n // error where the app can act on it.\n const parsed = KindedPayload.safeParse({\n kind: input.kind,\n payload: input.payload,\n });\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`invalid ${input.kind} payload — ${detail}`);\n }\n\n // Sealed before it is stored, to this site's own key. The app is the\n // endpoint, so it can open its own work later; the store, its backups and\n // anything reading them cannot.\n // Two different deadlines, deliberately not conflated:\n //\n // - the *job's* deadline is the app's business, may be absent, and for a\n // dependent job its TTL clock does not even start until the job becomes\n // claimable (`TTL_EXPIRY`). Setting one here broke exactly that.\n // - the *envelope's* deadline bounds how long a captured ciphertext is\n // worth keeping. It is bound into the signature, so it has to be\n // recomputable at open time from what the record stores — hence\n // creation plus TTL, which never moves.\n // Resolved *here*, once, and passed to the store — because the envelope\n // binds it. Letting the app default one value and the store default\n // another produced a job whose seal and record disagreed, and therefore\n // work nobody could open.\n // One reading of the clock, used for both the seal and the record.\n //\n // Two readings passed every fake-clock test and failed against a real\n // one: the envelope bound `createdAt + ttlMs` from the first call and the\n // record stored `createdAt` from the second, a millisecond later, so\n // nothing could be opened. A fixed clock returns the same number twice\n // and hides it completely.\n const createdAt = this.#now();\n // Independent of the job's TTL, deliberately. Binding the envelope to\n // `createdAt + ttl` meant the app had to decide a TTL in order to seal —\n // which overrode the store's own default and broke every expiry test.\n // The two answer different questions: how long the work is worth doing,\n // and how long the ciphertext is worth keeping.\n const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;\n const jobId = input.id ?? generateJobId();\n const senderKeyId = keyId(publicIdentityOf(this.#siteKeys).identity);\n const envelope = await seal({\n plaintext: JSON.stringify(parsed.data.payload),\n senderKeys: this.#siteKeys,\n recipientEncryptionPublic: this.#siteKeys.encryptionPublic,\n context: {\n jobId,\n senderKeyId,\n recipientKeyId: senderKeyId,\n deadlineAt: envelopeDeadlineAt,\n direction: \"payload\",\n },\n });\n\n const record = await this.#store.create(\n {\n ...input,\n /**\n * Derived here, because on the cloud lane it is derivable and nowhere\n * else knows the lane.\n *\n * Refusing the site's declaration is only half of \"derived, never\n * declared\" — the stub still carries an audience to the relay, and a\n * store that defaults it to `private` would keep every cloud job\n * private no matter who was forbidden from saying so. The half that\n * fixes anything is this one.\n *\n * `team` is the value that defers: it says a device whose owner\n * admits this person may serve, and the hub then decides whether one\n * does, from the mapping the person authored, its service's owner,\n * that owner's offer scope, and the roster. Nothing is widened by\n * saying it — both axes still have to agree, and the owner's scope is\n * the other axis.\n *\n * Direct mode keeps the store's `private` default: there is no\n * control plane there to derive from, and owner-only is the ruling.\n */\n ...(this.cloud === undefined ? {} : { audience: \"team\" as const }),\n id: jobId,\n envelope,\n sizeClass: sizeClassOf(\n payloadTextLength({\n kind: input.kind,\n payload: parsed.data.payload,\n } as Parameters<typeof payloadTextLength>[0]),\n ),\n },\n createdAt,\n );\n // The lane's only intrusion into enqueue, and it is additive: the record\n // is already stored and sealed at rest before anything is published, so a\n // relay that is down costs a routing delay rather than a lost job.\n await this.cloud?.publish(record);\n\n return {\n id: record.id,\n record,\n result: (options?: WaitOptions) =>\n this.#delivery.waitFor(record.id, options),\n cancel: async () => {\n await this.cancel(record.id);\n },\n };\n }\n\n /** Read a job's current state. */\n async job(jobId: string): Promise<JobRecord | null> {\n await this.#store.expireDue(this.#now());\n return this.#store.get(jobId);\n }\n\n /**\n * A job's result with its provenance attached.\n *\n * Check `provenance.untrusted` before rendering. It is true for every\n * `named`/`public` job, because that text came from someone else's machine\n * and the app must not present it as its own AI's answer\n * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n */\n async result(jobId: string): Promise<DeliveredResult | null> {\n const job = await this.job(jobId);\n if (!job) return null;\n return {\n jobId: job.id,\n state: job.state,\n ...(job.outcome === null ? {} : { outcome: job.outcome }),\n ...(job.provenance === null ? {} : { provenance: job.provenance }),\n };\n }\n\n /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */\n async cancel(jobId: string): Promise<JobRecord | null> {\n const cancelled = await this.#store.cancel(jobId, this.#now());\n // On the cloud lane the relay is the only party talking to the daemon, so\n // a cancellation that stops at this store stops a *future* seal and\n // nothing else — cloud_008 §2.2. Told after the row is terminal, so the\n // two can only disagree in the safe direction: the relay may briefly\n // still offer a job this site will now refuse to seal for.\n //\n // Not awaited into the caller's error path: an app cancelling a job has\n // cancelled it, and a relay that is unreachable must not turn that into a\n // thrown error. The relay's own deadline sweep is the backstop.\n if (cancelled && this.cloud) {\n await this.cloud.cancel(jobId).catch(() => undefined);\n }\n return cancelled;\n }\n\n /**\n * Is there a live runner that could take a job of this shape?\n *\n * Runs the identical {@link matchAudience} rule the claim path uses, so the\n * signal cannot promise a runner the claim would then refuse.\n */\n async runnerAvailability(\n query: AvailabilityQuery,\n ): Promise<RunnerAvailability> {\n /**\n * On the cloud lane this cannot see, so it does not answer.\n *\n * It counts runners in *this site's own store*. In direct mode that is\n * the whole world — devices pair with the site. On the cloud lane they\n * pair with the relay, nothing ever writes a runner here, and the honest\n * count is not zero but unknown.\n *\n * It reported zero, as `no-runner-paired` with `candidates: 0`, for every\n * cloud-lane app that ever called it. A teammate using a shared device\n * was told no device was paired to her account — true, irrelevant, and\n * rendered as advice to go and install software she did not need.\n *\n * **An instrument that cannot see must refuse, not report zero.** A wrong\n * answer given confidently is worse than no answer, and this one was\n * confident, specific and false all at once.\n */\n if (this.cloud !== undefined) {\n throw new Error(\n \"runnerAvailability cannot answer on the cloud lane. It counts \" +\n \"runners this site knows about, and on the cloud lane devices pair \" +\n \"with the relay rather than with you — so the answer would be \" +\n \"`none` whatever the truth is. Enqueue the job: the result says \" +\n \"whether it ran, and the person's own dashboard says why not.\",\n );\n }\n\n const now = this.#now();\n const all = await this.#store.listRunners();\n const live = all.filter(\n (runner) =>\n runner.revokedAt === null &&\n !runner.paused &&\n now - runner.lastHeartbeatAt <= this.#livenessMs,\n );\n\n if (all.length === 0) {\n return { available: false, reason: \"no-runner-paired\", candidates: 0 };\n }\n if (live.length === 0) {\n return { available: false, reason: \"no-runner-online\", candidates: 0 };\n }\n\n let capable = 0;\n let admitted = 0;\n let lastRefusal: MatchRefusal | undefined;\n /**\n * Every advertised service for this kind, not one chosen here.\n *\n * This used to pick a single row — the one a job named, or the one the\n * owner had made the default — because a site could name a service and a\n * router matched on the name. Amendment L removed the naming, so there is\n * no row to prefer: availability is now \"does *anything* this device\n * offers for this kind admit this person\", which is also the honest\n * question, since which service actually answers is resolved from the\n * person's own mapping at claim.\n */\n for (const runner of live) {\n for (const capability of runner.capabilities.filter(\n (c) => c.kind === query.kind,\n )) {\n capable += 1;\n\n const match = matchAudience(\n {\n owner: query.owner,\n audience: query.audience ?? \"private\",\n audienceAllow: query.audienceAllow,\n },\n {\n owner: runner.owner,\n offerScope: capability.offerScope,\n // A generic backend's cost depends on its base URL, which the\n // server never sees; assume the expensive reading (byollm_007 §4).\n cost: backendDescriptor(capability.backendId).cost ?? \"metered\",\n // Consent is the daemon's to hold, and it has already applied it:\n // the offer scope arriving here is the *effective* one, so a\n // metered backend nobody agreed to share advertises `self` and is\n // refused by the scope rule above. Re-deriving consent from\n // `false` here would instead refuse every backend an owner\n // deliberately shared, because the server has no way to learn they\n // did — the signal would be wrong in the direction that breaks\n // working setups.\n spend: { acknowledged: true },\n // Same conservative assumption the claim path makes: the server\n // cannot see a remote daemon's local allowlist (protocol §4.2).\n admits: () => true,\n },\n );\n if (match.ok) admitted += 1;\n else lastRefusal = match.refusal;\n }\n }\n\n if (capable === 0) {\n // Ordered most specific first, because each sends the reader somewhere\n // different: a name that cannot serve them, a decision the device's\n // owner has not made, or nothing installed at all.\n return {\n available: false,\n reason: \"no-matching-capability\",\n candidates: 0,\n };\n }\n if (admitted === 0) {\n // Something serves it and nothing may serve *this requester*. When the\n // block is the device owner's own setting, that is the\n // defaults-meet-audiences corner — every service for this kind is one\n // this person can never use — and it is worth its own word, because\n // \"nobody is admitted\" reads as a\n // permissions problem the requester could ask to have fixed, while this\n // one is fixed by the device's owner choosing differently.\n // Whose decision blocked it, not merely that something did. The first\n // draft asked \"did the job name a service\", which reclassified a job\n // whose *own* audience was `private` and whose only device belonged to\n // somebody else — telling that caller \"the owner's default cannot serve\n // you\" when the exclusion was their own choice. An existing test caught\n // it, which is the argument for keeping the older reason rather than\n // widening the new one.\n //\n // So it splits on the refusal `matchAudience` already produced: a scope\n // or billing refusal is the *device owner's* setting, which only they\n // can change; an audience refusal is the *caller's*, which they can.\n const ownersDoing =\n lastRefusal === \"offer-scope-too-narrow\" ||\n lastRefusal === \"subscription-self-lock\" ||\n lastRefusal === \"metered-no-spend-consent\" ||\n lastRefusal === \"metered-ceiling-reached\";\n return {\n available: false,\n reason: ownersDoing ? \"default-unusable\" : \"audience-admits-nobody\",\n candidates: 0,\n };\n }\n return { available: true, candidates: admitted };\n }\n\n /**\n * Approve a pairing on behalf of an authenticated user.\n *\n * `owner` MUST come from the approving user's own session. A daemon can\n * never assert who it is — that is the whole reason pairing is interactive\n * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).\n */\n async approvePairing(args: {\n userCode: string;\n owner: string;\n }): Promise<RunnerRecord> {\n return this.#store.approvePairing({\n userCode: normalizeUserCode(args.userCode),\n owner: args.owner,\n runnerId: generateRunnerId(),\n now: this.#now(),\n });\n }\n\n /** Deny a pairing the user did not initiate. */\n async denyPairing(userCode: string): Promise<void> {\n return this.#store.denyPairing(normalizeUserCode(userCode), this.#now());\n }\n\n /** What a pairing code refers to, for the approval page to show. */\n async pendingPairing(userCode: string): Promise<{\n label: string;\n platform: string;\n daemonVersion: string;\n capabilities: readonly { kind: string; model: string }[];\n expiresAt: number;\n } | null> {\n const pairing = await this.#store.getPairingByUserCode(\n normalizeUserCode(userCode),\n );\n if (pairing?.state !== \"pending\") return null;\n if (pairing.expiresAt <= this.#now()) return null;\n return {\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities.map((c) => ({\n kind: c.kind,\n model: c.model,\n })),\n expiresAt: pairing.expiresAt,\n };\n }\n\n /** The user's paired runners, for a settings page. */\n async runners(owner: string): Promise<RunnerRecord[]> {\n return this.#store.listRunners(owner);\n }\n\n /** Revoke a runner. It stops at its next heartbeat, mid-queue. */\n async revokeRunner(runnerId: string): Promise<void> {\n return this.#store.revokeRunner(runnerId, this.#now());\n }\n\n /** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */\n async sweep(): Promise<JobRecord[]> {\n return this.#store.expireDue(this.#now());\n }\n}\n\n/**\n * Accept a pairing code however the user typed it — lowercase, spaces, no\n * dash. The code is displayed as `XXXX-XXXX`; refusing `xxxxxxxx` would fail\n * a user for a formatting detail they were never told mattered.\n */\nexport function normalizeUserCode(input: string): string {\n const bare = input.toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;\n}\n","import {\n PROTOCOL_VERSION,\n SealedOutcome,\n type SealedEnvelope,\n keyId,\n open,\n publicIdentityOf,\n provenanceFor,\n signSiteRequest,\n type JobStub,\n type PublicIdentity,\n type StoredKeys,\n} from \"@byollm/protocol\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord } from \"./records.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/**\n * The cloud lane — cloud_004 §9.4.\n *\n * `app.enqueue(...)` is identical in every lane; the lane picks the connection\n * plane. In `direct` mode a daemon reaches the site's own handlers. In `cloud`\n * mode it reaches a relay instead, and the site's side of that is this file.\n *\n * ## What actually changes, and what deliberately does not\n *\n * Enqueue does not change at all. The job is validated, sealed at rest to the\n * site's own key and stored, exactly as before — jobs-at-rest encryption is a\n * direct-mode property that the cloud lane inherits rather than replaces.\n *\n * What changes is *who asks for the payload and when*. On the direct plane the\n * daemon asks, and the site answers synchronously because it is the upstream.\n * Through a relay the site is not the upstream, so nobody asks: the site has to\n * find out that a device claimed its job, and seal to that device. Hence a\n * pump rather than a handler.\n *\n * ```\n * enqueue ──stub──▶ relay (payload stays here, sealed at rest)\n * │\n * pump ◀──who claimed it, and what key?\n * ──payload sealed to that device──▶\n * pump ◀──sealed result── ──▶ store.complete → the app's delivery channel\n * ```\n *\n * ## Why the site polls\n *\n * Everything in this product is outbound. A relay that called site webhooks\n * would need every site publicly reachable, which is the connectivity problem\n * the hub exists to remove — and a serverless site has nowhere to receive a\n * webhook anyway. So the site polls, exactly as a daemon does.\n */\n\nexport interface CloudLaneOptions {\n /** Where the relay lives, e.g. `https://relay.byollm.cloud`. */\n readonly relayOrigin: string;\n /** This site's id at the relay. */\n readonly siteId: string;\n /** Injectable fetch, for tests and for proxies. */\n readonly fetch?: typeof fetch;\n}\n\n/** What one pump cycle did, for logging and for tests. */\n/**\n * A relay that could not answer this request — alpha.31.\n *\n * `retryable` is the whole point: a draining pod and a bad signature are both\n * failures, and treating them alike is how a site either falls over on every\n * deploy or stays silently disconnected for a week.\n */\nexport class RelayUnavailable extends Error {\n readonly retryable: boolean;\n /** The protocol's own code, when the relay sent one. */\n readonly code: string;\n\n constructor(message: string, retryable: boolean, code: string) {\n super(message);\n this.name = \"RelayUnavailable\";\n this.retryable = retryable;\n this.code = code;\n }\n}\n\n/**\n * The job was not queued, and waiting will not change that.\n *\n * Distinct from {@link RelayUnavailable} because it is the opposite situation:\n * the relay answered, promptly and correctly, and the answer is that this job\n * has nowhere to go. Catching \"the relay is down\" to handle \"nobody has chosen\n * a model\" would retry forever against a fact.\n *\n * Two codes, and they belong to two different people.\n *\n * `purpose-not-declared` is the site's own manifest. It names the purpose and\n * the remedy, because a developer reading their own logs is entitled to both\n * and neither says anything about a person.\n *\n * `slot-unsatisfiable` is the person's own dashboard, and says only that.\n * Which service, whose device, whether one exists at all — none of it travels,\n * and the sentence is the same for everybody. A site learns *that* a slot\n * cannot be satisfied, which is exactly what the README has always promised\n * and what this class finally delivers.\n */\nexport class EnqueueRefused extends Error {\n /** `purpose-not-declared` or `slot-unsatisfiable`. */\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = \"EnqueueRefused\";\n this.code = code;\n }\n}\n\n/**\n * The one code that means \"ask me later\" on the enqueue endpoint.\n *\n * Inverted on 2026-09-02, and the inversion is the point. This was an\n * allowlist of *refusals* — `purpose-not-declared` and `slot-unsatisfiable` —\n * which made every refusal code the relay might add next a breaking change\n * for every client already deployed: an unknown code fell through to\n * `RelayUnavailable`, so a site's `catch (error instanceof EnqueueRefused)`\n * silently stopped matching and a permanent refusal was retried forever.\n *\n * The status class is the key; a list of codes is a description of it. A 409\n * on enqueue means the relay declined to queue the job, and there is nothing\n * to await and nothing to retry — whatever the code turns out to be.\n *\n * `409` alone is not the key, which is why this is not simply deleted: the\n * protocol overloads it. `not-ready` is a draining pod saying come back, and\n * `too-late` is about a job that already exists. Neither can reach the\n * enqueue endpoint as a refusal, so the discriminator is the class *on this\n * endpoint*, with the retryable code named because it is the exception.\n *\n * The direction of the default is deliberate. An unknown code treated as a\n * refusal surfaces as an error somebody can see; treated as unavailable it\n * becomes a silent retry against a condition that will never change — which\n * is the shape of the afternoon Kevin lost.\n */\nconst RETRYABLE_AT_ENQUEUE = new Set([\"not-ready\"]);\n\n/** The endpoint where a decline means the job was never queued. */\nconst ENQUEUE_ENDPOINT = \"enqueue\";\n\nexport interface PumpReport {\n /** Jobs sealed to a claiming device this cycle. */\n readonly sealed: string[];\n /** Results opened, verified and written to the store. */\n readonly completed: string[];\n /**\n * Jobs the relay offered that this site refused to seal for.\n *\n * Never silent: a site that cannot open its own at-rest envelope has a key\n * problem, and a device waiting on a payload that will never come is\n * exactly the case `awaiting-payload` exists to bound.\n */\n readonly refused: string[];\n /**\n * Why this cycle stopped early, when it did — alpha.31.\n *\n * A relay can legitimately say \"ask me later\": a pod draining through its\n * `preStop` window answers `503 not-ready` to every routed call, and that\n * happens on **every deploy**. Before this existed the lane read the body\n * of that answer, found no `jobs` in it, and threw `TypeError: finished.jobs\n * is not iterable` — a site falling over because its relay was polite.\n *\n * Absent on an ordinary cycle. Present, with the reason, when the lane\n * deferred: a site that quietly did nothing and a site that was told to wait\n * must not look the same in a log.\n */\n readonly deferred?: string;\n}\n\nexport class CloudLane {\n readonly #options: CloudLaneOptions;\n readonly #store: ByollmStore;\n readonly #siteKeys: StoredKeys;\n readonly #now: () => number;\n readonly #fetch: typeof fetch;\n\n constructor(deps: {\n options: CloudLaneOptions;\n store: ByollmStore;\n siteKeys: StoredKeys;\n now: () => number;\n }) {\n this.#options = deps.options;\n this.#store = deps.store;\n this.#siteKeys = deps.siteKeys;\n this.#now = deps.now;\n this.#fetch = deps.options.fetch ?? globalThis.fetch;\n }\n\n /**\n * Publish a job's stub for routing.\n *\n * The stub and nothing else — byollm_009 §6 makes that exhaustive by\n * construction, so this cannot leak a payload even by mistake: there is no\n * field on `JobStub` to put one in.\n */\n async publish(record: JobRecord): Promise<void> {\n const stub: JobStub = {\n id: record.id,\n kind: record.kind,\n owner: record.owner,\n // This site, by its identity key id — Amendment A §A.3. The relay\n // already knows which site it is routing for, so this discloses nothing\n // new to it; what it adds is that the *daemon* can check the stub\n // against the envelope's `senderKeyId` without asking the relay.\n site: keyId(publicIdentityOf(this.#siteKeys).identity),\n audience: record.audience,\n // `audienceAllow` is deliberately **not** published — cloud_008 §0.2.\n //\n // It is a list of the people who may run this job, and on the direct\n // plane that is unremarkable: the site authored the list and the site is\n // the upstream, so the party receiving it already has it. Through a\n // relay it is a third party, and byollm_009 §6's enumerated metadata —\n // \"exhaustive and normative… what an upstream can see, stated as a\n // commitment\" — does not include it. It was reaching the relay on every\n // named-audience job.\n //\n // Nothing is lost by withholding it, which is why this is a Tier 0 fix\n // rather than a trade. `matchAudience` treats it as a *narrowing*:\n // `job.audienceAllow !== undefined && !includes(daemon.owner)` refuses,\n // and its absence simply falls through to the checks that actually\n // enforce — the daemon's own allowlist (`NAMED_LOCAL_ALLOWLIST`) and the\n // backend's offer scope. On this lane the relay narrows too, from the\n // control plane's rosters. The enforcement was never here.\n ...(record.purpose === undefined ? {} : { purpose: record.purpose }),\n sizeClass: record.sizeClass,\n streaming: false,\n // The relay needs *a* deadline to bound routing. A job without one gets\n // the envelope's, which is the outer bound on how long the ciphertext\n // is worth carrying — never longer than the work could possibly matter.\n // The same fallback the direct plane uses — cloud_008 Tier 4, finding\n // 31. This said `createdAt + ENVELOPE_TTL_FALLBACK`, a local constant\n // whose value happened to equal `ENVELOPE_MAX_AGE_MS`; the direct plane\n // said `(claimableAt ?? now) + ttlMs`. One field, two meanings, and a\n // job that was blocked on a dependency got a deadline measured from\n // when it was *created* on one lane and from when it became *claimable*\n // on the other.\n deadlineAt: deadlineFor(record, this.#now()),\n };\n await this.#post(\"enqueue\", {\n siteId: this.#options.siteId,\n stub,\n });\n }\n\n /**\n * Withdraw a job at the relay — cloud_008 §2.2.\n *\n * `app.cancel()` marks the site's own row terminal, which stops the *next*\n * seal. It cannot stop a device that is already running the work, because\n * on this lane the site is not the upstream: only the relay talks to the\n * daemon, and it answered `cancel: []` unconditionally.\n *\n * So the cancellation has to travel. The relay marks the job, stops\n * offering it, and names it to the holding device at its next heartbeat —\n * the same path the direct plane has always had, arriving one hop later.\n */\n async cancel(jobId: string): Promise<void> {\n await this.#post(\"cancel\", { siteId: this.#options.siteId, jobId });\n }\n\n /**\n * One cycle: seal for anything claimed, collect anything finished.\n *\n * Idempotent and safe to call as often as you like. Exposed as a single\n * cycle rather than hidden behind a timer so a caller decides its own\n * cadence — a serverless site runs it on a cron, a long-lived one on an\n * interval, and a test runs it exactly when it means to.\n */\n async pump(): Promise<PumpReport> {\n const sealed: string[] = [];\n const refused: string[] = [];\n const completed: string[] = [];\n\n try {\n return await this.#cycle(sealed, refused, completed);\n } catch (error) {\n // Retryable: end the cycle, keep what was done, say why. Anything else\n // is a fact about this site's configuration and belongs to the caller —\n // a swallowed 401 is a site disconnected from its users with nothing in\n // any log to say so.\n if (error instanceof RelayUnavailable && error.retryable) {\n return { sealed, completed, refused, deferred: error.message };\n }\n throw error;\n }\n }\n\n async #cycle(\n sealed: string[],\n refused: string[],\n completed: string[],\n ): Promise<PumpReport> {\n const pending = (await this.#get(\"pending\")) as {\n jobs: {\n jobId: string;\n device: PublicIdentity;\n runnerId: string;\n leaseId: string;\n awaitingUntil: number;\n leaseExpiresAt: number;\n }[];\n };\n for (const claim of pending.jobs) {\n const record = await this.#store.get(claim.jobId);\n if (!record) continue;\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: {\n id: record.id,\n envelope: record.envelope,\n createdAt: record.createdAt,\n },\n device: claim.device,\n });\n if (!resealed.ok) {\n refused.push(claim.jobId);\n continue;\n }\n // Record the lease the relay granted, before handing over the work.\n //\n // The site is not the upstream here and does not decide who holds what\n // — but its own row has to know, or two things break that are not\n // cosmetic: `complete` refuses the result for want of a matching lease,\n // and the expiry sweep expires a job a device is in the middle of.\n // Adopting first means the worst case is a lease recorded for work that\n // never gets sealed, which the relay's own timeout already resolves.\n //\n // **The lease's clock, not the payload's** — cloud_008 §0.6. This\n // adopted `awaitingUntil`, which is how long the relay waits for *this\n // site to seal* (byollm_009 §7.1's third clock, ten seconds), and used\n // it as the expiry of a grant the device holds for a minute and renews\n // for as long as it works. Both of the breakages listed above then\n // happened to every job slower than the shorter clock — the site expired\n // the lease, the device finished anyway, and `complete` refused the\n // result the device had correctly produced.\n const adopted = await this.#store.adopt({\n jobId: claim.jobId,\n leaseId: claim.leaseId,\n expiresAt: claim.leaseExpiresAt,\n now: this.#now(),\n });\n // `null` means this store will not lend the job out — cloud_008 §2.2.\n //\n // Its own comment says why it refuses: a terminal or already-leased job\n // means the relay and this store disagree about reality. **And the\n // return value was being discarded**, so the site went on to seal the\n // payload to the claiming device anyway — for a job the app had already\n // cancelled, or whose deadline had passed, or that another lease\n // already owned.\n //\n // Sealing is the irreversible half: once the ciphertext is with the\n // relay, a device can fetch and run it. Refusing here is what makes\n // `adopt` a decision rather than a formality, and the job is reported\n // as refused so a site operator sees it rather than a device waiting\n // for work that will never be sealed.\n if (!adopted) {\n refused.push(claim.jobId);\n continue;\n }\n await this.#post(\"payload\", {\n siteId: this.#options.siteId,\n jobId: claim.jobId,\n envelope: resealed.envelope,\n });\n sealed.push(claim.jobId);\n }\n\n const finished = (await this.#get(\"results\")) as {\n jobs: {\n jobId: string;\n envelope: SealedEnvelope;\n disposition: string;\n runnerId: string;\n leaseId: string;\n device: PublicIdentity;\n runnerOwner: string;\n }[];\n };\n for (const done of finished.jobs) {\n const record = await this.#store.get(done.jobId);\n if (!record || record.state === \"ok\" || record.state === \"error\") {\n continue;\n }\n const outcome = await this.#openResult(done);\n if (!outcome) {\n refused.push(done.jobId);\n continue;\n }\n // Provenance is built here, from the job's audience and the device the\n // relay named — never from anything the daemon asserted. Identical to\n // the direct plane's rule, and it has to be: a result arriving via a\n // relay is not more trustworthy for having travelled further.\n await this.#store.complete({\n jobId: done.jobId,\n // The relay named the device; the signature above proved it — §3.6.\n runnerId: done.runnerId,\n // The grant, not the machine: this site never paired with the device\n // that ran it, and the signature it verified above is the stronger\n // claim about who did.\n holder: { by: \"lease\", leaseId: done.leaseId },\n outcome: outcome.outcome,\n provenance: provenanceFor({\n audience: record.audience,\n runnerId: done.runnerId,\n // The owner, from the relay's own record of who claimed it — not a\n // key id. cloud_008 §2.5: this said `keyId(device.identity)`, which\n // put a key id where the direct plane puts a user id, so an app\n // comparing provenance across lanes compared two namespaces and got\n // `false` for the same person. The device's key is still what the\n // signature was verified against, above; that is a different\n // question from whose machine it is.\n runnerOwner: done.runnerOwner,\n // From the envelope, not invented — cloud_008 §2.5. These were\n // hardcoded `\"http\"` and `\"unknown\"` because the daemon's declared\n // values stopped at the relay, which is right: a blind relay acts\n // on neither. Sealing them carries them past it untouched.\n backendClass: outcome.ran.backendClass,\n model: outcome.ran.model,\n }),\n now: this.#now(),\n });\n completed.push(done.jobId);\n }\n\n return { sealed, completed, refused };\n }\n\n /**\n * Open a sealed result and verify it came from the device that claimed it.\n *\n * The relay says which device ran the job; this checks that claim against a\n * signature the relay cannot produce. A relay that named the wrong device\n * gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`\n * from quietly becoming `RELAY_TRUSTED`.\n */\n async #openResult(done: {\n jobId: string;\n envelope: SealedEnvelope;\n disposition: string;\n device: PublicIdentity;\n }): Promise<SealedOutcome | null> {\n const opened = await open({\n envelope: done.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: done.device.identity,\n expected: {\n jobId: done.jobId,\n senderKeyId: keyId(done.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) return null;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(opened.plaintext);\n } catch {\n return null;\n }\n const sealed = SealedOutcome.safeParse(parsed);\n if (!sealed.success) return null;\n // The clear-text disposition is a routing hint the relay acted on. This\n // is the only place it can be checked, because this is the only party\n // that can open the envelope (byollm_009 §6.1).\n if (sealed.data.outcome.outcome !== done.disposition) return null;\n return sealed.data;\n }\n\n /**\n * Sign a site-plane call with this site's identity key.\n *\n * The same scheme the daemon uses against an upstream, because the site is\n * in the same position: an outbound caller whose key the relay already holds\n * for other reasons. Nothing else authenticates this plane — a relay that\n * took the `siteId` in a body at face value would let anyone enqueue work in\n * a site's name and read who claimed it.\n */\n #headers(endpoint: string, rawBody: string): Record<string, string> {\n const signature = signSiteRequest(this.#siteKeys, {\n endpoint,\n siteId: this.#options.siteId,\n issuedAt: this.#now(),\n body: rawBody,\n });\n return {\n \"x-byollm-site\": this.#options.siteId,\n \"x-byollm-issued-at\": String(signature.issuedAt),\n \"x-byollm-signature\": signature.signature,\n };\n }\n\n /**\n * A relay answer, checked before it is believed — alpha.31.\n *\n * The bug this closes is one line long and its shape is general: a response\n * body used without looking at the status. The daemon's client has always\n * done this properly (`client.ts` maps every status to a typed refusal); the\n * site's lane parsed JSON and hoped.\n *\n * Two classes, because they need opposite handling. **Retryable** — 503 from\n * a draining pod, 429, 5xx, and the protocol's own `not-ready` — means the\n * work is still there and this cycle should end quietly. **Refused** — a bad\n * signature, an unknown site, a version this relay does not speak — will\n * still be true in five seconds, and swallowing it would leave a site\n * silently disconnected from its own users.\n */\n async #answer(response: Response, endpoint: string): Promise<unknown> {\n if (response.ok) return response.json();\n\n let code = \"\";\n let message: string;\n try {\n const body = (await response.json()) as {\n error?: string;\n message?: string;\n };\n code = body.error ?? \"\";\n message = body.message ?? \"\";\n } catch {\n // A body that is not JSON is an intermediary answering, not the relay.\n message = `HTTP ${String(response.status)}`;\n }\n\n const retryable =\n response.status >= 500 ||\n response.status === 429 ||\n code === \"not-ready\" ||\n code === \"server-error\";\n\n if (\n endpoint === ENQUEUE_ENDPOINT &&\n response.status === 409 &&\n !RETRYABLE_AT_ENQUEUE.has(code)\n ) {\n // No job exists, so there is nothing to await and nothing to retry.\n // The code travels unread: a site branching on one it does not know\n // still learns that its job was refused, which is the fact it needs.\n throw new EnqueueRefused(message, code);\n }\n\n throw new RelayUnavailable(\n `${endpoint}: ${code || \"refused\"} — ${message}`,\n retryable,\n code,\n );\n }\n\n async #post(endpoint: string, body: unknown): Promise<unknown> {\n // The version travels in the body, as it does on the daemon plane — §B.4.\n // Added here rather than at each call site so a new site-plane call cannot\n // be written without it, which is how the site plane came to be outside\n // the handshake in the first place.\n const rawBody = JSON.stringify({\n protocolVersion: PROTOCOL_VERSION,\n ...(body as Record<string, unknown>),\n });\n const response = await this.#fetch(\n `${this.#options.relayOrigin}/relay/site/${endpoint}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...this.#headers(endpoint, rawBody),\n },\n body: rawBody,\n },\n );\n return this.#answer(response, endpoint);\n }\n\n async #get(endpoint: string): Promise<unknown> {\n // A GET has no body, so the version rides in the query — the other half\n // of `declaredVersion`, and the reason that helper takes both.\n const url =\n `${this.#options.relayOrigin}/relay/site/${endpoint}` +\n `?siteId=${encodeURIComponent(this.#options.siteId)}` +\n `&protocolVersion=${encodeURIComponent(PROTOCOL_VERSION)}`;\n // A read signs an empty body: the site id is in the query and in the\n // signed caller slot, and the relay refuses the request unless they agree.\n const response = await this.#fetch(url, {\n headers: this.#headers(endpoint, \"\"),\n });\n return this.#answer(response, endpoint);\n }\n}\n","import { StoredKeys, generateKeys, publicIdentityOf } from \"@byollm/protocol\";\nimport { fingerprint } from \"@byollm/protocol\";\n\n/**\n * A site's keypairs — byollm_009 §5.\n *\n * **Generate once, store, supply.** Not at startup, and not per process.\n *\n * A site is usually more than one process: several instances behind a load\n * balancer, or a serverless function whose module is evaluated per cold\n * start. Keys generated at startup would give each of those a different\n * identity. A daemon pins whichever one approved its pairing, and then every\n * request routed to a different instance fails a signature check with nothing\n * in the error explaining why — a failure that appears only under\n * horizontal scale, which is to say only in production.\n *\n * So the library takes keys as an input and never invents them. That is the\n * whole reason this module is three functions rather than a lazy singleton.\n */\n\n/** Make a fresh site identity. Call this once, ever, and keep the result. */\nexport const generateSiteKeys = (now: number = Date.now()): StoredKeys =>\n generateKeys(now);\n\n/**\n * Read site keys from an environment variable holding base64 JSON.\n *\n * The shape a deployment actually wants: one opaque secret, set the way every\n * other secret is set, with no file to mount and no key material in the\n * repository.\n *\n * @throws with a message naming the variable and the fix, because this fails\n * at boot and the person reading the log is the person who can fix it.\n */\nexport function siteKeysFromEnv(\n variable = \"BYOLLM_SITE_KEYS\",\n env: NodeJS.ProcessEnv = process.env,\n): StoredKeys {\n const raw = env[variable];\n if (raw === undefined || raw === \"\") {\n throw new Error(\n `${variable} is not set. Generate a site identity once with ` +\n `\\`npx @byollm/server keygen\\` and set it as ${variable}. ` +\n `Do not generate keys at startup: every instance would get a ` +\n `different identity and daemons would pin one and be refused by ` +\n `another.`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(raw, \"base64\").toString(\"utf8\"));\n } catch {\n throw new Error(\n `${variable} is not base64-encoded JSON. It should be exactly what ` +\n `\\`npx @byollm/server keygen\\` printed.`,\n );\n }\n\n const result = StoredKeys.safeParse(parsed);\n if (!result.success) {\n throw new Error(\n `${variable} does not contain a valid site identity. Regenerate it ` +\n `with \\`npx @byollm/server keygen\\` — and if this site has already ` +\n `paired daemons, they will need to pair again.`,\n );\n }\n return result.data;\n}\n\n/** What to print from `keygen`: the secret to store, and how to check it. */\nexport function formatSiteKeys(keys: StoredKeys): string {\n const encoded = Buffer.from(JSON.stringify(keys)).toString(\"base64\");\n const pub = publicIdentityOf(keys);\n return (\n `# ── 1. SECRET — set this on your server, and nowhere else ────────────\\n` +\n `#\\n` +\n `# This is the site's identity. Anything holding it can *be* this site,\\n` +\n `# so it goes wherever your deployment keeps secrets — never in a repo,\\n` +\n `# never in a browser, never pasted into a dashboard.\\n` +\n `BYOLLM_SITE_KEYS=${encoded}\\n` +\n `\\n` +\n `# ── 2. PUBLIC — paste this line into the byollm dashboard ────────────\\n` +\n `#\\n` +\n `# The public half. It proves signatures and seals nothing, so it is\\n` +\n `# safe to publish — which is the point: users pin it, and the relay\\n` +\n `# cannot forge work without the secret above.\\n` +\n `${JSON.stringify(pub)}\\n` +\n `\\n` +\n `# ── 3. Fingerprint — what a person compares by eye ───────────────────\\n` +\n `#\\n` +\n `# A fingerprint is not secret. Show it on your site so somebody\\n` +\n `# connecting can check it against what their daemon printed.\\n` +\n `# The dashboard derives this itself, so there is nothing to paste.\\n` +\n `# ${fingerprint(pub.identity)}\\n`\n );\n}\n","import {\n backendDescriptor,\n matchAudience,\n type Capability,\n} from \"@byollm/protocol\";\nimport { generateLeaseId } from \"./ids.js\";\nimport type {\n StoredJobInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"./records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n AdoptArgs,\n CompleteArgs,\n CompleteResult,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"./store.js\";\n\n/** Tunables an embedder may want to override in tests. */\nexport interface MemoryStoreOptions {\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\nconst DEFAULT_TTL_MS = 15 * 60_000;\n\n/**\n * The reference store: everything in one process, no persistence.\n *\n * This is not a toy — it is the implementation the conformance kit certifies\n * first, so its semantics *are* the specification's semantics for anything\n * the prose leaves implicit. A SQL adapter is correct when the same kit\n * passes against it.\n *\n * Concurrency: JavaScript's single-threaded turn is the atomicity primitive.\n * `claim` performs its read-decide-write with no `await` inside the critical\n * section, which is what makes {@link MUSTS.CLAIM_ATOMIC} hold here. A SQL\n * adapter gets the same property from `FOR UPDATE SKIP LOCKED`.\n */\nexport class MemoryStore implements ByollmStore {\n readonly #jobs = new Map<string, JobRecord>();\n readonly #runners = new Map<string, RunnerRecord>();\n readonly #pairings = new Map<string, PairingRecord>();\n /** Job ids the app has asked to cancel, not yet acknowledged by a runner. */\n readonly #cancelRequests = new Set<string>();\n readonly #defaultTtlMs: number;\n\n constructor(options: MemoryStoreOptions = {}) {\n this.#defaultTtlMs = options.defaultTtlMs ?? DEFAULT_TTL_MS;\n }\n\n // -- jobs ---------------------------------------------------------------\n\n create(input: StoredJobInput, now: number): Promise<JobRecord> {\n // Required now: the app mints the id before sealing, because the\n // envelope binds it.\n const id = input.id;\n const existing = this.#jobs.get(id);\n // Idempotent by caller-supplied id: re-enqueueing the same id is a no-op,\n // so an app's retry cannot duplicate work (the house \"one door\" rule).\n if (existing) return Promise.resolve(existing);\n\n const dependsOn = [...(input.dependsOn ?? [])];\n const blocked = dependsOn.some(\n (depId) => this.#jobs.get(depId)?.state !== \"ok\",\n );\n\n const job: JobRecord = {\n id,\n kind: input.kind,\n envelope: input.envelope,\n sizeClass: input.sizeClass,\n audience: input.audience ?? \"private\",\n purpose: input.purpose,\n owner: input.owner,\n audienceAllow: input.audienceAllow ? [...input.audienceAllow] : undefined,\n dependsOn,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n createdAt: now,\n // The TTL clock starts here only if nothing blocks the job.\n claimableAt: blocked ? null : now,\n ttlMs: input.ttlMs ?? this.#defaultTtlMs,\n deadlineAt: input.deadlineAt ?? null,\n refusedBy: [],\n attempts: 0,\n outcome: null,\n provenance: null,\n updatedAt: now,\n };\n this.#write(id, job);\n return Promise.resolve(job);\n }\n\n get(jobId: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(jobId) ?? null);\n }\n\n claim(args: ClaimArgs): Promise<JobRecord[]> {\n // Sweep first so an expired job is never handed out.\n this.#expireDueSync(args.now);\n\n const claimed: JobRecord[] = [];\n // Oldest-claimable first: a job that has waited longest goes next.\n const candidates = [...this.#jobs.values()].sort(\n (a, b) => (a.claimableAt ?? Infinity) - (b.claimableAt ?? Infinity),\n );\n\n for (const job of candidates) {\n if (claimed.length >= args.max) break;\n if (!this.#isClaimable(job, args)) continue;\n\n const updated: JobRecord = {\n ...job,\n state: \"claimed\",\n lease: {\n // A fresh id per grant. Two claims of the same job by the same\n // runner are two different leases, and must be distinguishable.\n id: generateLeaseId(),\n runnerId: args.runnerId,\n expiresAt: args.now + args.leaseMs,\n },\n attempts: job.attempts + 1,\n updatedAt: args.now,\n };\n this.#write(job.id, updated);\n claimed.push(updated);\n }\n return Promise.resolve(claimed);\n }\n\n /**\n * The claim predicate, shared by `claim` and the no-runner signal so the\n * two can never disagree about what \"a runner that could take this\" means.\n */\n #isClaimable(\n job: JobRecord,\n args: Pick<ClaimArgs, \"runnerId\" | \"runnerOwner\" | \"capabilities\" | \"now\">,\n ): boolean {\n if (job.state !== \"queued\") return false;\n if (job.claimableAt === null || job.claimableAt > args.now) return false;\n if (job.refusedBy.includes(args.runnerId)) return false;\n\n const capability = capabilityFor(args.capabilities, job.kind);\n if (!capability) return false;\n\n const match = matchAudience(\n {\n owner: job.owner,\n audience: job.audience,\n audienceAllow: job.audienceAllow,\n },\n {\n owner: args.runnerOwner,\n offerScope: capability.offerScope,\n // From the registry, not a local guess — the cost rules must mean the\n // same thing on both sides of the wire. The server cannot see a\n // remote daemon's base URL, so a generic backend with no declared\n // cost is treated as metered: the expensive side, and the daemon\n // refuses anyway if it disagrees (byollm_007 §2).\n cost: backendDescriptor(capability.backendId).cost ?? \"metered\",\n // Nor can it see the owner's spend consent. It offers; the daemon is\n // the enforcing side and releases with `refused` if its own rules say\n // no — the same shape as the `named` allowlist.\n spend: { acknowledged: true },\n // The server cannot see a remote daemon's local allowlist and must\n // not pretend to (protocol §4.2). It admits the job here; the daemon\n // is the enforcing side and releases with `refused` if its own list\n // says no.\n admits: () => true,\n },\n );\n return match.ok;\n }\n\n renewLeases(args: RenewArgs): Promise<RenewResult> {\n this.#expireDueSync(args.now);\n\n const renewed: { jobId: string; expiresAt: number }[] = [];\n const lost: { jobId: string; leaseId: string }[] = [];\n\n for (const { jobId, leaseId } of args.leases) {\n const job = this.#jobs.get(jobId);\n if (\n !job ||\n job.lease?.runnerId !== args.runnerId ||\n job.lease.id !== leaseId\n ) {\n // Reclaimed by someone else, terminal, or a different grant than the\n // one being renewed — either way this runner must stop. Named by the\n // grant the daemon asked about, which is the one it must abandon.\n lost.push({ jobId, leaseId });\n continue;\n }\n if (job.state !== \"claimed\" && job.state !== \"running\") {\n lost.push({ jobId, leaseId });\n continue;\n }\n const expiresAt = args.now + args.leaseMs;\n this.#write(jobId, {\n ...job,\n state: \"running\",\n // Renewal extends the existing grant; it does not mint a new one.\n lease: { ...job.lease, expiresAt },\n updatedAt: args.now,\n });\n renewed.push({ jobId, expiresAt });\n }\n return Promise.resolve({ renewed, lost });\n }\n\n adopt(args: AdoptArgs): Promise<JobRecord | null> {\n const job = this.#jobs.get(args.jobId);\n if (!job) return Promise.resolve(null);\n // Only a job that is genuinely available can be adopted. A terminal or\n // already-leased job means the relay and this store disagree about\n // reality, and the store's row is not the place to resolve that.\n if (job.state !== \"queued\" && job.state !== \"claimed\") {\n return Promise.resolve(null);\n }\n if (job.lease && job.lease.id !== args.leaseId) {\n return Promise.resolve(null);\n }\n const updated: JobRecord = {\n ...job,\n state: \"claimed\",\n lease: {\n id: args.leaseId,\n // No runner: this site never paired with the machine holding it.\n runnerId: \"\",\n expiresAt: args.expiresAt,\n },\n updatedAt: args.now,\n };\n this.#write(updated.id, updated);\n return Promise.resolve(updated);\n }\n\n complete(args: CompleteArgs): Promise<CompleteResult> {\n const job = this.#jobs.get(args.jobId);\n if (!job) return Promise.resolve({ accepted: false, job: null });\n\n // First terminal outcome wins; a later submission is discarded, not\n // applied ({@link MUSTS.RESULT_IDEMPOTENT}).\n // Terminal before holder — cloud_008 §3.6, and the same order in all four\n // stores. `RESULT_IDEMPOTENT` used to hold only because `complete` nulls\n // the lease, so a replay tripped the holder check first and the branch\n // named after the MUST was never reached. A MUST that byollm_009 §4's\n // case for signed requests leans on cannot hold by coincidence.\n //\n // **Scoped to the device that finished it.** A replay from that grant is\n // a duplicate and is told so; anyone else falls through to the holder\n // check and gets exactly the refusal they would get for a job that is not\n // terminal. Answering them differently would make a job id a terminality\n // probe.\n if (\n job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\"\n ) {\n // The same device *and* the same grant. Either alone is not the\n // device that finished the job: a lease id can be presented by whoever\n // learned it, and a device can hold a later grant on a job it never\n // completed.\n const sameDevice =\n job.provenance?.runnerId !== undefined &&\n job.provenance.runnerId === args.runnerId;\n const sameGrant =\n args.holder.by === \"lease\" &&\n job.completedByLeaseId !== null &&\n job.completedByLeaseId === args.holder.leaseId;\n if (sameDevice && sameGrant) {\n return Promise.resolve({ accepted: false, duplicate: true, job });\n }\n return Promise.resolve({ accepted: false, job });\n }\n if (job.state === \"expired\") {\n return Promise.resolve({ accepted: false, job });\n }\n // A runner that lost its lease may not write a result\n // ({@link MUSTS.LEASE_HONORED}). Named by lease id when the caller has\n // one — off the direct plane there is no runner this site knows.\n const holds =\n args.holder.by === \"runner\"\n ? job.lease?.runnerId === args.holder.runnerId\n : job.lease?.id === args.holder.leaseId;\n if (!holds) {\n return Promise.resolve({ accepted: false, job });\n }\n\n const state =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n const updated: JobRecord = {\n ...job,\n state,\n lease: null,\n // The grant that recorded it, kept after the lease is dropped — §3.6.\n completedByLeaseId:\n args.holder.by === \"lease\"\n ? args.holder.leaseId\n : (job.lease?.id ?? null),\n outcome: args.outcome,\n provenance: args.provenance,\n updatedAt: args.now,\n };\n this.#write(job.id, updated);\n this.#cancelRequests.delete(job.id);\n\n if (state === \"ok\") this.#unblockDependents(job.id, args.now);\n\n return Promise.resolve({ accepted: true, job: updated });\n }\n\n /**\n * Start the TTL clock on anything this job was blocking.\n *\n * Deliberately only on `ok`: a dependency that errored leaves its dependents\n * blocked forever rather than releasing them into a run whose input never\n * arrived. They expire at their absolute deadline if one was set, and the\n * app sees a chain that stopped where it broke.\n */\n #unblockDependents(completedId: string, now: number): void {\n for (const job of this.#jobs.values()) {\n if (job.claimableAt !== null) continue;\n if (!job.dependsOn.includes(completedId)) continue;\n const ready = job.dependsOn.every(\n (depId) => this.#jobs.get(depId)?.state === \"ok\",\n );\n if (ready) {\n this.#write(job.id, { ...job, claimableAt: now, updatedAt: now });\n }\n }\n }\n\n /**\n * Watchers, by job id (byollm_009 §8.3).\n *\n * A `Set` per job so an unsubscribe removes exactly the handler it\n * registered — two waiters on the same job are ordinary, and removing by\n * job id alone would silently cancel someone else's wait.\n */\n readonly #watchers = new Map<string, Set<() => void>>();\n\n subscribe(jobId: string, onChange: () => void): () => void {\n const existing = this.#watchers.get(jobId) ?? new Set<() => void>();\n existing.add(onChange);\n this.#watchers.set(jobId, existing);\n let live = true;\n return () => {\n // Idempotent: the contract says calling twice is safe, and a `finally`\n // that unsubscribes after an error path already did is the normal way\n // this gets called twice.\n if (!live) return;\n live = false;\n const set = this.#watchers.get(jobId);\n set?.delete(onChange);\n if (set?.size === 0) this.#watchers.delete(jobId);\n };\n }\n\n /**\n * The single write path for a job.\n *\n * Every mutation goes through here so notification cannot be forgotten by\n * a future one. Nine call sites existed when the push seam was added, and\n * \"remember to notify\" is not a property nine call sites keep.\n */\n #write(jobId: string, record: JobRecord): void {\n this.#jobs.set(jobId, record);\n this.#notify(jobId);\n }\n\n /**\n * Tell anyone watching that a job changed.\n *\n * A throwing watcher must not corrupt the store's own bookkeeping, so each\n * is isolated: this runs inside write paths, and one bad listener taking\n * out an unrelated write would be a far worse failure than a missed\n * notification.\n */\n #notify(jobId: string): void {\n for (const watcher of this.#watchers.get(jobId) ?? []) {\n try {\n watcher();\n } catch {\n // A watcher is a signal handler; the caller re-reads regardless.\n }\n }\n }\n\n release(args: ReleaseArgs): Promise<string[]> {\n const released: string[] = [];\n for (const { jobId, leaseId } of args.leases) {\n const job = this.#jobs.get(jobId);\n // The *grant*, not just its holder. Matching on runner id alone let a\n // replayed release from an earlier lease drop a later one, returning a\n // job to the queue while the daemon was still executing it.\n if (\n !job ||\n job.lease?.runnerId !== args.runnerId ||\n job.lease.id !== leaseId\n ) {\n continue;\n }\n\n this.#write(jobId, {\n ...job,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n // Newly available again, so the TTL clock restarts here too.\n claimableAt: args.now,\n // A refusal is remembered, or the pair spins between claim and\n // release forever ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n refusedBy:\n args.reason === \"refused\"\n ? [...new Set([...job.refusedBy, args.runnerId])]\n : job.refusedBy,\n updatedAt: args.now,\n });\n released.push(jobId);\n }\n return Promise.resolve(released);\n }\n\n expireDue(now: number): Promise<JobRecord[]> {\n return Promise.resolve(this.#expireDueSync(now));\n }\n\n /**\n * Lease expiry and TTL expiry in one idempotent sweep.\n *\n * Order matters: a lease is reclaimed *before* the TTL is judged, so a job\n * whose runner died is offered again rather than being expired for having\n * sat in `claimed` too long.\n */\n #expireDueSync(now: number): JobRecord[] {\n const changed: JobRecord[] = [];\n\n for (const job of this.#jobs.values()) {\n if (\n (job.state === \"claimed\" || job.state === \"running\") &&\n job.lease !== null &&\n job.lease.expiresAt <= now\n ) {\n const requeued: JobRecord = {\n ...job,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n // The TTL clock restarts: it measures how long a job has waited\n // *unclaimed*, and this job has just become available again. Without\n // this, a job whose runner died would expire for time it spent being\n // actively worked on — losing exactly the work `kill -9` recovery\n // exists to save. Total lifetime is bounded by `deadlineAt`, which\n // is absolute and unaffected by reclaim.\n claimableAt: now,\n updatedAt: now,\n };\n this.#write(job.id, requeued);\n changed.push(requeued);\n }\n }\n\n for (const job of this.#jobs.values()) {\n if (job.state !== \"queued\") continue;\n const pastDeadline = job.deadlineAt !== null && job.deadlineAt <= now;\n const pastTtl =\n job.claimableAt !== null && job.claimableAt + job.ttlMs <= now;\n if (!pastDeadline && !pastTtl) continue;\n\n const expired: JobRecord = {\n ...job,\n state: \"expired\",\n lease: null,\n completedByLeaseId: null,\n updatedAt: now,\n };\n this.#write(job.id, expired);\n changed.push(expired);\n }\n return changed;\n }\n\n cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const job = this.#jobs.get(jobId);\n if (!job) return Promise.resolve(null);\n if (job.state === \"queued\") {\n const canceled: JobRecord = {\n ...job,\n state: \"canceled\",\n lease: null,\n completedByLeaseId: null,\n updatedAt: now,\n };\n this.#write(jobId, canceled);\n return Promise.resolve(canceled);\n }\n if (job.state === \"claimed\" || job.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself, so the job's own state waits.\n this.#cancelRequests.add(jobId);\n return Promise.resolve(job);\n }\n return Promise.resolve(job);\n }\n\n listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n return Promise.resolve(\n [...this.#jobs.values()].filter(\n (job) => job.lease?.runnerId === runnerId,\n ),\n );\n }\n\n listCancelRequests(\n runnerId: string,\n ): Promise<{ jobId: string; leaseId: string }[]> {\n return Promise.resolve(\n [...this.#cancelRequests]\n .map((jobId) => ({ jobId, lease: this.#jobs.get(jobId)?.lease }))\n .filter((row) => row.lease?.runnerId === runnerId)\n // The grant, not the id — V1-3.\n .map((row) => ({ jobId: row.jobId, leaseId: row.lease?.id ?? \"\" })),\n );\n }\n\n // -- pairing and runners -------------------------------------------------\n\n createPairing(record: PairingRecord): Promise<void> {\n this.#pairings.set(record.deviceCodeHash, record);\n return Promise.resolve();\n }\n\n getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null> {\n return Promise.resolve(this.#pairings.get(hash) ?? null);\n }\n\n getPairingByUserCode(userCode: string): Promise<PairingRecord | null> {\n for (const pairing of this.#pairings.values()) {\n if (pairing.userCode === userCode) return Promise.resolve(pairing);\n }\n return Promise.resolve(null);\n }\n\n approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === args.userCode,\n );\n if (!pairing) throw new Error(`unknown pairing code: ${args.userCode}`);\n if (pairing.expiresAt <= args.now) {\n throw new Error(\"pairing code has expired\");\n }\n if (pairing.state !== \"pending\") {\n throw new Error(`pairing is already ${pairing.state}`);\n }\n\n const runner: RunnerRecord = {\n id: args.runnerId,\n owner: args.owner,\n // Carried from the pairing, not re-supplied at approval: the user\n // approved a specific machine, and the runner must be that machine.\n device: pairing.device,\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities,\n paused: false,\n revokedAt: null,\n lastHeartbeatAt: args.now,\n createdAt: args.now,\n };\n this.#runners.set(runner.id, runner);\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"approved\",\n owner: args.owner,\n runnerId: runner.id,\n collected: false,\n });\n return Promise.resolve(runner);\n }\n\n denyPairing(userCode: string, _now: number): Promise<void> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === userCode,\n );\n if (pairing) {\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"denied\",\n });\n }\n return Promise.resolve();\n }\n\n consumePairingToken(deviceCodeHash: string): Promise<void> {\n const pairing = this.#pairings.get(deviceCodeHash);\n if (pairing) {\n this.#pairings.set(deviceCodeHash, {\n ...pairing,\n collected: true,\n });\n }\n return Promise.resolve();\n }\n\n getRunner(runnerId: string): Promise<RunnerRecord | null> {\n return Promise.resolve(this.#runners.get(runnerId) ?? null);\n }\n\n touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const runner = this.#runners.get(args.runnerId);\n if (!runner) return Promise.resolve(null);\n const updated: RunnerRecord = {\n ...runner,\n capabilities: args.capabilities,\n daemonVersion: args.daemonVersion,\n paused: args.paused,\n lastHeartbeatAt: args.now,\n };\n this.#runners.set(runner.id, updated);\n return Promise.resolve(updated);\n }\n\n revokeRunner(runnerId: string, now: number): Promise<void> {\n const runner = this.#runners.get(runnerId);\n // Revocation is one-way: an already-revoked runner keeps its first\n // revocation time rather than being re-stamped.\n if (runner?.revokedAt === null) {\n this.#runners.set(runnerId, { ...runner, revokedAt: now });\n }\n return Promise.resolve();\n }\n\n listRunners(owner?: string): Promise<RunnerRecord[]> {\n const all = [...this.#runners.values()];\n return Promise.resolve(\n owner === undefined ? all : all.filter((r) => r.owner === owner),\n );\n }\n\n // -- test/demo helpers ---------------------------------------------------\n\n /** All jobs, for demos and assertions. Not part of the store interface. */\n allJobs(): JobRecord[] {\n return [...this.#jobs.values()];\n }\n}\n\n/** The capability that would serve a kind, if any. */\nexport function capabilityFor(\n capabilities: readonly Capability[],\n kind: string,\n): Capability | undefined {\n return capabilities.find((c) => c.kind === kind);\n}\n"],"mappings":"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","names":["keyId","publicIdentityOf","keyId","publicIdentityOf","publicIdentityOf","backendDescriptor","matchAudience","matchAudience","backendDescriptor"]}
1
+ {"version":3,"sources":["../src/app.ts","../src/cloud.ts","../src/keys.ts","../src/memory.ts"],"sourcesContent":["import {\n ENVELOPE_MAX_AGE_MS,\n KindedPayload,\n keyId,\n payloadTextLength,\n publicIdentityOf,\n seal,\n sizeClassOf,\n type StoredKeys,\n backendDescriptor,\n matchAudience,\n type Audience,\n type DeliveredResult,\n type JobKind,\n type MatchRefusal,\n} from \"@byollm/protocol\";\nimport {\n PollingDelivery,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n} from \"./delivery.js\";\nimport { generateJobId, generateRunnerId } from \"./ids.js\";\nimport { CloudLane, type CloudLaneOptions } from \"./cloud.js\";\nimport type { EnqueueInput, JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/**\n * How long since a runner's last heartbeat before it stops counting as live.\n * Three heartbeats of slack at the daemon's ~10s cadence.\n */\nconst DEFAULT_LIVENESS_MS = 35_000;\n\n/** Why a job cannot presently run. */\nexport type NoRunnerReason =\n | \"no-runner-paired\"\n | \"no-runner-online\"\n | \"no-matching-capability\"\n | \"audience-admits-nobody\"\n /**\n * The owner's default for this kind can never serve *this* requester —\n * byollm_016's defaults-meet-audiences corner.\n *\n * The specimen: a default of `claude-cli`, self-locked by\n * `SUBSCRIPTION_SELF_LOCK`, and a team member's unselected job. It resolves\n * to something that will never run it. Reported rather than left to time\n * out, because a wait that can never end is indistinguishable from one that\n * has not ended yet, and only one of them is worth waiting through.\n */\n | \"default-unusable\";\n\n/**\n * The no-runner signal (byollm_001 Rev 1 §D).\n *\n * `available: false` means an app should fall back — hosted model, \"start\n * your runner\" prompt — rather than awaiting something that will never\n * resolve. A job still blocked on dependencies is **not** unavailable; it is\n * waiting, and saying otherwise would make every multi-job flow look broken\n * ({@link MUSTS.NO_RUNNER_SIGNAL}).\n */\nexport interface RunnerAvailability {\n readonly available: boolean;\n readonly reason?: NoRunnerReason;\n /** Live runners that could take work of this shape. */\n readonly candidates: number;\n}\n\nexport interface AvailabilityQuery {\n readonly kind: JobKind;\n readonly owner: string;\n readonly audience?: Audience;\n readonly audienceAllow?: readonly string[];\n}\n\nexport interface ByollmAppOptions {\n readonly store: ByollmStore;\n /** Injectable clock. */\n readonly now?: () => number;\n /** Liveness window for the no-runner signal. */\n readonly livenessMs?: number;\n /**\n * How the app learns a job finished. Defaults to polling the store, which\n * is correct everywhere; the Supabase adapter substitutes Realtime.\n */\n readonly delivery?: (deps: PollingDeliveryDeps) => ResultDelivery;\n /**\n * How long a sustained no-runner signal must persist before `result()`\n * gives up. Longer tolerates a daemon restarting; shorter fails faster.\n */\n readonly noRunnerGraceMs?: number;\n /**\n * This site's keypairs — the same ones the handlers use.\n *\n * The app needs them because it is the *endpoint*: it seals work on the way\n * in and opens results on the way out. Nothing between those two points\n * holds plaintext (byollm_009 §10).\n */\n readonly siteKeys: StoredKeys;\n /**\n * Which connection plane this site uses — cloud_004 §9.4.\n *\n * Omitted means `direct`: a daemon reaches this site's own handlers, and\n * everything works as it always has. Supplying a relay switches the plane\n * and nothing else — `enqueue` is identical in every lane, which is the\n * property that lets the same app move between them by config.\n */\n readonly lane?: CloudLaneOptions;\n}\n\n/**\n * An enqueued job, with the delivery channel attached.\n *\n * `result()` is sugar over the channel — with a timeout and a\n * `noRunnerAvailable` path — never a bare promise that can hang forever\n * (byollm_003 Rev 1).\n */\nexport interface JobHandle {\n readonly id: string;\n /** The job as stored at enqueue time. */\n readonly record: JobRecord;\n /** Wait for a terminal outcome. */\n result(options?: WaitOptions): Promise<DeliveredResult>;\n /** Ask the runner to stop. */\n cancel(): Promise<void>;\n}\n\n/**\n * The app-facing half of `@byollm/server`.\n *\n * The daemon talks to {@link ByollmHandlers}; the app talks to this. Keeping\n * them separate is what makes \"one door per state write\" hold — an app\n * enqueues and cancels through these methods and never writes job rows by\n * hand.\n */\n/**\n * Every option `enqueue` accepts, as data.\n *\n * `Record<keyof EnqueueInput, true>` rather than a hand-kept array, so the\n * compiler refuses this file when a field is added to `EnqueueInput` and not\n * to this list. An allowlist that silently falls behind the type it guards is\n * worse than none: it would start rejecting the very field somebody just\n * added, in the name of catching typos.\n */\nconst ENQUEUE_OPTIONS: Readonly<Record<keyof EnqueueInput, true>> =\n Object.freeze({\n kind: true,\n payload: true,\n owner: true,\n audience: true,\n purpose: true,\n audienceAllow: true,\n dependsOn: true,\n ttlMs: true,\n deadlineAt: true,\n id: true,\n });\n\nexport class ByollmApp {\n readonly #store: ByollmStore;\n readonly #siteKeys: StoredKeys;\n readonly #now: () => number;\n readonly #livenessMs: number;\n readonly #delivery: ResultDelivery;\n /** Present only in the cloud lane; the site's side of the relay. */\n readonly cloud: CloudLane | undefined;\n\n constructor(options: ByollmAppOptions) {\n this.#store = options.store;\n this.#siteKeys = options.siteKeys;\n this.#now = options.now ?? Date.now;\n this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;\n this.cloud =\n options.lane === undefined\n ? undefined\n : new CloudLane({\n options: options.lane,\n store: options.store,\n siteKeys: options.siteKeys,\n now: this.#now,\n });\n\n /**\n * The delivery's dependencies — and on the cloud lane, one fewer.\n *\n * `runnerAvailability` refuses on the cloud lane, deliberately: it counts\n * runners in this site's own store, devices there pair with the relay\n * instead, and it spent a release reporting `no-runner-paired` with\n * confidence for every cloud-lane app that asked.\n *\n * The refusal shipped and this wrapper kept calling it. Delivery asks\n * every 500ms, so `job.result()` threw on its first poll for every\n * cloud-lane site — found by Kevin, on the ordinary consumer loop that\n * none of our own proofs ran.\n *\n * **The law it earned: when you make a function refuse, grep its callers\n * first.** We audited what branched on the untrusted flag and never\n * audited this method's internal callers. A refusal aimed at outsiders\n * that your own loop trips over is a crash wearing a principle.\n *\n * So the question is not asked. Delivery gets no availability instrument\n * on a lane where nothing can answer, rather than an instrument that\n * throws and a `catch` upstream pretending that means \"keep waiting\".\n */\n const deps: PollingDeliveryDeps = {\n ...(options.noRunnerGraceMs === undefined\n ? {}\n : { graceMs: options.noRunnerGraceMs }),\n read: (jobId) => this.result(jobId),\n ...(this.cloud !== undefined\n ? {}\n : { availability: this.#availabilityFor() }),\n };\n this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);\n }\n\n /**\n * The no-runner instrument, for a lane that can actually see runners.\n *\n * A method rather than an inline closure so the branch above reads as one\n * decision — whether this deployment has the instrument at all — instead of\n * a conditional wrapped around thirty lines of body.\n */\n #availabilityFor() {\n return async (jobId: string) => {\n const job = await this.#store.get(jobId);\n if (!job)\n return { available: false, reason: \"unknown-job\", blocked: false };\n // A job waiting on a dependency is waiting, not unavailable.\n if (job.claimableAt === null) {\n return { available: true, blocked: true };\n }\n const availability = await this.runnerAvailability({\n kind: job.kind,\n owner: job.owner,\n audience: job.audience,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: job.audienceAllow }),\n });\n return {\n available: availability.available,\n ...(availability.reason === undefined\n ? {}\n : { reason: availability.reason }),\n blocked: false,\n };\n };\n }\n\n /**\n * Enqueue a job.\n *\n * `audience` defaults to `self` — the safe direction. Widening it means the\n * result comes back marked untrusted (see {@link ByollmApp.result}), and\n * the app is obliged to disclose that to whoever reads it.\n */\n async enqueue<K extends JobKind>(input: EnqueueInput<K>): Promise<JobHandle> {\n // An option this SDK does not know is refused, never ignored.\n //\n // A caller newer than its SDK is the ordinary way this happens, and the\n // case that produced the rule: a site called `enqueue({ service })`\n // against a version that predated the field, the key went nowhere, and\n // nothing said so. The app believed it was selecting a service, was not,\n // and the only symptom was work running on one nobody chose. Silence is\n // the hazard rather than the missing feature.\n const unknown = Object.keys(input).filter(\n (key) => !(key in ENQUEUE_OPTIONS),\n );\n if (unknown.length > 0) {\n throw new Error(\n `enqueue does not understand ${unknown.map((k) => `\\`${k}\\``).join(\", \")}. ` +\n `An option this @byollm/server does not know is refused rather than ` +\n `ignored, because an ignored option is a job that runs differently ` +\n `than you asked with nothing to see — most often an SDK older than ` +\n `the code calling it. Upgrade @byollm/server, or remove the option.`,\n );\n }\n\n /**\n * `audience` is not a fact a cloud-lane site holds — so it may not state\n * one.\n *\n * Who may serve a job is decided by the person: their mapping names a\n * service and its owner, that owner's offer scope says who the service\n * serves, and the hub holds both at claim. The site's declaration was a\n * third vote cast by the one party the disclosure fence forbids from\n * knowing the answer.\n *\n * Which is exactly how its default came to disable the headline feature\n * in silence. It defaults to `private` — own devices only — so a site that\n * simply never mentioned it broke team sharing for every user who had a\n * team, while working perfectly for everyone testing alone. **A\n * declaration required from the party that cannot know is a default in\n * disguise.**\n *\n * Refused rather than ignored, by this method's own rule two paragraphs\n * up: an ignored option is a job that runs differently than asked with\n * nothing to see. The remedy travels with the refusal, because a caller\n * who set it was trying to express something real and deserves to know\n * where that decision now lives.\n */\n if (this.cloud !== undefined && input.audience !== undefined) {\n throw new Error(\n \"enqueue does not take `audience` on the cloud lane. Who may serve a \" +\n \"job is derived from the person's own mapping — the service they \" +\n \"chose, its owner, and that owner's sharing — which your site is \" +\n \"not told and cannot compute. Remove `audience`; ask for the kind \" +\n \"and the purpose, and their decision does the rest.\",\n );\n }\n\n // Validate the payload against its kind before anything stores it.\n //\n // The schemas are `.strict()`, so this drops a payload carrying fields\n // the kind does not define — `command`, `argv`, `model`, `baseUrl`. Types\n // do not survive a JSON boundary, and an app assembling a payload from\n // user input is the ordinary case, so \"the caller is typed\" is not a\n // check ({@link MUSTS.KIND_NO_CODE}, {@link MUSTS.NO_PAYLOAD_ROUTING}).\n //\n // Refusing here rather than relying on the daemon is deliberate. The\n // daemon does re-validate and would reject this — but it parses a whole\n // claim response at once, so one malformed job would fail the batch it\n // arrived in and stall unrelated work. Rejecting at enqueue puts the\n // error where the app can act on it.\n const parsed = KindedPayload.safeParse({\n kind: input.kind,\n payload: input.payload,\n });\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((issue) => `${issue.path.join(\".\") || \"(root)\"}: ${issue.message}`)\n .join(\"; \");\n throw new Error(`invalid ${input.kind} payload — ${detail}`);\n }\n\n // Sealed before it is stored, to this site's own key. The app is the\n // endpoint, so it can open its own work later; the store, its backups and\n // anything reading them cannot.\n // Two different deadlines, deliberately not conflated:\n //\n // - the *job's* deadline is the app's business, may be absent, and for a\n // dependent job its TTL clock does not even start until the job becomes\n // claimable (`TTL_EXPIRY`). Setting one here broke exactly that.\n // - the *envelope's* deadline bounds how long a captured ciphertext is\n // worth keeping. It is bound into the signature, so it has to be\n // recomputable at open time from what the record stores — hence\n // creation plus TTL, which never moves.\n // Resolved *here*, once, and passed to the store — because the envelope\n // binds it. Letting the app default one value and the store default\n // another produced a job whose seal and record disagreed, and therefore\n // work nobody could open.\n // One reading of the clock, used for both the seal and the record.\n //\n // Two readings passed every fake-clock test and failed against a real\n // one: the envelope bound `createdAt + ttlMs` from the first call and the\n // record stored `createdAt` from the second, a millisecond later, so\n // nothing could be opened. A fixed clock returns the same number twice\n // and hides it completely.\n const createdAt = this.#now();\n // Independent of the job's TTL, deliberately. Binding the envelope to\n // `createdAt + ttl` meant the app had to decide a TTL in order to seal —\n // which overrode the store's own default and broke every expiry test.\n // The two answer different questions: how long the work is worth doing,\n // and how long the ciphertext is worth keeping.\n const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;\n const jobId = input.id ?? generateJobId();\n const senderKeyId = keyId(publicIdentityOf(this.#siteKeys).identity);\n const envelope = await seal({\n plaintext: JSON.stringify(parsed.data.payload),\n senderKeys: this.#siteKeys,\n recipientEncryptionPublic: this.#siteKeys.encryptionPublic,\n context: {\n jobId,\n senderKeyId,\n recipientKeyId: senderKeyId,\n deadlineAt: envelopeDeadlineAt,\n direction: \"payload\",\n },\n });\n\n const record = await this.#store.create(\n {\n ...input,\n /**\n * Derived here, because on the cloud lane it is derivable and nowhere\n * else knows the lane.\n *\n * Refusing the site's declaration is only half of \"derived, never\n * declared\" — the stub still carries an audience to the relay, and a\n * store that defaults it to `private` would keep every cloud job\n * private no matter who was forbidden from saying so. The half that\n * fixes anything is this one.\n *\n * `team` is the value that defers: it says a device whose owner\n * admits this person may serve, and the hub then decides whether one\n * does, from the mapping the person authored, its service's owner,\n * that owner's offer scope, and the roster. Nothing is widened by\n * saying it — both axes still have to agree, and the owner's scope is\n * the other axis.\n *\n * Direct mode keeps the store's `private` default: there is no\n * control plane there to derive from, and owner-only is the ruling.\n */\n ...(this.cloud === undefined ? {} : { audience: \"team\" as const }),\n id: jobId,\n envelope,\n sizeClass: sizeClassOf(\n payloadTextLength({\n kind: input.kind,\n payload: parsed.data.payload,\n } as Parameters<typeof payloadTextLength>[0]),\n ),\n },\n createdAt,\n );\n // The lane's only intrusion into enqueue, and it is additive: the record\n // is already stored and sealed at rest before anything is published, so a\n // relay that is down costs a routing delay rather than a lost job.\n await this.cloud?.publish(record);\n\n return {\n id: record.id,\n record,\n result: (options?: WaitOptions) =>\n this.#delivery.waitFor(record.id, options),\n cancel: async () => {\n await this.cancel(record.id);\n },\n };\n }\n\n /** Read a job's current state. */\n async job(jobId: string): Promise<JobRecord | null> {\n await this.#store.expireDue(this.#now());\n return this.#store.get(jobId);\n }\n\n /**\n * A job's result with its provenance attached.\n *\n * Check `provenance.untrusted` before rendering. It is true for every\n * `named`/`public` job, because that text came from someone else's machine\n * and the app must not present it as its own AI's answer\n * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n */\n async result(jobId: string): Promise<DeliveredResult | null> {\n const job = await this.job(jobId);\n if (!job) return null;\n return {\n jobId: job.id,\n state: job.state,\n ...(job.outcome === null ? {} : { outcome: job.outcome }),\n ...(job.provenance === null ? {} : { provenance: job.provenance }),\n };\n }\n\n /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */\n async cancel(jobId: string): Promise<JobRecord | null> {\n const cancelled = await this.#store.cancel(jobId, this.#now());\n // On the cloud lane the relay is the only party talking to the daemon, so\n // a cancellation that stops at this store stops a *future* seal and\n // nothing else — cloud_008 §2.2. Told after the row is terminal, so the\n // two can only disagree in the safe direction: the relay may briefly\n // still offer a job this site will now refuse to seal for.\n //\n // Not awaited into the caller's error path: an app cancelling a job has\n // cancelled it, and a relay that is unreachable must not turn that into a\n // thrown error. The relay's own deadline sweep is the backstop.\n if (cancelled && this.cloud) {\n await this.cloud.cancel(jobId).catch(() => undefined);\n }\n return cancelled;\n }\n\n /**\n * Is there a live runner that could take a job of this shape?\n *\n * Runs the identical {@link matchAudience} rule the claim path uses, so the\n * signal cannot promise a runner the claim would then refuse.\n */\n async runnerAvailability(\n query: AvailabilityQuery,\n ): Promise<RunnerAvailability> {\n /**\n * On the cloud lane this cannot see, so it does not answer.\n *\n * It counts runners in *this site's own store*. In direct mode that is\n * the whole world — devices pair with the site. On the cloud lane they\n * pair with the relay, nothing ever writes a runner here, and the honest\n * count is not zero but unknown.\n *\n * It reported zero, as `no-runner-paired` with `candidates: 0`, for every\n * cloud-lane app that ever called it. A teammate using a shared device\n * was told no device was paired to her account — true, irrelevant, and\n * rendered as advice to go and install software she did not need.\n *\n * **An instrument that cannot see must refuse, not report zero.** A wrong\n * answer given confidently is worse than no answer, and this one was\n * confident, specific and false all at once.\n */\n if (this.cloud !== undefined) {\n throw new Error(\n \"runnerAvailability cannot answer on the cloud lane. It counts \" +\n \"runners this site knows about, and on the cloud lane devices pair \" +\n \"with the relay rather than with you — so the answer would be \" +\n \"`none` whatever the truth is. Enqueue the job: the result says \" +\n \"whether it ran, and the person's own dashboard says why not.\",\n );\n }\n\n const now = this.#now();\n const all = await this.#store.listRunners();\n const live = all.filter(\n (runner) =>\n runner.revokedAt === null &&\n !runner.paused &&\n now - runner.lastHeartbeatAt <= this.#livenessMs,\n );\n\n if (all.length === 0) {\n return { available: false, reason: \"no-runner-paired\", candidates: 0 };\n }\n if (live.length === 0) {\n return { available: false, reason: \"no-runner-online\", candidates: 0 };\n }\n\n let capable = 0;\n let admitted = 0;\n let lastRefusal: MatchRefusal | undefined;\n /**\n * Every advertised service for this kind, not one chosen here.\n *\n * This used to pick a single row — the one a job named, or the one the\n * owner had made the default — because a site could name a service and a\n * router matched on the name. Amendment L removed the naming, so there is\n * no row to prefer: availability is now \"does *anything* this device\n * offers for this kind admit this person\", which is also the honest\n * question, since which service actually answers is resolved from the\n * person's own mapping at claim.\n */\n for (const runner of live) {\n for (const capability of runner.capabilities.filter(\n (c) => c.kind === query.kind,\n )) {\n capable += 1;\n\n const match = matchAudience(\n {\n owner: query.owner,\n audience: query.audience ?? \"private\",\n audienceAllow: query.audienceAllow,\n },\n {\n owner: runner.owner,\n offerScope: capability.offerScope,\n // A generic backend's cost depends on its base URL, which the\n // server never sees; assume the expensive reading (byollm_007 §4).\n cost: backendDescriptor(capability.backendId).cost ?? \"metered\",\n // Consent is the daemon's to hold, and it has already applied it:\n // the offer scope arriving here is the *effective* one, so a\n // metered backend nobody agreed to share advertises `self` and is\n // refused by the scope rule above. Re-deriving consent from\n // `false` here would instead refuse every backend an owner\n // deliberately shared, because the server has no way to learn they\n // did — the signal would be wrong in the direction that breaks\n // working setups.\n spend: { acknowledged: true },\n // Same conservative assumption the claim path makes: the server\n // cannot see a remote daemon's local allowlist (protocol §4.2).\n admits: () => true,\n },\n );\n if (match.ok) admitted += 1;\n else lastRefusal = match.refusal;\n }\n }\n\n if (capable === 0) {\n // Ordered most specific first, because each sends the reader somewhere\n // different: a name that cannot serve them, a decision the device's\n // owner has not made, or nothing installed at all.\n return {\n available: false,\n reason: \"no-matching-capability\",\n candidates: 0,\n };\n }\n if (admitted === 0) {\n // Something serves it and nothing may serve *this requester*. When the\n // block is the device owner's own setting, that is the\n // defaults-meet-audiences corner — every service for this kind is one\n // this person can never use — and it is worth its own word, because\n // \"nobody is admitted\" reads as a\n // permissions problem the requester could ask to have fixed, while this\n // one is fixed by the device's owner choosing differently.\n // Whose decision blocked it, not merely that something did. The first\n // draft asked \"did the job name a service\", which reclassified a job\n // whose *own* audience was `private` and whose only device belonged to\n // somebody else — telling that caller \"the owner's default cannot serve\n // you\" when the exclusion was their own choice. An existing test caught\n // it, which is the argument for keeping the older reason rather than\n // widening the new one.\n //\n // So it splits on the refusal `matchAudience` already produced: a scope\n // or billing refusal is the *device owner's* setting, which only they\n // can change; an audience refusal is the *caller's*, which they can.\n const ownersDoing =\n lastRefusal === \"offer-scope-too-narrow\" ||\n lastRefusal === \"subscription-self-lock\" ||\n lastRefusal === \"metered-no-spend-consent\" ||\n lastRefusal === \"metered-ceiling-reached\";\n return {\n available: false,\n reason: ownersDoing ? \"default-unusable\" : \"audience-admits-nobody\",\n candidates: 0,\n };\n }\n return { available: true, candidates: admitted };\n }\n\n /**\n * Approve a pairing on behalf of an authenticated user.\n *\n * `owner` MUST come from the approving user's own session. A daemon can\n * never assert who it is — that is the whole reason pairing is interactive\n * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).\n */\n async approvePairing(args: {\n userCode: string;\n owner: string;\n }): Promise<RunnerRecord> {\n return this.#store.approvePairing({\n userCode: normalizeUserCode(args.userCode),\n owner: args.owner,\n runnerId: generateRunnerId(),\n now: this.#now(),\n });\n }\n\n /** Deny a pairing the user did not initiate. */\n async denyPairing(userCode: string): Promise<void> {\n return this.#store.denyPairing(normalizeUserCode(userCode), this.#now());\n }\n\n /** What a pairing code refers to, for the approval page to show. */\n async pendingPairing(userCode: string): Promise<{\n label: string;\n platform: string;\n daemonVersion: string;\n capabilities: readonly { kind: string; model: string }[];\n expiresAt: number;\n } | null> {\n const pairing = await this.#store.getPairingByUserCode(\n normalizeUserCode(userCode),\n );\n if (pairing?.state !== \"pending\") return null;\n if (pairing.expiresAt <= this.#now()) return null;\n return {\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities.map((c) => ({\n kind: c.kind,\n model: c.model,\n })),\n expiresAt: pairing.expiresAt,\n };\n }\n\n /** The user's paired runners, for a settings page. */\n async runners(owner: string): Promise<RunnerRecord[]> {\n return this.#store.listRunners(owner);\n }\n\n /** Revoke a runner. It stops at its next heartbeat, mid-queue. */\n async revokeRunner(runnerId: string): Promise<void> {\n return this.#store.revokeRunner(runnerId, this.#now());\n }\n\n /** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */\n async sweep(): Promise<JobRecord[]> {\n return this.#store.expireDue(this.#now());\n }\n}\n\n/**\n * Accept a pairing code however the user typed it — lowercase, spaces, no\n * dash. The code is displayed as `XXXX-XXXX`; refusing `xxxxxxxx` would fail\n * a user for a formatting detail they were never told mattered.\n */\nexport function normalizeUserCode(input: string): string {\n const bare = input.toUpperCase().replace(/[^A-Z0-9]/g, \"\");\n return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;\n}\n","import { openSealedOutcome } from \"./sealed-outcome.js\";\nimport {\n PROTOCOL_VERSION,\n type SealedOutcome,\n type ResultDisposition,\n type SealedEnvelope,\n keyId,\n open,\n publicIdentityOf,\n provenanceFor,\n signSiteRequest,\n type JobStub,\n type PublicIdentity,\n type StoredKeys,\n} from \"@byollm/protocol\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord } from \"./records.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/**\n * The cloud lane — cloud_004 §9.4.\n *\n * `app.enqueue(...)` is identical in every lane; the lane picks the connection\n * plane. In `direct` mode a daemon reaches the site's own handlers. In `cloud`\n * mode it reaches a relay instead, and the site's side of that is this file.\n *\n * ## What actually changes, and what deliberately does not\n *\n * Enqueue does not change at all. The job is validated, sealed at rest to the\n * site's own key and stored, exactly as before — jobs-at-rest encryption is a\n * direct-mode property that the cloud lane inherits rather than replaces.\n *\n * What changes is *who asks for the payload and when*. On the direct plane the\n * daemon asks, and the site answers synchronously because it is the upstream.\n * Through a relay the site is not the upstream, so nobody asks: the site has to\n * find out that a device claimed its job, and seal to that device. Hence a\n * pump rather than a handler.\n *\n * ```\n * enqueue ──stub──▶ relay (payload stays here, sealed at rest)\n * │\n * pump ◀──who claimed it, and what key?\n * ──payload sealed to that device──▶\n * pump ◀──sealed result── ──▶ store.complete → the app's delivery channel\n * ```\n *\n * ## Why the site polls\n *\n * Everything in this product is outbound. A relay that called site webhooks\n * would need every site publicly reachable, which is the connectivity problem\n * the hub exists to remove — and a serverless site has nowhere to receive a\n * webhook anyway. So the site polls, exactly as a daemon does.\n */\n\nexport interface CloudLaneOptions {\n /** Where the relay lives, e.g. `https://relay.byollm.cloud`. */\n readonly relayOrigin: string;\n /** This site's id at the relay. */\n readonly siteId: string;\n /** Injectable fetch, for tests and for proxies. */\n readonly fetch?: typeof fetch;\n}\n\n/** What one pump cycle did, for logging and for tests. */\n/**\n * A relay that could not answer this request — alpha.31.\n *\n * `retryable` is the whole point: a draining pod and a bad signature are both\n * failures, and treating them alike is how a site either falls over on every\n * deploy or stays silently disconnected for a week.\n */\nexport class RelayUnavailable extends Error {\n readonly retryable: boolean;\n /** The protocol's own code, when the relay sent one. */\n readonly code: string;\n\n constructor(message: string, retryable: boolean, code: string) {\n super(message);\n this.name = \"RelayUnavailable\";\n this.retryable = retryable;\n this.code = code;\n }\n}\n\n/**\n * The job was not queued, and waiting will not change that.\n *\n * Distinct from {@link RelayUnavailable} because it is the opposite situation:\n * the relay answered, promptly and correctly, and the answer is that this job\n * has nowhere to go. Catching \"the relay is down\" to handle \"nobody has chosen\n * a model\" would retry forever against a fact.\n *\n * Two codes, and they belong to two different people.\n *\n * `purpose-not-declared` is the site's own manifest. It names the purpose and\n * the remedy, because a developer reading their own logs is entitled to both\n * and neither says anything about a person.\n *\n * `slot-unsatisfiable` is the person's own dashboard, and says only that.\n * Which service, whose device, whether one exists at all — none of it travels,\n * and the sentence is the same for everybody. A site learns *that* a slot\n * cannot be satisfied, which is exactly what the README has always promised\n * and what this class finally delivers.\n */\nexport class EnqueueRefused extends Error {\n /** `purpose-not-declared` or `slot-unsatisfiable`. */\n readonly code: string;\n\n constructor(message: string, code: string) {\n super(message);\n this.name = \"EnqueueRefused\";\n this.code = code;\n }\n}\n\n/**\n * The one code that means \"ask me later\" on the enqueue endpoint.\n *\n * Inverted on 2026-09-02, and the inversion is the point. This was an\n * allowlist of *refusals* — `purpose-not-declared` and `slot-unsatisfiable` —\n * which made every refusal code the relay might add next a breaking change\n * for every client already deployed: an unknown code fell through to\n * `RelayUnavailable`, so a site's `catch (error instanceof EnqueueRefused)`\n * silently stopped matching and a permanent refusal was retried forever.\n *\n * The status class is the key; a list of codes is a description of it. A 409\n * on enqueue means the relay declined to queue the job, and there is nothing\n * to await and nothing to retry — whatever the code turns out to be.\n *\n * `409` alone is not the key, which is why this is not simply deleted: the\n * protocol overloads it. `not-ready` is a draining pod saying come back, and\n * `too-late` is about a job that already exists. Neither can reach the\n * enqueue endpoint as a refusal, so the discriminator is the class *on this\n * endpoint*, with the retryable code named because it is the exception.\n *\n * The direction of the default is deliberate. An unknown code treated as a\n * refusal surfaces as an error somebody can see; treated as unavailable it\n * becomes a silent retry against a condition that will never change — which\n * is the shape of the afternoon Kevin lost.\n */\nconst RETRYABLE_AT_ENQUEUE = new Set([\"not-ready\"]);\n\n/** The endpoint where a decline means the job was never queued. */\nconst ENQUEUE_ENDPOINT = \"enqueue\";\n\nexport interface PumpReport {\n /** Jobs sealed to a claiming device this cycle. */\n readonly sealed: string[];\n /** Results opened, verified and written to the store. */\n readonly completed: string[];\n /**\n * Jobs the relay offered that this site refused to seal for.\n *\n * Never silent: a site that cannot open its own at-rest envelope has a key\n * problem, and a device waiting on a payload that will never come is\n * exactly the case `awaiting-payload` exists to bound.\n */\n readonly refused: string[];\n /**\n * Why this cycle stopped early, when it did — alpha.31.\n *\n * A relay can legitimately say \"ask me later\": a pod draining through its\n * `preStop` window answers `503 not-ready` to every routed call, and that\n * happens on **every deploy**. Before this existed the lane read the body\n * of that answer, found no `jobs` in it, and threw `TypeError: finished.jobs\n * is not iterable` — a site falling over because its relay was polite.\n *\n * Absent on an ordinary cycle. Present, with the reason, when the lane\n * deferred: a site that quietly did nothing and a site that was told to wait\n * must not look the same in a log.\n */\n readonly deferred?: string;\n}\n\nexport class CloudLane {\n readonly #options: CloudLaneOptions;\n readonly #store: ByollmStore;\n readonly #siteKeys: StoredKeys;\n readonly #now: () => number;\n readonly #fetch: typeof fetch;\n\n constructor(deps: {\n options: CloudLaneOptions;\n store: ByollmStore;\n siteKeys: StoredKeys;\n now: () => number;\n }) {\n this.#options = deps.options;\n this.#store = deps.store;\n this.#siteKeys = deps.siteKeys;\n this.#now = deps.now;\n this.#fetch = deps.options.fetch ?? globalThis.fetch;\n }\n\n /**\n * Publish a job's stub for routing.\n *\n * The stub and nothing else — byollm_009 §6 makes that exhaustive by\n * construction, so this cannot leak a payload even by mistake: there is no\n * field on `JobStub` to put one in.\n */\n async publish(record: JobRecord): Promise<void> {\n const stub: JobStub = {\n id: record.id,\n kind: record.kind,\n owner: record.owner,\n // This site, by its identity key id — Amendment A §A.3. The relay\n // already knows which site it is routing for, so this discloses nothing\n // new to it; what it adds is that the *daemon* can check the stub\n // against the envelope's `senderKeyId` without asking the relay.\n site: keyId(publicIdentityOf(this.#siteKeys).identity),\n audience: record.audience,\n // `audienceAllow` is deliberately **not** published — cloud_008 §0.2.\n //\n // It is a list of the people who may run this job, and on the direct\n // plane that is unremarkable: the site authored the list and the site is\n // the upstream, so the party receiving it already has it. Through a\n // relay it is a third party, and byollm_009 §6's enumerated metadata —\n // \"exhaustive and normative… what an upstream can see, stated as a\n // commitment\" — does not include it. It was reaching the relay on every\n // named-audience job.\n //\n // Nothing is lost by withholding it, which is why this is a Tier 0 fix\n // rather than a trade. `matchAudience` treats it as a *narrowing*:\n // `job.audienceAllow !== undefined && !includes(daemon.owner)` refuses,\n // and its absence simply falls through to the checks that actually\n // enforce — the daemon's own allowlist (`NAMED_LOCAL_ALLOWLIST`) and the\n // backend's offer scope. On this lane the relay narrows too, from the\n // control plane's rosters. The enforcement was never here.\n ...(record.purpose === undefined ? {} : { purpose: record.purpose }),\n sizeClass: record.sizeClass,\n streaming: false,\n // The relay needs *a* deadline to bound routing. A job without one gets\n // the envelope's, which is the outer bound on how long the ciphertext\n // is worth carrying — never longer than the work could possibly matter.\n // The same fallback the direct plane uses — cloud_008 Tier 4, finding\n // 31. This said `createdAt + ENVELOPE_TTL_FALLBACK`, a local constant\n // whose value happened to equal `ENVELOPE_MAX_AGE_MS`; the direct plane\n // said `(claimableAt ?? now) + ttlMs`. One field, two meanings, and a\n // job that was blocked on a dependency got a deadline measured from\n // when it was *created* on one lane and from when it became *claimable*\n // on the other.\n deadlineAt: deadlineFor(record, this.#now()),\n };\n await this.#post(\"enqueue\", {\n siteId: this.#options.siteId,\n stub,\n });\n }\n\n /**\n * Withdraw a job at the relay — cloud_008 §2.2.\n *\n * `app.cancel()` marks the site's own row terminal, which stops the *next*\n * seal. It cannot stop a device that is already running the work, because\n * on this lane the site is not the upstream: only the relay talks to the\n * daemon, and it answered `cancel: []` unconditionally.\n *\n * So the cancellation has to travel. The relay marks the job, stops\n * offering it, and names it to the holding device at its next heartbeat —\n * the same path the direct plane has always had, arriving one hop later.\n */\n async cancel(jobId: string): Promise<void> {\n await this.#post(\"cancel\", { siteId: this.#options.siteId, jobId });\n }\n\n /**\n * One cycle: seal for anything claimed, collect anything finished.\n *\n * Idempotent and safe to call as often as you like. Exposed as a single\n * cycle rather than hidden behind a timer so a caller decides its own\n * cadence — a serverless site runs it on a cron, a long-lived one on an\n * interval, and a test runs it exactly when it means to.\n */\n async pump(): Promise<PumpReport> {\n const sealed: string[] = [];\n const refused: string[] = [];\n const completed: string[] = [];\n\n try {\n return await this.#cycle(sealed, refused, completed);\n } catch (error) {\n // Retryable: end the cycle, keep what was done, say why. Anything else\n // is a fact about this site's configuration and belongs to the caller —\n // a swallowed 401 is a site disconnected from its users with nothing in\n // any log to say so.\n if (error instanceof RelayUnavailable && error.retryable) {\n return { sealed, completed, refused, deferred: error.message };\n }\n throw error;\n }\n }\n\n async #cycle(\n sealed: string[],\n refused: string[],\n completed: string[],\n ): Promise<PumpReport> {\n const pending = (await this.#get(\"pending\")) as {\n jobs: {\n jobId: string;\n device: PublicIdentity;\n runnerId: string;\n leaseId: string;\n awaitingUntil: number;\n leaseExpiresAt: number;\n }[];\n };\n for (const claim of pending.jobs) {\n const record = await this.#store.get(claim.jobId);\n if (!record) continue;\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: {\n id: record.id,\n envelope: record.envelope,\n createdAt: record.createdAt,\n },\n device: claim.device,\n });\n if (!resealed.ok) {\n refused.push(claim.jobId);\n continue;\n }\n // Record the lease the relay granted, before handing over the work.\n //\n // The site is not the upstream here and does not decide who holds what\n // — but its own row has to know, or two things break that are not\n // cosmetic: `complete` refuses the result for want of a matching lease,\n // and the expiry sweep expires a job a device is in the middle of.\n // Adopting first means the worst case is a lease recorded for work that\n // never gets sealed, which the relay's own timeout already resolves.\n //\n // **The lease's clock, not the payload's** — cloud_008 §0.6. This\n // adopted `awaitingUntil`, which is how long the relay waits for *this\n // site to seal* (byollm_009 §7.1's third clock, ten seconds), and used\n // it as the expiry of a grant the device holds for a minute and renews\n // for as long as it works. Both of the breakages listed above then\n // happened to every job slower than the shorter clock — the site expired\n // the lease, the device finished anyway, and `complete` refused the\n // result the device had correctly produced.\n const adopted = await this.#store.adopt({\n jobId: claim.jobId,\n leaseId: claim.leaseId,\n expiresAt: claim.leaseExpiresAt,\n now: this.#now(),\n });\n // `null` means this store will not lend the job out — cloud_008 §2.2.\n //\n // Its own comment says why it refuses: a terminal or already-leased job\n // means the relay and this store disagree about reality. **And the\n // return value was being discarded**, so the site went on to seal the\n // payload to the claiming device anyway — for a job the app had already\n // cancelled, or whose deadline had passed, or that another lease\n // already owned.\n //\n // Sealing is the irreversible half: once the ciphertext is with the\n // relay, a device can fetch and run it. Refusing here is what makes\n // `adopt` a decision rather than a formality, and the job is reported\n // as refused so a site operator sees it rather than a device waiting\n // for work that will never be sealed.\n if (!adopted) {\n refused.push(claim.jobId);\n continue;\n }\n await this.#post(\"payload\", {\n siteId: this.#options.siteId,\n jobId: claim.jobId,\n envelope: resealed.envelope,\n });\n sealed.push(claim.jobId);\n }\n\n /* `ResultDisposition`, not `string` — B064 step 4's extraction surfaced\n this. A hand-written cast of a wire response retypes the shape at the\n consumer, and this field lost its union on the way: the relay declares\n `\"ok\" | \"error\" | \"canceled\"` and this said `string`, so nothing here\n could tell a disposition from any other text. Instruction 9, in the\n small. */\n const finished = (await this.#get(\"results\")) as {\n jobs: {\n jobId: string;\n envelope: SealedEnvelope;\n disposition: ResultDisposition;\n runnerId: string;\n leaseId: string;\n device: PublicIdentity;\n runnerOwner: string;\n }[];\n };\n for (const done of finished.jobs) {\n const record = await this.#store.get(done.jobId);\n if (!record || record.state === \"ok\" || record.state === \"error\") {\n continue;\n }\n const outcome = await this.#openResult(done);\n if (!outcome) {\n refused.push(done.jobId);\n continue;\n }\n // Provenance is built here, from the job's audience and the device the\n // relay named — never from anything the daemon asserted. Identical to\n // the direct plane's rule, and it has to be: a result arriving via a\n // relay is not more trustworthy for having travelled further.\n await this.#store.complete({\n jobId: done.jobId,\n // The relay named the device; the signature above proved it — §3.6.\n runnerId: done.runnerId,\n // The grant, not the machine: this site never paired with the device\n // that ran it, and the signature it verified above is the stronger\n // claim about who did.\n holder: { by: \"lease\", leaseId: done.leaseId },\n outcome: outcome.outcome,\n provenance: provenanceFor({\n audience: record.audience,\n runnerId: done.runnerId,\n // The owner, from the relay's own record of who claimed it — not a\n // key id. cloud_008 §2.5: this said `keyId(device.identity)`, which\n // put a key id where the direct plane puts a user id, so an app\n // comparing provenance across lanes compared two namespaces and got\n // `false` for the same person. The device's key is still what the\n // signature was verified against, above; that is a different\n // question from whose machine it is.\n runnerOwner: done.runnerOwner,\n // From the envelope, not invented — cloud_008 §2.5. These were\n // hardcoded `\"http\"` and `\"unknown\"` because the daemon's declared\n // values stopped at the relay, which is right: a blind relay acts\n // on neither. Sealing them carries them past it untouched.\n backendClass: outcome.ran.backendClass,\n model: outcome.ran.model,\n }),\n now: this.#now(),\n });\n completed.push(done.jobId);\n }\n\n return { sealed, completed, refused };\n }\n\n /**\n * Open a sealed result and verify it came from the device that claimed it.\n *\n * The relay says which device ran the job; this checks that claim against a\n * signature the relay cannot produce. A relay that named the wrong device\n * gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`\n * from quietly becoming `RELAY_TRUSTED`.\n */\n async #openResult(done: {\n jobId: string;\n envelope: SealedEnvelope;\n disposition: ResultDisposition;\n device: PublicIdentity;\n }): Promise<SealedOutcome | null> {\n const opened = await open({\n envelope: done.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: done.device.identity,\n expected: {\n jobId: done.jobId,\n senderKeyId: keyId(done.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) return null;\n\n // One function, both lanes — see `sealed-outcome.ts`. The disposition\n // check lives inside it, because this is the only party that can open the\n // envelope (byollm_009 §6.1) and therefore the only place the relay's\n // clear-text routing hint can be checked against the truth.\n const outcome = openSealedOutcome({\n plaintext: opened.plaintext,\n disposition: done.disposition,\n });\n return outcome.ok ? outcome.value : null;\n }\n\n /**\n * Sign a site-plane call with this site's identity key.\n *\n * The same scheme the daemon uses against an upstream, because the site is\n * in the same position: an outbound caller whose key the relay already holds\n * for other reasons. Nothing else authenticates this plane — a relay that\n * took the `siteId` in a body at face value would let anyone enqueue work in\n * a site's name and read who claimed it.\n */\n #headers(endpoint: string, rawBody: string): Record<string, string> {\n const signature = signSiteRequest(this.#siteKeys, {\n endpoint,\n siteId: this.#options.siteId,\n issuedAt: this.#now(),\n body: rawBody,\n });\n return {\n \"x-byollm-site\": this.#options.siteId,\n \"x-byollm-issued-at\": String(signature.issuedAt),\n \"x-byollm-signature\": signature.signature,\n };\n }\n\n /**\n * A relay answer, checked before it is believed — alpha.31.\n *\n * The bug this closes is one line long and its shape is general: a response\n * body used without looking at the status. The daemon's client has always\n * done this properly (`client.ts` maps every status to a typed refusal); the\n * site's lane parsed JSON and hoped.\n *\n * Two classes, because they need opposite handling. **Retryable** — 503 from\n * a draining pod, 429, 5xx, and the protocol's own `not-ready` — means the\n * work is still there and this cycle should end quietly. **Refused** — a bad\n * signature, an unknown site, a version this relay does not speak — will\n * still be true in five seconds, and swallowing it would leave a site\n * silently disconnected from its own users.\n */\n async #answer(response: Response, endpoint: string): Promise<unknown> {\n if (response.ok) return response.json();\n\n let code = \"\";\n let message: string;\n try {\n const body = (await response.json()) as {\n error?: string;\n message?: string;\n };\n code = body.error ?? \"\";\n message = body.message ?? \"\";\n } catch {\n // A body that is not JSON is an intermediary answering, not the relay.\n message = `HTTP ${String(response.status)}`;\n }\n\n const retryable =\n response.status >= 500 ||\n response.status === 429 ||\n code === \"not-ready\" ||\n code === \"server-error\";\n\n if (\n endpoint === ENQUEUE_ENDPOINT &&\n response.status === 409 &&\n !RETRYABLE_AT_ENQUEUE.has(code)\n ) {\n // No job exists, so there is nothing to await and nothing to retry.\n // The code travels unread: a site branching on one it does not know\n // still learns that its job was refused, which is the fact it needs.\n throw new EnqueueRefused(message, code);\n }\n\n throw new RelayUnavailable(\n `${endpoint}: ${code || \"refused\"} — ${message}`,\n retryable,\n code,\n );\n }\n\n async #post(endpoint: string, body: unknown): Promise<unknown> {\n // The version travels in the body, as it does on the daemon plane — §B.4.\n // Added here rather than at each call site so a new site-plane call cannot\n // be written without it, which is how the site plane came to be outside\n // the handshake in the first place.\n const rawBody = JSON.stringify({\n protocolVersion: PROTOCOL_VERSION,\n ...(body as Record<string, unknown>),\n });\n const response = await this.#fetch(\n `${this.#options.relayOrigin}/relay/site/${endpoint}`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...this.#headers(endpoint, rawBody),\n },\n body: rawBody,\n },\n );\n return this.#answer(response, endpoint);\n }\n\n async #get(endpoint: string): Promise<unknown> {\n // A GET has no body, so the version rides in the query — the other half\n // of `declaredVersion`, and the reason that helper takes both.\n const url =\n `${this.#options.relayOrigin}/relay/site/${endpoint}` +\n `?siteId=${encodeURIComponent(this.#options.siteId)}` +\n `&protocolVersion=${encodeURIComponent(PROTOCOL_VERSION)}`;\n // A read signs an empty body: the site id is in the query and in the\n // signed caller slot, and the relay refuses the request unless they agree.\n const response = await this.#fetch(url, {\n headers: this.#headers(endpoint, \"\"),\n });\n return this.#answer(response, endpoint);\n }\n}\n","import { StoredKeys, generateKeys, publicIdentityOf } from \"@byollm/protocol\";\nimport { fingerprint } from \"@byollm/protocol\";\n\n/**\n * A site's keypairs — byollm_009 §5.\n *\n * **Generate once, store, supply.** Not at startup, and not per process.\n *\n * A site is usually more than one process: several instances behind a load\n * balancer, or a serverless function whose module is evaluated per cold\n * start. Keys generated at startup would give each of those a different\n * identity. A daemon pins whichever one approved its pairing, and then every\n * request routed to a different instance fails a signature check with nothing\n * in the error explaining why — a failure that appears only under\n * horizontal scale, which is to say only in production.\n *\n * So the library takes keys as an input and never invents them. That is the\n * whole reason this module is three functions rather than a lazy singleton.\n */\n\n/** Make a fresh site identity. Call this once, ever, and keep the result. */\nexport const generateSiteKeys = (now: number = Date.now()): StoredKeys =>\n generateKeys(now);\n\n/**\n * Read site keys from an environment variable holding base64 JSON.\n *\n * The shape a deployment actually wants: one opaque secret, set the way every\n * other secret is set, with no file to mount and no key material in the\n * repository.\n *\n * @throws with a message naming the variable and the fix, because this fails\n * at boot and the person reading the log is the person who can fix it.\n */\nexport function siteKeysFromEnv(\n variable = \"BYOLLM_SITE_KEYS\",\n env: NodeJS.ProcessEnv = process.env,\n): StoredKeys {\n const raw = env[variable];\n if (raw === undefined || raw === \"\") {\n throw new Error(\n `${variable} is not set. Generate a site identity once with ` +\n `\\`npx @byollm/server keygen\\` and set it as ${variable}. ` +\n `Do not generate keys at startup: every instance would get a ` +\n `different identity and daemons would pin one and be refused by ` +\n `another.`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(raw, \"base64\").toString(\"utf8\"));\n } catch {\n throw new Error(\n `${variable} is not base64-encoded JSON. It should be exactly what ` +\n `\\`npx @byollm/server keygen\\` printed.`,\n );\n }\n\n const result = StoredKeys.safeParse(parsed);\n if (!result.success) {\n throw new Error(\n `${variable} does not contain a valid site identity. Regenerate it ` +\n `with \\`npx @byollm/server keygen\\` — and if this site has already ` +\n `paired daemons, they will need to pair again.`,\n );\n }\n return result.data;\n}\n\n/** What to print from `keygen`: the secret to store, and how to check it. */\nexport function formatSiteKeys(keys: StoredKeys): string {\n const encoded = Buffer.from(JSON.stringify(keys)).toString(\"base64\");\n const pub = publicIdentityOf(keys);\n return (\n `# ── 1. SECRET — set this on your server, and nowhere else ────────────\\n` +\n `#\\n` +\n `# This is the site's identity. Anything holding it can *be* this site,\\n` +\n `# so it goes wherever your deployment keeps secrets — never in a repo,\\n` +\n `# never in a browser, never pasted into a dashboard.\\n` +\n `BYOLLM_SITE_KEYS=${encoded}\\n` +\n `\\n` +\n `# ── 2. PUBLIC — paste this line into the byollm dashboard ────────────\\n` +\n `#\\n` +\n `# The public half. It proves signatures and seals nothing, so it is\\n` +\n `# safe to publish — which is the point: users pin it, and the relay\\n` +\n `# cannot forge work without the secret above.\\n` +\n `${JSON.stringify(pub)}\\n` +\n `\\n` +\n `# ── 3. Fingerprint — what a person compares by eye ───────────────────\\n` +\n `#\\n` +\n `# A fingerprint is not secret. Show it on your site so somebody\\n` +\n `# connecting can check it against what their daemon printed.\\n` +\n `# The dashboard derives this itself, so there is nothing to paste.\\n` +\n `# ${fingerprint(pub.identity)}\\n`\n );\n}\n","import {\n backendDescriptor,\n matchAudience,\n type Capability,\n} from \"@byollm/protocol\";\nimport { generateLeaseId } from \"./ids.js\";\nimport type {\n StoredJobInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"./records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n AdoptArgs,\n CompleteArgs,\n CompleteResult,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"./store.js\";\n\n/** Tunables an embedder may want to override in tests. */\nexport interface MemoryStoreOptions {\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\nconst DEFAULT_TTL_MS = 15 * 60_000;\n\n/**\n * The reference store: everything in one process, no persistence.\n *\n * This is not a toy — it is the implementation the conformance kit certifies\n * first, so its semantics *are* the specification's semantics for anything\n * the prose leaves implicit. A SQL adapter is correct when the same kit\n * passes against it.\n *\n * Concurrency: JavaScript's single-threaded turn is the atomicity primitive.\n * `claim` performs its read-decide-write with no `await` inside the critical\n * section, which is what makes {@link MUSTS.CLAIM_ATOMIC} hold here. A SQL\n * adapter gets the same property from `FOR UPDATE SKIP LOCKED`.\n */\nexport class MemoryStore implements ByollmStore {\n readonly #jobs = new Map<string, JobRecord>();\n readonly #runners = new Map<string, RunnerRecord>();\n readonly #pairings = new Map<string, PairingRecord>();\n /** Job ids the app has asked to cancel, not yet acknowledged by a runner. */\n readonly #cancelRequests = new Set<string>();\n readonly #defaultTtlMs: number;\n\n constructor(options: MemoryStoreOptions = {}) {\n this.#defaultTtlMs = options.defaultTtlMs ?? DEFAULT_TTL_MS;\n }\n\n // -- jobs ---------------------------------------------------------------\n\n create(input: StoredJobInput, now: number): Promise<JobRecord> {\n // Required now: the app mints the id before sealing, because the\n // envelope binds it.\n const id = input.id;\n const existing = this.#jobs.get(id);\n // Idempotent by caller-supplied id: re-enqueueing the same id is a no-op,\n // so an app's retry cannot duplicate work (the house \"one door\" rule).\n if (existing) return Promise.resolve(existing);\n\n const dependsOn = [...(input.dependsOn ?? [])];\n const blocked = dependsOn.some(\n (depId) => this.#jobs.get(depId)?.state !== \"ok\",\n );\n\n const job: JobRecord = {\n id,\n kind: input.kind,\n envelope: input.envelope,\n sizeClass: input.sizeClass,\n audience: input.audience ?? \"private\",\n purpose: input.purpose,\n owner: input.owner,\n audienceAllow: input.audienceAllow ? [...input.audienceAllow] : undefined,\n dependsOn,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n createdAt: now,\n // The TTL clock starts here only if nothing blocks the job.\n claimableAt: blocked ? null : now,\n ttlMs: input.ttlMs ?? this.#defaultTtlMs,\n deadlineAt: input.deadlineAt ?? null,\n refusedBy: [],\n attempts: 0,\n outcome: null,\n provenance: null,\n updatedAt: now,\n };\n this.#write(id, job);\n return Promise.resolve(job);\n }\n\n get(jobId: string): Promise<JobRecord | null> {\n return Promise.resolve(this.#jobs.get(jobId) ?? null);\n }\n\n claim(args: ClaimArgs): Promise<JobRecord[]> {\n // Sweep first so an expired job is never handed out.\n this.#expireDueSync(args.now);\n\n const claimed: JobRecord[] = [];\n // Oldest-claimable first: a job that has waited longest goes next.\n const candidates = [...this.#jobs.values()].sort(\n (a, b) => (a.claimableAt ?? Infinity) - (b.claimableAt ?? Infinity),\n );\n\n for (const job of candidates) {\n if (claimed.length >= args.max) break;\n if (!this.#isClaimable(job, args)) continue;\n\n const updated: JobRecord = {\n ...job,\n state: \"claimed\",\n lease: {\n // A fresh id per grant. Two claims of the same job by the same\n // runner are two different leases, and must be distinguishable.\n id: generateLeaseId(),\n runnerId: args.runnerId,\n expiresAt: args.now + args.leaseMs,\n },\n attempts: job.attempts + 1,\n updatedAt: args.now,\n };\n this.#write(job.id, updated);\n claimed.push(updated);\n }\n return Promise.resolve(claimed);\n }\n\n /**\n * The claim predicate, shared by `claim` and the no-runner signal so the\n * two can never disagree about what \"a runner that could take this\" means.\n */\n #isClaimable(\n job: JobRecord,\n args: Pick<ClaimArgs, \"runnerId\" | \"runnerOwner\" | \"capabilities\" | \"now\">,\n ): boolean {\n if (job.state !== \"queued\") return false;\n if (job.claimableAt === null || job.claimableAt > args.now) return false;\n if (job.refusedBy.includes(args.runnerId)) return false;\n\n const capability = capabilityFor(args.capabilities, job.kind);\n if (!capability) return false;\n\n const match = matchAudience(\n {\n owner: job.owner,\n audience: job.audience,\n audienceAllow: job.audienceAllow,\n },\n {\n owner: args.runnerOwner,\n offerScope: capability.offerScope,\n // From the registry, not a local guess — the cost rules must mean the\n // same thing on both sides of the wire. The server cannot see a\n // remote daemon's base URL, so a generic backend with no declared\n // cost is treated as metered: the expensive side, and the daemon\n // refuses anyway if it disagrees (byollm_007 §2).\n cost: backendDescriptor(capability.backendId).cost ?? \"metered\",\n // Nor can it see the owner's spend consent. It offers; the daemon is\n // the enforcing side and releases with `refused` if its own rules say\n // no — the same shape as the `named` allowlist.\n spend: { acknowledged: true },\n // The server cannot see a remote daemon's local allowlist and must\n // not pretend to (protocol §4.2). It admits the job here; the daemon\n // is the enforcing side and releases with `refused` if its own list\n // says no.\n admits: () => true,\n },\n );\n return match.ok;\n }\n\n renewLeases(args: RenewArgs): Promise<RenewResult> {\n this.#expireDueSync(args.now);\n\n const renewed: { jobId: string; expiresAt: number }[] = [];\n const lost: { jobId: string; leaseId: string }[] = [];\n\n for (const { jobId, leaseId } of args.leases) {\n const job = this.#jobs.get(jobId);\n if (\n !job ||\n job.lease?.runnerId !== args.runnerId ||\n job.lease.id !== leaseId\n ) {\n // Reclaimed by someone else, terminal, or a different grant than the\n // one being renewed — either way this runner must stop. Named by the\n // grant the daemon asked about, which is the one it must abandon.\n lost.push({ jobId, leaseId });\n continue;\n }\n if (job.state !== \"claimed\" && job.state !== \"running\") {\n lost.push({ jobId, leaseId });\n continue;\n }\n const expiresAt = args.now + args.leaseMs;\n this.#write(jobId, {\n ...job,\n state: \"running\",\n // Renewal extends the existing grant; it does not mint a new one.\n lease: { ...job.lease, expiresAt },\n updatedAt: args.now,\n });\n renewed.push({ jobId, expiresAt });\n }\n return Promise.resolve({ renewed, lost });\n }\n\n adopt(args: AdoptArgs): Promise<JobRecord | null> {\n const job = this.#jobs.get(args.jobId);\n if (!job) return Promise.resolve(null);\n // Only a job that is genuinely available can be adopted. A terminal or\n // already-leased job means the relay and this store disagree about\n // reality, and the store's row is not the place to resolve that.\n if (job.state !== \"queued\" && job.state !== \"claimed\") {\n return Promise.resolve(null);\n }\n if (job.lease && job.lease.id !== args.leaseId) {\n return Promise.resolve(null);\n }\n const updated: JobRecord = {\n ...job,\n state: \"claimed\",\n lease: {\n id: args.leaseId,\n // No runner: this site never paired with the machine holding it.\n runnerId: \"\",\n expiresAt: args.expiresAt,\n },\n updatedAt: args.now,\n };\n this.#write(updated.id, updated);\n return Promise.resolve(updated);\n }\n\n complete(args: CompleteArgs): Promise<CompleteResult> {\n const job = this.#jobs.get(args.jobId);\n if (!job) return Promise.resolve({ accepted: false, job: null });\n\n // First terminal outcome wins; a later submission is discarded, not\n // applied ({@link MUSTS.RESULT_IDEMPOTENT}).\n // Terminal before holder — cloud_008 §3.6, and the same order in all four\n // stores. `RESULT_IDEMPOTENT` used to hold only because `complete` nulls\n // the lease, so a replay tripped the holder check first and the branch\n // named after the MUST was never reached. A MUST that byollm_009 §4's\n // case for signed requests leans on cannot hold by coincidence.\n //\n // **Scoped to the device that finished it.** A replay from that grant is\n // a duplicate and is told so; anyone else falls through to the holder\n // check and gets exactly the refusal they would get for a job that is not\n // terminal. Answering them differently would make a job id a terminality\n // probe.\n if (\n job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\"\n ) {\n // The same device *and* the same grant. Either alone is not the\n // device that finished the job: a lease id can be presented by whoever\n // learned it, and a device can hold a later grant on a job it never\n // completed.\n const sameDevice =\n job.provenance?.runnerId !== undefined &&\n job.provenance.runnerId === args.runnerId;\n const sameGrant =\n args.holder.by === \"lease\" &&\n job.completedByLeaseId !== null &&\n job.completedByLeaseId === args.holder.leaseId;\n if (sameDevice && sameGrant) {\n return Promise.resolve({ accepted: false, duplicate: true, job });\n }\n return Promise.resolve({ accepted: false, job });\n }\n if (job.state === \"expired\") {\n return Promise.resolve({ accepted: false, job });\n }\n // A runner that lost its lease may not write a result\n // ({@link MUSTS.LEASE_HONORED}). Named by lease id when the caller has\n // one — off the direct plane there is no runner this site knows.\n const holds =\n args.holder.by === \"runner\"\n ? job.lease?.runnerId === args.holder.runnerId\n : job.lease?.id === args.holder.leaseId;\n if (!holds) {\n return Promise.resolve({ accepted: false, job });\n }\n\n const state =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n const updated: JobRecord = {\n ...job,\n state,\n lease: null,\n // The grant that recorded it, kept after the lease is dropped — §3.6.\n completedByLeaseId:\n args.holder.by === \"lease\"\n ? args.holder.leaseId\n : (job.lease?.id ?? null),\n outcome: args.outcome,\n provenance: args.provenance,\n updatedAt: args.now,\n };\n this.#write(job.id, updated);\n this.#cancelRequests.delete(job.id);\n\n if (state === \"ok\") this.#unblockDependents(job.id, args.now);\n\n return Promise.resolve({ accepted: true, job: updated });\n }\n\n /**\n * Start the TTL clock on anything this job was blocking.\n *\n * Deliberately only on `ok`: a dependency that errored leaves its dependents\n * blocked forever rather than releasing them into a run whose input never\n * arrived. They expire at their absolute deadline if one was set, and the\n * app sees a chain that stopped where it broke.\n */\n #unblockDependents(completedId: string, now: number): void {\n for (const job of this.#jobs.values()) {\n if (job.claimableAt !== null) continue;\n if (!job.dependsOn.includes(completedId)) continue;\n const ready = job.dependsOn.every(\n (depId) => this.#jobs.get(depId)?.state === \"ok\",\n );\n if (ready) {\n this.#write(job.id, { ...job, claimableAt: now, updatedAt: now });\n }\n }\n }\n\n /**\n * Watchers, by job id (byollm_009 §8.3).\n *\n * A `Set` per job so an unsubscribe removes exactly the handler it\n * registered — two waiters on the same job are ordinary, and removing by\n * job id alone would silently cancel someone else's wait.\n */\n readonly #watchers = new Map<string, Set<() => void>>();\n\n subscribe(jobId: string, onChange: () => void): () => void {\n const existing = this.#watchers.get(jobId) ?? new Set<() => void>();\n existing.add(onChange);\n this.#watchers.set(jobId, existing);\n let live = true;\n return () => {\n // Idempotent: the contract says calling twice is safe, and a `finally`\n // that unsubscribes after an error path already did is the normal way\n // this gets called twice.\n if (!live) return;\n live = false;\n const set = this.#watchers.get(jobId);\n set?.delete(onChange);\n if (set?.size === 0) this.#watchers.delete(jobId);\n };\n }\n\n /**\n * The single write path for a job.\n *\n * Every mutation goes through here so notification cannot be forgotten by\n * a future one. Nine call sites existed when the push seam was added, and\n * \"remember to notify\" is not a property nine call sites keep.\n */\n #write(jobId: string, record: JobRecord): void {\n this.#jobs.set(jobId, record);\n this.#notify(jobId);\n }\n\n /**\n * Tell anyone watching that a job changed.\n *\n * A throwing watcher must not corrupt the store's own bookkeeping, so each\n * is isolated: this runs inside write paths, and one bad listener taking\n * out an unrelated write would be a far worse failure than a missed\n * notification.\n */\n #notify(jobId: string): void {\n for (const watcher of this.#watchers.get(jobId) ?? []) {\n try {\n watcher();\n } catch {\n // A watcher is a signal handler; the caller re-reads regardless.\n }\n }\n }\n\n release(args: ReleaseArgs): Promise<string[]> {\n const released: string[] = [];\n for (const { jobId, leaseId } of args.leases) {\n const job = this.#jobs.get(jobId);\n // The *grant*, not just its holder. Matching on runner id alone let a\n // replayed release from an earlier lease drop a later one, returning a\n // job to the queue while the daemon was still executing it.\n if (\n !job ||\n job.lease?.runnerId !== args.runnerId ||\n job.lease.id !== leaseId\n ) {\n continue;\n }\n\n this.#write(jobId, {\n ...job,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n // Newly available again, so the TTL clock restarts here too.\n claimableAt: args.now,\n // A refusal is remembered, or the pair spins between claim and\n // release forever ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n refusedBy:\n args.reason === \"refused\"\n ? [...new Set([...job.refusedBy, args.runnerId])]\n : job.refusedBy,\n updatedAt: args.now,\n });\n released.push(jobId);\n }\n return Promise.resolve(released);\n }\n\n expireDue(now: number): Promise<JobRecord[]> {\n return Promise.resolve(this.#expireDueSync(now));\n }\n\n /**\n * Lease expiry and TTL expiry in one idempotent sweep.\n *\n * Order matters: a lease is reclaimed *before* the TTL is judged, so a job\n * whose runner died is offered again rather than being expired for having\n * sat in `claimed` too long.\n */\n #expireDueSync(now: number): JobRecord[] {\n const changed: JobRecord[] = [];\n\n for (const job of this.#jobs.values()) {\n if (\n (job.state === \"claimed\" || job.state === \"running\") &&\n job.lease !== null &&\n job.lease.expiresAt <= now\n ) {\n const requeued: JobRecord = {\n ...job,\n state: \"queued\",\n lease: null,\n completedByLeaseId: null,\n // The TTL clock restarts: it measures how long a job has waited\n // *unclaimed*, and this job has just become available again. Without\n // this, a job whose runner died would expire for time it spent being\n // actively worked on — losing exactly the work `kill -9` recovery\n // exists to save. Total lifetime is bounded by `deadlineAt`, which\n // is absolute and unaffected by reclaim.\n claimableAt: now,\n updatedAt: now,\n };\n this.#write(job.id, requeued);\n changed.push(requeued);\n }\n }\n\n for (const job of this.#jobs.values()) {\n if (job.state !== \"queued\") continue;\n const pastDeadline = job.deadlineAt !== null && job.deadlineAt <= now;\n const pastTtl =\n job.claimableAt !== null && job.claimableAt + job.ttlMs <= now;\n if (!pastDeadline && !pastTtl) continue;\n\n const expired: JobRecord = {\n ...job,\n state: \"expired\",\n lease: null,\n completedByLeaseId: null,\n updatedAt: now,\n };\n this.#write(job.id, expired);\n changed.push(expired);\n }\n return changed;\n }\n\n cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const job = this.#jobs.get(jobId);\n if (!job) return Promise.resolve(null);\n if (job.state === \"queued\") {\n const canceled: JobRecord = {\n ...job,\n state: \"canceled\",\n lease: null,\n completedByLeaseId: null,\n updatedAt: now,\n };\n this.#write(jobId, canceled);\n return Promise.resolve(canceled);\n }\n if (job.state === \"claimed\" || job.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself, so the job's own state waits.\n this.#cancelRequests.add(jobId);\n return Promise.resolve(job);\n }\n return Promise.resolve(job);\n }\n\n listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n return Promise.resolve(\n [...this.#jobs.values()].filter(\n (job) => job.lease?.runnerId === runnerId,\n ),\n );\n }\n\n listCancelRequests(\n runnerId: string,\n ): Promise<{ jobId: string; leaseId: string }[]> {\n return Promise.resolve(\n [...this.#cancelRequests]\n .map((jobId) => ({ jobId, lease: this.#jobs.get(jobId)?.lease }))\n .filter((row) => row.lease?.runnerId === runnerId)\n // The grant, not the id — V1-3.\n .map((row) => ({ jobId: row.jobId, leaseId: row.lease?.id ?? \"\" })),\n );\n }\n\n // -- pairing and runners -------------------------------------------------\n\n createPairing(record: PairingRecord): Promise<void> {\n this.#pairings.set(record.deviceCodeHash, record);\n return Promise.resolve();\n }\n\n getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null> {\n return Promise.resolve(this.#pairings.get(hash) ?? null);\n }\n\n getPairingByUserCode(userCode: string): Promise<PairingRecord | null> {\n for (const pairing of this.#pairings.values()) {\n if (pairing.userCode === userCode) return Promise.resolve(pairing);\n }\n return Promise.resolve(null);\n }\n\n approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === args.userCode,\n );\n if (!pairing) throw new Error(`unknown pairing code: ${args.userCode}`);\n if (pairing.expiresAt <= args.now) {\n throw new Error(\"pairing code has expired\");\n }\n if (pairing.state !== \"pending\") {\n throw new Error(`pairing is already ${pairing.state}`);\n }\n\n const runner: RunnerRecord = {\n id: args.runnerId,\n owner: args.owner,\n // Carried from the pairing, not re-supplied at approval: the user\n // approved a specific machine, and the runner must be that machine.\n device: pairing.device,\n label: pairing.label,\n platform: pairing.platform,\n daemonVersion: pairing.daemonVersion,\n capabilities: pairing.capabilities,\n paused: false,\n revokedAt: null,\n lastHeartbeatAt: args.now,\n createdAt: args.now,\n };\n this.#runners.set(runner.id, runner);\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"approved\",\n owner: args.owner,\n runnerId: runner.id,\n collected: false,\n });\n return Promise.resolve(runner);\n }\n\n denyPairing(userCode: string, _now: number): Promise<void> {\n const pairing = [...this.#pairings.values()].find(\n (p) => p.userCode === userCode,\n );\n if (pairing) {\n this.#pairings.set(pairing.deviceCodeHash, {\n ...pairing,\n state: \"denied\",\n });\n }\n return Promise.resolve();\n }\n\n consumePairingToken(deviceCodeHash: string): Promise<void> {\n const pairing = this.#pairings.get(deviceCodeHash);\n if (pairing) {\n this.#pairings.set(deviceCodeHash, {\n ...pairing,\n collected: true,\n });\n }\n return Promise.resolve();\n }\n\n getRunner(runnerId: string): Promise<RunnerRecord | null> {\n return Promise.resolve(this.#runners.get(runnerId) ?? null);\n }\n\n touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const runner = this.#runners.get(args.runnerId);\n if (!runner) return Promise.resolve(null);\n const updated: RunnerRecord = {\n ...runner,\n capabilities: args.capabilities,\n daemonVersion: args.daemonVersion,\n paused: args.paused,\n lastHeartbeatAt: args.now,\n };\n this.#runners.set(runner.id, updated);\n return Promise.resolve(updated);\n }\n\n revokeRunner(runnerId: string, now: number): Promise<void> {\n const runner = this.#runners.get(runnerId);\n // Revocation is one-way: an already-revoked runner keeps its first\n // revocation time rather than being re-stamped.\n if (runner?.revokedAt === null) {\n this.#runners.set(runnerId, { ...runner, revokedAt: now });\n }\n return Promise.resolve();\n }\n\n listRunners(owner?: string): Promise<RunnerRecord[]> {\n const all = [...this.#runners.values()];\n return Promise.resolve(\n owner === undefined ? all : all.filter((r) => r.owner === owner),\n );\n }\n\n // -- test/demo helpers ---------------------------------------------------\n\n /** All jobs, for demos and assertions. Not part of the store interface. */\n allJobs(): JobRecord[] {\n return [...this.#jobs.values()];\n }\n}\n\n/** The capability that would serve a kind, if any. */\nexport function capabilityFor(\n capabilities: readonly Capability[],\n kind: string,\n): Capability | undefined {\n return capabilities.find((c) => c.kind === kind);\n}\n"],"mappings":"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","names":["keyId","publicIdentityOf","keyId","publicIdentityOf","publicIdentityOf","backendDescriptor","matchAudience","matchAudience","backendDescriptor"]}
package/dist/next.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createFetchHandler
3
- } from "./chunk-5WS55FRU.js";
3
+ } from "./chunk-36Y77FUD.js";
4
4
 
5
5
  // src/next.ts
6
6
  function createHandler(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byollm/server",
3
- "version": "0.1.0-alpha.87",
3
+ "version": "0.1.0-alpha.89",
4
4
  "description": "Framework-agnostic BYOLLM protocol handlers, a reference in-memory store, a Next.js mount, and a Supabase adapter.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "node": ">=22.14"
31
31
  },
32
32
  "dependencies": {
33
- "@byollm/protocol": "0.1.0-alpha.87"
33
+ "@byollm/protocol": "0.1.0-alpha.89"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@supabase/supabase-js": "^2.58.0"
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ids.ts","../src/handlers.ts","../src/reseal.ts","../src/records.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n // A bare UUID, not a prefixed one.\n //\n // The app mints this now, because byollm_009 §6 binds the job id into the\n // envelope's signature — so the id must exist before the row does. A\n // `job_`-prefixed string is not a `uuid`, and the Supabase adapter's column\n // is, so the prefix would have made every enqueue fail there while passing\n // in memory. Ids are opaque to the protocol; the prefix was only ever\n // decoration.\n return randomUUID();\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n\n/**\n * A fresh id for one lease grant.\n *\n * Not a secret and not guessed at — a daemon is told its lease id in the claim\n * response. It exists to distinguish *this* grant from the next one over the\n * same job by the same runner, which is what stops a replayed release landing\n * on a lease the sender never meant.\n */\nexport const generateLeaseId = (): string => randomUUID();\n","import {\n FetchRequest,\n SealedOutcome,\n keyId,\n open,\n publicIdentityOf,\n type FetchResponse,\n RequestSignature,\n verifyRequest,\n verifyPublicIdentity,\n type StoredKeys,\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\n/**\n * What a transport must hand the handler to authenticate a call.\n *\n * `rawBody` is the exact bytes received, not a re-serialisation of the parsed\n * object: JSON.stringify does not round-trip byte-for-byte, and a signature\n * over re-serialised input verifies something the sender never signed.\n */\nexport interface AuthContext {\n readonly endpoint: string;\n readonly rawBody: string;\n readonly signature: unknown;\n}\n\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n /**\n * This site's keypairs (byollm_009 §5) — **supplied, never generated here.**\n *\n * A site is usually more than one process. Generating keys at startup would\n * work perfectly in development and fail only in production, silently: each\n * instance would have a different identity, a daemon would pin whichever\n * one approved its pairing, and every request routed to a different\n * instance would fail a signature check it had no way to explain. So this\n * is a required input, and there is a `keygen` script that produces one.\n */\n readonly siteKeys: StoredKeys;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n readonly #siteKeys: StoredKeys;\n /** This site's identity key id — Amendment A's `stub.site`. Derived once. */\n readonly #siteKeyId: string;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n // Fail at construction, not at the first pairing. A site whose keys are\n // malformed should not start and then refuse its users one at a time.\n if (!verifyPublicIdentity(publicIdentityOf(config.siteKeys))) {\n throw new Error(\n \"siteKeys are not internally consistent: the encryption key is not \" +\n \"signed by the identity key. Generate a fresh pair with \" +\n \"`npx @byollm/server keygen`.\",\n );\n }\n this.#siteKeys = config.siteKeys;\n this.#siteKeyId = keyId(publicIdentityOf(config.siteKeys).identity);\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param auth - the signature and the exact bytes it covers\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n auth: AuthContext,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(auth, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on — and since V1-2 it\n // travels as the refusal itself ({@link MUSTS.REVOCATION_HONORED}).\n //\n // It used to be answered with an empty site set, which the daemon\n // read as \"revoked\". That reading is gone: an empty set now means\n // \"nothing is consented right now\", because a projection can arrive\n // empty by accident and the daemon's response to revocation is to\n // delete its pairing. So the one call every daemon always makes — a\n // daemon with no working backend never claims — carries the\n // unambiguous version: 403 with `revoked`, which is a code and not an\n // inference.\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n );\n case \"fetch\":\n return this.#authed(auth, body, FetchRequest, this.#fetch.bind(this));\n case \"result\":\n return this.#authed(auth, body, ResultRequest, this.#result.bind(this));\n case \"release\":\n return this.#authed(\n auth,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: verify the\n * signature, reject a revoked runner, and parse the body.\n *\n * Authentication happens before schema validation so a stranger probing the\n * endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n auth: AuthContext,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(\"unauthorized\", \"this request is not signed\");\n }\n\n const runner = await this.#store.getRunner(signature.data.runnerId);\n if (!runner) {\n return fail(\"unauthorized\", \"this runner is not recognised\");\n }\n\n // Verified against the identity pinned when the user approved this\n // machine — not against anything the request carries. A signature that\n // authenticates itself authenticates nothing.\n const failure = verifyRequest({\n identityPublic: runner.device.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#now(),\n });\n if (failure !== null) {\n // Deliberately one message for both causes. Telling a caller whether\n // their clock or their key is wrong tells an attacker which half of a\n // forgery already works.\n return fail(\"unauthorized\", \"this request's signature is not valid\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n /**\n * Hand over the payload for a lease this runner holds — byollm_009 §6.\n *\n * The second half of claim-then-fetch. A claim answers with a stub, and the\n * work itself is collected separately by the device that took it, because a\n * payload can only be sealed once its recipient is known.\n *\n * Scoped to the lease, not the job: answering for whatever lease happens to\n * exist would hand the work to a runner whose grant had already been\n * superseded.\n */\n async #fetch(\n request: FetchRequest,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n const job = await this.#store.get(request.jobId);\n if (\n !job ||\n job.lease?.runnerId !== runner.id ||\n job.lease.id !== request.leaseId\n ) {\n // One answer for \"no such job\", \"not yours\" and \"a lease you no longer\n // hold\". A caller who is allowed to know already knows which.\n return fail(\"not-found\", \"no such lease on this job\");\n }\n // One implementation of open-and-reseal, shared with the cloud lane: the\n // deadline and key ids are bound into a signature, and two copies of a\n // bound value is the bug this codebase keeps finding.\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: { id: job.id, envelope: job.envelope, createdAt: job.createdAt },\n device: runner.device,\n });\n if (!resealed.ok) {\n return fail(\"server-error\", \"this job's payload could not be opened\");\n }\n return ok({ envelope: resealed.envelope } satisfies FetchResponse);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n // The machine must prove its encryption key belongs to the identity it\n // is presenting, before either is stored. Otherwise a caller could pair\n // a real identity with an encryption key it holds the secret for, and\n // read everything later sealed to that runner.\n if (!verifyPublicIdentity(request.device)) {\n return fail(\n \"bad-request\",\n \"the device's encryption key is not signed by the identity it was presented with\",\n );\n }\n\n await this.#store.createPairing({\n device: request.device,\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n collected: false,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n !pairing.collected &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n // Only on approval: a pending or denied poll learns nothing, so an\n // unapproved code cannot be used to enumerate a site's keys.\n //\n // One entry, because a direct site *is* one site — the same shape a\n // hub answers with rather than a special case (cloud_009 §5). The\n // daemon's lookup is one map read on every lane, which is what keeps\n // the two lanes one protocol.\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n audience: job.audience,\n owner: job.owner,\n // This site, named by its identity key id — Amendment A §A.3. The\n // daemon pinned this exact value at pairing, so it can check the stub\n // against the envelope it later opens rather than taking our word for\n // which site sent it. On this plane that is redundant, which is the\n // point: the direct and relayed stubs are the same shape, and a daemon\n // serving both cannot tell which upstream it is talking to.\n site: this.#siteKeyId,\n // byollm_016 Phase B. Present only when the site named one, and\n // omitted rather than sent as undefined — the stub is `.strict()` and\n // an explicit undefined is not the same as an absent key.\n ...(job.purpose === undefined ? {} : { purpose: job.purpose }),\n // Bucketed, not measured: an exact size is a stronger fingerprint\n // than routing needs (byollm_009 §6).\n sizeClass: job.sizeClass,\n // Reserved for byollm_006; no job declares it yet.\n streaming: false,\n // The stub's deadline bounds how long a captured envelope is worth\n // keeping, so it is always present — falling back to the TTL window\n // when the app named no absolute one.\n deadlineAt: deadlineFor(job, now),\n // `audienceAllow` is not sent — cloud_008 §0.2. The list stays on\n // `JobRecord`, where `claim` already filtered candidates with it; the\n // daemon's own allowlist is what decides `named` (byollm_001 Rev 1\n // §B) and always was.\n //\n // Removing it from `JobStub` did **not** make this line a type error.\n // A conditional spread is not excess-property-checked, so the field\n // would have gone on being sent to a daemon whose `.strict()` parse\n // now rejects the entire claim response — every daemon on the version\n // pair, refusing all work, for a field nobody read. Worth stating\n // where it happened: the schema is the contract, and the compiler\n // does not enforce it through a spread.\n // No fallback. A job returned from `claim` holds a lease by\n // definition, and synthesising one here would hand the daemon a lease\n // id the store has never heard of — every later release naming it\n // would silently match nothing. A store that returns an unleased job\n // has broken its contract, and this says so.\n lease: leaseOf(job),\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n // No revoked branch here any more — V1-2. A revoked runner is refused by\n // `#authed` before this handler is reached, on heartbeat as on every\n // other endpoint, because \"revoked\" and \"nothing consented right now\"\n // must not arrive as the same empty body.\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n // `renewed` is not reported back — cloud_008 §1.4b. The grants are still\n // extended; the daemon simply never read the list, and `lost` is the\n // signal it acts on.\n const { lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n leases: request.activeLeases,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n // A direct site has no disclosure of its own to go stale: consent to it\n // *is* the pairing, and withdrawing it empties the set above.\n awaitingConsent: [],\n cancel: [...cancel],\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n const outcome = await this.#openResult(request, runner);\n if (!outcome.ok) return outcome.failure;\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n // From the envelope the device signed, not from the request beside it\n // — cloud_008 §2.5. A daemon can no longer seal one answer and declare\n // it came from a different model.\n backendClass: outcome.value.ran.backendClass,\n model: outcome.value.ran.model,\n });\n\n const {\n accepted,\n duplicate,\n job: updated,\n } = await this.#store.complete({\n jobId: request.jobId,\n // Who is asking, for the duplicate answer only — §3.6. Authorisation\n // is `holder`, below, and still is.\n runnerId: runner.id,\n // The grant, not the runner — cloud_008 §1.4a. `CompleteHolder`'s own\n // docstring already called the lease \"the more exact check anyway\";\n // this plane simply had no lease id to give it until now.\n holder: { by: \"lease\", leaseId: request.leaseId },\n outcome: outcome.value.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n // Only when true — cloud_008 §3.6. Absent means \"not a duplicate\", and\n // an optional field that is always present is a required one wearing a\n // question mark.\n ...(duplicate === true ? { duplicate: true } : {}),\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n /**\n * Open a sealed result, or refuse it.\n *\n * The mirror of the daemon's `#openPayload`, and refuses for the same\n * reason: an outcome that does not verify against the device's pinned key is\n * an assertion by whoever relayed it, and storing it would let an\n * intermediary write answers into the app.\n *\n * The clear-text `disposition` is checked here rather than trusted. It is on\n * the wire so a relay can route without opening anything, which means the\n * one thing it must not be is authoritative — a daemon that sealed an error\n * and declared `ok` would otherwise have its declaration believed by\n * everything upstream of this line.\n */\n async #openResult(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<\n { ok: true; value: SealedOutcome } | { ok: false; failure: HandlerResult }\n > {\n const refuse = (why: string) =>\n ({ ok: false as const, failure: fail(\"bad-request\", why) }) as const;\n\n const opened = await open({\n envelope: request.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: runner.device.identity,\n expected: {\n jobId: request.jobId,\n senderKeyId: keyId(runner.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) {\n return refuse(\"the result did not verify as coming from this device\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(opened.plaintext);\n } catch {\n return refuse(\"the sealed result was not valid JSON\");\n }\n const sealed = SealedOutcome.safeParse(parsed);\n if (!sealed.success) return refuse(\"the sealed result was not an outcome\");\n\n if (sealed.data.outcome.outcome !== request.disposition) {\n return refuse(\"the declared disposition is not the one that was sealed\");\n }\n return { ok: true, value: sealed.data };\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n leases: request.leases,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n\n/** The lease a claimed job must have, or a loud failure. */\nfunction leaseOf(job: JobRecord): NonNullable<JobRecord[\"lease\"]> {\n if (!job.lease) {\n throw new Error(\n `store returned job ${job.id} from claim with no lease — the store ` +\n `contract requires a claimed job to hold one`,\n );\n }\n return job.lease;\n}\n","import {\n ENVELOPE_MAX_AGE_MS,\n keyId,\n open,\n publicIdentityOf,\n seal,\n type PublicIdentity,\n type SealedEnvelope,\n type StoredKeys,\n} from \"@byollm/protocol\";\n\n/**\n * Open this site's own at-rest envelope and re-seal it to a claiming device.\n *\n * The single operation that makes byollm_009 §6 work, and it now has two\n * callers: {@link ByollmHandlers} answering `fetch` on the direct plane, and\n * the cloud lane answering the relay's \"who claimed it\" poll. Both do exactly\n * this, and the reason it lives in one file is the reason everything else in\n * this codebase does: the deadline, the key ids and the direction are all\n * bound into a signature, and two implementations of a bound value is the same\n * bug as two clock readings — it works until they disagree, and then nothing\n * opens.\n *\n * The plaintext exists for one statement and never reaches a wire, a store, or\n * a log. That is the whole guarantee: the site is an endpoint, so it is\n * entitled to read its own work, and it is the only party between the app and\n * the device that is.\n */\n\ntype ResealFailure = \"unopenable\";\n\nexport type ResealResult =\n | { readonly ok: true; readonly envelope: SealedEnvelope }\n | { readonly ok: false; readonly reason: ResealFailure };\n\nexport async function resealForDevice(input: {\n siteKeys: StoredKeys;\n /** The job's identity and its at-rest ciphertext. */\n job: {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly createdAt: number;\n };\n /** The device that claimed it, as the upstream reported. */\n device: PublicIdentity;\n}): Promise<ResealResult> {\n const senderKeyId = keyId(publicIdentityOf(input.siteKeys).identity);\n\n const opened = await open({\n envelope: input.job.envelope,\n recipientKeys: input.siteKeys,\n senderIdentityPublic: input.siteKeys.identityPublic,\n expected: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: senderKeyId,\n direction: \"payload\",\n },\n });\n if (!opened.ok) {\n // The store holds something this site cannot open: rotated keys, a\n // corrupted row, or someone else's envelope. Not the device's problem and\n // not something a retry fixes.\n return { ok: false, reason: \"unopenable\" };\n }\n\n const envelope = await seal({\n plaintext: opened.plaintext,\n senderKeys: input.siteKeys,\n recipientEncryptionPublic: input.device.encryption,\n context: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: keyId(input.device.identity),\n // From the record, never recomputed from a fresh clock read — the\n // envelope's own deadline is what the signature bound.\n deadlineAt: input.job.createdAt + ENVELOPE_MAX_AGE_MS,\n direction: \"payload\",\n },\n });\n return { ok: true, envelope };\n}\n","import type {\n PayloadFor,\n PublicIdentity,\n Audience,\n Capability,\n JobKind,\n JobOutcome,\n SealedEnvelope,\n SizeClass,\n JobState,\n Lease,\n ResultProvenance,\n} from \"@byollm/protocol\";\n\n/**\n * A job as the server stores it.\n *\n * Adapters map this shape onto their own storage; the field meanings are\n * normative because the conformance kit asserts behaviour that depends on\n * them (TTL clock start, dependency gating, refusal tracking).\n */\nexport interface JobRecord {\n readonly id: string;\n readonly kind: JobKind;\n /**\n * The work, sealed to this site's own encryption key (byollm_009 §10).\n *\n * The store never holds plaintext. The app sees plaintext at enqueue and at\n * result because the app *is* the endpoint; everything in between —\n * database, backups, log aggregators, a support engineer with read access —\n * sees ciphertext.\n *\n * This is not protection from the application the user deliberately sent\n * their work to. It is protection from everything the application's storage\n * touches, which is a longer list than most people picture.\n */\n readonly envelope: SealedEnvelope;\n /** Fixed at enqueue, where the plaintext is. */\n readonly sizeClass: SizeClass;\n readonly audience: Audience;\n /**\n * The service the site named, if it named one — byollm_016 Phase B.\n *\n * Stored rather than derived, because the stub carries it to the router and\n * the router matches on it. `undefined` means the owner's default answers,\n * which is every job written before this field existed.\n */\n readonly purpose: string | undefined;\n /** The app's id for the user who enqueued it. */\n readonly owner: string;\n /** Server-side restriction on which runner owners may take a `named` job. */\n readonly audienceAllow: readonly string[] | undefined;\n /** Job ids that must all be `ok` before this becomes claimable. */\n readonly dependsOn: readonly string[];\n readonly state: JobState;\n readonly lease: Lease | null;\n /**\n * The grant that recorded this job's result — cloud_008 §3.6.\n *\n * Kept after `lease` is nulled, because \"who finished this\" outlives \"who\n * holds this\" and the two are asked for different reasons. It is what lets\n * a replay from the device that finished the job be answered *as a\n * duplicate* rather than as a stale lease — and lets a replay from any\n * other device be refused exactly as it would be for a job that is not\n * terminal, so a job id is not a terminality probe.\n */\n readonly completedByLeaseId: string | null;\n readonly createdAt: number;\n /**\n * When the job became claimable — enqueue time for a job with no\n * dependencies, or the moment its last dependency reached `ok`.\n *\n * **The TTL clock starts here, not at `createdAt`.** Starting it at enqueue\n * would expire a dependent job for the crime of waiting on a slow\n * dependency (byollm_001 Rev 1 §D, TTL clock resolved in build review).\n * `null` means still blocked.\n */\n readonly claimableAt: number | null;\n /** How long an unclaimed job may wait once claimable. */\n readonly ttlMs: number;\n /** Optional absolute deadline, independent of the TTL. */\n readonly deadlineAt: number | null;\n /**\n * Runners that released this job with reason `refused` — their local\n * allowlist declined it. Never offered to them again\n * ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n */\n readonly refusedBy: readonly string[];\n /** How many times this job has been claimed, including lease-expiry retries. */\n readonly attempts: number;\n readonly outcome: JobOutcome | null;\n readonly provenance: ResultProvenance | null;\n readonly updatedAt: number;\n}\n\n/** A paired daemon as the server stores it. */\nexport interface RunnerRecord {\n readonly id: string;\n /** The app's id for the user this runner is bound to — exactly one. */\n readonly owner: string;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n readonly paused: boolean;\n /** Set once; a revoked runner never un-revokes. */\n readonly revokedAt: number | null;\n readonly lastHeartbeatAt: number;\n readonly createdAt: number;\n /**\n * The device's pinned public keys. What later signatures verify against —\n * a runner id names a machine, this proves it.\n */\n readonly device: PublicIdentity;\n}\n\n/** An in-flight device-code pairing. */\nexport interface PairingRecord {\n /** SHA-256 of the device code. The code itself is never stored. */\n readonly deviceCodeHash: string;\n /** The short code the user reads. Unique among live pairings. */\n readonly userCode: string;\n readonly state: \"pending\" | \"approved\" | \"denied\";\n /** Set when approved — learned from the approving user's own session. */\n readonly owner: string | null;\n readonly runnerId: string | null;\n /**\n * Whether this approval has already been collected — cloud_008 §2.4.\n *\n * This was `runnerTokenOnce`, a bearer token held until the daemon's next\n * poll and then nulled. The token is gone (finding 37: minted, hashed,\n * written to two disks, never sent or compared), but the *deliver-once*\n * property it carried is real and separate: a replayed device code must get\n * nothing, or a code seen in a shell history is a second pairing.\n *\n * So the flag stays and the secret does not. Nulling a token to mean\n * \"collected\" was one field doing two jobs, and only one of them was load\n * bearing.\n */\n readonly collected: boolean;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n /**\n * The device's public keys, presented at pair start (byollm_009 §5).\n *\n * Kept on the pairing so the approving user is approving a *specific\n * machine*, not a code that any machine could later redeem. It is copied\n * onto the runner at approval.\n */\n readonly device: PublicIdentity;\n readonly expiresAt: number;\n readonly createdAt: number;\n}\n\n/**\n * What the app supplies to enqueue a job.\n *\n * Generic over the kind, so the payload has to be the payload *for* that kind.\n * These were independent — `kind: JobKind` beside `payload: JobPayload`, the\n * union of both shapes — and the pairing was left to the author's memory. A\n * chat job carrying a generate payload typechecked, built, shipped, and was\n * refused at the relay's ingress with a precise sentence nobody sees until\n * somebody clicks.\n *\n * `PayloadFor<K>` was already exported by the protocol when that happened, and\n * `enqueue` did not use it. A wrong pairing is now a compile error at the call\n * site, which is the only place that knows what it meant.\n *\n * A caller whose `kind` is a variable rather than a literal still gets the old\n * permissive union — the conditional distributes — so nothing that was legal\n * and correct stops compiling.\n */\nexport interface EnqueueInput<K extends JobKind = JobKind> {\n readonly kind: K;\n /** The work, in plaintext. The server seals it before it is stored. */\n readonly payload: PayloadFor<K>;\n readonly owner: string;\n /**\n * Direct lane only. Refused on the cloud lane, where it is derived.\n *\n * On the cloud lane, who may serve a job comes from the person's own\n * mapping — the service they chose, its owner, and that owner's offer scope\n * — none of which a site is told, and all of which the hub holds at claim.\n * A site declaring an audience there was a third vote cast by the one party\n * the disclosure fence forbids from knowing the answer, and its `private`\n * default silently disabled team sharing for every user who had a team.\n *\n * On the direct lane it still selects something real, which is why it stays\n * rather than going in the same release: it is the switch that turns\n * {@link EnqueueInput.audienceAllow} on. `private` is own-devices-only;\n * `team` hands the decision to the allowlist. Without it there is no way to\n * say \"these runner owners, and no others\", and supplier trust needs one.\n *\n * Defaults to `private` — the safe direction, and on this lane a direction\n * a caller can meaningfully choose.\n */\n readonly audience?: Audience;\n /**\n * Which of *your site's* declared purposes this job serves — Amendment L.\n *\n * **A need, never a name.** You declare purposes at registration —\n * `\"revenue\"`, `\"writing-assistant\"` — and each of your users maps them to\n * one of their own services on the consent screen. This field names the\n * purpose; the mapping does the rest.\n *\n * There is no model field, no base URL, no flags, and — since Amendment L —\n * no way to name a service either. Your vocabulary is your purposes; theirs\n * is their services; the two never meet. You learn whether a slot was\n * satisfiable and nothing else.\n *\n * Use the purpose **key**, not its label. Labels are prose for the consent\n * screen and may change; a key travels on every job and is what mappings\n * are stored against.\n *\n * Leave it out only in direct mode, which has no control plane to hold a\n * mapping and answers by kind alone.\n */\n readonly purpose?: string;\n readonly audienceAllow?: readonly string[];\n readonly dependsOn?: readonly string[];\n /** Defaults to the server config's `defaultTtlMs`. */\n readonly ttlMs?: number;\n readonly deadlineAt?: number;\n /** Caller-supplied id, for idempotent enqueue. */\n readonly id?: string;\n}\n\n/**\n * What the *store* is given — the sealed form.\n *\n * Distinct from {@link EnqueueInput} because the two are genuinely different\n * things: an app hands over work in plaintext, and what gets written down is\n * sealed. Collapsing them into one type would mean a field that is sometimes\n * readable and sometimes not, which is the kind of ambiguity that ends with\n * plaintext in a database.\n */\nexport interface StoredJobInput extends Omit<EnqueueInput, \"payload\" | \"id\"> {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly sizeClass: SizeClass;\n}\n\n/**\n * When a job's ciphertext stops being worth carrying — cloud_008 §31.\n *\n * One function because it was two expressions. The direct plane computed\n * `job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs`; the cloud lane\n * computed `record.deadlineAt ?? record.createdAt + <a local constant>`. The\n * first branch agreed and the fallback did not, so a job with no explicit\n * deadline got two different ones depending on which lane published it — and\n * the difference is largest exactly where it matters, for a job blocked on a\n * dependency, whose `claimableAt` may be hours after `createdAt`.\n *\n * The TTL clock starts when a job becomes *claimable*, which is the rule\n * `DEPENDS_ON_GATING` and `TTL_EXPIRY` already share: a dependent job must not\n * spend its life waiting for its dependency.\n */\nexport function deadlineFor(\n job: Pick<JobRecord, \"deadlineAt\" | \"claimableAt\" | \"ttlMs\">,\n now: number,\n): number {\n return job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs;\n}\n","import {\n ENDPOINTS,\n ERROR_STATUS,\n MAX_ENVELOPE_BYTES,\n tooLargeMessage,\n PROTOCOL_PREFIX,\n checkProtocolVersion,\n type Endpoint,\n} from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * Derived, not chosen. This was `8 * 1024 * 1024` beside a comment saying the\n * protocol caps a payload at 4 MB — true when it was written, and the cap has\n * since moved to {@link MAX_ENVELOPE_BYTES}, which is 10 MiB. So the direct\n * lane refused envelopes the protocol permits, and the hub — which derives\n * its own limit the same way this now does — accepted them.\n *\n * That is the failure this codebase keeps finding in other clothes: one rule\n * with two implementations, and only one of them moved. A site self-hosting\n * the SDK and a site on the hub must not disagree about whether a job is too\n * big, so neither of them gets to hold the number.\n *\n * The 512 KiB of headroom is for JSON overhead and a batch of results around\n * the envelope, matching the hub's `MAX_ENVELOPE_BYTES + 512 * 1024`.\n */\nconst MAX_BODY_BYTES = MAX_ENVELOPE_BYTES + 512 * 1024;\n\n/**\n * What a message that is too big is told — B061, Kevin's bisection.\n *\n * This said \"request body too large\" and nothing else, at both call sites,\n * with both numbers already in scope. Somebody who hits it learns that\n * something was too big and not what, not by how much, not whether the limit\n * is per-message or per-account, and not what to do — so the only way\n * forward is to bisect, which is exactly what Kevin did.\n *\n * The sentence and its rounding come from the protocol now — B072. The first\n * version of this copied the relay's WORDS and rediscovered the relay's bug\n * with them: `toFixed` rounds to nearest, so one byte over printed \"this\n * message is 10.5 MB and the limit is 10.5 MB\". The relay had already found\n * that, fixed it, and written the reasoning beside the fix. Copying a\n * sentence copies everything about it except the part that was learned.\n */\nconst tooLarge = (bytes: number): string =>\n tooLargeMessage({ bytes, limit: MAX_BODY_BYTES });\n\n/**\n * Where the protocol endpoints are mounted.\n *\n * Defaults to {@link PROTOCOL_PREFIX}. Pass the real mount point when it is\n * anything else — a Next.js route at `app/api/byollm/[...route]/route.ts`\n * serves `/api/byollm/...`, so it needs `basePath: \"/api/byollm\"`.\n *\n * @throws if the path is not an absolute, single-segment-per-slash path. A\n * mount point is configuration, and a malformed one should fail at startup\n * rather than silently match nothing.\n */\nfunction normalizeBasePath(basePath: string): string {\n const trimmed = basePath.endsWith(\"/\") ? basePath.slice(0, -1) : basePath;\n if (!trimmed.startsWith(\"/\")) {\n throw new Error(`basePath must start with \"/\": got ${basePath}`);\n }\n if (trimmed.includes(\"//\") || /[?#*]/.test(trimmed)) {\n throw new Error(`basePath must be a plain path: got ${basePath}`);\n }\n return trimmed;\n}\n\n/**\n * Pull the endpoint name out of a URL path, or null if it isn't ours.\n *\n * The full path must match `<basePath>/<endpoint>` exactly. This used to\n * compare only the *last* segment, which meant `/anything/at/all/claim`\n * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it\n * appeared in a 404 message and was never matched against. For the handler\n * that serves claim, result and heartbeat, dispatching on a suffix is a\n * looser rule than anyone reading the constant would assume, and loose\n * matching in a security surface should at least be a decision.\n *\n * The cost is that the mount point is now something a deployment has to state\n * rather than something that works by accident. That is the intended trade:\n * a 404 at startup naming the mount point beats a handler answering on paths\n * nobody meant to expose.\n */\nexport function routeEndpoint(\n pathname: string,\n basePath: string = PROTOCOL_PREFIX,\n): Endpoint | null {\n const base = normalizeBasePath(basePath);\n const path = pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n if (!path.startsWith(`${base}/`)) return null;\n const rest = path.slice(base.length + 1);\n return (ENDPOINTS as readonly string[]).includes(rest)\n ? (rest as Endpoint)\n : null;\n}\n\n/**\n * Read the request signature from headers (byollm_009 §4.2).\n *\n * In headers rather than the body so the signature covers the body whole,\n * with no field to exclude from its own hash — a scheme that signs a body\n * minus one field has to agree, byte for byte, on how that field is removed.\n */\nexport function signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const rawIssuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n if (runnerId === null || signature === null || rawIssuedAt === null) {\n return undefined;\n }\n // Checked against null *before* Number(), because `Number(null)` is 0 —\n // finite, plausible-looking, and wrong. A missing timestamp would have\n // become a timestamp of the epoch, which the freshness check would then\n // reject for the wrong reason.\n const issuedAt = Number(rawIssuedAt);\n if (!Number.isFinite(issuedAt)) return undefined;\n return { runnerId, issuedAt, signature };\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig & {\n /**\n * Where these endpoints are mounted. Defaults to\n * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere.\n */\n readonly basePath?: string;\n },\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n // Validate once, at construction: a bad mount point is a deployment bug and\n // should surface when the server starts, not as a silent 404 per request.\n const basePath = normalizeBasePath(config.basePath ?? PROTOCOL_PREFIX);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname, basePath);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${basePath} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: tooLarge(Number(declared)),\n });\n }\n\n let body: unknown;\n let rawBody: string;\n try {\n rawBody = await request.text();\n const text = rawBody;\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: tooLarge(text.length),\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n // byollm_009 §4: version before anything else. A mismatch must name the\n // disagreement and the fix, not surface as a generic bad-request from a\n // schema literal buried in an endpoint — which is what happened before,\n // and is why \"the connection is versionless\" was listed as a defect.\n const refusal = checkProtocolVersion(body);\n if (refusal) {\n return json(ERROR_STATUS[refusal.error], refusal);\n }\n\n const result = await handlers.handle(endpoint, body, {\n endpoint,\n // The bytes as received. Re-serialising the parsed object would verify\n // a signature over something the sender never sent.\n rawBody,\n signature: signatureFrom(request.headers),\n });\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["keyId","open","publicIdentityOf","publicIdentityOf","keyId","open","ERROR_STATUS","ERROR_STATUS"]}