cursedops 0.10.1 → 0.10.3
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/edgeFetch.ts +33 -1
- package/src/relayLink.ts +33 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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), 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
|
@@ -54,6 +54,34 @@ export interface EdgeFetchOptions {
|
|
|
54
54
|
tmpDir?: string;
|
|
55
55
|
/** curl's `--max-time`. Default 60. */
|
|
56
56
|
maxTimeSec?: number;
|
|
57
|
+
/** Wait before retry `n` (1-based) of a connection-level failure. Default `500 * n` ms. */
|
|
58
|
+
retryDelayMs?: (retry: number) => number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 🔴 How many MORE times a request is sent after a connection-level curl failure that carried
|
|
63
|
+
* no answer. Measured 2026-09-24: vault's prod deploy of `953e19a` reported "smoke FAILED" on one
|
|
64
|
+
* `curl exit 35` (TLS handshake) while the Worker was serving the new build, and `worker:smoke`
|
|
65
|
+
* straight after was green on both addresses — a green deploy reported red, pointing its reader
|
|
66
|
+
* at `wrangler rollback`.
|
|
67
|
+
*/
|
|
68
|
+
const EDGE_RETRIES = 2;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a curl failure may be sent again. Only when NOTHING answered (empty stdout), and then:
|
|
72
|
+
*
|
|
73
|
+
* · 6 resolve, 7 connect, 35 TLS handshake — the request never left this machine, so a replay
|
|
74
|
+
* is always safe, whatever the method;
|
|
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).
|
|
77
|
+
*
|
|
78
|
+
* Every other exit — 28 (timeout) above all, where the far end may still be working — and every
|
|
79
|
+
* HTTP status, whatever it is, is returned exactly as before.
|
|
80
|
+
*/
|
|
81
|
+
function retryableCurlFailure(exit: number | null, stdout: Buffer, method = "GET"): boolean {
|
|
82
|
+
if (stdout.length > 0 || exit === null) return false;
|
|
83
|
+
if (exit === 6 || exit === 7 || exit === 35) return true;
|
|
84
|
+
return (exit === 52 || exit === 56) && ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"].includes(method.toUpperCase());
|
|
57
85
|
}
|
|
58
86
|
|
|
59
87
|
function digCloudflare(host: string): string | null {
|
|
@@ -124,7 +152,11 @@ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, i
|
|
|
124
152
|
args.push("--data-binary", `@${bodyFile}`);
|
|
125
153
|
}
|
|
126
154
|
try {
|
|
127
|
-
|
|
155
|
+
let out = curl([...args, target.toString()]);
|
|
156
|
+
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method); retry++) {
|
|
157
|
+
await new Promise((r) => setTimeout(r, options.retryDelayMs?.(retry) ?? 500 * retry));
|
|
158
|
+
out = curl([...args, target.toString()]);
|
|
159
|
+
}
|
|
128
160
|
return parseCurlResponse(out.stdout, target.origin, out.status);
|
|
129
161
|
} finally {
|
|
130
162
|
if (bodyFile) rmSync(bodyFile, { force: true });
|
package/src/relayLink.ts
CHANGED
|
@@ -24,18 +24,37 @@ export interface LinkSettings {
|
|
|
24
24
|
key: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The link's three names, spelled WHOLE by the app — the env file's URL and key, and the Worker
|
|
29
|
+
* secret holding the key's digest. 🔴 Whole, not a prefix this module glues together: the app is
|
|
30
|
+
* then the visible reader of its own secrets, which is what `tools/check-secret-readers.ts` looks
|
|
31
|
+
* for (a built name like `` `${prefix}_LINK_URL` `` is invisible to it, and station's two keys read
|
|
32
|
+
* as "read by nothing" the day this module first took a prefix). A bare prefix string still works.
|
|
33
|
+
*/
|
|
34
|
+
export interface LinkNames {
|
|
35
|
+
url: string;
|
|
36
|
+
key: string;
|
|
37
|
+
digest: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `"ROMS"` → `ROMS_LINK_URL` / `ROMS_LINK_KEY` / `ROMS_LINK_KEY_SHA256`; names pass through. */
|
|
41
|
+
export function linkNames(names: string | LinkNames): LinkNames {
|
|
42
|
+
return typeof names === "string" ? { url: `${names}_LINK_URL`, key: `${names}_LINK_KEY`, digest: `${names}_LINK_KEY_SHA256` } : names;
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
/**
|
|
28
46
|
* The link's URL and key out of its 0600 env file (`<APP>_LINK_URL`, `<APP>_LINK_KEY`), or a sentence
|
|
29
47
|
* saying what is wrong. `wss://<host>/link`, or `ws://127.0.0.1:<port>/link` for a local workerd.
|
|
30
48
|
*/
|
|
31
|
-
export function readLinkSettings(text: string,
|
|
49
|
+
export function readLinkSettings(text: string, names: string | LinkNames, mintCommand = "bun run link:key"): LinkSettings | string {
|
|
50
|
+
const n = linkNames(names);
|
|
32
51
|
const found = readEnvFile(text);
|
|
33
|
-
const url = found[
|
|
34
|
-
const key = found[
|
|
52
|
+
const url = found[n.url]?.trim() ?? "";
|
|
53
|
+
const key = found[n.key]?.trim() ?? "";
|
|
35
54
|
if (!/^wss:\/\/[^/]+\/link$/.test(url) && !/^ws:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/link$/.test(url)) {
|
|
36
|
-
return `${
|
|
55
|
+
return `${n.url} must be wss://<host>/link (or ws://127.0.0.1:<port>/link for a local workerd), got "${url}"`;
|
|
37
56
|
}
|
|
38
|
-
if (key.length < 32) return `${
|
|
57
|
+
if (key.length < 32) return `${n.key} is missing or short — \`${mintCommand}\` mints one`;
|
|
39
58
|
return { url, key };
|
|
40
59
|
}
|
|
41
60
|
|
|
@@ -159,8 +178,9 @@ export function mintLinkKey(): string {
|
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
/** The link's env file, single-quoted so a shell `source` cannot mangle it. */
|
|
162
|
-
export function linkEnvText(
|
|
163
|
-
|
|
181
|
+
export function linkEnvText(names: string | LinkNames, url: string, key: string, mintCommand = "bun run link:key"): string {
|
|
182
|
+
const n = linkNames(names);
|
|
183
|
+
return `# the link — written by \`${mintCommand}\`. The key never leaves this Mac.\n${n.url}='${url}'\n${n.key}='${key}'\n`;
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
export interface RotateSpec {
|
|
@@ -168,8 +188,8 @@ export interface RotateSpec {
|
|
|
168
188
|
appDir: string;
|
|
169
189
|
/** The 0600 env file the Mac reads. */
|
|
170
190
|
file: string;
|
|
171
|
-
/** `ROMS` → `ROMS_LINK_URL` / `ROMS_LINK_KEY` / `ROMS_LINK_KEY_SHA256`. */
|
|
172
|
-
|
|
191
|
+
/** The three names ({@link LinkNames}) — or a prefix, `ROMS` → `ROMS_LINK_URL` / `ROMS_LINK_KEY` / `ROMS_LINK_KEY_SHA256`. */
|
|
192
|
+
names: string | LinkNames;
|
|
173
193
|
/** A new URL, or undefined to keep the file's. */
|
|
174
194
|
url?: string;
|
|
175
195
|
/** `cloudflareCredential(...)` — the env overlay wrangler runs with. */
|
|
@@ -187,10 +207,11 @@ export interface RotateSpec {
|
|
|
187
207
|
*/
|
|
188
208
|
export async function rotateLinkKey(spec: RotateSpec): Promise<{ ok: true; file: string } | { ok: false; why: string }> {
|
|
189
209
|
const previous = existsSync(spec.file) ? readEnvFile(readFileSync(spec.file, "utf8")) : {};
|
|
190
|
-
const
|
|
210
|
+
const n = linkNames(spec.names);
|
|
211
|
+
const url = spec.url ?? previous[n.url];
|
|
191
212
|
if (!url) return { ok: false, why: `no link URL yet — pass \`--url wss://<worker host>/link\` the first time` };
|
|
192
213
|
const key = mintLinkKey();
|
|
193
|
-
const secret =
|
|
214
|
+
const secret = n.digest;
|
|
194
215
|
const put =
|
|
195
216
|
spec.putSecret ??
|
|
196
217
|
((name: string, value: string) => {
|
|
@@ -204,7 +225,7 @@ export async function rotateLinkKey(spec: RotateSpec): Promise<{ ok: true; file:
|
|
|
204
225
|
});
|
|
205
226
|
const done = put(secret, await sha256Hex(key));
|
|
206
227
|
if (!done.ok) return { ok: false, why: `the Worker refused the digest — nothing was changed on this Mac:\n${done.detail}` };
|
|
207
|
-
writeFileSync(spec.file, linkEnvText(
|
|
228
|
+
writeFileSync(spec.file, linkEnvText(n, url, key), { mode: 0o600 });
|
|
208
229
|
chmodSync(spec.file, 0o600);
|
|
209
230
|
return { ok: true, file: spec.file };
|
|
210
231
|
}
|