@byollm/relay 0.1.0-alpha.12 → 0.1.0-alpha.13

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.12`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.13`) — 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
@@ -80,7 +80,7 @@ daemons pin at pairing, verified against the `sites` half of the projection.
80
80
  Nothing here trusts a `siteId` in a body or a query string.
81
81
 
82
82
  That is newer than the rest of this package. The site plane took the caller's
83
- word for who it was until `0.1.0-alpha.12`, which on a relay reachable from the
83
+ word for who it was until `0.1.0-alpha.13`, which on a relay reachable from the
84
84
  internet is an open enqueue endpoint into consenting users' machines and an
85
85
  open read of who is online. It was blind the whole time — nothing could open a
86
86
  payload — and blind is not the same as safe.
@@ -88,7 +88,7 @@ payload — and blind is not the same as safe.
88
88
  If you are running this: the site plane is authenticated but this is still a
89
89
  single-tenant relay with in-memory state. One site, one replica.
90
90
 
91
- ## Breaking in `0.1.0-alpha.12`: `RelayState` is async
91
+ ## Breaking in `0.1.0-alpha.13`: `RelayState` is async
92
92
 
93
93
  Every method on `RelayState` now returns a `Promise`, and `Relay.sweep()` and
94
94
  `debugPage()` with it. `RelayState.requeue` is private — it was only ever a
package/dist/index.d.ts CHANGED
@@ -131,7 +131,33 @@ interface ClaimInput {
131
131
  readonly owners: ReadonlySet<string>;
132
132
  readonly max: number;
133
133
  readonly leaseMs: number;
134
- readonly now: number;
134
+ }
135
+ /**
136
+ * Where the store's sense of time comes from — cloud_006 §3.4.
137
+ *
138
+ * **The store owns its clock; callers do not pass one.** Every deadline the
139
+ * relay decides — a lease's expiry, the `awaiting-payload` window, what a
140
+ * sweep considers due — is now stamped by one source, and it is the same
141
+ * source that will later stamp them for every replica.
142
+ *
143
+ * It used to be a parameter. `claim` took `now`, `sweep` took `now`, and each
144
+ * plane called its own `now()` before calling in — which is fine in one
145
+ * process and is the recurring bug the moment there are two. A lease granted
146
+ * by a pod whose clock runs fast is short; the same lease swept by a pod whose
147
+ * clock runs slow outlives it. Nobody is wrong and the lease has no length.
148
+ *
149
+ * A Valkey-backed store returns `TIME` here, so the deadline and the sweep
150
+ * that enforces it are read from the same server. The injected clock stays for
151
+ * tests, which is what lets them move time instead of sleeping.
152
+ *
153
+ * **What deliberately does not use this**: request-signature freshness. That
154
+ * is checked against the *local* clock on purpose — it is a question about the
155
+ * caller's clock versus this process's, `MAX_CLOCK_SKEW_MS` already tolerates
156
+ * two minutes of disagreement, and a network round trip to timestamp every
157
+ * inbound request would be a cost with no property behind it.
158
+ */
159
+ interface RelayStateOptions {
160
+ readonly now?: () => number | Promise<number>;
135
161
  }
136
162
  /** Why a lease-scoped operation was refused, in the caller's vocabulary. */
137
163
  type HolderRefusal = "not-found" | "not-holder" | "stale-lease" | "not-ready";
@@ -144,8 +170,11 @@ type HolderRefusal = "not-found" | "not-holder" | "stale-lease" | "not-ready";
144
170
  * returns to its site's queue — which is the behaviour a lapsed lease already
145
171
  * has to produce, so nothing new needs to be true for this to be safe.
146
172
  */
147
- declare class RelayState {
173
+ declare class RelayState implements RoutingStore {
148
174
  #private;
175
+ constructor(options?: RelayStateOptions);
176
+ /** The one clock every deadline in this store is stamped from. */
177
+ now(): Promise<number>;
149
178
  /**
150
179
  * Take a stub for routing. The payload is not here and will not be.
151
180
  *
@@ -251,7 +280,7 @@ declare class RelayState {
251
280
  jobId: string;
252
281
  leaseId: string;
253
282
  }[]): Promise<string[]>;
254
- seen(presence: Omit<Presence, "revoked">): Promise<Presence>;
283
+ seen(presence: Omit<Presence, "revoked" | "lastSeenAt">): Promise<Presence>;
255
284
  presence(runnerId: string): Promise<Presence | undefined>;
256
285
  everyone(): Promise<Presence[]>;
257
286
  /**
@@ -261,10 +290,125 @@ declare class RelayState {
261
290
  * timeout that fires invisibly is indistinguishable from a job that was
262
291
  * never claimed, and those want very different debugging.
263
292
  */
264
- sweep(now: number): Promise<RoutedJob[]>;
293
+ sweep(): Promise<RoutedJob[]>;
265
294
  }
266
295
 
267
- declare function debugPage(state: RelayState, now: number): Promise<string>;
296
+ /**
297
+ * What the relay needs from a place to keep routing state — cloud_006 §3.2.
298
+ *
299
+ * `RelayState` implements this in memory and is the reference; a Valkey-backed
300
+ * store implements the same thing across replicas. The interface exists so the
301
+ * relay depends on the *contract* rather than on either, and so the properties
302
+ * below are stated once rather than rediscovered per implementation.
303
+ *
304
+ * ## Every method is a decision plus its write
305
+ *
306
+ * Not one is a read the caller follows with a mutation. That is the whole
307
+ * design, and it is not a style preference: `claim` used to be
308
+ * `jobs()` → filter → mutate in the plane, which is atomic for exactly one
309
+ * reason — Node is single-threaded and the Maps are local. Neither survives a
310
+ * store on a network, and `packages/relay/test/two-replicas.test.ts` holds the
311
+ * resulting race as a failing assertion.
312
+ *
313
+ * **The rule for anything added here:** if a caller has to read, decide, and
314
+ * write back, the operation is in the wrong place. Move the decision in.
315
+ *
316
+ * ## What an implementation must guarantee
317
+ *
318
+ * 1. **`claim` is atomic.** Two callers claiming concurrently must not both
319
+ * receive the same job. `CLAIM_ATOMIC` is a protocol MUST.
320
+ * 2. **`enqueue` is idempotent by job id.** A known id returns what is already
321
+ * routing rather than rebuilding it — byollm_009 §4.2's replay argument
322
+ * rests on every write being idempotent per the instance it names.
323
+ * 3. **`complete` is idempotent.** A replayed result changes nothing, and the
324
+ * decision is made by the same operation that would have written it.
325
+ * 4. **Lease-scoped operations name the grant.** `takePayload`, `complete` and
326
+ * `releaseLeases` check the lease *id*, not just the runner — a runner
327
+ * survives a claim-release-reclaim cycle and a grant does not.
328
+ * 5. **`now()` is the only clock.** Every deadline the store stamps and every
329
+ * deadline it enforces come from here (§3.4). An implementation backed by a
330
+ * server returns that server's time, so two replicas cannot disagree about
331
+ * how long a lease is.
332
+ *
333
+ * ## What it must not do
334
+ *
335
+ * Hold a key, or learn about consent. `claim` takes `owners` as data because a
336
+ * predicate cannot travel to Valkey — and the effect is that the store cannot
337
+ * express an opinion about who may route, only about what it was told. That is
338
+ * what keeps `RELAY_BLIND` a property of the shape rather than of the code.
339
+ */
340
+ interface RoutingStore {
341
+ /** The one clock every deadline in this store is stamped from. */
342
+ now(): Promise<number>;
343
+ /** Take a stub for routing. Idempotent by id. */
344
+ enqueue(input: {
345
+ id: string;
346
+ siteId: string;
347
+ stub: JobStub;
348
+ }): Promise<RoutedJob>;
349
+ job(jobId: string): Promise<RoutedJob | undefined>;
350
+ jobs(): Promise<RoutedJob[]>;
351
+ /** Jobs a site must seal for, right now. */
352
+ awaiting(siteId: string): Promise<RoutedJob[]>;
353
+ /** Sealed results waiting to go home. */
354
+ finished(siteId: string): Promise<RoutedJob[]>;
355
+ /** Grant work to a device — one operation, because it has to be. */
356
+ claim(input: ClaimInput): Promise<ClaimedStub[]>;
357
+ /** Hand the sealed payload to the device that holds the lease. */
358
+ takePayload(input: {
359
+ jobId: string;
360
+ runnerId: string;
361
+ leaseId: string;
362
+ }): Promise<{
363
+ envelope: SealedEnvelope;
364
+ } | {
365
+ refused: HolderRefusal;
366
+ }>;
367
+ /** Record a finished job. Idempotent. */
368
+ complete(input: {
369
+ jobId: string;
370
+ runnerId: string;
371
+ envelope: SealedEnvelope;
372
+ disposition: "ok" | "error" | "canceled";
373
+ }): Promise<{
374
+ accepted: boolean;
375
+ state: RoutedState;
376
+ } | {
377
+ refused: HolderRefusal;
378
+ }>;
379
+ /** Give back the grants this runner names. */
380
+ releaseLeases(input: {
381
+ runnerId: string;
382
+ leases: readonly {
383
+ jobId: string;
384
+ leaseId: string;
385
+ }[];
386
+ }): Promise<string[]>;
387
+ /** Take a site's sealed payload for a claimed job. */
388
+ seal(input: {
389
+ jobId: string;
390
+ siteId: string;
391
+ envelope: SealedEnvelope;
392
+ }): Promise<{
393
+ state: RoutedState;
394
+ } | {
395
+ refused: "not-found" | "too-late";
396
+ was?: RoutedState;
397
+ }>;
398
+ /** Which of these leases this runner no longer holds. */
399
+ lostLeases(runnerId: string, active: readonly {
400
+ jobId: string;
401
+ leaseId: string;
402
+ }[]): Promise<string[]>;
403
+ /** Record a device as present. The store stamps when. */
404
+ seen(presence: Omit<Presence, "revoked" | "lastSeenAt">): Promise<Presence>;
405
+ presence(runnerId: string): Promise<Presence | undefined>;
406
+ everyone(): Promise<Presence[]>;
407
+ /** Fire whatever the clock says is due, and report it. */
408
+ sweep(): Promise<RoutedJob[]>;
409
+ }
410
+
411
+ declare function debugPage(state: RoutingStore, now: number): Promise<string>;
268
412
 
269
413
  /**
270
414
  * What the relay is told about the world — cloud_004 §14.
@@ -522,11 +666,27 @@ interface RelayOptions {
522
666
  readonly now?: () => number;
523
667
  /** Where the daemon plane is mounted. */
524
668
  readonly basePath?: string;
669
+ /**
670
+ * Where routing state lives — cloud_006.
671
+ *
672
+ * Defaults to an in-process {@link RelayState}, which is correct for one
673
+ * replica and is what this package ships. A hub running more than one
674
+ * replica supplies a shared implementation of {@link RoutingStore} instead;
675
+ * `packages/relay/test/two-replicas.test.ts` is why that is not optional.
676
+ *
677
+ * **The implementation is deliberately not in this package.** A Valkey
678
+ * client is a dependency every consumer would carry to get a feature only a
679
+ * multi-replica deployment uses, and the production hub is the closed piece
680
+ * (cloud_001). What ships here is the interface, the reference
681
+ * implementation, and the tests that say what an implementation must
682
+ * guarantee.
683
+ */
684
+ readonly store?: RoutingStore;
525
685
  }
526
686
  /** A running relay: one fetch handler, two planes, one debug page. */
527
687
  declare class Relay {
528
688
  #private;
529
- readonly state: RelayState;
689
+ readonly state: RoutingStore;
530
690
  readonly projection: Projection;
531
691
  constructor(options: RelayOptions);
532
692
  /** Replace the projection — a control-plane push, or a fixture edit. */
@@ -547,4 +707,4 @@ declare class Relay {
547
707
  handle(request: Request): Promise<Response>;
548
708
  }
549
709
 
550
- 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, SiteRecord, debugPage };
710
+ export { AWAITING_PAYLOAD_MS, type ClaimInput, ConsentRecord, DeviceRecord, EMPTY_FIXTURE, type HolderRefusal, type Presence, Projection, Relay, RelayFixture, RelayFixture as RelayFixtureSchema, type RelayOptions, RelayState, RevocationRecord, RosterRecord, type RoutedJob, type RoutedState, type RoutingStore, SiteRecord, debugPage };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  ResultRequest,
9
9
  RequestSignature,
10
10
  keyId,
11
+ MAX_CLOCK_SKEW_MS,
11
12
  verifyRequest,
12
13
  verifyPublicIdentity,
13
14
  PublicIdentity
@@ -93,8 +94,7 @@ var DaemonPlane = class {
93
94
  await this.#deps.state.seen({
94
95
  runnerId,
95
96
  owner: parsed.data.owner,
96
- device: parsed.data.device,
97
- lastSeenAt: this.#deps.now()
97
+ device: parsed.data.device
98
98
  });
