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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,173 @@
1
- import { JobKind, DeliveredResult, Endpoint, Capability } from '@byollm/protocol';
2
- import { P as PollingDeliveryDeps, R as ResultDelivery, W as WaitOptions } from './delivery-36nIe-b3.js';
3
- export { N as NoRunnerAvailableError, a as PollingDelivery, b as ResultTimeoutError } from './delivery-36nIe-b3.js';
4
- import { B as ByollmStore, E as EnqueueInput, J as JobRecord, R as RunnerRecord, C as ClaimArgs, a as RenewArgs, b as RenewResult, c as CompleteArgs, d as CompleteResult, e as ReleaseArgs, P as PairingRecord, A as ApproveArgs, T as TouchArgs } from './store-D23N6iiP.js';
5
- export { f as JobStore, g as RunnerStore } from './store-D23N6iiP.js';
6
- import { H as HandlerConfig } from './handlers-D7lWfwno.js';
7
- export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-D7lWfwno.js';
1
+ import { StoredKeys, JobKind, Audience, DeliveredResult, Endpoint, Capability } from '@byollm/protocol';
2
+ import { P as PollingDeliveryDeps, R as ResultDelivery, W as WaitOptions } from './delivery-C5mz_Ykm.js';
3
+ export { N as NoRunnerAvailableError, a as PollingDelivery, b as ResultTimeoutError } from './delivery-C5mz_Ykm.js';
4
+ import { B as ByollmStore, J as JobRecord, E as EnqueueInput, R as RunnerRecord, S as StoredJobInput, C as ClaimArgs, a as RenewArgs, b as RenewResult, A as AdoptArgs, c as CompleteArgs, d as CompleteResult, e as ReleaseArgs, P as PairingRecord, f as ApproveArgs, T as TouchArgs } from './store-Cx2_bck1.js';
5
+ export { g as CompleteHolder, h as JobStore, i as RunnerStore } from './store-Cx2_bck1.js';
6
+ import { H as HandlerConfig } from './handlers-CTV3Jc6Q.js';
7
+ export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-CTV3Jc6Q.js';
8
+
9
+ /**
10
+ * The cloud lane — cloud_004 §9.4.
11
+ *
12
+ * `app.enqueue(...)` is identical in every lane; the lane picks the connection
13
+ * plane. In `direct` mode a daemon reaches the site's own handlers. In `cloud`
14
+ * mode it reaches a relay instead, and the site's side of that is this file.
15
+ *
16
+ * ## What actually changes, and what deliberately does not
17
+ *
18
+ * Enqueue does not change at all. The job is validated, sealed at rest to the
19
+ * site's own key and stored, exactly as before — jobs-at-rest encryption is a
20
+ * direct-mode property that the cloud lane inherits rather than replaces.
21
+ *
22
+ * What changes is *who asks for the payload and when*. On the direct plane the
23
+ * daemon asks, and the site answers synchronously because it is the upstream.
24
+ * Through a relay the site is not the upstream, so nobody asks: the site has to
25
+ * find out that a device claimed its job, and seal to that device. Hence a
26
+ * pump rather than a handler.
27
+ *
28
+ * ```
29
+ * enqueue ──stub──▶ relay (payload stays here, sealed at rest)
30
+ * │
31
+ * pump ◀──who claimed it, and what key?
32
+ * ──payload sealed to that device──▶
33
+ * pump ◀──sealed result── ──▶ store.complete → the app's delivery channel
34
+ * ```
35
+ *
36
+ * ## Why the site polls
37
+ *
38
+ * Everything in this product is outbound. A relay that called site webhooks
39
+ * would need every site publicly reachable, which is the connectivity problem
40
+ * the hub exists to remove — and a serverless site has nowhere to receive a
41
+ * webhook anyway. So the site polls, exactly as a daemon does.
42
+ */
43
+ interface CloudLaneOptions {
44
+ /** Where the relay lives, e.g. `https://relay.byollm.cloud`. */
45
+ readonly relayOrigin: string;
46
+ /** This site's id at the relay. */
47
+ readonly siteId: string;
48
+ /** Injectable fetch, for tests and for proxies. */
49
+ readonly fetch?: typeof fetch;
50
+ }
51
+ /** What one pump cycle did, for logging and for tests. */
52
+ /**
53
+ * A relay that could not answer this request — alpha.31.
54
+ *
55
+ * `retryable` is the whole point: a draining pod and a bad signature are both
56
+ * failures, and treating them alike is how a site either falls over on every
57
+ * deploy or stays silently disconnected for a week.
58
+ */
59
+ declare class RelayUnavailable extends Error {
60
+ readonly retryable: boolean;
61
+ /** The protocol's own code, when the relay sent one. */
62
+ readonly code: string;
63
+ constructor(message: string, retryable: boolean, code: string);
64
+ }
65
+ /**
66
+ * The job was not queued, and waiting will not change that.
67
+ *
68
+ * Distinct from {@link RelayUnavailable} because it is the opposite situation:
69
+ * the relay answered, promptly and correctly, and the answer is that this job
70
+ * has nowhere to go. Catching "the relay is down" to handle "nobody has chosen
71
+ * a model" would retry forever against a fact.
72
+ *
73
+ * Two codes, and they belong to two different people.
74
+ *
75
+ * `purpose-not-declared` is the site's own manifest. It names the purpose and
76
+ * the remedy, because a developer reading their own logs is entitled to both
77
+ * and neither says anything about a person.
78
+ *
79
+ * `slot-unsatisfiable` is the person's own dashboard, and says only that.
80
+ * Which service, whose device, whether one exists at all — none of it travels,
81
+ * and the sentence is the same for everybody. A site learns *that* a slot
82
+ * cannot be satisfied, which is exactly what the README has always promised
83
+ * and what this class finally delivers.
84
+ */
85
+ declare class EnqueueRefused extends Error {
86
+ /** `purpose-not-declared` or `slot-unsatisfiable`. */
87
+ readonly code: string;
88
+ constructor(message: string, code: string);
89
+ }
90
+ interface PumpReport {
91
+ /** Jobs sealed to a claiming device this cycle. */
92
+ readonly sealed: string[];
93
+ /** Results opened, verified and written to the store. */
94
+ readonly completed: string[];
95
+ /**
96
+ * Jobs the relay offered that this site refused to seal for.
97
+ *
98
+ * Never silent: a site that cannot open its own at-rest envelope has a key
99
+ * problem, and a device waiting on a payload that will never come is
100
+ * exactly the case `awaiting-payload` exists to bound.
101
+ */
102
+ readonly refused: string[];
103
+ /**
104
+ * Why this cycle stopped early, when it did — alpha.31.
105
+ *
106
+ * A relay can legitimately say "ask me later": a pod draining through its
107
+ * `preStop` window answers `503 not-ready` to every routed call, and that
108
+ * happens on **every deploy**. Before this existed the lane read the body
109
+ * of that answer, found no `jobs` in it, and threw `TypeError: finished.jobs
110
+ * is not iterable` — a site falling over because its relay was polite.
111
+ *
112
+ * Absent on an ordinary cycle. Present, with the reason, when the lane
113
+ * deferred: a site that quietly did nothing and a site that was told to wait
114
+ * must not look the same in a log.
115
+ */
116
+ readonly deferred?: string;
117
+ }
118
+ declare class CloudLane {
119
+ #private;
120
+ constructor(deps: {
121
+ options: CloudLaneOptions;
122
+ store: ByollmStore;
123
+ siteKeys: StoredKeys;
124
+ now: () => number;
125
+ });
126
+ /**
127
+ * Publish a job's stub for routing.
128
+ *
129
+ * The stub and nothing else — byollm_009 §6 makes that exhaustive by
130
+ * construction, so this cannot leak a payload even by mistake: there is no
131
+ * field on `JobStub` to put one in.
132
+ */
133
+ publish(record: JobRecord): Promise<void>;
134
+ /**
135
+ * Withdraw a job at the relay — cloud_008 §2.2.
136
+ *
137
+ * `app.cancel()` marks the site's own row terminal, which stops the *next*
138
+ * seal. It cannot stop a device that is already running the work, because
139
+ * on this lane the site is not the upstream: only the relay talks to the
140
+ * daemon, and it answered `cancel: []` unconditionally.
141
+ *
142
+ * So the cancellation has to travel. The relay marks the job, stops
143
+ * offering it, and names it to the holding device at its next heartbeat —
144
+ * the same path the direct plane has always had, arriving one hop later.
145
+ */
146
+ cancel(jobId: string): Promise<void>;
147
+ /**
148
+ * One cycle: seal for anything claimed, collect anything finished.
149
+ *
150
+ * Idempotent and safe to call as often as you like. Exposed as a single
151
+ * cycle rather than hidden behind a timer so a caller decides its own
152
+ * cadence — a serverless site runs it on a cron, a long-lived one on an
153
+ * interval, and a test runs it exactly when it means to.
154
+ */
155
+ pump(): Promise<PumpReport>;
156
+ }
8
157
 
