cursedops 0.10.3 → 0.10.5
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 +24 -6
- package/src/relayLink.ts +26 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedops",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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
|
@@ -119,7 +119,7 @@ export function parseCurlResponse(raw: Buffer, origin = "the edge", exit: number
|
|
|
119
119
|
return new Response(status === 204 || status === 304 ? null : new Uint8Array(body), { status: status || 599, headers });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/** A `fetch`-shaped function over curl — see the header. Each host is resolved once per instance. */
|
|
122
|
+
/** A `fetch`-shaped function over curl — see the header. Each host is resolved once per instance — once it has an answer. */
|
|
123
123
|
export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, init?: RequestInit) => Promise<Response> {
|
|
124
124
|
const pinned = new Map<string, string>();
|
|
125
125
|
const resolve = options.resolve === undefined ? digCloudflare : options.resolve;
|
|
@@ -130,12 +130,29 @@ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, i
|
|
|
130
130
|
return { status: out.status, stdout: out.stdout ?? Buffer.alloc(0) };
|
|
131
131
|
});
|
|
132
132
|
let sequence = 0;
|
|
133
|
+
/**
|
|
134
|
+
* The host's pinned address, asked for until there IS one. 🔴 An empty answer is never cached:
|
|
135
|
+
* measured 2026-09-24 on roms' first stage deploy, the smoke asked `1.1.1.1` for a custom domain
|
|
136
|
+
* seconds after wrangler created it, got nothing, and pinned that NOTHING for the life of the
|
|
137
|
+
* process — so every request (the retries included) fell through to this Mac's resolver, which
|
|
138
|
+
* had just cached the NXDOMAIN for the zone's 1800 s negative TTL. A green Worker, a red smoke.
|
|
139
|
+
*/
|
|
140
|
+
const pinFor = (host: string): string => {
|
|
141
|
+
if (!resolve) return "";
|
|
142
|
+
const known = pinned.get(host);
|
|
143
|
+
if (known) return known;
|
|
144
|
+
const ip = resolve(host) ?? "";
|
|
145
|
+
if (ip) pinned.set(host, ip);
|
|
146
|
+
return ip;
|
|
147
|
+
};
|
|
133
148
|
return async (url, init = {}) => {
|
|
134
149
|
const target = new URL(url);
|
|
135
|
-
if (resolve && !pinned.has(target.hostname)) pinned.set(target.hostname, resolve(target.hostname) ?? "");
|
|
136
|
-
const ip = pinned.get(target.hostname);
|
|
137
150
|
const port = target.port || (target.protocol === "http:" ? "80" : "443");
|
|
138
|
-
const
|
|
151
|
+
const head = () => {
|
|
152
|
+
const ip = pinFor(target.hostname);
|
|
153
|
+
return ["-s", "-i", "--max-time", String(options.maxTimeSec ?? 60), ...(ip ? ["--resolve", `${target.hostname}:${port}:${ip}`] : [])];
|
|
154
|
+
};
|
|
155
|
+
const args: string[] = [];
|
|
139
156
|
for (const [k, v] of Object.entries(options.headersFor?.(target.hostname) ?? {})) args.push("-H", `${k}: ${v}`);
|
|
140
157
|
new Headers(init.headers).forEach((v, k) => {
|
|
141
158
|
args.push("-H", `${k}: ${v}`);
|
|
@@ -152,10 +169,11 @@ export function createEdgeFetch(options: EdgeFetchOptions = {}): (url: string, i
|
|
|
152
169
|
args.push("--data-binary", `@${bodyFile}`);
|
|
153
170
|
}
|
|
154
171
|
try {
|
|
155
|
-
let out = curl([...args, target.toString()]);
|
|
172
|
+
let out = curl([...head(), ...args, target.toString()]);
|
|
156
173
|
for (let retry = 1; retry <= EDGE_RETRIES && retryableCurlFailure(out.status, out.stdout, init.method); retry++) {
|
|
157
174
|
await new Promise((r) => setTimeout(r, options.retryDelayMs?.(retry) ?? 500 * retry));
|
|
158
|
-
|
|
175
|
+
// `head()` again: a host with no pin yet is asked again, so a retry can find the address.
|
|
176
|
+
out = curl([...head(), ...args, target.toString()]);
|
|
159
177
|
}
|
|
160
178
|
return parseCurlResponse(out.stdout, target.origin, out.status);
|
|
161
179
|
} finally {
|
package/src/relayLink.ts
CHANGED
|
@@ -74,6 +74,24 @@ export function linkAccessHeaders(env: Record<string, string | undefined>): Reco
|
|
|
74
74
|
return id && secret ? { "CF-Access-Client-Id": id, "CF-Access-Client-Secret": secret } : {};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Why a link would dial into Access's login page instead of the Worker, or null.
|
|
79
|
+
*
|
|
80
|
+
* 🔴 Measured at station's cutover (task 2156, 2026-09-24): `station.cursedalchemy.com` is behind the
|
|
81
|
+
* owner's Access application, so a link pointed at it was refused its handshake (1002, "Expected 101")
|
|
82
|
+
* on every try — a reconnect loop with no other error while the browser half answered 503. The
|
|
83
|
+
* preview (`*.workers.dev`) is the same Worker and the same Durable Object with no Access in front —
|
|
84
|
+
* `/link`'s own guard is the bearer key. Any other host needs the Access token in the handshake
|
|
85
|
+
* headers ({@link linkAccessHeaders}). {@link dialLink} asks this before its first connect.
|
|
86
|
+
*/
|
|
87
|
+
export function linkUrlRefusal(url: string, headers: Record<string, string> = {}): string | null {
|
|
88
|
+
const host = new URL(url.replace(/^ws/, "http")).hostname;
|
|
89
|
+
if (host.endsWith(".workers.dev") || host === "127.0.0.1" || host === "localhost") return null;
|
|
90
|
+
const has = (name: string) => Object.keys(headers).some((k) => k.toLowerCase() === name && headers[k]?.trim());
|
|
91
|
+
if (has("cf-access-client-id") && has("cf-access-client-secret")) return null;
|
|
92
|
+
return `${host} is behind Cloudflare Access and this link carries no Access token — dial the preview (wss://<name>.<subdomain>.workers.dev/link), the same Worker without Access, or set CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET`;
|
|
93
|
+
}
|
|
94
|
+
|
|
77
95
|
/** Answer one frame with `fetch`, never throwing: a crash in a route is a 500 on the wire. */
|
|
78
96
|
export async function answerFrame(
|
|
79
97
|
fetch: (request: Request) => Response | Promise<Response>,
|
|
@@ -127,8 +145,15 @@ const bunSocket = (url: string, headers: Record<string, string>): DialSocket =>
|
|
|
127
145
|
// Bun's WebSocket takes `{ headers }` as its second argument; the DOM typing says protocols.
|
|
128
146
|
new WebSocket(url, { headers } as unknown as string[]) as unknown as DialSocket;
|
|
129
147
|
|
|
130
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Dial, answer, and redial on close with a 1 s → 60 s backoff. See the header.
|
|
150
|
+
*
|
|
151
|
+
* THROWS before the first connect when {@link linkUrlRefusal} refuses the URL: a link that can never
|
|
152
|
+
* be let through Access must fail loudly at start, not retry 1002 forever.
|
|
153
|
+
*/
|
|
131
154
|
export function dialLink(options: DialOptions): DialedLink {
|
|
155
|
+
const refused = linkUrlRefusal(options.settings.url, options.headers);
|
|
156
|
+
if (refused) throw new Error(`[${options.app}-link] ✗ ${refused}`);
|
|
132
157
|
const origin = options.origin ?? publicOriginOf(options.settings.url);
|
|
133
158
|
const log = options.log ?? ((line: string) => console.log(line));
|
|
134
159
|
const open = options.openSocket ?? bunSocket;
|