@byollm/server 0.1.0-alpha.43 → 0.1.0-alpha.45

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.43`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.45`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npm install @byollm/server@alpha`.
5
5
  >
@@ -105,7 +105,7 @@
105
105
  > packages published and `@byollm/server` did not: a Sigstore
106
106
  > transparency-log 409 on its provenance attestation. The workflow's
107
107
  > "already published" guard correctly refuses to resume a partial publish,
108
- > so `0.1.0-alpha.43` is that release, whole.
108
+ > so `0.1.0-alpha.45` is that release, whole.
109
109
  >
110
110
  > If you run the Supabase adapter, `alpha.21` needs
111
111
  > `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
@@ -113,7 +113,7 @@
113
113
 
114
114
  <!-- release-note 0.1.0-alpha.40 -->
115
115
  **`byollm install` — stop keeping a terminal open.** The daemon can now run
116
- under your machine's own supervisor and restart itself if it stops: a launchd
116
+ under your computer's own supervisor and restart itself if it stops: a launchd
117
117
  agent on macOS, a `systemd --user` unit on Linux, a logon task on Windows. All
118
118
  user-level — no root, no system directories, and `byollm uninstall` takes it
119
119
  away. `byollm status` gained a line saying whether it is actually supervised
@@ -137,7 +137,7 @@ const { outcome, fallback } = await job.result({
137
137
 
138
138
  Whatever you return, `result()` labels it `fallback: true` — the stamp is
139
139
  applied by the wait, not taken from you, so an answer that did not run on
140
- somebody's machine cannot be reported as though it did (`FALLBACK_LABELED`).
140
+ somebody's device cannot be reported as though it did (`FALLBACK_LABELED`).
141
141
  Both delivery channels do it, polling and Supabase Realtime. Records still
142
142
  work; they just get labelled too.
143
143
 
@@ -211,7 +211,7 @@ npx @byollm/server@alpha keygen # prints BYOLLM_SITE_KEYS=...
211
211
  ```
212
212
 
213
213
  Once, not per deploy and never at startup — a daemon pins this identity when
214
- its owner approves the pairing, and regenerating it means every paired machine
214
+ its owner approves the pairing, and regenerating it means every paired device
215
215
  must pair again. Generating at startup fails only under horizontal scale: each
216
216
  instance would have a different identity, and a daemon would be refused by
217
217
  whichever one it did not pair with.
@@ -221,7 +221,7 @@ whichever one it did not pair with.
221
221
  ```ts
222
222
  const job = await getApp().enqueue({
223
223
  kind: "llm.generate",
224
- audience: "self", // this user's own machine only — the default
224
+ audience: "private", // this user's own device only — the default
225
225
  owner: userId,
226
226
  payload: { prompt: `Summarize this transcript:\n\n${transcript}` },
227
227
  });
@@ -232,7 +232,7 @@ const { outcome, fallback } = await job.result({
232
232
  onNoRunner: () => runOnHostedModel(transcript),
233
233
  });
234
234
 
235
- // `fallback` is true when nobody's machine ran it and this came from your
235
+ // `fallback` is true when nobody's device ran it and this came from your
236
236
  // own substitute. Say so wherever you show the answer: work that did not run
237
237
  // on the user's compute must not be presented as though it did.
238
238
  ```
@@ -254,7 +254,7 @@ const runner = await getApp().approvePairing({
254
254
  });
255
255
  ```
256
256
 
257
- `app.pendingPairing(code)` tells you what they are about to approve — machine
257
+ `app.pendingPairing(code)` tells you what they are about to approve — device
258
258
  label, platform, and which models it is offering — so the page can show it.
259
259
 
260
260
  ## Stores
@@ -292,7 +292,7 @@ everything the browser does.
292
292
  ## Two things the API makes you confront
293
293
 
294
294
  **Community results are untrusted.** A `named`/`public` result came from
295
- someone else's machine and can be anything. Every result carries provenance,
295
+ someone else's device and can be anything. Every result carries provenance,
296
296
  and `untrusted` is derived from the audience — you cannot mark volunteer
297
297
  output as first-party:
298
298
 
@@ -307,7 +307,7 @@ if (provenance?.untrusted) {
307
307
  **Jobs can depend on each other.** `dependsOn: [jobId]` keeps a job
308
308
  unclaimable until its dependencies are `ok`. One field and one claim
309
309
  predicate, not a DAG engine — so the two halves of a piece of work can land on
310
- two different people's machines, in order, without your app orchestrating the
310
+ two different people's devices, in order, without your app orchestrating the
311
311
  wait.
312
312
 
313
313
  ## Certifying an adapter
package/dist/index.d.ts CHANGED
@@ -150,7 +150,7 @@ interface RunnerAvailability {
150
150
  interface AvailabilityQuery {
151
151
  readonly kind: JobKind;
152
152
  readonly owner: string;
153
- readonly audience?: "self" | "named" | "public";
153
+ readonly audience?: "private" | "team" | "public";
154
154
  readonly audienceAllow?: readonly string[];
155
155
  }
156
156
  interface ByollmAppOptions {
package/dist/index.js CHANGED
@@ -510,7 +510,7 @@ var ByollmApp = class {
510
510
  const match = matchAudience(
511
511
  {
512
512
  owner: query.owner,
513
- audience: query.audience ?? "self",
513
+ audience: query.audience ?? "private",
514
514
  audienceAllow: query.audienceAllow
515
515
  },
516
516
  {
@@ -689,7 +689,7 @@ var MemoryStore = class {
689
689
  kind: input.kind,
690
690
  envelope: input.envelope,
691
691
  sizeClass: input.sizeClass,
692
- audience: input.audience ?? "self",
692
+ audience: input.audience ?? "private",
693
693
  owner: input.owner,
694
694
  audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
695
695
  dependsOn,
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 DeliveredResult,\n type JobKind,\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/**\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?: \"self\" | \"named\" | \"public\";\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 */\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 const deps: PollingDeliveryDeps = {\n ...(options.noRunnerGraceMs === undefined\n ? {}\n : { graceMs: options.noRunnerGraceMs }),\n read: (jobId) => this.result(jobId),\n availability: async (jobId) => {\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 this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);\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(input: EnqueueInput): Promise<JobHandle> {\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 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 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 for (const runner of live) {\n const capability = runner.capabilities.find((c) => c.kind === query.kind);\n if (!capability) continue;\n capable += 1;\n\n const match = matchAudience(\n {\n owner: query.owner,\n audience: query.audience ?? \"self\",\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 locallyAllows: () => true,\n },\n );\n if (match.ok) admitted += 1;\n }\n\n if (capable === 0) {\n return {\n available: false,\n reason: \"no-matching-capability\",\n candidates: 0,\n };\n }\n if (admitted === 0) {\n return {\n available: false,\n reason: \"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\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 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 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 ?? \"self\",\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 locallyAllows: () => 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 DeliveredResult,\n type JobKind,\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/**\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?: \"private\" | \"team\" | \"public\";\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 */\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 const deps: PollingDeliveryDeps = {\n ...(options.noRunnerGraceMs === undefined\n ? {}\n : { graceMs: options.noRunnerGraceMs }),\n read: (jobId) => this.result(jobId),\n availability: async (jobId) => {\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 this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);\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(input: EnqueueInput): Promise<JobHandle> {\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 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 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 for (const runner of live) {\n const capability = runner.capabilities.find((c) => c.kind === query.kind);\n if (!capability) continue;\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 locallyAllows: () => true,\n },\n );\n if (match.ok) admitted += 1;\n }\n\n if (capable === 0) {\n return {\n available: false,\n reason: \"no-matching-capability\",\n candidates: 0,\n };\n }\n if (admitted === 0) {\n return {\n available: false,\n reason: \"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\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 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 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 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 locallyAllows: () => 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"]}
@@ -191,7 +191,7 @@ function supabaseStore(options) {
191
191
  kind: input.kind,
192
192
  envelope: input.envelope,
193
193
  size_class: input.sizeClass,
194
- audience: input.audience ?? "self",
194
+ audience: input.audience ?? "private",
195
195
  owner: input.owner,
196
196
  audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,
197
197
  depends_on: dependsOn,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/supabase/realtime.ts","../../src/supabase/index.ts"],"sourcesContent":["import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { DeliveredResult } from \"@byollm/protocol\";\nimport {\n NoRunnerAvailableError,\n ResultTimeoutError,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n labelFallback,\n} from \"../delivery.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n/**\n * How long a sustained no-runner signal must persist before it is believed.\n * A daemon restarting must not fail every job in flight.\n */\nconst NO_RUNNER_GRACE_MS = 10_000;\n\n/**\n * Realtime delivery: the app learns a job finished when Postgres says so.\n *\n * byollm_003 Rev 1 requires the server→app path be an explicit channel rather\n * than an implied in-request `await`. Polling is the portable default;\n * this is the one worth having when the app is already on Supabase, because\n * a result arrives in milliseconds instead of on the next poll tick.\n *\n * The no-runner watch still polls, deliberately: runner liveness is a\n * *derived* signal (nobody with matching capability has heartbeated lately),\n * and there is no row change to subscribe to for \"something stopped\n * happening\".\n */\nexport function supabaseRealtimeDelivery(\n client: SupabaseClient,\n): (deps: PollingDeliveryDeps) => ResultDelivery {\n return (deps) => new SupabaseRealtimeDelivery(client, deps);\n}\n\nclass SupabaseRealtimeDelivery implements ResultDelivery {\n readonly #client: SupabaseClient;\n readonly #deps: PollingDeliveryDeps;\n\n constructor(client: SupabaseClient, deps: PollingDeliveryDeps) {\n this.#client = client;\n this.#deps = deps;\n }\n\n async waitFor(\n jobId: string,\n options: WaitOptions = {},\n ): Promise<DeliveredResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n // Read first. The job may already be terminal, and subscribing to a\n // channel for an event that has already happened waits forever.\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) return current;\n\n // Declared before the subscription so the channel callback closes over a\n // `settled` that already exists. Every async path below routes its failure\n // here: a rejection that escapes this object becomes an unhandled\n // rejection, and an unhandled rejection ends the process.\n const settled = Promise.withResolvers<DeliveredResult>();\n this.#resolve = settled.resolve;\n\n const channel = this.#client.channel(`byollm_job_${jobId}`).on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n this.#check(jobId).catch(settled.reject);\n },\n );\n\n // `subscribe()` returns the channel, not a promise — awaiting it would be\n // a no-op that reads as if it waited for the subscription to be live.\n channel.subscribe();\n\n // A second read after subscribing closes the race where the job finished\n // between the first read and the subscription taking effect.\n this.#check(jobId).catch(settled.reject);\n\n const timer = setTimeout(() => {\n settled.reject(new ResultTimeoutError(jobId, timeoutMs));\n }, timeoutMs);\n\n const watcher = this.#watchAvailability(jobId, options, settled);\n const abort = (): void => {\n settled.reject(new Error(\"wait aborted\"));\n };\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n\n try {\n return await settled.promise;\n } finally {\n clearTimeout(timer);\n clearInterval(watcher);\n options.signal?.removeEventListener(\"abort\", abort);\n await this.#client.removeChannel(channel);\n }\n }\n\n #resolve: ((result: DeliveredResult) => void) | undefined;\n\n async #check(jobId: string): Promise<void> {\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) this.#resolve?.(current);\n }\n\n /** Poll runner liveness; there is no row event for \"nothing is happening\". */\n #watchAvailability(\n jobId: string,\n options: WaitOptions,\n settled: PromiseWithResolvers<DeliveredResult>,\n ): NodeJS.Timeout {\n let noRunnerSince: number | null = null;\n\n return setInterval(() => {\n // `.catch`, not `void`. Two things in here can reject — the store read\n // and the caller's own `onNoRunner` — and discarding either made a\n // transient store error, or an app whose fallback throws, terminate the\n // process. The caller is awaiting `result()`; that is where a failure\n // belongs, and it is what the polling channel already does by virtue of\n // running inside the awaited chain. A delivery adapter must not change\n // what a failure means.\n (async () => {\n const availability = await this.#deps.availability(jobId);\n if (availability.available || availability.blocked) {\n noRunnerSince = null;\n return;\n }\n noRunnerSince ??= Date.now();\n if (Date.now() - noRunnerSince < NO_RUNNER_GRACE_MS) return;\n\n const reason = availability.reason ?? \"no-runner-online\";\n const substitute = await options.onNoRunner?.(reason);\n if (substitute !== undefined) {\n // The same labelling the polling channel applies, from the same\n // function — {@link MUSTS.FALLBACK_LABELED} cannot depend on which\n // store an app happened to choose.\n settled.resolve(labelFallback(jobId, substitute));\n } else {\n settled.reject(new NoRunnerAvailableError(jobId, reason));\n }\n })().catch(settled.reject);\n }, 2_000);\n }\n}\n\nfunction isTerminal(state: string): boolean {\n return (\n state === \"ok\" ||\n state === \"error\" ||\n state === \"canceled\" ||\n state === \"expired\"\n );\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type {\n Capability,\n JobOutcome,\n JobState,\n PublicIdentity,\n} from \"@byollm/protocol\";\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 LeaseRef,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"../store.js\";\n\n/**\n * `@byollm/server/supabase` — the first-party Supabase adapter.\n *\n * The piece the of-tomorrow-framework's runner module consumes verbatim.\n * Migrations ship in `supabase/migrations`; the atomic claim lives in a\n * `security definer` RPC using `FOR UPDATE SKIP LOCKED`, and the audience\n * rules are mirrored in SQL so the server refuses independently of the daemon\n * (byollm_003 §Server-side MUSTs).\n *\n * Requires the **service role** key: a runner authenticates with a bearer\n * token of its own, which is not a Supabase session, so the protocol handler\n * cannot run under RLS as the runner's user. RLS still governs everything the\n * *browser* does — the app-side policies in the migration are what protect\n * one user's jobs from another.\n *\n * @packageDocumentation\n */\n\n/** Row shape of `byollm_jobs`. */\ninterface JobRow {\n id: string;\n kind: string;\n envelope: unknown;\n size_class: \"small\" | \"medium\" | \"large\" | \"unbounded\";\n audience: \"self\" | \"named\" | \"public\";\n owner: string;\n audience_allow: string[] | null;\n depends_on: string[];\n state: JobState;\n lease_id: string | null;\n lease_runner: string | null;\n completed_by_lease_id: string | null;\n lease_expires_at: string | null;\n claimable_at: string | null;\n ttl_ms: number;\n deadline_at: string | null;\n refused_by: string[];\n attempts: number;\n outcome: JobOutcome | null;\n provenance: JobRecord[\"provenance\"];\n created_at: string;\n updated_at: string;\n}\n\n/** Row shape of `byollm_runners`. */\ninterface RunnerRow {\n id: string;\n owner: string;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n paused: boolean;\n revoked_at: string | null;\n last_heartbeat_at: string;\n created_at: string;\n}\n\n/** Row shape of `byollm_pairings`. */\ninterface PairingRow {\n device_code_hash: string;\n user_code: string;\n state: \"pending\" | \"approved\" | \"denied\";\n owner: string | null;\n runner_id: string | null;\n collected_at: string | null;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n expires_at: string;\n created_at: string;\n}\n\nconst ms = (iso: string | null): number | null =>\n iso === null ? null : Date.parse(iso);\n\nconst iso = (epochMs: number): string => new Date(epochMs).toISOString();\n\nfunction toJob(row: JobRow): JobRecord {\n const leaseExpires = ms(row.lease_expires_at);\n return {\n id: row.id,\n kind: row.kind as JobRecord[\"kind\"],\n envelope: row.envelope as JobRecord[\"envelope\"],\n sizeClass: row.size_class,\n audience: row.audience,\n owner: row.owner,\n audienceAllow: row.audience_allow ?? undefined,\n dependsOn: row.depends_on,\n state: row.state,\n completedByLeaseId: row.completed_by_lease_id ?? null,\n lease:\n // Keyed on the lease id, not the runner. A relayed grant has no runner\n // row to point at (see AdoptArgs), and reading the lease as absent\n // because `lease_runner` is null would make an actively-held job look\n // claimable — the exact bug `adopt` exists to prevent.\n leaseExpires !== null && row.lease_id !== null\n ? {\n id: row.lease_id,\n runnerId: row.lease_runner ?? \"\",\n expiresAt: leaseExpires,\n }\n : null,\n createdAt: Date.parse(row.created_at),\n claimableAt: ms(row.claimable_at),\n ttlMs: row.ttl_ms,\n deadlineAt: ms(row.deadline_at),\n refusedBy: row.refused_by,\n attempts: row.attempts,\n outcome: row.outcome,\n provenance: row.provenance,\n updatedAt: Date.parse(row.updated_at),\n };\n}\n\n/**\n * A PostgREST filter matching exactly these (job, lease) pairs.\n *\n * Not two `IN` lists: `id IN (…) AND lease_id IN (…)` is a cross product, and\n * while UUID uniqueness makes a mismatch improbable, \"improbable\" is not the\n * property a lease check should rest on. This says what it means.\n */\nconst leasePairs = (leases: readonly LeaseRef[]): string =>\n leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(\",\");\n\nfunction toRunner(row: RunnerRow): RunnerRecord {\n return {\n id: row.id,\n owner: row.owner,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n paused: row.paused,\n revokedAt: ms(row.revoked_at),\n lastHeartbeatAt: Date.parse(row.last_heartbeat_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nfunction toPairing(row: PairingRow): PairingRecord {\n return {\n deviceCodeHash: row.device_code_hash,\n userCode: row.user_code,\n state: row.state,\n owner: row.owner,\n runnerId: row.runner_id,\n // Collected when it has a timestamp. This was `runner_token_once ===\n // null` — a nulled token standing in for a fact about delivery, which is\n // one field doing two jobs (cloud_008 §2.4a).\n collected: row.collected_at !== null,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n expiresAt: Date.parse(row.expires_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nexport interface SupabaseStoreOptions {\n /** A client built with the **service role** key. */\n readonly client: SupabaseClient;\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\n/** Build the Supabase-backed store. */\nexport function supabaseStore(options: SupabaseStoreOptions): ByollmStore {\n const db = options.client;\n const defaultTtlMs = options.defaultTtlMs ?? 15 * 60_000;\n\n /**\n * Narrow one PostgREST response, or throw with the Postgres message.\n *\n * `supabase-js` types rows as `any` unless the project has generated\n * database types, so the assertion has to live somewhere. Confining it to\n * these two helpers — against the row interfaces declared above — keeps\n * every call site typed and leaves exactly one place to review.\n */\n /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters --\n T appears only in the return type because these helpers *are* the cast.\n That is the point: one reviewable place where PostgREST's `any` becomes\n one of the row interfaces above. */\n function unwrap<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n if (result.data === null || result.data === undefined) {\n throw new Error(\"supabase: no data returned\");\n }\n return result.data as T;\n }\n\n /** Same, but a missing row is a legitimate answer rather than an error. */\n function unwrapMaybe<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T | null {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n return (result.data ?? null) as T | null;\n }\n /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */\n\n return {\n // -- jobs ---------------------------------------------------------------\n\n async create(input: StoredJobInput, now: number): Promise<JobRecord> {\n const dependsOn = [...(input.dependsOn ?? [])];\n\n // A job with dependencies starts blocked; the trigger sets\n // `claimable_at` when the last one reaches `ok`, which is where its TTL\n // clock starts.\n let claimableAt: string | null = iso(now);\n if (dependsOn.length > 0) {\n const deps = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select(\"id,state\").in(\"id\", dependsOn),\n ) as { id: string; state: JobState }[];\n const allDone =\n deps.length === dependsOn.length &&\n deps.every((dep) => dep.state === \"ok\");\n claimableAt = allDone ? iso(now) : null;\n }\n\n const row = {\n id: input.id,\n kind: input.kind,\n envelope: input.envelope,\n size_class: input.sizeClass,\n audience: input.audience ?? \"self\",\n owner: input.owner,\n audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,\n depends_on: dependsOn,\n claimable_at: claimableAt,\n ttl_ms: input.ttlMs ?? defaultTtlMs,\n deadline_at:\n input.deadlineAt === undefined ? null : iso(input.deadlineAt),\n };\n\n // Idempotent by caller-supplied id, matching the reference store: an\n // app's retry must not duplicate work.\n const inserted = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .upsert(row, { onConflict: \"id\", ignoreDuplicates: true })\n .select()\n .maybeSingle(),\n );\n\n if (inserted) return toJob(inserted);\n const existing = unwrap<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", input.id).single(),\n );\n return toJob(existing);\n },\n\n async get(jobId: string): Promise<JobRecord | null> {\n const row = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n return row === null ? null : toJob(row);\n },\n\n async claim(args: ClaimArgs): Promise<JobRecord[]> {\n // One RPC, one transaction, `FOR UPDATE SKIP LOCKED` inside\n // ({@link MUSTS.CLAIM_ATOMIC}).\n const rows = unwrap<JobRow[]>(\n await db.rpc(\"byollm_claim_jobs\", {\n p_runner_id: args.runnerId,\n p_capabilities: args.capabilities,\n p_max: args.max,\n p_lease_ms: args.leaseMs,\n }),\n );\n return rows.map(toJob);\n },\n\n async renewLeases(args: RenewArgs): Promise<RenewResult> {\n await db.rpc(\"byollm_expire_due\");\n if (args.leases.length === 0) return { renewed: [], lost: [] };\n\n const expiresAt = iso(args.now + args.leaseMs);\n const renewedRows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"running\",\n lease_expires_at: expiresAt,\n updated_at: iso(args.now),\n })\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases))\n .in(\"state\", [\"claimed\", \"running\"])\n .select(\"id\"),\n );\n\n const renewedIds = new Set(renewedRows.map((row) => row.id));\n return {\n renewed: renewedRows.map((row) => ({\n jobId: row.id,\n expiresAt: args.now + args.leaseMs,\n })),\n // Anything the runner thinks it holds but did not renew is gone,\n // named by the grant it asked about rather than by a bare id — V1-3.\n lost: args.leases.filter((lease) => !renewedIds.has(lease.jobId)),\n };\n },\n\n async adopt(args: AdoptArgs): Promise<JobRecord | null> {\n // The predicates are the guard, evaluated in the database rather than\n // read-then-written here: `state in (queued, claimed)` is what makes\n // adopting a terminal or expired job impossible under concurrency.\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"claimed\",\n lease_id: args.leaseId,\n // Left null on purpose: `lease_runner` is a foreign key into\n // `byollm_runners`, and a relayed device has no row there. See\n // AdoptArgs — the site records the grant, not a machine it has\n // no relationship with.\n lease_runner: null,\n lease_expires_at: iso(args.expiresAt),\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"queued\", \"claimed\"])\n .select(),\n );\n const written = rows[0];\n return written === undefined ? null : toJob(written);\n },\n\n async complete(args: CompleteArgs): Promise<CompleteResult> {\n const state: JobState =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n // The `in('state', ...)` predicate is the idempotency guard: a job that\n // already reached a terminal state matches nothing, so the first\n // outcome wins ({@link MUSTS.RESULT_IDEMPOTENT}). The `lease_runner`\n // predicate is {@link MUSTS.LEASE_HONORED}.\n // The `in('state', ...)` predicate is the idempotency guard. The\n // second predicate is LEASE_HONORED, and which column carries it\n // depends on the plane: a direct runner is named by id, a relayed\n // grant only by its lease. Built as a query rather than branched into\n // two, so there is one update statement and no chance of the two\n // drifting.\n let update = db\n .from(\"byollm_jobs\")\n .update({\n state,\n lease_runner: null,\n lease_expires_at: null,\n // Which grant recorded it, kept after the lease is dropped — §3.6.\n completed_by_lease_id:\n args.holder.by === \"lease\" ? args.holder.leaseId : null,\n outcome: args.outcome,\n provenance: args.provenance,\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"claimed\", \"running\"]);\n update =\n args.holder.by === \"runner\"\n ? update.eq(\"lease_runner\", args.holder.runnerId)\n : update.eq(\"lease_id\", args.holder.leaseId);\n const rows = unwrap<JobRow[]>(await update.select());\n\n const written = rows[0];\n if (written === undefined) {\n const current = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .select()\n .eq(\"id\", args.jobId)\n .maybeSingle(),\n );\n const job = current === null ? null : toJob(current);\n // Terminal before holder — cloud_008 §3.6. The update above matched\n // nothing for one of two reasons, and the caller is owed the\n // difference: the device that already recorded this job hears\n // \"duplicate\", and anybody else hears the same refusal they would get\n // for a job that is not terminal, so a job id is not a terminality\n // probe.\n //\n // Decided on the row that is there rather than by a second predicate,\n // because the update is the atomic part and this is only a diagnosis\n // of why it matched nothing.\n const duplicate =\n job !== null &&\n job.provenance?.runnerId === args.runnerId &&\n (job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\") &&\n args.holder.by === \"lease\" &&\n job.completedByLeaseId !== null &&\n job.completedByLeaseId === args.holder.leaseId;\n return duplicate\n ? { accepted: false, duplicate: true, job }\n : { accepted: false, job };\n }\n return { accepted: true, job: toJob(written) };\n },\n\n async release(args: ReleaseArgs): Promise<string[]> {\n if (args.leases.length === 0) return [];\n\n const held = unwrap<{ id: string; refused_by: string[] }[]>(\n await db\n .from(\"byollm_jobs\")\n .select(\"id,refused_by\")\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases)),\n );\n\n const released: string[] = [];\n for (const row of held) {\n const refusedBy =\n args.reason === \"refused\"\n ? [...new Set([...row.refused_by, args.runnerId])]\n : row.refused_by;\n\n const { error } = await db\n .from(\"byollm_jobs\")\n .update({\n state: \"queued\",\n lease_id: null,\n lease_runner: null,\n lease_expires_at: null,\n // Newly available again, so the TTL clock restarts.\n claimable_at: iso(args.now),\n refused_by: refusedBy,\n updated_at: iso(args.now),\n })\n .eq(\"id\", row.id)\n .eq(\"lease_runner\", args.runnerId);\n if (error) throw new Error(`supabase: ${error.message}`);\n released.push(row.id);\n }\n return released;\n },\n\n async expireDue(_now: number): Promise<JobRecord[]> {\n // The sweep is a single idempotent SQL function; it reports a count\n // rather than rows, and the caller only needs to know it ran.\n const { error } = await db.rpc(\"byollm_expire_due\");\n if (error) throw new Error(`supabase: ${error.message}`);\n return [];\n },\n\n async cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const current = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n if (current === null) return null;\n\n if (current.state === \"queued\") {\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({ state: \"canceled\", updated_at: iso(now) })\n .eq(\"id\", jobId)\n .eq(\"state\", \"queued\")\n .select(),\n );\n const canceled = rows[0];\n return toJob(canceled ?? current);\n }\n\n if (current.state === \"claimed\" || current.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself.\n const { error } = await db\n .from(\"byollm_job_cancels\")\n .upsert({ job_id: jobId, requested_at: iso(now) });\n if (error) throw new Error(`supabase: ${error.message}`);\n }\n return toJob(current);\n },\n\n async listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n const rows = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select().eq(\"lease_runner\", runnerId),\n );\n return rows.map(toJob);\n },\n\n async listCancelRequests(runnerId: string): Promise<LeaseRef[]> {\n // The lease comes back with the row — V1-3. A bare job id is ambiguous\n // to a daemon serving two sites that chose the same one, and the lease\n // is already on the joined row.\n const rows = unwrap<{ job_id: string; byollm_jobs: unknown }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner, lease_id)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as {\n job_id: string;\n byollm_jobs: { lease_id: string | null };\n }[];\n return rows\n .filter((row) => row.byollm_jobs.lease_id !== null)\n .map((row) => ({\n jobId: row.job_id,\n leaseId: row.byollm_jobs.lease_id ?? \"\",\n }));\n },\n\n // -- pairing and runners -------------------------------------------------\n\n /**\n * The push seam (byollm_009 §8.3), over Postgres Realtime.\n *\n * Native here, which is the point of requiring it of every adapter: the\n * backend that can push does, the one that cannot polls, and the\n * interface does not change again when streaming arrives.\n */\n subscribe(jobId: string, onChange: () => void): () => void {\n const channel = db\n .channel(`byollm_job_${jobId}`)\n .on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n onChange();\n },\n )\n .subscribe();\n\n let live = true;\n return () => {\n if (!live) return;\n live = false;\n // `removeChannel` is async and nothing awaits an unsubscribe, so the\n // rejection is routed rather than dropped — an unhandled one here\n // would end the process (see the Realtime delivery channel).\n void db.removeChannel(channel).catch(() => undefined);\n };\n },\n\n async createPairing(record: PairingRecord): Promise<void> {\n const { error } = await db.from(\"byollm_pairings\").insert({\n device_code_hash: record.deviceCodeHash,\n device: record.device,\n user_code: record.userCode,\n state: record.state,\n label: record.label,\n platform: record.platform,\n daemon_version: record.daemonVersion,\n capabilities: record.capabilities,\n expires_at: iso(record.expiresAt),\n });\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getPairingByDeviceCodeHash(\n hash: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"device_code_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async getPairingByUserCode(\n userCode: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", userCode)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n // Deliberately *not* the browser RPC: this path runs under the service\n // role with an `owner` the caller has already authenticated. Apps using\n // Supabase Auth in the browser should call `byollm_approve_pairing`\n // instead, which takes the owner from `auth.uid()` and cannot be told\n // who the user is.\n const pairing = unwrap<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", args.userCode)\n .single(),\n );\n\n if (Date.parse(pairing.expires_at) <= 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 = unwrap<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .insert({\n owner: args.owner,\n label: pairing.label,\n platform: pairing.platform,\n daemon_version: pairing.daemon_version,\n capabilities: pairing.capabilities,\n // Carried from the pairing, exactly as the SQL RPC does. There\n // are two approval paths — this service-role one and\n // `byollm_approve_pairing` for browser callers — and a field\n // added to one and not the other produces a runner that is\n // correct through one door and broken through the other.\n device: pairing.device,\n })\n .select()\n .single(),\n );\n\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({\n state: \"approved\",\n owner: args.owner,\n runner_id: runner.id,\n // Marks the approval collectable — cloud_008 §2.4. The column held\n // a bearer token; it now holds a marker, and the next migration\n // renames it. Written as a constant rather than left null because\n // `collected` reads `=== null`, and a schema change and a code\n // change landing in one step is how a rollback strands rows.\n collected_at: null,\n })\n .eq(\"device_code_hash\", pairing.device_code_hash);\n if (error) throw new Error(`supabase: ${error.message}`);\n\n return toRunner(runner);\n },\n\n async denyPairing(userCode: string, _now: number): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({ state: \"denied\" })\n .eq(\"user_code\", userCode);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async consumePairingToken(deviceCodeHash: string): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n // The database's clock, not this process's — the same rule the\n // relay's lease stamps follow: two writers measuring one fact against\n // two clocks is how a \"collected\" row looks uncollected.\n .update({ collected_at: \"now()\" })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunner(runnerId: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"id\", runnerId)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .update({\n capabilities: args.capabilities,\n daemon_version: args.daemonVersion,\n paused: args.paused,\n last_heartbeat_at: iso(args.now),\n })\n .eq(\"id\", args.runnerId)\n .select()\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async revokeRunner(runnerId: string, now: number): Promise<void> {\n // `is('revoked_at', null)` keeps revocation one-way: an already-revoked\n // runner keeps its first revocation time.\n const { error } = await db\n .from(\"byollm_runners\")\n .update({ revoked_at: iso(now) })\n .eq(\"id\", runnerId)\n .is(\"revoked_at\", null);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async listRunners(owner?: string): Promise<RunnerRecord[]> {\n const query = db.from(\"byollm_runners\").select();\n const rows = unwrap<RunnerRow[]>(\n owner === undefined ? await query : await query.eq(\"owner\", owner),\n );\n return rows.map(toRunner);\n },\n };\n}\n\nexport { supabaseRealtimeDelivery } from \"./realtime.js\";\n"],"mappings":"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","names":["iso"]}
1
+ {"version":3,"sources":["../../src/supabase/realtime.ts","../../src/supabase/index.ts"],"sourcesContent":["import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { DeliveredResult } from \"@byollm/protocol\";\nimport {\n NoRunnerAvailableError,\n ResultTimeoutError,\n type PollingDeliveryDeps,\n type ResultDelivery,\n type WaitOptions,\n labelFallback,\n} from \"../delivery.js\";\n\nconst DEFAULT_TIMEOUT_MS = 5 * 60_000;\n/**\n * How long a sustained no-runner signal must persist before it is believed.\n * A daemon restarting must not fail every job in flight.\n */\nconst NO_RUNNER_GRACE_MS = 10_000;\n\n/**\n * Realtime delivery: the app learns a job finished when Postgres says so.\n *\n * byollm_003 Rev 1 requires the server→app path be an explicit channel rather\n * than an implied in-request `await`. Polling is the portable default;\n * this is the one worth having when the app is already on Supabase, because\n * a result arrives in milliseconds instead of on the next poll tick.\n *\n * The no-runner watch still polls, deliberately: runner liveness is a\n * *derived* signal (nobody with matching capability has heartbeated lately),\n * and there is no row change to subscribe to for \"something stopped\n * happening\".\n */\nexport function supabaseRealtimeDelivery(\n client: SupabaseClient,\n): (deps: PollingDeliveryDeps) => ResultDelivery {\n return (deps) => new SupabaseRealtimeDelivery(client, deps);\n}\n\nclass SupabaseRealtimeDelivery implements ResultDelivery {\n readonly #client: SupabaseClient;\n readonly #deps: PollingDeliveryDeps;\n\n constructor(client: SupabaseClient, deps: PollingDeliveryDeps) {\n this.#client = client;\n this.#deps = deps;\n }\n\n async waitFor(\n jobId: string,\n options: WaitOptions = {},\n ): Promise<DeliveredResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n // Read first. The job may already be terminal, and subscribing to a\n // channel for an event that has already happened waits forever.\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) return current;\n\n // Declared before the subscription so the channel callback closes over a\n // `settled` that already exists. Every async path below routes its failure\n // here: a rejection that escapes this object becomes an unhandled\n // rejection, and an unhandled rejection ends the process.\n const settled = Promise.withResolvers<DeliveredResult>();\n this.#resolve = settled.resolve;\n\n const channel = this.#client.channel(`byollm_job_${jobId}`).on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n this.#check(jobId).catch(settled.reject);\n },\n );\n\n // `subscribe()` returns the channel, not a promise — awaiting it would be\n // a no-op that reads as if it waited for the subscription to be live.\n channel.subscribe();\n\n // A second read after subscribing closes the race where the job finished\n // between the first read and the subscription taking effect.\n this.#check(jobId).catch(settled.reject);\n\n const timer = setTimeout(() => {\n settled.reject(new ResultTimeoutError(jobId, timeoutMs));\n }, timeoutMs);\n\n const watcher = this.#watchAvailability(jobId, options, settled);\n const abort = (): void => {\n settled.reject(new Error(\"wait aborted\"));\n };\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n\n try {\n return await settled.promise;\n } finally {\n clearTimeout(timer);\n clearInterval(watcher);\n options.signal?.removeEventListener(\"abort\", abort);\n await this.#client.removeChannel(channel);\n }\n }\n\n #resolve: ((result: DeliveredResult) => void) | undefined;\n\n async #check(jobId: string): Promise<void> {\n const current = await this.#deps.read(jobId);\n if (current && isTerminal(current.state)) this.#resolve?.(current);\n }\n\n /** Poll runner liveness; there is no row event for \"nothing is happening\". */\n #watchAvailability(\n jobId: string,\n options: WaitOptions,\n settled: PromiseWithResolvers<DeliveredResult>,\n ): NodeJS.Timeout {\n let noRunnerSince: number | null = null;\n\n return setInterval(() => {\n // `.catch`, not `void`. Two things in here can reject — the store read\n // and the caller's own `onNoRunner` — and discarding either made a\n // transient store error, or an app whose fallback throws, terminate the\n // process. The caller is awaiting `result()`; that is where a failure\n // belongs, and it is what the polling channel already does by virtue of\n // running inside the awaited chain. A delivery adapter must not change\n // what a failure means.\n (async () => {\n const availability = await this.#deps.availability(jobId);\n if (availability.available || availability.blocked) {\n noRunnerSince = null;\n return;\n }\n noRunnerSince ??= Date.now();\n if (Date.now() - noRunnerSince < NO_RUNNER_GRACE_MS) return;\n\n const reason = availability.reason ?? \"no-runner-online\";\n const substitute = await options.onNoRunner?.(reason);\n if (substitute !== undefined) {\n // The same labelling the polling channel applies, from the same\n // function — {@link MUSTS.FALLBACK_LABELED} cannot depend on which\n // store an app happened to choose.\n settled.resolve(labelFallback(jobId, substitute));\n } else {\n settled.reject(new NoRunnerAvailableError(jobId, reason));\n }\n })().catch(settled.reject);\n }, 2_000);\n }\n}\n\nfunction isTerminal(state: string): boolean {\n return (\n state === \"ok\" ||\n state === \"error\" ||\n state === \"canceled\" ||\n state === \"expired\"\n );\n}\n","import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type {\n Capability,\n JobOutcome,\n JobState,\n PublicIdentity,\n} from \"@byollm/protocol\";\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 LeaseRef,\n ReleaseArgs,\n RenewArgs,\n RenewResult,\n TouchArgs,\n} from \"../store.js\";\n\n/**\n * `@byollm/server/supabase` — the first-party Supabase adapter.\n *\n * The piece the of-tomorrow-framework's runner module consumes verbatim.\n * Migrations ship in `supabase/migrations`; the atomic claim lives in a\n * `security definer` RPC using `FOR UPDATE SKIP LOCKED`, and the audience\n * rules are mirrored in SQL so the server refuses independently of the daemon\n * (byollm_003 §Server-side MUSTs).\n *\n * Requires the **service role** key: a runner authenticates with a bearer\n * token of its own, which is not a Supabase session, so the protocol handler\n * cannot run under RLS as the runner's user. RLS still governs everything the\n * *browser* does — the app-side policies in the migration are what protect\n * one user's jobs from another.\n *\n * @packageDocumentation\n */\n\n/** Row shape of `byollm_jobs`. */\ninterface JobRow {\n id: string;\n kind: string;\n envelope: unknown;\n size_class: \"small\" | \"medium\" | \"large\" | \"unbounded\";\n audience: \"private\" | \"team\" | \"public\";\n owner: string;\n audience_allow: string[] | null;\n depends_on: string[];\n state: JobState;\n lease_id: string | null;\n lease_runner: string | null;\n completed_by_lease_id: string | null;\n lease_expires_at: string | null;\n claimable_at: string | null;\n ttl_ms: number;\n deadline_at: string | null;\n refused_by: string[];\n attempts: number;\n outcome: JobOutcome | null;\n provenance: JobRecord[\"provenance\"];\n created_at: string;\n updated_at: string;\n}\n\n/** Row shape of `byollm_runners`. */\ninterface RunnerRow {\n id: string;\n owner: string;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n paused: boolean;\n revoked_at: string | null;\n last_heartbeat_at: string;\n created_at: string;\n}\n\n/** Row shape of `byollm_pairings`. */\ninterface PairingRow {\n device_code_hash: string;\n user_code: string;\n state: \"pending\" | \"approved\" | \"denied\";\n owner: string | null;\n runner_id: string | null;\n collected_at: string | null;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\n device: PublicIdentity;\n expires_at: string;\n created_at: string;\n}\n\nconst ms = (iso: string | null): number | null =>\n iso === null ? null : Date.parse(iso);\n\nconst iso = (epochMs: number): string => new Date(epochMs).toISOString();\n\nfunction toJob(row: JobRow): JobRecord {\n const leaseExpires = ms(row.lease_expires_at);\n return {\n id: row.id,\n kind: row.kind as JobRecord[\"kind\"],\n envelope: row.envelope as JobRecord[\"envelope\"],\n sizeClass: row.size_class,\n audience: row.audience,\n owner: row.owner,\n audienceAllow: row.audience_allow ?? undefined,\n dependsOn: row.depends_on,\n state: row.state,\n completedByLeaseId: row.completed_by_lease_id ?? null,\n lease:\n // Keyed on the lease id, not the runner. A relayed grant has no runner\n // row to point at (see AdoptArgs), and reading the lease as absent\n // because `lease_runner` is null would make an actively-held job look\n // claimable — the exact bug `adopt` exists to prevent.\n leaseExpires !== null && row.lease_id !== null\n ? {\n id: row.lease_id,\n runnerId: row.lease_runner ?? \"\",\n expiresAt: leaseExpires,\n }\n : null,\n createdAt: Date.parse(row.created_at),\n claimableAt: ms(row.claimable_at),\n ttlMs: row.ttl_ms,\n deadlineAt: ms(row.deadline_at),\n refusedBy: row.refused_by,\n attempts: row.attempts,\n outcome: row.outcome,\n provenance: row.provenance,\n updatedAt: Date.parse(row.updated_at),\n };\n}\n\n/**\n * A PostgREST filter matching exactly these (job, lease) pairs.\n *\n * Not two `IN` lists: `id IN (…) AND lease_id IN (…)` is a cross product, and\n * while UUID uniqueness makes a mismatch improbable, \"improbable\" is not the\n * property a lease check should rest on. This says what it means.\n */\nconst leasePairs = (leases: readonly LeaseRef[]): string =>\n leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(\",\");\n\nfunction toRunner(row: RunnerRow): RunnerRecord {\n return {\n id: row.id,\n owner: row.owner,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n paused: row.paused,\n revokedAt: ms(row.revoked_at),\n lastHeartbeatAt: Date.parse(row.last_heartbeat_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nfunction toPairing(row: PairingRow): PairingRecord {\n return {\n deviceCodeHash: row.device_code_hash,\n userCode: row.user_code,\n state: row.state,\n owner: row.owner,\n runnerId: row.runner_id,\n // Collected when it has a timestamp. This was `runner_token_once ===\n // null` — a nulled token standing in for a fact about delivery, which is\n // one field doing two jobs (cloud_008 §2.4a).\n collected: row.collected_at !== null,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\n device: row.device,\n expiresAt: Date.parse(row.expires_at),\n createdAt: Date.parse(row.created_at),\n };\n}\n\nexport interface SupabaseStoreOptions {\n /** A client built with the **service role** key. */\n readonly client: SupabaseClient;\n /** Default TTL for a job once claimable. */\n readonly defaultTtlMs?: number;\n}\n\n/** Build the Supabase-backed store. */\nexport function supabaseStore(options: SupabaseStoreOptions): ByollmStore {\n const db = options.client;\n const defaultTtlMs = options.defaultTtlMs ?? 15 * 60_000;\n\n /**\n * Narrow one PostgREST response, or throw with the Postgres message.\n *\n * `supabase-js` types rows as `any` unless the project has generated\n * database types, so the assertion has to live somewhere. Confining it to\n * these two helpers — against the row interfaces declared above — keeps\n * every call site typed and leaves exactly one place to review.\n */\n /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters --\n T appears only in the return type because these helpers *are* the cast.\n That is the point: one reviewable place where PostgREST's `any` becomes\n one of the row interfaces above. */\n function unwrap<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n if (result.data === null || result.data === undefined) {\n throw new Error(\"supabase: no data returned\");\n }\n return result.data as T;\n }\n\n /** Same, but a missing row is a legitimate answer rather than an error. */\n function unwrapMaybe<T>(result: {\n data: unknown;\n error: { message: string } | null;\n }): T | null {\n if (result.error) throw new Error(`supabase: ${result.error.message}`);\n return (result.data ?? null) as T | null;\n }\n /* eslint-enable @typescript-eslint/no-unnecessary-type-parameters */\n\n return {\n // -- jobs ---------------------------------------------------------------\n\n async create(input: StoredJobInput, now: number): Promise<JobRecord> {\n const dependsOn = [...(input.dependsOn ?? [])];\n\n // A job with dependencies starts blocked; the trigger sets\n // `claimable_at` when the last one reaches `ok`, which is where its TTL\n // clock starts.\n let claimableAt: string | null = iso(now);\n if (dependsOn.length > 0) {\n const deps = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select(\"id,state\").in(\"id\", dependsOn),\n ) as { id: string; state: JobState }[];\n const allDone =\n deps.length === dependsOn.length &&\n deps.every((dep) => dep.state === \"ok\");\n claimableAt = allDone ? iso(now) : null;\n }\n\n const row = {\n id: input.id,\n kind: input.kind,\n envelope: input.envelope,\n size_class: input.sizeClass,\n audience: input.audience ?? \"private\",\n owner: input.owner,\n audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,\n depends_on: dependsOn,\n claimable_at: claimableAt,\n ttl_ms: input.ttlMs ?? defaultTtlMs,\n deadline_at:\n input.deadlineAt === undefined ? null : iso(input.deadlineAt),\n };\n\n // Idempotent by caller-supplied id, matching the reference store: an\n // app's retry must not duplicate work.\n const inserted = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .upsert(row, { onConflict: \"id\", ignoreDuplicates: true })\n .select()\n .maybeSingle(),\n );\n\n if (inserted) return toJob(inserted);\n const existing = unwrap<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", input.id).single(),\n );\n return toJob(existing);\n },\n\n async get(jobId: string): Promise<JobRecord | null> {\n const row = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n return row === null ? null : toJob(row);\n },\n\n async claim(args: ClaimArgs): Promise<JobRecord[]> {\n // One RPC, one transaction, `FOR UPDATE SKIP LOCKED` inside\n // ({@link MUSTS.CLAIM_ATOMIC}).\n const rows = unwrap<JobRow[]>(\n await db.rpc(\"byollm_claim_jobs\", {\n p_runner_id: args.runnerId,\n p_capabilities: args.capabilities,\n p_max: args.max,\n p_lease_ms: args.leaseMs,\n }),\n );\n return rows.map(toJob);\n },\n\n async renewLeases(args: RenewArgs): Promise<RenewResult> {\n await db.rpc(\"byollm_expire_due\");\n if (args.leases.length === 0) return { renewed: [], lost: [] };\n\n const expiresAt = iso(args.now + args.leaseMs);\n const renewedRows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"running\",\n lease_expires_at: expiresAt,\n updated_at: iso(args.now),\n })\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases))\n .in(\"state\", [\"claimed\", \"running\"])\n .select(\"id\"),\n );\n\n const renewedIds = new Set(renewedRows.map((row) => row.id));\n return {\n renewed: renewedRows.map((row) => ({\n jobId: row.id,\n expiresAt: args.now + args.leaseMs,\n })),\n // Anything the runner thinks it holds but did not renew is gone,\n // named by the grant it asked about rather than by a bare id — V1-3.\n lost: args.leases.filter((lease) => !renewedIds.has(lease.jobId)),\n };\n },\n\n async adopt(args: AdoptArgs): Promise<JobRecord | null> {\n // The predicates are the guard, evaluated in the database rather than\n // read-then-written here: `state in (queued, claimed)` is what makes\n // adopting a terminal or expired job impossible under concurrency.\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state: \"claimed\",\n lease_id: args.leaseId,\n // Left null on purpose: `lease_runner` is a foreign key into\n // `byollm_runners`, and a relayed device has no row there. See\n // AdoptArgs — the site records the grant, not a machine it has\n // no relationship with.\n lease_runner: null,\n lease_expires_at: iso(args.expiresAt),\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"queued\", \"claimed\"])\n .select(),\n );\n const written = rows[0];\n return written === undefined ? null : toJob(written);\n },\n\n async complete(args: CompleteArgs): Promise<CompleteResult> {\n const state: JobState =\n args.outcome.outcome === \"ok\"\n ? \"ok\"\n : args.outcome.outcome === \"canceled\"\n ? \"canceled\"\n : \"error\";\n\n // The `in('state', ...)` predicate is the idempotency guard: a job that\n // already reached a terminal state matches nothing, so the first\n // outcome wins ({@link MUSTS.RESULT_IDEMPOTENT}). The `lease_runner`\n // predicate is {@link MUSTS.LEASE_HONORED}.\n // The `in('state', ...)` predicate is the idempotency guard. The\n // second predicate is LEASE_HONORED, and which column carries it\n // depends on the plane: a direct runner is named by id, a relayed\n // grant only by its lease. Built as a query rather than branched into\n // two, so there is one update statement and no chance of the two\n // drifting.\n let update = db\n .from(\"byollm_jobs\")\n .update({\n state,\n lease_runner: null,\n lease_expires_at: null,\n // Which grant recorded it, kept after the lease is dropped — §3.6.\n completed_by_lease_id:\n args.holder.by === \"lease\" ? args.holder.leaseId : null,\n outcome: args.outcome,\n provenance: args.provenance,\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .in(\"state\", [\"claimed\", \"running\"]);\n update =\n args.holder.by === \"runner\"\n ? update.eq(\"lease_runner\", args.holder.runnerId)\n : update.eq(\"lease_id\", args.holder.leaseId);\n const rows = unwrap<JobRow[]>(await update.select());\n\n const written = rows[0];\n if (written === undefined) {\n const current = unwrapMaybe<JobRow>(\n await db\n .from(\"byollm_jobs\")\n .select()\n .eq(\"id\", args.jobId)\n .maybeSingle(),\n );\n const job = current === null ? null : toJob(current);\n // Terminal before holder — cloud_008 §3.6. The update above matched\n // nothing for one of two reasons, and the caller is owed the\n // difference: the device that already recorded this job hears\n // \"duplicate\", and anybody else hears the same refusal they would get\n // for a job that is not terminal, so a job id is not a terminality\n // probe.\n //\n // Decided on the row that is there rather than by a second predicate,\n // because the update is the atomic part and this is only a diagnosis\n // of why it matched nothing.\n const duplicate =\n job !== null &&\n job.provenance?.runnerId === args.runnerId &&\n (job.state === \"ok\" ||\n job.state === \"error\" ||\n job.state === \"canceled\") &&\n args.holder.by === \"lease\" &&\n job.completedByLeaseId !== null &&\n job.completedByLeaseId === args.holder.leaseId;\n return duplicate\n ? { accepted: false, duplicate: true, job }\n : { accepted: false, job };\n }\n return { accepted: true, job: toJob(written) };\n },\n\n async release(args: ReleaseArgs): Promise<string[]> {\n if (args.leases.length === 0) return [];\n\n const held = unwrap<{ id: string; refused_by: string[] }[]>(\n await db\n .from(\"byollm_jobs\")\n .select(\"id,refused_by\")\n .eq(\"lease_runner\", args.runnerId)\n .or(leasePairs(args.leases)),\n );\n\n const released: string[] = [];\n for (const row of held) {\n const refusedBy =\n args.reason === \"refused\"\n ? [...new Set([...row.refused_by, args.runnerId])]\n : row.refused_by;\n\n const { error } = await db\n .from(\"byollm_jobs\")\n .update({\n state: \"queued\",\n lease_id: null,\n lease_runner: null,\n lease_expires_at: null,\n // Newly available again, so the TTL clock restarts.\n claimable_at: iso(args.now),\n refused_by: refusedBy,\n updated_at: iso(args.now),\n })\n .eq(\"id\", row.id)\n .eq(\"lease_runner\", args.runnerId);\n if (error) throw new Error(`supabase: ${error.message}`);\n released.push(row.id);\n }\n return released;\n },\n\n async expireDue(_now: number): Promise<JobRecord[]> {\n // The sweep is a single idempotent SQL function; it reports a count\n // rather than rows, and the caller only needs to know it ran.\n const { error } = await db.rpc(\"byollm_expire_due\");\n if (error) throw new Error(`supabase: ${error.message}`);\n return [];\n },\n\n async cancel(jobId: string, now: number): Promise<JobRecord | null> {\n const current = unwrapMaybe<JobRow>(\n await db.from(\"byollm_jobs\").select().eq(\"id\", jobId).maybeSingle(),\n );\n if (current === null) return null;\n\n if (current.state === \"queued\") {\n const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({ state: \"canceled\", updated_at: iso(now) })\n .eq(\"id\", jobId)\n .eq(\"state\", \"queued\")\n .select(),\n );\n const canceled = rows[0];\n return toJob(canceled ?? current);\n }\n\n if (current.state === \"claimed\" || current.state === \"running\") {\n // Held by a runner: the cancel travels on the next heartbeat and the\n // runner reports `canceled` itself.\n const { error } = await db\n .from(\"byollm_job_cancels\")\n .upsert({ job_id: jobId, requested_at: iso(now) });\n if (error) throw new Error(`supabase: ${error.message}`);\n }\n return toJob(current);\n },\n\n async listClaimedBy(runnerId: string): Promise<JobRecord[]> {\n const rows = unwrap<JobRow[]>(\n await db.from(\"byollm_jobs\").select().eq(\"lease_runner\", runnerId),\n );\n return rows.map(toJob);\n },\n\n async listCancelRequests(runnerId: string): Promise<LeaseRef[]> {\n // The lease comes back with the row — V1-3. A bare job id is ambiguous\n // to a daemon serving two sites that chose the same one, and the lease\n // is already on the joined row.\n const rows = unwrap<{ job_id: string; byollm_jobs: unknown }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner, lease_id)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as {\n job_id: string;\n byollm_jobs: { lease_id: string | null };\n }[];\n return rows\n .filter((row) => row.byollm_jobs.lease_id !== null)\n .map((row) => ({\n jobId: row.job_id,\n leaseId: row.byollm_jobs.lease_id ?? \"\",\n }));\n },\n\n // -- pairing and runners -------------------------------------------------\n\n /**\n * The push seam (byollm_009 §8.3), over Postgres Realtime.\n *\n * Native here, which is the point of requiring it of every adapter: the\n * backend that can push does, the one that cannot polls, and the\n * interface does not change again when streaming arrives.\n */\n subscribe(jobId: string, onChange: () => void): () => void {\n const channel = db\n .channel(`byollm_job_${jobId}`)\n .on(\n \"postgres_changes\",\n {\n event: \"UPDATE\",\n schema: \"public\",\n table: \"byollm_jobs\",\n filter: `id=eq.${jobId}`,\n },\n () => {\n onChange();\n },\n )\n .subscribe();\n\n let live = true;\n return () => {\n if (!live) return;\n live = false;\n // `removeChannel` is async and nothing awaits an unsubscribe, so the\n // rejection is routed rather than dropped — an unhandled one here\n // would end the process (see the Realtime delivery channel).\n void db.removeChannel(channel).catch(() => undefined);\n };\n },\n\n async createPairing(record: PairingRecord): Promise<void> {\n const { error } = await db.from(\"byollm_pairings\").insert({\n device_code_hash: record.deviceCodeHash,\n device: record.device,\n user_code: record.userCode,\n state: record.state,\n label: record.label,\n platform: record.platform,\n daemon_version: record.daemonVersion,\n capabilities: record.capabilities,\n expires_at: iso(record.expiresAt),\n });\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getPairingByDeviceCodeHash(\n hash: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"device_code_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async getPairingByUserCode(\n userCode: string,\n ): Promise<PairingRecord | null> {\n const row = unwrapMaybe<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", userCode)\n .maybeSingle(),\n );\n return row === null ? null : toPairing(row);\n },\n\n async approvePairing(args: ApproveArgs): Promise<RunnerRecord> {\n // Deliberately *not* the browser RPC: this path runs under the service\n // role with an `owner` the caller has already authenticated. Apps using\n // Supabase Auth in the browser should call `byollm_approve_pairing`\n // instead, which takes the owner from `auth.uid()` and cannot be told\n // who the user is.\n const pairing = unwrap<PairingRow>(\n await db\n .from(\"byollm_pairings\")\n .select()\n .eq(\"user_code\", args.userCode)\n .single(),\n );\n\n if (Date.parse(pairing.expires_at) <= 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 = unwrap<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .insert({\n owner: args.owner,\n label: pairing.label,\n platform: pairing.platform,\n daemon_version: pairing.daemon_version,\n capabilities: pairing.capabilities,\n // Carried from the pairing, exactly as the SQL RPC does. There\n // are two approval paths — this service-role one and\n // `byollm_approve_pairing` for browser callers — and a field\n // added to one and not the other produces a runner that is\n // correct through one door and broken through the other.\n device: pairing.device,\n })\n .select()\n .single(),\n );\n\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({\n state: \"approved\",\n owner: args.owner,\n runner_id: runner.id,\n // Marks the approval collectable — cloud_008 §2.4. The column held\n // a bearer token; it now holds a marker, and the next migration\n // renames it. Written as a constant rather than left null because\n // `collected` reads `=== null`, and a schema change and a code\n // change landing in one step is how a rollback strands rows.\n collected_at: null,\n })\n .eq(\"device_code_hash\", pairing.device_code_hash);\n if (error) throw new Error(`supabase: ${error.message}`);\n\n return toRunner(runner);\n },\n\n async denyPairing(userCode: string, _now: number): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n .update({ state: \"denied\" })\n .eq(\"user_code\", userCode);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async consumePairingToken(deviceCodeHash: string): Promise<void> {\n const { error } = await db\n .from(\"byollm_pairings\")\n // The database's clock, not this process's — the same rule the\n // relay's lease stamps follow: two writers measuring one fact against\n // two clocks is how a \"collected\" row looks uncollected.\n .update({ collected_at: \"now()\" })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunner(runnerId: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"id\", runnerId)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async touchRunner(args: TouchArgs): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .update({\n capabilities: args.capabilities,\n daemon_version: args.daemonVersion,\n paused: args.paused,\n last_heartbeat_at: iso(args.now),\n })\n .eq(\"id\", args.runnerId)\n .select()\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\n },\n\n async revokeRunner(runnerId: string, now: number): Promise<void> {\n // `is('revoked_at', null)` keeps revocation one-way: an already-revoked\n // runner keeps its first revocation time.\n const { error } = await db\n .from(\"byollm_runners\")\n .update({ revoked_at: iso(now) })\n .eq(\"id\", runnerId)\n .is(\"revoked_at\", null);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async listRunners(owner?: string): Promise<RunnerRecord[]> {\n const query = db.from(\"byollm_runners\").select();\n const rows = unwrap<RunnerRow[]>(\n owner === undefined ? await query : await query.eq(\"owner\", owner),\n );\n return rows.map(toRunner);\n },\n };\n}\n\nexport { supabaseRealtimeDelivery } from \"./realtime.js\";\n"],"mappings":"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","names":["iso"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byollm/server",
3
- "version": "0.1.0-alpha.43",
3
+ "version": "0.1.0-alpha.45",
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.43"
33
+ "@byollm/protocol": "0.1.0-alpha.45"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@supabase/supabase-js": "^2.58.0"
@@ -0,0 +1,109 @@
1
+ -- One sharing vocabulary, in the database too — byollm_016.
2
+ --
3
+ -- `private | team | public` replaced `self | named | public` everywhere else
4
+ -- in this release. The enums did not move with it, and the gap was invisible
5
+ -- to `pnpm run verify`: the TypeScript compiled, the unit suites passed
6
+ -- against the in-memory store, and only the freeze gate — which runs the same
7
+ -- code against real Postgres — refused the insert with `invalid input value
8
+ -- for enum byollm_audience: "private"`. A schema is not typechecked by the
9
+ -- language that talks to it.
10
+ --
11
+ -- Values are renamed rather than the enums recreated. `alter type ... rename
12
+ -- value` keeps every existing row's identity: the label changes, the stored
13
+ -- value does not move, and nothing has to be rewritten or backfilled. A
14
+ -- drop-and-recreate would need every column that uses the type dropped first,
15
+ -- which is how a rename turns into data loss.
16
+ --
17
+ -- Landing in one step, with the code that reads it, for the reason
18
+ -- `20260821000000_rename_collected` gives: transitional shapes exist to
19
+ -- protect a party who has not agreed to change, and pre-1.0 that party is us.
20
+
21
+ alter type byollm_audience rename value 'self' to 'private';
22
+ alter type byollm_audience rename value 'named' to 'team';
23
+
24
+ alter type byollm_offer_scope rename value 'self' to 'private';
25
+ alter type byollm_offer_scope rename value 'named' to 'team';
26
+
27
+ -- The column default is stored as a reference to the value, not as its text,
28
+ -- so the rename already carried it. Restated anyway: a default that reads
29
+ -- `'self'` in a dump nobody re-ran is exactly the kind of thing that gets
30
+ -- copied into the next schema.
31
+ alter table byollm_jobs
32
+ alter column audience set default 'private';
33
+
34
+ comment on column byollm_jobs.audience is
35
+ 'Who this job may run for: private (the owner alone), team (the owner and '
36
+ 'the named allowlist), public (anyone). byollm_016; these were self/named.';
37
+
38
+ -- Recreated whole rather than patched — two functions with one name is how a
39
+ -- caller ends up invoking the one nobody maintains. The logic is unchanged;
40
+ -- only the vocabulary moves.
41
+ create or replace function byollm_audience_admits(
42
+ p_job byollm_jobs,
43
+ p_runner_id uuid,
44
+ p_runner_owner uuid,
45
+ p_capabilities jsonb
46
+ )
47
+ returns boolean
48
+ language plpgsql
49
+ stable
50
+ security definer
51
+ set search_path = public
52
+ as $$
53
+ declare
54
+ v_cap jsonb;
55
+ v_scope text;
56
+ v_backend text;
57
+ v_same_owner boolean := (p_job.owner = p_runner_owner);
58
+ begin
59
+ select value into v_cap
60
+ from jsonb_array_elements(p_capabilities)
61
+ where value ->> 'kind' = p_job.kind
62
+ limit 1;
63
+
64
+ if v_cap is null then
65
+ return false;
66
+ end if;
67
+
68
+ v_scope := v_cap ->> 'offerScope';
69
+ v_backend := v_cap ->> 'backendId';
70
+
71
+ -- Side 1: does the job's audience admit this runner's owner?
72
+ if p_job.audience = 'private' and not v_same_owner then
73
+ return false;
74
+ end if;
75
+ if p_job.audience = 'team'
76
+ and not v_same_owner
77
+ and p_job.audience_allow is not null
78
+ and not (p_runner_owner = any (p_job.audience_allow)) then
79
+ return false;
80
+ end if;
81
+
82
+ -- A daemon always runs its own owner's work.
83
+ if v_same_owner then
84
+ return true;
85
+ end if;
86
+
87
+ -- The subscription self-lock. Applied here regardless of the scope the
88
+ -- daemon advertised: a widened scope on a subscription backend is refused,
89
+ -- not obeyed (SUBSCRIPTION_SELF_LOCK).
90
+ if v_backend = 'claude-cli' then
91
+ return false;
92
+ end if;
93
+
94
+ -- Side 2: does the backend's offer scope admit the job's owner?
95
+ if v_scope = 'private' then
96
+ return false;
97
+ end if;
98
+ if v_scope = 'team' then
99
+ -- The server cannot verify a remote daemon's local allowlist and must not
100
+ -- pretend to. It offers the job; the daemon refuses if its own list says
101
+ -- no, and the refusal is remembered in refused_by. In this build that
102
+ -- allowlist is the *only* enforcement — there is no central roster yet.
103
+ return true;
104
+ end if;
105
+ return v_scope = 'public';
106
+ end;
107
+ $$;
108
+
109
+ revoke all on function byollm_audience_admits(byollm_jobs, uuid, uuid, jsonb) from public;