@enbox/browser 0.3.39 → 0.3.40

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.
@@ -1,63 +1,333 @@
1
1
  /**
2
- * DWeb Connect client — initiates the popup/postMessage connect flow.
2
+ * DWeb Connect client — the dapp side of the popup/postMessage connect flow.
3
3
  *
4
- * This is the dapp-side counterpart to the wallet's `/dweb-connect` page.
5
- * The wallet is opened as a popup window, and communication happens via
6
- * `window.postMessage`. No relay server, QR codes, or PINs are needed.
4
+ * {@link PopupClientTransport} implements the `@enbox/connect` kernel's
5
+ * `ConnectTransport` over a wallet popup:
6
+ *
7
+ * 1. The dapp opens `${walletUrl}/dweb-connect` as a popup window.
8
+ * 2. The wallet emits a `loaded` beacon carrying a fresh ephemeral X25519
9
+ * public key and its origin.
10
+ * 3. The kernel seals the signed connect request to that key (ECDH-ES,
11
+ * `apv` bound to the wallet origin) and the transport posts the
12
+ * ciphertext to the popup.
13
+ * 4. The wallet posts back the sealed response JWE (or the deny token),
14
+ * which the kernel opens and value-checks.
15
+ *
16
+ * Only ciphertext crosses the postMessage channel. Every message is checked
17
+ * against the wallet origin derived from the configured wallet URL (never
18
+ * `'*'`), and against the popup window as `event.source`.
7
19
  *
8
20
  * @module
9
21
  */
10
22
 
11
- import type { PortableDid } from '@enbox/dids';
12
- import type { ConnectClientMetadata, ConnectPermissionRequest, DwnDataEncodedRecordsWriteMessage } from '@enbox/agent';
13
- import type { ConnectResult, PortableIdentity } from '@enbox/auth';
23
+ import type { Jwk } from '@enbox/crypto';
24
+ import type {
25
+ ConnectClientMetadata,
26
+ ConnectPermissionRequest,
27
+ ConnectRequestProfile,
28
+ ConnectResult,
29
+ ConnectTransport,
30
+ } from '@enbox/connect';
31
+
32
+ import { assertX25519PublicJwk, CONNECT_DENIED_TOKEN, ConnectClient } from '@enbox/connect';
33
+
34
+ import {
35
+ DWEB_CONNECT_LOADED_MESSAGE_TYPE,
36
+ DWEB_CONNECT_PATH,
37
+ DWEB_CONNECT_REQUEST_MESSAGE_TYPE,
38
+ DWEB_CONNECT_RESPONSE_MESSAGE_TYPE,
39
+ getTrustedMessage,
40
+ } from './dweb-connect-messages.js';
41
+
42
+ /** Default budget for the whole popup handshake (beacon + user consent + response). */
43
+ const DEFAULT_TIMEOUT_MS = 300_000;
44
+
45
+ /** Interval at which the transport checks whether the user closed the popup. */
46
+ const CLOSED_POLL_INTERVAL_MS = 500;
47
+
48
+ /** Window name given to the wallet popup. */
49
+ const POPUP_WINDOW_NAME = 'enbox-dweb-connect';
14
50
 
