@enbox/auth 0.6.19 → 0.6.20

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.
@@ -10,6 +10,14 @@ import type { AuthSession } from '../identity-session.js';
10
10
  import type { FlowContext } from './lifecycle.js';
11
11
  import type { RestoreSessionOptions } from '../types.js';
12
12
 
13
+ import type { StorageAdapter } from '../types.js';
14
+
15
+ import type { EnboxUserAgent } from '@enbox/agent';
16
+
17
+ import { Convert } from '@enbox/common';
18
+ import { DataStream } from '@enbox/dwn-sdk-js';
19
+ import { DwnInterface, DwnPermissionGrant } from '@enbox/agent';
20
+
13
21
  import { applyLocalDwnDiscovery } from '../discovery.js';
14
22
  import { STORAGE_KEYS } from '../types.js';
15
23
  import { ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
@@ -19,6 +27,11 @@ import { ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword
19
27
  *
20
28
  * Returns `undefined` if no previous session exists.
21
29
  * Returns an `AuthSession` if the session was successfully restored.
30
+ *
31
+ * Two independent concerns are handled here:
32
+ * 1. Revocation retry maintenance (from a previous partial disconnect)
33
+ * 2. Normal session restore
34
+ * They do NOT depend on each other. Both can run in the same call.
22
35
  */
23
36
  export async function restoreSession(
24
37
  ctx: FlowContext,
@@ -26,27 +39,27 @@ export async function restoreSession(
26
39
  ): Promise<AuthSession | undefined> {
27
40
  const { userAgent, emitter, storage } = ctx;
28
41
 
29
- // Check if there was a previous session.
42
+ // Two independent concerns:
43
+ // 1. PREVIOUSLY_CONNECTED — normal session restore
44
+ // 2. REVOCATION_RETRY_CONTEXT — orphaned revocations from partial disconnect
45
+ // If neither is set, nothing to do.
30
46
  const previouslyConnected = await storage.get(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
31
- if (previouslyConnected !== 'true') {
47
+ const retryContextJson = await storage.get(STORAGE_KEYS.REVOCATION_RETRY_CONTEXT);
48
+ if (previouslyConnected !== 'true' && !retryContextJson) {
32
49
  return undefined;
33
50
  }
34
51
 
35
- // Resolve password: explicit option → callback → provider → manager default → insecure fallback.
36
- // Note: restoreSession has an extra `onPasswordRequired` callback that sits between
37
- // the explicit password and the provider. We handle that here, then delegate the
38
- // remainder of the chain to `resolvePassword()`.
52
+ // Resolve password.
39
53
  let explicitPassword = options.password;
40
-
41
54
  if (!explicitPassword && !ctx.defaultPassword && options.onPasswordRequired) {
42
55
  explicitPassword = await options.onPasswordRequired();
43
56
  }
44
57
 
45
- // Check for stale session marker: if the vault was never initialized,
46
- // previouslyConnected is a leftover — clean up and bail.
58
+ // Check for stale session marker.
47
59
  const isFirstLaunch = await userAgent.firstLaunch();
48
60
  if (isFirstLaunch) {
49
61
  await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
62
+ await storage.remove(STORAGE_KEYS.REVOCATION_RETRY_CONTEXT);
50
63
  return undefined;
51
64
  }
52
65
 
@@ -60,12 +73,37 @@ export async function restoreSession(
60
73
  isFirstLaunch: false,
61
74
  });
62
75
 
63
- // Apply local DWN discovery (browser redirect payload or persisted endpoint).
64
- // In remote mode, discovery already ran before agent creation — skip.
76
+ // Apply local DWN discovery.
65
77
  if (!userAgent.dwn.isRemoteMode) {
66
78
  await applyLocalDwnDiscovery(userAgent, storage, emitter);
67
79
  }
68
80
 
81
+ // --- Retry maintenance (independent from session restore) ---
82
+ // Best-effort: start sync temporarily for remote delivery, run retry,
83
+ // then stop. Failures here must NOT break a legitimate restore path.
84
+ if (retryContextJson) {
85
+ try {
86
+ await startSyncIfEnabled(userAgent, ctx.defaultSync);
87
+ try {
88
+ await retryOrphanedRevocations(userAgent, storage);
89
+ } finally {
90
+ await userAgent.sync.stopSync(2000);
91
+ }
92
+ } catch {
93
+ // Retry maintenance is best-effort. If sync startup or retry
94
+ // fails, the retry context remains in storage for next attempt.
95
+ // Do NOT let this block normal session restore below.
96
+ }
97
+ }
98
+
99
+ // --- Normal session restore ---
100
+ if (previouslyConnected !== 'true') {
101
+ return undefined;
102
+ }
103
+
104
+ // Start sync for the restored session.
105
+ await startSyncIfEnabled(userAgent, ctx.defaultSync);
106
+
69
107
  // Determine which identity to reconnect.
70
108
  const activeIdentityDid = await storage.get(STORAGE_KEYS.ACTIVE_IDENTITY);
71
109
  const storedDelegateDid = await storage.get(STORAGE_KEYS.DELEGATE_DID);
@@ -86,8 +124,7 @@ export async function restoreSession(
86
124
  }
87
125
  }
88
126
 
89
- // Start sync.
90
- await startSyncIfEnabled(userAgent, ctx.defaultSync);
127
+ // Sync was already started above (for the restored session).
91
128
 
92
129
  if (!identity) {
93
130
  // No identity found — this is valid for agent-only sessions created
@@ -102,6 +139,13 @@ export async function restoreSession(
102
139
  await storage.remove(STORAGE_KEYS.ACTIVE_IDENTITY);
103
140
  await storage.remove(STORAGE_KEYS.DELEGATE_DID);
104
141
  await storage.remove(STORAGE_KEYS.CONNECTED_DID);
142
+ await storage.remove(STORAGE_KEYS.DELEGATE_DECRYPTION_KEYS);
143
+ await storage.remove(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS);
144
+ await storage.remove(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS);
145
+ await storage.remove(STORAGE_KEYS.SESSION_REVOCATIONS);
146
+ // Do NOT remove REVOCATION_RETRY_CONTEXT here — it has its own
147
+ // lifecycle managed by the retry maintenance path. Stale session
148
+ // cleanup must not silently drop pending revocations.
105
149
  return undefined;
106
150
  }
107
151
 
@@ -118,6 +162,53 @@ export async function restoreSession(
118
162
  identity, storedDelegateDid ?? undefined,
119
163
  );
120
164
 
165
+ // Restore delegate decryption keys if persisted.
166
+ if (delegateDid && connectedDid) {
167
+ const keysJson = await storage.get(STORAGE_KEYS.DELEGATE_DECRYPTION_KEYS);
168
+ if (keysJson) {
169
+ try {
170
+ const keys = JSON.parse(keysJson);
171
+ if (Array.isArray(keys) && keys.length > 0) {
172
+ userAgent.dwn.importDelegateDecryptionKeys(delegateDid, keys);
173
+ }
174
+ } catch { /* best effort — keys will be refreshed on next connect */ }
175
+ }
176
+
177
+ // Restore context keys for multi-party encrypted protocols.
178
+ const ctxKeysJson = await storage.get(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS);
179
+ // Restore multi-party protocol registrations.
180
+ const mpProtocolsJson = await storage.get(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS);
181
+ let multiPartyProtocols: string[] | undefined;
182
+ if (mpProtocolsJson) {
183
+ try {
184
+ const parsed = JSON.parse(mpProtocolsJson);
185
+ if (Array.isArray(parsed)) { multiPartyProtocols = parsed; }
186
+ } catch { /* best effort */ }
187
+ }
188
+
189
+ if (ctxKeysJson || multiPartyProtocols) {
190
+ try {
191
+ const ctxKeys = ctxKeysJson ? JSON.parse(ctxKeysJson) : [];
192
+ userAgent.dwn.importDelegateContextKeys(
193
+ delegateDid,
194
+ Array.isArray(ctxKeys) ? ctxKeys : [],
195
+ multiPartyProtocols,
196
+ );
197
+ } catch { /* best effort — keys will be refreshed on next connect */ }
198
+ }
199
+
200
+ // Wire post-connect context key persistence so keys delivered after
201
+ // restore survive the next restart. Same callback as finalizeDelegateSession.
202
+ const restoreDelegateDid = delegateDid;
203
+ userAgent.dwn.onDelegateContextKeysChanged = async (changedDelegateDid: string): Promise<void> => {
204
+ if (changedDelegateDid !== restoreDelegateDid) { return; }
205
+ try {
206
+ const keys = userAgent.dwn.exportDelegateContextKeys(restoreDelegateDid);
207
+ await storage.set(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS, JSON.stringify(keys));
208
+ } catch { /* best effort — keys will be re-derived on next connect */ }
209
+ };
210
+ }
211
+
121
212
  // Persist session info, build AuthSession, and emit lifecycle events.
122
213
  // Session restore does not emit `identity-added` (identity was already added in the original flow).
123
214
  return finalizeSession({
@@ -131,3 +222,247 @@ export async function restoreSession(
131
222
  emitIdentityAdded : false,
132
223
  });
133
224
  }
225
+
226
+ // ─── Revocation retry helpers ───────────────────────────────────
227
+
228
+ type RevocationEntry = { grantId: string; revocationGrantId: string };
229
+
230
+ type RetryEntry = {
231
+ delegateDid: string;
232
+ connectedDid: string;
233
+ revocations: RevocationEntry[];
234
+ };
235
+
236
+ /**
237
+ * Load all retry entries from `REVOCATION_RETRY_CONTEXT`.
238
+ * Returns an empty array if the data is missing or malformed.
239
+ */
240
+ async function loadRetryEntries(
241
+ storage: StorageAdapter,
242
+ ): Promise<RetryEntry[]> {
243
+ const json = await storage.get(STORAGE_KEYS.REVOCATION_RETRY_CONTEXT);
244
+ if (!json) { return []; }
245
+
246
+ try {
247
+ const parsed = JSON.parse(json);
248
+
249
+ // Handle legacy single-object format: wrap in array.
250
+ const entries = Array.isArray(parsed)
251
+ ? parsed
252
+ : (parsed?.delegateDid && parsed?.connectedDid && Array.isArray(parsed?.revocations))
253
+ ? [parsed]
254
+ : [];
255
+
256
+ if (entries.length === 0 && !Array.isArray(parsed)) {
257
+ // Truly malformed (not a valid legacy object either).
258
+ await clearRetryState(storage);
259
+ return [];
260
+ }
261
+
262
+ // Filter out malformed entries.
263
+ return entries.filter(
264
+ (e: any): e is RetryEntry => e?.delegateDid && e?.connectedDid && Array.isArray(e?.revocations),
265
+ );
266
+ } catch {
267
+ await clearRetryState(storage);
268
+ return [];
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Revoke a single grant and send the revocation to remote DWN endpoints.
274
+ * Returns `true` if at least one remote endpoint confirmed (202/409).
275
+ */
276
+ /**
277
+ * Ensure the revocation grant exists on the owner's remote DWN before
278
+ * attempting to use it. Reads the grant locally by recordId and sends
279
+ * it to all remote endpoints. This closes the gap where best-effort
280
+ * fanout at connect time may have failed.
281
+ */
282
+ async function ensureRevocationGrantOnRemote(
283
+ userAgent: EnboxUserAgent,
284
+ connectedDid: string,
285
+ delegateDid: string,
286
+ revocationGrantId: string,
287
+ dwnEndpointUrls: string[],
288
+ ): Promise<void> {
289
+ if (dwnEndpointUrls.length === 0) { return; }
290
+
291
+ try {
292
+ // Read as the delegate (grant recipient), not the owner.
293
+ const { reply } = await userAgent.dwn.processRequest({
294
+ author : delegateDid,
295
+ target : connectedDid,
296
+ messageType : DwnInterface.RecordsRead,
297
+ messageParams : { filter: { recordId: revocationGrantId } },
298
+ });
299
+ if (reply.status.code !== 200 || !reply.entry?.recordsWrite) { return; }
300
+
301
+ const { encodedData, ...rawMessage } = reply.entry.recordsWrite as any;
302
+ const data = reply.entry.data
303
+ ? new Blob([await DataStream.toBytes(reply.entry.data) as BlobPart])
304
+ : undefined;
305
+
306
+ for (const dwnUrl of dwnEndpointUrls) {
307
+ try {
308
+ await userAgent.rpc.sendDwnRequest({
309
+ dwnUrl,
310
+ targetDid : connectedDid,
311
+ message : rawMessage,
312
+ data,
313
+ });
314
+ } catch {
315
+ // Per-endpoint failure — continue.
316
+ }
317
+ }
318
+ } catch {
319
+ // Best-effort — if the grant can't be read or sent, the revocation
320
+ // attempt will fail on auth and be retried next time.
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Revoke a single grant and send the revocation to remote DWN endpoints.
326
+ * First ensures the revocation grant is on the remote DWN (self-healing).
327
+ * Returns `true` if at least one remote endpoint confirmed (202/409).
328
+ */
329
+ async function revokeAndSendSingle(
330
+ userAgent: EnboxUserAgent,
331
+ connectedDid: string,
332
+ delegateDid: string,
333
+ entry: RevocationEntry,
334
+ dwnEndpointUrls: string[],
335
+ ): Promise<boolean> {
336
+ // Self-healing: ensure the revocation grant is on the remote DWN.
337
+ await ensureRevocationGrantOnRemote(
338
+ userAgent, connectedDid, delegateDid, entry.revocationGrantId, dwnEndpointUrls,
339
+ );
340
+
341
+ // Read as the delegate (grant recipient), not the owner.
342
+ const { reply: readReply } = await userAgent.dwn.processRequest({
343
+ author : delegateDid,
344
+ target : connectedDid,
345
+ messageType : DwnInterface.RecordsRead,
346
+ messageParams : { filter: { recordId: entry.grantId } },
347
+ });
348
+ if (readReply.status.code !== 200 || !readReply.entry) { return false; }
349
+
350
+ // Reconstruct DwnDataEncodedRecordsWriteMessage: RecordsRead returns
351
+ // the data as a stream, but PermissionGrant.parse needs encodedData.
352
+ const grantDataBytes = readReply.entry.data
353
+ ? await DataStream.toBytes(readReply.entry.data)
354
+ : new Uint8Array(0);
355
+ const grantMessageWithData = {
356
+ ...readReply.entry.recordsWrite,
357
+ encodedData: Convert.uint8Array(grantDataBytes).toBase64Url(),
358
+ };
359
+ const grant = DwnPermissionGrant.parse(grantMessageWithData as any);
360
+
361
+ const { message } = await userAgent.permissions.createRevocation({
362
+ author : connectedDid,
363
+ store : true,
364
+ grant,
365
+ granteeDid : delegateDid,
366
+ permissionGrantId : entry.revocationGrantId,
367
+ });
368
+
369
+ return sendRevocationToEndpoints(userAgent, connectedDid, message, dwnEndpointUrls);
370
+ }
371
+
372
+ /**
373
+ * Send a revocation message to all owner DWN endpoints.
374
+ * Returns `true` if at least one endpoint confirmed (202/409).
375
+ */
376
+ async function sendRevocationToEndpoints(
377
+ userAgent: EnboxUserAgent,
378
+ connectedDid: string,
379
+ revocationMessage: any,
380
+ dwnEndpointUrls: string[],
381
+ ): Promise<boolean> {
382
+ if (!revocationMessage || dwnEndpointUrls.length === 0) { return false; }
383
+
384
+ const { encodedData, ...rawMessage } = revocationMessage;
385
+ const data = encodedData
386
+ ? new Blob([Convert.base64Url(encodedData).toUint8Array() as BlobPart])
387
+ : undefined;
388
+
389
+ for (const dwnUrl of dwnEndpointUrls) {
390
+ try {
391
+ const reply = await userAgent.rpc.sendDwnRequest({
392
+ dwnUrl,
393
+ targetDid : connectedDid,
394
+ message : rawMessage as any,
395
+ data,
396
+ });
397
+ if (reply?.status?.code === 202 || reply?.status?.code === 409) {
398
+ return true;
399
+ }
400
+ } catch {
401
+ // Per-endpoint failure — try the next one.
402
+ }
403
+ }
404
+ return false;
405
+ }
406
+
407
+ /** Clear the self-contained revocation retry context from storage. */
408
+ async function clearRetryState(storage: StorageAdapter): Promise<void> {
409
+ await storage.remove(STORAGE_KEYS.REVOCATION_RETRY_CONTEXT);
410
+ }
411
+
412
+ /**
413
+ * Retry grant revocations that were not confirmed by the owner's remote
414
+ * DWN during a previous disconnect. Called from `restoreSession()` AFTER
415
+ * sync is started and only when `REVOCATION_RETRY_CONTEXT` exists.
416
+ *
417
+ * This function does NOT restore a session — the user explicitly
418
+ * disconnected and the retry is purely a background cleanup.
419
+ */
420
+ export async function retryOrphanedRevocations(
421
+ userAgent: EnboxUserAgent,
422
+ storage: StorageAdapter,
423
+ ): Promise<void> {
424
+ let entries = await loadRetryEntries(storage);
425
+ if (entries.length === 0) {
426
+ await clearRetryState(storage);
427
+ return;
428
+ }
429
+
430
+ for (const entry of [...entries]) {
431
+ let remoteDwnUrls: string[] = [];
432
+ try {
433
+ remoteDwnUrls = await userAgent.dwn.getRemoteDwnEndpointUrls(entry.connectedDid);
434
+ } catch {
435
+ continue; // Can't resolve endpoints for this entry — try next.
436
+ }
437
+
438
+ const succeeded: string[] = [];
439
+ for (const revEntry of entry.revocations) {
440
+ try {
441
+ const confirmed = await revokeAndSendSingle(
442
+ userAgent, entry.connectedDid, entry.delegateDid, revEntry, remoteDwnUrls,
443
+ );
444
+ if (confirmed) { succeeded.push(revEntry.grantId); }
445
+ } catch {
446
+ // Individual failure — continue.
447
+ }
448
+ }
449
+
450
+ // Update the in-memory collection so the next iteration sees
451
+ // the correct state (avoid stale-snapshot overwrites).
452
+ const remaining = entry.revocations.filter((r) => !succeeded.includes(r.grantId));
453
+ if (remaining.length === 0) {
454
+ entries = entries.filter((e) => e.delegateDid !== entry.delegateDid);
455
+ } else {
456
+ entries = entries.map((e) =>
457
+ e.delegateDid === entry.delegateDid ? { ...e, revocations: remaining } : e,
458
+ );
459
+ }
460
+ }
461
+
462
+ // Write the final state once after processing all entries.
463
+ if (entries.length === 0) {
464
+ await clearRetryState(storage);
465
+ } else {
466
+ await storage.set(STORAGE_KEYS.REVOCATION_RETRY_CONTEXT, JSON.stringify(entries));
467
+ }
468
+ }
@@ -53,9 +53,14 @@ export async function walletConnect(
53
53
  }
54
54
 
55
55
  // Import delegate DID, process grants, and set up sync.
56
- const { delegatePortableDid, connectedDid, delegateGrants } = result;
56
+ const {
57
+ delegatePortableDid, connectedDid, delegateGrants, delegateDecryptionKeys,
58
+ delegateContextKeys, delegateMultiPartyProtocols, sessionRevocations,
59
+ } = result;
57
60
  const identity = await importDelegateAndSetupSync({
58
61
  userAgent, delegatePortableDid, connectedDid, delegateGrants,
62
+ delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols,
63
+ sessionRevocations,
59
64
  flowName: 'Wallet connect',
60
65
  });
61
66
 
package/src/index.ts CHANGED
@@ -63,6 +63,12 @@ export {
63
63
  // Storage adapters
64
64
  export { BrowserStorage, LevelStorage, MemoryStorage, createDefaultStorage } from './storage/storage.js';
65
65
 
66
+ // Revocation retry (exported for cross-package integration testing)
67
+ export { retryOrphanedRevocations } from './connect/restore.js';
68
+
69
+ // Storage keys (exported for cross-package integration testing)
70
+ export { STORAGE_KEYS } from './types.js';
71
+
66
72
  // Types
67
73
  export type {
68
74
  AuthEvent,
package/src/types.ts CHANGED
@@ -4,12 +4,12 @@
4
4
  */
5
5
 
6
6
  import type { PortableDid } from '@enbox/dids';
7
- import type { ConnectPermissionRequest, DwnDataEncodedRecordsWriteMessage, DwnProtocolDefinition, EnboxUserAgent, HdIdentityVault, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
7
+ import type { ConnectPermissionRequest, DelegateContextKey, DelegateDecryptionKey, DwnDataEncodedRecordsWriteMessage, DwnProtocolDefinition, EnboxUserAgent, HdIdentityVault, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
8
8
 
9
9
  import type { PasswordProvider } from './password-provider.js';
10
10
 
11
11
  // Re-export types that consumers will need
12
- export type { ConnectPermissionRequest, HdIdentityVault, IdentityVaultBackup, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
12
+ export type { ConnectPermissionRequest, DelegateContextKey, DelegateDecryptionKey, HdIdentityVault, IdentityVaultBackup, LocalDwnStrategy, PortableIdentity } from '@enbox/agent';
13
13
 
14
14
  // Re-export EnboxUserAgent so consumers don't need a direct @enbox/agent dep
15
15
  export type { EnboxUserAgent } from '@enbox/agent';
@@ -229,6 +229,30 @@ export interface ConnectResult {
229
229
 
230
230
  /** The DID of the identity the user approved (the wallet owner's DID). */
231
231
  connectedDid: string;
232
+
233
+ /**
234
+ * Scope-aware decryption keys for encrypted protocols.
235
+ *
236
+ * Derived only for read-like permission scopes (Read/Query/Subscribe) on
237
+ * protocols with `encryptionRequired: true` types. Write-only delegates
238
+ * receive no decryption keys.
239
+ */
240
+ delegateDecryptionKeys?: DelegateDecryptionKey[];
241
+
242
+ /**
243
+ * Context-scoped decryption keys for multi-party encrypted protocols.
244
+ * Each key unlocks one rootContextId for records using ProtocolContext encryption.
245
+ */
246
+ delegateContextKeys?: DelegateContextKey[];
247
+
248
+ /**
249
+ * Protocol URIs that have multi-party encrypted access patterns.
250
+ * Delivered even when no contexts exist yet (cold-start).
251
+ */
252
+ delegateMultiPartyProtocols?: string[];
253
+
254
+ /** Per-grant revocation mappings for session-bound self-revocation on disconnect. */
255
+ sessionRevocations?: { grantId: string; revocationGrantId: string }[];
232
256
  }
233
257
 
234
258
  /**
@@ -648,6 +672,26 @@ export const STORAGE_KEYS = {
648
672
  /** The connected DID (for wallet-connected sessions). */
649
673
  CONNECTED_DID: 'enbox:auth:connectedDid',
650
674
 
675
+ /**
676
+ * JSON-serialised `DelegateDecryptionKey[]` for delegate decryption of
677
+ * encrypted protocol records. Persisted so session restore can re-populate
678
+ * the delegate decryption key cache without requiring a new connect flow.
679
+ */
680
+ DELEGATE_DECRYPTION_KEYS: 'enbox:auth:delegateDecryptionKeys',
681
+
682
+ /**
683
+ * JSON-serialised `DelegateContextKey[]` for multi-party encrypted protocol
684
+ * records. Persisted for session restore.
685
+ */
686
+ DELEGATE_CONTEXT_KEYS: 'enbox:auth:delegateContextKeys',
687
+
688
+ /**
689
+ * JSON-serialised `string[]` of multi-party protocol URIs for delegate
690
+ * context key eligibility. Persisted for session restore so cold-start
691
+ * delegates (who connected with zero contexts) still receive future keys.
692
+ */
693
+ DELEGATE_MULTI_PARTY_PROTOCOLS: 'enbox:auth:delegateMultiPartyProtocols',
694
+
651
695
  /**
652
696
  * The base URL of the local DWN server discovered via the `dwn://connect`
653
697
  * browser redirect flow. Persisted so subsequent page loads can skip the
@@ -665,4 +709,20 @@ export const STORAGE_KEYS = {
665
709
  * @see https://github.com/enboxorg/enbox/issues/690
666
710
  */
667
711
  REGISTRATION_TOKENS: 'enbox:auth:registrationTokens',
712
+
713
+ /**
714
+ * JSON-serialised `SessionRevocationEntry[]` mapping session grant IDs to
715
+ * their corresponding revocation grant IDs for disconnect.
716
+ */
717
+ SESSION_REVOCATIONS: 'enbox:auth:sessionRevocations',
718
+
719
+ /**
720
+ * Self-contained collection of revocation retry entries from previous
721
+ * partial disconnects. JSON-serialised array of
722
+ * `{ delegateDid, connectedDid, revocations }` entries, one per
723
+ * session. Keyed by `delegateDid` (unique per session).
724
+ *
725
+ * Completely independent from active session state.
726
+ */
727
+ REVOCATION_RETRY_CONTEXT: 'enbox:auth:revocationRetryContext',
668
728
  } as const;
@@ -94,6 +94,10 @@ async function initClient({
94
94
  delegateGrants: EnboxConnectResponse['delegateGrants'];
95
95
  delegatePortableDid: EnboxConnectResponse['delegatePortableDid'];
96
96
  connectedDid: string;
97
+ delegateDecryptionKeys?: EnboxConnectResponse['delegateDecryptionKeys'];
98
+ delegateContextKeys?: EnboxConnectResponse['delegateContextKeys'];
99
+ delegateMultiPartyProtocols?: EnboxConnectResponse['delegateMultiPartyProtocols'];
100
+ sessionRevocations?: EnboxConnectResponse['sessionRevocations'];
97
101
  } | undefined> {
98
102
  // ephemeral client did for ECDH, signing, verification
99
103
  const clientDid = await DidJwk.create();
@@ -187,9 +191,13 @@ async function initClient({
187
191
  })) as unknown as EnboxConnectResponse;
188
192
 
189
193
  return {
190
- delegateGrants : verifiedResponse.delegateGrants,
191
- delegatePortableDid : verifiedResponse.delegatePortableDid,
192
- connectedDid : verifiedResponse.providerDid,
194
+ delegateGrants : verifiedResponse.delegateGrants,
195
+ delegatePortableDid : verifiedResponse.delegatePortableDid,
196
+ connectedDid : verifiedResponse.providerDid,
197
+ delegateDecryptionKeys : verifiedResponse.delegateDecryptionKeys,
198
+ delegateContextKeys : verifiedResponse.delegateContextKeys,
199
+ delegateMultiPartyProtocols : verifiedResponse.delegateMultiPartyProtocols,
200
+ sessionRevocations : verifiedResponse.sessionRevocations,
193
201
  };
194
202
  }
195
203
  }