@byollm/server 0.1.0-alpha.57 → 0.1.0-alpha.58
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 +2 -2
- package/dist/{chunk-I4MSFVRA.js → chunk-FQXTIQDP.js} +2 -2
- package/dist/{chunk-I4MSFVRA.js.map → chunk-FQXTIQDP.js.map} +1 -1
- package/dist/{handlers-D1ni1D8r.d.ts → handlers-BvVqALXO.d.ts} +1 -1
- package/dist/index.d.ts +6 -35
- package/dist/index.js +42 -49
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +2 -2
- package/dist/next.js +1 -1
- package/dist/{store-DoUlJWaO.d.ts → store-DCgYMftk.d.ts} +17 -14
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +2 -2
- package/dist/supabase/index.js.map +1 -1
- package/package.json +2 -2
- package/supabase/migrations/20260827000000_job_purpose.sql +36 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.58`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npm install @byollm/server@alpha`.
|
|
5
5
|
>
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
> packages published and `@byollm/server` did not: a Sigstore
|
|
106
106
|
> transparency-log 409 on its provenance attestation. The workflow's
|
|
107
107
|
> "already published" guard correctly refuses to resume a partial publish,
|
|
108
|
-
> so `0.1.0-alpha.
|
|
108
|
+
> so `0.1.0-alpha.58` is that release, whole.
|
|
109
109
|
>
|
|
110
110
|
> If you run the Supabase adapter, `alpha.21` needs
|
|
111
111
|
> `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
|
|
@@ -349,7 +349,7 @@ var ByollmHandlers = class {
|
|
|
349
349
|
// byollm_016 Phase B. Present only when the site named one, and
|
|
350
350
|
// omitted rather than sent as undefined — the stub is `.strict()` and
|
|
351
351
|
// an explicit undefined is not the same as an absent key.
|
|
352
|
-
...job.
|
|
352
|
+
...job.purpose === void 0 ? {} : { purpose: job.purpose },
|
|
353
353
|
// Bucketed, not measured: an exact size is a stronger fingerprint
|
|
354
354
|
// than routing needs (byollm_009 §6).
|
|
355
355
|
sizeClass: job.sizeClass,
|
|
@@ -656,4 +656,4 @@ export {
|
|
|
656
656
|
signatureFrom,
|
|
657
657
|
createFetchHandler
|
|
658
658
|
};
|
|
659
|
-
//# sourceMappingURL=chunk-
|
|
659
|
+
//# sourceMappingURL=chunk-FQXTIQDP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ids.ts","../src/handlers.ts","../src/reseal.ts","../src/records.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n // A bare UUID, not a prefixed one.\n //\n // The app mints this now, because byollm_009 §6 binds the job id into the\n // envelope's signature — so the id must exist before the row does. A\n // `job_`-prefixed string is not a `uuid`, and the Supabase adapter's column\n // is, so the prefix would have made every enqueue fail there while passing\n // in memory. Ids are opaque to the protocol; the prefix was only ever\n // decoration.\n return randomUUID();\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n\n/**\n * A fresh id for one lease grant.\n *\n * Not a secret and not guessed at — a daemon is told its lease id in the claim\n * response. It exists to distinguish *this* grant from the next one over the\n * same job by the same runner, which is what stops a replayed release landing\n * on a lease the sender never meant.\n */\nexport const generateLeaseId = (): string => randomUUID();\n","import {\n FetchRequest,\n SealedOutcome,\n keyId,\n open,\n publicIdentityOf,\n type FetchResponse,\n RequestSignature,\n verifyRequest,\n verifyPublicIdentity,\n type StoredKeys,\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\n/**\n * What a transport must hand the handler to authenticate a call.\n *\n * `rawBody` is the exact bytes received, not a re-serialisation of the parsed\n * object: JSON.stringify does not round-trip byte-for-byte, and a signature\n * over re-serialised input verifies something the sender never signed.\n */\nexport interface AuthContext {\n readonly endpoint: string;\n readonly rawBody: string;\n readonly signature: unknown;\n}\n\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n /**\n * This site's keypairs (byollm_009 §5) — **supplied, never generated here.**\n *\n * A site is usually more than one process. Generating keys at startup would\n * work perfectly in development and fail only in production, silently: each\n * instance would have a different identity, a daemon would pin whichever\n * one approved its pairing, and every request routed to a different\n * instance would fail a signature check it had no way to explain. So this\n * is a required input, and there is a `keygen` script that produces one.\n */\n readonly siteKeys: StoredKeys;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n readonly #siteKeys: StoredKeys;\n /** This site's identity key id — Amendment A's `stub.site`. Derived once. */\n readonly #siteKeyId: string;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n // Fail at construction, not at the first pairing. A site whose keys are\n // malformed should not start and then refuse its users one at a time.\n if (!verifyPublicIdentity(publicIdentityOf(config.siteKeys))) {\n throw new Error(\n \"siteKeys are not internally consistent: the encryption key is not \" +\n \"signed by the identity key. Generate a fresh pair with \" +\n \"`npx @byollm/server keygen`.\",\n );\n }\n this.#siteKeys = config.siteKeys;\n this.#siteKeyId = keyId(publicIdentityOf(config.siteKeys).identity);\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param auth - the signature and the exact bytes it covers\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n auth: AuthContext,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(auth, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on — and since V1-2 it\n // travels as the refusal itself ({@link MUSTS.REVOCATION_HONORED}).\n //\n // It used to be answered with an empty site set, which the daemon\n // read as \"revoked\". That reading is gone: an empty set now means\n // \"nothing is consented right now\", because a projection can arrive\n // empty by accident and the daemon's response to revocation is to\n // delete its pairing. So the one call every daemon always makes — a\n // daemon with no working backend never claims — carries the\n // unambiguous version: 403 with `revoked`, which is a code and not an\n // inference.\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n );\n case \"fetch\":\n return this.#authed(auth, body, FetchRequest, this.#fetch.bind(this));\n case \"result\":\n return this.#authed(auth, body, ResultRequest, this.#result.bind(this));\n case \"release\":\n return this.#authed(\n auth,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: verify the\n * signature, reject a revoked runner, and parse the body.\n *\n * Authentication happens before schema validation so a stranger probing the\n * endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n auth: AuthContext,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(\"unauthorized\", \"this request is not signed\");\n }\n\n const runner = await this.#store.getRunner(signature.data.runnerId);\n if (!runner) {\n return fail(\"unauthorized\", \"this runner is not recognised\");\n }\n\n // Verified against the identity pinned when the user approved this\n // machine — not against anything the request carries. A signature that\n // authenticates itself authenticates nothing.\n const failure = verifyRequest({\n identityPublic: runner.device.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#now(),\n });\n if (failure !== null) {\n // Deliberately one message for both causes. Telling a caller whether\n // their clock or their key is wrong tells an attacker which half of a\n // forgery already works.\n return fail(\"unauthorized\", \"this request's signature is not valid\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n /**\n * Hand over the payload for a lease this runner holds — byollm_009 §6.\n *\n * The second half of claim-then-fetch. A claim answers with a stub, and the\n * work itself is collected separately by the device that took it, because a\n * payload can only be sealed once its recipient is known.\n *\n * Scoped to the lease, not the job: answering for whatever lease happens to\n * exist would hand the work to a runner whose grant had already been\n * superseded.\n */\n async #fetch(\n request: FetchRequest,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n const job = await this.#store.get(request.jobId);\n if (\n !job ||\n job.lease?.runnerId !== runner.id ||\n job.lease.id !== request.leaseId\n ) {\n // One answer for \"no such job\", \"not yours\" and \"a lease you no longer\n // hold\". A caller who is allowed to know already knows which.\n return fail(\"not-found\", \"no such lease on this job\");\n }\n // One implementation of open-and-reseal, shared with the cloud lane: the\n // deadline and key ids are bound into a signature, and two copies of a\n // bound value is the bug this codebase keeps finding.\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: { id: job.id, envelope: job.envelope, createdAt: job.createdAt },\n device: runner.device,\n });\n if (!resealed.ok) {\n return fail(\"server-error\", \"this job's payload could not be opened\");\n }\n return ok({ envelope: resealed.envelope } satisfies FetchResponse);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n // The machine must prove its encryption key belongs to the identity it\n // is presenting, before either is stored. Otherwise a caller could pair\n // a real identity with an encryption key it holds the secret for, and\n // read everything later sealed to that runner.\n if (!verifyPublicIdentity(request.device)) {\n return fail(\n \"bad-request\",\n \"the device's encryption key is not signed by the identity it was presented with\",\n );\n }\n\n await this.#store.createPairing({\n device: request.device,\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n collected: false,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n !pairing.collected &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n // Only on approval: a pending or denied poll learns nothing, so an\n // unapproved code cannot be used to enumerate a site's keys.\n //\n // One entry, because a direct site *is* one site — the same shape a\n // hub answers with rather than a special case (cloud_009 §5). The\n // daemon's lookup is one map read on every lane, which is what keeps\n // the two lanes one protocol.\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n audience: job.audience,\n owner: job.owner,\n // This site, named by its identity key id — Amendment A §A.3. The\n // daemon pinned this exact value at pairing, so it can check the stub\n // against the envelope it later opens rather than taking our word for\n // which site sent it. On this plane that is redundant, which is the\n // point: the direct and relayed stubs are the same shape, and a daemon\n // serving both cannot tell which upstream it is talking to.\n site: this.#siteKeyId,\n // byollm_016 Phase B. Present only when the site named one, and\n // omitted rather than sent as undefined — the stub is `.strict()` and\n // an explicit undefined is not the same as an absent key.\n ...(job.service === undefined ? {} : { service: job.service }),\n // Bucketed, not measured: an exact size is a stronger fingerprint\n // than routing needs (byollm_009 §6).\n sizeClass: job.sizeClass,\n // Reserved for byollm_006; no job declares it yet.\n streaming: false,\n // The stub's deadline bounds how long a captured envelope is worth\n // keeping, so it is always present — falling back to the TTL window\n // when the app named no absolute one.\n deadlineAt: deadlineFor(job, now),\n // `audienceAllow` is not sent — cloud_008 §0.2. The list stays on\n // `JobRecord`, where `claim` already filtered candidates with it; the\n // daemon's own allowlist is what decides `named` (byollm_001 Rev 1\n // §B) and always was.\n //\n // Removing it from `JobStub` did **not** make this line a type error.\n // A conditional spread is not excess-property-checked, so the field\n // would have gone on being sent to a daemon whose `.strict()` parse\n // now rejects the entire claim response — every daemon on the version\n // pair, refusing all work, for a field nobody read. Worth stating\n // where it happened: the schema is the contract, and the compiler\n // does not enforce it through a spread.\n // No fallback. A job returned from `claim` holds a lease by\n // definition, and synthesising one here would hand the daemon a lease\n // id the store has never heard of — every later release naming it\n // would silently match nothing. A store that returns an unleased job\n // has broken its contract, and this says so.\n lease: leaseOf(job),\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n // No revoked branch here any more — V1-2. A revoked runner is refused by\n // `#authed` before this handler is reached, on heartbeat as on every\n // other endpoint, because \"revoked\" and \"nothing consented right now\"\n // must not arrive as the same empty body.\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n // `renewed` is not reported back — cloud_008 §1.4b. The grants are still\n // extended; the daemon simply never read the list, and `lost` is the\n // signal it acts on.\n const { lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n leases: request.activeLeases,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n // A direct site has no disclosure of its own to go stale: consent to it\n // *is* the pairing, and withdrawing it empties the set above.\n awaitingConsent: [],\n cancel: [...cancel],\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n const outcome = await this.#openResult(request, runner);\n if (!outcome.ok) return outcome.failure;\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n // From the envelope the device signed, not from the request beside it\n // — cloud_008 §2.5. A daemon can no longer seal one answer and declare\n // it came from a different model.\n backendClass: outcome.value.ran.backendClass,\n model: outcome.value.ran.model,\n });\n\n const {\n accepted,\n duplicate,\n job: updated,\n } = await this.#store.complete({\n jobId: request.jobId,\n // Who is asking, for the duplicate answer only — §3.6. Authorisation\n // is `holder`, below, and still is.\n runnerId: runner.id,\n // The grant, not the runner — cloud_008 §1.4a. `CompleteHolder`'s own\n // docstring already called the lease \"the more exact check anyway\";\n // this plane simply had no lease id to give it until now.\n holder: { by: \"lease\", leaseId: request.leaseId },\n outcome: outcome.value.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n // Only when true — cloud_008 §3.6. Absent means \"not a duplicate\", and\n // an optional field that is always present is a required one wearing a\n // question mark.\n ...(duplicate === true ? { duplicate: true } : {}),\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n /**\n * Open a sealed result, or refuse it.\n *\n * The mirror of the daemon's `#openPayload`, and refuses for the same\n * reason: an outcome that does not verify against the device's pinned key is\n * an assertion by whoever relayed it, and storing it would let an\n * intermediary write answers into the app.\n *\n * The clear-text `disposition` is checked here rather than trusted. It is on\n * the wire so a relay can route without opening anything, which means the\n * one thing it must not be is authoritative — a daemon that sealed an error\n * and declared `ok` would otherwise have its declaration believed by\n * everything upstream of this line.\n */\n async #openResult(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<\n { ok: true; value: SealedOutcome } | { ok: false; failure: HandlerResult }\n > {\n const refuse = (why: string) =>\n ({ ok: false as const, failure: fail(\"bad-request\", why) }) as const;\n\n const opened = await open({\n envelope: request.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: runner.device.identity,\n expected: {\n jobId: request.jobId,\n senderKeyId: keyId(runner.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) {\n return refuse(\"the result did not verify as coming from this device\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(opened.plaintext);\n } catch {\n return refuse(\"the sealed result was not valid JSON\");\n }\n const sealed = SealedOutcome.safeParse(parsed);\n if (!sealed.success) return refuse(\"the sealed result was not an outcome\");\n\n if (sealed.data.outcome.outcome !== request.disposition) {\n return refuse(\"the declared disposition is not the one that was sealed\");\n }\n return { ok: true, value: sealed.data };\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n leases: request.leases,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n\n/** The lease a claimed job must have, or a loud failure. */\nfunction leaseOf(job: JobRecord): NonNullable<JobRecord[\"lease\"]> {\n if (!job.lease) {\n throw new Error(\n `store returned job ${job.id} from claim with no lease — the store ` +\n `contract requires a claimed job to hold one`,\n );\n }\n return job.lease;\n}\n","import {\n ENVELOPE_MAX_AGE_MS,\n keyId,\n open,\n publicIdentityOf,\n seal,\n type PublicIdentity,\n type SealedEnvelope,\n type StoredKeys,\n} from \"@byollm/protocol\";\n\n/**\n * Open this site's own at-rest envelope and re-seal it to a claiming device.\n *\n * The single operation that makes byollm_009 §6 work, and it now has two\n * callers: {@link ByollmHandlers} answering `fetch` on the direct plane, and\n * the cloud lane answering the relay's \"who claimed it\" poll. Both do exactly\n * this, and the reason it lives in one file is the reason everything else in\n * this codebase does: the deadline, the key ids and the direction are all\n * bound into a signature, and two implementations of a bound value is the same\n * bug as two clock readings — it works until they disagree, and then nothing\n * opens.\n *\n * The plaintext exists for one statement and never reaches a wire, a store, or\n * a log. That is the whole guarantee: the site is an endpoint, so it is\n * entitled to read its own work, and it is the only party between the app and\n * the device that is.\n */\n\ntype ResealFailure = \"unopenable\";\n\nexport type ResealResult =\n | { readonly ok: true; readonly envelope: SealedEnvelope }\n | { readonly ok: false; readonly reason: ResealFailure };\n\nexport async function resealForDevice(input: {\n siteKeys: StoredKeys;\n /** The job's identity and its at-rest ciphertext. */\n job: {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly createdAt: number;\n };\n /** The device that claimed it, as the upstream reported. */\n device: PublicIdentity;\n}): Promise<ResealResult> {\n const senderKeyId = keyId(publicIdentityOf(input.siteKeys).identity);\n\n const opened = await open({\n envelope: input.job.envelope,\n recipientKeys: input.siteKeys,\n senderIdentityPublic: input.siteKeys.identityPublic,\n expected: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: senderKeyId,\n direction: \"payload\",\n },\n });\n if (!opened.ok) {\n // The store holds something this site cannot open: rotated keys, a\n // corrupted row, or someone else's envelope. Not the device's problem and\n // not something a retry fixes.\n return { ok: false, reason: \"unopenable\" };\n }\n\n const envelope = await seal({\n plaintext: opened.plaintext,\n senderKeys: input.siteKeys,\n recipientEncryptionPublic: input.device.encryption,\n context: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: keyId(input.device.identity),\n // From the record, never recomputed from a fresh clock read — the\n // envelope's own deadline is what the signature bound.\n deadlineAt: input.job.createdAt + ENVELOPE_MAX_AGE_MS,\n direction: \"payload\",\n },\n });\n return { ok: true, envelope };\n}\n","import type {\n PublicIdentity,\n Audience,\n Capability,\n JobKind,\n JobOutcome,\n JobPayload,\n SealedEnvelope,\n SizeClass,\n JobState,\n Lease,\n ResultProvenance,\n} from \"@byollm/protocol\";\n\n/**\n * A job as the server stores it.\n *\n * Adapters map this shape onto their own storage; the field meanings are\n * normative because the conformance kit asserts behaviour that depends on\n * them (TTL clock start, dependency gating, refusal tracking).\n */\nexport interface JobRecord {\n readonly id: string;\n readonly kind: JobKind;\n /**\n * The work, sealed to this site's own encryption key (byollm_009 §10).\n *\n * The store never holds plaintext. The app sees plaintext at enqueue and at\n * result because the app *is* the endpoint; everything in between —\n * database, backups, log aggregators, a support engineer with read access —\n * sees ciphertext.\n *\n * This is not protection from the application the user deliberately sent\n * their work to. It is protection from everything the application's storage\n * touches, which is a longer list than most people picture.\n */\n readonly envelope: SealedEnvelope;\n /** Fixed at enqueue, where the plaintext is. */\n readonly sizeClass: SizeClass;\n readonly audience: Audience;\n /**\n * The service the site named, if it named one — byollm_016 Phase B.\n *\n * Stored rather than derived, because the stub carries it to the router and\n * the router matches on it. `undefined` means the owner's default answers,\n * which is every job written before this field existed.\n */\n readonly service: string | undefined;\n /** The app's id for the user who enqueued it. */\n readonly owner: string;\n /** Server-side restriction on which runner owners may take a `named` job. */\n readonly audienceAllow: readonly string[] | undefined;\n /** Job ids that must all be `ok` before this becomes claimable. */\n readonly dependsOn: readonly string[];\n readonly state: JobState;\n readonly lease: Lease | null;\n /**\n * The grant that recorded this job's result — cloud_008 §3.6.\n *\n * Kept after `lease` is nulled, because \"who finished this\" outlives \"who\n * holds this\" and the two are asked for different reasons. It is what lets\n * a replay from the device that finished the job be answered *as a\n * duplicate* rather than as a stale lease — and lets a replay from any\n * other device be refused exactly as it would be for a job that is not\n * terminal, so a job id is not a terminality probe.\n */\n readonly completedByLeaseId: string | null;\n readonly createdAt: number;\n /**\n * When the job became claimable — enqueue time for a job with no\n * dependencies, or the moment its last dependency reached `ok`.\n *\n * **The TTL clock starts here, not at `createdAt`.** Starting it at enqueue\n * would expire a dependent job for the crime of waiting on a slow\n * dependency (byollm_001 Rev 1 §D, TTL clock resolved in build review).\n * `null` means still blocked.\n */\n readonly claimableAt: number | null;\n /** How long an unclaimed job may wait once claimable. */\n readonly ttlMs: number;\n /** Optional absolute deadline, independent of the TTL. */\n readonly deadlineAt: number | null;\n /**\n * Runners that released this job with reason `refused` — their local\n * allowlist declined it. Never offered to them again\n * ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n */\n readonly refusedBy: readonly string[];\n /** How many times this job has been claimed, including lease-expiry retries. */\n readonly attempts: number;\n readonly outcome: JobOutcome | null;\n readonly provenance: ResultProvenance | null;\n readonly updatedAt: number;\n}\n\n/** A paired daemon as the server stores it. */\nexport interface RunnerRecord {\n readonly id: string;\n /** The app's id for the user this runner is bound to — exactly one. */\n readonly owner: string;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n readonly paused: boolean;\n /** Set once; a revoked runner never un-revokes. */\n readonly revokedAt: number | null;\n readonly lastHeartbeatAt: number;\n readonly createdAt: number;\n /**\n * The device's pinned public keys. What later signatures verify against —\n * a runner id names a machine, this proves it.\n */\n readonly device: PublicIdentity;\n}\n\n/** An in-flight device-code pairing. */\nexport interface PairingRecord {\n /** SHA-256 of the device code. The code itself is never stored. */\n readonly deviceCodeHash: string;\n /** The short code the user reads. Unique among live pairings. */\n readonly userCode: string;\n readonly state: \"pending\" | \"approved\" | \"denied\";\n /** Set when approved — learned from the approving user's own session. */\n readonly owner: string | null;\n readonly runnerId: string | null;\n /**\n * Whether this approval has already been collected — cloud_008 §2.4.\n *\n * This was `runnerTokenOnce`, a bearer token held until the daemon's next\n * poll and then nulled. The token is gone (finding 37: minted, hashed,\n * written to two disks, never sent or compared), but the *deliver-once*\n * property it carried is real and separate: a replayed device code must get\n * nothing, or a code seen in a shell history is a second pairing.\n *\n * So the flag stays and the secret does not. Nulling a token to mean\n * \"collected\" was one field doing two jobs, and only one of them was load\n * bearing.\n */\n readonly collected: boolean;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n /**\n * The device's public keys, presented at pair start (byollm_009 §5).\n *\n * Kept on the pairing so the approving user is approving a *specific\n * machine*, not a code that any machine could later redeem. It is copied\n * onto the runner at approval.\n */\n readonly device: PublicIdentity;\n readonly expiresAt: number;\n readonly createdAt: number;\n}\n\n/** What the app supplies to enqueue a job. */\nexport interface EnqueueInput {\n readonly kind: JobKind;\n /** The work, in plaintext. The server seals it before it is stored. */\n readonly payload: JobPayload;\n readonly owner: string;\n /** Defaults to `private` — the safe direction. */\n readonly audience?: Audience;\n /**\n * Which of the device owner's services should answer — byollm_016 Phase B.\n *\n * A **name from their menu**, never a description of what you want. There is\n * still no model field, no base URL and no flags: this names one entry in\n * that owner's own config, and what it resolves to is entirely theirs. A\n * name they do not advertise is refused rather than served by something\n * else, because substituting is how \"pick from my list\" becomes \"ask for\n * anything and get something\".\n *\n * Leave it out and the owner's default answers, which is what every job\n * written before this field did.\n *\n * You will not usually know the name. It is for the case where a person has\n * told your app which of *their* devices' services to use — a fine-tune they\n * named, say — not something an app invents.\n */\n readonly service?: string;\n readonly audienceAllow?: readonly string[];\n readonly dependsOn?: readonly string[];\n /** Defaults to the server config's `defaultTtlMs`. */\n readonly ttlMs?: number;\n readonly deadlineAt?: number;\n /** Caller-supplied id, for idempotent enqueue. */\n readonly id?: string;\n}\n\n/**\n * What the *store* is given — the sealed form.\n *\n * Distinct from {@link EnqueueInput} because the two are genuinely different\n * things: an app hands over work in plaintext, and what gets written down is\n * sealed. Collapsing them into one type would mean a field that is sometimes\n * readable and sometimes not, which is the kind of ambiguity that ends with\n * plaintext in a database.\n */\nexport interface StoredJobInput extends Omit<EnqueueInput, \"payload\" | \"id\"> {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly sizeClass: SizeClass;\n}\n\n/**\n * When a job's ciphertext stops being worth carrying — cloud_008 §31.\n *\n * One function because it was two expressions. The direct plane computed\n * `job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs`; the cloud lane\n * computed `record.deadlineAt ?? record.createdAt + <a local constant>`. The\n * first branch agreed and the fallback did not, so a job with no explicit\n * deadline got two different ones depending on which lane published it — and\n * the difference is largest exactly where it matters, for a job blocked on a\n * dependency, whose `claimableAt` may be hours after `createdAt`.\n *\n * The TTL clock starts when a job becomes *claimable*, which is the rule\n * `DEPENDS_ON_GATING` and `TTL_EXPIRY` already share: a dependent job must not\n * spend its life waiting for its dependency.\n */\nexport function deadlineFor(\n job: Pick<JobRecord, \"deadlineAt\" | \"claimableAt\" | \"ttlMs\">,\n now: number,\n): number {\n return job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs;\n}\n","import {\n ENDPOINTS,\n ERROR_STATUS,\n PROTOCOL_PREFIX,\n checkProtocolVersion,\n type Endpoint,\n} from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * A payload is capped at 4 MB of text by the protocol; this leaves room for\n * JSON overhead and a batch of results, and refuses anything wilder at the\n * door rather than after parsing it.\n */\nconst MAX_BODY_BYTES = 8 * 1024 * 1024;\n\n/**\n * Where the protocol endpoints are mounted.\n *\n * Defaults to {@link PROTOCOL_PREFIX}. Pass the real mount point when it is\n * anything else — a Next.js route at `app/api/byollm/[...route]/route.ts`\n * serves `/api/byollm/...`, so it needs `basePath: \"/api/byollm\"`.\n *\n * @throws if the path is not an absolute, single-segment-per-slash path. A\n * mount point is configuration, and a malformed one should fail at startup\n * rather than silently match nothing.\n */\nfunction normalizeBasePath(basePath: string): string {\n const trimmed = basePath.endsWith(\"/\") ? basePath.slice(0, -1) : basePath;\n if (!trimmed.startsWith(\"/\")) {\n throw new Error(`basePath must start with \"/\": got ${basePath}`);\n }\n if (trimmed.includes(\"//\") || /[?#*]/.test(trimmed)) {\n throw new Error(`basePath must be a plain path: got ${basePath}`);\n }\n return trimmed;\n}\n\n/**\n * Pull the endpoint name out of a URL path, or null if it isn't ours.\n *\n * The full path must match `<basePath>/<endpoint>` exactly. This used to\n * compare only the *last* segment, which meant `/anything/at/all/claim`\n * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it\n * appeared in a 404 message and was never matched against. For the handler\n * that serves claim, result and heartbeat, dispatching on a suffix is a\n * looser rule than anyone reading the constant would assume, and loose\n * matching in a security surface should at least be a decision.\n *\n * The cost is that the mount point is now something a deployment has to state\n * rather than something that works by accident. That is the intended trade:\n * a 404 at startup naming the mount point beats a handler answering on paths\n * nobody meant to expose.\n */\nexport function routeEndpoint(\n pathname: string,\n basePath: string = PROTOCOL_PREFIX,\n): Endpoint | null {\n const base = normalizeBasePath(basePath);\n const path = pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n if (!path.startsWith(`${base}/`)) return null;\n const rest = path.slice(base.length + 1);\n return (ENDPOINTS as readonly string[]).includes(rest)\n ? (rest as Endpoint)\n : null;\n}\n\n/**\n * Read the request signature from headers (byollm_009 §4.2).\n *\n * In headers rather than the body so the signature covers the body whole,\n * with no field to exclude from its own hash — a scheme that signs a body\n * minus one field has to agree, byte for byte, on how that field is removed.\n */\nexport function signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const rawIssuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n if (runnerId === null || signature === null || rawIssuedAt === null) {\n return undefined;\n }\n // Checked against null *before* Number(), because `Number(null)` is 0 —\n // finite, plausible-looking, and wrong. A missing timestamp would have\n // become a timestamp of the epoch, which the freshness check would then\n // reject for the wrong reason.\n const issuedAt = Number(rawIssuedAt);\n if (!Number.isFinite(issuedAt)) return undefined;\n return { runnerId, issuedAt, signature };\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig & {\n /**\n * Where these endpoints are mounted. Defaults to\n * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere.\n */\n readonly basePath?: string;\n },\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n // Validate once, at construction: a bad mount point is a deployment bug and\n // should surface when the server starts, not as a silent 404 per request.\n const basePath = normalizeBasePath(config.basePath ?? PROTOCOL_PREFIX);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname, basePath);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${basePath} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n\n let body: unknown;\n let rawBody: string;\n try {\n rawBody = await request.text();\n const text = rawBody;\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n // byollm_009 §4: version before anything else. A mismatch must name the\n // disagreement and the fix, not surface as a generic bad-request from a\n // schema literal buried in an endpoint — which is what happened before,\n // and is why \"the connection is versionless\" was listed as a defect.\n const refusal = checkProtocolVersion(body);\n if (refusal) {\n return json(ERROR_STATUS[refusal.error], refusal);\n }\n\n const result = await handlers.handle(endpoint, body, {\n endpoint,\n // The bytes as received. Re-serialising the parsed object would verify\n // a signature over something the sender never sent.\n rawBody,\n signature: signatureFrom(request.headers),\n });\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["keyId","open","publicIdentityOf","publicIdentityOf","keyId","open","ERROR_STATUS","ERROR_STATUS"]}
|
|
1
|
+
{"version":3,"sources":["../src/ids.ts","../src/handlers.ts","../src/reseal.ts","../src/records.ts","../src/http.ts"],"sourcesContent":["import {\n createHash,\n randomBytes,\n randomUUID,\n timingSafeEqual,\n} from \"node:crypto\";\n\n/**\n * Alphabet for the user-facing pairing code.\n *\n * Excludes `0/O`, `1/I/L`, `5/S` and `U/V` — a code is read aloud or copied\n * off a terminal into a browser, and a user who mistypes it gets a failure\n * they cannot diagnose. 27 symbols over 8 characters is ~38 bits, which is\n * ample for a code that lives ten minutes, is single-use, and is rate-limited.\n */\nconst USER_CODE_ALPHABET = \"ABCDEFGHJKMNPQRTWXYZ2346789\";\n\n/** A device code: the secret the daemon polls with. Never shown to a user. */\nexport function generateDeviceCode(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** A runner id. */\nexport function generateRunnerId(): string {\n return `runner_${randomUUID()}`;\n}\n\n/** A job id. */\nexport function generateJobId(): string {\n // A bare UUID, not a prefixed one.\n //\n // The app mints this now, because byollm_009 §6 binds the job id into the\n // envelope's signature — so the id must exist before the row does. A\n // `job_`-prefixed string is not a `uuid`, and the Supabase adapter's column\n // is, so the prefix would have made every enqueue fail there while passing\n // in memory. Ids are opaque to the protocol; the prefix was only ever\n // decoration.\n return randomUUID();\n}\n\n/**\n * A short code the user reads and confirms, formatted `XXXX-XXXX`.\n * Drawn with rejection sampling so the alphabet stays uniform.\n */\nexport function generateUserCode(): string {\n const chars: string[] = [];\n while (chars.length < 8) {\n for (const byte of randomBytes(16)) {\n // 256 % 28 !== 0, so bytes at or above the largest whole multiple are\n // discarded rather than folded — folding would bias the low symbols.\n const limit = 256 - (256 % USER_CODE_ALPHABET.length);\n if (byte >= limit) continue;\n const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];\n if (symbol === undefined) continue;\n chars.push(symbol);\n if (chars.length === 8) break;\n }\n }\n return `${chars.slice(0, 4).join(\"\")}-${chars.slice(4).join(\"\")}`;\n}\n\n/** SHA-256, hex. Tokens and device codes are stored only as this. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret, \"utf8\").digest(\"hex\");\n}\n\n/**\n * Compare two hex digests without leaking their difference through timing.\n * Lengths are compared first because `timingSafeEqual` throws on a mismatch.\n */\nexport function secretsMatch(aHex: string, bHex: string): boolean {\n if (aHex.length !== bHex.length) return false;\n return timingSafeEqual(Buffer.from(aHex, \"hex\"), Buffer.from(bHex, \"hex\"));\n}\n\n/**\n * A fresh id for one lease grant.\n *\n * Not a secret and not guessed at — a daemon is told its lease id in the claim\n * response. It exists to distinguish *this* grant from the next one over the\n * same job by the same runner, which is what stops a replayed release landing\n * on a lease the sender never meant.\n */\nexport const generateLeaseId = (): string => randomUUID();\n","import {\n FetchRequest,\n SealedOutcome,\n keyId,\n open,\n publicIdentityOf,\n type FetchResponse,\n RequestSignature,\n verifyRequest,\n verifyPublicIdentity,\n type StoredKeys,\n ClaimRequest,\n type ClaimRequest as ClaimRequestType,\n type HeartbeatRequest as HeartbeatRequestType,\n type ReleaseRequest as ReleaseRequestType,\n type ResultRequest as ResultRequestType,\n ERROR_STATUS,\n HeartbeatRequest,\n PairRequest,\n PROTOCOL_VERSION,\n ReleaseRequest,\n ResultRequest,\n provenanceFor,\n type ClaimResponse,\n type Endpoint,\n type HeartbeatResponse,\n type PairPollResponse,\n type PairStartResponse,\n type ReleaseResponse,\n type ResultResponse,\n type WireErrorCode,\n} from \"@byollm/protocol\";\nimport { generateDeviceCode, generateUserCode, hashSecret } from \"./ids.js\";\nimport { resealForDevice } from \"./reseal.js\";\nimport { deadlineFor } from \"./records.js\";\nimport type { JobRecord, RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\n/**\n * What a transport must hand the handler to authenticate a call.\n *\n * `rawBody` is the exact bytes received, not a re-serialisation of the parsed\n * object: JSON.stringify does not round-trip byte-for-byte, and a signature\n * over re-serialised input verifies something the sender never signed.\n */\nexport interface AuthContext {\n readonly endpoint: string;\n readonly rawBody: string;\n readonly signature: unknown;\n}\n\nexport interface HandlerConfig {\n readonly store: ByollmStore;\n /**\n * Absolute URL of the page where a user approves a pairing. The device code\n * is *not* appended — the user types the short code into the app's own\n * authenticated page, which is what keeps pairing interactive.\n */\n readonly verificationUrl: string;\n /** How long a lease lasts. Default 60s — six heartbeats of headroom. */\n readonly leaseMs?: number;\n /** How long an unapproved pairing code lives. Default 10 minutes. */\n readonly pairingTtlMs?: number;\n /** How often a daemon may poll for pairing approval. Default 2s. */\n readonly pollIntervalMs?: number;\n /** Injectable clock, so tests can move time without sleeping. */\n readonly now?: () => number;\n /**\n * This site's keypairs (byollm_009 §5) — **supplied, never generated here.**\n *\n * A site is usually more than one process. Generating keys at startup would\n * work perfectly in development and fail only in production, silently: each\n * instance would have a different identity, a daemon would pin whichever\n * one approved its pairing, and every request routed to a different\n * instance would fail a signature check it had no way to explain. So this\n * is a required input, and there is a `keygen` script that produces one.\n */\n readonly siteKeys: StoredKeys;\n}\n\nconst DEFAULTS = {\n leaseMs: 60_000,\n pairingTtlMs: 10 * 60_000,\n pollIntervalMs: 2_000,\n} as const;\n\n/** A handled protocol call: a status and a JSON body. */\nexport interface HandlerResult {\n readonly status: number;\n readonly body: unknown;\n /** Set for `rate-limited` and `server-error`. */\n readonly retryAfterSeconds?: number;\n}\n\nfunction fail(\n error: WireErrorCode,\n message: string,\n retryAfterSeconds?: number,\n): HandlerResult {\n return {\n status: ERROR_STATUS[error],\n body: {\n error,\n message,\n ...(retryAfterSeconds === undefined\n ? {}\n : { retryAfter: retryAfterSeconds }),\n },\n ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),\n };\n}\n\nfunction ok(body: unknown): HandlerResult {\n return { status: 200, body };\n}\n\n/**\n * The five protocol endpoints, over any {@link ByollmStore}.\n *\n * Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,\n * or whatever) onto {@link ByollmHandlers.handle}, and everything the\n * protocol actually specifies lives here where the conformance kit can reach\n * it without an HTTP server in the way.\n */\nexport class ByollmHandlers {\n readonly #store: ByollmStore;\n readonly #verificationUrl: string;\n readonly #leaseMs: number;\n readonly #pairingTtlMs: number;\n readonly #pollIntervalMs: number;\n readonly #now: () => number;\n readonly #siteKeys: StoredKeys;\n /** This site's identity key id — Amendment A's `stub.site`. Derived once. */\n readonly #siteKeyId: string;\n\n constructor(config: HandlerConfig) {\n this.#store = config.store;\n // Fail at construction, not at the first pairing. A site whose keys are\n // malformed should not start and then refuse its users one at a time.\n if (!verifyPublicIdentity(publicIdentityOf(config.siteKeys))) {\n throw new Error(\n \"siteKeys are not internally consistent: the encryption key is not \" +\n \"signed by the identity key. Generate a fresh pair with \" +\n \"`npx @byollm/server keygen`.\",\n );\n }\n this.#siteKeys = config.siteKeys;\n this.#siteKeyId = keyId(publicIdentityOf(config.siteKeys).identity);\n this.#verificationUrl = config.verificationUrl;\n this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;\n this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;\n this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;\n this.#now = config.now ?? Date.now;\n }\n\n /**\n * Dispatch one protocol call.\n *\n * @param endpoint - which of the five, already routed from the path\n * @param body - the parsed JSON request body, untrusted\n * @param auth - the signature and the exact bytes it covers\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n auth: AuthContext,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(auth, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on — and since V1-2 it\n // travels as the refusal itself ({@link MUSTS.REVOCATION_HONORED}).\n //\n // It used to be answered with an empty site set, which the daemon\n // read as \"revoked\". That reading is gone: an empty set now means\n // \"nothing is consented right now\", because a projection can arrive\n // empty by accident and the daemon's response to revocation is to\n // delete its pairing. So the one call every daemon always makes — a\n // daemon with no working backend never claims — carries the\n // unambiguous version: 403 with `revoked`, which is a code and not an\n // inference.\n return this.#authed(\n auth,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n );\n case \"fetch\":\n return this.#authed(auth, body, FetchRequest, this.#fetch.bind(this));\n case \"result\":\n return this.#authed(auth, body, ResultRequest, this.#result.bind(this));\n case \"release\":\n return this.#authed(\n auth,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: verify the\n * signature, reject a revoked runner, and parse the body.\n *\n * Authentication happens before schema validation so a stranger probing the\n * endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n auth: AuthContext,\n body: unknown,\n schema: { safeParse: (v: unknown) => { success: boolean; data?: T } },\n run: (request: T, runner: RunnerRecord) => Promise<HandlerResult>,\n options: { allowRevoked?: boolean } = {},\n ): Promise<HandlerResult> {\n const signature = RequestSignature.safeParse(auth.signature);\n if (!signature.success) {\n return fail(\"unauthorized\", \"this request is not signed\");\n }\n\n const runner = await this.#store.getRunner(signature.data.runnerId);\n if (!runner) {\n return fail(\"unauthorized\", \"this runner is not recognised\");\n }\n\n // Verified against the identity pinned when the user approved this\n // machine — not against anything the request carries. A signature that\n // authenticates itself authenticates nothing.\n const failure = verifyRequest({\n identityPublic: runner.device.identity,\n endpoint: auth.endpoint,\n body: auth.rawBody,\n signature: signature.data,\n now: this.#now(),\n });\n if (failure !== null) {\n // Deliberately one message for both causes. Telling a caller whether\n // their clock or their key is wrong tells an attacker which half of a\n // forgery already works.\n return fail(\"unauthorized\", \"this request's signature is not valid\");\n }\n if (runner.revokedAt !== null && options.allowRevoked !== true) {\n // A distinct truth from \"unauthorized\": the daemon should stop and say\n // so, not retry or re-pair silently.\n return fail(\"revoked\", \"this runner has been revoked by its owner\");\n }\n\n const parsed = schema.safeParse(body);\n if (!parsed.success || parsed.data === undefined) {\n return fail(\"bad-request\", \"request body failed schema validation\");\n }\n return run(parsed.data, runner);\n }\n\n /**\n * Hand over the payload for a lease this runner holds — byollm_009 §6.\n *\n * The second half of claim-then-fetch. A claim answers with a stub, and the\n * work itself is collected separately by the device that took it, because a\n * payload can only be sealed once its recipient is known.\n *\n * Scoped to the lease, not the job: answering for whatever lease happens to\n * exist would hand the work to a runner whose grant had already been\n * superseded.\n */\n async #fetch(\n request: FetchRequest,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n const job = await this.#store.get(request.jobId);\n if (\n !job ||\n job.lease?.runnerId !== runner.id ||\n job.lease.id !== request.leaseId\n ) {\n // One answer for \"no such job\", \"not yours\" and \"a lease you no longer\n // hold\". A caller who is allowed to know already knows which.\n return fail(\"not-found\", \"no such lease on this job\");\n }\n // One implementation of open-and-reseal, shared with the cloud lane: the\n // deadline and key ids are bound into a signature, and two copies of a\n // bound value is the bug this codebase keeps finding.\n const resealed = await resealForDevice({\n siteKeys: this.#siteKeys,\n job: { id: job.id, envelope: job.envelope, createdAt: job.createdAt },\n device: runner.device,\n });\n if (!resealed.ok) {\n return fail(\"server-error\", \"this job's payload could not be opened\");\n }\n return ok({ envelope: resealed.envelope } satisfies FetchResponse);\n }\n\n // -- 1. pair --------------------------------------------------------------\n\n async #pair(body: unknown): Promise<HandlerResult> {\n const parsed = PairRequest.safeParse(body);\n if (!parsed.success) {\n return fail(\"bad-request\", \"pair request failed schema validation\");\n }\n const request = parsed.data;\n const now = this.#now();\n\n if (request.action === \"start\") {\n const deviceCode = generateDeviceCode();\n const userCode = generateUserCode();\n const expiresAt = now + this.#pairingTtlMs;\n\n // The machine must prove its encryption key belongs to the identity it\n // is presenting, before either is stored. Otherwise a caller could pair\n // a real identity with an encryption key it holds the secret for, and\n // read everything later sealed to that runner.\n if (!verifyPublicIdentity(request.device)) {\n return fail(\n \"bad-request\",\n \"the device's encryption key is not signed by the identity it was presented with\",\n );\n }\n\n await this.#store.createPairing({\n device: request.device,\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n collected: false,\n label: request.daemon.label,\n platform: request.daemon.platform,\n daemonVersion: request.daemon.version,\n capabilities: request.capabilities,\n expiresAt,\n createdAt: now,\n });\n\n const response: PairStartResponse = {\n deviceCode,\n userCode,\n verificationUrl: this.#verificationUrl,\n expiresAt,\n pollIntervalMs: this.#pollIntervalMs,\n };\n return ok(response);\n }\n\n // action === \"poll\"\n const pairing = await this.#store.getPairingByDeviceCodeHash(\n hashSecret(request.deviceCode),\n );\n if (!pairing) {\n return fail(\"not-found\", \"unknown device code\");\n }\n if (pairing.state === \"denied\") {\n return ok({ status: \"denied\" } satisfies PairPollResponse);\n }\n // Expiry is checked before approval state so a code approved after it\n // lapsed is still dead ({@link MUSTS.PAIR_CODE_EXPIRES}).\n if (pairing.expiresAt <= now && pairing.state === \"pending\") {\n return ok({ status: \"expired\" } satisfies PairPollResponse);\n }\n if (\n pairing.state === \"approved\" &&\n !pairing.collected &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerId: pairing.runnerId,\n owner: pairing.owner,\n // Only on approval: a pending or denied poll learns nothing, so an\n // unapproved code cannot be used to enumerate a site's keys.\n //\n // One entry, because a direct site *is* one site — the same shape a\n // hub answers with rather than a special case (cloud_009 §5). The\n // daemon's lookup is one map read on every lane, which is what keeps\n // the two lanes one protocol.\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n };\n // Delivered exactly once — a replayed device code gets nothing.\n await this.#store.consumePairingToken(pairing.deviceCodeHash);\n return ok(response);\n }\n if (pairing.state === \"approved\") {\n return fail(\"not-found\", \"this pairing has already been collected\");\n }\n return ok({ status: \"pending\" } satisfies PairPollResponse);\n }\n\n // -- 2. claim -------------------------------------------------------------\n\n async #claim(\n request: ClaimRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n\n // Capabilities from *this* request, never the stored matrix — a daemon\n // that just lost a backend must not be handed work for it\n // ({@link MUSTS.CLAIM_REQUIRES_CAPABILITY}).\n const jobs = await this.#store.claim({\n runnerId: runner.id,\n runnerOwner: runner.owner,\n capabilities: request.capabilities,\n max: request.max,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const response: ClaimResponse = {\n jobs: jobs.map((job) => ({\n id: job.id,\n kind: job.kind,\n audience: job.audience,\n owner: job.owner,\n // This site, named by its identity key id — Amendment A §A.3. The\n // daemon pinned this exact value at pairing, so it can check the stub\n // against the envelope it later opens rather than taking our word for\n // which site sent it. On this plane that is redundant, which is the\n // point: the direct and relayed stubs are the same shape, and a daemon\n // serving both cannot tell which upstream it is talking to.\n site: this.#siteKeyId,\n // byollm_016 Phase B. Present only when the site named one, and\n // omitted rather than sent as undefined — the stub is `.strict()` and\n // an explicit undefined is not the same as an absent key.\n ...(job.purpose === undefined ? {} : { purpose: job.purpose }),\n // Bucketed, not measured: an exact size is a stronger fingerprint\n // than routing needs (byollm_009 §6).\n sizeClass: job.sizeClass,\n // Reserved for byollm_006; no job declares it yet.\n streaming: false,\n // The stub's deadline bounds how long a captured envelope is worth\n // keeping, so it is always present — falling back to the TTL window\n // when the app named no absolute one.\n deadlineAt: deadlineFor(job, now),\n // `audienceAllow` is not sent — cloud_008 §0.2. The list stays on\n // `JobRecord`, where `claim` already filtered candidates with it; the\n // daemon's own allowlist is what decides `named` (byollm_001 Rev 1\n // §B) and always was.\n //\n // Removing it from `JobStub` did **not** make this line a type error.\n // A conditional spread is not excess-property-checked, so the field\n // would have gone on being sent to a daemon whose `.strict()` parse\n // now rejects the entire claim response — every daemon on the version\n // pair, refusing all work, for a field nobody read. Worth stating\n // where it happened: the schema is the contract, and the compiler\n // does not enforce it through a spread.\n // No fallback. A job returned from `claim` holds a lease by\n // definition, and synthesising one here would hand the daemon a lease\n // id the store has never heard of — every later release naming it\n // would silently match nothing. A store that returns an unleased job\n // has broken its contract, and this says so.\n lease: leaseOf(job),\n })),\n leaseMs: this.#leaseMs,\n };\n return ok(response);\n }\n\n // -- 3. heartbeat ---------------------------------------------------------\n\n async #heartbeat(\n request: HeartbeatRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n // No revoked branch here any more — V1-2. A revoked runner is refused by\n // `#authed` before this handler is reached, on heartbeat as on every\n // other endpoint, because \"revoked\" and \"nothing consented right now\"\n // must not arrive as the same empty body.\n\n await this.#store.touchRunner({\n runnerId: runner.id,\n capabilities: request.capabilities,\n daemonVersion: request.daemonVersion,\n paused: request.paused,\n now,\n });\n\n // `renewed` is not reported back — cloud_008 §1.4b. The grants are still\n // extended; the daemon simply never read the list, and `lost` is the\n // signal it acts on.\n const { lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n leases: request.activeLeases,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n sites: { [this.#siteKeyId]: publicIdentityOf(this.#siteKeys) },\n // A direct site has no disclosure of its own to go stale: consent to it\n // *is* the pairing, and withdrawing it empties the set above.\n awaitingConsent: [],\n cancel: [...cancel],\n lost: [...lost],\n serverTime: now,\n };\n return ok(response);\n }\n\n // -- 4. result ------------------------------------------------------------\n\n async #result(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const now = this.#now();\n const job = await this.#store.get(request.jobId);\n if (!job) return fail(\"not-found\", \"unknown job\");\n\n const outcome = await this.#openResult(request, runner);\n if (!outcome.ok) return outcome.failure;\n\n // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.PROVENANCE_NAMES_DEVICE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n // From the envelope the device signed, not from the request beside it\n // — cloud_008 §2.5. A daemon can no longer seal one answer and declare\n // it came from a different model.\n backendClass: outcome.value.ran.backendClass,\n model: outcome.value.ran.model,\n });\n\n const {\n accepted,\n duplicate,\n job: updated,\n } = await this.#store.complete({\n jobId: request.jobId,\n // Who is asking, for the duplicate answer only — §3.6. Authorisation\n // is `holder`, below, and still is.\n runnerId: runner.id,\n // The grant, not the runner — cloud_008 §1.4a. `CompleteHolder`'s own\n // docstring already called the lease \"the more exact check anyway\";\n // this plane simply had no lease id to give it until now.\n holder: { by: \"lease\", leaseId: request.leaseId },\n outcome: outcome.value.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n // Only when true — cloud_008 §3.6. Absent means \"not a duplicate\", and\n // an optional field that is always present is a required one wearing a\n // question mark.\n ...(duplicate === true ? { duplicate: true } : {}),\n state: updated?.state ?? job.state,\n };\n return ok(response);\n }\n\n /**\n * Open a sealed result, or refuse it.\n *\n * The mirror of the daemon's `#openPayload`, and refuses for the same\n * reason: an outcome that does not verify against the device's pinned key is\n * an assertion by whoever relayed it, and storing it would let an\n * intermediary write answers into the app.\n *\n * The clear-text `disposition` is checked here rather than trusted. It is on\n * the wire so a relay can route without opening anything, which means the\n * one thing it must not be is authoritative — a daemon that sealed an error\n * and declared `ok` would otherwise have its declaration believed by\n * everything upstream of this line.\n */\n async #openResult(\n request: ResultRequestType,\n runner: RunnerRecord,\n ): Promise<\n { ok: true; value: SealedOutcome } | { ok: false; failure: HandlerResult }\n > {\n const refuse = (why: string) =>\n ({ ok: false as const, failure: fail(\"bad-request\", why) }) as const;\n\n const opened = await open({\n envelope: request.envelope,\n recipientKeys: this.#siteKeys,\n senderIdentityPublic: runner.device.identity,\n expected: {\n jobId: request.jobId,\n senderKeyId: keyId(runner.device.identity),\n recipientKeyId: keyId(publicIdentityOf(this.#siteKeys).identity),\n direction: \"result\",\n },\n });\n if (!opened.ok) {\n return refuse(\"the result did not verify as coming from this device\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(opened.plaintext);\n } catch {\n return refuse(\"the sealed result was not valid JSON\");\n }\n const sealed = SealedOutcome.safeParse(parsed);\n if (!sealed.success) return refuse(\"the sealed result was not an outcome\");\n\n if (sealed.data.outcome.outcome !== request.disposition) {\n return refuse(\"the declared disposition is not the one that was sealed\");\n }\n return { ok: true, value: sealed.data };\n }\n\n // -- 5. release -----------------------------------------------------------\n\n async #release(\n request: ReleaseRequestType,\n runner: RunnerRecord,\n ): Promise<HandlerResult> {\n if (request.runnerId !== runner.id) {\n return fail(\"unauthorized\", \"runner id does not match the signing key\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n leases: request.leases,\n reason: request.reason,\n now: this.#now(),\n });\n const response: ReleaseResponse = { released };\n return ok(response);\n }\n}\n\n/** The protocol version this build speaks. */\nexport const SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;\n\n/** The lease a claimed job must have, or a loud failure. */\nfunction leaseOf(job: JobRecord): NonNullable<JobRecord[\"lease\"]> {\n if (!job.lease) {\n throw new Error(\n `store returned job ${job.id} from claim with no lease — the store ` +\n `contract requires a claimed job to hold one`,\n );\n }\n return job.lease;\n}\n","import {\n ENVELOPE_MAX_AGE_MS,\n keyId,\n open,\n publicIdentityOf,\n seal,\n type PublicIdentity,\n type SealedEnvelope,\n type StoredKeys,\n} from \"@byollm/protocol\";\n\n/**\n * Open this site's own at-rest envelope and re-seal it to a claiming device.\n *\n * The single operation that makes byollm_009 §6 work, and it now has two\n * callers: {@link ByollmHandlers} answering `fetch` on the direct plane, and\n * the cloud lane answering the relay's \"who claimed it\" poll. Both do exactly\n * this, and the reason it lives in one file is the reason everything else in\n * this codebase does: the deadline, the key ids and the direction are all\n * bound into a signature, and two implementations of a bound value is the same\n * bug as two clock readings — it works until they disagree, and then nothing\n * opens.\n *\n * The plaintext exists for one statement and never reaches a wire, a store, or\n * a log. That is the whole guarantee: the site is an endpoint, so it is\n * entitled to read its own work, and it is the only party between the app and\n * the device that is.\n */\n\ntype ResealFailure = \"unopenable\";\n\nexport type ResealResult =\n | { readonly ok: true; readonly envelope: SealedEnvelope }\n | { readonly ok: false; readonly reason: ResealFailure };\n\nexport async function resealForDevice(input: {\n siteKeys: StoredKeys;\n /** The job's identity and its at-rest ciphertext. */\n job: {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly createdAt: number;\n };\n /** The device that claimed it, as the upstream reported. */\n device: PublicIdentity;\n}): Promise<ResealResult> {\n const senderKeyId = keyId(publicIdentityOf(input.siteKeys).identity);\n\n const opened = await open({\n envelope: input.job.envelope,\n recipientKeys: input.siteKeys,\n senderIdentityPublic: input.siteKeys.identityPublic,\n expected: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: senderKeyId,\n direction: \"payload\",\n },\n });\n if (!opened.ok) {\n // The store holds something this site cannot open: rotated keys, a\n // corrupted row, or someone else's envelope. Not the device's problem and\n // not something a retry fixes.\n return { ok: false, reason: \"unopenable\" };\n }\n\n const envelope = await seal({\n plaintext: opened.plaintext,\n senderKeys: input.siteKeys,\n recipientEncryptionPublic: input.device.encryption,\n context: {\n jobId: input.job.id,\n senderKeyId,\n recipientKeyId: keyId(input.device.identity),\n // From the record, never recomputed from a fresh clock read — the\n // envelope's own deadline is what the signature bound.\n deadlineAt: input.job.createdAt + ENVELOPE_MAX_AGE_MS,\n direction: \"payload\",\n },\n });\n return { ok: true, envelope };\n}\n","import type {\n PublicIdentity,\n Audience,\n Capability,\n JobKind,\n JobOutcome,\n JobPayload,\n SealedEnvelope,\n SizeClass,\n JobState,\n Lease,\n ResultProvenance,\n} from \"@byollm/protocol\";\n\n/**\n * A job as the server stores it.\n *\n * Adapters map this shape onto their own storage; the field meanings are\n * normative because the conformance kit asserts behaviour that depends on\n * them (TTL clock start, dependency gating, refusal tracking).\n */\nexport interface JobRecord {\n readonly id: string;\n readonly kind: JobKind;\n /**\n * The work, sealed to this site's own encryption key (byollm_009 §10).\n *\n * The store never holds plaintext. The app sees plaintext at enqueue and at\n * result because the app *is* the endpoint; everything in between —\n * database, backups, log aggregators, a support engineer with read access —\n * sees ciphertext.\n *\n * This is not protection from the application the user deliberately sent\n * their work to. It is protection from everything the application's storage\n * touches, which is a longer list than most people picture.\n */\n readonly envelope: SealedEnvelope;\n /** Fixed at enqueue, where the plaintext is. */\n readonly sizeClass: SizeClass;\n readonly audience: Audience;\n /**\n * The service the site named, if it named one — byollm_016 Phase B.\n *\n * Stored rather than derived, because the stub carries it to the router and\n * the router matches on it. `undefined` means the owner's default answers,\n * which is every job written before this field existed.\n */\n readonly purpose: string | undefined;\n /** The app's id for the user who enqueued it. */\n readonly owner: string;\n /** Server-side restriction on which runner owners may take a `named` job. */\n readonly audienceAllow: readonly string[] | undefined;\n /** Job ids that must all be `ok` before this becomes claimable. */\n readonly dependsOn: readonly string[];\n readonly state: JobState;\n readonly lease: Lease | null;\n /**\n * The grant that recorded this job's result — cloud_008 §3.6.\n *\n * Kept after `lease` is nulled, because \"who finished this\" outlives \"who\n * holds this\" and the two are asked for different reasons. It is what lets\n * a replay from the device that finished the job be answered *as a\n * duplicate* rather than as a stale lease — and lets a replay from any\n * other device be refused exactly as it would be for a job that is not\n * terminal, so a job id is not a terminality probe.\n */\n readonly completedByLeaseId: string | null;\n readonly createdAt: number;\n /**\n * When the job became claimable — enqueue time for a job with no\n * dependencies, or the moment its last dependency reached `ok`.\n *\n * **The TTL clock starts here, not at `createdAt`.** Starting it at enqueue\n * would expire a dependent job for the crime of waiting on a slow\n * dependency (byollm_001 Rev 1 §D, TTL clock resolved in build review).\n * `null` means still blocked.\n */\n readonly claimableAt: number | null;\n /** How long an unclaimed job may wait once claimable. */\n readonly ttlMs: number;\n /** Optional absolute deadline, independent of the TTL. */\n readonly deadlineAt: number | null;\n /**\n * Runners that released this job with reason `refused` — their local\n * allowlist declined it. Never offered to them again\n * ({@link MUSTS.REFUSAL_NOT_REOFFERED}).\n */\n readonly refusedBy: readonly string[];\n /** How many times this job has been claimed, including lease-expiry retries. */\n readonly attempts: number;\n readonly outcome: JobOutcome | null;\n readonly provenance: ResultProvenance | null;\n readonly updatedAt: number;\n}\n\n/** A paired daemon as the server stores it. */\nexport interface RunnerRecord {\n readonly id: string;\n /** The app's id for the user this runner is bound to — exactly one. */\n readonly owner: string;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n readonly paused: boolean;\n /** Set once; a revoked runner never un-revokes. */\n readonly revokedAt: number | null;\n readonly lastHeartbeatAt: number;\n readonly createdAt: number;\n /**\n * The device's pinned public keys. What later signatures verify against —\n * a runner id names a machine, this proves it.\n */\n readonly device: PublicIdentity;\n}\n\n/** An in-flight device-code pairing. */\nexport interface PairingRecord {\n /** SHA-256 of the device code. The code itself is never stored. */\n readonly deviceCodeHash: string;\n /** The short code the user reads. Unique among live pairings. */\n readonly userCode: string;\n readonly state: \"pending\" | \"approved\" | \"denied\";\n /** Set when approved — learned from the approving user's own session. */\n readonly owner: string | null;\n readonly runnerId: string | null;\n /**\n * Whether this approval has already been collected — cloud_008 §2.4.\n *\n * This was `runnerTokenOnce`, a bearer token held until the daemon's next\n * poll and then nulled. The token is gone (finding 37: minted, hashed,\n * written to two disks, never sent or compared), but the *deliver-once*\n * property it carried is real and separate: a replayed device code must get\n * nothing, or a code seen in a shell history is a second pairing.\n *\n * So the flag stays and the secret does not. Nulling a token to mean\n * \"collected\" was one field doing two jobs, and only one of them was load\n * bearing.\n */\n readonly collected: boolean;\n readonly label: string;\n readonly platform: \"darwin\" | \"linux\" | \"win32\";\n readonly daemonVersion: string;\n readonly capabilities: readonly Capability[];\n /**\n * The device's public keys, presented at pair start (byollm_009 §5).\n *\n * Kept on the pairing so the approving user is approving a *specific\n * machine*, not a code that any machine could later redeem. It is copied\n * onto the runner at approval.\n */\n readonly device: PublicIdentity;\n readonly expiresAt: number;\n readonly createdAt: number;\n}\n\n/** What the app supplies to enqueue a job. */\nexport interface EnqueueInput {\n readonly kind: JobKind;\n /** The work, in plaintext. The server seals it before it is stored. */\n readonly payload: JobPayload;\n readonly owner: string;\n /** Defaults to `private` — the safe direction. */\n readonly audience?: Audience;\n /**\n * Which of *your site's* declared purposes this job serves — Amendment L.\n *\n * **A need, never a name.** You declare purposes at registration —\n * `\"revenue\"`, `\"writing-assistant\"` — and each of your users maps them to\n * one of their own services on the consent screen. This field names the\n * purpose; the mapping does the rest.\n *\n * There is no model field, no base URL, no flags, and — since Amendment L —\n * no way to name a service either. Your vocabulary is your purposes; theirs\n * is their services; the two never meet. You learn whether a slot was\n * satisfiable and nothing else.\n *\n * Use the purpose **key**, not its label. Labels are prose for the consent\n * screen and may change; a key travels on every job and is what mappings\n * are stored against.\n *\n * Leave it out only in direct mode, which has no control plane to hold a\n * mapping and answers by kind alone.\n */\n readonly purpose?: string;\n readonly audienceAllow?: readonly string[];\n readonly dependsOn?: readonly string[];\n /** Defaults to the server config's `defaultTtlMs`. */\n readonly ttlMs?: number;\n readonly deadlineAt?: number;\n /** Caller-supplied id, for idempotent enqueue. */\n readonly id?: string;\n}\n\n/**\n * What the *store* is given — the sealed form.\n *\n * Distinct from {@link EnqueueInput} because the two are genuinely different\n * things: an app hands over work in plaintext, and what gets written down is\n * sealed. Collapsing them into one type would mean a field that is sometimes\n * readable and sometimes not, which is the kind of ambiguity that ends with\n * plaintext in a database.\n */\nexport interface StoredJobInput extends Omit<EnqueueInput, \"payload\" | \"id\"> {\n readonly id: string;\n readonly envelope: SealedEnvelope;\n readonly sizeClass: SizeClass;\n}\n\n/**\n * When a job's ciphertext stops being worth carrying — cloud_008 §31.\n *\n * One function because it was two expressions. The direct plane computed\n * `job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs`; the cloud lane\n * computed `record.deadlineAt ?? record.createdAt + <a local constant>`. The\n * first branch agreed and the fallback did not, so a job with no explicit\n * deadline got two different ones depending on which lane published it — and\n * the difference is largest exactly where it matters, for a job blocked on a\n * dependency, whose `claimableAt` may be hours after `createdAt`.\n *\n * The TTL clock starts when a job becomes *claimable*, which is the rule\n * `DEPENDS_ON_GATING` and `TTL_EXPIRY` already share: a dependent job must not\n * spend its life waiting for its dependency.\n */\nexport function deadlineFor(\n job: Pick<JobRecord, \"deadlineAt\" | \"claimableAt\" | \"ttlMs\">,\n now: number,\n): number {\n return job.deadlineAt ?? (job.claimableAt ?? now) + job.ttlMs;\n}\n","import {\n ENDPOINTS,\n ERROR_STATUS,\n PROTOCOL_PREFIX,\n checkProtocolVersion,\n type Endpoint,\n} from \"@byollm/protocol\";\nimport { ByollmHandlers, type HandlerConfig } from \"./handlers.js\";\n\n/**\n * Largest protocol request body accepted, before schema validation.\n *\n * A payload is capped at 4 MB of text by the protocol; this leaves room for\n * JSON overhead and a batch of results, and refuses anything wilder at the\n * door rather than after parsing it.\n */\nconst MAX_BODY_BYTES = 8 * 1024 * 1024;\n\n/**\n * Where the protocol endpoints are mounted.\n *\n * Defaults to {@link PROTOCOL_PREFIX}. Pass the real mount point when it is\n * anything else — a Next.js route at `app/api/byollm/[...route]/route.ts`\n * serves `/api/byollm/...`, so it needs `basePath: \"/api/byollm\"`.\n *\n * @throws if the path is not an absolute, single-segment-per-slash path. A\n * mount point is configuration, and a malformed one should fail at startup\n * rather than silently match nothing.\n */\nfunction normalizeBasePath(basePath: string): string {\n const trimmed = basePath.endsWith(\"/\") ? basePath.slice(0, -1) : basePath;\n if (!trimmed.startsWith(\"/\")) {\n throw new Error(`basePath must start with \"/\": got ${basePath}`);\n }\n if (trimmed.includes(\"//\") || /[?#*]/.test(trimmed)) {\n throw new Error(`basePath must be a plain path: got ${basePath}`);\n }\n return trimmed;\n}\n\n/**\n * Pull the endpoint name out of a URL path, or null if it isn't ours.\n *\n * The full path must match `<basePath>/<endpoint>` exactly. This used to\n * compare only the *last* segment, which meant `/anything/at/all/claim`\n * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it\n * appeared in a 404 message and was never matched against. For the handler\n * that serves claim, result and heartbeat, dispatching on a suffix is a\n * looser rule than anyone reading the constant would assume, and loose\n * matching in a security surface should at least be a decision.\n *\n * The cost is that the mount point is now something a deployment has to state\n * rather than something that works by accident. That is the intended trade:\n * a 404 at startup naming the mount point beats a handler answering on paths\n * nobody meant to expose.\n */\nexport function routeEndpoint(\n pathname: string,\n basePath: string = PROTOCOL_PREFIX,\n): Endpoint | null {\n const base = normalizeBasePath(basePath);\n const path = pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n if (!path.startsWith(`${base}/`)) return null;\n const rest = path.slice(base.length + 1);\n return (ENDPOINTS as readonly string[]).includes(rest)\n ? (rest as Endpoint)\n : null;\n}\n\n/**\n * Read the request signature from headers (byollm_009 §4.2).\n *\n * In headers rather than the body so the signature covers the body whole,\n * with no field to exclude from its own hash — a scheme that signs a body\n * minus one field has to agree, byte for byte, on how that field is removed.\n */\nexport function signatureFrom(headers: Headers): unknown {\n const runnerId = headers.get(\"x-byollm-runner\");\n const rawIssuedAt = headers.get(\"x-byollm-issued-at\");\n const signature = headers.get(\"x-byollm-signature\");\n if (runnerId === null || signature === null || rawIssuedAt === null) {\n return undefined;\n }\n // Checked against null *before* Number(), because `Number(null)` is 0 —\n // finite, plausible-looking, and wrong. A missing timestamp would have\n // become a timestamp of the epoch, which the freshness check would then\n // reject for the wrong reason.\n const issuedAt = Number(rawIssuedAt);\n if (!Number.isFinite(issuedAt)) return undefined;\n return { runnerId, issuedAt, signature };\n}\n\n/**\n * A `Request` → `Response` handler for the whole protocol.\n *\n * Web-standard types, so this works unchanged in Next.js route handlers, Hono,\n * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.\n */\nexport function createFetchHandler(\n config: HandlerConfig & {\n /**\n * Where these endpoints are mounted. Defaults to\n * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere.\n */\n readonly basePath?: string;\n },\n): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\n // Validate once, at construction: a bad mount point is a deployment bug and\n // should surface when the server starts, not as a silent 404 per request.\n const basePath = normalizeBasePath(config.basePath ?? PROTOCOL_PREFIX);\n\n return async function handle(request: Request): Promise<Response> {\n if (request.method !== \"POST\") {\n return json(405, {\n error: \"bad-request\",\n message: \"protocol endpoints accept POST only\",\n });\n }\n\n const endpoint = routeEndpoint(new URL(request.url).pathname, basePath);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${basePath} endpoint`,\n });\n }\n\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n\n let body: unknown;\n let rawBody: string;\n try {\n rawBody = await request.text();\n const text = rawBody;\n if (text.length > MAX_BODY_BYTES) {\n return json(400, {\n error: \"bad-request\",\n message: \"request body too large\",\n });\n }\n body = JSON.parse(text);\n } catch {\n // Deliberately not echoing the parse error: it would quote attacker\n // input back into a response an operator later reads in a terminal.\n return json(400, {\n error: \"bad-request\",\n message: \"request body is not valid JSON\",\n });\n }\n\n // byollm_009 §4: version before anything else. A mismatch must name the\n // disagreement and the fix, not surface as a generic bad-request from a\n // schema literal buried in an endpoint — which is what happened before,\n // and is why \"the connection is versionless\" was listed as a defect.\n const refusal = checkProtocolVersion(body);\n if (refusal) {\n return json(ERROR_STATUS[refusal.error], refusal);\n }\n\n const result = await handlers.handle(endpoint, body, {\n endpoint,\n // The bytes as received. Re-serialising the parsed object would verify\n // a signature over something the sender never sent.\n rawBody,\n signature: signatureFrom(request.headers),\n });\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n };\n if (result.retryAfterSeconds !== undefined) {\n headers[\"retry-after\"] = String(result.retryAfterSeconds);\n }\n return new Response(JSON.stringify(result.body), {\n status: result.status,\n headers,\n });\n };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n"],"mappings":"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","names":["keyId","open","publicIdentityOf","publicIdentityOf","keyId","open","ERROR_STATUS","ERROR_STATUS"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { StoredKeys, JobKind, DeliveredResult, Endpoint, Capability } from '@byollm/protocol';
|
|
1
|
+
import { StoredKeys, JobKind, Audience, DeliveredResult, Endpoint, Capability } from '@byollm/protocol';
|
|
2
2
|
import { P as PollingDeliveryDeps, R as ResultDelivery, W as WaitOptions } from './delivery-C6VzgMgH.js';
|
|
3
3
|
export { N as NoRunnerAvailableError, a as PollingDelivery, b as ResultTimeoutError } from './delivery-C6VzgMgH.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-
|
|
5
|
-
export { g as CompleteHolder, h as JobStore, i as RunnerStore } from './store-
|
|
6
|
-
import { H as HandlerConfig } from './handlers-
|
|
7
|
-
export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-
|
|
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-DCgYMftk.js';
|
|
5
|
+
export { g as CompleteHolder, h as JobStore, i as RunnerStore } from './store-DCgYMftk.js';
|
|
6
|
+
import { H as HandlerConfig } from './handlers-BvVqALXO.js';
|
|
7
|
+
export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-BvVqALXO.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The cloud lane — cloud_004 §9.4.
|
|
@@ -132,33 +132,6 @@ declare class CloudLane {
|
|
|
132
132
|
|
|
133
133
|
/** Why a job cannot presently run. */
|
|
134
134
|
type NoRunnerReason = "no-runner-paired" | "no-runner-online" | "no-matching-capability" | "audience-admits-nobody"
|
|
135
|
-
/**
|
|
136
|
-
* A named service cannot serve this requester — byollm_016 Phase B.
|
|
137
|
-
*
|
|
138
|
-
* **One reason for two causes, and the collapse is the point.** The name may
|
|
139
|
-
* be one nobody advertises, or one advertised and not offered to this
|
|
140
|
-
* person. Those are different facts and a requester may learn neither,
|
|
141
|
-
* because telling them apart turns the advisory into an inventory oracle:
|
|
142
|
-
* try names, sort the answers, enumerate a device you were never offered.
|
|
143
|
-
*
|
|
144
|
-
* My first version split them, on the argument that this answers a site
|
|
145
|
-
* about its own users' devices. That argument does not survive a team job.
|
|
146
|
-
* The site holds capabilities for devices belonging to *other people* on the
|
|
147
|
-
* roster, and it is free to relay a reason to the requester who asked — so
|
|
148
|
-
* the split leaks through a side door rather than the front one, which is
|
|
149
|
-
* how the collapse in `RefusalReason` gets defeated by a helpful SDK.
|
|
150
|
-
*
|
|
151
|
-
* The finer cause stays owner-side, where `byollm services` reports it to
|
|
152
|
-
* the person who already knows what their machine runs.
|
|
153
|
-
*/
|
|
154
|
-
| "selection-unavailable"
|
|
155
|
-
/**
|
|
156
|
-
* A kind two services answer with no default chosen, so the daemon
|
|
157
|
-
* withholds it — byollm_016. Nothing is wrong with the device; its owner has
|
|
158
|
-
* a decision to make, and saying "no matching capability" would send them
|
|
159
|
-
* looking for a missing install instead.
|
|
160
|
-
*/
|
|
161
|
-
| "awaiting-default"
|
|
162
135
|
/**
|
|
163
136
|
* The owner's default for this kind can never serve *this* requester —
|
|
164
137
|
* byollm_016's defaults-meet-audiences corner.
|
|
@@ -188,9 +161,7 @@ interface RunnerAvailability {
|
|
|
188
161
|
interface AvailabilityQuery {
|
|
189
162
|
readonly kind: JobKind;
|
|
190
163
|
readonly owner: string;
|
|
191
|
-
|
|
192
|
-
readonly service?: string;
|
|
193
|
-
readonly audience?: "private" | "team" | "public";
|
|
164
|
+
readonly audience?: Audience;
|
|
194
165
|
readonly audienceAllow?: readonly string[];
|
|
195
166
|
}
|
|
196
167
|
interface ByollmAppOptions {
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
routeEndpoint,
|
|
14
14
|
secretsMatch,
|
|
15
15
|
signatureFrom
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-FQXTIQDP.js";
|
|
17
17
|
import {
|
|
18
18
|
NoRunnerAvailableError,
|
|
19
19
|
PollingDelivery,
|
|
@@ -102,7 +102,7 @@ var CloudLane = class {
|
|
|
102
102
|
// enforce — the daemon's own allowlist (`NAMED_LOCAL_ALLOWLIST`) and the
|
|
103
103
|
// backend's offer scope. On this lane the relay narrows too, from the
|
|
104
104
|
// control plane's rosters. The enforcement was never here.
|
|
105
|
-
...record.
|
|
105
|
+
...record.purpose === void 0 ? {} : { purpose: record.purpose },
|
|
106
106
|
sizeClass: record.sizeClass,
|
|
107
107
|
streaming: false,
|
|
108
108
|
// The relay needs *a* deadline to bound routing. A job without one gets
|
|
@@ -358,7 +358,7 @@ var ENQUEUE_OPTIONS = Object.freeze({
|
|
|
358
358
|
payload: true,
|
|
359
359
|
owner: true,
|
|
360
360
|
audience: true,
|
|
361
|
-
|
|
361
|
+
purpose: true,
|
|
362
362
|
audienceAllow: true,
|
|
363
363
|
dependsOn: true,
|
|
364
364
|
ttlMs: true,
|
|
@@ -524,58 +524,51 @@ var ByollmApp = class {
|
|
|
524
524
|
}
|
|
525
525
|
let capable = 0;
|
|
526
526
|
let admitted = 0;
|
|
527
|
-
let withheldSomewhere = 0;
|
|
528
527
|
let lastRefusal;
|
|
529
528
|
for (const runner of live) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
529
|
+
for (const capability of runner.capabilities.filter(
|
|
530
|
+
(c) => c.kind === query.kind
|
|
531
|
+
)) {
|
|
532
|
+
capable += 1;
|
|
533
|
+
const match = matchAudience(
|
|
534
|
+
{
|
|
535
|
+
owner: query.owner,
|
|
536
|
+
audience: query.audience ?? "private",
|
|
537
|
+
audienceAllow: query.audienceAllow
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
owner: runner.owner,
|
|
541
|
+
offerScope: capability.offerScope,
|
|
542
|
+
// A generic backend's cost depends on its base URL, which the
|
|
543
|
+
// server never sees; assume the expensive reading (byollm_007 §4).
|
|
544
|
+
cost: backendDescriptor(capability.backendId).cost ?? "metered",
|
|
545
|
+
// Consent is the daemon's to hold, and it has already applied it:
|
|
546
|
+
// the offer scope arriving here is the *effective* one, so a
|
|
547
|
+
// metered backend nobody agreed to share advertises `self` and is
|
|
548
|
+
// refused by the scope rule above. Re-deriving consent from
|
|
549
|
+
// `false` here would instead refuse every backend an owner
|
|
550
|
+
// deliberately shared, because the server has no way to learn they
|
|
551
|
+
// did — the signal would be wrong in the direction that breaks
|
|
552
|
+
// working setups.
|
|
553
|
+
spend: { acknowledged: true },
|
|
554
|
+
// Same conservative assumption the claim path makes: the server
|
|
555
|
+
// cannot see a remote daemon's local allowlist (protocol §4.2).
|
|
556
|
+
admits: () => true
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
if (match.ok) admitted += 1;
|
|
560
|
+
else lastRefusal = match.refusal;
|
|
534
561
|
}
|
|
535
|
-
if (!capability) continue;
|
|
536
|
-
capable += 1;
|
|
537
|
-
const match = matchAudience(
|
|
538
|
-
{
|
|
539
|
-
owner: query.owner,
|
|
540
|
-
audience: query.audience ?? "private",
|
|
541
|
-
audienceAllow: query.audienceAllow
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
owner: runner.owner,
|
|
545
|
-
offerScope: capability.offerScope,
|
|
546
|
-
// A generic backend's cost depends on its base URL, which the
|
|
547
|
-
// server never sees; assume the expensive reading (byollm_007 §4).
|
|
548
|
-
cost: backendDescriptor(capability.backendId).cost ?? "metered",
|
|
549
|
-
// Consent is the daemon's to hold, and it has already applied it:
|
|
550
|
-
// the offer scope arriving here is the *effective* one, so a
|
|
551
|
-
// metered backend nobody agreed to share advertises `self` and is
|
|
552
|
-
// refused by the scope rule above. Re-deriving consent from
|
|
553
|
-
// `false` here would instead refuse every backend an owner
|
|
554
|
-
// deliberately shared, because the server has no way to learn they
|
|
555
|
-
// did — the signal would be wrong in the direction that breaks
|
|
556
|
-
// working setups.
|
|
557
|
-
spend: { acknowledged: true },
|
|
558
|
-
// Same conservative assumption the claim path makes: the server
|
|
559
|
-
// cannot see a remote daemon's local allowlist (protocol §4.2).
|
|
560
|
-
locallyAllows: () => true
|
|
561
|
-
}
|
|
562
|
-
);
|
|
563
|
-
if (match.ok) admitted += 1;
|
|
564
|
-
else lastRefusal = match.refusal;
|
|
565
562
|
}
|
|
566
563
|
if (capable === 0) {
|
|
567
|
-
|
|
568
|
-
|
|
564
|
+
return {
|
|
565
|
+
available: false,
|
|
566
|
+
reason: "no-matching-capability",
|
|
567
|
+
candidates: 0
|
|
568
|
+
};
|
|
569
569
|
}
|
|
570
570
|
if (admitted === 0) {
|
|
571
571
|
const ownersDoing = lastRefusal === "offer-scope-too-narrow" || lastRefusal === "subscription-self-lock" || lastRefusal === "metered-no-spend-consent" || lastRefusal === "metered-ceiling-reached";
|
|
572
|
-
if (query.service !== void 0) {
|
|
573
|
-
return {
|
|
574
|
-
available: false,
|
|
575
|
-
reason: "selection-unavailable",
|
|
576
|
-
candidates: 0
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
572
|
return {
|
|
580
573
|
available: false,
|
|
581
574
|
reason: ownersDoing ? "default-unusable" : "audience-admits-nobody",
|
|
@@ -723,7 +716,7 @@ var MemoryStore = class {
|
|
|
723
716
|
envelope: input.envelope,
|
|
724
717
|
sizeClass: input.sizeClass,
|
|
725
718
|
audience: input.audience ?? "private",
|
|
726
|
-
|
|
719
|
+
purpose: input.purpose,
|
|
727
720
|
owner: input.owner,
|
|
728
721
|
audienceAllow: input.audienceAllow ? [...input.audienceAllow] : void 0,
|
|
729
722
|
dependsOn,
|
|
@@ -807,7 +800,7 @@ var MemoryStore = class {
|
|
|
807
800
|
// not pretend to (protocol §4.2). It admits the job here; the daemon
|
|
808
801
|
// is the enforcing side and releases with `refused` if its own list
|
|
809
802
|
// says no.
|
|
810
|
-
|
|
803
|
+
admits: () => true
|
|
811
804
|
}
|
|
812
805
|
);
|
|
813
806
|
return match.ok;
|