cursedops 0.10.12 → 0.10.13

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
@@ -419,8 +419,8 @@ most fragile part (a hand-rolled HTTP parser) in two places.
419
419
  |---|---|
420
420
  | the deploy SEQUENCE (HEAD, clean tree, app refusals, stage first, build, schema, `--var` stamp, secrets, smoke) | **moved** — `runWorkerDeploy(spec, deps)`; the commands are data |
421
421
  | `parseWorkerEnv`, `wranglerEnvArgs`, `readWranglerJsonc`, `readEnvFile`, `cloudflareCredential` | **moved** — identical or converged in all four |
422
- | exact-set secrets over a pipe, read back | **moved** — `planSecrets` + `syncSecrets` + `wranglerSecretPut`; `envFileLines` carries `collections`' pre-quoted-seed fix |
423
- | origin-first rollback, route found by pattern | **moved** — `rollbackToOrigin`, `findWorkerRoute`, `deleteWorkerRoute` |
422
+ | exact-set secrets over a pipe, read back | **moved** — `planSecrets` + `syncSecrets` + `wranglerSecretPut`; `envFileLines` carries `collections`' pre-quoted-seed fix; `--mint` carries the keys `mintCarries` names (0.10.13; default `FILE_TOKEN_PRIVATE_KEY`, flix's is `PLEX_ACCOUNT_TOKEN`) |
423
+ | origin-first rollback, route found by pattern | **moved** — `rollbackToOrigin`, `findWorkerRoute`, `deleteWorkerRoute`; the whole `worker:rollback` main is `runWorkerRollback(spec, argv)` (0.10.13 — flix and music were one copied file) |
424
424
  | `edgeFetch` (curl `--resolve` past the negative DNS cache) | **moved** — `createEdgeFetch`; the Access header now THROWS on an incomplete token instead of sending empty headers |
425
425
  | the import's literal and row-for-row proof | **moved** — `sqlLiteral`, `rowDigest`, `sameRows`, `rowDifferences`, `wranglerRows` |
426
426
  | the steady `/healthz` wait | **moved**, into `cursedops/smoke` as `steadyHealth` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedops",
