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

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,23 +2,23 @@ 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
12
  resealForDevice,
13
13
  routeEndpoint,
14
14
  secretsMatch,
15
15
  signatureFrom
16
- } from "./chunk-4NIHWQAT.js";
16
+ } from "./chunk-I4MSFVRA.js";
17
17
  import {
18
18
  NoRunnerAvailableError,
19
19
  PollingDelivery,
20
20
  ResultTimeoutError
21
- } from "./chunk-7RKXFPBZ.js";
21
+ } from "./chunk-SAK63KNU.js";
22
22
 
23
23
  // src/app.ts
24
24
  import {
@@ -35,12 +35,25 @@ import {
35
35
 
36
36
  // src/cloud.ts
37
37
  import {
38
- JobOutcome,
38
+ PROTOCOL_VERSION,
39
+ SealedOutcome,
39
40
  keyId,
40
41
  open,
41
42
  publicIdentityOf,
42
- provenanceFor
43
+ provenanceFor,
44
+ signSiteRequest
43
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
+ };
44
57
  var CloudLane = class {
45
58
  #options;
46
59
  #store;
@@ -66,20 +79,64 @@ var CloudLane = class {
66
79
  id: record.id,
67
80
  kind: record.kind,
68
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),
69
87
  audience: record.audience,
70
- ...record.audienceAllow === void 0 ? {} : { audienceAllow: [...record.audienceAllow] },
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
+ ...record.service === void 0 ? {} : { service: record.service },
71
106
  sizeClass: record.sizeClass,
72
107
  streaming: false,
73
108
  // The relay needs *a* deadline to bound routing. A job without one gets
74
109
  // the envelope's, which is the outer bound on how long the ciphertext
75
110
  // is worth carrying — never longer than the work could possibly matter.
76
- deadlineAt: record.deadlineAt ?? record.createdAt + ENVELOPE_TTL_FALLBACK
111
+ // The same fallback the direct plane uses — cloud_008 Tier 4, finding
112
+ // 31. This said `createdAt + ENVELOPE_TTL_FALLBACK`, a local constant
113
+ // whose value happened to equal `ENVELOPE_MAX_AGE_MS`; the direct plane
114
+ // said `(claimableAt ?? now) + ttlMs`. One field, two meanings, and a
115
+ // job that was blocked on a dependency got a deadline measured from
116
+ // when it was *created* on one lane and from when it became *claimable*
117
+ // on the other.
118
+ deadlineAt: deadlineFor(record, this.#now())
77
119
  };
78
- await this.#post("/relay/site/enqueue", {
120
+ await this.#post("enqueue", {
79
121
  siteId: this.#options.siteId,
80
122
  stub
81
123
  });
82
124
  }
