@enbox/auth 0.6.32 → 0.6.34
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 +105 -52
- package/dist/esm/auth-manager.js.map +1 -1
- package/dist/esm/connect/import.js +17 -0
- package/dist/esm/connect/import.js.map +1 -1
- package/dist/esm/connect/lifecycle.js +31 -33
- package/dist/esm/connect/lifecycle.js.map +1 -1
- package/dist/esm/connect/restore.js +3 -2
- package/dist/esm/connect/restore.js.map +1 -1
- package/dist/esm/connect/{local.js → vault.js} +32 -5
- package/dist/esm/connect/vault.js.map +1 -0
- package/dist/esm/connect/wallet.js +10 -2
- package/dist/esm/connect/wallet.js.map +1 -1
- package/dist/esm/identity-session.js +11 -23
- package/dist/esm/identity-session.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/password-provider.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/wallet-connect-client.js +12 -10
- package/dist/esm/wallet-connect-client.js.map +1 -1
- package/dist/types/auth-manager.d.ts +57 -19
- 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 +19 -5
- package/dist/types/connect/lifecycle.d.ts.map +1 -1
- package/dist/types/connect/restore.d.ts.map +1 -1
- package/dist/types/connect/{local.d.ts → vault.d.ts} +9 -6
- package/dist/types/connect/vault.d.ts.map +1 -0
- package/dist/types/connect/wallet.d.ts.map +1 -1
- package/dist/types/identity-session.d.ts +11 -42
- package/dist/types/identity-session.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/password-provider.d.ts +25 -9
- package/dist/types/password-provider.d.ts.map +1 -1
- package/dist/types/types.d.ts +25 -18
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/wallet-connect-client.d.ts.map +1 -1
- package/package.json +11 -7
- package/src/auth-manager.ts +124 -66
- package/src/connect/import.ts +19 -0
- package/src/connect/lifecycle.ts +61 -39
- package/src/connect/restore.ts +15 -5
- package/src/connect/{local.ts → vault.ts} +35 -6
- package/src/connect/wallet.ts +11 -3
- package/src/identity-session.ts +11 -55
- package/src/index.ts +2 -1
- package/src/password-provider.ts +27 -11
- package/src/types.ts +27 -20
- package/src/wallet-connect-client.ts +13 -11
- package/dist/esm/connect/local.js.map +0 -1
- package/dist/types/connect/local.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enbox/auth",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "Headless authentication and identity management SDK for Enbox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/esm/index.js",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
".": {
|
|
38
38
|
"types": "./dist/types/index.d.ts",
|
|
39
39
|
"import": "./dist/esm/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./auth-manager": {
|
|
42
|
+
"types": "./dist/types/auth-manager.d.ts",
|
|
43
|
+
"import": "./dist/esm/auth-manager.js"
|
|
40
44
|
}
|
|
41
45
|
},
|
|
42
46
|
"keywords": [
|
|
@@ -56,12 +60,12 @@
|
|
|
56
60
|
"bun": ">=1.0.0"
|
|
57
61
|
},
|
|
58
62
|
"dependencies": {
|
|
59
|
-
"@enbox/agent": "0.7.
|
|
60
|
-
"@enbox/common": "0.1.
|
|
61
|
-
"@enbox/crypto": "0.1.
|
|
62
|
-
"@enbox/dids": "0.1.
|
|
63
|
-
"@enbox/dwn-clients": "0.4.
|
|
64
|
-
"@enbox/dwn-sdk-js": "0.3.
|
|
63
|
+
"@enbox/agent": "0.7.2",
|
|
64
|
+
"@enbox/common": "0.1.1",
|
|
65
|
+
"@enbox/crypto": "0.1.1",
|
|
66
|
+
"@enbox/dids": "0.1.1",
|
|
67
|
+
"@enbox/dwn-clients": "0.4.1",
|
|
68
|
+
"@enbox/dwn-sdk-js": "0.3.6",
|
|
65
69
|
"level": "8.0.1"
|
|
66
70
|
},
|
|
67
71
|
"devDependencies": {
|
package/src/auth-manager.ts
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Replaces `Enbox.connect()` (formerly `Web5.connect()`) with a composable,
|
|
5
5
|
* multi-identity-aware auth system that works in both browser and CLI environments.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: this file is also exposed as the `@enbox/auth/auth-manager` subpath
|
|
8
|
+
* export so `@enbox/api` can import the AuthManager class without pulling in
|
|
9
|
+
* the full barrel. The subpath is **internal to the monorepo** — external
|
|
10
|
+
* consumers should import from `@enbox/auth` (or `@enbox/browser`) instead;
|
|
11
|
+
* the subpath's file layout is not part of any stability guarantee.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
6
14
|
* @module
|
|
7
15
|
*/
|
|
8
16
|
|
|
9
|
-
import type {
|
|
17
|
+
import type { RecordsWriteMessage } from '@enbox/dwn-sdk-js';
|
|
18
|
+
import type { AgentSessionIdentity, BearerIdentity, DwnDataEncodedRecordsWriteMessage, HdIdentityVault, PortableIdentity } from '@enbox/agent';
|
|
10
19
|
|
|
11
20
|
import type { FlowContext } from './connect/lifecycle.js';
|
|
12
21
|
import type { PasswordProvider } from './password-provider.js';
|
|
@@ -20,15 +29,14 @@ import type {
|
|
|
20
29
|
DisconnectOptions,
|
|
21
30
|
HandlerConnectOptions,
|
|
22
31
|
HeadlessConnectOptions,
|
|
23
|
-
IdentityInfo,
|
|
24
32
|
ImportFromPhraseOptions,
|
|
25
33
|
ImportFromPortableOptions,
|
|
26
|
-
LocalConnectOptions,
|
|
27
34
|
RegistrationOptions,
|
|
28
35
|
RestoreSessionOptions,
|
|
29
36
|
ShutdownOptions,
|
|
30
37
|
StorageAdapter,
|
|
31
38
|
SyncOption,
|
|
39
|
+
VaultConnectOptions,
|
|
32
40
|
WalletConnectOptions,
|
|
33
41
|
} from './types.js';
|
|
34
42
|
|
|
@@ -40,10 +48,10 @@ import { AuthEventEmitter } from './events.js';
|
|
|
40
48
|
import { AuthSession } from './identity-session.js';
|
|
41
49
|
import { createDefaultStorage } from './storage/storage.js';
|
|
42
50
|
import { discoverLocalDwn } from './discovery.js';
|
|
43
|
-
import { localConnect } from './connect/local.js';
|
|
44
51
|
import { normalizeProtocolRequests } from './permissions.js';
|
|
45
52
|
import { restoreSession } from './connect/restore.js';
|
|
46
53
|
import { STORAGE_KEYS } from './types.js';
|
|
54
|
+
import { vaultConnect } from './connect/vault.js';
|
|
47
55
|
import { walletConnect } from './connect/wallet.js';
|
|
48
56
|
import { deriveActiveSyncScope, ensureVaultReady, finalizeDelegateSession, importDelegateAndSetupSync, resolveIdentityDids, resolvePassword, startSyncIfEnabled, toSyncIdentityProtocols } from './connect/lifecycle.js';
|
|
49
57
|
import { importFromPhrase, importFromPortable } from './connect/import.js';
|
|
@@ -241,27 +249,45 @@ export class AuthManager {
|
|
|
241
249
|
const restored = await restoreSession(this._flowContext());
|
|
242
250
|
if (restored) { return restored; }
|
|
243
251
|
|
|
244
|
-
// 2. Route to the appropriate flow.
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
// 2. Route to the appropriate flow. `_isVaultConnect` is a type guard
|
|
253
|
+
// so the two branches receive correctly narrowed `options` types
|
|
254
|
+
// without casts.
|
|
255
|
+
if (this._isVaultConnect(options)) {
|
|
256
|
+
return vaultConnect(this._flowContext(), options);
|
|
247
257
|
}
|
|
248
258
|
|
|
249
|
-
return this._handlerConnect(options
|
|
259
|
+
return this._handlerConnect(options);
|
|
250
260
|
});
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
/**
|
|
254
|
-
* Create or reconnect a local identity (explicit
|
|
264
|
+
* Create or reconnect a local identity (explicit vault connect).
|
|
255
265
|
*
|
|
256
|
-
* Use this when you explicitly want the
|
|
266
|
+
* Use this when you explicitly want the vault flow, bypassing
|
|
257
267
|
* auto-detection. This is the preferred method for wallet apps.
|
|
258
268
|
*
|
|
259
|
-
* @param options -
|
|
269
|
+
* @param options - Vault connect options.
|
|
260
270
|
* @returns An active AuthSession.
|
|
261
271
|
* @throws If a connection attempt is already in progress.
|
|
272
|
+
* @deprecated Use {@link connectVault} instead. Will be removed in 1.0.
|
|
262
273
|
*/
|
|
263
|
-
async connectLocal(options?:
|
|
264
|
-
return this.
|
|
274
|
+
async connectLocal(options?: VaultConnectOptions): Promise<AuthSession> {
|
|
275
|
+
return this.connectVault(options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create or reconnect an identity via the local HD vault.
|
|
280
|
+
*
|
|
281
|
+
* Use this when you explicitly want the vault flow, bypassing
|
|
282
|
+
* auto-detection. This is the preferred method for wallet apps
|
|
283
|
+
* and CLI tools that own the identity vault directly.
|
|
284
|
+
*
|
|
285
|
+
* @param options - Vault connect options.
|
|
286
|
+
* @returns An active AuthSession.
|
|
287
|
+
* @throws If a connection attempt is already in progress.
|
|
288
|
+
*/
|
|
289
|
+
async connectVault(options?: VaultConnectOptions): Promise<AuthSession> {
|
|
290
|
+
return this._withConnect(() => vaultConnect(this._flowContext(), options));
|
|
265
291
|
}
|
|
266
292
|
|
|
267
293
|
/**
|
|
@@ -305,6 +331,7 @@ export class AuthManager {
|
|
|
305
331
|
* This replaces the manual `previouslyConnected` localStorage pattern.
|
|
306
332
|
*/
|
|
307
333
|
async restoreSession(options?: RestoreSessionOptions): Promise<AuthSession | undefined> {
|
|
334
|
+
this._guardShutdown();
|
|
308
335
|
this._guardConcurrency();
|
|
309
336
|
this._isConnecting = true;
|
|
310
337
|
|
|
@@ -345,6 +372,7 @@ export class AuthManager {
|
|
|
345
372
|
* ```
|
|
346
373
|
*/
|
|
347
374
|
async connectHeadless(options?: HeadlessConnectOptions): Promise<AuthSession> {
|
|
375
|
+
this._guardShutdown();
|
|
348
376
|
let password = options?.password ?? this._defaultPassword;
|
|
349
377
|
const isFirstLaunch = await this._userAgent.firstLaunch();
|
|
350
378
|
|
|
@@ -385,7 +413,7 @@ export class AuthManager {
|
|
|
385
413
|
|
|
386
414
|
const { connectedDid, delegateDid } = resolveIdentityDids(identity);
|
|
387
415
|
|
|
388
|
-
const identityInfo:
|
|
416
|
+
const identityInfo: AgentSessionIdentity = {
|
|
389
417
|
didUri : connectedDid,
|
|
390
418
|
name : identity.metadata.name,
|
|
391
419
|
connectedDid : identity.metadata.connectedDid,
|
|
@@ -496,17 +524,17 @@ export class AuthManager {
|
|
|
496
524
|
messageType : DwnInterface.RecordsRead,
|
|
497
525
|
messageParams : { filter: { recordId: grantId } },
|
|
498
526
|
});
|
|
499
|
-
if (readReply.status.code !== 200 || !readReply.entry) { continue; }
|
|
527
|
+
if (readReply.status.code !== 200 || !readReply.entry?.recordsWrite) { continue; }
|
|
500
528
|
// Reconstruct DwnDataEncodedRecordsWriteMessage: RecordsRead returns
|
|
501
529
|
// data as a stream, but PermissionGrant.parse needs encodedData.
|
|
502
530
|
const grantDataBytes = readReply.entry.data
|
|
503
531
|
? await DataStream.toBytes(readReply.entry.data)
|
|
504
532
|
: new Uint8Array(0);
|
|
505
|
-
const grantMsgWithData = {
|
|
533
|
+
const grantMsgWithData: DwnDataEncodedRecordsWriteMessage = {
|
|
506
534
|
...readReply.entry.recordsWrite,
|
|
507
535
|
encodedData: Convert.uint8Array(grantDataBytes).toBase64Url(),
|
|
508
536
|
};
|
|
509
|
-
const grant = DwnPermissionGrant.parse(grantMsgWithData
|
|
537
|
+
const grant = DwnPermissionGrant.parse(grantMsgWithData);
|
|
510
538
|
|
|
511
539
|
// Self-healing: ensure the revocation grant is on the remote
|
|
512
540
|
// DWN. The best-effort fanout at connect time may have failed.
|
|
@@ -519,7 +547,19 @@ export class AuthManager {
|
|
|
519
547
|
messageParams : { filter: { recordId: revocationGrantId } },
|
|
520
548
|
});
|
|
521
549
|
if (revGrantReply.status.code === 200 && revGrantReply.entry?.recordsWrite) {
|
|
522
|
-
|
|
550
|
+
// Strip `encodedData` from the wire payload — the
|
|
551
|
+
// bytes are sent via `data` (Blob) on the next line.
|
|
552
|
+
// `RecordsWriteMessage` doesn't declare `encodedData`,
|
|
553
|
+
// but the wire-format reply may include it; widen the
|
|
554
|
+
// local type to acknowledge that without `any`.
|
|
555
|
+
// NOSONAR S4325 false positive: the cast is required to
|
|
556
|
+
// typecheck the destructuring of the undeclared
|
|
557
|
+
// optional `encodedData` property; removing it fails
|
|
558
|
+
// TS2339. Sonar reads the intersection-with-optional-
|
|
559
|
+
// field as a no-op widening, which it isn't here.
|
|
560
|
+
type RecordsWriteWireMessage = RecordsWriteMessage & { encodedData?: string };
|
|
561
|
+
const { encodedData: _encoded, ...revGrantRaw } =
|
|
562
|
+
revGrantReply.entry.recordsWrite as RecordsWriteWireMessage; // NOSONAR
|
|
523
563
|
const revGrantData = revGrantReply.entry.data
|
|
524
564
|
? new Blob([await DataStream.toBytes(revGrantReply.entry.data) as BlobPart])
|
|
525
565
|
: undefined;
|
|
@@ -552,7 +592,7 @@ export class AuthManager {
|
|
|
552
592
|
// delivery, the owner-side authority source won't see it.
|
|
553
593
|
let remoteDelivered = false;
|
|
554
594
|
if (revocationMessage && remoteDwnUrls.length > 0) {
|
|
555
|
-
const { encodedData, ...rawMessage } = revocationMessage
|
|
595
|
+
const { encodedData, ...rawMessage } = revocationMessage;
|
|
556
596
|
const data = encodedData
|
|
557
597
|
? new Blob([Convert.base64Url(encodedData).toUint8Array() as BlobPart])
|
|
558
598
|
: undefined;
|
|
@@ -783,7 +823,7 @@ export class AuthManager {
|
|
|
783
823
|
* Each identity has a DID URI, name, and optional connected DID
|
|
784
824
|
* (for wallet-connected/delegated identities).
|
|
785
825
|
*/
|
|
786
|
-
async listIdentities(): Promise<
|
|
826
|
+
async listIdentities(): Promise<AgentSessionIdentity[]> {
|
|
787
827
|
const identities = await this._userAgent.identity.list();
|
|
788
828
|
return identities.map((identity: BearerIdentity) => ({
|
|
789
829
|
didUri : identity.did.uri,
|
|
@@ -815,7 +855,7 @@ export class AuthManager {
|
|
|
815
855
|
await this._storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
816
856
|
await this._storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
817
857
|
|
|
818
|
-
const identityInfo:
|
|
858
|
+
const identityInfo: AgentSessionIdentity = {
|
|
819
859
|
didUri : connectedDid,
|
|
820
860
|
name : identity.metadata.name,
|
|
821
861
|
connectedDid : identity.metadata.connectedDid,
|
|
@@ -952,47 +992,40 @@ export class AuthManager {
|
|
|
952
992
|
// ─── Private helpers ───────────────────────────────────────────
|
|
953
993
|
|
|
954
994
|
/**
|
|
955
|
-
* Determine whether the given options indicate a
|
|
995
|
+
* Determine whether the given options indicate a vault connect flow.
|
|
956
996
|
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
997
|
+
* Handler intent is asserted by the presence of a non-empty `protocols`
|
|
998
|
+
* array OR a non-null `connectHandler`; everything else (including the
|
|
999
|
+
* no-options case, an empty `protocols: []`, and `null` values from JS
|
|
1000
|
+
* callers) routes to vault connect. An empty `protocols` array is
|
|
1001
|
+
* intentionally NOT a handler signal — it carries no permission scopes
|
|
1002
|
+
* for the handler to authorize, so treating it as handler-flow would
|
|
1003
|
+
* produce a zero-grant "connected" session indistinguishable from a
|
|
1004
|
+
* denied connect.
|
|
1005
|
+
*
|
|
1006
|
+
* Acts as a TypeScript type guard: a `true` return narrows `options` to
|
|
1007
|
+
* `VaultConnectOptions | undefined` at call sites, so the routing in
|
|
1008
|
+
* {@link AuthManager.connect} can dispatch without unsafe casts.
|
|
961
1009
|
*/
|
|
962
|
-
private
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const hasLocalSignals = (
|
|
967
|
-
o.password !== undefined ||
|
|
968
|
-
o.createIdentity !== undefined ||
|
|
969
|
-
o.recoveryPhrase !== undefined ||
|
|
970
|
-
o.dwnEndpoints !== undefined ||
|
|
971
|
-
o.metadata !== undefined
|
|
972
|
-
);
|
|
973
|
-
if (hasLocalSignals) { return true; }
|
|
974
|
-
|
|
975
|
-
// If any handler-connect signals are present, use the handler flow.
|
|
976
|
-
const hasHandlerSignals = (
|
|
977
|
-
o.protocols !== undefined ||
|
|
978
|
-
o.connectHandler !== undefined
|
|
979
|
-
);
|
|
980
|
-
if (hasHandlerSignals) { return false; }
|
|
981
|
-
|
|
982
|
-
// No explicit signals → default to local connect.
|
|
983
|
-
// Callers that want handler-based connect must provide protocols
|
|
984
|
-
// or a connectHandler.
|
|
1010
|
+
private _isVaultConnect(options?: ConnectOptions): options is VaultConnectOptions | undefined {
|
|
1011
|
+
if (options === undefined || options === null) { return true; }
|
|
1012
|
+
if ('protocols' in options && Array.isArray(options.protocols) && options.protocols.length > 0) { return false; }
|
|
1013
|
+
if ('connectHandler' in options && options.connectHandler !== undefined && options.connectHandler !== null) { return false; }
|
|
985
1014
|
return true;
|
|
986
1015
|
}
|
|
987
1016
|
|
|
988
1017
|
/**
|
|
989
1018
|
* Run a handler-based (delegated) connect flow.
|
|
990
1019
|
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
1020
|
+
* Handler resolution happens BEFORE any vault I/O so a misconfigured call
|
|
1021
|
+
* (no handler reachable) fails fast without mutating on-disk state.
|
|
1022
|
+
*
|
|
1023
|
+
* 1. Resolve the connect handler (fast-fail when none is reachable).
|
|
1024
|
+
* 2. Initialize the vault (agent-only, no identity).
|
|
1025
|
+
* 3. Normalize protocol permission requests.
|
|
1026
|
+
* 4. Delegate to the connect handler for credential acquisition.
|
|
1027
|
+
* 5. Import the delegate DID, process grants, set up sync.
|
|
1028
|
+
* 6. Finalize and return the AuthSession.
|
|
996
1029
|
*/
|
|
997
1030
|
private async _handlerConnect(
|
|
998
1031
|
options?: HandlerConnectOptions,
|
|
@@ -1001,15 +1034,8 @@ export class AuthManager {
|
|
|
1001
1034
|
const { userAgent, emitter, storage } = ctx;
|
|
1002
1035
|
const sync = options?.sync ?? ctx.defaultSync;
|
|
1003
1036
|
|
|
1004
|
-
// 1.
|
|
1005
|
-
|
|
1006
|
-
const password = await resolvePassword(ctx, undefined, isFirstLaunch);
|
|
1007
|
-
await ensureVaultReady({ userAgent, emitter, password, isFirstLaunch });
|
|
1008
|
-
|
|
1009
|
-
// 2. Normalize protocol requests.
|
|
1010
|
-
const permissionRequests = normalizeProtocolRequests(options?.protocols);
|
|
1011
|
-
|
|
1012
|
-
// 3. Resolve the handler.
|
|
1037
|
+
// 1. Resolve the handler FIRST. Anything past this point may mutate the
|
|
1038
|
+
// vault on disk, so misconfiguration must fail before that happens.
|
|
1013
1039
|
const handler = options?.connectHandler ?? this._connectHandler;
|
|
1014
1040
|
if (!handler) {
|
|
1015
1041
|
throw new Error(
|
|
@@ -1019,6 +1045,16 @@ export class AuthManager {
|
|
|
1019
1045
|
);
|
|
1020
1046
|
}
|
|
1021
1047
|
|
|
1048
|
+
// 2. Initialize vault (agent-only, no identity). The per-call password
|
|
1049
|
+
// (when supplied via `HandlerConnectOptions.password`) wins over the
|
|
1050
|
+
// manager default, matching the behavior of the vault-connect flow.
|
|
1051
|
+
const isFirstLaunch = await userAgent.firstLaunch();
|
|
1052
|
+
const password = await resolvePassword(ctx, options?.password, isFirstLaunch);
|
|
1053
|
+
await ensureVaultReady({ userAgent, emitter, password, isFirstLaunch });
|
|
1054
|
+
|
|
1055
|
+
// 3. Normalize protocol requests.
|
|
1056
|
+
const permissionRequests = normalizeProtocolRequests(options?.protocols);
|
|
1057
|
+
|
|
1022
1058
|
// 4. Delegate to the handler.
|
|
1023
1059
|
const result = await handler.requestAccess({ permissionRequests });
|
|
1024
1060
|
if (!result) {
|
|
@@ -1033,14 +1069,22 @@ export class AuthManager {
|
|
|
1033
1069
|
const identity = await importDelegateAndSetupSync({
|
|
1034
1070
|
userAgent, delegatePortableDid, connectedDid, delegateGrants,
|
|
1035
1071
|
delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols,
|
|
1036
|
-
sessionRevocations,
|
|
1037
1072
|
flowName: 'Connect',
|
|
1038
1073
|
});
|
|
1039
1074
|
|
|
1040
|
-
// 6. Finalize session.
|
|
1075
|
+
// 6. Finalize session. Pass the transient delegate state explicitly
|
|
1076
|
+
// so `persistOrClearDelegateSecrets` doesn't need to read it back
|
|
1077
|
+
// off the identity (which was the old `(identity as any)._foo`
|
|
1078
|
+
// smuggling pattern).
|
|
1041
1079
|
return finalizeDelegateSession({
|
|
1042
1080
|
userAgent, emitter, storage, identity,
|
|
1043
|
-
connectedDid, delegateDid: delegatePortableDid.uri, sync,
|
|
1081
|
+
connectedDid, delegateDid : delegatePortableDid.uri, sync,
|
|
1082
|
+
delegateState : {
|
|
1083
|
+
delegateDecryptionKeys,
|
|
1084
|
+
delegateContextKeys,
|
|
1085
|
+
delegateMultiPartyProtocols,
|
|
1086
|
+
sessionRevocations,
|
|
1087
|
+
},
|
|
1044
1088
|
});
|
|
1045
1089
|
}
|
|
1046
1090
|
|
|
@@ -1070,8 +1114,13 @@ export class AuthManager {
|
|
|
1070
1114
|
* Consolidates the duplicated concurrency guard, `_isConnecting` flag management,
|
|
1071
1115
|
* session assignment, and state transition across `connect()`, `walletConnect()`,
|
|
1072
1116
|
* `importFromPhrase()`, and `importFromPortable()`.
|
|
1117
|
+
*
|
|
1118
|
+
* Also short-circuits if the manager has already been shut down — using
|
|
1119
|
+
* a closed manager would otherwise fail deep inside sync/storage with an
|
|
1120
|
+
* unhelpful error.
|
|
1073
1121
|
*/
|
|
1074
1122
|
private async _withConnect(fn: () => Promise<AuthSession>): Promise<AuthSession> {
|
|
1123
|
+
this._guardShutdown();
|
|
1075
1124
|
this._guardConcurrency();
|
|
1076
1125
|
this._isConnecting = true;
|
|
1077
1126
|
|
|
@@ -1186,4 +1235,13 @@ export class AuthManager {
|
|
|
1186
1235
|
);
|
|
1187
1236
|
}
|
|
1188
1237
|
}
|
|
1238
|
+
|
|
1239
|
+
private _guardShutdown(): void {
|
|
1240
|
+
if (this._isShutDown) {
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
'[@enbox/auth] AuthManager has been shut down and cannot be reused. ' +
|
|
1243
|
+
'Create a new instance via AuthManager.create().'
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1189
1247
|
}
|
package/src/connect/import.ts
CHANGED
|
@@ -10,10 +10,17 @@ import type { AuthSession } from '../identity-session.js';
|
|
|
10
10
|
import type { FlowContext } from './lifecycle.js';
|
|
11
11
|
import type { ImportFromPhraseOptions, ImportFromPortableOptions } from '../types.js';
|
|
12
12
|
|
|
13
|
+
import { IdentityProtocolDefinition, JwkProtocolDefinition } from '@enbox/agent';
|
|
14
|
+
|
|
13
15
|
import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
|
|
14
16
|
import { registerWithDwnEndpoints } from '../registration.js';
|
|
15
17
|
import { createDefaultIdentity, ensureVaultReady, finalizeSession, registerSyncScopeForIdentity, resolveIdentityDids, startSyncIfEnabled } from './lifecycle.js';
|
|
16
18
|
|
|
19
|
+
const AGENT_DID_SYNC_PROTOCOLS: [string, ...string[]] = [
|
|
20
|
+
IdentityProtocolDefinition.protocol,
|
|
21
|
+
JwkProtocolDefinition.protocol,
|
|
22
|
+
];
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Import (or recover) an identity from a BIP-39 recovery phrase.
|
|
19
26
|
*
|
|
@@ -80,6 +87,18 @@ export async function importFromPhrase(
|
|
|
80
87
|
await registerSyncScopeForIdentity({ userAgent, connectedDid });
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
// Register the agent DID for sync (same rationale as vaultConnect).
|
|
91
|
+
if (sync !== 'off') {
|
|
92
|
+
try {
|
|
93
|
+
await userAgent.sync.registerIdentity({
|
|
94
|
+
did : userAgent.agentDid.uri,
|
|
95
|
+
options : { protocols: AGENT_DID_SYNC_PROTOCOLS },
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
// Already registered from a previous session.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
// Start sync.
|
|
84
103
|
await startSyncIfEnabled(userAgent, sync);
|
|
85
104
|
|
package/src/connect/lifecycle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared helpers for connect flows.
|
|
3
3
|
*
|
|
4
|
-
* Consolidates duplicated logic across `
|
|
4
|
+
* Consolidates duplicated logic across `vault-connect`, `session-restore`,
|
|
5
5
|
* `wallet-connect`, and `import-identity` flows:
|
|
6
6
|
*
|
|
7
7
|
* - Password resolution chain
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { PortableDid } from '@enbox/dids';
|
|
18
|
-
import type { BearerIdentity, DelegateContextKey, DelegateDecryptionKey, DwnDataEncodedRecordsWriteMessage, EnboxUserAgent } from '@enbox/agent';
|
|
18
|
+
import type { AgentSessionIdentity, BearerIdentity, DelegateContextKey, DelegateDecryptionKey, DwnDataEncodedRecordsWriteMessage, EnboxUserAgent } from '@enbox/agent';
|
|
19
19
|
|
|
20
20
|
import type { AuthEventEmitter } from '../events.js';
|
|
21
21
|
import type { PasswordProvider } from '../password-provider.js';
|
|
22
|
-
import type {
|
|
22
|
+
import type { RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
|
|
23
23
|
|
|
24
24
|
import { Convert } from '@enbox/common';
|
|
25
25
|
import type { GenericMessage } from '@enbox/dwn-sdk-js';
|
|
@@ -35,7 +35,7 @@ import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '
|
|
|
35
35
|
/**
|
|
36
36
|
* Unified context passed from `AuthManager` to every connect flow.
|
|
37
37
|
*
|
|
38
|
-
* Replaces the per-flow `
|
|
38
|
+
* Replaces the per-flow `VaultConnectContext`, `SessionRestoreContext`,
|
|
39
39
|
* `WalletConnectContext`, and `ImportContext` interfaces. All fields are
|
|
40
40
|
* optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
|
|
41
41
|
* flows only consume what they need.
|
|
@@ -91,11 +91,15 @@ export async function resolvePassword(
|
|
|
91
91
|
|
|
92
92
|
password ??= INSECURE_DEFAULT_PASSWORD;
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// Two cases reach this branch: no password was supplied at any level
|
|
95
|
+
// (and we fell through to the insecure default), or the caller explicitly
|
|
96
|
+
// supplied an empty string. Both produce a vault with effectively zero
|
|
97
|
+
// password protection — surface a single warning in both cases.
|
|
98
|
+
if (password === INSECURE_DEFAULT_PASSWORD || password.length === 0) {
|
|
95
99
|
console.warn(
|
|
96
|
-
'[@enbox/auth] SECURITY WARNING: No password set
|
|
97
|
-
'
|
|
98
|
-
'
|
|
100
|
+
'[@enbox/auth] SECURITY WARNING: No password set (or an empty string was supplied). ' +
|
|
101
|
+
'Using an insecure default; this leaves the identity vault unprotected. ' +
|
|
102
|
+
'Set a non-empty password via AuthManager.create({ password }) or connect({ password }).'
|
|
99
103
|
);
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -180,7 +184,7 @@ export async function startSyncIfEnabled(
|
|
|
180
184
|
* encryption keys, and a DWN service endpoint.
|
|
181
185
|
*
|
|
182
186
|
* This consolidates the identical identity creation block that was
|
|
183
|
-
* duplicated in `
|
|
187
|
+
* duplicated in `vaultConnect` and `importFromPhrase`.
|
|
184
188
|
*
|
|
185
189
|
* @internal
|
|
186
190
|
*/
|
|
@@ -270,9 +274,11 @@ export function deriveSyncScopeFromGrants(grants: DwnPermissionGrant[]): 'all' |
|
|
|
270
274
|
const protocols = new Set<string>();
|
|
271
275
|
|
|
272
276
|
for (const grant of grants) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
//
|
|
277
|
+
// Only Messages.Read grants authorize sync. The discriminated union
|
|
278
|
+
// (`PermissionScope = Messages | Protocols | Records`) narrows
|
|
279
|
+
// `scope.protocol` to `string | undefined` after the interface +
|
|
280
|
+
// method checks — no cast needed.
|
|
281
|
+
const { scope } = grant;
|
|
276
282
|
if (scope.interface !== 'Messages' || scope.method !== 'Read') {
|
|
277
283
|
continue;
|
|
278
284
|
}
|
|
@@ -282,7 +288,7 @@ export function deriveSyncScopeFromGrants(grants: DwnPermissionGrant[]): 'all' |
|
|
|
282
288
|
continue;
|
|
283
289
|
}
|
|
284
290
|
|
|
285
|
-
const protocol = scope.protocol
|
|
291
|
+
const protocol = scope.protocol;
|
|
286
292
|
if (protocol === undefined) {
|
|
287
293
|
// Unrestricted Messages.Read — delegate can sync all protocols.
|
|
288
294
|
return 'all';
|
|
@@ -331,10 +337,15 @@ export async function deriveActiveSyncScope(
|
|
|
331
337
|
if (revocationResponse.reply.status.code !== 200) { return []; }
|
|
332
338
|
|
|
333
339
|
// Build the set of revoked grant IDs from revocation parent context.
|
|
340
|
+
// `descriptor.parentId` is the canonical location; the top-level
|
|
341
|
+
// `parentId` is a legacy/alternate-format fallback. Typed narrowly
|
|
342
|
+
// (no `any`) so additions to the shape land in the type system.
|
|
334
343
|
const revokedGrantIds = new Set<string>();
|
|
335
344
|
if (revocationResponse.reply.entries) {
|
|
336
345
|
for (const entry of revocationResponse.reply.entries as DwnDataEncodedRecordsWriteMessage[]) {
|
|
337
|
-
const parentId =
|
|
346
|
+
const parentId =
|
|
347
|
+
entry.descriptor.parentId
|
|
348
|
+
?? (entry as { parentId?: string }).parentId;
|
|
338
349
|
if (parentId) { revokedGrantIds.add(parentId); }
|
|
339
350
|
}
|
|
340
351
|
}
|
|
@@ -586,13 +597,12 @@ export async function importDelegateAndSetupSync(params: {
|
|
|
586
597
|
delegateDecryptionKeys?: DelegateDecryptionKey[];
|
|
587
598
|
delegateContextKeys?: DelegateContextKey[];
|
|
588
599
|
delegateMultiPartyProtocols?: string[];
|
|
589
|
-
sessionRevocations?: { grantId: string; revocationGrantId: string }[];
|
|
590
600
|
flowName: string;
|
|
591
601
|
}): Promise<BearerIdentity> {
|
|
592
602
|
const {
|
|
593
603
|
userAgent, delegatePortableDid, connectedDid, delegateGrants,
|
|
594
604
|
delegateDecryptionKeys, delegateContextKeys, delegateMultiPartyProtocols,
|
|
595
|
-
|
|
605
|
+
flowName,
|
|
596
606
|
} = params;
|
|
597
607
|
|
|
598
608
|
let identity: BearerIdentity | undefined;
|
|
@@ -683,20 +693,11 @@ export async function importDelegateAndSetupSync(params: {
|
|
|
683
693
|
// Doing a manual pull first would double the startup burst and can
|
|
684
694
|
// trigger rate limits on the remote DWN.
|
|
685
695
|
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
(identity as any)._delegateContextKeys = delegateContextKeys;
|
|
692
|
-
}
|
|
693
|
-
if (delegateMultiPartyProtocols && delegateMultiPartyProtocols.length > 0) {
|
|
694
|
-
(identity as any)._delegateMultiPartyProtocols = delegateMultiPartyProtocols;
|
|
695
|
-
}
|
|
696
|
-
if (sessionRevocations && sessionRevocations.length > 0) {
|
|
697
|
-
(identity as any)._sessionRevocations = sessionRevocations;
|
|
698
|
-
}
|
|
699
|
-
|
|
696
|
+
// Delegate protocol keys / multi-party state / revocation map are
|
|
697
|
+
// passed back to the caller explicitly via the return value so
|
|
698
|
+
// `finalizeDelegateSession` can persist them. (The previous flow
|
|
699
|
+
// smuggled them through `(identity as any)._foo`, which traded
|
|
700
|
+
// type safety for one fewer return value.)
|
|
700
701
|
return identity;
|
|
701
702
|
} catch (error: unknown) {
|
|
702
703
|
if (identity) {
|
|
@@ -720,6 +721,21 @@ export async function importDelegateAndSetupSync(params: {
|
|
|
720
721
|
|
|
721
722
|
// ─── finalizeDelegateSession ────────────────────────────────────
|
|
722
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Transient state produced by a successful delegated connect flow that
|
|
726
|
+
* `finalizeDelegateSession` persists into the SecretStore + storage
|
|
727
|
+
* adapter. Passed explicitly through the call chain rather than
|
|
728
|
+
* smuggled via private fields on `BearerIdentity`.
|
|
729
|
+
*
|
|
730
|
+
* @internal
|
|
731
|
+
*/
|
|
732
|
+
export type DelegateSessionState = {
|
|
733
|
+
delegateDecryptionKeys?: DelegateDecryptionKey[];
|
|
734
|
+
delegateContextKeys?: DelegateContextKey[];
|
|
735
|
+
delegateMultiPartyProtocols?: string[];
|
|
736
|
+
sessionRevocations?: { grantId: string; revocationGrantId: string }[];
|
|
737
|
+
};
|
|
738
|
+
|
|
723
739
|
/**
|
|
724
740
|
* Build an `AuthSession` for a delegated connect flow (DWeb Connect or
|
|
725
741
|
* relay WalletConnect). Starts sync and persists delegate/connected DID
|
|
@@ -735,8 +751,12 @@ export async function finalizeDelegateSession(params: {
|
|
|
735
751
|
connectedDid: string;
|
|
736
752
|
delegateDid: string;
|
|
737
753
|
sync: SyncOption | undefined;
|
|
754
|
+
delegateState?: DelegateSessionState;
|
|
738
755
|
}): Promise<AuthSession> {
|
|
739
|
-
const {
|
|
756
|
+
const {
|
|
757
|
+
userAgent, emitter, storage, identity, connectedDid, delegateDid, sync,
|
|
758
|
+
delegateState = {},
|
|
759
|
+
} = params;
|
|
740
760
|
|
|
741
761
|
await startSyncIfEnabled(userAgent, sync);
|
|
742
762
|
|
|
@@ -752,7 +772,7 @@ export async function finalizeDelegateSession(params: {
|
|
|
752
772
|
// Persist or clear delegate keys/revocations. Clearing stale values
|
|
753
773
|
// from prior sessions prevents a reconnect with fewer capabilities
|
|
754
774
|
// from retaining old decryption material.
|
|
755
|
-
await persistOrClearDelegateSecrets(userAgent, storage,
|
|
775
|
+
await persistOrClearDelegateSecrets(userAgent, storage, extraStorageKeys, delegateState);
|
|
756
776
|
|
|
757
777
|
// Wire post-connect context key persistence: when the owner creates a
|
|
758
778
|
// new multi-party context, the agent injects the key into the delegate
|
|
@@ -837,8 +857,8 @@ export async function finalizeSession(params: {
|
|
|
837
857
|
await Promise.all(storageWrites);
|
|
838
858
|
|
|
839
859
|
// When identityName is undefined, no user identity exists (agent-only session).
|
|
840
|
-
// Build an
|
|
841
|
-
const identityInfo:
|
|
860
|
+
// Build an AgentSessionIdentity with the agent DID as a fallback.
|
|
861
|
+
const identityInfo: AgentSessionIdentity = {
|
|
842
862
|
didUri : connectedDid,
|
|
843
863
|
name : identityName ?? 'Agent',
|
|
844
864
|
connectedDid : identityConnectedDid,
|
|
@@ -869,11 +889,15 @@ export async function finalizeSession(params: {
|
|
|
869
889
|
async function persistOrClearDelegateSecrets(
|
|
870
890
|
userAgent: EnboxUserAgent,
|
|
871
891
|
storage: StorageAdapter,
|
|
872
|
-
identity: BearerIdentity,
|
|
873
892
|
extraStorageKeys: Record<string, string>,
|
|
893
|
+
delegateState: DelegateSessionState,
|
|
874
894
|
): Promise<void> {
|
|
875
|
-
const
|
|
876
|
-
|
|
895
|
+
const {
|
|
896
|
+
delegateDecryptionKeys,
|
|
897
|
+
delegateContextKeys,
|
|
898
|
+
delegateMultiPartyProtocols,
|
|
899
|
+
sessionRevocations,
|
|
900
|
+
} = delegateState;
|
|
877
901
|
|
|
878
902
|
// Persist or clear keys in the SecretStore + legacy StorageAdapter.
|
|
879
903
|
const secretWrites: Promise<void>[] = [];
|
|
@@ -895,14 +919,12 @@ async function persistOrClearDelegateSecrets(
|
|
|
895
919
|
try { await storage.remove(STORAGE_KEYS.DELEGATE_CONTEXT_KEYS); } catch { /* best-effort */ }
|
|
896
920
|
}
|
|
897
921
|
|
|
898
|
-
const delegateMultiPartyProtocols = (identity as any)._delegateMultiPartyProtocols as string[] | undefined;
|
|
899
922
|
if (delegateMultiPartyProtocols?.length) {
|
|
900
923
|
extraStorageKeys[STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS] = JSON.stringify(delegateMultiPartyProtocols);
|
|
901
924
|
} else {
|
|
902
925
|
try { await storage.remove(STORAGE_KEYS.DELEGATE_MULTI_PARTY_PROTOCOLS); } catch { /* best-effort */ }
|
|
903
926
|
}
|
|
904
927
|
|
|
905
|
-
const sessionRevocations = (identity as any)._sessionRevocations as { grantId: string; revocationGrantId: string }[] | undefined;
|
|
906
928
|
if (sessionRevocations?.length) {
|
|
907
929
|
extraStorageKeys[STORAGE_KEYS.SESSION_REVOCATIONS] = JSON.stringify(sessionRevocations);
|
|
908
930
|
} else {
|