@byollm/server 0.1.0-alpha.1 → 0.1.0-alpha.11

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/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
- } from "./chunk-HL6EYHQ7.js";
14
+ secretsMatch,
15
+ signatureFrom
16
+ } from "./chunk-4NIHWQAT.js";
15
17
  import {
16
18
  NoRunnerAvailableError,
17
19
  PollingDelivery,
@@ -20,19 +22,238 @@ 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
+ signSiteRequest
44
+ } from "@byollm/protocol";
45
+ var CloudLane = class {
46
+ #options;
47
+ #store;
48
+ #siteKeys;
49
+ #now;
50
+ #fetch;
51
+ constructor(deps) {
52
+ this.#options = deps.options;
53
+ this.#store = deps.store;
54
+ this.#siteKeys = deps.siteKeys;
55
+ this.#now = deps.now;
56
+ this.#fetch = deps.options.fetch ?? globalThis.fetch;
57
+ }
58
+ /**
59
+ * Publish a job's stub for routing.
60
+ *
61
+ * The stub and nothing else — byollm_009 §6 makes that exhaustive by
62
+ * construction, so this cannot leak a payload even by mistake: there is no
63
+ * field on `JobStub` to put one in.
64
+ */
65
+ async publish(record) {
66
+ const stub = {
67
+ id: record.id,
68
+ kind: record.kind,
69
+ owner: record.owner,
70
+ audience: record.audience,
71
+ ...record.audienceAllow === void 0 ? {} : { audienceAllow: [...record.audienceAllow] },
72
+ sizeClass: record.sizeClass,
73
+ streaming: false,
74
+ // The relay needs *a* deadline to bound routing. A job without one gets
75
+ // the envelope's, which is the outer bound on how long the ciphertext
76
+ // is worth carrying — never longer than the work could possibly matter.
77
+ deadlineAt: record.deadlineAt ?? record.createdAt + ENVELOPE_TTL_FALLBACK
78
+ };
79
+ await this.#post("enqueue", {
80
+ siteId: this.#options.siteId,
81
+ stub
82
+ });
83
+ }
84
+ /**
85
+ * One cycle: seal for anything claimed, collect anything finished.
86
+ *
87
+ * Idempotent and safe to call as often as you like. Exposed as a single
88
+ * cycle rather than hidden behind a timer so a caller decides its own
89
+ * cadence — a serverless site runs it on a cron, a long-lived one on an
90
+ * interval, and a test runs it exactly when it means to.
91
+ */
92
+ async pump() {
93
+ const sealed = [];
94
+ const refused = [];
95
+ const completed = [];
96
+ const pending = await this.#get("pending");
97
+ for (const claim of pending.jobs) {
98
+ const record = await this.#store.get(claim.jobId);
99
+ if (!record) continue;
100
+ const resealed = await resealForDevice({
101
+ siteKeys: this.#siteKeys,
102
+ job: {
103
+ id: record.id,
104
+ envelope: record.envelope,
105
+ createdAt: record.createdAt
106
+ },
107
+ device: claim.device
108
+ });
109
+ if (!resealed.ok) {
110
+ refused.push(claim.jobId);
111
+ continue;
112
+ }
113
+ await this.#store.adopt({
114
+ jobId: claim.jobId,
115
+ leaseId: claim.leaseId,
116
+ expiresAt: claim.awaitingUntil,
117
+ now: this.#now()
118
+ });
119
+ await this.#post("payload", {
120
+ siteId: this.#options.siteId,
121
+ jobId: claim.jobId,
122
+ envelope: resealed.envelope
123
+ });
124
+ sealed.push(claim.jobId);
125
+ }
126
+ const finished = await this.#get("results");
127
+ for (const done of finished.jobs) {
128
+ const record = await this.#store.get(done.jobId);
129
+ if (!record || record.state === "ok" || record.state === "error") {
130
+ continue;
131
+ }
132
+ const outcome = await this.#openResult(done);
133
+ if (!outcome) {
134
+ refused.push(done.jobId);
135
+ continue;
136
+ }
137
+ await this.#store.complete({
138
+ jobId: done.jobId,
139
+ // The grant, not the machine: this site never paired with the device
140
+ // that ran it, and the signature it verified above is the stronger
141
+ // claim about who did.
142
+ holder: { by: "lease", leaseId: done.leaseId },
143
+ outcome,
144
+ provenance: provenanceFor({
145
+ audience: record.audience,
146
+ runnerId: done.runnerId,
147
+ runnerOwner: keyId(done.device.identity),
148
+ backendClass: "http",
149
+ model: "unknown"
150
+ }),
151
+ now: this.#now()
152
+ });
153
+ completed.push(done.jobId);
154
+ }
155
+ return { sealed, completed, refused };
156
+ }
157
+ /**
158
+ * Open a sealed result and verify it came from the device that claimed it.
159
+ *
160
+ * The relay says which device ran the job; this checks that claim against a
161
+ * signature the relay cannot produce. A relay that named the wrong device
162
+ * gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`
163
+ * from quietly becoming `RELAY_TRUSTED`.
164
+ */
165
+ async #openResult(done) {
166
+ const opened = await open({
167
+ envelope: done.envelope,
168
+ recipientKeys: this.#siteKeys,
169
+ senderIdentityPublic: done.device.identity,
170
+ expected: {
171
+ jobId: done.jobId,
172
+ senderKeyId: keyId(done.device.identity),
173
+ recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),
174
+ direction: "result"
175
+ }
176
+ });
177
+ if (!opened.ok) return null;
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(opened.plaintext);
181
+ } catch {
182
+ return null;
183
+ }
184
+ const outcome = JobOutcome.safeParse(parsed);
185
+ if (!outcome.success) return null;
186
+ if (outcome.data.outcome !== done.disposition) return null;
187
+ return outcome.data;
188
+ }
189
+ /**
190
+ * Sign a site-plane call with this site's identity key.
191
+ *
192
+ * The same scheme the daemon uses against an upstream, because the site is
193
+ * in the same position: an outbound caller whose key the relay already holds
194
+ * for other reasons. Nothing else authenticates this plane — a relay that
195
+ * took the `siteId` in a body at face value would let anyone enqueue work in
196
+ * a site's name and read who claimed it.
197
+ */
198
+ #headers(endpoint, rawBody) {
199
+ const signature = signSiteRequest(this.#siteKeys, {
200
+ endpoint,
201
+ siteId: this.#options.siteId,
202
+ issuedAt: this.#now(),
203
+ body: rawBody
204
+ });
205
+ return {
206
+ "x-byollm-site": this.#options.siteId,
207
+ "x-byollm-issued-at": String(signature.issuedAt),
208
+ "x-byollm-signature": signature.signature
209
+ };
210
+ }
211
+ async #post(endpoint, body) {
212
+ const rawBody = JSON.stringify(body);
213
+ const response = await this.#fetch(
214
+ `${this.#options.relayOrigin}/relay/site/${endpoint}`,
215
+ {
216
+ method: "POST",
217
+ headers: {
218
+ "content-type": "application/json",
219
+ ...this.#headers(endpoint, rawBody)
220
+ },
221
+ body: rawBody
222
+ }
223
+ );
224
+ return response.json();
225
+ }
226
+ async #get(endpoint) {
227
+ const url = `${this.#options.relayOrigin}/relay/site/${endpoint}?siteId=${encodeURIComponent(this.#options.siteId)}`;
228
+ const response = await this.#fetch(url, {
229
+ headers: this.#headers(endpoint, "")
230
+ });
231
+ return response.json();
232
+ }
233
+ };
234
+ var ENVELOPE_TTL_FALLBACK = 24 * 60 * 6e4;
235
+
236
+ // src/app.ts
26
237
  var DEFAULT_LIVENESS_MS = 35e3;
