import { StoredKeys, JobKind, Audience, DeliveredResult, Endpoint, Capability } from '@byollm/protocol'; import { P as PollingDeliveryDeps, R as ResultDelivery, W as WaitOptions } from './delivery-C5mz_Ykm.js'; export { N as NoRunnerAvailableError, a as PollingDelivery, b as ResultTimeoutError } from './delivery-C5mz_Ykm.js'; 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-BkDHRir0.js'; export { g as CompleteHolder, h as JobStore, i as RunnerStore } from './store-BkDHRir0.js'; import { H as HandlerConfig } from './handlers-D9ngfDB-.js'; export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-D9ngfDB-.js'; /** * The cloud lane — cloud_004 §9.4. * * `app.enqueue(...)` is identical in every lane; the lane picks the connection * plane. In `direct` mode a daemon reaches the site's own handlers. In `cloud` * mode it reaches a relay instead, and the site's side of that is this file. * * ## What actually changes, and what deliberately does not * * Enqueue does not change at all. The job is validated, sealed at rest to the * site's own key and stored, exactly as before — jobs-at-rest encryption is a * direct-mode property that the cloud lane inherits rather than replaces. * * What changes is *who asks for the payload and when*. On the direct plane the * daemon asks, and the site answers synchronously because it is the upstream. * Through a relay the site is not the upstream, so nobody asks: the site has to * find out that a device claimed its job, and seal to that device. Hence a * pump rather than a handler. * * ``` * enqueue ──stub──▶ relay (payload stays here, sealed at rest) * │ * pump ◀──who claimed it, and what key? * ──payload sealed to that device──▶ * pump ◀──sealed result── ──▶ store.complete → the app's delivery channel * ``` * * ## Why the site polls * * Everything in this product is outbound. A relay that called site webhooks * would need every site publicly reachable, which is the connectivity problem * the hub exists to remove — and a serverless site has nowhere to receive a * webhook anyway. So the site polls, exactly as a daemon does. */ interface CloudLaneOptions { /** * Where the relay lives, e.g. `https://hub.byollm.cloud`. * * This said `relay.byollm.cloud`, and that name **has no DNS record**. * The comment ships in the published `.d.ts`, so it is among the first * examples a site author reads: Kevin's team copied the dead host into * every app's `.env.example` and then worked out why nothing connected. * * The old name is written here without a scheme on purpose, so that * `the-links-we-ship-resolve.mjs` does not report this explanation as one * more dead host. */ readonly relayOrigin: string; /** This site's id at the relay. */ readonly siteId: string; /** Injectable fetch, for tests and for proxies. */ readonly fetch?: typeof fetch; } /** What one pump cycle did, for logging and for tests. */ /** * A relay that could not answer this request — alpha.31. * * `retryable` is the whole point: a draining pod and a bad signature are both * failures, and treating them alike is how a site either falls over on every * deploy or stays silently disconnected for a week. */ declare class RelayUnavailable extends Error { readonly retryable: boolean; /** The protocol's own code, when the relay sent one. */ readonly code: string; constructor(message: string, retryable: boolean, code: string); } /** * The job was not queued, and waiting will not change that. * * Distinct from {@link RelayUnavailable} because it is the opposite situation: * the relay answered, promptly and correctly, and the answer is that this job * has nowhere to go. Catching "the relay is down" to handle "nobody has chosen * a model" would retry forever against a fact. * * Three codes, and they belong to different people. * * `purpose-not-declared` is the site's own manifest. It names the purpose and * the remedy, because a developer reading their own logs is entitled to both * and neither says anything about a person. * * `slot-unsatisfiable` is the person's own dashboard, and says only that. * Which service, whose device, whether one exists at all — none of it travels, * and the sentence is the same for everybody. A site learns *that* a slot * cannot be satisfied, which is exactly what the README has always promised * and what this class finally delivers. * * `slot-waiting` is the one bit beyond that a site may learn, and it answers a * different question: **does this need the person, or only time.** * `slot-unsatisfiable` means somebody has to go and choose a model, and no * amount of waiting helps. `slot-waiting` means the slot may recover with * nobody acting — device asleep, service withdrawn, account blocked all * arrive as one sentence, which is what makes the bit safe to give. It is * about the slot's future, not the person's day. * * **This said "two codes" for the release that shipped the third.** The relay * has answered `slot-waiting` since 019 §6.3 and nothing on this side named * it, so a site reading the SDK's own contract would have branched on two of * three — see `the-refusals-we-name.test.ts`, which compares this list against * what the relay can actually send. */ declare class EnqueueRefused extends Error { /** `purpose-not-declared`, `slot-unsatisfiable` or `slot-waiting`. */ readonly code: string; constructor(message: string, code: string); } interface PumpReport { /** Jobs sealed to a claiming device this cycle. */ readonly sealed: string[]; /** Results opened, verified and written to the store. */ readonly completed: string[]; /** * Jobs the relay offered that this site refused to seal for. * * Never silent: a site that cannot open its own at-rest envelope has a key * problem, and a device waiting on a payload that will never come is * exactly the case `awaiting-payload` exists to bound. */ readonly refused: string[]; /** * Why this cycle stopped early, when it did — alpha.31. * * A relay can legitimately say "ask me later": a pod draining through its * `preStop` window answers `503 not-ready` to every routed call, and that * happens on **every deploy**. Before this existed the lane read the body * of that answer, found no `jobs` in it, and threw `TypeError: finished.jobs * is not iterable` — a site falling over because its relay was polite. * * Absent on an ordinary cycle. Present, with the reason, when the lane * deferred: a site that quietly did nothing and a site that was told to wait * must not look the same in a log. */ readonly deferred?: string; } declare class CloudLane { #private; constructor(deps: { options: CloudLaneOptions; store: ByollmStore; siteKeys: StoredKeys; now: () => number; }); /** * Publish a job's stub for routing. * * The stub and nothing else — byollm_009 §6 makes that exhaustive by * construction, so this cannot leak a payload even by mistake: there is no * field on `JobStub` to put one in. */ publish(record: JobRecord): Promise; /** * Withdraw a job at the relay — cloud_008 §2.2. * * `app.cancel()` marks the site's own row terminal, which stops the *next* * seal. It cannot stop a device that is already running the work, because * on this lane the site is not the upstream: only the relay talks to the * daemon, and it answered `cancel: []` unconditionally. * * So the cancellation has to travel. The relay marks the job, stops * offering it, and names it to the holding device at its next heartbeat — * the same path the direct plane has always had, arriving one hop later. */ cancel(jobId: string): Promise; /** * One cycle: seal for anything claimed, collect anything finished. * * Idempotent and safe to call as often as you like. Exposed as a single * cycle rather than hidden behind a timer so a caller decides its own * cadence — a serverless site runs it on a cron, a long-lived one on an * interval, and a test runs it exactly when it means to. */ pump(): Promise; } /** Why a job cannot presently run. */ type NoRunnerReason = "no-runner-paired" | "no-runner-online" | "no-matching-capability" | "audience-admits-nobody" /** * The owner's default for this kind can never serve *this* requester — * byollm_016's defaults-meet-audiences corner. * * The specimen: a default of `claude-cli`, self-locked by * `SUBSCRIPTION_SELF_LOCK`, and a team member's unselected job. It resolves * to something that will never run it. Reported rather than left to time * out, because a wait that can never end is indistinguishable from one that * has not ended yet, and only one of them is worth waiting through. */ | "default-unusable"; /** * The no-runner signal (byollm_001 Rev 1 §D). * * `available: false` means an app should fall back — hosted model, "start * your runner" prompt — rather than awaiting something that will never * resolve. A job still blocked on dependencies is **not** unavailable; it is * waiting, and saying otherwise would make every multi-job flow look broken * ({@link MUSTS.NO_RUNNER_SIGNAL}). */ interface RunnerAvailability { readonly available: boolean; readonly reason?: NoRunnerReason; /** Live runners that could take work of this shape. */ readonly candidates: number; } interface AvailabilityQuery { readonly kind: JobKind; readonly owner: string; readonly audience?: Audience; readonly audienceAllow?: readonly string[]; } interface ByollmAppOptions { readonly store: ByollmStore; /** Injectable clock. */ readonly now?: () => number; /** Liveness window for the no-runner signal. */ readonly livenessMs?: number; /** * How the app learns a job finished. Defaults to polling the store, which * is correct everywhere; the Supabase adapter substitutes Realtime. */ readonly delivery?: (deps: PollingDeliveryDeps) => ResultDelivery; /** * How long a sustained no-runner signal must persist before `result()` * gives up. Longer tolerates a daemon restarting; shorter fails faster. */ readonly noRunnerGraceMs?: number; /** * This site's keypairs — the same ones the handlers use. * * The app needs them because it is the *endpoint*: it seals work on the way * in and opens results on the way out. Nothing between those two points * holds plaintext (byollm_009 §10). */ readonly siteKeys: StoredKeys; /** * Which connection plane this site uses — cloud_004 §9.4. * * Omitted means `direct`: a daemon reaches this site's own handlers, and * everything works as it always has. Supplying a relay switches the plane * and nothing else — `enqueue` is identical in every lane, which is the * property that lets the same app move between them by config. */ readonly lane?: CloudLaneOptions; } /** * An enqueued job, with the delivery channel attached. * * `result()` is sugar over the channel — with a timeout and a * `noRunnerAvailable` path — never a bare promise that can hang forever * (byollm_003 Rev 1). */ interface JobHandle { readonly id: string; /** The job as stored at enqueue time. */ readonly record: JobRecord; /** Wait for a terminal outcome. */ result(options?: WaitOptions): Promise; /** Ask the runner to stop. */ cancel(): Promise; } declare class ByollmApp { #private; /** Present only in the cloud lane; the site's side of the relay. */ readonly cloud: CloudLane | undefined; constructor(options: ByollmAppOptions); /** * Enqueue a job. * * `audience` defaults to `private` — the safe direction. Widening it means the * result comes back marked untrusted (see {@link ByollmApp.result}), and * the app is obliged to disclose that to whoever reads it. */ enqueue(input: EnqueueInput): Promise; /** Read a job's current state. */ job(jobId: string): Promise; /** * A job's result with its provenance attached. * * Check `provenance.untrusted` before rendering. It is true for every * `team` job, because that text came from someone else's machine * and the app must not present it as its own AI's answer * ({@link MUSTS.PROVENANCE_NAMES_DEVICE}). */ result(jobId: string): Promise; /** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */ cancel(jobId: string): Promise; /** * Is there a live runner that could take a job of this shape? * * Runs the identical {@link matchAudience} rule the claim path uses, so the * signal cannot promise a runner the claim would then refuse. */ runnerAvailability(query: AvailabilityQuery): Promise; /** * Approve a pairing on behalf of an authenticated user. * * `owner` MUST come from the approving user's own session. A daemon can * never assert who it is — that is the whole reason pairing is interactive * ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}). */ approvePairing(args: { userCode: string; owner: string; }): Promise; /** Deny a pairing the user did not initiate. */ denyPairing(userCode: string): Promise; /** What a pairing code refers to, for the approval page to show. */ pendingPairing(userCode: string): Promise<{ label: string; platform: string; daemonVersion: string; capabilities: readonly { kind: string; model: string; }[]; expiresAt: number; } | null>; /** The user's paired runners, for a settings page. */ runners(owner: string): Promise; /** Revoke a runner. It stops at its next heartbeat, mid-queue. */ revokeRunner(runnerId: string): Promise; /** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */ sweep(): Promise; } /** * Accept a pairing code however the user typed it — lowercase, spaces, no * dash. The code is displayed as `XXXX-XXXX`; refusing `xxxxxxxx` would fail * a user for a formatting detail they were never told mattered. */ declare function normalizeUserCode(input: string): string; /** * Pull the endpoint name out of a URL path, or null if it isn't ours. * * The full path must match `/` exactly. This used to * compare only the *last* segment, which meant `/anything/at/all/claim` * dispatched to `claim` and {@link PROTOCOL_PREFIX} was decorative — it * appeared in a 404 message and was never matched against. For the handler * that serves claim, result and heartbeat, dispatching on a suffix is a * looser rule than anyone reading the constant would assume, and loose * matching in a security surface should at least be a decision. * * The cost is that the mount point is now something a deployment has to state * rather than something that works by accident. That is the intended trade: * a 404 at startup naming the mount point beats a handler answering on paths * nobody meant to expose. */ declare function routeEndpoint(pathname: string, basePath?: string): Endpoint | null; /** * Read the request signature from headers (byollm_009 §4.2). * * In headers rather than the body so the signature covers the body whole, * with no field to exclude from its own hash — a scheme that signs a body * minus one field has to agree, byte for byte, on how that field is removed. */ declare function signatureFrom(headers: Headers): unknown; /** * A `Request` → `Response` handler for the whole protocol. * * Web-standard types, so this works unchanged in Next.js route handlers, Hono, * Bun, Deno, Cloudflare Workers, and anything else that speaks fetch. */ declare function createFetchHandler(config: HandlerConfig & { /** * Where these endpoints are mounted. Defaults to * {@link PROTOCOL_PREFIX}; set it when the app serves them elsewhere. */ readonly basePath?: string; }): (request: Request) => Promise; /** * A site's keypairs — byollm_009 §5. * * **Generate once, store, supply.** Not at startup, and not per process. * * A site is usually more than one process: several instances behind a load * balancer, or a serverless function whose module is evaluated per cold * start. Keys generated at startup would give each of those a different * identity. A daemon pins whichever one approved its pairing, and then every * request routed to a different instance fails a signature check with nothing * in the error explaining why — a failure that appears only under * horizontal scale, which is to say only in production. * * So the library takes keys as an input and never invents them. That is the * whole reason this module is three functions rather than a lazy singleton. */ /** Make a fresh site identity. Call this once, ever, and keep the result. */ declare const generateSiteKeys: (now?: number) => StoredKeys; /** * Read site keys from an environment variable holding base64 JSON. * * The shape a deployment actually wants: one opaque secret, set the way every * other secret is set, with no file to mount and no key material in the * repository. * * @throws with a message naming the variable and the fix, because this fails * at boot and the person reading the log is the person who can fix it. */ declare function siteKeysFromEnv(variable?: string, env?: NodeJS.ProcessEnv): StoredKeys; /** What to print from `keygen`: the secret to store, and how to check it. */ declare function formatSiteKeys(keys: StoredKeys): string; /** A device code: the secret the daemon polls with. Never shown to a user. */ declare function generateDeviceCode(): string; /** A runner id. */ declare function generateRunnerId(): string; /** A job id. */ declare function generateJobId(): string; /** * A short code the user reads and confirms, formatted `XXXX-XXXX`. * Drawn with rejection sampling so the alphabet stays uniform. */ declare function generateUserCode(): string; /** SHA-256, hex. Tokens and device codes are stored only as this. */ declare function hashSecret(secret: string): string; /** * Compare two hex digests without leaking their difference through timing. * Lengths are compared first because `timingSafeEqual` throws on a mismatch. */ declare function secretsMatch(aHex: string, bHex: string): boolean; /** Tunables an embedder may want to override in tests. */ interface MemoryStoreOptions { /** Default TTL for a job once claimable. */ readonly defaultTtlMs?: number; } /** * The reference store: everything in one process, no persistence. * * This is not a toy — it is the implementation the conformance kit certifies * first, so its semantics *are* the specification's semantics for anything * the prose leaves implicit. A SQL adapter is correct when the same kit * passes against it. * * Concurrency: JavaScript's single-threaded turn is the atomicity primitive. * `claim` performs its read-decide-write with no `await` inside the critical * section, which is what makes {@link MUSTS.CLAIM_ATOMIC} hold here. A SQL * adapter gets the same property from `FOR UPDATE SKIP LOCKED`. */ declare class MemoryStore implements ByollmStore { #private; constructor(options?: MemoryStoreOptions); create(input: StoredJobInput, now: number): Promise; get(jobId: string): Promise; claim(args: ClaimArgs): Promise; renewLeases(args: RenewArgs): Promise; adopt(args: AdoptArgs): Promise; complete(args: CompleteArgs): Promise; subscribe(jobId: string, onChange: () => void): () => void; release(args: ReleaseArgs): Promise; expireDue(now: number): Promise; cancel(jobId: string, now: number): Promise; listClaimedBy(runnerId: string): Promise; listCancelRequests(runnerId: string): Promise<{ jobId: string; leaseId: string; }[]>; createPairing(record: PairingRecord): Promise; getPairingByDeviceCodeHash(hash: string): Promise; getPairingByUserCode(userCode: string): Promise; approvePairing(args: ApproveArgs): Promise; denyPairing(userCode: string, _now: number): Promise; consumePairingToken(deviceCodeHash: string): Promise; getRunner(runnerId: string): Promise; touchRunner(args: TouchArgs): Promise; revokeRunner(runnerId: string, now: number): Promise; listRunners(owner?: string): Promise; /** All jobs, for demos and assertions. Not part of the store interface. */ allJobs(): JobRecord[]; } /** The capability that would serve a kind, if any. */ declare function capabilityFor(capabilities: readonly Capability[], kind: string): Capability | undefined; export { AdoptArgs, ApproveArgs, type AvailabilityQuery, ByollmApp, type ByollmAppOptions, ByollmStore, ClaimArgs, CloudLane, type CloudLaneOptions, CompleteArgs, CompleteResult, EnqueueInput, EnqueueRefused, HandlerConfig, type JobHandle, JobRecord, MemoryStore, type MemoryStoreOptions, type NoRunnerReason, PairingRecord, PollingDeliveryDeps, type PumpReport, RelayUnavailable, ReleaseArgs, RenewArgs, RenewResult, ResultDelivery, type RunnerAvailability, RunnerRecord, TouchArgs, WaitOptions, capabilityFor, createFetchHandler, formatSiteKeys, generateDeviceCode, generateJobId, generateRunnerId, generateSiteKeys, generateUserCode, hashSecret, normalizeUserCode, routeEndpoint, secretsMatch, signatureFrom, siteKeysFromEnv };