@enbox/browser 0.3.52 → 0.3.54

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/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
 
@@ -147,6 +156,10 @@ export function visibilityAwareSleep(ms: number): Promise<void> {
147
156
  * delegated credentials, or `undefined` when the user denied in the wallet.
148
157
  */
149
158
  export async function runRelayConnect(options: RelayConnectOptions): Promise<ConnectResult | undefined> {
159
+ if (options.requestType === 'refresh' && options.delegatePortableDid === undefined) {
160
+ throw new Error('Connect: refresh requests require an existing `delegatePortableDid`.');
161
+ }
162
+
150
163
  const createTransport = options.createTransport
151
164
  ?? ((transportOptions): RelayClientTransport => new RelayClientTransport({
152
165
  ...transportOptions,
@@ -179,6 +192,8 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
179
192
  appIcon : options.appIcon,
180
193
  clientMetadata : options.clientMetadata,
181
194
  permissionRequests : options.permissionRequests,
195
+ delegateDid : options.delegatePortableDid?.uri,
196
+ requestType : options.requestType,
182
197
  supportedDidMethods : ['did:dht', 'did:jwk'],
183
198
  nonce,
184
199
  state,
@@ -220,9 +235,10 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
220
235
  pin,
221
236
  });
222
237
 
223
- if (response.delegatePortableDid === undefined) {
224
- throw new Error('Connect: wallet response omitted `delegatePortableDid`.');
225
- }
238
+ const delegatePortableDid = resolveDelegatePortableDid({
239
+ localDelegatePortableDid: options.delegatePortableDid,
240
+ response,
241
+ });
226
242
 
227
243
  // Best-effort completion signal so the wallet can flip its pairing
228
244
  // screen to a confirmed "connected" state instead of leaving the user
@@ -230,10 +246,10 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
230
246
  void transport.confirmComplete?.().catch((): undefined => undefined);
231
247
 
232
248
  return {
233
- delegatePortableDid : response.delegatePortableDid,
234
- delegateGrants : response.delegateGrants,
235
- connectedDid : response.providerDid,
236
- sessionRevocations : response.sessionRevocations,
249
+ delegatePortableDid,
250
+ delegateGrants : response.delegateGrants,
251
+ connectedDid : response.providerDid,
252
+ sessionRevocations : response.sessionRevocations,
237
253
  };
238
254
  } catch (error) {
239
255
  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');