nterminal 1.3.4 → 1.3.5
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/bin/nterminal.js +8 -3
- package/dist/client/assets/MarkdownPreview-BIrwUAaa.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-H8ZsISc6.js → TranscriptMarkdownBody-CI0ztvMX.js} +1 -1
- package/dist/client/assets/{index-DxfBq-4P.js → index-EN8ngMgv.js} +1 -1
- package/dist/client/assets/index-JQJ1Eet1.js +58 -0
- package/dist/client/assets/index-xkL4h3qt.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/scripts/hooks.js +83 -0
- package/dist/scripts/hooks.js.map +1 -0
- package/dist/server/accounts/accountService.d.ts +23 -1
- package/dist/server/accounts/accountService.js +235 -103
- package/dist/server/accounts/accountService.js.map +1 -1
- package/dist/server/accounts/providerRequests.d.ts +12 -0
- package/dist/server/accounts/providerRequests.js +159 -0
- package/dist/server/accounts/providerRequests.js.map +1 -0
- package/dist/server/agent/agentRoutes.js +11 -0
- package/dist/server/agent/agentRoutes.js.map +1 -1
- package/dist/server/hooks/helperJournal.d.ts +16 -0
- package/dist/server/hooks/helperJournal.js +74 -0
- package/dist/server/hooks/helperJournal.js.map +1 -0
- package/dist/server/hooks/hookBinding.d.ts +39 -0
- package/dist/server/hooks/hookBinding.js +91 -0
- package/dist/server/hooks/hookBinding.js.map +1 -0
- package/dist/server/hooks/hookFiles.d.ts +24 -0
- package/dist/server/hooks/hookFiles.js +160 -0
- package/dist/server/hooks/hookFiles.js.map +1 -0
- package/dist/server/hooks/hookInstallation.d.ts +79 -0
- package/dist/server/hooks/hookInstallation.js +978 -0
- package/dist/server/hooks/hookInstallation.js.map +1 -0
- package/dist/server/hooks/hookProcess.d.ts +5 -0
- package/dist/server/hooks/hookProcess.js +85 -0
- package/dist/server/hooks/hookProcess.js.map +1 -0
- package/dist/server/hooks/hookReceiver.d.ts +66 -0
- package/dist/server/hooks/hookReceiver.js +549 -0
- package/dist/server/hooks/hookReceiver.js.map +1 -0
- package/dist/server/hooks/lifecycle.d.ts +40 -0
- package/dist/server/hooks/lifecycle.js +200 -0
- package/dist/server/hooks/lifecycle.js.map +1 -0
- package/dist/server/hooks/publicationLock.d.ts +3 -0
- package/dist/server/hooks/publicationLock.js +137 -0
- package/dist/server/hooks/publicationLock.js.map +1 -0
- package/dist/server/hooks/verifiedLock.d.ts +24 -0
- package/dist/server/hooks/verifiedLock.js +207 -0
- package/dist/server/hooks/verifiedLock.js.map +1 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +6 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.js +69 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/notifications/agentCompletionMonitor.d.ts +16 -1
- package/dist/server/notifications/agentCompletionMonitor.js +261 -47
- package/dist/server/notifications/agentCompletionMonitor.js.map +1 -1
- package/dist/server/notifications/completionNotificationRouter.d.ts +2 -0
- package/dist/server/notifications/completionNotificationRouter.js +9 -0
- package/dist/server/notifications/completionNotificationRouter.js.map +1 -1
- package/dist/server/routes/accountRoutes.js +11 -0
- package/dist/server/routes/accountRoutes.js.map +1 -1
- package/dist/server/routes/activityRoutes.d.ts +13 -0
- package/dist/server/routes/activityRoutes.js +165 -0
- package/dist/server/routes/activityRoutes.js.map +1 -0
- package/dist/server/routes/agentManagementRoutes.js +8 -1
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/routes/hookRoutes.d.ts +16 -0
- package/dist/server/routes/hookRoutes.js +77 -0
- package/dist/server/routes/hookRoutes.js.map +1 -0
- package/dist/server/routes/terminalRoutes.js +4 -0
- package/dist/server/routes/terminalRoutes.js.map +1 -1
- package/dist/server/routes/transcriptQuery.d.ts +2 -1
- package/dist/server/routes/transcriptQuery.js +3 -1
- package/dist/server/routes/transcriptQuery.js.map +1 -1
- package/dist/server/terminal/PtyAdapter.d.ts +18 -0
- package/dist/server/terminal/PtyAdapter.js.map +1 -1
- package/dist/server/terminal/TerminalManager.d.ts +61 -0
- package/dist/server/terminal/TerminalManager.js +665 -12
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/TmuxPtyAdapter.d.ts +14 -2
- package/dist/server/terminal/TmuxPtyAdapter.js +99 -15
- package/dist/server/terminal/TmuxPtyAdapter.js.map +1 -1
- package/dist/server/terminal/activityChannel.d.ts +57 -0
- package/dist/server/terminal/activityChannel.js +166 -0
- package/dist/server/terminal/activityChannel.js.map +1 -0
- package/dist/server/terminal/agentTranscriptStatus.d.ts +12 -0
- package/dist/server/terminal/agentTranscriptStatus.js +78 -7
- package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
- package/dist/server/terminal/codexTranscriptSource.d.ts +2 -0
- package/dist/server/terminal/codexTranscriptSource.js +41 -37
- package/dist/server/terminal/codexTranscriptSource.js.map +1 -1
- package/dist/server/terminal/subagentSources.js +2 -1
- package/dist/server/terminal/subagentSources.js.map +1 -1
- package/dist/server/terminal/transcriptHeader.d.ts +85 -0
- package/dist/server/terminal/transcriptHeader.js +376 -0
- package/dist/server/terminal/transcriptHeader.js.map +1 -0
- package/dist/server/terminal/transcriptHistory.d.ts +16 -2
- package/dist/server/terminal/transcriptHistory.js +120 -21
- package/dist/server/terminal/transcriptHistory.js.map +1 -1
- package/dist/server/terminal/transcriptStore.d.ts +6 -0
- package/dist/server/terminal/transcriptStore.js +222 -26
- package/dist/server/terminal/transcriptStore.js.map +1 -1
- package/dist/shared/activityProbe.d.ts +10 -0
- package/dist/shared/activityProbe.js +24 -0
- package/dist/shared/activityProbe.js.map +1 -0
- package/dist/shared/protocol.d.ts +45 -0
- package/dist/shared/protocol.js.map +1 -1
- package/dist/shared/types.d.ts +9 -1
- package/docs/features.md +10 -0
- package/docs/provider-hooks.md +287 -0
- package/docs/terminal-history.md +10 -0
- package/package.json +2 -1
- package/scripts/nterminal-hook.cjs +171 -0
- package/scripts/nterminalctl +26 -2
- package/dist/client/assets/MarkdownPreview-DjDO4lEl.js +0 -1
- package/dist/client/assets/index-CIYazAGv.css +0 -1
- package/dist/client/assets/index-DwvLN_WJ.js +0 -56
|
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { ProviderRequestError, requestAccountUsage, requestTokenRefresh, unavailableUsage } from './providerRequests.js';
|
|
7
8
|
const VAULT_DIRECTORY = 'credential-vault';
|
|
8
9
|
const VAULT_INDEX = 'index.json';
|
|
9
10
|
const VAULT_KEY = 'key';
|
|
@@ -19,13 +20,8 @@ const UNKNOWN_EXPIRY_RETRY_MS = 15 * 60_000;
|
|
|
19
20
|
const UNKNOWN_EXPIRY_REFRESH_INTERVAL_MS = 7 * 24 * 60 * 60_000;
|
|
20
21
|
const PROVIDER_TERMINATE_GRACE_MS = 2_000;
|
|
21
22
|
const TERMINATION_ACKNOWLEDGEMENT_TTL_MS = 5 * 60_000;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// JSON (unlike Claude's form-encoded endpoint), and the client identifier is
|
|
25
|
-
// validated by auth.openai.com.
|
|
26
|
-
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
27
|
-
const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
28
|
-
const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
23
|
+
const USAGE_CACHE_TTL_MS = 2 * 60_000;
|
|
24
|
+
const USAGE_FAILURE_TTL_MS = 30_000;
|
|
29
25
|
export class AccountServiceError extends Error {
|
|
30
26
|
code;
|
|
31
27
|
constructor(code, message = code) {
|
|
@@ -36,7 +32,7 @@ export class AccountServiceError extends Error {
|
|
|
36
32
|
get statusCode() {
|
|
37
33
|
if (this.code === 'preview_mode')
|
|
38
34
|
return 403;
|
|
39
|
-
if (['provider_panes_running', 'account_operation_busy', 'account_credential_changed', 'account_login_already_pending'].includes(this.code))
|
|
35
|
+
if (['provider_panes_running', 'account_operation_busy', 'account_credential_changed', 'account_login_already_pending', 'active_cli_refresh_deferred'].includes(this.code))
|
|
40
36
|
return 409;
|
|
41
37
|
return 400;
|
|
42
38
|
}
|
|
@@ -52,8 +48,14 @@ export class AccountService {
|
|
|
52
48
|
operationInFlight = false;
|
|
53
49
|
disposed = false;
|
|
54
50
|
retryAfter = new Map();
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
usageCache = new Map();
|
|
52
|
+
usageInFlight = new Map();
|
|
53
|
+
fetcher;
|
|
54
|
+
onDiagnostic;
|
|
55
|
+
diagnosticSignatures = new Map();
|
|
56
|
+
// In-memory acknowledgement set once an explicit stop left no provider
|
|
57
|
+
// processes. It only bridges the pane-status poll lag; live processes always
|
|
58
|
+
// block, so a session started afterwards cannot race an account change.
|
|
57
59
|
terminationAcknowledgements = new Map();
|
|
58
60
|
processSnapshotInFlight = null;
|
|
59
61
|
processProbe;
|
|
@@ -70,6 +72,8 @@ export class AccountService {
|
|
|
70
72
|
// vault instead of touching production account metadata.
|
|
71
73
|
this.vaultPath = path.join(path.dirname(config.statePath), VAULT_DIRECTORY);
|
|
72
74
|
this.now = options.now ?? (() => new Date());
|
|
75
|
+
this.fetcher = options.fetcher ?? ((...args) => fetch(...args));
|
|
76
|
+
this.onDiagnostic = options.onDiagnostic ?? (() => undefined);
|
|
73
77
|
this.processLister = options.processLister ?? (async (provider) => providerProcesses(provider, await this.listProcessSnapshot()));
|
|
74
78
|
this.paneProcessResolver = options.paneProcessResolver ?? (options.processLister
|
|
75
79
|
? async () => new Map()
|
|
@@ -95,7 +99,7 @@ export class AccountService {
|
|
|
95
99
|
async synchronize() {
|
|
96
100
|
return this.mutate(async () => {
|
|
97
101
|
const index = await this.readIndex();
|
|
98
|
-
await this.captureActiveCredentials(index);
|
|
102
|
+
await this.captureActiveCredentials(index, undefined, true);
|
|
99
103
|
});
|
|
100
104
|
}
|
|
101
105
|
select(provider, profileId) {
|
|
@@ -116,6 +120,80 @@ export class AccountService {
|
|
|
116
120
|
refresh(provider, profileId) {
|
|
117
121
|
return this.mutate(() => this.refreshUnlocked(provider, profileId));
|
|
118
122
|
}
|
|
123
|
+
usage(provider) {
|
|
124
|
+
if (this.config.previewMode)
|
|
125
|
+
return Promise.reject(new AccountServiceError('preview_mode'));
|
|
126
|
+
if (this.disposed)
|
|
127
|
+
return Promise.reject(new AccountServiceError('account_operation_busy'));
|
|
128
|
+
const pending = this.usageInFlight.get(provider);
|
|
129
|
+
if (pending)
|
|
130
|
+
return pending;
|
|
131
|
+
const request = this.readUsage(provider);
|
|
132
|
+
this.usageInFlight.set(provider, request);
|
|
133
|
+
void request.finally(() => {
|
|
134
|
+
if (this.usageInFlight.get(provider) === request)
|
|
135
|
+
this.usageInFlight.delete(provider);
|
|
136
|
+
}).catch(() => undefined);
|
|
137
|
+
return request;
|
|
138
|
+
}
|
|
139
|
+
async readUsage(provider) {
|
|
140
|
+
const index = await this.readIndex();
|
|
141
|
+
const profiles = index.profiles.filter((profile) => profile.provider === provider);
|
|
142
|
+
const results = [];
|
|
143
|
+
const signal = AbortSignal.timeout(10_000);
|
|
144
|
+
let cursor = 0;
|
|
145
|
+
const worker = async () => {
|
|
146
|
+
while (cursor < profiles.length && !this.disposed) {
|
|
147
|
+
const profile = profiles[cursor++];
|
|
148
|
+
const usage = signal.aborted ? unavailableUsage(this.now(), 'usage_timeout') : await this.readProfileUsage(index, profile, signal);
|
|
149
|
+
if (usage)
|
|
150
|
+
results.push({ profileId: profile.id, usage });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
await Promise.all([worker(), worker()]);
|
|
154
|
+
for (const id of this.usageCache.keys())
|
|
155
|
+
if (!index.profiles.some((profile) => profile.id === id))
|
|
156
|
+
this.usageCache.delete(id);
|
|
157
|
+
return { provider, profiles: results };
|
|
158
|
+
}
|
|
159
|
+
async readProfileUsage(index, profile, signal) {
|
|
160
|
+
const provider = profile.provider;
|
|
161
|
+
const cached = this.usageCache.get(profile.id);
|
|
162
|
+
let usage;
|
|
163
|
+
const credentialHash = profile.credentialHash;
|
|
164
|
+
try {
|
|
165
|
+
if (index.active[provider] !== profile.id && cached && cached.until > this.now().getTime() && cached.credentialHash === credentialHash)
|
|
166
|
+
return cached.usage;
|
|
167
|
+
const credentials = index.active[provider] === profile.id
|
|
168
|
+
? await this.readCredentialFile(provider) : await this.readVaultCredentials(profile.id);
|
|
169
|
+
if (!credentials || hashCredential(credentials) !== credentialHash)
|
|
170
|
+
throw new AccountServiceError('account_credential_changed');
|
|
171
|
+
if (cached && cached.until > this.now().getTime() && cached.credentialHash === credentialHash)
|
|
172
|
+
return cached.usage;
|
|
173
|
+
usage = await requestAccountUsage(provider, credentials, this.now(), this.fetcher, signal);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
usage = unavailableUsage(this.now(), error instanceof AccountServiceError ? error.code : 'usage_unavailable');
|
|
177
|
+
}
|
|
178
|
+
// A switch, login, removal, or refresh can finish while a quota request is
|
|
179
|
+
// in flight. Never associate its response with a different credential.
|
|
180
|
+
const latest = await this.readIndex();
|
|
181
|
+
const current = latest.profiles.find((entry) => entry.id === profile.id && entry.provider === provider);
|
|
182
|
+
if (!current)
|
|
183
|
+
return null;
|
|
184
|
+
if (current.credentialHash !== credentialHash || latest.active[provider] !== index.active[provider]) {
|
|
185
|
+
usage = unavailableUsage(this.now(), 'account_credential_changed');
|
|
186
|
+
}
|
|
187
|
+
else if (latest.active[provider] === profile.id) {
|
|
188
|
+
const live = await this.readCredentialFile(provider).catch(() => null);
|
|
189
|
+
if (!live || hashCredential(live) !== credentialHash)
|
|
190
|
+
usage = unavailableUsage(this.now(), 'account_credential_changed');
|
|
191
|
+
}
|
|
192
|
+
if (usage.code !== 'account_credential_changed' && credentialHash) {
|
|
193
|
+
this.usageCache.set(profile.id, { credentialHash, until: this.now().getTime() + (usage.status === 'available' ? USAGE_CACHE_TTL_MS : USAGE_FAILURE_TTL_MS), usage });
|
|
194
|
+
}
|
|
195
|
+
return usage;
|
|
196
|
+
}
|
|
119
197
|
terminateProviderSessions(provider) {
|
|
120
198
|
return this.mutate(() => this.terminateProviderSessionsUnlocked(provider));
|
|
121
199
|
}
|
|
@@ -143,7 +221,7 @@ export class AccountService {
|
|
|
143
221
|
}));
|
|
144
222
|
const pendingReady = new Map();
|
|
145
223
|
await Promise.all(['codex', 'claude'].map(async (provider) => {
|
|
146
|
-
pendingReady.set(provider, Boolean(refreshed.pending[provider] && await this.readCredentialFile(provider)));
|
|
224
|
+
pendingReady.set(provider, Boolean(refreshed.pending[provider] && await this.readCredentialFile(provider).catch(() => null)));
|
|
147
225
|
}));
|
|
148
226
|
return {
|
|
149
227
|
providers: ['codex', 'claude'].map((provider) => this.providerStatus(refreshed, provider, running, processes.get(provider) ?? [], processCheckUnavailable, pendingReady.get(provider) === true))
|
|
@@ -154,7 +232,7 @@ export class AccountService {
|
|
|
154
232
|
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
155
233
|
if (index.pending[provider])
|
|
156
234
|
throw new AccountServiceError('account_login_already_pending');
|
|
157
|
-
await this.captureActiveCredentials(index);
|
|
235
|
+
await this.captureActiveCredentials(index, provider);
|
|
158
236
|
const profile = index.profiles.find((candidate) => candidate.id === profileId && candidate.provider === provider);
|
|
159
237
|
if (!profile)
|
|
160
238
|
throw new AccountServiceError('account_not_found');
|
|
@@ -187,7 +265,7 @@ export class AccountService {
|
|
|
187
265
|
const index = await this.readIndex();
|
|
188
266
|
if (index.pending[provider])
|
|
189
267
|
throw new AccountServiceError('account_login_already_pending');
|
|
190
|
-
await this.captureActiveCredentials(index);
|
|
268
|
+
await this.captureActiveCredentials(index, provider);
|
|
191
269
|
const profile = index.profiles.find((candidate) => candidate.id === profileId && candidate.provider === provider);
|
|
192
270
|
if (!profile)
|
|
193
271
|
throw new AccountServiceError('account_not_found');
|
|
@@ -198,6 +276,8 @@ export class AccountService {
|
|
|
198
276
|
await this.writeIndex(index);
|
|
199
277
|
await rm(this.profilePath(profile.id), { force: true });
|
|
200
278
|
this.retryAfter.delete(profile.id);
|
|
279
|
+
this.usageCache.delete(profile.id);
|
|
280
|
+
this.diagnosticSignatures.delete(`account.refresh:${provider}:${profile.id}`);
|
|
201
281
|
return this.status();
|
|
202
282
|
}
|
|
203
283
|
async beginLoginUnlocked(provider) {
|
|
@@ -205,7 +285,7 @@ export class AccountService {
|
|
|
205
285
|
await this.assertNoRunningPanes(provider, { allowTerminationAcknowledgement: true });
|
|
206
286
|
if (index.pending[provider])
|
|
207
287
|
throw new AccountServiceError('account_login_already_pending');
|
|
208
|
-
await this.captureActiveCredentials(index);
|
|
288
|
+
await this.captureActiveCredentials(index, provider);
|
|
209
289
|
const current = await this.archiveActiveCredential(index, provider);
|
|
210
290
|
index.pending[provider] = {
|
|
211
291
|
provider,
|
|
@@ -254,7 +334,7 @@ export class AccountService {
|
|
|
254
334
|
const credentials = await this.readCredentialFile(provider);
|
|
255
335
|
if (!credentials)
|
|
256
336
|
throw new AccountServiceError('account_login_not_complete');
|
|
257
|
-
const profile = await this.storeProfile(index, provider, credentials, label
|
|
337
|
+
const profile = await this.storeProfile(index, provider, credentials, label);
|
|
258
338
|
index.active[provider] = profile.id;
|
|
259
339
|
delete index.pending[provider];
|
|
260
340
|
await this.writeIndex(index);
|
|
@@ -286,15 +366,12 @@ export class AccountService {
|
|
|
286
366
|
}
|
|
287
367
|
async refreshUnlocked(provider, profileId) {
|
|
288
368
|
const index = await this.readIndex();
|
|
289
|
-
|
|
290
|
-
throw new AccountServiceError('account_login_already_pending');
|
|
291
|
-
await this.captureActiveCredentials(index);
|
|
369
|
+
await this.captureActiveCredentials(index, provider, true);
|
|
292
370
|
const targetId = profileId ?? index.active[provider];
|
|
293
371
|
const profile = targetId ? index.profiles.find((candidate) => candidate.id === targetId && candidate.provider === provider) : undefined;
|
|
294
372
|
if (!profile)
|
|
295
373
|
throw new AccountServiceError('account_not_found');
|
|
296
|
-
await this.
|
|
297
|
-
await this.writeIndex(index);
|
|
374
|
+
await this.refreshAndRecord(index, profile);
|
|
298
375
|
return this.status();
|
|
299
376
|
}
|
|
300
377
|
start() {
|
|
@@ -328,17 +405,11 @@ export class AccountService {
|
|
|
328
405
|
try {
|
|
329
406
|
await this.mutate(async () => {
|
|
330
407
|
const index = await this.readIndex();
|
|
331
|
-
|
|
332
|
-
return;
|
|
333
|
-
await this.captureActiveCredentials(index);
|
|
408
|
+
await this.captureActiveCredentials(index, profile.provider, true);
|
|
334
409
|
const current = index.profiles.find((entry) => entry.id === profile.id);
|
|
335
410
|
if (!current || !shouldRefreshProfile(current, this.now()))
|
|
336
411
|
return;
|
|
337
|
-
|
|
338
|
-
return;
|
|
339
|
-
await this.refreshProfile(index, current);
|
|
340
|
-
await this.writeIndex(index);
|
|
341
|
-
this.retryAfter.delete(current.id);
|
|
412
|
+
await this.refreshAndRecord(index, current);
|
|
342
413
|
});
|
|
343
414
|
}
|
|
344
415
|
catch (error) {
|
|
@@ -347,20 +418,62 @@ export class AccountService {
|
|
|
347
418
|
// expiry instead of delaying the profile on stale metadata.
|
|
348
419
|
if (error instanceof AccountServiceError && ['account_operation_busy', 'account_credential_changed'].includes(error.code))
|
|
349
420
|
continue;
|
|
350
|
-
|
|
351
|
-
? this.now().getTime() + UNKNOWN_EXPIRY_RETRY_MS
|
|
352
|
-
: refreshRetryAt(profile, this.now());
|
|
353
|
-
this.retryAfter.set(profile.id, retryAt);
|
|
421
|
+
this.retryAfter.set(profile.id, refreshRetryAt(profile, this.now()));
|
|
354
422
|
}
|
|
355
423
|
}
|
|
356
424
|
}
|
|
357
|
-
catch {
|
|
358
|
-
|
|
425
|
+
catch (error) {
|
|
426
|
+
this.reportDiagnostic({ event: 'account.refresh_scan', state: 'retrying', code: serviceErrorCode(error) });
|
|
359
427
|
}
|
|
360
428
|
finally {
|
|
361
429
|
this.refreshInFlight = false;
|
|
362
430
|
}
|
|
363
431
|
}
|
|
432
|
+
async refreshAndRecord(index, profile) {
|
|
433
|
+
const previous = profile.refresh;
|
|
434
|
+
const attemptedAt = this.now().toISOString();
|
|
435
|
+
profile.refresh = {
|
|
436
|
+
state: 'retrying', lastAttemptAt: attemptedAt, lastSuccessAt: previous?.lastSuccessAt ?? null,
|
|
437
|
+
nextRetryAt: null, code: null, httpStatus: null
|
|
438
|
+
};
|
|
439
|
+
await this.writeIndex(index);
|
|
440
|
+
try {
|
|
441
|
+
await this.refreshProfile(index, profile);
|
|
442
|
+
profile.refresh = { ...profile.refresh, state: 'success', lastAttemptAt: attemptedAt, lastSuccessAt: this.now().toISOString(), nextRetryAt: null, code: null, httpStatus: null };
|
|
443
|
+
this.retryAfter.delete(profile.id);
|
|
444
|
+
this.usageCache.delete(profile.id);
|
|
445
|
+
await this.writeIndex(index);
|
|
446
|
+
this.reportDiagnostic({ event: 'account.refresh', provider: profile.provider, profileId: profile.id, state: 'success', code: null });
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (error instanceof AccountServiceError && error.code === 'account_credential_changed') {
|
|
450
|
+
this.reportDiagnostic({ event: 'account.refresh', provider: profile.provider, profileId: profile.id, state: 'deferred', code: error.code });
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
const failure = error instanceof ProviderRequestError ? error : null;
|
|
454
|
+
const code = failure?.code ?? serviceErrorCode(error);
|
|
455
|
+
const state = failure?.kind === 'reauth' || code === 'refresh_token_unavailable' ? 'reauth_required' : ['active_cli_refresh_deferred', 'account_process_check_unavailable'].includes(code) ? 'deferred' : 'retrying';
|
|
456
|
+
const retryMs = failure?.kind === 'rejected' ? UNKNOWN_EXPIRY_RETRY_MS : null;
|
|
457
|
+
const retryAt = state === 'reauth_required' ? null : new Date(Math.max(retryMs ? this.now().getTime() + retryMs : refreshRetryAt(profile, this.now()), this.now().getTime() + (failure?.retryAfterMs ?? 0))).toISOString();
|
|
458
|
+
profile.refresh = { state, lastAttemptAt: attemptedAt, lastSuccessAt: previous?.lastSuccessAt ?? null, nextRetryAt: retryAt, code, httpStatus: failure?.httpStatus ?? null };
|
|
459
|
+
this.reportDiagnostic({ event: 'account.refresh', provider: profile.provider, profileId: profile.id, state, code, httpStatus: failure?.httpStatus ?? null, nextRetryAt: retryAt });
|
|
460
|
+
await this.writeIndex(index);
|
|
461
|
+
if (error instanceof AccountServiceError)
|
|
462
|
+
throw error;
|
|
463
|
+
throw new AccountServiceError(state === 'reauth_required' ? 'account_reauth_required' : 'account_refresh_unavailable');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
reportDiagnostic(event) {
|
|
467
|
+
const key = `${event.event}:${event.provider ?? ''}:${event.profileId ?? ''}`;
|
|
468
|
+
const signature = `${event.state}:${event.code}:${event.httpStatus ?? ''}`;
|
|
469
|
+
if (this.diagnosticSignatures.get(key) === signature)
|
|
470
|
+
return;
|
|
471
|
+
this.diagnosticSignatures.set(key, signature);
|
|
472
|
+
try {
|
|
473
|
+
this.onDiagnostic(event);
|
|
474
|
+
}
|
|
475
|
+
catch { /* Logging cannot interrupt token storage. */ }
|
|
476
|
+
}
|
|
364
477
|
providerStatus(index, provider, running, runningProcesses, processCheckUnavailable, pendingReady) {
|
|
365
478
|
const activeId = index.active[provider] ?? null;
|
|
366
479
|
const profile = activeId ? index.profiles.find((candidate) => candidate.id === activeId) ?? null : null;
|
|
@@ -386,23 +499,18 @@ export class AccountService {
|
|
|
386
499
|
});
|
|
387
500
|
}
|
|
388
501
|
async assertNoRunningPanes(provider, options = {}) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (this.runningPanes().some((pane) => pane.provider === provider) || await this.processProbe(provider)) {
|
|
502
|
+
const acknowledged = options.allowTerminationAcknowledgement === true && this.hasTerminationAcknowledgement(provider);
|
|
503
|
+
if ((!acknowledged && this.runningPanes().some((pane) => pane.provider === provider)) || await this.processProbe(provider)) {
|
|
392
504
|
throw new AccountServiceError('provider_panes_running');
|
|
393
505
|
}
|
|
394
506
|
}
|
|
395
507
|
async terminateProviderSessionsUnlocked(provider) {
|
|
396
508
|
const processes = await this.processLister(provider);
|
|
509
|
+
this.clearTerminationAcknowledgement(provider);
|
|
397
510
|
await this.processTerminator(provider, processes);
|
|
398
|
-
if (provider
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
throw new AccountServiceError('provider_panes_running');
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
this.terminationAcknowledgements.set(provider, this.now().getTime() + TERMINATION_ACKNOWLEDGEMENT_TTL_MS);
|
|
405
|
-
}
|
|
511
|
+
if (await this.processProbe(provider))
|
|
512
|
+
throw new AccountServiceError('provider_panes_running');
|
|
513
|
+
this.terminationAcknowledgements.set(provider, this.now().getTime() + TERMINATION_ACKNOWLEDGEMENT_TTL_MS);
|
|
406
514
|
return this.status();
|
|
407
515
|
}
|
|
408
516
|
hasTerminationAcknowledgement(provider) {
|
|
@@ -433,17 +541,28 @@ export class AccountService {
|
|
|
433
541
|
}
|
|
434
542
|
return this.processSnapshotInFlight;
|
|
435
543
|
}
|
|
436
|
-
async captureActiveCredentials(index) {
|
|
544
|
+
async captureActiveCredentials(index, onlyProvider, tolerateErrors = false) {
|
|
437
545
|
const before = JSON.stringify(index);
|
|
438
|
-
|
|
439
|
-
|
|
546
|
+
const providers = onlyProvider ? [onlyProvider] : ['codex', 'claude'];
|
|
547
|
+
for (const provider of providers) {
|
|
548
|
+
if (index.pending[provider])
|
|
549
|
+
continue;
|
|
550
|
+
try {
|
|
440
551
|
await this.captureActiveCredential(index, provider);
|
|
552
|
+
this.diagnosticSignatures.delete(`account.refresh_scan:${provider}:`);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (!tolerateErrors)
|
|
556
|
+
throw error;
|
|
557
|
+
this.reportDiagnostic({ event: 'account.refresh_scan', provider, state: 'retrying', code: serviceErrorCode(error) });
|
|
558
|
+
}
|
|
441
559
|
}
|
|
442
560
|
if (JSON.stringify(index) !== before)
|
|
443
561
|
await this.writeIndex(index);
|
|
444
562
|
// Only metadata is retained for active accounts. A duplicate can exist
|
|
445
563
|
// briefly during a switch, until the source file and index are committed.
|
|
446
|
-
for (const
|
|
564
|
+
for (const provider of providers) {
|
|
565
|
+
const id = index.active[provider];
|
|
447
566
|
if (id)
|
|
448
567
|
await rm(this.profilePath(id), { force: true });
|
|
449
568
|
}
|
|
@@ -467,7 +586,7 @@ export class AccountService {
|
|
|
467
586
|
await this.removeUnavailableProfile(index, previousId);
|
|
468
587
|
return;
|
|
469
588
|
}
|
|
470
|
-
const profile = await this.storeProfile(index, provider, credentials
|
|
589
|
+
const profile = await this.storeProfile(index, provider, credentials);
|
|
471
590
|
index.active[provider] = profile.id;
|
|
472
591
|
if (previousId !== profile.id)
|
|
473
592
|
await this.removeUnavailableProfile(index, previousId);
|
|
@@ -495,21 +614,27 @@ export class AccountService {
|
|
|
495
614
|
await this.writeVaultCredentials(profile.id, credentials);
|
|
496
615
|
return credentials;
|
|
497
616
|
}
|
|
498
|
-
|
|
617
|
+
// A blank label only names new profiles; re-authenticating an existing
|
|
618
|
+
// account keeps the label the user gave it.
|
|
619
|
+
async storeProfile(index, provider, credentials, label = '') {
|
|
499
620
|
const fingerprint = fingerprintFor(provider, credentials);
|
|
500
621
|
const existing = index.profiles.find((profile) => profile.provider === provider && profile.fingerprint === fingerprint);
|
|
501
622
|
const now = this.now().toISOString();
|
|
502
623
|
const timing = credentialTiming(provider, credentials);
|
|
503
624
|
const profile = existing ?? {
|
|
504
|
-
id: randomUUID(), provider, label: sanitizeLabel(label), createdAt: now, updatedAt: now,
|
|
625
|
+
id: randomUUID(), provider, label: sanitizeLabel(label.trim() || defaultLabel(provider, index.profiles)), createdAt: now, updatedAt: now,
|
|
505
626
|
expiresAt: timing.expiresAt, refreshTokenExpiresAt: timing.refreshTokenExpiresAt, fingerprint
|
|
506
627
|
};
|
|
507
|
-
|
|
628
|
+
if (label.trim())
|
|
629
|
+
profile.label = sanitizeLabel(label);
|
|
508
630
|
profile.updatedAt = now;
|
|
509
631
|
profile.expiresAt = timing.expiresAt;
|
|
510
632
|
profile.refreshTokenExpiresAt = timing.refreshTokenExpiresAt;
|
|
511
633
|
profile.fingerprint = fingerprint;
|
|
512
634
|
profile.credentialHash = hashCredential(credentials);
|
|
635
|
+
delete profile.refresh;
|
|
636
|
+
this.retryAfter.delete(profile.id);
|
|
637
|
+
this.usageCache.delete(profile.id);
|
|
513
638
|
if (!existing)
|
|
514
639
|
index.profiles.push(profile);
|
|
515
640
|
return profile;
|
|
@@ -519,6 +644,9 @@ export class AccountService {
|
|
|
519
644
|
profile.expiresAt = timing.expiresAt;
|
|
520
645
|
profile.refreshTokenExpiresAt = timing.refreshTokenExpiresAt;
|
|
521
646
|
profile.credentialHash = hashCredential(credentials);
|
|
647
|
+
delete profile.refresh;
|
|
648
|
+
this.retryAfter.delete(profile.id);
|
|
649
|
+
this.usageCache.delete(profile.id);
|
|
522
650
|
return profile;
|
|
523
651
|
}
|
|
524
652
|
async refreshProfile(index, profile) {
|
|
@@ -533,46 +661,30 @@ export class AccountService {
|
|
|
533
661
|
if (!refreshToken)
|
|
534
662
|
throw new AccountServiceError('refresh_token_unavailable');
|
|
535
663
|
if (profile.refreshTokenExpiresAt && Date.parse(profile.refreshTokenExpiresAt) <= this.now().getTime()) {
|
|
536
|
-
throw new
|
|
664
|
+
throw new ProviderRequestError('refresh_token_expired', 'reauth');
|
|
537
665
|
}
|
|
538
|
-
|
|
539
|
-
|
|
666
|
+
// Idle describes a conversation, not the CLI's background auth work. Until
|
|
667
|
+
// we can delegate to its native auth owner, never compete with a live CLI.
|
|
668
|
+
if (active)
|
|
669
|
+
await this.assertNoRefreshOwner(profile.provider);
|
|
670
|
+
let refreshResponse;
|
|
540
671
|
try {
|
|
541
|
-
|
|
542
|
-
method: 'POST', headers: request.headers, body: request.body, signal: AbortSignal.timeout(15_000)
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
throw new AccountServiceError('account_refresh_unavailable');
|
|
672
|
+
refreshResponse = await requestTokenRefresh(profile.provider, refreshToken, this.fetcher);
|
|
547
673
|
}
|
|
548
|
-
|
|
674
|
+
catch (error) {
|
|
549
675
|
// A local CLI may have rotated the shared credential while this request
|
|
550
676
|
// was in flight. Prefer that credential instead of treating its old
|
|
551
677
|
// refresh token rejection as a persistent account failure.
|
|
552
678
|
if (active) {
|
|
553
679
|
const live = await this.readCredentialFile(profile.provider);
|
|
554
680
|
if (live && hashCredential(live) !== hashCredential(credentials)) {
|
|
555
|
-
await this.captureActiveCredentials(index);
|
|
681
|
+
await this.captureActiveCredentials(index, profile.provider);
|
|
556
682
|
throw new AccountServiceError('account_credential_changed');
|
|
557
683
|
}
|
|
558
684
|
}
|
|
559
|
-
|
|
560
|
-
// provider failures must still retry before the access token expires.
|
|
561
|
-
if (response.status === 429 || response.status >= 500)
|
|
562
|
-
throw new AccountServiceError('account_refresh_unavailable');
|
|
563
|
-
throw new AccountServiceError('account_refresh_failed');
|
|
564
|
-
}
|
|
565
|
-
let refreshed;
|
|
566
|
-
try {
|
|
567
|
-
refreshed = await response.json();
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
throw new AccountServiceError('account_refresh_failed');
|
|
685
|
+
throw error;
|
|
571
686
|
}
|
|
572
|
-
if (!refreshed || typeof refreshed !== 'object')
|
|
573
|
-
throw new AccountServiceError('account_refresh_failed');
|
|
574
687
|
const refreshedAt = this.now();
|
|
575
|
-
const refreshResponse = refreshed;
|
|
576
688
|
const next = applyRefresh(profile.provider, credentials, refreshResponse, refreshedAt);
|
|
577
689
|
const timing = refreshTiming(profile.provider, next, refreshResponse, refreshedAt, profile);
|
|
578
690
|
if (active) {
|
|
@@ -589,7 +701,7 @@ export class AccountService {
|
|
|
589
701
|
// Preserve the credential that the CLI wrote, whether it refreshed the
|
|
590
702
|
// same account or completed a login for another one. This also updates
|
|
591
703
|
// expiry metadata before the next background scan.
|
|
592
|
-
await this.captureActiveCredentials(index);
|
|
704
|
+
await this.captureActiveCredentials(index, profile.provider);
|
|
593
705
|
throw new AccountServiceError('account_credential_changed');
|
|
594
706
|
}
|
|
595
707
|
// The live credential is shared by every pane for this OS user. The
|
|
@@ -602,6 +714,19 @@ export class AccountService {
|
|
|
602
714
|
}
|
|
603
715
|
await this.updateProfile(profile, profile.provider, next, timing);
|
|
604
716
|
}
|
|
717
|
+
async assertNoRefreshOwner(provider) {
|
|
718
|
+
if (this.runningPanes().some((pane) => pane.provider === provider))
|
|
719
|
+
throw new AccountServiceError('active_cli_refresh_deferred');
|
|
720
|
+
let running;
|
|
721
|
+
try {
|
|
722
|
+
running = await this.processProbe(provider);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
throw new AccountServiceError('account_process_check_unavailable');
|
|
726
|
+
}
|
|
727
|
+
if (running)
|
|
728
|
+
throw new AccountServiceError('active_cli_refresh_deferred');
|
|
729
|
+
}
|
|
605
730
|
async readIndex() {
|
|
606
731
|
try {
|
|
607
732
|
const text = await readFile(path.join(this.vaultPath, VAULT_INDEX), 'utf8');
|
|
@@ -920,9 +1045,15 @@ function sleep(milliseconds) {
|
|
|
920
1045
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
921
1046
|
}
|
|
922
1047
|
function toPublicProfile(profile) {
|
|
923
|
-
return { id: profile.id, provider: profile.provider, label: profile.label, createdAt: profile.createdAt, updatedAt: profile.updatedAt, expiresAt: profile.expiresAt, refreshTokenExpiresAt: profile.refreshTokenExpiresAt };
|
|
1048
|
+
return { id: profile.id, provider: profile.provider, label: profile.label, createdAt: profile.createdAt, updatedAt: profile.updatedAt, expiresAt: profile.expiresAt, refreshTokenExpiresAt: profile.refreshTokenExpiresAt, ...(profile.refresh ? { refresh: profile.refresh } : {}) };
|
|
924
1049
|
}
|
|
925
1050
|
function shouldRefreshProfile(profile, now) {
|
|
1051
|
+
if (profile.refresh?.state === 'reauth_required')
|
|
1052
|
+
return false;
|
|
1053
|
+
if (profile.refresh?.nextRetryAt && Date.parse(profile.refresh.nextRetryAt) > now.getTime())
|
|
1054
|
+
return false;
|
|
1055
|
+
if (profile.refreshTokenExpiresAt && Date.parse(profile.refreshTokenExpiresAt) - now.getTime() <= REFRESH_WINDOW_MS)
|
|
1056
|
+
return true;
|
|
926
1057
|
if (profile.expiresAt !== null) {
|
|
927
1058
|
const expiresAt = Date.parse(profile.expiresAt);
|
|
928
1059
|
return Number.isFinite(expiresAt) && expiresAt - now.getTime() <= REFRESH_WINDOW_MS;
|
|
@@ -932,11 +1063,11 @@ function shouldRefreshProfile(profile, now) {
|
|
|
932
1063
|
}
|
|
933
1064
|
function refreshRetryAt(profile, now) {
|
|
934
1065
|
const minimum = now.getTime() + REFRESH_RETRY_MIN_MS;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (!Number.isFinite(expiresAt))
|
|
1066
|
+
const deadlines = [profile.expiresAt, profile.refreshTokenExpiresAt]
|
|
1067
|
+
.flatMap((value) => value && Number.isFinite(Date.parse(value)) ? [Date.parse(value)] : []);
|
|
1068
|
+
if (deadlines.length === 0)
|
|
939
1069
|
return now.getTime() + UNKNOWN_EXPIRY_RETRY_MS;
|
|
1070
|
+
const expiresAt = Math.min(...deadlines);
|
|
940
1071
|
const latestSafeRetry = expiresAt - REFRESH_RETRY_SAFETY_MS;
|
|
941
1072
|
return Math.max(minimum, Math.min(now.getTime() + REFRESH_RETRY_MAX_MS, latestSafeRetry));
|
|
942
1073
|
}
|
|
@@ -952,9 +1083,9 @@ function refreshTiming(provider, document, response, refreshedAt, previous) {
|
|
|
952
1083
|
const timing = credentialTiming(provider, document);
|
|
953
1084
|
if (!timing.expiresAt) {
|
|
954
1085
|
const expiresIn = numberValue(response.expires_in);
|
|
955
|
-
timing.expiresAt = expiresIn ?
|
|
1086
|
+
timing.expiresAt = expiresIn ? safeFutureTime(refreshedAt, expiresIn) : null;
|
|
956
1087
|
}
|
|
957
|
-
if (!timing.refreshTokenExpiresAt)
|
|
1088
|
+
if (!timing.refreshTokenExpiresAt && !stringValue(response.refresh_token))
|
|
958
1089
|
timing.refreshTokenExpiresAt = previous.refreshTokenExpiresAt;
|
|
959
1090
|
return timing;
|
|
960
1091
|
}
|
|
@@ -963,30 +1094,29 @@ function refreshTokenFor(provider, document) {
|
|
|
963
1094
|
const token = source.refreshToken ?? source.refresh_token;
|
|
964
1095
|
return typeof token === 'string' && token ? token : null;
|
|
965
1096
|
}
|
|
966
|
-
function refreshRequest(provider, refreshToken) {
|
|
967
|
-
const values = {
|
|
968
|
-
grant_type: 'refresh_token',
|
|
969
|
-
refresh_token: refreshToken,
|
|
970
|
-
client_id: provider === 'codex' ? CODEX_CLIENT_ID : CLAUDE_CLIENT_ID
|
|
971
|
-
};
|
|
972
|
-
if (provider === 'codex') {
|
|
973
|
-
return { headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(values) };
|
|
974
|
-
}
|
|
975
|
-
return { headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(values).toString() };
|
|
976
|
-
}
|
|
977
1097
|
function applyRefresh(provider, document, response, now) {
|
|
978
1098
|
const accessToken = stringValue(response.access_token);
|
|
979
1099
|
const refreshToken = stringValue(response.refresh_token);
|
|
980
1100
|
if (!accessToken)
|
|
981
|
-
throw new
|
|
1101
|
+
throw new ProviderRequestError('provider_invalid_response', 'transient');
|
|
982
1102
|
if (provider === 'claude') {
|
|
983
1103
|
const oauth = isRecord(document.claudeAiOauth) ? { ...document.claudeAiOauth } : {};
|
|
984
1104
|
oauth.accessToken = accessToken;
|
|
985
|
-
if (refreshToken)
|
|
1105
|
+
if (refreshToken) {
|
|
1106
|
+
if (refreshToken !== oauth.refreshToken)
|
|
1107
|
+
delete oauth.refreshTokenExpiresAt;
|
|
986
1108
|
oauth.refreshToken = refreshToken;
|
|
1109
|
+
}
|
|
987
1110
|
const expiresIn = numberValue(response.expires_in);
|
|
988
|
-
|
|
989
|
-
|
|
1111
|
+
const accessExpiry = expiresIn ? safeFutureTime(now, expiresIn) : null;
|
|
1112
|
+
if (accessExpiry)
|
|
1113
|
+
oauth.expiresAt = Date.parse(accessExpiry);
|
|
1114
|
+
else
|
|
1115
|
+
delete oauth.expiresAt;
|
|
1116
|
+
const refreshExpiresIn = numberValue(response.refresh_token_expires_in);
|
|
1117
|
+
const refreshExpiry = refreshExpiresIn ? safeFutureTime(now, refreshExpiresIn) : null;
|
|
1118
|
+
if (refreshExpiry)
|
|
1119
|
+
oauth.refreshTokenExpiresAt = Date.parse(refreshExpiry);
|
|
990
1120
|
return { ...document, claudeAiOauth: oauth };
|
|
991
1121
|
}
|
|
992
1122
|
const tokens = isRecord(document.tokens) ? { ...document.tokens } : {};
|
|
@@ -1043,6 +1173,7 @@ function isRecord(value) { return Boolean(value) && typeof value === 'object' &&
|
|
|
1043
1173
|
function isVaultProfile(value) { return isRecord(value) && typeof value.id === 'string' && (value.provider === 'codex' || value.provider === 'claude') && typeof value.label === 'string' && typeof value.createdAt === 'string' && typeof value.updatedAt === 'string' && (value.expiresAt === null || typeof value.expiresAt === 'string') && (value.refreshTokenExpiresAt === null || typeof value.refreshTokenExpiresAt === 'string') && typeof value.fingerprint === 'string'; }
|
|
1044
1174
|
function stringValue(value) { return typeof value === 'string' && value ? value : null; }
|
|
1045
1175
|
function numberValue(value) { return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null; }
|
|
1176
|
+
function safeFutureTime(now, seconds) { const millis = now.getTime() + seconds * 1000; return Number.isFinite(millis) && Math.abs(millis) <= 8.64e15 ? new Date(millis).toISOString() : null; }
|
|
1046
1177
|
function isoTime(value) { if (typeof value === 'string' && Number.isFinite(Date.parse(value)))
|
|
1047
1178
|
return new Date(value).toISOString(); if (typeof value === 'number' && Number.isFinite(value))
|
|
1048
1179
|
return new Date(value).toISOString(); return null; }
|
|
@@ -1056,6 +1187,7 @@ catch {
|
|
|
1056
1187
|
return null;
|
|
1057
1188
|
} }
|
|
1058
1189
|
function isNotFound(error) { return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT'); }
|
|
1190
|
+
function serviceErrorCode(error) { return error instanceof AccountServiceError ? error.code : 'account_storage_unavailable'; }
|
|
1059
1191
|
async function atomicWrite(target, value, mode) {
|
|
1060
1192
|
const temp = `${target}.${randomUUID()}.tmp`;
|
|
1061
1193
|
try {
|