cursedops 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,15 @@ bun add cursedops
8
8
 
9
9
  | subpath | what it is |
10
10
  |---|---|
11
- | `cursedops/roots` | finding a generation's roots, and a checkout's package root, without knowing a path |
12
- | `cursedops/launchd` | installing, replacing and removing a macOS launchd user agent |
13
- | `cursedops/smoke` | the scaffolding of a deployed smoke — the ledger, the fetch, the DNS hint, the exit code — and the one check no app owns: every address of a deployment serving the same built client |
11
+ | `cursedops/roots` | finding a generation's roots, and a checkout's package root, without knowing a path — and the `cd` prefix of a printed command that RUNS when pasted (`repoCd`, 0.5.0) |
12
+ | `cursedops/paths` | the generation's whole-tree laws (`check-paths`, `check-doc-citations`, plus any `--also`) run over ONE repo, from a checkout or a worktree — and its `forge-paths` bin (0.5.0) |
13
+ | `cursedops/launchd` | installing, replacing and removing a macOS launchd user agent, and the port a LIVE job serves on (`livePort`, 0.5.0) |
14
+ | `cursedops/smoke` | the scaffolding of a deployed smoke — the ledger, the fetch, the DNS hint, the exit code — the one check no app owns (every address of a deployment serving the same built client), and since 0.5.0 its VERDICTS: the origin asked on loopback, the smoke's own environment, a network that lies about DNS, and a version that has settled |
15
+ | `cursedops/worker-deploy` | the sequence that ships a Cloudflare Worker — clean tree, stage first, schema, `--var` stamp, secrets, smoke — and the readers it needs (0.5.0) |
16
+ | `cursedops/worker-secrets` | a Worker holding EXACTLY its deployment's secrets, uploaded over a pipe, read back (0.5.0) |
17
+ | `cursedops/worker-rollback` | a hostname back on its Mac origin: origin first, route second, the route found rather than typed (0.5.0) |
18
+ | `cursedops/edge-fetch` | a request to a deployed Worker as a script must make it — curl pinned past the Mac's negative DNS cache (0.5.0) |
19
+ | `cursedops/d1-import` | a cutover's data proof — SQL literals and the row-for-row comparison, never a count (0.5.0) |
14
20
  | `cursedops/serve` | the static tier's four helpers — the path-traversal guard, the MIME table, the hashed-asset test, the crash handlers |
15
21
  | `cursedops/api-floor` | the rule that an unmatched `/api/...` is a phrase and never the app shell — the namespace predicates, the trailing-slash normaliser and the default 404 body. No `node:` import, so it mounts inside a Worker |
16
22
  | `cursedops/build-info` | which commit a checkout-served process is running and whether its tree was dirty — read once at load, and a `null` retried in the background rather than cached for the life of the process. `node:child_process`, so never in a Worker |
@@ -99,8 +105,15 @@ needs to read it:
99
105
  `deploy.ts` is a fourth: the two scripts share a shape and roughly thirty lines of
100
106
  helpers, but they sequence genuinely different steps and a shared deploy driver is
101
107
  exactly the 277-file mistake starting again. What they share instead is the **exit-code
