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

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.
@@ -1,422 +0,0 @@
1
- // src/ids.ts
2
- import {
3
- createHash,
4
- randomBytes,
5
- randomUUID,
6
- timingSafeEqual
7
- } from "crypto";
8
- var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRTWXYZ2346789";
9
- function generateDeviceCode() {
10
- return randomBytes(32).toString("base64url");
11
- }
12
- function generateRunnerToken() {
13
- return randomBytes(32).toString("base64url");
14
- }
15
- function generateRunnerId() {
16
- return `runner_${randomUUID()}`;
17
- }
18
- function generateJobId() {
19
- return `job_${randomUUID()}`;
20
- }
21
- function generateUserCode() {
22
- const chars = [];
23
- while (chars.length < 8) {
24
- for (const byte of randomBytes(16)) {
25
- const limit = 256 - 256 % USER_CODE_ALPHABET.length;
26
- if (byte >= limit) continue;
27
- const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];
28
- if (symbol === void 0) continue;
29
- chars.push(symbol);
30
- if (chars.length === 8) break;
31
- }
32
- }
33
- return `${chars.slice(0, 4).join("")}-${chars.slice(4).join("")}`;
34
- }
35
- function hashSecret(secret) {
36
- return createHash("sha256").update(secret, "utf8").digest("hex");
37
- }
38
- function secretsMatch(aHex, bHex) {
39
- if (aHex.length !== bHex.length) return false;
40
- return timingSafeEqual(Buffer.from(aHex, "hex"), Buffer.from(bHex, "hex"));
41
- }
42
-
43
- // src/handlers.ts
44
- import {
45
- ClaimRequest,
46
- ERROR_STATUS,
47
- HeartbeatRequest,
48
- PairRequest,
49
- PROTOCOL_VERSION,
50
- ReleaseRequest,
51
- ResultRequest,
52
- provenanceFor
53
- } from "@byollm/protocol";
54
- var DEFAULTS = {
55
- leaseMs: 6e4,
56
- pairingTtlMs: 10 * 6e4,
57
- pollIntervalMs: 2e3
58
- };
59
- function fail(error, message, retryAfterSeconds) {
60
- return {
61
- status: ERROR_STATUS[error],
62
- body: {
63
- error,
64
- message,
65
- ...retryAfterSeconds === void 0 ? {} : { retryAfter: retryAfterSeconds }
66
- },
67
- ...retryAfterSeconds === void 0 ? {} : { retryAfterSeconds }
68
- };
69
- }
70
- function ok(body) {
71
- return { status: 200, body };
72
- }
73
- var ByollmHandlers = class {
74
- #store;
75
- #verificationUrl;
76
- #leaseMs;
77
- #pairingTtlMs;
78
- #pollIntervalMs;
79
- #now;
80
- constructor(config) {
81
- this.#store = config.store;
82
- this.#verificationUrl = config.verificationUrl;
83
- this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;
84
- this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;
85
- this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;
86
- this.#now = config.now ?? Date.now;
87
- }
88
- /**
89
- * Dispatch one protocol call.
90
- *
91
- * @param endpoint - which of the five, already routed from the path
92
- * @param body - the parsed JSON request body, untrusted
93
- * @param bearer - the `Authorization: Bearer` value, if any
94
- */
95
- async handle(endpoint, body, bearer) {
96
- switch (endpoint) {
97
- case "pair":
98
- return this.#pair(body);
99
- case "claim":
100
- return this.#authed(bearer, body, ClaimRequest, this.#claim.bind(this));
101
- case "heartbeat":
102
- return this.#authed(
103
- bearer,
104
- body,
105
- HeartbeatRequest,
106
- this.#heartbeat.bind(this),
107
- { allowRevoked: true }
108
- );
109
- case "result":
110
- return this.#authed(
111
- bearer,
112
- body,
113
- ResultRequest,
114
- this.#result.bind(this)
115
- );
116
- case "release":
117
- return this.#authed(
118
- bearer,
119
- body,
120
- ReleaseRequest,
121
- this.#release.bind(this)
122
- );
123
- }
124
- }
125
- /**
126
- * Shared preamble for the four authenticated endpoints: resolve the bearer
127
- * token to a runner, reject a revoked one, and parse the body.
128
- *
129
- * The token→runner lookup happens before schema validation so a stranger
130
- * probing the endpoint learns nothing about the wire format.
131
- */
132
- async #authed(bearer, body, schema, run, options = {}) {
133
- if (bearer === void 0 || bearer.length === 0) {
134
- return fail("unauthorized", "a runner token is required");
135
- }
136
- const runner = await this.#store.getRunnerByTokenHash(hashSecret(bearer));
137
- if (!runner) {
138
- return fail("unauthorized", "this runner token is not recognised");
139
- }
140
- if (runner.revokedAt !== null && options.allowRevoked !== true) {
141
- return fail("revoked", "this runner has been revoked by its owner");
142
- }
143
- const parsed = schema.safeParse(body);
144
- if (!parsed.success || parsed.data === void 0) {
145
- return fail("bad-request", "request body failed schema validation");
146
- }
147
- return run(parsed.data, runner);
148
- }
149
- // -- 1. pair --------------------------------------------------------------
150
- async #pair(body) {
151
- const parsed = PairRequest.safeParse(body);
152
- if (!parsed.success) {
153
- return fail("bad-request", "pair request failed schema validation");
154
- }
155
- const request = parsed.data;
156
- const now = this.#now();
157
- if (request.action === "start") {
158
- const deviceCode = generateDeviceCode();
159
- const userCode = generateUserCode();
160
- const expiresAt = now + this.#pairingTtlMs;
161
- await this.#store.createPairing({
162
- deviceCodeHash: hashSecret(deviceCode),
163
- userCode,
164
- state: "pending",
165
- owner: null,
166
- runnerId: null,
167
- runnerTokenOnce: null,
168
- label: request.daemon.label,
169
- platform: request.daemon.platform,
170
- daemonVersion: request.daemon.version,
171
- capabilities: request.capabilities,
172
- expiresAt,
173
- createdAt: now
174
- });
175
- const response = {
176
- deviceCode,
177
- userCode,
178
- verificationUrl: this.#verificationUrl,
179
- expiresAt,
180
- pollIntervalMs: this.#pollIntervalMs
181
- };
182
- return ok(response);
183
- }
184
- const pairing = await this.#store.getPairingByDeviceCodeHash(
185
- hashSecret(request.deviceCode)
186
- );
187
- if (!pairing) {
188
- return fail("not-found", "unknown device code");
189
- }
190
- if (pairing.state === "denied") {
191
- return ok({ status: "denied" });
192
- }
193
- if (pairing.expiresAt <= now && pairing.state === "pending") {
194
- return ok({ status: "expired" });
195
- }
196
- if (pairing.state === "approved" && pairing.runnerTokenOnce !== null && pairing.runnerId !== null && pairing.owner !== null) {
197
- const response = {
198
- status: "approved",
199
- runnerToken: pairing.runnerTokenOnce,
200
- runnerId: pairing.runnerId,
201
- owner: pairing.owner
202
- };
203
- await this.#store.consumePairingToken(pairing.deviceCodeHash);
204
- return ok(response);
205
- }
206
- if (pairing.state === "approved") {
207
- return fail("not-found", "this pairing has already been collected");
208
- }
209
- return ok({ status: "pending" });
210
- }
211
- // -- 2. claim -------------------------------------------------------------
212
- async #claim(request, runner) {
213
- if (request.runnerId !== runner.id) {
214
- return fail("unauthorized", "runner id does not match the bearer token");
215
- }
216
- const now = this.#now();
217
- const jobs = await this.#store.claim({
218
- runnerId: runner.id,
219
- runnerOwner: runner.owner,
220
- capabilities: request.capabilities,
221
- max: request.max,
222
- leaseMs: this.#leaseMs,
223
- now
224
- });
225
- const response = {
226
- jobs: jobs.map((job) => ({
227
- id: job.id,
228
- kind: job.kind,
229
- payload: job.payload,
230
- audience: job.audience,
231
- owner: job.owner,
232
- ...job.audienceAllow === void 0 ? {} : { audienceAllow: [...job.audienceAllow] },
233
- lease: job.lease ?? {
234
- runnerId: runner.id,
235
- expiresAt: now + this.#leaseMs
236
- }
237
- })),
238
- leaseMs: this.#leaseMs
239
- };
240
- return ok(response);
241
- }
242
- // -- 3. heartbeat ---------------------------------------------------------
243
- async #heartbeat(request, runner) {
244
- if (request.runnerId !== runner.id) {
245
- return fail("unauthorized", "runner id does not match the bearer token");
246
- }
247
- const now = this.#now();
248
- const revoked = runner.revokedAt !== null;
249
- if (revoked) {
250
- const held = await this.#store.listClaimedBy(runner.id);
251
- const response2 = {
252
- revoked: true,
253
- cancel: [],
254
- leases: [],
255
- lost: held.map((job) => job.id),
256
- serverTime: now
257
- };
258
- return ok(response2);
259
- }
260
- await this.#store.touchRunner({
261
- runnerId: runner.id,
262
- capabilities: request.capabilities,
263
- daemonVersion: request.daemonVersion,
264
- paused: request.paused,
265
- now
266
- });
267
- const { renewed, lost } = await this.#store.renewLeases({
268
- runnerId: runner.id,
269
- jobIds: request.activeJobIds,
270
- leaseMs: this.#leaseMs,
271
- now
272
- });
273
- const cancel = await this.#store.listCancelRequests(runner.id);
274
- const response = {
275
- revoked: false,
276
- cancel,
277
- leases: renewed.map((r) => ({ jobId: r.jobId, expiresAt: r.expiresAt })),
278
- lost: [...lost],
279
- serverTime: now
280
- };
281
- return ok(response);
282
- }
283
- // -- 4. result ------------------------------------------------------------
284
- async #result(request, runner) {
285
- if (request.runnerId !== runner.id) {
286
- return fail("unauthorized", "runner id does not match the bearer token");
287
- }
288
- const now = this.#now();
289
- const job = await this.#store.get(request.jobId);
290
- if (!job) return fail("not-found", "unknown job");
291
- const provenance = provenanceFor({
292
- audience: job.audience,
293
- runnerId: runner.id,
294
- runnerOwner: runner.owner,
295
- backendClass: request.backendClass,
296
- model: request.model
297
- });
298
- const { accepted, job: updated } = await this.#store.complete({
299
- jobId: request.jobId,
300
- runnerId: runner.id,
301
- outcome: request.outcome,
302
- provenance,
303
- now
304
- });
305
- const response = {
306
- accepted,
307
- state: updated?.state ?? job.state
308
- };
309
- return ok(response);
310
- }
311
- // -- 5. release -----------------------------------------------------------
312
- async #release(request, runner) {
313
- if (request.runnerId !== runner.id) {
314
- return fail("unauthorized", "runner id does not match the bearer token");
315
- }
316
- const released = await this.#store.release({
317
- runnerId: runner.id,
318
- jobIds: request.jobIds,
319
- reason: request.reason,
320
- now: this.#now()
321
- });
322
- const response = { released };
323
- return ok(response);
324
- }
325
- };
326
- var SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;
327
-
328
- // src/http.ts
329
- import { ENDPOINTS, PROTOCOL_PREFIX } from "@byollm/protocol";
330
- var MAX_BODY_BYTES = 8 * 1024 * 1024;
331
- function routeEndpoint(pathname) {
332
- const index = pathname.lastIndexOf("/");
333
- const last = index === -1 ? pathname : pathname.slice(index + 1);
334
- return ENDPOINTS.includes(last) ? last : null;
335
- }
336
- function bearerFrom(header) {
337
- if (!header) return void 0;
338
- const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());
339
- return match?.[1];
340
- }
341
- function createFetchHandler(config) {
342
- const handlers = new ByollmHandlers(config);
343
- return async function handle(request) {
344
- if (request.method !== "POST") {
345
- return json(405, {
346
- error: "bad-request",
347
- message: "protocol endpoints accept POST only"
348
- });
349
- }
350
- const endpoint = routeEndpoint(new URL(request.url).pathname);
351
- if (endpoint === null) {
352
- return json(404, {
353
- error: "not-found",
354
- message: `not a ${PROTOCOL_PREFIX} endpoint`
355
- });
356
- }
357
- const declared = request.headers.get("content-length");
358
- if (declared !== null && Number(declared) > MAX_BODY_BYTES) {
359
- return json(400, {
360
- error: "bad-request",
361
- message: "request body too large"
362
- });
363
- }
364
- let body;
365
- try {
366
- const text = await request.text();
367
- if (text.length > MAX_BODY_BYTES) {
368
- return json(400, {
369
- error: "bad-request",
370
- message: "request body too large"
371
- });
372
- }
373
- body = JSON.parse(text);
374
- } catch {
375
- return json(400, {
376
- error: "bad-request",
377
- message: "request body is not valid JSON"
378
- });
379
- }
380
- const result = await handlers.handle(
381
- endpoint,
382
- body,
383
- bearerFrom(request.headers.get("authorization"))
384
- );
385
- const headers = {
386
- "content-type": "application/json",
387
- "cache-control": "no-store"
388
- };
389
- if (result.retryAfterSeconds !== void 0) {
390
- headers["retry-after"] = String(result.retryAfterSeconds);
391
- }
392
- return new Response(JSON.stringify(result.body), {
393
- status: result.status,
394
- headers
395
- });
396
- };
397
- }
398
- function json(status, body) {
399
- return new Response(JSON.stringify(body), {
400
- status,
401
- headers: {
402
- "content-type": "application/json",
403
- "cache-control": "no-store"
404
- }
405
- });
406
- }
407
-
408
- export {
409
- generateDeviceCode,
410
- generateRunnerToken,
411
- generateRunnerId,
412
- generateJobId,
413
- generateUserCode,
414
- hashSecret,
415
- secretsMatch,
416
- ByollmHandlers,
417
- SERVED_PROTOCOL_VERSION,
418
- routeEndpoint,
419
- bearerFrom,
420
- createFetchHandler
421
- };
422
- //# sourceMappingURL=chunk-HL6EYHQ7.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/ids.ts","../src/handlers.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner bearer token. */\nexport function generateRunnerToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n return `job_${randomUUID()}`;\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n","import {\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport type { RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param bearer - the `Authorization: Bearer` value, if any\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n bearer: string | undefined,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(bearer, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on, so a revoked runner\n // must reach the handler and be told `revoked: true` rather than be\n // bounced with a 403 it would treat as a transport problem\n // ({@link MUSTS.REVOCATION_HONORED}).\n return this.#authed(\n bearer,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n { allowRevoked: true },\n );\n case \"result\":\n return this.#authed(\n bearer,\n body,\n ResultRequest,\n this.#result.bind(this),\n );\n case \"release\":\n return this.#authed(\n bearer,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: resolve the bearer\n * token to a runner, reject a revoked one, and parse the body.\n *\n * The token→runner lookup happens before schema validation so a stranger\n * probing the endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n bearer: string | undefined,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n if (bearer === undefined || bearer.length === 0) {\n return fail(\"unauthorized\", \"a runner token is required\");\n }\n const runner = await this.#store.getRunnerByTokenHash(hashSecret(bearer));\n if (!runner) {\n return fail(\"unauthorized\", \"this runner token is not recognised\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n await this.#store.createPairing({\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n runnerTokenOnce: null,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n pairing.runnerTokenOnce !== null &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerToken: pairing.runnerTokenOnce,\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n payload: job.payload,\n audience: job.audience,\n owner: job.owner,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: [...job.audienceAllow] }),\n lease: job.lease ?? {\n runnerId: runner.id,\n expiresAt: now + this.#leaseMs,\n },\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n const revoked = runner.revokedAt !== null;\n\n if (revoked) {\n // Nothing is renewed for a revoked runner: every job it holds is\n // reported lost so it abandons the queue rather than finishing it.\n const held = await this.#store.listClaimedBy(runner.id);\n const response: HeartbeatResponse = {\n revoked: true,\n cancel: [],\n leases: [],\n lost: held.map((job) => job.id),\n serverTime: now,\n };\n return ok(response);\n }\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n const { renewed, lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n jobIds: request.activeJobIds,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n revoked: false,\n cancel,\n leases: renewed.map((r) => ({ jobId: r.jobId, expiresAt: r.expiresAt })),\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.RESULT_PROVENANCE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n backendClass: request.backendClass,\n model: request.model,\n });\n\n const { accepted, job: updated } = await this.#store.complete({\n jobId: request.jobId,\n runnerId: runner.id,\n outcome: request.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the bearer token\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n jobIds: request.jobIds,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n","import { ENDPOINTS, PROTOCOL_PREFIX, type Endpoint } from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * A payload is capped at 4 MB of text by the protocol; this leaves room for\n * JSON overhead and a batch of results, and refuses anything wilder at the\n * door rather than after parsing it.\n */\nconst MAX_BODY_BYTES = 8 * 1024 * 1024;\n\n/** Pull the endpoint name out of a URL path, or null if it isn't ours. */\nexport function routeEndpoint(pathname: string): Endpoint | null {\n const index = pathname.lastIndexOf(\"/\");\n const last = index === -1 ? pathname : pathname.slice(index + 1);\n return (ENDPOINTS as readonly string[]).includes(last)\n ? (last as Endpoint)\n : null;\n}\n\n/** Read the bearer token from an `Authorization` header. */\nexport function bearerFrom(header: string | null): string | undefined {\n if (!header) return undefined;\n const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());\n return match?.[1];\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig,\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${PROTOCOL_PREFIX} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n\n let body: unknown;\n try {\n const text = await request.text();\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n const result = await handlers.handle(\n endpoint,\n body,\n bearerFrom(request.headers.get(\"authorization\")),\n );\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["response"]}
@@ -1,73 +0,0 @@
1
- import { DeliveredResult } from '@byollm/protocol';
2
-
3
- /** Why a wait ended without a result. */
4
- declare class NoRunnerAvailableError extends Error {
5
- readonly jobId: string;
6
- readonly reason: string;
7
- readonly name = "NoRunnerAvailableError";
8
- constructor(jobId: string, reason: string);
9
- }
10
- /** The wait exceeded its timeout while a runner was still plausibly working. */
11
- declare class ResultTimeoutError extends Error {
12
- readonly jobId: string;
13
- readonly timeoutMs: number;
14
- readonly name = "ResultTimeoutError";
15
- constructor(jobId: string, timeoutMs: number);
16
- }
17
- interface WaitOptions {
18
- /** Give up after this long. Default 5 minutes. */
19
- readonly timeoutMs?: number;
20
- /**
21
- * Called instead of throwing when no runner can take the job. Return a
22
- * substitute result (a hosted-model answer, say) and the wait resolves with
23
- * it; return nothing and {@link NoRunnerAvailableError} is thrown.
24
- */
25
- readonly onNoRunner?: (reason: string) => DeliveredResult | undefined | Promise<DeliveredResult | undefined>;
26
- /** Abort the wait. */
27
- readonly signal?: AbortSignal;
28
- }
29
- /**
30
- * How an app learns a job finished.
31
- *
32
- * byollm_003 Rev 1 is explicit that this is a *channel* — webhook, Realtime
33
- * subscription, or poll — and never an implied in-request `await`. The
34
- * polling implementation below is the portable default; the Supabase adapter
35
- * substitutes Realtime for the same interface.
36
- */
37
- interface ResultDelivery {
38
- waitFor(jobId: string, options?: WaitOptions): Promise<DeliveredResult>;
39
- }
40
- interface PollingDeliveryDeps {
41
- /** Current state of the job, or null if unknown. */
42
- readonly read: (jobId: string) => Promise<DeliveredResult | null>;
43
- /** Whether a runner could still take this job. */
44
- readonly availability: (jobId: string) => Promise<{
45
- available: boolean;
46
- reason?: string;
47
- blocked: boolean;
48
- }>;
49
- readonly sleep?: (ms: number) => Promise<void>;
50
- /**
51
- * Injectable clock. It must advance in step with {@link sleep}: a test that
52
- * stubs one and not the other gets a loop whose grace window never elapses.
53
- */
54
- readonly now?: () => number;
55
- /**
56
- * How long a sustained no-runner signal must persist before it is believed.
57
- * Defaults to {@link NO_RUNNER_GRACE_MS}.
58
- */
59
- readonly graceMs?: number;
60
- }
61
- /**
62
- * The portable delivery channel: poll the store until the job is terminal.
63
- *
64
- * Correct everywhere and adequate for most apps. An adapter with a push
65
- * channel should replace it — see the Supabase adapter's Realtime delivery.
66
- */
67
- declare class PollingDelivery implements ResultDelivery {
68
- #private;
69
- constructor(deps: PollingDeliveryDeps);
70
- waitFor(jobId: string, options?: WaitOptions): Promise<DeliveredResult>;
71
- }
72
-
73
- export { NoRunnerAvailableError as N, type PollingDeliveryDeps as P, type ResultDelivery as R, type WaitOptions as W, PollingDelivery as a, ResultTimeoutError as b };