@byollm/server 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +151 -12
- package/bin/keygen.mjs +21 -0
- package/dist/chunk-K5E6JS5A.js +655 -0
- package/dist/chunk-K5E6JS5A.js.map +1 -0
- package/dist/{handlers-D7lWfwno.d.ts → handlers-BJYm2kdq.d.ts} +27 -4
- package/dist/index.d.ts +221 -17
- package/dist/index.js +549 -44
- 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-Dno2fnHH.d.ts +436 -0
- package/dist/supabase/index.d.ts +1 -1
- package/dist/supabase/index.js +115 -42
- package/dist/supabase/index.js.map +1 -1
- package/package.json +7 -3
- 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/supabase/migrations/20260821000000_rename_collected.sql +91 -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;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- The deliver-once flag stops pretending to be a token — cloud_008 §2.4a.
|
|
2
|
+
--
|
|
3
|
+
-- `20260819000000_drop_runner_token` removed the bearer credential and left
|
|
4
|
+
-- `byollm_pairings.runner_token_once` carrying a marker string, with the
|
|
5
|
+
-- reason written down: *"Renaming a column and changing the code that reads
|
|
6
|
+
-- it in one step is how a rollback strands rows, so the rename waits for a
|
|
7
|
+
-- later release."*
|
|
8
|
+
--
|
|
9
|
+
-- This is that release. The rename lands with the code that reads it, in one
|
|
10
|
+
-- step, because the transitional shape exists to protect a party who has not
|
|
11
|
+
-- agreed to change — and pre-1.0, with one deployment and one operator, that
|
|
12
|
+
-- party is us. Carrying a column named after a secret it no longer holds is
|
|
13
|
+
-- a comment that has to be re-read by everybody who meets it.
|
|
14
|
+
--
|
|
15
|
+
-- The property is unchanged and is the reason the column survives at all: a
|
|
16
|
+
-- replayed device code must get nothing, or a code left in a shell history is
|
|
17
|
+
-- a second pairing. That was riding on a token's nullability, which was one
|
|
18
|
+
-- field doing two jobs — and only one of them load-bearing.
|
|
19
|
+
|
|
20
|
+
alter table byollm_pairings
|
|
21
|
+
rename column runner_token_once to collected_at;
|
|
22
|
+
|
|
23
|
+
-- A timestamp rather than a marker string. `'pending-collection'` was the
|
|
24
|
+
-- shape a nulled token left behind; what the flag actually records is *when*
|
|
25
|
+
-- the approval was handed over, which is worth having when somebody asks why
|
|
26
|
+
-- a pairing did not complete.
|
|
27
|
+
alter table byollm_pairings
|
|
28
|
+
alter column collected_at type timestamptz
|
|
29
|
+
using case when collected_at is null then now() else null end;
|
|
30
|
+
|
|
31
|
+
comment on column byollm_pairings.collected_at is
|
|
32
|
+
'When the approval was collected. Null until then — a replayed device code '
|
|
33
|
+
'gets nothing. cloud_008 §2.4a; this was runner_token_once.';
|
|
34
|
+
|
|
35
|
+
-- The approval function moves with the column it writes. Recreated whole
|
|
36
|
+
-- rather than patched, because two functions with one name is how a caller
|
|
37
|
+
-- ends up invoking the one nobody maintains — the argument the migration
|
|
38
|
+
-- before this one made when it replaced the old signature.
|
|
39
|
+
|
|
40
|
+
create or replace function byollm_approve_pairing(
|
|
41
|
+
p_user_code text
|
|
42
|
+
)
|
|
43
|
+
returns byollm_runners
|
|
44
|
+
language plpgsql
|
|
45
|
+
security definer
|
|
46
|
+
set search_path = public
|
|
47
|
+
as $$
|
|
48
|
+
declare
|
|
49
|
+
v_pairing byollm_pairings;
|
|
50
|
+
v_runner byollm_runners;
|
|
51
|
+
v_owner uuid := (select auth.uid());
|
|
52
|
+
begin
|
|
53
|
+
if v_owner is null then
|
|
54
|
+
raise exception 'approving a pairing requires an authenticated user';
|
|
55
|
+
end if;
|
|
56
|
+
|
|
57
|
+
select * into v_pairing from byollm_pairings
|
|
58
|
+
where user_code = p_user_code for update;
|
|
59
|
+
|
|
60
|
+
if v_pairing is null then
|
|
61
|
+
raise exception 'unknown pairing code';
|
|
62
|
+
end if;
|
|
63
|
+
if v_pairing.expires_at <= now() then
|
|
64
|
+
raise exception 'pairing code has expired';
|
|
65
|
+
end if;
|
|
66
|
+
if v_pairing.state <> 'pending' then
|
|
67
|
+
raise exception 'pairing is already %', v_pairing.state;
|
|
68
|
+
end if;
|
|
69
|
+
|
|
70
|
+
insert into byollm_runners (owner, label, platform,
|
|
71
|
+
daemon_version, capabilities, device)
|
|
72
|
+
values (v_owner, v_pairing.label, v_pairing.platform,
|
|
73
|
+
v_pairing.daemon_version, v_pairing.capabilities, v_pairing.device)
|
|
74
|
+
returning * into v_runner;
|
|
75
|
+
|
|
76
|
+
-- The marker, not a token: this is what makes a replayed device code get
|
|
77
|
+
-- nothing. The service-role path in `supabase/index.ts` writes the same
|
|
78
|
+
-- value, and the two approval doors have to agree — a field set by one and
|
|
79
|
+
-- not the other produces a runner that is correct through one and broken
|
|
80
|
+
-- through the other.
|
|
81
|
+
update byollm_pairings
|
|
82
|
+
set state = 'approved',
|
|
83
|
+
owner = v_owner,
|
|
84
|
+
runner_id = v_runner.id,
|
|
85
|
+
collected_at = null
|
|
86
|
+
where device_code_hash = v_pairing.device_code_hash;
|
|
87
|
+
|
|
88
|
+
return v_runner;
|
|
89
|
+
end;
|
|
90
|
+
$$;
|
|
91
|
+
|
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
|