@modelprofile.com/authswitch 8.0.0 → 8.1.0

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 (43) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-import-contract.d.ts +207 -0
  3. package/dist_ts/authority-import-contract.js +18 -0
  4. package/dist_ts/classes.authoritycli.d.ts +4 -0
  5. package/dist_ts/classes.authoritycli.js +148 -11
  6. package/dist_ts/classes.authorityclient.d.ts +7 -0
  7. package/dist_ts/classes.authorityclient.js +13 -1
  8. package/dist_ts/classes.authoritydaemon.d.ts +4 -0
  9. package/dist_ts/classes.authoritydaemon.js +72 -1
  10. package/dist_ts/classes.authoritydatabase.d.ts +5 -0
  11. package/dist_ts/classes.authoritydatabase.js +20 -5
  12. package/dist_ts/classes.authorityimport.d.ts +161 -0
  13. package/dist_ts/classes.authorityimport.js +638 -0
  14. package/dist_ts/classes.authoritymodels.d.ts +12 -0
  15. package/dist_ts/classes.authoritymodels.js +17 -1
  16. package/dist_ts/index.d.ts +1 -0
  17. package/dist_ts/index.js +6 -3
  18. package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
  19. package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
  20. package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
  21. package/dist_ts/ts_migration/legacysources/index.js +24 -0
  22. package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
  23. package/dist_ts/ts_migration/legacysources/material.js +120 -0
  24. package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
  25. package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
  26. package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
  27. package/dist_ts/ts_migration/legacysources/shared.js +142 -0
  28. package/package.json +5 -1
  29. package/readme.md +46 -0
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/authority-import-contract.ts +216 -0
  32. package/ts/classes.authoritycli.ts +164 -10
  33. package/ts/classes.authorityclient.ts +22 -0
  34. package/ts/classes.authoritydaemon.ts +72 -0
  35. package/ts/classes.authoritydatabase.ts +20 -4
  36. package/ts/classes.authorityimport.ts +718 -0
  37. package/ts/classes.authoritymodels.ts +21 -0
  38. package/ts/index.ts +5 -2
  39. package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
  40. package/ts/ts_migration/legacysources/index.ts +35 -0
  41. package/ts/ts_migration/legacysources/material.ts +134 -0
  42. package/ts/ts_migration/legacysources/nativestores.ts +204 -0
  43. package/ts/ts_migration/legacysources/shared.ts +220 -0