125
+ /**
126
+ * Withdraw a job at the relay — cloud_008 §2.2.
127
+ *
128
+ * `app.cancel()` marks the site's own row terminal, which stops the *next*
129
+ * seal. It cannot stop a device that is already running the work, because
130
+ * on this lane the site is not the upstream: only the relay talks to the
131
+ * daemon, and it answered `cancel: []` unconditionally.
132
+ *
133
+ * So the cancellation has to travel. The relay marks the job, stops
134
+ * offering it, and names it to the holding device at its next heartbeat —
135
+ * the same path the direct plane has always had, arriving one hop later.
136
+ */
137
+ async cancel(jobId) {
138
+ await this.#post("cancel", { siteId: this.#options.siteId, jobId });
139
+ }
83
140
  /**
84
141
  * One cycle: seal for anything claimed, collect anything finished.
85
142
  *
@@ -92,7 +149,17 @@ var CloudLane = class {
92
149
  const sealed = [];
93
150
  const refused = [];
94
151
  const completed = [];
95
- const pending = await this.#get("/relay/site/pending");
152
+ try {
153
+ return await this.#cycle(sealed, refused, completed);
154
+ } catch (error) {
155
+ if (error instanceof RelayUnavailable && error.retryable) {
156
+ return { sealed, completed, refused, deferred: error.message };
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+ async #cycle(sealed, refused, completed) {
162
+ const pending = await this.#get("pending");
96
163
  for (const claim of pending.jobs) {
97
164
  const record = await this.#store.get(claim.jobId);
98
165
  if (!record) continue;
@@ -109,20 +176,24 @@ var CloudLane = class {
109
176
  refused.push(claim.jobId);
110
177
  continue;
111
178
  }
112
- await this.#store.adopt({
179
+ const adopted = await this.#store.adopt({
113
180
  jobId: claim.jobId,
114
181
  leaseId: claim.leaseId,
115
- expiresAt: claim.awaitingUntil,
182
+ expiresAt: claim.leaseExpiresAt,
116
183
  now: this.#now()
117
184
  });
118
- await this.#post("/relay/site/payload", {
185
+ if (!adopted) {
186
+ refused.push(claim.jobId);
187
+ continue;
188
+ }
189
+ await this.#post("payload", {
119
190
  siteId: this.#options.siteId,
120
191
  jobId: claim.jobId,
121
192
  envelope: resealed.envelope
122
193
  });
123
194
  sealed.push(claim.jobId);
124
195
  }
125
- const finished = await this.#get("/relay/site/results");
196
+ const finished = await this.#get("results");
126
197
  for (const done of finished.jobs) {
127
198
  const record = await this.#store.get(done.jobId);
128
199
  if (!record || record.state === "ok" || record.state === "error") {
@@ -135,17 +206,30 @@ var CloudLane = class {
135
206
  }
136
207
  await this.#store.complete({
137
208
  jobId: done.jobId,
209
+ // The relay named the device; the signature above proved it — §3.6.
210
+ runnerId: done.runnerId,
138
211
  // The grant, not the machine: this site never paired with the device
139
212
  // that ran it, and the signature it verified above is the stronger
140
213
  // claim about who did.
141
214
  holder: { by: "lease", leaseId: done.leaseId },
142
- outcome,
215
+ outcome: outcome.outcome,
143
216
  provenance: provenanceFor({
144
217
  audience: record.audience,
145
218
  runnerId: done.runnerId,
146
- runnerOwner: keyId(done.device.identity),
147
- backendClass: "http",
148
- model: "unknown"
219
+ // The owner, from the relay's own record of who claimed it — not a
220
+ // key id. cloud_008 §2.5: this said `keyId(device.identity)`, which
221
+ // put a key id where the direct plane puts a user id, so an app
222
+ // comparing provenance across lanes compared two namespaces and got
223
+ // `false` for the same person. The device's key is still what the
224
+ // signature was verified against, above; that is a different
225
+ // question from whose machine it is.
226
+ runnerOwner: done.runnerOwner,
227
+ // From the envelope, not invented — cloud_008 §2.5. These were
228
+ // hardcoded `"http"` and `"unknown"` because the daemon's declared
229
+ // values stopped at the relay, which is right: a blind relay acts
230
+ // on neither. Sealing them carries them past it untouched.
231
+ backendClass: outcome.ran.backendClass,
232
+ model: outcome.ran.model
149
233
  }),
150
234
  now: this.#now()
151
235
  });
@@ -180,29 +264,107 @@ var CloudLane = class {
180
264
  } catch {
181
265
  return null;
182
266
  }
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;
267
+ const sealed = SealedOutcome.safeParse(parsed);
268
+ if (!sealed.success) return null;
269
+ if (sealed.data.outcome.outcome !== done.disposition) return null;
270
+ return sealed.data;
187
271
  }
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)
272
+ /**
273
+ * Sign a site-plane call with this site's identity key.
274
+ *
275
+ * The same scheme the daemon uses against an upstream, because the site is
276
+ * in the same position: an outbound caller whose key the relay already holds
277
+ * for other reasons. Nothing else authenticates this plane — a relay that
278
+ * took the `siteId` in a body at face value would let anyone enqueue work in
279
+ * a site's name and read who claimed it.
280
+ */
281
+ #headers(endpoint, rawBody) {
282
+ const signature = signSiteRequest(this.#siteKeys, {
283
+ endpoint,
284
+ siteId: this.#options.siteId,
285
+ issuedAt: this.#now(),
286
+ body: rawBody
193
287
  });
