@enbox/browser 0.3.43 → 0.3.45

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.
Files changed (48) hide show
  1. package/dist/browser.mjs +161 -155
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/browser-connect-handler.js +17 -20
  4. package/dist/esm/browser-connect-handler.js.map +1 -1
  5. package/dist/esm/dweb-connect-client.js +1 -1
  6. package/dist/esm/dweb-connect-client.js.map +1 -1
  7. package/dist/esm/index.js +4 -1
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/relay-connect-runner.js +128 -0
  10. package/dist/esm/relay-connect-runner.js.map +1 -0
  11. package/dist/esm/ui/connect-modal.js +990 -0
  12. package/dist/esm/ui/connect-modal.js.map +1 -0
  13. package/dist/esm/ui/qr.js +468 -0
  14. package/dist/esm/ui/qr.js.map +1 -0
  15. package/dist/esm/ui/wallet-icon.js +55 -0
  16. package/dist/esm/ui/wallet-icon.js.map +1 -0
  17. package/dist/esm/ui/wallet-well-known.js +61 -0
  18. package/dist/esm/ui/wallet-well-known.js.map +1 -0
  19. package/dist/types/browser-connect-handler.d.ts +32 -7
  20. package/dist/types/browser-connect-handler.d.ts.map +1 -1
  21. package/dist/types/dweb-connect-client.d.ts +3 -1
  22. package/dist/types/dweb-connect-client.d.ts.map +1 -1
  23. package/dist/types/index.d.ts +8 -2
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/relay-connect-runner.d.ts +82 -0
  26. package/dist/types/relay-connect-runner.d.ts.map +1 -0
  27. package/dist/types/ui/connect-modal.d.ts +108 -0
  28. package/dist/types/ui/connect-modal.d.ts.map +1 -0
  29. package/dist/types/ui/qr.d.ts +55 -0
  30. package/dist/types/ui/qr.d.ts.map +1 -0
  31. package/dist/types/ui/wallet-icon.d.ts +13 -0
  32. package/dist/types/ui/wallet-icon.d.ts.map +1 -0
  33. package/dist/types/ui/wallet-well-known.d.ts +33 -0
  34. package/dist/types/ui/wallet-well-known.d.ts.map +1 -0
  35. package/package.json +7 -5
  36. package/src/browser-connect-handler.ts +51 -21
  37. package/src/dweb-connect-client.ts +1 -1
  38. package/src/index.ts +8 -2
  39. package/src/relay-connect-runner.ts +209 -0
  40. package/src/ui/connect-modal.ts +1201 -0
  41. package/src/ui/qr.ts +549 -0
  42. package/src/ui/wallet-icon.ts +63 -0
  43. package/src/ui/wallet-well-known.ts +57 -0
  44. package/dist/esm/ui/wallet-selector.js +0 -585
  45. package/dist/esm/ui/wallet-selector.js.map +0 -1
  46. package/dist/types/ui/wallet-selector.d.ts +0 -44
  47. package/dist/types/ui/wallet-selector.d.ts.map +0 -1
  48. package/src/ui/wallet-selector.ts +0 -658
@@ -10,8 +10,9 @@
10
10
  import type { ConnectPermissionRequest } from '@enbox/connect';
11
11
  import type { ConnectHandler, ConnectResult } from '@enbox/auth';
12
12
 
13
- import { connectViaPopup } from './dweb-connect-client.js';
14
- import { showWalletSelector } from './ui/wallet-selector.js';
13
+ import { runConnectModal } from './ui/connect-modal.js';
14
+
15
+ import type { ConnectMethod } from './ui/connect-modal.js';
15
16
 
16
17
  /** A wallet entry shown in the wallet selector modal. */