3
- "version": "0.10.12",
3
+ "version": "0.10.13",
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/smoke.ts CHANGED
@@ -902,12 +902,30 @@ export interface CensusRoute {
902
902
 
903
903
  /** What {@link runWorkerSmoke} needs to know about one deployment — data, and one hook. */
904
904
  export interface WorkerSmokeSpec {
905
+ /**
906
+ * Which door the Worker keeps. `"sso"` (the default): an anonymous caller gets **401** from
907
+ * every census route, and `/healthz` says `apiServed` and names `publicUrl`. `"access"`
908
+ * (desk, 2026-09-25): Cloudflare Access is the door and the Worker re-checks Access's
909
+ * assertion itself — so on the PREVIEW (no Access in front) every census route and shell path
910
+ * is **403** and `/healthz` says `access: true`; on the PUBLIC host a caller without the
911
+ * token is turned away, and THROUGH the token every census route answers **200** and every
912
+ * shell path is the HTML shell.
913
+ */
914
+ door?: "sso" | "access";
915
+ /**
916
+ * `door: "access"` only — the fetch that carries NO Access token, for the preview and for
917
+ * the no-token half of the public door. `fetch` is the one that carries it.
918
+ */
919
+ anonymousFetch?: (url: string, init?: RequestInit) => Promise<Response>;
905
920
  deployment: { env: string; workerName: string; publicUrl: string; previewUrl: string | null };
906
921
  /** Addresses given on the command line. Empty: the preview (no cache in front) and the public URL. */
907
922
  urls?: readonly string[];
908
923
  /** The checkout's HEAD, as `/healthz` reports a commit (`git rev-parse --short=8 HEAD`). */
909
924
  head: string;
910
- /** Every private route; each must refuse an anonymous caller with 401. Empty is a FAILURE. */
925
+ /**
926
+ * Every private route; each must refuse an anonymous caller with 401 (`door: "access"`: 403 on
927
+ * the preview, 200 through the token on the public host). Empty is a FAILURE.
928
+ */
911
929
  census: readonly CensusRoute[];
912
930
  /** Client routes that must get the HTML shell THROUGH the Worker. */
913
931
  shellPaths: readonly string[];
@@ -919,6 +937,8 @@ export interface WorkerSmokeSpec {
919
937
  /** The app's own checks, per address, after the census — `family`'s `/ws/tree`, say. */
920
938
  extraChecks?: (context: {
921
939
  tag: string;
940
+ /** `door: "access"`: which half this address is — the preview is asked with no token. */
941
+ address: "preview" | "public";
922
942
  ask: (path: string, init?: RequestInit) => Promise<Response>;
923
943
  health: Record<string, unknown>;
924
944
  record: Smoke["record"];
@@ -962,6 +982,8 @@ export async function runWorkerSmoke(spec: WorkerSmokeSpec): Promise<number> {
962
982
  }
963
983
  if (smoke.refusals.length > 0) return smoke.report();
964
984
 
985
+ if (spec.door === "access") return await accessDoorSmoke(spec, urls, smoke, log);
986
+
965
987
  for (const base of urls) {
966
988
  const tag = new URL(base).hostname;
967
989
  log(`\n▸ ${base} (the ${deployment.env} deployment: ${deployment.workerName})`);
@@ -1010,7 +1032,8 @@ export async function runWorkerSmoke(spec: WorkerSmokeSpec): Promise<number> {
1010
1032
  : `🔴 ${open.join(", ")}`,
1011
1033
  );
1012
1034
 
1013
- await spec.extraChecks?.({ tag, ask, health, record: (check, ok, detail) => smoke.record(check, ok, detail) });
1035
+ const address = base === deployment.previewUrl ? "preview" : "public";
1036
+ await spec.extraChecks?.({ tag, address, ask, health, record: (check, ok, detail) => smoke.record(check, ok, detail) });
1014
1037
 
1015
1038
  for (const path of spec.shellPaths) {
1016
1039
  const res = await ask(path, { headers: { accept: "text/html" } });
@@ -1026,3 +1049,101 @@ export async function runWorkerSmoke(spec: WorkerSmokeSpec): Promise<number> {
1026
1049
  }
1027
1050
  return await smoke.settle();
1028
1051
  }
1052
+
1053
+ /**
1054
+ * `runWorkerSmoke` for a Worker behind Cloudflare ACCESS (`door: "access"`). An address is the
1055
+ * PREVIEW when it is `deployment.previewUrl` (no Access in front: the Worker's own check is the
1056
+ * whole door, so everything private must be 403), and otherwise the PUBLIC host (Access turns a
1057
+ * tokenless caller away; with the token the app must work). 🔴 A preview that answers 200 is
1058
+ * the Worker's own Access check switched off — the address no cache can lie on.
1059
+ */
1060
+ async function accessDoorSmoke(
1061
+ spec: WorkerSmokeSpec,
1062
+ urls: readonly string[],
1063
+ smoke: Smoke,
1064
+ log: (line: string) => void,
1065
+ ): Promise<number> {
1066
+ const { deployment } = spec;
1067
+ const anonymous = spec.anonymousFetch;
1068
+ if (!anonymous) {
1069
+ smoke.refuseEnvironment("the anonymous fetch", 'door: "access" needs `anonymousFetch` — a fetch that carries NO Access token');
1070
+ return smoke.report();
1071
+ }
1072
+ for (const base of urls) {
1073
+ const tag = new URL(base).hostname;
1074
+ const address = base === deployment.previewUrl ? "preview" : "public";
1075
+ const fetcher = address === "preview" ? anonymous : spec.fetch;
1076
+ log(
1077
+ address === "preview"
1078
+ ? `\n▸ ${base} (the ${deployment.env} preview — no Access in front; everything private must be refused)`
1079
+ : `\n▸ ${base} (the ${deployment.env} deployment through Access, with the service token)`,
1080
+ );
1081
+ const ask = (path: string, init: RequestInit = {}) => fetcher(new URL(path, base).toString(), init);
1082
+
1083
+ let health: Record<string, unknown>;
1084
+ let status: number;
1085
+ try {
1086
+ const read = await steadyHealth<Record<string, unknown>>(() => ask("/healthz"), (body) => body.commit === spec.head, spec.steady);
1087
+ health = read.body;
1088
+ status = read.response.status;
1089
+ } catch (error) {
1090
+ smoke.markEdgeFault();
1091
+ smoke.record(`${tag} healthz`, false, `${(error as Error).message}${smoke.dnsHint(error as Error)}`);
1092
+ continue;
1093
+ }
1094
+ smoke.record(`${tag} healthz`, status === 200 && health.runtime === "worker", `${status}, runtime ${JSON.stringify(health.runtime)}`);
1095
+ smoke.record(`${tag} commit`, health.commit === spec.head, `serving ${JSON.stringify(health.commit)}, HEAD ${spec.head}`);
1096
+ smoke.record(
1097
+ `${tag} access`,
1098
+ health.access === true,
1099
+ `the Worker's own Access check is ${health.access === true ? "configured" : "🔴 UNCONFIGURED — it refuses everything"}`,
1100
+ );
1101
+
1102
+ if (spec.census.length === 0) {
1103
+ smoke.record(`${tag} census`, false, "🔴 the route census is EMPTY, so this proves nothing");
1104
+ } else if (address === "preview") {
1105
+ const open: string[] = [];
1106
+ for (const route of spec.census) {
1107
+ const answered = (await ask(route.path, { method: route.method })).status;
1108
+ if (answered !== 403) open.push(`${route.method} ${route.path} → ${answered}`);
1109
+ }
1110
+ for (const path of spec.shellPaths) {
1111
+ const answered = (await ask(path, { headers: { accept: "text/html" } })).status;
1112
+ if (answered !== 403) open.push(`GET ${path} → ${answered}`);
1113
+ }
1114
+ const asked = spec.census.length + spec.shellPaths.length;
1115
+ smoke.record(`${tag} refuses`, open.length === 0, open.length === 0 ? `all ${asked} private paths → 403` : `🔴 ${open.join(", ")}`);
1116
+ } else {
1117
+ const first = spec.census[0] as CensusRoute;
1118
+ const turnedAway = (await anonymous(new URL(first.path, base).toString(), { method: first.method })).status;
1119
+ smoke.record(`${tag} access-door`, turnedAway !== 200, `no token → ${turnedAway} (Access)`);
1120
+ const refused: string[] = [];
1121
+ for (const route of spec.census) {
1122
+ const answered = (await ask(route.path, { method: route.method })).status;
1123
+ if (answered !== 200) refused.push(`${route.method} ${route.path} → ${answered}`);
1124
+ }
1125
+ smoke.record(
1126
+ `${tag} census`,
1127
+ refused.length === 0,
1128
+ refused.length === 0 ? `all ${spec.census.length} routes answer through the token` : `🔴 ${refused.join(", ")}`,
1129
+ );
1130
+ }
1131
+
1132
+ await spec.extraChecks?.({ tag, address, ask, health, record: (check, ok, detail) => smoke.record(check, ok, detail) });
1133
+
1134
+ if (address === "public") {
1135
+ for (const path of spec.shellPaths) {
1136
+ const res = await ask(path, { headers: { accept: "text/html" } });
1137
+ const type = res.headers.get("content-type") ?? "";
1138
+ smoke.record(`${tag} shell ${path}`, res.status === 200 && type.includes("text/html"), `${res.status} ${type}`);
1139
+ }
1140
+ const miss = await ask("/assets/index-no-such-bundle.js");
1141
+ smoke.record(
1142
+ `${tag} missing-bundle`,
1143
+ miss.status === 404 && !(miss.headers.get("content-type") ?? "").includes("text/html"),
1144
+ `a missing bundle is a miss — ${miss.status}`,
1145
+ );
1146
+ }
1147
+ }
1148
+ return await smoke.settle();
1149
+ }
@@ -32,6 +32,9 @@
32
32
  * prints the size of that loss before anybody decides.
33
33
  */
34
34
 
35
+ import { spawnSync } from "node:child_process";
36
+ import { livePort } from "cursedops/launchd";
37
+
35
38
  /** The Cloudflare v4 API's envelope — only what is read here. Structural, so any caller fits. */
36
39
  export interface ApiResult {
37
40
  success?: boolean;
@@ -131,3 +134,82 @@ export async function rollbackToOrigin(steps: RollbackSteps): Promise<RollbackRe
131
134
  }
132
135
  return { ok: true, stage: "done", detail: "the origin answers and the route is gone — prove it with a cache-busted /healthz, because a cached 200 is not a working origin." };
133
136
  }
137
+
138
+ /** What {@link runWorkerRollback} needs to know about ONE app's production hostname. */
139
+ export interface WorkerRollbackSpec {
140
+ /** `flix`, `music` — prefixes a refusal. */
141
+ app: string;
142
+ /** The app's checkout — where the origin is started and the comparison runs. */
143
+ root: string;
144
+ /** The production deployment's public URL — its hostname is the route that is found. */
145
+ publicUrl: string;
146
+ /** The Mac host's launchd label (`com.<app>.host`) and the port its file declares. */
147
+ label: string;
148
+ deployPort: number;
149
+ /** The Cloudflare API, authenticated (`cursedops/worker-secrets`' `cloudflareApi`). */
150
+ api: CloudflareApi;
151
+ /** Words appended to step 1 of `--check` — music's "com.music.downloads removed …". */
152
+ startNote?: string;
153
+ /** What makes the rollback STICK, printed after it succeeds — each app's own configuration. */
154
+ stickNotes: readonly string[];
155
+ }
156
+
157
+ export interface WorkerRollbackDeps {
158
+ /** Run `bun <args>` in the app's checkout, inheriting stdio. Answers the exit code. */
159
+ bun?: (args: readonly string[], cwd: string) => number;
160
+ /** The port the LOADED job declares (`cursedops/launchd`'s `livePort`). */
161
+ livePort?: (label: string) => number | null;
162
+ originUp?: (port: number) => Promise<boolean>;
163
+ sleep?: RollbackSteps["sleep"];
164
+ log?: (line: string) => void;
165
+ error?: (line: string) => void;
166
+ }
167
+
168
+ /**
169
+ * The whole `worker:rollback` main, lifted out of `apps/music` and `apps/flix` (they were one
170
+ * 78-line file with the names changed). `--check` says what would happen and changes nothing;
171
+ * without it: print the route (FOUND on the account), print the D1-vs-Mac row comparison (the
172
+ * writes a rollback loses — the app's `scripts/worker-import.ts --verify-only`), start the origin, and only check-doc-citations:ignore app-relative, in the consuming app's tree
173
+ * once it answers delete the route ({@link rollbackToOrigin}). Answers the exit code.
174
+ */
175
+ export async function runWorkerRollback(spec: WorkerRollbackSpec, argv: readonly string[], deps: WorkerRollbackDeps = {}): Promise<number> {
176
+ const log = deps.log ?? ((line: string) => console.log(line));
177
+ const error = deps.error ?? ((line: string) => console.error(line));
178
+ const bun =
179
+ deps.bun ??
180
+ ((args: readonly string[], cwd: string) => spawnSync("bun", [...args], { cwd, stdio: "inherit" }).status ?? 1);
181
+ const port = () => (deps.livePort ?? livePort)(spec.label) ?? spec.deployPort;
182
+ const host = new URL(spec.publicUrl).hostname;
183
+
184
+ const found = await findWorkerRoute(spec.api, host);
185
+ log(`route ${found.pattern} → ${found.route ? `${found.script} (${found.route})` : "(none — the hostname already falls through to the Mac)"}`);
186
+
187
+ log("\n▸ what D1 holds against the Mac's frozen catalogue (the writes a rollback loses):");
188
+ bun(["run", "scripts/worker-import.ts", "--verify-only"], spec.root);
189
+
190
+ if (argv.includes("--check")) {
191
+ log("\nwould, in this order:");
192
+ log(` 1. bun run service install --rollback (${spec.label} back on :${spec.deployPort}${spec.startNote ? `; ${spec.startNote}` : ""})`);
193
+ log(` 2. wait for http://127.0.0.1:${spec.deployPort}/healthz to answer`);
194
+ log(` 3. DELETE the route ${found.pattern} (the hostname falls through to the tunnel and the Mac)`);
195
+ log("\n(--check — nothing was changed)");
196
+ return 0;
197
+ }
198
+
199
+ log(`\n▸ starting ${spec.label}, then — only once it answers — deleting the route`);
200
+ const result = await rollbackToOrigin({
201
+ startOrigin: () => bun(["run", "scripts/service.ts", "install", "--rollback"], spec.root),
202
+ // The port the LOADED job declares, never the app's constant — the two have diverged before.
203
+ originUp: () => (deps.originUp ?? loopbackHealthy)(port()),
204
+ removeRoute: () => deleteWorkerRoute(spec.api, found),
205
+ sleep: deps.sleep,
206
+ });
207
+ if (!result.ok) {
208
+ error(`\n🔴 [${spec.app}] ${result.detail}`);
209
+ return 1;
210
+ }
211
+ log(`\n✅ rolled back — ${result.detail}`);
212
+ log(` curl -s "${spec.publicUrl}/healthz?cb=$RANDOM" # must say runtime:"bun"`);
213
+ for (const line of spec.stickNotes) log(line);
214
+ return 0;
215
+ }
@@ -220,6 +220,12 @@ export interface WorkerSecretsSpec {
220
220
  cutOver: boolean;
221
221
  /** The Mac's `<app>.env` — where a mint reads the app's signing key from. */
222
222
  macSecretsFile: string;
223
+ /**
224
+ * The keys a `--mint` carries over from {@link macSecretsFile}, beside the throwaway
225
+ * `SESSION_SECRET`. Default `["FILE_TOKEN_PRIVATE_KEY"]` — every port that signs binary-server
226
+ * URLs. flix signs none; what its preview needs is `["PLEX_ACCOUNT_TOKEN"]`.
227
+ */
228
+ mintCarries?: readonly string[];
223
229
  /** What a deployment without a signing key cannot do — "signs no portrait URL". */
224
230
  withoutSigningKey: string;
225
231
  /** The app's own sentence about its signing key, for the minted file's header. */
@@ -251,10 +257,15 @@ export async function runWorkerSecrets(
251
257
  console.error(`${tag} --mint makes a THROWAWAY set. Production after the cutover uploads the Mac's own ${spec.app}.env.`);
252
258
  return 2;
253
259
  }
254
- const signing = readEnvFile(read(spec.macSecretsFile) ?? "").FILE_TOKEN_PRIVATE_KEY;
255
- if (!signing) {
256
- console.error(`${tag} ${spec.app}.env carries no FILE_TOKEN_PRIVATE_KEY — without it this deployment ${spec.withoutSigningKey}.`);
257
- return 2;
260
+ const mac = readEnvFile(read(spec.macSecretsFile) ?? "");
261
+ const carried: Record<string, string> = {};
262
+ for (const key of spec.mintCarries ?? ["FILE_TOKEN_PRIVATE_KEY"]) {
263
+ const value = mac[key];
264
+ if (!value) {
265
+ console.error(`${tag} ${spec.app}.env carries no ${key} — without it this deployment ${spec.withoutSigningKey}.`);
266
+ return 2;
267
+ }
268
+ carried[key] = value;
258
269
  }
259
270
  const who = spec.env === "stage" ? "the STAGE" : "the PREVIEW";
260
271
  (deps.write ?? writeSecretsFile)(
@@ -265,7 +276,7 @@ export async function runWorkerSecrets(
265
276
  "throwaway and opens nothing the owner owns.",
266
277
  spec.signingKeyNote,
267
278
  ],
268
- { SESSION_SECRET: throwawaySecret(), FILE_TOKEN_PRIVATE_KEY: signing },
279
+ { SESSION_SECRET: throwawaySecret(), ...carried },
269
280
  );
270
281
  console.log(`✅ minted ${who}'s secrets into ${spec.secretsFile} (0600)`);
271
282
  return 0;