@byollm/server 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +151 -12
- package/bin/keygen.mjs +21 -0
- package/dist/chunk-K5E6JS5A.js +655 -0
- package/dist/chunk-K5E6JS5A.js.map +1 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-BJYm2kdq.d.ts} +27 -4
- package/dist/index.d.ts +194 -17
- package/dist/index.js +494 -44
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +40 -8
- package/dist/next.js +8 -2
- package/dist/next.js.map +1 -1
- package/dist/store-Dno2fnHH.d.ts +436 -0
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +115 -42
- package/dist/supabase/index.js.map +1 -1
- package/package.json +7 -3
- package/supabase/migrations/20260809000000_byollm_runner.sql +22 -3
- package/supabase/migrations/20260819000000_drop_runner_token.sql +87 -0
- package/supabase/migrations/20260819010000_completed_by_lease_id.sql +25 -0
- package/supabase/migrations/20260821000000_rename_collected.sql +91 -0
- package/dist/chunk-HL6EYHQ7.js +0 -422
- package/dist/chunk-HL6EYHQ7.js.map +0 -1
- package/dist/store-D23N6iiP.d.ts +0 -255
package/dist/supabase/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,25 @@ function toJob(row) {
|
|
|
93
93
|
return {
|
|
94
94
|
id: row.id,
|
|
95
95
|
kind: row.kind,
|
|
96
|
-
|
|
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
|
-
|
|
103
|
+
completedByLeaseId: row.completed_by_lease_id ?? null,
|
|
104
|
+
lease: (
|
|
105
|
+
// Keyed on the lease id, not the runner. A relayed grant has no runner
|
|
106
|
+
// row to point at (see AdoptArgs), and reading the lease as absent
|
|
107
|
+
// because `lease_runner` is null would make an actively-held job look
|
|
108
|
+
// claimable — the exact bug `adopt` exists to prevent.
|
|
109
|
+
leaseExpires !== null && row.lease_id !== null ? {
|
|
110
|
+
id: row.lease_id,
|
|
111
|
+
runnerId: row.lease_runner ?? "",
|
|
112
|
+
expiresAt: leaseExpires
|
|
113
|
+
} : null
|
|
114
|
+
),
|
|
103
115
|
createdAt: Date.parse(row.created_at),
|
|
104
116
|
claimableAt: ms(row.claimable_at),
|
|
105
117
|
ttlMs: row.ttl_ms,
|
|
@@ -111,15 +123,16 @@ function toJob(row) {
|
|
|
111
123
|
updatedAt: Date.parse(row.updated_at)
|
|
112
124
|
};
|
|
113
125
|
}
|
|
126
|
+
var leasePairs = (leases) => leases.map((l) => `and(id.eq.${l.jobId},lease_id.eq.${l.leaseId})`).join(",");
|
|
114
127
|
function toRunner(row) {
|
|
115
128
|
return {
|
|
116
129
|
id: row.id,
|
|
117
130
|
owner: row.owner,
|
|
118
|
-
tokenHash: row.token_hash,
|
|
119
131
|
label: row.label,
|
|
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),
|
|
@@ -133,11 +146,15 @@ function toPairing(row) {
|
|
|
133
146
|
state: row.state,
|
|
134
147
|
owner: row.owner,
|
|
135
148
|
runnerId: row.runner_id,
|
|
136
|
-
|
|
149
|
+
// Collected when it has a timestamp. This was `runner_token_once ===
|
|
150
|
+
// null` — a nulled token standing in for a fact about delivery, which is
|
|
151
|
+
// one field doing two jobs (cloud_008 §2.4a).
|
|
152
|
+
collected: row.collected_at !== null,
|
|
137
153
|
label: row.label,
|
|
138
154
|
platform: row.platform,
|
|
139
155
|
daemonVersion: row.daemon_version,
|
|
140
156
|
capabilities: row.capabilities,
|
|
157
|
+
device: row.device,
|
|
141
158
|
expiresAt: Date.parse(row.expires_at),
|
|
142
159
|
createdAt: Date.parse(row.created_at)
|
|
143
160
|
};
|
|
@@ -169,9 +186,10 @@ function supabaseStore(options) {
|
|
|
169
186
|
claimableAt = allDone ? iso(now) : null;
|
|
170
187
|
}
|
|
171
188
|
const row = {
|
|
172
|
-
|
|
189
|
+
id: input.id,
|
|
173
190
|
kind: input.kind,
|
|
174
|
-
|
|
191
|
+
envelope: input.envelope,
|
|
192
|
+
size_class: input.sizeClass,
|
|
175
193
|
audience: input.audience ?? "self",
|
|
176
194
|
owner: input.owner,
|
|
177
195
|
audience_allow: input.audienceAllow ? [...input.audienceAllow] : null,
|
|
@@ -185,7 +203,7 @@ function supabaseStore(options) {
|
|
|
185
203
|
);
|
|
186
204
|
if (inserted) return toJob(inserted);
|
|
187
205
|
const existing = unwrap(
|
|
188
|
-
await db.from("byollm_jobs").select().eq("id", input.id
|
|
206
|
+
await db.from("byollm_jobs").select().eq("id", input.id).single()
|
|
189
207
|
);
|
|
190
208
|
return toJob(existing);
|
|
191
209
|
},
|
|
@@ -208,14 +226,14 @@ function supabaseStore(options) {
|
|
|
208
226
|
},
|
|
209
227
|
async renewLeases(args) {
|
|
210
228
|
await db.rpc("byollm_expire_due");
|
|
211
|
-
if (args.
|
|
229
|
+
if (args.leases.length === 0) return { renewed: [], lost: [] };
|
|
212
230
|
const expiresAt = iso(args.now + args.leaseMs);
|
|
213
231
|
const renewedRows = unwrap(
|
|
214
232
|
await db.from("byollm_jobs").update({
|
|
215
233
|
state: "running",
|
|
216
234
|
lease_expires_at: expiresAt,
|
|
217
235
|
updated_at: iso(args.now)
|
|
218
|
-
}).eq("lease_runner", args.runnerId).
|
|
236
|
+
}).eq("lease_runner", args.runnerId).or(leasePairs(args.leases)).in("state", ["claimed", "running"]).select("id")
|
|
219
237
|
);
|
|
220
238
|
const renewedIds = new Set(renewedRows.map((row) => row.id));
|
|
221
239
|
return {
|
|
@@ -223,44 +241,64 @@ function supabaseStore(options) {
|
|
|
223
241
|
jobId: row.id,
|
|
224
242
|
expiresAt: args.now + args.leaseMs
|
|
225
243
|
})),
|
|
226
|
-
// Anything the runner thinks it holds but did not renew is gone
|
|
227
|
-
|
|
244
|
+
// Anything the runner thinks it holds but did not renew is gone,
|
|
245
|
+
// named by the grant it asked about rather than by a bare id — V1-3.
|
|
246
|
+
lost: args.leases.filter((lease) => !renewedIds.has(lease.jobId))
|
|
228
247
|
};
|
|
229
248
|
},
|
|
230
|
-
async
|
|
231
|
-
const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
|
|
249
|
+
async adopt(args) {
|
|
232
250
|
const rows = unwrap(
|
|
233
251
|
await db.from("byollm_jobs").update({
|
|
234
|
-
state,
|
|
252
|
+
state: "claimed",
|
|
253
|
+
lease_id: args.leaseId,
|
|
254
|
+
// Left null on purpose: `lease_runner` is a foreign key into
|
|
255
|
+
// `byollm_runners`, and a relayed device has no row there. See
|
|
256
|
+
// AdoptArgs — the site records the grant, not a machine it has
|
|
257
|
+
// no relationship with.
|
|
235
258
|
lease_runner: null,
|
|
236
|
-
lease_expires_at:
|
|
237
|
-
outcome: args.outcome,
|
|
238
|
-
provenance: args.provenance,
|
|
259
|
+
lease_expires_at: iso(args.expiresAt),
|
|
239
260
|
updated_at: iso(args.now)
|
|
240
|
-
}).eq("id", args.jobId).
|
|
261
|
+
}).eq("id", args.jobId).in("state", ["queued", "claimed"]).select()
|
|
241
262
|
);
|
|
242
263
|
const written = rows[0];
|
|
264
|
+
return written === void 0 ? null : toJob(written);
|
|
265
|
+
},
|
|
266
|
+
async complete(args) {
|
|
267
|
+
const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
|
|
268
|
+
let update = db.from("byollm_jobs").update({
|
|
269
|
+
state,
|
|
270
|
+
lease_runner: null,
|
|
271
|
+
lease_expires_at: null,
|
|
272
|
+
// Which grant recorded it, kept after the lease is dropped — §3.6.
|
|
273
|
+
completed_by_lease_id: args.holder.by === "lease" ? args.holder.leaseId : null,
|
|
274
|
+
outcome: args.outcome,
|
|
275
|
+
provenance: args.provenance,
|
|
276
|
+
updated_at: iso(args.now)
|
|
277
|
+
}).eq("id", args.jobId).in("state", ["claimed", "running"]);
|
|
278
|
+
update = args.holder.by === "runner" ? update.eq("lease_runner", args.holder.runnerId) : update.eq("lease_id", args.holder.leaseId);
|
|
279
|
+
const rows = unwrap(await update.select());
|
|
280
|
+
const written = rows[0];
|
|
243
281
|
if (written === void 0) {
|
|
244
282
|
const current = unwrapMaybe(
|
|
245
283
|
await db.from("byollm_jobs").select().eq("id", args.jobId).maybeSingle()
|
|
246
284
|
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
};
|
|
285
|
+
const job = current === null ? null : toJob(current);
|
|
286
|
+
const duplicate = job !== null && job.provenance?.runnerId === args.runnerId && (job.state === "ok" || job.state === "error" || job.state === "canceled") && args.holder.by === "lease" && job.completedByLeaseId !== null && job.completedByLeaseId === args.holder.leaseId;
|
|
287
|
+
return duplicate ? { accepted: false, duplicate: true, job } : { accepted: false, job };
|
|
251
288
|
}
|
|
252
289
|
return { accepted: true, job: toJob(written) };
|
|
253
290
|
},
|
|
254
291
|
async release(args) {
|
|
255
|
-
if (args.
|
|
292
|
+
if (args.leases.length === 0) return [];
|
|
256
293
|
const held = unwrap(
|
|
257
|
-
await db.from("byollm_jobs").select("id,refused_by").eq("lease_runner", args.runnerId).
|
|
294
|
+
await db.from("byollm_jobs").select("id,refused_by").eq("lease_runner", args.runnerId).or(leasePairs(args.leases))
|
|
258
295
|
);
|
|
259
296
|
const released = [];
|
|
260
297
|
for (const row of held) {
|
|
261
298
|
const refusedBy = args.reason === "refused" ? [.../* @__PURE__ */ new Set([...row.refused_by, args.runnerId])] : row.refused_by;
|
|
262
299
|
const { error } = await db.from("byollm_jobs").update({
|
|
263
300
|
state: "queued",
|
|
301
|
+
lease_id: null,
|
|
264
302
|
lease_runner: null,
|
|
265
303
|
lease_expires_at: null,
|
|
266
304
|
// Newly available again, so the TTL clock restarts.
|
|
@@ -304,14 +342,45 @@ function supabaseStore(options) {
|
|
|
304
342
|
},
|
|
305
343
|
async listCancelRequests(runnerId) {
|
|
306
344
|
const rows = unwrap(
|
|
307
|
-
await db.from("byollm_job_cancels").select("job_id, byollm_jobs!inner(lease_runner)").eq("byollm_jobs.lease_runner", runnerId)
|
|
345
|
+
await db.from("byollm_job_cancels").select("job_id, byollm_jobs!inner(lease_runner, lease_id)").eq("byollm_jobs.lease_runner", runnerId)
|
|
308
346
|
);
|
|
309
|
-
return rows.
|
|
347
|
+
return rows.filter((row) => row.byollm_jobs.lease_id !== null).map((row) => ({
|
|
348
|
+
jobId: row.job_id,
|
|
349
|
+
leaseId: row.byollm_jobs.lease_id ?? ""
|
|
350
|
+
}));
|
|
310
351
|
},
|
|
311
352
|
// -- pairing and runners -------------------------------------------------
|
|
353
|
+
/**
|
|
354
|
+
* The push seam (byollm_009 §8.3), over Postgres Realtime.
|
|
355
|
+
*
|
|
356
|
+
* Native here, which is the point of requiring it of every adapter: the
|
|
357
|
+
* backend that can push does, the one that cannot polls, and the
|
|
358
|
+
* interface does not change again when streaming arrives.
|
|
359
|
+
*/
|
|
360
|
+
subscribe(jobId, onChange) {
|
|
361
|
+
const channel = db.channel(`byollm_job_${jobId}`).on(
|
|
362
|
+
"postgres_changes",
|
|
363
|
+
{
|
|
364
|
+
event: "UPDATE",
|
|
365
|
+
schema: "public",
|
|
366
|
+
table: "byollm_jobs",
|
|
367
|
+
filter: `id=eq.${jobId}`
|
|
368
|
+
},
|
|
369
|
+
() => {
|
|
370
|
+
onChange();
|
|
371
|
+
}
|
|
372
|
+
).subscribe();
|
|
373
|
+
let live = true;
|
|
374
|
+
return () => {
|
|
375
|
+
if (!live) return;
|
|
376
|
+
live = false;
|
|
377
|
+
void db.removeChannel(channel).catch(() => void 0);
|
|
378
|
+
};
|
|
379
|
+
},
|
|
312
380
|
async createPairing(record) {
|
|
313
381
|
const { error } = await db.from("byollm_pairings").insert({
|
|
314
382
|
device_code_hash: record.deviceCodeHash,
|
|
383
|
+
device: record.device,
|
|
315
384
|
user_code: record.userCode,
|
|
316
385
|
state: record.state,
|
|
317
386
|
label: record.label,
|
|
@@ -347,18 +416,28 @@ function supabaseStore(options) {
|
|
|
347
416
|
const runner = unwrap(
|
|
348
417
|
await db.from("byollm_runners").insert({
|
|
349
418
|
owner: args.owner,
|
|
350
|
-
token_hash: args.tokenHash,
|
|
351
419
|
label: pairing.label,
|
|
352
420
|
platform: pairing.platform,
|
|
353
421
|
daemon_version: pairing.daemon_version,
|
|
354
|
-
capabilities: pairing.capabilities
|
|
422
|
+
capabilities: pairing.capabilities,
|
|
423
|
+
// Carried from the pairing, exactly as the SQL RPC does. There
|
|
424
|
+
// are two approval paths — this service-role one and
|
|
425
|
+
// `byollm_approve_pairing` for browser callers — and a field
|
|
426
|
+
// added to one and not the other produces a runner that is
|
|
427
|
+
// correct through one door and broken through the other.
|
|
428
|
+
device: pairing.device
|
|
355
429
|
}).select().single()
|
|
356
430
|
);
|
|
357
431
|
const { error } = await db.from("byollm_pairings").update({
|
|
358
432
|
state: "approved",
|
|
359
433
|
owner: args.owner,
|
|
360
434
|
runner_id: runner.id,
|
|
361
|
-
|
|
435
|
+
// Marks the approval collectable — cloud_008 §2.4. The column held
|
|
436
|
+
// a bearer token; it now holds a marker, and the next migration
|
|
437
|
+
// renames it. Written as a constant rather than left null because
|
|
438
|
+
// `collected` reads `=== null`, and a schema change and a code
|
|
439
|
+
// change landing in one step is how a rollback strands rows.
|
|
440
|
+
collected_at: null
|
|
362
441
|
}).eq("device_code_hash", pairing.device_code_hash);
|
|
363
442
|
if (error) throw new Error(`supabase: ${error.message}`);
|
|
364
443
|
return toRunner(runner);
|
|
@@ -368,15 +447,9 @@ function supabaseStore(options) {
|
|
|
368
447
|
if (error) throw new Error(`supabase: ${error.message}`);
|
|
369
448
|
},
|
|
370
449
|
async consumePairingToken(deviceCodeHash) {
|
|
371
|
-
const { error } = await db.from("byollm_pairings").update({
|
|
450
|
+
const { error } = await db.from("byollm_pairings").update({ collected_at: "now()" }).eq("device_code_hash", deviceCodeHash);
|
|
372
451
|
if (error) throw new Error(`supabase: ${error.message}`);
|
|
373
452
|
},
|
|
374
|
-
async getRunnerByTokenHash(hash) {
|
|
375
|
-
const row = unwrapMaybe(
|
|
376
|
-
await db.from("byollm_runners").select().eq("token_hash", hash).maybeSingle()
|
|
377
|
-
);
|
|
378
|
-
return row === null ? null : toRunner(row);
|
|
379
|
-
},
|
|
380
453
|
async getRunner(runnerId) {
|
|
381
454
|
const row = unwrapMaybe(
|
|
382
455
|
await db.from("byollm_runners").select().eq("id", runnerId).maybeSingle()
|
|
@@ -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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byollm/server",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.30",
|
|
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.
|
|
33
|
+
"@byollm/protocol": "0.1.0-alpha.30"
|
|
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
|
-
|
|
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
|