@askalf/dario 6.0.38 → 6.0.39
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/dist/accounts.d.ts +87 -0
- package/dist/accounts.js +162 -19
- package/dist/admin-api.d.ts +14 -1
- package/dist/admin-api.js +10 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +80 -3
- package/dist/doctor-core.d.ts +34 -0
- package/dist/doctor-core.js +91 -3
- package/dist/pool.d.ts +146 -17
- package/dist/pool.js +264 -54
- package/dist/proxy.js +86 -28
- package/docs/admin-api.md +1 -3
- package/docs/commands.md +1 -0
- package/docs/multi-account-pool.md +26 -7
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -15,7 +15,26 @@ export interface AccountCredentials {
|
|
|
15
15
|
* on a fresh grant.
|
|
16
16
|
*/
|
|
17
17
|
organizationId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The Anthropic account uuid this grant belongs to, from the OAuth profile
|
|
20
|
+
* (`fetchOAuthProfile`) at grant time — or, for a record from before this
|
|
21
|
+
* field, on its next token refresh. THE fact that says whether two aliases
|
|
22
|
+
* are one subscription (dario#1244, #1263); everything else was inference.
|
|
23
|
+
*/
|
|
24
|
+
accountId?: string;
|
|
25
|
+
/** Email on that account. Masked on every listing (`maskEmail`); stored whole. */
|
|
26
|
+
accountEmail?: string;
|
|
27
|
+
/** `organization.organization_type` / `rate_limit_tier` / `seat_tier` from the same profile. */
|
|
28
|
+
organizationType?: string;
|
|
29
|
+
rateLimitTier?: string;
|
|
30
|
+
seatTier?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Where `deviceId`/`accountUuid` came from: the machine's Claude Code identity,
|
|
33
|
+
* or generated for this alias. Absent on records written before this field.
|
|
34
|
+
*/
|
|
35
|
+
identityFrom?: IdentitySource;
|
|
18
36
|
}
|
|
37
|
+
export type IdentitySource = 'claude-code' | 'generated';
|
|
19
38
|
export declare function listAccountAliases(): Promise<string[]>;
|
|
20
39
|
export declare function loadAccount(alias: string): Promise<AccountCredentials | null>;
|
|
21
40
|
export declare function loadAllAccounts(): Promise<AccountCredentials[]>;
|
|
@@ -37,8 +56,76 @@ export declare function detectClaudeIdentity(): Promise<{
|
|
|
37
56
|
deviceId: string;
|
|
38
57
|
accountUuid: string;
|
|
39
58
|
} | null>;
|
|
59
|
+
/**
|
|
60
|
+
* The OAuth profile behind an access token. This is what an Anthropic
|
|
61
|
+
* subscription token can say about itself, and dario never asked (#1263):
|
|
62
|
+
* the account uuid, its email, the organization uuid and tier fields. Probed
|
|
63
|
+
* 2026-09-08 with a live token — `account.uuid`, `account.email`,
|
|
64
|
+
* `organization.uuid`, `organization.rate_limit_tier`, `organization.seat_tier`
|
|
65
|
+
* are all present. Read-only, one GET, and it is the one fact that settles
|
|
66
|
+
* "are these two aliases the same subscription" without guessing.
|
|
67
|
+
*/
|
|
68
|
+
export declare const OAUTH_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
69
|
+
export interface OAuthProfile {
|
|
70
|
+
accountId: string;
|
|
71
|
+
accountEmail?: string;
|
|
72
|
+
organizationId?: string;
|
|
73
|
+
organizationType?: string;
|
|
74
|
+
rateLimitTier?: string;
|
|
75
|
+
seatTier?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fetch the profile for `accessToken`. Null on ANY failure — a non-2xx, a
|
|
79
|
+
* timeout, a body without an account uuid — because identity is a nicety
|
|
80
|
+
* layered on a grant that already succeeded; a profile outage must never
|
|
81
|
+
* turn a working login into a failed one.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fetchOAuthProfile(accessToken: string, fetchImpl?: typeof fetch): Promise<OAuthProfile | null>;
|
|
84
|
+
/** The record fields a profile fills in; nothing when there is no profile. */
|
|
85
|
+
export declare function profileFields(profile: OAuthProfile | null): Partial<AccountCredentials>;
|
|
86
|
+
/**
|
|
87
|
+
* Which client identity a NEW alias presents in `metadata.user_id`.
|
|
88
|
+
*
|
|
89
|
+
* Every add path used to copy the machine's Claude Code identity into every
|
|
90
|
+
* alias when one was installed. On a machine running a pool of colleagues'
|
|
91
|
+
* tokens that meant 18 different OAuth accounts all presenting ONE
|
|
92
|
+
* `device_id`/`account_uuid` (dario#1244, 2026-09-08) — and Anthropic ties what
|
|
93
|
+
* it sees to that identity (doctor-core.ts, identity drift: the bearer is
|
|
94
|
+
* cross-validated against it). The startup banner says as much: "requests
|
|
95
|
+
* may be billed as Extra Usage" without one.
|
|
96
|
+
*
|
|
97
|
+
* Rule: the local Claude Code identity belongs to the account it was granted
|
|
98
|
+
* to. A new alias takes it only when no other alias holds it, or when the
|
|
99
|
+
* holder is PROVEN to be the same account (same OAuth `accountId`). Otherwise
|
|
100
|
+
* the alias gets its own — the same fresh identity a machine without Claude
|
|
101
|
+
* Code has always produced, which the fleet box runs every seat on.
|
|
102
|
+
*/
|
|
103
|
+
export declare function chooseClientIdentity(existing: readonly AccountCredentials[], profile: OAuthProfile | null, local?: {
|
|
104
|
+
deviceId: string;
|
|
105
|
+
accountUuid: string;
|
|
106
|
+
} | null): Promise<{
|
|
107
|
+
deviceId: string;
|
|
108
|
+
accountUuid: string;
|
|
109
|
+
identityFrom: IdentitySource;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Give each named alias its own client identity (`dario accounts identity
|
|
113
|
+
* --fresh`). Tokens, organization and account identity are untouched — only
|
|
114
|
+
* `deviceId`/`accountUuid` change, and the running proxy presents the new pair on
|
|
115
|
+
* the seat's next request (pool.add takes a changed on-disk identity).
|
|
116
|
+
* Returns the aliases rewritten; an unknown alias is skipped, not an error.
|
|
117
|
+
*/
|
|
118
|
+
export declare function regenerateClientIdentity(aliases: readonly string[]): Promise<string[]>;
|
|
40
119
|
/** Refresh an account's OAuth token using dario's auto-detected CC OAuth config. */
|
|
41
120
|
export declare function refreshAccountToken(creds: AccountCredentials): Promise<AccountCredentials>;
|
|
121
|
+
/**
|
|
122
|
+
* Fill in `accountId` (and the profile's other fields) on a record that predates
|
|
123
|
+
* them, from one profile GET; saves and returns the record, unchanged when
|
|
124
|
+
* nothing could be learned. Kept OUT of the token refresh itself so the
|
|
125
|
+
* refresh stays a single, sequenced token exchange — a caller with a mocked
|
|
126
|
+
* or injected fetch sees exactly the calls it always did (dario#1263).
|
|
127
|
+
*/
|
|
128
|
+
export declare function backfillIdentity(creds: AccountCredentials, fetchImpl?: typeof fetch): Promise<AccountCredentials>;
|
|
42
129
|
/** Test-only — inspect the in-flight map. Production code has no business peeking. */
|
|
43
130
|
export declare function _accountRefreshesInFlightSizeForTest(): number;
|
|
44
131
|
/**
|
package/dist/accounts.js
CHANGED
|
@@ -135,6 +135,125 @@ export async function detectClaudeIdentity() {
|
|
|
135
135
|
}
|
|
136
136
|
return null;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The OAuth profile behind an access token. This is what an Anthropic
|
|
140
|
+
* subscription token can say about itself, and dario never asked (#1263):
|
|
141
|
+
* the account uuid, its email, the organization uuid and tier fields. Probed
|
|
142
|
+
* 2026-09-08 with a live token — `account.uuid`, `account.email`,
|
|
143
|
+
* `organization.uuid`, `organization.rate_limit_tier`, `organization.seat_tier`
|
|
144
|
+
* are all present. Read-only, one GET, and it is the one fact that settles
|
|
145
|
+
* "are these two aliases the same subscription" without guessing.
|
|
146
|
+
*/
|
|
147
|
+
export const OAUTH_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
148
|
+
/**
|
|
149
|
+
* Fetch the profile for `accessToken`. Null on ANY failure — a non-2xx, a
|
|
150
|
+
* timeout, a body without an account uuid — because identity is a nicety
|
|
151
|
+
* layered on a grant that already succeeded; a profile outage must never
|
|
152
|
+
* turn a working login into a failed one.
|
|
153
|
+
*/
|
|
154
|
+
export async function fetchOAuthProfile(accessToken, fetchImpl = fetch) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchImpl(OAUTH_PROFILE_URL, {
|
|
157
|
+
method: 'GET',
|
|
158
|
+
headers: {
|
|
159
|
+
Authorization: `Bearer ${accessToken}`,
|
|
160
|
+
Accept: 'application/json',
|
|
161
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
162
|
+
},
|
|
163
|
+
signal: AbortSignal.timeout(8_000),
|
|
164
|
+
});
|
|
165
|
+
if (!res.ok)
|
|
166
|
+
return null;
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
const uuid = data.account?.uuid;
|
|
169
|
+
if (typeof uuid !== 'string' || uuid.length === 0)
|
|
170
|
+
return null;
|
|
171
|
+
const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
|
|
172
|
+
const profile = { accountId: uuid };
|
|
173
|
+
const email = str(data.account?.email);
|
|
174
|
+
if (email)
|
|
175
|
+
profile.accountEmail = email;
|
|
176
|
+
const org = str(data.organization?.uuid);
|
|
177
|
+
if (org)
|
|
178
|
+
profile.organizationId = org;
|
|
179
|
+
const orgType = str(data.organization?.organization_type);
|
|
180
|
+
if (orgType)
|
|
181
|
+
profile.organizationType = orgType;
|
|
182
|
+
const tier = str(data.organization?.rate_limit_tier);
|
|
183
|
+
if (tier)
|
|
184
|
+
profile.rateLimitTier = tier;
|
|
185
|
+
const seat = str(data.organization?.seat_tier);
|
|
186
|
+
if (seat)
|
|
187
|
+
profile.seatTier = seat;
|
|
188
|
+
return profile;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The record fields a profile fills in; nothing when there is no profile. */
|
|
195
|
+
export function profileFields(profile) {
|
|
196
|
+
if (!profile)
|
|
197
|
+
return {};
|
|
198
|
+
const out = { accountId: profile.accountId };
|
|
199
|
+
if (profile.accountEmail)
|
|
200
|
+
out.accountEmail = profile.accountEmail;
|
|
201
|
+
if (profile.organizationId)
|
|
202
|
+
out.organizationId = profile.organizationId;
|
|
203
|
+
if (profile.organizationType)
|
|
204
|
+
out.organizationType = profile.organizationType;
|
|
205
|
+
if (profile.rateLimitTier)
|
|
206
|
+
out.rateLimitTier = profile.rateLimitTier;
|
|
207
|
+
if (profile.seatTier)
|
|
208
|
+
out.seatTier = profile.seatTier;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Which client identity a NEW alias presents in `metadata.user_id`.
|
|
213
|
+
*
|
|
214
|
+
* Every add path used to copy the machine's Claude Code identity into every
|
|
215
|
+
* alias when one was installed. On a machine running a pool of colleagues'
|
|
216
|
+
* tokens that meant 18 different OAuth accounts all presenting ONE
|
|
217
|
+
* `device_id`/`account_uuid` (dario#1244, 2026-09-08) — and Anthropic ties what
|
|
218
|
+
* it sees to that identity (doctor-core.ts, identity drift: the bearer is
|
|
219
|
+
* cross-validated against it). The startup banner says as much: "requests
|
|
220
|
+
* may be billed as Extra Usage" without one.
|
|
221
|
+
*
|
|
222
|
+
* Rule: the local Claude Code identity belongs to the account it was granted
|
|
223
|
+
* to. A new alias takes it only when no other alias holds it, or when the
|
|
224
|
+
* holder is PROVEN to be the same account (same OAuth `accountId`). Otherwise
|
|
225
|
+
* the alias gets its own — the same fresh identity a machine without Claude
|
|
226
|
+
* Code has always produced, which the fleet box runs every seat on.
|
|
227
|
+
*/
|
|
228
|
+
export async function chooseClientIdentity(existing, profile, local) {
|
|
229
|
+
const cc = local === undefined ? await detectClaudeIdentity() : local;
|
|
230
|
+
if (cc && (cc.deviceId || cc.accountUuid)) {
|
|
231
|
+
const holder = existing.find((a) => a.deviceId === cc.deviceId && a.accountUuid === cc.accountUuid);
|
|
232
|
+
const sameAccount = holder !== undefined && profile !== null
|
|
233
|
+
&& typeof holder.accountId === 'string' && holder.accountId === profile.accountId;
|
|
234
|
+
if (!holder || sameAccount)
|
|
235
|
+
return { deviceId: cc.deviceId, accountUuid: cc.accountUuid, identityFrom: 'claude-code' };
|
|
236
|
+
}
|
|
237
|
+
return { deviceId: randomUUID(), accountUuid: randomUUID(), identityFrom: 'generated' };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Give each named alias its own client identity (`dario accounts identity
|
|
241
|
+
* --fresh`). Tokens, organization and account identity are untouched — only
|
|
242
|
+
* `deviceId`/`accountUuid` change, and the running proxy presents the new pair on
|
|
243
|
+
* the seat's next request (pool.add takes a changed on-disk identity).
|
|
244
|
+
* Returns the aliases rewritten; an unknown alias is skipped, not an error.
|
|
245
|
+
*/
|
|
246
|
+
export async function regenerateClientIdentity(aliases) {
|
|
247
|
+
const done = [];
|
|
248
|
+
for (const alias of aliases) {
|
|
249
|
+
const acc = await loadAccount(alias);
|
|
250
|
+
if (!acc)
|
|
251
|
+
continue;
|
|
252
|
+
await saveAccount({ ...acc, deviceId: randomUUID(), accountUuid: randomUUID(), identityFrom: 'generated' });
|
|
253
|
+
done.push(alias);
|
|
254
|
+
}
|
|
255
|
+
return done;
|
|
256
|
+
}
|
|
138
257
|
// Per-alias single-flight map: if a refresh is in flight for an alias,
|
|
139
258
|
// concurrent callers share the same promise instead of issuing parallel
|
|
140
259
|
// refresh_token requests. The pool's 15-min background timer is the only
|
|
@@ -290,6 +409,23 @@ async function doRefreshAccountToken(creds) {
|
|
|
290
409
|
await saveAccount(updated);
|
|
291
410
|
return updated;
|
|
292
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Fill in `accountId` (and the profile's other fields) on a record that predates
|
|
414
|
+
* them, from one profile GET; saves and returns the record, unchanged when
|
|
415
|
+
* nothing could be learned. Kept OUT of the token refresh itself so the
|
|
416
|
+
* refresh stays a single, sequenced token exchange — a caller with a mocked
|
|
417
|
+
* or injected fetch sees exactly the calls it always did (dario#1263).
|
|
418
|
+
*/
|
|
419
|
+
export async function backfillIdentity(creds, fetchImpl = fetch) {
|
|
420
|
+
if (creds.accountId)
|
|
421
|
+
return creds;
|
|
422
|
+
const profile = await fetchOAuthProfile(creds.accessToken, fetchImpl);
|
|
423
|
+
if (!profile)
|
|
424
|
+
return creds;
|
|
425
|
+
const updated = { ...creds, ...profileFields(profile) };
|
|
426
|
+
await saveAccount(updated);
|
|
427
|
+
return updated;
|
|
428
|
+
}
|
|
293
429
|
/** Test-only — inspect the in-flight map. Production code has no business peeking. */
|
|
294
430
|
export function _accountRefreshesInFlightSizeForTest() {
|
|
295
431
|
return accountRefreshesInFlight.size;
|
|
@@ -373,11 +509,11 @@ export async function addAccountViaOAuth(alias) {
|
|
|
373
509
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${redactSecrets(body.slice(0, 200))}`);
|
|
374
510
|
}
|
|
375
511
|
const tokens = await tokenRes.json();
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
512
|
+
// Who this token is (one GET, never fatal), then which client identity
|
|
513
|
+
// the alias presents — see chooseClientIdentity.
|
|
514
|
+
const profile = await fetchOAuthProfile(tokens.access_token);
|
|
515
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
516
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
381
517
|
const creds = {
|
|
382
518
|
alias,
|
|
383
519
|
accessToken: tokens.access_token,
|
|
@@ -386,7 +522,9 @@ export async function addAccountViaOAuth(alias) {
|
|
|
386
522
|
scopes: tokens.scope?.split(' ') ?? cfg.scopes.split(' '),
|
|
387
523
|
deviceId: identity.deviceId,
|
|
388
524
|
accountUuid: identity.accountUuid,
|
|
525
|
+
identityFrom: identity.identityFrom,
|
|
389
526
|
grantedAt: Date.now(),
|
|
527
|
+
...profileFields(profile),
|
|
390
528
|
};
|
|
391
529
|
await saveAccount(creds);
|
|
392
530
|
resolve(creds);
|
|
@@ -510,10 +648,9 @@ export async function completeAddAccount(alias, code, codeVerifier, state) {
|
|
|
510
648
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${redactSecrets(body.slice(0, 200))}`);
|
|
511
649
|
}
|
|
512
650
|
const tokens = await tokenRes.json();
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
};
|
|
651
|
+
const profile = await fetchOAuthProfile(tokens.access_token);
|
|
652
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
653
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
517
654
|
const creds = {
|
|
518
655
|
alias,
|
|
519
656
|
accessToken: tokens.access_token,
|
|
@@ -522,7 +659,9 @@ export async function completeAddAccount(alias, code, codeVerifier, state) {
|
|
|
522
659
|
scopes: tokens.scope?.split(' ') ?? cfg.scopes.split(' '),
|
|
523
660
|
deviceId: identity.deviceId,
|
|
524
661
|
accountUuid: identity.accountUuid,
|
|
662
|
+
identityFrom: identity.identityFrom,
|
|
525
663
|
grantedAt: Date.now(),
|
|
664
|
+
...profileFields(profile),
|
|
526
665
|
};
|
|
527
666
|
await saveAccount(creds);
|
|
528
667
|
return creds;
|
|
@@ -583,12 +722,10 @@ export async function addAccountFromKeychain(alias, target) {
|
|
|
583
722
|
if (!oauth?.accessToken || !oauth?.refreshToken) {
|
|
584
723
|
throw new KeychainImportError(`Keychain entry "${chosen.target}" is missing accessToken/refreshToken — re-authenticate Claude Code.`, 'empty');
|
|
585
724
|
}
|
|
586
|
-
// Same
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
accountUuid: randomUUID(),
|
|
591
|
-
};
|
|
725
|
+
// Same profile-and-policy as addAccountViaOAuth — see chooseClientIdentity.
|
|
726
|
+
const profile = await fetchOAuthProfile(oauth.accessToken);
|
|
727
|
+
const others = (await loadAllAccounts()).filter((a) => a.alias !== alias);
|
|
728
|
+
const identity = await chooseClientIdentity(others, profile);
|
|
592
729
|
const creds = {
|
|
593
730
|
alias,
|
|
594
731
|
accessToken: oauth.accessToken,
|
|
@@ -597,7 +734,9 @@ export async function addAccountFromKeychain(alias, target) {
|
|
|
597
734
|
scopes: oauth.scopes ?? ['user:inference'],
|
|
598
735
|
deviceId: identity.deviceId,
|
|
599
736
|
accountUuid: identity.accountUuid,
|
|
737
|
+
identityFrom: identity.identityFrom,
|
|
600
738
|
grantedAt: oauth.grantedAt,
|
|
739
|
+
...profileFields(profile),
|
|
601
740
|
};
|
|
602
741
|
await saveAccount(creds);
|
|
603
742
|
return creds;
|
|
@@ -645,10 +784,12 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
|
|
|
645
784
|
const tok = creds?.claudeAiOauth;
|
|
646
785
|
if (!tok?.accessToken || !tok?.refreshToken)
|
|
647
786
|
return null;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
787
|
+
// The login seat IS the machine's Claude Code account, so its identity is
|
|
788
|
+
// the local one when there is one — the policy in chooseClientIdentity
|
|
789
|
+
// exists for the OTHER aliases.
|
|
790
|
+
const cc = await detectClaudeIdentity();
|
|
791
|
+
const identity = cc ?? { deviceId: randomUUID(), accountUuid: randomUUID() };
|
|
792
|
+
const profile = await fetchOAuthProfile(tok.accessToken);
|
|
652
793
|
await saveAccount({
|
|
653
794
|
alias,
|
|
654
795
|
accessToken: tok.accessToken,
|
|
@@ -657,7 +798,9 @@ export async function ensureLoginCredentialsInPool(alias = MIGRATED_LOGIN_ALIAS)
|
|
|
657
798
|
scopes: tok.scopes ?? [],
|
|
658
799
|
deviceId: identity.deviceId,
|
|
659
800
|
accountUuid: identity.accountUuid,
|
|
801
|
+
identityFrom: cc ? 'claude-code' : 'generated',
|
|
660
802
|
grantedAt: tok.grantedAt,
|
|
803
|
+
...profileFields(profile),
|
|
661
804
|
});
|
|
662
805
|
return alias;
|
|
663
806
|
}
|
package/dist/admin-api.d.ts
CHANGED
|
@@ -115,8 +115,21 @@ export interface AdminAccountLive {
|
|
|
115
115
|
lastRejectedAt: number | null;
|
|
116
116
|
/** Organization observed on this seat's responses, or `null` if none yet (dario#1244). */
|
|
117
117
|
organizationId: string | null;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Other aliases that are the SAME Anthropic account (same OAuth account
|
|
120
|
+
* uuid) — one subscription under several aliases (dario#1244). Kept under
|
|
121
|
+
* its original name for readers of the older payload; `sameAccountAs` is the
|
|
122
|
+
* same list. Until 6.0.38 this was inferred from a shared reset second,
|
|
123
|
+
* which Anthropic's 20-minute reset grid makes meaningless (dario#1263).
|
|
124
|
+
*/
|
|
119
125
|
sharesWindowWith: string[];
|
|
126
|
+
sameAccountAs?: string[];
|
|
127
|
+
/** OAuth account uuid behind the seat's token, or null until identified. */
|
|
128
|
+
accountId?: string | null;
|
|
129
|
+
/** Masked email on that account, or null. */
|
|
130
|
+
accountEmail?: string | null;
|
|
131
|
+
rateLimitTier?: string | null;
|
|
132
|
+
seatTier?: string | null;
|
|
120
133
|
/** Peer instance whose reading this seat currently carries (shared pool state), or `null` for this instance's own. */
|
|
121
134
|
readingFrom: string | null;
|
|
122
135
|
/**
|
package/dist/admin-api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskEmail } from './pool.js';
|
|
1
2
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases, loadAccount, } from './accounts.js';
|
|
3
4
|
import { parseManualPaste } from './oauth.js';
|
|
@@ -362,6 +363,10 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
362
363
|
// From the record (written with the seat's last token refresh), so
|
|
363
364
|
// it is known without a live pool entry.
|
|
364
365
|
organization_id: r.organizationId ?? null,
|
|
366
|
+
// Identity from the record (grant-time OAuth profile); the live
|
|
367
|
+
// block below repeats it when the seat is in the running pool.
|
|
368
|
+
account_id: r.accountId ?? null,
|
|
369
|
+
account_email: maskEmail(r.accountEmail),
|
|
365
370
|
// Inline the running pool's live status when this account is in it.
|
|
366
371
|
...(l ? {
|
|
367
372
|
util5h: l.util5h,
|
|
@@ -375,6 +380,11 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
375
380
|
reset_in_ms: l.resetInMs ?? null,
|
|
376
381
|
...(l.organizationId ? { organization_id: l.organizationId } : {}),
|
|
377
382
|
shares_window_with: l.sharesWindowWith ?? [],
|
|
383
|
+
same_account_as: l.sameAccountAs ?? l.sharesWindowWith ?? [],
|
|
384
|
+
...(l.accountId ? { account_id: l.accountId } : {}),
|
|
385
|
+
...(l.accountEmail ? { account_email: l.accountEmail } : {}),
|
|
386
|
+
...(l.rateLimitTier ? { rate_limit_tier: l.rateLimitTier } : {}),
|
|
387
|
+
...(l.seatTier ? { seat_tier: l.seatTier } : {}),
|
|
378
388
|
reading_from: l.readingFrom ?? null,
|
|
379
389
|
claim: l.claim,
|
|
380
390
|
status: l.status,
|
package/dist/cli.d.ts
CHANGED
|
@@ -130,6 +130,9 @@ export interface LiveSeat {
|
|
|
130
130
|
organizationId?: string | null;
|
|
131
131
|
sharesWindowWith?: string[];
|
|
132
132
|
grantedAt?: number | null;
|
|
133
|
+
sameAccountAs?: string[];
|
|
134
|
+
accountId?: string | null;
|
|
135
|
+
accountEmail?: string | null;
|
|
133
136
|
}
|
|
134
137
|
export interface LivePayload {
|
|
135
138
|
mode?: string;
|
|
@@ -165,3 +168,11 @@ export declare function formatLiveAccountsListing(payload: LivePayload, port: nu
|
|
|
165
168
|
* guard from #137 (v3.31.15).
|
|
166
169
|
*/
|
|
167
170
|
export declare function isMainEntry(argv1: string | undefined | null, moduleHref: string, realpath?: (p: string) => string): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* `dario accounts identity` — which client identity each seat presents in
|
|
173
|
+
* `metadata.user_id`, where it came from, and which seats share one across
|
|
174
|
+
* DIFFERENT accounts (dario#1244, 2026-09-08). `--fresh <alias>...` / `--all`
|
|
175
|
+
* rewrites the named seats' identity; the running proxy presents the new one
|
|
176
|
+
* on each seat's next request, no restart.
|
|
177
|
+
*/
|
|
178
|
+
export declare function runAccountsIdentity(args: string[]): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
|
|
18
18
|
// other startup side effect.
|
|
19
19
|
import { unlink } from 'node:fs/promises';
|
|
20
|
+
import { loadAllAccounts as loadAllAccountsForIdentity, regenerateClientIdentity } from './accounts.js';
|
|
21
|
+
import { maskEmail } from './pool.js';
|
|
20
22
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
21
23
|
import { join } from 'node:path';
|
|
22
24
|
import { homedir } from 'node:os';
|
|
@@ -915,7 +917,7 @@ export function formatLiveAccountsListing(payload, port, now) {
|
|
|
915
917
|
lines.push(' ────────────────');
|
|
916
918
|
lines.push('');
|
|
917
919
|
const windows = typeof payload.distinctWindows === 'number' ? payload.distinctWindows : seats.length;
|
|
918
|
-
lines.push(` Pool of ${seats.length} (${seats.length === 1 ? '1 seat' : seats.length + ' seats'} on ${windows} distinct
|
|
920
|
+
lines.push(` Pool of ${seats.length} (${seats.length === 1 ? '1 seat' : seats.length + ' seats'} on ${windows} distinct account${windows === 1 ? '' : 's'})`);
|
|
919
921
|
lines.push('');
|
|
920
922
|
for (const s of seats) {
|
|
921
923
|
const alias = typeof s.alias === 'string' ? s.alias : '(unnamed)';
|
|
@@ -932,7 +934,11 @@ export function formatLiveAccountsListing(payload, port, now) {
|
|
|
932
934
|
`429s ${num(s.rejectedCount)}`,
|
|
933
935
|
...(next ? [next] : []),
|
|
934
936
|
typeof s.organizationId === 'string' && s.organizationId ? `org ${s.organizationId.slice(0, 8)}…` : 'org not yet observed',
|
|
935
|
-
...(
|
|
937
|
+
...(typeof s.accountEmail === 'string' && s.accountEmail ? [`account ${s.accountEmail}`] : []),
|
|
938
|
+
...((() => {
|
|
939
|
+
const same = Array.isArray(s.sameAccountAs) ? s.sameAccountAs : Array.isArray(s.sharesWindowWith) ? s.sharesWindowWith : [];
|
|
940
|
+
return same.length > 0 ? [`same account as ${same.join(', ')}`] : [];
|
|
941
|
+
})()),
|
|
936
942
|
];
|
|
937
943
|
lines.push(` ${''.padEnd(20)} ${facts.join(' · ')}`);
|
|
938
944
|
lines.push(` ${''.padEnd(20)} ${describeGrantAge(grantAge(typeof s.grantedAt === 'number' ? s.grantedAt : undefined, now))}`);
|
|
@@ -1232,8 +1238,12 @@ async function accounts() {
|
|
|
1232
1238
|
}
|
|
1233
1239
|
return;
|
|
1234
1240
|
}
|
|
1241
|
+
if (sub === 'identity') {
|
|
1242
|
+
await runAccountsIdentity(args);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1235
1245
|
console.error(`[dario] Unknown accounts subcommand: ${sub}`);
|
|
1236
|
-
console.error('Usage: dario accounts [list|add <alias>|check <alias>|remove <alias
|
|
1246
|
+
console.error('Usage: dario accounts [list|add <alias>|check <alias>|remove <alias>|identity [--fresh <alias>...|--all]]');
|
|
1237
1247
|
process.exit(1);
|
|
1238
1248
|
}
|
|
1239
1249
|
/**
|
|
@@ -2571,3 +2581,70 @@ if (isDirectEntry) {
|
|
|
2571
2581
|
process.exit(1);
|
|
2572
2582
|
});
|
|
2573
2583
|
}
|
|
2584
|
+
/**
|
|
2585
|
+
* `dario accounts identity` — which client identity each seat presents in
|
|
2586
|
+
* `metadata.user_id`, where it came from, and which seats share one across
|
|
2587
|
+
* DIFFERENT accounts (dario#1244, 2026-09-08). `--fresh <alias>...` / `--all`
|
|
2588
|
+
* rewrites the named seats' identity; the running proxy presents the new one
|
|
2589
|
+
* on each seat's next request, no restart.
|
|
2590
|
+
*/
|
|
2591
|
+
export async function runAccountsIdentity(args) {
|
|
2592
|
+
const fresh = args.includes('--fresh');
|
|
2593
|
+
const all = args.includes('--all');
|
|
2594
|
+
const named = args.slice(2).filter((a) => !a.startsWith('--'));
|
|
2595
|
+
const accounts = await loadAllAccountsForIdentity();
|
|
2596
|
+
if (fresh) {
|
|
2597
|
+
const targets = all ? accounts.map((a) => a.alias) : named;
|
|
2598
|
+
if (targets.length === 0) {
|
|
2599
|
+
console.error('');
|
|
2600
|
+
console.error(' Usage: dario accounts identity --fresh <alias> [<alias>...] or --fresh --all');
|
|
2601
|
+
console.error('');
|
|
2602
|
+
process.exit(1);
|
|
2603
|
+
}
|
|
2604
|
+
const done = await regenerateClientIdentity(targets);
|
|
2605
|
+
for (const alias of done)
|
|
2606
|
+
console.log(`[dario] "${alias}": fresh client identity written — the running proxy presents it on the seat's next request.`);
|
|
2607
|
+
const missing = targets.filter((t) => !done.includes(t));
|
|
2608
|
+
for (const alias of missing)
|
|
2609
|
+
console.error(`[dario] No account "${alias}" found.`);
|
|
2610
|
+
if (missing.length > 0)
|
|
2611
|
+
process.exit(1);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
console.log('');
|
|
2615
|
+
console.log(' dario — Client identity per seat');
|
|
2616
|
+
console.log(' ────────────────');
|
|
2617
|
+
console.log('');
|
|
2618
|
+
const byIdentity = new Map();
|
|
2619
|
+
for (const a of accounts) {
|
|
2620
|
+
const k = `${a.deviceId}|${a.accountUuid}`;
|
|
2621
|
+
const list = byIdentity.get(k);
|
|
2622
|
+
if (list)
|
|
2623
|
+
list.push(a.alias);
|
|
2624
|
+
else
|
|
2625
|
+
byIdentity.set(k, [a.alias]);
|
|
2626
|
+
}
|
|
2627
|
+
for (const a of accounts) {
|
|
2628
|
+
const source = a.identityFrom ?? (a.deviceId ? 'unrecorded' : 'none');
|
|
2629
|
+
const who = a.accountEmail ? maskEmail(a.accountEmail) : a.accountId ? `${a.accountId.slice(0, 8)}…` : 'not yet identified';
|
|
2630
|
+
const shared = (byIdentity.get(`${a.deviceId}|${a.accountUuid}`) ?? []).filter((x) => x !== a.alias);
|
|
2631
|
+
const sharedNote = shared.length > 0 ? ` · same client identity as ${shared.join(', ')}` : '';
|
|
2632
|
+
console.log(` ${a.alias.padEnd(20)} identity ${source.padEnd(11)} device ${(a.deviceId || '(empty)').slice(0, 8)}… account ${who}${sharedNote}`);
|
|
2633
|
+
}
|
|
2634
|
+
const spanning = [...byIdentity.values()].filter((aliases) => {
|
|
2635
|
+
if (aliases.length < 2)
|
|
2636
|
+
return false;
|
|
2637
|
+
const ids = new Set(aliases.map((al) => accounts.find((a) => a.alias === al)?.accountId));
|
|
2638
|
+
return ids.size > 1 || ids.has(undefined);
|
|
2639
|
+
});
|
|
2640
|
+
console.log('');
|
|
2641
|
+
if (spanning.length > 0) {
|
|
2642
|
+
console.log(` ⚠ ${spanning.length} client identit${spanning.length === 1 ? 'y is' : 'ies are'} shared across different (or unidentified) accounts.`);
|
|
2643
|
+
console.log(' Anthropic ties usage and limits to the identity it sees. Give each seat its own:');
|
|
2644
|
+
console.log(` dario accounts identity --fresh ${spanning.flat().join(' ')}`);
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
console.log(' OK every seat presents its own client identity, or shares one only with the same account.');
|
|
2648
|
+
}
|
|
2649
|
+
console.log('');
|
|
2650
|
+
}
|
package/dist/doctor-core.d.ts
CHANGED
|
@@ -156,6 +156,40 @@ export interface OrganizationsInput {
|
|
|
156
156
|
* until at least two seats have been observed.
|
|
157
157
|
*/
|
|
158
158
|
export declare function checkOrganizations(input: OrganizationsInput): Check[];
|
|
159
|
+
export interface AccountIdentityInput {
|
|
160
|
+
accounts: Array<{
|
|
161
|
+
alias: string;
|
|
162
|
+
accountId?: string;
|
|
163
|
+
accountEmail?: string;
|
|
164
|
+
}>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The Accounts doctor row (dario#1244, #1263): how many distinct Anthropic
|
|
168
|
+
* accounts the pool's seats are, from the OAuth account uuid each record
|
|
169
|
+
* carries — the fact, not an inference from reset seconds. Two seats with one
|
|
170
|
+
* uuid ARE one subscription under two aliases; a seat without one is not yet
|
|
171
|
+
* identified (its next token refresh fills it in) and is never guessed at.
|
|
172
|
+
*/
|
|
173
|
+
export declare function checkAccountIdentity(input: AccountIdentityInput): Check[];
|
|
174
|
+
export interface SharedClientIdentityInput {
|
|
175
|
+
accounts: Array<{
|
|
176
|
+
alias: string;
|
|
177
|
+
deviceId: string;
|
|
178
|
+
accountUuid: string;
|
|
179
|
+
accountId?: string;
|
|
180
|
+
identityFrom?: string;
|
|
181
|
+
}>;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The Client identity doctor row (dario#1244, 2026-09-08). Every `accounts add`
|
|
185
|
+
* used to copy the machine's Claude Code identity into the new alias, so a
|
|
186
|
+
* pool of colleagues' tokens on one machine presented ONE `device_id` /
|
|
187
|
+
* `account_uuid` in `metadata.user_id` across every OAuth account it held.
|
|
188
|
+
* Anthropic ties what it sees to that identity (see checkIdentityDrift).
|
|
189
|
+
* Seats that are genuinely the same account may share it; seats that are
|
|
190
|
+
* different accounts, or not yet identified, must not.
|
|
191
|
+
*/
|
|
192
|
+
export declare function checkSharedClientIdentity(input: SharedClientIdentityInput): Check[];
|
|
159
193
|
export declare function checkIdentityDrift(input: IdentityDriftInput): Check[];
|
|
160
194
|
export declare function probeNpmLatestCC(): string | null;
|
|
161
195
|
/**
|