@byollm/server 0.1.0-alpha.4 → 0.1.0-alpha.41

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
@@ -2,35 +2,355 @@ import {
2
2
  ByollmHandlers,
3
3
  SERVED_PROTOCOL_VERSION,
4
4
  createFetchHandler,
5
+ deadlineFor,
5
6
  generateDeviceCode,
6
7
  generateJobId,
7
8
  generateLeaseId,
8
9
  generateRunnerId,
9
- generateRunnerToken,
10
10
  generateUserCode,
11
11
  hashSecret,
12
+ resealForDevice,
12
13
  routeEndpoint,
13
14
  secretsMatch,
14
15
  signatureFrom
15
- } from "./chunk-MGJX6626.js";
16
+ } from "./chunk-K5E6JS5A.js";
16
17
  import {
17
18
  NoRunnerAvailableError,
18
19
  PollingDelivery,
19
20
  ResultTimeoutError
20
- } from "./chunk-7RKXFPBZ.js";
21
+ } from "./chunk-SAK63KNU.js";
21
22
 
22
23
  // src/app.ts
23
24
  import {
24
25
  ENVELOPE_MAX_AGE_MS,
25
26
  KindedPayload,
26
- keyId,
27
+ keyId as keyId2,
27
28
  payloadTextLength,
28
- publicIdentityOf,
29
+ publicIdentityOf as publicIdentityOf2,
29
30
  seal,
30
31
  sizeClassOf,
31
32
  backendDescriptor,
32
33
  matchAudience
33
34
  } from "@byollm/protocol";