9
158
  /** Why a job cannot presently run. */
10
- type NoRunnerReason = "no-runner-paired" | "no-runner-online" | "no-matching-capability" | "audience-admits-nobody";
159
+ type NoRunnerReason = "no-runner-paired" | "no-runner-online" | "no-matching-capability" | "audience-admits-nobody"
160
+ /**
161
+ * The owner's default for this kind can never serve *this* requester —
162
+ * byollm_016's defaults-meet-audiences corner.
163
+ *
164
+ * The specimen: a default of `claude-cli`, self-locked by
165
+ * `SUBSCRIPTION_SELF_LOCK`, and a team member's unselected job. It resolves
166
+ * to something that will never run it. Reported rather than left to time
167
+ * out, because a wait that can never end is indistinguishable from one that
168
+ * has not ended yet, and only one of them is worth waiting through.
169
+ */
170
+ | "default-unusable";
11
171
  /**
12
172
  * The no-runner signal (byollm_001 Rev 1 §D).
13
173
  *
@@ -26,7 +186,7 @@ interface RunnerAvailability {
26
186
  interface AvailabilityQuery {
27
187
  readonly kind: JobKind;
28
188
  readonly owner: string;
29
- readonly audience?: "self" | "named" | "public";
189
+ readonly audience?: Audience;
30
190
  readonly audienceAllow?: readonly string[];
31
191
  }
32
192
  interface ByollmAppOptions {
@@ -45,6 +205,23 @@ interface ByollmAppOptions {
45
205
  * gives up. Longer tolerates a daemon restarting; shorter fails faster.
46
206
  */
