@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.
- package/dist/esm/auth-manager.js +204 -6
- package/dist/esm/auth-manager.js.map +1 -1
- package/dist/esm/connect/lifecycle.js +62 -5
- package/dist/esm/connect/lifecycle.js.map +1 -1
- package/dist/esm/connect/restore.js +321 -12
- package/dist/esm/connect/restore.js.map +1 -1
- package/dist/esm/connect/wallet.js +3 -1
- package/dist/esm/connect/wallet.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types.js +31 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/wallet-connect-client.js +4 -0
- package/dist/esm/wallet-connect-client.js.map +1 -1
- package/dist/types/auth-manager.d.ts.map +1 -1
- package/dist/types/connect/lifecycle.d.ts +8 -1
- package/dist/types/connect/lifecycle.d.ts.map +1 -1
- package/dist/types/connect/restore.d.ts +16 -0
- package/dist/types/connect/restore.d.ts.map +1 -1
- package/dist/types/connect/wallet.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/types.d.ts +56 -2
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/wallet-connect-client.d.ts +4 -0
- package/dist/types/wallet-connect-client.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/auth-manager.ts +198 -4
- package/src/connect/lifecycle.ts +77 -6
- package/src/connect/restore.ts +348 -13
- package/src/connect/wallet.ts +6 -1
- package/src/index.ts +6 -0
- package/src/types.ts +62 -2
- package/src/wallet-connect-client.ts +11 -3
package/src/connect/restore.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
+
}
|
package/src/connect/wallet.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
191
|
-
delegatePortableDid
|
|
192
|
-
connectedDid
|
|
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
|
}
|