cursedops 0.10.18 → 0.10.20
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 +7 -1
- package/package.json +1 -1
- package/src/publicClient.ts +27 -6
- package/src/smoke.ts +72 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ bun add cursedops
|
|
|
28
28
|
| `cursedops/relay` | the WORKER half of a relay — a Worker in front of a Mac-bound app over ONE outbound WebSocket: the frames (station's wire, byte for byte), the key-digest door (`acceptLink`), and `RelayLink`, the Durable Object an app subclasses with its name, body ceiling, timeout and (optionally) station's per-cookie offline cache. No `node:` import (0.10.0, lifted from station for roms, task 065) |
|
|
29
29
|
| `cursedops/relay-link` | the MAC half — `dialLink` (answer each frame with the app's own `fetch`, redial 1 s → 60 s on close, no timer otherwise), `readLinkSettings` for `<APP>_LINK_URL`/`<APP>_LINK_KEY`, and `rotateLinkKey` (digest to the Worker FIRST, the key to the 0600 file only if it took) (0.10.0) |
|
|
30
30
|
| `cursedops/relay-stage` | the signed-in STAGE WALK's skeleton both relay apps shared — `stageByteCheck` (SHA-256 of every built file, a stale byte re-asked 4 × 10 s), `putStageLinkDigest` (`--env stage` only), `grantStageOwner` (auth-stage's 2FA grant, never production's), `oldKeyRefused` (the rotation leg: 20 s refused window, 3 min ceiling), `awaitSystemResolver`, `servedPath`, `STAGE_AUTH_URL`. With `relayWorkerDeployCli` in `cursedops/worker-deploy`, a relay app's whole `worker:deploy` as data (0.10.6) |
|
|
31
|
-
| `cursedops/public-client` | does a relay app's PUBLIC hostname serve the client this release built? `comparePublicClient` (built assets the shells name, never bodies), `stagedShell`, `publicShell` (cache-busted, Access headers as an option), `recordPublicClient` — the smoke's `public-client` row, an EDGE fault (exit 2, never a rollback) — and `shipClientIfBehind`, the deploy step that runs `worker:deploy` when the public shell is behind. Lifted from roms for station (0.10.8). `stagedBuild` + `publicBuild` (0.10.18): pass `stagedBuild` and the commit the public `/build.json` names must be this release's too — `/`'s asset names cannot see a change to another page or a fixed-name file |
|
|
31
|
+
| `cursedops/public-client` | does a relay app's PUBLIC hostname serve the client this release built? `comparePublicClient` (built assets the shells name, never bodies), `stagedShell`, `publicShell` (cache-busted, Access headers as an option), `recordPublicClient` — the smoke's `public-client` row, an EDGE fault (exit 2, never a rollback) — and `shipClientIfBehind`, the deploy step that runs `worker:deploy` when the public shell is behind. Lifted from roms for station (0.10.8). `stagedBuild` + `publicBuild` (0.10.18): pass `stagedBuild` and the commit the public `/build.json` names must be this release's too — `/`'s asset names cannot see a change to another page or a fixed-name file. `builtClient(dir)` (0.10.20): the commit and entry chunk a built `dist/` carries — what a relay smoke's `awaitConverged` asks the edge for |
|
|
32
32
|
| `cursedops/public-surface` | the ratchet on a LIBRARY's public surface — a symbol count per export subpath against a committed baseline that may only fall — and its `public-surface` bin. Not an app's: the one entry here admitted for three published libraries, see below |
|
|
33
33
|
|
|
34
34
|
Bun, zero runtime dependencies, ships TypeScript source. Nothing here knows an app's
|
|
@@ -297,6 +297,12 @@ pins an address** — the deliverable is the distinction, not a way around the n
|
|
|
297
297
|
row, because on `music`'s first deploy (2026-09-23, `a54ef71`) one good `/healthz` was followed
|
|
298
298
|
by thirteen routes answering the pre-secrets version. It returns what answered and never judges.
|
|
299
299
|
|
|
300
|
+
`awaitConverged(probes)` (0.10.19) is the same wait over SEVERAL paths: every probe must agree
|
|
301
|
+
three rounds in a row, bounded (120 s), before any leg is judged — a Worker's code and its static
|
|
302
|
+
assets reach the edge separately (roms `f9273ea`, task 2196: `/healthz` at HEAD while the old
|
|
303
|
+
script answered `/host/install.sh` and the new chunk 404'd). A wait that runs out says
|
|
304
|
+
`never converged in Ns — <what lagged>`, and the caller records it as a red row.
|
|
305
|
+
|
|
300
306
|
Nothing else. The checks are the part `desk` and `flix` wrote differently on purpose,
|
|
301
307
|
and a shared kit that starts absorbing route lists and health-payload shapes is how the
|
|
302
308
|
last one reached 277 files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.20",
|
|
4
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, the billed-CPU tail check around a deploy's walk and smoke, and each app's worker:secrets and worker:smoke main as one function of its data), the relay a Worker fronts a Mac-bound app with (the Durable Object, the frames, the Mac's dialer and key rotation — lifted from station for roms — and the signed-in stage walk's skeleton and the relay app's whole worker:deploy), the Worker import-graph and await-port checks every Worker app's suite runs over its own source, 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": {
|
package/src/publicClient.ts
CHANGED
|
@@ -78,13 +78,34 @@ export function stagedShell(dataDir: string): string | null {
|
|
|
78
78
|
*/
|
|
79
79
|
export function stagedBuild(dataDir: string): string | null {
|
|
80
80
|
const dir = stagedClientDir(dataDir);
|
|
81
|
-
|
|
81
|
+
return dir ? builtClient(dir).commit : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** What a built client directory says it is — see {@link builtClient}. */
|
|
85
|
+
export interface BuiltClient {
|
|
86
|
+
/** `build.json`'s commit (`forge-client stamp`), or `null`. */
|
|
87
|
+
commit: string | null;
|
|
88
|
+
/** The first hashed chunk its `index.html` names (`/assets/index-XXXXXXXX.js`), or `null`. */
|
|
89
|
+
chunk: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The commit and entry chunk of a built client directory — a Worker's `dist/`, the one
|
|
94
|
+
* `worker:deploy` uploads, or a staged client. A missing file is a `null`, never a throw: the
|
|
95
|
+
* caller's smoke judges it. What a relay smoke's `awaitConverged` asks the edge for (0.10.20,
|
|
96
|
+
* task 2196): the public `/build.json` naming this commit and this chunk answering 200.
|
|
97
|
+
*/
|
|
98
|
+
export function builtClient(dir: string): BuiltClient {
|
|
99
|
+
let commit: string | null = null;
|
|
82
100
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
} catch {
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
const value = (JSON.parse(readFileSync(join(dir, "build.json"), "utf8")) as { commit?: unknown }).commit;
|
|
102
|
+
commit = typeof value === "string" && value !== "" ? value : null;
|
|
103
|
+
} catch {}
|
|
104
|
+
let shell = "";
|
|
105
|
+
try {
|
|
106
|
+
shell = readFileSync(join(dir, "index.html"), "utf8");
|
|
107
|
+
} catch {}
|
|
108
|
+
return { commit, chunk: /\/assets\/[^"']+-[A-Za-z0-9_-]{8,}\.js/.exec(shell)?.[0] ?? null };
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
/**
|
package/src/smoke.ts
CHANGED
|
@@ -894,6 +894,78 @@ export async function steadyHealth<T = Record<string, unknown>>(
|
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
+
/** One question a deploy must answer the NEW way — `null` when it does, else why not. */
|
|
898
|
+
export interface ConvergenceProbe {
|
|
899
|
+
name: string;
|
|
900
|
+
check: () => Promise<string | null>;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export interface ConvergenceOptions {
|
|
904
|
+
/** Rounds in a row in which EVERY probe agreed before the deploy is believed. Default 3. */
|
|
905
|
+
rounds?: number;
|
|
906
|
+
/** How long to keep asking before judging it never converged. Default 120 s. */
|
|
907
|
+
deadlineMs?: number;
|
|
908
|
+
/** Pause after a round that agreed. Default 2 s. */
|
|
909
|
+
pauseMs?: number;
|
|
910
|
+
/** Pause after a round that did not. Default 3 s. */
|
|
911
|
+
retryMs?: number;
|
|
912
|
+
/** Injected for tests. */
|
|
913
|
+
sleep?: (ms: number) => Promise<void>;
|
|
914
|
+
now?: () => number;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export interface Convergence {
|
|
918
|
+
converged: boolean;
|
|
919
|
+
/** Rounds asked in all. */
|
|
920
|
+
asked: number;
|
|
921
|
+
/** The probes that disagreed in the LAST round that did, with why — empty when none ever did. */
|
|
922
|
+
lagging: string[];
|
|
923
|
+
/** One line for a smoke row: how it converged, or "never converged in Ns — <what lagged>". */
|
|
924
|
+
detail: string;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Ask every probe, round after round, until `rounds` rounds IN A ROW all agree — or the deadline.
|
|
929
|
+
*
|
|
930
|
+
* 🔴 **Several different paths, several rounds.** `steadyHealth` asks one path; a Worker's CODE and
|
|
931
|
+
* its static ASSETS are two uploads that reach the edge separately. Measured on roms `f9273ea`
|
|
932
|
+
* (2026-09-26, task 2196): `/healthz` already named the new commit while, in the same run,
|
|
933
|
+
* `/host/install.sh` was answered by the previous script (503, a route the new one 404s) and the
|
|
934
|
+
* new build's hashed chunk 404'd from the old asset set. Sixty seconds later all 17 legs were
|
|
935
|
+
* green. Judging any leg before every probe agrees for several rounds is a false red that stops
|
|
936
|
+
* `deploy` half-way. Like `steadyHealth`, it never decides pass or fail on its own — the caller
|
|
937
|
+
* records `converged` as a row, so a wait that timed out is still a red.
|
|
938
|
+
*/
|
|
939
|
+
export async function awaitConverged(probes: readonly ConvergenceProbe[], options: ConvergenceOptions = {}): Promise<Convergence> {
|
|
940
|
+
const rounds = options.rounds ?? 3;
|
|
941
|
+
const deadlineMs = options.deadlineMs ?? 120_000;
|
|
942
|
+
const sleep = options.sleep ?? ((ms: number) => new Promise<void>((done) => setTimeout(done, ms)));
|
|
943
|
+
const now = options.now ?? Date.now;
|
|
944
|
+
const started = now();
|
|
945
|
+
let steady = 0;
|
|
946
|
+
let asked = 0;
|
|
947
|
+
let lagging: string[] = [];
|
|
948
|
+
for (;;) {
|
|
949
|
+
asked++;
|
|
950
|
+
const wrong: string[] = [];
|
|
951
|
+
for (const probe of probes) {
|
|
952
|
+
const why = await probe.check().catch((error: Error) => `threw ${error.message.split("\n")[0]}`);
|
|
953
|
+
if (why !== null) wrong.push(`${probe.name}: ${why}`);
|
|
954
|
+
}
|
|
955
|
+
if (wrong.length > 0) lagging = wrong;
|
|
956
|
+
steady = wrong.length === 0 ? steady + 1 : 0;
|
|
957
|
+
const waited = Math.round((now() - started) / 1000);
|
|
958
|
+
if (probes.length > 0 && steady >= rounds) {
|
|
959
|
+
return { converged: true, asked, lagging, detail: `${probes.length} probe(s) agreed ${rounds} round(s) in a row after ${waited}s (${asked} asked)` };
|
|
960
|
+
}
|
|
961
|
+
if (probes.length === 0 || now() - started >= deadlineMs) {
|
|
962
|
+
const why = probes.length === 0 ? "no probes — nothing was asked" : lagging.join(" · ");
|
|
963
|
+
return { converged: false, asked, lagging, detail: `never converged in ${waited}s — ${why}` };
|
|
964
|
+
}
|
|
965
|
+
await sleep(steady > 0 ? (options.pauseMs ?? 2_000) : (options.retryMs ?? 3_000));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
897
969
|
/** One census entry — a private route and the method it is asked with. */
|
|
898
970
|
export interface CensusRoute {
|
|
899
971
|
method: string;
|