@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1
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 +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
|
@@ -86,6 +86,72 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
86
86
|
import { exec as exec2 } from "node:child_process";
|
|
87
87
|
import { pathToFileURL } from "node:url";
|
|
88
88
|
|
|
89
|
+
// src/account-identity.ts
|
|
90
|
+
function isCCAccountSource(source) {
|
|
91
|
+
return source === "cc-keychain" || source === "cc-file";
|
|
92
|
+
}
|
|
93
|
+
function isAccountIdentity(value) {
|
|
94
|
+
if (!value || typeof value !== "object") return false;
|
|
95
|
+
const candidate = value;
|
|
96
|
+
switch (candidate.kind) {
|
|
97
|
+
case "oauth":
|
|
98
|
+
return typeof candidate.email === "string" && candidate.email.length > 0;
|
|
99
|
+
case "cc":
|
|
100
|
+
return isCCAccountSource(candidate.source) && typeof candidate.label === "string";
|
|
101
|
+
case "legacy":
|
|
102
|
+
return typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0;
|
|
103
|
+
default:
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function resolveIdentity(account) {
|
|
108
|
+
if (isAccountIdentity(account.identity)) {
|
|
109
|
+
return account.identity;
|
|
110
|
+
}
|
|
111
|
+
if (account.source === "oauth" && account.email) {
|
|
112
|
+
return { kind: "oauth", email: account.email };
|
|
113
|
+
}
|
|
114
|
+
if (isCCAccountSource(account.source) && account.label) {
|
|
115
|
+
return { kind: "cc", source: account.source, label: account.label };
|
|
116
|
+
}
|
|
117
|
+
return { kind: "legacy", refreshToken: account.refreshToken };
|
|
118
|
+
}
|
|
119
|
+
function resolveIdentityFromOAuthExchange(result) {
|
|
120
|
+
if (result.email) {
|
|
121
|
+
return {
|
|
122
|
+
kind: "oauth",
|
|
123
|
+
email: result.email
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: "legacy",
|
|
128
|
+
refreshToken: result.refresh
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function identitiesMatch(a, b) {
|
|
132
|
+
if (a.kind !== b.kind) return false;
|
|
133
|
+
switch (a.kind) {
|
|
134
|
+
case "oauth": {
|
|
135
|
+
return a.email === b.email;
|
|
136
|
+
}
|
|
137
|
+
case "cc": {
|
|
138
|
+
const ccIdentity = b;
|
|
139
|
+
return a.source === ccIdentity.source && a.label === ccIdentity.label;
|
|
140
|
+
}
|
|
141
|
+
case "legacy": {
|
|
142
|
+
return a.refreshToken === b.refreshToken;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function findByIdentity(accounts, id) {
|
|
147
|
+
for (const account of accounts) {
|
|
148
|
+
if (identitiesMatch(resolveIdentity(account), id)) {
|
|
149
|
+
return account;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
89
155
|
// src/config.ts
|
|
90
156
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
91
157
|
import { homedir } from "node:os";
|
|
@@ -585,6 +651,8 @@ function validateAccount(raw, now) {
|
|
|
585
651
|
return {
|
|
586
652
|
id,
|
|
587
653
|
email: typeof acc.email === "string" ? acc.email : void 0,
|
|
654
|
+
identity: isAccountIdentity2(acc.identity) ? acc.identity : void 0,
|
|
655
|
+
label: typeof acc.label === "string" ? acc.label : void 0,
|
|
588
656
|
refreshToken: acc.refreshToken,
|
|
589
657
|
access: typeof acc.access === "string" ? acc.access : void 0,
|
|
590
658
|
expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : void 0,
|
|
@@ -600,6 +668,106 @@ function validateAccount(raw, now) {
|
|
|
600
668
|
source: acc.source === "cc-keychain" || acc.source === "cc-file" || acc.source === "oauth" ? acc.source : void 0
|
|
601
669
|
};
|
|
602
670
|
}
|
|
671
|
+
function isAccountIdentity2(value) {
|
|
672
|
+
if (!value || typeof value !== "object") return false;
|
|
673
|
+
const candidate = value;
|
|
674
|
+
switch (candidate.kind) {
|
|
675
|
+
case "oauth":
|
|
676
|
+
return typeof candidate.email === "string" && candidate.email.length > 0;
|
|
677
|
+
case "cc":
|
|
678
|
+
return (candidate.source === "cc-keychain" || candidate.source === "cc-file") && typeof candidate.label === "string";
|
|
679
|
+
case "legacy":
|
|
680
|
+
return typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0;
|
|
681
|
+
default:
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function resolveStoredIdentity(candidate) {
|
|
686
|
+
if (isAccountIdentity2(candidate.identity)) {
|
|
687
|
+
return candidate.identity;
|
|
688
|
+
}
|
|
689
|
+
if (candidate.source === "oauth" && candidate.email) {
|
|
690
|
+
return { kind: "oauth", email: candidate.email };
|
|
691
|
+
}
|
|
692
|
+
if ((candidate.source === "cc-keychain" || candidate.source === "cc-file") && candidate.label) {
|
|
693
|
+
return {
|
|
694
|
+
kind: "cc",
|
|
695
|
+
source: candidate.source,
|
|
696
|
+
label: candidate.label
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return { kind: "legacy", refreshToken: candidate.refreshToken };
|
|
700
|
+
}
|
|
701
|
+
function resolveTokenUpdatedAt(account) {
|
|
702
|
+
return typeof account.token_updated_at === "number" && Number.isFinite(account.token_updated_at) ? account.token_updated_at : account.addedAt;
|
|
703
|
+
}
|
|
704
|
+
function clampActiveIndex(accounts, activeIndex) {
|
|
705
|
+
if (accounts.length === 0) {
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
return Math.max(0, Math.min(activeIndex, accounts.length - 1));
|
|
709
|
+
}
|
|
710
|
+
function findStoredAccountMatch(accounts, candidate) {
|
|
711
|
+
const byId = accounts.find((account) => account.id === candidate.id);
|
|
712
|
+
if (byId) {
|
|
713
|
+
return byId;
|
|
714
|
+
}
|
|
715
|
+
const byIdentity = findByIdentity(accounts, resolveStoredIdentity(candidate));
|
|
716
|
+
if (byIdentity) {
|
|
717
|
+
return byIdentity;
|
|
718
|
+
}
|
|
719
|
+
const byAddedAt = accounts.filter((account) => account.addedAt === candidate.addedAt);
|
|
720
|
+
if (byAddedAt.length === 1) {
|
|
721
|
+
return byAddedAt[0];
|
|
722
|
+
}
|
|
723
|
+
const byRefreshToken = accounts.find((account) => account.refreshToken === candidate.refreshToken);
|
|
724
|
+
if (byRefreshToken) {
|
|
725
|
+
return byRefreshToken;
|
|
726
|
+
}
|
|
727
|
+
return byAddedAt[0] ?? null;
|
|
728
|
+
}
|
|
729
|
+
function mergeAccountWithFresherAuth(account, diskMatch) {
|
|
730
|
+
const memoryTokenUpdatedAt = resolveTokenUpdatedAt(account);
|
|
731
|
+
const diskTokenUpdatedAt = diskMatch ? resolveTokenUpdatedAt(diskMatch) : 0;
|
|
732
|
+
if (!diskMatch || diskTokenUpdatedAt <= memoryTokenUpdatedAt) {
|
|
733
|
+
return {
|
|
734
|
+
...account,
|
|
735
|
+
token_updated_at: memoryTokenUpdatedAt
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
...account,
|
|
740
|
+
refreshToken: diskMatch.refreshToken,
|
|
741
|
+
access: diskMatch.access,
|
|
742
|
+
expires: diskMatch.expires,
|
|
743
|
+
token_updated_at: diskTokenUpdatedAt
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function unionAccountsWithDisk(storage, disk) {
|
|
747
|
+
if (!disk || storage.accounts.length === 0) {
|
|
748
|
+
return {
|
|
749
|
+
...storage,
|
|
750
|
+
activeIndex: clampActiveIndex(storage.accounts, storage.activeIndex)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const activeAccountId = storage.accounts[storage.activeIndex]?.id ?? null;
|
|
754
|
+
const matchedDiskAccounts = /* @__PURE__ */ new Set();
|
|
755
|
+
const mergedAccounts = storage.accounts.map((account) => {
|
|
756
|
+
const diskMatch = findStoredAccountMatch(disk.accounts, account);
|
|
757
|
+
if (diskMatch) {
|
|
758
|
+
matchedDiskAccounts.add(diskMatch);
|
|
759
|
+
}
|
|
760
|
+
return mergeAccountWithFresherAuth(account, diskMatch);
|
|
761
|
+
});
|
|
762
|
+
const diskOnlyAccounts = disk.accounts.filter((account) => !matchedDiskAccounts.has(account));
|
|
763
|
+
const accounts = [...mergedAccounts, ...diskOnlyAccounts];
|
|
764
|
+
const activeIndex = activeAccountId ? accounts.findIndex((account) => account.id === activeAccountId) : -1;
|
|
765
|
+
return {
|
|
766
|
+
...storage,
|
|
767
|
+
accounts,
|
|
768
|
+
activeIndex: activeIndex >= 0 ? activeIndex : clampActiveIndex(accounts, storage.activeIndex)
|
|
769
|
+
};
|
|
770
|
+
}
|
|
603
771
|
async function loadAccounts() {
|
|
604
772
|
const storagePath = getStoragePath();
|
|
605
773
|
try {
|
|
@@ -609,7 +777,9 @@ async function loadAccounts() {
|
|
|
609
777
|
return null;
|
|
610
778
|
}
|
|
611
779
|
if (data.version !== CURRENT_VERSION) {
|
|
612
|
-
|
|
780
|
+
console.warn(
|
|
781
|
+
`Storage version mismatch: ${String(data.version)} vs ${CURRENT_VERSION}. Attempting best-effort migration.`
|
|
782
|
+
);
|
|
613
783
|
}
|
|
614
784
|
const now = Date.now();
|
|
615
785
|
const accounts = data.accounts.map((raw) => validateAccount(raw, now)).filter((acc) => acc !== null);
|
|
@@ -636,53 +806,13 @@ async function saveAccounts(storage) {
|
|
|
636
806
|
const configDir = dirname2(storagePath);
|
|
637
807
|
await fs.mkdir(configDir, { recursive: true });
|
|
638
808
|
ensureGitignore(configDir);
|
|
639
|
-
let storageToWrite =
|
|
809
|
+
let storageToWrite = {
|
|
810
|
+
...storage,
|
|
811
|
+
activeIndex: clampActiveIndex(storage.accounts, storage.activeIndex)
|
|
812
|
+
};
|
|
640
813
|
try {
|
|
641
814
|
const disk = await loadAccounts();
|
|
642
|
-
|
|
643
|
-
const diskById = new Map(disk.accounts.map((a) => [a.id, a]));
|
|
644
|
-
const diskByAddedAt = /* @__PURE__ */ new Map();
|
|
645
|
-
const diskByToken = new Map(disk.accounts.map((a) => [a.refreshToken, a]));
|
|
646
|
-
for (const d3 of disk.accounts) {
|
|
647
|
-
const bucket = diskByAddedAt.get(d3.addedAt) || [];
|
|
648
|
-
bucket.push(d3);
|
|
649
|
-
diskByAddedAt.set(d3.addedAt, bucket);
|
|
650
|
-
}
|
|
651
|
-
const findDiskMatch = (acc) => {
|
|
652
|
-
const byId = diskById.get(acc.id);
|
|
653
|
-
if (byId) return byId;
|
|
654
|
-
const byAddedAt = diskByAddedAt.get(acc.addedAt);
|
|
655
|
-
if (byAddedAt?.length === 1) return byAddedAt[0];
|
|
656
|
-
const byToken = diskByToken.get(acc.refreshToken);
|
|
657
|
-
if (byToken) return byToken;
|
|
658
|
-
if (byAddedAt && byAddedAt.length > 0) return byAddedAt[0];
|
|
659
|
-
return null;
|
|
660
|
-
};
|
|
661
|
-
const mergedAccounts = storage.accounts.map((acc) => {
|
|
662
|
-
const diskAcc = findDiskMatch(acc);
|
|
663
|
-
const memTs = typeof acc.token_updated_at === "number" && Number.isFinite(acc.token_updated_at) ? acc.token_updated_at : acc.addedAt;
|
|
664
|
-
const diskTs = diskAcc?.token_updated_at || 0;
|
|
665
|
-
const useDiskAuth = !!diskAcc && diskTs > memTs;
|
|
666
|
-
return {
|
|
667
|
-
...acc,
|
|
668
|
-
refreshToken: useDiskAuth ? diskAcc.refreshToken : acc.refreshToken,
|
|
669
|
-
access: useDiskAuth ? diskAcc.access : acc.access,
|
|
670
|
-
expires: useDiskAuth ? diskAcc.expires : acc.expires,
|
|
671
|
-
token_updated_at: useDiskAuth ? diskTs : memTs
|
|
672
|
-
};
|
|
673
|
-
});
|
|
674
|
-
let activeIndex = storage.activeIndex;
|
|
675
|
-
if (mergedAccounts.length > 0) {
|
|
676
|
-
activeIndex = Math.max(0, Math.min(activeIndex, mergedAccounts.length - 1));
|
|
677
|
-
} else {
|
|
678
|
-
activeIndex = 0;
|
|
679
|
-
}
|
|
680
|
-
storageToWrite = {
|
|
681
|
-
...storage,
|
|
682
|
-
accounts: mergedAccounts,
|
|
683
|
-
activeIndex
|
|
684
|
-
};
|
|
685
|
-
}
|
|
815
|
+
storageToWrite = unionAccountsWithDisk(storageToWrite, disk);
|
|
686
816
|
} catch {
|
|
687
817
|
}
|
|
688
818
|
const tempPath = `${storagePath}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
@@ -1837,14 +1967,20 @@ async function cmdLogin() {
|
|
|
1837
1967
|
const credentials = await runOAuthFlow();
|
|
1838
1968
|
if (!credentials) return 1;
|
|
1839
1969
|
const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1970
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
1971
|
+
const existing = findByIdentity(storage.accounts, identity) || storage.accounts.find((acc) => acc.refreshToken === credentials.refresh);
|
|
1972
|
+
if (existing) {
|
|
1973
|
+
const existingIdx = storage.accounts.indexOf(existing);
|
|
1974
|
+
existing.refreshToken = credentials.refresh;
|
|
1975
|
+
existing.access = credentials.access;
|
|
1976
|
+
existing.expires = credentials.expires;
|
|
1977
|
+
existing.token_updated_at = Date.now();
|
|
1978
|
+
if (credentials.email) existing.email = credentials.email;
|
|
1979
|
+
existing.identity = identity;
|
|
1980
|
+
existing.source = existing.source ?? "oauth";
|
|
1981
|
+
existing.enabled = true;
|
|
1846
1982
|
await saveAccounts(storage);
|
|
1847
|
-
const label2 = credentials.email || `Account ${existingIdx + 1}`;
|
|
1983
|
+
const label2 = credentials.email || existing.email || `Account ${existingIdx + 1}`;
|
|
1848
1984
|
O2.success(`Updated existing account #${existingIdx + 1} (${label2}).`);
|
|
1849
1985
|
return 0;
|
|
1850
1986
|
}
|
|
@@ -1856,6 +1992,7 @@ async function cmdLogin() {
|
|
|
1856
1992
|
storage.accounts.push({
|
|
1857
1993
|
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
1858
1994
|
email: credentials.email,
|
|
1995
|
+
identity,
|
|
1859
1996
|
refreshToken: credentials.refresh,
|
|
1860
1997
|
access: credentials.access,
|
|
1861
1998
|
expires: credentials.expires,
|
|
@@ -1866,7 +2003,8 @@ async function cmdLogin() {
|
|
|
1866
2003
|
rateLimitResetTimes: {},
|
|
1867
2004
|
consecutiveFailures: 0,
|
|
1868
2005
|
lastFailureTime: null,
|
|
1869
|
-
stats: createDefaultStats(now)
|
|
2006
|
+
stats: createDefaultStats(now),
|
|
2007
|
+
source: "oauth"
|
|
1870
2008
|
});
|
|
1871
2009
|
await saveAccounts(storage);
|
|
1872
2010
|
const label = credentials.email || `Account ${storage.accounts.length}`;
|
|
@@ -2064,7 +2202,8 @@ async function cmdList() {
|
|
|
2064
2202
|
}
|
|
2065
2203
|
}
|
|
2066
2204
|
if (anyRefreshed) {
|
|
2067
|
-
await saveAccounts(stored).catch(() => {
|
|
2205
|
+
await saveAccounts(stored).catch((err) => {
|
|
2206
|
+
console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", err);
|
|
2068
2207
|
});
|
|
2069
2208
|
}
|
|
2070
2209
|
O2.message(c2.bold("Anthropic Multi-Account Status"));
|