35
+
36
+ // src/cloud.ts
37
+ import {
38
+ PROTOCOL_VERSION,
39
+ SealedOutcome,
40
+ keyId,
41
+ open,
42
+ publicIdentityOf,
43
+ provenanceFor,
44
+ signSiteRequest
45
+ } from "@byollm/protocol";
46
+ var RelayUnavailable = class extends Error {
47
+ retryable;
48
+ /** The protocol's own code, when the relay sent one. */
49
+ code;
50
+ constructor(message, retryable, code) {
51
+ super(message);
52
+ this.name = "RelayUnavailable";
53
+ this.retryable = retryable;
54
+ this.code = code;
55
+ }
56
+ };
57
+ var CloudLane = class {
58
+ #options;
59
+ #store;
60
+ #siteKeys;
61
+ #now;
62
+ #fetch;
63
+ constructor(deps) {
64
+ this.#options = deps.options;
65
+ this.#store = deps.store;
66
+ this.#siteKeys = deps.siteKeys;
67
+ this.#now = deps.now;
68
+ this.#fetch = deps.options.fetch ?? globalThis.fetch;
69
+ }
70
+ /**
71
+ * Publish a job's stub for routing.
72
+ *
73
+ * The stub and nothing else — byollm_009 §6 makes that exhaustive by
74
+ * construction, so this cannot leak a payload even by mistake: there is no
75
+ * field on `JobStub` to put one in.
76
+ */
77
+ async publish(record) {
78
+ const stub = {
79
+ id: record.id,
80
+ kind: record.kind,
81
+ owner: record.owner,
82
+ // This site, by its identity key id — Amendment A §A.3. The relay
83
+ // already knows which site it is routing for, so this discloses nothing
84
+ // new to it; what it adds is that the *daemon* can check the stub
85
+ // against the envelope's `senderKeyId` without asking the relay.
86
+ site: keyId(publicIdentityOf(this.#siteKeys).identity),
87
+ audience: record.audience,
88
+ // `audienceAllow` is deliberately **not** published — cloud_008 §0.2.
89
+ //
90
+ // It is a list of the people who may run this job, and on the direct
91
+ // plane that is unremarkable: the site authored the list and the site is
92
+ // the upstream, so the party receiving it already has it. Through a
93
+ // relay it is a third party, and byollm_009 §6's enumerated metadata —
94
+ // "exhaustive and normative… what an upstream can see, stated as a
95
+ // commitment" — does not include it. It was reaching the relay on every
96
+ // named-audience job.
97
+ //
98
+ // Nothing is lost by withholding it, which is why this is a Tier 0 fix
99
+ // rather than a trade. `matchAudience` treats it as a *narrowing*:
100
+ // `job.audienceAllow !== undefined && !includes(daemon.owner)` refuses,
101
+ // and its absence simply falls through to the checks that actually
102
+ // enforce — the daemon's own allowlist (`NAMED_LOCAL_ALLOWLIST`) and the
103
+ // backend's offer scope. On this lane the relay narrows too, from the
104
+ // control plane's rosters. The enforcement was never here.
105
+ sizeClass: record.sizeClass,
106
+ streaming: false,
107
+ // The relay needs *a* deadline to bound routing. A job without one gets
108
+ // the envelope's, which is the outer bound on how long the ciphertext
109
+ // is worth carrying — never longer than the work could possibly matter.
110
+ // The same fallback the direct plane uses — cloud_008 Tier 4, finding
111
+ // 31. This said `createdAt + ENVELOPE_TTL_FALLBACK`, a local constant
112
+ // whose value happened to equal `ENVELOPE_MAX_AGE_MS`; the direct plane
113
+ // said `(claimableAt ?? now) + ttlMs`. One field, two meanings, and a
114
+ // job that was blocked on a dependency got a deadline measured from
115
+ // when it was *created* on one lane and from when it became *claimable*
116
+ // on the other.
117
+ deadlineAt: deadlineFor(record, this.#now())
118
+ };
119
+ await this.#post("enqueue", {
120
+ siteId: this.#options.siteId,
121
+ stub
122
+ });
123
+ }
124
+ /**
125
+ * Withdraw a job at the relay — cloud_008 §2.2.
126
+ *
127
+ * `app.cancel()` marks the site's own row terminal, which stops the *next*
128
+ * seal. It cannot stop a device that is already running the work, because
129
+ * on this lane the site is not the upstream: only the relay talks to the
130
+ * daemon, and it answered `cancel: []` unconditionally.
131
+ *
132
+ * So the cancellation has to travel. The relay marks the job, stops
133
+ * offering it, and names it to the holding device at its next heartbeat —
134
+ * the same path the direct plane has always had, arriving one hop later.
135
+ */
136
+ async cancel(jobId) {
137
+ await this.#post("cancel", { siteId: this.#options.siteId, jobId });
138
+ }
139
+ /**
140
+ * One cycle: seal for anything claimed, collect anything finished.
141
+ *
142
+ * Idempotent and safe to call as often as you like. Exposed as a single
143
+ * cycle rather than hidden behind a timer so a caller decides its own
144
+ * cadence — a serverless site runs it on a cron, a long-lived one on an
145
+ * interval, and a test runs it exactly when it means to.
146
+ */
147
+ async pump() {
148
+ const sealed = [];
149
+ const refused = [];
150
+ const completed = [];
151
+ try {
152
+ return await this.#cycle(sealed, refused, completed);
153
+ } catch (error) {
154
+ if (error instanceof RelayUnavailable && error.retryable) {
155
+ return { sealed, completed, refused, deferred: error.message };
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+ async #cycle(sealed, refused, completed) {
161
+ const pending = await this.#get("pending");
162
+ for (const claim of pending.jobs) {
163
+ const record = await this.#store.get(claim.jobId);
164
+ if (!record) continue;
165
+ const resealed = await resealForDevice({
166
+ siteKeys: this.#siteKeys,
167
+ job: {
168
+ id: record.id,
169
+ envelope: record.envelope,
170
+ createdAt: record.createdAt
171
+ },
172
+ device: claim.device
173
+ });
174
+ if (!resealed.ok) {
175
+ refused.push(claim.jobId);
176
+ continue;
177
+ }
178
+ const adopted = await this.#store.adopt({
179
+ jobId: claim.jobId,
180
+ leaseId: claim.leaseId,
181
+ expiresAt: claim.leaseExpiresAt,
182
+ now: this.#now()
183
+ });
184
+ if (!adopted) {
185
+ refused.push(claim.jobId);
186
+ continue;
187
+ }
188
+ await this.#post("payload", {
189
+ siteId: this.#options.siteId,
190
+ jobId: claim.jobId,
191
+ envelope: resealed.envelope
192
+ });
193
+ sealed.push(claim.jobId);
194
+ }
195
+ const finished = await this.#get("results");
196
+ for (const done of finished.jobs) {
197
+ const record = await this.#store.get(done.jobId);
198
+ if (!record || record.state === "ok" || record.state === "error") {
199
+ continue;
200
+ }
201
+ const outcome = await this.#openResult(done);
202
+ if (!outcome) {
203
+ refused.push(done.jobId);
204
+ continue;
205
+ }
206
+ await this.#store.complete({
207
+ jobId: done.jobId,
208
+ // The relay named the device; the signature above proved it — §3.6.
209
+ runnerId: done.runnerId,
210
+ // The grant, not the machine: this site never paired with the device
211
+ // that ran it, and the signature it verified above is the stronger
212
+ // claim about who did.
213
+ holder: { by: "lease", leaseId: done.leaseId },
214
+ outcome: outcome.outcome,
215
+ provenance: provenanceFor({
216
+ audience: record.audience,
217
+ runnerId: done.runnerId,
218
+ // The owner, from the relay's own record of who claimed it — not a
219
+ // key id. cloud_008 §2.5: this said `keyId(device.identity)`, which
220
+ // put a key id where the direct plane puts a user id, so an app
221
+ // comparing provenance across lanes compared two namespaces and got
222
+ // `false` for the same person. The device's key is still what the
223
+ // signature was verified against, above; that is a different
224
+ // question from whose machine it is.
225
+ runnerOwner: done.runnerOwner,
226
+ // From the envelope, not invented — cloud_008 §2.5. These were
227
+ // hardcoded `"http"` and `"unknown"` because the daemon's declared
228
+ // values stopped at the relay, which is right: a blind relay acts
229
+ // on neither. Sealing them carries them past it untouched.
230
+ backendClass: outcome.ran.backendClass,
231
+ model: outcome.ran.model
232
+ }),
233
+ now: this.#now()
234
+ });
235
+ completed.push(done.jobId);
236
+ }
237
+ return { sealed, completed, refused };
238
+ }
239
+ /**
240
+ * Open a sealed result and verify it came from the device that claimed it.
241
+ *
242
+ * The relay says which device ran the job; this checks that claim against a
243
+ * signature the relay cannot produce. A relay that named the wrong device
244
+ * gets a refusal, not a stored result — which is what keeps `RELAY_BLIND`
245
+ * from quietly becoming `RELAY_TRUSTED`.
246
+ */
247
+ async #openResult(done) {
248
+ const opened = await open({
249
+ envelope: done.envelope,
250
+ recipientKeys: this.#siteKeys,
251
+ senderIdentityPublic: done.device.identity,
252
+ expected: {
253
+ jobId: done.jobId,
254
+ senderKeyId: keyId(done.device.identity),
255
+ recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),
256
+ direction: "result"
257
+ }
258
+ });
259
+ if (!opened.ok) return null;
260
+ let parsed;
261
+ try {
262
+ parsed = JSON.parse(opened.plaintext);
263
+ } catch {
264
+ return null;
265
+ }
266
+ const sealed = SealedOutcome.safeParse(parsed);
267
+ if (!sealed.success) return null;
268
+ if (sealed.data.outcome.outcome !== done.disposition) return null;
269
+ return sealed.data;
270
+ }
271
+ /**
272
+ * Sign a site-plane call with this site's identity key.
273
+ *
274
+ * The same scheme the daemon uses against an upstream, because the site is
275
+ * in the same position: an outbound caller whose key the relay already holds
276
+ * for other reasons. Nothing else authenticates this plane — a relay that
277
+ * took the `siteId` in a body at face value would let anyone enqueue work in
278
+ * a site's name and read who claimed it.
279
+ */
280
+ #headers(endpoint, rawBody) {
281
+ const signature = signSiteRequest(this.#siteKeys, {
282
+ endpoint,
283
+ siteId: this.#options.siteId,
284
+ issuedAt: this.#now(),
285
+ body: rawBody
286
+ });
287
+ return {
288
+ "x-byollm-site": this.#options.siteId,
289
+ "x-byollm-issued-at": String(signature.issuedAt),
290
+ "x-byollm-signature": signature.signature
291
+ };
292
+ }
293
+ /**
294
+ * A relay answer, checked before it is believed — alpha.31.
295
+ *
296
+ * The bug this closes is one line long and its shape is general: a response
297
+ * body used without looking at the status. The daemon's client has always
298
+ * done this properly (`client.ts` maps every status to a typed refusal); the
299
+ * site's lane parsed JSON and hoped.
300
+ *
301
+ * Two classes, because they need opposite handling. **Retryable** — 503 from
302
+ * a draining pod, 429, 5xx, and the protocol's own `not-ready` — means the
303
+ * work is still there and this cycle should end quietly. **Refused** — a bad
304
+ * signature, an unknown site, a version this relay does not speak — will
305
+ * still be true in five seconds, and swallowing it would leave a site
306
+ * silently disconnected from its own users.
307
+ */
308
+ async #answer(response, endpoint) {
309
+ if (response.ok) return response.json();
310
+ let code = "";
311
+ let message;
312
+ try {
313
+ const body = await response.json();
314
+ code = body.error ?? "";
315
+ message = body.message ?? "";
316
+ } catch {
317
+ message = `HTTP ${String(response.status)}`;
318
+ }
319
+ const retryable = response.status >= 500 || response.status === 429 || code === "not-ready" || code === "server-error";
320
+ throw new RelayUnavailable(
321
+ `${endpoint}: ${code || "refused"} \u2014 ${message}`,
322
+ retryable,
323
+ code
324
+ );
325
+ }
326
+ async #post(endpoint, body) {
327
+ const rawBody = JSON.stringify({
328
+ protocolVersion: PROTOCOL_VERSION,
329
+ ...body
330
+ });
331
+ const response = await this.#fetch(
332
+ `${this.#options.relayOrigin}/relay/site/${endpoint}`,
333
+ {
334
+ method: "POST",
335
+ headers: {
336
+ "content-type": "application/json",
337
+ ...this.#headers(endpoint, rawBody)
338
+ },
339
+ body: rawBody
340
+ }
341
+ );
342
+ return this.#answer(response, endpoint);
343
+ }
344
+ async #get(endpoint) {
345
+ const url = `${this.#options.relayOrigin}/relay/site/${endpoint}?siteId=${encodeURIComponent(this.#options.siteId)}&protocolVersion=${encodeURIComponent(PROTOCOL_VERSION)}`;
346
+ const response = await this.#fetch(url, {
347
+ headers: this.#headers(endpoint, "")
348
+ });
349
+ return this.#answer(response, endpoint);
350
+ }
351
+ };
352
+
353
+ // src/app.ts
34
354
  var DEFAULT_LIVENESS_MS = 35e3;
