@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.
- package/README.md +72 -11
- package/bin/keygen.mjs +21 -0
- package/dist/{chunk-HL6EYHQ7.js → chunk-4NIHWQAT.js} +253 -53
- package/dist/chunk-4NIHWQAT.js.map +1 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-DgW0QNTf.d.ts} +27 -4
- package/dist/index.d.ts +176 -12
- package/dist/index.js +375 -23
- 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-D23N6iiP.d.ts → store-Cj5b6A9j.d.ts} +140 -7
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +91 -24
- package/dist/supabase/index.js.map +1 -1
- package/package.json +7 -3
- package/supabase/migrations/20260809000000_byollm_runner.sql +22 -3
- package/dist/chunk-HL6EYHQ7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ByollmHandlers,
|
|
3
3
|
SERVED_PROTOCOL_VERSION,
|
|
4
|
-
bearerFrom,
|
|
5
4
|
createFetchHandler,
|
|
6
5
|
generateDeviceCode,
|
|
7
6
|
generateJobId,
|
|
7
|
+
generateLeaseId,
|
|
8
8
|
generateRunnerId,
|
|
9
9
|
generateRunnerToken,
|
|
10
10
|
generateUserCode,
|
|
11
11
|
hashSecret,
|
|
12
|
+
resealForDevice,
|
|
12
13
|
routeEndpoint,
|
|
13
|
-
secretsMatch
|
|
14
|
-
|
|
14
|
+
secretsMatch,
|
|
15
|
+
signatureFrom
|
|
16
|
+
} from "./chunk-4NIHWQAT.js";
|
|
15
17
|
import {
|
|
16
18
|
NoRunnerAvailableError,
|
|
17
19
|
PollingDelivery,
|
|
@@ -20,19 +22,206 @@ import {
|
|
|
20
22
|
|
|
21
23
|
// src/app.ts
|
|
22
24
|
import {
|
|
25
|
+
ENVELOPE_MAX_AGE_MS,
|
|
26
|
+
KindedPayload,
|
|
27
|
+
keyId as keyId2,
|
|
28
|
+
payloadTextLength,
|
|
29
|
+
publicIdentityOf as publicIdentityOf2,
|
|
30
|
+
seal,
|
|
31
|
+
sizeClassOf,
|
|
23
32
|
backendDescriptor,
|
|
24
33
|
matchAudience
|
|
25
34
|
} from "@byollm/protocol";
|
|
35
|
+
|
|
36
|
+
// src/cloud.ts
|
|
37
|
+
import {
|
|
38
|
+
JobOutcome,
|
|
39
|
+
keyId,
|
|
40
|
+
open,
|
|
41
|
+
publicIdentityOf,
|
|
42
|
+
provenanceFor
|
|
43
|
+
} from "@byollm/protocol";
|
|
44
|
+
var CloudLane = class {
|
|
45
|
+
#options;
|
|
46
|
+
#store;
|
|
47
|
+
#siteKeys;
|
|
48
|
+
#now;
|
|
49
|
+
#fetch;
|
|
50
|
+
constructor(deps) {
|
|
51
|
+
this.#options = deps.options;
|
|
52
|
+
this.#store = deps.store;
|
|
53
|
+
this.#siteKeys = deps.siteKeys;
|
|
54
|
+
this.#now = deps.now;
|
|
55
|
+
this.#fetch = deps.options.fetch ?? globalThis.fetch;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Publish a job's stub for routing.
|
|
59
|
+
*
|
|
60
|
+
* The stub and nothing else — byollm_009 §6 makes that exhaustive by
|
|
61
|
+
* construction, so this cannot leak a payload even by mistake: there is no
|
|
62
|
+
* field on `JobStub` to put one in.
|
|
63
|
+
*/
|
|
64
|
+
async publish(record) {
|
|
65
|
+
const stub = {
|
|
66
|
+
id: record.id,
|
|
67
|
+
kind: record.kind,
|
|
68
|
+
owner: record.owner,
|
|
69
|
+
audience: record.audience,
|
|
70
|
+
...record.audienceAllow === void 0 ? {} : { audienceAllow: [...record.audienceAllow] },
|
|
71
|
+
sizeClass: record.sizeClass,
|
|
72
|
+
streaming: false,
|
|
73
|
+
// The relay needs *a* deadline to bound routing. A job without one gets
|
|
74
|
+
// the envelope's, which is the outer bound on how long the ciphertext
|
|
75
|
+
// is worth carrying — never longer than the work could possibly matter.
|
|
76
|
+
deadlineAt: record.deadlineAt ?? record.createdAt + ENVELOPE_TTL_FALLBACK
|
|
77
|
+
};
|
|
78
|
+
await this.#post("/relay/site/enqueue", {
|
|
79
|
+
siteId: this.#options.siteId,
|
|
80
|
+
stub
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One cycle: seal for anything claimed, collect anything finished.
|
|
85
|
+
*
|
|
86
|
+
* Idempotent and safe to call as often as you like. Exposed as a single
|
|
87
|
+
* cycle rather than hidden behind a timer so a caller decides its own
|
|
88
|
+
* cadence — a serverless site runs it on a cron, a long-lived one on an
|
|
89
|
+
* interval, and a test runs it exactly when it means to.
|
|
90
|
+
*/
|
|
91
|
+
async pump() {
|
|
92
|
+
const sealed = [];
|
|
93
|
+
const refused = [];
|
|
94
|
+
const completed = [];
|
|
95
|
+
const pending = await this.#get("/relay/site/pending");
|
|
96
|
+
for (const claim of pending.jobs) {
|
|
97
|
+
const record = await this.#store.get(claim.jobId);
|
|
98
|
+
if (!record) continue;
|
|
99
|
+
const resealed = await resealForDevice({
|
|
100
|
+
siteKeys: this.#siteKeys,
|
|
101
|
+
job: {
|
|
102
|
+
id: record.id,
|
|
103
|
+
envelope: record.envelope,
|
|
104
|
+
createdAt: record.createdAt
|
|
105
|
+
},
|
|
106
|
+
device: claim.device
|
|
107
|
+
});
|
|
108
|
+
if (!resealed.ok) {
|
|
109
|
+
refused.push(claim.jobId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
await this.#store.adopt({
|
|
113
|
+
jobId: claim.jobId,
|
|
114
|
+
leaseId: claim.leaseId,
|
|
115
|
+
expiresAt: claim.awaitingUntil,
|
|
116
|
+
now: this.#now()
|
|
117
|
+
});
|
|
118
|
+
await this.#post("/relay/site/payload", {
|
|
119
|
+
siteId: this.#options.siteId,
|
|
120
|
+
jobId: claim.jobId,
|
|
121
|
+
envelope: resealed.envelope
|
|
122
|
+
});
|
|
123
|
+
sealed.push(claim.jobId);
|
|
124
|
+
}
|
|
125
|
+
const finished = await this.#get("/relay/site/results");
|
|
126
|
+
for (const done of finished.jobs) {
|
|
127
|
+
const record = await this.#store.get(done.jobId);
|
|
128
|
+
if (!record || record.state === "ok" || record.state === "error") {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const outcome = await this.#openResult(done);
|
|
132
|
+
if (!outcome) {
|
|
133
|
+
refused.push(done.jobId);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
await this.#store.complete({
|
|
137
|
+
jobId: done.jobId,
|
|
138
|
+
// The grant, not the machine: this site never paired with the device
|
|
139
|
+
// that ran it, and the signature it verified above is the stronger
|
|
140
|
+
// claim about who did.
|
|
141
|
+
holder: { by: "lease", leaseId: done.leaseId },
|
|
142
|
+
outcome,
|
|
143
|
+
provenance: provenanceFor({
|
|
144
|
+
audience: record.audience,
|
|
145
|
+
runnerId: done.runnerId,
|
|
146
|
+
runnerOwner: keyId(done.device.identity),
|
|
147
|
+
backendClass: "http",
|
|
148
|
+
model: "unknown"
|
|
149
|
+
}),
|
|
150
|
+
now: this.#now()
|
|
151
|
+
});
|
|
152
|
+
completed.push(done.jobId);
|
|
153
|
+
}
|
|
154
|
+
return { sealed, completed, refused };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Open a sealed result and verify it came from the device that claimed it.
|
|
158
|
+
*
|
|
159
|
+
* The relay says which device ran the job; this checks that claim against a
|
|
160
|
+
* signature the relay cannot produce. A relay that named the wrong device
|
|
161
|
+
* gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`
|
|
162
|
+
* from quietly becoming `RELAY_TRUSTED`.
|
|
163
|
+
*/
|
|
164
|
+
async #openResult(done) {
|
|
165
|
+
const opened = await open({
|
|
166
|
+
envelope: done.envelope,
|
|
167
|
+
recipientKeys: this.#siteKeys,
|
|
168
|
+
senderIdentityPublic: done.device.identity,
|
|
169
|
+
expected: {
|
|
170
|
+
jobId: done.jobId,
|
|
171
|
+
senderKeyId: keyId(done.device.identity),
|
|
172
|
+
recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),
|
|
173
|
+
direction: "result"
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (!opened.ok) return null;
|
|
177
|
+
let parsed;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(opened.plaintext);
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const outcome = JobOutcome.safeParse(parsed);
|
|
184
|
+
if (!outcome.success) return null;
|
|
185
|
+
if (outcome.data.outcome !== done.disposition) return null;
|
|
186
|
+
return outcome.data;
|
|
187
|
+
}
|
|
188
|
+
async #post(path, body) {
|
|
189
|
+
const response = await this.#fetch(`${this.#options.relayOrigin}${path}`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "content-type": "application/json" },
|
|
192
|
+
body: JSON.stringify(body)
|
|
193
|
+
});
|
|
194
|
+
return response.json();
|
|
195
|
+
}
|
|
196
|
+
async #get(path) {
|
|
197
|
+
const url = `${this.#options.relayOrigin}${path}?siteId=${encodeURIComponent(this.#options.siteId)}`;
|
|
198
|
+
const response = await this.#fetch(url);
|
|
199
|
+
return response.json();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var ENVELOPE_TTL_FALLBACK = 24 * 60 * 6e4;
|
|
203
|
+
|
|
204
|
+
// src/app.ts
|
|
26
205
|
var DEFAULT_LIVENESS_MS = 35e3;
|
|
27
206
|
var ByollmApp = class {
|
|
28
207
|
#store;
|
|
208
|
+
#siteKeys;
|
|
29
209
|
#now;
|
|
30
210
|
#livenessMs;
|
|
31
211
|
#delivery;
|
|
212
|
+
/** Present only in the cloud lane; the site's side of the relay. */
|
|
213
|
+
cloud;
|
|
32
214
|
constructor(options) {
|
|
33
215
|
this.#store = options.store;
|
|
216
|
+
this.#siteKeys = options.siteKeys;
|
|
34
217
|
this.#now = options.now ?? Date.now;
|
|
35
218
|
this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;
|
|
219
|
+
this.cloud = options.lane === void 0 ? void 0 : new CloudLane({
|
|
220
|
+
options: options.lane,
|
|
221
|
+
store: options.store,
|
|
222
|
+
siteKeys: options.siteKeys,
|
|
223
|
+
now: this.#now
|
|
224
|
+
});
|
|
36
225
|
const deps = {
|
|
37
226
|
...options.noRunnerGraceMs === void 0 ? {} : { graceMs: options.noRunnerGraceMs },
|
|
38
227
|
read: (jobId) => this.result(jobId),
|
|
@@ -66,7 +255,45 @@ var ByollmApp = class {
|
|
|
66
255
|
* the app is obliged to disclose that to whoever reads it.
|
|
67
256
|
*/
|
|
68
257
|
async enqueue(input) {
|
|
69
|
-
const
|
|
258
|
+
const parsed = KindedPayload.safeParse({
|
|
259
|
+
kind: input.kind,
|
|
260
|
+
payload: input.payload
|
|
261
|
+
});
|
|
262
|
+
if (!parsed.success) {
|
|
263
|
+
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
264
|
+
throw new Error(`invalid ${input.kind} payload \u2014 ${detail}`);
|
|
265
|
+
}
|
|
266
|
+
const createdAt = this.#now();
|
|
267
|
+
const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;
|
|
268
|
+
const jobId = input.id ?? generateJobId();
|
|
269
|
+
const senderKeyId = keyId2(publicIdentityOf2(this.#siteKeys).identity);
|
|
270
|
+
const envelope = await seal({
|
|
271
|
+
plaintext: JSON.stringify(parsed.data.payload),
|
|
272
|
+
senderKeys: this.#siteKeys,
|
|
273
|
+
recipientEncryptionPublic: this.#siteKeys.encryptionPublic,
|
|
274
|
+
context: {
|
|
275
|
+
jobId,
|
|
276
|
+
senderKeyId,
|
|
277
|
+
recipientKeyId: senderKeyId,
|
|
278
|
+
deadlineAt: envelopeDeadlineAt,
|
|
279
|
+
direction: "payload"
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
const record = await this.#store.create(
|
|
283
|
+
{
|
|
284
|
+
...input,
|
|
285
|
+
id: jobId,
|
|
286
|
+
envelope,
|
|
287
|
+
sizeClass: sizeClassOf(
|
|
288
|
+
payloadTextLength({
|
|
289
|
+
kind: input.kind,
|
|
290
|
+
payload: parsed.data.payload
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
},
|
|
294
|
+
createdAt
|
|
295
|
+
);
|
|
296
|
+
await this.cloud?.publish(record);
|
|
70
297
|
return {
|
|
71
298
|
id: record.id,
|
|
72
299
|
record,
|
|
@@ -229,6 +456,45 @@ function normalizeUserCode(input) {
|
|
|
229
456
|
return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;
|
|
230
457
|
}
|
|
231
458
|
|
|
459
|
+
// src/keys.ts
|
|
460
|
+
import { StoredKeys, generateKeys, publicIdentityOf as publicIdentityOf3 } from "@byollm/protocol";
|
|
461
|
+
import { fingerprint } from "@byollm/protocol";
|
|
462
|
+
var generateSiteKeys = (now = Date.now()) => generateKeys(now);
|
|
463
|
+
function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
|
|
464
|
+
const raw = env[variable];
|
|
465
|
+
if (raw === void 0 || raw === "") {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`${variable} is not set. Generate a site identity once with \`npx @byollm/server keygen\` and set it as ${variable}. Do not generate keys at startup: every instance would get a different identity and daemons would pin one and be refused by another.`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
let parsed;
|
|
471
|
+
try {
|
|
472
|
+
parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
473
|
+
} catch {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`${variable} is not base64-encoded JSON. It should be exactly what \`npx @byollm/server keygen\` printed.`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
const result = StoredKeys.safeParse(parsed);
|
|
479
|
+
if (!result.success) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`${variable} does not contain a valid site identity. Regenerate it with \`npx @byollm/server keygen\` \u2014 and if this site has already paired daemons, they will need to pair again.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return result.data;
|
|
485
|
+
}
|
|
486
|
+
function formatSiteKeys(keys) {
|
|
487
|
+
const encoded = Buffer.from(JSON.stringify(keys)).toString("base64");
|
|
488
|
+
return `# Set this as BYOLLM_SITE_KEYS. It is a secret: it is this site's
|
|
489
|
+
# identity, and anything holding it can be this site.
|
|
490
|
+
BYOLLM_SITE_KEYS=${encoded}
|
|
491
|
+
|
|
492
|
+
# Fingerprint (not secret \u2014 show it to users so they can check what
|
|
493
|
+
# their daemon pinned):
|
|
494
|
+
# ${fingerprint(publicIdentityOf3(keys).identity)}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
|
|
232
498
|
// src/memory.ts
|
|
233
499
|
import {
|
|
234
500
|
backendDescriptor as backendDescriptor2,
|
|
@@ -247,7 +513,7 @@ var MemoryStore = class {
|
|
|
247
513
|
}
|
|
248
514
|
// -- jobs ---------------------------------------------------------------
|
|
249
515
|
create(input, now) {
|
|
250
|
-
const id = input.id
|
|
516
|
+
const id = input.id;
|
|
251
517
|
const existing = this.#jobs.get(id);
|
|
252
518
|
if (existing) return Promise.resolve(existing);
|
|
253
519
|
const dependsOn = [...input.dependsOn ?? []];
|
|
@@ -257,7 +523,8 @@ var MemoryStore = class {
|
|
|
257
523
|
const job = {
|
|
258
524
|
id,
|
|
259
525
|
kind: input.kind,
|
|
260
|
-
|
|
526
|
+
envelope: input.envelope,
|
|
527
|
+
sizeClass: input.sizeClass,
|
|
261
528
|
audience: input.audience ?? "self",
|
|
262
529
|
owner: input.owner,
|
|
263
530
|
audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
|
|
@@ -275,7 +542,7 @@ var MemoryStore = class {
|
|
|
275
542
|
provenance: null,
|
|
276
543
|
updatedAt: now
|
|
277
544
|
};
|
|
278
|
-
this.#
|
|
545
|
+
this.#write(id, job);
|
|
279
546
|
return Promise.resolve(job);
|
|
280
547
|
}
|
|
281
548
|
get(jobId) {
|
|
@@ -294,13 +561,16 @@ var MemoryStore = class {
|
|
|
294
561
|
...job,
|
|
295
562
|
state: "claimed",
|
|
296
563
|
lease: {
|
|
564
|
+
// A fresh id per grant. Two claims of the same job by the same
|
|
565
|
+
// runner are two different leases, and must be distinguishable.
|
|
566
|
+
id: generateLeaseId(),
|
|
297
567
|
runnerId: args.runnerId,
|
|
298
568
|
expiresAt: args.now + args.leaseMs
|
|
299
569
|
},
|
|
300
570
|
attempts: job.attempts + 1,
|
|
301
571
|
updatedAt: args.now
|
|
302
572
|
};
|
|
303
|
-
this.#
|
|
573
|
+
this.#write(job.id, updated);
|
|
304
574
|
claimed.push(updated);
|
|
305
575
|
}
|
|
306
576
|
return Promise.resolve(claimed);
|
|
@@ -347,9 +617,9 @@ var MemoryStore = class {
|
|
|
347
617
|
this.#expireDueSync(args.now);
|
|
348
618
|
const renewed = [];
|
|
349
619
|
const lost = [];
|
|
350
|
-
for (const jobId of args.
|
|
620
|
+
for (const { jobId, leaseId } of args.leases) {
|
|
351
621
|
const job = this.#jobs.get(jobId);
|
|
352
|
-
if (!job || job.lease?.runnerId !== args.runnerId) {
|
|
622
|
+
if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
|
|
353
623
|
lost.push(jobId);
|
|
354
624
|
continue;
|
|
355
625
|
}
|
|
@@ -358,16 +628,40 @@ var MemoryStore = class {
|
|
|
358
628
|
continue;
|
|
359
629
|
}
|
|
360
630
|
const expiresAt = args.now + args.leaseMs;
|
|
361
|
-
this.#
|
|
631
|
+
this.#write(jobId, {
|
|
362
632
|
...job,
|
|
363
633
|
state: "running",
|
|
364
|
-
|
|
634
|
+
// Renewal extends the existing grant; it does not mint a new one.
|
|
635
|
+
lease: { ...job.lease, expiresAt },
|
|
365
636
|
updatedAt: args.now
|
|
366
637
|
});
|
|
367
638
|
renewed.push({ jobId, expiresAt });
|
|
368
639
|
}
|
|
369
640
|
return Promise.resolve({ renewed, lost });
|
|
370
641
|
}
|
|
642
|
+
adopt(args) {
|
|
643
|
+
const job = this.#jobs.get(args.jobId);
|
|
644
|
+
if (!job) return Promise.resolve(null);
|
|
645
|
+
if (job.state !== "queued" && job.state !== "claimed") {
|
|
646
|
+
return Promise.resolve(null);
|
|
647
|
+
}
|
|
648
|
+
if (job.lease && job.lease.id !== args.leaseId) {
|
|
649
|
+
return Promise.resolve(null);
|
|
650
|
+
}
|
|
651
|
+
const updated = {
|
|
652
|
+
...job,
|
|
653
|
+
state: "claimed",
|
|
654
|
+
lease: {
|
|
655
|
+
id: args.leaseId,
|
|
656
|
+
// No runner: this site never paired with the machine holding it.
|
|
657
|
+
runnerId: "",
|
|
658
|
+
expiresAt: args.expiresAt
|
|
659
|
+
},
|
|
660
|
+
updatedAt: args.now
|
|
661
|
+
};
|
|
662
|
+
this.#write(updated.id, updated);
|
|
663
|
+
return Promise.resolve(updated);
|
|
664
|
+
}
|
|
371
665
|
complete(args) {
|
|
372
666
|
const job = this.#jobs.get(args.jobId);
|
|
373
667
|
if (!job) return Promise.resolve({ accepted: false, job: null });
|
|
@@ -377,7 +671,8 @@ var MemoryStore = class {
|
|
|
377
671
|
if (job.state === "expired") {
|
|
378
672
|
return Promise.resolve({ accepted: false, job });
|
|
379
673
|
}
|
|
380
|
-
|
|
674
|
+
const holds = args.holder.by === "runner" ? job.lease?.runnerId === args.holder.runnerId : job.lease?.id === args.holder.leaseId;
|
|
675
|
+
if (!holds) {
|
|
381
676
|
return Promise.resolve({ accepted: false, job });
|
|
382
677
|
}
|
|
383
678
|
const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
|
|
@@ -389,7 +684,7 @@ var MemoryStore = class {
|
|
|
389
684
|
provenance: args.provenance,
|
|
390
685
|
updatedAt: args.now
|
|
391
686
|
};
|
|
392
|
-
this.#
|
|
687
|
+
this.#write(job.id, updated);
|
|
393
688
|
this.#cancelRequests.delete(job.id);
|
|
394
689
|
if (state === "ok") this.#unblockDependents(job.id, args.now);
|
|
395
690
|
return Promise.resolve({ accepted: true, job: updated });
|
|
@@ -410,16 +705,66 @@ var MemoryStore = class {
|
|
|
410
705
|
(depId) => this.#jobs.get(depId)?.state === "ok"
|
|
411
706
|
);
|
|
412
707
|
if (ready) {
|
|
413
|
-
this.#
|
|
708
|
+
this.#write(job.id, { ...job, claimableAt: now, updatedAt: now });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Watchers, by job id (byollm_009 §8.3).
|
|
714
|
+
*
|
|
715
|
+
* A `Set` per job so an unsubscribe removes exactly the handler it
|
|
716
|
+
* registered — two waiters on the same job are ordinary, and removing by
|
|
717
|
+
* job id alone would silently cancel someone else's wait.
|
|
718
|
+
*/
|
|
719
|
+
#watchers = /* @__PURE__ */ new Map();
|
|
720
|
+
subscribe(jobId, onChange) {
|
|
721
|
+
const existing = this.#watchers.get(jobId) ?? /* @__PURE__ */ new Set();
|
|
722
|
+
existing.add(onChange);
|
|
723
|
+
this.#watchers.set(jobId, existing);
|
|
724
|
+
let live = true;
|
|
725
|
+
return () => {
|
|
726
|
+
if (!live) return;
|
|
727
|
+
live = false;
|
|
728
|
+
const set = this.#watchers.get(jobId);
|
|
729
|
+
set?.delete(onChange);
|
|
730
|
+
if (set?.size === 0) this.#watchers.delete(jobId);
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* The single write path for a job.
|
|
735
|
+
*
|
|
736
|
+
* Every mutation goes through here so notification cannot be forgotten by
|
|
737
|
+
* a future one. Nine call sites existed when the push seam was added, and
|
|
738
|
+
* "remember to notify" is not a property nine call sites keep.
|
|
739
|
+
*/
|
|
740
|
+
#write(jobId, record) {
|
|
741
|
+
this.#jobs.set(jobId, record);
|
|
742
|
+
this.#notify(jobId);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Tell anyone watching that a job changed.
|
|
746
|
+
*
|
|
747
|
+
* A throwing watcher must not corrupt the store's own bookkeeping, so each
|
|
748
|
+
* is isolated: this runs inside write paths, and one bad listener taking
|
|
749
|
+
* out an unrelated write would be a far worse failure than a missed
|
|
750
|
+
* notification.
|
|
751
|
+
*/
|
|
752
|
+
#notify(jobId) {
|
|
753
|
+
for (const watcher of this.#watchers.get(jobId) ?? []) {
|
|
754
|
+
try {
|
|
755
|
+
watcher();
|
|
756
|
+
} catch {
|
|
414
757
|
}
|
|
415
758
|
}
|
|
416
759
|
}
|
|
417
760
|
release(args) {
|
|
418
761
|
const released = [];
|
|
419
|
-
for (const jobId of args.
|
|
762
|
+
for (const { jobId, leaseId } of args.leases) {
|
|
420
763
|
const job = this.#jobs.get(jobId);
|
|
421
|
-
if (!job || job.lease?.runnerId !== args.runnerId)
|
|
422
|
-
|
|
764
|
+
if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
this.#write(jobId, {
|
|
423
768
|
...job,
|
|
424
769
|
state: "queued",
|
|
425
770
|
lease: null,
|
|
@@ -461,7 +806,7 @@ var MemoryStore = class {
|
|
|
461
806
|
claimableAt: now,
|
|
462
807
|
updatedAt: now
|
|
463
808
|
};
|
|
464
|
-
this.#
|
|
809
|
+
this.#write(job.id, requeued);
|
|
465
810
|
changed.push(requeued);
|
|
466
811
|
}
|
|
467
812
|
}
|
|
@@ -476,7 +821,7 @@ var MemoryStore = class {
|
|
|
476
821
|
lease: null,
|
|
477
822
|
updatedAt: now
|
|
478
823
|
};
|
|
479
|
-
this.#
|
|
824
|
+
this.#write(job.id, expired);
|
|
480
825
|
changed.push(expired);
|
|
481
826
|
}
|
|
482
827
|
return changed;
|
|
@@ -491,7 +836,7 @@ var MemoryStore = class {
|
|
|
491
836
|
lease: null,
|
|
492
837
|
updatedAt: now
|
|
493
838
|
};
|
|
494
|
-
this.#
|
|
839
|
+
this.#write(jobId, canceled);
|
|
495
840
|
return Promise.resolve(canceled);
|
|
496
841
|
}
|
|
497
842
|
if (job.state === "claimed" || job.state === "running") {
|
|
@@ -543,6 +888,9 @@ var MemoryStore = class {
|
|
|
543
888
|
id: args.runnerId,
|
|
544
889
|
owner: args.owner,
|
|
545
890
|
tokenHash: args.tokenHash,
|
|
891
|
+
// Carried from the pairing, not re-supplied at approval: the user
|
|
892
|
+
// approved a specific machine, and the runner must be that machine.
|
|
893
|
+
device: pairing.device,
|
|
546
894
|
label: pairing.label,
|
|
547
895
|
platform: pairing.platform,
|
|
548
896
|
daemonVersion: pairing.daemonVersion,
|
|
@@ -631,22 +979,26 @@ function capabilityFor(capabilities, kind) {
|
|
|
631
979
|
export {
|
|
632
980
|
ByollmApp,
|
|
633
981
|
ByollmHandlers,
|
|
982
|
+
CloudLane,
|
|
634
983
|
MemoryStore,
|
|
635
984
|
NoRunnerAvailableError,
|
|
636
985
|
PollingDelivery,
|
|
637
986
|
ResultTimeoutError,
|
|
638
987
|
SERVED_PROTOCOL_VERSION,
|
|
639
|
-
bearerFrom,
|
|
640
988
|
capabilityFor,
|
|
641
989
|
createFetchHandler,
|
|
990
|
+
formatSiteKeys,
|
|
642
991
|
generateDeviceCode,
|
|
643
992
|
generateJobId,
|
|
644
993
|
generateRunnerId,
|
|
645
994
|
generateRunnerToken,
|
|
995
|
+
generateSiteKeys,
|
|
646
996
|
generateUserCode,
|
|
647
997
|
hashSecret,
|
|
648
998
|
normalizeUserCode,
|
|
649
999
|
routeEndpoint,
|
|
650
|
-
secretsMatch
|
|
1000
|
+
secretsMatch,
|
|
1001
|
+
signatureFrom,
|
|
1002
|
+
siteKeysFromEnv
|
|
651
1003
|
};
|
|
652
1004
|
//# sourceMappingURL=index.js.map
|