47
207
  readonly noRunnerGraceMs?: number;
208
+ /**
209
+ * This site's keypairs — the same ones the handlers use.
210
+ *
211
+ * The app needs them because it is the *endpoint*: it seals work on the way
212
+ * in and opens results on the way out. Nothing between those two points
213
+ * holds plaintext (byollm_009 §10).
214
+ */
215
+ readonly siteKeys: StoredKeys;
216
+ /**
217
+ * Which connection plane this site uses — cloud_004 §9.4.
218
+ *
219
+ * Omitted means `direct`: a daemon reaches this site's own handlers, and
220
+ * everything works as it always has. Supplying a relay switches the plane
221
+ * and nothing else — `enqueue` is identical in every lane, which is the
222
+ * property that lets the same app move between them by config.
223
+ */
224
+ readonly lane?: CloudLaneOptions;
48
225
  }
49
226
  /**
50
227
  * An enqueued job, with the delivery channel attached.
@@ -62,16 +239,10 @@ interface JobHandle {
62
239
  /** Ask the runner to stop. */
63
240
  cancel(): Promise<void>;
64
241
  }
65
- /**
66
- * The app-facing half of `@byollm/server`.
67
- *
68
- * The daemon talks to {@link ByollmHandlers}; the app talks to this. Keeping
69
- * them separate is what makes "one door per state write" hold — an app
70
- * enqueues and cancels through these methods and never writes job rows by
71
- * hand.
72
- */
73
242
  declare class ByollmApp {
74
243
  #private;
244
+ /** Present only in the cloud lane; the site's side of the relay. */
245
+ readonly cloud: CloudLane | undefined;
75
246
  constructor(options: ByollmAppOptions);
76
247
  /**
77
248
  * Enqueue a job.
@@ -80,7 +251,7 @@ declare class ByollmApp {
80
251
  * result comes back marked untrusted (see {@link ByollmApp.result}), and
81
252
  * the app is obliged to disclose that to whoever reads it.
82
253
  */
83
- enqueue(input: EnqueueInput): Promise<JobHandle>;
254
+ enqueue<K extends JobKind>(input: EnqueueInput<K>): Promise<JobHandle>;
84
255
  /** Read a job's current state. */
85
256
  job(jobId: string): Promise<JobRecord | null>;
86
257
  /**
@@ -89,7 +260,7 @@ declare class ByollmApp {
89
260
  * Check `provenance.untrusted` before rendering. It is true for every
90
261
  * `named`/`public` job, because that text came from someone else's machine
91
262
  * and the app must not present it as its own AI's answer
92
- * ({@link MUSTS.RESULT_PROVENANCE}).
263
+ * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).
93
264
  */
94
265
  result(jobId: string): Promise<DeliveredResult | null>;
95
266
  /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
@@ -139,22 +310,79 @@ declare class ByollmApp {
139
310
  */
140
311
  declare function normalizeUserCode(input: string): string;
141
312
 
142
- /** Pull the endpoint name out of a URL path, or null if it isn't ours. */
143
- declare function routeEndpoint(pathname: string): Endpoint | null;
144
- /** Read the bearer token from an `Authorization` header. */
145
- declare function bearerFrom(header: string | null): string | undefined;
313
+ /**
314
+ * Pull the endpoint name out of a URL path, or null if it isn't ours.
315
+ *
316
+ * The full path must match `<basePath>/<endpoint>` exactly. This used to
317
+ * compare only the *last* segment, which meant `/anything/at/all/claim`
318
+ * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it
319
+ * appeared in a 404 message and was never matched against. For the handler
320
+ * that serves claim, result and heartbeat, dispatching on a suffix is a
321
+ * looser rule than anyone reading the constant would assume, and loose
322
+ * matching in a security surface should at least be a decision.
323
+ *
324
+ * The cost is that the mount point is now something a deployment has to state
325
+ * rather than something that works by accident. That is the intended trade:
326
+ * a 404 at startup naming the mount point beats a handler answering on paths
327
+ * nobody meant to expose.
328
+ */
329
+ declare function routeEndpoint(pathname: string, basePath?: string): Endpoint | null;
330
+ /**
331
+ * Read the request signature from headers (byollm_009 §4.2).
332
+ *
333
+ * In headers rather than the body so the signature covers the body whole,
334
+ * with no field to exclude from its own hash — a scheme that signs a body
335
+ * minus one field has to agree, byte for byte, on how that field is removed.
336
+ */
337
+ declare function signatureFrom(headers: Headers): unknown;
146
338
  /**
147
339
  * A `Request` → `Response` handler for the whole protocol.
148
340
  *
149
341
  * Web-standard types, so this works unchanged in Next.js route handlers, Hono,
150
342
  * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.
151
343
  */
152
- declare function createFetchHandler(config: HandlerConfig): (request: Request) => Promise<Response>;
344
+ declare function createFetchHandler(config: HandlerConfig & {
345
+ /**
346
+ * Where these endpoints are mounted. Defaults to
347
+ * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere.
348
+ */
349
+ readonly basePath?: string;
350
+ }): (request: Request) => Promise<Response>;
351
+
352
+ /**
353
+ * A site's keypairs — byollm_009 §5.
354
+ *
355
+ * **Generate once, store, supply.** Not at startup, and not per process.
356
+ *
357
+ * A site is usually more than one process: several instances behind a load
358
+ * balancer, or a serverless function whose module is evaluated per cold
359
+ * start. Keys generated at startup would give each of those a different
360
+ * identity. A daemon pins whichever one approved its pairing, and then every
361
+ * request routed to a different instance fails a signature check with nothing
362
+ * in the error explaining why — a failure that appears only under
363
+ * horizontal scale, which is to say only in production.
364
+ *
365
+ * So the library takes keys as an input and never invents them. That is the
366
+ * whole reason this module is three functions rather than a lazy singleton.
367
+ */
368
+ /** Make a fresh site identity. Call this once, ever, and keep the result. */
369
+ declare const generateSiteKeys: (now?: number) => StoredKeys;
370
+ /**
371
+ * Read site keys from an environment variable holding base64 JSON.
372
+ *
373
+ * The shape a deployment actually wants: one opaque secret, set the way every
374
+ * other secret is set, with no file to mount and no key material in the
375
+ * repository.
376
+ *
377
+ * @throws with a message naming the variable and the fix, because this fails
378
+ * at boot and the person reading the log is the person who can fix it.
379
+ */
380
+ declare function siteKeysFromEnv(variable?: string, env?: NodeJS.ProcessEnv): StoredKeys;
381
+ /** What to print from `keygen`: the secret to store, and how to check it. */
382
+ declare function formatSiteKeys(keys: StoredKeys): string;
153
383
 
154
384
  /** A device code: the secret the daemon polls with. Never shown to a user. */
155
385
  declare function generateDeviceCode(): string;
156
- /** A runner bearer token. */
157
- declare function generateRunnerToken(): string;
158
386
  /** A runner id. */
159
387
  declare function generateRunnerId(): string;
160
388
  /** A job id. */
@@ -193,23 +421,27 @@ interface MemoryStoreOptions {
193
421
  declare class MemoryStore implements ByollmStore {
194
422
  #private;
195
423
  constructor(options?: MemoryStoreOptions);
196
- create(input: EnqueueInput, now: number): Promise<JobRecord>;
424
+ create(input: StoredJobInput, now: number): Promise<JobRecord>;
197
425
  get(jobId: string): Promise<JobRecord | null>;
198
426
  claim(args: ClaimArgs): Promise<JobRecord[]>;
199
427
  renewLeases(args: RenewArgs): Promise<RenewResult>;
428
+ adopt(args: AdoptArgs): Promise<JobRecord | null>;
200
429
  complete(args: CompleteArgs): Promise<CompleteResult>;
430
+ subscribe(jobId: string, onChange: () => void): () => void;
201
431
  release(args: ReleaseArgs): Promise<string[]>;
202
432
  expireDue(now: number): Promise<JobRecord[]>;
203
433
  cancel(jobId: string, now: number): Promise<JobRecord | null>;
204
434
  listClaimedBy(runnerId: string): Promise<JobRecord[]>;
205
- listCancelRequests(runnerId: string): Promise<string[]>;
435
+ listCancelRequests(runnerId: string): Promise<{
436
+ jobId: string;
437
+ leaseId: string;
438
+ }[]>;
206
439
  createPairing(record: PairingRecord): Promise<void>;
207
440
  getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null>;
208
441
  getPairingByUserCode(userCode: string): Promise<PairingRecord | null>;
209
442
  approvePairing(args: ApproveArgs): Promise<RunnerRecord>;
210
443
  denyPairing(userCode: string, _now: number): Promise<void>;
211
444
  consumePairingToken(deviceCodeHash: string): Promise<void>;
212
- getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null>;
213
445
  getRunner(runnerId: string): Promise<RunnerRecord | null>;
214
446
  touchRunner(args: TouchArgs): Promise<RunnerRecord | null>;
215
447
  revokeRunner(runnerId: string, now: number): Promise<void>;
@@ -220,4 +452,4 @@ declare class MemoryStore implements ByollmStore {
220
452
  /** The capability that would serve a kind, if any. */
221
453
  declare function capabilityFor(capabilities: readonly Capability[], kind: string): Capability | undefined;
222
454
 
223
- export { ApproveArgs, type AvailabilityQuery, ByollmApp, type ByollmAppOptions, ByollmStore, ClaimArgs, CompleteArgs, CompleteResult, EnqueueInput, HandlerConfig, type JobHandle, JobRecord, MemoryStore, type MemoryStoreOptions, type NoRunnerReason, PairingRecord, PollingDeliveryDeps, ReleaseArgs, RenewArgs, RenewResult, ResultDelivery, type RunnerAvailability, RunnerRecord, TouchArgs, WaitOptions, bearerFrom, capabilityFor, createFetchHandler, generateDeviceCode, generateJobId, generateRunnerId, generateRunnerToken, generateUserCode, hashSecret, normalizeUserCode, routeEndpoint, secretsMatch };
455
+ export { AdoptArgs, ApproveArgs, type AvailabilityQuery, ByollmApp, type ByollmAppOptions, ByollmStore, ClaimArgs, CloudLane, type CloudLaneOptions, CompleteArgs, CompleteResult, EnqueueInput, EnqueueRefused, HandlerConfig, type JobHandle, JobRecord, MemoryStore, type MemoryStoreOptions, type NoRunnerReason, PairingRecord, PollingDeliveryDeps, type PumpReport, RelayUnavailable, ReleaseArgs, RenewArgs, RenewResult, ResultDelivery, type RunnerAvailability, RunnerRecord, TouchArgs, WaitOptions, capabilityFor, createFetchHandler, formatSiteKeys, generateDeviceCode, generateJobId, generateRunnerId, generateSiteKeys, generateUserCode, hashSecret, normalizeUserCode, routeEndpoint, secretsMatch, signatureFrom, siteKeysFromEnv };