@byollm/server 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +139 -12
- package/bin/keygen.mjs +21 -0
- package/dist/chunk-U4FB6ZRE.js +653 -0
- package/dist/chunk-U4FB6ZRE.js.map +1 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-BQsGZoRL.d.ts} +27 -4
- package/dist/index.d.ts +190 -16
- package/dist/index.js +502 -43
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +40 -8
- package/dist/next.js +8 -2
- package/dist/next.js.map +1 -1
- package/dist/store-BWG1tpqc.d.ts +431 -0
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +104 -38
- package/dist/supabase/index.js.map +1 -1
- package/package.json +8 -4
- package/supabase/migrations/20260809000000_byollm_runner.sql +22 -3
- package/supabase/migrations/20260819000000_drop_runner_token.sql +87 -0
- package/supabase/migrations/20260819010000_completed_by_lease_id.sql +25 -0
- package/dist/chunk-HL6EYHQ7.js +0 -422
- package/dist/chunk-HL6EYHQ7.js.map +0 -1
- package/dist/store-D23N6iiP.d.ts +0 -255
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- The bearer token nobody used — cloud_008 §2.4, finding 37.
|
|
2
|
+
--
|
|
3
|
+
-- `byollm_runners.token_hash` held the SHA-256 of a token minted at pairing,
|
|
4
|
+
-- returned to the daemon, written to its pairings file, and then **never
|
|
5
|
+
-- sent, never looked up and never compared**. The only reader was
|
|
6
|
+
-- `getRunnerByTokenHash`, which both store adapters implemented and nothing
|
|
7
|
+
-- called except a test asserting it returns null.
|
|
8
|
+
--
|
|
9
|
+
-- That is not dead wire in the ordinary sense. It was a *secret*: minted,
|
|
10
|
+
-- transmitted once, and written to two disks at rest for no purpose. A
|
|
11
|
+
-- credential with no consumer cannot be used correctly and can still leak,
|
|
12
|
+
-- which makes it strictly a liability.
|
|
13
|
+
--
|
|
14
|
+
-- `REQUESTS_SIGNED_NOT_BEARER` was the rule the whole time and was enforced
|
|
15
|
+
-- the whole time: every authenticated call is signed by the device's pinned
|
|
16
|
+
-- identity key, and `C016` proves an endpoint refuses a bearer token. This
|
|
17
|
+
-- drops the thing the MUST is named after.
|
|
18
|
+
--
|
|
19
|
+
-- `byollm_pairings.runner_token_once` stays and now carries a marker rather
|
|
20
|
+
-- than a secret. It is the deliver-once flag — a replayed device code must
|
|
21
|
+
-- get nothing — a real property that was riding on the token's nullability.
|
|
22
|
+
-- Renaming a column and changing the code that reads it in one step is how a
|
|
23
|
+
-- rollback strands rows, so the rename waits for a later release.
|
|
24
|
+
|
|
25
|
+
alter table byollm_runners drop column if exists token_hash;
|
|
26
|
+
|
|
27
|
+
-- The browser-facing approval path takes one fewer argument. Replaced with
|
|
28
|
+
-- the old signature dropped rather than left beside it: two functions with
|
|
29
|
+
-- one name is how a caller ends up invoking the one nobody maintains.
|
|
30
|
+
--
|
|
31
|
+
-- Everything else is unchanged from the original, deliberately — the owner
|
|
32
|
+
-- still comes from `auth.uid()` because a daemon can never assert who it is,
|
|
33
|
+
-- which is the whole reason pairing is interactive.
|
|
34
|
+
drop function if exists byollm_approve_pairing(text, text);
|
|
35
|
+
|
|
36
|
+
create or replace function byollm_approve_pairing(
|
|
37
|
+
p_user_code text
|
|
38
|
+
)
|
|
39
|
+
returns byollm_runners
|
|
40
|
+
language plpgsql
|
|
41
|
+
security definer
|
|
42
|
+
set search_path = public
|
|
43
|
+
as $$
|
|
44
|
+
declare
|
|
45
|
+
v_pairing byollm_pairings;
|
|
46
|
+
v_runner byollm_runners;
|
|
47
|
+
v_owner uuid := (select auth.uid());
|
|
48
|
+
begin
|
|
49
|
+
if v_owner is null then
|
|
50
|
+
raise exception 'approving a pairing requires an authenticated user';
|
|
51
|
+
end if;
|
|
52
|
+
|
|
53
|
+
select * into v_pairing from byollm_pairings
|
|
54
|
+
where user_code = p_user_code for update;
|
|
55
|
+
|
|
56
|
+
if v_pairing is null then
|
|
57
|
+
raise exception 'unknown pairing code';
|
|
58
|
+
end if;
|
|
59
|
+
if v_pairing.expires_at <= now() then
|
|
60
|
+
raise exception 'pairing code has expired';
|
|
61
|
+
end if;
|
|
62
|
+
if v_pairing.state <> 'pending' then
|
|
63
|
+
raise exception 'pairing is already %', v_pairing.state;
|
|
64
|
+
end if;
|
|
65
|
+
|
|
66
|
+
insert into byollm_runners (owner, label, platform,
|
|
67
|
+
daemon_version, capabilities, device)
|
|
68
|
+
values (v_owner, v_pairing.label, v_pairing.platform,
|
|
69
|
+
v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
|
|
70
|
+
returning * into v_runner;
|
|
71
|
+
|
|
72
|
+
-- The marker, not a token: this is what makes a replayed device code get
|
|
73
|
+
-- nothing. The service-role path in `supabase/index.ts` writes the same
|
|
74
|
+
-- value, and the two approval doors have to agree — a field set by one and
|
|
75
|
+
-- not the other produces a runner that is correct through one and broken
|
|
76
|
+
-- through the other.
|
|
77
|
+
update byollm_pairings
|
|
78
|
+
set state = 'approved',
|
|
79
|
+
owner = v_owner,
|
|
80
|
+
runner_id = v_runner.id,
|
|
81
|
+
runner_token_once = 'pending-collection'
|
|
82
|
+
where device_code_hash = v_pairing.device_code_hash;
|
|
83
|
+
|
|
84
|
+
return v_runner;
|
|
85
|
+
end;
|
|
86
|
+
$$;
|
|
87
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Which grant recorded a result — cloud_008 §3.6.
|
|
2
|
+
--
|
|
3
|
+
-- `complete` now checks terminal state **before** the holder, so
|
|
4
|
+
-- `RESULT_IDEMPOTENT` is enforced by the branch named after it rather than as
|
|
5
|
+
-- a side effect of the lease being nulled on success. Answering a replay
|
|
6
|
+
-- correctly means knowing which grant recorded the result, and `lease_id` is
|
|
7
|
+
-- cleared at completion by design: "who holds this" and "who finished this"
|
|
8
|
+
-- are different questions with different lifetimes.
|
|
9
|
+
--
|
|
10
|
+
-- ## Why this is its own file
|
|
11
|
+
--
|
|
12
|
+
-- It was first appended to `20260819000000_drop_runner_token.sql`, which had
|
|
13
|
+
-- already shipped in alpha.19. An applied migration is immutable — that is the
|
|
14
|
+
-- whole reason a migrations folder can be trusted — and the fact that this one
|
|
15
|
+
-- is only days old and probably applied nowhere does not make editing it a
|
|
16
|
+
-- different act. The rule is worth more than the tidiness.
|
|
17
|
+
--
|
|
18
|
+
-- ## Nullable, and not backfilled
|
|
19
|
+
--
|
|
20
|
+
-- Rows completed before this migration have no recorded grant, so a replay of
|
|
21
|
+
-- one of them is answered as a plain refusal rather than as a duplicate. That
|
|
22
|
+
-- is the safe direction: it withholds a reassurance rather than inventing one,
|
|
23
|
+
-- and the daemons that produced those rows stopped retrying long ago.
|
|
24
|
+
alter table byollm_jobs
|
|
25
|
+
add column if not exists completed_by_lease_id text;
|
package/dist/chunk-HL6EYHQ7.js
DELETED
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
// src/ids.ts
|
|
2
|
-
import {
|
|
3
|
-
createHash,
|
|
4
|
-
randomBytes,
|
|
5
|
-
randomUUID,
|
|
6
|
-
timingSafeEqual
|
|
7
|
-
} from "crypto";
|
|
8
|
-
var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRTWXYZ2346789";
|
|
9
|
-
function generateDeviceCode() {
|
|
10
|
-
return randomBytes(32).toString("base64url");
|
|
11
|
-
}
|
|
12
|
-
function generateRunnerToken() {
|
|
13
|
-
return randomBytes(32).toString("base64url");
|
|
14
|
-
}
|
|
15
|
-
function generateRunnerId() {
|
|
16
|
-
return `runner_${randomUUID()}`;
|
|
17
|
-
}
|
|
18
|
-
function generateJobId() {
|
|
19
|
-
return `job_${randomUUID()}`;
|
|
20
|
-
}
|
|
21
|
-
function generateUserCode() {
|
|
22
|
-
const chars = [];
|
|
23
|
-
while (chars.length < 8) {
|
|
24
|
-
for (const byte of randomBytes(16)) {
|
|
25
|
-
const limit = 256 - 256 % USER_CODE_ALPHABET.length;
|
|
26
|
-
if (byte >= limit) continue;
|
|
27
|
-
const symbol = USER_CODE_ALPHABET[byte % USER_CODE_ALPHABET.length];
|
|
28
|
-
if (symbol === void 0) continue;
|
|
29
|
-
chars.push(symbol);
|
|
30
|
-
if (chars.length === 8) break;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return `${chars.slice(0, 4).join("")}-${chars.slice(4).join("")}`;
|
|
34
|
-
}
|
|
35
|
-
function hashSecret(secret) {
|
|
36
|
-
return createHash("sha256").update(secret, "utf8").digest("hex");
|
|
37
|
-
}
|
|
38
|
-
function secretsMatch(aHex, bHex) {
|
|
39
|
-
if (aHex.length !== bHex.length) return false;
|
|
40
|
-
return timingSafeEqual(Buffer.from(aHex, "hex"), Buffer.from(bHex, "hex"));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/handlers.ts
|
|
44
|
-
import {
|
|
45
|
-
ClaimRequest,
|
|
46
|
-
ERROR_STATUS,
|
|
47
|
-
HeartbeatRequest,
|
|
48
|
-
PairRequest,
|
|
49
|
-
PROTOCOL_VERSION,
|
|
50
|
-
ReleaseRequest,
|
|
51
|
-
ResultRequest,
|
|
52
|
-
provenanceFor
|
|
53
|
-
} from "@byollm/protocol";
|
|
54
|
-
var DEFAULTS = {
|
|
55
|
-
leaseMs: 6e4,
|
|
56
|
-
pairingTtlMs: 10 * 6e4,
|
|
57
|
-
pollIntervalMs: 2e3
|
|
58
|
-
};
|
|
59
|
-
function fail(error, message, retryAfterSeconds) {
|
|
60
|
-
return {
|
|
61
|
-
status: ERROR_STATUS[error],
|
|
62
|
-
body: {
|
|
63
|
-
error,
|
|
64
|
-
message,
|
|
65
|
-
...retryAfterSeconds === void 0 ? {} : { retryAfter: retryAfterSeconds }
|
|
66
|
-
},
|
|
67
|
-
...retryAfterSeconds === void 0 ? {} : { retryAfterSeconds }
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function ok(body) {
|
|
71
|
-
return { status: 200, body };
|
|
72
|
-
}
|
|
73
|
-
var ByollmHandlers = class {
|
|
74
|
-
#store;
|
|
75
|
-
#verificationUrl;
|
|
76
|
-
#leaseMs;
|
|
77
|
-
#pairingTtlMs;
|
|
78
|
-
#pollIntervalMs;
|
|
79
|
-
#now;
|
|
80
|
-
constructor(config) {
|
|
81
|
-
this.#store = config.store;
|
|
82
|
-
this.#verificationUrl = config.verificationUrl;
|
|
83
|
-
this.#leaseMs = config.leaseMs ?? DEFAULTS.leaseMs;
|
|
84
|
-
this.#pairingTtlMs = config.pairingTtlMs ?? DEFAULTS.pairingTtlMs;
|
|
85
|
-
this.#pollIntervalMs = config.pollIntervalMs ?? DEFAULTS.pollIntervalMs;
|
|
86
|
-
this.#now = config.now ?? Date.now;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Dispatch one protocol call.
|
|
90
|
-
*
|
|
91
|
-
* @param endpoint - which of the five, already routed from the path
|
|
92
|
-
* @param body - the parsed JSON request body, untrusted
|
|
93
|
-
* @param bearer - the `Authorization: Bearer` value, if any
|
|
94
|
-
*/
|
|
95
|
-
async handle(endpoint, body, bearer) {
|
|
96
|
-
switch (endpoint) {
|
|
97
|
-
case "pair":
|
|
98
|
-
return this.#pair(body);
|
|
99
|
-
case "claim":
|
|
100
|
-
return this.#authed(bearer, body, ClaimRequest, this.#claim.bind(this));
|
|
101
|
-
case "heartbeat":
|
|
102
|
-
return this.#authed(
|
|
103
|
-
bearer,
|
|
104
|
-
body,
|
|
105
|
-
HeartbeatRequest,
|
|
106
|
-
this.#heartbeat.bind(this),
|
|
107
|
-
{ allowRevoked: true }
|
|
108
|
-
);
|
|
109
|
-
case "result":
|
|
110
|
-
return this.#authed(
|
|
111
|
-
bearer,
|
|
112
|
-
body,
|
|
113
|
-
ResultRequest,
|
|
114
|
-
this.#result.bind(this)
|
|
115
|
-
);
|
|
116
|
-
case "release":
|
|
117
|
-
return this.#authed(
|
|
118
|
-
bearer,
|
|
119
|
-
body,
|
|
120
|
-
ReleaseRequest,
|
|
121
|
-
this.#release.bind(this)
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Shared preamble for the four authenticated endpoints: resolve the bearer
|
|
127
|
-
* token to a runner, reject a revoked one, and parse the body.
|
|
128
|
-
*
|
|
129
|
-
* The token→runner lookup happens before schema validation so a stranger
|
|
130
|
-
* probing the endpoint learns nothing about the wire format.
|
|
131
|
-
*/
|
|
132
|
-
async #authed(bearer, body, schema, run, options = {}) {
|
|
133
|
-
if (bearer === void 0 || bearer.length === 0) {
|
|
134
|
-
return fail("unauthorized", "a runner token is required");
|
|
135
|
-
}
|
|
136
|
-
const runner = await this.#store.getRunnerByTokenHash(hashSecret(bearer));
|
|
137
|
-
if (!runner) {
|
|
138
|
-
return fail("unauthorized", "this runner token is not recognised");
|
|
139
|
-
}
|
|
140
|
-
if (runner.revokedAt !== null && options.allowRevoked !== true) {
|
|
141
|
-
return fail("revoked", "this runner has been revoked by its owner");
|
|
142
|
-
}
|
|
143
|
-
const parsed = schema.safeParse(body);
|
|
144
|
-
if (!parsed.success || parsed.data === void 0) {
|
|
145
|
-
return fail("bad-request", "request body failed schema validation");
|
|
146
|
-
}
|
|
147
|
-
return run(parsed.data, runner);
|
|
148
|
-
}
|
|
149
|
-
// -- 1. pair --------------------------------------------------------------
|
|
150
|
-
async #pair(body) {
|
|
151
|
-
const parsed = PairRequest.safeParse(body);
|
|
152
|
-
if (!parsed.success) {
|
|
153
|
-
return fail("bad-request", "pair request failed schema validation");
|
|
154
|
-
}
|
|
155
|
-
const request = parsed.data;
|
|
156
|
-
const now = this.#now();
|
|
157
|
-
if (request.action === "start") {
|
|
158
|
-
const deviceCode = generateDeviceCode();
|
|
159
|
-
const userCode = generateUserCode();
|
|
160
|
-
const expiresAt = now + this.#pairingTtlMs;
|
|
161
|
-
await this.#store.createPairing({
|
|
162
|
-
deviceCodeHash: hashSecret(deviceCode),
|
|
163
|
-
userCode,
|
|
164
|
-
state: "pending",
|
|
165
|
-
owner: null,
|
|
166
|
-
runnerId: null,
|
|
167
|
-
runnerTokenOnce: null,
|
|
168
|
-
label: request.daemon.label,
|
|
169
|
-
platform: request.daemon.platform,
|
|
170
|
-
daemonVersion: request.daemon.version,
|
|
171
|
-
capabilities: request.capabilities,
|
|
172
|
-
expiresAt,
|
|
173
|
-
createdAt: now
|
|
174
|
-
});
|
|
175
|
-
const response = {
|
|
176
|
-
deviceCode,
|
|
177
|
-
userCode,
|
|
178
|
-
verificationUrl: this.#verificationUrl,
|
|
179
|
-
expiresAt,
|
|
180
|
-
pollIntervalMs: this.#pollIntervalMs
|
|
181
|
-
};
|
|
182
|
-
return ok(response);
|
|
183
|
-
}
|
|
184
|
-
const pairing = await this.#store.getPairingByDeviceCodeHash(
|
|
185
|
-
hashSecret(request.deviceCode)
|
|
186
|
-
);
|
|
187
|
-
if (!pairing) {
|
|
188
|
-
return fail("not-found", "unknown device code");
|
|
189
|
-
}
|
|
190
|
-
if (pairing.state === "denied") {
|
|
191
|
-
return ok({ status: "denied" });
|
|
192
|
-
}
|
|
193
|
-
if (pairing.expiresAt <= now && pairing.state === "pending") {
|
|
194
|
-
return ok({ status: "expired" });
|
|
195
|
-
}
|
|
196
|
-
if (pairing.state === "approved" && pairing.runnerTokenOnce !== null && pairing.runnerId !== null && pairing.owner !== null) {
|
|
197
|
-
const response = {
|
|
198
|
-
status: "approved",
|
|
199
|
-
runnerToken: pairing.runnerTokenOnce,
|
|
200
|
-
runnerId: pairing.runnerId,
|
|
201
|
-
owner: pairing.owner
|
|
202
|
-
};
|
|
203
|
-
await this.#store.consumePairingToken(pairing.deviceCodeHash);
|
|
204
|
-
return ok(response);
|
|
205
|
-
}
|
|
206
|
-
if (pairing.state === "approved") {
|
|
207
|
-
return fail("not-found", "this pairing has already been collected");
|
|
208
|
-
}
|
|
209
|
-
return ok({ status: "pending" });
|
|
210
|
-
}
|
|
211
|
-
// -- 2. claim -------------------------------------------------------------
|
|
212
|
-
async #claim(request, runner) {
|
|
213
|
-
if (request.runnerId !== runner.id) {
|
|
214
|
-
return fail("unauthorized", "runner id does not match the bearer token");
|
|
215
|
-
}
|
|
216
|
-
const now = this.#now();
|
|
217
|
-
const jobs = await this.#store.claim({
|
|
218
|
-
runnerId: runner.id,
|
|
219
|
-
runnerOwner: runner.owner,
|
|
220
|
-
capabilities: request.capabilities,
|
|
221
|
-
max: request.max,
|
|
222
|
-
leaseMs: this.#leaseMs,
|
|
223
|
-
now
|
|
224
|
-
});
|
|
225
|
-
const response = {
|
|
226
|
-
jobs: jobs.map((job) => ({
|
|
227
|
-
id: job.id,
|
|
228
|
-
kind: job.kind,
|
|
229
|
-
payload: job.payload,
|
|
230
|
-
audience: job.audience,
|
|
231
|
-
owner: job.owner,
|
|
232
|
-
...job.audienceAllow === void 0 ? {} : { audienceAllow: [...job.audienceAllow] },
|
|
233
|
-
lease: job.lease ?? {
|
|
234
|
-
runnerId: runner.id,
|
|
235
|
-
expiresAt: now + this.#leaseMs
|
|
236
|
-
}
|
|
237
|
-
})),
|
|
238
|
-
leaseMs: this.#leaseMs
|
|
239
|
-
};
|
|
240
|
-
return ok(response);
|
|
241
|
-
}
|
|
242
|
-
// -- 3. heartbeat ---------------------------------------------------------
|
|
243
|
-
async #heartbeat(request, runner) {
|
|
244
|
-
if (request.runnerId !== runner.id) {
|
|
245
|
-
return fail("unauthorized", "runner id does not match the bearer token");
|
|
246
|
-
}
|
|
247
|
-
const now = this.#now();
|
|
248
|
-
const revoked = runner.revokedAt !== null;
|
|
249
|
-
if (revoked) {
|
|
250
|
-
const held = await this.#store.listClaimedBy(runner.id);
|
|
251
|
-
const response2 = {
|
|
252
|
-
revoked: true,
|
|
253
|
-
cancel: [],
|
|
254
|
-
leases: [],
|
|
255
|
-
lost: held.map((job) => job.id),
|
|
256
|
-
serverTime: now
|
|
257
|
-
};
|
|
258
|
-
return ok(response2);
|
|
259
|
-
}
|
|
260
|
-
await this.#store.touchRunner({
|
|
261
|
-
runnerId: runner.id,
|
|
262
|
-
capabilities: request.capabilities,
|
|
263
|
-
daemonVersion: request.daemonVersion,
|
|
264
|
-
paused: request.paused,
|
|
265
|
-
now
|
|
266
|
-
});
|
|
267
|
-
const { renewed, lost } = await this.#store.renewLeases({
|
|
268
|
-
runnerId: runner.id,
|
|
269
|
-
jobIds: request.activeJobIds,
|
|
270
|
-
leaseMs: this.#leaseMs,
|
|
271
|
-
now
|
|
272
|
-
});
|
|
273
|
-
const cancel = await this.#store.listCancelRequests(runner.id);
|
|
274
|
-
const response = {
|
|
275
|
-
revoked: false,
|
|
276
|
-
cancel,
|
|
277
|
-
leases: renewed.map((r) => ({ jobId: r.jobId, expiresAt: r.expiresAt })),
|
|
278
|
-
lost: [...lost],
|
|
279
|
-
serverTime: now
|
|
280
|
-
};
|
|
281
|
-
return ok(response);
|
|
282
|
-
}
|
|
283
|
-
// -- 4. result ------------------------------------------------------------
|
|
284
|
-
async #result(request, runner) {
|
|
285
|
-
if (request.runnerId !== runner.id) {
|
|
286
|
-
return fail("unauthorized", "runner id does not match the bearer token");
|
|
287
|
-
}
|
|
288
|
-
const now = this.#now();
|
|
289
|
-
const job = await this.#store.get(request.jobId);
|
|
290
|
-
if (!job) return fail("not-found", "unknown job");
|
|
291
|
-
const provenance = provenanceFor({
|
|
292
|
-
audience: job.audience,
|
|
293
|
-
runnerId: runner.id,
|
|
294
|
-
runnerOwner: runner.owner,
|
|
295
|
-
backendClass: request.backendClass,
|
|
296
|
-
model: request.model
|
|
297
|
-
});
|
|
298
|
-
const { accepted, job: updated } = await this.#store.complete({
|
|
299
|
-
jobId: request.jobId,
|
|
300
|
-
runnerId: runner.id,
|
|
301
|
-
outcome: request.outcome,
|
|
302
|
-
provenance,
|
|
303
|
-
now
|
|
304
|
-
});
|
|
305
|
-
const response = {
|
|
306
|
-
accepted,
|
|
307
|
-
state: updated?.state ?? job.state
|
|
308
|
-
};
|
|
309
|
-
return ok(response);
|
|
310
|
-
}
|
|
311
|
-
// -- 5. release -----------------------------------------------------------
|
|
312
|
-
async #release(request, runner) {
|
|
313
|
-
if (request.runnerId !== runner.id) {
|
|
314
|
-
return fail("unauthorized", "runner id does not match the bearer token");
|
|
315
|
-
}
|
|
316
|
-
const released = await this.#store.release({
|
|
317
|
-
runnerId: runner.id,
|
|
318
|
-
jobIds: request.jobIds,
|
|
319
|
-
reason: request.reason,
|
|
320
|
-
now: this.#now()
|
|
321
|
-
});
|
|
322
|
-
const response = { released };
|
|
323
|
-
return ok(response);
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
var SERVED_PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
327
|
-
|
|
328
|
-
// src/http.ts
|
|
329
|
-
import { ENDPOINTS, PROTOCOL_PREFIX } from "@byollm/protocol";
|
|
330
|
-
var MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
331
|
-
function routeEndpoint(pathname) {
|
|
332
|
-
const index = pathname.lastIndexOf("/");
|
|
333
|
-
const last = index === -1 ? pathname : pathname.slice(index + 1);
|
|
334
|
-
return ENDPOINTS.includes(last) ? last : null;
|
|
335
|
-
}
|
|
336
|
-
function bearerFrom(header) {
|
|
337
|
-
if (!header) return void 0;
|
|
338
|
-
const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());
|
|
339
|
-
return match?.[1];
|
|
340
|
-
}
|
|
341
|
-
function createFetchHandler(config) {
|
|
342
|
-
const handlers = new ByollmHandlers(config);
|
|
343
|
-
return async function handle(request) {
|
|
344
|
-
if (request.method !== "POST") {
|
|
345
|
-
return json(405, {
|
|
346
|
-
error: "bad-request",
|
|
347
|
-
message: "protocol endpoints accept POST only"
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const endpoint = routeEndpoint(new URL(request.url).pathname);
|
|
351
|
-
if (endpoint === null) {
|
|
352
|
-
return json(404, {
|
|
353
|
-
error: "not-found",
|
|
354
|
-
message: `not a ${PROTOCOL_PREFIX} endpoint`
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
const declared = request.headers.get("content-length");
|
|
358
|
-
if (declared !== null && Number(declared) > MAX_BODY_BYTES) {
|
|
359
|
-
return json(400, {
|
|
360
|
-
error: "bad-request",
|
|
361
|
-
message: "request body too large"
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
let body;
|
|
365
|
-
try {
|
|
366
|
-
const text = await request.text();
|
|
367
|
-
if (text.length > MAX_BODY_BYTES) {
|
|
368
|
-
return json(400, {
|
|
369
|
-
error: "bad-request",
|
|
370
|
-
message: "request body too large"
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
body = JSON.parse(text);
|
|
374
|
-
} catch {
|
|
375
|
-
return json(400, {
|
|
376
|
-
error: "bad-request",
|
|
377
|
-
message: "request body is not valid JSON"
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
const result = await handlers.handle(
|
|
381
|
-
endpoint,
|
|
382
|
-
body,
|
|
383
|
-
bearerFrom(request.headers.get("authorization"))
|
|
384
|
-
);
|
|
385
|
-
const headers = {
|
|
386
|
-
"content-type": "application/json",
|
|
387
|
-
"cache-control": "no-store"
|
|
388
|
-
};
|
|
389
|
-
if (result.retryAfterSeconds !== void 0) {
|
|
390
|
-
headers["retry-after"] = String(result.retryAfterSeconds);
|
|
391
|
-
}
|
|
392
|
-
return new Response(JSON.stringify(result.body), {
|
|
393
|
-
status: result.status,
|
|
394
|
-
headers
|
|
395
|
-
});
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function json(status, body) {
|
|
399
|
-
return new Response(JSON.stringify(body), {
|
|
400
|
-
status,
|
|
401
|
-
headers: {
|
|
402
|
-
"content-type": "application/json",
|
|
403
|
-
"cache-control": "no-store"
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
export {
|
|
409
|
-
generateDeviceCode,
|
|
410
|
-
generateRunnerToken,
|
|
411
|
-
generateRunnerId,
|
|
412
|
-
generateJobId,
|
|
413
|
-
generateUserCode,
|
|
414
|
-
hashSecret,
|
|
415
|
-
secretsMatch,
|
|
416
|
-
ByollmHandlers,
|
|
417
|
-
SERVED_PROTOCOL_VERSION,
|
|
418
|
-
routeEndpoint,
|
|
419
|
-
bearerFrom,
|
|
420
|
-
createFetchHandler
|
|
421
|
-
};
|
|
422
|
-
//# sourceMappingURL=chunk-HL6EYHQ7.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|