15
- import type { EncryptedPostMessagePayload } from './dweb-connect-crypto.js';
16
- import { decryptPostMessagePayload, generateEphemeralKeyPair } from './dweb-connect-crypto.js';
51
+ /** Window features given to the wallet popup. */
52
+ const POPUP_WINDOW_FEATURES = 'width=500,height=700,menubar=no,toolbar=no,location=no,status=no';
53
+
54
+ /** Guards against calling browser-only entry points outside a browser. */
55
+ function assertBrowserEnvironment(): void {
56
+ if (typeof window === 'undefined') {
57
+ throw new Error('[@enbox/browser] DWeb Connect is only available in browser environments.');
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Thrown when the user closes the wallet popup before the wallet finished
63
+ * loading (i.e. before its `loaded` beacon arrived). Callers typically treat
64
+ * this the same as a user denial.
65
+ */
66
+ export class PopupWindowClosedError extends Error {
67
+ constructor() {
68
+ super('[@enbox/browser] Wallet popup was closed before the wallet finished loading.');
69
+ this.name = 'PopupWindowClosedError';
70
+ }
71
+ }
17
72
 
18
- /** Options for initiating a DWeb Connect flow via popup. */
19
- export interface DWebConnectClientOptions {
20
- /** Base URL of the wallet app (e.g. "https://enbox-wallet.pages.dev"). */
73
+ /** Options for constructing a {@link PopupClientTransport}. */
74
+ export type PopupClientTransportOptions = {
75
+ /** Base URL of the wallet app (e.g. `https://enbox-wallet.pages.dev`). */
21
76
  walletUrl: string;
22
77
 
23
- /** The DID to pre-select in the wallet's identity picker. */
24
- did?: string;
78
+ /**
79
+ * Total budget in milliseconds for the popup handshake. The popup is closed
80
+ * and the handshake rejected when the wallet has not responded in time.
81
+ * @default 300_000 (5 minutes)
82
+ */
83
+ timeoutMs?: number;
84
+ };
85
+
86
+ /**
87
+ * Popup/postMessage {@link ConnectTransport} for browser dapps.
88
+ *
89
+ * Single-use: construct a new transport per handshake. The **constructor**
90
+ * opens the wallet popup synchronously — so `window.open` runs inside the user
91
+ * gesture, ahead of the kernel's awaited key generation, and survives strict
92
+ * popup blockers; `requestProfile()` then awaits the wallet's `loaded` beacon;
93
+ * `deliverRequest()` posts the sealed request ciphertext; `awaitResponse()`
94
+ * resolves with the sealed response JWE, or the deny token when the user denies
95
+ * or closes the popup after the handshake was established.
96
+ *
97
+ * Security properties:
98
+ * - `targetOrigin` of every outbound `postMessage` is pinned to the origin
99
+ * derived from `walletUrl` — never `'*'`.
100
+ * - Inbound messages are dropped unless `event.origin` equals the pinned
101
+ * wallet origin AND `event.source` is the popup window this transport opened.
102
+ * - The beacon's `walletOrigin` field must equal the pinned origin, and its
103
+ * `walletEpk` must be a public-only X25519 JWK; a malformed beacon from the
104
+ * genuine popup fails the handshake closed.
105
+ */
106
+ export class PopupClientTransport implements ConnectTransport {
107
+ /** {@inheritDoc ConnectTransport.requiresPin} */
108
+ public readonly requiresPin: boolean = false;
109
+
110
+ private readonly _walletOrigin: string;
111
+ private readonly _timeoutMs: number;
112
+
113
+ private readonly _popup: Window;
114
+ private _settled = false;
115
+ private _walletEpkReceived = false;
116
+ private _timeoutId?: ReturnType<typeof setTimeout>;
117
+ private _closedPollId?: ReturnType<typeof setInterval>;
118
+ private _messageListener?: (event: MessageEvent) => void;
119
+
120
+ private readonly _walletEpkPromise: Promise<Jwk>;
121
+ private _resolveWalletEpk?: (walletEpk: Jwk) => void;
122
+ private _rejectWalletEpk?: (error: Error) => void;
123
+
124
+ private readonly _responsePromise: Promise<string>;
125
+ private _resolveResponse?: (payload: string) => void;
126
+ private _rejectResponse?: (error: Error) => void;
127
+
128
+ constructor(options: PopupClientTransportOptions) {
129
+ assertBrowserEnvironment();
130
+
131
+ this._walletOrigin = new URL(options.walletUrl).origin;
132
+ this._timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
133
+
134
+ // Open the wallet popup SYNCHRONOUSLY, in the user-gesture call stack,
135
+ // before the kernel's awaited `DidJwk.create()` / `X25519.generateKey()`
136
+ // run. Strict popup blockers (Safari) reject a `window.open` issued once
137
+ // the stack has yielded to a prior `await`, so opening it here — instead of
138
+ // inside the awaited `requestProfile()` — is what keeps the flow usable.
139
+ const popupUrl = new URL(DWEB_CONNECT_PATH, options.walletUrl).toString();
140
+ const popup = window.open(popupUrl, POPUP_WINDOW_NAME, POPUP_WINDOW_FEATURES);
141
+ if (!popup) {
142
+ throw new Error('[@enbox/browser] Popup blocked. Allow popups for this site to connect to a wallet.');
143
+ }
144
+ this._popup = popup;
145
+
146
+ this._walletEpkPromise = new Promise<Jwk>((resolve, reject): void => {
147
+ this._resolveWalletEpk = resolve;
148
+ this._rejectWalletEpk = reject;
149
+ });
150
+
151
+ // Created up-front so a response (or deny) arriving before
152
+ // `awaitResponse()` is called is never dropped. The no-op catch marks
153
+ // early rejections (timeout, popup closed during load) as handled until
154
+ // the kernel attaches its own handlers via `awaitResponse()`.
155
+ this._responsePromise = new Promise<string>((resolve, reject): void => {
156
+ this._resolveResponse = resolve;
157
+ this._rejectResponse = reject;
158
+ });
159
+ this._responsePromise.catch((): undefined => undefined);
160
+
161
+ // Listen synchronously, before the wallet page can emit its `loaded`
162
+ // beacon, so the beacon is never missed while the kernel awaits key
163
+ // generation ahead of `requestProfile()`.
164
+ this._messageListener = (event: MessageEvent): void => { this.onMessage(event); };
165
+ window.addEventListener('message', this._messageListener);
166
+
167
+ this._timeoutId = setTimeout((): void => {
168
+ this.fail(new Error('[@enbox/browser] DWeb Connect timed out waiting for wallet response.'));
169
+ }, this._timeoutMs);
170
+
171
+ this._closedPollId = setInterval((): void => { this.onClosedPoll(); }, CLOSED_POLL_INTERVAL_MS);
172
+ }
173
+
174
+ /**
175
+ * Awaits the already-open popup's `loaded` beacon, then returns the popup
176
+ * channel profile: ECDH-ES encryption to the beacon's ephemeral key with the
177
+ * pinned wallet origin as the `apv` binding, a `post_message` reply
178
+ * descriptor, and the client-supplied `state` correlator.
179
+ *
180
+ * The popup was opened synchronously at construction, so this method only
181
+ * awaits the beacon — it never issues the blocker-sensitive `window.open`.
182
+ *
183
+ * @param state - The handshake correlator minted by the {@link ConnectClient}.
184
+ * @throws If the user closes the popup before the beacon arrives
185
+ * ({@link PopupWindowClosedError}), the beacon is malformed, or on
186
+ * timeout.
187
+ */
188
+ public async requestProfile(state: string): Promise<ConnectRequestProfile> {
189
+ const walletEpk = await this._walletEpkPromise;
190
+
191
+ return {
192
+ encryption : { mode: 'ecdh-es', walletEpk, walletOrigin: this._walletOrigin },
193
+ reply : { mode: 'post_message' },
194
+ state,
195
+ };
196
+ }
25
197
 
26
198
  /**
27
- * Protocol permission requests to send to the wallet.
28
- * Each entry contains a `protocolDefinition` and `permissionScopes`.
199
+ * Posts the sealed request ciphertext to the wallet popup with the
200
+ * `targetOrigin` pinned to the wallet origin.
29
201
  */
202
+ public async deliverRequest(jwe: string): Promise<void> {
203
+ this._popup.postMessage({ type: DWEB_CONNECT_REQUEST_MESSAGE_TYPE, jwe }, this._walletOrigin);
204
+ }
205
+
206
+ /**
207
+ * Awaits the wallet's response ciphertext: the sealed response JWE, or the
208
+ * deny token when the user denies in the wallet or closes the popup.
209
+ * Rejects on timeout.
210
+ */
211
+ public async awaitResponse(): Promise<string> {
212
+ return await this._responsePromise;
213
+ }
214
+
215
+ private onMessage(event: MessageEvent): void {
216
+ const message = getTrustedMessage(event, this._walletOrigin, this._popup);
217
+ if (message === undefined) { return; }
218
+
219
+ if (message.type === DWEB_CONNECT_LOADED_MESSAGE_TYPE) {
220
+ this.onLoadedBeacon(message);
221
+ return;
222
+ }
223
+
224
+ if (message.type === DWEB_CONNECT_RESPONSE_MESSAGE_TYPE) {
225
+ this.onResponseMessage(message);
226
+ }
227
+ }
228
+
229
+ private onLoadedBeacon(message: Record<string, unknown>): void {
230
+ if (this._settled || this._walletEpkReceived) { return; }
231
+
232
+ // The beacon passed the origin and source checks, so a malformed payload
233
+ // is a protocol violation by the genuine popup — fail closed. The X25519
234
+ // public-JWK shape check is delegated to the connect kernel.
235
+ let walletEpk: Jwk;
236
+ try {
237
+ assertX25519PublicJwk(message.walletEpk);
238
+ walletEpk = message.walletEpk;
239
+ } catch {
240
+ this.fail(new Error('[@enbox/browser] Wallet loaded beacon did not carry a valid X25519 public key.'));
241
+ return;
242
+ }
243
+
244
+ if (message.walletOrigin !== this._walletOrigin) {
245
+ this.fail(new Error('[@enbox/browser] Wallet loaded beacon origin does not match the configured wallet origin.'));
246
+ return;
247
+ }
248
+
249
+ this._walletEpkReceived = true;
250
+ this._resolveWalletEpk?.(walletEpk);
251
+ }
252
+
253
+ private onResponseMessage(message: Record<string, unknown>): void {
254
+ if (this._settled) { return; }
255
+
256
+ if (typeof message.payload !== 'string') {
257
+ this.fail(new Error('[@enbox/browser] Wallet response message did not carry a ciphertext payload.'));
258
+ return;
259
+ }
260
+
261
+ this._settled = true;
262
+ this.cleanup();
263
+ // No-op on the normal path; unblocks `requestProfile()` if a response
264
+ // somehow arrives before the beacon.
265
+ this._rejectWalletEpk?.(new Error('[@enbox/browser] Wallet responded before the handshake was established.'));
266
+ this._resolveResponse?.(message.payload);
267
+ }
268
+
269
+ private onClosedPoll(): void {
270
+ if (this._settled || this._popup.closed !== true) { return; }
271
+
272
+ if (!this._walletEpkReceived) {
273
+ // Closed before the wallet ever loaded — the handshake cannot proceed.
274
+ this.fail(new PopupWindowClosedError());
275
+ return;
276
+ }
277
+
278
+ // Closed after the handshake was established — treat as a user denial.
279
+ this._settled = true;
280
+ this.cleanup();
281
+ this._resolveResponse?.(CONNECT_DENIED_TOKEN);
282
+ }
283
+
284
+ private fail(error: Error): void {
285
+ if (this._settled) { return; }
286
+ this._settled = true;
287
+ this.cleanup();
288
+ try { this._popup.close(); } catch { /* best effort */ }
289
+ this._rejectWalletEpk?.(error);
290
+ this._rejectResponse?.(error);
291
+ }
292
+
293
+ private cleanup(): void {
294
+ clearTimeout(this._timeoutId);
295
+ clearInterval(this._closedPollId);
296
+ if (this._messageListener !== undefined) {
297
+ window.removeEventListener('message', this._messageListener);
298
+ this._messageListener = undefined;
299
+ }
300
+ }
301
+ }
302
+
303
+ /** Options for a one-shot {@link connectViaPopup} flow. */
304
+ export type PopupConnectOptions = {
305
+ /** Base URL of the wallet app (e.g. `https://enbox-wallet.pages.dev`). */
306
+ walletUrl: string;
307
+
308
+ /** DWN protocols and permission scopes to request from the wallet. */
30
309
  permissionRequests: ConnectPermissionRequest[];
31
310
 
32
311
  /**
33
- * Timeout in milliseconds. The popup is closed and an error thrown
34
- * if the wallet doesn't respond within this time.
312
+ * Timeout in milliseconds for the popup handshake.
35
313
  * @default 300_000 (5 minutes)
36
314
  */
37
315
  timeout?: number;
38
316
 
39
317
  /**
40
- * Display name of the requesting application.
41
- * Shown in the wallet's permission consent screen.
318
+ * Display name of the requesting application, shown in the wallet's
319
+ * consent screen. Defaults to the dapp's host.
42
320
  */
43
321
  appName?: string;
44
322
 
45
323
  /**
46
- * Icon URL of the requesting application.
47
- * Shown alongside the app name in the wallet's consent screen.
324
+ * Icon URL of the requesting application, shown alongside the app name in
325
+ * the wallet's consent screen.
48
326
  */
49
327
  appIcon?: string;
328
+ };
50
329
 
51
- /**
52
- * A portable identity to transfer to the wallet for import.
53
- *
54
- * When provided, the wallet imports this identity and creates
55
- * delegate grants for the dapp, allowing the dapp to transition
56
- * from local-DID mode to delegate mode while keeping the same DID.
57
- */
58
- portableIdentity?: PortableIdentity;
59
- }
60
-
330
+ /** Collects self-reported environment hints for the wallet's session display. */
61
331
  function collectBrowserClientMetadata(): ConnectClientMetadata {
62
332
  const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
63
333
  const languages = Array.isArray(globalThis.navigator.languages)
@@ -75,228 +345,34 @@ function collectBrowserClientMetadata(): ConnectClientMetadata {
75
345
  }
76
346
 
77
347
  /**
78
- * Open a wallet popup and run the DWeb Connect postMessage flow.
348
+ * Runs one DWeb Connect popup handshake end-to-end: opens the wallet popup,
349
+ * drives the `@enbox/connect` kernel client over a {@link PopupClientTransport},
350
+ * and returns the delegated credentials.
79
351
  *
80
- * Protocol:
81
- * 1. Dapp opens `${walletUrl}/dweb-connect` as a popup.
82
- * 2. Wallet sends `{ type: 'dweb-connect-loaded' }` when ready.
83
- * 3. Dapp sends `{ type: 'dweb-connect-authorization-request', did, permissions,
84
- * appName?, appIcon?, portableIdentity? }`.
85
- * 4. Wallet shows consent UI, then sends back
86
- * `{ type: 'dweb-connect-authorization-response', delegateDid, grants }`.
87
- * 5. Popup closes.
88
- *
89
- * @returns The delegate credentials, or `undefined` if the user denied.
90
- * @throws If the popup is blocked, times out, or the wallet returns an error.
352
+ * @returns The delegate credentials, or `undefined` when the user denied the
353
+ * request or closed the popup.
354
+ * @throws If the popup is blocked, the handshake times out, or the wallet
355
+ * returns an invalid response.
91
356
  */
92
- async function initClient(options: DWebConnectClientOptions): Promise<ConnectResult | undefined> {
93
- const {
94
- walletUrl,
95
- did,
96
- permissionRequests,
97
- timeout = 300_000,
98
- appName,
99
- appIcon,
100
- portableIdentity,
101
- } = options;
102
-
103
- if (typeof window === 'undefined') {
104
- throw new Error(
105
- '[@enbox/browser] DWeb Connect is only available in browser environments.'
106
- );
107
- }
108
-
109
- // Open the wallet's DWeb Connect page as a popup.
110
- const popupUrl = new URL('/dweb-connect', walletUrl).toString();
111
- const popup = window.open(
112
- popupUrl,
113
- 'enbox-dweb-connect',
114
- 'width=500,height=700,menubar=no,toolbar=no,location=no,status=no',
115
- );
116
-
117
- if (!popup) {
118
- throw new Error(
119
- '[@enbox/browser] Popup blocked. Allow popups for this site to connect to a wallet.'
120
- );
121
- }
122
-
123
- // Generate an ephemeral ECDH keypair for this connect session.
124
- // The public key is sent to the wallet so it can encrypt the response
125
- // containing delegate private keys and decryption material.
126
- // Encryption is mandatory — if key generation fails (e.g. non-secure
127
- // context where crypto.subtle is unavailable), the connect flow aborts.
128
- let dappKeyPair: CryptoKeyPair;
129
- let dappPublicKeyBase64url: string;
130
-
131
- try {
132
- const ephemeral = await generateEphemeralKeyPair();
133
- dappKeyPair = ephemeral.keyPair;
134
- dappPublicKeyBase64url = ephemeral.publicKeyBase64url;
135
- } catch {
136
- popup.close();
137
- throw new Error(
138
- '[@enbox/browser] DWeb Connect requires a secure context (HTTPS) for encrypted key exchange. '
139
- + 'crypto.subtle is not available in this environment.'
140
- );
141
- }
142
-
143
- return new Promise<ConnectResult | undefined>((resolve, reject) => {
144
- let settled = false;
145
-
146
- const cleanup = (): void => {
147
- settled = true;
148
- clearTimeout(timeoutId);
149
- window.removeEventListener('message', onMessage);
150
- };
151
-
152
- // Set up timeout.
153
- const timeoutId = setTimeout(() => {
154
- if (!settled) {
155
- cleanup();
156
- try { popup.close(); } catch { /* best effort */ }
157
- reject(new Error(
158
- '[@enbox/browser] DWeb Connect timed out waiting for wallet response.'
159
- ));
160
- }
161
- }, timeout);
162
-
163
- // Monitor popup closed without response (user closed the window).
164
- const pollClosed = setInterval(() => {
165
- if (popup.closed && !settled) {
166
- clearInterval(pollClosed);
167
- cleanup();
168
- resolve(undefined);
169
- }
170
- }, 500);
171
-
172
- const onMessage = (event: MessageEvent): void => {
173
- // Validate origin matches the wallet URL.
174
- const walletOrigin = new URL(walletUrl).origin;
175
- if (event.origin !== walletOrigin) {return;}
176
-
177
- const { type } = event.data ?? {};
178
-
179
- if (type === 'dweb-connect-loaded') {
180
- // Wallet is ready — send the authorization request with
181
- // ephemeral public key and optional app metadata.
182
- popup.postMessage({
183
- type : 'dweb-connect-authorization-request',
184
- did,
185
- permissions : permissionRequests,
186
- ephemeralPublicKey : dappPublicKeyBase64url,
187
- appName,
188
- appIcon,
189
- clientMetadata : collectBrowserClientMetadata(),
190
- portableIdentity,
191
- }, walletOrigin);
192
- return;
193
- }
194
-
195
- if (type === 'dweb-connect-authorization-response') {
196
- clearInterval(pollClosed);
197
- cleanup();
198
-
199
- // If the wallet sent an explicit error, treat as denied.
200
- if (event.data.error) {
201
- resolve(undefined);
202
- return;
203
- }
204
-
205
- // Require an encrypted response. The dapp always sends an
206
- // ephemeralPublicKey, so a plaintext response means either the
207
- // wallet is outdated or something intercepted the flow.
208
- const encrypted = event.data.encryptedPayload as EncryptedPostMessagePayload | undefined;
209
- if (!encrypted) {
210
- resolve(undefined);
211
- return;
212
- }
213
-
214
- decryptPostMessagePayload(encrypted, dappKeyPair).then((payload: Record<string, unknown>) => {
215
- const p = payload as any;
216
- if (!p.delegateDid || !p.grants) {
217
- resolve(undefined);
218
- return;
219
- }
220
- resolve({
221
- delegatePortableDid : p.delegateDid as PortableDid,
222
- delegateGrants : p.grants as DwnDataEncodedRecordsWriteMessage[],
223
- connectedDid : p.connectedDid ?? did ?? (p.delegateDid as PortableDid).uri,
224
- sessionRevocations : p.sessionRevocations ?? undefined,
225
- });
226
- }).catch(() => {
227
- // Decryption failed — treat as denied.
228
- resolve(undefined);
229
- });
230
- }
231
- };
232
-
233
- window.addEventListener('message', onMessage);
357
+ export async function connectViaPopup(options: PopupConnectOptions): Promise<ConnectResult | undefined> {
358
+ const transport = new PopupClientTransport({
359
+ walletUrl : options.walletUrl,
360
+ timeoutMs : options.timeout,
234
361
  });
235
- }
362
+ const client = new ConnectClient({ transport });
236
363
 
237
- /**
238
- * Probe whether a wallet supports a specific DID via a hidden iframe.
239
- *
240
- * Sends a `dweb-connect-support-request` message to the wallet and
241
- * waits for a `dweb-connect-support-response`. Useful for checking
242
- * multiple wallet URLs to find the one that manages a given DID.
243
- *
244
- * @param walletUrl - Base URL of the wallet to probe.
245
- * @param did - The DID to check.
246
- * @param timeout - Probe timeout in ms. Default: 5_000 (5 seconds).
247
- * @returns `true` if the wallet manages the DID, `false` otherwise.
248
- */
249
- async function probeWalletSupport(
250
- walletUrl: string,
251
- did: string,
252
- timeout = 5_000,
253
- ): Promise<boolean> {
254
- if (typeof window === 'undefined') {return false;}
255
-
256
- return new Promise<boolean>((resolve) => {
257
- const iframe = document.createElement('iframe');
258
- iframe.style.display = 'none';
259
- iframe.src = walletUrl;
260
-
261
- let settled = false;
262
-
263
- const cleanup = (): void => {
264
- settled = true;
265
- window.removeEventListener('message', onMessage);
266
- try { document.body.removeChild(iframe); } catch { /* best effort */ }
267
- };
268
-
269
- const timeoutId = setTimeout(() => {
270
- if (!settled) {
271
- cleanup();
272
- resolve(false);
273
- }
274
- }, timeout);
275
-
276
- const onMessage = (event: MessageEvent): void => {
277
- const walletOrigin = new URL(walletUrl).origin;
278
- if (event.origin !== walletOrigin) { return; }
279
-
280
- const { type, supported } = event.data ?? {};
281
- if (type === 'dweb-connect-support-response') {
282
- clearTimeout(timeoutId);
283
- cleanup();
284
- resolve(!!supported);
285
- }
286
- };
287
-
288
- window.addEventListener('message', onMessage);
289
-
290
- iframe.addEventListener('load', () => {
291
- const walletOrigin = new URL(walletUrl).origin;
292
- iframe.contentWindow?.postMessage({
293
- type: 'dweb-connect-support-request',
294
- did,
295
- }, walletOrigin);
364
+ try {
365
+ return await client.connect({
366
+ appName : options.appName ?? globalThis.location.host,
367
+ appIcon : options.appIcon,
368
+ clientMetadata : collectBrowserClientMetadata(),
369
+ permissionRequests : options.permissionRequests,
296
370
  });
297
-
298
- document.body.appendChild(iframe);
299
- });
371
+ } catch (error) {
372
+ if (error instanceof PopupWindowClosedError) {
373
+ // The user closed the wallet before it loaded — same as a denial.
374
+ return undefined;
375
+ }
376
+ throw error;
377
+ }
300
378
  }
301
-
302
- export const DWebConnect = { initClient, probeWalletSupport };