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 +2 -2
- package/package.json +1 -1
- package/src/smoke.ts +123 -2
- package/src/workerRollback.ts +82 -0
- package/src/workerSecrets.ts +16 -5
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
}
|
package/src/workerRollback.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/workerSecrets.ts
CHANGED
|
@@ -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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(),
|
|
279
|
+
{ SESSION_SECRET: throwawaySecret(), ...carried },
|
|
269
280
|
);
|
|
270
281
|
console.log(`✅ minted ${who}'s secrets into ${spec.secretsFile} (0600)`);
|
|
271
282
|
return 0;
|