99
99
  return ok({
100
100
  protocolVersion: PROTOCOL_VERSION,
@@ -120,6 +120,7 @@ var DaemonPlane = class {
120
120
  signature: signature.data,
121
121
  now: this.#deps.now()
122
122
  });
123
+ if (failure === "stale") return this.#clockSkew();
123
124
  if (failure) return fail(401, "unauthorized", "signature check failed");
124
125
  const revoked = this.#deps.projection.consentFor(known.owner, this.#deps.siteId) === null;
125
126
  if (revoked && options.allowRevoked !== true) {
@@ -132,6 +133,30 @@ var DaemonPlane = class {
132
133
  }
133
134
  return run(parsed.data, known);
134
135
  }
136
+ /**
137
+ * A clock too far from ours, said plainly and with the number to fix it by.
138
+ *
139
+ * Its own error code rather than a generic `unauthorized`, because it is the
140
+ * one refusal a retry can never fix and an `ntpdate` always can — the same
141
+ * reasoning `version-unsupported` already carries on the daemon side. A
142
+ * daemon that reports this as a generic rejection sends its owner looking at
143
+ * their network.
144
+ *
145
+ * `serverTime` is included so the far side can say *how far off* rather than
146
+ * *that something is wrong*. It is not a disclosure: the heartbeat response
147
+ * returns the same value, and so does every `Date` header.
148
+ */
149
+ #clockSkew() {
150
+ return {
151
+ status: 401,
152
+ body: {
153
+ error: "clock-skew",
154
+ message: "this request's timestamp is too far from the server's clock; check the machine's time and try again",
155
+ serverTime: this.#deps.now(),
156
+ maxSkewMs: MAX_CLOCK_SKEW_MS
157
+ }
158
+ };
159
+ }
135
160
  claim(auth, body) {
136
161
  return this.#authed(auth, body, ClaimRequest, async (request, device) => {
137
162
  if (request.runnerId !== device.runnerId) {
@@ -147,8 +172,7 @@ var DaemonPlane = class {
147
172
  // predicate does not travel.
148
173
  owners: new Set(this.#deps.projection.ownersRunnableBy(device.owner)),
149
174
  max: request.max,
150
- leaseMs: this.#deps.leaseMs,
151
- now: this.#deps.now()
175
+ leaseMs: this.#deps.leaseMs
152
176
  });
153
177
  return ok({ jobs: granted, leaseMs: this.#deps.leaseMs });
154
178
  });
@@ -202,7 +226,7 @@ var DaemonPlane = class {
202
226
  HeartbeatRequest,
203
227
  async (request, device) => {
204
228
  const now = this.#deps.now();
205
- await this.#deps.state.sweep(now);
229
+ await this.#deps.state.sweep();
206
230
  const known = await this.#deps.state.presence(device.runnerId);
207
231
  const consent = this.#deps.projection.consentFor(
208
232
  device.owner,
@@ -583,7 +607,7 @@ var SitePlane = class {
583
607
  QueryRequest,
584
608
  (request) => request.siteId,
585
609
  async (_request, site) => {
586
- await this.#deps.state.sweep(this.#deps.now());
610
+ await this.#deps.state.sweep();
587
611
  const jobs = (await this.#deps.state.awaiting(site)).map((job) => ({
588
612
  jobId: job.id,
589
613
  // Non-null by construction: `awaiting` only returns claimed jobs.
@@ -657,6 +681,14 @@ var AWAITING_PAYLOAD_MS = 1e4;
657
681
  var RelayState = class {
658
682
  #jobs = /* @__PURE__ */ new Map();
659
683
  #presence = /* @__PURE__ */ new Map();
684
+ #now;
685
+ constructor(options = {}) {
686
+ this.#now = options.now ?? Date.now;
687
+ }
688
+ /** The one clock every deadline in this store is stamped from. */
689
+ async now() {
690
+ return this.#now();
691
+ }
660
692
  /**
661
693
  * Take a stub for routing. The payload is not here and will not be.
662
694
  *
@@ -718,7 +750,8 @@ var RelayState = class {
718
750
  * needed the projection.
719
751
  */
720
752
  async claim(input) {
721
- await this.sweep(input.now);
753
+ const now = await this.now();
754
+ await this.sweep();
722
755
  const granted = [];
723
756
  for (const job of this.#jobs.values()) {
724
757
  if (granted.length >= input.max) break;
@@ -733,9 +766,9 @@ var RelayState = class {
733
766
  owner: input.owner,
734
767
  device: input.device,
735
768
  leaseId,
736
- leaseExpiresAt: input.now + input.leaseMs
769
+ leaseExpiresAt: now + input.leaseMs
737
770
  };
738
- job.awaitingUntil = input.now + AWAITING_PAYLOAD_MS;
771
+ job.awaitingUntil = now + AWAITING_PAYLOAD_MS;
739
772
  granted.push({
740
773
  ...job.stub,
741
774
  lease: {
@@ -829,15 +862,16 @@ var RelayState = class {
829
862
  }).map(({ jobId }) => jobId)
830
863
  );
831
864
  }
832
- seen(presence) {
865
+ async seen(presence) {
866
+ const lastSeenAt = await this.now();
833
867
  const existing = this.#presence.get(presence.runnerId);
834
868
  if (existing) {
835
- existing.lastSeenAt = presence.lastSeenAt;
836
- return Promise.resolve(existing);
869
+ existing.lastSeenAt = lastSeenAt;
870
+ return existing;
837
871
  }
838
- const fresh = { ...presence, revoked: false };
872
+ const fresh = { ...presence, lastSeenAt, revoked: false };
839
873
  this.#presence.set(presence.runnerId, fresh);
840
- return Promise.resolve(fresh);
874
+ return fresh;
841
875
  }
842
876
  presence(runnerId) {
843
877
  return Promise.resolve(this.#presence.get(runnerId));
@@ -865,7 +899,8 @@ var RelayState = class {
865
899
  * timeout that fires invisibly is indistinguishable from a job that was
866
900
  * never claimed, and those want very different debugging.
867
901
  */
868
- sweep(now) {
902
+ async sweep() {
903
+ const now = await this.now();
869
904
  const requeued = [];
870
905
  for (const job of this.#jobs.values()) {
871
906
  if (job.state === "awaiting-payload" && (job.awaitingUntil ?? 0) <= now) {
@@ -878,7 +913,7 @@ var RelayState = class {
878
913
  requeued.push(job);
879
914
  }
880
915
  }
881
- return Promise.resolve(requeued);
916
+ return requeued;
882
917
  }
883
918
  };
884
919
 
@@ -891,7 +926,7 @@ var Relay = class {
891
926
  #now;
892
927
  #basePath;
893
928
  constructor(options) {
894
- this.state = new RelayState();
929
+ this.state = options.store ?? new RelayState({ now: options.now ?? Date.now });
895
930
  this.projection = new Projection(options.fixture);
896
931
  this.#now = options.now ?? Date.now;
897
932
  this.#basePath = (options.basePath ?? "/byollm").replace(/\/+$/, "");
@@ -923,7 +958,7 @@ var Relay = class {
923
958
  * ask about it.
924
959
  */
925
960
  async sweep() {
926
- const requeued = await this.state.sweep(this.#now());
961
+ const requeued = await this.state.sweep();
927
962
  return { requeued: requeued.map((j) => j.id) };
928
963
  }
929
964
  /** The whole HTTP surface. */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/daemon-plane.ts","../src/debug.ts","../src/fixture.ts","../src/site-plane.ts","../src/state.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} from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { Projection } from \"./fixture.js\";\nimport type { HolderRefusal, 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 });\n\n/**\n * A store refusal, in HTTP.\n *\n * The store says *why* in its own vocabulary and this decides what a daemon is\n * told, which keeps the two independent: a store that grows a reason does not\n * get to invent a status code, and a status code that changes does not reach\n * into the store.\n *\n * `not-ready` is the one that matters. It means claimed-but-not-yet-sealed, and\n * a daemon must retry rather than abandon — the job is legitimately still\n * theirs until the lease or the awaiting-payload clock says otherwise. It was\n * the protocol gap that produced the 409 in the first place.\n */\nconst REFUSALS: Record<HolderRefusal, PlaneResult> = {\n \"not-found\": {\n status: 404,\n body: { error: \"not-found\", message: \"unknown job\" },\n },\n \"not-holder\": {\n status: 403,\n body: {\n error: \"unauthorized\",\n message: \"this runner does not hold the job\",\n },\n },\n \"stale-lease\": {\n status: 403,\n body: { error: \"unauthorized\", message: \"that lease is no longer current\" },\n },\n \"not-ready\": {\n status: 409,\n body: {\n error: \"not-ready\",\n message: \"the site has not sealed this job yet\",\n },\n },\n};\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 async pair(body: unknown): Promise<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 // The key comes from the site registry, which is its one home. It used to\n // be inlined on the consent record, which gave a site's key one copy per\n // consenting user and nothing to reconcile them against.\n const site = this.#deps.projection.siteFor(this.#deps.siteId);\n if (!site) {\n return fail(403, \"unauthorized\", \"this site is not registered\");\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 await 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: site.site,\n });\n }\n\n /** Every authenticated call: signature first, then consent, then work. */\n async #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 ) => Promise<PlaneResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<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 = await 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 // Asked of the projection, not of the cached flag.\n //\n // `known.revoked` is set by `heartbeat`, and enforcing on it made\n // revocation depend on the client calling an endpoint: a daemon that\n // simply never heartbeats would go on claiming after its consent was\n // withdrawn, forever. A well-behaved daemon beats every few seconds, which\n // is why the freeze gate's \"within one heartbeat\" demo passed and why this\n // was invisible — the guarantee held for every client that wanted it to.\n //\n // The cached flag survives as what heartbeat *reports* to the daemon. It\n // is a message, not an authority, and the authority is the projection.\n // Two copies of one value where one is a stale mirror of the other is this\n // project's most-repeated bug; here it was also an enforcement hole.\n const revoked =\n this.#deps.projection.consentFor(known.owner, this.#deps.siteId) === null;\n if (revoked && options.allowRevoked !== true) {\n // Revocation reaches the daemon through heartbeat too, so heartbeat\n // itself must be answerable by a revoked runner — bouncing it with a 403\n // would read as a transport problem and it would keep trying.\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, ClaimRequest, async (request, device) => {\n if (request.runnerId !== device.runnerId) {\n return fail(401, \"unauthorized\", \"runner id does not match the key\");\n }\n // One store call. The decision and its write are the store's, because a\n // caller that reads, filters and writes back cannot be made atomic once\n // the store is on a network (cloud_006 §3.2).\n const granted = await this.#deps.state.claim({\n runnerId: device.runnerId,\n owner: device.owner,\n device: device.device,\n siteId: this.#deps.siteId,\n kinds: new Set(request.capabilities.map((c) => c.kind)),\n // The projection, collapsed to data the store can match on — a\n // predicate does not travel.\n owners: new Set(this.#deps.projection.ownersRunnableBy(device.owner)),\n max: request.max,\n leaseMs: this.#deps.leaseMs,\n now: this.#deps.now(),\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, FetchRequest, async (request, device) => {\n const taken = await this.#deps.state.takePayload({\n jobId: request.jobId,\n runnerId: device.runnerId,\n leaseId: request.leaseId,\n });\n if (\"refused\" in taken) return REFUSALS[taken.refused];\n return ok({ envelope: taken.envelope });\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, ResultRequest, async (request, device) => {\n const recorded = await this.#deps.state.complete({\n jobId: request.jobId,\n runnerId: device.runnerId,\n envelope: request.envelope,\n disposition: request.disposition,\n });\n if (\"refused\" in recorded) return REFUSALS[recorded.refused];\n return ok(recorded);\n });\n }\n\n heartbeat(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n async (request, device) => {\n const now = this.#deps.now();\n await this.#deps.state.sweep(now);\n\n const known = await 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 = await this.#deps.state.lostLeases(\n device.runnerId,\n request.activeLeases,\n );\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, ReleaseRequest, async (request, device) => {\n const released = await this.#deps.state.releaseLeases({\n runnerId: device.runnerId,\n leases: request.leases,\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 { 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) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[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 async function debugPage(\n state: RelayState,\n now: number,\n): Promise<string> {\n const jobs = await state.jobs();\n const devices = await 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 site the control plane registered and domain-verified — cloud_004 §5.\n *\n * **The one authority for a site's public identity.** It used to be inlined on\n * every consent record, which meant a site's key had as many homes as it had\n * users and nothing checked they agreed — the exact shape this project has now\n * found in a version constant, a clock read, an envelope deadline, a reseal\n * implementation, a package list and a docs page. Consents now reference a\n * site by id and the key is looked up here.\n *\n * The relay needs it for two things it cannot do without:\n *\n * 1. **Telling a daemon who to pin** at pairing — the key that makes relayed\n * work unforgeable, since the relay holds no key that could produce it.\n * 2. **Authenticating the site plane.** A site calls a relay the way a daemon\n * does, signing with this identity, and this is the key those signatures\n * are checked against.\n */\nexport const SiteRecord = z\n .object({\n /** How the control plane names the site. */\n siteId: z.string().min(1),\n /**\n * The site's public identity.\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 SiteRecord = z.infer<typeof SiteRecord>;\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 .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 /** Registered sites, by id. A consent for a site absent here routes not. */\n sites: z.array(SiteRecord).default([]),\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 sites: [],\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 site this id names, if the control plane registered it.\n *\n * The only source of a site's public identity in this package. Everything\n * that pins, verifies or seals to a site starts here.\n */\n siteFor(siteId: string): SiteRecord | null {\n return this.#fixture.sites.find((s) => s.siteId === siteId) ?? null;\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 * Every owner whose work this device's owner may run, as a list.\n *\n * The same question {@link mayRunFor} answers, asked in the direction a\n * *store* can use. That difference is the crux of making `claim` atomic\n * (cloud_006 §3.2).\n *\n * Today `claim` scans every job and calls `mayRunFor` per candidate, which\n * works because the projection is a local object. A shared routing store\n * cannot do that: the filter has to travel to the store, and a predicate\n * does not travel — you cannot send a closure to Valkey. So the projection\n * is collapsed to **data** here and handed over as a set the store can\n * match on.\n *\n * That the collapse is possible at all is a property of the design worth\n * noticing: `mayRunFor` is a finite lookup over consent and rosters, not a\n * computation over the jobs. If it ever became job-dependent — \"may run\n * work of this size\", say — an atomic claim would stop being expressible,\n * and that is the moment to argue rather than to add a parameter.\n *\n * The owner is always included: a device runs its owner's work, and the\n * relay checks that before it checks a roster.\n */\n ownersRunnableBy(deviceOwner: string): string[] {\n const owners = new Set([deviceOwner]);\n for (const roster of this.#fixture.rosters) {\n if (roster.owner !== deviceOwner) continue;\n for (const member of roster.members) owners.add(member);\n }\n return [...owners];\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 {\n JobStub,\n RequestSignature,\n SealedEnvelope,\n verifySiteRequest,\n} from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { PlaneResult } from \"./daemon-plane.js\";\nimport type { Projection } from \"./fixture.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 * ## Every call is signed, and this plane once was not\n *\n * A site authenticates exactly as a daemon does: it signs each request with\n * the identity key the control plane registered for it, and the relay checks\n * that signature against the projection. Nothing here trusts a `siteId` in a\n * body or a query string.\n *\n * This was the ninth finding, and it was found by reading the code in\n * preparation for the first public deploy rather than by any test — the whole\n * plane took the caller's word for who it was. What that bought an anonymous\n * caller, against a relay reachable on the internet:\n *\n * - **`enqueue` as anyone.** Publish stubs in a site's name and consenting\n * users' machines claim them. The payload that follows is sealed by the real\n * site or not at all, so no forged *work* runs — but unsolicited dispatch to\n * private hardware is a product-level breach whatever the ciphertext does.\n * - **`payload` as anyone**, over a live claim: substitute an envelope the\n * daemon will refuse to open, and the job is burned rather than run.\n * - **`pending` and `results` as anyone**: a metadata read of who is online\n * for a site, which device claimed what, and every lease id in flight.\n *\n * `RELAY_BLIND` held throughout — none of it opens a payload, which is the\n * point of building it that way. But blind is not the same as safe, and the\n * distance between them is this file.\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\n/** A read: the site id arrives in the query and is signed as an empty body. */\nconst QueryRequest = z.object({ siteId: z.string().min(1) }).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 projection: Projection;\n readonly now: () => number;\n /**\n * The one site this relay routes for.\n *\n * The same value the daemon plane holds, from the same option, because it is\n * the same fact — a relay that accepted enqueues for sites its daemons never\n * paired with would route work nobody can open.\n */\n readonly routesFor: string;\n}\n\n/** What `Relay.handle` reconstructs from the request, for signature checking. */\nexport interface SiteAuth {\n /** The endpoint name alone — the domain separator is applied by protocol. */\n readonly endpoint: string;\n /** The exact bytes received, hashed into the signature. */\n readonly rawBody: string;\n /** From the headers, or undefined if any part was missing. */\n readonly signature: unknown;\n}\n\nexport class SitePlane {\n readonly #deps: SitePlaneDeps;\n\n constructor(deps: SitePlaneDeps) {\n this.#deps = deps;\n }\n\n /**\n * Signature first, then the site id, then the work.\n *\n * The caller is whoever the signature says, verified against the key the\n * control plane registered — never whoever the request claims. The `siteId`\n * every request carries is then required to *match* that caller, so the two\n * can never name different sites; a request that says one thing in its\n * signed material and another in its body is refused rather than reconciled.\n *\n * Every endpoint goes through here, including the reads. That is deliberate:\n * an authenticated write plane beside an open read plane would still hand a\n * stranger presence, claims and lease ids, and \"who is online right now\" is\n * exactly the fact a blind relay is otherwise so careful not to reveal.\n */\n async #authed<T>(\n auth: SiteAuth,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n siteIdOf: (request: T) => string,\n run: (request: T, siteId: string) => Promise<PlaneResult>,\n ): Promise<PlaneResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(401, \"unauthorized\", \"this request is not signed\");\n }\n // The signature's caller slot carries the site id (byollm_009 §4.2's\n // site-plane note). Resolving the key through the site registry rather\n // than the device registry is what keeps a device signature from ever\n // authenticating as a site.\n const siteId = signature.data.runnerId;\n const site = this.#deps.projection.siteFor(siteId);\n if (!site) {\n return fail(401, \"unauthorized\", \"this site is not registered\");\n }\n\n const failure = verifySiteRequest({\n identityPublic: site.site.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#deps.now(),\n });\n if (failure) return fail(401, \"unauthorized\", \"signature check failed\");\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 if (siteIdOf(parsed.data) !== siteId) {\n return fail(403, \"unauthorized\", \"that is not your site\");\n }\n // This relay routes for exactly one site, and now says so.\n //\n // The daemon plane has always been single-tenant — `DaemonPlaneDeps.siteId`\n // is the field multi-tenancy replaces — but the site plane accepted any\n // registered site, and `claim` never looked at a job's `siteId` at all. A\n // second registered site's jobs would therefore be offered to a daemon\n // paired with the first, which pinned a different key and could only fail\n // to open the payload. Contained by the crypto, and still a job burned by\n // routing rather than by anything the device did.\n if (siteId !== this.#deps.routesFor) {\n return fail(403, \"unauthorized\", \"this relay does not route for you\");\n }\n return run(parsed.data, siteId);\n }\n\n enqueue(auth: SiteAuth, body: unknown): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n EnqueueRequest,\n (request) => request.siteId,\n async (request, siteId) => {\n const job = await this.#deps.state.enqueue({\n id: request.stub.id,\n siteId,\n stub: request.stub,\n });\n // Idempotent by id: a known id returns what is already routing. Only\n // one site can reach this relay, so a known id is always this site's\n // republish. The multi-tenant router needs a collision check here, and\n // it gets one when it can be exercised — an unreachable guard is a\n // test that cannot fail, which this project has now written twice.\n return ok({ jobId: job.id, state: job.state });\n },\n );\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(auth: SiteAuth, siteId: string): Promise<PlaneResult> {\n return this.#authed(\n auth,\n { siteId },\n QueryRequest,\n (request) => request.siteId,\n async (_request, site) => {\n await this.#deps.state.sweep(this.#deps.now());\n const jobs = (await this.#deps.state.awaiting(site)).map((job) => ({\n jobId: job.id,\n // Non-null by construction: `awaiting` only returns claimed jobs.\n // The optional chain is here so a future state-machine edit that\n // broke that invariant would produce a missing field rather than a\n // crash on the 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 }\n\n payload(auth: SiteAuth, body: unknown): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n PayloadRequest,\n (request) => request.siteId,\n async (request, siteId) => {\n // One store call: the check and the write together. A site that read\n // \"awaiting-payload\" and then wrote would be racing the timeout that\n // makes the state mean anything.\n const sealed = await this.#deps.state.seal({\n jobId: request.jobId,\n siteId,\n envelope: request.envelope,\n });\n if (\"refused\" in sealed) {\n return sealed.refused === \"not-found\"\n ? fail(404, \"not-found\", \"unknown job\")\n : fail(\n 409,\n \"too-late\",\n `job is ${sealed.was ?? \"gone\"}, not awaiting payload`,\n );\n }\n return ok({ jobId: request.jobId, state: sealed.state });\n },\n );\n }\n\n /** Sealed results, for the site to open and verify. */\n results(auth: SiteAuth, siteId: string): Promise<PlaneResult> {\n return this.#authed(\n auth,\n { siteId },\n QueryRequest,\n (request) => request.siteId,\n async (_request, site) => {\n const jobs = (await this.#deps.state.finished(site)).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\n * the key it was told to seal to — and so `RESULT_PROVENANCE` can\n * name a foreign device rather than guessing (cloud_004 §11.2).\n */\n device: job.claimedBy?.device,\n }));\n return ok({ jobs });\n },\n );\n }\n}\n","import type {\n ClaimedStub,\n JobStub,\n PublicIdentity,\n SealedEnvelope,\n} from \"@byollm/protocol\";\nimport { randomUUID } from \"node:crypto\";\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 * What a routing store must do, expressed as operations — cloud_006 §3.2.\n *\n * Every method below is a **decision plus its write**, never a read the caller\n * follows with a mutation. That is the whole point, and it is the difference\n * between an interface a shared store can implement and one it cannot.\n *\n * `claim` is the specimen. It used to live in `DaemonPlane` as\n * `jobs()` → filter → mutate, which is atomic for exactly one reason: Node is\n * single-threaded and these Maps are local, so nothing runs between the read\n * and the write. Neither survives a store on a network, and\n * `packages/relay/test/two-replicas.test.ts` holds the resulting race as a\n * failing assertion.\n *\n * So the rule for anything added here: **if a caller has to read, decide, and\n * write back, the operation is in the wrong place.** Move the decision in.\n *\n * ## Why the projection does not come with it\n *\n * `claim` takes `owners: string[]` rather than a projection or a predicate.\n * A closure cannot travel to Valkey, and the projection replicates for free\n * from the control plane — so the caller collapses it with\n * `Projection.ownersRunnableBy` and hands over data the store can match on.\n * That keeps the store ignorant of consent, which is also what keeps it\n * replaceable.\n */\nexport interface ClaimInput {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n /** The site this relay routes for. Multi-tenancy widens this to a set. */\n readonly siteId: string;\n /** Job kinds this device can actually run. */\n readonly kinds: ReadonlySet<string>;\n /** Whose work it may run — the projection, already collapsed to data. */\n readonly owners: ReadonlySet<string>;\n readonly max: number;\n readonly leaseMs: number;\n readonly now: number;\n}\n\n/** Why a lease-scoped operation was refused, in the caller's vocabulary. */\nexport type HolderRefusal =\n \"not-found\" | \"not-holder\" | \"stale-lease\" | \"not-ready\";\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 /**\n * Take a stub for routing. The payload is not here and will not be.\n *\n * **Idempotent by job id, and that is a security property rather than a\n * convenience.** Site-plane calls are authenticated by signature, and\n * byollm_009 §4.2's argument for signing the request instead of a\n * server-issued nonce rests entirely on every write being idempotent per the\n * instance it names. This one was not: re-enqueueing a known id built a\n * fresh `queued` job over the top of the old one, discarding a live claim,\n * its lease and any payload the site had already sealed to a device. A\n * replayed enqueue inside the two-minute freshness window was therefore a\n * way to yank a job back from the machine running it — the `release` bug of\n * §4.2, rediscovered on the other plane.\n *\n * So a known id returns what is already routing, unchanged. A site that\n * restarts and republishes its queue is the normal case, and it must not\n * disturb work in flight.\n */\n enqueue(input: {\n id: string;\n siteId: string;\n stub: JobStub;\n }): Promise<RoutedJob> {\n const existing = this.#jobs.get(input.id);\n if (existing) return Promise.resolve(existing);\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 Promise.resolve(job);\n }\n\n job(jobId: string): Promise<RoutedJob | undefined> {\n return Promise.resolve(this.#jobs.get(jobId));\n }\n\n jobs(): Promise<RoutedJob[]> {\n return Promise.resolve([...this.#jobs.values()]);\n }\n\n /** Jobs a site must seal for, right now. */\n async awaiting(siteId: string): Promise<RoutedJob[]> {\n return (await this.jobs()).filter(\n (j) => j.siteId === siteId && j.state === \"awaiting-payload\",\n );\n }\n\n /** Sealed results waiting to go home. */\n async finished(siteId: string): Promise<RoutedJob[]> {\n return (await this.jobs()).filter(\n (j) =>\n j.siteId === siteId && j.state === \"done\" && j.result !== undefined,\n );\n }\n\n /**\n * Claim work — one operation, because it has to be.\n *\n * Moved here wholesale from `DaemonPlane`, where it was a scan followed by\n * per-job mutation. Nothing about the *decision* changed; what changed is\n * that a store can now implement it, because the filter and the write are\n * one call rather than a loop the caller drives.\n *\n * The order of the guards is worth preserving as-is when this becomes a Lua\n * script: cheapest first, and `owners` last because it is the only one that\n * needed the projection.\n */\n async claim(input: ClaimInput): Promise<ClaimedStub[]> {\n await this.sweep(input.now);\n\n const granted: ClaimedStub[] = [];\n for (const job of this.#jobs.values()) {\n if (granted.length >= input.max) break;\n if (job.state !== \"queued\") continue;\n // Only this relay's site. A device paired against one site's key and\n // pinned it; a job from another site could only ever produce an envelope\n // it refuses to open — contained by the crypto, and still a burned job.\n if (job.siteId !== input.siteId) continue;\n if (!input.kinds.has(job.stub.kind)) continue;\n // The relay's half of AUDIENCE_BOTH_SIDES. The daemon re-checks its own\n // allowlist and may still refuse — this only ever narrows.\n if (!input.owners.has(job.stub.owner)) continue;\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`, so\n // a relay minting `lease_<job>_<time>` would route perfectly against a\n // memory store and fail the moment a real site adopted the lease.\n const leaseId = randomUUID();\n job.state = \"awaiting-payload\";\n job.claimedBy = {\n runnerId: input.runnerId,\n owner: input.owner,\n device: input.device,\n leaseId,\n leaseExpiresAt: input.now + input.leaseMs,\n };\n // Not the lease: this bounds how long we wait for a *site*, not how long\n // the device may work. byollm_009 §7.1's third clock.\n job.awaitingUntil = input.now + AWAITING_PAYLOAD_MS;\n\n granted.push({\n ...job.stub,\n lease: {\n id: leaseId,\n runnerId: input.runnerId,\n expiresAt: job.claimedBy.leaseExpiresAt,\n },\n });\n }\n return granted;\n }\n\n /**\n * Hand over the sealed payload to the device that holds the lease.\n *\n * The read and the state transition are one operation for the same reason\n * `claim` is: `running` must be set by whoever was told the envelope, or two\n * replicas can both hand out the same work and both believe they were first.\n */\n takePayload(input: {\n jobId: string;\n runnerId: string;\n leaseId: string;\n }): Promise<{ envelope: SealedEnvelope } | { refused: HolderRefusal }> {\n const job = this.#jobs.get(input.jobId);\n if (!job) return Promise.resolve({ refused: \"not-found\" });\n if (job.claimedBy?.runnerId !== input.runnerId) {\n return Promise.resolve({ refused: \"not-holder\" });\n }\n // LEASE_HONORED per *instance*: a stale lease id names a grant that is\n // over, and answering it would hand work to a previous holder.\n if (job.claimedBy.leaseId !== input.leaseId) {\n return Promise.resolve({ refused: \"stale-lease\" });\n }\n if (!job.payload) return Promise.resolve({ refused: \"not-ready\" });\n job.state = \"running\";\n return Promise.resolve({ envelope: job.payload });\n }\n\n /**\n * Record a finished job.\n *\n * `RESULT_IDEMPOTENT` lives here rather than in the caller: a replayed\n * result must be a no-op decided by the same operation that would have\n * written it, or two replicas can both decide they were the first.\n */\n complete(input: {\n jobId: string;\n runnerId: string;\n envelope: SealedEnvelope;\n disposition: \"ok\" | \"error\" | \"canceled\";\n }): Promise<\n { accepted: boolean; state: RoutedState } | { refused: HolderRefusal }\n > {\n const job = this.#jobs.get(input.jobId);\n if (!job) return Promise.resolve({ refused: \"not-found\" });\n if (job.claimedBy?.runnerId !== input.runnerId) {\n return Promise.resolve({ refused: \"not-holder\" });\n }\n if (job.state === \"done\") {\n return Promise.resolve({ accepted: false, state: job.state });\n }\n job.result = input.envelope;\n job.disposition = input.disposition;\n job.state = \"done\";\n return Promise.resolve({ accepted: true, state: job.state });\n }\n\n /** Give back leases this runner holds, naming each grant it means. */\n releaseLeases(input: {\n runnerId: string;\n leases: readonly { jobId: string; leaseId: string }[];\n }): Promise<string[]> {\n const released: string[] = [];\n for (const { jobId, leaseId } of input.leases) {\n const job = this.#jobs.get(jobId);\n if (!job || job.claimedBy?.runnerId !== input.runnerId) continue;\n if (job.claimedBy.leaseId !== leaseId) continue;\n this.#requeue(job);\n released.push(jobId);\n }\n return Promise.resolve(released);\n }\n\n /**\n * Take a site's sealed payload for a claimed job.\n *\n * Refuses anything not `awaiting-payload`, which is what makes the timeout\n * mean something: a late seal must not land on a claim that has moved.\n */\n seal(input: {\n jobId: string;\n siteId: string;\n envelope: SealedEnvelope;\n }): Promise<\n | { state: RoutedState }\n | { refused: \"not-found\" | \"too-late\"; was?: RoutedState }\n > {\n const job = this.#jobs.get(input.jobId);\n if (job?.siteId !== input.siteId) {\n return Promise.resolve({ refused: \"not-found\" });\n }\n if (job.state !== \"awaiting-payload\") {\n return Promise.resolve({ refused: \"too-late\", was: job.state });\n }\n job.payload = input.envelope;\n job.state = \"ready\";\n delete job.awaitingUntil;\n return Promise.resolve({ state: job.state });\n }\n\n /** Which of these leases this runner no longer holds. */\n lostLeases(\n runnerId: string,\n active: readonly { jobId: string; leaseId: string }[],\n ): Promise<string[]> {\n void runnerId;\n return Promise.resolve(\n active\n .filter(({ jobId, leaseId }) => {\n const job = this.#jobs.get(jobId);\n return job?.claimedBy?.leaseId !== leaseId;\n })\n .map(({ jobId }) => jobId),\n );\n }\n\n seen(presence: Omit<Presence, \"revoked\">): Promise<Presence> {\n const existing = this.#presence.get(presence.runnerId);\n if (existing) {\n existing.lastSeenAt = presence.lastSeenAt;\n return Promise.resolve(existing);\n }\n const fresh: Presence = { ...presence, revoked: false };\n this.#presence.set(presence.runnerId, fresh);\n return Promise.resolve(fresh);\n }\n\n presence(runnerId: string): Promise<Presence | undefined> {\n return Promise.resolve(this.#presence.get(runnerId));\n }\n\n everyone(): Promise<Presence[]> {\n return Promise.resolve([...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): Promise<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 Promise.resolve(requeued);\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({\n state: this.state,\n projection: this.projection,\n now: this.#now,\n routesFor: options.siteId,\n });\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 async sweep(): Promise<{ requeued: string[] }> {\n const requeued = await this.state.sweep(this.#now());\n return { requeued: requeued.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(await 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 endpoint = path.slice(path.lastIndexOf(\"/\") + 1);\n const auth = {\n endpoint,\n rawBody,\n signature: signatureFrom(request.headers, \"x-byollm-runner\"),\n };\n // The caller header differs by plane, so a signature meant for one can\n // never be presented to the other by moving the request. The endpoint's\n // domain separator (`site/…`) already covers this; the header makes it\n // true at parse time rather than at verification time.\n const siteAuth = {\n endpoint,\n rawBody,\n signature: signatureFrom(request.headers, \"x-byollm-site\"),\n };\n\n // -- the site plane -----------------------------------------------------\n if (path === \"/relay/site/enqueue\") {\n return json(await this.#site.enqueue(siteAuth, body));\n }\n if (path === \"/relay/site/payload\") {\n return json(await this.#site.payload(siteAuth, body));\n }\n if (path === \"/relay/site/pending\") {\n return json(\n await this.#site.pending(\n siteAuth,\n url.searchParams.get(\"siteId\") ?? \"\",\n ),\n );\n }\n if (path === \"/relay/site/results\") {\n return json(\n await this.#site.results(\n siteAuth,\n url.searchParams.get(\"siteId\") ?? \"\",\n ),\n );\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(await this.#daemon.pair(body));\n case \"claim\":\n return json(await this.#daemon.claim(auth, body));\n case \"fetch\":\n return json(await this.#daemon.fetch(auth, body));\n case \"result\":\n return json(await this.#daemon.result(auth, body));\n case \"heartbeat\":\n return json(await this.#daemon.heartbeat(auth, body));\n case \"release\":\n return json(await 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, callerHeader: string): unknown {\n const runnerId = headers.get(callerHeader);\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 SiteRecord,\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":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS;AAwBlB,IAAM,KAAK,CAAC,UAAgC,EAAE,QAAQ,KAAK,KAAK;AAehE,IAAM,WAA+C;AAAA,EACnD,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,aAAa,SAAS,cAAc;AAAA,EACrD;AAAA,EACA,cAAc;AAAA,IACZ,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EACA,eAAe;AAAA,IACb,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,gBAAgB,SAAS,kCAAkC;AAAA,EAC5E;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,SAAS;AAAA,IACX;AAAA,EACF;AACF;AACA,IAAM,OAAO,CAAC,QAAgB,OAAe,aAAkC;AAAA,EAC7E;AAAA,EACA,MAAM,EAAE,OAAO,QAAQ;AACzB;AAkBO,IAAM,cAAN,MAAkB;AAAA,EACd;AAAA,EAET,YAAY,MAAuB;AACjC,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,KAAK,MAAqC;AAC9C,UAAM,SAAS,mBAAmB,UAAU,IAAI;AAChD,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,KAAK,KAAK,eAAe,uCAAuC;AAAA,IACzE;AACA,QAAI,CAAC,qBAAqB,OAAO,KAAK,MAAM,GAAG;AAC7C,aAAO,KAAK,KAAK,eAAe,uCAAuC;AAAA,IACzE;AAEA,UAAM,UAAU,KAAK,MAAM,WAAW;AAAA,MACpC,OAAO,KAAK;AAAA,MACZ,KAAK,MAAM;AAAA,IACb;AACA,QAAI,CAAC,SAAS;AAGZ,aAAO,KAAK,KAAK,gBAAgB,iCAAiC;AAAA,IACpE;AAIA,UAAM,OAAO,KAAK,MAAM,WAAW,QAAQ,KAAK,MAAM,MAAM;AAC5D,QAAI,CAAC,MAAM;AACT,aAAO,KAAK,KAAK,gBAAgB,6BAA6B;AAAA,IAChE;AAaA,UAAM,WAAW,KAAK,MAAM,WAAW;AAAA,MACrC,OAAO,KAAK,OAAO;AAAA,IACrB;AACA,QAAI,CAAC,UAAU;AACb,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS,UAAU,OAAO,KAAK,OAAO;AAGxC,aAAO,KAAK,KAAK,gBAAgB,sCAAsC;AAAA,IACzE;AAMA,UAAM,WAAW,SAAS;AAE1B,UAAM,KAAK,MAAM,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA,OAAO,OAAO,KAAK;AAAA,MACnB,QAAQ,OAAO,KAAK;AAAA,MACpB,YAAY,KAAK,MAAM,IAAI;AAAA,IAC7B,CAAC;AAED,WAAO,GAAG;AAAA,MACR,iBAAiB;AAAA,MACjB;AAAA;AAAA,MAEA,MAAM,KAAK;AAAA,IACb,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,QACJ,OACA,MACA,QACA,KAIA,UAAsC,CAAC,GACjB;AACtB,UAAM,YAAY,iBAAiB,UAAU,MAAM,SAAS;AAC5D,QAAI,CAAC,UAAU,SAAS;AACtB,aAAO,KAAK,KAAK,gBAAgB,4BAA4B;AAAA,IAC/D;AACA,UAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,SAAS,UAAU,KAAK,QAAQ;AACrE,QAAI,CAAC,OAAO;AACV,aAAO,KAAK,KAAK,gBAAgB,+BAA+B;AAAA,IAClE;AAEA,UAAM,UAAU,cAAc;AAAA,MAC5B,gBAAgB,MAAM,OAAO;AAAA,MAC7B,UAAU,MAAM;AAAA,MAChB,MAAM,MAAM;AAAA,MACZ,WAAW,UAAU;AAAA,MACrB,KAAK,KAAK,MAAM,IAAI;AAAA,IACtB,CAAC;AACD,QAAI,QAAS,QAAO,KAAK,KAAK,gBAAgB,wBAAwB;AAetE,UAAM,UACJ,KAAK,MAAM,WAAW,WAAW,MAAM,OAAO,KAAK,MAAM,MAAM,MAAM;AACvE,QAAI,WAAW,QAAQ,iBAAiB,MAAM;AAI5C,aAAO,KAAK,KAAK,WAAW,0CAA0C;AAAA,IACxE;AAEA,UAAM,aAAa,KAAK,MAAM,IAAI;AAElC,UAAM,SAAS,OAAO,UAAU,IAAI;AACpC,QAAI,CAAC,OAAO,WAAW,OAAO,SAAS,QAAW;AAChD,aAAO,KAAK,KAAK,eAAe,kCAAkC;AAAA,IACpE;AACA,WAAO,IAAI,OAAO,MAAM,KAAK;AAAA,EAC/B;AAAA,EAEA,MACE,MACA,MACsB;AACtB,WAAO,KAAK,QAAQ,MAAM,MAAM,cAAc,OAAO,SAAS,WAAW;AACvE,UAAI,QAAQ,aAAa,OAAO,UAAU;AACxC,eAAO,KAAK,KAAK,gBAAgB,kCAAkC;AAAA,MACrE;AAIA,YAAM,UAAU,MAAM,KAAK,MAAM,MAAM,MAAM;AAAA,QAC3C,UAAU,OAAO;AAAA,QACjB,OAAO,OAAO;AAAA,QACd,QAAQ,OAAO;AAAA,QACf,QAAQ,KAAK,MAAM;AAAA,QACnB,OAAO,IAAI,IAAI,QAAQ,aAAa,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA,QAGtD,QAAQ,IAAI,IAAI,KAAK,MAAM,WAAW,iBAAiB,OAAO,KAAK,CAAC;AAAA,QACpE,KAAK,QAAQ;AAAA,QACb,SAAS,KAAK,MAAM;AAAA,QACpB,KAAK,KAAK,MAAM,IAAI;AAAA,MACtB,CAAC;AAED,aAAO,GAAG,EAAE,MAAM,SAAS,SAAS,KAAK,MAAM,QAAQ,CAAC;AAAA,IAC1D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MACE,MACA,MACsB;AACtB,WAAO,KAAK,QAAQ,MAAM,MAAM,cAAc,OAAO,SAAS,WAAW;AACvE,YAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,YAAY;AAAA,QAC/C,OAAO,QAAQ;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,SAAS,QAAQ;AAAA,MACnB,CAAC;AACD,UAAI,aAAa,MAAO,QAAO,SAAS,MAAM,OAAO;AACrD,aAAO,GAAG,EAAE,UAAU,MAAM,SAAS,CAAC;AAAA,IACxC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OACE,MACA,MACsB;AACtB,WAAO,KAAK,QAAQ,MAAM,MAAM,eAAe,OAAO,SAAS,WAAW;AACxE,YAAM,WAAW,MAAM,KAAK,MAAM,MAAM,SAAS;AAAA,QAC/C,OAAO,QAAQ;AAAA,QACf,UAAU,OAAO;AAAA,QACjB,UAAU,QAAQ;AAAA,QAClB,aAAa,QAAQ;AAAA,MACvB,CAAC;AACD,UAAI,aAAa,SAAU,QAAO,SAAS,SAAS,OAAO;AAC3D,aAAO,GAAG,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AAAA,EAEA,UACE,MACA,MACsB;AACtB,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,SAAS,WAAW;AACzB,cAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,cAAM,KAAK,MAAM,MAAM,MAAM,GAAG;AAEhC,cAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,SAAS,OAAO,QAAQ;AAG7D,cAAM,UAAU,KAAK,MAAM,WAAW;AAAA,UACpC,OAAO;AAAA,UACP,KAAK,MAAM;AAAA,QACb;AACA,cAAM,UAAU,YAAY;AAC5B,YAAI,MAAO,OAAM,UAAU;AAK3B,cAAM,OAAO,MAAM,KAAK,MAAM,MAAM;AAAA,UAClC,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAEA,eAAO,GAAG;AAAA,UACR;AAAA,UACA,QAAQ,CAAC;AAAA,UACT,QAAQ,CAAC;AAAA,UACT;AAAA,UACA,YAAY;AAAA,QACd,CAAC;AAAA,MACH;AAAA,MACA,EAAE,cAAc,KAAK;AAAA,IACvB;AAAA,EACF;AAAA,EAEA,QACE,MACA,MACsB;AACtB,WAAO,KAAK,QAAQ,MAAM,MAAM,gBAAgB,OAAO,SAAS,WAAW;AACzE,YAAM,WAAW,MAAM,KAAK,MAAM,MAAM,cAAc;AAAA,QACpD,UAAU,OAAO;AAAA,QACjB,QAAQ,QAAQ;AAAA,MAClB,CAAC;AACD,aAAO,GAAG,EAAE,SAAS,CAAC;AAAA,IACxB,CAAC;AAAA,EACH;AACF;AAYA,IAAM,qBAAqB,EACxB,OAAO;AAAA,EACN,iBAAiB,EAAE,QAAQ,gBAAgB;AAAA,EAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,QAAQ;AACV,CAAC,EACA,OAAO;AAGH,IAAM,gBAAgB,CAAC,aAC5B,MAAM,SAAS,QAAQ;;;AClYzB,IAAM,SAAS,CAAC,UACd,MAAM;AAAA,EACJ;AAAA,EACA,CAAC,OAAO,EAAE,KAAK,SAAS,KAAK,QAAQ,KAAK,QAAQ,KAAK,SAAS,GAAG,CAAC,KAAK;AAC3E;AAEF,IAAM,eAAuC;AAAA,EAC3C,QAAQ;AAAA,EACR,oBAAoB;AAAA,EACpB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AACR;AAEA,SAAS,OAAO,KAAgB,KAAqB;AACnD,QAAM,UAAU,IAAI;AACpB,QAAM,UACJ,IAAI,UAAU,sBAAsB,IAAI,kBAAkB,SACtD,GAAG,OAAO,KAAK,IAAI,GAAG,IAAI,gBAAgB,GAAG,CAAC,CAAC,YAC/C;AACN,SAAO;AAAA,gBACO,OAAO,IAAI,EAAE,CAAC;AAAA,UACpB,OAAO,IAAI,KAAK,IAAI,CAAC;AAAA,UACrB,OAAO,IAAI,KAAK,KAAK,CAAC;AAAA,UACtB,OAAO,IAAI,KAAK,QAAQ,CAAC;AAAA,UACzB,OAAO,IAAI,KAAK,SAAS,CAAC;AAAA,UAC1B,IAAI,KAAK,YAAY,QAAQ,IAAI;AAAA,0BACjB,aAAa,IAAI,KAAK,KAAK,MAAM,KAAK,OAAO,IAAI,KAAK,CAAC,0BAA0B,OAAO,OAAO,CAAC;AAAA,UAChH,UAAU,SAAS,OAAO,cAAc,QAAQ,MAAM,CAAC,CAAC,YAAY,iCAA4B;AAAA,UAChG,IAAI,UAAU,WAAW,iCAA4B;AAAA,UACrD,IAAI,SAAS,OAAO,IAAI,eAAe,GAAG,IAAI,iCAA4B;AAAA;AAEpF;AAEA,eAAsB,UACpB,OACA,KACiB;AACjB,QAAM,OAAO,MAAM,MAAM,KAAK;AAC9B,QAAM,UAAU,MAAM,MAAM,SAAS;AAErC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAqBU,OAAO,KAAK,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA,EAIpC,KAAK,SAAS,KAAK,IAAI,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,KAAK,IAAI,IAAI,+DAA+D;AAAA;AAAA;AAAA,gBAG5G,OAAO,QAAQ,MAAM,CAAC;AAAA;AAAA;AAAA,EAIpC,QAAQ,SACJ,QACG;AAAA,IACC,CAAC,MAAM;AAAA,cACH,OAAO,EAAE,QAAQ,CAAC;AAAA,QACxB,OAAO,EAAE,KAAK,CAAC;AAAA,cACT,OAAO,cAAc,EAAE,MAAM,CAAC,CAAC;AAAA,QACrC,OAAO,KAAK,IAAI,GAAG,MAAM,EAAE,UAAU,CAAC,CAAC;AAAA,QACvC,EAAE,UAAU,sCAAsC,QAAQ;AAAA;AAAA,EAE1D,EACC,KAAK,IAAI,IACZ,gEACN;AAAA;AAAA;AAGA;;;AC1GA,SAAS,kBAAAA,uBAAsB;AAC/B,SAAS,KAAAC,UAAS;AAqDX,IAAM,aAAaA,GACvB,OAAO;AAAA;AAAA,EAEN,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQxB,MAAMD;AACR,CAAC,EACA,OAAO;AAUH,IAAM,gBAAgBC,GAC1B,OAAO;AAAA;AAAA,EAEN,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAEvB,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC;AAC1B,CAAC,EACA,OAAO;AAWH,IAAM,eAAeA,GACzB,OAAO;AAAA;AAAA,EAEN,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAEpB,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAEvB,SAASA,GAAE,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,CAAC;AACpC,CAAC,EACA,OAAO;AAeH,IAAM,eAAeA,GACzB,OAAO;AAAA;AAAA,EAEN,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAEvB,UAAUA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAE1B,QAAQD;AACV,CAAC,EACA,OAAO;AAIH,IAAM,mBAAmBC,GAC7B,OAAO,EAAE,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC,GAAG,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,EAC9D,OAAO;AAGH,IAAM,eAAeA,GACzB,OAAO;AAAA;AAAA,EAEN,OAAOA,GAAE,MAAM,UAAU,EAAE,QAAQ,CAAC,CAAC;AAAA,EACrC,UAAUA,GAAE,MAAM,aAAa;AAAA,EAC/B,SAASA,GAAE,MAAM,YAAY,EAAE,QAAQ,CAAC,CAAC;AAAA,EACzC,SAASA,GAAE,MAAM,YAAY,EAAE,QAAQ,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,SAASA,GAAE,MAAM,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAC/C,CAAC,EACA,OAAO;AAIH,IAAM,gBAA8B;AAAA,EACzC,OAAO,CAAC;AAAA,EACR,UAAU,CAAC;AAAA,EACX,SAAS,CAAC;AAAA,EACV,SAAS,CAAC;AAAA,EACV,SAAS,CAAC;AACZ;AAUO,IAAM,aAAN,MAAiB;AAAA,EACtB;AAAA,EAEA,YAAY,UAAwB,eAAe;AACjD,SAAK,WAAW,aAAa,MAAM,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,QAAQ,SAA6B;AACnC,SAAK,WAAW,aAAa,MAAM,OAAO;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAQ,QAAmC;AACzC,WAAO,KAAK,SAAS,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM,KAAK;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,UAAuC;AAC/C,WAAO,KAAK,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,aAAa,QAAQ,KAAK;AAAA,EACvE;AAAA;AAAA,EAGA,oBAAoB,gBAA6C;AAC/D,WACE,KAAK,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,aAAa,cAAc,KACtE;AAAA,EAEJ;AAAA;AAAA,EAGA,WAAW,OAAe,QAAsC;AAC9D,UAAM,UAAU,KAAK,SAAS,QAAQ;AAAA,MACpC,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE,WAAW;AAAA,IAC3C;AACA,QAAI,QAAS,QAAO;AACpB,WACE,KAAK,SAAS,SAAS;AAAA,MACrB,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE,WAAW;AAAA,IAC3C,KAAK;AAAA,EAET;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyBA,iBAAiB,aAA+B;AAC9C,UAAM,SAAS,oBAAI,IAAI,CAAC,WAAW,CAAC;AACpC,eAAW,UAAU,KAAK,SAAS,SAAS;AAC1C,UAAI,OAAO,UAAU,YAAa;AAClC,iBAAW,UAAU,OAAO,QAAS,QAAO,IAAI,MAAM;AAAA,IACxD;AACA,WAAO,CAAC,GAAG,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,UAAU,aAAqB,UAA2B;AACxD,QAAI,gBAAgB,SAAU,QAAO;AACrC,WAAO,KAAK,SAAS,QAAQ;AAAA,MAC3B,CAAC,MAAM,EAAE,UAAU,eAAe,EAAE,QAAQ,SAAS,QAAQ;AAAA,IAC/D;AAAA,EACF;AACF;;;ACvRA;AAAA,EACE;AAAA,EACA,oBAAAC;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,KAAAC,UAAS;AA0DlB,IAAM,iBAAiBA,GACpB,OAAO;AAAA,EACN,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQxB,MAAM;AACR,CAAC,EACA,OAAO;AAEV,IAAM,iBAAiBA,GACpB,OAAO;AAAA,EACN,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA,EAEvB,UAAU;AACZ,CAAC,EACA,OAAO;AAGV,IAAM,eAAeA,GAAE,OAAO,EAAE,QAAQA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,OAAO;AAEpE,IAAMC,MAAK,CAAC,UAAgC,EAAE,QAAQ,KAAK,KAAK;AAChE,IAAMC,QAAO,CAAC,QAAgB,OAAe,aAAkC;AAAA,EAC7E;AAAA,EACA,MAAM,EAAE,OAAO,QAAQ;AACzB;AA0BO,IAAM,YAAN,MAAgB;AAAA,EACZ;AAAA,EAET,YAAY,MAAqB;AAC/B,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,QACJ,MACA,MACA,QACA,UACA,KACsB;AACtB,UAAM,YAAYH,kBAAiB,UAAU,KAAK,SAAS;AAC3D,QAAI,CAAC,UAAU,SAAS;AACtB,aAAOG,MAAK,KAAK,gBAAgB,4BAA4B;AAAA,IAC/D;AAKA,UAAM,SAAS,UAAU,KAAK;AAC9B,UAAM,OAAO,KAAK,MAAM,WAAW,QAAQ,MAAM;AACjD,QAAI,CAAC,MAAM;AACT,aAAOA,MAAK,KAAK,gBAAgB,6BAA6B;AAAA,IAChE;AAEA,UAAM,UAAU,kBAAkB;AAAA,MAChC,gBAAgB,KAAK,KAAK;AAAA,MAC1B,UAAU,KAAK;AAAA,MACf,MAAM,KAAK;AAAA,MACX,WAAW,UAAU;AAAA,MACrB,KAAK,KAAK,MAAM,IAAI;AAAA,IACtB,CAAC;AACD,QAAI,QAAS,QAAOA,MAAK,KAAK,gBAAgB,wBAAwB;AAEtE,UAAM,SAAS,OAAO,UAAU,IAAI;AACpC,QAAI,CAAC,OAAO,WAAW,OAAO,SAAS,QAAW;AAChD,aAAOA,MAAK,KAAK,eAAe,kCAAkC;AAAA,IACpE;AACA,QAAI,SAAS,OAAO,IAAI,MAAM,QAAQ;AACpC,aAAOA,MAAK,KAAK,gBAAgB,uBAAuB;AAAA,IAC1D;AAUA,QAAI,WAAW,KAAK,MAAM,WAAW;AACnC,aAAOA,MAAK,KAAK,gBAAgB,mCAAmC;AAAA,IACtE;AACA,WAAO,IAAI,OAAO,MAAM,MAAM;AAAA,EAChC;AAAA,EAEA,QAAQ,MAAgB,MAAqC;AAC3D,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,CAAC,YAAY,QAAQ;AAAA,MACrB,OAAO,SAAS,WAAW;AACzB,cAAM,MAAM,MAAM,KAAK,MAAM,MAAM,QAAQ;AAAA,UACzC,IAAI,QAAQ,KAAK;AAAA,UACjB;AAAA,UACA,MAAM,QAAQ;AAAA,QAChB,CAAC;AAMD,eAAOD,IAAG,EAAE,OAAO,IAAI,IAAI,OAAO,IAAI,MAAM,CAAC;AAAA,MAC/C;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAQ,MAAgB,QAAsC;AAC5D,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,OAAO;AAAA,MACT;AAAA,MACA,CAAC,YAAY,QAAQ;AAAA,MACrB,OAAO,UAAU,SAAS;AACxB,cAAM,KAAK,MAAM,MAAM,MAAM,KAAK,MAAM,IAAI,CAAC;AAC7C,cAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,SAAS,IAAI,GAAG,IAAI,CAAC,SAAS;AAAA,UACjE,OAAO,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,UAKX,QAAQ,IAAI,WAAW;AAAA,UACvB,UAAU,IAAI,WAAW;AAAA,UACzB,SAAS,IAAI,WAAW;AAAA;AAAA,UAExB,eAAe,IAAI;AAAA,QACrB,EAAE;AACF,eAAOA,IAAG,EAAE,KAAK,CAAC;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QAAQ,MAAgB,MAAqC;AAC3D,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,MACA,CAAC,YAAY,QAAQ;AAAA,MACrB,OAAO,SAAS,WAAW;AAIzB,cAAM,SAAS,MAAM,KAAK,MAAM,MAAM,KAAK;AAAA,UACzC,OAAO,QAAQ;AAAA,UACf;AAAA,UACA,UAAU,QAAQ;AAAA,QACpB,CAAC;AACD,YAAI,aAAa,QAAQ;AACvB,iBAAO,OAAO,YAAY,cACtBC,MAAK,KAAK,aAAa,aAAa,IACpCA;AAAA,YACE;AAAA,YACA;AAAA,YACA,UAAU,OAAO,OAAO,MAAM;AAAA,UAChC;AAAA,QACN;AACA,eAAOD,IAAG,EAAE,OAAO,QAAQ,OAAO,OAAO,OAAO,MAAM,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,QAAQ,MAAgB,QAAsC;AAC5D,WAAO,KAAK;AAAA,MACV;AAAA,MACA,EAAE,OAAO;AAAA,MACT;AAAA,MACA,CAAC,YAAY,QAAQ;AAAA,MACrB,OAAO,UAAU,SAAS;AACxB,cAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,SAAS,IAAI,GAAG,IAAI,CAAC,SAAS;AAAA,UACjE,OAAO,IAAI;AAAA,UACX,UAAU,IAAI;AAAA,UACd,aAAa,IAAI;AAAA,UACjB,UAAU,IAAI,WAAW;AAAA;AAAA,UAEzB,SAAS,IAAI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMxB,QAAQ,IAAI,WAAW;AAAA,QACzB,EAAE;AACF,eAAOA,IAAG,EAAE,KAAK,CAAC;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;;;ACzSA,SAAS,kBAAkB;AAkDpB,IAAM,sBAAsB;AAwG5B,IAAM,aAAN,MAAiB;AAAA,EACb,QAAQ,oBAAI,IAAuB;AAAA,EACnC,YAAY,oBAAI,IAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoB/C,QAAQ,OAIe;AACrB,UAAM,WAAW,KAAK,MAAM,IAAI,MAAM,EAAE;AACxC,QAAI,SAAU,QAAO,QAAQ,QAAQ,QAAQ;AAC7C,UAAM,MAAiB;AAAA,MACrB,IAAI,MAAM;AAAA,MACV,QAAQ,MAAM;AAAA,MACd,MAAM,MAAM;AAAA,MACZ,OAAO;AAAA,IACT;AACA,SAAK,MAAM,IAAI,IAAI,IAAI,GAAG;AAC1B,WAAO,QAAQ,QAAQ,GAAG;AAAA,EAC5B;AAAA,EAEA,IAAI,OAA+C;AACjD,WAAO,QAAQ,QAAQ,KAAK,MAAM,IAAI,KAAK,CAAC;AAAA,EAC9C;AAAA,EAEA,OAA6B;AAC3B,WAAO,QAAQ,QAAQ,CAAC,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC;AAAA,EACjD;AAAA;AAAA,EAGA,MAAM,SAAS,QAAsC;AACnD,YAAQ,MAAM,KAAK,KAAK,GAAG;AAAA,MACzB,CAAC,MAAM,EAAE,WAAW,UAAU,EAAE,UAAU;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,SAAS,QAAsC;AACnD,YAAQ,MAAM,KAAK,KAAK,GAAG;AAAA,MACzB,CAAC,MACC,EAAE,WAAW,UAAU,EAAE,UAAU,UAAU,EAAE,WAAW;AAAA,IAC9D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,MAAM,OAA2C;AACrD,UAAM,KAAK,MAAM,MAAM,GAAG;AAE1B,UAAM,UAAyB,CAAC;AAChC,eAAW,OAAO,KAAK,MAAM,OAAO,GAAG;AACrC,UAAI,QAAQ,UAAU,MAAM,IAAK;AACjC,UAAI,IAAI,UAAU,SAAU;AAI5B,UAAI,IAAI,WAAW,MAAM,OAAQ;AACjC,UAAI,CAAC,MAAM,MAAM,IAAI,IAAI,KAAK,IAAI,EAAG;AAGrC,UAAI,CAAC,MAAM,OAAO,IAAI,IAAI,KAAK,KAAK,EAAG;AAMvC,YAAM,UAAU,WAAW;AAC3B,UAAI,QAAQ;AACZ,UAAI,YAAY;AAAA,QACd,UAAU,MAAM;AAAA,QAChB,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd;AAAA,QACA,gBAAgB,MAAM,MAAM,MAAM;AAAA,MACpC;AAGA,UAAI,gBAAgB,MAAM,MAAM;AAEhC,cAAQ,KAAK;AAAA,QACX,GAAG,IAAI;AAAA,QACP,OAAO;AAAA,UACL,IAAI;AAAA,UACJ,UAAU,MAAM;AAAA,UAChB,WAAW,IAAI,UAAU;AAAA,QAC3B;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAY,OAI2D;AACrE,UAAM,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AACtC,QAAI,CAAC,IAAK,QAAO,QAAQ,QAAQ,EAAE,SAAS,YAAY,CAAC;AACzD,QAAI,IAAI,WAAW,aAAa,MAAM,UAAU;AAC9C,aAAO,QAAQ,QAAQ,EAAE,SAAS,aAAa,CAAC;AAAA,IAClD;AAGA,QAAI,IAAI,UAAU,YAAY,MAAM,SAAS;AAC3C,aAAO,QAAQ,QAAQ,EAAE,SAAS,cAAc,CAAC;AAAA,IACnD;AACA,QAAI,CAAC,IAAI,QAAS,QAAO,QAAQ,QAAQ,EAAE,SAAS,YAAY,CAAC;AACjE,QAAI,QAAQ;AACZ,WAAO,QAAQ,QAAQ,EAAE,UAAU,IAAI,QAAQ,CAAC;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,SAAS,OAOP;AACA,UAAM,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AACtC,QAAI,CAAC,IAAK,QAAO,QAAQ,QAAQ,EAAE,SAAS,YAAY,CAAC;AACzD,QAAI,IAAI,WAAW,aAAa,MAAM,UAAU;AAC9C,aAAO,QAAQ,QAAQ,EAAE,SAAS,aAAa,CAAC;AAAA,IAClD;AACA,QAAI,IAAI,UAAU,QAAQ;AACxB,aAAO,QAAQ,QAAQ,EAAE,UAAU,OAAO,OAAO,IAAI,MAAM,CAAC;AAAA,IAC9D;AACA,QAAI,SAAS,MAAM;AACnB,QAAI,cAAc,MAAM;AACxB,QAAI,QAAQ;AACZ,WAAO,QAAQ,QAAQ,EAAE,UAAU,MAAM,OAAO,IAAI,MAAM,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,cAAc,OAGQ;AACpB,UAAM,WAAqB,CAAC;AAC5B,eAAW,EAAE,OAAO,QAAQ,KAAK,MAAM,QAAQ;AAC7C,YAAM,MAAM,KAAK,MAAM,IAAI,KAAK;AAChC,UAAI,CAAC,OAAO,IAAI,WAAW,aAAa,MAAM,SAAU;AACxD,UAAI,IAAI,UAAU,YAAY,QAAS;AACvC,WAAK,SAAS,GAAG;AACjB,eAAS,KAAK,KAAK;AAAA,IACrB;AACA,WAAO,QAAQ,QAAQ,QAAQ;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,OAOH;AACA,UAAM,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK;AACtC,QAAI,KAAK,WAAW,MAAM,QAAQ;AAChC,aAAO,QAAQ,QAAQ,EAAE,SAAS,YAAY,CAAC;AAAA,IACjD;AACA,QAAI,IAAI,UAAU,oBAAoB;AACpC,aAAO,QAAQ,QAAQ,EAAE,SAAS,YAAY,KAAK,IAAI,MAAM,CAAC;AAAA,IAChE;AACA,QAAI,UAAU,MAAM;AACpB,QAAI,QAAQ;AACZ,WAAO,IAAI;AACX,WAAO,QAAQ,QAAQ,EAAE,OAAO,IAAI,MAAM,CAAC;AAAA,EAC7C;AAAA;AAAA,EAGA,WACE,UACA,QACmB;AACnB,SAAK;AACL,WAAO,QAAQ;AAAA,MACb,OACG,OAAO,CAAC,EAAE,OAAO,QAAQ,MAAM;AAC9B,cAAM,MAAM,KAAK,MAAM,IAAI,KAAK;AAChC,eAAO,KAAK,WAAW,YAAY;AAAA,MACrC,CAAC,EACA,IAAI,CAAC,EAAE,MAAM,MAAM,KAAK;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,KAAK,UAAwD;AAC3D,UAAM,WAAW,KAAK,UAAU,IAAI,SAAS,QAAQ;AACrD,QAAI,UAAU;AACZ,eAAS,aAAa,SAAS;AAC/B,aAAO,QAAQ,QAAQ,QAAQ;AAAA,IACjC;AACA,UAAM,QAAkB,EAAE,GAAG,UAAU,SAAS,MAAM;AACtD,SAAK,UAAU,IAAI,SAAS,UAAU,KAAK;AAC3C,WAAO,QAAQ,QAAQ,KAAK;AAAA,EAC9B;AAAA,EAEA,SAAS,UAAiD;AACxD,WAAO,QAAQ,QAAQ,KAAK,UAAU,IAAI,QAAQ,CAAC;AAAA,EACrD;AAAA,EAEA,WAAgC;AAC9B,WAAO,QAAQ,QAAQ,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,SAAS,KAAsB;AAC7B,QAAI,QAAQ;AACZ,WAAO,IAAI;AACX,WAAO,IAAI;AACX,WAAO,IAAI;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,KAAmC;AACvC,UAAM,WAAwB,CAAC;AAC/B,eAAW,OAAO,KAAK,MAAM,OAAO,GAAG;AACrC,UAAI,IAAI,UAAU,uBAAuB,IAAI,iBAAiB,MAAM,KAAK;AACvE,aAAK,SAAS,GAAG;AACjB,iBAAS,KAAK,GAAG;AAAA,MACnB;AACA,YAAM,QAAQ,IAAI;AAClB,UACE,UACC,IAAI,UAAU,WAAW,IAAI,UAAU,cACxC,MAAM,kBAAkB,KACxB;AACA,aAAK,SAAS,GAAG;AACjB,iBAAS,KAAK,GAAG;AAAA,MACnB;AAAA,IACF;AACA,WAAO,QAAQ,QAAQ,QAAQ;AAAA,EACjC;AACF;;;AClZO,IAAM,QAAN,MAAY;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,SAAuB;AACjC,SAAK,QAAQ,IAAI,WAAW;AAC5B,SAAK,aAAa,IAAI,WAAW,QAAQ,OAAO;AAChD,SAAK,OAAO,QAAQ,OAAO,KAAK;AAChC,SAAK,aAAa,QAAQ,YAAY,WAAW,QAAQ,QAAQ,EAAE;AACnE,SAAK,UAAU,IAAI,YAAY;AAAA,MAC7B,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,KAAK,KAAK;AAAA,MACV,SAAS,QAAQ,WAAW;AAAA,MAC5B,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,SAAK,QAAQ,IAAI,UAAU;AAAA,MACzB,OAAO,KAAK;AAAA,MACZ,YAAY,KAAK;AAAA,MACjB,KAAK,KAAK;AAAA,MACV,WAAW,QAAQ;AAAA,IACrB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,QAAQ,SAA6B;AACnC,SAAK,WAAW,QAAQ,OAAO;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,QAAyC;AAC7C,UAAM,WAAW,MAAM,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;AACnD,WAAO,EAAE,UAAU,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE;AAAA,EAC/C;AAAA;AAAA,EAGA,MAAM,OAAO,SAAqC;AAChD,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,OAAO,IAAI;AAEjB,QAAI,SAAS,YAAY,SAAS,GAAG,KAAK,SAAS,UAAU;AAC3D,aAAO,IAAI,SAAS,MAAM,UAAU,KAAK,OAAO,KAAK,KAAK,CAAC,GAAG;AAAA,QAC5D,SAAS,EAAE,gBAAgB,2BAA2B;AAAA,MACxD,CAAC;AAAA,IACH;AAEA,UAAM,UAAU,QAAQ,WAAW,SAAS,MAAM,QAAQ,KAAK,IAAI;AACnE,UAAM,OAAO,YAAY,KAAK,SAAY,SAAS,OAAO;AAC1D,UAAM,WAAW,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;AACrD,UAAM,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA,WAAW,cAAc,QAAQ,SAAS,iBAAiB;AAAA,IAC7D;AAKA,UAAM,WAAW;AAAA,MACf;AAAA,MACA;AAAA,MACA,WAAW,cAAc,QAAQ,SAAS,eAAe;AAAA,IAC3D;AAGA,QAAI,SAAS,uBAAuB;AAClC,aAAO,KAAK,MAAM,KAAK,MAAM,QAAQ,UAAU,IAAI,CAAC;AAAA,IACtD;AACA,QAAI,SAAS,uBAAuB;AAClC,aAAO,KAAK,MAAM,KAAK,MAAM,QAAQ,UAAU,IAAI,CAAC;AAAA,IACtD;AACA,QAAI,SAAS,uBAAuB;AAClC,aAAO;AAAA,QACL,MAAM,KAAK,MAAM;AAAA,UACf;AAAA,UACA,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS,uBAAuB;AAClC,aAAO;AAAA,QACL,MAAM,KAAK,MAAM;AAAA,UACf;AAAA,UACA,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,QACpC;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,KAAK,WAAW,GAAG,KAAK,SAAS,GAAG,GAAG;AAC1C,aAAO,KAAK,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,YAAY,EAAE,CAAC;AAAA,IAC3D;AACA,YAAQ,KAAK,UAAU;AAAA,MACrB,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,MAC3C,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,MAAM,MAAM,IAAI,CAAC;AAAA,MAClD,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,MAAM,MAAM,IAAI,CAAC;AAAA,MAClD,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,OAAO,MAAM,IAAI,CAAC;AAAA,MACnD,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,UAAU,MAAM,IAAI,CAAC;AAAA,MACtD,KAAK;AACH,eAAO,KAAK,MAAM,KAAK,QAAQ,QAAQ,MAAM,IAAI,CAAC;AAAA,MACpD;AACE,eAAO,KAAK,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,YAAY,EAAE,CAAC;AAAA,IAC7D;AAAA,EACF;AACF;AAEA,SAAS,SAAS,KAAsB;AACtC,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,cAAc,SAAkB,cAA+B;AACtE,QAAM,WAAW,QAAQ,IAAI,YAAY;AACzC,QAAM,WAAW,QAAQ,IAAI,oBAAoB;AACjD,QAAM,YAAY,QAAQ,IAAI,oBAAoB;AAGlD,MAAI,aAAa,QAAQ,aAAa,QAAQ,cAAc,MAAM;AAChE,WAAO;AAAA,EACT;AACA,SAAO,EAAE,UAAU,UAAU,OAAO,QAAQ,GAAG,UAAU;AAC3D;AAEA,IAAM,OAAO,CAAC,WACZ,IAAI,SAAS,KAAK,UAAU,OAAO,IAAI,GAAG;AAAA,EACxC,QAAQ,OAAO;AAAA,EACf,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;","names":["PublicIdentity","z","RequestSignature","z","ok","fail"]}
1
+ {"version":3,"sources":["../src/daemon-plane.ts","../src/debug.ts","../src/fixture.ts","../src/site-plane.ts","../src/state.ts","../src/index.ts"],"sourcesContent":["import {\n ClaimRequest,\n FetchRequest,\n HeartbeatRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n RequestSignature,\n keyId,\n MAX_CLOCK_SKEW_MS,\n verifyRequest,\n verifyPublicIdentity,\n PublicIdentity,\n} from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { Projection } from \"./fixture.js\";\nimport type { HolderRefusal } from \"./state.js\";\nimport type { RoutingStore } from \"./store.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 });\n\n/**\n * A store refusal, in HTTP.\n *\n * The store says *why* in its own vocabulary and this decides what a daemon is\n * told, which keeps the two independent: a store that grows a reason does not\n * get to invent a status code, and a status code that changes does not reach\n * into the store.\n *\n * `not-ready` is the one that matters. It means claimed-but-not-yet-sealed, and\n * a daemon must retry rather than abandon — the job is legitimately still\n * theirs until the lease or the awaiting-payload clock says otherwise. It was\n * the protocol gap that produced the 409 in the first place.\n */\nconst REFUSALS: Record<HolderRefusal, PlaneResult> = {\n \"not-found\": {\n status: 404,\n body: { error: \"not-found\", message: \"unknown job\" },\n },\n \"not-holder\": {\n status: 403,\n body: {\n error: \"unauthorized\",\n message: \"this runner does not hold the job\",\n },\n },\n \"stale-lease\": {\n status: 403,\n body: { error: \"unauthorized\", message: \"that lease is no longer current\" },\n },\n \"not-ready\": {\n status: 409,\n body: {\n error: \"not-ready\",\n message: \"the site has not sealed this job yet\",\n },\n },\n};\nconst fail = (status: number, error: string, message: string): PlaneResult => ({\n status,\n body: { error, message },\n});\n\nexport interface DaemonPlaneDeps {\n readonly state: RoutingStore;\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 async pair(body: unknown): Promise<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 // The key comes from the site registry, which is its one home. It used to\n // be inlined on the consent record, which gave a site's key one copy per\n // consenting user and nothing to reconcile them against.\n const site = this.#deps.projection.siteFor(this.#deps.siteId);\n if (!site) {\n return fail(403, \"unauthorized\", \"this site is not registered\");\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 // No timestamp: the store stamps `lastSeenAt` from its own clock, so\n // presence and the deadlines that reason about it agree (cloud_006 §3.4).\n await this.#deps.state.seen({\n runnerId,\n owner: parsed.data.owner,\n device: parsed.data.device,\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: site.site,\n });\n }\n\n /** Every authenticated call: signature first, then consent, then work. */\n async #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 ) => Promise<PlaneResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<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 = await 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 === \"stale\") return this.#clockSkew();\n if (failure) return fail(401, \"unauthorized\", \"signature check failed\");\n\n // Asked of the projection, not of the cached flag.\n //\n // `known.revoked` is set by `heartbeat`, and enforcing on it made\n // revocation depend on the client calling an endpoint: a daemon that\n // simply never heartbeats would go on claiming after its consent was\n // withdrawn, forever. A well-behaved daemon beats every few seconds, which\n // is why the freeze gate's \"within one heartbeat\" demo passed and why this\n // was invisible — the guarantee held for every client that wanted it to.\n //\n // The cached flag survives as what heartbeat *reports* to the daemon. It\n // is a message, not an authority, and the authority is the projection.\n // Two copies of one value where one is a stale mirror of the other is this\n // project's most-repeated bug; here it was also an enforcement hole.\n const revoked =\n this.#deps.projection.consentFor(known.owner, this.#deps.siteId) === null;\n if (revoked && options.allowRevoked !== true) {\n // Revocation reaches the daemon through heartbeat too, so heartbeat\n // itself must be answerable by a revoked runner — bouncing it with a 403\n // would read as a transport problem and it would keep trying.\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 /**\n * A clock too far from ours, said plainly and with the number to fix it by.\n *\n * Its own error code rather than a generic `unauthorized`, because it is the\n * one refusal a retry can never fix and an `ntpdate` always can — the same\n * reasoning `version-unsupported` already carries on the daemon side. A\n * daemon that reports this as a generic rejection sends its owner looking at\n * their network.\n *\n * `serverTime` is included so the far side can say *how far off* rather than\n * *that something is wrong*. It is not a disclosure: the heartbeat response\n * returns the same value, and so does every `Date` header.\n */\n #clockSkew(): PlaneResult {\n return {\n status: 401,\n body: {\n error: \"clock-skew\",\n message:\n \"this request's timestamp is too far from the server's clock; \" +\n \"check the machine's time and try again\",\n serverTime: this.#deps.now(),\n maxSkewMs: MAX_CLOCK_SKEW_MS,\n },\n };\n }\n\n claim(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): Promise<PlaneResult> {\n return this.#authed(auth, body, ClaimRequest, async (request, device) => {\n if (request.runnerId !== device.runnerId) {\n return fail(401, \"unauthorized\", \"runner id does not match the key\");\n }\n // One store call. The decision and its write are the store's, because a\n // caller that reads, filters and writes back cannot be made atomic once\n // the store is on a network (cloud_006 §3.2).\n const granted = await this.#deps.state.claim({\n runnerId: device.runnerId,\n owner: device.owner,\n device: device.device,\n siteId: this.#deps.siteId,\n kinds: new Set(request.capabilities.map((c) => c.kind)),\n // The projection, collapsed to data the store can match on — a\n // predicate does not travel.\n owners: new Set(this.#deps.projection.ownersRunnableBy(device.owner)),\n max: request.max,\n leaseMs: this.#deps.leaseMs,\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, FetchRequest, async (request, device) => {\n const taken = await this.#deps.state.takePayload({\n jobId: request.jobId,\n runnerId: device.runnerId,\n leaseId: request.leaseId,\n });\n if (\"refused\" in taken) return REFUSALS[taken.refused];\n return ok({ envelope: taken.envelope });\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, ResultRequest, async (request, device) => {\n const recorded = await this.#deps.state.complete({\n jobId: request.jobId,\n runnerId: device.runnerId,\n envelope: request.envelope,\n disposition: request.disposition,\n });\n if (\"refused\" in recorded) return REFUSALS[recorded.refused];\n return ok(recorded);\n });\n }\n\n heartbeat(\n auth: { endpoint: string; rawBody: string; signature: unknown },\n body: unknown,\n ): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n async (request, device) => {\n const now = this.#deps.now();\n await this.#deps.state.sweep();\n\n const known = await 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 = await this.#deps.state.lostLeases(\n device.runnerId,\n request.activeLeases,\n );\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 ): Promise<PlaneResult> {\n return this.#authed(auth, body, ReleaseRequest, async (request, device) => {\n const released = await this.#deps.state.releaseLeases({\n runnerId: device.runnerId,\n leases: request.leases,\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 { fingerprintOf } from \"./daemon-plane.js\";\nimport type { RoutedJob } from \"./state.js\";\nimport type { RoutingStore } from \"./store.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) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[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 async function debugPage(\n state: RoutingStore,\n now: number,\n): Promise<string> {\n const jobs = await state.jobs();\n const devices = await 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 site the control plane registered and domain-verified — cloud_004 §5.\n *\n * **The one authority for a site's public identity.** It used to be inlined on\n * every consent record, which meant a site's key had as many homes as it had\n * users and nothing checked they agreed — the exact shape this project has now\n * found in a version constant, a clock read, an envelope deadline, a reseal\n * implementation, a package list and a docs page. Consents now reference a\n * site by id and the key is looked up here.\n *\n * The relay needs it for two things it cannot do without:\n *\n * 1. **Telling a daemon who to pin** at pairing — the key that makes relayed\n * work unforgeable, since the relay holds no key that could produce it.\n * 2. **Authenticating the site plane.** A site calls a relay the way a daemon\n * does, signing with this identity, and this is the key those signatures\n * are checked against.\n */\nexport const SiteRecord = z\n .object({\n /** How the control plane names the site. */\n siteId: z.string().min(1),\n /**\n * The site's public identity.\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 SiteRecord = z.infer<typeof SiteRecord>;\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 .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 /** Registered sites, by id. A consent for a site absent here routes not. */\n sites: z.array(SiteRecord).default([]),\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 sites: [],\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 site this id names, if the control plane registered it.\n *\n * The only source of a site's public identity in this package. Everything\n * that pins, verifies or seals to a site starts here.\n */\n siteFor(siteId: string): SiteRecord | null {\n return this.#fixture.sites.find((s) => s.siteId === siteId) ?? null;\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 * Every owner whose work this device's owner may run, as a list.\n *\n * The same question {@link mayRunFor} answers, asked in the direction a\n * *store* can use. That difference is the crux of making `claim` atomic\n * (cloud_006 §3.2).\n *\n * Today `claim` scans every job and calls `mayRunFor` per candidate, which\n * works because the projection is a local object. A shared routing store\n * cannot do that: the filter has to travel to the store, and a predicate\n * does not travel — you cannot send a closure to Valkey. So the projection\n * is collapsed to **data** here and handed over as a set the store can\n * match on.\n *\n * That the collapse is possible at all is a property of the design worth\n * noticing: `mayRunFor` is a finite lookup over consent and rosters, not a\n * computation over the jobs. If it ever became job-dependent — \"may run\n * work of this size\", say — an atomic claim would stop being expressible,\n * and that is the moment to argue rather than to add a parameter.\n *\n * The owner is always included: a device runs its owner's work, and the\n * relay checks that before it checks a roster.\n */\n ownersRunnableBy(deviceOwner: string): string[] {\n const owners = new Set([deviceOwner]);\n for (const roster of this.#fixture.rosters) {\n if (roster.owner !== deviceOwner) continue;\n for (const member of roster.members) owners.add(member);\n }\n return [...owners];\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 {\n JobStub,\n RequestSignature,\n SealedEnvelope,\n verifySiteRequest,\n} from \"@byollm/protocol\";\nimport { z } from \"zod\";\nimport type { PlaneResult } from \"./daemon-plane.js\";\nimport type { Projection } from \"./fixture.js\";\nimport type { RoutingStore } from \"./store.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 * ## Every call is signed, and this plane once was not\n *\n * A site authenticates exactly as a daemon does: it signs each request with\n * the identity key the control plane registered for it, and the relay checks\n * that signature against the projection. Nothing here trusts a `siteId` in a\n * body or a query string.\n *\n * This was the ninth finding, and it was found by reading the code in\n * preparation for the first public deploy rather than by any test — the whole\n * plane took the caller's word for who it was. What that bought an anonymous\n * caller, against a relay reachable on the internet:\n *\n * - **`enqueue` as anyone.** Publish stubs in a site's name and consenting\n * users' machines claim them. The payload that follows is sealed by the real\n * site or not at all, so no forged *work* runs — but unsolicited dispatch to\n * private hardware is a product-level breach whatever the ciphertext does.\n * - **`payload` as anyone**, over a live claim: substitute an envelope the\n * daemon will refuse to open, and the job is burned rather than run.\n * - **`pending` and `results` as anyone**: a metadata read of who is online\n * for a site, which device claimed what, and every lease id in flight.\n *\n * `RELAY_BLIND` held throughout — none of it opens a payload, which is the\n * point of building it that way. But blind is not the same as safe, and the\n * distance between them is this file.\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\n/** A read: the site id arrives in the query and is signed as an empty body. */\nconst QueryRequest = z.object({ siteId: z.string().min(1) }).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: RoutingStore;\n readonly projection: Projection;\n readonly now: () => number;\n /**\n * The one site this relay routes for.\n *\n * The same value the daemon plane holds, from the same option, because it is\n * the same fact — a relay that accepted enqueues for sites its daemons never\n * paired with would route work nobody can open.\n */\n readonly routesFor: string;\n}\n\n/** What `Relay.handle` reconstructs from the request, for signature checking. */\nexport interface SiteAuth {\n /** The endpoint name alone — the domain separator is applied by protocol. */\n readonly endpoint: string;\n /** The exact bytes received, hashed into the signature. */\n readonly rawBody: string;\n /** From the headers, or undefined if any part was missing. */\n readonly signature: unknown;\n}\n\nexport class SitePlane {\n readonly #deps: SitePlaneDeps;\n\n constructor(deps: SitePlaneDeps) {\n this.#deps = deps;\n }\n\n /**\n * Signature first, then the site id, then the work.\n *\n * The caller is whoever the signature says, verified against the key the\n * control plane registered — never whoever the request claims. The `siteId`\n * every request carries is then required to *match* that caller, so the two\n * can never name different sites; a request that says one thing in its\n * signed material and another in its body is refused rather than reconciled.\n *\n * Every endpoint goes through here, including the reads. That is deliberate:\n * an authenticated write plane beside an open read plane would still hand a\n * stranger presence, claims and lease ids, and \"who is online right now\" is\n * exactly the fact a blind relay is otherwise so careful not to reveal.\n */\n async #authed<T>(\n auth: SiteAuth,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n siteIdOf: (request: T) => string,\n run: (request: T, siteId: string) => Promise<PlaneResult>,\n ): Promise<PlaneResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(401, \"unauthorized\", \"this request is not signed\");\n }\n // The signature's caller slot carries the site id (byollm_009 §4.2's\n // site-plane note). Resolving the key through the site registry rather\n // than the device registry is what keeps a device signature from ever\n // authenticating as a site.\n const siteId = signature.data.runnerId;\n const site = this.#deps.projection.siteFor(siteId);\n if (!site) {\n return fail(401, \"unauthorized\", \"this site is not registered\");\n }\n\n const failure = verifySiteRequest({\n identityPublic: site.site.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#deps.now(),\n });\n if (failure) return fail(401, \"unauthorized\", \"signature check failed\");\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 if (siteIdOf(parsed.data) !== siteId) {\n return fail(403, \"unauthorized\", \"that is not your site\");\n }\n // This relay routes for exactly one site, and now says so.\n //\n // The daemon plane has always been single-tenant — `DaemonPlaneDeps.siteId`\n // is the field multi-tenancy replaces — but the site plane accepted any\n // registered site, and `claim` never looked at a job's `siteId` at all. A\n // second registered site's jobs would therefore be offered to a daemon\n // paired with the first, which pinned a different key and could only fail\n // to open the payload. Contained by the crypto, and still a job burned by\n // routing rather than by anything the device did.\n if (siteId !== this.#deps.routesFor) {\n return fail(403, \"unauthorized\", \"this relay does not route for you\");\n }\n return run(parsed.data, siteId);\n }\n\n enqueue(auth: SiteAuth, body: unknown): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n EnqueueRequest,\n (request) => request.siteId,\n async (request, siteId) => {\n const job = await this.#deps.state.enqueue({\n id: request.stub.id,\n siteId,\n stub: request.stub,\n });\n // Idempotent by id: a known id returns what is already routing. Only\n // one site can reach this relay, so a known id is always this site's\n // republish. The multi-tenant router needs a collision check here, and\n // it gets one when it can be exercised — an unreachable guard is a\n // test that cannot fail, which this project has now written twice.\n return ok({ jobId: job.id, state: job.state });\n },\n );\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(auth: SiteAuth, siteId: string): Promise<PlaneResult> {\n return this.#authed(\n auth,\n { siteId },\n QueryRequest,\n (request) => request.siteId,\n async (_request, site) => {\n await this.#deps.state.sweep();\n const jobs = (await this.#deps.state.awaiting(site)).map((job) => ({\n jobId: job.id,\n // Non-null by construction: `awaiting` only returns claimed jobs.\n // The optional chain is here so a future state-machine edit that\n // broke that invariant would produce a missing field rather than a\n // crash on the 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 }\n\n payload(auth: SiteAuth, body: unknown): Promise<PlaneResult> {\n return this.#authed(\n auth,\n body,\n PayloadRequest,\n (request) => request.siteId,\n async (request, siteId) => {\n // One store call: the check and the write together. A site that read\n // \"awaiting-payload\" and then wrote would be racing the timeout that\n // makes the state mean anything.\n const sealed = await this.#deps.state.seal({\n jobId: request.jobId,\n siteId,\n envelope: request.envelope,\n });\n if (\"refused\" in sealed) {\n return sealed.refused === \"not-found\"\n ? fail(404, \"not-found\", \"unknown job\")\n : fail(\n 409,\n \"too-late\",\n `job is ${sealed.was ?? \"gone\"}, not awaiting payload`,\n );\n }\n return ok({ jobId: request.jobId, state: sealed.state });\n },\n );\n }\n\n /** Sealed results, for the site to open and verify. */\n results(auth: SiteAuth, siteId: string): Promise<PlaneResult> {\n return this.#authed(\n auth,\n { siteId },\n QueryRequest,\n (request) => request.siteId,\n async (_request, site) => {\n const jobs = (await this.#deps.state.finished(site)).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\n * the key it was told to seal to — and so `RESULT_PROVENANCE` can\n * name a foreign device rather than guessing (cloud_004 §11.2).\n */\n device: job.claimedBy?.device,\n }));\n return ok({ jobs });\n },\n );\n }\n}\n","import type {\n ClaimedStub,\n JobStub,\n PublicIdentity,\n SealedEnvelope,\n} from \"@byollm/protocol\";\nimport { randomUUID } from \"node:crypto\";\nimport type { RoutingStore } from \"./store.js\";\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 * What a routing store must do, expressed as operations — cloud_006 §3.2.\n *\n * Every method below is a **decision plus its write**, never a read the caller\n * follows with a mutation. That is the whole point, and it is the difference\n * between an interface a shared store can implement and one it cannot.\n *\n * `claim` is the specimen. It used to live in `DaemonPlane` as\n * `jobs()` → filter → mutate, which is atomic for exactly one reason: Node is\n * single-threaded and these Maps are local, so nothing runs between the read\n * and the write. Neither survives a store on a network, and\n * `packages/relay/test/two-replicas.test.ts` holds the resulting race as a\n * failing assertion.\n *\n * So the rule for anything added here: **if a caller has to read, decide, and\n * write back, the operation is in the wrong place.** Move the decision in.\n *\n * ## Why the projection does not come with it\n *\n * `claim` takes `owners: string[]` rather than a projection or a predicate.\n * A closure cannot travel to Valkey, and the projection replicates for free\n * from the control plane — so the caller collapses it with\n * `Projection.ownersRunnableBy` and hands over data the store can match on.\n * That keeps the store ignorant of consent, which is also what keeps it\n * replaceable.\n */\nexport interface ClaimInput {\n readonly runnerId: string;\n readonly owner: string;\n readonly device: PublicIdentity;\n /** The site this relay routes for. Multi-tenancy widens this to a set. */\n readonly siteId: string;\n /** Job kinds this device can actually run. */\n readonly kinds: ReadonlySet<string>;\n /** Whose work it may run — the projection, already collapsed to data. */\n readonly owners: ReadonlySet<string>;\n readonly max: number;\n readonly leaseMs: number;\n}\n\n/**\n * Where the store's sense of time comes from — cloud_006 §3.4.\n *\n * **The store owns its clock; callers do not pass one.** Every deadline the\n * relay decides — a lease's expiry, the `awaiting-payload` window, what a\n * sweep considers due — is now stamped by one source, and it is the same\n * source that will later stamp them for every replica.\n *\n * It used to be a parameter. `claim` took `now`, `sweep` took `now`, and each\n * plane called its own `now()` before calling in — which is fine in one\n * process and is the recurring bug the moment there are two. A lease granted\n * by a pod whose clock runs fast is short; the same lease swept by a pod whose\n * clock runs slow outlives it. Nobody is wrong and the lease has no length.\n *\n * A Valkey-backed store returns `TIME` here, so the deadline and the sweep\n * that enforces it are read from the same server. The injected clock stays for\n * tests, which is what lets them move time instead of sleeping.\n *\n * **What deliberately does not use this**: request-signature freshness. That\n * is checked against the *local* clock on purpose — it is a question about the\n * caller's clock versus this process's, `MAX_CLOCK_SKEW_MS` already tolerates\n * two minutes of disagreement, and a network round trip to timestamp every\n * inbound request would be a cost with no property behind it.\n */\nexport interface RelayStateOptions {\n readonly now?: () => number | Promise<number>;\n}\n\n/** Why a lease-scoped operation was refused, in the caller's vocabulary. */\nexport type HolderRefusal =\n \"not-found\" | \"not-holder\" | \"stale-lease\" | \"not-ready\";\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 implements RoutingStore {\n readonly #jobs = new Map<string, RoutedJob>();\n readonly #presence = new Map<string, Presence>();\n readonly #now: () => number | Promise<number>;\n\n constructor(options: RelayStateOptions = {}) {\n this.#now = options.now ?? Date.now;\n }\n\n /** The one clock every deadline in this store is stamped from. */\n async now(): Promise<number> {\n return this.#now();\n }\n\n /**\n * Take a stub for routing. The payload is not here and will not be.\n *\n * **Idempotent by job id, and that is a security property rather than a\n * convenience.** Site-plane calls are authenticated by signature, and\n * byollm_009 §4.2's argument for signing the request instead of a\n * server-issued nonce rests entirely on every write being idempotent per the\n * instance it names. This one was not: re-enqueueing a known id built a\n * fresh `queued` job over the top of the old one, discarding a live claim,\n * its lease and any payload the site had already sealed to a device. A\n * replayed enqueue inside the two-minute freshness window was therefore a\n * way to yank a job back from the machine running it — the `release` bug of\n * §4.2, rediscovered on the other plane.\n *\n * So a known id returns what is already routing, unchanged. A site that\n * restarts and republishes its queue is the normal case, and it must not\n * disturb work in flight.\n */\n enqueue(input: {\n id: string;\n siteId: string;\n stub: JobStub;\n }): Promise<RoutedJob> {\n const existing = this.#jobs.get(input.id);\n if (existing) return Promise.resolve(existing);\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 Promise.resolve(job);\n }\n\n job(jobId: string): Promise<RoutedJob | undefined> {\n return Promise.resolve(this.#jobs.get(jobId));\n }\n\n jobs(): Promise<RoutedJob[]> {\n return Promise.resolve([...this.#jobs.values()]);\n }\n\n /** Jobs a site must seal for, right now. */\n async awaiting(siteId: string): Promise<RoutedJob[]> {\n return (await this.jobs()).filter(\n (j) => j.siteId === siteId && j.state === \"awaiting-payload\",\n );\n }\n\n /** Sealed results waiting to go home. */\n async finished(siteId: string): Promise<RoutedJob[]> {\n return (await this.jobs()).filter(\n (j) =>\n j.siteId === siteId && j.state === \"done\" && j.result !== undefined,\n );\n }\n\n /**\n * Claim work — one operation, because it has to be.\n *\n * Moved here wholesale from `DaemonPlane`, where it was a scan followed by\n * per-job mutation. Nothing about the *decision* changed; what changed is\n * that a store can now implement it, because the filter and the write are\n * one call rather than a loop the caller drives.\n *\n * The order of the guards is worth preserving as-is when this becomes a Lua\n * script: cheapest first, and `owners` last because it is the only one that\n * needed the projection.\n */\n async claim(input: ClaimInput): Promise<ClaimedStub[]> {\n const now = await this.now();\n await this.sweep();\n\n const granted: ClaimedStub[] = [];\n for (const job of this.#jobs.values()) {\n if (granted.length >= input.max) break;\n if (job.state !== \"queued\") continue;\n // Only this relay's site. A device paired against one site's key and\n // pinned it; a job from another site could only ever produce an envelope\n // it refuses to open — contained by the crypto, and still a burned job.\n if (job.siteId !== input.siteId) continue;\n if (!input.kinds.has(job.stub.kind)) continue;\n // The relay's half of AUDIENCE_BOTH_SIDES. The daemon re-checks its own\n // allowlist and may still refuse — this only ever narrows.\n if (!input.owners.has(job.stub.owner)) continue;\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`, so\n // a relay minting `lease_<job>_<time>` would route perfectly against a\n // memory store and fail the moment a real site adopted the lease.\n const leaseId = randomUUID();\n job.state = \"awaiting-payload\";\n job.claimedBy = {\n runnerId: input.runnerId,\n owner: input.owner,\n device: input.device,\n leaseId,\n leaseExpiresAt: now + input.leaseMs,\n };\n // Not the lease: this bounds how long we wait for a *site*, not how long\n // the device may work. byollm_009 §7.1's third clock.\n job.awaitingUntil = now + AWAITING_PAYLOAD_MS;\n\n granted.push({\n ...job.stub,\n lease: {\n id: leaseId,\n runnerId: input.runnerId,\n expiresAt: job.claimedBy.leaseExpiresAt,\n },\n });\n }\n return granted;\n }\n\n /**\n * Hand over the sealed payload to the device that holds the lease.\n *\n * The read and the state transition are one operation for the same reason\n * `claim` is: `running` must be set by whoever was told the envelope, or two\n * replicas can both hand out the same work and both believe they were first.\n */\n takePayload(input: {\n jobId: string;\n runnerId: string;\n leaseId: string;\n }): Promise<{ envelope: SealedEnvelope } | { refused: HolderRefusal }> {\n const job = this.#jobs.get(input.jobId);\n if (!job) return Promise.resolve({ refused: \"not-found\" });\n if (job.claimedBy?.runnerId !== input.runnerId) {\n return Promise.resolve({ refused: \"not-holder\" });\n }\n // LEASE_HONORED per *instance*: a stale lease id names a grant that is\n // over, and answering it would hand work to a previous holder.\n if (job.claimedBy.leaseId !== input.leaseId) {\n return Promise.resolve({ refused: \"stale-lease\" });\n }\n if (!job.payload) return Promise.resolve({ refused: \"not-ready\" });\n job.state = \"running\";\n return Promise.resolve({ envelope: job.payload });\n }\n\n /**\n * Record a finished job.\n *\n * `RESULT_IDEMPOTENT` lives here rather than in the caller: a replayed\n * result must be a no-op decided by the same operation that would have\n * written it, or two replicas can both decide they were the first.\n */\n complete(input: {\n jobId: string;\n runnerId: string;\n envelope: SealedEnvelope;\n disposition: \"ok\" | \"error\" | \"canceled\";\n }): Promise<\n { accepted: boolean; state: RoutedState } | { refused: HolderRefusal }\n > {\n const job = this.#jobs.get(input.jobId);\n if (!job) return Promise.resolve({ refused: \"not-found\" });\n if (job.claimedBy?.runnerId !== input.runnerId) {\n return Promise.resolve({ refused: \"not-holder\" });\n }\n if (job.state === \"done\") {\n return Promise.resolve({ accepted: false, state: job.state });\n }\n job.result = input.envelope;\n job.disposition = input.disposition;\n job.state = \"done\";\n return Promise.resolve({ accepted: true, state: job.state });\n }\n\n /** Give back leases this runner holds, naming each grant it means. */\n releaseLeases(input: {\n runnerId: string;\n leases: readonly { jobId: string; leaseId: string }[];\n }): Promise<string[]> {\n const released: string[] = [];\n for (const { jobId, leaseId } of input.leases) {\n const job = this.#jobs.get(jobId);\n if (!job || job.claimedBy?.runnerId !== input.runnerId) continue;\n if (job.claimedBy.leaseId !== leaseId) continue;\n this.#requeue(job);\n released.push(jobId);\n }\n return Promise.resolve(released);\n }\n\n /**\n * Take a site's sealed payload for a claimed job.\n *\n * Refuses anything not `awaiting-payload`, which is what makes the timeout\n * mean something: a late seal must not land on a claim that has moved.\n */\n seal(input: {\n jobId: string;\n siteId: string;\n envelope: SealedEnvelope;\n }): Promise<\n | { state: RoutedState }\n | { refused: \"not-found\" | \"too-late\"; was?: RoutedState }\n > {\n const job = this.#jobs.get(input.jobId);\n if (job?.siteId !== input.siteId) {\n return Promise.resolve({ refused: \"not-found\" });\n }\n if (job.state !== \"awaiting-payload\") {\n return Promise.resolve({ refused: \"too-late\", was: job.state });\n }\n job.payload = input.envelope;\n job.state = \"ready\";\n delete job.awaitingUntil;\n return Promise.resolve({ state: job.state });\n }\n\n /** Which of these leases this runner no longer holds. */\n lostLeases(\n runnerId: string,\n active: readonly { jobId: string; leaseId: string }[],\n ): Promise<string[]> {\n void runnerId;\n return Promise.resolve(\n active\n .filter(({ jobId, leaseId }) => {\n const job = this.#jobs.get(jobId);\n return job?.claimedBy?.leaseId !== leaseId;\n })\n .map(({ jobId }) => jobId),\n );\n }\n\n async seen(\n presence: Omit<Presence, \"revoked\" | \"lastSeenAt\">,\n ): Promise<Presence> {\n const lastSeenAt = await this.now();\n const existing = this.#presence.get(presence.runnerId);\n if (existing) {\n existing.lastSeenAt = lastSeenAt;\n return existing;\n }\n const fresh: Presence = { ...presence, lastSeenAt, revoked: false };\n this.#presence.set(presence.runnerId, fresh);\n return fresh;\n }\n\n presence(runnerId: string): Promise<Presence | undefined> {\n return Promise.resolve(this.#presence.get(runnerId));\n }\n\n everyone(): Promise<Presence[]> {\n return Promise.resolve([...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 async sweep(): Promise<RoutedJob[]> {\n const now = await this.now();\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 { 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\";\nimport type { RoutingStore } from \"./store.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 * Where routing state lives — cloud_006.\n *\n * Defaults to an in-process {@link RelayState}, which is correct for one\n * replica and is what this package ships. A hub running more than one\n * replica supplies a shared implementation of {@link RoutingStore} instead;\n * `packages/relay/test/two-replicas.test.ts` is why that is not optional.\n *\n * **The implementation is deliberately not in this package.** A Valkey\n * client is a dependency every consumer would carry to get a feature only a\n * multi-replica deployment uses, and the production hub is the closed piece\n * (cloud_001). What ships here is the interface, the reference\n * implementation, and the tests that say what an implementation must\n * guarantee.\n */\n readonly store?: RoutingStore;\n}\n\n/** A running relay: one fetch handler, two planes, one debug page. */\nexport class Relay {\n readonly state: RoutingStore;\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 =\n options.store ?? new RelayState({ now: options.now ?? Date.now });\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({\n state: this.state,\n projection: this.projection,\n now: this.#now,\n routesFor: options.siteId,\n });\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 async sweep(): Promise<{ requeued: string[] }> {\n const requeued = await this.state.sweep();\n return { requeued: requeued.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(await 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 endpoint = path.slice(path.lastIndexOf(\"/\") + 1);\n const auth = {\n endpoint,\n rawBody,\n signature: signatureFrom(request.headers, \"x-byollm-runner\"),\n };\n // The caller header differs by plane, so a signature meant for one can\n // never be presented to the other by moving the request. The endpoint's\n // domain separator (`site/…`) already covers this; the header makes it\n // true at parse time rather than at verification time.\n const siteAuth = {\n endpoint,\n rawBody,\n signature: signatureFrom(request.headers, \"x-byollm-site\"),\n };\n\n // -- the site plane -----------------------------------------------------\n if (path === \"/relay/site/enqueue\") {\n return json(await this.#site.enqueue(siteAuth, body));\n }\n if (path === \"/relay/site/payload\") {\n return json(await this.#site.payload(siteAuth, body));\n }\n if (path === \"/relay/site/pending\") {\n return json(\n await this.#site.pending(\n siteAuth,\n url.searchParams.get(\"siteId\") ?? \"\",\n ),\n );\n }\n if (path === \"/relay/site/results\") {\n return json(\n await this.#site.results(\n siteAuth,\n url.searchParams.get(\"siteId\") ?? \"\",\n ),\n );\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(await this.#daemon.pair(body));\n case \"claim\":\n return json(await this.#daemon.claim(auth, body));\n case \"fetch\":\n return json(await this.#daemon.fetch(auth, body));\n case \"result\":\n return json(await this.#daemon.result(auth, body));\n case \"heartbeat\":\n return json(await this.#daemon.heartbeat(auth, body));\n case \"release\":\n return json(await 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, callerHeader: string): unknown {\n const runnerId = headers.get(callerHeader);\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 { RoutingStore } from \"./store.js\";\nexport type { RelayFixture };\nexport {\n ConsentRecord,\n DeviceRecord,\n RevocationRecord,\n RosterRecord,\n SiteRecord,\n RelayFixture as RelayFixtureSchema,\n EMPTY_FIXTURE,\n} from \"./fixture.js\";\nexport { AWAITING_PAYLOAD_MS } from \"./state.js\";\n/**\n * Everything an implementer of {@link RoutingStore} needs.\n *\n * `ClaimInput` and `HolderRefusal` were missing from this list, which made the\n * interface unimplementable outside this package — found by writing the second\n * implementation, which is the only thing that could have found it. An\n * exported interface whose parameter types are private is a contract nobody\n * can sign.\n */\nexport type {\n ClaimInput,\n HolderRefusal,\n Presence,\n RoutedJob,\n RoutedState,\n} from \"./state.js\";\n"],"mappings":"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","names":["PublicIdentity","z","RequestSignature","z","ok","fail"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byollm/relay",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
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",
@@ -27,16 +27,16 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "zod": "^4.1.13",
30
- "@byollm/protocol": "0.1.0-alpha.12"
30
+ "@byollm/protocol": "0.1.0-alpha.13"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@supabase/supabase-js": "^2.112.2",
37
- "byollm": "0.1.0-alpha.12",
38
- "@byollm/server": "0.1.0-alpha.12",
39
- "@byollm/conformance": "0.1.0-alpha.12"
37
+ "@byollm/server": "0.1.0-alpha.13",
38
+ "@byollm/conformance": "0.1.0-alpha.13",
39
+ "byollm": "0.1.0-alpha.13"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsup"