@enbox/browser 0.3.71 → 0.3.72

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.
@@ -96,8 +96,9 @@ export interface ConnectModalOptions {
96
96
  /** Preferred method on open; overrides the remembered choice. */
97
97
  preferredMethod?: ConnectMethod;
98
98
  /**
99
- * Remember the successful method + wallet in localStorage and pre-shape
100
- * the next session accordingly.
99
+ * Remember the successful method + wallet in localStorage. Refreshes reuse
100
+ * the route saved for their delegate DID; new connections use the latest
101
+ * successful route as a convenience.
101
102
  * @default true
102
103
  */
103
104
  rememberChoice?: boolean;
@@ -105,6 +106,13 @@ export interface ConnectModalOptions {
105
106
  appName?: string;
106
107
  /** Icon URL of the requesting application. */
107
108
  appIcon?: string;
109
+ /**
110
+ * Stable application identifier hint available to the wallet during approval.
111
+ * Defaults to the browser's canonical origin.
112
+ */
113
+ applicationId?: string;
114
+ /** Wallet profile DID that a refresh must renew. */
115
+ expectedProviderDid?: string;
108
116
  /**
109
117
  * Relay base URL override for the phone path. When omitted, each wallet's
110
118
  * `connectServerUrl` is discovered from its `/.well-known/enbox-connect`
@@ -134,15 +142,7 @@ export interface ConnectModalDeps {
134
142
  /** Runs one relay handshake (phone path). */
135
143
  runRelay: typeof runRelayConnect;
136
144
  /** Runs one popup handshake (browser path); must open the popup synchronously. */
137
- runPopup: (options: {
138
- walletUrl: string;
139
- permissionRequests: ConnectPermissionRequest[];
140
- appName: string;
141
- appIcon?: string;
142
- timeoutMs?: number;
143
- delegatePortableDid?: PortableDid;
144
- requestType?: ConnectRequestType;
145
- }) => Promise<ConnectResult | undefined>;
145
+ runPopup: typeof runPopupConnect;
146
146
  /** Resolves a wallet origin to its relay `connectServerUrl` (or undefined). */
147
147
  discoverConnectServerUrl: (walletOrigin: string) => Promise<string | undefined>;
148
148
  /** Validates a pasted custom wallet URL. */
@@ -158,6 +158,18 @@ export interface ConnectModalDeps {
158
158
  * same machinery the wallet selector uses for custom-URL validation).
159
159
  */
160
160
  export declare function discoverWalletConnectServerUrl(walletOrigin: string): Promise<string | undefined>;
161
+ /** Runs one popup handshake without collapsing early-close into a denial. */
162
+ declare function runPopupConnect(options: {
163
+ walletUrl: string;
164
+ permissionRequests: ConnectPermissionRequest[];
165
+ appName: string;
166
+ appIcon?: string;
167
+ applicationId?: string;
168
+ timeoutMs?: number;
169
+ delegatePortableDid?: PortableDid;
170
+ requestType?: ConnectRequestType;
171
+ expectedProviderDid?: string;
172
+ }): Promise<ConnectResult | undefined>;
161
173
  /**
162
174
  * Opens the connect modal and resolves when the session ends.
163
175
  *
@@ -167,4 +179,5 @@ export declare function discoverWalletConnectServerUrl(walletOrigin: string): Pr
167
179
  * user dismisses the error.
168
180
  */
169
181
  export declare function runConnectModal(options: ConnectModalOptions): Promise<ConnectResult | undefined>;
182
+ export {};
170
183
  //# sourceMappingURL=connect-modal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connect-modal.d.ts","sourceRoot":"","sources":["../../../src/ui/connect-modal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connect-modal.d.ts","sourceRoot":"","sources":["../../../src/ui/connect-modal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,aAAa,EAAoB,MAAM,gBAAgB,CAAC;AAYpH,OAAO,EAA8B,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAEzF,6CAA6C;AAC7C,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAkChD,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,qBAAqB;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,2DAA2D;AAC3D,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAEvC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,oEAAoE;IACpE,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAE5B,mEAAmE;IACnE,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B;AAeD,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IAClC,mDAAmD;IACnD,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IAEzB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAE1B,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAElC,iEAAiE;IACjE,eAAe,CAAC,EAAE,aAAa,CAAC;IAEhC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAE1B,2DAA2D;IAC3D,kBAAkB,EAAE,wBAAwB,EAAE,CAAC;IAE/C,yCAAyC;IACzC,IAAI,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAClC;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,eAAe,CAAC;IAEjC,kFAAkF;IAClF,QAAQ,EAAE,OAAO,eAAe,CAAC;IAEjC,+EAA+E;IAC/E,wBAAwB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAEhF,4CAA4C;IAC5C,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAExD,gFAAgF;IAChF,QAAQ,EAAE,MAAM,OAAO,CAAC;IAExB,6CAA6C;IAC7C,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED;;;;GAIG;AACH,wBAAsB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAEtG;AAED,6EAA6E;AAC7E,iBAAe,eAAe,CAAC,OAAO,EAAE;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,wBAAwB,EAAE,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAClC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAmBrC;AAmKD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAo5BhG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enbox/browser",
3
- "version": "0.3.71",
3
+ "version": "0.3.72",
4
4
  "description": "Enbox tools and features to use in the browser",
5
5
  "type": "module",
6
6
  "main": "./dist/esm/index.js",
@@ -66,15 +66,15 @@
66
66
  "access": "public"
67
67
  },
68
68
  "dependencies": {
69
- "@enbox/agent": "0.8.40",
70
- "@enbox/api": "0.6.79",
71
- "@enbox/auth": "0.6.86",
69
+ "@enbox/agent": "0.8.41",
70
+ "@enbox/api": "0.6.80",
71
+ "@enbox/auth": "0.6.87",
72
72
  "@enbox/common": "0.1.6",
73
- "@enbox/connect": "0.1.20",
73
+ "@enbox/connect": "0.1.21",
74
74
  "@enbox/crypto": "0.1.9",
75
75
  "@enbox/dids": "0.1.10",
76
- "@enbox/dwn-sdk-js": "0.4.24",
77
- "@enbox/protocols": "0.2.106"
76
+ "@enbox/dwn-sdk-js": "0.4.25",
77
+ "@enbox/protocols": "0.2.107"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/qrcode": "^1.5.6",
@@ -107,6 +107,12 @@ export interface BrowserConnectHandlerOptions {
107
107
  */
108
108
  appIcon?: string;
109
109
 
110
+ /**
111
+ * Stable application identifier hint available to the wallet during approval.
112
+ * Defaults to the browser's canonical origin.
113
+ */
114
+ applicationId?: string;
115
+
110
116
  /**
111
117
  * Preferred connect method on open: `'phone'` (QR / deep link via the
112
118
  * relay) or `'browser'` (wallet popup on this device). Overrides the
@@ -116,8 +122,9 @@ export interface BrowserConnectHandlerOptions {
116
122
  preferredMethod?: ConnectMethod;
117
123
 
118
124
  /**
119
- * Remember the successful method + wallet in localStorage and pre-shape
120
- * the next connect session accordingly.
125
+ * Remember the successful method + wallet in localStorage. Refreshes reuse
126
+ * the route saved for their delegate DID; new connections use the latest
127
+ * successful route as a convenience.
121
128
  * @default true
122
129
  */
123
130
  rememberChoice?: boolean;
@@ -176,6 +183,7 @@ export function BrowserConnectHandler(
176
183
  timeout,
177
184
  appName,
178
185
  appIcon,
186
+ applicationId,
179
187
  preferredMethod,
180
188
  rememberChoice,
181
189
  connectServerUrl,
@@ -188,6 +196,7 @@ export function BrowserConnectHandler(
188
196
  permissionRequests: ConnectPermissionRequest[];
189
197
  delegatePortableDid?: PortableDid;
190
198
  requestType?: ConnectRequestType;
199
+ expectedProviderDid?: string;
191
200
  }): Promise<ConnectResult | undefined> {
192
201
  return runConnectModal({
193
202
  wallets,
@@ -196,12 +205,14 @@ export function BrowserConnectHandler(
196
205
  rememberChoice,
197
206
  appName,
198
207
  appIcon : appIcon ?? `${window.location.origin}/favicon.ico`,
208
+ applicationId,
199
209
  connectServerUrl,
200
210
  relayWalletPath,
201
211
  timeout,
202
212
  theme,
203
213
  mode : params.requestType,
204
214
  delegatePortableDid : params.delegatePortableDid,
215
+ expectedProviderDid : params.expectedProviderDid,
205
216
  permissionRequests : params.permissionRequests,
206
217
  });
207
218
  },
@@ -66,6 +66,9 @@ export interface RelayConnectOptions {
66
66
  /** Optional icon URL for the app, shown in the wallet consent UI. */
67
67
  appIcon?: string;
68
68
 
69
+ /** Stable application identifier hint available to the wallet during approval. */
70
+ applicationId?: string;
71
+
69
72
  /** Optional client/environment metadata for wallet session display. */
70
73
  clientMetadata?: ConnectClientMetadata;
71
74
 
@@ -78,6 +81,9 @@ export interface RelayConnectOptions {
78
81
  /** User-facing request purpose. Absent means a normal connect. */
79
82
  requestType?: ConnectRequestType;
80
83
 
84
+ /** Wallet profile DID that a refresh must renew. */
85
+ expectedProviderDid?: string;
86
+
81
87
  /** Total milliseconds to poll the relay for a wallet response. */
82
88
  timeoutMs?: number;
83
89
 
@@ -193,10 +199,12 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
193
199
  clientDid : clientDid.uri,
194
200
  appName : options.appName,
195
201
  appIcon : options.appIcon,
202
+ applicationId : options.applicationId,
196
203
  clientMetadata : options.clientMetadata,
197
204
  permissionRequests : options.permissionRequests,
198
205
  delegateDid : options.delegatePortableDid?.uri,
199
206
  requestType : options.requestType,
207
+ expectedProviderDid : options.expectedProviderDid,
200
208
  supportedDidMethods : ['did:dht', 'did:jwk'],
201
209
  nonce,
202
210
  state,
@@ -243,6 +251,7 @@ export async function runRelayConnect(options: RelayConnectOptions): Promise<Con
243
251
  nonce,
244
252
  state,
245
253
  pin,
254
+ expectedProviderDid : options.expectedProviderDid,
246
255
  delegatePortableDid : options.delegatePortableDid,
247
256
  confirmComplete : transport.confirmComplete?.bind(transport),
248
257
  });
@@ -274,6 +283,7 @@ async function attemptPinOpen(params: {
274
283
  nonce: string;
275
284
  state: string;
276
285
  pin: string;
286
+ expectedProviderDid: string | undefined;
277
287
  delegatePortableDid: PortableDid | undefined;
278
288
  confirmComplete: (() => Promise<void>) | undefined;
279
289
  }): Promise<PinAttemptOutcome> {
@@ -281,8 +291,13 @@ async function attemptPinOpen(params: {
281
291
  const response = await openResponse({
282
292
  jwe : params.responseCiphertext,
283
293
  recipientPrivateKey : params.responsePrivateKey,
284
- expected : { clientDid: params.clientDid, nonce: params.nonce, state: params.state },
285
- pin : params.pin,
294
+ expected : {
295
+ clientDid : params.clientDid,
296
+ nonce : params.nonce,
297
+ state : params.state,
298
+ providerDid : params.expectedProviderDid,
299
+ },
300
+ pin: params.pin,
286
301
  });
287
302
 
288
303
  const delegatePortableDid = resolveDelegatePortableDid({
@@ -59,6 +59,12 @@ export type ConnectMethod = 'phone' | 'browser';
59
59
  /** localStorage key remembering the last successful method + wallet. */
60
60
  const LAST_CHOICE_STORAGE_KEY = 'enbox:connect:lastChoice';
61
61
 
62
+ /** localStorage key remembering the exact wallet route for each delegate/profile pair. */
63
+ const SESSION_CHOICES_STORAGE_KEY = 'enbox:connect:sessionChoices';
64
+
65
+ /** Bounds local reconnect provenance retained by one dapp origin. */
66
+ const MAX_SESSION_CHOICES = 20;
67
+
62
68
  /**
63
69
  * Safety margin subtracted from the relay pointer TTL before re-minting.
64
70
  *
@@ -168,8 +174,9 @@ export interface ConnectModalOptions {
168
174
  preferredMethod?: ConnectMethod;
169
175
 
170
176
  /**
171
- * Remember the successful method + wallet in localStorage and pre-shape
172
- * the next session accordingly.
177
+ * Remember the successful method + wallet in localStorage. Refreshes reuse
178
+ * the route saved for their delegate DID; new connections use the latest
179
+ * successful route as a convenience.
173
180
  * @default true
174
181
  */
175
182
  rememberChoice?: boolean;
@@ -180,6 +187,15 @@ export interface ConnectModalOptions {
180
187
  /** Icon URL of the requesting application. */
181
188
  appIcon?: string;
182
189
 
190
+ /**
191
+ * Stable application identifier hint available to the wallet during approval.
192
+ * Defaults to the browser's canonical origin.
193
+ */
194
+ applicationId?: string;
195
+
196
+ /** Wallet profile DID that a refresh must renew. */
197
+ expectedProviderDid?: string;
198
+
183
199
  /**
184
200
  * Relay base URL override for the phone path. When omitted, each wallet's
185
201
  * `connectServerUrl` is discovered from its `/.well-known/enbox-connect`
@@ -216,15 +232,7 @@ export interface ConnectModalDeps {
216
232
  runRelay: typeof runRelayConnect;
217
233
 
218
234
  /** Runs one popup handshake (browser path); must open the popup synchronously. */
219
- runPopup: (options: {
220
- walletUrl: string;
221
- permissionRequests: ConnectPermissionRequest[];
222
- appName: string;
223
- appIcon?: string;
224
- timeoutMs?: number;
225
- delegatePortableDid?: PortableDid;
226
- requestType?: ConnectRequestType;
227
- }) => Promise<ConnectResult | undefined>;
235
+ runPopup: typeof runPopupConnect;
228
236
 
229
237
  /** Resolves a wallet origin to its relay `connectServerUrl` (or undefined). */
230
238
  discoverConnectServerUrl: (walletOrigin: string) => Promise<string | undefined>;
@@ -254,9 +262,11 @@ async function runPopupConnect(options: {
254
262
  permissionRequests: ConnectPermissionRequest[];
255
263
  appName: string;
256
264
  appIcon?: string;
265
+ applicationId?: string;
257
266
  timeoutMs?: number;
258
267
  delegatePortableDid?: PortableDid;
259
268
  requestType?: ConnectRequestType;
269
+ expectedProviderDid?: string;
260
270
  }): Promise<ConnectResult | undefined> {
261
271
  // The transport constructor calls `window.open` synchronously — callers
262
272
  // must invoke this inside the user-gesture call stack.
@@ -269,10 +279,12 @@ async function runPopupConnect(options: {
269
279
  return await client.connect({
270
280
  appName : options.appName,
271
281
  appIcon : options.appIcon,
282
+ applicationId : options.applicationId,
272
283
  clientMetadata : collectBrowserClientMetadata(),
273
284
  permissionRequests : options.permissionRequests,
274
285
  delegatePortableDid : options.delegatePortableDid,
275
286
  requestType : options.requestType,
287
+ expectedProviderDid : options.expectedProviderDid,
276
288
  });
277
289
  }
278
290
 
@@ -301,6 +313,12 @@ interface LastChoice {
301
313
  walletUrl: string;
302
314
  }
303
315
 
316
+ /** Stable reconnect provenance for one delegated app session. */
317
+ interface SessionChoice extends LastChoice {
318
+ delegateDid: string;
319
+ providerDid: string;
320
+ }
321
+
304
322
  function readLastChoice(storage: ConnectModalDeps['storage']): LastChoice | undefined {
305
323
  try {
306
324
  const raw = storage?.getItem(LAST_CHOICE_STORAGE_KEY);
@@ -321,6 +339,78 @@ function writeLastChoice(storage: ConnectModalDeps['storage'], choice: LastChoic
321
339
  } catch { /* best effort */ }
322
340
  }
323
341
 
342
+ function isSessionChoice(value: unknown): value is SessionChoice {
343
+ if (typeof value !== 'object' || value === null) {
344
+ return false;
345
+ }
346
+ const choice = value as Partial<SessionChoice>;
347
+ return (choice.method === 'phone' || choice.method === 'browser')
348
+ && isValidRememberedWalletUrl(choice.walletUrl)
349
+ && typeof choice.delegateDid === 'string'
350
+ && choice.delegateDid.length > 0
351
+ && typeof choice.providerDid === 'string'
352
+ && choice.providerDid.length > 0;
353
+ }
354
+
355
+ function readSessionChoices(storage: ConnectModalDeps['storage']): SessionChoice[] {
356
+ try {
357
+ const raw = storage?.getItem(SESSION_CHOICES_STORAGE_KEY);
358
+ if (raw === null || raw === undefined) {
359
+ return [];
360
+ }
361
+ const parsed = JSON.parse(raw) as unknown;
362
+ return Array.isArray(parsed) ? parsed.filter(isSessionChoice).slice(-MAX_SESSION_CHOICES) : [];
363
+ } catch {
364
+ return [];
365
+ }
366
+ }
367
+
368
+ function readSessionChoice(
369
+ storage: ConnectModalDeps['storage'],
370
+ delegateDid: string | undefined,
371
+ expectedProviderDid: string | undefined,
372
+ ): SessionChoice | undefined {
373
+ if (delegateDid === undefined) {
374
+ return undefined;
375
+ }
376
+ const choices = readSessionChoices(storage);
377
+ for (let index = choices.length - 1; index >= 0; index--) {
378
+ const choice = choices[index];
379
+ if (choice.delegateDid === delegateDid
380
+ && (expectedProviderDid === undefined || choice.providerDid === expectedProviderDid)) {
381
+ return choice;
382
+ }
383
+ }
384
+ return undefined;
385
+ }
386
+
387
+ function writeSessionChoice(storage: ConnectModalDeps['storage'], choice: SessionChoice): void {
388
+ try {
389
+ const choices = readSessionChoices(storage).filter(entry =>
390
+ entry.delegateDid !== choice.delegateDid || entry.providerDid !== choice.providerDid
391
+ );
392
+ choices.push(choice);
393
+ storage?.setItem(SESSION_CHOICES_STORAGE_KEY, JSON.stringify(choices.slice(-MAX_SESSION_CHOICES)));
394
+ } catch { /* best effort */ }
395
+ }
396
+
397
+ function removeSessionChoice(
398
+ storage: ConnectModalDeps['storage'],
399
+ delegateDid: string,
400
+ providerDid: string,
401
+ ): void {
402
+ try {
403
+ const choices = readSessionChoices(storage).filter(choice =>
404
+ choice.delegateDid !== delegateDid || choice.providerDid !== providerDid
405
+ );
406
+ if (choices.length === 0) {
407
+ storage?.removeItem(SESSION_CHOICES_STORAGE_KEY);
408
+ } else {
409
+ storage?.setItem(SESSION_CHOICES_STORAGE_KEY, JSON.stringify(choices));
410
+ }
411
+ } catch { /* best effort */ }
412
+ }
413
+
324
414
  /** One cancellable relay session. */
325
415
  interface RelaySession {
326
416
  cancelled: Promise<never>;
@@ -379,24 +469,37 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
379
469
  const wallets = dedupeWalletsByUrl(options.wallets ?? []);
380
470
  const rememberChoice = options.rememberChoice ?? true;
381
471
  const appName = options.appName ?? window.location.host;
382
- const relayWalletPath = options.relayWalletPath ?? '/connect/app';
472
+ const applicationId = options.applicationId ?? window.location.origin;
383
473
  const isMobile = deps.isMobile();
384
474
  const refreshing = options.mode === 'refresh';
385
475
 
386
476
  const lastChoice = rememberChoice ? readLastChoice(deps.storage) : undefined;
477
+ let savedRoute = rememberChoice && refreshing
478
+ ? readSessionChoice(
479
+ deps.storage,
480
+ options.delegatePortableDid?.uri,
481
+ options.expectedProviderDid,
482
+ )
483
+ : undefined;
484
+ const expectedProviderDid = options.expectedProviderDid ?? savedRoute?.providerDid;
485
+ const relayWalletPath = options.relayWalletPath ?? '/connect/app';
486
+ const savedRouteEscapeLabel = options.walletUrl === undefined
487
+ ? 'Choose another wallet or method'
488
+ : 'Choose another method';
387
489
 
388
- // Wallet resolution order: explicit option → remembered → catalog head.
490
+ // Refresh routes resolve by delegate first; general preferences remain a
491
+ // best-effort fallback for sessions created before route-aware storage.
389
492
  const rememberedWalletUrl = lastChoice !== undefined
390
493
  && (refreshing || walletInCatalog(wallets, lastChoice.walletUrl))
391
494
  ? lastChoice.walletUrl
392
495
  : undefined;
393
- const lockedWallet = options.walletUrl !== undefined || (refreshing && rememberedWalletUrl !== undefined);
394
- let walletUrl = options.walletUrl
496
+ let walletUrl = savedRoute?.walletUrl
497
+ ?? options.walletUrl
395
498
  ?? rememberedWalletUrl
396
499
  ?? wallets[0]?.url;
397
500
 
398
- // Method resolution order: explicit option → remembered → phone.
399
- let method: ConnectMethod = options.preferredMethod ?? lastChoice?.method ?? 'phone';
501
+ // An exact refresh route wins over generic handler and global preferences.
502
+ let method: ConnectMethod = savedRoute?.method ?? options.preferredMethod ?? lastChoice?.method ?? 'phone';
400
503
 
401
504
  return new Promise<ConnectResult | undefined>((resolve, reject) => {
402
505
  // ── Host + shadow root ─────────────────────────────────────
@@ -517,8 +620,14 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
517
620
  };
518
621
 
519
622
  const succeed = (result: ConnectResult): void => {
520
- if (rememberChoice) {
521
- writeLastChoice(deps.storage, { method, walletUrl: walletUrl ?? '' });
623
+ if (rememberChoice && walletUrl !== undefined) {
624
+ writeLastChoice(deps.storage, { method, walletUrl });
625
+ writeSessionChoice(deps.storage, {
626
+ delegateDid : result.delegatePortableDid.uri,
627
+ providerDid : result.connectedDid,
628
+ method,
629
+ walletUrl,
630
+ });
522
631
  }
523
632
  renderConnected();
524
633
  setTimeout(settleWith(settle, () => resolve(result)), 1_200);
@@ -703,15 +812,47 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
703
812
  );
704
813
  };
705
814
 
815
+ const escapeSavedRoute = (): void => {
816
+ if (savedRoute === undefined) {
817
+ return;
818
+ }
819
+
820
+ relaySession?.cancel();
821
+ relaySession = undefined;
822
+ clearRemint();
823
+ removeSessionChoice(deps.storage, savedRoute.delegateDid, savedRoute.providerDid);
824
+ savedRoute = undefined;
825
+ if (options.walletUrl !== undefined) {
826
+ walletUrl = options.walletUrl;
827
+ }
828
+ renderFooter();
829
+ setStage(
830
+ el('p', 'stage-caption', 'Choose another way to reconnect.'),
831
+ el('p', 'stage-subline', 'We’ll still require the same wallet profile used for this session.'),
832
+ );
833
+ };
834
+
706
835
  const renderError = (message: string, error?: Error): void => {
707
836
  setStage(
708
837
  el('p', 'stage-caption', message),
709
838
  stageButton('Start over', () => { void startMethod(); }),
839
+ ...(savedRoute !== undefined
840
+ ? [stageLinkButton(savedRouteEscapeLabel, escapeSavedRoute)]
841
+ : []),
710
842
  stageLinkButton('Close', settleWith(settle, () => reject(error ?? new Error(`[@enbox/browser] ${message}`)))),
711
843
  );
712
844
  };
713
845
 
714
846
  const renderPhoneUnavailable = (): void => {
847
+ if (savedRoute !== undefined) {
848
+ setStage(
849
+ el('p', 'stage-caption', 'Your saved wallet connection is unavailable right now.'),
850
+ el('p', 'stage-subline', 'Try again in a moment to reconnect the same session.'),
851
+ stageButton('Try again', () => { void startPhone(); }),
852
+ stageLinkButton(savedRouteEscapeLabel, escapeSavedRoute),
853
+ );
854
+ return;
855
+ }
715
856
  setStage(
716
857
  el('p', 'stage-caption', 'This wallet can’t connect by phone here.'),
717
858
  el('p', 'stage-subline', 'You can continue in this browser instead.'),
@@ -724,12 +865,24 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
724
865
  const popupSurface = isMobile ? 'tab' : 'window';
725
866
 
726
867
  const renderPopupPrompt = (): void => {
868
+ const walletName = walletUrl !== undefined ? walletByUrl(walletUrl).name : 'your wallet';
869
+ const exactReconnect = savedRoute !== undefined;
870
+ const connectDescription = isMobile
871
+ ? 'Your wallet opens in a new tab to approve this connection — no code needed.'
872
+ : 'Your wallet opens in a small window to approve this connection.';
873
+ const description = exactReconnect
874
+ ? `We’ll open the same ${walletName} profile used for this session.`
875
+ : connectDescription;
876
+ const connectButtonLabel = isMobile ? 'Open wallet' : 'Open wallet window';
877
+ const buttonLabel = exactReconnect
878
+ ? `Open ${walletName} ${popupSurface}`
879
+ : connectButtonLabel;
727
880
  setStage(
728
- el('p', 'stage-caption', 'Connect with a wallet in this browser'),
729
- el('p', 'stage-subline', isMobile
730
- ? 'Your wallet opens in a new tab to approve this connection — no code needed.'
731
- : 'Your wallet opens in a small window to approve this connection.'),
732
- stageButton(isMobile ? 'Open wallet' : 'Open wallet window', () => { startPopup(); }),
881
+ el('p', 'stage-caption', exactReconnect
882
+ ? `Reconnect with ${walletName}`
883
+ : 'Connect with a wallet in this browser'),
884
+ el('p', 'stage-subline', description),
885
+ stageButton(buttonLabel, () => { startPopup(); }),
733
886
  );
734
887
  };
735
888
 
@@ -747,7 +900,9 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
747
900
  ? `Your browser blocked the wallet ${popupSurface}.`
748
901
  : `The wallet ${popupSurface} was closed.`),
749
902
  stageButton(blocked ? 'Open it now' : `Reopen ${popupSurface}`, () => { startPopup(); }),
750
- stageLinkButton(isMobile ? 'Use a code instead' : 'Use your phone instead', () => { void switchMethod('phone'); }),
903
+ ...(savedRoute !== undefined
904
+ ? [stageLinkButton(savedRouteEscapeLabel, escapeSavedRoute)]
905
+ : [stageLinkButton(isMobile ? 'Use a code instead' : 'Use your phone instead', () => { void switchMethod('phone'); })]),
751
906
  );
752
907
  };
753
908
 
@@ -833,10 +988,12 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
833
988
  walletUri : new URL(relayWalletPath, origin).toString(),
834
989
  appName,
835
990
  appIcon : options.appIcon,
991
+ applicationId,
836
992
  clientMetadata : collectBrowserClientMetadata(),
837
993
  permissionRequests : options.permissionRequests,
838
994
  delegatePortableDid : options.delegatePortableDid,
839
995
  requestType : options.mode,
996
+ expectedProviderDid,
840
997
  timeoutMs : options.timeout,
841
998
  cancelled : session.cancelled,
842
999
  onWalletUriReady : (handoff): void => {
@@ -907,9 +1064,11 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
907
1064
  permissionRequests : options.permissionRequests,
908
1065
  appName,
909
1066
  appIcon : options.appIcon,
1067
+ applicationId,
910
1068
  timeoutMs : options.timeout,
911
1069
  delegatePortableDid : options.delegatePortableDid,
912
1070
  requestType : options.mode,
1071
+ expectedProviderDid,
913
1072
  });
914
1073
  if (settled) { return; }
915
1074
  if (result === undefined) {
@@ -945,7 +1104,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
945
1104
  const renderFooter = (): void => {
946
1105
  footer.replaceChildren();
947
1106
 
948
- if (!lockedWallet) {
1107
+ if (savedRoute === undefined && options.walletUrl === undefined) {
949
1108
  composeWalletRow();
950
1109
  const switcher = buildWalletSwitcher();
951
1110
  if (switcher.label !== undefined) {
@@ -958,7 +1117,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
958
1117
  const row = document.createElement('div');
959
1118
  row.className = 'footer-row';
960
1119
 
961
- if (method === 'phone') {
1120
+ if (method === 'phone' && savedRoute === undefined) {
962
1121
  const alt = document.createElement('button');
963
1122
  alt.className = 'footer-link method-link';
964
1123
  // On a phone both methods open the wallet on this device — what the
@@ -967,7 +1126,7 @@ export function runConnectModal(options: ConnectModalOptions): Promise<ConnectRe
967
1126
  // startPopup runs synchronously in this click handler.
968
1127
  alt.addEventListener('click', () => { startPopup(); });
969
1128
  row.appendChild(alt);
970
- } else {
1129
+ } else if (method === 'browser' && savedRoute === undefined) {
971
1130
  const alt = document.createElement('button');
972
1131
  alt.className = 'footer-link method-link';
973
1132
  alt.textContent = isMobile ? 'Use a code instead →' : 'Use your phone instead →';