102
- contract** in `cursedops/smoke` — `0` keep, `1` roll back, `2` edge fault, do not roll
103
- back — which is the only part both deploy scripts actually read.
108
+ contract** in `cursedops/smoke` — `0` keep, `1` roll back, `2` not the app (the edge, the
109
+ network, or the smoke's own environment), do not roll back — which is the only part both
110
+ deploy scripts actually read.
111
+
112
+ 🔴 **The WORKER deploy is the other way round, and `cursedops/worker-deploy` is not this
113
+ mistake made again.** The Mac `deploy.ts` scripts sequence genuinely different steps; the
114
+ four Worker `worker-deploy.ts` scripts sequence the SAME nine, and a `diff` says so — what
115
+ differed between them was which of the nine each copy had forgotten. So the sequence is
116
+ shared and every step's command is data the app passes; see "The Worker toolkit" below.
104
117
 
105
118
  ## `cursedops/roots`
106
119
 
@@ -127,6 +140,54 @@ runs from there. A clone is its own primary and still gets `null`.
127
140
  library that hardcoded `$HOME/.<name>` would be correct for exactly one generation and
128
141
  silently wrong for its successor — which is the defect this replaced.
129
142
 
143
+ ### `repoCd` — a printed command that RUNS (0.5.0, task 092)
144
+
145
+ ```ts
146
+ import { otherRepoCommand, repoCommand } from "cursedops/roots";
147
+
148
+ const REPO_IN_FORGE = "apps/auth"; // the ONE thing the app keeps
149
+ repoCommand(REPO_IN_FORGE, "bun run geo:build", import.meta.dir);
150
+ // → source "…/forge.env" && cd "$FORGE/apps/auth" && bun run geo:build
151
+ otherRepoCommand("apps/station", REPO_IN_FORGE, "bun run geo", import.meta.dir);
152
+ ```
153
+
154
+ Rule 1: four repos wrote it — `apps/binary-server/src/repoCommand.ts` (the original,
155
+ 2026-09-18), `apps/auth/src/kit/repoCommand.ts`, and `scripts/forgeRoot.ts` in `family` and
156
+ `roms`. Rule 3: `check-paths`' `deadCd` rule measured twelve printed commands in five repos
157
+ `cd`-ing into directories two generations gone, each read at a moment something was already
158
+ wrong. The segment is always the CALLER's (the library may not know a repo's name) and is
159
+ refused if it is not a plain relative path; `from` is REQUIRED, because a default would be this
160
+ library's own directory inside `node_modules`. A `$FORGE` with no `forge.env` in it is stale and
161
+ is not honoured (`verifiedForgeRoot`; `forgeCodeRoot` keeps its older contract). Every fallback is
162
+ a directory that EXISTS, and nothing composed here ever spells `cd ~/`. `repoCommand.test.ts`
163
+ asserts on the COMPOSED string, which is the only thing `check-paths` cannot see.
164
+
165
+ ### The state root (task 160)
166
+
167
+ Nothing new was needed: `forgeState(from, env)` / `requireForgeState(from, env)` already read
168
+ `FORGE_STATE` out of the `forge.env` above the calling module, which is what a launchd job with
169
+ no environment and a worktree both need. An app's hand-spelled `join(HOME, ".code", "<name>")`
170
+ becomes `requireForgeState(import.meta.dir, env)`. Text generated for ANOTHER machine (a systemd
171
+ unit, a shell installer) interpolates the VALUE — `relative(homedir(), requireForgeState(…))`
172
+ after `%h/` or `$HOME/` — never the generation's name.
173
+
174
+ ## `cursedops/paths` (0.5.0, task 2089)
175
+
176
+ ```jsonc
177
+ "paths": "forge-paths" // check-paths + check-doc-citations
178
+ "paths": "forge-paths --also check-gate-graph.ts" // cursedbelt's extra law, as an OPTION
179
+ ```
180
+
181
+ Rule 1, eighteen times: `scripts/paths.ts` was in every app, `autopilot` and every library,
182
+ sixteen byte-identical and three already forked (`cursedbelt` +`check-gate-graph.ts`, `station`
183
+ +`check-no-clock.ts`, `autopilot` split into its own `generation.ts`). `check-copies` prices
184
+ exported definitions and this file exports nothing, so eighteen copies read as zero. Rule 3: the
185
+ wrapper exists because `bun ../../tools/check-paths.ts .` died on its first step in every
186
+ worktree (2026-09-17). The walk and the worktree follow-back are the copies' own; **no
187
+ generation is still no pass** — a lone clone exits 1 saying which walk came back empty. Every
188
+ argument that is not `--also` passes through to the laws (`bun run paths --prune`). This repo
189
+ dogfoods it: `"paths": "bun src/paths.ts"`, and its own copy is gone.
190
+
130
191
  ## `cursedops/launchd`
131
192
 
132
193
  ```ts
@@ -140,6 +201,10 @@ if (boot.code !== 0) throw new Error(boot.out);
140
201
  if (!(await answering(`http://127.0.0.1:${port}/healthz`))) throw new Error("a pid is not a service");
