@enbox/auth 0.5.0 → 0.6.0
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 +94 -167
- 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 +235 -0
- package/dist/esm/connect/lifecycle.js.map +1 -0
- package/dist/esm/connect/local.js +91 -0
- package/dist/esm/connect/local.js.map +1 -0
- package/dist/esm/{flows/session-restore.js → connect/restore.js} +34 -56
- package/dist/esm/connect/restore.js.map +1 -0
- package/dist/esm/{flows/wallet-connect.js → connect/wallet.js} +28 -35
- 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 +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/{flows/dwn-registration.js → registration.js} +2 -2
- package/dist/esm/registration.js.map +1 -0
- package/dist/esm/types.js +2 -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 +20 -6
- 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 +152 -0
- package/dist/types/connect/lifecycle.d.ts.map +1 -0
- package/dist/types/connect/local.d.ts +18 -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/{flows/wallet-connect.d.ts → connect/wallet.d.ts} +5 -14
- 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 +5 -4
- package/dist/types/index.d.ts.map +1 -1
- 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 +6 -3
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/wallet-connect-client.d.ts +89 -0
- package/dist/types/wallet-connect-client.d.ts.map +1 -0
- package/package.json +9 -5
- package/src/auth-manager.ts +94 -188
- package/src/connect/import.ts +148 -0
- package/src/connect/lifecycle.ts +321 -0
- package/src/connect/local.ts +101 -0
- package/src/connect/restore.ts +117 -0
- package/src/{flows/wallet-connect.ts → connect/wallet.ts} +37 -54
- package/src/{flows/dwn-discovery.ts → discovery.ts} +4 -3
- package/src/index.ts +5 -4
- package/src/{flows/dwn-registration.ts → registration.ts} +2 -2
- package/src/types.ts +7 -3
- package/src/wallet-connect-client.ts +278 -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.map +0 -1
- 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.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/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,321 @@
|
|
|
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 { BearerIdentity, EnboxUserAgent } from '@enbox/agent';
|
|
18
|
+
|
|
19
|
+
import type { AuthEventEmitter } from '../events.js';
|
|
20
|
+
import type { PasswordProvider } from '../password-provider.js';
|
|
21
|
+
import type { IdentityInfo, RegistrationOptions, StorageAdapter, SyncOption } from '../types.js';
|
|
22
|
+
|
|
23
|
+
import { AuthSession } from '../identity-session.js';
|
|
24
|
+
import { DEFAULT_DWN_ENDPOINTS, INSECURE_DEFAULT_PASSWORD, STORAGE_KEYS } from '../types.js';
|
|
25
|
+
|
|
26
|
+
// ─── FlowContext ─────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Unified context passed from `AuthManager` to every connect flow.
|
|
30
|
+
*
|
|
31
|
+
* Replaces the per-flow `LocalConnectContext`, `SessionRestoreContext`,
|
|
32
|
+
* `WalletConnectContext`, and `ImportContext` interfaces. All fields are
|
|
33
|
+
* optional beyond the core triple (`userAgent`, `emitter`, `storage`) so
|
|
34
|
+
* flows only consume what they need.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export interface FlowContext {
|
|
39
|
+
userAgent: EnboxUserAgent;
|
|
40
|
+
emitter: AuthEventEmitter;
|
|
41
|
+
storage: StorageAdapter;
|
|
42
|
+
defaultPassword?: string;
|
|
43
|
+
passwordProvider?: PasswordProvider;
|
|
44
|
+
defaultSync?: SyncOption;
|
|
45
|
+
defaultDwnEndpoints?: string[];
|
|
46
|
+
registration?: RegistrationOptions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── resolvePassword ─────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a password through the standard chain:
|
|
53
|
+
* explicit option → manager default → provider → insecure fallback.
|
|
54
|
+
*
|
|
55
|
+
* Emits a console warning when the insecure default is used.
|
|
56
|
+
*
|
|
57
|
+
* @param ctx - The flow context (provides `defaultPassword` and `passwordProvider`).
|
|
58
|
+
* @param explicit - An explicit password from the caller (highest priority).
|
|
59
|
+
* @param isFirstLaunch - Whether the vault has never been initialized.
|
|
60
|
+
* @returns The resolved password string.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export async function resolvePassword(
|
|
65
|
+
ctx: Pick<FlowContext, 'defaultPassword' | 'passwordProvider'>,
|
|
66
|
+
explicit: string | undefined,
|
|
67
|
+
isFirstLaunch: boolean,
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
let password = explicit ?? ctx.defaultPassword;
|
|
70
|
+
|
|
71
|
+
if (!password && ctx.passwordProvider) {
|
|
72
|
+
try {
|
|
73
|
+
password = await ctx.passwordProvider.getPassword({
|
|
74
|
+
reason: isFirstLaunch ? 'create' : 'unlock',
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
// Provider failed — fall through to insecure default.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
password ??= INSECURE_DEFAULT_PASSWORD;
|
|
82
|
+
|
|
83
|
+
if (password === INSECURE_DEFAULT_PASSWORD) {
|
|
84
|
+
console.warn(
|
|
85
|
+
'[@enbox/auth] SECURITY WARNING: No password set. Using insecure default. ' +
|
|
86
|
+
'Set a password via AuthManager.create({ password }) or connect({ password }) ' +
|
|
87
|
+
'to protect your identity vault.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return password;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── ensureVaultReady ────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize (on first launch) and start the agent, then emit `vault-unlocked`.
|
|
98
|
+
*
|
|
99
|
+
* This consolidates the 5 copies of:
|
|
100
|
+
* ```ts
|
|
101
|
+
* if (isFirstLaunch) { await userAgent.initialize({ password, ... }); }
|
|
102
|
+
* await userAgent.start({ password });
|
|
103
|
+
* emitter.emit('vault-unlocked', {});
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @returns The recovery phrase if the vault was just initialized, otherwise `undefined`.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
export async function ensureVaultReady(params: {
|
|
111
|
+
userAgent: EnboxUserAgent;
|
|
112
|
+
emitter: AuthEventEmitter;
|
|
113
|
+
password: string;
|
|
114
|
+
isFirstLaunch: boolean;
|
|
115
|
+
recoveryPhrase?: string;
|
|
116
|
+
dwnEndpoints?: string[];
|
|
117
|
+
}): Promise<string | undefined> {
|
|
118
|
+
const { userAgent, emitter, password, isFirstLaunch } = params;
|
|
119
|
+
let recoveryPhrase: string | undefined;
|
|
120
|
+
|
|
121
|
+
if (isFirstLaunch) {
|
|
122
|
+
recoveryPhrase = await userAgent.initialize({
|
|
123
|
+
password,
|
|
124
|
+
recoveryPhrase : params.recoveryPhrase,
|
|
125
|
+
dwnEndpoints : params.dwnEndpoints,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await userAgent.start({ password });
|
|
130
|
+
emitter.emit('vault-unlocked', {});
|
|
131
|
+
|
|
132
|
+
return recoveryPhrase;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── startSyncIfEnabled ─────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start DWN synchronisation if `sync` is not `'off'`.
|
|
139
|
+
*
|
|
140
|
+
* Consolidates 6 copies of:
|
|
141
|
+
* ```ts
|
|
142
|
+
* const syncMode = sync === undefined ? 'live' : 'poll';
|
|
143
|
+
* const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
144
|
+
* userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
145
|
+
* .catch((err) => console.error('[@enbox/auth] Sync failed:', err));
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
export function startSyncIfEnabled(
|
|
151
|
+
userAgent: EnboxUserAgent,
|
|
152
|
+
sync: SyncOption | undefined,
|
|
153
|
+
): void {
|
|
154
|
+
if (sync === 'off') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const syncMode = sync === undefined ? 'live' : 'poll';
|
|
159
|
+
const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
|
|
160
|
+
|
|
161
|
+
userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
|
|
162
|
+
.catch((err: unknown) => {
|
|
163
|
+
console.error('[@enbox/auth] Sync failed:', err);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── createDefaultIdentity ──────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a new `did:dht` identity with Ed25519 signing and X25519
|
|
171
|
+
* encryption keys, and a DWN service endpoint.
|
|
172
|
+
*
|
|
173
|
+
* This consolidates the identical identity creation block that was
|
|
174
|
+
* duplicated in `localConnect` and `importFromPhrase`.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export async function createDefaultIdentity(
|
|
179
|
+
userAgent: EnboxUserAgent,
|
|
180
|
+
dwnEndpoints: string[] = DEFAULT_DWN_ENDPOINTS,
|
|
181
|
+
name = 'Default',
|
|
182
|
+
): Promise<BearerIdentity> {
|
|
183
|
+
return userAgent.identity.create({
|
|
184
|
+
didMethod : 'dht',
|
|
185
|
+
metadata : { name },
|
|
186
|
+
didOptions : {
|
|
187
|
+
services: [
|
|
188
|
+
{
|
|
189
|
+
id : 'dwn',
|
|
190
|
+
type : 'DecentralizedWebNode',
|
|
191
|
+
serviceEndpoint : dwnEndpoints,
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
verificationMethods: [
|
|
195
|
+
{
|
|
196
|
+
algorithm : 'Ed25519',
|
|
197
|
+
id : 'sig',
|
|
198
|
+
purposes : ['assertionMethod', 'authentication'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
algorithm : 'X25519',
|
|
202
|
+
id : 'enc',
|
|
203
|
+
purposes : ['keyAgreement'],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── resolveIdentityDids ────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Derive `connectedDid` and `delegateDid` from identity metadata.
|
|
214
|
+
*
|
|
215
|
+
* For a **local** identity: `connectedDid` is the identity's own DID URI
|
|
216
|
+
* and `delegateDid` is `undefined`.
|
|
217
|
+
*
|
|
218
|
+
* For a **wallet-connected** identity: `connectedDid` is the external wallet
|
|
219
|
+
* DID, and `delegateDid` is the local identity's DID URI.
|
|
220
|
+
*
|
|
221
|
+
* @param identity - The bearer identity to extract DIDs from.
|
|
222
|
+
* @param storedDelegateDid - Optional fallback delegate DID from storage,
|
|
223
|
+
* used by session-restore when the identity metadata doesn't include a
|
|
224
|
+
* `connectedDid` but a delegate DID was persisted in a prior session.
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
export function resolveIdentityDids(
|
|
229
|
+
identity: BearerIdentity,
|
|
230
|
+
storedDelegateDid?: string,
|
|
231
|
+
): {
|
|
232
|
+
connectedDid: string;
|
|
233
|
+
delegateDid: string | undefined;
|
|
234
|
+
} {
|
|
235
|
+
const connectedDid = identity.metadata.connectedDid ?? identity.did.uri;
|
|
236
|
+
const delegateDid = identity.metadata.connectedDid
|
|
237
|
+
? identity.did.uri
|
|
238
|
+
: (storedDelegateDid ?? undefined);
|
|
239
|
+
return { connectedDid, delegateDid };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── finalizeSession ────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Persist session markers, build an `AuthSession`, and emit lifecycle events.
|
|
246
|
+
*
|
|
247
|
+
* Consolidates 5 copies of:
|
|
248
|
+
* ```ts
|
|
249
|
+
* await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
250
|
+
* await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
251
|
+
* const session = new AuthSession({ ... });
|
|
252
|
+
* emitter.emit('identity-added', { identity: identityInfo });
|
|
253
|
+
* emitter.emit('session-start', { session: { ... } });
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* @param params.emitIdentityAdded - Whether to emit `identity-added`. Defaults to `true`.
|
|
257
|
+
* Set to `false` for session-restore (identity was already added in the original flow).
|
|
258
|
+
* @param params.extraStorageKeys - Additional key-value pairs to persist (e.g. delegate/connected DIDs
|
|
259
|
+
* for wallet-connect flows).
|
|
260
|
+
*
|
|
261
|
+
* @internal
|
|
262
|
+
*/
|
|
263
|
+
export async function finalizeSession(params: {
|
|
264
|
+
userAgent: EnboxUserAgent;
|
|
265
|
+
emitter: AuthEventEmitter;
|
|
266
|
+
storage: StorageAdapter;
|
|
267
|
+
connectedDid: string;
|
|
268
|
+
delegateDid?: string;
|
|
269
|
+
recoveryPhrase?: string;
|
|
270
|
+
identityName: string;
|
|
271
|
+
identityConnectedDid?: string;
|
|
272
|
+
emitIdentityAdded?: boolean;
|
|
273
|
+
extraStorageKeys?: Record<string, string>;
|
|
274
|
+
}): Promise<AuthSession> {
|
|
275
|
+
const {
|
|
276
|
+
userAgent,
|
|
277
|
+
emitter,
|
|
278
|
+
storage,
|
|
279
|
+
connectedDid,
|
|
280
|
+
delegateDid,
|
|
281
|
+
recoveryPhrase,
|
|
282
|
+
identityName,
|
|
283
|
+
identityConnectedDid,
|
|
284
|
+
emitIdentityAdded = true,
|
|
285
|
+
extraStorageKeys,
|
|
286
|
+
} = params;
|
|
287
|
+
|
|
288
|
+
// Persist session markers.
|
|
289
|
+
await storage.set(STORAGE_KEYS.PREVIOUSLY_CONNECTED, 'true');
|
|
290
|
+
await storage.set(STORAGE_KEYS.ACTIVE_IDENTITY, connectedDid);
|
|
291
|
+
|
|
292
|
+
if (extraStorageKeys) {
|
|
293
|
+
for (const [key, value] of Object.entries(extraStorageKeys)) {
|
|
294
|
+
await storage.set(key, value);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const identityInfo: IdentityInfo = {
|
|
299
|
+
didUri : connectedDid,
|
|
300
|
+
name : identityName,
|
|
301
|
+
connectedDid : identityConnectedDid,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const session = new AuthSession({
|
|
305
|
+
agent : userAgent,
|
|
306
|
+
did : connectedDid,
|
|
307
|
+
delegateDid,
|
|
308
|
+
recoveryPhrase,
|
|
309
|
+
identity : identityInfo,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (emitIdentityAdded) {
|
|
313
|
+
emitter.emit('identity-added', { identity: identityInfo });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
emitter.emit('session-start', {
|
|
317
|
+
session: { did: connectedDid, delegateDid, identity: identityInfo },
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return session;
|
|
321
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local DID connect flow.
|
|
3
|
+
*
|
|
4
|
+
* Creates or reconnects a local identity with vault-protected keys.
|
|
5
|
+
* This replaces the "Mode D/E" paths in Enbox.connect().
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AuthSession } from '../identity-session.js';
|
|
10
|
+
import type { FlowContext } from './lifecycle.js';
|
|
11
|
+
import type { LocalConnectOptions } from '../types.js';
|
|
12
|
+
|
|
13
|
+
import { applyLocalDwnDiscovery } from '../discovery.js';
|
|
14
|
+
import { DEFAULT_DWN_ENDPOINTS } from '../types.js';
|
|
15
|
+
import { registerWithDwnEndpoints } from '../registration.js';
|
|
16
|
+
import { createDefaultIdentity, ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute the local connect flow.
|
|
20
|
+
*
|
|
21
|
+
* - On first launch: initializes the vault, creates a new DID, returns recovery phrase.
|
|
22
|
+
* - On subsequent launches: unlocks the vault and reconnects to the existing identity.
|
|
23
|
+
*/
|
|
24
|
+
export async function localConnect(
|
|
25
|
+
ctx: FlowContext,
|
|
26
|
+
options: LocalConnectOptions = {},
|
|
27
|
+
): Promise<AuthSession> {
|
|
28
|
+
const { userAgent, emitter, storage } = ctx;
|
|
29
|
+
|
|
30
|
+
// Resolve password through the standard chain.
|
|
31
|
+
const isFirstLaunch = await userAgent.firstLaunch();
|
|
32
|
+
const password = await resolvePassword(ctx, options.password, isFirstLaunch);
|
|
33
|
+
|
|
34
|
+
const sync = options.sync ?? ctx.defaultSync;
|
|
35
|
+
const dwnEndpoints = options.dwnEndpoints ?? ctx.defaultDwnEndpoints ?? DEFAULT_DWN_ENDPOINTS;
|
|
36
|
+
|
|
37
|
+
// Initialize vault on first launch and start the agent.
|
|
38
|
+
const recoveryPhrase = await ensureVaultReady({
|
|
39
|
+
userAgent,
|
|
40
|
+
emitter,
|
|
41
|
+
password,
|
|
42
|
+
isFirstLaunch,
|
|
43
|
+
recoveryPhrase: options.recoveryPhrase,
|
|
44
|
+
dwnEndpoints,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Apply local DWN discovery (browser redirect payload or persisted endpoint).
|
|
48
|
+
// In remote mode, discovery already ran before agent creation — skip.
|
|
49
|
+
if (!userAgent.dwn.isRemoteMode) {
|
|
50
|
+
await applyLocalDwnDiscovery(userAgent, storage, emitter);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find or create the user identity.
|
|
54
|
+
const identities = await userAgent.identity.list();
|
|
55
|
+
let identity = identities[0];
|
|
56
|
+
let isNewIdentity = false;
|
|
57
|
+
|
|
58
|
+
if (!identity) {
|
|
59
|
+
isNewIdentity = true;
|
|
60
|
+
identity = await createDefaultIdentity(userAgent, dwnEndpoints, options.metadata?.name ?? 'Default');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { connectedDid, delegateDid } = resolveIdentityDids(identity);
|
|
64
|
+
|
|
65
|
+
// Register with DWN endpoints (if registration options are provided).
|
|
66
|
+
if (ctx.registration) {
|
|
67
|
+
await registerWithDwnEndpoints(
|
|
68
|
+
{
|
|
69
|
+
userAgent : userAgent,
|
|
70
|
+
dwnEndpoints,
|
|
71
|
+
agentDid : userAgent.agentDid.uri,
|
|
72
|
+
connectedDid,
|
|
73
|
+
storage : storage,
|
|
74
|
+
},
|
|
75
|
+
ctx.registration,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Register sync for new identities.
|
|
80
|
+
if (isNewIdentity && sync !== 'off') {
|
|
81
|
+
await userAgent.sync.registerIdentity({
|
|
82
|
+
did : connectedDid,
|
|
83
|
+
options : { delegateDid, protocols: [] },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Start sync.
|
|
88
|
+
startSyncIfEnabled(userAgent, sync);
|
|
89
|
+
|
|
90
|
+
// Persist session info, build AuthSession, and emit lifecycle events.
|
|
91
|
+
return finalizeSession({
|
|
92
|
+
userAgent,
|
|
93
|
+
emitter,
|
|
94
|
+
storage,
|
|
95
|
+
connectedDid,
|
|
96
|
+
delegateDid,
|
|
97
|
+
recoveryPhrase,
|
|
98
|
+
identityName : identity.metadata.name,
|
|
99
|
+
identityConnectedDid : identity.metadata.connectedDid,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session restore flow.
|
|
3
|
+
*
|
|
4
|
+
* Restores a previously established session from persisted storage,
|
|
5
|
+
* replacing the "previouslyConnected" pattern in apps.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AuthSession } from '../identity-session.js';
|
|
10
|
+
import type { FlowContext } from './lifecycle.js';
|
|
11
|
+
import type { RestoreSessionOptions } from '../types.js';
|
|
12
|
+
|
|
13
|
+
import { applyLocalDwnDiscovery } from '../discovery.js';
|
|
14
|
+
import { STORAGE_KEYS } from '../types.js';
|
|
15
|
+
import { ensureVaultReady, finalizeSession, resolveIdentityDids, resolvePassword, startSyncIfEnabled } from './lifecycle.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Attempt to restore a previous session.
|
|
19
|
+
*
|
|
20
|
+
* Returns `undefined` if no previous session exists.
|
|
21
|
+
* Returns an `AuthSession` if the session was successfully restored.
|
|
22
|
+
*/
|
|
23
|
+
export async function restoreSession(
|
|
24
|
+
ctx: FlowContext,
|
|
25
|
+
options: RestoreSessionOptions = {},
|
|
26
|
+
): Promise<AuthSession | undefined> {
|
|
27
|
+
const { userAgent, emitter, storage } = ctx;
|
|
28
|
+
|
|
29
|
+
// Check if there was a previous session.
|
|
30
|
+
const previouslyConnected = await storage.get(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
|
|
31
|
+
if (previouslyConnected !== 'true') {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve password: explicit option → callback → provider → manager default → insecure fallback.
|
|
36
|
+
// Note: restoreSession has an extra `onPasswordRequired` callback that sits between
|
|
37
|
+
// the explicit password and the provider. We handle that here, then delegate the
|
|
38
|
+
// remainder of the chain to `resolvePassword()`.
|
|
39
|
+
let explicitPassword = options.password;
|
|
40
|
+
|
|
41
|
+
if (!explicitPassword && !ctx.defaultPassword && options.onPasswordRequired) {
|
|
42
|
+
explicitPassword = await options.onPasswordRequired();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check for stale session marker: if the vault was never initialized,
|
|
46
|
+
// previouslyConnected is a leftover — clean up and bail.
|
|
47
|
+
const isFirstLaunch = await userAgent.firstLaunch();
|
|
48
|
+
if (isFirstLaunch) {
|
|
49
|
+
await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const password = await resolvePassword(ctx, explicitPassword, false);
|
|
54
|
+
|
|
55
|
+
// Start the agent (vault is known to exist).
|
|
56
|
+
await ensureVaultReady({
|
|
57
|
+
userAgent,
|
|
58
|
+
emitter,
|
|
59
|
+
password,
|
|
60
|
+
isFirstLaunch: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Apply local DWN discovery (browser redirect payload or persisted endpoint).
|
|
64
|
+
// In remote mode, discovery already ran before agent creation — skip.
|
|
65
|
+
if (!userAgent.dwn.isRemoteMode) {
|
|
66
|
+
await applyLocalDwnDiscovery(userAgent, storage, emitter);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Determine which identity to reconnect.
|
|
70
|
+
const activeIdentityDid = await storage.get(STORAGE_KEYS.ACTIVE_IDENTITY);
|
|
71
|
+
const storedDelegateDid = await storage.get(STORAGE_KEYS.DELEGATE_DID);
|
|
72
|
+
|
|
73
|
+
// First try the connected identity (wallet-connected sessions).
|
|
74
|
+
let identity = await userAgent.identity.connectedIdentity();
|
|
75
|
+
|
|
76
|
+
if (!identity) {
|
|
77
|
+
// Try to find the specific active identity.
|
|
78
|
+
if (activeIdentityDid) {
|
|
79
|
+
identity = await userAgent.identity.get({ didUri: activeIdentityDid });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fall back to the first available identity.
|
|
83
|
+
if (!identity) {
|
|
84
|
+
const identities = await userAgent.identity.list();
|
|
85
|
+
identity = identities[0];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!identity) {
|
|
90
|
+
// No identity found — clean up stale session data.
|
|
91
|
+
await storage.remove(STORAGE_KEYS.PREVIOUSLY_CONNECTED);
|
|
92
|
+
await storage.remove(STORAGE_KEYS.ACTIVE_IDENTITY);
|
|
93
|
+
await storage.remove(STORAGE_KEYS.DELEGATE_DID);
|
|
94
|
+
await storage.remove(STORAGE_KEYS.CONNECTED_DID);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { connectedDid, delegateDid } = resolveIdentityDids(
|
|
99
|
+
identity, storedDelegateDid ?? undefined,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Start sync.
|
|
103
|
+
startSyncIfEnabled(userAgent, ctx.defaultSync);
|
|
104
|
+
|
|
105
|
+
// Persist session info, build AuthSession, and emit lifecycle events.
|
|
106
|
+
// Session restore does not emit `identity-added` (identity was already added in the original flow).
|
|
107
|
+
return finalizeSession({
|
|
108
|
+
userAgent,
|
|
109
|
+
emitter,
|
|
110
|
+
storage,
|
|
111
|
+
connectedDid,
|
|
112
|
+
delegateDid,
|
|
113
|
+
identityName : identity.metadata.name,
|
|
114
|
+
identityConnectedDid : identity.metadata.connectedDid,
|
|
115
|
+
emitIdentityAdded : false,
|
|
116
|
+
});
|
|
117
|
+
}
|