




















@@ -0,0 +1,109 @@
1+#!/usr/bin/env -S node --import tsx
2+import fs from "node:fs/promises";
3+import path from "node:path";
4+5+type CodexAuthJson = {
6+tokens?: {
7+account_id?: unknown;
8+id_token?: unknown;
9+};
10+};
11+12+type JwtParts = {
13+header: string;
14+ payload: Record<string, unknown>;
15+ signature: string;
16+};
17+18+function decodeBase64UrlJson(value: string): Record<string, unknown> {
19+const decoded = Buffer.from(value, "base64url").toString("utf-8");
20+const parsed: unknown = JSON.parse(decoded);
21+if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
22+throw new Error("JWT payload is not a JSON object.");
23+}
24+return parsed as Record<string, unknown>;
25+}
26+27+function encodeBase64UrlJson(value: Record<string, unknown>): string {
28+return Buffer.from(JSON.stringify(value), "utf-8").toString("base64url");
29+}
30+31+function parseJwt(value: string): JwtParts {
32+ const parts = value.split(".");
33+if (parts.length !== 3 || !parts[0] || !parts[1]) {
34+throw new Error("id_token is not a JWT.");
35+}
36+return {
37+header: parts[0],
38+payload: decodeBase64UrlJson(parts[1]),
39+signature: parts[2] ?? "",
40+};
41+}
42+43+function stringifyJwt(parts: JwtParts): string {
44+return [parts.header, encodeBase64UrlJson(parts.payload), parts.signature].join(".");
45+}
46+47+export function patchCodexAuthForCi(auth: CodexAuthJson): {
48+ auth: CodexAuthJson;
49+ changed: boolean;
50+} {
51+const tokens = auth.tokens;
52+if (!tokens) {
53+return { auth, changed: false };
54+}
55+const accountId = typeof tokens.account_id === "string" ? tokens.account_id.trim() : "";
56+const idToken = typeof tokens.id_token === "string" ? tokens.id_token.trim() : "";
57+if (!accountId || !idToken) {
58+return { auth, changed: false };
59+}
60+61+const jwt = parseJwt(idToken);
62+if (typeof jwt.payload.chatgpt_account_id === "string" && jwt.payload.chatgpt_account_id) {
63+return { auth, changed: false };
64+}
65+66+return {
67+auth: {
68+ ...auth,
69+tokens: {
70+ ...tokens,
71+// Newer Codex app-server builds read ChatGPT account metadata from
72+// id_token claims. Older local auth files can have the same value only
73+// at tokens.account_id, so patch the staged Docker copy for CI.
74+id_token: stringifyJwt({
75+ ...jwt,
76+payload: {
77+ ...jwt.payload,
78+chatgpt_account_id: accountId,
79+},
80+}),
81+},
82+},
83+changed: true,
84+};
85+}
86+87+export async function prepareCodexCiAuth(authPath: string): Promise<boolean> {
88+ const raw = await fs.readFile(authPath, "utf-8");
89+const parsed = JSON.parse(raw) as CodexAuthJson;
90+const { auth, changed } = patchCodexAuthForCi(parsed);
91+if (!changed) {
92+return false;
93+}
94+const stat = await fs.stat(authPath);
95+await fs.writeFile(authPath, `${JSON.stringify(auth, null, 2)}\n`, "utf-8");
96+await fs.chmod(authPath, stat.mode);
97+return true;
98+}
99+100+if (path.basename(process.argv[1] ?? "") === "prepare-codex-ci-auth.ts") {
101+const authPath = process.argv[2];
102+if (!authPath) {
103+throw new Error("Usage: node --import tsx scripts/prepare-codex-ci-auth.ts <auth-json-path>");
104+}
105+const changed = await prepareCodexCiAuth(authPath);
106+if (changed) {
107+console.error("Prepared staged Codex auth metadata for CI.");
108+}
109+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。