@enbox/auth 0.6.28 → 0.6.30
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/esm/auth-manager.js +82 -46
- package/dist/esm/auth-manager.js.map +1 -1
- package/dist/esm/connect/import.js +20 -13
- package/dist/esm/connect/import.js.map +1 -1
- package/dist/esm/connect/lifecycle.js +356 -68
- package/dist/esm/connect/lifecycle.js.map +1 -1
- package/dist/esm/connect/local.js +2 -1
- package/dist/esm/connect/local.js.map +1 -1
- package/dist/esm/connect/restore.js +87 -64
- package/dist/esm/connect/restore.js.map +1 -1
- package/dist/esm/connect/wallet.js +1 -0
- package/dist/esm/connect/wallet.js.map +1 -1
- package/dist/esm/discovery.js +2 -1
- package/dist/esm/discovery.js.map +1 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/registration.js +70 -12
- package/dist/esm/registration.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/types/auth-manager.d.ts +26 -15
- package/dist/types/auth-manager.d.ts.map +1 -1
- package/dist/types/connect/import.d.ts.map +1 -1
- package/dist/types/connect/lifecycle.d.ts +60 -1
- package/dist/types/connect/lifecycle.d.ts.map +1 -1
- package/dist/types/connect/local.d.ts.map +1 -1
- package/dist/types/connect/restore.d.ts +8 -0
- package/dist/types/connect/restore.d.ts.map +1 -1
- package/dist/types/connect/wallet.d.ts.map +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/registration.d.ts +28 -3
- package/dist/types/registration.d.ts.map +1 -1
- package/dist/types/types.d.ts +18 -9
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/auth-manager.ts +100 -63
- package/src/connect/import.ts +24 -19
- package/src/connect/lifecycle.ts +360 -74
- package/src/connect/local.ts +5 -4
- package/src/connect/restore.ts +79 -66
- package/src/connect/wallet.ts +2 -1
- package/src/discovery.ts +1 -1
- package/src/events.ts +1 -1
- package/src/registration.ts +82 -15
- package/src/types.ts +18 -9
package/src/connect/restore.ts
CHANGED
|
@@ -14,15 +14,13 @@ import type { StorageAdapter } from '../types.js';
|
|
|
14
14
|
|
|
15
15
|
import type { EnboxUserAgent } from '@enbox/agent';
|
|
16
16
|
|
|
17
|
-
import type { DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, DwnRecordsPermissionScope } from '@enbox/agent';
|
|
18
|
-
|
|
19
17
|
import { Convert } from '@enbox/common';
|
|
20
|
-
import { DataStream
|
|
18
|
+
import { DataStream } from '@enbox/dwn-sdk-js';
|
|
21
19
|
import { DwnInterface, DwnPermissionGrant } from '@enbox/agent';
|
|
22
20
|
|
|
23
21
|
import { applyLocalDwnDiscovery } from '../discovery.js';
|
|
24
22
|
import { STORAGE_KEYS } from '../types.js';
|
|
25
|
-
import { ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
|
|
23
|
+
import { ensureVaultReady, finalizeSession, registerSyncScopeForIdentity, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
26
|
* Decrypt a stored key blob.
|
|
@@ -45,6 +43,45 @@ async function decryptStoredKeys(
|
|
|
45
43
|
return stored;
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Load delegate keys from SecretStore, falling back to legacy StorageAdapter.
|
|
48
|
+
*
|
|
49
|
+
* On a successful legacy read the keys are migrated into the SecretStore and
|
|
50
|
+
* the plaintext/JWE copy is removed (best-effort). Returns the JSON string
|
|
51
|
+
* representation of the keys, or `undefined` if nothing is stored.
|
|
52
|
+
*/
|
|
53
|
+
async function loadDelegateKeysWithFallback(
|
|
54
|
+
userAgent: EnboxUserAgent,
|
|
55
|
+
storage: StorageAdapter,
|
|
56
|
+
key: string,
|
|
57
|
+
): Promise<string | undefined> {
|
|
58
|
+
// 1. Try SecretStore (preferred path).
|
|
59
|
+
try {
|
|
60
|
+
const bytes = await userAgent.secrets.get(key);
|
|
61
|
+
if (bytes) {
|
|
62
|
+
return Convert.uint8Array(bytes).toString();
|
|
63
|
+
}
|
|
64
|
+
} catch { /* vault may be locked — fall through to legacy path */ }
|
|
65
|
+
|
|
66
|
+
// 2. Fall back to legacy StorageAdapter (JWE or plaintext JSON).
|
|
67
|
+
const legacy = await storage.get(key);
|
|
68
|
+
if (!legacy) { return undefined; }
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const json = await decryptStoredKeys(userAgent, legacy);
|
|
72
|
+
|
|
73
|
+
// Migrate into SecretStore and remove legacy copy (best-effort).
|
|
74
|
+
try {
|
|
75
|
+
await userAgent.secrets.put(key, Convert.string(json).toUint8Array());
|
|
76
|
+
await storage.remove(key);
|
|
77
|
+
} catch { /* best-effort migration */ }
|
|
78
|
+
|
|
79
|
+
return json;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
86
|
* Attempt to restore a previous session.
|
|
50
87
|
*
|
|
@@ -153,6 +190,7 @@ export async function restoreSession(
|
|
|
153
190
|
|
|
154
191
|
if (!isAgentOnlySession) {
|
|
155
192
|
// Truly stale session data — clean up and bail.
|
|
193
|
+
// Remove delegate keys from both SecretStore and legacy StorageAdapter.
|
|
156
194
|
await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
|
|
157
195
|
await storage.remove(STORAGE_KEYS.ACTIVE_IDENTITY);
|
|
158
196
|
await storage.remove(STORAGE_KEYS.DELEGATE_DID);
|
|
@@ -161,6 +199,8 @@ export async function restoreSession(
|
|
|
161
199
|
await storage.remove(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS);
|
|
162
200
|
await storage.remove(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS);
|
|
163
201
|
await storage.remove(STORAGE_KEYS.SESSION_REVOCATIONS);
|
|
202
|
+
try { await userAgent.secrets.delete(STORAGE_KEYS.DELEGATE_DECRYPTION_KEYS); } catch { /* best-effort */ }
|
|
203
|
+
try { await userAgent.secrets.delete(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS); } catch { /* best-effort */ }
|
|
164
204
|
// Do NOT remove REVOCATION_RETRY_CONTEXT here — it has its own
|
|
165
205
|
// lifecycle managed by the retry maintenance path. Stale session
|
|
166
206
|
// cleanup must not silently drop pending revocations.
|
|
@@ -185,37 +225,36 @@ export async function restoreSession(
|
|
|
185
225
|
// which causes the sync engine to attempt syncing the permissions protocol
|
|
186
226
|
// and fail with "No permissions found for MessagesSync".
|
|
187
227
|
// Derive the correct protocol list from stored grants and update.
|
|
188
|
-
|
|
228
|
+
// Always repair regardless of sync state — a stale registration persists
|
|
229
|
+
// on disk and would take effect if sync is later enabled.
|
|
230
|
+
let syncRepairFailed = false;
|
|
231
|
+
if (delegateDid) {
|
|
189
232
|
try {
|
|
190
|
-
|
|
191
|
-
const options = { delegateDid, protocols };
|
|
192
|
-
try {
|
|
193
|
-
await userAgent.sync.registerIdentity({ did: connectedDid, options });
|
|
194
|
-
} catch (regError: unknown) {
|
|
195
|
-
const msg = regError instanceof Error ? regError.message : '';
|
|
196
|
-
if (msg.includes('already registered')) {
|
|
197
|
-
await userAgent.sync.updateIdentityOptions({ did: connectedDid, options });
|
|
198
|
-
}
|
|
199
|
-
// Other errors are ignored — sync will still work with existing registration.
|
|
200
|
-
}
|
|
233
|
+
await registerSyncScopeForIdentity({ userAgent, connectedDid, delegateDid });
|
|
201
234
|
} catch {
|
|
202
|
-
//
|
|
235
|
+
// Grant query or registration repair failed — don't block restore,
|
|
236
|
+
// but don't let a stale registration remain usable.
|
|
237
|
+
syncRepairFailed = true;
|
|
238
|
+
// Best-effort: remove any existing registration so a later manual
|
|
239
|
+
// startSync() cannot use stale scope for this connected DID.
|
|
240
|
+
try { await userAgent.sync.unregisterIdentity(connectedDid); } catch { /* already gone or store error */ }
|
|
203
241
|
}
|
|
204
242
|
}
|
|
205
243
|
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// created before the encryption-at-rest fix.
|
|
244
|
+
// Restore delegate decryption/context keys BEFORE starting sync so that
|
|
245
|
+
// the first sync cycle can decrypt encrypted records.
|
|
246
|
+
//
|
|
247
|
+
// Keys are loaded from the vault-backed SecretStore (preferred). When
|
|
248
|
+
// the SecretStore is empty, we fall back to the legacy StorageAdapter
|
|
249
|
+
// which may hold either a compact JWE (encrypted with the vault CEK)
|
|
250
|
+
// or raw JSON (from sessions created before the encryption-at-rest fix).
|
|
251
|
+
// On successful fallback, the legacy copy is removed (best-effort) so
|
|
252
|
+
// all future reads come from the SecretStore.
|
|
213
253
|
if (delegateDid && connectedDid) {
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
254
|
+
const decryptionKeysJson = await loadDelegateKeysWithFallback(userAgent, storage, STORAGE_KEYS.DELEGATE_DECRYPTION_KEYS);
|
|
255
|
+
if (decryptionKeysJson) {
|
|
216
256
|
try {
|
|
217
|
-
const
|
|
218
|
-
const keys = JSON.parse(keysJson);
|
|
257
|
+
const keys = JSON.parse(decryptionKeysJson);
|
|
219
258
|
if (Array.isArray(keys) && keys.length > 0) {
|
|
220
259
|
userAgent.dwn.importDelegateDecryptionKeys(delegateDid, keys);
|
|
221
260
|
}
|
|
@@ -223,7 +262,7 @@ export async function restoreSession(
|
|
|
223
262
|
}
|
|
224
263
|
|
|
225
264
|
// Restore context keys for multi-party encrypted protocols.
|
|
226
|
-
const
|
|
265
|
+
const contextKeysJson = await loadDelegateKeysWithFallback(userAgent, storage, STORAGE_KEYS.DELEGATE_CONTEXT_KEYS);
|
|
227
266
|
// Restore multi-party protocol registrations (not encrypted — no key material).
|
|
228
267
|
const mpProtocolsJson = await storage.get(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS);
|
|
229
268
|
let multiPartyProtocols: string[] | undefined;
|
|
@@ -234,12 +273,9 @@ export async function restoreSession(
|
|
|
234
273
|
} catch { /* best effort */ }
|
|
235
274
|
}
|
|
236
275
|
|
|
237
|
-
if (
|
|
276
|
+
if (contextKeysJson || multiPartyProtocols) {
|
|
238
277
|
try {
|
|
239
|
-
const
|
|
240
|
-
? await decryptStoredKeys(userAgent, storedCtxKeys)
|
|
241
|
-
: '[]';
|
|
242
|
-
const ctxKeys = JSON.parse(ctxKeysJson);
|
|
278
|
+
const ctxKeys = contextKeysJson ? JSON.parse(contextKeysJson) : [];
|
|
243
279
|
userAgent.dwn.importDelegateContextKeys(
|
|
244
280
|
delegateDid,
|
|
245
281
|
Array.isArray(ctxKeys) ? ctxKeys : [],
|
|
@@ -255,13 +291,18 @@ export async function restoreSession(
|
|
|
255
291
|
if (changedDelegateDid !== restoreDelegateDid) { return; }
|
|
256
292
|
try {
|
|
257
293
|
const keys = userAgent.dwn.exportDelegateContextKeys(restoreDelegateDid);
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
await storage.set(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS, encrypted);
|
|
294
|
+
const bytes = Convert.string(JSON.stringify(keys)).toUint8Array();
|
|
295
|
+
await userAgent.secrets.put(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS, bytes);
|
|
261
296
|
} catch { /* best effort — keys will be re-derived on next connect */ }
|
|
262
297
|
};
|
|
263
298
|
}
|
|
264
299
|
|
|
300
|
+
// Start sync only if the registration repair succeeded (or was not needed).
|
|
301
|
+
// If repair failed, don't start sync with potentially stale scope.
|
|
302
|
+
if (!syncRepairFailed) {
|
|
303
|
+
await startSyncIfEnabled(userAgent, ctx.defaultSync);
|
|
304
|
+
}
|
|
305
|
+
|
|
265
306
|
// Persist session info, build AuthSession, and emit lifecycle events.
|
|
266
307
|
// Session restore does not emit `identity-added` (identity was already added in the original flow).
|
|
267
308
|
return finalizeSession({
|
|
@@ -444,7 +485,7 @@ async function sendRevocationToEndpoints(
|
|
|
444
485
|
const reply = await userAgent.rpc.sendDwnRequest({
|
|
445
486
|
dwnUrl,
|
|
446
487
|
targetDid : connectedDid,
|
|
447
|
-
message : rawMessage
|
|
488
|
+
message : rawMessage,
|
|
448
489
|
data,
|
|
449
490
|
});
|
|
450
491
|
if (reply?.status?.code === 202 || reply?.status?.code === 409) {
|
|
@@ -530,32 +571,4 @@ export async function retryOrphanedRevocations(
|
|
|
530
571
|
* permissions protocol itself (permission records are already included
|
|
531
572
|
* in each protocol's sync stream via `constructAdditionalMessageFilter`).
|
|
532
573
|
*/
|
|
533
|
-
async function deriveProtocolsFromGrants(
|
|
534
|
-
userAgent: EnboxUserAgent,
|
|
535
|
-
delegateDid: string,
|
|
536
|
-
): Promise<string[]> {
|
|
537
|
-
const response = await userAgent.processDwnRequest({
|
|
538
|
-
author : delegateDid,
|
|
539
|
-
target : delegateDid,
|
|
540
|
-
messageType : DwnInterface.RecordsQuery,
|
|
541
|
-
messageParams : {
|
|
542
|
-
filter: {
|
|
543
|
-
protocol : PermissionsProtocol.uri,
|
|
544
|
-
protocolPath : PermissionsProtocol.grantPath,
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
const protocols: string[] = [];
|
|
550
|
-
if (response.reply.status.code === 200 && response.reply.entries) {
|
|
551
|
-
for (const entry of response.reply.entries as DwnDataEncodedRecordsWriteMessage[]) {
|
|
552
|
-
const grant = DwnPermissionGrant.parse(entry);
|
|
553
|
-
const scopeProtocol = (grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope).protocol;
|
|
554
|
-
if (scopeProtocol && scopeProtocol !== PermissionsProtocol.uri) {
|
|
555
|
-
protocols.push(scopeProtocol);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
574
|
|
|
560
|
-
return [...new Set(protocols)];
|
|
561
|
-
}
|
package/src/connect/wallet.ts
CHANGED
package/src/discovery.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function checkUrlForDwnDiscoveryPayload(): string | undefined {
|
|
|
58
58
|
|
|
59
59
|
// Clear the fragment to prevent re-reading on subsequent calls or
|
|
60
60
|
// if the user refreshes the page after the redirect.
|
|
61
|
-
if (
|
|
61
|
+
if (globalThis.history?.replaceState) {
|
|
62
62
|
const cleanUrl = globalThis.location.href.split('#')[0];
|
|
63
63
|
globalThis.history.replaceState(null, '', cleanUrl);
|
|
64
64
|
}
|
package/src/events.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { AuthEvent, AuthEventHandler, AuthEventMap } from './types.js';
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export class AuthEventEmitter {
|
|
22
|
-
private _listeners = new Map<AuthEvent, Set<AuthEventHandler<AuthEvent>>>();
|
|
22
|
+
private readonly _listeners = new Map<AuthEvent, Set<AuthEventHandler<AuthEvent>>>();
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Subscribe to an event. Returns an unsubscribe function.
|
package/src/registration.ts
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { EnboxUserAgent } from '@enbox/agent';
|
|
14
|
+
import type { EnboxUserAgent, SecretStore } from '@enbox/agent';
|
|
15
15
|
|
|
16
|
+
import { Convert } from '@enbox/common';
|
|
16
17
|
import { DwnRegistrar } from '@enbox/dwn-clients';
|
|
17
18
|
|
|
18
19
|
import { STORAGE_KEYS } from './types.js';
|
|
@@ -38,8 +39,20 @@ export interface RegistrationContext {
|
|
|
38
39
|
connectedDid: string;
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* Vault-backed secret store for encrypted token persistence.
|
|
43
|
+
*
|
|
44
|
+
* When provided **and** the vault is unlocked, registration tokens are
|
|
45
|
+
* stored here instead of in the plaintext `StorageAdapter`, keeping
|
|
46
|
+
* bearer credentials out of `localStorage`.
|
|
47
|
+
*/
|
|
48
|
+
secretStore?: SecretStore;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Plaintext storage adapter for automatic token persistence.
|
|
52
|
+
*
|
|
53
|
+
* @deprecated Prefer {@link secretStore} when the vault is available.
|
|
54
|
+
* This field is retained for backwards compatibility with
|
|
55
|
+
* callers that have not yet migrated.
|
|
43
56
|
*/
|
|
44
57
|
storage?: StorageAdapter;
|
|
45
58
|
}
|
|
@@ -60,14 +73,21 @@ export async function registerWithDwnEndpoints(
|
|
|
60
73
|
ctx: RegistrationContext,
|
|
61
74
|
registration: RegistrationOptions,
|
|
62
75
|
): Promise<void> {
|
|
63
|
-
const { userAgent, dwnEndpoints, agentDid, connectedDid, storage } = ctx;
|
|
76
|
+
const { userAgent, dwnEndpoints, agentDid, connectedDid, secretStore, storage } = ctx;
|
|
64
77
|
|
|
65
|
-
// Load initial tokens: when persistTokens is enabled,
|
|
66
|
-
//
|
|
78
|
+
// Load initial tokens: when persistTokens is enabled, try the
|
|
79
|
+
// vault-backed SecretStore first, then fall back to the legacy plaintext
|
|
80
|
+
// StorageAdapter. This ensures upgraded clients that already have tokens
|
|
81
|
+
// in localStorage can still read them (and migrate them on the next save).
|
|
67
82
|
let seedTokens: Record<string, RegistrationTokenData> = {};
|
|
68
83
|
|
|
69
|
-
if (registration.persistTokens
|
|
70
|
-
|
|
84
|
+
if (registration.persistTokens) {
|
|
85
|
+
if (secretStore) {
|
|
86
|
+
seedTokens = await loadTokensFromSecretStore(secretStore);
|
|
87
|
+
}
|
|
88
|
+
if (Object.keys(seedTokens).length === 0 && storage) {
|
|
89
|
+
seedTokens = await loadTokensFromStorage(storage);
|
|
90
|
+
}
|
|
71
91
|
} else {
|
|
72
92
|
seedTokens = registration.registrationTokens ?? {};
|
|
73
93
|
}
|
|
@@ -103,8 +123,8 @@ export async function registerWithDwnEndpoints(
|
|
|
103
123
|
tokenData = {
|
|
104
124
|
registrationToken : refreshed.registrationToken,
|
|
105
125
|
refreshToken : refreshed.refreshToken,
|
|
106
|
-
expiresAt : refreshed.expiresIn
|
|
107
|
-
? Date.now() + (refreshed.expiresIn * 1000)
|
|
126
|
+
expiresAt : refreshed.expiresIn === undefined
|
|
127
|
+
? undefined : Date.now() + (refreshed.expiresIn * 1000),
|
|
108
128
|
tokenUrl : tokenData.tokenUrl,
|
|
109
129
|
refreshUrl : tokenData.refreshUrl,
|
|
110
130
|
};
|
|
@@ -140,8 +160,8 @@ export async function registerWithDwnEndpoints(
|
|
|
140
160
|
tokenData = {
|
|
141
161
|
registrationToken : tokenResponse.registrationToken,
|
|
142
162
|
refreshToken : tokenResponse.refreshToken,
|
|
143
|
-
expiresAt : tokenResponse.expiresIn
|
|
144
|
-
? Date.now() + (tokenResponse.expiresIn * 1000)
|
|
163
|
+
expiresAt : tokenResponse.expiresIn === undefined
|
|
164
|
+
? undefined : Date.now() + (tokenResponse.expiresIn * 1000),
|
|
145
165
|
tokenUrl : providerAuth.tokenUrl,
|
|
146
166
|
refreshUrl : providerAuth.refreshUrl,
|
|
147
167
|
};
|
|
@@ -162,9 +182,21 @@ export async function registerWithDwnEndpoints(
|
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
|
|
165
|
-
// Persist tokens
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
// Persist tokens: prefer vault-backed SecretStore over plaintext StorageAdapter.
|
|
186
|
+
// When migrating to SecretStore, also clear the legacy plaintext copy so
|
|
187
|
+
// bearer tokens no longer sit in localStorage.
|
|
188
|
+
if (registration.persistTokens) {
|
|
189
|
+
if (secretStore) {
|
|
190
|
+
await saveTokensToSecretStore(secretStore, updatedTokens);
|
|
191
|
+
// Best-effort cleanup: remove the legacy plaintext copy so bearer
|
|
192
|
+
// tokens no longer sit in localStorage. A failure here must not
|
|
193
|
+
// turn a successful registration into an error.
|
|
194
|
+
if (storage) {
|
|
195
|
+
try { await storage.remove(STORAGE_KEYS.REGISTRATION_TOKENS); } catch { /* best-effort */ }
|
|
196
|
+
}
|
|
197
|
+
} else if (storage) {
|
|
198
|
+
await saveTokensToStorage(storage, updatedTokens);
|
|
199
|
+
}
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
// Notify app of updated tokens (always, even when auto-persisting).
|
|
@@ -210,3 +242,38 @@ export async function saveTokensToStorage(
|
|
|
210
242
|
): Promise<void> {
|
|
211
243
|
await storage.set(STORAGE_KEYS.REGISTRATION_TOKENS, JSON.stringify(tokens));
|
|
212
244
|
}
|
|
245
|
+
|
|
246
|
+
// ─── SecretStore helpers ─────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Load registration tokens from the vault-backed {@link SecretStore}.
|
|
250
|
+
*
|
|
251
|
+
* Returns an empty record if no tokens are stored, the stored value is
|
|
252
|
+
* corrupt, or the vault is locked (best-effort — never throws).
|
|
253
|
+
*
|
|
254
|
+
* @internal
|
|
255
|
+
*/
|
|
256
|
+
export async function loadTokensFromSecretStore(
|
|
257
|
+
secretStore: SecretStore,
|
|
258
|
+
): Promise<Record<string, RegistrationTokenData>> {
|
|
259
|
+
try {
|
|
260
|
+
const bytes = await secretStore.get(STORAGE_KEYS.REGISTRATION_TOKENS);
|
|
261
|
+
if (!bytes) { return {}; }
|
|
262
|
+
const json = Convert.uint8Array(bytes).toString();
|
|
263
|
+
return JSON.parse(json) as Record<string, RegistrationTokenData>;
|
|
264
|
+
} catch {
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Save registration tokens to the vault-backed {@link SecretStore}.
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
export async function saveTokensToSecretStore(
|
|
274
|
+
secretStore: SecretStore,
|
|
275
|
+
tokens: Record<string, RegistrationTokenData>,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const bytes = Convert.string(JSON.stringify(tokens)).toUint8Array();
|
|
278
|
+
await secretStore.put(STORAGE_KEYS.REGISTRATION_TOKENS, bytes);
|
|
279
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -171,7 +171,8 @@ export interface RegistrationOptions {
|
|
|
171
171
|
* endpoint, it is used directly without re-running the auth flow.
|
|
172
172
|
*
|
|
173
173
|
* When {@link persistTokens} is `true`, this field is ignored —
|
|
174
|
-
* tokens are loaded automatically from the
|
|
174
|
+
* tokens are loaded automatically from the agent's vault-backed
|
|
175
|
+
* `SecretStore` (preferred) or the legacy `StorageAdapter` (fallback).
|
|
175
176
|
*/
|
|
176
177
|
registrationTokens?: Record<string, RegistrationTokenData>;
|
|
177
178
|
|
|
@@ -180,21 +181,29 @@ export interface RegistrationOptions {
|
|
|
180
181
|
* The app should persist these for future sessions.
|
|
181
182
|
*
|
|
182
183
|
* When {@link persistTokens} is `true`, tokens are saved automatically
|
|
183
|
-
* to the
|
|
184
|
-
*
|
|
185
|
-
*
|
|
184
|
+
* to the agent's vault-backed `SecretStore` (or the legacy
|
|
185
|
+
* `StorageAdapter` when no `SecretStore` is available). This callback
|
|
186
|
+
* is still invoked (if provided) **after** the automatic save, so
|
|
187
|
+
* consumers can observe token changes without handling persistence
|
|
188
|
+
* themselves.
|
|
186
189
|
*/
|
|
187
190
|
onRegistrationTokens?: (tokens: Record<string, RegistrationTokenData>) => void;
|
|
188
191
|
|
|
189
192
|
/**
|
|
190
|
-
* Automatically persist and restore registration tokens
|
|
191
|
-
* auth manager's `StorageAdapter`.
|
|
193
|
+
* Automatically persist and restore registration tokens.
|
|
192
194
|
*
|
|
193
|
-
* When `true`, tokens are loaded
|
|
194
|
-
*
|
|
195
|
-
*
|
|
195
|
+
* When `true`, tokens are loaded before registration and saved back
|
|
196
|
+
* after new or refreshed tokens are obtained, removing the need for
|
|
197
|
+
* consumers to implement their own token I/O via
|
|
196
198
|
* {@link registrationTokens} and {@link onRegistrationTokens}.
|
|
197
199
|
*
|
|
200
|
+
* **Storage preference:** tokens are stored in the agent's vault-backed
|
|
201
|
+
* `SecretStore` (encrypted at rest) when available. On the first run
|
|
202
|
+
* after an upgrade, any tokens left in the legacy plaintext
|
|
203
|
+
* `StorageAdapter` are migrated into the `SecretStore` and the
|
|
204
|
+
* plaintext copy is removed. If no `SecretStore` is provided, the
|
|
205
|
+
* `StorageAdapter` is used as a fallback.
|
|
206
|
+
*
|
|
198
207
|
* Defaults to `false` for backward compatibility.
|
|
199
208
|
*
|
|
200
209
|
* @example
|