@enbox/auth 0.5.0 → 0.6.1
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 +240 -171
- package/dist/esm/auth-manager.js.map +1 -1
- package/dist/esm/connect/import.js +131 -0
- package/dist/esm/connect/import.js.map +1 -0
- package/dist/esm/connect/lifecycle.js +378 -0
- package/dist/esm/connect/lifecycle.js.map +1 -0
- package/dist/esm/connect/local.js +105 -0
- package/dist/esm/connect/local.js.map +1 -0
- package/dist/esm/connect/restore.js +117 -0
- package/dist/esm/connect/restore.js.map +1 -0
- package/dist/esm/connect/wallet.js +80 -0
- package/dist/esm/connect/wallet.js.map +1 -0
- package/dist/esm/{flows/dwn-discovery.js → discovery.js} +2 -2
- package/dist/esm/discovery.js.map +1 -0
- package/dist/esm/index.js +13 -19
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/permissions.js +41 -0
- package/dist/esm/permissions.js.map +1 -0
- package/dist/esm/{flows/dwn-registration.js → registration.js} +2 -2
- package/dist/esm/registration.js.map +1 -0
- package/dist/esm/types.js +4 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/wallet-connect-client.js +188 -0
- package/dist/esm/wallet-connect-client.js.map +1 -0
- package/dist/types/auth-manager.d.ts +89 -11
- package/dist/types/auth-manager.d.ts.map +1 -1
- package/dist/types/connect/import.d.ts +25 -0
- package/dist/types/connect/import.d.ts.map +1 -0
- package/dist/types/connect/lifecycle.d.ts +199 -0
- package/dist/types/connect/lifecycle.d.ts.map +1 -0
- package/dist/types/connect/local.d.ts +23 -0
- package/dist/types/connect/local.d.ts.map +1 -0
- package/dist/types/connect/restore.d.ts +18 -0
- package/dist/types/connect/restore.d.ts.map +1 -0
- package/dist/types/connect/wallet.d.ts +21 -0
- package/dist/types/connect/wallet.d.ts.map +1 -0
- package/dist/types/{flows/dwn-discovery.d.ts → discovery.d.ts} +3 -3
- package/dist/types/discovery.d.ts.map +1 -0
- package/dist/types/index.d.ts +14 -19
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/permissions.d.ts +18 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/{flows/dwn-registration.d.ts → registration.d.ts} +2 -2
- package/dist/types/registration.d.ts.map +1 -0
- package/dist/types/types.d.ts +154 -4
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/wallet-connect-client.d.ts +86 -0
- package/dist/types/wallet-connect-client.d.ts.map +1 -0
- package/package.json +9 -5
- package/src/auth-manager.ts +258 -191
- package/src/connect/import.ts +148 -0
- package/src/connect/lifecycle.ts +487 -0
- package/src/connect/local.ts +116 -0
- package/src/connect/restore.ts +133 -0
- package/src/connect/wallet.ts +89 -0
- package/src/{flows/dwn-discovery.ts → discovery.ts} +4 -3
- package/src/index.ts +20 -19
- package/src/permissions.ts +48 -0
- package/src/{flows/dwn-registration.ts → registration.ts} +2 -2
- package/src/types.ts +171 -4
- package/src/wallet-connect-client.ts +275 -0
- package/dist/esm/flows/dwn-discovery.js.map +0 -1
- package/dist/esm/flows/dwn-registration.js.map +0 -1
- package/dist/esm/flows/import-identity.js +0 -177
- package/dist/esm/flows/import-identity.js.map +0 -1
- package/dist/esm/flows/local-connect.js +0 -158
- package/dist/esm/flows/local-connect.js.map +0 -1
- package/dist/esm/flows/session-restore.js +0 -125
- package/dist/esm/flows/session-restore.js.map +0 -1
- package/dist/esm/flows/wallet-connect.js +0 -200
- package/dist/esm/flows/wallet-connect.js.map +0 -1
- package/dist/esm/vault/vault-manager.js +0 -95
- package/dist/esm/vault/vault-manager.js.map +0 -1
- package/dist/types/flows/dwn-discovery.d.ts.map +0 -1
- package/dist/types/flows/dwn-registration.d.ts.map +0 -1
- package/dist/types/flows/import-identity.d.ts +0 -35
- package/dist/types/flows/import-identity.d.ts.map +0 -1
- package/dist/types/flows/local-connect.d.ts +0 -31
- package/dist/types/flows/local-connect.d.ts.map +0 -1
- package/dist/types/flows/session-restore.d.ts +0 -29
- package/dist/types/flows/session-restore.d.ts.map +0 -1
- package/dist/types/flows/wallet-connect.d.ts +0 -44
- package/dist/types/flows/wallet-connect.d.ts.map +0 -1
- package/dist/types/vault/vault-manager.d.ts +0 -57
- package/dist/types/vault/vault-manager.d.ts.map +0 -1
- package/src/flows/import-identity.ts +0 -219
- package/src/flows/local-connect.ts +0 -192
- package/src/flows/session-restore.ts +0 -155
- package/src/flows/wallet-connect.ts +0 -226
- package/src/vault/vault-manager.ts +0 -89
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity import flows.
|
|
3
|
+
*
|
|
4
|
+
* - Import from BIP-39 recovery phrase (re-derive vault + identity).
|
|
5
|
+
* - Import from PortableIdentity JSON.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AuthSession } from '../identity-session.js';
|
|
10
|
+
import type { FlowContext } from './lifecycle.js';
|
|
11
|
+
import type { ImportFromPhraseOptions, ImportFromPortableOptions } from '../types.js';
|
|
12
|
+
|
|
13
|
+
import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
|
|
14
|
+
import { registerWithDwnEndpoints } from '../registration.js';
|
|
15
|
+
import { createDefaultIdentity, ensureVaultReady, finalizeSession, resolveIdentityDids, startSyncIfEnabled } from './lifecycle.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Import (or recover) an identity from a BIP-39 recovery phrase.
|
|
19
|
+
*
|
|
20
|
+
* This re-initializes the vault with the given phrase and password,
|
|
21
|
+
* recovering the agent DID and all derived keys.
|
|
22
|
+
*/
|
|
23
|
+
export async function importFromPhrase(
|
|
24
|
+
ctx: FlowContext,
|
|
25
|
+
options: ImportFromPhraseOptions,
|
|
26
|
+
): Promise<AuthSession> {
|
|
27
|
+
const { userAgent, emitter, storage } = ctx;
|
|
28
|
+
const { recoveryPhrase, password } = options;
|
|
29
|
+
const sync = options.sync ?? ctx.defaultSync;
|
|
30
|
+
const dwnEndpoints = options.dwnEndpoints ?? ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
|
|
31
|
+
|
|
32
|
+
// Initialize the vault with the recovery phrase and start the agent.
|
|
33
|
+
const isFirstLaunch = await userAgent.firstLaunch();
|
|
34
|
+
await ensureVaultReady({
|
|
35
|
+
userAgent,
|
|
36
|
+
emitter,
|
|
37
|
+
password,
|
|
38
|
+
isFirstLaunch,
|
|
39
|
+
recoveryPhrase,
|
|
40
|
+
dwnEndpoints,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// The recovery phrase re-derives the same agent DID,
|
|
44
|
+
// but the user identity might not exist yet — create one if needed.
|
|
45
|
+
const identities = await userAgent.identity.list();
|
|
46
|
+
let identity = identities[0];
|
|
47
|
+
let isNewIdentity = false;
|
|
48
|
+
|
|
49
|
+
if (!identity) {
|
|
50
|
+
isNewIdentity = true;
|
|
51
|
+
identity = await createDefaultIdentity(userAgent, dwnEndpoints);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { connectedDid, delegateDid } = resolveIdentityDids(identity);
|
|
55
|
+
|
|
56
|
+
// Register with DWN endpoints (if registration options are provided).
|
|
57
|
+
if (ctx.registration) {
|
|
58
|
+
await registerWithDwnEndpoints(
|
|
59
|
+
{
|
|
60
|
+
userAgent : userAgent,
|
|
61
|
+
dwnEndpoints,
|
|
62
|
+
agentDid : userAgent.agentDid.uri,
|
|
63
|
+
connectedDid,
|
|
64
|
+
storage : storage,
|
|
65
|
+
},
|
|
66
|
+
ctx.registration,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Register sync for new identities.
|
|
71
|
+
if (isNewIdentity && sync !== 'off') {
|
|
72
|
+
await userAgent.sync.registerIdentity({
|
|
73
|
+
did : connectedDid,
|
|
74
|
+
options : { delegateDid, protocols: [] },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Start sync.
|
|
79
|
+
startSyncIfEnabled(userAgent, sync);
|
|
80
|
+
|
|
81
|
+
// Persist session info, build AuthSession, and emit lifecycle events.
|
|
82
|
+
return finalizeSession({
|
|
83
|
+
userAgent,
|
|
84
|
+
emitter,
|
|
85
|
+
storage,
|
|
86
|
+
connectedDid,
|
|
87
|
+
delegateDid,
|
|
88
|
+
identityName : identity.metadata.name,
|
|
89
|
+
identityConnectedDid : identity.metadata.connectedDid,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Import an identity from a PortableIdentity JSON object.
|
|
95
|
+
*
|
|
96
|
+
* The portable identity contains the DID's private keys and metadata,
|
|
97
|
+
* allowing it to be used on this device.
|
|
98
|
+
*/
|
|
99
|
+
export async function importFromPortable(
|
|
100
|
+
ctx: FlowContext,
|
|
101
|
+
options: ImportFromPortableOptions,
|
|
102
|
+
): Promise<AuthSession> {
|
|
103
|
+
const { userAgent, emitter, storage } = ctx;
|
|
104
|
+
const sync = options.sync ?? ctx.defaultSync;
|
|
105
|
+
|
|
106
|
+
const identity = await userAgent.identity.import({
|
|
107
|
+
portableIdentity: options.portableIdentity,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const { connectedDid, delegateDid } = resolveIdentityDids(identity);
|
|
111
|
+
|
|
112
|
+
// Register with DWN endpoints (if registration options are provided).
|
|
113
|
+
// For portable imports, extract endpoints from the DID document's DWN service.
|
|
114
|
+
if (ctx.registration) {
|
|
115
|
+
const dwnEndpoints = ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
|
|
116
|
+
await registerWithDwnEndpoints(
|
|
117
|
+
{
|
|
118
|
+
userAgent : userAgent,
|
|
119
|
+
dwnEndpoints,
|
|
120
|
+
agentDid : userAgent.agentDid.uri,
|
|
121
|
+
connectedDid,
|
|
122
|
+
storage : storage,
|
|
123
|
+
},
|
|
124
|
+
ctx.registration,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Register and start sync.
|
|
129
|
+
if (sync !== 'off') {
|
|
130
|
+
await userAgent.sync.registerIdentity({
|
|
131
|
+
did : connectedDid,
|
|
132
|
+
options : { delegateDid, protocols: [] },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
startSyncIfEnabled(userAgent, sync);
|
|
137
|
+
|
|
138
|
+
// Persist session info, build AuthSession, and emit lifecycle events.
|
|
139
|
+
return finalizeSession({
|
|
140
|
+
userAgent,
|
|
141
|
+
emitter,
|
|
142
|
+
storage,
|
|
143
|
+
connectedDid,
|
|
144
|
+
delegateDid,
|
|
145
|
+
identityName : identity.metadata.name,
|
|
146
|
+
identityConnectedDid : identity.metadata.connectedDid,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for connect flows.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates duplicated logic across `local-connect`, `session-restore`,
|
|
5
|
+
* `wallet-connect`, and `import-identity` flows:
|
|
6
|
+
*
|
|
7
|
+
* - Password resolution chain
|
|
8
|
+
* - Vault init/start lifecycle
|
|
9
|
+
* - Sync mode/interval calculation and startup
|
|
10
|
+
* - `connectedDid` / `delegateDid` derivation from identity metadata
|
|
11
|
+
* - Session finalization (storage persistence + AuthSession construction + events)
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PortableDid } from '@enbox/dids';
|
|
18
|
+
import type { BearerIdentity, DwnDataEncodedRecordsWriteMessage, DwnMessagesPermissionScope, DwnRecordsPermissionScope, EnboxUserAgent } from '@enbox/agent';
|
|
19
|
+
|
|
20
|
+
import type { AuthEventEmitter } from '../events.js';
|
|
21
|
+
import type { PasswordProvider } from '../password-provider.js';
|
|
22
|
+
import type { IdentityInfo, RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
|
|
23
|
+
|
|
24
|
+
import { Convert } from '@enbox/common';
|
|
25
|
+
import { DwnInterface, DwnPermissionGrant } from '@enbox/agent';
|
|
26
|
+
|
|
27
|
+
import { AuthSession } from '../identity-session.js';
|
|
28
|
+
import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '../types.js';
|
|
29
|
+
|
|
30
|
+
// ─── FlowContext ─────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unified context passed from `AuthManager` to every connect flow.
|
|
34
|
+
*
|
|
35
|
+
* Replaces the per-flow `LocalConnectContext`, `SessionRestoreContext`,
|
|
36
|
+
* `WalletConnectContext`, and `ImportContext` interfaces. All fields are
|
|
37
|
+
* optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
|
|
38
|
+
* flows only consume what they need.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export interface FlowContext {
|
|
43
|
+
userAgent: EnboxUserAgent;
|
|
44
|
+
emitter: AuthEventEmitter;
|
|
45
|
+
storage: StorageAdapter;
|
|
46
|
+
defaultPassword?: string;
|
|
47
|
+
passwordProvider?: PasswordProvider;
|
|
48
|
+
defaultSync?: SyncOption;
|
|
49
|
+
defaultDwnEndpoints?: string[];
|
|
50
|
+
registration?: RegistrationOptions;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── resolvePassword ─────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a password through the standard chain:
|
|
57
|
+
* explicit option → manager default → provider → insecure fallback.
|
|
58
|
+
*
|
|
59
|
+
* Emits a console warning when the insecure default is used.
|
|
60
|
+
*
|
|
61
|
+
* @param ctx - The flow context (provides `defaultPassword` and `passwordProvider`).
|
|
62
|
+
* @param explicit - An explicit password from the caller (highest priority).
|
|
63
|
+
* @param isFirstLaunch - Whether the vault has never been initialized.
|
|
64
|
+
* @returns The resolved password string.
|
|
65
|
+
*
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
export async function resolvePassword(
|
|
69
|
+
ctx: Pick<FlowContext, 'defaultPassword' | 'passwordProvider'>,
|
|
70
|
+
explicit: string | undefined,
|
|
71
|
+
isFirstLaunch: boolean,
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
let password = explicit ?? ctx.defaultPassword;
|
|
74
|
+
|
|
75
|
+
if (!password && ctx.passwordProvider) {
|
|
76
|
+
try {
|
|
77
|
+
password = await ctx.passwordProvider.getPassword({
|
|
78
|
+
reason: isFirstLaunch ? 'create' : 'unlock',
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
// Provider failed — fall through to insecure default.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
password ??= INSECURE_DEFAULT_PASSWORD;
|
|
86
|
+
|
|
87
|
+
if (password === INSECURE_DEFAULT_PASSWORD) {
|
|
88
|
+
console.warn(
|
|
89
|
+
'[@enbox/auth] SECURITY WARNING: No password set. Using insecure default. ' +
|
|
90
|
+
'Set a password via AuthManager.create({ password }) or connect({ password }) ' +
|
|
91
|
+
'to protect your identity vault.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return password;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── ensureVaultReady ────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Initialize (on first launch) and start the agent, then emit `vault-unlocked`.
|
|
102
|
+
*
|
|
103
|
+
* This consolidates the 5 copies of:
|
|
104
|
+
* ```ts
|
|
105
|
+
* if (isFirstLaunch) { await userAgent.initialize({ password, ... }); }
|
|
106
|
+
* await userAgent.start({ password });
|
|
107
|
+
* emitter.emit('vault-unlocked', {});
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @returns The recovery phrase if the vault was just initialized, otherwise `undefined`.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export async function ensureVaultReady(params: {
|
|
115
|
+
userAgent: EnboxUserAgent;
|
|
116
|
+
emitter: AuthEventEmitter;
|
|
117
|
+
password: string;
|
|
118
|
+
isFirstLaunch: boolean;
|
|
119
|
+
recoveryPhrase?: string;
|
|
120
|
+
dwnEndpoints?: string[];
|
|
121
|
+
}): Promise<string | undefined> {
|
|
122
|
+
const { userAgent, emitter, password, isFirstLaunch } = params;
|
|
123
|
+
let recoveryPhrase: string | undefined;
|
|
124
|
+
|
|
125
|
+
if (isFirstLaunch) {
|
|
126
|
+
recoveryPhrase = await userAgent.initialize({
|
|
127
|
+
password,
|
|
128
|
+
recoveryPhrase : params.recoveryPhrase,
|
|
129
|
+
dwnEndpoints : params.dwnEndpoints,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await userAgent.start({ password });
|
|
134
|
+
emitter.emit('vault-unlocked', {});
|
|
135
|
+
|
|
136
|
+
return recoveryPhrase;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── startSyncIfEnabled ─────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Start DWN synchronisation if `sync` is not `'off'`.
|
|
143
|
+
*
|
|
144
|
+
* Consolidates 6 copies of:
|
|
145
|
+
* ```ts
|
|
146
|
+
* const syncMode = sync === undefined ? 'live' : 'poll';
|
|
147
|
+
* const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
148
|
+
* userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
149
|
+
* .catch((err) => console.error('[@enbox/auth] Sync failed:', err));
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
export function startSyncIfEnabled(
|
|
155
|
+
userAgent: EnboxUserAgent,
|
|
156
|
+
sync: SyncOption | undefined,
|
|
157
|
+
): void {
|
|
158
|
+
if (sync === 'off') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const syncMode = sync === undefined ? 'live' : 'poll';
|
|
163
|
+
const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
164
|
+
|
|
165
|
+
userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
166
|
+
.catch((err: unknown) => {
|
|
167
|
+
console.error('[@enbox/auth] Sync failed:', err);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── createDefaultIdentity ──────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create a new `did:dht` identity with Ed25519 signing and X25519
|
|
175
|
+
* encryption keys, and a DWN service endpoint.
|
|
176
|
+
*
|
|
177
|
+
* This consolidates the identical identity creation block that was
|
|
178
|
+
* duplicated in `localConnect` and `importFromPhrase`.
|
|
179
|
+
*
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
export async function createDefaultIdentity(
|
|
183
|
+
userAgent: EnboxUserAgent,
|
|
184
|
+
dwnEndpoints: string[] = DEFAULT_DWN_ENDPOINTS,
|
|
185
|
+
name = 'Default',
|
|
186
|
+
): Promise<BearerIdentity> {
|
|
187
|
+
return userAgent.identity.create({
|
|
188
|
+
didMethod : 'dht',
|
|
189
|
+
metadata : { name },
|
|
190
|
+
didOptions : {
|
|
191
|
+
services: [
|
|
192
|
+
{
|
|
193
|
+
id : 'dwn',
|
|
194
|
+
type : 'DecentralizedWebNode',
|
|
195
|
+
serviceEndpoint : dwnEndpoints,
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
verificationMethods: [
|
|
199
|
+
{
|
|
200
|
+
algorithm : 'Ed25519',
|
|
201
|
+
id : 'sig',
|
|
202
|
+
purposes : ['assertionMethod', 'authentication'],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
algorithm : 'X25519',
|
|
206
|
+
id : 'enc',
|
|
207
|
+
purposes : ['keyAgreement'],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── resolveIdentityDids ────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Derive `connectedDid` and `delegateDid` from identity metadata.
|
|
218
|
+
*
|
|
219
|
+
* For a **local** identity: `connectedDid` is the identity's own DID URI
|
|
220
|
+
* and `delegateDid` is `undefined`.
|
|
221
|
+
*
|
|
222
|
+
* For a **wallet-connected** identity: `connectedDid` is the external wallet
|
|
223
|
+
* DID, and `delegateDid` is the local identity's DID URI.
|
|
224
|
+
*
|
|
225
|
+
* @param identity - The bearer identity to extract DIDs from.
|
|
226
|
+
* @param storedDelegateDid - Optional fallback delegate DID from storage,
|
|
227
|
+
* used by session-restore when the identity metadata doesn't include a
|
|
228
|
+
* `connectedDid` but a delegate DID was persisted in a prior session.
|
|
229
|
+
*
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
export function resolveIdentityDids(
|
|
233
|
+
identity: BearerIdentity,
|
|
234
|
+
storedDelegateDid?: string,
|
|
235
|
+
): {
|
|
236
|
+
connectedDid: string;
|
|
237
|
+
delegateDid: string | undefined;
|
|
238
|
+
} {
|
|
239
|
+
const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
|
|
240
|
+
const delegateDid = identity.metadata.connectedDid
|
|
241
|
+
? identity.did.uri
|
|
242
|
+
: (storedDelegateDid ?? undefined);
|
|
243
|
+
return { connectedDid, delegateDid };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── processConnectedGrants ─────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Process connected grants by storing them in the local DWN as the owner.
|
|
250
|
+
*
|
|
251
|
+
* This is the agent-level equivalent of `Enbox.processConnectedGrants()`.
|
|
252
|
+
* It stores each grant, signed as owner, and returns the deduplicated
|
|
253
|
+
* list of protocol URIs represented by the grants.
|
|
254
|
+
*
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
export async function processConnectedGrants(params: {
|
|
258
|
+
agent: EnboxUserAgent;
|
|
259
|
+
delegateDid: string;
|
|
260
|
+
grants: DwnDataEncodedRecordsWriteMessage[];
|
|
261
|
+
}): Promise<string[]> {
|
|
262
|
+
const { agent, delegateDid, grants } = params;
|
|
263
|
+
const connectedProtocols = new Set<string>();
|
|
264
|
+
|
|
265
|
+
for (const grantMessage of grants) {
|
|
266
|
+
const grant = DwnPermissionGrant.parse(grantMessage);
|
|
267
|
+
|
|
268
|
+
// Store the grant as the owner of the DWN so the delegateDid
|
|
269
|
+
// can use it when impersonating the connectedDid.
|
|
270
|
+
const { encodedData, ...rawMessage } = grantMessage;
|
|
271
|
+
const dataStream = new Blob([Convert.base64Url(encodedData).toUint8Array() as BlobPart]);
|
|
272
|
+
|
|
273
|
+
const { reply } = await agent.processDwnRequest({
|
|
274
|
+
store : true,
|
|
275
|
+
author : delegateDid,
|
|
276
|
+
target : delegateDid,
|
|
277
|
+
messageType : DwnInterface.RecordsWrite,
|
|
278
|
+
signAsOwner : true,
|
|
279
|
+
rawMessage,
|
|
280
|
+
dataStream,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (reply.status.code !== 202) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`[@enbox/auth] Failed to process connected grant: ${reply.status.detail}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const protocol = (grant.scope as DwnMessagesPermissionScope | DwnRecordsPermissionScope).protocol;
|
|
290
|
+
if (protocol) {
|
|
291
|
+
connectedProtocols.add(protocol);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return [...connectedProtocols];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── importDelegateAndSetupSync ─────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Import a delegated DID, process its grants, register sync, and pull.
|
|
302
|
+
*
|
|
303
|
+
* This is the shared post-connect lifecycle used by both the DWeb Connect
|
|
304
|
+
* and relay WalletConnect flows. On failure, the imported identity is
|
|
305
|
+
* cleaned up before re-throwing.
|
|
306
|
+
*
|
|
307
|
+
* @internal
|
|
308
|
+
*/
|
|
309
|
+
export async function importDelegateAndSetupSync(params: {
|
|
310
|
+
userAgent: EnboxUserAgent;
|
|
311
|
+
delegatePortableDid: PortableDid;
|
|
312
|
+
connectedDid: string;
|
|
313
|
+
delegateGrants: DwnDataEncodedRecordsWriteMessage[];
|
|
314
|
+
flowName: string;
|
|
315
|
+
}): Promise<BearerIdentity> {
|
|
316
|
+
const { userAgent, delegatePortableDid, connectedDid, delegateGrants, flowName } = params;
|
|
317
|
+
|
|
318
|
+
let identity: BearerIdentity | undefined;
|
|
319
|
+
try {
|
|
320
|
+
identity = await userAgent.identity.import({
|
|
321
|
+
portableIdentity: {
|
|
322
|
+
portableDid : delegatePortableDid,
|
|
323
|
+
metadata : {
|
|
324
|
+
connectedDid,
|
|
325
|
+
name : 'Default',
|
|
326
|
+
uri : delegatePortableDid.uri,
|
|
327
|
+
tenant : userAgent.agentDid.uri,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const connectedProtocols = await processConnectedGrants({
|
|
333
|
+
agent : userAgent,
|
|
334
|
+
delegateDid : delegatePortableDid.uri,
|
|
335
|
+
grants : delegateGrants,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await userAgent.sync.registerIdentity({
|
|
339
|
+
did : connectedDid,
|
|
340
|
+
options : {
|
|
341
|
+
delegateDid : delegatePortableDid.uri,
|
|
342
|
+
protocols : connectedProtocols,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await userAgent.sync.sync('pull');
|
|
347
|
+
|
|
348
|
+
return identity;
|
|
349
|
+
} catch (error: unknown) {
|
|
350
|
+
if (identity) {
|
|
351
|
+
try {
|
|
352
|
+
await userAgent.did.delete({
|
|
353
|
+
didUri : identity.did.uri,
|
|
354
|
+
tenant : identity.metadata.tenant,
|
|
355
|
+
deleteKey : true,
|
|
356
|
+
});
|
|
357
|
+
} catch { /* best effort */ }
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await userAgent.identity.delete({ didUri: identity.did.uri });
|
|
361
|
+
} catch { /* best effort */ }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
throw new Error(`[@enbox/auth] ${flowName} failed: ${message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── finalizeDelegateSession ────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Build an `AuthSession` for a delegated connect flow (DWeb Connect or
|
|
373
|
+
* relay WalletConnect). Starts sync and persists delegate/connected DID
|
|
374
|
+
* markers.
|
|
375
|
+
*
|
|
376
|
+
* @internal
|
|
377
|
+
*/
|
|
378
|
+
export async function finalizeDelegateSession(params: {
|
|
379
|
+
userAgent: EnboxUserAgent;
|
|
380
|
+
emitter: AuthEventEmitter;
|
|
381
|
+
storage: StorageAdapter;
|
|
382
|
+
identity: BearerIdentity;
|
|
383
|
+
connectedDid: string;
|
|
384
|
+
delegateDid: string;
|
|
385
|
+
sync: SyncOption | undefined;
|
|
386
|
+
}): Promise<AuthSession> {
|
|
387
|
+
const { userAgent, emitter, storage, identity, connectedDid, delegateDid, sync } = params;
|
|
388
|
+
|
|
389
|
+
startSyncIfEnabled(userAgent, sync);
|
|
390
|
+
|
|
391
|
+
return finalizeSession({
|
|
392
|
+
userAgent,
|
|
393
|
+
emitter,
|
|
394
|
+
storage,
|
|
395
|
+
connectedDid,
|
|
396
|
+
delegateDid,
|
|
397
|
+
identityName : identity.metadata.name,
|
|
398
|
+
identityConnectedDid : identity.metadata.connectedDid,
|
|
399
|
+
extraStorageKeys : {
|
|
400
|
+
[STORAGE_KEYS.DELEGATE_DID] : delegateDid,
|
|
401
|
+
[STORAGE_KEYS.CONNECTED_DID] : connectedDid,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── finalizeSession ────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Persist session markers, build an `AuthSession`, and emit lifecycle events.
|
|
410
|
+
*
|
|
411
|
+
* Consolidates 5 copies of:
|
|
412
|
+
* ```ts
|
|
413
|
+
* await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
414
|
+
* await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
415
|
+
* const session = new AuthSession({ ... });
|
|
416
|
+
* emitter.emit('identity-added', { identity: identityInfo });
|
|
417
|
+
* emitter.emit('session-start', { session: { ... } });
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @param params.emitIdentityAdded - Whether to emit `identity-added`. Defaults to `true`.
|
|
421
|
+
* Set to `false` for session-restore (identity was already added in the original flow).
|
|
422
|
+
* @param params.extraStorageKeys - Additional key-value pairs to persist (e.g. delegate/connected DIDs
|
|
423
|
+
* for wallet-connect flows).
|
|
424
|
+
*
|
|
425
|
+
* @internal
|
|
426
|
+
*/
|
|
427
|
+
export async function finalizeSession(params: {
|
|
428
|
+
userAgent: EnboxUserAgent;
|
|
429
|
+
emitter: AuthEventEmitter;
|
|
430
|
+
storage: StorageAdapter;
|
|
431
|
+
connectedDid: string;
|
|
432
|
+
delegateDid?: string;
|
|
433
|
+
recoveryPhrase?: string;
|
|
434
|
+
identityName?: string;
|
|
435
|
+
identityConnectedDid?: string;
|
|
436
|
+
emitIdentityAdded?: boolean;
|
|
437
|
+
extraStorageKeys?: Record<string, string>;
|
|
438
|
+
}): Promise<AuthSession> {
|
|
439
|
+
const {
|
|
440
|
+
userAgent,
|
|
441
|
+
emitter,
|
|
442
|
+
storage,
|
|
443
|
+
connectedDid,
|
|
444
|
+
delegateDid,
|
|
445
|
+
recoveryPhrase,
|
|
446
|
+
identityName,
|
|
447
|
+
identityConnectedDid,
|
|
448
|
+
emitIdentityAdded = true,
|
|
449
|
+
extraStorageKeys,
|
|
450
|
+
} = params;
|
|
451
|
+
|
|
452
|
+
// Persist session markers.
|
|
453
|
+
await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
454
|
+
await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
455
|
+
|
|
456
|
+
if (extraStorageKeys) {
|
|
457
|
+
for (const [key, value] of Object.entries(extraStorageKeys)) {
|
|
458
|
+
await storage.set(key, value);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// When identityName is undefined, no user identity exists (agent-only session).
|
|
463
|
+
// Build an IdentityInfo with the agent DID as a fallback.
|
|
464
|
+
const identityInfo: IdentityInfo = {
|
|
465
|
+
didUri : connectedDid,
|
|
466
|
+
name : identityName ?? 'Agent',
|
|
467
|
+
connectedDid : identityConnectedDid,
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const session = new AuthSession({
|
|
471
|
+
agent : userAgent,
|
|
472
|
+
did : connectedDid,
|
|
473
|
+
delegateDid,
|
|
474
|
+
recoveryPhrase,
|
|
475
|
+
identity : identityInfo,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (emitIdentityAdded && identityName !== undefined) {
|
|
479
|
+
emitter.emit('identity-added', { identity: identityInfo });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
emitter.emit('session-start', {
|
|
483
|
+
session: { did: connectedDid, delegateDid, identity: identityInfo },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return session;
|
|
487
|
+
}
|