35
355
  var ByollmApp = class {
36
356
  #store;
@@ -38,11 +358,19 @@ var ByollmApp = class {
38
358
  #now;
39
359
  #livenessMs;
40
360
  #delivery;
361
+ /** Present only in the cloud lane; the site's side of the relay. */
362
+ cloud;
41
363
  constructor(options) {
42
364
  this.#store = options.store;
43
365
  this.#siteKeys = options.siteKeys;
44
366
  this.#now = options.now ?? Date.now;
45
367
  this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;
368
+ this.cloud = options.lane === void 0 ? void 0 : new CloudLane({
369
+ options: options.lane,
370
+ store: options.store,
371
+ siteKeys: options.siteKeys,
372
+ now: this.#now
373
+ });
46
374
  const deps = {
47
375
  ...options.noRunnerGraceMs === void 0 ? {} : { graceMs: options.noRunnerGraceMs },
48
376
  read: (jobId) => this.result(jobId),
@@ -87,7 +415,7 @@ var ByollmApp = class {
87
415
  const createdAt = this.#now();
88
416
  const envelopeDeadlineAt = createdAt + ENVELOPE_MAX_AGE_MS;
89
417
  const jobId = input.id ?? generateJobId();
90
- const senderKeyId = keyId(publicIdentityOf(this.#siteKeys).identity);
418
+ const senderKeyId = keyId2(publicIdentityOf2(this.#siteKeys).identity);
91
419
  const envelope = await seal({
92
420
  plaintext: JSON.stringify(parsed.data.payload),
93
421
  senderKeys: this.#siteKeys,
@@ -114,6 +442,7 @@ var ByollmApp = class {
114
442
  },
115
443
  createdAt
116
444
  );
445
+ await this.cloud?.publish(record);
117
446
  return {
118
447
  id: record.id,
119
448
  record,
@@ -134,7 +463,7 @@ var ByollmApp = class {
134
463
  * Check `provenance.untrusted` before rendering. It is true for every
135
464
  * `named`/`public` job, because that text came from someone else's machine
136
465
  * and the app must not present it as its own AI's answer
137
- * ({@link MUSTS.RESULT_PROVENANCE}).
466
+ * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
138
467
  */
139
468
  async result(jobId) {
140
469
  const job = await this.job(jobId);
@@ -148,7 +477,11 @@ var ByollmApp = class {
148
477
  }
149
478
  /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
150
479
  async cancel(jobId) {
151
- return this.#store.cancel(jobId, this.#now());
480
+ const cancelled = await this.#store.cancel(jobId, this.#now());
481
+ if (cancelled && this.cloud) {
482
+ await this.cloud.cancel(jobId).catch(() => void 0);
483
+ }
484
+ return cancelled;
152
485
  }
153
486
  /**
154
487
  * Is there a live runner that could take a job of this shape?
@@ -226,13 +559,10 @@ var ByollmApp = class {
226
559
  * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).
227
560
  */
228
561
  async approvePairing(args) {
229
- const token = generateRunnerToken();
230
562
  return this.#store.approvePairing({
231
563
  userCode: normalizeUserCode(args.userCode),
232
564
  owner: args.owner,
233
565
  runnerId: generateRunnerId(),
234
- runnerToken: token,
235
- tokenHash: hashSecret(token),
236
566
  now: this.#now()
237
567
  });
238
568
  }
@@ -277,7 +607,7 @@ function normalizeUserCode(input) {
277
607
  }
278
608
 
279
609
  // src/keys.ts
280
- import { StoredKeys, generateKeys, publicIdentityOf as publicIdentityOf2 } from "@byollm/protocol";
610
+ import { StoredKeys, generateKeys, publicIdentityOf as publicIdentityOf3 } from "@byollm/protocol";
281
611
  import { fingerprint } from "@byollm/protocol";
282
612
  var generateSiteKeys = (now = Date.now()) => generateKeys(now);
283
613
  function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
@@ -305,13 +635,27 @@ function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
305
635
  }
306
636
  function formatSiteKeys(keys) {
307
637
  const encoded = Buffer.from(JSON.stringify(keys)).toString("base64");
308
- return `# Set this as BYOLLM_SITE_KEYS. It is a secret: it is this site's
309
- # identity, and anything holding it can be this site.
638
+ const pub = publicIdentityOf3(keys);
639
+ return `# \u2500\u2500 1. SECRET \u2014 set this on your server, and nowhere else \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
640
+ #
641
+ # This is the site's identity. Anything holding it can *be* this site,
642
+ # so it goes wherever your deployment keeps secrets \u2014 never in a repo,
643
+ # never in a browser, never pasted into a dashboard.
310
644
  BYOLLM_SITE_KEYS=${encoded}
311
645
 
312
- # Fingerprint (not secret \u2014 show it to users so they can check what
313
- # their daemon pinned):
314
- # ${fingerprint(publicIdentityOf2(keys).identity)}
646
+ # \u2500\u2500 2. PUBLIC \u2014 paste this line into the byollm dashboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
647
+ #
648
+ # The public half. It proves signatures and seals nothing, so it is
649
+ # safe to publish \u2014 which is the point: users pin it, and the relay
650
+ # cannot forge work without the secret above.
651
+ ${JSON.stringify(pub)}
652
+
653
+ # \u2500\u2500 3. Fingerprint \u2014 what a person compares by eye \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
654
+ #
655
+ # A fingerprint is not secret. Show it on your site so somebody
656
+ # connecting can check it against what their daemon printed.
657
+ # The dashboard derives this itself, so there is nothing to paste.
658
+ # ${fingerprint(pub.identity)}
315
659
  `;
316
660
  }
317
661
 
@@ -351,6 +695,7 @@ var MemoryStore = class {
351
695
  dependsOn,
352
696
  state: "queued",
353
697
  lease: null,
698
+ completedByLeaseId: null,
354
699
  createdAt: now,
355
700
  // The TTL clock starts here only if nothing blocks the job.
356
701
  claimableAt: blocked ? null : now,
@@ -440,11 +785,11 @@ var MemoryStore = class {
440
785
  for (const { jobId, leaseId } of args.leases) {
441
786
  const job = this.#jobs.get(jobId);
442
787
  if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
443
- lost.push(jobId);
788
+ lost.push({ jobId, leaseId });
444
789
  continue;
445
790
  }
446
791
  if (job.state !== "claimed" && job.state !== "running") {
447
- lost.push(jobId);
792
+ lost.push({ jobId, leaseId });
448
793
  continue;
449
794
  }
450
795
  const expiresAt = args.now + args.leaseMs;
@@ -459,16 +804,45 @@ var MemoryStore = class {
459
804
  }
460
805
  return Promise.resolve({ renewed, lost });
461
806
  }
807
+ adopt(args) {
808
+ const job = this.#jobs.get(args.jobId);
809
+ if (!job) return Promise.resolve(null);
810
+ if (job.state !== "queued" && job.state !== "claimed") {
811
+ return Promise.resolve(null);
812
+ }
813
+ if (job.lease && job.lease.id !== args.leaseId) {
814
+ return Promise.resolve(null);
815
+ }
816
+ const updated = {
817
+ ...job,
818
+ state: "claimed",
819
+ lease: {
820
+ id: args.leaseId,
821
+ // No runner: this site never paired with the machine holding it.
822
+ runnerId: "",
823
+ expiresAt: args.expiresAt
824
+ },
825
+ updatedAt: args.now
826
+ };
827
+ this.#write(updated.id, updated);
828
+ return Promise.resolve(updated);
829
+ }
462
830
  complete(args) {
463
831
  const job = this.#jobs.get(args.jobId);
464
832
  if (!job) return Promise.resolve({ accepted: false, job: null });
465
833
  if (job.state === "ok" || job.state === "error" || job.state === "canceled") {
834
+ const sameDevice = job.provenance?.runnerId !== void 0 && job.provenance.runnerId === args.runnerId;
835
+ const sameGrant = args.holder.by === "lease" && job.completedByLeaseId !== null && job.completedByLeaseId === args.holder.leaseId;
836
+ if (sameDevice && sameGrant) {
837
+ return Promise.resolve({ accepted: false, duplicate: true, job });
838
+ }
466
839
  return Promise.resolve({ accepted: false, job });
467
840
  }
468
841
  if (job.state === "expired") {
469
842
  return Promise.resolve({ accepted: false, job });
470
843
  }
471
- if (job.lease?.runnerId !== args.runnerId) {
844
+ const holds = args.holder.by === "runner" ? job.lease?.runnerId === args.holder.runnerId : job.lease?.id === args.holder.leaseId;
845
+ if (!holds) {
472
846
  return Promise.resolve({ accepted: false, job });
473
847
  }
474
848
  const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
@@ -476,6 +850,8 @@ var MemoryStore = class {
476
850
  ...job,
477
851
  state,
478
852
  lease: null,
853
+ // The grant that recorded it, kept after the lease is dropped — §3.6.
854
+ completedByLeaseId: args.holder.by === "lease" ? args.holder.leaseId : job.lease?.id ?? null,
479
855
  outcome: args.outcome,
480
856
  provenance: args.provenance,
481
857
  updatedAt: args.now
@@ -564,6 +940,7 @@ var MemoryStore = class {
564
940
  ...job,
565
941
  state: "queued",
566
942
  lease: null,
943
+ completedByLeaseId: null,
567
944
  // Newly available again, so the TTL clock restarts here too.
568
945
  claimableAt: args.now,
569
946
  // A refusal is remembered, or the pair spins between claim and
@@ -593,6 +970,7 @@ var MemoryStore = class {
593
970
  ...job,
594
971
  state: "queued",
595
972
  lease: null,
973
+ completedByLeaseId: null,
596
974
  // The TTL clock restarts: it measures how long a job has waited
597
975
  // *unclaimed*, and this job has just become available again. Without
598
976
  // this, a job whose runner died would expire for time it spent being
@@ -615,6 +993,7 @@ var MemoryStore = class {
615
993
  ...job,
616
994
  state: "expired",
617
995
  lease: null,
996
+ completedByLeaseId: null,
618
997
  updatedAt: now
619
998
  };
620
999
  this.#write(job.id, expired);
@@ -630,6 +1009,7 @@ var MemoryStore = class {
630
1009
  ...job,
631
1010
  state: "canceled",
632
1011
  lease: null,
1012
+ completedByLeaseId: null,
633
1013
  updatedAt: now
634
1014
  };
635
1015
  this.#write(jobId, canceled);
@@ -650,9 +1030,7 @@ var MemoryStore = class {
650
1030
  }
651
1031
  listCancelRequests(runnerId) {
652
1032
  return Promise.resolve(
653
- [...this.#cancelRequests].filter(
654
- (jobId) => this.#jobs.get(jobId)?.lease?.runnerId === runnerId
655
- )
1033
+ [...this.#cancelRequests].map((jobId) => ({ jobId, lease: this.#jobs.get(jobId)?.lease })).filter((row) => row.lease?.runnerId === runnerId).map((row) => ({ jobId: row.jobId, leaseId: row.lease?.id ?? "" }))
656
1034
  );
657
1035
  }
658
1036
  // -- pairing and runners -------------------------------------------------
@@ -683,7 +1061,6 @@ var MemoryStore = class {
683
1061
  const runner = {
684
1062
  id: args.runnerId,
685
1063
  owner: args.owner,
686
- tokenHash: args.tokenHash,
687
1064
  // Carried from the pairing, not re-supplied at approval: the user
688
1065
  // approved a specific machine, and the runner must be that machine.
689
1066
  device: pairing.device,
@@ -702,7 +1079,7 @@ var MemoryStore = class {
702
1079
  state: "approved",
703
1080
  owner: args.owner,
704
1081
  runnerId: runner.id,
705
- runnerTokenOnce: args.runnerToken
1082
+ collected: false
706
1083
  });
707
1084
  return Promise.resolve(runner);
708
1085
  }
@@ -723,17 +1100,11 @@ var MemoryStore = class {
723
1100
  if (pairing) {
724
1101
  this.#pairings.set(deviceCodeHash, {
725
1102
  ...pairing,
726
- runnerTokenOnce: null
1103
+ collected: true
727
1104
  });
728
1105
  }
729
1106
  return Promise.resolve();
730
1107
  }
731
- getRunnerByTokenHash(hash) {
732
- for (const runner of this.#runners.values()) {
733
- if (runner.tokenHash === hash) return Promise.resolve(runner);
734
- }
735
- return Promise.resolve(null);
736
- }
737
1108
  getRunner(runnerId) {
738
1109
  return Promise.resolve(this.#runners.get(runnerId) ?? null);
739
1110
  }
@@ -775,9 +1146,11 @@ function capabilityFor(capabilities, kind) {
775
1146
  export {
776
1147
  ByollmApp,
777
1148
  ByollmHandlers,
1149
+ CloudLane,
778
1150
  MemoryStore,
779
1151
  NoRunnerAvailableError,
780
1152
  PollingDelivery,
1153
+ RelayUnavailable,
781
1154
  ResultTimeoutError,
782
1155
  SERVED_PROTOCOL_VERSION,
783
1156
  capabilityFor,
@@ -786,7 +1159,6 @@ export {
786
1159
  generateDeviceCode,
787
1160
  generateJobId,
788
1161
  generateRunnerId,
789
- generateRunnerToken,
790
1162
  generateSiteKeys,
791
1163
  generateUserCode,
792
1164
  hashSecret,