@byollm/conformance 0.1.0-alpha.8 → 0.1.0-alpha.81
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 +107 -5
- package/dist/audit-cli.d.ts +1 -0
- package/dist/audit-cli.js +25 -0
- package/dist/audit-cli.js.map +1 -0
- package/dist/chunk-2EJHYOQS.js +469 -0
- package/dist/chunk-2EJHYOQS.js.map +1 -0
- package/dist/{chunk-EXNALQ5D.js → chunk-MSM6QP2N.js} +439 -185
- package/dist/chunk-MSM6QP2N.js.map +1 -0
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +116 -7
- package/dist/index.js +9 -1
- package/package.json +8 -6
- package/dist/chunk-EXNALQ5D.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.81`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npm install @byollm/conformance@alpha`.
|
|
5
5
|
>
|
|
@@ -11,7 +11,109 @@
|
|
|
11
11
|
> npm assigns `latest` on a first publish and won't let it be removed, so a
|
|
12
12
|
> bare install resolves here too. This notice is the only guard — deliberately
|
|
13
13
|
> not an npm deprecation, which would read as *abandoned* rather than *early*.
|
|
14
|
-
> Ask for `@alpha` explicitly so your lockfile records that you meant to
|
|
14
|
+
> Ask for `@alpha` explicitly so your lockfile records that you meant to.>
|
|
15
|
+
> **`alpha.15` is a breaking wire change, and it breaks daemons and relays —
|
|
16
|
+
> not app authors.** If you call `app.enqueue(...)` and read results, nothing
|
|
17
|
+
> in your code changes. If you run a daemon or an upstream, every package must
|
|
18
|
+
> move together: a mixed pair refuses on both sides, because both ends parse
|
|
19
|
+
> `.strict()`.
|
|
20
|
+
>
|
|
21
|
+
> What moved, all of it reconciling the frozen `byollm_009` with its code:
|
|
22
|
+
> `JobStub` gains `site` (the site's identity key id) and loses
|
|
23
|
+
> `audienceAllow`; `ResultRequest` gains `leaseId`; `HeartbeatResponse` loses
|
|
24
|
+
> `leases`, which nothing read; `WireErrorCode` gains `not-ready`,
|
|
25
|
+
> `clock-skew` and `forbidden`, and `403` is `forbidden` rather than
|
|
26
|
+
> `unauthorized`. `RESULT_PROVENANCE` is superseded by
|
|
27
|
+
> `PROVENANCE_NAMES_DEVICE`. See `byollm_009` Amendment A.>
|
|
28
|
+
> **`alpha.16` is a breaking wire change — daemons and relays again, not app
|
|
29
|
+
> authors.** `app.enqueue(...)` and reading results are unchanged. All five
|
|
30
|
+
> packages move together: both ends parse `.strict()`, so a mixed pair
|
|
31
|
+
> refuses.
|
|
32
|
+
>
|
|
33
|
+
> What moved, all of it Tier 2 of `cloud_008`: `model`, `backendClass` and
|
|
34
|
+
> `durationMs` come off `ResultRequest` and are sealed **inside** the result
|
|
35
|
+
> envelope as `SealedOutcome = { outcome, ran }` — so a daemon can no longer
|
|
36
|
+
> declare a model it did not sign, and a relay carries neither.
|
|
37
|
+
> `HeartbeatResponse` loses `leases` (nothing read it) and now reports real
|
|
38
|
+
> cancellations instead of an empty list. `WireErrorCode` gains `forbidden`
|
|
39
|
+
> for 403, leaving `unauthorized` at exactly 401. The relay gained a
|
|
40
|
+
> site-plane `cancel` endpoint, honours `stub.deadlineAt`, honours
|
|
41
|
+
> `stub.audience`, and remembers a refusal.>
|
|
42
|
+
> **`alpha.17` is additive** — no wire change. It exports `ReleaseReason`,
|
|
43
|
+
> which `RoutingStore.releaseLeases` names and the package did not export, so
|
|
44
|
+
> the interface was unimplementable outside this repo.>
|
|
45
|
+
> **`alpha.18` is a breaking wire change — daemons and relays, not app
|
|
46
|
+
> authors.** `app.enqueue(...)` and reading results are unchanged. All five
|
|
47
|
+
> packages move together.
|
|
48
|
+
>
|
|
49
|
+
> The **bearer token is gone**: off `PairPollResponse`, off the runner row,
|
|
50
|
+
> off the daemon's pairings file, out of the adapter's schema. It was minted,
|
|
51
|
+
> hashed and stored on two disks and never sent, looked up or compared —
|
|
52
|
+
> `REQUESTS_SIGNED_NOT_BEARER` was enforced by signatures the whole time. If
|
|
53
|
+
> you run the Supabase adapter, apply
|
|
54
|
+
> `20260819000000_drop_runner_token.sql`; `byollm_approve_pairing` now takes
|
|
55
|
+
> one argument. A pairings file written by an older daemon still loads.
|
|
56
|
+
>
|
|
57
|
+
> `model`, `backendClass` and `durationMs` moved **inside** the sealed result
|
|
58
|
+
> (`SealedOutcome = { outcome, ran }`), so a daemon cannot declare a model it
|
|
59
|
+
> did not sign and a relay carries none of them. Writing a `RoutingStore`?
|
|
60
|
+
> `releaseLeases` takes an optional `reason` and `complete` requires
|
|
61
|
+
> `leaseId`, and **an implementation that ignores either still typechecks** —
|
|
62
|
+
> run the store contract tests.>
|
|
63
|
+
> **`alpha.19` is additive on the wire and a behaviour change in every
|
|
64
|
+
> store.** `ResultResponse` gains an optional `duplicate`. Nothing is removed,
|
|
65
|
+
> so an older daemon keeps working — but the *order* two rules are checked in
|
|
66
|
+
> has changed, and a `RoutingStore` implementation must change with it.
|
|
67
|
+
>
|
|
68
|
+
> `complete` now checks **terminal state before holder**, scoped to the device
|
|
69
|
+
> that finished the job: a replay from that device is answered `duplicate:
|
|
70
|
+
> true` with a 2xx, and anyone else gets exactly the refusal they would get
|
|
71
|
+
> for a job that is not terminal. Previously `RESULT_IDEMPOTENT` held only
|
|
72
|
+
> because the lease is nulled on success, so the holder check tripped first —
|
|
73
|
+
> deleting the idempotency branch failed no test. Run the store contract
|
|
74
|
+
> tests; the compiler cannot see this.
|
|
75
|
+
|
|
76
|
+
<!-- release-note 0.1.0-alpha.21 -->
|
|
77
|
+
> [!NOTE]
|
|
78
|
+
> **`0.1.0-alpha.20` is not a complete release — do not pin it.** Four
|
|
79
|
+
> packages published and `@byollm/server` did not: a Sigstore
|
|
80
|
+
> transparency-log 409 on its provenance attestation. The workflow's
|
|
81
|
+
> "already published" guard correctly refuses to resume a partial publish,
|
|
82
|
+
> so `0.1.0-alpha.21` is that release, whole.
|
|
83
|
+
>
|
|
84
|
+
> If you run the Supabase adapter, `alpha.21` needs
|
|
85
|
+
> `20260819010000_completed_by_lease_id.sql`: alpha.19 shipped §3.6's
|
|
86
|
+
> ordering without the column it stores the grant in.
|
|
87
|
+
|
|
88
|
+
<!-- release-note 0.1.0-alpha.40 -->
|
|
89
|
+
**`byollm install` — stop keeping a terminal open.** The daemon can now run
|
|
90
|
+
under your computer's own supervisor and restart itself if it stops: a launchd
|
|
91
|
+
agent on macOS, a `systemd --user` unit on Linux, a logon task on Windows. All
|
|
92
|
+
user-level — no root, no system directories, and `byollm uninstall` takes it
|
|
93
|
+
away. `byollm status` gained a line saying whether it is actually supervised
|
|
94
|
+
right now, including the state that matters most: installed but not running,
|
|
95
|
+
which looks fine from an app's dashboard and serves nothing.
|
|
96
|
+
|
|
97
|
+
If you are running via `npx`, install properly first (`npm install -g
|
|
98
|
+
byollm@alpha`) — `install` refuses to supervise a copy in npx's cache, because
|
|
99
|
+
npm deletes that directory and the service would fail at some later boot.
|
|
100
|
+
|
|
101
|
+
<!-- release-note 0.1.0-alpha.41 -->
|
|
102
|
+
**`onNoRunner` takes a string.** Your fallback answer is your own value, not
|
|
103
|
+
wire data, and handing back a whole result record for it was ceremony — the
|
|
104
|
+
README's own example got the shape wrong, which is how this was found.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const { outcome, fallback } = await job.result({
|
|
108
|
+
onNoRunner: () => runOnHostedModel(transcript),
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Whatever you return, `result()` labels it `fallback: true` — the stamp is
|
|
113
|
+
applied by the wait, not taken from you, so an answer that did not run on
|
|
114
|
+
somebody's device cannot be reported as though it did (`FALLBACK_LABELED`).
|
|
115
|
+
Both delivery channels do it, polling and Supabase Realtime. Records still
|
|
116
|
+
work; they just get labelled too.
|
|
15
117
|
|
|
16
118
|
# `@byollm/conformance`
|
|
17
119
|
|
|
@@ -29,7 +131,7 @@ byollm conformance — my server
|
|
|
29
131
|
✓ C001_PAIRING_BINDS_ONE_USER a runner token is bound to exactly the approving user (279ms)
|
|
30
132
|
✓ C002_JOB_ROUND_TRIP an enqueued job runs on the owner's daemon and the result comes back (83ms)
|
|
31
133
|
…
|
|
32
|
-
|
|
134
|
+
32 checks passed — my server is byollm-compatible.
|
|
33
135
|
```
|
|
34
136
|
|
|
35
137
|
## What it actually does
|
|
@@ -79,7 +181,7 @@ Two optional hooks worth knowing about:
|
|
|
79
181
|
|
|
80
182
|
- **`ownerId`** exists because owner ids are server-namespace-local. A target
|
|
81
183
|
backed by real auth uses uuids, not names, and a kit that assumed names
|
|
82
|
-
round-tripped would be assuming away the very thing the `
|
|
184
|
+
round-tripped would be assuming away the very thing the `team` allowlist is
|
|
83
185
|
about.
|
|
84
186
|
- **`advanceTime`** lets an in-memory server run the lease and TTL checks in
|
|
85
187
|
milliseconds. A real Postgres cannot fake its clock, so the kit waits for
|
|
@@ -95,7 +197,7 @@ Two optional hooks worth knowing about:
|
|
|
95
197
|
| `C003_UNKNOWN_KIND_REFUSED` | a daemon is never handed a kind it did not advertise |
|
|
96
198
|
| `C004_LEASE_RECLAIM` | a job whose runner vanished is offered again, losing nothing |
|
|
97
199
|
| `C005_AUDIENCE_MATRIX` | all nine audience × offer-scope combinations |
|
|
98
|
-
| `C006_NAMED_LOCAL_ALLOWLIST` | `
|
|
200
|
+
| `C006_NAMED_LOCAL_ALLOWLIST` | `team` runs only once the daemon's own list admits it |
|
|
99
201
|
| `C007_SUBSCRIPTION_SELF_LOCK` | a subscription backend refuses others' work at any scope |
|
|
100
202
|
| `C008_REVOCATION` | a revoked daemon stops mid-queue |
|
|
101
203
|
| `C009_CANCEL_MID_FLIGHT` | cancel aborts a running job's backend call |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
auditDeployment,
|
|
4
|
+
formatPostureReport
|
|
5
|
+
} from "./chunk-2EJHYOQS.js";
|
|
6
|
+
|
|
7
|
+
// src/audit-cli.ts
|
|
8
|
+
var [url, basePath, originAddress] = process.argv.slice(2);
|
|
9
|
+
if (url === void 0) {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
"usage: byollm-audit-deployment <url> [base-path] [origin-address]\n e.g. byollm-audit-deployment https://hub.byollm.cloud\n"
|
|
12
|
+
);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
var report = await auditDeployment({
|
|
16
|
+
url,
|
|
17
|
+
...basePath === void 0 ? {} : { basePath },
|
|
18
|
+
// `D008` asks the origin directly. Without it that check says so rather
|
|
19
|
+
// than guessing an address and reporting a posture it never tested.
|
|
20
|
+
...originAddress === void 0 ? {} : { originAddress }
|
|
21
|
+
});
|
|
22
|
+
process.stdout.write(`
|
|
23
|
+
${formatPostureReport(report)}`);
|
|
24
|
+
process.exit(report.passed ? 0 : 1);
|
|
25
|
+
//# sourceMappingURL=audit-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audit-cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { auditDeployment, formatPostureReport } from \"./deployment.js\";\n\n/**\n * `byollm-audit-deployment <url>` — what a stranger can do to a running relay.\n *\n * Distinct from `byollm-certify`, which drives a real daemon against a target\n * that may be an in-process handler. This one has a URL and nothing else,\n * which is exactly what an attacker has, and it exists because eight\n * freeze-gate findings came from a suite in which nothing was ever a stranger.\n *\n * ```bash\n * npx byollm-audit-deployment https://hub.byollm.cloud\n * ```\n *\n * Safe to run against production: nothing writes, nothing floods, and every\n * request is one an ordinary scanner would make.\n */\nconst [url, basePath, originAddress] = process.argv.slice(2);\n\nif (url === undefined) {\n process.stderr.write(\n \"usage: byollm-audit-deployment <url> [base-path] [origin-address]\\n\" +\n \" e.g. byollm-audit-deployment https://hub.byollm.cloud\\n\",\n );\n process.exit(2);\n}\n\nconst report = await auditDeployment({\n url,\n ...(basePath === undefined ? {} : { basePath }),\n // `D008` asks the origin directly. Without it that check says so rather\n // than guessing an address and reporting a posture it never tested.\n ...(originAddress === undefined ? {} : { originAddress }),\n});\nprocess.stdout.write(`\\n${formatPostureReport(report)}`);\nprocess.exit(report.passed ? 0 : 1);\n"],"mappings":";;;;;;;AAkBA,IAAM,CAAC,KAAK,UAAU,aAAa,IAAI,QAAQ,KAAK,MAAM,CAAC;AAE3D,IAAI,QAAQ,QAAW;AACrB,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,MAAM,gBAAgB;AAAA,EACnC;AAAA,EACA,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;AAAA;AAAA;AAAA,EAG7C,GAAI,kBAAkB,SAAY,CAAC,IAAI,EAAE,cAAc;AACzD,CAAC;AACD,QAAQ,OAAO,MAAM;AAAA,EAAK,oBAAoB,MAAM,CAAC,EAAE;AACvD,QAAQ,KAAK,OAAO,SAAS,IAAI,CAAC;","names":[]}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// src/deployment.ts
|
|
2
|
+
import { connect as tlsConnect } from "tls";
|
|
3
|
+
import { request as httpsRequest } from "https";
|
|
4
|
+
import {
|
|
5
|
+
PROTOCOL_VERSION,
|
|
6
|
+
generateKeys,
|
|
7
|
+
signRequest,
|
|
8
|
+
signSiteRequest
|
|
9
|
+
} from "@byollm/protocol";
|
|
10
|
+
var STUB = {
|
|
11
|
+
id: "posture-probe",
|
|
12
|
+
kind: "llm.generate",
|
|
13
|
+
owner: "nobody",
|
|
14
|
+
audience: "private",
|
|
15
|
+
sizeClass: "small",
|
|
16
|
+
streaming: false,
|
|
17
|
+
// Far enough out that a deployment cannot pass by calling it expired.
|
|
18
|
+
deadlineAt: 41024448e5
|
|
19
|
+
};
|
|
20
|
+
var REFUSED = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
21
|
+
async function refusedByByollm(response) {
|
|
22
|
+
if (!REFUSED.has(response.status)) {
|
|
23
|
+
return { ok: false, why: `answered ${String(response.status)}` };
|
|
24
|
+
}
|
|
25
|
+
let body;
|
|
26
|
+
try {
|
|
27
|
+
body = await response.json();
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
why: `answered ${String(response.status)} but not as byollm \u2014 something else is serving this path`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const shaped = typeof body === "object" && body !== null && typeof body.error === "string";
|
|
35
|
+
return shaped ? { ok: true, why: `answered ${String(response.status)}` } : {
|
|
36
|
+
ok: false,
|
|
37
|
+
why: `answered ${String(response.status)} with a body byollm would not send \u2014 something else is serving this path`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var outcome = (passed, detail) => ({
|
|
41
|
+
passed,
|
|
42
|
+
detail
|
|
43
|
+
});
|
|
44
|
+
async function servedCertificate(address, host) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const socket = tlsConnect(
|
|
47
|
+
{
|
|
48
|
+
host: address,
|
|
49
|
+
servername: host,
|
|
50
|
+
port: 443,
|
|
51
|
+
timeout: 15e3,
|
|
52
|
+
rejectUnauthorized: false
|
|
53
|
+
},
|
|
54
|
+
() => {
|
|
55
|
+
const peer = socket.getPeerCertificate();
|
|
56
|
+
const alt = (peer.subjectaltname ?? "").split(",").map((entry) => entry.trim().replace(/^DNS:/, "")).filter((entry) => entry.length > 0);
|
|
57
|
+
const cn = peer.subject.CN;
|
|
58
|
+
socket.end();
|
|
59
|
+
resolve({
|
|
60
|
+
names: alt.length > 0 ? alt : [typeof cn === "string" ? cn : ""],
|
|
61
|
+
validTo: peer.valid_to
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
socket.on("error", () => {
|
|
66
|
+
resolve(void 0);
|
|
67
|
+
});
|
|
68
|
+
socket.on("timeout", () => {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
resolve(void 0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
var POSTURE_CHECKS = Object.freeze([
|
|
75
|
+
{
|
|
76
|
+
id: "D001_SITE_ENQUEUE_REFUSES_UNSIGNED",
|
|
77
|
+
title: "an anonymous caller cannot enqueue work in a site's name",
|
|
78
|
+
cites: ["REQUESTS_SIGNED_NOT_BEARER"],
|
|
79
|
+
async run({ origin, fetch: f }) {
|
|
80
|
+
const response = await f(`${origin}/relay/site/enqueue`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json" },
|
|
83
|
+
// The version, because the auditor is a site-plane client like any
|
|
84
|
+
// other (§B.4). Without it the relay refuses on the handshake and
|
|
85
|
+
// this check would report "refused" for a reason that has nothing to
|
|
86
|
+
// do with signatures — success for an unrelated reason, which is the
|
|
87
|
+
// failure this whole file was written against.
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
90
|
+
siteId: "any-site",
|
|
91
|
+
stub: STUB
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
const refusal = await refusedByByollm(response);
|
|
95
|
+
return outcome(refusal.ok, `POST /relay/site/enqueue ${refusal.why}`);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "D002_SITE_READS_REFUSE_UNSIGNED",
|
|
100
|
+
title: "an anonymous caller cannot read who is online",
|
|
101
|
+
cites: ["REQUESTS_SIGNED_NOT_BEARER"],
|
|
102
|
+
async run({ origin, fetch: f }) {
|
|
103
|
+
const why = [];
|
|
104
|
+
let ok = true;
|
|
105
|
+
for (const path of ["pending", "results"]) {
|
|
106
|
+
const response = await f(
|
|
107
|
+
`${origin}/relay/site/${path}?siteId=any-site&protocolVersion=${PROTOCOL_VERSION}`
|
|
108
|
+
);
|
|
109
|
+
const refusal = await refusedByByollm(response);
|
|
110
|
+
ok &&= refusal.ok;
|
|
111
|
+
why.push(`${path} ${refusal.why}`);
|
|
112
|
+
}
|
|
113
|
+
return outcome(ok, why.join("; "));
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "D003_DAEMON_PLANE_REFUSES_UNSIGNED",
|
|
118
|
+
title: "an anonymous caller cannot claim work",
|
|
119
|
+
cites: ["REQUESTS_SIGNED_NOT_BEARER", "CLAIM_REQUIRES_CAPABILITY"],
|
|
120
|
+
async run({ origin, basePath, fetch: f }) {
|
|
121
|
+
const response = await f(`${origin}${basePath}/claim`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "content-type": "application/json" },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
126
|
+
runnerId: "nobody",
|
|
127
|
+
max: 1,
|
|
128
|
+
capabilities: [{ kind: "llm.generate", models: ["any"] }]
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
const refusal = await refusedByByollm(response);
|
|
132
|
+
return outcome(refusal.ok, `POST ${basePath}/claim ${refusal.why}`);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "D004_REFUSES_A_STRANGER_S_VALID_SIGNATURE",
|
|
137
|
+
title: "a well-formed signature from an unknown key is not enough",
|
|
138
|
+
cites: ["REQUESTS_SIGNED_NOT_BEARER", "KEYS_EXCHANGED_AT_CONSENT"],
|
|
139
|
+
async run({ origin, basePath, fetch: f }) {
|
|
140
|
+
const stranger = generateKeys(Date.now());
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const siteBody = JSON.stringify({
|
|
143
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
144
|
+
siteId: "any-site",
|
|
145
|
+
stub: STUB
|
|
146
|
+
});
|
|
147
|
+
const siteSignature = signSiteRequest(stranger, {
|
|
148
|
+
endpoint: "enqueue",
|
|
149
|
+
siteId: "any-site",
|
|
150
|
+
issuedAt: now,
|
|
151
|
+
body: siteBody
|
|
152
|
+
});
|
|
153
|
+
const site = await f(`${origin}/relay/site/enqueue`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
"content-type": "application/json",
|
|
157
|
+
"x-byollm-site": "any-site",
|
|
158
|
+
"x-byollm-issued-at": String(siteSignature.issuedAt),
|
|
159
|
+
"x-byollm-signature": siteSignature.signature
|
|
160
|
+
},
|
|
161
|
+
body: siteBody
|
|
162
|
+
});
|
|
163
|
+
const daemonBody = JSON.stringify({
|
|
164
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
165
|
+
runnerId: "nobody",
|
|
166
|
+
max: 1,
|
|
167
|
+
capabilities: [{ kind: "llm.generate", models: ["any"] }]
|
|
168
|
+
});
|
|
169
|
+
const daemonSignature = signRequest(stranger, {
|
|
170
|
+
endpoint: "claim",
|
|
171
|
+
runnerId: "nobody",
|
|
172
|
+
issuedAt: now,
|
|
173
|
+
body: daemonBody
|
|
174
|
+
});
|
|
175
|
+
const daemon = await f(`${origin}${basePath}/claim`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: {
|
|
178
|
+
"content-type": "application/json",
|
|
179
|
+
"x-byollm-runner": "nobody",
|
|
180
|
+
"x-byollm-issued-at": String(daemonSignature.issuedAt),
|
|
181
|
+
"x-byollm-signature": daemonSignature.signature
|
|
182
|
+
},
|
|
183
|
+
body: daemonBody
|
|
184
|
+
});
|
|
185
|
+
const siteRefusal = await refusedByByollm(site);
|
|
186
|
+
const daemonRefusal = await refusedByByollm(daemon);
|
|
187
|
+
return outcome(
|
|
188
|
+
siteRefusal.ok && daemonRefusal.ok,
|
|
189
|
+
`signed by a stranger: site ${siteRefusal.why}, daemon ${daemonRefusal.why}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "D011_VERSION_NAMED_ON_BOTH_PLANES",
|
|
195
|
+
title: "an unknown protocol version is refused by name, not by accident",
|
|
196
|
+
cites: ["VERSION_HANDSHAKE_REQUIRED"],
|
|
197
|
+
async run({ origin, fetch: f }) {
|
|
198
|
+
const probes = [
|
|
199
|
+
{
|
|
200
|
+
where: "daemon",
|
|
201
|
+
response: await f(`${origin}/byollm/claim`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "content-type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ protocolVersion: "99", max: 1 })
|
|
205
|
+
})
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
where: "site",
|
|
209
|
+
response: await f(`${origin}/relay/site/enqueue`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: { "content-type": "application/json" },
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
protocolVersion: "99",
|
|
214
|
+
siteId: "any-site",
|
|
215
|
+
stub: STUB
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
const wrong = [];
|
|
221
|
+
for (const probe of probes) {
|
|
222
|
+
let body = {};
|
|
223
|
+
try {
|
|
224
|
+
body = await probe.response.json();
|
|
225
|
+
} catch {
|
|
226
|
+
wrong.push(
|
|
227
|
+
`${probe.where}: answered ${String(probe.response.status)} and not as byollm`
|
|
228
|
+
);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (body.error !== "unsupported-protocol-version") {
|
|
232
|
+
wrong.push(
|
|
233
|
+
`${probe.where}: answered ${String(probe.response.status)} ${String(body.error)}`
|
|
234
|
+
);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(body.supported)) {
|
|
238
|
+
wrong.push(`${probe.where}: refused without naming what it speaks`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return outcome(
|
|
242
|
+
wrong.length === 0,
|
|
243
|
+
wrong.length === 0 ? "both planes name the version they speak" : wrong.join("; ")
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "D005_NO_DEBUG_SURFACE",
|
|
249
|
+
title: "the debug page is not on the internet",
|
|
250
|
+
cites: [],
|
|
251
|
+
async run({ origin, basePath, fetch: f }) {
|
|
252
|
+
const paths = ["/debug", `${basePath}/debug`];
|
|
253
|
+
const statuses = [];
|
|
254
|
+
for (const path of paths) {
|
|
255
|
+
const response = await f(`${origin}${path}`);
|
|
256
|
+
statuses.push(response.status);
|
|
257
|
+
}
|
|
258
|
+
return outcome(
|
|
259
|
+
// **Not merely "not 200"** — cloud_009 §3 made the page per-site, so
|
|
260
|
+
// an enabled one answers a probe with no site id `400` rather than
|
|
261
|
+
// rendering. A check that accepted anything but 200 would pass
|
|
262
|
+
// against a deployment whose debug page is one query parameter away,
|
|
263
|
+
// which is finding eleven with a shorter walk.
|
|
264
|
+
//
|
|
265
|
+
// `404` is the only answer that means the route does not exist. The
|
|
266
|
+
// reference relay says exactly that when its debug page is off, and
|
|
267
|
+
// says `400` when it is on and the caller named no site.
|
|
268
|
+
statuses.every((status) => status === 404),
|
|
269
|
+
`debug paths answered ${statuses.map(String).join("/")}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "D006_NO_PATH_DISPATCH",
|
|
275
|
+
title: "a handler cannot be reached by dressing a path up to look like it",
|
|
276
|
+
cites: ["REQUESTS_SIGNED_NOT_BEARER"],
|
|
277
|
+
async run({ origin, fetch: f }) {
|
|
278
|
+
const response = await f(`${origin}/literally/anything/claim`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "content-type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ protocolVersion: PROTOCOL_VERSION, max: 1 })
|
|
282
|
+
});
|
|
283
|
+
const refusal = await refusedByByollm(response);
|
|
284
|
+
return outcome(
|
|
285
|
+
refusal.ok && response.status === 404,
|
|
286
|
+
`POST /literally/anything/claim ${refusal.why}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "D007_TLS_ONLY",
|
|
292
|
+
title: "the deployment is reached over TLS, and plaintext is not served",
|
|
293
|
+
cites: [],
|
|
294
|
+
async run({ origin, fetch: f }) {
|
|
295
|
+
if (!origin.startsWith("https://")) {
|
|
296
|
+
return outcome(false, `origin is ${origin} \u2014 this audit saw no TLS`);
|
|
297
|
+
}
|
|
298
|
+
const plain = origin.replace("https://", "http://");
|
|
299
|
+
const response = await f(`${plain}/healthz`, { redirect: "manual" });
|
|
300
|
+
const redirected = response.status >= 300 && response.status < 400 && (response.headers.get("location") ?? "").startsWith("https://");
|
|
301
|
+
return outcome(
|
|
302
|
+
redirected || REFUSED.has(response.status),
|
|
303
|
+
`plaintext answered ${String(response.status)}${redirected ? " and redirects to https" : ""}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "D008_ORIGIN_NOT_PUBLIC",
|
|
309
|
+
title: "the origin answers the edge, and nobody else",
|
|
310
|
+
cites: [],
|
|
311
|
+
async run({ origin, originAddress }) {
|
|
312
|
+
if (originAddress === void 0) {
|
|
313
|
+
return outcome(
|
|
314
|
+
false,
|
|
315
|
+
"no origin address given \u2014 this posture was not measured (pass the origin's address as the third argument)"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const host = new URL(origin).host;
|
|
319
|
+
const status = await new Promise((resolve) => {
|
|
320
|
+
const request = httpsRequest(
|
|
321
|
+
{
|
|
322
|
+
host: originAddress,
|
|
323
|
+
servername: host,
|
|
324
|
+
headers: { host },
|
|
325
|
+
path: "/readyz",
|
|
326
|
+
method: "GET",
|
|
327
|
+
rejectUnauthorized: false,
|
|
328
|
+
timeout: 15e3
|
|
329
|
+
},
|
|
330
|
+
(response) => {
|
|
331
|
+
response.resume();
|
|
332
|
+
resolve(response.statusCode ?? 0);
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
request.on("error", () => {
|
|
336
|
+
resolve("refused");
|
|
337
|
+
});
|
|
338
|
+
request.on("timeout", () => {
|
|
339
|
+
request.destroy();
|
|
340
|
+
resolve("refused");
|
|
341
|
+
});
|
|
342
|
+
request.end();
|
|
343
|
+
});
|
|
344
|
+
if (status === "refused") {
|
|
345
|
+
return outcome(true, "the origin address refused the connection");
|
|
346
|
+
}
|
|
347
|
+
return outcome(
|
|
348
|
+
status === 403,
|
|
349
|
+
`the origin answered ${String(status)} to a direct request`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "D009_CERT_NAMES_THE_PINNED_HOST",
|
|
355
|
+
title: "the certificate names the hostname daemons pin",
|
|
356
|
+
cites: [],
|
|
357
|
+
async run({ origin, originAddress }) {
|
|
358
|
+
const host = new URL(origin).host;
|
|
359
|
+
if (originAddress === void 0) {
|
|
360
|
+
return outcome(
|
|
361
|
+
false,
|
|
362
|
+
"no origin address given \u2014 this posture was not measured. Asking the hostname reads the edge's certificate, which names it by construction and proves nothing about the origin"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const served = await servedCertificate(originAddress, host);
|
|
366
|
+
const names = served?.names;
|
|
367
|
+
if (names === void 0) {
|
|
368
|
+
return outcome(
|
|
369
|
+
false,
|
|
370
|
+
`could not read a certificate from ${originAddress}`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const covered = names.some(
|
|
374
|
+
(name) => name === host || name.startsWith("*.") && host.endsWith(name.slice(1))
|
|
375
|
+
);
|
|
376
|
+
return outcome(
|
|
377
|
+
covered,
|
|
378
|
+
covered ? `served a certificate naming ${host}` : `the certificate names ${names.join(", ")} \u2014 not ${host}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "D010_CERT_HAS_LIFE_LEFT",
|
|
384
|
+
title: "the certificate is not about to expire",
|
|
385
|
+
cites: [],
|
|
386
|
+
async run({ origin, originAddress }) {
|
|
387
|
+
const host = new URL(origin).host;
|
|
388
|
+
if (originAddress === void 0) {
|
|
389
|
+
return outcome(
|
|
390
|
+
false,
|
|
391
|
+
"no origin address given \u2014 this posture was not measured (the edge's certificate is Cloudflare's to renew, not ours)"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const served = await servedCertificate(originAddress, host);
|
|
395
|
+
if (served === void 0) {
|
|
396
|
+
return outcome(
|
|
397
|
+
false,
|
|
398
|
+
`could not read a certificate from ${originAddress}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const days = Math.round(
|
|
402
|
+
(Date.parse(served.validTo) - Date.now()) / 864e5
|
|
403
|
+
);
|
|
404
|
+
return outcome(
|
|
405
|
+
days > 90,
|
|
406
|
+
`the certificate expires ${served.validTo} (${String(days)} days)`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
]);
|
|
411
|
+
async function auditDeployment(options) {
|
|
412
|
+
const origin = options.url.replace(/\/+$/, "");
|
|
413
|
+
const context = {
|
|
414
|
+
origin,
|
|
415
|
+
basePath: (options.basePath ?? "/byollm").replace(/\/+$/, ""),
|
|
416
|
+
...options.originAddress === void 0 ? {} : { originAddress: options.originAddress },
|
|
417
|
+
fetch: options.fetch ?? globalThis.fetch
|
|
418
|
+
};
|
|
419
|
+
const results = [];
|
|
420
|
+
for (const check of POSTURE_CHECKS) {
|
|
421
|
+
let result;
|
|
422
|
+
try {
|
|
423
|
+
result = { ...await check.run(context), ...describe(check) };
|
|
424
|
+
} catch (error) {
|
|
425
|
+
result = {
|
|
426
|
+
...describe(check),
|
|
427
|
+
passed: false,
|
|
428
|
+
detail: `the probe failed: ${error instanceof Error ? error.message : "unknown"}`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
results.push(result);
|
|
432
|
+
options.onProgress?.(result);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
origin,
|
|
436
|
+
passed: results.every((result) => result.passed),
|
|
437
|
+
results
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
var describe = (check) => ({
|
|
441
|
+
id: check.id,
|
|
442
|
+
title: check.title,
|
|
443
|
+
cites: check.cites
|
|
444
|
+
});
|
|
445
|
+
function formatPostureReport(report) {
|
|
446
|
+
const lines = [`deployment posture \u2014 ${report.origin}`, ""];
|
|
447
|
+
for (const result of report.results) {
|
|
448
|
+
lines.push(` ${result.passed ? "ok " : "FAIL"} ${result.id}`);
|
|
449
|
+
lines.push(` ${result.title}`);
|
|
450
|
+
lines.push(` ${result.detail}`);
|
|
451
|
+
if (result.cites.length > 0) {
|
|
452
|
+
lines.push(` cites ${result.cites.join(", ")}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const passed = report.results.filter((result) => result.passed).length;
|
|
456
|
+
lines.push(
|
|
457
|
+
"",
|
|
458
|
+
report.passed ? `${String(passed)}/${String(report.results.length)} \u2014 a stranger got nowhere.` : `${String(passed)}/${String(report.results.length)} \u2014 a stranger got somewhere. See FAIL above.`,
|
|
459
|
+
""
|
|
460
|
+
);
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export {
|
|
465
|
+
POSTURE_CHECKS,
|
|
466
|
+
auditDeployment,
|
|
467
|
+
formatPostureReport
|
|
468
|
+
};
|
|
469
|
+
//# sourceMappingURL=chunk-2EJHYOQS.js.map
|