@@ -0,0 +1,220 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { readCredentialRaw } from '../../classes.credentialstore.js';
3
+ import type { TAuthSwitchImportProof, TAuthSwitchImportSourceKind,
4
+ TAuthSwitchImportTreatment } from '../../authority-import-contract.js';
5
+
6
+ /**
7
+ * Shared vocabulary of the legacy-source readers.
8
+ *
9
+ * These readers exist for one migration and are deliberately isolated from the runtime: nothing here is a
10
+ * fallback an ordinary command may reach for. They only read, and they report what they could not read
11
+ * instead of returning a shorter list -- a silently skipped entry is an account the owner would lose.
12
+ */
13
+
14
+ /** One source as the readers see it. `path` stays inside the daemon; only `sourcePathHash` is published. */
15
+ export interface ILegacySourceRecord {
16
+ id: string;
17
+ sourceKind: TAuthSwitchImportSourceKind;
18
+ path: string;
19
+ sourcePathHash: string;
20
+ sourceDigest: string;
21
+ treatment: TAuthSwitchImportTreatment;
22
+ proof: TAuthSwitchImportProof;
23
+ accountId: string | null;
24
+ /** The provider's own stable identity, kept on the account so a sealed credential can be matched to it. */
25
+ subject: string | null;
26
+ workspaceId: string | null;
27
+ providerId: string | null;
28
+ email: string | null;
29
+ plan: string | null;
30
+ label: string | null;
31
+ accessExpiresAt: string | null;
32
+ enrollmentCount: number | null;
33
+ problems: string[];
34
+ }
35
+
36
+ export interface ILegacySourceReport {
37
+ sources: ILegacySourceRecord[];
38
+ /** Locations that could not be read at all, so an empty result is never mistaken for an empty store. */
39
+ problems: string[];
40
+ }
41
+
42
+ /**
43
+ * Where the legacy stores live.
44
+ *
45
+ * Each field repeats the rule of the class that owns that location, so a fixture can point the whole reader
46
+ * at a temporary directory: `authSwitchHome` (ts/classes.credentialstore.ts), `CodexHome`
47
+ * (ts/classes.codexhome.ts), `ClaudeCodeHarness` (ts/classes.claudecodeharness.ts) and `OpenCodeHarness`
48
+ * (ts/classes.opencodeharness.ts). Drift in any of those is a bug here, not a reason to guess.
49
+ */
50
+ export interface ILegacySourceLocations {
51
+ authSwitchHome: string;
52
+ codexHome: string;
53
+ claudeCredentialFile: string;
54
+ claudeConfigFile: string;
55
+ openCodeAuthFile: string;
56
+ }
57
+
58
+ export const resolveLegacySourceLocations = (env: NodeJS.ProcessEnv,
59
+ homeDirectory: string): ILegacySourceLocations => {
60
+ const claudeConfigDirectory = env.CLAUDE_CONFIG_DIR || plugins.path.join(homeDirectory, '.claude');
61
+ const dataHome = env.XDG_DATA_HOME || plugins.path.join(homeDirectory, '.local', 'share');
62
+ return {
63
+ authSwitchHome: env.AUTHSWITCH_HOME || plugins.path.join(homeDirectory, '.authswitch'),
64
+ codexHome: env.CODEX_HOME || plugins.path.join(homeDirectory, '.codex'),
65
+ claudeCredentialFile: plugins.path.join(claudeConfigDirectory, '.credentials.json'),
66
+ // Claude Code keeps its account metadata beside the home directory, not inside a custom config directory.
67
+ claudeConfigFile: plugins.path.join(env.CLAUDE_CONFIG_DIR || homeDirectory, '.claude.json'),
68
+ openCodeAuthFile: plugins.path.join(dataHome, 'opencode', 'auth.json'),
69
+ };
70
+ };
71
+
72
+ export const legacyHash = (value: string): string =>
73
+ plugins.crypto.createHash('sha256').update(value).digest('hex');
74
+
75
+ /** The ledger row id of a source. Stable across inventory runs, so a resumed import finds its own row. */
76
+ export const legacySourceId = (sourceKind: TAuthSwitchImportSourceKind, sourcePathHash: string): string =>
77
+ legacyHash(JSON.stringify(['authswitch-migration-v1', sourceKind, sourcePathHash]));
78
+
79
+ /**
80
+ * The digest that binds an import to exactly these bytes.
81
+ *
82
+ * A source is often more than one file -- a stash directory, or Claude's credential/config pair -- so the
83
+ * digest covers an ordered list of per-file digests rather than one file. The file names are part of it: a
84
+ * source that gains or loses a file is a different source.
85
+ */
86
+ export const legacySourceDigest = (files: ReadonlyArray<{ name: string; raw: string }>): string =>
87
+ legacyHash(JSON.stringify([...files]
88
+ .sort((left, right) => left.name.localeCompare(right.name))
89
+ .map(file => [file.name, legacyHash(file.raw)])));
90
+
91
+ export interface ILegacySourceFile {
92
+ name: string;
93
+ raw: string;
94
+ }
95
+
96
+ /** A bounded read that reports unreadable and absent apart, through the store's own reader. */
97
+ export const readLegacyFile = (file: string, name: string):
98
+ { file: ILegacySourceFile | null; problem: string | null } => {
99
+ try {
100
+ const raw = readCredentialRaw(file);
101
+ return { file: raw === null ? null : { name, raw }, problem: null };
102
+ } catch {
103
+ return { file: null, problem: `${name} is unreadable, malformed or too large` };
104
+ }
105
+ };
106
+
107
+ export const parseLegacyJson = (raw: string): Record<string, unknown> | null => {
108
+ try {
109
+ const value: unknown = JSON.parse(raw);
110
+ return value && typeof value === 'object' && !Array.isArray(value)
111
+ ? value as Record<string, unknown> : null;
112
+ } catch { return null; }
113
+ };
114
+
115
+ export interface IOpenAiSourceIdentity {
116
+ accountId: string;
117
+ /** The stable provider identity the account record keeps, so a sealed credential can be matched to it. */
118
+ subject: string;
119
+ workspaceId: string;
120
+ email: string | null;
121
+ plan: string | null;
122
+ }
123
+
124
+ /**
125
+ * The authority account a ChatGPT login resolves to.
126
+ *
127
+ * Derived from the id_token's claims exactly as `AuthSwitchAuthorityBroker.identity` derives it
128
+ * (ts/classes.authoritybroker.ts), because an import that produced a different account id would create a
129
+ * second account for a login the owner already has. No provider call is involved: this is a local read of
130
+ * claims the token already carries, which is what lets the inventory run offline.
131
+ */
132
+ export const openAiSourceIdentity = (idToken: string): IOpenAiSourceIdentity | null => {
133
+ let info: ReturnType<typeof plugins.flexAuth.parseOpenAiChatGptTokenInfo>;
134
+ try { info = plugins.flexAuth.parseOpenAiChatGptTokenInfo(idToken); }
135
+ catch { return null; }
136
+ if (!info.chatgptUserId || !info.chatgptAccountId) return null;
137
+ return {
138
+ accountId: legacyHash(JSON.stringify(['authswitch-account-v1', 'openai',
139
+ info.chatgptUserId, info.chatgptAccountId])),
140
+ subject: info.chatgptUserId,
141
+ workspaceId: info.chatgptAccountId,
142
+ email: info.email?.toLowerCase() ?? null,
143
+ plan: info.chatgptPlanType ?? null,
144
+ };
145
+ };
146
+
147
+ export interface IClaudeSourceIdentity {
148
+ accountId: string;
149
+ /** Claude's immutable pair, kept as the account's subject and workspace so a sealed login matches it. */
150
+ subject: string;
151
+ workspaceId: string;
152
+ email: string | null;
153
+ accessExpiresAt: string | null;
154
+ }
155
+
156
+ /**
157
+ * The authority account a Claude subscriber login resolves to.
158
+ *
159
+ * Claude's immutable identity is the account/organization UUID pair that `ClaudeNativeAdapter` proves
160
+ * against the provider (ts/classes.claudenative.ts). Nothing in the package derived an authority account id
161
+ * from it before this import, so it follows the OpenAI rule -- provider, subject, workspace -- and stays the
162
+ * single formula both the inventory and the import use.
163
+ */
164
+ export const claudeSourceIdentity = (credential: Record<string, unknown>): IClaudeSourceIdentity | null => {
165
+ const oauth = credential.claudeAiOauth;
166
+ const account = credential.oauthAccount;
167
+ if (!oauth || typeof oauth !== 'object' || Array.isArray(oauth)
168
+ || !account || typeof account !== 'object' || Array.isArray(account)) return null;
169
+ const oauthRecord = oauth as Record<string, unknown>;
170
+ const accountRecord = account as Record<string, unknown>;
171
+ const accountUuid = accountRecord.accountUuid;
172
+ const organizationUuid = accountRecord.organizationUuid;
173
+ if (typeof accountUuid !== 'string' || !accountUuid || typeof organizationUuid !== 'string'
174
+ || !organizationUuid) return null;
175
+ const expiresAt = oauthRecord.expiresAt;
176
+ const email = accountRecord.emailAddress;
177
+ return {
178
+ accountId: legacyHash(JSON.stringify(['authswitch-account-v1', 'anthropic',
179
+ accountUuid, organizationUuid])),
180
+ subject: accountUuid,
181
+ workspaceId: organizationUuid,
182
+ email: typeof email === 'string' && email ? email.toLowerCase() : null,
183
+ accessExpiresAt: typeof expiresAt === 'number' && Number.isFinite(expiresAt)
184
+ ? new Date(expiresAt).toISOString() : null,
185
+ };
186
+ };
187
+
188
+ /**
189
+ * The identity of an OpenCode OpenAI provider entry.
190
+ *
191
+ * OpenCode stores no id_token, so `OpenCodeHarness.openAiInfo` (ts/classes.opencodeharness.ts) reads the
192
+ * claims of the access token instead. An expired or opaque access token therefore leaves such an entry
193
+ * without a derivable identity, which is reported rather than guessed.
194
+ */
195
+ export const openCodeOpenAiIdentity = (entry: Record<string, unknown>):
196
+ { identity: IOpenAiSourceIdentity | null; accessExpiresAt: string | null } => {
197
+ const access = typeof entry.access === 'string' ? entry.access : null;
198
+ const expires = entry.expires;
199
+ const accessExpiresAt = typeof expires === 'number' && Number.isFinite(expires)
200
+ ? new Date(expires).toISOString() : accessTokenExpiry(access);
201
+ return { identity: access === null ? null : openAiSourceIdentity(access), accessExpiresAt };
202
+ };
203
+
204
+ /** The expiry claim of an access token, as the broker reads it. Absent or unparsable means unknown. */
205
+ export const accessTokenExpiry = (accessToken: string | null): string | null => {
206
+ if (!accessToken) return null;
207
+ try { return plugins.flexAuth.parseOpenAiChatGptTokenInfo(accessToken).expiresAt ?? null; }
208
+ catch { return null; }
209
+ };
210
+
211
+ /** Whether a non-rotating proof can still be taken with this access token. */
212
+ export const accessProofAvailable = (expiresAt: string | null, now: number): boolean => {
213
+ if (expiresAt === null) return false;
214
+ const value = Date.parse(expiresAt);
215
+ return Number.isFinite(value) && value > now;
216
+ };
217
+
218
+ export const EXPIRED_ACCESS_PROBLEM =
219
+ 'the stored access token has expired, so the non-rotating proof is unavailable until the native tool '
220
+ + 'refreshes its own store';