141
202
  ```
142
203
 
204
+ `livePort(label)` (0.5.0) reads `PORT` out of the LIVE job's own `environment` block — the
205
+ declaration the tunnel routes to — never `package.json`, which advertised the dev pair in eight
206
+ of eight apps (`autopilot doctor`'s `ports` section, 2026-09-15, lifted with its fixture).
207
+
143
208
  `RunAtLoad` and `KeepAlive` are **not defaulted** — a server wants both, a nightly
144
209
  snapshot wants neither, and a kit that decides gets one of them wrong.
145
210
 
@@ -175,6 +240,40 @@ Only same-origin `.js`/`.css` references are compared, because the zone injects
175
240
  Analytics beacon in front of the edge and not on loopback — a check that reds on a
176
241
  healthy deploy is one somebody turns off.
177
242
 
243
+ ### The verdicts (0.5.0, task 069)
244
+
245
+ ```ts
246
+ import { livePort } from "cursedops/launchd";
247
+ import { createSmoke, probeOrigin } from "cursedops/smoke";
248
+
249
+ const smoke = createSmoke({ base: PUBLIC_URL });
250
+ if (!smoke.requireEnvironment({ CF_ACCESS_CLIENT_ID: env.CF_ACCESS_CLIENT_ID }, "source $FORGE_STATE/secrets/cloudflare-access.env")) {
251
+ process.exit(smoke.report()); // 2 — the smoke could not judge; not the app
252
+ }
253
+ await probeOrigin(smoke, livePort(LABEL), { label: LABEL }); // a dead origin is 1, always
254
+ // … the app's own checks against the public hostname …
255
+ process.exit(await smoke.settle()); // asks the NETWORK before charging the app
256
+ ```
257
+
258
+ A deployed smoke lied three ways, and each is now a verdict with a test of its failure path
259
+ (`src/smokeVerdict.test.ts`):
260
+
261
+ | lie | measured | verdict | exit |
262
+ |---|---|---|---:|
263
+ | PASSED about a dead origin | `patterns` and `collections` served a cached 200 over a 503 origin, every check green; four smokes made no loopback request at all | `origin` — `probeOrigin` asks `127.0.0.1:<livePort>`; no live port is no origin | **1**, whatever else the run saw |
264
+ | FAILED about a healthy app, for want of the smoke's own credential | `desk`: `✗ FAILED — 5 of 6` because only the deploy script loaded the Access token | `environment` — `requireEnvironment` / `refuseEnvironment` | 2 |
265
+ | FAILED about a healthy app, because the network substituted DNS | 2026-09-19, airline wifi answered `1.1.1.1` for a proxied host → `403 · Error 1034` | `network` — `settle()` runs `diagnoseNetwork`, `tools/check-public-dns.ts`' range invariant lifted | 2 |
266
+
267
+ 🔴 **No new exit code**, on purpose: every deploy script reads `0` keep, `2` stop without
268
+ reverting, anything else REVERT (`apps/collections/scripts/deploy.ts:413-424` and four
269
+ siblings). A `3` for "credential missing" would be reverted by all five. The distinction is in
270
+ `verdict()` and in the sentence printed. And the network diagnosis **never retries and never
271
+ pins an address** — the deliverable is the distinction, not a way around the network.
272
+
273
+ `steadyHealth(ask, ready)` is the Worker smoke's wait: `ready` must hold for THREE reads in a
274
+ row, because on `music`'s first deploy (2026-09-23, `a54ef71`) one good `/healthz` was followed
275
+ by thirteen routes answering the pre-secrets version. It returns what answered and never judges.
276
+
178
277
  Nothing else. The checks are the part `desk` and `flix` wrote differently on purpose,
179
278
  and a shared kit that starts absorbing route lists and health-payload shapes is how the
180
279
  last one reached 277 files.
@@ -274,6 +373,40 @@ input the eight copies refused refused, at the same cost. A symlinked ancestor o
274
373
  root cancels out — which is the failure a one-sided `realpath` would have shipped to
275
374
  every app on this machine, and is its own test.
276
375
 
376
+ ## The Worker toolkit (0.5.0, task 2097)
377
+
378
+ `cursedops/worker-deploy`, `/worker-secrets`, `/worker-rollback`, `/edge-fetch`, `/d1-import`.
379
+ Rule 1, four times: `collections`, `vault`, `patterns` and `music` (copied from `vault` on
380
+ 2026-09-23) each carried the toolkit in `scripts/`, which `check-copies` did not scan. Rule 3:
381
+ the copies had drifted in ways that cost something — `patterns` never refused a dirty tree nor
382
+ stamped the commit, `vault` and `collections` judged a deploy after one `/healthz` read,
383
+ `collections`' secrets upload never deleted an extra, `patterns`' rollback typed its route id,
384
+ and only `collections`' import normalised a REAL. Each module header names its incident.
385
+ Everything that shells out (wrangler, git, curl, dig) is injectable; the suite never touches a
386
+ network.
387
+
388
+ 🔴 **`edge-fetch` is the fourth recorded exception.** Rule 1 asks for two INDEPENDENT
389
+ implementations and this has one: `vault` wrote it and `music` copied it byte for byte. What
390
+ admits it is that a verbatim copy is precisely what `check-copies` exists to refuse, the task
391
+ that asked for this move named it, and every other part of the toolkit it serves is here — a
392
+ Worker smoke that had to keep its transport in the app would keep a copy of the toolkit's
393
+ most fragile part (a hand-rolled HTTP parser) in two places.
394
+
395
+ | part | verdict |
396
+ |---|---|
397
+ | the deploy SEQUENCE (HEAD, clean tree, app refusals, stage first, build, schema, `--var` stamp, secrets, smoke) | **moved** — `runWorkerDeploy(spec, deps)`; the commands are data |
398
+ | `parseWorkerEnv`, `wranglerEnvArgs`, `readWranglerJsonc`, `readEnvFile`, `cloudflareCredential` | **moved** — identical or converged in all four |
399
+ | exact-set secrets over a pipe, read back | **moved** — `planSecrets` + `syncSecrets` + `wranglerSecretPut`; `envFileLines` carries `collections`' pre-quoted-seed fix |
400
+ | origin-first rollback, route found by pattern | **moved** — `rollbackToOrigin`, `findWorkerRoute`, `deleteWorkerRoute` |
401
+ | `edgeFetch` (curl `--resolve` past the negative DNS cache) | **moved** — `createEdgeFetch`; the Access header now THROWS on an incomplete token instead of sending empty headers |
402
+ | the import's literal and row-for-row proof | **moved** — `sqlLiteral`, `rowDigest`, `sameRows`, `rowDifferences`, `wranglerRows` |
403
+ | the steady `/healthz` wait | **moved**, into `cursedops/smoke` as `steadyHealth` |
404
+ | the deployment table (`workerEnvs.ts`: names, database, URLs, secret map, which file) | **stays** — identity, rule 2 |
405
+ | the smoke's CHECKS (gated-route census, health identity fields, shell/CSP) | **stays** — the same argument as the rest of `smoke` |
406
+ | the walk (`workerWalk.ts`) and the stage walk's grant through `apps/auth-stage` | **stays** — what a signed-in owner does is the app |
407
+ | the import's table PLAN (what is skipped, parents first, what is rebuilt: FTS, `art_lookups`) | **stays** — the app's schema |
408
+ | minting a stage's secret FILE (which keys are copied from production, which are throwaway) | **stays** — `throwawaySecret` and `writeSecretsFile` are the mechanism it uses |
409
+
277
410
  ## `cursedops/public-surface`
278
411
 
279
412
  ```sh
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.4.0",
4
- "description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots without knowing a path, macOS launchd agent install/replace/remove, the scaffolding of a deployed smoke, and the static-serving helpers eight apps copied — the path-traversal guard among them — and the API floor that keeps an unmatched /api/... from ever being answered with the app shell — the commit and dirty flag a checkout-served process reports, retried rather than cached when git loses a boot race — and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
3
+ "version": "0.5.0",
4
+ "description": "The build-and-ops answers this generation's apps wrote independently and identically: finding a generation's roots — and printing a command that runs when pasted — without knowing a path, the generation's whole-tree laws run over one repo from a checkout or a worktree, macOS launchd agent install/replace/remove and the live port a job serves, the scaffolding and verdicts of a deployed smoke (origin probe, the smoke's own environment, a network that lies about DNS, a settled version), the static-serving helpers eight apps copied — the path-traversal guard among them — the API floor that keeps an unmatched /api/... from ever being answered with the app shell, the commit and dirty flag a checkout-served process reports, the Cloudflare Worker deploy toolkit four apps copied (the deploy sequence, exact-set secrets over a pipe, origin-first rollback, the curl edge fetch, the row-for-row D1 import proof), and the public-surface ratchet three published libraries each carried a forked copy of. Mechanism only — no app knows its name from here. Bun, zero runtime dependencies (typescript is an optional peer, for public-surface only), ships source.",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "typecheck": "tsc -p tsconfig.json --noEmit",
8
8
  "lint": "biome check .",