194
- return response.json();
288
+ return {
289
+ "x-byollm-site": this.#options.siteId,
290
+ "x-byollm-issued-at": String(signature.issuedAt),
291
+ "x-byollm-signature": signature.signature
292
+ };
195
293
  }
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();
294
+ /**
295
+ * A relay answer, checked before it is believed — alpha.31.
296
+ *
297
+ * The bug this closes is one line long and its shape is general: a response
298
+ * body used without looking at the status. The daemon's client has always
299
+ * done this properly (`client.ts` maps every status to a typed refusal); the
300
+ * site's lane parsed JSON and hoped.
301
+ *
302
+ * Two classes, because they need opposite handling. **Retryable** — 503 from
303
+ * a draining pod, 429, 5xx, and the protocol's own `not-ready` — means the
304
+ * work is still there and this cycle should end quietly. **Refused** — a bad
305
+ * signature, an unknown site, a version this relay does not speak — will
306
+ * still be true in five seconds, and swallowing it would leave a site
307
+ * silently disconnected from its own users.
308
+ */
309
+ async #answer(response, endpoint) {
310
+ if (response.ok) return response.json();
311
+ let code = "";
312
+ let message;
313
+ try {
314
+ const body = await response.json();
315
+ code = body.error ?? "";
316
+ message = body.message ?? "";
317
+ } catch {
318
+ message = `HTTP ${String(response.status)}`;
319
+ }
320
+ const retryable = response.status >= 500 || response.status === 429 || code === "not-ready" || code === "server-error";
321
+ throw new RelayUnavailable(
322
+ `${endpoint}: ${code || "refused"} \u2014 ${message}`,
323
+ retryable,
324
+ code
325
+ );
326
+ }
327
+ async #post(endpoint, body) {
328
+ const rawBody = JSON.stringify({
329
+ protocolVersion: PROTOCOL_VERSION,
330
+ ...body
331
+ });
332
+ const response = await this.#fetch(
333
+ `${this.#options.relayOrigin}/relay/site/${endpoint}`,
334
+ {
335
+ method: "POST",
336
+ headers: {
337
+ "content-type": "application/json",
338
+ ...this.#headers(endpoint, rawBody)
339
+ },
340
+ body: rawBody
341
+ }
342
+ );
343
+ return this.#answer(response, endpoint);
344
+ }
345
+ async #get(endpoint) {
346
+ const url = `${this.#options.relayOrigin}/relay/site/${endpoint}?siteId=${encodeURIComponent(this.#options.siteId)}&protocolVersion=${encodeURIComponent(PROTOCOL_VERSION)}`;
347
+ const response = await this.#fetch(url, {
348
+ headers: this.#headers(endpoint, "")
349
+ });
350
+ return this.#answer(response, endpoint);
200
351
  }
201
352
  };
202
- var ENVELOPE_TTL_FALLBACK = 24 * 60 * 6e4;
203
353
 
204
354
  // src/app.ts
205
355
  var DEFAULT_LIVENESS_MS = 35e3;