27
238
  var ByollmApp = class {
28
239
  #store;
240
+ #siteKeys;
29
241
  #now;
30
242
  #livenessMs;
31
243
  #delivery;
244
+ /** Present only in the cloud lane; the site's side of the relay. */
245
+ cloud;
32
246
  constructor(options) {
33
247
  this.#store = options.store;
248
+ this.#siteKeys = options.siteKeys;
34
249
  this.#now = options.now ?? Date.now;
35
250
  this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;
251
+ this.cloud = options.lane === void 0 ? void 0 : new CloudLane({
252
+ options: options.lane,
253
+ store: options.store,
254
+ siteKeys: options.siteKeys,
255
+ now: this.#now
256
+ });
36
257
  const deps = {
37
258
  ...options.noRunnerGraceMs === void 0 ? {} : { graceMs: options.noRunnerGraceMs },
38
259
  read: (jobId) => this.result(jobId),
@@ -66,7 +287,45 @@ var ByollmApp = class {
66
287
  * the app is obliged to disclose that to whoever reads it.
67
288
  */
68
289
  async enqueue(input) {
69
- const record = await this.#store.create(input, this.#now());
290
+ const parsed = KindedPayload.safeParse({
291
+ kind: input.kind,
292
+ payload: input.payload
293
+ });
294
+ if (!parsed.success) {
295
+ const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
296
+ throw new Error(`invalid ${input.kind} payload \u2014 ${detail}`);
297
+ }
298
+ const createdAt = this.#now();
299
+ const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;
300
+ const jobId = input.id ?? generateJobId();
301
+ const senderKeyId = keyId2(publicIdentityOf2(this.#siteKeys).identity);
302
+ const envelope = await seal({
303
+ plaintext: JSON.stringify(parsed.data.payload),
304
+ senderKeys: this.#siteKeys,
305
+ recipientEncryptionPublic: this.#siteKeys.encryptionPublic,
306
+ context: {
307
+ jobId,
308
+ senderKeyId,
309
+ recipientKeyId: senderKeyId,
310
+ deadlineAt: envelopeDeadlineAt,
311
+ direction: "payload"
312
+ }
313
+ });
314
+ const record = await this.#store.create(
315
+ {
316
+ ...input,
317
+ id: jobId,
318
+ envelope,
319
+ sizeClass: sizeClassOf(
320
+ payloadTextLength({
321
+ kind: input.kind,
322
+ payload: parsed.data.payload
323
+ })
324
+ )
325
+ },
326
+ createdAt
327
+ );
328
+ await this.cloud?.publish(record);
70
329
  return {
71
330
  id: record.id,
72
331
  record,
@@ -136,7 +395,18 @@ var ByollmApp = class {
136
395
  {
137
396
  owner: runner.owner,
138
397
  offerScope: capability.offerScope,
139
- account: backendDescriptor(capability.backendId).account,
398
+ // A generic backend's cost depends on its base URL, which the
399
+ // server never sees; assume the expensive reading (byollm_007 §4).
400
+ cost: backendDescriptor(capability.backendId).cost ?? "metered",
401
+ // Consent is the daemon's to hold, and it has already applied it:
402
+ // the offer scope arriving here is the *effective* one, so a
403
+ // metered backend nobody agreed to share advertises `self` and is
404
+ // refused by the scope rule above. Re-deriving consent from
405
+ // `false` here would instead refuse every backend an owner
406
+ // deliberately shared, because the server has no way to learn they
407
+ // did — the signal would be wrong in the direction that breaks
408
+ // working setups.
409
+ spend: { acknowledged: true },
140
410
  // Same conservative assumption the claim path makes: the server
141
411
  // cannot see a remote daemon's local allowlist (protocol §4.2).
142
412
  locallyAllows: () => true
@@ -218,6 +488,45 @@ function normalizeUserCode(input) {
218
488
  return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;
219
489
  }
220
490
 
491
+ // src/keys.ts
492
+ import { StoredKeys, generateKeys, publicIdentityOf as publicIdentityOf3 } from "@byollm/protocol";
493
+ import { fingerprint } from "@byollm/protocol";
494
+ var generateSiteKeys = (now = Date.now()) => generateKeys(now);
495
+ function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
496
+ const raw = env[variable];
497
+ if (raw === void 0 || raw === "") {
498
+ throw new Error(
499
+ `${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.`
500
+ );
501
+ }
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
505
+ } catch {
506
+ throw new Error(
507
+ `${variable} is not base64-encoded JSON. It should be exactly what \`npx @byollm/server keygen\` printed.`
508
+ );
509
+ }
510
+ const result = StoredKeys.safeParse(parsed);
511
+ if (!result.success) {
512
+ throw new Error(
513
+ `${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.`
514
+ );
515
+ }
516
+ return result.data;
517
+ }
518
+ function formatSiteKeys(keys) {
519
+ const encoded = Buffer.from(JSON.stringify(keys)).toString("base64");
520
+ return `# Set this as BYOLLM_SITE_KEYS. It is a secret: it is this site's
521
+ # identity, and anything holding it can be this site.
522
+ BYOLLM_SITE_KEYS=${encoded}
523
+
524
+ # Fingerprint (not secret \u2014 show it to users so they can check what
525
+ # their daemon pinned):
526
+ # ${fingerprint(publicIdentityOf3(keys).identity)}
527
+ `;
528
+ }
529
+
221
530
  // src/memory.ts
222
531
  import {
223
532
  backendDescriptor as backendDescriptor2,
@@ -236,7 +545,7 @@ var MemoryStore = class {
236
545
  }
237
546
  // -- jobs ---------------------------------------------------------------
238
547
  create(input, now) {
239
- const id = input.id ?? generateJobId();
548
+ const id = input.id;
240
549
  const existing = this.#jobs.get(id);
241
550
  if (existing) return Promise.resolve(existing);
242
551
  const dependsOn = [...input.dependsOn ?? []];
@@ -246,7 +555,8 @@ var MemoryStore = class {
246
555
  const job = {
247
556
  id,
248
557
  kind: input.kind,
249
- payload: input.payload,
558
+ envelope: input.envelope,
559
+ sizeClass: input.sizeClass,
250
560
  audience: input.audience ?? "self",
251
561
  owner: input.owner,
252
562
  audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
@@ -264,7 +574,7 @@ var MemoryStore = class {
264
574
  provenance: null,
265
575
  updatedAt: now
266
576
  };
267
- this.#jobs.set(id, job);
577
+ this.#write(id, job);
268
578
  return Promise.resolve(job);
269
579
  }
270
580
  get(jobId) {
@@ -283,13 +593,16 @@ var MemoryStore = class {
283
593
  ...job,
284
594
  state: "claimed",
285
595
  lease: {
596
+ // A fresh id per grant. Two claims of the same job by the same
597
+ // runner are two different leases, and must be distinguishable.
598
+ id: generateLeaseId(),
286
599
  runnerId: args.runnerId,
287
600
  expiresAt: args.now + args.leaseMs
288
601
  },
289
602
  attempts: job.attempts + 1,
290
603
  updatedAt: args.now
291
604
  };
292
- this.#jobs.set(job.id, updated);
605
+ this.#write(job.id, updated);
293
606
  claimed.push(updated);
294
607
  }
295
608
  return Promise.resolve(claimed);
@@ -313,9 +626,16 @@ var MemoryStore = class {
313
626
  {
314
627
  owner: args.runnerOwner,
315
628
  offerScope: capability.offerScope,
316
- // From the registry, not a local guess — the subscription self-lock
317
- // must mean the same thing on both sides of the wire.
318
- account: backendDescriptor2(capability.backendId).account,
629
+ // From the registry, not a local guess — the cost rules must mean the
630
+ // same thing on both sides of the wire. The server cannot see a
631
+ // remote daemon's base URL, so a generic backend with no declared
632
+ // cost is treated as metered: the expensive side, and the daemon
633
+ // refuses anyway if it disagrees (byollm_007 §2).
634
+ cost: backendDescriptor2(capability.backendId).cost ?? "metered",
635
+ // Nor can it see the owner's spend consent. It offers; the daemon is
636
+ // the enforcing side and releases with `refused` if its own rules say
637
+ // no — the same shape as the `named` allowlist.
638
+ spend: { acknowledged: true },
319
639
  // The server cannot see a remote daemon's local allowlist and must
320
640
  // not pretend to (protocol §4.2). It admits the job here; the daemon
321
641
  // is the enforcing side and releases with `refused` if its own list
@@ -329,9 +649,9 @@ var MemoryStore = class {
329
649
  this.#expireDueSync(args.now);
330
650
  const renewed = [];
331
651
  const lost = [];
332
- for (const jobId of args.jobIds) {
652
+ for (const { jobId, leaseId } of args.leases) {
333
653
  const job = this.#jobs.get(jobId);
334
- if (!job || job.lease?.runnerId !== args.runnerId) {
654
+ if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
335
655
  lost.push(jobId);
336
656
  continue;
337
657
  }
@@ -340,16 +660,40 @@ var MemoryStore = class {
340
660
  continue;
341
661
  }
342
662
  const expiresAt = args.now + args.leaseMs;
343
- this.#jobs.set(jobId, {
663
+ this.#write(jobId, {
344
664
  ...job,
345
665
  state: "running",
346
- lease: { runnerId: args.runnerId, expiresAt },
666
+ // Renewal extends the existing grant; it does not mint a new one.
667
+ lease: { ...job.lease, expiresAt },
347
668
  updatedAt: args.now
348
669
  });
349
670
  renewed.push({ jobId, expiresAt });
350
671
  }
351
672
  return Promise.resolve({ renewed, lost });
352
673
  }
674
+ adopt(args) {
675
+ const job = this.#jobs.get(args.jobId);
676
+ if (!job) return Promise.resolve(null);
677
+ if (job.state !== "queued" && job.state !== "claimed") {
678
+ return Promise.resolve(null);
679
+ }
680
+ if (job.lease && job.lease.id !== args.leaseId) {
681
+ return Promise.resolve(null);
682
+ }
683
+ const updated = {
684
+ ...job,
685
+ state: "claimed",
686
+ lease: {
687
+ id: args.leaseId,
688
+ // No runner: this site never paired with the machine holding it.
689
+ runnerId: "",
690
+ expiresAt: args.expiresAt
691
+ },
692
+ updatedAt: args.now
693
+ };
694
+ this.#write(updated.id, updated);
695
+ return Promise.resolve(updated);
696
+ }
353
697
  complete(args) {
354
698
  const job = this.#jobs.get(args.jobId);
355
699
  if (!job) return Promise.resolve({ accepted: false, job: null });
@@ -359,7 +703,8 @@ var MemoryStore = class {
359
703
  if (job.state === "expired") {
360
704
  return Promise.resolve({ accepted: false, job });
361
705
  }
362
- if (job.lease?.runnerId !== args.runnerId) {
706
+ const holds = args.holder.by === "runner" ? job.lease?.runnerId === args.holder.runnerId : job.lease?.id === args.holder.leaseId;
707
+ if (!holds) {
363
708
  return Promise.resolve({ accepted: false, job });
364
709
  }
365
710
  const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
@@ -371,7 +716,7 @@ var MemoryStore = class {
371
716
  provenance: args.provenance,
372
717
  updatedAt: args.now
373
718
  };
374
- this.#jobs.set(job.id, updated);
719
+ this.#write(job.id, updated);
375
720
  this.#cancelRequests.delete(job.id);
376
721
  if (state === "ok") this.#unblockDependents(job.id, args.now);
377
722
  return Promise.resolve({ accepted: true, job: updated });
@@ -392,16 +737,66 @@ var MemoryStore = class {
392
737
  (depId) => this.#jobs.get(depId)?.state === "ok"
393
738
  );
394
739
  if (ready) {
395
- this.#jobs.set(job.id, { ...job, claimableAt: now, updatedAt: now });
740
+ this.#write(job.id, { ...job, claimableAt: now, updatedAt: now });
741
+ }
742
+ }
743
+ }
744
+ /**
745
+ * Watchers, by job id (byollm_009 §8.3).
746
+ *
747
+ * A `Set` per job so an unsubscribe removes exactly the handler it
748
+ * registered — two waiters on the same job are ordinary, and removing by
749
+ * job id alone would silently cancel someone else's wait.
750
+ */
751
+ #watchers = /* @__PURE__ */ new Map();
752
+ subscribe(jobId, onChange) {
753
+ const existing = this.#watchers.get(jobId) ?? /* @__PURE__ */ new Set();
754
+ existing.add(onChange);
755
+ this.#watchers.set(jobId, existing);
756
+ let live = true;
757
+ return () => {
758
+ if (!live) return;
759
+ live = false;
760
+ const set = this.#watchers.get(jobId);
761
+ set?.delete(onChange);
762
+ if (set?.size === 0) this.#watchers.delete(jobId);
763
+ };
764
+ }
765
+ /**
766
+ * The single write path for a job.
767
+ *
768
+ * Every mutation goes through here so notification cannot be forgotten by
769
+ * a future one. Nine call sites existed when the push seam was added, and
770
+ * "remember to notify" is not a property nine call sites keep.
771
+ */
772
+ #write(jobId, record) {
773
+ this.#jobs.set(jobId, record);
774
+ this.#notify(jobId);
775
+ }
776
+ /**
777
+ * Tell anyone watching that a job changed.
778
+ *
779
+ * A throwing watcher must not corrupt the store's own bookkeeping, so each
780
+ * is isolated: this runs inside write paths, and one bad listener taking
781
+ * out an unrelated write would be a far worse failure than a missed
782
+ * notification.
783
+ */
784
+ #notify(jobId) {
785
+ for (const watcher of this.#watchers.get(jobId) ?? []) {
786
+ try {
787
+ watcher();
788
+ } catch {
396
789
  }
397
790
  }
398
791
  }
399
792
  release(args) {
400
793
  const released = [];
401
- for (const jobId of args.jobIds) {
794
+ for (const { jobId, leaseId } of args.leases) {
402
795
  const job = this.#jobs.get(jobId);
403
- if (!job || job.lease?.runnerId !== args.runnerId) continue;
404
- this.#jobs.set(jobId, {
796
+ if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
797
+ continue;
798
+ }
799
+ this.#write(jobId, {
405
800
  ...job,
406
801
  state: "queued",
407
802
  lease: null,
@@ -443,7 +838,7 @@ var MemoryStore = class {
443
838
  claimableAt: now,
444
839
  updatedAt: now
445
840
  };
446
- this.#jobs.set(job.id, requeued);
841
+ this.#write(job.id, requeued);
447
842
  changed.push(requeued);
448
843
  }
449
844
  }
@@ -458,7 +853,7 @@ var MemoryStore = class {
458
853
  lease: null,
459
854
  updatedAt: now
460
855
  };
461
- this.#jobs.set(job.id, expired);
856
+ this.#write(job.id, expired);
462
857
  changed.push(expired);
463
858
  }
464
859
  return changed;
@@ -473,7 +868,7 @@ var MemoryStore = class {
473
868
  lease: null,
474
869
  updatedAt: now
475
870
  };
476
- this.#jobs.set(jobId, canceled);
871
+ this.#write(jobId, canceled);
477
872
  return Promise.resolve(canceled);
478
873
  }
479
874
  if (job.state === "claimed" || job.state === "running") {
@@ -525,6 +920,9 @@ var MemoryStore = class {
525
920
  id: args.runnerId,
526
921
  owner: args.owner,
527
922
  tokenHash: args.tokenHash,
923
+ // Carried from the pairing, not re-supplied at approval: the user
924
+ // approved a specific machine, and the runner must be that machine.
925
+ device: pairing.device,
528
926
  label: pairing.label,
529
927
  platform: pairing.platform,
530
928
  daemonVersion: pairing.daemonVersion,
@@ -613,22 +1011,26 @@ function capabilityFor(capabilities, kind) {
613
1011
  export {
614
1012
  ByollmApp,
615
1013
  ByollmHandlers,
1014
+ CloudLane,
616
1015
  MemoryStore,
617
1016
  NoRunnerAvailableError,
618
1017
  PollingDelivery,
619
1018
  ResultTimeoutError,
620
1019
  SERVED_PROTOCOL_VERSION,
621
- bearerFrom,
622
1020
  capabilityFor,
623
1021
  createFetchHandler,
1022
+ formatSiteKeys,
624
1023
  generateDeviceCode,
625
1024
  generateJobId,
626
1025
  generateRunnerId,
627
1026
  generateRunnerToken,
1027
+ generateSiteKeys,
628
1028
  generateUserCode,
629
1029
  hashSecret,
630
1030
  normalizeUserCode,
631
1031
  routeEndpoint,
632
- secretsMatch
1032
+ secretsMatch,
1033
+ signatureFrom,
1034
+ siteKeysFromEnv
633
1035
  };
634
1036
  //# sourceMappingURL=index.js.map