9
9
  "test": "bun test src",
10
- "paths": "bun run scripts/paths.ts",
10
+ "paths": "bun src/paths.ts",
11
11
  "verify": "bun run paths && bun run typecheck && bun run lint && bun run test",
12
12
  "prepublishOnly": "bun run verify"
13
13
  },
@@ -60,10 +60,47 @@
60
60
  "source": "./src/publicSurface.ts",
61
61
  "import": "./src/publicSurface.ts"
62
62
  },
63
+ "./paths": {
64
+ "types": "./src/paths.ts",
65
+ "bun": "./src/paths.ts",
66
+ "source": "./src/paths.ts",
67
+ "import": "./src/paths.ts"
68
+ },
69
+ "./worker-deploy": {
70
+ "types": "./src/workerDeploy.ts",
71
+ "bun": "./src/workerDeploy.ts",
72
+ "source": "./src/workerDeploy.ts",
73
+ "import": "./src/workerDeploy.ts"
74
+ },
75
+ "./worker-secrets": {
76
+ "types": "./src/workerSecrets.ts",
77
+ "bun": "./src/workerSecrets.ts",
78
+ "source": "./src/workerSecrets.ts",
79
+ "import": "./src/workerSecrets.ts"
80
+ },
81
+ "./worker-rollback": {
82
+ "types": "./src/workerRollback.ts",
83
+ "bun": "./src/workerRollback.ts",
84
+ "source": "./src/workerRollback.ts",
85
+ "import": "./src/workerRollback.ts"
86
+ },
87
+ "./edge-fetch": {
88
+ "types": "./src/edgeFetch.ts",
89
+ "bun": "./src/edgeFetch.ts",
90
+ "source": "./src/edgeFetch.ts",
91
+ "import": "./src/edgeFetch.ts"
92
+ },
93
+ "./d1-import": {
94
+ "types": "./src/d1Import.ts",
95
+ "bun": "./src/d1Import.ts",
96
+ "source": "./src/d1Import.ts",
97
+ "import": "./src/d1Import.ts"
98
+ },
63
99
  "./package.json": "./package.json"
