@enbox/browser 0.3.53 → 0.3.55

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.
@@ -7,8 +7,9 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type { ConnectPermissionRequest } from '@enbox/connect';
10
+ import type { PortableDid } from '@enbox/dids';
11
11
  import type { ConnectHandler, ConnectResult } from '@enbox/auth';
12
+ import type { ConnectPermissionRequest, ConnectRequestType } from '@enbox/connect';
12
13
 
13
14
  import { runConnectModal } from './ui/connect-modal.js';
14
15
 
@@ -175,6 +176,8 @@ export function BrowserConnectHandler(
175
176
  return {
176
177
  async requestAccess(params: {
177
178
  permissionRequests: ConnectPermissionRequest[];
179
+ delegatePortableDid?: PortableDid;
180
+ requestType?: ConnectRequestType;
178
181
  }): Promise<ConnectResult | undefined> {
179
182
  return runConnectModal({
180
183
  wallets,
@@ -182,12 +185,14 @@ export function BrowserConnectHandler(
182
185
  preferredMethod,
183
186
  rememberChoice,
184
187
  appName,
185
- appIcon : appIcon ?? `${window.location.origin}/favicon.ico`,
188
+ appIcon : appIcon ?? `${window.location.origin}/favicon.ico`,
186
189
  connectServerUrl,
187
190
  relayWalletPath,
188
191
  timeout,
189
192
  theme,
190
- permissionRequests : params.permissionRequests,
193
+ mode : params.requestType,
194
+ delegatePortableDid : params.delegatePortableDid,
195
+ permissionRequests : params.permissionRequests,
191
196
  });
192
197
  },
193
198
  };
package/src/index.ts CHANGED
@@ -49,6 +49,11 @@ export type {
49
49
  export {
50
50
  AuthManager,
51
51
  AuthSession,
52
+ SESSION_EXPIRED_ERROR_CODE,
53
+ SESSION_REVOKED_ERROR_CODE,
54
+ computeConnectionStatus,
55
+ isSessionExpiredError,
56
+ isSessionInvalidError,
52
57
  normalizeProtocolRequests,
53
58
  WalletConnect,
54
59
  } from '@enbox/auth/browser';
@@ -58,16 +63,23 @@ export type {
58
63
  AuthEventHandler,
59
64
  AuthManagerOptions,
60
65
  AuthState,
66
+ ComputeConnectionStatusOptions,
67
+ ConnectionMonitorOptions,
68
+ ConnectionState,
69
+ ConnectionStatus,
70
+ ConnectionStatusGrant,
61
71
  ConnectHandler,
62
72
  ConnectOptions,
63
73
  ConnectResult,
64
74
  DisconnectOptions,
75
+ GetConnectionStatusOptions,
65
76
  HandlerConnectOptions,
66
77
  ImportFromPortableOptions,
67
78
  VaultConnectOptions,
68
79
  Permission,
69
80
  PortableIdentity,
70
81
  ProtocolRequest,
82
+ RefreshOptions,
71
83
  RegistrationOptions,
72
84
  StorageAdapter,
73
85
  SyncOption,
@@ -80,7 +92,7 @@ export type {
80
92
  // transports: opening/sealing envelopes and the deny token.
81
93
 
82
94
  export { CONNECT_DENIED_TOKEN, ConnectProvider } from '@enbox/connect';
83
- export type { ConnectApproval, ConnectPermissionRequest, ConnectRequest } from '@enbox/connect';
95
+ export type { ConnectApproval, ConnectPermissionRequest, ConnectRequest, ConnectRequestType } from '@enbox/connect';
84
96
 
85
97
  // ─── Browser-specific exports ───────────────────────────────────
86
98
  //
@@ -20,10 +20,12 @@
20
20
  */
21
21
 
22
22
  import type { Jwk } from '@enbox/crypto';
23
+ import type { PortableDid } from '@enbox/dids';
23
24
  import type {
24
25
  ConnectClientMetadata,
25
26
  ConnectPermissionRequest,
26
27
  ConnectRequest,
28
+ ConnectRequestType,
27
29
  ConnectResult,
28
30
  WalletUriHandoff,
29
31
  } from '@enbox/connect';
@@ -35,6 +37,7 @@ import {
35
37
  openResponse,
36
38
  randomToken,
37
39
  RelayClientTransport,
40
+ resolveDelegatePortableDid,
38
41
  sealRequest,
39
42
  } from '@enbox/connect';
40
43
 
@@ -69,6 +72,12 @@ export interface RelayConnectOptions {
69
72
  /** DWN protocols and permission scopes being requested. */
70
73
  permissionRequests: ConnectPermissionRequest[];
71
74
 
75
+ /** Existing delegate credentials reused by a refresh request. */
76
+ delegatePortableDid?: PortableDid;
77
+
78
+ /** User-facing request purpose. Absent means a normal connect. */
79
+ requestType?: ConnectRequestType;
80
+
72
81
  /** Total milliseconds to poll the relay for a wallet response. */
73
82
  timeoutMs?: number;
74
83
 
@@ -107,6 +116,7 @@ export interface RelayConnectOptions {
107
116
  timeoutMs?: number;
108
117
  pollIntervalMs?: number;
109
118
  onClaimed?: () => void;
119
+ signal?: AbortSignal;
110
120
  }) => Pick<RelayClientTransport, 'requestProfile' | 'deliverRequest' | 'awaitResponse' | 'requiresPin'>
111
121
  & Partial<Pick<RelayClientTransport, 'confirmComplete'>>;
112
122
  }
@@ -147,18 +157,24 @@ export function visibilityAwareSleep(ms: number): Promise<void> {
147
157
  * delegated credentials, or `undefined` when the user denied in the wallet.
148
158
  */
149
159
  export async function runRelayConnect(options: RelayConnectOptions): Promise<ConnectResult | undefined> {
160
+ if (options.requestType === 'refresh' && options.delegatePortableDid === undefined) {
161
+ throw new Error('Connect: refresh requests require an existing `delegatePortableDid`.');
162
+ }
163
+
150
164
  const createTransport = options.createTransport
151
165
  ?? ((transportOptions): RelayClientTransport => new RelayClientTransport({
152
166
  ...transportOptions,
153
167
  sleep: visibilityAwareSleep,
154
168
  }));
155
169
 
170
+ const cancellationController = options.cancelled === undefined ? undefined : new AbortController();
156
171
  const transport = createTransport({
157
172
  connectServerUrl : options.connectServerUrl,
158
173
  walletUri : options.walletUri,
159
174
  timeoutMs : options.timeoutMs,
160
175
  pollIntervalMs : options.pollIntervalMs,
161
176
  onClaimed : options.onClaimed,
177
+ signal : cancellationController?.signal,
162
178
  });
163
179
 
164
180
  // Ephemeral client DID for request signing and response addressing, and a
@@ -179,6 +195,8 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
179
195
  appIcon : options.appIcon,
180
196
  clientMetadata : options.clientMetadata,
181
197
  permissionRequests : options.permissionRequests,
198
+ delegateDid : options.delegatePortableDid?.uri,
199
+ requestType : options.requestType,
182
200
  supportedDidMethods : ['did:dht', 'did:jwk'],
183
201
  nonce,
184
202
  state,
@@ -197,7 +215,13 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
197
215
  ? transport.awaitResponse()
198
216
  : Promise.race([transport.awaitResponse(), options.cancelled]);
199
217
 
200
- const responseCiphertext = await race;
218
+ let responseCiphertext: string;
219
+ try {
220
+ responseCiphertext = await race;
221
+ } catch (error) {
222
+ cancellationController?.abort();
223
+ throw error;
224
+ }
201
225
  if (responseCiphertext === CONNECT_DENIED_TOKEN) {
202
226
  return undefined;
203
227
  }
@@ -220,9 +244,10 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
220
244
  pin,
221
245
  });
222
246
 
223
- if (response.delegatePortableDid === undefined) {
224
- throw new Error('Connect: wallet response omitted `delegatePortableDid`.');
225
- }
247
+ const delegatePortableDid = resolveDelegatePortableDid({
248
+ localDelegatePortableDid: options.delegatePortableDid,
249
+ response,
250
+ });
226
251
 
227
252
  // Best-effort completion signal so the wallet can flip its pairing
228
253
  // screen to a confirmed "connected" state instead of leaving the user
@@ -230,10 +255,10 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
230
255
  void transport.confirmComplete?.().catch((): undefined => undefined);
231
256
 
232
257
  return {
233
- delegatePortableDid : response.delegatePortableDid,
234
- delegateGrants : response.delegateGrants,
235
- connectedDid : response.providerDid,
236
- sessionRevocations : response.sessionRevocations,
258
+ delegatePortableDid,
259
+ delegateGrants : response.delegateGrants,
260
+ connectedDid : response.providerDid,
261
+ sessionRevocations : response.sessionRevocations,
237
262
  };
238
263
  } catch (error) {
239
264
  const failure = error instanceof Error ? error : new Error(String(error));
@@ -37,8 +37,9 @@
37
37
  * @module
38
38
  */
39
39
 
40
+ import type { PortableDid } from '@enbox/dids';
40
41
  import type { WalletOption } from '../browser-connect-handler.js';
41
- import type { ConnectPermissionRequest, ConnectResult, WalletUriHandoff } from '@enbox/connect';
42
+ import type { ConnectPermissionRequest, ConnectRequestType, ConnectResult, WalletUriHandoff } from '@enbox/connect';
42
43
 
43
44
  import { ConnectClient } from '@enbox/connect';
44
45
 
@@ -157,6 +158,12 @@ export interface ConnectModalOptions {
157
158
  */
158
159
  walletUrl?: string;
159
160
 
161
+ /** User-facing mode. Absent means a normal connect. */
162
+ mode?: ConnectRequestType;
163
+
164
+ /** Existing delegate credentials reused by a refresh request. */
165
+ delegatePortableDid?: PortableDid;
166
+
160
167
  /** Preferred method on open; overrides the remembered choice. */
161
168
  preferredMethod?: ConnectMethod;
162
169
 
@@ -215,6 +222,8 @@ export interface ConnectModalDeps {
215
222
  appName: string;
216
223
  appIcon?: string;
217
224
  timeoutMs?: number;
225
+ delegatePortableDid?: PortableDid;
226
+ requestType?: ConnectRequestType;
218
227
  }) => Promise<ConnectResult | undefined>;
219
228
 
220
229
  /** Resolves a wallet origin to its relay `connectServerUrl` (or undefined). */
@@ -246,6 +255,8 @@ async function runPopupConnect(options: {
246
255
  appName: string;
247
256
  appIcon?: string;
248
257
  timeoutMs?: number;
258
+ delegatePortableDid?: PortableDid;
259
+ requestType?: ConnectRequestType;
249
260
  }): Promise<ConnectResult | undefined> {
250
261
  // The transport constructor calls `window.open` synchronously — callers
251
262
  // must invoke this inside the user-gesture call stack.
@@ -256,10 +267,12 @@ async function runPopupConnect(options: {
256
267
  const client = new ConnectClient({ transport });
257
268
 
258
269
  return await client.connect({
259
- appName : options.appName,
260
- appIcon : options.appIcon,
261
- clientMetadata : collectBrowserClientMetadata(),
262
- permissionRequests : options.permissionRequests,
270
+ appName : options.appName,
271
+ appIcon : options.appIcon,
272
+ clientMetadata : collectBrowserClientMetadata(),
273
+ permissionRequests : options.permissionRequests,
274
+ delegatePortableDid : options.delegatePortableDid,
275
+ requestType : options.requestType,
263
276
  });
264
277
  }
265
278
 
@@ -295,7 +308,7 @@ function readLastChoice(storage: ConnectModalDeps['storage']): LastChoice | unde
295
308
  return undefined;
296
309
  }
297
310
  const parsed = JSON.parse(raw) as Partial<LastChoice>;
298
- if ((parsed.method === 'phone' || parsed.method === 'browser') && typeof parsed.walletUrl === 'string') {
311
+ if ((parsed.method === 'phone' || parsed.method === 'browser') && isValidRememberedWalletUrl(parsed.walletUrl)) {
299
312
  return { method: parsed.method, walletUrl: parsed.walletUrl };
300
313
  }
301
314
  } catch { /* unreadable storage — behave as first visit */ }
@@ -345,6 +358,9 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
345
358
  if (typeof window === 'undefined' || typeof document === 'undefined') {
346
359
  throw new Error('[@enbox/browser] The connect modal is only available in browser environments.');
347
360
  }
361
+ if (options.mode === 'refresh' && options.delegatePortableDid === undefined) {
362
+ throw new Error('Connect: refresh requests require an existing `delegatePortableDid`.');
363
+ }
348
364
 
349
365
  const deps: ConnectModalDeps = { ...defaultDeps(), ...options.deps };
350
366
  const wallets = dedupeWalletsByUrl(options.wallets ?? []);
@@ -352,13 +368,18 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
352
368
  const appName = options.appName ?? window.location.host;
353
369
  const relayWalletPath = options.relayWalletPath ?? '/connect/app';
354
370
  const isMobile = deps.isMobile();
371
+ const refreshing = options.mode === 'refresh';
355
372
 
356
373
  const lastChoice = rememberChoice ? readLastChoice(deps.storage) : undefined;
357
374
 
358
375
  // Wallet resolution order: explicit option → remembered → catalog head.
359
- const lockedWallet = options.walletUrl !== undefined;
376
+ const rememberedWalletUrl = lastChoice !== undefined
377
+ && (refreshing || walletInCatalog(wallets, lastChoice.walletUrl))
378
+ ? lastChoice.walletUrl
379
+ : undefined;
380
+ const lockedWallet = options.walletUrl !== undefined || (refreshing && rememberedWalletUrl !== undefined);
360
381
  let walletUrl = options.walletUrl
361
- ?? (lastChoice !== undefined && (lockedWallet || walletInCatalog(wallets, lastChoice.walletUrl)) ? lastChoice.walletUrl : undefined)
382
+ ?? rememberedWalletUrl
362
383
  ?? wallets[0]?.url;
363
384
 
364
385
  // Method resolution order: explicit option → remembered → phone.
@@ -380,7 +401,8 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
380
401
  modal.className = 'modal';
381
402
  modal.setAttribute('role', 'dialog');
382
403
  modal.setAttribute('aria-modal', 'true');
383
- modal.setAttribute('aria-label', `Connect to ${appName}`);
404
+ const title = refreshing ? `Reconnect to ${appName}` : `Connect to ${appName}`;
405
+ modal.setAttribute('aria-label', title);
384
406
 
385
407
  // ── Theme ──────────────────────────────────────────────────
386
408
  // System light/dark is pure CSS (`prefers-color-scheme`), so it tracks
@@ -489,7 +511,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
489
511
  const cancelModal = (): void => settle(() => reject(new Error('[@enbox/browser] Connect cancelled.')));
490
512
 
491
513
  // ── Zones ──────────────────────────────────────────────────
492
- modal.appendChild(buildHeader(appName, options.appIcon, cancelModal));
514
+ modal.appendChild(buildHeader(title, options.appIcon, cancelModal));
493
515
 
494
516
  const stage = document.createElement('div');
495
517
  stage.className = 'stage';
@@ -839,14 +861,16 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
839
861
  try {
840
862
  const result = await deps.runRelay({
841
863
  connectServerUrl,
842
- walletUri : new URL(relayWalletPath, origin).toString(),
864
+ walletUri : new URL(relayWalletPath, origin).toString(),
843
865
  appName,
844
- appIcon : options.appIcon,
845
- clientMetadata : collectBrowserClientMetadata(),
846
- permissionRequests : options.permissionRequests,
847
- timeoutMs : options.timeout,
848
- cancelled : session.cancelled,
849
- onWalletUriReady : (handoff): void => {
866
+ appIcon : options.appIcon,
867
+ clientMetadata : collectBrowserClientMetadata(),
868
+ permissionRequests : options.permissionRequests,
869
+ delegatePortableDid : options.delegatePortableDid,
870
+ requestType : options.mode,
871
+ timeoutMs : options.timeout,
872
+ cancelled : session.cancelled,
873
+ onWalletUriReady : (handoff): void => {
850
874
  if (session.active && !settled) {
851
875
  renderQr(handoff);
852
876
  scheduleRemint(handoff.expiresIn);
@@ -907,10 +931,12 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
907
931
  try {
908
932
  deps.runPopup({
909
933
  walletUrl,
910
- permissionRequests : options.permissionRequests,
934
+ permissionRequests : options.permissionRequests,
911
935
  appName,
912
- appIcon : options.appIcon,
913
- timeoutMs : options.timeout,
936
+ appIcon : options.appIcon,
937
+ timeoutMs : options.timeout,
938
+ delegatePortableDid : options.delegatePortableDid,
939
+ requestType : options.mode,
914
940
  })
915
941
  .then((result) => {
916
942
  if (settled) { return; }
@@ -1226,6 +1252,19 @@ function walletInCatalog(wallets: WalletOption[], url: string): boolean {
1226
1252
  return wallets.some((wallet) => wallet.url === url);
1227
1253
  }
1228
1254
 
1255
+ function isValidRememberedWalletUrl(value: unknown): value is string {
1256
+ if (typeof value !== 'string' || value.trim() === '') {
1257
+ return false;
1258
+ }
1259
+
1260
+ try {
1261
+ const url = new URL(value);
1262
+ return url.protocol === 'http:' || url.protocol === 'https:';
1263
+ } catch {
1264
+ return false;
1265
+ }
1266
+ }
1267
+
1229
1268
  /** Drops catalog entries that repeat an earlier wallet's URL. */
1230
1269
  function dedupeWalletsByUrl(wallets: WalletOption[]): WalletOption[] {
1231
1270
  return wallets.filter((wallet, index) => wallets.findIndex((candidate) => candidate.url === wallet.url) === index);
@@ -1268,7 +1307,7 @@ function stageLinkButton(label: string, onClick: () => void): HTMLButtonElement
1268
1307
  return button;
1269
1308
  }
1270
1309
 
1271
- function buildHeader(appName: string, appIcon: string | undefined, onCancel: () => void): HTMLDivElement {
1310
+ function buildHeader(titleText: string, appIcon: string | undefined, onCancel: () => void): HTMLDivElement {
1272
1311
  const header = document.createElement('div');
1273
1312
  header.className = 'header';
1274
1313
 
@@ -1287,7 +1326,7 @@ function buildHeader(appName: string, appIcon: string | undefined, onCancel: ()
1287
1326
  }
1288
1327
 
1289
1328
  const title = document.createElement('h2');
1290
- title.textContent = `Connect to ${appName}`;
1329
+ title.textContent = titleText;
1291
1330
  identity.appendChild(title);
1292
1331
 
1293
1332
  const closeBtn = document.createElement('button');