cursedops 0.16.3 → 0.18.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/package.json +1 -1
- package/src/relayStage.ts +66 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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/relayStage.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { lookup as dnsLookup } from "node:dns/promises";
|
|
|
20
20
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
21
|
import { join, relative } from "node:path";
|
|
22
22
|
import { sha256Hex } from "cursedops/relay";
|
|
23
|
+
import { awaitConverged, type ConvergenceOptions, type ConvergenceProbe } from "cursedops/smoke";
|
|
23
24
|
|
|
24
25
|
/** The accounts door every stage signs in at — `apps/auth` deployed a second time. */
|
|
25
26
|
export const STAGE_AUTH_URL = "https://auth-stage.cursedalchemy.com";
|
|
@@ -159,6 +160,30 @@ export const realRun: Run = (argv, options) => {
|
|
|
159
160
|
return { status: done.status, stdout: done.stdout ?? "", stderr: done.stderr ?? "" };
|
|
160
161
|
};
|
|
161
162
|
|
|
163
|
+
/**
|
|
164
|
+
* 🔴 How many rounds in a row every `/healthz` must name a just-put link digest before a walk uses it.
|
|
165
|
+
* A digest put is a new Worker version, and the edge takes it up instance by instance: the link
|
|
166
|
+
* staying up for 15 s proves only that the instance the link dialled has it. The Worker signs the
|
|
167
|
+
* Mac's identity with that digest, so a request answered by an instance still on the old one is the
|
|
168
|
+
* Mac's own 401 (and station's kept read a 503). Measured 2026-10-15 (forge task 3235): station's
|
|
169
|
+
* fast-lane walk at baf8495, the link steady for 15 s, then four writes 401 at the Mac; on the real
|
|
170
|
+
* stage the put needed 8 rounds to agree 5 in a row.
|
|
171
|
+
*/
|
|
172
|
+
export const LINK_KEY_ROUNDS = 5;
|
|
173
|
+
|
|
174
|
+
/** The question {@link putStageLinkDigest} asks after a put: does this `/healthz` answer's `worker.linkKeyId` equal `expected`? */
|
|
175
|
+
export function linkKeyProbe(health: () => Promise<Response>, expected: string): ConvergenceProbe {
|
|
176
|
+
return {
|
|
177
|
+
name: "/healthz linkKeyId",
|
|
178
|
+
check: async () => {
|
|
179
|
+
const res = await health();
|
|
180
|
+
if (!res.ok) return `status ${res.status}`;
|
|
181
|
+
const id = ((await res.json()) as { worker?: { linkKeyId?: string } }).worker?.linkKeyId ?? "";
|
|
182
|
+
return id === expected ? null : `answers ${id || "no key"}, not ${expected}`;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
export interface DigestPut {
|
|
163
188
|
/** The app's checkout, where `wrangler.jsonc` is. */
|
|
164
189
|
cwd: string;
|
|
@@ -167,31 +192,42 @@ export interface DigestPut {
|
|
|
167
192
|
key: string;
|
|
168
193
|
/** `cloudflareCredential(...)`. */
|
|
169
194
|
credential: Record<string, string>;
|
|
195
|
+
/** One `/healthz` read of the stage Worker — asked until every answer names the new digest. */
|
|
196
|
+
health: () => Promise<Response>;
|
|
197
|
+
/** The fingerprint `/healthz` publishes for a digest — the app's `cursedauth/relay-door` `linkKeyId(app, digest)`. */
|
|
198
|
+
keyId: (digest: string) => Promise<string>;
|
|
170
199
|
run?: Run;
|
|
200
|
+
/** Injected for tests; `rounds` defaults to {@link LINK_KEY_ROUNDS}. */
|
|
201
|
+
convergence?: ConvergenceOptions;
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
/**
|
|
174
205
|
* Put `key`'s digest on the STAGE Worker (`--env stage`, always — this module has no production
|
|
175
|
-
* path)
|
|
176
|
-
*
|
|
177
|
-
*
|
|
206
|
+
* path), then return only once every `/healthz` answer has named it {@link LINK_KEY_ROUNDS} rounds in
|
|
207
|
+
* a row — so no walk can put a digest and use it before the edge has (forge tasks 3235, 3237). A
|
|
208
|
+
* per-run key for the walk, and again for the rotation leg. `ok: false` is a refused put (wrangler's
|
|
209
|
+
* stderr) or an edge that never agreed; `detail` says which, for the walk's row.
|
|
178
210
|
*/
|
|
179
|
-
export async function putStageLinkDigest(put: DigestPut): Promise<{ ok: boolean; stderr: string }> {
|
|
211
|
+
export async function putStageLinkDigest(put: DigestPut): Promise<{ ok: boolean; stderr: string; detail: string }> {
|
|
212
|
+
const digest = await sha256Hex(put.key);
|
|
180
213
|
const done = (put.run ?? realRun)(["bunx", "wrangler", "secret", "put", put.secretName, "--env", "stage"], {
|
|
181
214
|
cwd: put.cwd,
|
|
182
|
-
input:
|
|
215
|
+
input: digest,
|
|
183
216
|
env: put.credential,
|
|
184
217
|
});
|
|
185
|
-
return { ok:
|
|
218
|
+
if (done.status !== 0) return { ok: false, stderr: done.stderr, detail: `wrangler refused the put (exit ${done.status})` };
|
|
219
|
+
const held = await awaitConverged([linkKeyProbe(put.health, await put.keyId(digest))], { rounds: LINK_KEY_ROUNDS, ...put.convergence });
|
|
220
|
+
return { ok: held.converged, stderr: done.stderr, detail: `every answer holds it — ${held.detail}` };
|
|
186
221
|
}
|
|
187
222
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
/** Which stage account a grant signs in as — `inbox.ts`'s `StageAccount`. */
|
|
224
|
+
type StageAccount = "owner" | "member";
|
|
225
|
+
|
|
226
|
+
type GrantSpec = { forge: string; app: string; run?: Run; stderr?: (text: string) => void };
|
|
227
|
+
|
|
228
|
+
/** `apps/auth-stage/scripts/inbox.ts` `grant <app> [--member]`, whose last stdout line is the grant. */
|
|
229
|
+
function grantStage(spec: GrantSpec, account: StageAccount): { ok: boolean; grant: string } {
|
|
230
|
+
const done = (spec.run ?? realRun)(["bun", "run", "scripts/inbox.ts", "grant", spec.app, ...(account === "member" ? ["--member"] : [])], {
|
|
195
231
|
cwd: join(spec.forge, "apps", "auth-stage"),
|
|
196
232
|
timeout: 300_000,
|
|
197
233
|
});
|
|
@@ -200,6 +236,23 @@ export function grantStageOwner(spec: { forge: string; app: string; run?: Run; s
|
|
|
200
236
|
return { ok: done.status === 0 && grant.length > 100, grant };
|
|
201
237
|
}
|
|
202
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Sign the stage owner in through auth-stage's emailed second factor and grant `app`'s stage.
|
|
241
|
+
* Never production's accounts service: nothing under test signs in there (`tools/check-prod-auth.ts`).
|
|
242
|
+
*/
|
|
243
|
+
export function grantStageOwner(spec: GrantSpec): { ok: boolean; grant: string } {
|
|
244
|
+
return grantStage(spec, "owner");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The same grant for the stage's seeded household MEMBER — an ordinary account, never the owner —
|
|
249
|
+
* so a walk can prove on the real edge what an app keeps from everyone but its owner (roms' closed
|
|
250
|
+
* consoles, forge task 3288). Same door, same rules: never production's accounts service.
|
|
251
|
+
*/
|
|
252
|
+
export function grantStageMember(spec: GrantSpec): { ok: boolean; grant: string } {
|
|
253
|
+
return grantStage(spec, "member");
|
|
254
|
+
}
|
|
255
|
+
|
|
203
256
|
export interface RotationSpec {
|
|
204
257
|
/** Stop the Mac end, resolving once it has exited. */
|
|
205
258
|
stop: () => Promise<void>;
|