cursedops 0.10.16 → 0.10.18
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 +1 -1
- package/package.json +1 -1
- package/src/edgeFetch.ts +11 -6
- package/src/publicClient.ts +81 -9
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) |
|
|
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 |
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.18",
|
|
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/edgeFetch.ts
CHANGED
|
@@ -73,15 +73,20 @@ const EDGE_RETRIES = 2;
|
|
|
73
73
|
* · 6 resolve, 7 connect, 35 TLS handshake — the request never left this machine, so a replay
|
|
74
74
|
* is always safe, whatever the method;
|
|
75
75
|
* · 52 empty reply, 56 receive failure — the request MAY have reached the Worker and done its
|
|
76
|
-
* work, so these are replayed only for a method that is safe to run twice (never a POST)
|
|
76
|
+
* work, so these are replayed only for a method that is safe to run twice (never a POST);
|
|
77
|
+
* · 28 timeout — the far end may still be working, so the same rule, and ONCE only: a replay
|
|
78
|
+
* costs a whole `--max-time`, and a Worker that is really down must not hold a smoke for
|
|
79
|
+
* three of them. Measured 2026-09-26: flix's prod smoke went red on one `curl exit 28` for
|
|
80
|
+
* `GET /healthz` while the same URL answered 200 in 0.2 s seconds later (task 2190).
|
|
77
81
|
*
|
|
78
|
-
* Every other exit
|
|
79
|
-
* HTTP status, whatever it is, is returned exactly as before.
|
|
82
|
+
* Every other exit, and every HTTP status, whatever it is, is returned exactly as before.
|
|
80
83
|
*/
|
|
81
|
-
function retryableCurlFailure(exit: number | null, stdout: Buffer, method = "GET"): boolean {
|
|
84
|
+
function retryableCurlFailure(exit: number | null, stdout: Buffer, method = "GET", retry = 1): boolean {
|
|
82
85
|
if (stdout.length > 0 || exit === null) return false;
|
|
83
86
|
if (exit === 6 || exit === 7 || exit === 35) return true;
|
|
84
|
-
|
|
87
|
+
const replayable = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"].includes(method.toUpperCase());
|
|
88
|
+
if (exit === 28) return replayable && retry === 1;
|
|
89
|
+
return (exit === 52 || exit === 56) && replayable;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
function digCloudflare(host: string): string | null {
|
|
@@ -170,7 +175,7 @@ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, i
|
|
|
170
175
|
}
|
|
171
176
|
try {
|
|
172
177
|
let out = curl([...head(), ...args, target.toString()]);
|
|
173
|
-
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method); retry++) {
|
|
178
|
+
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method, retry); retry++) {
|
|
174
179
|
await new Promise((r) => setTimeout(r, options.retryDelayMs?.(retry) ?? 500 * retry));
|
|
175
180
|
// `head()` again: a host with no pin yet is asked again, so a retry can find the address.
|
|
176
181
|
out = curl([...head(), ...args, target.toString()]);
|
package/src/publicClient.ts
CHANGED
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
*
|
|
25
25
|
* `headers` is how an app behind Cloudflare Access (station) passes the fleet's service token; an
|
|
26
26
|
* anonymous `/` there is a 302 to the Access issuer, which names no built asset and never matches.
|
|
27
|
+
*
|
|
28
|
+
* ## 🔴 `/` is ONE page — pass `stagedBuild` and the BUILD is compared too (0.10.18)
|
|
29
|
+
*
|
|
30
|
+
* The asset names in `/` only see a change that reaches `/`'s own chunks. roms `95fa4b24`
|
|
31
|
+
* (2026-09-27) changed only its PS2 player — `ps2.html` and the `ps2-*.js` chunk nothing else
|
|
32
|
+
* loads — and this said "already serving it": the public `/` named the same files, and players
|
|
33
|
+
* kept the old player until `worker:deploy` was run by hand. A fixed-name file (roms'
|
|
34
|
+
* `emulator-runtime.js`, a service worker) is invisible to a name comparison by construction.
|
|
35
|
+
* So an app whose client is stamped (`forge-client stamp` writes `build.json`, which the Worker
|
|
36
|
+
* serves with the rest of the client) passes `stagedBuild(dataDir)`, and the commit the public
|
|
37
|
+
* `/build.json` names must equal the staged one. That is every page and every file at once.
|
|
27
38
|
*/
|
|
28
39
|
import { spawnSync } from "node:child_process";
|
|
29
40
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
@@ -61,6 +72,34 @@ export function stagedShell(dataDir: string): string | null {
|
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The commit the staged client was built from — its `build.json` (`forge-client stamp`) — or
|
|
77
|
+
* `null` when nothing is staged or it carries no commit.
|
|
78
|
+
*/
|
|
79
|
+
export function stagedBuild(dataDir: string): string | null {
|
|
80
|
+
const dir = stagedClientDir(dataDir);
|
|
81
|
+
if (!dir) return null;
|
|
82
|
+
try {
|
|
83
|
+
const commit = (JSON.parse(readFileSync(join(dir, "build.json"), "utf8")) as { commit?: unknown }).commit;
|
|
84
|
+
return typeof commit === "string" && commit !== "" ? commit : null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pure: `null` when the public client's build commit is this release's, otherwise the sentence
|
|
92
|
+
* saying why not. A missing public commit is behind — nothing then proves which client people get.
|
|
93
|
+
*/
|
|
94
|
+
export function buildBehind(served: string | null, built: string): string | null {
|
|
95
|
+
if (!served) return "the public /build.json names no commit — nothing proves which client people get";
|
|
96
|
+
if (served.startsWith(built) || built.startsWith(served)) return null;
|
|
97
|
+
return (
|
|
98
|
+
`the public client was built from ${served.slice(0, 8)} and this release from ${built.slice(0, 8)} — ` +
|
|
99
|
+
"a page other than / (or a file with a fixed name) changed while / did not"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
64
103
|
export interface PublicShellOptions {
|
|
65
104
|
/** Sent with the request — Cloudflare Access service-token headers for an app behind Access. */
|
|
66
105
|
headers?: Record<string, string>;
|
|
@@ -82,6 +121,25 @@ export async function publicShell(base: string, options: PublicShellOptions = {}
|
|
|
82
121
|
return { status: response.status, html: await response.text().catch(() => "") };
|
|
83
122
|
}
|
|
84
123
|
|
|
124
|
+
/** The commit the PUBLIC client's `/build.json` names, cache-busted — or `null` when it cannot be read. */
|
|
125
|
+
export async function publicBuild(base: string, options: PublicShellOptions = {}): Promise<string | null> {
|
|
126
|
+
const url = new URL("/build.json", base);
|
|
127
|
+
url.searchParams.set("cb", Math.random().toString(36).slice(2));
|
|
128
|
+
const ask = options.fetch ?? ((u: string, init: RequestInit) => fetch(u, init));
|
|
129
|
+
try {
|
|
130
|
+
const response = await ask(url.toString(), {
|
|
131
|
+
redirect: "manual",
|
|
132
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 20_000),
|
|
133
|
+
headers: { accept: "application/json", ...(options.headers ?? {}) },
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) return null;
|
|
136
|
+
const commit = ((await response.json()) as { commit?: unknown }).commit;
|
|
137
|
+
return typeof commit === "string" && commit !== "" ? commit : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
/** The sentence both the smoke and the deploy print when the halves disagree. `root` is the app's checkout. */
|
|
86
144
|
export function workerBehind(verdict: PublicClientVerdict, base: string, root: string): string {
|
|
87
145
|
return (
|
|
@@ -94,6 +152,8 @@ export function workerBehind(verdict: PublicClientVerdict, base: string, root: s
|
|
|
94
152
|
export interface RecordPublicClientOptions extends PublicShellOptions {
|
|
95
153
|
/** The app's checkout, for the pasteable `worker:deploy` command. */
|
|
96
154
|
root: string;
|
|
155
|
+
/** {@link stagedBuild} — when given, the public `/build.json` must name the same commit. */
|
|
156
|
+
stagedBuild?: string | null;
|
|
97
157
|
}
|
|
98
158
|
|
|
99
159
|
/**
|
|
@@ -113,15 +173,22 @@ export async function recordPublicClient(smoke: Smoke, staged: string | null, op
|
|
|
113
173
|
}
|
|
114
174
|
const { html } = await publicShell(smoke.base, options);
|
|
115
175
|
const verdict = comparePublicClient(html, staged, smoke.base);
|
|
176
|
+
let behind: string | null = null;
|
|
177
|
+
if (verdict.matched && options.stagedBuild) {
|
|
178
|
+
behind = buildBehind(await publicBuild(smoke.base, options), options.stagedBuild);
|
|
179
|
+
}
|
|
180
|
+
const matched = verdict.matched && behind === null;
|
|
116
181
|
smoke.record(
|
|
117
182
|
PUBLIC_CLIENT_CHECK,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
? `the public shell names this release's build — ${verdict.built.join(", ")}`
|
|
121
|
-
:
|
|
183
|
+
matched,
|
|
184
|
+
matched
|
|
185
|
+
? `the public shell names this release's build — ${verdict.built.join(", ")}${options.stagedBuild ? `, built from ${options.stagedBuild.slice(0, 8)}` : ""}`
|
|
186
|
+
: behind !== null
|
|
187
|
+
? `🔴 ${behind}. The public client is the relay Worker's static assets — \`cd ${options.root} && bun run worker:deploy\``
|
|
188
|
+
: workerBehind(verdict, smoke.base, options.root),
|
|
122
189
|
);
|
|
123
|
-
if (!
|
|
124
|
-
return
|
|
190
|
+
if (!matched) smoke.markEdgeFault();
|
|
191
|
+
return matched;
|
|
125
192
|
}
|
|
126
193
|
|
|
127
194
|
/**
|
|
@@ -187,6 +254,8 @@ export interface ShipClientOptions extends PublicShellOptions {
|
|
|
187
254
|
git?: (root: string, args: readonly string[]) => { status: number | null; stdout: string };
|
|
188
255
|
/** What the Worker is built from ({@link workerInputs}); default computed from `root`. */
|
|
189
256
|
workerInputs?: readonly string[];
|
|
257
|
+
/** {@link stagedBuild} — when given, a public `/build.json` naming another commit is behind too. */
|
|
258
|
+
stagedBuild?: string | null;
|
|
190
259
|
log?: (line: string) => void;
|
|
191
260
|
}
|
|
192
261
|
|
|
@@ -241,7 +310,11 @@ export async function shipClientIfBehind(options: ShipClientOptions): Promise<Sh
|
|
|
241
310
|
() => "",
|
|
242
311
|
);
|
|
243
312
|
before = comparePublicClient(html, options.staged, options.base);
|
|
244
|
-
|
|
313
|
+
const clientBehind =
|
|
314
|
+
before.matched && options.stagedBuild ? buildBehind(await publicBuild(options.base, options), options.stagedBuild) : null;
|
|
315
|
+
if (clientBehind !== null) {
|
|
316
|
+
log(` ${clientBehind} — shipping the Worker's client`);
|
|
317
|
+
} else if (before.matched) {
|
|
245
318
|
// 🔴 The client matching is not the Worker matching: roms 86e5e522 changed only
|
|
246
319
|
// worker/index.ts, this said "already serving it", and /healthz kept answering the
|
|
247
320
|
// previous Worker until worker:deploy was run by hand (2026-09-25).
|
|
@@ -261,8 +334,7 @@ export async function shipClientIfBehind(options: ShipClientOptions): Promise<Sh
|
|
|
261
334
|
log(" running `bun run worker:deploy` — the stage walk, then production");
|
|
262
335
|
const status = (options.runWorkerDeploy ?? spawnWorkerDeploy)(options.root);
|
|
263
336
|
return { outcome: status === 0 ? "shipped" : "failed", status, before, workerBehind: behind };
|
|
264
|
-
}
|
|
265
|
-
log(` ${workerBehind(before, options.base, options.root)}`);
|
|
337
|
+
} else log(` ${workerBehind(before, options.base, options.root)}`);
|
|
266
338
|
} else {
|
|
267
339
|
log(" nothing staged to compare with — shipping the Worker's client anyway");
|
|
268
340
|
}
|