17
18
  export interface WalletOption {
@@ -85,17 +86,44 @@ export interface BrowserConnectHandlerOptions {
85
86
  */
86
87
  appIcon?: string;
87
88
 
89
+ /**
90
+ * Preferred connect method on open: `'phone'` (QR / deep link via the
91
+ * relay) or `'browser'` (wallet popup on this device). Overrides the
92
+ * remembered choice.
93
+ * @default 'phone'
94
+ */
95
+ preferredMethod?: ConnectMethod;
96
+
97
+ /**
98
+ * Remember the successful method + wallet in localStorage and pre-shape
99
+ * the next connect session accordingly.
100
+ * @default true
101
+ */
102
+ rememberChoice?: boolean;
103
+
104
+ /**
105
+ * Relay base URL override for the phone path. When omitted, each wallet's
106
+ * `connectServerUrl` is discovered from its `/.well-known/enbox-connect`
107
+ * document.
108
+ */
109
+ connectServerUrl?: string;
110
+
111
+ /**
112
+ * Path appended to the wallet origin to form the phone path's QR /
113
+ * deep-link target.
114
+ * @default '/connect/app'
115
+ */
116
+ relayWalletPath?: string;
88
117
  }
89
118
 
90
119
  /**
91
- * Create a browser-native connect handler using DWeb Connect
92
- * (popup + postMessage).
120
+ * Create a browser-native connect handler over the connect modal.
93
121
  *
94
- * This handler:
95
- * 1. Shows a wallet selector modal (or uses a provided walletUrl).
96
- * 2. Opens the selected wallet as a popup window.
97
- * 3. Runs the DWeb Connect postMessage protocol.
98
- * 4. Returns the delegate credentials on success.
122
+ * The modal owns the whole session on one surface: the phone path (a QR /
123
+ * deep link over the relay, with pairing-code entry) is the default, the
124
+ * in-browser wallet popup is the alternate, and the wallet catalog plus
125
+ * custom-URL entry live in a disclosure. It resolves with the delegate
126
+ * credentials on success and `undefined` when the user denies.
99
127
  *
100
128
  * @example
101
129
  * ```ts
@@ -115,29 +143,31 @@ export function BrowserConnectHandler(
115
143
  ): ConnectHandler {
116
144
  const {
117
145
  wallets = DEFAULT_WALLETS,
118
- walletUrl: fixedWalletUrl,
146
+ walletUrl,
119
147
  timeout,
120
148
  appName,
121
149
  appIcon,
150
+ preferredMethod,
151
+ rememberChoice,
152
+ connectServerUrl,
153
+ relayWalletPath,
122
154
  } = options;
123
155
 
124
156
  return {
125
157
  async requestAccess(params: {
126
158
  permissionRequests: ConnectPermissionRequest[];
127
159
  }): Promise<ConnectResult | undefined> {
128
- // 1. Determine wallet URL.
129
- let walletUrl = fixedWalletUrl;
130
- if (!walletUrl) {
131
- walletUrl = await showWalletSelector(wallets);
132
- }
133
-
134
- // 2. Run the DWeb Connect popup flow over the connect kernel.
135
- return connectViaPopup({
160
+ return runConnectModal({
161
+ wallets,
136
162
  walletUrl,
137
- permissionRequests: params.permissionRequests,
138
- timeout,
163
+ preferredMethod,
164
+ rememberChoice,
139
165
  appName,
140
- appIcon,
166
+ appIcon : appIcon ?? `${window.location.origin}/favicon.ico`,
167
+ connectServerUrl,
168
+ relayWalletPath,
169
+ timeout,
170
+ permissionRequests : params.permissionRequests,
141
171
  });
142
172
  },
143
173
  };
@@ -328,7 +328,7 @@ export type PopupConnectOptions = {
328
328
  };
329
329
 
330
330
  /** Collects self-reported environment hints for the wallet's session display. */
331
- function collectBrowserClientMetadata(): ConnectClientMetadata {
331
+ export function collectBrowserClientMetadata(): ConnectClientMetadata {
332
332
  const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
333
333
  const languages = Array.isArray(globalThis.navigator.languages)
334
334
  ? globalThis.navigator.languages.filter((language) => typeof language === 'string' && language.length > 0)
package/src/index.ts CHANGED
@@ -104,8 +104,14 @@ export type {
104
104
  DWebConnectRequestMessage,
105
105
  DWebConnectResponseMessage,
106
106
  } from './dweb-connect-messages.js';
107
- export type { WalletSelectorOptions } from './ui/wallet-selector.js';
108
- export { probeWalletWellKnown, showWalletSelector, WALLET_WELL_KNOWN_PATH } from './ui/wallet-selector.js';
107
+ export { fetchWalletWellKnown, probeWalletWellKnown, WALLET_WELL_KNOWN_PATH } from './ui/wallet-well-known.js';
108
+ export type { WalletWellKnownDocument } from './ui/wallet-well-known.js';
109
+ export { discoverWalletConnectServerUrl, runConnectModal } from './ui/connect-modal.js';
110
+ export type { ConnectMethod, ConnectModalDeps, ConnectModalOptions } from './ui/connect-modal.js';
111
+ export { MAX_PIN_ATTEMPTS, RelayConnectCancelledError, runRelayConnect } from './relay-connect-runner.js';
112
+ export type { RelayConnectOptions } from './relay-connect-runner.js';
113
+ export { encodeQr, qrToSvg } from './ui/qr.js';
114
+ export type { QrCode, QrSvgOptions } from './ui/qr.js';
109
115
 
110
116
  // ─── Convenience re-exports for dapps ───────────────────────────
111
117
  //
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Relay connect runner — the dapp side of the QR/deep-link connect flow,
3
+ * with interactive PIN retry.
4
+ *
5
+ * This mirrors the `@enbox/connect` kernel `ConnectClient` step-for-step and
6
+ * is composed entirely from the kernel's exported primitives (`sealRequest`,
7
+ * `openResponse`, `RelayClientTransport`); no cryptography is reimplemented
8
+ * here. It exists as a separate orchestrator for one reason the kernel
9
+ * client cannot express today: **PIN retry against the delivered response**.
10
+ * `ConnectClient` collects the PIN once and opens the response once, so a
11
+ * mistyped code fails the whole handshake — the wallet user would have to
12
+ * scan and approve again. The connect modal instead keeps the sealed
13
+ * response and re-runs `openResponse` per attempt, which is safe because
14
+ * opening is a pure decrypt-and-verify step.
15
+ *
16
+ * If the kernel later grows a PIN-attempts seam, this module collapses into
17
+ * `ConnectClient` usage.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ import type { Jwk } from '@enbox/crypto';
23
+ import type {
24
+ ConnectClientMetadata,
25
+ ConnectPermissionRequest,
26
+ ConnectRequest,
27
+ ConnectResult,
28
+ WalletUriHandoff,
29
+ } from '@enbox/connect';
30
+
31
+ import { DidJwk } from '@enbox/dids';
32
+ import { X25519 } from '@enbox/crypto';
33
+ import {
34
+ CONNECT_DENIED_TOKEN,
35
+ openResponse,
36
+ randomToken,
37
+ RelayClientTransport,
38
+ sealRequest,
39
+ } from '@enbox/connect';
40
+
41
+ /** Maximum interactive PIN attempts before the handshake fails. */
42
+ export const MAX_PIN_ATTEMPTS = 6;
43
+
44
+ /** Thrown internally when a relay handshake is cancelled by the UI. */
45
+ export class RelayConnectCancelledError extends Error {
46
+ constructor() {
47
+ super('[@enbox/browser] Relay connect cancelled.');
48
+ this.name = 'RelayConnectCancelledError';
49
+ }
50
+ }
51
+
52
+ /** Options for {@link runRelayConnect}. */
53
+ export interface RelayConnectOptions {
54
+ /** Relay base URL (the wallet's `connectServerUrl`). */
55
+ connectServerUrl: string;
56
+
57
+ /** Wallet URI the request pointer is attached to (QR / deep-link target). */
58
+ walletUri: string;
59
+
60
+ /** The user-friendly name of the app, shown in the wallet consent UI. */
61
+ appName: string;
62
+
63
+ /** Optional icon URL for the app, shown in the wallet consent UI. */
64
+ appIcon?: string;
65
+
66
+ /** Optional client/environment metadata for wallet session display. */
67
+ clientMetadata?: ConnectClientMetadata;
68
+
69
+ /** DWN protocols and permission scopes being requested. */
70
+ permissionRequests: ConnectPermissionRequest[];
71
+
72
+ /** Total milliseconds to poll the relay for a wallet response. */
73
+ timeoutMs?: number;
74
+
75
+ /** Milliseconds between relay polling attempts. */
76
+ pollIntervalMs?: number;
77
+
78
+ /**
79
+ * Receives the wallet URI handoff (with the request pointer TTL) as soon
80
+ * as the request is delivered — render it as a QR code or deep link.
81
+ */
82
+ onWalletUriReady: (handoff: WalletUriHandoff) => void;
83
+
84
+ /**
85
+ * Collects one PIN attempt from the user. Called again (with the attempt
86
+ * number, 1-based, and the previous failure) after a wrong code, up to
87
+ * {@link MAX_PIN_ATTEMPTS} times.
88
+ */
89
+ requestPin: (attempt: number, previousError?: Error) => Promise<string>;
90
+
91
+ /**
92
+ * Cancellation signal: when it settles, polling stops and the handshake
93
+ * rejects with {@link RelayConnectCancelledError}.
94
+ */
95
+ cancelled?: Promise<never> | undefined;
96
+
97
+ /**
98
+ * Invoked once when the relay reports the wallet has claimed (fetched)
99
+ * the pushed request — the app can show "phone connected" progress.
100
+ */
101
+ onClaimed?: () => void;
102
+
103
+ /** Transport factory override (tests). */
104
+ createTransport?: (options: {
105
+ connectServerUrl: string;
106
+ walletUri: string;
107
+ timeoutMs?: number;
108
+ pollIntervalMs?: number;
109
+ onClaimed?: () => void;
110
+ }) => Pick<RelayClientTransport, 'requestProfile' | 'deliverRequest' | 'awaitResponse' | 'requiresPin'>;
111
+ }
112
+
113
+ /**
114
+ * Runs one relay-mediated connect handshake end-to-end and returns the
115
+ * delegated credentials, or `undefined` when the user denied in the wallet.
116
+ */
117
+ export async function runRelayConnect(options: RelayConnectOptions): Promise<ConnectResult | undefined> {
118
+ const createTransport = options.createTransport
119
+ ?? ((transportOptions): RelayClientTransport => new RelayClientTransport(transportOptions));
120
+
121
+ const transport = createTransport({
122
+ connectServerUrl : options.connectServerUrl,
123
+ walletUri : options.walletUri,
124
+ timeoutMs : options.timeoutMs,
125
+ pollIntervalMs : options.pollIntervalMs,
126
+ onClaimed : options.onClaimed,
127
+ });
128
+
129
+ // Ephemeral client DID for request signing and response addressing, and a
130
+ // fresh X25519 pair the wallet seals the response to — same shape as the
131
+ // kernel `ConnectClient`.
132
+ const clientDid = await DidJwk.create();
133
+ const responsePrivateKey = await X25519.generateKey();
134
+ const responsePublicKey: Jwk = { kty: 'OKP', crv: 'X25519', x: responsePrivateKey.x };
135
+
136
+ const nonce = randomToken();
137
+ const state = randomToken();
138
+
139
+ const profile = await transport.requestProfile(state);
140
+
141
+ const request: ConnectRequest = {
142
+ clientDid : clientDid.uri,
143
+ appName : options.appName,
144
+ appIcon : options.appIcon,
145
+ clientMetadata : options.clientMetadata,
146
+ permissionRequests : options.permissionRequests,
147
+ supportedDidMethods : ['did:dht', 'did:jwk'],
148
+ nonce,
149
+ state,
150
+ responseKey : responsePublicKey,
151
+ reply : profile.reply,
152
+ };
153
+
154
+ const requestJwe = await sealRequest({ request, signer: clientDid, encryption: profile.encryption });
155
+
156
+ const handoff = await transport.deliverRequest(requestJwe);
157
+ if (handoff !== undefined) {
158
+ options.onWalletUriReady(handoff);
159
+ }
160
+
161
+ const race = options.cancelled === undefined
162
+ ? transport.awaitResponse()
163
+ : Promise.race([transport.awaitResponse(), options.cancelled]);
164
+
165
+ const responseCiphertext = await race;
166
+ if (responseCiphertext === CONNECT_DENIED_TOKEN) {
167
+ return undefined;
168
+ }
169
+
170
+ // Interactive PIN loop: opening is a pure decrypt-and-verify step, so a
171
+ // wrong code costs one more attempt, not a whole new wallet approval.
172
+ // Errors raised by the kernel's value checks carry a 'Connect:' prefix and
173
+ // indicate a protocol violation, not a mistyped code — those fail closed.
174
+ let lastError: Error | undefined;
175
+ for (let attempt = 1; attempt <= MAX_PIN_ATTEMPTS; attempt++) {
176
+ const pin = await (options.cancelled === undefined
177
+ ? options.requestPin(attempt, lastError)
178
+ : Promise.race([options.requestPin(attempt, lastError), options.cancelled]));
179
+
180
+ try {
181
+ const response = await openResponse({
182
+ jwe : responseCiphertext,
183
+ recipientPrivateKey : responsePrivateKey,
184
+ expected : { clientDid: clientDid.uri, nonce, state },
185
+ pin,
186
+ });
187
+
188
+ if (response.delegatePortableDid === undefined) {
189
+ throw new Error('Connect: wallet response omitted `delegatePortableDid`.');
190
+ }
191
+
192
+ return {
193
+ delegatePortableDid : response.delegatePortableDid,
194
+ delegateGrants : response.delegateGrants,
195
+ connectedDid : response.providerDid,
196
+ sessionRevocations : response.sessionRevocations,
197
+ };
198
+ } catch (error) {
199
+ const failure = error instanceof Error ? error : new Error(String(error));
200
+ if (failure.message.startsWith('Connect:')) {
201
+ // Structural/value-check failure — retrying the PIN cannot help.
202
+ throw failure;
203
+ }
204
+ lastError = failure;
205
+ }
206
+ }
207
+
208
+ throw new Error(`[@enbox/browser] The pairing code did not match after ${MAX_PIN_ATTEMPTS} attempts.`);
209
+ }