opencode-anthropic-fix 0.0.25 → 0.0.27
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/LICENSE +648 -21
- package/README.md +41 -5
- package/index.mjs +4040 -2838
- package/lib/accounts.mjs +69 -11
- package/lib/accounts.test.mjs +22 -22
- package/lib/backoff.mjs +25 -0
- package/lib/config.mjs +66 -1
- package/lib/oauth.mjs +283 -77
- package/lib/oauth.test.mjs +120 -3
- package/lib/rotation.mjs +21 -1
- package/lib/rotation.test.mjs +3 -3
- package/lib/storage.mjs +34 -6
- package/lib/storage.test.mjs +14 -7
- package/package.json +3 -2
package/lib/storage.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
5
5
|
import { getConfigDir } from "./config.mjs";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -153,7 +153,11 @@ function validateAccount(raw, now) {
|
|
|
153
153
|
|
|
154
154
|
const addedAt = typeof acc.addedAt === "number" && Number.isFinite(acc.addedAt) ? acc.addedAt : now;
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
// QA fix M12: hash token fragment instead of leaking raw prefix
|
|
157
|
+
const id =
|
|
158
|
+
typeof acc.id === "string" && acc.id
|
|
159
|
+
? acc.id
|
|
160
|
+
: `${addedAt}:${createHash("sha256").update(acc.refreshToken).digest("hex").slice(0, 12)}`;
|
|
157
161
|
|
|
158
162
|
return {
|
|
159
163
|
id,
|
|
@@ -198,8 +202,21 @@ export async function loadAccounts() {
|
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
if (data.version !== CURRENT_VERSION) {
|
|
201
|
-
//
|
|
202
|
-
|
|
205
|
+
// QA fix H12: warn about version mismatch but attempt best-effort load
|
|
206
|
+
// rather than silently discarding all accounts
|
|
207
|
+
if (typeof data.version === "number" && data.version > CURRENT_VERSION) {
|
|
208
|
+
// Forward-compat: newer versions likely only add fields
|
|
209
|
+
|
|
210
|
+
console.warn(
|
|
211
|
+
`[anthropic-auth] accounts file version ${data.version} is newer than expected ${CURRENT_VERSION}; loading with best effort`,
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
// Unknown/older version — still try to load
|
|
215
|
+
|
|
216
|
+
console.warn(
|
|
217
|
+
`[anthropic-auth] accounts file version mismatch (${data.version} vs ${CURRENT_VERSION}); attempting load`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
203
220
|
}
|
|
204
221
|
|
|
205
222
|
const now = Date.now();
|
|
@@ -225,7 +242,12 @@ export async function loadAccounts() {
|
|
|
225
242
|
} catch (error) {
|
|
226
243
|
const code = /** @type {NodeJS.ErrnoException} */ (error).code;
|
|
227
244
|
if (code === "ENOENT") return null;
|
|
228
|
-
|
|
245
|
+
// QA fix H11: only swallow ENOENT and JSON parse errors; throw for real I/O failures
|
|
246
|
+
if (error instanceof SyntaxError) {
|
|
247
|
+
// Corrupted JSON file — treat as empty
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
229
251
|
}
|
|
230
252
|
}
|
|
231
253
|
|
|
@@ -244,11 +266,13 @@ export async function saveAccounts(storage) {
|
|
|
244
266
|
/** @type {AccountStorage} */
|
|
245
267
|
let storageToWrite = storage;
|
|
246
268
|
|
|
269
|
+
// QA fix M15: Accept pre-loaded disk data to avoid double-read (TOCTOU mitigation).
|
|
270
|
+
// Callers can pass diskData from their own loadAccounts() call.
|
|
247
271
|
// Merge auth fields against disk by freshness to avoid stale-process clobber.
|
|
248
272
|
// We do not resurrect removed accounts; only merge for accounts present in
|
|
249
273
|
// the incoming storage payload.
|
|
250
274
|
try {
|
|
251
|
-
const disk = await loadAccounts();
|
|
275
|
+
const disk = arguments[1] ?? (await loadAccounts());
|
|
252
276
|
if (disk && storage.accounts.length > 0) {
|
|
253
277
|
const diskById = new Map(disk.accounts.map((a) => [a.id, a]));
|
|
254
278
|
/** @type {Map<number, AccountMetadata[]>} */
|
|
@@ -313,6 +337,10 @@ export async function saveAccounts(storage) {
|
|
|
313
337
|
const content = JSON.stringify(storageToWrite, null, 2);
|
|
314
338
|
|
|
315
339
|
try {
|
|
340
|
+
// QA note H9: mode 0o600 (owner-only read/write) is enforced on Linux/macOS.
|
|
341
|
+
// On Windows, POSIX mode is silently ignored by Node.js — ACLs govern permissions.
|
|
342
|
+
// Windows file inherits parent directory ACLs, which typically restrict access
|
|
343
|
+
// to the current user on per-user directories (%APPDATA%).
|
|
316
344
|
await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
317
345
|
await fs.rename(tempPath, storagePath);
|
|
318
346
|
} catch (error) {
|
package/lib/storage.test.mjs
CHANGED
|
@@ -27,11 +27,15 @@ vi.mock("node:fs", () => ({
|
|
|
27
27
|
},
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
vi.mock("node:crypto", () =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
vi.mock("node:crypto", async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal();
|
|
32
|
+
return {
|
|
33
|
+
randomBytes: vi.fn(() => ({
|
|
34
|
+
toString: () => "abcdef123456",
|
|
35
|
+
})),
|
|
36
|
+
createHash: actual.createHash,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
35
39
|
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
41
|
// deduplicateByRefreshToken
|
|
@@ -206,10 +210,13 @@ describe("loadAccounts", () => {
|
|
|
206
210
|
expect(result).toBeNull();
|
|
207
211
|
});
|
|
208
212
|
|
|
209
|
-
it("returns
|
|
213
|
+
it("returns best-effort data for newer version (forward compat)", async () => {
|
|
210
214
|
fs.readFile.mockResolvedValue(JSON.stringify({ version: 99, accounts: [] }));
|
|
211
215
|
const result = await loadAccounts();
|
|
212
|
-
|
|
216
|
+
// H12 fix: no longer returns null; attempts best-effort load with a warning
|
|
217
|
+
expect(result).not.toBeNull();
|
|
218
|
+
expect(result.accounts).toEqual([]);
|
|
219
|
+
expect(result.version).toBe(1); // normalized to current version
|
|
213
220
|
});
|
|
214
221
|
|
|
215
222
|
it("returns null when accounts is not an array", async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-anthropic-fix",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
4
5
|
"main": "./index.mjs",
|
|
5
6
|
"files": [
|
|
6
7
|
"index.mjs",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@eslint/js": "^10.0.1",
|
|
35
|
-
"@opencode-ai/plugin": "^
|
|
36
|
+
"@opencode-ai/plugin": "^1.2.27",
|
|
36
37
|
"esbuild": "^0.27.3",
|
|
37
38
|
"eslint": "^10.0.0",
|
|
38
39
|
"husky": "^9.1.7",
|