@nema-project/nema 0.0.0-stage → 0.1.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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunks/chunk-HEIQINX3.js +825 -0
  3. package/dist/chunks/chunk-WT3XUFWU.js +86 -0
  4. package/dist/claude/adapter.d.ts +19 -0
  5. package/dist/claude/adapter.js +258 -0
  6. package/dist/claude/events.d.ts +29 -0
  7. package/dist/claude/events.js +75 -0
  8. package/dist/claude/launch.d.ts +22 -0
  9. package/dist/claude/launch.js +80 -0
  10. package/dist/claude/token.d.ts +9 -0
  11. package/dist/claude/token.js +87 -0
  12. package/dist/codex/adapter.d.ts +17 -0
  13. package/dist/codex/adapter.js +151 -0
  14. package/dist/codex/events.d.ts +16 -0
  15. package/dist/codex/events.js +47 -0
  16. package/dist/codex/isolation.d.ts +33 -0
  17. package/dist/codex/isolation.js +115 -0
  18. package/dist/config.d.ts +29 -0
  19. package/dist/config.js +75 -0
  20. package/dist/configCommands.d.ts +13 -0
  21. package/dist/configCommands.js +69 -0
  22. package/dist/daemon/cleanup.d.ts +5 -0
  23. package/dist/daemon/cleanup.js +49 -0
  24. package/dist/daemon/connection.d.ts +22 -0
  25. package/dist/daemon/connection.js +166 -0
  26. package/dist/daemon/rpc.d.ts +14 -0
  27. package/dist/daemon/rpc.js +20 -0
  28. package/dist/daemon/runner.d.ts +34 -0
  29. package/dist/daemon/runner.js +94 -0
  30. package/dist/daemon/setup.d.ts +39 -0
  31. package/dist/daemon/setup.js +95 -0
  32. package/dist/direct/adapter.d.ts +30 -0
  33. package/dist/direct/adapter.js +190 -0
  34. package/dist/direct/authCommands.d.ts +54 -0
  35. package/dist/direct/authCommands.js +268 -0
  36. package/dist/direct/authStore.d.ts +120 -0
  37. package/dist/direct/authStore.js +351 -0
  38. package/dist/direct/byok.d.ts +27 -0
  39. package/dist/direct/byok.js +67 -0
  40. package/dist/direct/codexLogin.d.ts +34 -0
  41. package/dist/direct/codexLogin.js +93 -0
  42. package/dist/direct/jwt.d.ts +4 -0
  43. package/dist/direct/jwt.js +22 -0
  44. package/dist/direct/lock.d.ts +16 -0
  45. package/dist/direct/lock.js +134 -0
  46. package/dist/direct/loop.d.ts +33 -0
  47. package/dist/direct/loop.js +110 -0
  48. package/dist/direct/siwc.d.ts +63 -0
  49. package/dist/direct/siwc.js +403 -0
  50. package/dist/direct/tools.d.ts +17 -0
  51. package/dist/direct/tools.js +104 -0
  52. package/dist/direct/transport.d.ts +177 -0
  53. package/dist/direct/transport.js +437 -0
  54. package/dist/doctor.d.ts +28 -0
  55. package/dist/doctor.js +178 -0
  56. package/dist/extensions/handlers.d.ts +2 -0
  57. package/dist/extensions/handlers.js +10 -0
  58. package/dist/extensions/store.d.ts +11 -0
  59. package/dist/extensions/store.js +132 -0
  60. package/dist/github/client.d.ts +47 -0
  61. package/dist/github/client.js +383 -0
  62. package/dist/github/gh.d.ts +56 -0
  63. package/dist/github/gh.js +385 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +144 -0
  66. package/dist/localSettings.d.ts +8 -0
  67. package/dist/localSettings.js +26 -0
  68. package/dist/mcp/server.d.ts +5 -0
  69. package/dist/mcp/server.js +83 -0
  70. package/dist/ownedProcess.d.ts +39 -0
  71. package/dist/ownedProcess.js +229 -0
  72. package/dist/platform.d.ts +10 -0
  73. package/dist/platform.js +60 -0
  74. package/dist/report/commands.d.ts +41 -0
  75. package/dist/report/commands.js +159 -0
  76. package/dist/report/desktop.d.ts +3 -0
  77. package/dist/report/desktop.js +123 -0
  78. package/dist/report/git.d.ts +5 -0
  79. package/dist/report/git.js +42 -0
  80. package/dist/report/install.d.ts +22 -0
  81. package/dist/report/install.js +183 -0
  82. package/dist/report/send.d.ts +35 -0
  83. package/dist/report/send.js +208 -0
  84. package/dist/report/token.d.ts +5 -0
  85. package/dist/report/token.js +86 -0
  86. package/dist/repos/git.d.ts +48 -0
  87. package/dist/repos/git.js +370 -0
  88. package/dist/repos/graphify.d.ts +32 -0
  89. package/dist/repos/graphify.js +292 -0
  90. package/dist/repos/handlers.d.ts +20 -0
  91. package/dist/repos/handlers.js +255 -0
  92. package/dist/repos/indexState.d.ts +10 -0
  93. package/dist/repos/indexState.js +45 -0
  94. package/dist/repos/pickDirectory.d.ts +11 -0
  95. package/dist/repos/pickDirectory.js +140 -0
  96. package/dist/repos/reportCheck.d.ts +13 -0
  97. package/dist/repos/reportCheck.js +268 -0
  98. package/dist/repos/watcher.d.ts +64 -0
  99. package/dist/repos/watcher.js +202 -0
  100. package/dist/room/client.d.ts +26 -0
  101. package/dist/room/client.js +76 -0
  102. package/dist/secrets/file.d.ts +12 -0
  103. package/dist/secrets/file.js +89 -0
  104. package/dist/secrets/github.d.ts +10 -0
  105. package/dist/secrets/github.js +29 -0
  106. package/dist/secrets/permissions.d.ts +7 -0
  107. package/dist/secrets/permissions.js +86 -0
  108. package/dist/systemGit.d.ts +14 -0
  109. package/dist/systemGit.js +75 -0
  110. package/dist/wp/commands.d.ts +15 -0
  111. package/dist/wp/commands.js +103 -0
  112. package/package.json +26 -4
  113. package/src/report/SKILL.md +21 -0
  114. package/README.md +0 -3