356
+ var ENQUEUE_OPTIONS = Object.freeze({
357
+ kind: true,
358
+ payload: true,
359
+ owner: true,
360
+ audience: true,
361
+ service: true,
362
+ audienceAllow: true,
363
+ dependsOn: true,
364
+ ttlMs: true,
365
+ deadlineAt: true,
366
+ id: true
367
+ });
206
368
  var ByollmApp = class {
207
369
  #store;
208
370
  #siteKeys;
@@ -255,6 +417,14 @@ var ByollmApp = class {
255
417
  * the app is obliged to disclose that to whoever reads it.
256
418
  */
257
419
  async enqueue(input) {
420
+ const unknown = Object.keys(input).filter(
421
+ (key) => !(key in ENQUEUE_OPTIONS)
422
+ );
423
+ if (unknown.length > 0) {
424
+ throw new Error(
425
+ `enqueue does not understand ${unknown.map((k) => `\`${k}\``).join(", ")}. An option this @byollm/server does not know is refused rather than ignored, because an ignored option is a job that runs differently than you asked with nothing to see \u2014 most often an SDK older than the code calling it. Upgrade @byollm/server, or remove the option.`
426
+ );
427
+ }
258
428
  const parsed = KindedPayload.safeParse({
259
429
  kind: input.kind,
260
430
  payload: input.payload
@@ -314,7 +484,7 @@ var ByollmApp = class {
314
484
  * Check `provenance.untrusted` before rendering. It is true for every
315
485
  * `named`/`public` job, because that text came from someone else's machine
316
486
  * and the app must not present it as its own AI's answer
317
- * ({@link MUSTS.RESULT_PROVENANCE}).
487
+ * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
318
488
  */
319
489
  async result(jobId) {
320
490
  const job = await this.job(jobId);
@@ -328,7 +498,11 @@ var ByollmApp = class {
328
498
  }
329
499
  /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
330
500
  async cancel(jobId) {
331
- return this.#store.cancel(jobId, this.#now());
501
+ const cancelled = await this.#store.cancel(jobId, this.#now());
502
+ if (cancelled && this.cloud) {
503
+ await this.cloud.cancel(jobId).catch(() => void 0);
504
+ }
505
+ return cancelled;
332
506
  }
333
507
  /**
334
508
  * Is there a live runner that could take a job of this shape?
@@ -350,14 +524,20 @@ var ByollmApp = class {
350
524
  }
351
525
  let capable = 0;
352
526
  let admitted = 0;
527
+ let withheldSomewhere = 0;
528
+ let lastRefusal;
353
529
  for (const runner of live) {
354
- const capability = runner.capabilities.find((c) => c.kind === query.kind);
530
+ const forKind = runner.capabilities.filter((c) => c.kind === query.kind);
531
+ const capability = query.service === void 0 ? forKind.find((c) => c.isDefault) : forKind.find((c) => c.service === query.service);
532
+ if (capability === void 0 && query.service === void 0 && forKind.length > 0) {
533
+ withheldSomewhere += 1;
534
+ }
355
535
  if (!capability) continue;
356
536
  capable += 1;
357
537
  const match = matchAudience(
358
538
  {
359
539
  owner: query.owner,
360
- audience: query.audience ?? "self",
540
+ audience: query.audience ?? "private",
361
541
  audienceAllow: query.audienceAllow
362
542
  },
363
543
  {
@@ -381,18 +561,24 @@ var ByollmApp = class {
381
561
  }
382
562
  );
383
563
  if (match.ok) admitted += 1;
564
+ else lastRefusal = match.refusal;
384
565
  }
385
566
  if (capable === 0) {
386
- return {
387
- available: false,
388
- reason: "no-matching-capability",
389
- candidates: 0
390
- };
567
+ const reason = query.service !== void 0 ? "selection-unavailable" : withheldSomewhere > 0 ? "awaiting-default" : "no-matching-capability";
568
+ return { available: false, reason, candidates: 0 };
391
569
  }
392
570
  if (admitted === 0) {
571
+ const ownersDoing = lastRefusal === "offer-scope-too-narrow" || lastRefusal === "subscription-self-lock" || lastRefusal === "metered-no-spend-consent" || lastRefusal === "metered-ceiling-reached";
572
+ if (query.service !== void 0) {
573
+ return {
574
+ available: false,
575
+ reason: "selection-unavailable",
576
+ candidates: 0
577
+ };
578
+ }
393
579
  return {
394
580
  available: false,
395
- reason: "audience-admits-nobody",
581
+ reason: ownersDoing ? "default-unusable" : "audience-admits-nobody",
396
582
  candidates: 0
397
583
  };
398
584
  }
@@ -406,13 +592,10 @@ var ByollmApp = class {
406
592
  * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).
407
593
  */
408
594
  async approvePairing(args) {
409
- const token = generateRunnerToken();
410
595
  return this.#store.approvePairing({
411
596
  userCode: normalizeUserCode(args.userCode),
412
597
  owner: args.owner,
413
598
  runnerId: generateRunnerId(),
414
- runnerToken: token,
415
- tokenHash: hashSecret(token),
416
599
  now: this.#now()
417
600
  });
418
601
  }
@@ -485,13 +668,27 @@ function siteKeysFromEnv(variable = "BYOLLM_SITE_KEYS", env = process.env) {
485
668
  }
486
669
  function formatSiteKeys(keys) {
487
670
  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.
671
+ const pub = publicIdentityOf3(keys);
672
+ 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
673
+ #
674
+ # This is the site's identity. Anything holding it can *be* this site,
675
+ # so it goes wherever your deployment keeps secrets \u2014 never in a repo,
676
+ # never in a browser, never pasted into a dashboard.
490
677
  BYOLLM_SITE_KEYS=${encoded}
491
678
 
492
- # Fingerprint (not secret \u2014 show it to users so they can check what
493
- # their daemon pinned):
494
- # ${fingerprint(publicIdentityOf3(keys).identity)}
679
+ # \u2500\u2500 2. PUBLIC \u2014 paste this line into the byollm dashboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
680
+ #
681
+ # The public half. It proves signatures and seals nothing, so it is
682
+ # safe to publish \u2014 which is the point: users pin it, and the relay
683
+ # cannot forge work without the secret above.
684
+ ${JSON.stringify(pub)}
685
+
686
+ # \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
687
+ #
688
+ # A fingerprint is not secret. Show it on your site so somebody
689
+ # connecting can check it against what their daemon printed.
690
+ # The dashboard derives this itself, so there is nothing to paste.
691
+ # ${fingerprint(pub.identity)}
495
692
  `;
496
693
  }
497
694
 
@@ -525,12 +722,14 @@ var MemoryStore = class {
525
722
  kind: input.kind,
526
723
  envelope: input.envelope,
527
724
  sizeClass: input.sizeClass,
528
- audience: input.audience ?? "self",
725
+ audience: input.audience ?? "private",
726
+ service: input.service,
529
727
  owner: input.owner,
530
728
  audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
531
729
  dependsOn,
532
730
  state: "queued",
533
731
  lease: null,
732
+ completedByLeaseId: null,
534
733
  createdAt: now,
535
734
  // The TTL clock starts here only if nothing blocks the job.
536
735
  claimableAt: blocked ? null : now,
@@ -620,11 +819,11 @@ var MemoryStore = class {
620
819
  for (const { jobId, leaseId } of args.leases) {
621
820
  const job = this.#jobs.get(jobId);
622
821
  if (!job || job.lease?.runnerId !== args.runnerId || job.lease.id !== leaseId) {
623
- lost.push(jobId);
822
+ lost.push({ jobId, leaseId });
624
823
  continue;
625
824
  }
626
825
  if (job.state !== "claimed" && job.state !== "running") {
627
- lost.push(jobId);
826
+ lost.push({ jobId, leaseId });
628
827
  continue;
629
828
  }
630
829
  const expiresAt = args.now + args.leaseMs;
@@ -666,6 +865,11 @@ var MemoryStore = class {
666
865
  const job = this.#jobs.get(args.jobId);
667
866
  if (!job) return Promise.resolve({ accepted: false, job: null });
668
867
  if (job.state === "ok" || job.state === "error" || job.state === "canceled") {
868
+ const sameDevice = job.provenance?.runnerId !== void 0 && job.provenance.runnerId === args.runnerId;
869
+ const sameGrant = args.holder.by === "lease" && job.completedByLeaseId !== null && job.completedByLeaseId === args.holder.leaseId;
870
+ if (sameDevice && sameGrant) {
871
+ return Promise.resolve({ accepted: false, duplicate: true, job });
872
+ }
669
873
  return Promise.resolve({ accepted: false, job });
670
874
  }
671
875
  if (job.state === "expired") {
@@ -680,6 +884,8 @@ var MemoryStore = class {
680
884
  ...job,
681
885
  state,
682
886
  lease: null,
887
+ // The grant that recorded it, kept after the lease is dropped — §3.6.
888
+ completedByLeaseId: args.holder.by === "lease" ? args.holder.leaseId : job.lease?.id ?? null,
683
889
  outcome: args.outcome,
684
890
  provenance: args.provenance,
685
891
  updatedAt: args.now
@@ -768,6 +974,7 @@ var MemoryStore = class {
768
974
  ...job,
769
975
  state: "queued",
770
976
  lease: null,
977
+ completedByLeaseId: null,
771
978
  // Newly available again, so the TTL clock restarts here too.
772
979
  claimableAt: args.now,
773
980
  // A refusal is remembered, or the pair spins between claim and
@@ -797,6 +1004,7 @@ var MemoryStore = class {
797
1004
  ...job,
798
1005
  state: "queued",
799
1006
  lease: null,
1007
+ completedByLeaseId: null,
800
1008
  // The TTL clock restarts: it measures how long a job has waited
801
1009
  // *unclaimed*, and this job has just become available again. Without
802
1010
  // this, a job whose runner died would expire for time it spent being
@@ -819,6 +1027,7 @@ var MemoryStore = class {
819
1027
  ...job,
820
1028
  state: "expired",
821
1029
  lease: null,
1030
+ completedByLeaseId: null,
822
1031
  updatedAt: now
823
1032
  };
824
1033
  this.#write(job.id, expired);
@@ -834,6 +1043,7 @@ var MemoryStore = class {
834
1043
  ...job,
835
1044
  state: "canceled",
836
1045
  lease: null,
1046
+ completedByLeaseId: null,
837
1047
  updatedAt: now
838
1048
  };
839
1049
  this.#write(jobId, canceled);
@@ -854,9 +1064,7 @@ var MemoryStore = class {
854
1064
  }
855
1065
  listCancelRequests(runnerId) {
856
1066
  return Promise.resolve(
857
- [...this.#cancelRequests].filter(
858
- (jobId) => this.#jobs.get(jobId)?.lease?.runnerId === runnerId
859
- )
1067
+ [...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 ?? "" }))
860
1068
  );
861
1069
  }
862
1070
  // -- pairing and runners -------------------------------------------------
@@ -887,7 +1095,6 @@ var MemoryStore = class {
887
1095
  const runner = {
888
1096
  id: args.runnerId,
889
1097
  owner: args.owner,
890
- tokenHash: args.tokenHash,
891
1098
  // Carried from the pairing, not re-supplied at approval: the user
892
1099
  // approved a specific machine, and the runner must be that machine.
893
1100
  device: pairing.device,
@@ -906,7 +1113,7 @@ var MemoryStore = class {
906
1113
  state: "approved",
907
1114
  owner: args.owner,
908
1115
  runnerId: runner.id,
909
- runnerTokenOnce: args.runnerToken
1116
+ collected: false
910
1117
  });
911
1118
  return Promise.resolve(runner);
912
1119
  }
@@ -927,17 +1134,11 @@ var MemoryStore = class {
927
1134
  if (pairing) {
928
1135
  this.#pairings.set(deviceCodeHash, {
929
1136
  ...pairing,
930
- runnerTokenOnce: null
1137
+ collected: true
931
1138
  });
932
1139
  }
933
1140
  return Promise.resolve();
934
1141
  }
935
- getRunnerByTokenHash(hash) {
936
- for (const runner of this.#runners.values()) {
937
- if (runner.tokenHash === hash) return Promise.resolve(runner);
938
- }
939
- return Promise.resolve(null);
940
- }
941
1142
  getRunner(runnerId) {
942
1143
  return Promise.resolve(this.#runners.get(runnerId) ?? null);
943
1144
  }
@@ -983,6 +1184,7 @@ export {
983
1184
  MemoryStore,
984
1185
  NoRunnerAvailableError,
985
1186
  PollingDelivery,
1187
+ RelayUnavailable,
986
1188
  ResultTimeoutError,
987
1189
  SERVED_PROTOCOL_VERSION,
988
1190
  capabilityFor,
@@ -991,7 +1193,6 @@ export {
991
1193
  generateDeviceCode,
992
1194
  generateJobId,
993
1195
  generateRunnerId,
994
- generateRunnerToken,
995
1196
  generateSiteKeys,
996
1197
  generateUserCode,
997
1198
  hashSecret,