@byollm/server 0.1.0-alpha.0

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 ADDED
@@ -0,0 +1,634 @@
1
+ import {
2
+ ByollmHandlers,
3
+ SERVED_PROTOCOL_VERSION,
4
+ bearerFrom,
5
+ createFetchHandler,
6
+ generateDeviceCode,
7
+ generateJobId,
8
+ generateRunnerId,
9
+ generateRunnerToken,
10
+ generateUserCode,
11
+ hashSecret,
12
+ routeEndpoint,
13
+ secretsMatch
14
+ } from "./chunk-HL6EYHQ7.js";
15
+ import {
16
+ NoRunnerAvailableError,
17
+ PollingDelivery,
18
+ ResultTimeoutError
19
+ } from "./chunk-7RKXFPBZ.js";
20
+
21
+ // src/app.ts
22
+ import {
23
+ backendDescriptor,
24
+ matchAudience
25
+ } from "@byollm/protocol";
26
+ var DEFAULT_LIVENESS_MS = 35e3;
27
+ var ByollmApp = class {
28
+ #store;
29
+ #now;
30
+ #livenessMs;
31
+ #delivery;
32
+ constructor(options) {
33
+ this.#store = options.store;
34
+ this.#now = options.now ?? Date.now;
35
+ this.#livenessMs = options.livenessMs ?? DEFAULT_LIVENESS_MS;
36
+ const deps = {
37
+ ...options.noRunnerGraceMs === void 0 ? {} : { graceMs: options.noRunnerGraceMs },
38
+ read: (jobId) => this.result(jobId),
39
+ availability: async (jobId) => {
40
+ const job = await this.#store.get(jobId);
41
+ if (!job)
42
+ return { available: false, reason: "unknown-job", blocked: false };
43
+ if (job.claimableAt === null) {
44
+ return { available: true, blocked: true };
45
+ }
46
+ const availability = await this.runnerAvailability({
47
+ kind: job.kind,
48
+ owner: job.owner,
49
+ audience: job.audience,
50
+ ...job.audienceAllow === void 0 ? {} : { audienceAllow: job.audienceAllow }
51
+ });
52
+ return {
53
+ available: availability.available,
54
+ ...availability.reason === void 0 ? {} : { reason: availability.reason },
55
+ blocked: false
56
+ };
57
+ }
58
+ };
59
+ this.#delivery = options.delivery?.(deps) ?? new PollingDelivery(deps);
60
+ }
61
+ /**
62
+ * Enqueue a job.
63
+ *
64
+ * `audience` defaults to `self` — the safe direction. Widening it means the
65
+ * result comes back marked untrusted (see {@link ByollmApp.result}), and
66
+ * the app is obliged to disclose that to whoever reads it.
67
+ */
68
+ async enqueue(input) {
69
+ const record = await this.#store.create(input, this.#now());
70
+ return {
71
+ id: record.id,
72
+ record,
73
+ result: (options) => this.#delivery.waitFor(record.id, options),
74
+ cancel: async () => {
75
+ await this.cancel(record.id);
76
+ }
77
+ };
78
+ }
79
+ /** Read a job's current state. */
80
+ async job(jobId) {
81
+ await this.#store.expireDue(this.#now());
82
+ return this.#store.get(jobId);
83
+ }
84
+ /**
85
+ * A job's result with its provenance attached.
86
+ *
87
+ * Check `provenance.untrusted` before rendering. It is true for every
88
+ * `named`/`public` job, because that text came from someone else's machine
89
+ * and the app must not present it as its own AI's answer
90
+ * ({@link MUSTS.RESULT_PROVENANCE}).
91
+ */
92
+ async result(jobId) {
93
+ const job = await this.job(jobId);
94
+ if (!job) return null;
95
+ return {
96
+ jobId: job.id,
97
+ state: job.state,
98
+ ...job.outcome === null ? {} : { outcome: job.outcome },
99
+ ...job.provenance === null ? {} : { provenance: job.provenance }
100
+ };
101
+ }
102
+ /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
103
+ async cancel(jobId) {
104
+ return this.#store.cancel(jobId, this.#now());
105
+ }
106
+ /**
107
+ * Is there a live runner that could take a job of this shape?
108
+ *
109
+ * Runs the identical {@link matchAudience} rule the claim path uses, so the
110
+ * signal cannot promise a runner the claim would then refuse.
111
+ */
112
+ async runnerAvailability(query) {
113
+ const now = this.#now();
114
+ const all = await this.#store.listRunners();
115
+ const live = all.filter(
116
+ (runner) => runner.revokedAt === null && !runner.paused && now - runner.lastHeartbeatAt <= this.#livenessMs
117
+ );
118
+ if (all.length === 0) {
119
+ return { available: false, reason: "no-runner-paired", candidates: 0 };
120
+ }
121
+ if (live.length === 0) {
122
+ return { available: false, reason: "no-runner-online", candidates: 0 };
123
+ }
124
+ let capable = 0;
125
+ let admitted = 0;
126
+ for (const runner of live) {
127
+ const capability = runner.capabilities.find((c) => c.kind === query.kind);
128
+ if (!capability) continue;
129
+ capable += 1;
130
+ const match = matchAudience(
131
+ {
132
+ owner: query.owner,
133
+ audience: query.audience ?? "self",
134
+ audienceAllow: query.audienceAllow
135
+ },
136
+ {
137
+ owner: runner.owner,
138
+ offerScope: capability.offerScope,
139
+ account: backendDescriptor(capability.backendId).account,
140
+ // Same conservative assumption the claim path makes: the server
141
+ // cannot see a remote daemon's local allowlist (protocol §4.2).
142
+ locallyAllows: () => true
143
+ }
144
+ );
145
+ if (match.ok) admitted += 1;
146
+ }
147
+ if (capable === 0) {
148
+ return {
149
+ available: false,
150
+ reason: "no-matching-capability",
151
+ candidates: 0
152
+ };
153
+ }
154
+ if (admitted === 0) {
155
+ return {
156
+ available: false,
157
+ reason: "audience-admits-nobody",
158
+ candidates: 0
159
+ };
160
+ }
161
+ return { available: true, candidates: admitted };
162
+ }
163
+ /**
164
+ * Approve a pairing on behalf of an authenticated user.
165
+ *
166
+ * `owner` MUST come from the approving user's own session. A daemon can
167
+ * never assert who it is — that is the whole reason pairing is interactive
168
+ * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).
169
+ */
170
+ async approvePairing(args) {
171
+ const token = generateRunnerToken();
172
+ return this.#store.approvePairing({
173
+ userCode: normalizeUserCode(args.userCode),
174
+ owner: args.owner,
175
+ runnerId: generateRunnerId(),
176
+ runnerToken: token,
177
+ tokenHash: hashSecret(token),
178
+ now: this.#now()
179
+ });
180
+ }
181
+ /** Deny a pairing the user did not initiate. */
182
+ async denyPairing(userCode) {
183
+ return this.#store.denyPairing(normalizeUserCode(userCode), this.#now());
184
+ }
185
+ /** What a pairing code refers to, for the approval page to show. */
186
+ async pendingPairing(userCode) {
187
+ const pairing = await this.#store.getPairingByUserCode(
188
+ normalizeUserCode(userCode)
189
+ );
190
+ if (pairing?.state !== "pending") return null;
191
+ if (pairing.expiresAt <= this.#now()) return null;
192
+ return {
193
+ label: pairing.label,
194
+ platform: pairing.platform,
195
+ daemonVersion: pairing.daemonVersion,
196
+ capabilities: pairing.capabilities.map((c) => ({
197
+ kind: c.kind,
198
+ model: c.model
199
+ })),
200
+ expiresAt: pairing.expiresAt
201
+ };
202
+ }
203
+ /** The user's paired runners, for a settings page. */
204
+ async runners(owner) {
205
+ return this.#store.listRunners(owner);
206
+ }
207
+ /** Revoke a runner. It stops at its next heartbeat, mid-queue. */
208
+ async revokeRunner(runnerId) {
209
+ return this.#store.revokeRunner(runnerId, this.#now());
210
+ }
211
+ /** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */
212
+ async sweep() {
213
+ return this.#store.expireDue(this.#now());
214
+ }
215
+ };
216
+ function normalizeUserCode(input) {
217
+ const bare = input.toUpperCase().replace(/[^A-Z0-9]/g, "");
218
+ return bare.length === 8 ? `${bare.slice(0, 4)}-${bare.slice(4)}` : bare;
219
+ }
220
+
221
+ // src/memory.ts
222
+ import {
223
+ backendDescriptor as backendDescriptor2,
224
+ matchAudience as matchAudience2
225
+ } from "@byollm/protocol";
226
+ var DEFAULT_TTL_MS = 15 * 6e4;
227
+ var MemoryStore = class {
228
+ #jobs = /* @__PURE__ */ new Map();
229
+ #runners = /* @__PURE__ */ new Map();
230
+ #pairings = /* @__PURE__ */ new Map();
231
+ /** Job ids the app has asked to cancel, not yet acknowledged by a runner. */
232
+ #cancelRequests = /* @__PURE__ */ new Set();
233
+ #defaultTtlMs;
234
+ constructor(options = {}) {
235
+ this.#defaultTtlMs = options.defaultTtlMs ?? DEFAULT_TTL_MS;
236
+ }
237
+ // -- jobs ---------------------------------------------------------------
238
+ create(input, now) {
239
+ const id = input.id ?? generateJobId();
240
+ const existing = this.#jobs.get(id);
241
+ if (existing) return Promise.resolve(existing);
242
+ const dependsOn = [...input.dependsOn ?? []];
243
+ const blocked = dependsOn.some(
244
+ (depId) => this.#jobs.get(depId)?.state !== "ok"
245
+ );
246
+ const job = {
247
+ id,
248
+ kind: input.kind,
249
+ payload: input.payload,
250
+ audience: input.audience ?? "self",
251
+ owner: input.owner,
252
+ audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
253
+ dependsOn,
254
+ state: "queued",
255
+ lease: null,
256
+ createdAt: now,
257
+ // The TTL clock starts here only if nothing blocks the job.
258
+ claimableAt: blocked ? null : now,
259
+ ttlMs: input.ttlMs ?? this.#defaultTtlMs,
260
+ deadlineAt: input.deadlineAt ?? null,
261
+ refusedBy: [],
262
+ attempts: 0,
263
+ outcome: null,
264
+ provenance: null,
265
+ updatedAt: now
266
+ };
267
+ this.#jobs.set(id, job);
268
+ return Promise.resolve(job);
269
+ }
270
+ get(jobId) {
271
+ return Promise.resolve(this.#jobs.get(jobId) ?? null);
272
+ }
273
+ claim(args) {
274
+ this.#expireDueSync(args.now);
275
+ const claimed = [];
276
+ const candidates = [...this.#jobs.values()].sort(
277
+ (a, b) => (a.claimableAt ?? Infinity) - (b.claimableAt ?? Infinity)
278
+ );
279
+ for (const job of candidates) {
280
+ if (claimed.length >= args.max) break;
281
+ if (!this.#isClaimable(job, args)) continue;
282
+ const updated = {
283
+ ...job,
284
+ state: "claimed",
285
+ lease: {
286
+ runnerId: args.runnerId,
287
+ expiresAt: args.now + args.leaseMs
288
+ },
289
+ attempts: job.attempts + 1,
290
+ updatedAt: args.now
291
+ };
292
+ this.#jobs.set(job.id, updated);
293
+ claimed.push(updated);
294
+ }
295
+ return Promise.resolve(claimed);
296
+ }
297
+ /**
298
+ * The claim predicate, shared by `claim` and the no-runner signal so the
299
+ * two can never disagree about what "a runner that could take this" means.
300
+ */
301
+ #isClaimable(job, args) {
302
+ if (job.state !== "queued") return false;
303
+ if (job.claimableAt === null || job.claimableAt > args.now) return false;
304
+ if (job.refusedBy.includes(args.runnerId)) return false;
305
+ const capability = capabilityFor(args.capabilities, job.kind);
306
+ if (!capability) return false;
307
+ const match = matchAudience2(
308
+ {
309
+ owner: job.owner,
310
+ audience: job.audience,
311
+ audienceAllow: job.audienceAllow
312
+ },
313
+ {
314
+ owner: args.runnerOwner,
315
+ 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,
319
+ // The server cannot see a remote daemon's local allowlist and must
320
+ // not pretend to (protocol §4.2). It admits the job here; the daemon
321
+ // is the enforcing side and releases with `refused` if its own list
322
+ // says no.
323
+ locallyAllows: () => true
324
+ }
325
+ );
326
+ return match.ok;
327
+ }
328
+ renewLeases(args) {
329
+ this.#expireDueSync(args.now);
330
+ const renewed = [];
331
+ const lost = [];
332
+ for (const jobId of args.jobIds) {
333
+ const job = this.#jobs.get(jobId);
334
+ if (!job || job.lease?.runnerId !== args.runnerId) {
335
+ lost.push(jobId);
336
+ continue;
337
+ }
338
+ if (job.state !== "claimed" && job.state !== "running") {
339
+ lost.push(jobId);
340
+ continue;
341
+ }
342
+ const expiresAt = args.now + args.leaseMs;
343
+ this.#jobs.set(jobId, {
344
+ ...job,
345
+ state: "running",
346
+ lease: { runnerId: args.runnerId, expiresAt },
347
+ updatedAt: args.now
348
+ });
349
+ renewed.push({ jobId, expiresAt });
350
+ }
351
+ return Promise.resolve({ renewed, lost });
352
+ }
353
+ complete(args) {
354
+ const job = this.#jobs.get(args.jobId);
355
+ if (!job) return Promise.resolve({ accepted: false, job: null });
356
+ if (job.state === "ok" || job.state === "error" || job.state === "canceled") {
357
+ return Promise.resolve({ accepted: false, job });
358
+ }
359
+ if (job.state === "expired") {
360
+ return Promise.resolve({ accepted: false, job });
361
+ }
362
+ if (job.lease?.runnerId !== args.runnerId) {
363
+ return Promise.resolve({ accepted: false, job });
364
+ }
365
+ const state = args.outcome.outcome === "ok" ? "ok" : args.outcome.outcome === "canceled" ? "canceled" : "error";
366
+ const updated = {
367
+ ...job,
368
+ state,
369
+ lease: null,
370
+ outcome: args.outcome,
371
+ provenance: args.provenance,
372
+ updatedAt: args.now
373
+ };
374
+ this.#jobs.set(job.id, updated);
375
+ this.#cancelRequests.delete(job.id);
376
+ if (state === "ok") this.#unblockDependents(job.id, args.now);
377
+ return Promise.resolve({ accepted: true, job: updated });
378
+ }
379
+ /**
380
+ * Start the TTL clock on anything this job was blocking.
381
+ *
382
+ * Deliberately only on `ok`: a dependency that errored leaves its dependents
383
+ * blocked forever rather than releasing them into a run whose input never
384
+ * arrived. They expire at their absolute deadline if one was set, and the
385
+ * app sees a chain that stopped where it broke.
386
+ */
387
+ #unblockDependents(completedId, now) {
388
+ for (const job of this.#jobs.values()) {
389
+ if (job.claimableAt !== null) continue;
390
+ if (!job.dependsOn.includes(completedId)) continue;
391
+ const ready = job.dependsOn.every(
392
+ (depId) => this.#jobs.get(depId)?.state === "ok"
393
+ );
394
+ if (ready) {
395
+ this.#jobs.set(job.id, { ...job, claimableAt: now, updatedAt: now });
396
+ }
397
+ }
398
+ }
399
+ release(args) {
400
+ const released = [];
401
+ for (const jobId of args.jobIds) {
402
+ const job = this.#jobs.get(jobId);
403
+ if (!job || job.lease?.runnerId !== args.runnerId) continue;
404
+ this.#jobs.set(jobId, {
405
+ ...job,
406
+ state: "queued",
407
+ lease: null,
408
+ // Newly available again, so the TTL clock restarts here too.
409
+ claimableAt: args.now,
410
+ // A refusal is remembered, or the pair spins between claim and
411
+ // release forever ({@link MUSTS.REFUSAL_NOT_REOFFERED}).
412
+ refusedBy: args.reason === "refused" ? [.../* @__PURE__ */ new Set([...job.refusedBy, args.runnerId])] : job.refusedBy,
413
+ updatedAt: args.now
414
+ });
415
+ released.push(jobId);
416
+ }
417
+ return Promise.resolve(released);
418
+ }
419
+ expireDue(now) {
420
+ return Promise.resolve(this.#expireDueSync(now));
421
+ }
422
+ /**
423
+ * Lease expiry and TTL expiry in one idempotent sweep.
424
+ *
425
+ * Order matters: a lease is reclaimed *before* the TTL is judged, so a job
426
+ * whose runner died is offered again rather than being expired for having
427
+ * sat in `claimed` too long.
428
+ */
429
+ #expireDueSync(now) {
430
+ const changed = [];
431
+ for (const job of this.#jobs.values()) {
432
+ if ((job.state === "claimed" || job.state === "running") && job.lease !== null && job.lease.expiresAt <= now) {
433
+ const requeued = {
434
+ ...job,
435
+ state: "queued",
436
+ lease: null,
437
+ // The TTL clock restarts: it measures how long a job has waited
438
+ // *unclaimed*, and this job has just become available again. Without
439
+ // this, a job whose runner died would expire for time it spent being
440
+ // actively worked on — losing exactly the work `kill -9` recovery
441
+ // exists to save. Total lifetime is bounded by `deadlineAt`, which
442
+ // is absolute and unaffected by reclaim.
443
+ claimableAt: now,
444
+ updatedAt: now
445
+ };
446
+ this.#jobs.set(job.id, requeued);
447
+ changed.push(requeued);
448
+ }
449
+ }
450
+ for (const job of this.#jobs.values()) {
451
+ if (job.state !== "queued") continue;
452
+ const pastDeadline = job.deadlineAt !== null && job.deadlineAt <= now;
453
+ const pastTtl = job.claimableAt !== null && job.claimableAt + job.ttlMs <= now;
454
+ if (!pastDeadline && !pastTtl) continue;
455
+ const expired = {
456
+ ...job,
457
+ state: "expired",
458
+ lease: null,
459
+ updatedAt: now
460
+ };
461
+ this.#jobs.set(job.id, expired);
462
+ changed.push(expired);
463
+ }
464
+ return changed;
465
+ }
466
+ cancel(jobId, now) {
467
+ const job = this.#jobs.get(jobId);
468
+ if (!job) return Promise.resolve(null);
469
+ if (job.state === "queued") {
470
+ const canceled = {
471
+ ...job,
472
+ state: "canceled",
473
+ lease: null,
474
+ updatedAt: now
475
+ };
476
+ this.#jobs.set(jobId, canceled);
477
+ return Promise.resolve(canceled);
478
+ }
479
+ if (job.state === "claimed" || job.state === "running") {
480
+ this.#cancelRequests.add(jobId);
481
+ return Promise.resolve(job);
482
+ }
483
+ return Promise.resolve(job);
484
+ }
485
+ listClaimedBy(runnerId) {
486
+ return Promise.resolve(
487
+ [...this.#jobs.values()].filter(
488
+ (job) => job.lease?.runnerId === runnerId
489
+ )
490
+ );
491
+ }
492
+ listCancelRequests(runnerId) {
493
+ return Promise.resolve(
494
+ [...this.#cancelRequests].filter(
495
+ (jobId) => this.#jobs.get(jobId)?.lease?.runnerId === runnerId
496
+ )
497
+ );
498
+ }
499
+ // -- pairing and runners -------------------------------------------------
500
+ createPairing(record) {
501
+ this.#pairings.set(record.deviceCodeHash, record);
502
+ return Promise.resolve();
503
+ }
504
+ getPairingByDeviceCodeHash(hash) {
505
+ return Promise.resolve(this.#pairings.get(hash) ?? null);
506
+ }
507
+ getPairingByUserCode(userCode) {
508
+ for (const pairing of this.#pairings.values()) {
509
+ if (pairing.userCode === userCode) return Promise.resolve(pairing);
510
+ }
511
+ return Promise.resolve(null);
512
+ }
513
+ approvePairing(args) {
514
+ const pairing = [...this.#pairings.values()].find(
515
+ (p) => p.userCode === args.userCode
516
+ );
517
+ if (!pairing) throw new Error(`unknown pairing code: ${args.userCode}`);
518
+ if (pairing.expiresAt <= args.now) {
519
+ throw new Error("pairing code has expired");
520
+ }
521
+ if (pairing.state !== "pending") {
522
+ throw new Error(`pairing is already ${pairing.state}`);
523
+ }
524
+ const runner = {
525
+ id: args.runnerId,
526
+ owner: args.owner,
527
+ tokenHash: args.tokenHash,
528
+ label: pairing.label,
529
+ platform: pairing.platform,
530
+ daemonVersion: pairing.daemonVersion,
531
+ capabilities: pairing.capabilities,
532
+ paused: false,
533
+ revokedAt: null,
534
+ lastHeartbeatAt: args.now,
535
+ createdAt: args.now
536
+ };
537
+ this.#runners.set(runner.id, runner);
538
+ this.#pairings.set(pairing.deviceCodeHash, {
539
+ ...pairing,
540
+ state: "approved",
541
+ owner: args.owner,
542
+ runnerId: runner.id,
543
+ runnerTokenOnce: args.runnerToken
544
+ });
545
+ return Promise.resolve(runner);
546
+ }
547
+ denyPairing(userCode, _now) {
548
+ const pairing = [...this.#pairings.values()].find(
549
+ (p) => p.userCode === userCode
550
+ );
551
+ if (pairing) {
552
+ this.#pairings.set(pairing.deviceCodeHash, {
553
+ ...pairing,
554
+ state: "denied"
555
+ });
556
+ }
557
+ return Promise.resolve();
558
+ }
559
+ consumePairingToken(deviceCodeHash) {
560
+ const pairing = this.#pairings.get(deviceCodeHash);
561
+ if (pairing) {
562
+ this.#pairings.set(deviceCodeHash, {
563
+ ...pairing,
564
+ runnerTokenOnce: null
565
+ });
566
+ }
567
+ return Promise.resolve();
568
+ }
569
+ getRunnerByTokenHash(hash) {
570
+ for (const runner of this.#runners.values()) {
571
+ if (runner.tokenHash === hash) return Promise.resolve(runner);
572
+ }
573
+ return Promise.resolve(null);
574
+ }
575
+ getRunner(runnerId) {
576
+ return Promise.resolve(this.#runners.get(runnerId) ?? null);
577
+ }
578
+ touchRunner(args) {
579
+ const runner = this.#runners.get(args.runnerId);
580
+ if (!runner) return Promise.resolve(null);
581
+ const updated = {
582
+ ...runner,
583
+ capabilities: args.capabilities,
584
+ daemonVersion: args.daemonVersion,
585
+ paused: args.paused,
586
+ lastHeartbeatAt: args.now
587
+ };
588
+ this.#runners.set(runner.id, updated);
589
+ return Promise.resolve(updated);
590
+ }
591
+ revokeRunner(runnerId, now) {
592
+ const runner = this.#runners.get(runnerId);
593
+ if (runner?.revokedAt === null) {
594
+ this.#runners.set(runnerId, { ...runner, revokedAt: now });
595
+ }
596
+ return Promise.resolve();
597
+ }
598
+ listRunners(owner) {
599
+ const all = [...this.#runners.values()];
600
+ return Promise.resolve(
601
+ owner === void 0 ? all : all.filter((r) => r.owner === owner)
602
+ );
603
+ }
604
+ // -- test/demo helpers ---------------------------------------------------
605
+ /** All jobs, for demos and assertions. Not part of the store interface. */
606
+ allJobs() {
607
+ return [...this.#jobs.values()];
608
+ }
609
+ };
610
+ function capabilityFor(capabilities, kind) {
611
+ return capabilities.find((c) => c.kind === kind);
612
+ }
613
+ export {
614
+ ByollmApp,
615
+ ByollmHandlers,
616
+ MemoryStore,
617
+ NoRunnerAvailableError,
618
+ PollingDelivery,
619
+ ResultTimeoutError,
620
+ SERVED_PROTOCOL_VERSION,
621
+ bearerFrom,
622
+ capabilityFor,
623
+ createFetchHandler,
624
+ generateDeviceCode,
625
+ generateJobId,
626
+ generateRunnerId,
627
+ generateRunnerToken,
628
+ generateUserCode,
629
+ hashSecret,
630
+ normalizeUserCode,
631
+ routeEndpoint,
632
+ secretsMatch
633
+ };
634
+ //# sourceMappingURL=index.js.map