@byollm/server 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -20,6 +20,8 @@ var SupabaseRealtimeDelivery = class {
20
20
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
21
21
  const current = await this.#deps.read(jobId);
22
22
  if (current && isTerminal(current.state)) return current;
23
+ const settled = Promise.withResolvers();
24
+ this.#resolve = settled.resolve;
23
25
  const channel = this.#client.channel(`byollm_job_${jobId}`).on(
24
26
  "postgres_changes",
25
27
  {
@@ -29,13 +31,11 @@ var SupabaseRealtimeDelivery = class {
29
31
  filter: `id=eq.${jobId}`
30
32
  },
31
33
  () => {
32
- void this.#check(jobId);
34
+ this.#check(jobId).catch(settled.reject);
33
35
  }
34
36
  );
35
- const settled = Promise.withResolvers();
36
- this.#resolve = settled.resolve;
37
37
  channel.subscribe();
38
- void this.#check(jobId);
38
+ this.#check(jobId).catch(settled.reject);
39
39
  const timer = setTimeout(() => {
40
40
  settled.reject(new ResultTimeoutError(jobId, timeoutMs));
41
41
  }, timeoutMs);
@@ -62,7 +62,7 @@ var SupabaseRealtimeDelivery = class {
62
62
  #watchAvailability(jobId, options, settled) {
63
63
  let noRunnerSince = null;
64
64
  return setInterval(() => {
65
- void (async () => {
65
+ (async () => {
66
66
  const availability = await this.#deps.availability(jobId);
67
67
  if (availability.available || availability.blocked) {
68
68
  noRunnerSince = null;
@@ -77,7 +77,7 @@ var SupabaseRealtimeDelivery = class {
77
77
  } else {
78
78
  settled.reject(new NoRunnerAvailableError(jobId, reason));
79
79
  }
80
- })();
80
+ })().catch(settled.reject);
81
81
  }, 2e3);
82
82
  }
83
83
  };
@@ -93,13 +93,24 @@ function toJob(row) {
93
93
  return {
94
94
  id: row.id,
95
95
  kind: row.kind,
96
- payload: row.payload,
96
+ envelope: row.envelope,
97
+ sizeClass: row.size_class,
97
98
  audience: row.audience,
98
99
  owner: row.owner,
99
100
  audienceAllow: row.audience_allow ?? void 0,
100
101
  dependsOn: row.depends_on,
101
102
  state: row.state,
102
- lease: row.lease_runner !== null && leaseExpires !== null ? { runnerId: row.lease_runner, expiresAt: leaseExpires } : null,
103
+ lease: (
104
+ // Keyed on the lease id, not the runner. A relayed grant has no runner
105
+ // row to point at (see AdoptArgs), and reading the lease as absent
106
+ // because `lease_runner` is null would make an actively-held job look
107
+ // claimable — the exact bug `adopt` exists to prevent.
108
+ leaseExpires !== null && row.lease_id !== null ? {
109
+ id: row.lease_id,
110
+ runnerId: row.lease_runner ?? "",
111
+ expiresAt: leaseExpires
112
+ } : null
113
+ ),
103
114
  createdAt: Date.parse(row.created_at),
104
115
  claimableAt: ms(row.claimable_at),
105
116
  ttlMs: row.ttl_ms,
@@ -111,6 +122,7 @@ function toJob(row) {
111
122
  updatedAt: Date.parse(row.updated_at)
112
123
  };
113
124
  }
125
+ var leasePairs = (leases) => leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(",");
114
126
  function toRunner(row) {
115
127
  return {
116
128
  id: row.id,
@@ -120,6 +132,7 @@ function toRunner(row) {
120
132
  platform: row.platform,
121
133
  daemonVersion: row.daemon_version,
122
134
  capabilities: row.capabilities,
135
+ device: row.device,
123
136
  paused: row.paused,
124
137
  revokedAt: ms(row.revoked_at),
125
138
  lastHeartbeatAt: Date.parse(row.last_heartbeat_at),
@@ -138,6 +151,7 @@ function toPairing(row) {
138
151
  platform: row.platform,
139
152
  daemonVersion: row.daemon_version,
140
153
  capabilities: row.capabilities,
154
+ device: row.device,
141
155
  expiresAt: Date.parse(row.expires_at),
142
156
  createdAt: Date.parse(row.created_at)
143
157
  };
@@ -169,9 +183,10 @@ function supabaseStore(options) {
169
183
  claimableAt = allDone ? iso(now) : null;
170
184
  }
171
185
  const row = {
172
- ...input.id === void 0 ? {} : { id: input.id },
186
+ id: input.id,
173
187
  kind: input.kind,
174
- payload: input.payload,
188
+ envelope: input.envelope,
189
+ size_class: input.sizeClass,
175
190
  audience: input.audience ?? "self",
176
191
  owner: input.owner,
177
192
  audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,
@@ -185,7 +200,7 @@ function supabaseStore(options) {
185
200
  );
186
201
  if (inserted) return toJob(inserted);
187
202
  const existing = unwrap(
188
- await db.from("byollm_jobs").select().eq("id", input.id ?? "").single()
203
+ await db.from("byollm_jobs").select().eq("id", input.id).single()
189
204
  );
190
205
  return toJob(existing);
191
206
  },
@@ -208,14 +223,14 @@ function supabaseStore(options) {
208
223
  },
209
224
  async renewLeases(args) {
210
225
  await db.rpc("byollm_expire_due");
211
- if (args.jobIds.length === 0) return { renewed: [], lost: [] };
226
+ if (args.leases.length === 0) return { renewed: [], lost: [] };
212
227
  const expiresAt = iso(args.now + args.leaseMs);
213
228
  const renewedRows = unwrap(
214
229
  await db.from("byollm_jobs").update({
215
230
  state: "running",
216
231
  lease_expires_at: expiresAt,
217
232
  updated_at: iso(args.now)
218
- }).eq("lease_runner", args.runnerId).in("id", [...args.jobIds]).in("state", ["claimed", "running"]).select("id")
233
+ }).eq("lease_runner", args.runnerId).or(leasePairs(args.leases)).in("state", ["claimed", "running"]).select("id")
219
234
  );
220
235
  const renewedIds = new Set(renewedRows.map((row) => row.id));
221
236
  return {
@@ -224,22 +239,39 @@ function supabaseStore(options) {
224
239
  expiresAt: args.now + args.leaseMs
225
240
  })),
226
241
  // Anything the runner thinks it holds but did not renew is gone.
227
- lost: args.jobIds.filter((id) => !renewedIds.has(id))
242
+ lost: args.leases.map((l) => l.jobId).filter((id) => !renewedIds.has(id))
228
243
  };
229
244
  },
230
- async complete(args) {
231
- const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
245
+ async adopt(args) {
232
246
  const rows = unwrap(
233
247
  await db.from("byollm_jobs").update({
234
- state,
248
+ state: "claimed",
249
+ lease_id: args.leaseId,
250
+ // Left null on purpose: `lease_runner` is a foreign key into
251
+ // `byollm_runners`, and a relayed device has no row there. See
252
+ // AdoptArgs — the site records the grant, not a machine it has
253
+ // no relationship with.
235
254
  lease_runner: null,
236
- lease_expires_at: null,
237
- outcome: args.outcome,
238
- provenance: args.provenance,
255
+ lease_expires_at: iso(args.expiresAt),
239
256
  updated_at: iso(args.now)
240
- }).eq("id", args.jobId).eq("lease_runner", args.runnerId).in("state", ["claimed", "running"]).select()
257
+ }).eq("id", args.jobId).in("state", ["queued", "claimed"]).select()
241
258
  );
242
259
  const written = rows[0];
260
+ return written === void 0 ? null : toJob(written);
261
+ },
262
+ async complete(args) {
263
+ const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
264
+ let update = db.from("byollm_jobs").update({
265
+ state,
266
+ lease_runner: null,
267
+ lease_expires_at: null,
268
+ outcome: args.outcome,
269
+ provenance: args.provenance,
270
+ updated_at: iso(args.now)
271
+ }).eq("id", args.jobId).in("state", ["claimed", "running"]);
272
+ update = args.holder.by === "runner" ? update.eq("lease_runner", args.holder.runnerId) : update.eq("lease_id", args.holder.leaseId);
273
+ const rows = unwrap(await update.select());
274
+ const written = rows[0];
243
275
  if (written === void 0) {
244
276
  const current = unwrapMaybe(
245
277
  await db.from("byollm_jobs").select().eq("id", args.jobId).maybeSingle()
@@ -252,15 +284,16 @@ function supabaseStore(options) {
252
284
  return { accepted: true, job: toJob(written) };
253
285
  },
254
286
  async release(args) {
255
- if (args.jobIds.length === 0) return [];
287
+ if (args.leases.length === 0) return [];
256
288
  const held = unwrap(
257
- await db.from("byollm_jobs").select("id,refused_by").eq("lease_runner", args.runnerId).in("id", [...args.jobIds])
289
+ await db.from("byollm_jobs").select("id,refused_by").eq("lease_runner", args.runnerId).or(leasePairs(args.leases))
258
290
  );
259
291
  const released = [];
260
292
  for (const row of held) {
261
293
  const refusedBy = args.reason === "refused" ? [.../* @__PURE__ */ new Set([...row.refused_by, args.runnerId])] : row.refused_by;
262
294
  const { error } = await db.from("byollm_jobs").update({
263
295
  state: "queued",
296
+ lease_id: null,
264
297
  lease_runner: null,
265
298
  lease_expires_at: null,
266
299
  // Newly available again, so the TTL clock restarts.
@@ -309,9 +342,37 @@ function supabaseStore(options) {
309
342
  return rows.map((row) => row.job_id);
310
343
  },
311
344
  // -- pairing and runners -------------------------------------------------
345
+ /**
346
+ * The push seam (byollm_009 §8.3), over Postgres Realtime.
347
+ *
348
+ * Native here, which is the point of requiring it of every adapter: the
349
+ * backend that can push does, the one that cannot polls, and the
350
+ * interface does not change again when streaming arrives.
351
+ */
352
+ subscribe(jobId, onChange) {
353
+ const channel = db.channel(`byollm_job_${jobId}`).on(
354
+ "postgres_changes",
355
+ {
356
+ event: "UPDATE",
357
+ schema: "public",
358
+ table: "byollm_jobs",
359
+ filter: `id=eq.${jobId}`
360
+ },
361
+ () => {
362
+ onChange();
363
+ }
364
+ ).subscribe();
365
+ let live = true;
366
+ return () => {
367
+ if (!live) return;
368
+ live = false;
369
+ void db.removeChannel(channel).catch(() => void 0);
370
+ };
371
+ },
312
372
  async createPairing(record) {
313
373
  const { error } = await db.from("byollm_pairings").insert({
314
374
  device_code_hash: record.deviceCodeHash,
375
+ device: record.device,
315
376
  user_code: record.userCode,
316
377
  state: record.state,
317
378
  label: record.label,
@@ -351,7 +412,13 @@ function supabaseStore(options) {
351
412
  label: pairing.label,
352
413
  platform: pairing.platform,
353
414
  daemon_version: pairing.daemon_version,
354
- capabilities: pairing.capabilities
415
+ capabilities: pairing.capabilities,
416
+ // Carried from the pairing, exactly as the SQL RPC does. There
417
+ // are two approval paths — this service-role one and
418
+ // `byollm_approve_pairing` for browser callers — and a field
419
+ // added to one and not the other produces a runner that is
420
+ // correct through one door and broken through the other.
421
+ device: pairing.device
355
422
  }).select().single()
356
423
  );
357
424
  const { error } = await db.from("byollm_pairings").update({
@@ -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} 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 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 void this.#check(jobId);\n },\n );\n\n const settled = Promise.withResolvers<DeliveredResult>();\n this.#resolve = settled.resolve;\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 void this.#check(jobId);\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 void (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) {\n settled.resolve(substitute);\n } else {\n settled.reject(new NoRunnerAvailableError(jobId, reason));\n }\n })();\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 { Capability, JobOutcome, JobState } from \"@byollm/protocol\";\nimport type {\n EnqueueInput,\n JobRecord,\n PairingRecord,\n RunnerRecord,\n} from \"../records.js\";\nimport type {\n ApproveArgs,\n ByollmStore,\n ClaimArgs,\n CompleteArgs,\n CompleteResult,\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 payload: unknown;\n audience: \"self\" | \"named\" | \"public\";\n owner: string;\n audience_allow: string[] | null;\n depends_on: string[];\n state: JobState;\n lease_runner: 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 token_hash: string;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\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 runner_token_once: string | null;\n label: string;\n platform: \"darwin\" | \"linux\" | \"win32\";\n daemon_version: string;\n capabilities: Capability[];\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 payload: row.payload as JobRecord[\"payload\"],\n audience: row.audience,\n owner: row.owner,\n audienceAllow: row.audience_allow ?? undefined,\n dependsOn: row.depends_on,\n state: row.state,\n lease:\n row.lease_runner !== null && leaseExpires !== null\n ? { runnerId: row.lease_runner, expiresAt: leaseExpires }\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\nfunction toRunner(row: RunnerRow): RunnerRecord {\n return {\n id: row.id,\n owner: row.owner,\n tokenHash: row.token_hash,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\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 runnerTokenOnce: row.runner_token_once,\n label: row.label,\n platform: row.platform,\n daemonVersion: row.daemon_version,\n capabilities: row.capabilities,\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: EnqueueInput, 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 ...(input.id === undefined ? {} : { id: input.id }),\n kind: input.kind,\n payload: input.payload,\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\n .from(\"byollm_jobs\")\n .select()\n .eq(\"id\", input.id ?? \"\")\n .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.jobIds.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 .in(\"id\", [...args.jobIds])\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 lost: args.jobIds.filter((id) => !renewedIds.has(id)),\n };\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 const rows = unwrap<JobRow[]>(\n await db\n .from(\"byollm_jobs\")\n .update({\n state,\n lease_runner: null,\n lease_expires_at: null,\n outcome: args.outcome,\n provenance: args.provenance,\n updated_at: iso(args.now),\n })\n .eq(\"id\", args.jobId)\n .eq(\"lease_runner\", args.runnerId)\n .in(\"state\", [\"claimed\", \"running\"])\n .select(),\n );\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 return {\n accepted: false,\n job: current === null ? null : toJob(current),\n };\n }\n return { accepted: true, job: toJob(written) };\n },\n\n async release(args: ReleaseArgs): Promise<string[]> {\n if (args.jobIds.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 .in(\"id\", [...args.jobIds]),\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_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<string[]> {\n const rows = unwrap<{ job_id: string }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as { job_id: string }[];\n return rows.map((row) => row.job_id);\n },\n\n // -- pairing and runners -------------------------------------------------\n\n async createPairing(record: PairingRecord): Promise<void> {\n const { error } = await db.from(\"byollm_pairings\").insert({\n device_code_hash: record.deviceCodeHash,\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 token_hash: args.tokenHash,\n label: pairing.label,\n platform: pairing.platform,\n daemon_version: pairing.daemon_version,\n capabilities: pairing.capabilities,\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 runner_token_once: args.runnerToken,\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 .update({ runner_token_once: null })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"token_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\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} 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) {\n settled.resolve(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 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 token_hash: 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 runner_token_once: 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 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 tokenHash: row.token_hash,\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 runnerTokenOnce: row.runner_token_once,\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 lost: args.leases\n .map((l) => l.jobId)\n .filter((id) => !renewedIds.has(id)),\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 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 return {\n accepted: false,\n job: current === null ? null : toJob(current),\n };\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<string[]> {\n const rows = unwrap<{ job_id: string }[]>(\n await db\n .from(\"byollm_job_cancels\")\n .select(\"job_id, byollm_jobs!inner(lease_runner)\")\n .eq(\"byollm_jobs.lease_runner\", runnerId),\n ) as { job_id: string }[];\n return rows.map((row) => row.job_id);\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 token_hash: args.tokenHash,\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 runner_token_once: args.runnerToken,\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 .update({ runner_token_once: null })\n .eq(\"device_code_hash\", deviceCodeHash);\n if (error) throw new Error(`supabase: ${error.message}`);\n },\n\n async getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null> {\n const row = unwrapMaybe<RunnerRow>(\n await db\n .from(\"byollm_runners\")\n .select()\n .eq(\"token_hash\", hash)\n .maybeSingle(),\n );\n return row === null ? null : toRunner(row);\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.3",
3
+ "version": "0.1.0-alpha.5",
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",
@@ -23,13 +23,14 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "supabase/migrations",
26
- "README.md"
26
+ "README.md",
27
+ "bin"
27
28
  ],
28
29
  "engines": {
29
30
  "node": ">=22.14"
30
31
  },
31
32
  "dependencies": {
32
- "@byollm/protocol": "0.1.0-alpha.3"
33
+ "@byollm/protocol": "0.1.0-alpha.5"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@supabase/supabase-js": "^2.58.0"
@@ -53,6 +54,9 @@
53
54
  "bugs": {
54
55
  "url": "https://github.com/oftomorrowinc/byollm/issues"
55
56
  },
57
+ "bin": {
58
+ "keygen": "./bin/keygen.mjs"
59
+ },
56
60
  "scripts": {
57
61
  "build": "tsup",
58
62
  "clean": "rm -rf dist .tsbuild"
@@ -33,6 +33,9 @@ create table byollm_runners (
33
33
  platform text not null check (platform in ('darwin', 'linux', 'win32')),
34
34
  daemon_version text not null,
35
35
  capabilities jsonb not null default '[]'::jsonb,
36
+ -- byollm_009 §5: the device's pinned public identity. What a later
37
+ -- signature verifies against — a runner id names a machine, this proves it.
38
+ device jsonb not null,
36
39
  paused boolean not null default false,
37
40
  -- Set once. A revoked runner never un-revokes.
38
41
  revoked_at timestamptz,
@@ -63,6 +66,9 @@ create table byollm_pairings (
63
66
  platform text not null,
64
67
  daemon_version text not null,
65
68
  capabilities jsonb not null default '[]'::jsonb,
69
+ -- Presented at pair start, so the user approves a *machine* rather than a
70
+ -- code any machine could redeem. Copied onto the runner at approval.
71
+ device jsonb not null,
66
72
  expires_at timestamptz not null,
67
73
  created_at timestamptz not null default now()
68
74
  );
@@ -77,7 +83,13 @@ create index byollm_pairings_expiry_idx on byollm_pairings (expires_at);
77
83
  create table byollm_jobs (
78
84
  id uuid primary key default gen_random_uuid(),
79
85
  kind text not null,
80
- payload jsonb not null,
86
+ -- The work, sealed to the site's own key (byollm_009 §10). The database
87
+ -- holds ciphertext; the application opens it, because the application is
88
+ -- the endpoint. Backups, replicas and anyone with read access see this.
89
+ envelope jsonb not null,
90
+ -- Bucketed at enqueue, where the plaintext was.
91
+ size_class text not null default 'small'
92
+ check (size_class in ('small','medium','large','unbounded')),
81
93
  audience byollm_audience not null default 'self',
82
94
  owner uuid not null references auth.users (id) on delete cascade,
83
95
  -- Server-side restriction on which runner owners may take a `named` job.
@@ -85,6 +97,10 @@ create table byollm_jobs (
85
97
  audience_allow uuid[] ,
86
98
  depends_on uuid[] not null default '{}',
87
99
  state byollm_job_state not null default 'queued',
100
+ -- Identifies this *grant*, not just its holder. A runner can claim, release
101
+ -- and re-claim the same job; without an id per grant a replayed release
102
+ -- lands on a later lease and returns a job to the queue mid-execution.
103
+ lease_id uuid,
88
104
  lease_runner uuid references byollm_runners (id) on delete set null,
89
105
  lease_expires_at timestamptz,
90
106
  -- When the job became claimable. THE TTL CLOCK STARTS HERE, not at
@@ -182,6 +198,7 @@ begin
182
198
  -- lease reclaim exists to provide.
183
199
  update byollm_jobs
184
200
  set state = 'queued',
201
+ lease_id = null,
185
202
  lease_runner = null,
186
203
  lease_expires_at = null,
187
204
  claimable_at = now(),
@@ -196,6 +213,7 @@ begin
196
213
  -- past its absolute deadline.
197
214
  update byollm_jobs
198
215
  set state = 'expired',
216
+ lease_id = null,
199
217
  lease_runner = null,
200
218
  lease_expires_at = null,
201
219
  updated_at = now()
@@ -273,6 +291,7 @@ begin
273
291
  )
274
292
  update byollm_jobs j
275
293
  set state = 'claimed',
294
+ lease_id = gen_random_uuid(),
276
295
  lease_runner = p_runner_id,
277
296
  lease_expires_at = now() + (p_lease_ms || ' milliseconds')::interval,
278
297
  attempts = j.attempts + 1,
@@ -437,9 +456,9 @@ begin
437
456
  end if;
438
457
 
439
458
  insert into byollm_runners (owner, token_hash, label, platform,
440
- daemon_version, capabilities)
459
+ daemon_version, capabilities, device)
441
460
  values (v_owner, p_token_hash, v_pairing.label, v_pairing.platform,
442
- v_pairing.daemon_version, v_pairing.capabilities)
461
+ v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
443
462
  returning * into v_runner;
444
463
 
445
464
  update byollm_pairings
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ids.ts","../src/handlers.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner bearer token. */\nexport function generateRunnerToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n return `job_${randomUUID()}`;\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n","import {\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport type { RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param bearer - the `Authorization: Bearer` value, if any\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n bearer: string | undefined,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(bearer, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on, so a revoked runner\n // must reach the handler and be told `revoked: true` rather than be\n // bounced with a 403 it would treat as a transport problem\n // ({@link MUSTS.REVOCATION_HONORED}).\n return this.#authed(\n bearer,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n { allowRevoked: true },\n );\n case \"result\":\n return this.#authed(\n bearer,\n body,\n ResultRequest,\n this.#result.bind(this),\n );\n case \"release\":\n return this.#authed(\n bearer,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: resolve the bearer\n * token to a runner, reject a revoked one, and parse the body.\n *\n * The token→runner lookup happens before schema validation so a stranger\n * probing the endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n bearer: string | undefined,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n if (bearer === undefined || bearer.length === 0) {\n return fail(\"unauthorized\", \"a runner token is required\");\n }\n const runner = await this.#store.getRunnerByTokenHash(hashSecret(bearer));\n if (!runner) {\n return fail(\"unauthorized\", \"this runner token is not recognised\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n await this.#store.createPairing({\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n runnerTokenOnce: null,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n pairing.runnerTokenOnce !== null &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerToken: pairing.runnerTokenOnce,\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n payload: job.payload,\n audience: job.audience,\n owner: job.owner,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: [...job.audienceAllow] }),\n lease: job.lease ?? {\n runnerId: runner.id,\n expiresAt: now + this.#leaseMs,\n },\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n const revoked = runner.revokedAt !== null;\n\n if (revoked) {\n // Nothing is renewed for a revoked runner: every job it holds is\n // reported lost so it abandons the queue rather than finishing it.\n const held = await this.#store.listClaimedBy(runner.id);\n const response: HeartbeatResponse = {\n revoked: true,\n cancel: [],\n leases: [],\n lost: held.map((job) => job.id),\n serverTime: now,\n };\n return ok(response);\n }\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n const { renewed, lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n jobIds: request.activeJobIds,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n revoked: false,\n cancel,\n leases: renewed.map((r) => ({ jobId: r.jobId, expiresAt: r.expiresAt })),\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.RESULT_PROVENANCE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n backendClass: request.backendClass,\n model: request.model,\n });\n\n const { accepted, job: updated } = await this.#store.complete({\n jobId: request.jobId,\n runnerId: runner.id,\n outcome: request.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n jobIds: request.jobIds,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n","import { ENDPOINTS, PROTOCOL_PREFIX, type Endpoint } from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * A payload is capped at 4 MB of text by the protocol; this leaves room for\n * JSON overhead and a batch of results, and refuses anything wilder at the\n * door rather than after parsing it.\n */\nconst MAX_BODY_BYTES = 8 * 1024 * 1024;\n\n/** Pull the endpoint name out of a URL path, or null if it isn't ours. */\nexport function routeEndpoint(pathname: string): Endpoint | null {\n const index = pathname.lastIndexOf(\"/\");\n const last = index === -1 ? pathname : pathname.slice(index + 1);\n return (ENDPOINTS as readonly string[]).includes(last)\n ? (last as Endpoint)\n : null;\n}\n\n/** Read the bearer token from an `Authorization` header. */\nexport function bearerFrom(header: string | null): string | undefined {\n if (!header) return undefined;\n const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());\n return match?.[1];\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig,\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${PROTOCOL_PREFIX} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n\n let body: unknown;\n try {\n const text = await request.text();\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n const result = await handlers.handle(\n endpoint,\n body,\n bearerFrom(request.headers.get(\"authorization\")),\n );\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["response"]}