@byollm/relay 0.1.0-alpha.5 → 0.1.0-alpha.7
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/README.md +1 -1
- package/dist/index.d.ts +51 -2
- package/dist/index.js +54 -7
- package/dist/index.js.map +1 -1
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.7`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> This is a walking skeleton. It routes real jobs between real daemons and real
|
|
5
5
|
> sites, and it is the fixture byollm_009 freezes against — but it keeps its
|
package/dist/index.d.ts
CHANGED
|
@@ -202,6 +202,34 @@ declare const RosterRecord: z.ZodObject<{
|
|
|
202
202
|
members: z.ZodArray<z.ZodString>;
|
|
203
203
|
}, z.core.$strict>;
|
|
204
204
|
type RosterRecord = z.infer<typeof RosterRecord>;
|
|
205
|
+
/**
|
|
206
|
+
* A device its owner has approved — cloud_005 §7.1.
|
|
207
|
+
*
|
|
208
|
+
* The relay refuses a device that is not here, and that refusal is the point.
|
|
209
|
+
* byollm_009's seventh finding stopped a daemon from *naming* itself; this
|
|
210
|
+
* stops it from *keying* itself. A device that presents keys nobody approved
|
|
211
|
+
* is a device whose owner never saw a fingerprint, and pairing it would make
|
|
212
|
+
* the relay the authority on identity — which is exactly what it must not be.
|
|
213
|
+
*
|
|
214
|
+
* The three-party shape consent already has, applied to identity: the device
|
|
215
|
+
* asserts, a human confirms in the control plane, the relay checks.
|
|
216
|
+
*/
|
|
217
|
+
declare const DeviceRecord: z.ZodObject<{
|
|
218
|
+
owner: z.ZodString;
|
|
219
|
+
runnerId: z.ZodString;
|
|
220
|
+
device: z.ZodObject<{
|
|
221
|
+
identity: z.ZodString;
|
|
222
|
+
encryption: z.ZodString;
|
|
223
|
+
encryptionSig: z.ZodString;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
}, z.core.$strict>;
|
|
226
|
+
type DeviceRecord = z.infer<typeof DeviceRecord>;
|
|
227
|
+
/** A revoked route, named by its parts. */
|
|
228
|
+
declare const RevocationRecord: z.ZodObject<{
|
|
229
|
+
owner: z.ZodString;
|
|
230
|
+
siteId: z.ZodString;
|
|
231
|
+
}, z.core.$strict>;
|
|
232
|
+
type RevocationRecord = z.infer<typeof RevocationRecord>;
|
|
205
233
|
declare const RelayFixture: z.ZodObject<{
|
|
206
234
|
consents: z.ZodArray<z.ZodObject<{
|
|
207
235
|
owner: z.ZodString;
|
|
@@ -212,12 +240,24 @@ declare const RelayFixture: z.ZodObject<{
|
|
|
212
240
|
encryptionSig: z.ZodString;
|
|
213
241
|
}, z.core.$strict>;
|
|
214
242
|
}, z.core.$strict>>;
|
|
243
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
244
|
+
owner: z.ZodString;
|
|
245
|
+
runnerId: z.ZodString;
|
|
246
|
+
device: z.ZodObject<{
|
|
247
|
+
identity: z.ZodString;
|
|
248
|
+
encryption: z.ZodString;
|
|
249
|
+
encryptionSig: z.ZodString;
|
|
250
|
+
}, z.core.$strict>;
|
|
251
|
+
}, z.core.$strict>>>;
|
|
215
252
|
rosters: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
216
253
|
id: z.ZodString;
|
|
217
254
|
owner: z.ZodString;
|
|
218
255
|
members: z.ZodArray<z.ZodString>;
|
|
219
256
|
}, z.core.$strict>>>;
|
|
220
|
-
revoked: z.ZodDefault<z.ZodArray<z.
|
|
257
|
+
revoked: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
258
|
+
owner: z.ZodString;
|
|
259
|
+
siteId: z.ZodString;
|
|
260
|
+
}, z.core.$strict>>>;
|
|
221
261
|
}, z.core.$strict>;
|
|
222
262
|
type RelayFixture = z.infer<typeof RelayFixture>;
|
|
223
263
|
/** An empty projection: nothing consented, so nothing routes. */
|
|
@@ -235,6 +275,15 @@ declare class Projection {
|
|
|
235
275
|
constructor(fixture?: RelayFixture);
|
|
236
276
|
/** Replace the projection wholesale — the control plane pushed a new one. */
|
|
237
277
|
replace(fixture: RelayFixture): void;
|
|
278
|
+
/**
|
|
279
|
+
* The device this runner id names, if a human approved it.
|
|
280
|
+
*
|
|
281
|
+
* Returns null for a device the control plane does not know, which is how
|
|
282
|
+
* the relay refuses to be the authority on identity.
|
|
283
|
+
*/
|
|
284
|
+
deviceFor(runnerId: string): DeviceRecord | null;
|
|
285
|
+
/** The device approved for these exact keys, if any. */
|
|
286
|
+
deviceByFingerprint(identityPublic: string): DeviceRecord | null;
|
|
238
287
|
/** The consent binding this owner to this site, if it exists and stands. */
|
|
239
288
|
consentFor(owner: string, siteId: string): ConsentRecord | null;
|
|
240
289
|
/**
|
|
@@ -315,4 +364,4 @@ declare class Relay {
|
|
|
315
364
|
handle(request: Request): Promise<Response>;
|
|
316
365
|
}
|
|
317
366
|
|
|
318
|
-
export { AWAITING_PAYLOAD_MS, ConsentRecord, EMPTY_FIXTURE, type Presence, Projection, Relay, RelayFixture, RelayFixture as RelayFixtureSchema, type RelayOptions, RelayState, RosterRecord, type RoutedJob, type RoutedState, debugPage };
|
|
367
|
+
export { AWAITING_PAYLOAD_MS, ConsentRecord, DeviceRecord, EMPTY_FIXTURE, type Presence, Projection, Relay, RelayFixture, RelayFixture as RelayFixtureSchema, type RelayOptions, RelayState, RevocationRecord, RosterRecord, type RoutedJob, type RoutedState, debugPage };
|
package/dist/index.js
CHANGED
|
@@ -138,7 +138,20 @@ var DaemonPlane = class {
|
|
|
138
138
|
if (!consent) {
|
|
139
139
|
return fail(403, "unauthorized", "no consent record for this user");
|
|
140
140
|
}
|
|
141
|
-
const
|
|
141
|
+
const approved = this.#deps.projection.deviceByFingerprint(
|
|
142
|
+
parsed.data.device.identity
|
|
143
|
+
);
|
|
144
|
+
if (!approved) {
|
|
145
|
+
return fail(
|
|
146
|
+
403,
|
|
147
|
+
"unauthorized",
|
|
148
|
+
"this device has not been approved by its owner"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (approved.owner !== parsed.data.owner) {
|
|
152
|
+
return fail(403, "unauthorized", "this device belongs to another owner");
|
|
153
|
+
}
|
|
154
|
+
const runnerId = approved.runnerId;
|
|
142
155
|
this.#deps.state.seen({
|
|
143
156
|
runnerId,
|
|
144
157
|
owner: parsed.data.owner,
|
|
@@ -421,21 +434,37 @@ var RosterRecord = z2.object({
|
|
|
421
434
|
/** Members who may route to it. The owner is not implicitly a member. */
|
|
422
435
|
members: z2.array(z2.string().min(1))
|
|
423
436
|
}).strict();
|
|
437
|
+
var DeviceRecord = z2.object({
|
|
438
|
+
/** Who approved it. */
|
|
439
|
+
owner: z2.string().min(1),
|
|
440
|
+
/** The id the control plane assigned — the device does not choose it. */
|
|
441
|
+
runnerId: z2.string().min(1),
|
|
442
|
+
/** The keys a human compared a fingerprint of before approving. */
|
|
443
|
+
device: PublicIdentity2
|
|
444
|
+
}).strict();
|
|
445
|
+
var RevocationRecord = z2.object({ owner: z2.string().min(1), siteId: z2.string().min(1) }).strict();
|
|
424
446
|
var RelayFixture = z2.object({
|
|
425
447
|
consents: z2.array(ConsentRecord),
|
|
448
|
+
devices: z2.array(DeviceRecord).default([]),
|
|
426
449
|
rosters: z2.array(RosterRecord).default([]),
|
|
427
450
|
/**
|
|
428
|
-
*
|
|
451
|
+
* Routes that were revoked, as structured pairs.
|
|
429
452
|
*
|
|
430
453
|
* A separate list rather than deleting the consent record, because the
|
|
431
|
-
* freeze gate needs revocation to be
|
|
432
|
-
*
|
|
433
|
-
*
|
|
454
|
+
* freeze gate needs revocation to be an observable *event* rather than an
|
|
455
|
+
* absence — "the row is gone" and "the row was revoked" are different
|
|
456
|
+
* answers to someone debugging why routing stopped.
|
|
457
|
+
*
|
|
458
|
+
* `{owner, siteId}` and never the composite string `"owner:siteId"`. A
|
|
459
|
+
* composite key is a parser waiting to meet an id containing its
|
|
460
|
+
* separator, which is the lesson the composite lease ids taught against
|
|
461
|
+
* Postgres — applied here before it became a contract.
|
|
434
462
|
*/
|
|
435
|
-
revoked: z2.array(
|
|
463
|
+
revoked: z2.array(RevocationRecord).default([])
|
|
436
464
|
}).strict();
|
|
437
465
|
var EMPTY_FIXTURE = {
|
|
438
466
|
consents: [],
|
|
467
|
+
devices: [],
|
|
439
468
|
rosters: [],
|
|
440
469
|
revoked: []
|
|
441
470
|
};
|
|
@@ -448,9 +477,25 @@ var Projection = class {
|
|
|
448
477
|
replace(fixture) {
|
|
449
478
|
this.#fixture = RelayFixture.parse(fixture);
|
|
450
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The device this runner id names, if a human approved it.
|
|
482
|
+
*
|
|
483
|
+
* Returns null for a device the control plane does not know, which is how
|
|
484
|
+
* the relay refuses to be the authority on identity.
|
|
485
|
+
*/
|
|
486
|
+
deviceFor(runnerId) {
|
|
487
|
+
return this.#fixture.devices.find((d) => d.runnerId === runnerId) ?? null;
|
|
488
|
+
}
|
|
489
|
+
/** The device approved for these exact keys, if any. */
|
|
490
|
+
deviceByFingerprint(identityPublic) {
|
|
491
|
+
return this.#fixture.devices.find((d) => d.device.identity === identityPublic) ?? null;
|
|
492
|
+
}
|
|
451
493
|
/** The consent binding this owner to this site, if it exists and stands. */
|
|
452
494
|
consentFor(owner, siteId) {
|
|
453
|
-
|
|
495
|
+
const revoked = this.#fixture.revoked.some(
|
|
496
|
+
(r) => r.owner === owner && r.siteId === siteId
|
|
497
|
+
);
|
|
498
|
+
if (revoked) return null;
|
|
454
499
|
return this.#fixture.consents.find(
|
|
455
500
|
(c) => c.owner === owner && c.siteId === siteId
|
|
456
501
|
) ?? null;
|
|
@@ -681,11 +726,13 @@ var json = (result) => new Response(JSON.stringify(result.body), {
|
|
|
681
726
|
export {
|
|
682
727
|
AWAITING_PAYLOAD_MS,
|
|
683
728
|
ConsentRecord,
|
|
729
|
+
DeviceRecord,
|
|
684
730
|
EMPTY_FIXTURE,
|
|
685
731
|
Projection,
|
|
686
732
|
Relay,
|
|
687
733
|
RelayFixture as RelayFixtureSchema,
|
|
688
734
|
RelayState,
|
|
735
|
+
RevocationRecord,
|
|
689
736
|
RosterRecord,
|
|
690
737
|
debugPage
|
|
691
738
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon-plane.ts","../src/state.ts","../src/debug.ts","../src/fixture.ts","../src/site-plane.ts","../src/index.ts"],"sourcesContent":["import {\n ClaimRequest,\n FetchRequest,\n HeartbeatRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n RequestSignature,\n keyId,\n verifyRequest,\n verifyPublicIdentity,\n PublicIdentity,\n type ClaimedStub,\n} from \"@byollm/protocol\";\nimport { randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport type { Projection } from \"./fixture.js\";\nimport { AWAITING_PAYLOAD_MS, type RelayState } from \"./state.js\";\n\n/**\n * The plane a daemon talks to — cloud_004 §2.\n *\n * To a daemon this is an upstream like any other: it claims, fetches, reports\n * and heartbeats exactly as it does against a direct site. That sameness is\n * the point of §9's \"the hub is a deployment of the open parts\" — hub mode is\n * not a second daemon code path, it is a second upstream.\n *\n * What differs is invisible from the daemon's side and total from ours: **this\n * upstream cannot seal.** A direct site answers `fetch` by opening its own\n * envelope and re-sealing to the claiming device. The relay has nothing to\n * open and nothing to seal with, so it answers `fetch` with whatever the site\n * left for that device, or with nothing yet.\n */\n\nexport interface PlaneResult {\n readonly status: number;\n readonly body: unknown;\n}\n\nconst ok = (body: unknown): PlaneResult => ({ status: 200, body });\nconst fail = (status: number, error: string, message: string): PlaneResult => ({\n status,\n body: { error, message },\n});\n\nexport interface DaemonPlaneDeps {\n readonly state: RelayState;\n readonly projection: Projection;\n readonly now: () => number;\n readonly leaseMs: number;\n /**\n * Which site this relay routes for.\n *\n * The skeleton relays for one site because that is all the freeze gate\n * needs. The production hub's multi-tenant router is the closed piece that\n * replaces this field (cloud_004 §9) — recorded here so the seam is visible\n * rather than assumed away.\n */\n readonly siteId: string;\n}\n\nexport class DaemonPlane {\n readonly #deps: DaemonPlaneDeps;\n\n constructor(deps: DaemonPlaneDeps) {\n this.#deps = deps;\n }\n\n /**\n * Pair a device — cloud_004 §3, the key-exchange moment.\n *\n * The relay hands back **the site's** public identity, taken from the\n * consent projection, not its own. This is the sentence that makes hub mode\n * safe: the daemon pins the party that will actually seal its work, so an\n * envelope is verified against the site even though it arrived via us. A\n * relay that substituted its own identity here could inject work — and would\n * need a private key to do it, which is why it has none.\n */\n pair(body: unknown): PlaneResult {\n const parsed = PairFixtureRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"pair request failed schema validation\");\n }\n if (!verifyPublicIdentity(parsed.data.device)) {\n return fail(400, \"bad-request\", \"the device identity is not consistent\");\n }\n\n const consent = this.#deps.projection.consentFor(\n parsed.data.owner,\n this.#deps.siteId,\n );\n if (!consent) {\n // CONSENT_BEFORE_ROUTE. There is no discovery path that creates one:\n // consent is a click somewhere else, and the relay only reads it.\n return fail(403, \"unauthorized\", \"no consent record for this user\");\n }\n\n // Minted here, not accepted from the device. The direct plane's server\n // mints a runner id at approval and a device has never named itself; a\n // relay that let one would take an identifier from the least trusted\n // party in the exchange. A uuid because the stores that eventually\n // record leases against it type their id columns that way.\n const runnerId = randomUUID();\n\n this.#deps.state.seen({\n runnerId,\n owner: parsed.data.owner,\n device: parsed.data.device,\n lastSeenAt: this.#deps.now(),\n });\n\n return ok({\n protocolVersion: PROTOCOL_VERSION,\n runnerId,\n /** The *site's* key. See the note above — this is load-bearing. */\n site: consent.site,\n });\n }\n\n /** Every authenticated call: signature first, then consent, then work. */\n #authed<T>(\n input: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (\n request: T,\n device: { runnerId: string; owner: string; device: PublicIdentity },\n ) => PlaneResult,\n options: { allowRevoked?: boolean } = {},\n ): PlaneResult {\n const signature = RequestSignature.safeParse(input.signature);\n if (!signature.success) {\n return fail(401, \"unauthorized\", \"this request is not signed\");\n }\n const known = this.#deps.state.presence(signature.data.runnerId);\n if (!known) {\n return fail(401, \"unauthorized\", \"this runner is not recognised\");\n }\n\n const failure = verifyRequest({\n identityPublic: known.device.identity,\n endpoint: input.endpoint,\n body: input.rawBody,\n signature: signature.data,\n now: this.#deps.now(),\n });\n if (failure) return fail(401, \"unauthorized\", \"signature check failed\");\n\n // Revocation reaches the daemon through heartbeat, so heartbeat itself\n // must be answerable by a revoked runner — bouncing it with a 403 would\n // read to the daemon as a transport problem and it would keep trying.\n if (known.revoked && options.allowRevoked !== true) {\n return fail(403, \"revoked\", \"routing for this runner has been revoked\");\n }\n\n known.lastSeenAt = this.#deps.now();\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(400, \"bad-request\", \"request failed schema validation\");\n }\n return run(parsed.data, known);\n }\n\n claim(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ClaimRequest, (request, device) => {\n if (request.runnerId !== device.runnerId) {\n return fail(401, \"unauthorized\", \"runner id does not match the key\");\n }\n const now = this.#deps.now();\n this.#deps.state.sweep(now);\n\n const kinds = new Set(request.capabilities.map((c) => c.kind));\n const granted: ClaimedStub[] = [];\n\n for (const job of this.#deps.state.jobs()) {\n if (granted.length >= request.max) break;\n if (job.state !== \"queued\") continue;\n if (!kinds.has(job.stub.kind)) continue;\n // The relay's half of AUDIENCE_BOTH_SIDES. The daemon re-checks its\n // own allowlist and may still refuse — this only narrows.\n if (!this.#deps.projection.mayRunFor(device.owner, job.stub.owner)) {\n continue;\n }\n\n // A UUID, not a readable composite. The direct plane's lease ids are\n // UUIDs and the Supabase adapter's `lease_id` column is typed `uuid`\n // — so a relay minting `lease_<job>_<time>` would route perfectly\n // against a memory store and fail the moment a real site adopted the\n // lease into Postgres. Same shape as the `job_<uuid>` bug: an id that\n // is only a string until something declares what kind of string.\n const leaseId = randomUUID();\n job.state = \"awaiting-payload\";\n job.claimedBy = {\n runnerId: device.runnerId,\n owner: device.owner,\n device: device.device,\n leaseId,\n leaseExpiresAt: now + this.#deps.leaseMs,\n };\n // The clock §7 requires and the direct plane never needed. It is not\n // the lease: it bounds how long we wait for a site, not how long the\n // device may work.\n job.awaitingUntil = now + AWAITING_PAYLOAD_MS;\n\n granted.push({\n ...job.stub,\n lease: {\n id: leaseId,\n runnerId: device.runnerId,\n expiresAt: job.claimedBy.leaseExpiresAt,\n },\n });\n }\n\n return ok({ jobs: granted, leaseMs: this.#deps.leaseMs });\n });\n }\n\n /**\n * Hand over the sealed payload, if the site has left one.\n *\n * The one endpoint whose behaviour differs from a direct site's, and the\n * difference is the whole design: a direct site seals here, on demand,\n * because it holds the keys. The relay waits. A `409` means \"claimed, not\n * yet sealed\" — a daemon should retry, not treat it as a refusal, because\n * the job is still legitimately theirs until the lease or the\n * awaiting-payload clock says otherwise.\n */\n fetch(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, FetchRequest, (request, device) => {\n const job = this.#deps.state.job(request.jobId);\n if (!job) return fail(404, \"not-found\", \"unknown job\");\n if (job.claimedBy?.runnerId !== device.runnerId) {\n return fail(403, \"unauthorized\", \"this runner does not hold the job\");\n }\n if (job.claimedBy.leaseId !== request.leaseId) {\n // LEASE_HONORED per *instance*: a stale lease id names a grant that\n // is over, and answering it would hand work to a previous holder.\n return fail(403, \"unauthorized\", \"that lease is no longer current\");\n }\n if (!job.payload) {\n return fail(409, \"not-ready\", \"the site has not sealed this job yet\");\n }\n job.state = \"running\";\n return ok({ envelope: job.payload });\n });\n }\n\n /**\n * Take a sealed result.\n *\n * The relay stores ciphertext and records the disposition so it can stop\n * dispatching. It cannot check the two against each other — that requires\n * opening the envelope, which is the site's job and the site's key. This is\n * the asymmetry byollm_009 §6.1 describes: the hint is actionable here and\n * only verifiable there.\n */\n result(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ResultRequest, (request, device) => {\n const job = this.#deps.state.job(request.jobId);\n if (!job) return fail(404, \"not-found\", \"unknown job\");\n if (job.claimedBy?.runnerId !== device.runnerId) {\n return fail(403, \"unauthorized\", \"this runner does not hold the job\");\n }\n if (job.state === \"done\") {\n return ok({ accepted: false, state: job.state });\n }\n job.result = request.envelope;\n job.disposition = request.disposition;\n job.state = \"done\";\n return ok({ accepted: true, state: job.state });\n });\n }\n\n heartbeat(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n (request, device) => {\n const now = this.#deps.now();\n this.#deps.state.sweep(now);\n\n const known = this.#deps.state.presence(device.runnerId);\n // Revocation is a fixture edit, and this is where the daemon learns\n // of it — within one heartbeat, which is what the freeze gate times.\n const consent = this.#deps.projection.consentFor(\n device.owner,\n this.#deps.siteId,\n );\n const revoked = consent === null;\n if (known) known.revoked = revoked;\n\n // Anything this runner thinks it holds that we no longer agree it\n // holds. A daemon must stop work on these rather than finish and\n // report into a lease that is gone.\n const lost = request.activeLeases\n .filter(({ jobId, leaseId }) => {\n const job = this.#deps.state.job(jobId);\n return job?.claimedBy?.leaseId !== leaseId;\n })\n .map(({ jobId }) => jobId);\n\n return ok({\n revoked,\n cancel: [],\n leases: [],\n lost,\n serverTime: now,\n });\n },\n { allowRevoked: true },\n );\n }\n\n release(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ReleaseRequest, (request, device) => {\n const released: string[] = [];\n for (const { jobId, leaseId } of request.leases) {\n const job = this.#deps.state.job(jobId);\n if (!job || job.claimedBy?.runnerId !== device.runnerId) continue;\n if (job.claimedBy.leaseId !== leaseId) continue;\n this.#deps.state.requeue(job);\n released.push(jobId);\n }\n return ok({ released });\n });\n }\n}\n\n/**\n * Pairing, as the skeleton does it.\n *\n * Not the public `PairRequest`: that models a device-code exchange with a\n * human at a browser, and the skeleton's consent arrives from a fixture\n * instead (cloud_004 §14). The daemon-visible *outcome* is identical — a\n * runner id and the site's pinned public identity — so nothing downstream can\n * tell the difference, which is what makes this substitution honest rather\n * than a shortcut around the consent MUST.\n */\nconst PairFixtureRequest = z\n .object({\n protocolVersion: z.literal(PROTOCOL_VERSION),\n owner: z.string().min(1),\n device: PublicIdentity,\n })\n .strict();\n\n/** Exported so the debug page can name a device the way a human would. */\nexport const fingerprintOf = (identity: PublicIdentity): string =>\n keyId(identity.identity);\n","import type { JobStub, PublicIdentity, SealedEnvelope } from \"@byollm/protocol\";\n\n/**\n * The relay's routing state — byollm_009 §7, reachable at last.\n *\n * §7 described a state machine the direct plane could not produce. There, the\n * site and the upstream are the same party: it seals when it likes, and a job\n * is never claimed-but-unsealed. Here they are different parties, and the gap\n * between them is a state:\n *\n * ```\n * queued ──claim──▶ awaiting-payload ──sealed──▶ ready ──fetch──▶ running\n * ▲ │ │\n * └────────────────────┘ ▼\n * site never seals, or seals too late ok | error | canceled\n * ```\n *\n * The relay cannot seal, so it cannot shortcut this. A payload is encrypted\n * to *the device that claimed it*, and nobody knows which device that is until\n * the claim happens — which is precisely why claim-then-fetch makes a blind\n * relay possible at all. The window is the price.\n *\n * ## What the relay holds, and what it cannot\n *\n * Stubs (metadata the site chose to publish), sealed envelopes it cannot open,\n * and public keys. There is no field on any type in this file that could hold\n * a private key or a plaintext, which is `RELAY_BLIND` expressed as a data\n * model rather than as a policy.\n */\n\n/** Where a routed job is. */\nexport type RoutedState =\n \"queued\" | \"awaiting-payload\" | \"ready\" | \"running\" | \"done\";\n\n/**\n * How long a site has to seal after one of its jobs is claimed.\n *\n * **Distinct from the lease, and distinct from the job's TTL** — byollm_009\n * §7.1. Three clocks, three different questions:\n *\n * - the **TTL** asks how long the work is worth doing at all;\n * - the **lease** asks how long this device gets to run it;\n * - this asks how long we wait for a site that has gone away.\n *\n * Collapsing any pair of them looks harmless until a site restarts during a\n * deploy: with only a lease, the device sits politely holding a job whose\n * payload will never arrive, and the lease's whole minute is spent waiting on\n * a party that is not coming back. Short, because a site that is up answers in\n * milliseconds and a site that is down will not answer sooner for waiting.\n */\nexport const AWAITING_PAYLOAD_MS = 10_000;\n\n/** A job the relay is routing. Metadata and ciphertext, nothing else. */\nexport interface RoutedJob {\n readonly id: string;\n /** Which site enqueued it — the party that will be asked to seal. */\n readonly siteId: string;\n /**\n * Everything the relay knows about the work, which is everything the site\n * chose to publish and not one field more (byollm_009 §6).\n */\n readonly stub: JobStub;\n state: RoutedState;\n /** Set from the claim; the site seals to these keys. */\n claimedBy?: {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n readonly leaseId: string;\n readonly leaseExpiresAt: number;\n };\n /** When {@link AWAITING_PAYLOAD_MS} runs out for this claim. */\n awaitingUntil?: number;\n /** Sealed to the claiming device by the site. Opaque here. */\n payload?: SealedEnvelope;\n /** Sealed to the site by the device. Opaque here. */\n result?: SealedEnvelope;\n /**\n * The result's clear-text discriminator — byollm_009 §6.1.\n *\n * The one outcome fact the relay is given, and the reason it is given:\n * without it the relay cannot stop dispatching a finished job. A routing\n * hint and never a fact — the *site* verifies it against the sealed\n * outcome, because only the site can open the envelope. The relay acts on\n * it and is entitled to be wrong; a lying daemon costs it a dispatch\n * decision, not a security property.\n */\n disposition?: \"ok\" | \"error\" | \"canceled\";\n}\n\n/** A device the relay has seen recently. */\nexport interface Presence {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n lastSeenAt: number;\n /** Set on revocation so the next request is refused rather than routed. */\n revoked: boolean;\n}\n\n/**\n * In-memory routing state.\n *\n * Deliberately not durable. The skeleton proves the protocol, and the\n * production hub replaces this with the closed multi-tenant router behind the\n * same shape (cloud_004 §9). Anything a restart loses here is a job that\n * returns to its site's queue — which is the behaviour a lapsed lease already\n * has to produce, so nothing new needs to be true for this to be safe.\n */\nexport class RelayState {\n readonly #jobs = new Map<string, RoutedJob>();\n readonly #presence = new Map<string, Presence>();\n\n /** Take a stub for routing. The payload is not here and will not be. */\n enqueue(input: { id: string; siteId: string; stub: JobStub }): RoutedJob {\n const job: RoutedJob = {\n id: input.id,\n siteId: input.siteId,\n stub: input.stub,\n state: \"queued\",\n };\n this.#jobs.set(job.id, job);\n return job;\n }\n\n job(jobId: string): RoutedJob | undefined {\n return this.#jobs.get(jobId);\n }\n\n jobs(): RoutedJob[] {\n return [...this.#jobs.values()];\n }\n\n /** Jobs a site must seal for, right now. */\n awaiting(siteId: string): RoutedJob[] {\n return this.jobs().filter(\n (j) => j.siteId === siteId && j.state === \"awaiting-payload\",\n );\n }\n\n /** Sealed results waiting to go home. */\n finished(siteId: string): RoutedJob[] {\n return this.jobs().filter(\n (j) =>\n j.siteId === siteId && j.state === \"done\" && j.result !== undefined,\n );\n }\n\n seen(presence: Omit<Presence, \"revoked\">): Presence {\n const existing = this.#presence.get(presence.runnerId);\n if (existing) {\n existing.lastSeenAt = presence.lastSeenAt;\n return existing;\n }\n const fresh: Presence = { ...presence, revoked: false };\n this.#presence.set(presence.runnerId, fresh);\n return fresh;\n }\n\n presence(runnerId: string): Presence | undefined {\n return this.#presence.get(runnerId);\n }\n\n everyone(): Presence[] {\n return [...this.#presence.values()];\n }\n\n /**\n * Return a job to the queue, forgetting the claim.\n *\n * The stub survives; nothing is lost. That is `LEASE_RECLAIMABLE` and it is\n * why the awaiting-payload timeout is cheap to fire: the worst case is that\n * a device did nothing for ten seconds and another one gets a turn.\n */\n requeue(job: RoutedJob): void {\n job.state = \"queued\";\n delete job.claimedBy;\n delete job.awaitingUntil;\n delete job.payload;\n }\n\n /**\n * Fire whatever the clock says is due, and report it.\n *\n * Returns the jobs it requeued so a caller can log or surface them — a\n * timeout that fires invisibly is indistinguishable from a job that was\n * never claimed, and those want very different debugging.\n */\n sweep(now: number): RoutedJob[] {\n const requeued: RoutedJob[] = [];\n for (const job of this.#jobs.values()) {\n if (job.state === \"awaiting-payload\" && (job.awaitingUntil ?? 0) <= now) {\n this.requeue(job);\n requeued.push(job);\n }\n const lease = job.claimedBy;\n if (\n lease &&\n (job.state === \"ready\" || job.state === \"running\") &&\n lease.leaseExpiresAt <= now\n ) {\n this.requeue(job);\n requeued.push(job);\n }\n }\n return requeued;\n }\n}\n","import { fingerprintOf } from \"./daemon-plane.js\";\nimport type { RelayState, RoutedJob } from \"./state.js\";\n\n/**\n * The debug page — cloud_004 §10.\n *\n * It exists because watching a stub get claimed beats reading store rows, and\n * it earns its keep from the first routed job rather than being a thing\n * someone builds later when routing is already hard to follow.\n *\n * One screen, no build step, no dependencies. It renders from the relay's own\n * state, so it cannot show anything the relay does not actually know — which\n * makes it an honest demonstration of blindness as well as a debugging tool.\n * There is no view here that could show a prompt, because there is no prompt\n * to show.\n */\n\nconst escape = (value: string): string =>\n value.replace(\n /[&<>\"]/g,\n (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c] ?? c,\n );\n\nconst STATE_COLOUR: Record<string, string> = {\n queued: \"#8a8a8a\",\n \"awaiting-payload\": \"#c98a00\",\n ready: \"#0a7\",\n running: \"#06c\",\n done: \"#444\",\n};\n\nfunction jobRow(job: RoutedJob, now: number): string {\n const claimed = job.claimedBy;\n const waiting =\n job.state === \"awaiting-payload\" && job.awaitingUntil !== undefined\n ? `${String(Math.max(0, job.awaitingUntil - now))}ms left`\n : \"\";\n return `<tr>\n <td><code>${escape(job.id)}</code></td>\n <td>${escape(job.stub.kind)}</td>\n <td>${escape(job.stub.owner)}</td>\n <td>${escape(job.stub.audience)}</td>\n <td>${escape(job.stub.sizeClass)}</td>\n <td>${job.stub.streaming ? \"yes\" : \"no\"}</td>\n <td><b style=\"color:${STATE_COLOUR[job.state] ?? \"#000\"}\">${escape(job.state)}</b> <span class=\"dim\">${escape(waiting)}</span></td>\n <td>${claimed ? `<code>${escape(fingerprintOf(claimed.device))}</code>` : \"<span class='dim'>—</span>\"}</td>\n <td>${job.payload ? \"sealed\" : \"<span class='dim'>—</span>\"}</td>\n <td>${job.result ? escape(job.disposition ?? \"?\") : \"<span class='dim'>—</span>\"}</td>\n </tr>`;\n}\n\nexport function debugPage(state: RelayState, now: number): string {\n const jobs = state.jobs();\n const devices = state.everyone();\n\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>byollm relay — debug</title>\n<meta http-equiv=\"refresh\" content=\"1\">\n<style>\n body{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:24px;color:#111;background:#fff}\n h1{font-size:15px;margin:0 0 4px} h2{font-size:13px;margin:24px 0 6px}\n table{border-collapse:collapse;width:100%;margin-top:4px}\n th,td{text-align:left;padding:4px 8px;border-bottom:1px solid #eee;vertical-align:top}\n th{font-weight:600;color:#666;border-bottom:1px solid #ccc}\n code{background:#f5f5f5;padding:1px 4px;border-radius:3px}\n .dim{color:#aaa} .note{color:#666;max-width:70ch;margin:8px 0 0}\n @media(prefers-color-scheme:dark){\n body{background:#111;color:#eee} th{color:#999;border-color:#333}\n td{border-color:#222} code{background:#1c1c1c} .note{color:#999}\n }\n</style></head><body>\n<h1>byollm relay — debug</h1>\n<p class=\"note\">Everything this relay knows, which is everything on this page.\nThere is no prompt or result text here because it holds none: payloads and\nresults are sealed to their endpoints and pass through as ciphertext.</p>\n\n<h2>Routed jobs (${String(jobs.length)})</h2>\n<table>\n<tr><th>job</th><th>kind</th><th>owner</th><th>audience</th><th>size</th>\n <th>stream</th><th>state</th><th>claimed by</th><th>payload</th><th>result</th></tr>\n${jobs.length ? jobs.map((j) => jobRow(j, now)).join(\"\\n\") : `<tr><td colspan=\"10\" class=\"dim\">nothing routed yet</td></tr>`}\n</table>\n\n<h2>Presence (${String(devices.length)})</h2>\n<table>\n<tr><th>runner</th><th>owner</th><th>fingerprint</th><th>last seen</th><th>routing</th></tr>\n${\n devices.length\n ? devices\n .map(\n (d) => `<tr>\n <td><code>${escape(d.runnerId)}</code></td>\n <td>${escape(d.owner)}</td>\n <td><code>${escape(fingerprintOf(d.device))}</code></td>\n <td>${String(Math.max(0, now - d.lastSeenAt))}ms ago</td>\n <td>${d.revoked ? \"<b style='color:#c00'>revoked</b>\" : \"active\"}</td>\n</tr>`,\n )\n .join(\"\\n\")\n : `<tr><td colspan=\"5\" class=\"dim\">no devices connected</td></tr>`\n}\n</table>\n</body></html>`;\n}\n","import { PublicIdentity } from \"@byollm/protocol\";\nimport { z } from \"zod\";\n\n/**\n * What the relay is told about the world — cloud_004 §14.\n *\n * The relay decides nothing about who may talk to whom. It is handed a\n * projection of the control plane and routes according to it. Today that\n * projection is a file; later it is whatever the suite serves. Either way the\n * relay's own state is derived and disposable: delete it and the fixture\n * rebuilds it.\n *\n * ## This shape is a contract, not a test convenience\n *\n * cloud_004 §14 flags it and the flag is worth repeating here, where someone\n * will be tempted to add a field: **this is the projection contract.** The\n * first real control plane will be written to produce whatever this says, and\n * a field added carelessly now is a field the suite must produce forever.\n *\n * So two rules for anything added later:\n *\n * 1. **It must be something a control plane can actually know.** The relay\n * cannot be given facts that only a daemon or only a site holds — that is\n * how a blind relay stops being blind, one convenient field at a time.\n * 2. **It must be a decision, not a derivation.** Consent is a decision.\n * Presence is not: the relay learns that from heartbeats. Anything the\n * relay can observe does not belong in the projection.\n *\n * ## What is deliberately absent\n *\n * No private keys, of any party, ever. The relay holds public keys so it can\n * *verify* signatures and *tell a site who to seal to*. It holds no key that\n * can open anything, and {@link RelayFixture} has no field where one could be\n * put — `RELAY_BLIND` as a type, not as a promise.\n */\n\n/**\n * A user's decision to let one site use their compute — cloud_004 §3.\n *\n * `CONSENT_BEFORE_ROUTE`: with no record here, the relay refuses to route,\n * and there is no discovery path that creates one. Consent is a click in the\n * control plane; the relay only ever reads the result.\n */\nexport const ConsentRecord = z\n .object({\n /** The user, as the control plane identifies them. */\n owner: z.string().min(1),\n /** Which site this consent is for. Scoped: consent is never global. */\n siteId: z.string().min(1),\n /**\n * The site's public identity, as the relay will hand it to the daemon.\n *\n * The relay distributes it and cannot use it: an identity key verifies\n * signatures and seals nothing. This is the key-exchange half of consent\n * (cloud_004 §3), and both endpoints pin what they receive.\n */\n site: PublicIdentity,\n })\n .strict();\nexport type ConsentRecord = z.infer<typeof ConsentRecord>;\n\n/**\n * A named group whose members may use a shared machine — cloud_004 §11.\n *\n * The roster lives here and **never reaches a site**. A site learns whether a\n * consenting user has reachable compute; it never learns who else is on the\n * roster. That is `ROSTERS_NEVER_LEAK` in cloud_004 §11.4, and the reason\n * this type has no outbound representation anywhere in this package.\n */\nexport const RosterRecord = z\n .object({\n /** Stable id for the group, used only inside the relay. */\n id: z.string().min(1),\n /** Who owns the shared compute. */\n owner: z.string().min(1),\n /** Members who may route to it. The owner is not implicitly a member. */\n members: z.array(z.string().min(1)),\n })\n .strict();\nexport type RosterRecord = z.infer<typeof RosterRecord>;\n\nexport const RelayFixture = z\n .object({\n consents: z.array(ConsentRecord),\n rosters: z.array(RosterRecord).default([]),\n /**\n * Owners whose routing is revoked, by `owner:siteId`.\n *\n * A separate list rather than deleting the consent record, because the\n * freeze gate needs revocation to be a *fixture edit* observable within\n * one heartbeat, and \"the row is gone\" and \"the row was revoked\" are\n * different things to a reader debugging why routing stopped.\n */\n revoked: z.array(z.string().min(1)).default([]),\n })\n .strict();\nexport type RelayFixture = z.infer<typeof RelayFixture>;\n\n/** An empty projection: nothing consented, so nothing routes. */\nexport const EMPTY_FIXTURE: RelayFixture = {\n consents: [],\n rosters: [],\n revoked: [],\n};\n\n/**\n * The relay's read-only view of the projection.\n *\n * Deliberately a handful of questions rather than the raw fixture: every\n * caller asking \"may this route?\" through one method is what makes\n * `CONSENT_BEFORE_ROUTE` reviewable, and it leaves room for the projection to\n * become a service without touching a single call site.\n */\nexport class Projection {\n #fixture: RelayFixture;\n\n constructor(fixture: RelayFixture = EMPTY_FIXTURE) {\n this.#fixture = RelayFixture.parse(fixture);\n }\n\n /** Replace the projection wholesale — the control plane pushed a new one. */\n replace(fixture: RelayFixture): void {\n this.#fixture = RelayFixture.parse(fixture);\n }\n\n /** The consent binding this owner to this site, if it exists and stands. */\n consentFor(owner: string, siteId: string): ConsentRecord | null {\n if (this.#fixture.revoked.includes(`${owner}:${siteId}`)) return null;\n return (\n this.#fixture.consents.find(\n (c) => c.owner === owner && c.siteId === siteId,\n ) ?? null\n );\n }\n\n /**\n * May this device's owner run work belonging to `jobOwner`?\n *\n * The relay's half of `AUDIENCE_BOTH_SIDES`. It is only ever a *narrowing*:\n * the daemon re-checks its own allowlist locally and may still refuse, and\n * the site's audience already bounded who could be offered the job. A relay\n * that answered `true` for everyone would not widen anything — which is\n * exactly the property that lets it be blind.\n */\n mayRunFor(deviceOwner: string, jobOwner: string): boolean {\n if (deviceOwner === jobOwner) return true;\n return this.#fixture.rosters.some(\n (r) => r.owner === deviceOwner && r.members.includes(jobOwner),\n );\n }\n}\n","import { JobStub, SealedEnvelope } from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { PlaneResult } from \"./daemon-plane.js\";\nimport type { RelayState } from \"./state.js\";\n\n/**\n * The plane a site talks to.\n *\n * **Outbound from the site, like everything else in this product.** A relay\n * that called site webhooks would need every site publicly reachable, which is\n * the connectivity problem the hub exists to delete — and it would put the\n * relay in the position of initiating contact, which is the posture the whole\n * design avoids. So a site polls, exactly as a daemon does, and the relay\n * never opens a connection to anyone.\n *\n * ## The three-beat exchange\n *\n * A site cannot seal at enqueue: a payload is encrypted to the device that\n * claims it, and at enqueue nobody has. So enqueue publishes a **stub**, and\n * sealing happens later, on demand:\n *\n * 1. `enqueue` — here is a stub; route it.\n * 2. `pending` — who claimed anything of mine, and what key do I seal to?\n * 3. `payload` — here is the ciphertext for that device.\n *\n * Then `results` collects what comes back. Four endpoints, all polled, none of\n * which ever carries a plaintext or a private key.\n *\n * The gap between beats 2 and 3 is the `awaiting-payload` state, and the\n * reason it needs its own timeout: a site that dies between them leaves a\n * device holding a job whose work will never arrive.\n */\n\nconst EnqueueRequest = z\n .object({\n siteId: z.string().min(1),\n /**\n * Everything the relay learns about the job.\n *\n * `JobStub` is exhaustive by construction and asserted so in the protocol\n * package — a site that tried to attach a prompt here would be refused by\n * the schema, not by a reviewer.\n */\n stub: JobStub,\n })\n .strict();\n\nconst PayloadRequest = z\n .object({\n siteId: z.string().min(1),\n jobId: z.string().min(1),\n /** Sealed to the claiming device. Opaque to us and to the schema. */\n envelope: SealedEnvelope,\n })\n .strict();\n\nconst ok = (body: unknown): PlaneResult => ({ status: 200, body });\nconst fail = (status: number, error: string, message: string): PlaneResult => ({\n status,\n body: { error, message },\n});\n\nexport interface SitePlaneDeps {\n readonly state: RelayState;\n readonly now: () => number;\n}\n\nexport class SitePlane {\n readonly #deps: SitePlaneDeps;\n\n constructor(deps: SitePlaneDeps) {\n this.#deps = deps;\n }\n\n enqueue(body: unknown): PlaneResult {\n const parsed = EnqueueRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"enqueue failed schema validation\");\n }\n const job = this.#deps.state.enqueue({\n id: parsed.data.stub.id,\n siteId: parsed.data.siteId,\n stub: parsed.data.stub,\n });\n return ok({ jobId: job.id, state: job.state });\n }\n\n /**\n * What needs sealing, and who to seal it to.\n *\n * The response carries the claiming device's **public** keys — which is the\n * entire reason a blind relay can exist. The relay is a directory here, not\n * a participant: it tells the site an address, and what the site sends to\n * that address is unreadable on the way through.\n */\n pending(siteId: string): PlaneResult {\n this.#deps.state.sweep(this.#deps.now());\n const jobs = this.#deps.state.awaiting(siteId).map((job) => ({\n jobId: job.id,\n // Non-null by construction: `awaiting` only returns claimed jobs. The\n // optional chain is here so a future state-machine edit that broke that\n // invariant would produce a missing field rather than a crash on the\n // routing path.\n device: job.claimedBy?.device,\n runnerId: job.claimedBy?.runnerId,\n leaseId: job.claimedBy?.leaseId,\n /** So a site can decline to seal for a claim about to expire. */\n awaitingUntil: job.awaitingUntil,\n }));\n return ok({ jobs });\n }\n\n payload(body: unknown): PlaneResult {\n const parsed = PayloadRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"payload failed schema validation\");\n }\n const job = this.#deps.state.job(parsed.data.jobId);\n if (job?.siteId !== parsed.data.siteId) {\n return fail(404, \"not-found\", \"unknown job\");\n }\n if (job.state !== \"awaiting-payload\") {\n // The timeout fired and the job went back to the queue, or another\n // device already has it. Refusing is what makes the timeout mean\n // something: a late seal must not land on a claim that has moved.\n return fail(409, \"too-late\", `job is ${job.state}, not awaiting payload`);\n }\n job.payload = parsed.data.envelope;\n job.state = \"ready\";\n delete job.awaitingUntil;\n return ok({ jobId: job.id, state: job.state });\n }\n\n /** Sealed results, for the site to open and verify. */\n results(siteId: string): PlaneResult {\n const jobs = this.#deps.state.finished(siteId).map((job) => ({\n jobId: job.id,\n envelope: job.result,\n disposition: job.disposition,\n runnerId: job.claimedBy?.runnerId,\n /** The grant the site adopted, so it can complete against it. */\n leaseId: job.claimedBy?.leaseId,\n /**\n * Which device ran it, so the site can verify the signature against the\n * key it was told to seal to — and so `RESULT_PROVENANCE` can name a\n * foreign device rather than guessing (cloud_004 §11.2).\n */\n device: job.claimedBy?.device,\n }));\n return ok({ jobs });\n }\n}\n","import { DaemonPlane, type PlaneResult } from \"./daemon-plane.js\";\nimport { debugPage } from \"./debug.js\";\nimport { Projection, type RelayFixture } from \"./fixture.js\";\nimport { SitePlane } from \"./site-plane.js\";\nimport { RelayState } from \"./state.js\";\n\n/**\n * `@byollm/relay` — the reference relay (cloud_004 §14).\n *\n * A blind relay between byollm sites and daemons: it routes stubs, hands over\n * sealed envelopes it cannot open, and knows who is online. It is the first\n * consumer of byollm_009's session layer that is neither the site nor the\n * device, which makes it the thing that proves the protocol's central claim.\n *\n * ## Why this ships open\n *\n * It is the conformance kit's reference relay, and the kit is public — so it\n * starts where it ends rather than being written closed and ported. A relay\n * that claims to be blind should be readable by the people trusting it, and a\n * third-party daemon testing hub mode should test against real code rather\n * than a mock of it. The production hub — multi-tenant routing, presence at\n * scale, billing, ops — is built on these same interfaces and is not this.\n *\n * ## Blind by construction, not by policy\n *\n * {@link RelayOptions} has no field that can hold a private key, and no type\n * in this package has one either. `RELAY_BLIND` is therefore not a rule the\n * code follows; it is a shape the code has. The only way to make this relay\n * able to read a payload is to change its types, which is a review someone\n * would have to justify rather than a line someone could slip in.\n */\n\nexport interface RelayOptions {\n /**\n * Which site this relay routes for.\n *\n * One, in the skeleton. Multi-tenant routing is the closed piece\n * (cloud_004 §9), and it replaces this field rather than extending it.\n */\n readonly siteId: string;\n /** Consent and rosters, projected from the control plane. */\n readonly fixture?: RelayFixture;\n /** How long a claim is good for. */\n readonly leaseMs?: number;\n /** Injectable clock, so tests move time instead of sleeping. */\n readonly now?: () => number;\n /** Where the daemon plane is mounted. */\n readonly basePath?: string;\n}\n\n/** A running relay: one fetch handler, two planes, one debug page. */\nexport class Relay {\n readonly state: RelayState;\n readonly projection: Projection;\n readonly #daemon: DaemonPlane;\n readonly #site: SitePlane;\n readonly #now: () => number;\n readonly #basePath: string;\n\n constructor(options: RelayOptions) {\n this.state = new RelayState();\n this.projection = new Projection(options.fixture);\n this.#now = options.now ?? Date.now;\n this.#basePath = (options.basePath ?? \"/byollm\").replace(/\\/+$/, \"\");\n this.#daemon = new DaemonPlane({\n state: this.state,\n projection: this.projection,\n now: this.#now,\n leaseMs: options.leaseMs ?? 60_000,\n siteId: options.siteId,\n });\n this.#site = new SitePlane({ state: this.state, now: this.#now });\n }\n\n /** Replace the projection — a control-plane push, or a fixture edit. */\n project(fixture: RelayFixture): void {\n this.projection.replace(fixture);\n }\n\n /**\n * Fire due timers and report what moved.\n *\n * Exposed rather than run on an interval so a test can drive it, and so the\n * production hub can decide its own scheduling. The relay never needs a\n * timer to be *correct* — every read path sweeps first — but a job whose\n * site vanished should return to the queue without waiting for someone to\n * ask about it.\n */\n sweep(): { requeued: string[] } {\n return { requeued: this.state.sweep(this.#now()).map((j) => j.id) };\n }\n\n /** The whole HTTP surface. */\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n if (path === \"/debug\" || path === `${this.#basePath}/debug`) {\n return new Response(debugPage(this.state, this.#now()), {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n });\n }\n\n const rawBody = request.method === \"POST\" ? await request.text() : \"\";\n const body = rawBody === \"\" ? undefined : safeJson(rawBody);\n const auth = {\n endpoint: path.slice(path.lastIndexOf(\"/\") + 1),\n rawBody,\n signature: signatureFrom(request.headers),\n };\n\n // -- the site plane -----------------------------------------------------\n if (path === \"/relay/site/enqueue\") return json(this.#site.enqueue(body));\n if (path === \"/relay/site/payload\") return json(this.#site.payload(body));\n if (path === \"/relay/site/pending\") {\n return json(this.#site.pending(url.searchParams.get(\"siteId\") ?? \"\"));\n }\n if (path === \"/relay/site/results\") {\n return json(this.#site.results(url.searchParams.get(\"siteId\") ?? \"\"));\n }\n\n // -- the daemon plane ---------------------------------------------------\n if (!path.startsWith(`${this.#basePath}/`)) {\n return json({ status: 404, body: { error: \"not-found\" } });\n }\n switch (auth.endpoint) {\n case \"pair\":\n return json(this.#daemon.pair(body));\n case \"claim\":\n return json(this.#daemon.claim(auth, body));\n case \"fetch\":\n return json(this.#daemon.fetch(auth, body));\n case \"result\":\n return json(this.#daemon.result(auth, body));\n case \"heartbeat\":\n return json(this.#daemon.heartbeat(auth, body));\n case \"release\":\n return json(this.#daemon.release(auth, body));\n default:\n return json({ status: 404, body: { error: \"not-found\" } });\n }\n }\n}\n\nfunction safeJson(raw: string): unknown {\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/** Rebuild the signature from headers, refusing anything partial. */\nfunction signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const issuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n // Checked before `Number()`, which turns a missing header into the epoch —\n // a stale-timestamp check that silently passes is worse than none.\n if (runnerId === null || issuedAt === null || signature === null) {\n return undefined;\n }\n return { runnerId, issuedAt: Number(issuedAt), signature };\n}\n\nconst json = (result: PlaneResult): Response =>\n new Response(JSON.stringify(result.body), {\n status: result.status,\n headers: { \"content-type\": \"application/json\" },\n });\n\nexport { Projection, RelayState, debugPage };\nexport type { RelayFixture };\nexport {\n ConsentRecord,\n RosterRecord,\n RelayFixture as RelayFixtureSchema,\n EMPTY_FIXTURE,\n} from \"./fixture.js\";\nexport { AWAITING_PAYLOAD_MS } from \"./state.js\";\nexport type { RoutedJob, RoutedState, Presence } from \"./state.js\";\n"],"mappings":"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","names":["PublicIdentity","z","z","ok","fail"]}
|
|
1
|
+
{"version":3,"sources":["../src/daemon-plane.ts","../src/state.ts","../src/debug.ts","../src/fixture.ts","../src/site-plane.ts","../src/index.ts"],"sourcesContent":["import {\n ClaimRequest,\n FetchRequest,\n HeartbeatRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n RequestSignature,\n keyId,\n verifyRequest,\n verifyPublicIdentity,\n PublicIdentity,\n type ClaimedStub,\n} from \"@byollm/protocol\";\nimport { randomUUID } from \"node:crypto\";\nimport { z } from \"zod\";\nimport type { Projection } from \"./fixture.js\";\nimport { AWAITING_PAYLOAD_MS, type RelayState } from \"./state.js\";\n\n/**\n * The plane a daemon talks to — cloud_004 §2.\n *\n * To a daemon this is an upstream like any other: it claims, fetches, reports\n * and heartbeats exactly as it does against a direct site. That sameness is\n * the point of §9's \"the hub is a deployment of the open parts\" — hub mode is\n * not a second daemon code path, it is a second upstream.\n *\n * What differs is invisible from the daemon's side and total from ours: **this\n * upstream cannot seal.** A direct site answers `fetch` by opening its own\n * envelope and re-sealing to the claiming device. The relay has nothing to\n * open and nothing to seal with, so it answers `fetch` with whatever the site\n * left for that device, or with nothing yet.\n */\n\nexport interface PlaneResult {\n readonly status: number;\n readonly body: unknown;\n}\n\nconst ok = (body: unknown): PlaneResult => ({ status: 200, body });\nconst fail = (status: number, error: string, message: string): PlaneResult => ({\n status,\n body: { error, message },\n});\n\nexport interface DaemonPlaneDeps {\n readonly state: RelayState;\n readonly projection: Projection;\n readonly now: () => number;\n readonly leaseMs: number;\n /**\n * Which site this relay routes for.\n *\n * The skeleton relays for one site because that is all the freeze gate\n * needs. The production hub's multi-tenant router is the closed piece that\n * replaces this field (cloud_004 §9) — recorded here so the seam is visible\n * rather than assumed away.\n */\n readonly siteId: string;\n}\n\nexport class DaemonPlane {\n readonly #deps: DaemonPlaneDeps;\n\n constructor(deps: DaemonPlaneDeps) {\n this.#deps = deps;\n }\n\n /**\n * Pair a device — cloud_004 §3, the key-exchange moment.\n *\n * The relay hands back **the site's** public identity, taken from the\n * consent projection, not its own. This is the sentence that makes hub mode\n * safe: the daemon pins the party that will actually seal its work, so an\n * envelope is verified against the site even though it arrived via us. A\n * relay that substituted its own identity here could inject work — and would\n * need a private key to do it, which is why it has none.\n */\n pair(body: unknown): PlaneResult {\n const parsed = PairFixtureRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"pair request failed schema validation\");\n }\n if (!verifyPublicIdentity(parsed.data.device)) {\n return fail(400, \"bad-request\", \"the device identity is not consistent\");\n }\n\n const consent = this.#deps.projection.consentFor(\n parsed.data.owner,\n this.#deps.siteId,\n );\n if (!consent) {\n // CONSENT_BEFORE_ROUTE. There is no discovery path that creates one:\n // consent is a click somewhere else, and the relay only reads it.\n return fail(403, \"unauthorized\", \"no consent record for this user\");\n }\n\n // The device must already be approved, by a human, in the control plane.\n //\n // It presented keys; that is an assertion, not an identity. Somebody had\n // to look at a fingerprint and say yes, and this is where that decision is\n // enforced. byollm_009's seventh finding stopped a daemon from *naming*\n // itself and this stops it from *keying* itself — otherwise the relay\n // would be the authority on who a machine is, which is precisely the role\n // a blind relay must not hold.\n //\n // Matched on the identity key rather than a claimed id: the key is what\n // the human approved and what every later signature is checked against.\n const approved = this.#deps.projection.deviceByFingerprint(\n parsed.data.device.identity,\n );\n if (!approved) {\n return fail(\n 403,\n \"unauthorized\",\n \"this device has not been approved by its owner\",\n );\n }\n if (approved.owner !== parsed.data.owner) {\n // The device was approved by somebody else. Refused rather than\n // re-owned: an approval is for a person, not a key in general.\n return fail(403, \"unauthorized\", \"this device belongs to another owner\");\n }\n\n // The id comes from the control plane, not from the device and not from\n // here — one authority for identity, and it is the one with the human in\n // it. The relay's own uuid minting was a stopgap for a fixture with no\n // devices in it.\n const runnerId = approved.runnerId;\n\n this.#deps.state.seen({\n runnerId,\n owner: parsed.data.owner,\n device: parsed.data.device,\n lastSeenAt: this.#deps.now(),\n });\n\n return ok({\n protocolVersion: PROTOCOL_VERSION,\n runnerId,\n /** The *site's* key. See the note above — this is load-bearing. */\n site: consent.site,\n });\n }\n\n /** Every authenticated call: signature first, then consent, then work. */\n #authed<T>(\n input: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (\n request: T,\n device: { runnerId: string; owner: string; device: PublicIdentity },\n ) => PlaneResult,\n options: { allowRevoked?: boolean } = {},\n ): PlaneResult {\n const signature = RequestSignature.safeParse(input.signature);\n if (!signature.success) {\n return fail(401, \"unauthorized\", \"this request is not signed\");\n }\n const known = this.#deps.state.presence(signature.data.runnerId);\n if (!known) {\n return fail(401, \"unauthorized\", \"this runner is not recognised\");\n }\n\n const failure = verifyRequest({\n identityPublic: known.device.identity,\n endpoint: input.endpoint,\n body: input.rawBody,\n signature: signature.data,\n now: this.#deps.now(),\n });\n if (failure) return fail(401, \"unauthorized\", \"signature check failed\");\n\n // Revocation reaches the daemon through heartbeat, so heartbeat itself\n // must be answerable by a revoked runner — bouncing it with a 403 would\n // read to the daemon as a transport problem and it would keep trying.\n if (known.revoked && options.allowRevoked !== true) {\n return fail(403, \"revoked\", \"routing for this runner has been revoked\");\n }\n\n known.lastSeenAt = this.#deps.now();\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(400, \"bad-request\", \"request failed schema validation\");\n }\n return run(parsed.data, known);\n }\n\n claim(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ClaimRequest, (request, device) => {\n if (request.runnerId !== device.runnerId) {\n return fail(401, \"unauthorized\", \"runner id does not match the key\");\n }\n const now = this.#deps.now();\n this.#deps.state.sweep(now);\n\n const kinds = new Set(request.capabilities.map((c) => c.kind));\n const granted: ClaimedStub[] = [];\n\n for (const job of this.#deps.state.jobs()) {\n if (granted.length >= request.max) break;\n if (job.state !== \"queued\") continue;\n if (!kinds.has(job.stub.kind)) continue;\n // The relay's half of AUDIENCE_BOTH_SIDES. The daemon re-checks its\n // own allowlist and may still refuse — this only narrows.\n if (!this.#deps.projection.mayRunFor(device.owner, job.stub.owner)) {\n continue;\n }\n\n // A UUID, not a readable composite. The direct plane's lease ids are\n // UUIDs and the Supabase adapter's `lease_id` column is typed `uuid`\n // — so a relay minting `lease_<job>_<time>` would route perfectly\n // against a memory store and fail the moment a real site adopted the\n // lease into Postgres. Same shape as the `job_<uuid>` bug: an id that\n // is only a string until something declares what kind of string.\n const leaseId = randomUUID();\n job.state = \"awaiting-payload\";\n job.claimedBy = {\n runnerId: device.runnerId,\n owner: device.owner,\n device: device.device,\n leaseId,\n leaseExpiresAt: now + this.#deps.leaseMs,\n };\n // The clock §7 requires and the direct plane never needed. It is not\n // the lease: it bounds how long we wait for a site, not how long the\n // device may work.\n job.awaitingUntil = now + AWAITING_PAYLOAD_MS;\n\n granted.push({\n ...job.stub,\n lease: {\n id: leaseId,\n runnerId: device.runnerId,\n expiresAt: job.claimedBy.leaseExpiresAt,\n },\n });\n }\n\n return ok({ jobs: granted, leaseMs: this.#deps.leaseMs });\n });\n }\n\n /**\n * Hand over the sealed payload, if the site has left one.\n *\n * The one endpoint whose behaviour differs from a direct site's, and the\n * difference is the whole design: a direct site seals here, on demand,\n * because it holds the keys. The relay waits. A `409` means \"claimed, not\n * yet sealed\" — a daemon should retry, not treat it as a refusal, because\n * the job is still legitimately theirs until the lease or the\n * awaiting-payload clock says otherwise.\n */\n fetch(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, FetchRequest, (request, device) => {\n const job = this.#deps.state.job(request.jobId);\n if (!job) return fail(404, \"not-found\", \"unknown job\");\n if (job.claimedBy?.runnerId !== device.runnerId) {\n return fail(403, \"unauthorized\", \"this runner does not hold the job\");\n }\n if (job.claimedBy.leaseId !== request.leaseId) {\n // LEASE_HONORED per *instance*: a stale lease id names a grant that\n // is over, and answering it would hand work to a previous holder.\n return fail(403, \"unauthorized\", \"that lease is no longer current\");\n }\n if (!job.payload) {\n return fail(409, \"not-ready\", \"the site has not sealed this job yet\");\n }\n job.state = \"running\";\n return ok({ envelope: job.payload });\n });\n }\n\n /**\n * Take a sealed result.\n *\n * The relay stores ciphertext and records the disposition so it can stop\n * dispatching. It cannot check the two against each other — that requires\n * opening the envelope, which is the site's job and the site's key. This is\n * the asymmetry byollm_009 §6.1 describes: the hint is actionable here and\n * only verifiable there.\n */\n result(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ResultRequest, (request, device) => {\n const job = this.#deps.state.job(request.jobId);\n if (!job) return fail(404, \"not-found\", \"unknown job\");\n if (job.claimedBy?.runnerId !== device.runnerId) {\n return fail(403, \"unauthorized\", \"this runner does not hold the job\");\n }\n if (job.state === \"done\") {\n return ok({ accepted: false, state: job.state });\n }\n job.result = request.envelope;\n job.disposition = request.disposition;\n job.state = \"done\";\n return ok({ accepted: true, state: job.state });\n });\n }\n\n heartbeat(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n (request, device) => {\n const now = this.#deps.now();\n this.#deps.state.sweep(now);\n\n const known = this.#deps.state.presence(device.runnerId);\n // Revocation is a fixture edit, and this is where the daemon learns\n // of it — within one heartbeat, which is what the freeze gate times.\n const consent = this.#deps.projection.consentFor(\n device.owner,\n this.#deps.siteId,\n );\n const revoked = consent === null;\n if (known) known.revoked = revoked;\n\n // Anything this runner thinks it holds that we no longer agree it\n // holds. A daemon must stop work on these rather than finish and\n // report into a lease that is gone.\n const lost = request.activeLeases\n .filter(({ jobId, leaseId }) => {\n const job = this.#deps.state.job(jobId);\n return job?.claimedBy?.leaseId !== leaseId;\n })\n .map(({ jobId }) => jobId);\n\n return ok({\n revoked,\n cancel: [],\n leases: [],\n lost,\n serverTime: now,\n });\n },\n { allowRevoked: true },\n );\n }\n\n release(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): PlaneResult {\n return this.#authed(auth, body, ReleaseRequest, (request, device) => {\n const released: string[] = [];\n for (const { jobId, leaseId } of request.leases) {\n const job = this.#deps.state.job(jobId);\n if (!job || job.claimedBy?.runnerId !== device.runnerId) continue;\n if (job.claimedBy.leaseId !== leaseId) continue;\n this.#deps.state.requeue(job);\n released.push(jobId);\n }\n return ok({ released });\n });\n }\n}\n\n/**\n * Pairing, as the skeleton does it.\n *\n * Not the public `PairRequest`: that models a device-code exchange with a\n * human at a browser, and the skeleton's consent arrives from a fixture\n * instead (cloud_004 §14). The daemon-visible *outcome* is identical — a\n * runner id and the site's pinned public identity — so nothing downstream can\n * tell the difference, which is what makes this substitution honest rather\n * than a shortcut around the consent MUST.\n */\nconst PairFixtureRequest = z\n .object({\n protocolVersion: z.literal(PROTOCOL_VERSION),\n owner: z.string().min(1),\n device: PublicIdentity,\n })\n .strict();\n\n/** Exported so the debug page can name a device the way a human would. */\nexport const fingerprintOf = (identity: PublicIdentity): string =>\n keyId(identity.identity);\n","import type { JobStub, PublicIdentity, SealedEnvelope } from \"@byollm/protocol\";\n\n/**\n * The relay's routing state — byollm_009 §7, reachable at last.\n *\n * §7 described a state machine the direct plane could not produce. There, the\n * site and the upstream are the same party: it seals when it likes, and a job\n * is never claimed-but-unsealed. Here they are different parties, and the gap\n * between them is a state:\n *\n * ```\n * queued ──claim──▶ awaiting-payload ──sealed──▶ ready ──fetch──▶ running\n * ▲ │ │\n * └────────────────────┘ ▼\n * site never seals, or seals too late ok | error | canceled\n * ```\n *\n * The relay cannot seal, so it cannot shortcut this. A payload is encrypted\n * to *the device that claimed it*, and nobody knows which device that is until\n * the claim happens — which is precisely why claim-then-fetch makes a blind\n * relay possible at all. The window is the price.\n *\n * ## What the relay holds, and what it cannot\n *\n * Stubs (metadata the site chose to publish), sealed envelopes it cannot open,\n * and public keys. There is no field on any type in this file that could hold\n * a private key or a plaintext, which is `RELAY_BLIND` expressed as a data\n * model rather than as a policy.\n */\n\n/** Where a routed job is. */\nexport type RoutedState =\n \"queued\" | \"awaiting-payload\" | \"ready\" | \"running\" | \"done\";\n\n/**\n * How long a site has to seal after one of its jobs is claimed.\n *\n * **Distinct from the lease, and distinct from the job's TTL** — byollm_009\n * §7.1. Three clocks, three different questions:\n *\n * - the **TTL** asks how long the work is worth doing at all;\n * - the **lease** asks how long this device gets to run it;\n * - this asks how long we wait for a site that has gone away.\n *\n * Collapsing any pair of them looks harmless until a site restarts during a\n * deploy: with only a lease, the device sits politely holding a job whose\n * payload will never arrive, and the lease's whole minute is spent waiting on\n * a party that is not coming back. Short, because a site that is up answers in\n * milliseconds and a site that is down will not answer sooner for waiting.\n */\nexport const AWAITING_PAYLOAD_MS = 10_000;\n\n/** A job the relay is routing. Metadata and ciphertext, nothing else. */\nexport interface RoutedJob {\n readonly id: string;\n /** Which site enqueued it — the party that will be asked to seal. */\n readonly siteId: string;\n /**\n * Everything the relay knows about the work, which is everything the site\n * chose to publish and not one field more (byollm_009 §6).\n */\n readonly stub: JobStub;\n state: RoutedState;\n /** Set from the claim; the site seals to these keys. */\n claimedBy?: {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n readonly leaseId: string;\n readonly leaseExpiresAt: number;\n };\n /** When {@link AWAITING_PAYLOAD_MS} runs out for this claim. */\n awaitingUntil?: number;\n /** Sealed to the claiming device by the site. Opaque here. */\n payload?: SealedEnvelope;\n /** Sealed to the site by the device. Opaque here. */\n result?: SealedEnvelope;\n /**\n * The result's clear-text discriminator — byollm_009 §6.1.\n *\n * The one outcome fact the relay is given, and the reason it is given:\n * without it the relay cannot stop dispatching a finished job. A routing\n * hint and never a fact — the *site* verifies it against the sealed\n * outcome, because only the site can open the envelope. The relay acts on\n * it and is entitled to be wrong; a lying daemon costs it a dispatch\n * decision, not a security property.\n */\n disposition?: \"ok\" | \"error\" | \"canceled\";\n}\n\n/** A device the relay has seen recently. */\nexport interface Presence {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n lastSeenAt: number;\n /** Set on revocation so the next request is refused rather than routed. */\n revoked: boolean;\n}\n\n/**\n * In-memory routing state.\n *\n * Deliberately not durable. The skeleton proves the protocol, and the\n * production hub replaces this with the closed multi-tenant router behind the\n * same shape (cloud_004 §9). Anything a restart loses here is a job that\n * returns to its site's queue — which is the behaviour a lapsed lease already\n * has to produce, so nothing new needs to be true for this to be safe.\n */\nexport class RelayState {\n readonly #jobs = new Map<string, RoutedJob>();\n readonly #presence = new Map<string, Presence>();\n\n /** Take a stub for routing. The payload is not here and will not be. */\n enqueue(input: { id: string; siteId: string; stub: JobStub }): RoutedJob {\n const job: RoutedJob = {\n id: input.id,\n siteId: input.siteId,\n stub: input.stub,\n state: \"queued\",\n };\n this.#jobs.set(job.id, job);\n return job;\n }\n\n job(jobId: string): RoutedJob | undefined {\n return this.#jobs.get(jobId);\n }\n\n jobs(): RoutedJob[] {\n return [...this.#jobs.values()];\n }\n\n /** Jobs a site must seal for, right now. */\n awaiting(siteId: string): RoutedJob[] {\n return this.jobs().filter(\n (j) => j.siteId === siteId && j.state === \"awaiting-payload\",\n );\n }\n\n /** Sealed results waiting to go home. */\n finished(siteId: string): RoutedJob[] {\n return this.jobs().filter(\n (j) =>\n j.siteId === siteId && j.state === \"done\" && j.result !== undefined,\n );\n }\n\n seen(presence: Omit<Presence, \"revoked\">): Presence {\n const existing = this.#presence.get(presence.runnerId);\n if (existing) {\n existing.lastSeenAt = presence.lastSeenAt;\n return existing;\n }\n const fresh: Presence = { ...presence, revoked: false };\n this.#presence.set(presence.runnerId, fresh);\n return fresh;\n }\n\n presence(runnerId: string): Presence | undefined {\n return this.#presence.get(runnerId);\n }\n\n everyone(): Presence[] {\n return [...this.#presence.values()];\n }\n\n /**\n * Return a job to the queue, forgetting the claim.\n *\n * The stub survives; nothing is lost. That is `LEASE_RECLAIMABLE` and it is\n * why the awaiting-payload timeout is cheap to fire: the worst case is that\n * a device did nothing for ten seconds and another one gets a turn.\n */\n requeue(job: RoutedJob): void {\n job.state = \"queued\";\n delete job.claimedBy;\n delete job.awaitingUntil;\n delete job.payload;\n }\n\n /**\n * Fire whatever the clock says is due, and report it.\n *\n * Returns the jobs it requeued so a caller can log or surface them — a\n * timeout that fires invisibly is indistinguishable from a job that was\n * never claimed, and those want very different debugging.\n */\n sweep(now: number): RoutedJob[] {\n const requeued: RoutedJob[] = [];\n for (const job of this.#jobs.values()) {\n if (job.state === \"awaiting-payload\" && (job.awaitingUntil ?? 0) <= now) {\n this.requeue(job);\n requeued.push(job);\n }\n const lease = job.claimedBy;\n if (\n lease &&\n (job.state === \"ready\" || job.state === \"running\") &&\n lease.leaseExpiresAt <= now\n ) {\n this.requeue(job);\n requeued.push(job);\n }\n }\n return requeued;\n }\n}\n","import { fingerprintOf } from \"./daemon-plane.js\";\nimport type { RelayState, RoutedJob } from \"./state.js\";\n\n/**\n * The debug page — cloud_004 §10.\n *\n * It exists because watching a stub get claimed beats reading store rows, and\n * it earns its keep from the first routed job rather than being a thing\n * someone builds later when routing is already hard to follow.\n *\n * One screen, no build step, no dependencies. It renders from the relay's own\n * state, so it cannot show anything the relay does not actually know — which\n * makes it an honest demonstration of blindness as well as a debugging tool.\n * There is no view here that could show a prompt, because there is no prompt\n * to show.\n */\n\nconst escape = (value: string): string =>\n value.replace(\n /[&<>\"]/g,\n (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c] ?? c,\n );\n\nconst STATE_COLOUR: Record<string, string> = {\n queued: \"#8a8a8a\",\n \"awaiting-payload\": \"#c98a00\",\n ready: \"#0a7\",\n running: \"#06c\",\n done: \"#444\",\n};\n\nfunction jobRow(job: RoutedJob, now: number): string {\n const claimed = job.claimedBy;\n const waiting =\n job.state === \"awaiting-payload\" && job.awaitingUntil !== undefined\n ? `${String(Math.max(0, job.awaitingUntil - now))}ms left`\n : \"\";\n return `<tr>\n <td><code>${escape(job.id)}</code></td>\n <td>${escape(job.stub.kind)}</td>\n <td>${escape(job.stub.owner)}</td>\n <td>${escape(job.stub.audience)}</td>\n <td>${escape(job.stub.sizeClass)}</td>\n <td>${job.stub.streaming ? \"yes\" : \"no\"}</td>\n <td><b style=\"color:${STATE_COLOUR[job.state] ?? \"#000\"}\">${escape(job.state)}</b> <span class=\"dim\">${escape(waiting)}</span></td>\n <td>${claimed ? `<code>${escape(fingerprintOf(claimed.device))}</code>` : \"<span class='dim'>—</span>\"}</td>\n <td>${job.payload ? \"sealed\" : \"<span class='dim'>—</span>\"}</td>\n <td>${job.result ? escape(job.disposition ?? \"?\") : \"<span class='dim'>—</span>\"}</td>\n </tr>`;\n}\n\nexport function debugPage(state: RelayState, now: number): string {\n const jobs = state.jobs();\n const devices = state.everyone();\n\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>byollm relay — debug</title>\n<meta http-equiv=\"refresh\" content=\"1\">\n<style>\n body{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:24px;color:#111;background:#fff}\n h1{font-size:15px;margin:0 0 4px} h2{font-size:13px;margin:24px 0 6px}\n table{border-collapse:collapse;width:100%;margin-top:4px}\n th,td{text-align:left;padding:4px 8px;border-bottom:1px solid #eee;vertical-align:top}\n th{font-weight:600;color:#666;border-bottom:1px solid #ccc}\n code{background:#f5f5f5;padding:1px 4px;border-radius:3px}\n .dim{color:#aaa} .note{color:#666;max-width:70ch;margin:8px 0 0}\n @media(prefers-color-scheme:dark){\n body{background:#111;color:#eee} th{color:#999;border-color:#333}\n td{border-color:#222} code{background:#1c1c1c} .note{color:#999}\n }\n</style></head><body>\n<h1>byollm relay — debug</h1>\n<p class=\"note\">Everything this relay knows, which is everything on this page.\nThere is no prompt or result text here because it holds none: payloads and\nresults are sealed to their endpoints and pass through as ciphertext.</p>\n\n<h2>Routed jobs (${String(jobs.length)})</h2>\n<table>\n<tr><th>job</th><th>kind</th><th>owner</th><th>audience</th><th>size</th>\n <th>stream</th><th>state</th><th>claimed by</th><th>payload</th><th>result</th></tr>\n${jobs.length ? jobs.map((j) => jobRow(j, now)).join(\"\\n\") : `<tr><td colspan=\"10\" class=\"dim\">nothing routed yet</td></tr>`}\n</table>\n\n<h2>Presence (${String(devices.length)})</h2>\n<table>\n<tr><th>runner</th><th>owner</th><th>fingerprint</th><th>last seen</th><th>routing</th></tr>\n${\n devices.length\n ? devices\n .map(\n (d) => `<tr>\n <td><code>${escape(d.runnerId)}</code></td>\n <td>${escape(d.owner)}</td>\n <td><code>${escape(fingerprintOf(d.device))}</code></td>\n <td>${String(Math.max(0, now - d.lastSeenAt))}ms ago</td>\n <td>${d.revoked ? \"<b style='color:#c00'>revoked</b>\" : \"active\"}</td>\n</tr>`,\n )\n .join(\"\\n\")\n : `<tr><td colspan=\"5\" class=\"dim\">no devices connected</td></tr>`\n}\n</table>\n</body></html>`;\n}\n","import { PublicIdentity } from \"@byollm/protocol\";\nimport { z } from \"zod\";\n\n/**\n * What the relay is told about the world — cloud_004 §14.\n *\n * The relay decides nothing about who may talk to whom. It is handed a\n * projection of the control plane and routes according to it. Today that\n * projection is a file; later it is whatever the suite serves. Either way the\n * relay's own state is derived and disposable: delete it and the fixture\n * rebuilds it.\n *\n * ## This shape is a contract, not a test convenience\n *\n * cloud_004 §14 flags it and the flag is worth repeating here, where someone\n * will be tempted to add a field: **this is the projection contract.** The\n * first real control plane will be written to produce whatever this says, and\n * a field added carelessly now is a field the suite must produce forever.\n *\n * So two rules for anything added later:\n *\n * 1. **It must be something a control plane can actually know.** The relay\n * cannot be given facts that only a daemon or only a site holds — that is\n * how a blind relay stops being blind, one convenient field at a time.\n * 2. **It must be a decision, not a derivation.** Consent is a decision.\n * Presence is not: the relay learns that from heartbeats. Anything the\n * relay can observe does not belong in the projection.\n *\n * ## What is deliberately absent\n *\n * No private keys, of any party, ever. The relay holds public keys so it can\n * *verify* signatures and *tell a site who to seal to*. It holds no key that\n * can open anything, and {@link RelayFixture} has no field where one could be\n * put — `RELAY_BLIND` as a type, not as a promise.\n */\n\n/**\n * A user's decision to let one site use their compute — cloud_004 §3.\n *\n * `CONSENT_BEFORE_ROUTE`: with no record here, the relay refuses to route,\n * and there is no discovery path that creates one. Consent is a click in the\n * control plane; the relay only ever reads the result.\n */\nexport const ConsentRecord = z\n .object({\n /** The user, as the control plane identifies them. */\n owner: z.string().min(1),\n /** Which site this consent is for. Scoped: consent is never global. */\n siteId: z.string().min(1),\n /**\n * The site's public identity, as the relay will hand it to the daemon.\n *\n * The relay distributes it and cannot use it: an identity key verifies\n * signatures and seals nothing. This is the key-exchange half of consent\n * (cloud_004 §3), and both endpoints pin what they receive.\n */\n site: PublicIdentity,\n })\n .strict();\nexport type ConsentRecord = z.infer<typeof ConsentRecord>;\n\n/**\n * A named group whose members may use a shared machine — cloud_004 §11.\n *\n * The roster lives here and **never reaches a site**. A site learns whether a\n * consenting user has reachable compute; it never learns who else is on the\n * roster. That is `ROSTERS_NEVER_LEAK` in cloud_004 §11.4, and the reason\n * this type has no outbound representation anywhere in this package.\n */\nexport const RosterRecord = z\n .object({\n /** Stable id for the group, used only inside the relay. */\n id: z.string().min(1),\n /** Who owns the shared compute. */\n owner: z.string().min(1),\n /** Members who may route to it. The owner is not implicitly a member. */\n members: z.array(z.string().min(1)),\n })\n .strict();\nexport type RosterRecord = z.infer<typeof RosterRecord>;\n\n/**\n * A device its owner has approved — cloud_005 §7.1.\n *\n * The relay refuses a device that is not here, and that refusal is the point.\n * byollm_009's seventh finding stopped a daemon from *naming* itself; this\n * stops it from *keying* itself. A device that presents keys nobody approved\n * is a device whose owner never saw a fingerprint, and pairing it would make\n * the relay the authority on identity — which is exactly what it must not be.\n *\n * The three-party shape consent already has, applied to identity: the device\n * asserts, a human confirms in the control plane, the relay checks.\n */\nexport const DeviceRecord = z\n .object({\n /** Who approved it. */\n owner: z.string().min(1),\n /** The id the control plane assigned — the device does not choose it. */\n runnerId: z.string().min(1),\n /** The keys a human compared a fingerprint of before approving. */\n device: PublicIdentity,\n })\n .strict();\nexport type DeviceRecord = z.infer<typeof DeviceRecord>;\n\n/** A revoked route, named by its parts. */\nexport const RevocationRecord = z\n .object({ owner: z.string().min(1), siteId: z.string().min(1) })\n .strict();\nexport type RevocationRecord = z.infer<typeof RevocationRecord>;\n\nexport const RelayFixture = z\n .object({\n consents: z.array(ConsentRecord),\n devices: z.array(DeviceRecord).default([]),\n rosters: z.array(RosterRecord).default([]),\n /**\n * Routes that were revoked, as structured pairs.\n *\n * A separate list rather than deleting the consent record, because the\n * freeze gate needs revocation to be an observable *event* rather than an\n * absence — \"the row is gone\" and \"the row was revoked\" are different\n * answers to someone debugging why routing stopped.\n *\n * `{owner, siteId}` and never the composite string `\"owner:siteId\"`. A\n * composite key is a parser waiting to meet an id containing its\n * separator, which is the lesson the composite lease ids taught against\n * Postgres — applied here before it became a contract.\n */\n revoked: z.array(RevocationRecord).default([]),\n })\n .strict();\nexport type RelayFixture = z.infer<typeof RelayFixture>;\n\n/** An empty projection: nothing consented, so nothing routes. */\nexport const EMPTY_FIXTURE: RelayFixture = {\n consents: [],\n devices: [],\n rosters: [],\n revoked: [],\n};\n\n/**\n * The relay's read-only view of the projection.\n *\n * Deliberately a handful of questions rather than the raw fixture: every\n * caller asking \"may this route?\" through one method is what makes\n * `CONSENT_BEFORE_ROUTE` reviewable, and it leaves room for the projection to\n * become a service without touching a single call site.\n */\nexport class Projection {\n #fixture: RelayFixture;\n\n constructor(fixture: RelayFixture = EMPTY_FIXTURE) {\n this.#fixture = RelayFixture.parse(fixture);\n }\n\n /** Replace the projection wholesale — the control plane pushed a new one. */\n replace(fixture: RelayFixture): void {\n this.#fixture = RelayFixture.parse(fixture);\n }\n\n /**\n * The device this runner id names, if a human approved it.\n *\n * Returns null for a device the control plane does not know, which is how\n * the relay refuses to be the authority on identity.\n */\n deviceFor(runnerId: string): DeviceRecord | null {\n return this.#fixture.devices.find((d) => d.runnerId === runnerId) ?? null;\n }\n\n /** The device approved for these exact keys, if any. */\n deviceByFingerprint(identityPublic: string): DeviceRecord | null {\n return (\n this.#fixture.devices.find((d) => d.device.identity === identityPublic) ??\n null\n );\n }\n\n /** The consent binding this owner to this site, if it exists and stands. */\n consentFor(owner: string, siteId: string): ConsentRecord | null {\n const revoked = this.#fixture.revoked.some(\n (r) => r.owner === owner && r.siteId === siteId,\n );\n if (revoked) return null;\n return (\n this.#fixture.consents.find(\n (c) => c.owner === owner && c.siteId === siteId,\n ) ?? null\n );\n }\n\n /**\n * May this device's owner run work belonging to `jobOwner`?\n *\n * The relay's half of `AUDIENCE_BOTH_SIDES`. It is only ever a *narrowing*:\n * the daemon re-checks its own allowlist locally and may still refuse, and\n * the site's audience already bounded who could be offered the job. A relay\n * that answered `true` for everyone would not widen anything — which is\n * exactly the property that lets it be blind.\n */\n mayRunFor(deviceOwner: string, jobOwner: string): boolean {\n if (deviceOwner === jobOwner) return true;\n return this.#fixture.rosters.some(\n (r) => r.owner === deviceOwner && r.members.includes(jobOwner),\n );\n }\n}\n","import { JobStub, SealedEnvelope } from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { PlaneResult } from \"./daemon-plane.js\";\nimport type { RelayState } from \"./state.js\";\n\n/**\n * The plane a site talks to.\n *\n * **Outbound from the site, like everything else in this product.** A relay\n * that called site webhooks would need every site publicly reachable, which is\n * the connectivity problem the hub exists to delete — and it would put the\n * relay in the position of initiating contact, which is the posture the whole\n * design avoids. So a site polls, exactly as a daemon does, and the relay\n * never opens a connection to anyone.\n *\n * ## The three-beat exchange\n *\n * A site cannot seal at enqueue: a payload is encrypted to the device that\n * claims it, and at enqueue nobody has. So enqueue publishes a **stub**, and\n * sealing happens later, on demand:\n *\n * 1. `enqueue` — here is a stub; route it.\n * 2. `pending` — who claimed anything of mine, and what key do I seal to?\n * 3. `payload` — here is the ciphertext for that device.\n *\n * Then `results` collects what comes back. Four endpoints, all polled, none of\n * which ever carries a plaintext or a private key.\n *\n * The gap between beats 2 and 3 is the `awaiting-payload` state, and the\n * reason it needs its own timeout: a site that dies between them leaves a\n * device holding a job whose work will never arrive.\n */\n\nconst EnqueueRequest = z\n .object({\n siteId: z.string().min(1),\n /**\n * Everything the relay learns about the job.\n *\n * `JobStub` is exhaustive by construction and asserted so in the protocol\n * package — a site that tried to attach a prompt here would be refused by\n * the schema, not by a reviewer.\n */\n stub: JobStub,\n })\n .strict();\n\nconst PayloadRequest = z\n .object({\n siteId: z.string().min(1),\n jobId: z.string().min(1),\n /** Sealed to the claiming device. Opaque to us and to the schema. */\n envelope: SealedEnvelope,\n })\n .strict();\n\nconst ok = (body: unknown): PlaneResult => ({ status: 200, body });\nconst fail = (status: number, error: string, message: string): PlaneResult => ({\n status,\n body: { error, message },\n});\n\nexport interface SitePlaneDeps {\n readonly state: RelayState;\n readonly now: () => number;\n}\n\nexport class SitePlane {\n readonly #deps: SitePlaneDeps;\n\n constructor(deps: SitePlaneDeps) {\n this.#deps = deps;\n }\n\n enqueue(body: unknown): PlaneResult {\n const parsed = EnqueueRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"enqueue failed schema validation\");\n }\n const job = this.#deps.state.enqueue({\n id: parsed.data.stub.id,\n siteId: parsed.data.siteId,\n stub: parsed.data.stub,\n });\n return ok({ jobId: job.id, state: job.state });\n }\n\n /**\n * What needs sealing, and who to seal it to.\n *\n * The response carries the claiming device's **public** keys — which is the\n * entire reason a blind relay can exist. The relay is a directory here, not\n * a participant: it tells the site an address, and what the site sends to\n * that address is unreadable on the way through.\n */\n pending(siteId: string): PlaneResult {\n this.#deps.state.sweep(this.#deps.now());\n const jobs = this.#deps.state.awaiting(siteId).map((job) => ({\n jobId: job.id,\n // Non-null by construction: `awaiting` only returns claimed jobs. The\n // optional chain is here so a future state-machine edit that broke that\n // invariant would produce a missing field rather than a crash on the\n // routing path.\n device: job.claimedBy?.device,\n runnerId: job.claimedBy?.runnerId,\n leaseId: job.claimedBy?.leaseId,\n /** So a site can decline to seal for a claim about to expire. */\n awaitingUntil: job.awaitingUntil,\n }));\n return ok({ jobs });\n }\n\n payload(body: unknown): PlaneResult {\n const parsed = PayloadRequest.safeParse(body);\n if (!parsed.success) {\n return fail(400, \"bad-request\", \"payload failed schema validation\");\n }\n const job = this.#deps.state.job(parsed.data.jobId);\n if (job?.siteId !== parsed.data.siteId) {\n return fail(404, \"not-found\", \"unknown job\");\n }\n if (job.state !== \"awaiting-payload\") {\n // The timeout fired and the job went back to the queue, or another\n // device already has it. Refusing is what makes the timeout mean\n // something: a late seal must not land on a claim that has moved.\n return fail(409, \"too-late\", `job is ${job.state}, not awaiting payload`);\n }\n job.payload = parsed.data.envelope;\n job.state = \"ready\";\n delete job.awaitingUntil;\n return ok({ jobId: job.id, state: job.state });\n }\n\n /** Sealed results, for the site to open and verify. */\n results(siteId: string): PlaneResult {\n const jobs = this.#deps.state.finished(siteId).map((job) => ({\n jobId: job.id,\n envelope: job.result,\n disposition: job.disposition,\n runnerId: job.claimedBy?.runnerId,\n /** The grant the site adopted, so it can complete against it. */\n leaseId: job.claimedBy?.leaseId,\n /**\n * Which device ran it, so the site can verify the signature against the\n * key it was told to seal to — and so `RESULT_PROVENANCE` can name a\n * foreign device rather than guessing (cloud_004 §11.2).\n */\n device: job.claimedBy?.device,\n }));\n return ok({ jobs });\n }\n}\n","import { DaemonPlane, type PlaneResult } from \"./daemon-plane.js\";\nimport { debugPage } from \"./debug.js\";\nimport { Projection, type RelayFixture } from \"./fixture.js\";\nimport { SitePlane } from \"./site-plane.js\";\nimport { RelayState } from \"./state.js\";\n\n/**\n * `@byollm/relay` — the reference relay (cloud_004 §14).\n *\n * A blind relay between byollm sites and daemons: it routes stubs, hands over\n * sealed envelopes it cannot open, and knows who is online. It is the first\n * consumer of byollm_009's session layer that is neither the site nor the\n * device, which makes it the thing that proves the protocol's central claim.\n *\n * ## Why this ships open\n *\n * It is the conformance kit's reference relay, and the kit is public — so it\n * starts where it ends rather than being written closed and ported. A relay\n * that claims to be blind should be readable by the people trusting it, and a\n * third-party daemon testing hub mode should test against real code rather\n * than a mock of it. The production hub — multi-tenant routing, presence at\n * scale, billing, ops — is built on these same interfaces and is not this.\n *\n * ## Blind by construction, not by policy\n *\n * {@link RelayOptions} has no field that can hold a private key, and no type\n * in this package has one either. `RELAY_BLIND` is therefore not a rule the\n * code follows; it is a shape the code has. The only way to make this relay\n * able to read a payload is to change its types, which is a review someone\n * would have to justify rather than a line someone could slip in.\n */\n\nexport interface RelayOptions {\n /**\n * Which site this relay routes for.\n *\n * One, in the skeleton. Multi-tenant routing is the closed piece\n * (cloud_004 §9), and it replaces this field rather than extending it.\n */\n readonly siteId: string;\n /** Consent and rosters, projected from the control plane. */\n readonly fixture?: RelayFixture;\n /** How long a claim is good for. */\n readonly leaseMs?: number;\n /** Injectable clock, so tests move time instead of sleeping. */\n readonly now?: () => number;\n /** Where the daemon plane is mounted. */\n readonly basePath?: string;\n}\n\n/** A running relay: one fetch handler, two planes, one debug page. */\nexport class Relay {\n readonly state: RelayState;\n readonly projection: Projection;\n readonly #daemon: DaemonPlane;\n readonly #site: SitePlane;\n readonly #now: () => number;\n readonly #basePath: string;\n\n constructor(options: RelayOptions) {\n this.state = new RelayState();\n this.projection = new Projection(options.fixture);\n this.#now = options.now ?? Date.now;\n this.#basePath = (options.basePath ?? \"/byollm\").replace(/\\/+$/, \"\");\n this.#daemon = new DaemonPlane({\n state: this.state,\n projection: this.projection,\n now: this.#now,\n leaseMs: options.leaseMs ?? 60_000,\n siteId: options.siteId,\n });\n this.#site = new SitePlane({ state: this.state, now: this.#now });\n }\n\n /** Replace the projection — a control-plane push, or a fixture edit. */\n project(fixture: RelayFixture): void {\n this.projection.replace(fixture);\n }\n\n /**\n * Fire due timers and report what moved.\n *\n * Exposed rather than run on an interval so a test can drive it, and so the\n * production hub can decide its own scheduling. The relay never needs a\n * timer to be *correct* — every read path sweeps first — but a job whose\n * site vanished should return to the queue without waiting for someone to\n * ask about it.\n */\n sweep(): { requeued: string[] } {\n return { requeued: this.state.sweep(this.#now()).map((j) => j.id) };\n }\n\n /** The whole HTTP surface. */\n async handle(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n if (path === \"/debug\" || path === `${this.#basePath}/debug`) {\n return new Response(debugPage(this.state, this.#now()), {\n headers: { \"content-type\": \"text/html; charset=utf-8\" },\n });\n }\n\n const rawBody = request.method === \"POST\" ? await request.text() : \"\";\n const body = rawBody === \"\" ? undefined : safeJson(rawBody);\n const auth = {\n endpoint: path.slice(path.lastIndexOf(\"/\") + 1),\n rawBody,\n signature: signatureFrom(request.headers),\n };\n\n // -- the site plane -----------------------------------------------------\n if (path === \"/relay/site/enqueue\") return json(this.#site.enqueue(body));\n if (path === \"/relay/site/payload\") return json(this.#site.payload(body));\n if (path === \"/relay/site/pending\") {\n return json(this.#site.pending(url.searchParams.get(\"siteId\") ?? \"\"));\n }\n if (path === \"/relay/site/results\") {\n return json(this.#site.results(url.searchParams.get(\"siteId\") ?? \"\"));\n }\n\n // -- the daemon plane ---------------------------------------------------\n if (!path.startsWith(`${this.#basePath}/`)) {\n return json({ status: 404, body: { error: \"not-found\" } });\n }\n switch (auth.endpoint) {\n case \"pair\":\n return json(this.#daemon.pair(body));\n case \"claim\":\n return json(this.#daemon.claim(auth, body));\n case \"fetch\":\n return json(this.#daemon.fetch(auth, body));\n case \"result\":\n return json(this.#daemon.result(auth, body));\n case \"heartbeat\":\n return json(this.#daemon.heartbeat(auth, body));\n case \"release\":\n return json(this.#daemon.release(auth, body));\n default:\n return json({ status: 404, body: { error: \"not-found\" } });\n }\n }\n}\n\nfunction safeJson(raw: string): unknown {\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/** Rebuild the signature from headers, refusing anything partial. */\nfunction signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const issuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n // Checked before `Number()`, which turns a missing header into the epoch —\n // a stale-timestamp check that silently passes is worse than none.\n if (runnerId === null || issuedAt === null || signature === null) {\n return undefined;\n }\n return { runnerId, issuedAt: Number(issuedAt), signature };\n}\n\nconst json = (result: PlaneResult): Response =>\n new Response(JSON.stringify(result.body), {\n status: result.status,\n headers: { \"content-type\": \"application/json\" },\n });\n\nexport { Projection, RelayState, debugPage };\nexport type { RelayFixture };\nexport {\n ConsentRecord,\n DeviceRecord,\n RevocationRecord,\n RosterRecord,\n RelayFixture as RelayFixtureSchema,\n EMPTY_FIXTURE,\n} from \"./fixture.js\";\nexport { AWAITING_PAYLOAD_MS } from \"./state.js\";\nexport type { RoutedJob, RoutedState, Presence } from \"./state.js\";\n"],"mappings":"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","names":["PublicIdentity","z","z","ok","fail"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byollm/relay",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The reference relay: routes sealed byollm jobs between sites and daemons, holding no decryption keys by construction.",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/oftomorrowinc/byollm.git",
|
|
10
|
+
"directory": "packages/relay"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/oftomorrowinc/byollm/tree/main/packages/relay",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/oftomorrowinc/byollm/issues"
|
|
15
|
+
},
|
|
7
16
|
"main": "./dist/index.js",
|
|
8
17
|
"types": "./dist/index.d.ts",
|
|
9
18
|
"exports": {
|
|
@@ -18,15 +27,15 @@
|
|
|
18
27
|
],
|
|
19
28
|
"dependencies": {
|
|
20
29
|
"zod": "^4.1.13",
|
|
21
|
-
"@byollm/protocol": "0.1.0-alpha.
|
|
30
|
+
"@byollm/protocol": "0.1.0-alpha.7"
|
|
22
31
|
},
|
|
23
32
|
"publishConfig": {
|
|
24
33
|
"access": "public"
|
|
25
34
|
},
|
|
26
35
|
"devDependencies": {
|
|
27
36
|
"@supabase/supabase-js": "^2.112.2",
|
|
28
|
-
"@byollm/server": "0.1.0-alpha.
|
|
29
|
-
"byollm": "0.1.0-alpha.
|
|
37
|
+
"@byollm/server": "0.1.0-alpha.7",
|
|
38
|
+
"byollm": "0.1.0-alpha.7"
|
|
30
39
|
},
|
|
31
40
|
"scripts": {
|
|
32
41
|
"build": "tsup"
|