64
100
  },
65
101
  "bin": {
66
- "public-surface": "./src/publicSurface.ts"
102
+ "public-surface": "./src/publicSurface.ts",
103
+ "forge-paths": "./src/paths.ts"
67
104
  },
68
105
  "files": [
69
106
  "src",
@@ -0,0 +1,124 @@
1
+ /**
2
+ * `cursedops/d1-import` — the half of a cutover's data step that is the same in every app: turn
3
+ * a SQLite row into a SQL literal, and prove, ROW FOR ROW, that the source and D1 hold the same
4
+ * thing. Which tables move, in what order, and what is rebuilt rather than copied stay the app's.
5
+ *
6
+ * ```ts
7
+ * import { rowDifferences, sameRows, sqlLiteral, wranglerRows } from "cursedops/d1-import";
8
+ *
9
+ * const there = wranglerRows(d1(["--command", `SELECT ${cols} FROM "${table}"`]));
10
+ * const verdict = sameRows(here, there, columns); // { same, here, there }
11
+ * const which = rowDifferences(here, there, columns, "id"); // the rows, not "some differ"
12
+ * ```
13
+ *
14
+ * ## 🔴 Row for row, never a count
15
+ *
16
+ * Two tables can hold the same NUMBER of different rows, and a count is exactly the check that
17
+ * cannot tell — on `collections`' catalogue that would have been a caption, a rotation or a
18
+ * checksum silently replaced by another file's. `collections`, `vault`, `patterns` and `music`
19
+ * each wrote this comparison (task 2097); the `rowDigest` bodies had drifted:
20
+ *
21
+ * · `collections` normalised numbers — `bun:sqlite` hands back `0` where D1's JSON carries
22
+ * `0.0`, and float noise past twelve significant digits — because `duration_seconds` is REAL
23
+ * on 39 of its rows and a comparison that called those different reported the catalogue
24
+ * corrupt on every run. `vault` and `music` did not normalise, and would have.
25
+ * · `vault` and `music` spelled a BLOB as `X'…'` for the insert; `patterns` could not carry one.
26
+ *
27
+ * The body here is the union: numbers normalised, blobs compared as hex whichever side they came
28
+ * from (a `Uint8Array` from `bun:sqlite`, an array of bytes from D1's JSON), booleans as 0/1.
29
+ */
30
+
31
+ /** A SQL literal for a value read out of our OWN database. Not an escaping library for input. */
32
+ export function sqlLiteral(value: unknown): string {
33
+ if (value === null || value === undefined) return "NULL";
34
+ if (typeof value === "boolean") return value ? "1" : "0";
35
+ if (typeof value === "bigint") return String(value);
36
+ if (typeof value === "number") {
37
+ if (Number.isNaN(value)) return "NULL";
38
+ if (!Number.isFinite(value)) return value > 0 ? "9e999" : "-9e999";
39
+ return String(value);
40
+ }
41
+ if (value instanceof Uint8Array) return `X'${Buffer.from(value).toString("hex")}'`;
42
+ return `'${String(value).replaceAll("'", "''")}'`;
43
+ }
44
+
45
+ function cell(value: unknown): string | number | null {
46
+ if (value === null || value === undefined) return null;
47
+ if (typeof value === "boolean") return value ? 1 : 0;
48
+ if (typeof value === "bigint") return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
49
+ if (typeof value === "number") return Number.isInteger(value) ? value : Number(value.toPrecision(12));
50
+ if (value instanceof Uint8Array) return `blob:${Buffer.from(value).toString("hex")}`;
51
+ if (Array.isArray(value) && value.every((b) => typeof b === "number" && b >= 0 && b <= 255 && Number.isInteger(b))) {
52
+ return `blob:${Buffer.from(value as number[]).toString("hex")}`;
53
+ }
54
+ return String(value);
55
+ }
56
+
57
+ /** One row flattened to a comparable string over `columns` — see the header for the normalisation. */
58
+ export function rowDigest(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {
59
+ return JSON.stringify(columns.map((column) => cell(row[column])));
60
+ }
61
+
62
+ /** Are the two sides the same MULTISET of rows over `columns`? Order-insensitive. */
63
+ export function sameRows(
64
+ here: readonly Readonly<Record<string, unknown>>[],
65
+ there: readonly Readonly<Record<string, unknown>>[],
66
+ columns: readonly string[],
67
+ ): { same: boolean; here: number; there: number } {
68
+ const a = here.map((row) => rowDigest(row, columns)).sort();
69
+ const b = there.map((row) => rowDigest(row, columns)).sort();
70
+ return { same: a.length === b.length && a.every((digest, i) => digest === b[i]), here: a.length, there: b.length };
71
+ }
72
+
73
+ /**
74
+ * WHICH rows differ, by `key` — a row missing from D1, a column that differs, and (unless
75
+ * `allowExtras`) a row D1 has that the source does not. A cutover that has to go looking is a
76
+ * cutover that gets abandoned, so this names them rather than saying that some do.
77
+ *
78
+ * `allowExtras` is `collections`' SEED mode: a stage seeded from fixtures is then USED by its
79
+ * browser walk, so rows it gained are the stage working, not corruption. A MIRROR (production)
80
+ * never allows them — a row in production that is not in the source is a row from somewhere.
81
+ */
82
+ export function rowDifferences(
83
+ here: readonly Readonly<Record<string, unknown>>[],
84
+ there: readonly Readonly<Record<string, unknown>>[],
85
+ columns: readonly string[],
86
+ key: string | readonly string[],
87
+ { allowExtras = false } = {},
88
+ ): string[] {
89
+ const keys = typeof key === "string" ? [key] : [...key];
90
+ const id = (row: Readonly<Record<string, unknown>>) => rowDigest(row, keys);
91
+ const label = (row: Readonly<Record<string, unknown>>) => keys.map((k) => `${k}=${String(row[k])}`).join(",");
92
+ const byKey = new Map(there.map((row) => [id(row), row]));
93
+ const seen = new Set<string>();
94
+ const out: string[] = [];
95
+ for (const row of here) {
96
+ const digest = id(row);
97
+ seen.add(digest);
98
+ const other = byKey.get(digest);
99
+ if (!other) {
100
+ out.push(`${label(row)} is in the source and not in D1`);
101
+ continue;
102
+ }
103
+ for (const column of columns) {
104
+ if (rowDigest(row, [column]) !== rowDigest(other, [column])) {
105
+ out.push(`${label(row)} · ${column}: source ${rowDigest(row, [column])} ≠ D1 ${rowDigest(other, [column])}`);
106
+ }
107
+ }
108
+ }
109
+ if (!allowExtras) for (const row of there) if (!seen.has(id(row))) out.push(`${label(row)} is in D1 and not in the source`);
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * The rows out of `wrangler d1 execute --json` stdout. wrangler prints banner text before the
115
+ * JSON on some versions, so the parse starts at the first `[` that opens an array; none at all THROWS — an empty
116
+ * read reported as "no rows" is how a comparison passes against a database it never saw.
117
+ */
118
+ export function wranglerRows(stdout: string): Array<Record<string, unknown>> {
119
+ // The first `[` that opens a JSON array — `[{` or `[]` — never the `[ERROR]` of a failure banner.
120
+ const start = stdout.search(/\[\s*[{\]]/);
121
+ if (start < 0) throw new Error(`wrangler returned no JSON: ${stdout.slice(0, 200)}`);
122
+ const parsed = JSON.parse(stdout.slice(start)) as Array<{ results?: Array<Record<string, unknown>> }>;
123
+ return parsed.flatMap((result) => result.results ?? []);
124
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * `cursedops/edge-fetch` — a request to a deployed Worker as a SCRIPT must make it: `curl` with
3
+ * `--resolve` pinned to what Cloudflare's own resolver answers, plus whatever headers the app
4
+ * says a host needs (the fleet's Access service token on `*-stage`).
5
+ *
6
+ * ```ts
7
+ * import { cloudflareAccessHeaders, createEdgeFetch } from "cursedops/edge-fetch";
8
+ *
9
+ * export const edgeFetch = createEdgeFetch({
10
+ * headersFor: (host) => (/-stage\./.test(host) ? cloudflareAccessHeaders(readFileSync(accessFile, "utf8")) : {}),
11
+ * });
12
+ * const response = await edgeFetch("https://thing-stage.example.com/healthz");
13
+ * ```
14
+ *
15
+ * ## 🔴 Why not `fetch`
16
+ *
17
+ * This Mac caches a NEGATIVE DNS answer, for the zone's SOA minimum, for a hostname asked before
18
+ * its record existed — and a stage's custom domain is created by its own first deploy, seconds
19
+ * before the smoke asks it. Flushing that cache needs sudo. The smoke and the walk are about the
20
+ * Worker, not this laptop's resolver, so they ask `1.1.1.1` directly. `vault` wrote it; `music`
21
+ * copied it byte for byte on 2026-09-23 (task 2097).
22
+ *
23
+ * 🔴 **This is a transport for proving a WORKER, not a verdict on a network.** Pinning the address
24
+ * is exactly why it cannot tell you whether the network a person is sitting on is lying — that
25
+ * question is `diagnoseNetwork` in `cursedops/smoke`, which never pins and never retries. Pass
26
+ * `resolve: null` to ask through the system resolver instead.
27
+ *
28
+ * Bytes go through a temporary file, so a binary body (ciphertext, audio) arrives unmangled, and
29
+ * the response body after the LAST header block is bytes — an interim `1xx` block precedes it.
30
+ */
31
+ import { spawnSync } from "node:child_process";
32
+ import { rmSync, writeFileSync } from "node:fs";
33
+ import { tmpdir } from "node:os";
34
+ import { join } from "node:path";
35
+
36
+ export interface CurlRun {
37
+ status: number | null;
38
+ stdout: Buffer;
39
+ }
40
+
41
+ export interface EdgeFetchOptions {
42
+ /** Extra headers for a host — an Access service token, say. Default: none. */
43
+ headersFor?: (host: string) => Record<string, string>;
44
+ /**
45
+ * The IPv4 address to pin a host to, or `null`/`""` for none. Default: `dig +short <host> @1.1.1.1`.
46
+ * Pass `null` (not a function) to never pin.
47
+ */
48
+ resolve?: ((host: string) => string | null) | null;
49
+ /** Injected for tests. Runs curl with `args`. */
50
+ curl?: (args: readonly string[]) => CurlRun;
51
+ /** Where a request body is staged. Default: the OS temp dir. */
52
+ tmpDir?: string;
53
+ /** curl's `--max-time`. Default 60. */
54
+ maxTimeSec?: number;
55
+ }
56
+
57
+ function digCloudflare(host: string): string | null {
58
+ const dig = spawnSync("dig", ["+short", host, "@1.1.1.1"], { encoding: "utf8" });
59
+ return (
60
+ (dig.stdout ?? "")
61
+ .split("\n")
62
+ .map((line) => line.trim())
63
+ .find((line) => /^\d+\.\d+\.\d+\.\d+$/.test(line)) ?? null
64
+ );
65
+ }
66
+
67
+ /**
68
+ * `curl -s -i` output → a `Response`. THROWS on empty output (nothing answered), which a caller
69
+ * must see as a failure and never as a status.
70
+ */
71
+ export function parseCurlResponse(raw: Buffer, origin = "the edge", exit: number | null = null): Response {
72
+ if (raw.length === 0) throw new Error(`no answer from ${origin} (curl exit ${exit})`);
73
+ let offset = 0;
74
+ let head: string[] = [];
75
+ for (;;) {
76
+ const end = raw.indexOf("\r\n\r\n", offset);
77
+ const stop = end < 0 ? raw.length : end;
78
+ head = raw.subarray(offset, stop).toString("latin1").split("\r\n");
79
+ offset = end < 0 ? raw.length : end + 4;
80
+ if (!/^HTTP\/\S+ 1\d\d/.test(head[0] ?? "") || raw.subarray(offset, offset + 5).toString("latin1") !== "HTTP/") break;
81
+ }
82
+ const status = Number(/HTTP\/\S+ (\d+)/.exec(head[0] ?? "")?.[1] ?? 0);
83
+ const headers = new Headers();
84
+ for (const line of head.slice(1)) {
85
+ const i = line.indexOf(":");
86
+ if (i > 0) headers.append(line.slice(0, i).trim(), line.slice(i + 1).trim());
87
+ }
88
+ const body = raw.subarray(offset);
89
+ return new Response(status === 204 || status === 304 ? null : new Uint8Array(body), { status: status || 599, headers });
90
+ }
91
+
92
+ /** A `fetch`-shaped function over curl — see the header. Each host is resolved once per instance. */
93
+ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, init?: RequestInit) => Promise<Response> {
94
+ const pinned = new Map<string, string>();
95
+ const resolve = options.resolve === undefined ? digCloudflare : options.resolve;
96
+ const curl =
97
+ options.curl ??
98
+ ((args: readonly string[]): CurlRun => {
99
+ const out = spawnSync("curl", [...args], { maxBuffer: 256 * 1024 * 1024 });
100
+ return { status: out.status, stdout: out.stdout ?? Buffer.alloc(0) };
101
+ });
102
+ let sequence = 0;
103
+ return async (url, init = {}) => {
104
+ const target = new URL(url);
105
+ if (resolve && !pinned.has(target.hostname)) pinned.set(target.hostname, resolve(target.hostname) ?? "");
106
+ const ip = pinned.get(target.hostname);
107
+ const port = target.port || (target.protocol === "http:" ? "80" : "443");
108
+ const args = ["-s", "-i", "--max-time", String(options.maxTimeSec ?? 60), ...(ip ? ["--resolve", `${target.hostname}:${port}:${ip}`] : [])];
109
+ for (const [k, v] of Object.entries(options.headersFor?.(target.hostname) ?? {})) args.push("-H", `${k}: ${v}`);
110
+ new Headers(init.headers).forEach((v, k) => {
111
+ args.push("-H", `${k}: ${v}`);
112
+ });
113
+ if (init.method) args.push("-X", init.method);
114
+ let bodyFile: string | null = null;
115
+ if (init.body !== undefined && init.body !== null) {
116
+ bodyFile = join(options.tmpDir ?? tmpdir(), `edge-fetch-${process.pid}-${++sequence}-${Date.now()}.bin`);
117
+ const body = init.body;
118
+ writeFileSync(
119
+ bodyFile,
120
+ typeof body === "string" ? body : body instanceof Uint8Array ? body : new Uint8Array(await new Response(body as BodyInit).arrayBuffer()),
121
+ );
122
+ args.push("--data-binary", `@${bodyFile}`);
123
+ }
124
+ try {
125
+ const out = curl([...args, target.toString()]);
126
+ return parseCurlResponse(out.stdout, target.origin, out.status);
127
+ } finally {
128
+ if (bodyFile) rmSync(bodyFile, { force: true });
129
+ }
130
+ };
131
+ }
132
+
133
+ /**
134
+ * The Access service-token headers out of an env file holding `CF_ACCESS_CLIENT_ID` and
135
+ * `CF_ACCESS_CLIENT_SECRET`. THROWS, naming what is missing, rather than sending empty headers —
136
+ * an empty token reads at the far end as "Access refused the app", which is the exact
137
+ * misattribution a smoke must not make.
138
+ */
139
+ export function cloudflareAccessHeaders(envText: string): Record<string, string> {
140
+ const value = (key: string) =>
141
+ (envText.split("\n").find((line) => line.trim().replace(/^export\s+/, "").startsWith(`${key}=`)) ?? "")
142
+ .trim()
143
+ .replace(/^export\s+/, "")
144
+ .slice(key.length + 1)
145
+ .trim()
146
+ .replace(/^['"]|['"]$/g, "");
147
+ const id = value("CF_ACCESS_CLIENT_ID");
148
+ const secret = value("CF_ACCESS_CLIENT_SECRET");
149
+ const missing = [...(id ? [] : ["CF_ACCESS_CLIENT_ID"]), ...(secret ? [] : ["CF_ACCESS_CLIENT_SECRET"])];
150
+ if (missing.length > 0) throw new Error(`the Access service token is incomplete: no ${missing.join(" and no ")}`);
151
+ return { "CF-Access-Client-Id": id, "CF-Access-Client-Secret": secret };
152
+ }
package/src/launchd.ts CHANGED
@@ -403,3 +403,43 @@ export async function answering(
403
403
  await sleep(stepMs);
404
404
  }
405
405
  }
406
+
407
+ /**
408
+ * The `PORT` a `launchctl print` dump says the job is handed, or `null`.
409
+ *
410
+ * 🔴 Only the job's OWN `environment = {` block. The same dump carries `inherited environment`
411
+ * and `default environment`, and a `PORT` leaking in from the session that bootstrapped the job
412
+ * is not what launchd hands the next start — reading one of those reports a number nothing
413
+ * serves on. Lifted from `autopilot/src/ports.ts` (`portFromPrint`), whose fixture this carries.
414
+ */
415
+ export function portFromPrint(printed: string): number | null {
416
+ let inside = false;
417
+ for (const line of printed.split("\n")) {
418
+ if (/^[ \t]*environment[ \t]*=[ \t]*\{[ \t]*$/.test(line)) {
419
+ inside = true;
420
+ continue;
421
+ }
422
+ if (!inside) continue;
423
+ if (/^[ \t]*\}[ \t]*$/.test(line)) break;
424
+ const m = line.match(/^[ \t]*PORT[ \t]*=>[ \t]*(\d+)[ \t]*$/);
425
+ if (m?.[1]) return Number(m[1]);
426
+ }
427
+ return null;
428
+ }
429
+
430
+ /**
431
+ * The port the LIVE job `label` serves on — read off launchd, never off the repo — or `null`
432
+ * when the job is not loaded or declares no `PORT`.
433
+ *
434
+ * 🔴 Measured 2026-09-15 by `autopilot doctor`'s `ports` section: `package.json` advertised the
435
+ * wrong port in **eight of eight** apps (it is the dev pair), and a smoke that asked it would
436
+ * prove nothing about the process traffic reaches. The declaration launchd holds is what the
437
+ * tunnel routes to and what the next start is handed, so it is the one a deployed smoke's
438
+ * origin probe asks. A `null` is not a pass: the caller records it as "there is no origin".
439
+ */
440
+ export function livePort(label: string, ops: Partial<LaunchdOps> = {}): number | null {
441
+ const { run, domain } = realOps(ops);
442
+ const printed = run("print", `${domain}/${label}`);
443
+ if (printed.code !== 0) return null;
444
+ return portFromPrint(printed.out);
445
+ }