@@ -0,0 +1,351 @@
1
+ // direct/authStore.js
2
+ import { randomUUID } from "node:crypto";
3
+ import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ import { jwtExp } from "./jwt.js";
7
+ import { LockTimeoutError, withFileLock } from "./lock.js";
8
+ import { openPrivateFile, privateModeMatches, protectPrivatePath } from "../secrets/permissions.js";
9
+ var REFRESH_SKEW_MS = 12e4;
10
+ var TERMINAL_CODES = [
11
+ "invalid_grant",
12
+ "invalid_client",
13
+ "invalid_token",
14
+ "refresh_token_reused",
15
+ "refresh_token_expired",
16
+ "refresh_token_invalidated"
17
+ ];
18
+ var RELOGIN_MESSAGE = "\u76F4\u8FDE\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u5728\u7EC8\u7AEF\u8FD0\u884C nema auth login";
19
+ var UNUSABLE_FILE_MESSAGE = "\u51ED\u8BC1\u6587\u4EF6\u4E0D\u53EF\u7528\uFF08\u6743\u9650/\u7B26\u53F7\u94FE\u63A5/\u635F\u574F\uFF09\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C nema auth login";
20
+ var StoredAuthSchema = z.object({
21
+ version: z.literal(1),
22
+ mode: z.enum(["siwc", "compat"]),
23
+ clientId: z.string(),
24
+ tokens: z.object({ access_token: z.string(), refresh_token: z.string(), id_token: z.string().optional() }),
25
+ accountId: z.string().optional(),
26
+ lastRefresh: z.string(),
27
+ lastAuthError: z.object({ code: z.string(), message: z.string(), at: z.string() }).optional()
28
+ });
29
+ var REFRESH_TIMEOUT_MS = 2e4;
30
+ function isTrustedAuthStat(st, uid = process.getuid?.() ?? null, platform = process.platform) {
31
+ return st.isFile() && privateModeMatches(st, platform) && (uid === null || st.uid === uid);
32
+ }
33
+ var RefreshError = class extends Error {
34
+ status;
35
+ code;
36
+ retryAfterS;
37
+ constructor(status, opts = {}) {
38
+ super(opts.message ?? `Token refresh failed: HTTP ${status}${opts.code ? ` (${opts.code})` : ""}`);
39
+ this.status = status;
40
+ this.name = "RefreshError";
41
+ this.code = opts.code;
42
+ this.retryAfterS = opts.retryAfterS;
43
+ }
44
+ };
45
+ var ReloginRequiredError = class extends Error {
46
+ code;
47
+ constructor(code, options) {
48
+ super(RELOGIN_MESSAGE, options);
49
+ this.code = code;
50
+ this.name = "ReloginRequiredError";
51
+ }
52
+ };
53
+ var AuthRateLimitedError = class extends Error {
54
+ retryAfterS;
55
+ constructor(retryAfterS, options) {
56
+ super(`\u76F4\u8FDE\u767B\u5F55\u5237\u65B0\u88AB\u9650\u6D41${retryAfterS !== void 0 ? `\uFF0C\u8BF7\u7EA6 ${retryAfterS} \u79D2\u540E\u91CD\u8BD5` : "\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"}`, options);
57
+ this.retryAfterS = retryAfterS;
58
+ this.name = "AuthRateLimitedError";
59
+ }
60
+ };
61
+ var TransientAuthError = class extends Error {
62
+ constructor(message, options) {
63
+ super(message, options);
64
+ this.name = "TransientAuthError";
65
+ }
66
+ };
67
+ var result = (auth) => ({ accessToken: auth.tokens.access_token, auth });
68
+ var hasRefreshToken = (a) => !!a && a.tokens.refresh_token !== "";
69
+ var errMessage = (e) => e instanceof Error ? e.message : String(e);
70
+ var AuthStore = class {
71
+ nemaHome;
72
+ refresher;
73
+ now;
74
+ opts;
75
+ file;
76
+ lockPath;
77
+ constructor(nemaHome, refresher, now = Date.now, opts = {}) {
78
+ this.nemaHome = nemaHome;
79
+ this.refresher = refresher;
80
+ this.now = now;
81
+ this.opts = opts;
82
+ this.file = path.join(nemaHome, "auth.json");
83
+ this.lockPath = path.join(nemaHome, "auth.lock");
84
+ }
85
+ /** The stored login, or null if missing, a symlink, not a regular file, not 0600, not ours, or malformed. */
86
+ read() {
87
+ return this.inspect().auth;
88
+ }
89
+ /** Like read(), but says why there is no login: "missing" (no file) or "unusable" (present but untrusted/corrupt). */
90
+ inspect() {
91
+ try {
92
+ if (!isTrustedAuthStat(lstatSync(this.file)))
93
+ return { auth: null, reason: "unusable" };
94
+ const fd = openPrivateFile(this.file);
95
+ let raw;
96
+ try {
97
+ if (!isTrustedAuthStat(fstatSync(fd)))
98
+ return { auth: null, reason: "unusable" };
99
+ raw = readFileSync(fd, "utf8");
100
+ } finally {
101
+ closeSync(fd);
102
+ }
103
+ const parsed = StoredAuthSchema.safeParse(JSON.parse(raw));
104
+ return parsed.success ? { auth: parsed.data } : { auth: null, reason: "unusable" };
105
+ } catch (e) {
106
+ return { auth: null, reason: e.code === "ENOENT" ? "missing" : "unusable" };
107
+ }
108
+ }
109
+ /**
110
+ * Records an auth problem in the file WITHOUT clearing the tokens (used when the model API rejects a token
111
+ * that refreshing could not fix). Best effort: only if the file still holds the same refresh token.
112
+ */
113
+ async recordAuthError(reason, forAuth) {
114
+ try {
115
+ await withFileLock(this.lockPath, async () => {
116
+ const cur = this.read();
117
+ if (!cur || cur.tokens.refresh_token !== forAuth.tokens.refresh_token)
118
+ return;
119
+ this.save({ ...cur, lastAuthError: { code: reason.code, message: reason.message, at: new Date(this.now()).toISOString() } });
120
+ }, { timeoutMs: 5e3, ...this.opts.lock });
121
+ } catch {
122
+ }
123
+ }
124
+ /** Atomic write (exclusive 0600 temp file + fsync + rename) under a 0700 directory. */
125
+ save(auth) {
126
+ mkdirSync(this.nemaHome, { recursive: true, mode: 448 });
127
+ protectPrivatePath(this.nemaHome, 448);
128
+ const tmp = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
129
+ const fd = openSync(tmp, "wx", 384);
130
+ try {
131
+ try {
132
+ writeSync(fd, JSON.stringify(auth, null, 2) + "\n");
133
+ fsyncSync(fd);
134
+ } finally {
135
+ closeSync(fd);
136
+ }
137
+ protectPrivatePath(tmp, 384);
138
+ renameSync(tmp, this.file);
139
+ } catch (e) {
140
+ try {
141
+ unlinkSync(tmp);
142
+ } catch {
143
+ }
144
+ throw e;
145
+ }
146
+ try {
147
+ const dfd = openSync(this.nemaHome, "r");
148
+ try {
149
+ fsyncSync(dfd);
150
+ } finally {
151
+ closeSync(dfd);
152
+ }
153
+ } catch {
154
+ }
155
+ }
156
+ /** Without a reason: log out (delete the file). With a reason: drop the tokens and record lastAuthError. */
157
+ clear(reason) {
158
+ if (!reason) {
159
+ try {
160
+ unlinkSync(this.file);
161
+ } catch (e) {
162
+ if (e.code !== "ENOENT")
163
+ throw e;
164
+ }
165
+ return;
166
+ }
167
+ const base = this.read();
168
+ if (base)
169
+ this.save(this.cleared(base, reason));
170
+ }
171
+ /**
172
+ * A valid access token, refreshing under auth.lock when it is within 120 s of expiry or when forced.
173
+ * Pass the token the server just rejected as `rejectedAccessToken` with `force`: if a peer has already
174
+ * replaced it on disk, that token is returned without a refresh POST.
175
+ */
176
+ async getAccessToken(opts = {}) {
177
+ opts.signal?.throwIfAborted();
178
+ const held = this.read();
179
+ if (!hasRefreshToken(held))
180
+ throw new ReloginRequiredError();
181
+ const req = { force: !!opts.force, rejected: opts.rejectedAccessToken };
182
+ if (this.usableWithoutRefresh(held, held, req))
183
+ return result(held);
184
+ const refreshTimeoutMs = this.opts.refreshTimeoutMs ?? REFRESH_TIMEOUT_MS;
185
+ const lockOpts = { timeoutMs: Math.max(15e3, refreshTimeoutMs + 5e3), ...this.opts.lock };
186
+ try {
187
+ return await withFileLock(this.lockPath, () => this.refreshLocked(held, req, opts.signal), lockOpts);
188
+ } catch (e) {
189
+ if (e instanceof LockTimeoutError)
190
+ throw new TransientAuthError("\u7B49\u5F85\u76F4\u8FDE\u767B\u5F55\u51ED\u8BC1\u9501\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", { cause: e });
191
+ throw e;
192
+ }
193
+ }
194
+ expiring(auth) {
195
+ const exp = jwtExp(auth.tokens.access_token);
196
+ return exp === null || exp * 1e3 - this.now() < REFRESH_SKEW_MS;
197
+ }
198
+ // The normal refresh decision: an expiring token is never usable; otherwise usable unless forced, and a
199
+ // forced call still takes a token that a peer has rotated in (new refresh token or a different access token
200
+ // than the one rejected).
201
+ usableWithoutRefresh(auth, held, req) {
202
+ if (this.expiring(auth))
203
+ return false;
204
+ if (!req.force)
205
+ return true;
206
+ if (auth.tokens.refresh_token !== held.tokens.refresh_token)
207
+ return true;
208
+ return req.rejected !== void 0 && auth.tokens.access_token !== req.rejected;
209
+ }
210
+ cleared(base, reason) {
211
+ return {
212
+ ...base,
213
+ tokens: { access_token: "", refresh_token: "" },
214
+ lastAuthError: { code: reason.code, message: reason.message, at: new Date(this.now()).toISOString() }
215
+ };
216
+ }
217
+ // Runs with auth.lock held: re-read, adopt a peer's rotation, else refresh and persist before unlocking.
218
+ async refreshLocked(held, req, signal) {
219
+ signal?.throwIfAborted();
220
+ const disk = this.read();
221
+ if (!hasRefreshToken(disk))
222
+ throw new ReloginRequiredError();
223
+ if (this.usableWithoutRefresh(disk, held, req))
224
+ return result(disk);
225
+ return this.refreshWith(disk, 1, signal);
226
+ }
227
+ async callRefresher(auth, ownerSignal) {
228
+ const ms = this.opts.refreshTimeoutMs ?? REFRESH_TIMEOUT_MS;
229
+ const ac = new AbortController();
230
+ let timer;
231
+ const deadline = new Promise((_, reject) => {
232
+ timer = setTimeout(() => {
233
+ ac.abort();
234
+ reject(new TransientAuthError(`\u5237\u65B0\u76F4\u8FDE\u767B\u5F55\u8D85\u65F6\uFF08${Math.round(ms / 1e3)} \u79D2\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`));
235
+ }, ms);
236
+ });
237
+ try {
238
+ if (ownerSignal?.aborted)
239
+ ac.abort(ownerSignal.reason);
240
+ const abortFromOwner = () => ac.abort(ownerSignal?.reason);
241
+ ownerSignal?.addEventListener("abort", abortFromOwner, { once: true });
242
+ try {
243
+ return await Promise.race([this.refresher(auth, ac.signal), deadline]);
244
+ } finally {
245
+ ownerSignal?.removeEventListener("abort", abortFromOwner);
246
+ }
247
+ } finally {
248
+ clearTimeout(timer);
249
+ }
250
+ }
251
+ async refreshWith(disk, retries, signal) {
252
+ let res;
253
+ try {
254
+ res = await this.callRefresher(disk, signal);
255
+ } catch (e) {
256
+ if (signal?.aborted)
257
+ throw signal.reason ?? e;
258
+ return this.onRefreshFailure(disk, e, retries);
259
+ }
260
+ if (!res || typeof res.access_token !== "string" || res.access_token === "") {
261
+ throw new TransientAuthError("\u5237\u65B0\u54CD\u5E94\u7F3A\u5C11 access_token");
262
+ }
263
+ const idToken = res.id_token || disk.tokens.id_token;
264
+ const next = {
265
+ ...disk,
266
+ tokens: {
267
+ access_token: res.access_token,
268
+ refresh_token: res.refresh_token || disk.tokens.refresh_token,
269
+ ...idToken ? { id_token: idToken } : {}
270
+ },
271
+ lastRefresh: new Date(this.now()).toISOString()
272
+ };
273
+ delete next.lastAuthError;
274
+ try {
275
+ this.save(next);
276
+ } catch (e) {
277
+ const current = this.read();
278
+ if (current && current.tokens.refresh_token === disk.tokens.refresh_token) {
279
+ this.overwriteInPlace(this.cleared(current, { code: "credential_persist_failed", message: errMessage(e) }));
280
+ }
281
+ throw new ReloginRequiredError("credential_persist_failed", { cause: e });
282
+ }
283
+ signal?.throwIfAborted();
284
+ return result(next);
285
+ }
286
+ async onRefreshFailure(failed, e, retries) {
287
+ if (e instanceof AuthRateLimitedError || e instanceof TransientAuthError)
288
+ throw e;
289
+ if (e instanceof RefreshError && e.status === 429)
290
+ throw new AuthRateLimitedError(e.retryAfterS, { cause: e });
291
+ const terminal = e instanceof ReloginRequiredError || e instanceof RefreshError && (e.status === 401 || e.status === 403 || TERMINAL_CODES.includes(e.code ?? ""));
292
+ if (!terminal) {
293
+ if (e instanceof RefreshError) {
294
+ throw new TransientAuthError(`\u5237\u65B0\u76F4\u8FDE\u767B\u5F55\u5931\u8D25\uFF08HTTP ${e.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`, { cause: e });
295
+ }
296
+ throw new TransientAuthError(`\u5237\u65B0\u76F4\u8FDE\u767B\u5F55\u5931\u8D25\uFF1A${errMessage(e)}`, { cause: e });
297
+ }
298
+ const code = e instanceof ReloginRequiredError ? e.code ?? "relogin_required" : `${e.code ?? `http_${e.status}`}`;
299
+ const relogin = e instanceof ReloginRequiredError ? e : new ReloginRequiredError(code, { cause: e });
300
+ const current = this.read();
301
+ if (hasRefreshToken(current) && current.tokens.refresh_token !== failed.tokens.refresh_token) {
302
+ if (!this.expiring(current))
303
+ return result(current);
304
+ if (retries > 0)
305
+ return this.refreshWith(current, retries - 1);
306
+ throw relogin;
307
+ }
308
+ if (current && current.tokens.refresh_token === failed.tokens.refresh_token) {
309
+ const next = this.cleared(current, { code, message: errMessage(e) });
310
+ try {
311
+ this.save(next);
312
+ } catch {
313
+ this.overwriteInPlace(next);
314
+ }
315
+ }
316
+ throw relogin;
317
+ }
318
+ // Last-resort, non-atomic write into the existing regular file (no symlink following). Best effort.
319
+ overwriteInPlace(auth) {
320
+ try {
321
+ const before = lstatSync(this.file);
322
+ if (!before.isFile() || before.isSymbolicLink())
323
+ return;
324
+ const fd = openSync(this.file, constants.O_WRONLY | constants.O_NOFOLLOW);
325
+ try {
326
+ const opened = fstatSync(fd);
327
+ if (!isTrustedAuthStat(opened) || opened.dev !== before.dev || opened.ino !== before.ino)
328
+ return;
329
+ ftruncateSync(fd, 0);
330
+ writeSync(fd, JSON.stringify(auth, null, 2) + "\n");
331
+ fsyncSync(fd);
332
+ } finally {
333
+ closeSync(fd);
334
+ }
335
+ } catch {
336
+ }
337
+ }
338
+ };
339
+ export {
340
+ AuthRateLimitedError,
341
+ AuthStore,
342
+ REFRESH_SKEW_MS,
343
+ REFRESH_TIMEOUT_MS,
344
+ RELOGIN_MESSAGE,
345
+ RefreshError,
346
+ ReloginRequiredError,
347
+ TERMINAL_CODES,
348
+ TransientAuthError,
349
+ UNUSABLE_FILE_MESSAGE,
350
+ isTrustedAuthStat
351
+ };
@@ -0,0 +1,27 @@
1
+ import type { Out } from "./authCommands.js";
2
+ import type { StoredAuth } from "./authStore.js";
3
+ import type { TokenSource } from "./transport.js";
4
+ export declare const byokKeyPath: (nemaHome: string) => string;
5
+ export declare function saveByokKey(nemaHome: string, key: string): void;
6
+ export declare function readByokKey(nemaHome: string): string;
7
+ export declare class ByokTokenSource implements TokenSource {
8
+ private readonly nemaHome;
9
+ constructor(nemaHome: string);
10
+ getAccessToken(_opts?: {
11
+ force?: boolean;
12
+ rejectedAccessToken?: string;
13
+ signal?: AbortSignal;
14
+ }): Promise<{
15
+ accessToken: string;
16
+ auth: StoredAuth;
17
+ }>;
18
+ recordAuthError(_reason: {
19
+ code: string;
20
+ message: string;
21
+ }, _forAuth: StoredAuth): Promise<void>;
22
+ }
23
+ export declare function authByok(o: {
24
+ nemaHome: string;
25
+ out: Out;
26
+ stdin: NodeJS.ReadableStream;
27
+ }): Promise<number>;
@@ -0,0 +1,67 @@
1
+ // direct/byok.js
2
+ import path from "node:path";
3
+ import { readSecretLine, readStdinLine, saveSecretLine, SecretFileError } from "../secrets/file.js";
4
+ var INVALID_KEY = "key \u5FC5\u987B\u662F\u4E00\u884C\u975E\u7A7A\u6587\u672C";
5
+ var SYMLINK_KEY = "BYOK key \u6587\u4EF6\u662F\u7B26\u53F7\u94FE\u63A5\uFF0C\u5DF2\u62D2\u7EDD\u3002";
6
+ var MISSING_KEY = "\u6CA1\u6709 BYOK API key\u3002\u8BF7\u628A\u94A5\u5319\u4ECE\u6807\u51C6\u8F93\u5165\u4EA4\u7ED9 nema auth byok\u3002";
7
+ var KEY_MODE = "BYOK key \u6587\u4EF6\u6743\u9650\u5FC5\u987B\u662F 0600";
8
+ var INVALID_STORED_KEY = "BYOK key \u6587\u4EF6\u5185\u5BB9\u65E0\u6548\u3002\u8BF7\u91CD\u65B0\u628A\u94A5\u5319\u4ECE\u6807\u51C6\u8F93\u5165\u4EA4\u7ED9 nema auth byok\u3002";
9
+ function byokError(e) {
10
+ if (!(e instanceof SecretFileError))
11
+ return e;
12
+ return new Error(e.kind === "invalid" ? INVALID_KEY : e.kind === "symlink" ? SYMLINK_KEY : KEY_MODE);
13
+ }
14
+ var byokKeyPath = (nemaHome) => path.join(nemaHome, "byok-key");
15
+ function saveByokKey(nemaHome, key) {
16
+ try {
17
+ saveSecretLine(byokKeyPath(nemaHome), key);
18
+ } catch (e) {
19
+ throw byokError(e);
20
+ }
21
+ }
22
+ function readByokKey(nemaHome) {
23
+ try {
24
+ return readSecretLine(byokKeyPath(nemaHome), MISSING_KEY, INVALID_STORED_KEY);
25
+ } catch (e) {
26
+ throw byokError(e);
27
+ }
28
+ }
29
+ var ByokTokenSource = class {
30
+ nemaHome;
31
+ constructor(nemaHome) {
32
+ this.nemaHome = nemaHome;
33
+ }
34
+ async getAccessToken(_opts = {}) {
35
+ const accessToken = readByokKey(this.nemaHome);
36
+ return {
37
+ accessToken,
38
+ auth: {
39
+ version: 1,
40
+ mode: "byok",
41
+ clientId: "",
42
+ tokens: { access_token: accessToken, refresh_token: "" },
43
+ lastRefresh: ""
44
+ }
45
+ };
46
+ }
47
+ async recordAuthError(_reason, _forAuth) {
48
+ }
49
+ };
50
+ async function authByok(o) {
51
+ const key = await readStdinLine(o.stdin);
52
+ try {
53
+ saveByokKey(o.nemaHome, key);
54
+ o.out.log("\u5DF2\u4FDD\u5B58 BYOK API key");
55
+ return 0;
56
+ } catch (e) {
57
+ o.out.error(e instanceof Error ? e.message : String(e));
58
+ return 1;
59
+ }
60
+ }
61
+ export {
62
+ ByokTokenSource,
63
+ authByok,
64
+ byokKeyPath,
65
+ readByokKey,
66
+ saveByokKey
67
+ };
@@ -0,0 +1,34 @@
1
+ import type { StoredAuth } from "./authStore.js";
2
+ export declare const CODEX_AUTH_CLAIM = "https://api.openai.com/auth";
3
+ export declare class CodexLoginError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export declare function defaultCodexHome(env?: NodeJS.ProcessEnv): string;
7
+ /** Account headers Hermes sends on chatgpt.com/backend-api/codex. A malformed token yields none. */
8
+ export declare function codexAccountHeaders(accessToken: string): Record<string, string>;
9
+ export type CodexCliTokens = {
10
+ accessToken: string;
11
+ refreshToken: string;
12
+ accountId?: string;
13
+ exp: number | null;
14
+ };
15
+ /** Read the Codex CLI login. Throws CodexLoginError when it cannot be used. Does not write the file. */
16
+ export declare function readCodexCliTokens(codexHome: string, now?: number): CodexCliTokens;
17
+ /**
18
+ * Token source for authMode=compat. Re-reads the Codex file on every call, including after a 401,
19
+ * so a token Codex itself rotated is picked up. Never refreshes and never writes the file.
20
+ */
21
+ export declare class CodexFileTokenSource {
22
+ private readonly codexHome;
23
+ private readonly now;
24
+ constructor(codexHome: string, now?: () => number);
25
+ getAccessToken(opts?: {
26
+ force?: boolean;
27
+ rejectedAccessToken?: string;
28
+ signal?: AbortSignal;
29
+ }): Promise<{
30
+ accessToken: string;
31
+ auth: StoredAuth;
32
+ }>;
33
+ recordAuthError(): Promise<void>;
34
+ }
@@ -0,0 +1,93 @@
1
+ // direct/codexLogin.js
2
+ import { readFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { checkCodexAuth } from "../codex/isolation.js";
6
+ import { decodeJwtPayload, jwtExp } from "./jwt.js";
7
+ var CODEX_AUTH_CLAIM = "https://api.openai.com/auth";
8
+ var CodexLoginError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "CodexLoginError";
12
+ }
13
+ };
14
+ function defaultCodexHome(env = process.env) {
15
+ const raw = env.CODEX_HOME?.trim();
16
+ return raw ? raw : path.join(os.homedir(), ".codex");
17
+ }
18
+ function authClaim(accessToken) {
19
+ try {
20
+ const claim = decodeJwtPayload(accessToken)[CODEX_AUTH_CLAIM];
21
+ return claim && typeof claim === "object" && !Array.isArray(claim) ? claim : {};
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ function codexAccountHeaders(accessToken) {
27
+ const headers = {};
28
+ const claim = authClaim(accessToken);
29
+ if (typeof claim.chatgpt_account_id === "string" && claim.chatgpt_account_id !== "") {
30
+ headers["ChatGPT-Account-ID"] = claim.chatgpt_account_id;
31
+ }
32
+ const residency = claim.chatgpt_data_residency ?? claim.chatgpt_compute_residency;
33
+ if (typeof residency === "string" && residency.trim() !== "")
34
+ headers["x-openai-internal-codex-residency"] = residency.trim();
35
+ return headers;
36
+ }
37
+ function readCodexCliTokens(codexHome, now = Date.now()) {
38
+ const checked = checkCodexAuth(codexHome);
39
+ if (!checked.ok)
40
+ throw new CodexLoginError(checked.error);
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(readFileSync(checked.authPath, "utf8"));
44
+ } catch (e) {
45
+ throw new CodexLoginError(`\u65E0\u6CD5\u8BFB\u53D6 Codex \u767B\u5F55\u6587\u4EF6 ${checked.authPath}\uFF1A${e instanceof Error ? e.message : String(e)}`);
46
+ }
47
+ const tokens = parsed && typeof parsed === "object" ? parsed.tokens : void 0;
48
+ const access = tokens && typeof tokens === "object" ? tokens.access_token : void 0;
49
+ const refresh = tokens && typeof tokens === "object" ? tokens.refresh_token : void 0;
50
+ if (typeof access !== "string" || access === "" || typeof refresh !== "string" || refresh === "") {
51
+ throw new CodexLoginError(`Codex \u767B\u5F55\u6587\u4EF6 ${checked.authPath} \u91CC\u6CA1\u6709\u53EF\u7528\u7684 access_token \u548C refresh_token\u3002\u8BF7\u91CD\u65B0\u8FD0\u884C codex login\u3002`);
52
+ }
53
+ const exp = jwtExp(access);
54
+ if (exp !== null && exp * 1e3 <= now) {
55
+ throw new CodexLoginError(`Codex \u7684 access token \u5DF2\u8FC7\u671F\uFF08${checked.authPath}\uFF09\u3002\u8BF7\u6253\u5F00\u4E00\u6B21 Codex \u8BA9\u5B83\u5237\u65B0\u767B\u5F55\u3002Nema \u53EA\u8BFB\u53D6\u8BE5\u6587\u4EF6\uFF0C\u4E0D\u4F1A\u5237\u65B0\uFF0C\u4EE5\u514D\u4F5C\u5E9F Codex \u81EA\u5DF1\u7684\u767B\u5F55\u3002`);
56
+ }
57
+ const accountId = authClaim(access).chatgpt_account_id;
58
+ return { accessToken: access, refreshToken: refresh, accountId: typeof accountId === "string" && accountId !== "" ? accountId : void 0, exp };
59
+ }
60
+ var CodexFileTokenSource = class {
61
+ codexHome;
62
+ now;
63
+ constructor(codexHome, now = Date.now) {
64
+ this.codexHome = codexHome;
65
+ this.now = now;
66
+ }
67
+ async getAccessToken(opts = {}) {
68
+ opts.signal?.throwIfAborted();
69
+ const t = readCodexCliTokens(this.codexHome, this.now());
70
+ if (opts.rejectedAccessToken !== void 0 && t.accessToken === opts.rejectedAccessToken) {
71
+ throw new CodexLoginError("\u6A21\u578B\u63A5\u53E3\u62D2\u7EDD\u4E86\u5F53\u524D\u7684 Codex \u767B\u5F55\u3002Nema \u4E0D\u4F1A\u5237\u65B0 ~/.codex/auth.json\u3002\u8BF7\u5728 Codex \u91CC\u91CD\u65B0\u767B\u5F55\u540E\u518D\u8BD5\u3002");
72
+ }
73
+ const auth = {
74
+ version: 1,
75
+ mode: "compat",
76
+ clientId: "codex-cli-file",
77
+ tokens: { access_token: t.accessToken, refresh_token: t.refreshToken },
78
+ accountId: t.accountId,
79
+ lastRefresh: new Date(this.now()).toISOString()
80
+ };
81
+ return { accessToken: t.accessToken, auth };
82
+ }
83
+ async recordAuthError() {
84
+ }
85
+ };
86
+ export {
87
+ CODEX_AUTH_CLAIM,
88
+ CodexFileTokenSource,
89
+ CodexLoginError,
90
+ codexAccountHeaders,
91
+ defaultCodexHome,
92
+ readCodexCliTokens
93
+ };
@@ -0,0 +1,4 @@
1
+ /** Decode a JWT payload without verifying the signature. Throws on malformed input. */
2
+ export declare function decodeJwtPayload(token: string): Record<string, unknown>;
3
+ /** The `exp` claim in seconds since the epoch, or null when absent or undecodable. */
4
+ export declare function jwtExp(token: string): number | null;
@@ -0,0 +1,22 @@
1
+ // direct/jwt.js
2
+ function decodeJwtPayload(token) {
3
+ const parts = token.split(".");
4
+ if (parts.length !== 3 || !parts[1])
5
+ throw new Error("Malformed JWT");
6
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
7
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
8
+ throw new Error("Malformed JWT payload");
9
+ return payload;
10
+ }
11
+ function jwtExp(token) {
12
+ try {
13
+ const exp = decodeJwtPayload(token).exp;
14
+ return typeof exp === "number" && Number.isFinite(exp) ? exp : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ export {
20
+ decodeJwtPayload,
21
+ jwtExp
22
+ };
@@ -0,0 +1,16 @@
1
+ export declare class LockTimeoutError extends Error {
2
+ readonly lockPath: string;
3
+ readonly timeoutMs: number;
4
+ constructor(lockPath: string, timeoutMs: number);
5
+ }
6
+ export type LockOptions = {
7
+ timeoutMs?: number;
8
+ pollMs?: number;
9
+ staleMs?: number;
10
+ };
11
+ /**
12
+ * Cross-process mutual exclusion via an exclusively created lock file holding `{pid, at, token}`.
13
+ * A lock whose holder pid is gone is taken over. `staleMs` controls the heartbeat interval and
14
+ * age-based recovery of malformed partial lock records; it never overrides a live holder pid.
15
+ */
16
+ export declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, opts?: LockOptions): Promise<T>;