@byollm/server 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/chunk-7RKXFPBZ.js +72 -0
- package/dist/chunk-7RKXFPBZ.js.map +1 -0
- package/dist/chunk-HL6EYHQ7.js +422 -0
- package/dist/chunk-HL6EYHQ7.js.map +1 -0
- package/dist/delivery-36nIe-b3.d.ts +73 -0
- package/dist/handlers-D7lWfwno.d.ts +52 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +634 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +33 -0
- package/dist/next.js +17 -0
- package/dist/next.js.map +1 -0
- package/dist/store-D23N6iiP.d.ts +255 -0
- package/dist/supabase/index.d.ts +30 -0
- package/dist/supabase/index.js +414 -0
- package/dist/supabase/index.js.map +1 -0
- package/package.json +52 -0
- package/supabase/migrations/20260809000000_byollm_runner.sql +507 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ids.ts","../src/handlers.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 bearer token. */\nexport function generateRunnerToken(): 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 return `job_${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","import {\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 type { RunnerRecord } from \"./records.js\";\nimport type { ByollmStore } from \"./store.js\";\n\n/** Everything a mount needs to serve the protocol. */\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\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\n constructor(config: HandlerConfig) {\n this.#store = config.store;\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 bearer - the `Authorization: Bearer` value, if any\n */\n async handle(\n endpoint: Endpoint,\n body: unknown,\n bearer: string | undefined,\n ): Promise<HandlerResult> {\n switch (endpoint) {\n case \"pair\":\n return this.#pair(body);\n case \"claim\":\n return this.#authed(bearer, body, ClaimRequest, this.#claim.bind(this));\n case \"heartbeat\":\n // Heartbeat is the channel revocation travels on, so a revoked runner\n // must reach the handler and be told `revoked: true` rather than be\n // bounced with a 403 it would treat as a transport problem\n // ({@link MUSTS.REVOCATION_HONORED}).\n return this.#authed(\n bearer,\n body,\n HeartbeatRequest,\n this.#heartbeat.bind(this),\n { allowRevoked: true },\n );\n case \"result\":\n return this.#authed(\n bearer,\n body,\n ResultRequest,\n this.#result.bind(this),\n );\n case \"release\":\n return this.#authed(\n bearer,\n body,\n ReleaseRequest,\n this.#release.bind(this),\n );\n }\n }\n\n /**\n * Shared preamble for the four authenticated endpoints: resolve the bearer\n * token to a runner, reject a revoked one, and parse the body.\n *\n * The token→runner lookup happens before schema validation so a stranger\n * probing the endpoint learns nothing about the wire format.\n */\n async #authed<T>(\n bearer: string | undefined,\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 if (bearer === undefined || bearer.length === 0) {\n return fail(\"unauthorized\", \"a runner token is required\");\n }\n const runner = await this.#store.getRunnerByTokenHash(hashSecret(bearer));\n if (!runner) {\n return fail(\"unauthorized\", \"this runner token is not recognised\");\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 // -- 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 await this.#store.createPairing({\n deviceCodeHash: hashSecret(deviceCode),\n userCode,\n state: \"pending\",\n owner: null,\n runnerId: null,\n runnerTokenOnce: null,\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.runnerTokenOnce !== null &&\n pairing.runnerId !== null &&\n pairing.owner !== null\n ) {\n const response: PairPollResponse = {\n status: \"approved\",\n runnerToken: pairing.runnerTokenOnce,\n runnerId: pairing.runnerId,\n owner: pairing.owner,\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 bearer token\");\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 payload: job.payload,\n audience: job.audience,\n owner: job.owner,\n ...(job.audienceAllow === undefined\n ? {}\n : { audienceAllow: [...job.audienceAllow] }),\n lease: job.lease ?? {\n runnerId: runner.id,\n expiresAt: now + this.#leaseMs,\n },\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 bearer token\");\n }\n const now = this.#now();\n const revoked = runner.revokedAt !== null;\n\n if (revoked) {\n // Nothing is renewed for a revoked runner: every job it holds is\n // reported lost so it abandons the queue rather than finishing it.\n const held = await this.#store.listClaimedBy(runner.id);\n const response: HeartbeatResponse = {\n revoked: true,\n cancel: [],\n leases: [],\n lost: held.map((job) => job.id),\n serverTime: now,\n };\n return ok(response);\n }\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 const { renewed, lost } = await this.#store.renewLeases({\n runnerId: runner.id,\n jobIds: request.activeJobIds,\n leaseMs: this.#leaseMs,\n now,\n });\n\n const cancel = await this.#store.listCancelRequests(runner.id);\n\n const response: HeartbeatResponse = {\n revoked: false,\n cancel,\n leases: renewed.map((r) => ({ jobId: r.jobId, expiresAt: r.expiresAt })),\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 bearer token\");\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 // Provenance is built here, from the job's audience and the authenticated\n // runner — never from anything the daemon asserted\n // ({@link MUSTS.RESULT_PROVENANCE}).\n const provenance = provenanceFor({\n audience: job.audience,\n runnerId: runner.id,\n runnerOwner: runner.owner,\n backendClass: request.backendClass,\n model: request.model,\n });\n\n const { accepted, job: updated } = await this.#store.complete({\n jobId: request.jobId,\n runnerId: runner.id,\n outcome: request.outcome,\n provenance,\n now,\n });\n\n const response: ResultResponse = {\n accepted,\n state: updated?.state ?? job.state,\n };\n return ok(response);\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 bearer token\");\n }\n const released = await this.#store.release({\n runnerId: runner.id,\n jobIds: request.jobIds,\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","import { ENDPOINTS, PROTOCOL_PREFIX, type Endpoint } 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/** Pull the endpoint name out of a URL path, or null if it isn't ours. */\nexport function routeEndpoint(pathname: string): Endpoint | null {\n const index = pathname.lastIndexOf(\"/\");\n const last = index === -1 ? pathname : pathname.slice(index + 1);\n return (ENDPOINTS as readonly string[]).includes(last)\n ? (last as Endpoint)\n : null;\n}\n\n/** Read the bearer token from an `Authorization` header. */\nexport function bearerFrom(header: string | null): string | undefined {\n if (!header) return undefined;\n const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());\n return match?.[1];\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): (request: Request) => Promise<Response> {\n const handlers = new ByollmHandlers(config);\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);\n if (endpoint === null) {\n return json(404, {\n error: \"not-found\",\n message: `not a ${PROTOCOL_PREFIX} 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 try {\n const text = await request.text();\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 const result = await handlers.handle(\n endpoint,\n body,\n bearerFrom(request.headers.get(\"authorization\")),\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":["response"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DeliveredResult } from '@byollm/protocol';
|
|
2
|
+
|
|
3
|
+
/** Why a wait ended without a result. */
|
|
4
|
+
declare class NoRunnerAvailableError extends Error {
|
|
5
|
+
readonly jobId: string;
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
readonly name = "NoRunnerAvailableError";
|
|
8
|
+
constructor(jobId: string, reason: string);
|
|
9
|
+
}
|
|
10
|
+
/** The wait exceeded its timeout while a runner was still plausibly working. */
|
|
11
|
+
declare class ResultTimeoutError extends Error {
|
|
12
|
+
readonly jobId: string;
|
|
13
|
+
readonly timeoutMs: number;
|
|
14
|
+
readonly name = "ResultTimeoutError";
|
|
15
|
+
constructor(jobId: string, timeoutMs: number);
|
|
16
|
+
}
|
|
17
|
+
interface WaitOptions {
|
|
18
|
+
/** Give up after this long. Default 5 minutes. */
|
|
19
|
+
readonly timeoutMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Called instead of throwing when no runner can take the job. Return a
|
|
22
|
+
* substitute result (a hosted-model answer, say) and the wait resolves with
|
|
23
|
+
* it; return nothing and {@link NoRunnerAvailableError} is thrown.
|
|
24
|
+
*/
|
|
25
|
+
readonly onNoRunner?: (reason: string) => DeliveredResult | undefined | Promise<DeliveredResult | undefined>;
|
|
26
|
+
/** Abort the wait. */
|
|
27
|
+
readonly signal?: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* How an app learns a job finished.
|
|
31
|
+
*
|
|
32
|
+
* byollm_003 Rev 1 is explicit that this is a *channel* — webhook, Realtime
|
|
33
|
+
* subscription, or poll — and never an implied in-request `await`. The
|
|
34
|
+
* polling implementation below is the portable default; the Supabase adapter
|
|
35
|
+
* substitutes Realtime for the same interface.
|
|
36
|
+
*/
|
|
37
|
+
interface ResultDelivery {
|
|
38
|
+
waitFor(jobId: string, options?: WaitOptions): Promise<DeliveredResult>;
|
|
39
|
+
}
|
|
40
|
+
interface PollingDeliveryDeps {
|
|
41
|
+
/** Current state of the job, or null if unknown. */
|
|
42
|
+
readonly read: (jobId: string) => Promise<DeliveredResult | null>;
|
|
43
|
+
/** Whether a runner could still take this job. */
|
|
44
|
+
readonly availability: (jobId: string) => Promise<{
|
|
45
|
+
available: boolean;
|
|
46
|
+
reason?: string;
|
|
47
|
+
blocked: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Injectable clock. It must advance in step with {@link sleep}: a test that
|
|
52
|
+
* stubs one and not the other gets a loop whose grace window never elapses.
|
|
53
|
+
*/
|
|
54
|
+
readonly now?: () => number;
|
|
55
|
+
/**
|
|
56
|
+
* How long a sustained no-runner signal must persist before it is believed.
|
|
57
|
+
* Defaults to {@link NO_RUNNER_GRACE_MS}.
|
|
58
|
+
*/
|
|
59
|
+
readonly graceMs?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The portable delivery channel: poll the store until the job is terminal.
|
|
63
|
+
*
|
|
64
|
+
* Correct everywhere and adequate for most apps. An adapter with a push
|
|
65
|
+
* channel should replace it — see the Supabase adapter's Realtime delivery.
|
|
66
|
+
*/
|
|
67
|
+
declare class PollingDelivery implements ResultDelivery {
|
|
68
|
+
#private;
|
|
69
|
+
constructor(deps: PollingDeliveryDeps);
|
|
70
|
+
waitFor(jobId: string, options?: WaitOptions): Promise<DeliveredResult>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { NoRunnerAvailableError as N, type PollingDeliveryDeps as P, type ResultDelivery as R, type WaitOptions as W, PollingDelivery as a, ResultTimeoutError as b };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Endpoint } from '@byollm/protocol';
|
|
2
|
+
import { B as ByollmStore } from './store-D23N6iiP.js';
|
|
3
|
+
|
|
4
|
+
/** Everything a mount needs to serve the protocol. */
|
|
5
|
+
interface HandlerConfig {
|
|
6
|
+
readonly store: ByollmStore;
|
|
7
|
+
/**
|
|
8
|
+
* Absolute URL of the page where a user approves a pairing. The device code
|
|
9
|
+
* is *not* appended — the user types the short code into the app's own
|
|
10
|
+
* authenticated page, which is what keeps pairing interactive.
|
|
11
|
+
*/
|
|
12
|
+
readonly verificationUrl: string;
|
|
13
|
+
/** How long a lease lasts. Default 60s — six heartbeats of headroom. */
|
|
14
|
+
readonly leaseMs?: number;
|
|
15
|
+
/** How long an unapproved pairing code lives. Default 10 minutes. */
|
|
16
|
+
readonly pairingTtlMs?: number;
|
|
17
|
+
/** How often a daemon may poll for pairing approval. Default 2s. */
|
|
18
|
+
readonly pollIntervalMs?: number;
|
|
19
|
+
/** Injectable clock, so tests can move time without sleeping. */
|
|
20
|
+
readonly now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
/** A handled protocol call: a status and a JSON body. */
|
|
23
|
+
interface HandlerResult {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly body: unknown;
|
|
26
|
+
/** Set for `rate-limited` and `server-error`. */
|
|
27
|
+
readonly retryAfterSeconds?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The five protocol endpoints, over any {@link ByollmStore}.
|
|
31
|
+
*
|
|
32
|
+
* Transport-free on purpose: a mount adapts `Request`/`Response` (or Express,
|
|
33
|
+
* or whatever) onto {@link ByollmHandlers.handle}, and everything the
|
|
34
|
+
* protocol actually specifies lives here where the conformance kit can reach
|
|
35
|
+
* it without an HTTP server in the way.
|
|
36
|
+
*/
|
|
37
|
+
declare class ByollmHandlers {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(config: HandlerConfig);
|
|
40
|
+
/**
|
|
41
|
+
* Dispatch one protocol call.
|
|
42
|
+
*
|
|
43
|
+
* @param endpoint - which of the five, already routed from the path
|
|
44
|
+
* @param body - the parsed JSON request body, untrusted
|
|
45
|
+
* @param bearer - the `Authorization: Bearer` value, if any
|
|
46
|
+
*/
|
|
47
|
+
handle(endpoint: Endpoint, body: unknown, bearer: string | undefined): Promise<HandlerResult>;
|
|
48
|
+
}
|
|
49
|
+
/** The protocol version this build speaks. */
|
|
50
|
+
declare const SERVED_PROTOCOL_VERSION: "0";
|
|
51
|
+
|
|
52
|
+
export { ByollmHandlers as B, type HandlerConfig as H, SERVED_PROTOCOL_VERSION as S, type HandlerResult as a };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { JobKind, DeliveredResult, Endpoint, Capability } from '@byollm/protocol';
|
|
2
|
+
import { P as PollingDeliveryDeps, R as ResultDelivery, W as WaitOptions } from './delivery-36nIe-b3.js';
|
|
3
|
+
export { N as NoRunnerAvailableError, a as PollingDelivery, b as ResultTimeoutError } from './delivery-36nIe-b3.js';
|
|
4
|
+
import { B as ByollmStore, E as EnqueueInput, J as JobRecord, R as RunnerRecord, C as ClaimArgs, a as RenewArgs, b as RenewResult, c as CompleteArgs, d as CompleteResult, e as ReleaseArgs, P as PairingRecord, A as ApproveArgs, T as TouchArgs } from './store-D23N6iiP.js';
|
|
5
|
+
export { f as JobStore, g as RunnerStore } from './store-D23N6iiP.js';
|
|
6
|
+
import { H as HandlerConfig } from './handlers-D7lWfwno.js';
|
|
7
|
+
export { B as ByollmHandlers, a as HandlerResult, S as SERVED_PROTOCOL_VERSION } from './handlers-D7lWfwno.js';
|
|
8
|
+
|
|
9
|
+
/** Why a job cannot presently run. */
|
|
10
|
+
type NoRunnerReason = "no-runner-paired" | "no-runner-online" | "no-matching-capability" | "audience-admits-nobody";
|
|
11
|
+
/**
|
|
12
|
+
* The no-runner signal (byollm_001 Rev 1 §D).
|
|
13
|
+
*
|
|
14
|
+
* `available: false` means an app should fall back — hosted model, "start
|
|
15
|
+
* your runner" prompt — rather than awaiting something that will never
|
|
16
|
+
* resolve. A job still blocked on dependencies is **not** unavailable; it is
|
|
17
|
+
* waiting, and saying otherwise would make every multi-job flow look broken
|
|
18
|
+
* ({@link MUSTS.NO_RUNNER_SIGNAL}).
|
|
19
|
+
*/
|
|
20
|
+
interface RunnerAvailability {
|
|
21
|
+
readonly available: boolean;
|
|
22
|
+
readonly reason?: NoRunnerReason;
|
|
23
|
+
/** Live runners that could take work of this shape. */
|
|
24
|
+
readonly candidates: number;
|
|
25
|
+
}
|
|
26
|
+
interface AvailabilityQuery {
|
|
27
|
+
readonly kind: JobKind;
|
|
28
|
+
readonly owner: string;
|
|
29
|
+
readonly audience?: "self" | "named" | "public";
|
|
30
|
+
readonly audienceAllow?: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
interface ByollmAppOptions {
|
|
33
|
+
readonly store: ByollmStore;
|
|
34
|
+
/** Injectable clock. */
|
|
35
|
+
readonly now?: () => number;
|
|
36
|
+
/** Liveness window for the no-runner signal. */
|
|
37
|
+
readonly livenessMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* How the app learns a job finished. Defaults to polling the store, which
|
|
40
|
+
* is correct everywhere; the Supabase adapter substitutes Realtime.
|
|
41
|
+
*/
|
|
42
|
+
readonly delivery?: (deps: PollingDeliveryDeps) => ResultDelivery;
|
|
43
|
+
/**
|
|
44
|
+
* How long a sustained no-runner signal must persist before `result()`
|
|
45
|
+
* gives up. Longer tolerates a daemon restarting; shorter fails faster.
|
|
46
|
+
*/
|
|
47
|
+
readonly noRunnerGraceMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An enqueued job, with the delivery channel attached.
|
|
51
|
+
*
|
|
52
|
+
* `result()` is sugar over the channel — with a timeout and a
|
|
53
|
+
* `noRunnerAvailable` path — never a bare promise that can hang forever
|
|
54
|
+
* (byollm_003 Rev 1).
|
|
55
|
+
*/
|
|
56
|
+
interface JobHandle {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
/** The job as stored at enqueue time. */
|
|
59
|
+
readonly record: JobRecord;
|
|
60
|
+
/** Wait for a terminal outcome. */
|
|
61
|
+
result(options?: WaitOptions): Promise<DeliveredResult>;
|
|
62
|
+
/** Ask the runner to stop. */
|
|
63
|
+
cancel(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The app-facing half of `@byollm/server`.
|
|
67
|
+
*
|
|
68
|
+
* The daemon talks to {@link ByollmHandlers}; the app talks to this. Keeping
|
|
69
|
+
* them separate is what makes "one door per state write" hold — an app
|
|
70
|
+
* enqueues and cancels through these methods and never writes job rows by
|
|
71
|
+
* hand.
|
|
72
|
+
*/
|
|
73
|
+
declare class ByollmApp {
|
|
74
|
+
#private;
|
|
75
|
+
constructor(options: ByollmAppOptions);
|
|
76
|
+
/**
|
|
77
|
+
* Enqueue a job.
|
|
78
|
+
*
|
|
79
|
+
* `audience` defaults to `self` — the safe direction. Widening it means the
|
|
80
|
+
* result comes back marked untrusted (see {@link ByollmApp.result}), and
|
|
81
|
+
* the app is obliged to disclose that to whoever reads it.
|
|
82
|
+
*/
|
|
83
|
+
enqueue(input: EnqueueInput): Promise<JobHandle>;
|
|
84
|
+
/** Read a job's current state. */
|
|
85
|
+
job(jobId: string): Promise<JobRecord | null>;
|
|
86
|
+
/**
|
|
87
|
+
* A job's result with its provenance attached.
|
|
88
|
+
*
|
|
89
|
+
* Check `provenance.untrusted` before rendering. It is true for every
|
|
90
|
+
* `named`/`public` job, because that text came from someone else's machine
|
|
91
|
+
* and the app must not present it as its own AI's answer
|
|
92
|
+
* ({@link MUSTS.RESULT_PROVENANCE}).
|
|
93
|
+
*/
|
|
94
|
+
result(jobId: string): Promise<DeliveredResult | null>;
|
|
95
|
+
/** Ask a runner to stop. Queued jobs cancel at once; held jobs at the next heartbeat. */
|
|
96
|
+
cancel(jobId: string): Promise<JobRecord | null>;
|
|
97
|
+
/**
|
|
98
|
+
* Is there a live runner that could take a job of this shape?
|
|
99
|
+
*
|
|
100
|
+
* Runs the identical {@link matchAudience} rule the claim path uses, so the
|
|
101
|
+
* signal cannot promise a runner the claim would then refuse.
|
|
102
|
+
*/
|
|
103
|
+
runnerAvailability(query: AvailabilityQuery): Promise<RunnerAvailability>;
|
|
104
|
+
/**
|
|
105
|
+
* Approve a pairing on behalf of an authenticated user.
|
|
106
|
+
*
|
|
107
|
+
* `owner` MUST come from the approving user's own session. A daemon can
|
|
108
|
+
* never assert who it is — that is the whole reason pairing is interactive
|
|
109
|
+
* ({@link MUSTS.PAIR_ONE_USER}, {@link MUSTS.PAIR_INTERACTIVE}).
|
|
110
|
+
*/
|
|
111
|
+
approvePairing(args: {
|
|
112
|
+
userCode: string;
|
|
113
|
+
owner: string;
|
|
114
|
+
}): Promise<RunnerRecord>;
|
|
115
|
+
/** Deny a pairing the user did not initiate. */
|
|
116
|
+
denyPairing(userCode: string): Promise<void>;
|
|
117
|
+
/** What a pairing code refers to, for the approval page to show. */
|
|
118
|
+
pendingPairing(userCode: string): Promise<{
|
|
119
|
+
label: string;
|
|
120
|
+
platform: string;
|
|
121
|
+
daemonVersion: string;
|
|
122
|
+
capabilities: readonly {
|
|
123
|
+
kind: string;
|
|
124
|
+
model: string;
|
|
125
|
+
}[];
|
|
126
|
+
expiresAt: number;
|
|
127
|
+
} | null>;
|
|
128
|
+
/** The user's paired runners, for a settings page. */
|
|
129
|
+
runners(owner: string): Promise<RunnerRecord[]>;
|
|
130
|
+
/** Revoke a runner. It stops at its next heartbeat, mid-queue. */
|
|
131
|
+
revokeRunner(runnerId: string): Promise<void>;
|
|
132
|
+
/** Run the expiry sweep. Idempotent; safe to call on a timer or a request. */
|
|
133
|
+
sweep(): Promise<JobRecord[]>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Accept a pairing code however the user typed it — lowercase, spaces, no
|
|
137
|
+
* dash. The code is displayed as `XXXX-XXXX`; refusing `xxxxxxxx` would fail
|
|
138
|
+
* a user for a formatting detail they were never told mattered.
|
|
139
|
+
*/
|
|
140
|
+
declare function normalizeUserCode(input: string): string;
|
|
141
|
+
|
|
142
|
+
/** Pull the endpoint name out of a URL path, or null if it isn't ours. */
|
|
143
|
+
declare function routeEndpoint(pathname: string): Endpoint | null;
|
|
144
|
+
/** Read the bearer token from an `Authorization` header. */
|
|
145
|
+
declare function bearerFrom(header: string | null): string | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* A `Request` → `Response` handler for the whole protocol.
|
|
148
|
+
*
|
|
149
|
+
* Web-standard types, so this works unchanged in Next.js route handlers, Hono,
|
|
150
|
+
* Bun, Deno, Cloudflare Workers, and anything else that speaks fetch.
|
|
151
|
+
*/
|
|
152
|
+
declare function createFetchHandler(config: HandlerConfig): (request: Request) => Promise<Response>;
|
|
153
|
+
|
|
154
|
+
/** A device code: the secret the daemon polls with. Never shown to a user. */
|
|
155
|
+
declare function generateDeviceCode(): string;
|
|
156
|
+
/** A runner bearer token. */
|
|
157
|
+
declare function generateRunnerToken(): string;
|
|
158
|
+
/** A runner id. */
|
|
159
|
+
declare function generateRunnerId(): string;
|
|
160
|
+
/** A job id. */
|
|
161
|
+
declare function generateJobId(): string;
|
|
162
|
+
/**
|
|
163
|
+
* A short code the user reads and confirms, formatted `XXXX-XXXX`.
|
|
164
|
+
* Drawn with rejection sampling so the alphabet stays uniform.
|
|
165
|
+
*/
|
|
166
|
+
declare function generateUserCode(): string;
|
|
167
|
+
/** SHA-256, hex. Tokens and device codes are stored only as this. */
|
|
168
|
+
declare function hashSecret(secret: string): string;
|
|
169
|
+
/**
|
|
170
|
+
* Compare two hex digests without leaking their difference through timing.
|
|
171
|
+
* Lengths are compared first because `timingSafeEqual` throws on a mismatch.
|
|
172
|
+
*/
|
|
173
|
+
declare function secretsMatch(aHex: string, bHex: string): boolean;
|
|
174
|
+
|
|
175
|
+
/** Tunables an embedder may want to override in tests. */
|
|
176
|
+
interface MemoryStoreOptions {
|
|
177
|
+
/** Default TTL for a job once claimable. */
|
|
178
|
+
readonly defaultTtlMs?: number;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The reference store: everything in one process, no persistence.
|
|
182
|
+
*
|
|
183
|
+
* This is not a toy — it is the implementation the conformance kit certifies
|
|
184
|
+
* first, so its semantics *are* the specification's semantics for anything
|
|
185
|
+
* the prose leaves implicit. A SQL adapter is correct when the same kit
|
|
186
|
+
* passes against it.
|
|
187
|
+
*
|
|
188
|
+
* Concurrency: JavaScript's single-threaded turn is the atomicity primitive.
|
|
189
|
+
* `claim` performs its read-decide-write with no `await` inside the critical
|
|
190
|
+
* section, which is what makes {@link MUSTS.CLAIM_ATOMIC} hold here. A SQL
|
|
191
|
+
* adapter gets the same property from `FOR UPDATE SKIP LOCKED`.
|
|
192
|
+
*/
|
|
193
|
+
declare class MemoryStore implements ByollmStore {
|
|
194
|
+
#private;
|
|
195
|
+
constructor(options?: MemoryStoreOptions);
|
|
196
|
+
create(input: EnqueueInput, now: number): Promise<JobRecord>;
|
|
197
|
+
get(jobId: string): Promise<JobRecord | null>;
|
|
198
|
+
claim(args: ClaimArgs): Promise<JobRecord[]>;
|
|
199
|
+
renewLeases(args: RenewArgs): Promise<RenewResult>;
|
|
200
|
+
complete(args: CompleteArgs): Promise<CompleteResult>;
|
|
201
|
+
release(args: ReleaseArgs): Promise<string[]>;
|
|
202
|
+
expireDue(now: number): Promise<JobRecord[]>;
|
|
203
|
+
cancel(jobId: string, now: number): Promise<JobRecord | null>;
|
|
204
|
+
listClaimedBy(runnerId: string): Promise<JobRecord[]>;
|
|
205
|
+
listCancelRequests(runnerId: string): Promise<string[]>;
|
|
206
|
+
createPairing(record: PairingRecord): Promise<void>;
|
|
207
|
+
getPairingByDeviceCodeHash(hash: string): Promise<PairingRecord | null>;
|
|
208
|
+
getPairingByUserCode(userCode: string): Promise<PairingRecord | null>;
|
|
209
|
+
approvePairing(args: ApproveArgs): Promise<RunnerRecord>;
|
|
210
|
+
denyPairing(userCode: string, _now: number): Promise<void>;
|
|
211
|
+
consumePairingToken(deviceCodeHash: string): Promise<void>;
|
|
212
|
+
getRunnerByTokenHash(hash: string): Promise<RunnerRecord | null>;
|
|
213
|
+
getRunner(runnerId: string): Promise<RunnerRecord | null>;
|
|
214
|
+
touchRunner(args: TouchArgs): Promise<RunnerRecord | null>;
|
|
215
|
+
revokeRunner(runnerId: string, now: number): Promise<void>;
|
|
216
|
+
listRunners(owner?: string): Promise<RunnerRecord[]>;
|
|
217
|
+
/** All jobs, for demos and assertions. Not part of the store interface. */
|
|
218
|
+
allJobs(): JobRecord[];
|
|
219
|
+
}
|
|
220
|
+
/** The capability that would serve a kind, if any. */
|
|
221
|
+
declare function capabilityFor(capabilities: readonly Capability[], kind: string): Capability | undefined;
|
|
222
|
+
|
|
223
|
+
export { ApproveArgs, type AvailabilityQuery, ByollmApp, type ByollmAppOptions, ByollmStore, ClaimArgs, CompleteArgs, CompleteResult, EnqueueInput, HandlerConfig, type JobHandle, JobRecord, MemoryStore, type MemoryStoreOptions, type NoRunnerReason, PairingRecord, PollingDeliveryDeps, ReleaseArgs, RenewArgs, RenewResult, ResultDelivery, type RunnerAvailability, RunnerRecord, TouchArgs, WaitOptions, bearerFrom, capabilityFor, createFetchHandler, generateDeviceCode, generateJobId, generateRunnerId, generateRunnerToken, generateUserCode, hashSecret, normalizeUserCode, routeEndpoint, secretsMatch };
|