@byollm/relay 0.1.0-alpha.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Of Tomorrow, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ > [!WARNING]
2
+ > **Alpha (`0.1.0-alpha.10`) — under active development. Don't use this yet.**
3
+ >
4
+ > This is a walking skeleton. It routes real jobs between real daemons and real
5
+ > sites, and it is the fixture byollm_009 freezes against — but it keeps its
6
+ > state in memory, serves one site, and has never run anywhere but a test.
7
+
8
+ # `@byollm/relay`
9
+
10
+ The **reference relay**: it routes byollm jobs between a site and someone's
11
+ machine while holding no key that can open either end's traffic.
12
+
13
+ ```
14
+ site ──stub──▶ relay ◀──claim── daemon
15
+ ◀─who claimed?─┤
16
+ ──sealed payload─▶ ──────────▶ (opened only on the device)
17
+ ◀────────────── sealed result ◀──
18
+ ```
19
+
20
+ ## Why a relay can be blind
21
+
22
+ A payload is encrypted to the machine that runs it. Nobody knows which machine
23
+ that is until one claims the job — so the site publishes a **stub** first
24
+ (byollm_009 §6: user, kind, size class, audience, deadline, streaming flag, and
25
+ nothing else), a daemon claims it, and only then does the site seal the work to
26
+ that specific device.
27
+
28
+ The relay is a directory in that exchange, not a participant. It says "this
29
+ device claimed your job, here is its public key" and carries what comes back.
30
+ It cannot read a payload because it was never a recipient, and it cannot
31
+ substitute one because the daemon verifies every envelope against the site
32
+ identity it pinned at consent.
33
+
34
+ That is not a policy this code follows. It is a shape it has: no type in this
35
+ package has a field that could hold a private key, so making this relay able to
36
+ read a payload means changing its types — a review someone has to justify
37
+ rather than a line someone can slip in.
38
+
39
+ ## Why it ships open
40
+
41
+ It is the conformance kit's reference relay, and the kit is public — so it
42
+ starts where it ends rather than being written closed and ported. A relay that
43
+ claims to be blind should be readable by the people trusting it, and a
44
+ third-party daemon testing hub mode should test against real code rather than a
45
+ mock of it.
46
+
47
+ The production hub — multi-tenant routing, presence at scale, billing, ops — is
48
+ built on these same interfaces and is not this.
49
+
50
+ ## `awaiting-payload`
51
+
52
+ byollm_009 §7 described a state the direct plane cannot produce. There the site
53
+ *is* the upstream: it seals when asked, so a job is never claimed-but-unsealed.
54
+ Here they are different parties, and the gap between them is a state with its
55
+ own clock — separate from the lease and from the job's TTL, because they answer
56
+ different questions:
57
+
58
+ | clock | question |
59
+ | ------------------ | ---------------------------------------------- |
60
+ | TTL | is this work still worth doing? |
61
+ | lease | how long does this device get to run it? |
62
+ | `awaiting-payload` | how long do we wait for a site that went away? |
63
+
64
+ When it fires, the stub returns to the queue and nothing is lost.
65
+
66
+ ## Consent
67
+
68
+ The relay routes nothing without a consent record, and it cannot create one —
69
+ consent is a decision made elsewhere and projected in. In the skeleton that
70
+ projection is a fixture file; later it is whatever the control plane serves.
71
+ The shape is deliberately small, because it is a contract: anything added to it
72
+ has to be something a control plane can actually know, and a decision rather
73
+ than something the relay could observe for itself.
74
+
75
+ ## Both callers sign
76
+
77
+ A daemon signs every request with its device key. **A site signs every request
78
+ with its site key** — the same key the control plane registered, the same key
79
+ daemons pin at pairing, verified against the `sites` half of the projection.
80
+ Nothing here trusts a `siteId` in a body or a query string.
81
+
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.10`, which on a relay reachable from the
84
+ internet is an open enqueue endpoint into consenting users' machines and an
85
+ open read of who is online. It was blind the whole time — nothing could open a
86
+ payload — and blind is not the same as safe.
87
+
88
+ If you are running this: the site plane is authenticated but this is still a
89
+ single-tenant relay with in-memory state. One site, one replica.
90
+
91
+ ## Running it
92
+
93
+ ```ts
94
+ import { Relay } from "@byollm/relay";
95
+
96
+ const relay = new Relay({
97
+ siteId: "site_demo",
98
+ fixture: {
99
+ // The site registry: one home for a site's public identity, used both to
100
+ // tell daemons who to pin and to check the site's own signatures.
101
+ sites: [{ siteId: "site_demo", site: sitePublicKeys }],
102
+ consents: [{ owner: "alice", siteId: "site_demo" }],
103
+ },
104
+ });
105
+
106
+ // One fetch handler: the daemon plane, the site plane, and /debug.
107
+ const response = await relay.handle(request);
108
+ ```
109
+
110
+ `/debug` renders every routed job, its state, who claimed it, and how long an
111
+ `awaiting-payload` timer has left. It shows no prompt or result text — not
112
+ because it filters them out, but because the relay does not have them.
113
+
114
+ **Do not serve `/debug` on the internet.** It shows no payloads and it does
115
+ show who is online, which device holds what, and every lease in flight — the
116
+ same metadata the site plane's signatures exist to protect, through a
117
+ different door. Whoever serves this package decides that, which is why the
118
+ route is still here: refuse it at your gateway and reach it through an
119
+ authenticated channel instead.
120
+
121
+ ## Auditing a deployment
122
+
123
+ `@byollm/conformance` ships a posture audit that holds nothing but a URL,
124
+ which is what an attacker has:
125
+
126
+ ```bash
127
+ npx byollm-audit-deployment https://your-relay.example
128
+ ```
129
+
130
+ It exists because eight of byollm_009's findings came from a suite in which
131
+ nothing was ever a stranger — the site had a reference to the relay object and
132
+ called it. A harness that invokes the system under test directly cannot see
133
+ anything about how the system is *reached*, and the ninth finding was in that
134
+ gap. Safe to run against production: nothing writes, nothing floods.
135
+
136
+ MIT
@@ -0,0 +1,416 @@
1
+ import { PublicIdentity, JobStub, SealedEnvelope } from '@byollm/protocol';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * The relay's routing state — byollm_009 §7, reachable at last.
6
+ *
7
+ * §7 described a state machine the direct plane could not produce. There, the
8
+ * site and the upstream are the same party: it seals when it likes, and a job
9
+ * is never claimed-but-unsealed. Here they are different parties, and the gap
10
+ * between them is a state:
11
+ *
12
+ * ```
13
+ * queued ──claim──▶ awaiting-payload ──sealed──▶ ready ──fetch──▶ running
14
+ * ▲ │ │
15
+ * └────────────────────┘ ▼
16
+ * site never seals, or seals too late ok | error | canceled
17
+ * ```
18
+ *
19
+ * The relay cannot seal, so it cannot shortcut this. A payload is encrypted
20
+ * to *the device that claimed it*, and nobody knows which device that is until
21
+ * the claim happens — which is precisely why claim-then-fetch makes a blind
22
+ * relay possible at all. The window is the price.
23
+ *
24
+ * ## What the relay holds, and what it cannot
25
+ *
26
+ * Stubs (metadata the site chose to publish), sealed envelopes it cannot open,
27
+ * and public keys. There is no field on any type in this file that could hold
28
+ * a private key or a plaintext, which is `RELAY_BLIND` expressed as a data
29
+ * model rather than as a policy.
30
+ */
31
+ /** Where a routed job is. */
32
+ type RoutedState = "queued" | "awaiting-payload" | "ready" | "running" | "done";
33
+ /**
34
+ * How long a site has to seal after one of its jobs is claimed.
35
+ *
36
+ * **Distinct from the lease, and distinct from the job's TTL** — byollm_009
37
+ * §7.1. Three clocks, three different questions:
38
+ *
39
+ * - the **TTL** asks how long the work is worth doing at all;
40
+ * - the **lease** asks how long this device gets to run it;
41
+ * - this asks how long we wait for a site that has gone away.
42
+ *
43
+ * Collapsing any pair of them looks harmless until a site restarts during a
44
+ * deploy: with only a lease, the device sits politely holding a job whose
45
+ * payload will never arrive, and the lease's whole minute is spent waiting on
46
+ * a party that is not coming back. Short, because a site that is up answers in
47
+ * milliseconds and a site that is down will not answer sooner for waiting.
48
+ */
49
+ declare const AWAITING_PAYLOAD_MS = 10000;
50
+ /** A job the relay is routing. Metadata and ciphertext, nothing else. */
51
+ interface RoutedJob {
52
+ readonly id: string;
53
+ /** Which site enqueued it — the party that will be asked to seal. */
54
+ readonly siteId: string;
55
+ /**
56
+ * Everything the relay knows about the work, which is everything the site
57
+ * chose to publish and not one field more (byollm_009 §6).
58
+ */
59
+ readonly stub: JobStub;
60
+ state: RoutedState;
61
+ /** Set from the claim; the site seals to these keys. */
62
+ claimedBy?: {
63
+ readonly runnerId: string;
64
+ readonly owner: string;
65
+ readonly device: PublicIdentity;
66
+ readonly leaseId: string;
67
+ readonly leaseExpiresAt: number;
68
+ };
69
+ /** When {@link AWAITING_PAYLOAD_MS} runs out for this claim. */
70
+ awaitingUntil?: number;
71
+ /** Sealed to the claiming device by the site. Opaque here. */
72
+ payload?: SealedEnvelope;
73
+ /** Sealed to the site by the device. Opaque here. */
74
+ result?: SealedEnvelope;
75
+ /**
76
+ * The result's clear-text discriminator — byollm_009 §6.1.
77
+ *
78
+ * The one outcome fact the relay is given, and the reason it is given:
79
+ * without it the relay cannot stop dispatching a finished job. A routing
80
+ * hint and never a fact — the *site* verifies it against the sealed
81
+ * outcome, because only the site can open the envelope. The relay acts on
82
+ * it and is entitled to be wrong; a lying daemon costs it a dispatch
83
+ * decision, not a security property.
84
+ */
85
+ disposition?: "ok" | "error" | "canceled";
86
+ }
87
+ /** A device the relay has seen recently. */
88
+ interface Presence {
89
+ readonly runnerId: string;
90
+ readonly owner: string;
91
+ readonly device: PublicIdentity;
92
+ lastSeenAt: number;
93
+ /** Set on revocation so the next request is refused rather than routed. */
94
+ revoked: boolean;
95
+ }
96
+ /**
97
+ * In-memory routing state.
98
+ *
99
+ * Deliberately not durable. The skeleton proves the protocol, and the
100
+ * production hub replaces this with the closed multi-tenant router behind the
101
+ * same shape (cloud_004 §9). Anything a restart loses here is a job that
102
+ * returns to its site's queue — which is the behaviour a lapsed lease already
103
+ * has to produce, so nothing new needs to be true for this to be safe.
104
+ */
105
+ declare class RelayState {
106
+ #private;
107
+ /**
108
+ * Take a stub for routing. The payload is not here and will not be.
109
+ *
110
+ * **Idempotent by job id, and that is a security property rather than a
111
+ * convenience.** Site-plane calls are authenticated by signature, and
112
+ * byollm_009 §4.2's argument for signing the request instead of a
113
+ * server-issued nonce rests entirely on every write being idempotent per the
114
+ * instance it names. This one was not: re-enqueueing a known id built a
115
+ * fresh `queued` job over the top of the old one, discarding a live claim,
116
+ * its lease and any payload the site had already sealed to a device. A
117
+ * replayed enqueue inside the two-minute freshness window was therefore a
118
+ * way to yank a job back from the machine running it — the `release` bug of
119
+ * §4.2, rediscovered on the other plane.
120
+ *
121
+ * So a known id returns what is already routing, unchanged. A site that
122
+ * restarts and republishes its queue is the normal case, and it must not
123
+ * disturb work in flight.
124
+ */
125
+ enqueue(input: {
126
+ id: string;
127
+ siteId: string;
128
+ stub: JobStub;
129
+ }): RoutedJob;
130
+ job(jobId: string): RoutedJob | undefined;
131
+ jobs(): RoutedJob[];
132
+ /** Jobs a site must seal for, right now. */
133
+ awaiting(siteId: string): RoutedJob[];
134
+ /** Sealed results waiting to go home. */
135
+ finished(siteId: string): RoutedJob[];
136
+ seen(presence: Omit<Presence, "revoked">): Presence;
137
+ presence(runnerId: string): Presence | undefined;
138
+ everyone(): Presence[];
139
+ /**
140
+ * Return a job to the queue, forgetting the claim.
141
+ *
142
+ * The stub survives; nothing is lost. That is `LEASE_RECLAIMABLE` and it is
143
+ * why the awaiting-payload timeout is cheap to fire: the worst case is that
144
+ * a device did nothing for ten seconds and another one gets a turn.
145
+ */
146
+ requeue(job: RoutedJob): void;
147
+ /**
148
+ * Fire whatever the clock says is due, and report it.
149
+ *
150
+ * Returns the jobs it requeued so a caller can log or surface them — a
151
+ * timeout that fires invisibly is indistinguishable from a job that was
152
+ * never claimed, and those want very different debugging.
153
+ */
154
+ sweep(now: number): RoutedJob[];
155
+ }
156
+
157
+ declare function debugPage(state: RelayState, now: number): string;
158
+
159
+ /**
160
+ * What the relay is told about the world — cloud_004 §14.
161
+ *
162
+ * The relay decides nothing about who may talk to whom. It is handed a
163
+ * projection of the control plane and routes according to it. Today that
164
+ * projection is a file; later it is whatever the suite serves. Either way the
165
+ * relay's own state is derived and disposable: delete it and the fixture
166
+ * rebuilds it.
167
+ *
168
+ * ## This shape is a contract, not a test convenience
169
+ *
170
+ * cloud_004 §14 flags it and the flag is worth repeating here, where someone
171
+ * will be tempted to add a field: **this is the projection contract.** The
172
+ * first real control plane will be written to produce whatever this says, and
173
+ * a field added carelessly now is a field the suite must produce forever.
174
+ *
175
+ * So two rules for anything added later:
176
+ *
177
+ * 1. **It must be something a control plane can actually know.** The relay
178
+ * cannot be given facts that only a daemon or only a site holds — that is
179
+ * how a blind relay stops being blind, one convenient field at a time.
180
+ * 2. **It must be a decision, not a derivation.** Consent is a decision.
181
+ * Presence is not: the relay learns that from heartbeats. Anything the
182
+ * relay can observe does not belong in the projection.
183
+ *
184
+ * ## What is deliberately absent
185
+ *
186
+ * No private keys, of any party, ever. The relay holds public keys so it can
187
+ * *verify* signatures and *tell a site who to seal to*. It holds no key that
188
+ * can open anything, and {@link RelayFixture} has no field where one could be
189
+ * put — `RELAY_BLIND` as a type, not as a promise.
190
+ */
191
+ /**
192
+ * A site the control plane registered and domain-verified — cloud_004 §5.
193
+ *
194
+ * **The one authority for a site's public identity.** It used to be inlined on
195
+ * every consent record, which meant a site's key had as many homes as it had
196
+ * users and nothing checked they agreed — the exact shape this project has now
197
+ * found in a version constant, a clock read, an envelope deadline, a reseal
198
+ * implementation, a package list and a docs page. Consents now reference a
199
+ * site by id and the key is looked up here.
200
+ *
201
+ * The relay needs it for two things it cannot do without:
202
+ *
203
+ * 1. **Telling a daemon who to pin** at pairing — the key that makes relayed
204
+ * work unforgeable, since the relay holds no key that could produce it.
205
+ * 2. **Authenticating the site plane.** A site calls a relay the way a daemon
206
+ * does, signing with this identity, and this is the key those signatures
207
+ * are checked against.
208
+ */
209
+ declare const SiteRecord: z.ZodObject<{
210
+ siteId: z.ZodString;
211
+ site: z.ZodObject<{
212
+ identity: z.ZodString;
213
+ encryption: z.ZodString;
214
+ encryptionSig: z.ZodString;
215
+ }, z.core.$strict>;
216
+ }, z.core.$strict>;
217
+ type SiteRecord = z.infer<typeof SiteRecord>;
218
+ /**
219
+ * A user's decision to let one site use their compute — cloud_004 §3.
220
+ *
221
+ * `CONSENT_BEFORE_ROUTE`: with no record here, the relay refuses to route,
222
+ * and there is no discovery path that creates one. Consent is a click in the
223
+ * control plane; the relay only ever reads the result.
224
+ */
225
+ declare const ConsentRecord: z.ZodObject<{
226
+ owner: z.ZodString;
227
+ siteId: z.ZodString;
228
+ }, z.core.$strict>;
229
+ type ConsentRecord = z.infer<typeof ConsentRecord>;
230
+ /**
231
+ * A named group whose members may use a shared machine — cloud_004 §11.
232
+ *
233
+ * The roster lives here and **never reaches a site**. A site learns whether a
234
+ * consenting user has reachable compute; it never learns who else is on the
235
+ * roster. That is `ROSTERS_NEVER_LEAK` in cloud_004 §11.4, and the reason
236
+ * this type has no outbound representation anywhere in this package.
237
+ */
238
+ declare const RosterRecord: z.ZodObject<{
239
+ id: z.ZodString;
240
+ owner: z.ZodString;
241
+ members: z.ZodArray<z.ZodString>;
242
+ }, z.core.$strict>;
243
+ type RosterRecord = z.infer<typeof RosterRecord>;
244
+ /**
245
+ * A device its owner has approved — cloud_005 §7.1.
246
+ *
247
+ * The relay refuses a device that is not here, and that refusal is the point.
248
+ * byollm_009's seventh finding stopped a daemon from *naming* itself; this
249
+ * stops it from *keying* itself. A device that presents keys nobody approved
250
+ * is a device whose owner never saw a fingerprint, and pairing it would make
251
+ * the relay the authority on identity — which is exactly what it must not be.
252
+ *
253
+ * The three-party shape consent already has, applied to identity: the device
254
+ * asserts, a human confirms in the control plane, the relay checks.
255
+ */
256
+ declare const DeviceRecord: z.ZodObject<{
257
+ owner: z.ZodString;
258
+ runnerId: z.ZodString;
259
+ device: z.ZodObject<{
260
+ identity: z.ZodString;
261
+ encryption: z.ZodString;
262
+ encryptionSig: z.ZodString;
263
+ }, z.core.$strict>;
264
+ }, z.core.$strict>;
265
+ type DeviceRecord = z.infer<typeof DeviceRecord>;
266
+ /** A revoked route, named by its parts. */
267
+ declare const RevocationRecord: z.ZodObject<{
268
+ owner: z.ZodString;
269
+ siteId: z.ZodString;
270
+ }, z.core.$strict>;
271
+ type RevocationRecord = z.infer<typeof RevocationRecord>;
272
+ declare const RelayFixture: z.ZodObject<{
273
+ sites: z.ZodDefault<z.ZodArray<z.ZodObject<{
274
+ siteId: z.ZodString;
275
+ site: z.ZodObject<{
276
+ identity: z.ZodString;
277
+ encryption: z.ZodString;
278
+ encryptionSig: z.ZodString;
279
+ }, z.core.$strict>;
280
+ }, z.core.$strict>>>;
281
+ consents: z.ZodArray<z.ZodObject<{
282
+ owner: z.ZodString;
283
+ siteId: z.ZodString;
284
+ }, z.core.$strict>>;
285
+ devices: z.ZodDefault<z.ZodArray<z.ZodObject<{
286
+ owner: z.ZodString;
287
+ runnerId: z.ZodString;
288
+ device: z.ZodObject<{
289
+ identity: z.ZodString;
290
+ encryption: z.ZodString;
291
+ encryptionSig: z.ZodString;
292
+ }, z.core.$strict>;
293
+ }, z.core.$strict>>>;
294
+ rosters: z.ZodDefault<z.ZodArray<z.ZodObject<{
295
+ id: z.ZodString;
296
+ owner: z.ZodString;
297
+ members: z.ZodArray<z.ZodString>;
298
+ }, z.core.$strict>>>;
299
+ revoked: z.ZodDefault<z.ZodArray<z.ZodObject<{
300
+ owner: z.ZodString;
301
+ siteId: z.ZodString;
302
+ }, z.core.$strict>>>;
303
+ }, z.core.$strict>;
304
+ type RelayFixture = z.infer<typeof RelayFixture>;
305
+ /** An empty projection: nothing consented, so nothing routes. */
306
+ declare const EMPTY_FIXTURE: RelayFixture;
307
+ /**
308
+ * The relay's read-only view of the projection.
309
+ *
310
+ * Deliberately a handful of questions rather than the raw fixture: every
311
+ * caller asking "may this route?" through one method is what makes
312
+ * `CONSENT_BEFORE_ROUTE` reviewable, and it leaves room for the projection to
313
+ * become a service without touching a single call site.
314
+ */
315
+ declare class Projection {
316
+ #private;
317
+ constructor(fixture?: RelayFixture);
318
+ /** Replace the projection wholesale — the control plane pushed a new one. */
319
+ replace(fixture: RelayFixture): void;
320
+ /**
321
+ * The site this id names, if the control plane registered it.
322
+ *
323
+ * The only source of a site's public identity in this package. Everything
324
+ * that pins, verifies or seals to a site starts here.
325
+ */
326
+ siteFor(siteId: string): SiteRecord | null;
327
+ /**
328
+ * The device this runner id names, if a human approved it.
329
+ *
330
+ * Returns null for a device the control plane does not know, which is how
331
+ * the relay refuses to be the authority on identity.
332
+ */
333
+ deviceFor(runnerId: string): DeviceRecord | null;
334
+ /** The device approved for these exact keys, if any. */
335
+ deviceByFingerprint(identityPublic: string): DeviceRecord | null;
336
+ /** The consent binding this owner to this site, if it exists and stands. */
337
+ consentFor(owner: string, siteId: string): ConsentRecord | null;
338
+ /**
339
+ * May this device's owner run work belonging to `jobOwner`?
340
+ *
341
+ * The relay's half of `AUDIENCE_BOTH_SIDES`. It is only ever a *narrowing*:
342
+ * the daemon re-checks its own allowlist locally and may still refuse, and
343
+ * the site's audience already bounded who could be offered the job. A relay
344
+ * that answered `true` for everyone would not widen anything — which is
345
+ * exactly the property that lets it be blind.
346
+ */
347
+ mayRunFor(deviceOwner: string, jobOwner: string): boolean;
348
+ }
349
+
350
+ /**
351
+ * `@byollm/relay` — the reference relay (cloud_004 §14).
352
+ *
353
+ * A blind relay between byollm sites and daemons: it routes stubs, hands over
354
+ * sealed envelopes it cannot open, and knows who is online. It is the first
355
+ * consumer of byollm_009's session layer that is neither the site nor the
356
+ * device, which makes it the thing that proves the protocol's central claim.
357
+ *
358
+ * ## Why this ships open
359
+ *
360
+ * It is the conformance kit's reference relay, and the kit is public — so it
361
+ * starts where it ends rather than being written closed and ported. A relay
362
+ * that claims to be blind should be readable by the people trusting it, and a
363
+ * third-party daemon testing hub mode should test against real code rather
364
+ * than a mock of it. The production hub — multi-tenant routing, presence at
365
+ * scale, billing, ops — is built on these same interfaces and is not this.
366
+ *
367
+ * ## Blind by construction, not by policy
368
+ *
369
+ * {@link RelayOptions} has no field that can hold a private key, and no type
370
+ * in this package has one either. `RELAY_BLIND` is therefore not a rule the
371
+ * code follows; it is a shape the code has. The only way to make this relay
372
+ * able to read a payload is to change its types, which is a review someone
373
+ * would have to justify rather than a line someone could slip in.
374
+ */
375
+ interface RelayOptions {
376
+ /**
377
+ * Which site this relay routes for.
378
+ *
379
+ * One, in the skeleton. Multi-tenant routing is the closed piece
380
+ * (cloud_004 §9), and it replaces this field rather than extending it.
381
+ */
382
+ readonly siteId: string;
383
+ /** Consent and rosters, projected from the control plane. */
384
+ readonly fixture?: RelayFixture;
385
+ /** How long a claim is good for. */
386
+ readonly leaseMs?: number;
387
+ /** Injectable clock, so tests move time instead of sleeping. */
388
+ readonly now?: () => number;
389
+ /** Where the daemon plane is mounted. */
390
+ readonly basePath?: string;
391
+ }
392
+ /** A running relay: one fetch handler, two planes, one debug page. */
393
+ declare class Relay {
394
+ #private;
395
+ readonly state: RelayState;
396
+ readonly projection: Projection;
397
+ constructor(options: RelayOptions);
398
+ /** Replace the projection — a control-plane push, or a fixture edit. */
399
+ project(fixture: RelayFixture): void;
400
+ /**
401
+ * Fire due timers and report what moved.
402
+ *
403
+ * Exposed rather than run on an interval so a test can drive it, and so the
404
+ * production hub can decide its own scheduling. The relay never needs a
405
+ * timer to be *correct* — every read path sweeps first — but a job whose
406
+ * site vanished should return to the queue without waiting for someone to
407
+ * ask about it.
408
+ */
409
+ sweep(): {
410
+ requeued: string[];
411
+ };
412
+ /** The whole HTTP surface. */
413
+ handle(request: Request): Promise<Response>;
414
+ }
415
+
416
+ 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 };