mirror of
https://github.com/dohooo/helmor.git
synced 2026-09-22 16:40:02 +08:00
bdfea0731f
A fresh Cloudflare account never had BROKER_ENC_KEY (it was only ever set by
hand on the dev account), so the first agent authorize threw on
atob(undefined) inside the identity DOs and every fresh team 500'd its way to
an un-finishable create flow.
- provision-team.ts: after the companion-token put, probe `wrangler secret
list` and decide via pure logic in the new broker-key.ts. The four branches
(unit-tested): key present → skip; absent → put (32 random bytes, standard
base64, never logged); list failed with the Worker-not-found error (fresh
account, verbatim shape captured from wrangler 4.100.0) → put; ANY other
list failure → the whole provision fails. ⚠️ Writing only-when-absent is a
hard safety constraint: rotating an existing key bricks every stored
identity. Black-box check against the live dev account (key present since
the manual setup) decides "skip"; the live worker-not-found error decides
"put".
- codex/claude/forge identity DOs: importKey now names the real problem
("BROKER_ENC_KEY is not configured … re-run team setup") instead of an
opaque atob crash wrapped as a blind 500 — covers Workers provisioned
before this fix. Guard covered by the codex + claude twin suites; the forge
copy is verbatim-identical.
- wrangler.toml: BROKER_ENC_KEY added to the documented secrets list (it was
missing there too), with the do-not-rotate warning.
82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
// Decision logic for provisioning the BROKER_ENC_KEY Worker secret — the
|
|
// AES-256-GCM key all three identity brokers (Codex / Claude / Forge Durable
|
|
// Objects) encrypt member credentials with. Pure functions: unit-tested in
|
|
// cloud/test/broker-key.test.ts, consumed by provision-team.ts.
|
|
//
|
|
// ⚠️ HARD SAFETY CONSTRAINT (round6 P1-1b): the key is written ONLY when it is
|
|
// confidently absent. Re-putting an existing key ROTATES it and bricks every
|
|
// already-stored identity (their ciphertexts become undecryptable). When the
|
|
// existence check is inconclusive, the provision run FAILS — it never
|
|
// blind-writes.
|
|
|
|
export type SecretListOutcome =
|
|
| { kind: "ok"; names: string[] }
|
|
| { kind: "error"; output: string };
|
|
|
|
export type BrokerKeyAction = "skip" | "put" | "fail";
|
|
|
|
/** wrangler's "worker doesn't exist yet" signal, observed live on wrangler
|
|
* 4.100.0: `✘ [ERROR] Worker "helmor-team" not found.` (ANSI color codes wrap
|
|
* the line; the inner text is contiguous, so the regex matches raw output).
|
|
* A missing Worker means a FRESH deployment — no secret can exist on a Worker
|
|
* that doesn't — so writing the key is safe on this branch. */
|
|
const WORKER_NOT_FOUND = /worker "[^"]*" not found/i;
|
|
|
|
/** The four branches (in order): key present → skip; key absent → put; list
|
|
* failed because the Worker doesn't exist (fresh account) → put; list failed
|
|
* for ANY other reason (network, auth, unparseable output) → fail the whole
|
|
* provision rather than risk rotating an existing key. */
|
|
export function decideBrokerKeyAction(
|
|
outcome: SecretListOutcome,
|
|
): BrokerKeyAction {
|
|
if (outcome.kind === "ok") {
|
|
return outcome.names.includes("BROKER_ENC_KEY") ? "skip" : "put";
|
|
}
|
|
return WORKER_NOT_FOUND.test(outcome.output) ? "put" : "fail";
|
|
}
|
|
|
|
/** Map a raw `wrangler secret list` result to a decision input. Success means
|
|
* exit 0 AND parseable stdout — an exit-0 run whose output we can't read is
|
|
* an error outcome (→ fail), never a guess. */
|
|
export function toSecretListOutcome(result: {
|
|
code: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
}): SecretListOutcome {
|
|
if (result.code !== 0) {
|
|
return { kind: "error", output: `${result.stdout}\n${result.stderr}` };
|
|
}
|
|
const names = parseSecretList(result.stdout);
|
|
if (names === null) {
|
|
return {
|
|
kind: "error",
|
|
output: `unparseable secret list output:\n${result.stdout}`,
|
|
};
|
|
}
|
|
return { kind: "ok", names };
|
|
}
|
|
|
|
/** Parse `wrangler secret list` stdout — a JSON array of `{name, type}`
|
|
* entries (format observed live on wrangler 4.100.0). Tolerates banner noise
|
|
* before the array; returns null when no well-formed array is found. */
|
|
export function parseSecretList(stdout: string): string[] | null {
|
|
const raw = stdout.trim();
|
|
for (const candidate of [raw, raw.slice(raw.indexOf("["))]) {
|
|
if (!candidate.startsWith("[")) continue;
|
|
try {
|
|
const parsed: unknown = JSON.parse(candidate);
|
|
if (!Array.isArray(parsed)) return null;
|
|
return parsed
|
|
.map((entry) =>
|
|
typeof (entry as { name?: unknown }).name === "string"
|
|
? ((entry as { name: string }).name satisfies string)
|
|
: "",
|
|
)
|
|
.filter(Boolean);
|
|
} catch {
|
|
// fall through to the sliced candidate
|
|
}
|
|
}
|
|
return null;
|
|
}
|