@rhinestone/1auth 0.6.8 → 0.6.9

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 (49) hide show
  1. package/README.md +171 -12
  2. package/dist/chunk-IHBVEU33.mjs +20 -0
  3. package/dist/chunk-IHBVEU33.mjs.map +1 -0
  4. package/dist/chunk-IIACVHR3.mjs +28 -0
  5. package/dist/chunk-IIACVHR3.mjs.map +1 -0
  6. package/dist/chunk-N6KE5CII.mjs +72 -0
  7. package/dist/chunk-N6KE5CII.mjs.map +1 -0
  8. package/dist/{chunk-SXISYG2P.mjs → chunk-VZYHCFEH.mjs} +195 -140
  9. package/dist/chunk-VZYHCFEH.mjs.map +1 -0
  10. package/dist/{client-BrMrhetG.d.mts → client-BNluVe4_.d.ts} +305 -849
  11. package/dist/{client-BrMrhetG.d.ts → client-CLCdahyj.d.mts} +305 -849
  12. package/dist/headless.d.mts +90 -0
  13. package/dist/headless.d.ts +90 -0
  14. package/dist/headless.js +327 -0
  15. package/dist/headless.js.map +1 -0
  16. package/dist/headless.mjs +280 -0
  17. package/dist/headless.mjs.map +1 -0
  18. package/dist/index.d.mts +96 -144
  19. package/dist/index.d.ts +96 -144
  20. package/dist/index.js +2984 -718
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +2740 -627
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{provider-CDl9wYEc.d.mts → provider-BgD9PeDX.d.ts} +6 -5
  25. package/dist/{provider-Dgv533YQ.d.ts → provider-hxHGb6SX.d.mts} +6 -5
  26. package/dist/react.d.mts +42 -2
  27. package/dist/react.d.ts +42 -2
  28. package/dist/react.js +92 -2
  29. package/dist/react.js.map +1 -1
  30. package/dist/react.mjs +66 -1
  31. package/dist/react.mjs.map +1 -1
  32. package/dist/server.d.mts +118 -0
  33. package/dist/server.d.ts +118 -0
  34. package/dist/server.js +356 -0
  35. package/dist/server.js.map +1 -0
  36. package/dist/server.mjs +282 -0
  37. package/dist/server.mjs.map +1 -0
  38. package/dist/types-1BMD1PSH.d.mts +1425 -0
  39. package/dist/types-1BMD1PSH.d.ts +1425 -0
  40. package/dist/verify-CZe-m_Vf.d.ts +150 -0
  41. package/dist/verify-D3FaLeAi.d.mts +150 -0
  42. package/dist/wagmi.d.mts +5 -2
  43. package/dist/wagmi.d.ts +5 -2
  44. package/dist/wagmi.js +138 -43
  45. package/dist/wagmi.js.map +1 -1
  46. package/dist/wagmi.mjs +2 -1
  47. package/dist/wagmi.mjs.map +1 -1
  48. package/package.json +15 -2
  49. package/dist/chunk-SXISYG2P.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,13 @@
1
1
  import {
2
- buildTransactionReview,
2
+ tokenFetchError
3
+ } from "./chunk-IIACVHR3.mjs";
4
+ import {
5
+ ETHEREUM_MESSAGE_PREFIX,
6
+ hashMessage,
7
+ verifyMessageHash
8
+ } from "./chunk-IHBVEU33.mjs";
9
+ import {
3
10
  createOneAuthProvider,
4
- encodeWebAuthnSignature,
5
11
  getAllSupportedChainsAndTokens,
6
12
  getChainById,
7
13
  getChainExplorerUrl,
@@ -14,19 +20,258 @@ import {
14
20
  getTokenAddress,
15
21
  getTokenDecimals,
16
22
  getTokenSymbol,
17
- hashCalls,
18
23
  isTestnet,
19
24
  isTokenAddressSupported,
20
25
  resolveTokenAddress
21
- } from "./chunk-SXISYG2P.mjs";
26
+ } from "./chunk-VZYHCFEH.mjs";
27
+ import {
28
+ buildTransactionReview,
29
+ encodeWebAuthnSignature,
30
+ hashCalls
31
+ } from "./chunk-N6KE5CII.mjs";
32
+
33
+ // src/client.ts
34
+ import { hashTypedData } from "viem";
35
+
36
+ // src/assets.ts
37
+ function withNetworkBucket(balance) {
38
+ return {
39
+ ...balance,
40
+ isTestnet: balance.isTestnet ?? isTestnet(balance.chainId)
41
+ };
42
+ }
43
+ function bucketAssetBalances(balances) {
44
+ const mainnetBalances = [];
45
+ const testnetBalances = [];
46
+ for (const balance of balances) {
47
+ const normalized = withNetworkBucket(balance);
48
+ if (normalized.isTestnet) {
49
+ testnetBalances.push(normalized);
50
+ } else {
51
+ mainnetBalances.push(normalized);
52
+ }
53
+ }
54
+ return {
55
+ mainnets: { balances: mainnetBalances },
56
+ testnets: { balances: testnetBalances }
57
+ };
58
+ }
59
+ function normalizeAssetsResponse(payload) {
60
+ const data = payload && typeof payload === "object" ? payload : {};
61
+ if (Array.isArray(data.balances)) {
62
+ const balances2 = data.balances.map(withNetworkBucket);
63
+ const buckets = bucketAssetBalances(balances2);
64
+ return { ...data, balances: balances2, ...buckets };
65
+ }
66
+ const balances = [];
67
+ const assets = Array.isArray(data.assets) ? data.assets : [];
68
+ for (const asset of assets) {
69
+ const symbol = asset.symbol ?? "UNKNOWN";
70
+ const decimals = asset.decimals ?? 18;
71
+ const chains = Array.isArray(asset.chains) ? asset.chains : [];
72
+ if (chains.length === 0) {
73
+ const token = asset.token ?? asset.address;
74
+ if (token) {
75
+ balances.push(withNetworkBucket({
76
+ chainId: 0,
77
+ token,
78
+ symbol,
79
+ decimals,
80
+ balance: asset.balance ?? asset.amount ?? "0",
81
+ ...asset.usdValue !== void 0 && { usdValue: asset.usdValue }
82
+ }));
83
+ }
84
+ continue;
85
+ }
86
+ for (const chain of chains) {
87
+ const chainId = chain.chainId ?? chain.chain;
88
+ const token = chain.token ?? chain.address;
89
+ if (typeof chainId !== "number" || !token) continue;
90
+ balances.push(withNetworkBucket({
91
+ chainId,
92
+ token,
93
+ symbol,
94
+ decimals: chain.decimals ?? decimals,
95
+ balance: chain.balance ?? chain.amount ?? "0",
96
+ ...asset.usdValue !== void 0 && { usdValue: asset.usdValue },
97
+ ...chain.isTestnet !== void 0 && { isTestnet: chain.isTestnet }
98
+ }));
99
+ }
100
+ }
101
+ return { ...data, balances, ...bucketAssetBalances(balances) };
102
+ }
103
+ async function fetchAssetsResponse(options) {
104
+ const portfolioUrl = new URL(
105
+ `/api/users/${encodeURIComponent(options.identifier)}/portfolio`,
106
+ options.providerUrl
107
+ );
108
+ portfolioUrl.searchParams.set("all", "true");
109
+ const response = await fetch(portfolioUrl.toString(), {
110
+ headers: {
111
+ ...options.headers,
112
+ ...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {},
113
+ ...options.clientId ? { "x-client-id": options.clientId } : {}
114
+ }
115
+ });
116
+ if (!response.ok) {
117
+ const data = await response.json().catch(() => ({}));
118
+ throw new Error(data.error || "Failed to get assets");
119
+ }
120
+ return normalizeAssetsResponse(await response.json());
121
+ }
122
+
123
+ // ../loading-tokens/src/index.ts
124
+ var SPINNER_SIZE = 82;
125
+ var SPINNER_STROKE = 2.4118;
126
+ var SPINNER_RADIUS = (SPINNER_SIZE - SPINNER_STROKE) / 2;
127
+ var SPINNER_CIRCUMFERENCE = 2 * Math.PI * SPINNER_RADIUS;
128
+ var SPINNER_ARC_FRACTION = 0.17;
129
+ var SPINNER_ARC = SPINNER_CIRCUMFERENCE * SPINNER_ARC_FRACTION;
130
+ var LOADING_MODAL_WIDTH = 380;
131
+ var LOADING_MODAL_PADDING = 12;
132
+ var LOADING_MODAL_RADIUS = 16;
133
+ var LOADING_MOBILE_BREAKPOINT = 640;
134
+ var LOADING_CARD_GAP = 16;
135
+ var LOADING_BODY_GAP = 12;
136
+ var LOADING_ICON_PADDING = 12;
137
+ var LOADING_TITLE_BLOCK_GAP = 4;
138
+ var LOADING_FOOTER_GAP = 4;
139
+ var LOADING_TITLE_FONT_SIZE = 20;
140
+ var LOADING_TITLE_FONT_WEIGHT = 700;
141
+ var LOADING_SUBTITLE_FONT_SIZE = 12;
142
+ var LOADING_SUBTITLE_FONT_WEIGHT = 500;
143
+ var LOADING_FOOTER_FONT_SIZE = 11;
144
+ var LOADING_FOOTER_FONT_WEIGHT = 500;
145
+ var LOADING_LINE_HEIGHT = "normal";
146
+ var LOADING_FONT_FAMILY = "'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'";
147
+ var LOGO_WIDTH = 72;
148
+ var LOGO_HEIGHT = 16;
149
+ var BRAND_PURPLE = "#685bff";
150
+ var LIGHT_TOKENS = {
151
+ bgPrimary: "#ffffff",
152
+ borderColor: "#f4f4f5",
153
+ textPrimary: "#27272a",
154
+ textSecondary: "#71717b"
155
+ };
156
+ var DARK_TOKENS = {
157
+ bgPrimary: "#0a0a0a",
158
+ borderColor: "#18181b",
159
+ textPrimary: "#e4e4e7",
160
+ textSecondary: "#9f9fa9"
161
+ };
162
+ var FOOTER_TEXT_COLOR = "#71717b";
163
+ var LOADING_TITLE = "Loading...";
164
+ var LOADING_SUBTITLE = "This will only take a few moments, do not close the window.";
22
165
 
23
166
  // src/client.ts
24
- import { parseUnits, hashTypedData } from "viem";
25
- var POPUP_WIDTH = 450;
167
+ var POPUP_WIDTH = LOADING_MODAL_WIDTH;
26
168
  var POPUP_HEIGHT = 600;
27
- var DEFAULT_EMBED_WIDTH = "400px";
169
+ var DEFAULT_EMBED_WIDTH = `${LOADING_MODAL_WIDTH}px`;
28
170
  var DEFAULT_EMBED_HEIGHT = "500px";
29
171
  var DEFAULT_PROVIDER_URL = "https://passkey.1auth.box";
172
+ var MAX_LABEL_LEN = 20;
173
+ function generateModalNonce() {
174
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
175
+ return crypto.randomUUID();
176
+ }
177
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
178
+ }
179
+ function generateTelemetryOperationId() {
180
+ return `1auth-${generateModalNonce()}`;
181
+ }
182
+ function cleanTelemetryAttributes(attributes) {
183
+ if (!attributes) return void 0;
184
+ const clean = {};
185
+ for (const [key, value] of Object.entries(attributes)) {
186
+ if (value !== void 0) clean[key] = value;
187
+ }
188
+ return Object.keys(clean).length > 0 ? clean : void 0;
189
+ }
190
+ function describeTelemetryError(error) {
191
+ if (!error) return {};
192
+ if (error instanceof Error) {
193
+ return { errorCode: error.name, errorMessage: error.message };
194
+ }
195
+ if (typeof error === "object") {
196
+ const err = error;
197
+ return {
198
+ errorCode: typeof err.code === "string" ? err.code : void 0,
199
+ errorMessage: typeof err.message === "string" ? err.message : String(error)
200
+ };
201
+ }
202
+ return { errorMessage: String(error) };
203
+ }
204
+ function capCallLabels(calls) {
205
+ if (!calls) return calls;
206
+ return calls.map((c) => {
207
+ const cappedLabel = c.label && c.label.length > MAX_LABEL_LEN ? c.label.slice(0, MAX_LABEL_LEN - 1) + "\u2026" : c.label;
208
+ const cappedSublabel = c.sublabel && c.sublabel.length > MAX_LABEL_LEN ? c.sublabel.slice(0, MAX_LABEL_LEN - 1) + "\u2026" : c.sublabel;
209
+ if (cappedLabel === c.label && cappedSublabel === c.sublabel) return c;
210
+ return { ...c, label: cappedLabel, sublabel: cappedSublabel };
211
+ });
212
+ }
213
+ function getStoredSignerType() {
214
+ if (typeof window === "undefined") return "passkey";
215
+ try {
216
+ const raw = window.localStorage.getItem("1auth-user");
217
+ if (!raw) return "passkey";
218
+ const parsed = JSON.parse(raw);
219
+ return parsed?.signerType === "eoa" ? "eoa" : "passkey";
220
+ } catch {
221
+ return "passkey";
222
+ }
223
+ }
224
+ var DEFAULT_BLIND_SIGNING = true;
225
+ var MISSING_APP_CREDENTIALS_MESSAGE = "No sponsorship configured on OneAuthClient. Every user intent must carry the app's JWT \u2014 pass `sponsorship: { accessToken, getExtensionToken }` (or URL form) to the constructor.";
226
+ function normalizeSponsorship(config) {
227
+ if (!config) return null;
228
+ if ("accessToken" in config && "getExtensionToken" in config) {
229
+ return config;
230
+ }
231
+ const { accessTokenUrl, extensionTokenUrl } = config;
232
+ return {
233
+ accessToken: async () => {
234
+ const response = await fetch(accessTokenUrl, { credentials: "include" });
235
+ if (!response.ok) {
236
+ throw await tokenFetchError("access token", response);
237
+ }
238
+ const data = await response.json();
239
+ if (!data.token) {
240
+ throw new Error("Access token response missing `token` field");
241
+ }
242
+ return data.token;
243
+ },
244
+ getExtensionToken: async (intentOp) => {
245
+ const response = await fetch(extensionTokenUrl, {
246
+ method: "POST",
247
+ credentials: "include",
248
+ headers: { "Content-Type": "application/json" },
249
+ body: JSON.stringify({ intentOp })
250
+ });
251
+ if (!response.ok) {
252
+ throw await tokenFetchError("extension token", response);
253
+ }
254
+ const data = await response.json();
255
+ if (!data.token) {
256
+ throw new Error("Extension token response missing `token` field");
257
+ }
258
+ return data.token;
259
+ }
260
+ };
261
+ }
262
+ function grantPermissionReplacer(_key, value) {
263
+ return typeof value === "bigint" ? value.toString() : value;
264
+ }
265
+ function cloneGrantPayload(value) {
266
+ return JSON.parse(JSON.stringify(value, grantPermissionReplacer));
267
+ }
268
+ function normalizeGrantTargetChains(options) {
269
+ const chainCandidates = options.targetChains ?? (options.targetChain !== void 0 ? [options.targetChain] : []);
270
+ return Array.from(new Set(chainCandidates));
271
+ }
272
+ function normalizeGrantSourceChains(options) {
273
+ return Array.from(new Set(options.sourceChains ?? []));
274
+ }
30
275
  var OneAuthClient = class {
31
276
  /**
32
277
  * Create a new OneAuthClient.
@@ -40,16 +285,229 @@ var OneAuthClient = class {
40
285
  * clientId, theme, and redirect settings.
41
286
  */
42
287
  constructor(config) {
288
+ // Persistent state for EOA forwarding via `/dialog/sign-eoa`. The iframe
289
+ // is created lazily on the first `requestWithWallet` call and reused for
290
+ // every subsequent call so its WalletConnect SignClient survives between
291
+ // transactions — otherwise each call would spin up a fresh SignClient
292
+ // that restores stale relay state from localStorage, producing
293
+ // "Restore will override" warnings and "emitting session_request:N
294
+ // without any listeners" errors on the second and later txs.
295
+ this.eoaDialogState = null;
296
+ // Serialises concurrent `requestWithWallet` calls — they would otherwise
297
+ // both try to drive the same iframe and cross-resolve each other.
298
+ this.eoaSerialQueue = Promise.resolve();
43
299
  const providerUrl = config.providerUrl || DEFAULT_PROVIDER_URL;
44
300
  const dialogUrl = config.dialogUrl || providerUrl;
45
301
  this.config = { ...config, providerUrl, dialogUrl };
46
302
  this.theme = this.config.theme || {};
303
+ this.sponsorship = normalizeSponsorship(config.sponsorship);
47
304
  if (typeof document !== "undefined") {
48
305
  this.injectPreconnect(providerUrl);
49
306
  if (dialogUrl !== providerUrl) {
50
307
  this.injectPreconnect(dialogUrl);
51
308
  }
52
309
  }
310
+ const initOperation = this.createTelemetryOperation("client", {
311
+ hasClientId: !!this.config.clientId,
312
+ hasDialogUrlOverride: dialogUrl !== providerUrl
313
+ });
314
+ this.emitTelemetry("client.init", initOperation, { outcome: "started" });
315
+ }
316
+ /**
317
+ * Update the sponsorship configuration at runtime. Pass `undefined` to
318
+ * disable sponsorship.
319
+ */
320
+ setSponsorship(sponsorship) {
321
+ this.sponsorship = normalizeSponsorship(sponsorship);
322
+ }
323
+ /**
324
+ * Whether the caller opted into SDK telemetry propagation or callbacks.
325
+ */
326
+ shouldUseTelemetry() {
327
+ return !!this.config.telemetry && this.config.telemetry.enabled !== false;
328
+ }
329
+ /**
330
+ * Resolve the host app's current trace context. Callback failures are
331
+ * ignored because observability can never be allowed to break signing.
332
+ */
333
+ getTelemetryTraceContext() {
334
+ const traceContext = this.config.telemetry?.traceContext;
335
+ if (!traceContext || !this.shouldUseTelemetry()) return void 0;
336
+ try {
337
+ return typeof traceContext === "function" ? traceContext() : traceContext;
338
+ } catch {
339
+ return void 0;
340
+ }
341
+ }
342
+ /**
343
+ * Resolve host-provided SDK event attributes and remove undefined fields.
344
+ */
345
+ getTelemetryAttributes() {
346
+ const attributes = this.config.telemetry?.attributes;
347
+ if (!attributes || !this.shouldUseTelemetry()) return void 0;
348
+ try {
349
+ const resolved = typeof attributes === "function" ? attributes() : attributes;
350
+ return cleanTelemetryAttributes(resolved);
351
+ } catch {
352
+ return void 0;
353
+ }
354
+ }
355
+ /**
356
+ * Create per-operation telemetry context shared by callbacks, HTTP headers,
357
+ * dialog URL params, and PASSKEY_INIT messages.
358
+ */
359
+ createTelemetryOperation(flow, attributes) {
360
+ return {
361
+ operationId: generateTelemetryOperationId(),
362
+ flow,
363
+ startedAt: Date.now(),
364
+ trace: this.getTelemetryTraceContext(),
365
+ attributes: cleanTelemetryAttributes({
366
+ ...this.getTelemetryAttributes(),
367
+ ...attributes
368
+ })
369
+ };
370
+ }
371
+ /**
372
+ * Emit a telemetry event to the host app. The callback is intentionally
373
+ * fire-and-forget and exception-safe so app telemetry cannot affect users.
374
+ */
375
+ emitTelemetry(name, operation, patch = {}) {
376
+ const onEvent = this.config.telemetry?.onEvent;
377
+ if (!onEvent || !this.shouldUseTelemetry()) return;
378
+ const attributes = cleanTelemetryAttributes({
379
+ ...operation.attributes,
380
+ ...patch.attributes
381
+ });
382
+ const event = {
383
+ name,
384
+ operationId: operation.operationId,
385
+ flow: operation.flow,
386
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
387
+ durationMs: patch.durationMs ?? Date.now() - operation.startedAt,
388
+ clientId: this.config.clientId,
389
+ providerUrl: this.config.providerUrl,
390
+ dialogUrl: this.getDialogUrl(),
391
+ trace: operation.trace,
392
+ ...patch,
393
+ attributes
394
+ };
395
+ try {
396
+ onEvent(event);
397
+ } catch {
398
+ }
399
+ }
400
+ /**
401
+ * Attach SDK correlation and optional W3C trace context to dialog URLs.
402
+ */
403
+ appendTelemetryParams(params, operation) {
404
+ if (!this.shouldUseTelemetry()) return;
405
+ params.set("sdkOperationId", operation.operationId);
406
+ params.set("sdkTelemetryFlow", operation.flow);
407
+ if (operation.trace?.traceparent) {
408
+ params.set("traceparent", operation.trace.traceparent);
409
+ }
410
+ if (operation.trace?.tracestate) {
411
+ params.set("tracestate", operation.trace.tracestate);
412
+ }
413
+ if (operation.trace?.traceId) {
414
+ params.set("sdkTraceId", operation.trace.traceId);
415
+ }
416
+ if (operation.trace?.spanId) {
417
+ params.set("sdkSpanId", operation.trace.spanId);
418
+ }
419
+ }
420
+ /**
421
+ * Add SDK correlation headers to passkey API calls. These headers are also
422
+ * valid W3C trace propagation inputs when the host app supplies traceparent.
423
+ */
424
+ telemetryHeaders(operation) {
425
+ if (!operation || !this.shouldUseTelemetry()) return {};
426
+ return {
427
+ "x-1auth-sdk-operation-id": operation.operationId,
428
+ "x-1auth-sdk-flow": operation.flow,
429
+ ...operation.trace?.traceparent ? { traceparent: operation.trace.traceparent } : {},
430
+ ...operation.trace?.tracestate ? { tracestate: operation.trace.tracestate } : {}
431
+ };
432
+ }
433
+ /**
434
+ * Embed telemetry context in PASSKEY_INIT payloads for dialog-side API calls.
435
+ */
436
+ telemetryPayload(operation) {
437
+ if (!this.shouldUseTelemetry()) return void 0;
438
+ return {
439
+ operationId: operation.operationId,
440
+ flow: operation.flow,
441
+ ...operation.trace?.traceparent ? { traceparent: operation.trace.traceparent } : {},
442
+ ...operation.trace?.tracestate ? { tracestate: operation.trace.tracestate } : {},
443
+ ...operation.trace?.traceId ? { traceId: operation.trace.traceId } : {},
444
+ ...operation.trace?.spanId ? { spanId: operation.trace.spanId } : {}
445
+ };
446
+ }
447
+ /**
448
+ * Determine whether this signing call should request invisible dialog mode.
449
+ * Resolution order: per-call option wins over the constructor config, which
450
+ * wins over the platform-wide {@link DEFAULT_BLIND_SIGNING}. This lets an app
451
+ * flip the global default for itself while still overriding individual
452
+ * high-risk calls — e.g. default-blind but force the review UI for a large
453
+ * transfer, or default-visible but blind-sign selected low-risk signatures.
454
+ */
455
+ shouldRequestBlindSigning(options) {
456
+ return options?.blind_signing ?? this.config.blind_signing ?? DEFAULT_BLIND_SIGNING;
457
+ }
458
+ /**
459
+ * Fetch the app's access token (JWT). Called up front, before
460
+ * `/api/intent/prepare`, so the passkey server can authenticate the
461
+ * orchestrator quote call with the app's JWT instead of a service-level
462
+ * API key.
463
+ *
464
+ * Does not depend on `intentOp` — can run in parallel with anything that
465
+ * also doesn't depend on the quote.
466
+ */
467
+ async fetchAccessToken() {
468
+ if (!this.sponsorship) {
469
+ return {
470
+ ok: false,
471
+ code: "MISSING_APP_CREDENTIALS",
472
+ message: MISSING_APP_CREDENTIALS_MESSAGE
473
+ };
474
+ }
475
+ try {
476
+ const accessToken = await this.sponsorship.accessToken();
477
+ return { ok: true, accessToken };
478
+ } catch (err) {
479
+ return {
480
+ ok: false,
481
+ message: err instanceof Error ? err.message : String(err)
482
+ };
483
+ }
484
+ }
485
+ /**
486
+ * Fetch one extension token per intent, aligned with `sponsorFlags`.
487
+ * Returns an empty array when no intent is sponsored. Fired after
488
+ * `/prepare` so each token can be bound to its quote's `intentOp`, and
489
+ * overlaps the dialog / WebAuthn ceremony for no added serial latency.
490
+ */
491
+ async fetchExtensionTokens(intentOps, sponsorFlags) {
492
+ if (!this.sponsorship) {
493
+ return { ok: false, message: MISSING_APP_CREDENTIALS_MESSAGE };
494
+ }
495
+ const anySponsored = sponsorFlags.some(Boolean);
496
+ if (!anySponsored) return { ok: true, extensionTokens: [] };
497
+ try {
498
+ const sponsorship = this.sponsorship;
499
+ const tokens = await Promise.all(
500
+ intentOps.map(
501
+ (op, i) => sponsorFlags[i] ? sponsorship.getExtensionToken(op) : Promise.resolve("")
502
+ )
503
+ );
504
+ return { ok: true, extensionTokens: tokens };
505
+ } catch (err) {
506
+ return {
507
+ ok: false,
508
+ message: err instanceof Error ? err.message : String(err)
509
+ };
510
+ }
53
511
  }
54
512
  /**
55
513
  * Update the theme configuration at runtime
@@ -75,8 +533,11 @@ var OneAuthClient = class {
75
533
  if (theme.mode) {
76
534
  params.set("theme", theme.mode);
77
535
  }
78
- if (theme.accent) {
79
- params.set("accent", theme.accent);
536
+ const instancePrimary = this.theme.primaryColor ?? this.theme.accent;
537
+ const overridePrimary = overrideTheme?.primaryColor ?? overrideTheme?.accent;
538
+ const primary = overridePrimary ?? instancePrimary;
539
+ if (primary) {
540
+ params.set("accent", primary);
80
541
  }
81
542
  return params.toString();
82
543
  }
@@ -126,6 +587,60 @@ var OneAuthClient = class {
126
587
  getClientId() {
127
588
  return this.config.clientId;
128
589
  }
590
+ /**
591
+ * Whether this client operates on testnet chains only.
592
+ */
593
+ getTestnets() {
594
+ return this.config.testnets ?? false;
595
+ }
596
+ /**
597
+ * Get a unified token portfolio for a 1auth account across mainnets and
598
+ * testnets.
599
+ *
600
+ * @example
601
+ * ```typescript
602
+ * const assets = await client.getAssets({
603
+ * accountAddress: "0x1111111111111111111111111111111111111111",
604
+ * });
605
+ * console.log(assets.mainnets.balances, assets.testnets.balances);
606
+ * ```
607
+ */
608
+ async getAssets(options) {
609
+ if (!options.accountAddress) {
610
+ throw new Error("getAssets requires accountAddress");
611
+ }
612
+ const telemetry = this.createTelemetryOperation("assets", {
613
+ hasAccountAddress: !!options.accountAddress
614
+ });
615
+ try {
616
+ const accessTokenResult = await this.fetchAccessToken();
617
+ if (!accessTokenResult.ok) {
618
+ throw new Error(accessTokenResult.message);
619
+ }
620
+ const result = await fetchAssetsResponse({
621
+ providerUrl: this.config.providerUrl,
622
+ identifier: options.accountAddress,
623
+ clientId: this.config.clientId,
624
+ accessToken: accessTokenResult.accessToken,
625
+ headers: this.telemetryHeaders(telemetry)
626
+ });
627
+ this.emitTelemetry("assets.succeeded", telemetry, {
628
+ outcome: "success",
629
+ attributes: {
630
+ balanceCount: result.balances.length,
631
+ mainnetBalanceCount: result.mainnets.balances.length,
632
+ testnetBalanceCount: result.testnets.balances.length
633
+ }
634
+ });
635
+ return result;
636
+ } catch (error) {
637
+ this.emitTelemetry("assets.failed", telemetry, {
638
+ outcome: "failure",
639
+ ...describeTelemetryError(error)
640
+ });
641
+ throw error;
642
+ }
643
+ }
129
644
  /**
130
645
  * Poll the intent status endpoint until a transaction hash appears or the
131
646
  * intent reaches a terminal failure state.
@@ -144,7 +659,7 @@ var OneAuthClient = class {
144
659
  * @returns The transaction hash string, or `undefined` if the deadline was
145
660
  * reached or the intent failed/expired before a hash was produced.
146
661
  */
147
- async waitForTransactionHash(intentId, options = {}) {
662
+ async waitForTransactionHash(intentId, options = {}, telemetry) {
148
663
  const timeoutMs = options.timeoutMs ?? 12e4;
149
664
  const intervalMs = options.intervalMs ?? 2e3;
150
665
  const deadline = Date.now() + timeoutMs;
@@ -153,7 +668,10 @@ var OneAuthClient = class {
153
668
  const response = await fetch(
154
669
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
155
670
  {
156
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
671
+ headers: {
672
+ ...this.telemetryHeaders(telemetry),
673
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
674
+ }
157
675
  }
158
676
  );
159
677
  if (response.ok) {
@@ -181,6 +699,8 @@ var OneAuthClient = class {
181
699
  * @param options.username - Pre-fill the username field
182
700
  * @param options.theme - Override the theme for this modal invocation
183
701
  * @param options.oauthEnabled - Set to false to hide OAuth sign-in buttons
702
+ * @param options.flow - Force the login or account-creation route instead
703
+ * of the default auto-detect entry route
184
704
  * @returns {@link AuthResult} with user details and typed address
185
705
  *
186
706
  * @example
@@ -196,9 +716,54 @@ var OneAuthClient = class {
196
716
  * ```
197
717
  */
198
718
  async authWithModal(options) {
719
+ return this.openAuthModal(options);
720
+ }
721
+ /**
722
+ * Open the sign-in modal directly.
723
+ *
724
+ * This bypasses the `/dialog/auth` auto-router and targets the dedicated
725
+ * returning-user flow. Use it when the embedding app already knows the user
726
+ * is creating a login session, for example after an app-level "Log in"
727
+ * button. The result intentionally matches {@link authWithModal}.
728
+ *
729
+ * @param options.username - Optional account hint for the sign-in ceremony
730
+ * @param options.theme - Override the theme for this modal invocation
731
+ * @returns {@link AuthResult} with user details and typed address
732
+ */
733
+ async loginWithModal(options) {
734
+ return this.openAuthModal({ ...options, flow: "login" });
735
+ }
736
+ /**
737
+ * Open the account-creation modal directly.
738
+ *
739
+ * This bypasses the `/dialog/auth` auto-router and targets the dedicated
740
+ * sign-up flow. Account creation still happens server-side in the passkey
741
+ * service; the SDK only opens the dialog and receives the postMessage result.
742
+ *
743
+ * @param options.theme - Override the theme for this modal invocation
744
+ * @param options.oauthEnabled - Set to false to hide OAuth sign-in buttons
745
+ * @returns {@link AuthResult} with the newly authenticated account details
746
+ */
747
+ async createAccountWithModal(options) {
748
+ return this.openAuthModal({ ...options, flow: "create-account" });
749
+ }
750
+ /**
751
+ * Open one of the passkey authentication modal routes and wait for the
752
+ * login-result message.
753
+ *
754
+ * Keeping URL construction in one helper prevents the explicit login/signup
755
+ * methods from drifting away from the legacy `authWithModal` behavior.
756
+ */
757
+ async openAuthModal(options) {
758
+ const telemetry = this.createTelemetryOperation("auth", {
759
+ flow: options?.flow ?? "auto",
760
+ hasUsernameHint: !!options?.username
761
+ });
199
762
  const dialogUrl = this.getDialogUrl();
763
+ const requestId = this.createDialogRequestId();
200
764
  const params = new URLSearchParams({
201
- mode: "iframe"
765
+ mode: "iframe",
766
+ requestId
202
767
  });
203
768
  if (this.config.clientId) {
204
769
  params.set("clientId", this.config.clientId);
@@ -209,14 +774,60 @@ var OneAuthClient = class {
209
774
  if (options?.oauthEnabled === false) {
210
775
  params.set("oauth", "0");
211
776
  }
777
+ if (options?.flow === "create-account") {
778
+ params.set("switch", "1");
779
+ }
780
+ if (typeof window !== "undefined" && window.location?.origin) {
781
+ params.set("parentOrigin", window.location.origin);
782
+ }
783
+ this.appendTelemetryParams(params, telemetry);
212
784
  const themeParams = this.getThemeParams(options?.theme);
213
785
  if (themeParams) {
214
786
  const themeParsed = new URLSearchParams(themeParams);
215
787
  themeParsed.forEach((value, key) => params.set(key, value));
216
788
  }
217
- const url = `${dialogUrl}/dialog/auth?${params.toString()}`;
789
+ const authPath = this.getAuthDialogPath(options?.flow);
790
+ const url = `${dialogUrl}${authPath}?${params.toString()}`;
218
791
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
219
- return this.waitForModalAuthResponse(dialog, iframe, cleanup);
792
+ this.emitTelemetry("dialog.opened", telemetry, { outcome: "started" });
793
+ const result = await this.waitForModalAuthResponse(dialog, iframe, cleanup, requestId);
794
+ if (result.success) {
795
+ this.emitTelemetry("auth.succeeded", telemetry, {
796
+ outcome: "success",
797
+ attributes: { signerType: result.signerType ?? "passkey" }
798
+ });
799
+ } else {
800
+ this.emitTelemetry(
801
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "auth.failed",
802
+ telemetry,
803
+ {
804
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
805
+ ...describeTelemetryError(result.error)
806
+ }
807
+ );
808
+ }
809
+ return result;
810
+ }
811
+ /**
812
+ * Generate a per-dialog nonce used to correlate iframe close messages with
813
+ * the SDK modal that opened them. Chromium can report a different
814
+ * `MessageEvent.source` WindowProxy after the `/dialog/auth` redirect, so
815
+ * auth close handling needs a stable in-band identifier without accepting
816
+ * stale close messages from earlier dialogs.
817
+ */
818
+ createDialogRequestId() {
819
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
820
+ return crypto.randomUUID();
821
+ }
822
+ return `dialog-${Date.now()}-${Math.random().toString(36).slice(2)}`;
823
+ }
824
+ /**
825
+ * Resolve the passkey app route for a requested auth flow.
826
+ */
827
+ getAuthDialogPath(flow = "auto") {
828
+ if (flow === "login") return "/dialog/auth/signin";
829
+ if (flow === "create-account") return "/dialog/auth/signup";
830
+ return "/dialog/auth";
220
831
  }
221
832
  /**
222
833
  * Open the connect dialog (lightweight connection without passkey auth).
@@ -241,6 +852,7 @@ var OneAuthClient = class {
241
852
  * ```
242
853
  */
243
854
  async connectWithModal(options) {
855
+ const telemetry = this.createTelemetryOperation("connect");
244
856
  const dialogUrl = this.getDialogUrl();
245
857
  const params = new URLSearchParams({
246
858
  mode: "iframe"
@@ -248,6 +860,10 @@ var OneAuthClient = class {
248
860
  if (this.config.clientId) {
249
861
  params.set("clientId", this.config.clientId);
250
862
  }
863
+ if (typeof window !== "undefined" && window.location?.origin) {
864
+ params.set("parentOrigin", window.location.origin);
865
+ }
866
+ this.appendTelemetryParams(params, telemetry);
251
867
  const themeParams = this.getThemeParams(options?.theme);
252
868
  if (themeParams) {
253
869
  const themeParsed = new URLSearchParams(themeParams);
@@ -255,17 +871,104 @@ var OneAuthClient = class {
255
871
  }
256
872
  const url = `${dialogUrl}/dialog/connect?${params.toString()}`;
257
873
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
874
+ this.emitTelemetry("dialog.opened", telemetry, { outcome: "started" });
258
875
  const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
259
- mode: "iframe"
876
+ mode: "iframe",
877
+ telemetry: this.telemetryPayload(telemetry)
260
878
  });
261
879
  if (!ready) {
880
+ this.emitTelemetry("dialog.cancelled", telemetry, {
881
+ outcome: "cancelled",
882
+ errorCode: "USER_CANCELLED",
883
+ errorMessage: "Connection was cancelled before the dialog became ready"
884
+ });
262
885
  return {
263
886
  success: false,
264
887
  action: "cancel",
265
888
  error: { code: "USER_CANCELLED", message: "Connection was cancelled" }
266
889
  };
267
890
  }
268
- return this.waitForConnectResponse(dialog, iframe, cleanup);
891
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
892
+ const result = await this.waitForConnectResponse(dialog, iframe, cleanup);
893
+ if (result.success) {
894
+ this.emitTelemetry("connect.succeeded", telemetry, {
895
+ outcome: "success",
896
+ attributes: {
897
+ autoConnected: result.autoConnected === true,
898
+ signerType: result.signerType ?? "passkey"
899
+ }
900
+ });
901
+ } else {
902
+ this.emitTelemetry(
903
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "connect.failed",
904
+ telemetry,
905
+ {
906
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
907
+ ...describeTelemetryError(result.error),
908
+ attributes: { action: result.action }
909
+ }
910
+ );
911
+ }
912
+ return result;
913
+ }
914
+ /**
915
+ * High-level connect entry point — the method most integrators want.
916
+ *
917
+ * Tries the lightweight {@link connectWithModal} first (no passkey ceremony
918
+ * for returning users on the same browser), and transparently falls back to
919
+ * {@link authWithModal} when the connect dialog reports it has no stored
920
+ * credentials for this origin (`action: "switch"`). First-time visitors get
921
+ * one passkey ceremony to register; subsequent connects on the same browser
922
+ * resolve with no biometric prompt — and silently when the user has enabled
923
+ * "Connect automatically".
924
+ *
925
+ * The return shape is the same {@link ConnectResult} discriminated union as
926
+ * {@link connectWithModal}, so callers can treat both paths uniformly. When
927
+ * the auth-modal fallback runs, its successful result is normalized into a
928
+ * `ConnectResult` (no `id` field — the embedder origin must not see it).
929
+ *
930
+ * Cancellation: if the user cancels either dialog, the result is
931
+ * `{ success: false, action: "cancel", error: { code: "USER_CANCELLED", … } }`.
932
+ *
933
+ * @param options - Forwarded to whichever underlying modal is shown.
934
+ * `username` and `oauthEnabled` only apply to the auth-modal fallback path.
935
+ * @returns A {@link ConnectResult} discriminated union.
936
+ *
937
+ * @example
938
+ * ```typescript
939
+ * const result = await client.connect();
940
+ * if (result.success) {
941
+ * console.log("Connected as", result.user?.username ?? result.user?.address);
942
+ * }
943
+ * ```
944
+ */
945
+ async connect(options) {
946
+ const connectResult = await this.connectWithModal(
947
+ options?.theme ? { theme: options.theme } : void 0
948
+ );
949
+ if (connectResult.success) return connectResult;
950
+ if (connectResult.action === "switch") {
951
+ const authResult = await this.authWithModal(options);
952
+ if (authResult.success && authResult.user) {
953
+ return {
954
+ success: true,
955
+ user: {
956
+ username: authResult.user.username,
957
+ address: authResult.user.address
958
+ },
959
+ autoConnected: false
960
+ };
961
+ }
962
+ return {
963
+ success: false,
964
+ action: "cancel",
965
+ error: authResult.error ?? {
966
+ code: "USER_CANCELLED",
967
+ message: "Connection was cancelled"
968
+ }
969
+ };
970
+ }
971
+ return connectResult;
269
972
  }
270
973
  /**
271
974
  * Open the account management dialog.
@@ -315,131 +1018,273 @@ var OneAuthClient = class {
315
1018
  });
316
1019
  }
317
1020
  /**
318
- * Check if a user has already granted consent for the requested fields.
319
- * This is a read-only check — no dialog is shown.
1021
+ * Open the account-recovery backup flow directly.
320
1022
  *
321
- * @example
322
- * ```typescript
323
- * const result = await client.checkConsent({
324
- * username: "alice",
325
- * fields: ["email"],
326
- * });
327
- * if (result.hasConsent) {
328
- * console.log(result.data?.email);
329
- * }
330
- * ```
1023
+ * Deep-links into the "Backup your account" flow inside the account dialog
1024
+ * (rather than the account overview): the user verifies with their passkey,
1025
+ * then saves a recovery passphrase and downloads an encrypted backup file.
1026
+ * Use this to prompt users to secure their account after sign-up.
1027
+ *
1028
+ * Requires an authenticated user (call `authWithModal()` / `connect()`
1029
+ * first) — the dialog reads the signed-in user from the passkey origin.
1030
+ *
1031
+ * Resolves `{ completed: true }` when the user finishes the backup, or
1032
+ * `{ completed: false }` if they dismiss it — so callers can mark an
1033
+ * onboarding step done or re-prompt later.
331
1034
  */
332
- async checkConsent(options) {
333
- const clientId = options.clientId ?? this.config.clientId;
334
- if (!clientId) {
335
- return {
336
- hasConsent: false
337
- };
338
- }
339
- const username = options.username ?? options.accountAddress;
340
- if (!username) {
341
- return {
342
- hasConsent: false
343
- };
344
- }
345
- try {
346
- const res = await fetch(`${this.config.providerUrl || "https://passkey.1auth.box"}/api/consent`, {
347
- method: "POST",
348
- headers: {
349
- "Content-Type": "application/json",
350
- ...clientId ? { "x-client-id": clientId } : {}
351
- },
352
- body: JSON.stringify({
353
- username,
354
- requestedFields: options.fields,
355
- clientId
356
- })
357
- });
358
- if (!res.ok) {
359
- return { hasConsent: false };
360
- }
361
- const data = await res.json();
362
- return {
363
- hasConsent: data.hasConsent ?? false,
364
- data: data.data,
365
- grantedAt: data.grantedAt
366
- };
367
- } catch {
368
- return { hasConsent: false };
1035
+ async setupRecovery(options) {
1036
+ const dialogUrl = this.getDialogUrl();
1037
+ const params = new URLSearchParams({
1038
+ mode: "iframe",
1039
+ // Deep-link the account dialog straight into the backup nudge instead
1040
+ // of the account overview.
1041
+ action: "backup"
1042
+ });
1043
+ const themeParams = this.getThemeParams(options?.theme);
1044
+ if (themeParams) {
1045
+ const themeParsed = new URLSearchParams(themeParams);
1046
+ themeParsed.forEach((value, key) => params.set(key, value));
369
1047
  }
1048
+ const url = `${dialogUrl}/dialog/account?${params.toString()}`;
1049
+ const { dialog, iframe, cleanup } = this.createModalDialog(url);
1050
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
1051
+ mode: "iframe"
1052
+ });
1053
+ if (!ready) return { completed: false };
1054
+ const dialogOrigin = this.getDialogOrigin();
1055
+ return new Promise((resolve) => {
1056
+ let completed = false;
1057
+ const finish = () => {
1058
+ window.removeEventListener("message", handleMessage);
1059
+ dialog.removeEventListener("close", handleClose);
1060
+ cleanup();
1061
+ resolve({ completed });
1062
+ };
1063
+ const handleMessage = (event) => {
1064
+ if (event.origin !== dialogOrigin) return;
1065
+ const type = event.data?.type;
1066
+ if (type === "PASSKEY_RECOVERY_RESULT") {
1067
+ completed = Boolean(event.data?.data?.completed ?? event.data?.success);
1068
+ return;
1069
+ }
1070
+ if (type === "PASSKEY_CLOSE" || type === "PASSKEY_DISCONNECT") {
1071
+ finish();
1072
+ }
1073
+ };
1074
+ const handleClose = () => {
1075
+ window.removeEventListener("message", handleMessage);
1076
+ resolve({ completed });
1077
+ };
1078
+ window.addEventListener("message", handleMessage);
1079
+ dialog.addEventListener("close", handleClose);
1080
+ });
1081
+ }
1082
+ /**
1083
+ * Check if a user has already granted consent for the requested fields.
1084
+ *
1085
+ * @deprecated Data-sharing consent is disabled.
1086
+ *
1087
+ * Always returns `{ hasConsent: false }`.
1088
+ */
1089
+ async checkConsent(options) {
1090
+ void options;
1091
+ return { hasConsent: false };
370
1092
  }
371
1093
  /**
372
1094
  * Request consent from the user to share their data.
373
1095
  *
374
- * First checks if consent was already granted (returns cached data immediately).
375
- * If not, opens the consent dialog where the user can review and approve sharing.
1096
+ * @deprecated Data-sharing consent is disabled.
376
1097
  *
377
- * @example
378
- * ```typescript
379
- * const result = await client.requestConsent({
380
- * username: "alice",
381
- * fields: ["email", "deviceNames"],
382
- * });
383
- * if (result.success) {
384
- * console.log(result.data?.email);
385
- * console.log(result.cached); // true if no dialog was shown
386
- * }
387
- * ```
1098
+ * Always returns an `INVALID_REQUEST` result.
388
1099
  */
389
1100
  async requestConsent(options) {
390
- const clientId = options.clientId ?? this.config.clientId;
391
- if (!clientId) {
1101
+ void options;
1102
+ return {
1103
+ success: false,
1104
+ error: {
1105
+ code: "INVALID_REQUEST",
1106
+ message: "Data-sharing consent is disabled"
1107
+ }
1108
+ };
1109
+ }
1110
+ /**
1111
+ * Open the dedicated SmartSession permission grant dialog.
1112
+ *
1113
+ * The app owns the session key material and sends only the public
1114
+ * `sessionKeyAddress`. 1auth renders the permission review, collects the
1115
+ * owner's passkey authorization, submits the install/enable intent, and
1116
+ * returns a persistable session handle.
1117
+ */
1118
+ async grantPermissions(options) {
1119
+ const telemetry = this.createTelemetryOperation("grant_permission", {
1120
+ targetChains: normalizeGrantTargetChains(options).join(","),
1121
+ sponsor: options.sponsor !== false
1122
+ });
1123
+ if (!options.username && !options.accountAddress) {
1124
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1125
+ outcome: "failure",
1126
+ errorCode: "INVALID_OPTIONS",
1127
+ errorMessage: "Either username or accountAddress is required"
1128
+ });
392
1129
  return {
393
1130
  success: false,
394
- error: { code: "INVALID_REQUEST", message: "clientId is required (set in config or options)" }
1131
+ error: {
1132
+ code: "INVALID_OPTIONS",
1133
+ message: "Either username or accountAddress is required"
1134
+ }
395
1135
  };
396
1136
  }
397
- const username = options.username ?? options.accountAddress;
398
- if (!username) {
1137
+ const targetChains = normalizeGrantTargetChains(options);
1138
+ const sourceChains = normalizeGrantSourceChains(options);
1139
+ if (targetChains.length === 0 || !options.sessionKeyAddress || !options.permissions?.length) {
1140
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1141
+ outcome: "failure",
1142
+ errorCode: "INVALID_OPTIONS",
1143
+ errorMessage: "targetChains, sessionKeyAddress, and permissions are required"
1144
+ });
399
1145
  return {
400
1146
  success: false,
401
- error: { code: "INVALID_REQUEST", message: "username or accountAddress is required" }
1147
+ error: {
1148
+ code: "INVALID_OPTIONS",
1149
+ message: "targetChains, sessionKeyAddress, and permissions are required"
1150
+ }
402
1151
  };
403
1152
  }
404
- if (!options.fields || options.fields.length === 0) {
1153
+ if (!this.sponsorship) {
1154
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1155
+ outcome: "failure",
1156
+ errorCode: "MISSING_APP_CREDENTIALS",
1157
+ errorMessage: MISSING_APP_CREDENTIALS_MESSAGE
1158
+ });
405
1159
  return {
406
1160
  success: false,
407
- error: { code: "INVALID_REQUEST", message: "At least one field is required" }
1161
+ error: {
1162
+ code: "MISSING_APP_CREDENTIALS",
1163
+ message: MISSING_APP_CREDENTIALS_MESSAGE
1164
+ }
408
1165
  };
409
1166
  }
410
- const existing = await this.checkConsent({ ...options, clientId });
411
- if (existing.hasConsent && existing.data) {
1167
+ const accessTokenResult = await this.fetchAccessToken();
1168
+ if (!accessTokenResult.ok) {
1169
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1170
+ outcome: "failure",
1171
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
1172
+ errorMessage: accessTokenResult.message
1173
+ });
412
1174
  return {
413
- success: true,
414
- data: existing.data,
415
- grantedAt: existing.grantedAt,
416
- cached: true
1175
+ success: false,
1176
+ error: {
1177
+ code: "SPONSORSHIP_FETCH_FAILED",
1178
+ message: accessTokenResult.message
1179
+ }
417
1180
  };
418
1181
  }
419
1182
  const dialogUrl = this.getDialogUrl();
420
- const params = new URLSearchParams({
421
- mode: "iframe",
422
- username,
423
- clientId,
424
- fields: options.fields.join(",")
425
- });
1183
+ const params = new URLSearchParams({ mode: "iframe" });
1184
+ this.appendTelemetryParams(params, telemetry);
426
1185
  const themeParams = this.getThemeParams(options.theme);
427
1186
  if (themeParams) {
428
1187
  const themeParsed = new URLSearchParams(themeParams);
429
1188
  themeParsed.forEach((value, key) => params.set(key, value));
430
1189
  }
431
- const url = `${dialogUrl}/dialog/consent?${params.toString()}`;
432
- const { dialog, iframe, cleanup } = this.createModalDialog(url);
433
- const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
434
- mode: "iframe"
1190
+ const grantUrl = `${dialogUrl}/dialog/grant-permission?${params.toString()}`;
1191
+ const { dialog, iframe, cleanup } = this.createModalDialog(grantUrl);
1192
+ const initPayload = {
1193
+ mode: "iframe",
1194
+ clientId: this.config.clientId,
1195
+ grant: cloneGrantPayload({
1196
+ username: options.username,
1197
+ accountAddress: options.accountAddress,
1198
+ targetChain: targetChains[0],
1199
+ targetChains,
1200
+ sourceChains,
1201
+ sessionKeyAddress: options.sessionKeyAddress,
1202
+ permissions: options.permissions,
1203
+ crossChainPermits: options.crossChainPermits,
1204
+ validUntil: options.validUntil,
1205
+ validAfter: options.validAfter,
1206
+ maxUses: options.maxUses,
1207
+ contracts: options.contracts
1208
+ }),
1209
+ auth: { accessToken: accessTokenResult.accessToken },
1210
+ sponsor: options.sponsor !== false,
1211
+ telemetry: this.telemetryPayload(telemetry)
1212
+ };
1213
+ this.emitTelemetry("dialog.opened", telemetry, {
1214
+ outcome: "started",
1215
+ targetChains
435
1216
  });
436
- if (!ready) {
1217
+ const result = await this.waitForGrantPermissionResponse(
1218
+ dialog,
1219
+ iframe,
1220
+ cleanup,
1221
+ options.sponsor !== false,
1222
+ initPayload
1223
+ );
1224
+ if (result.success) {
1225
+ this.emitTelemetry("grant_permission.succeeded", telemetry, {
1226
+ outcome: "success",
1227
+ targetChains,
1228
+ status: result.status ? String(result.status) : void 0
1229
+ });
1230
+ } else {
1231
+ this.emitTelemetry(
1232
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "grant_permission.failed",
1233
+ telemetry,
1234
+ {
1235
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
1236
+ targetChains,
1237
+ ...describeTelemetryError(result.error)
1238
+ }
1239
+ );
1240
+ }
1241
+ return result;
1242
+ }
1243
+ /**
1244
+ * List SmartSession grants previously granted to the current browser origin.
1245
+ *
1246
+ * 1auth derives the app identity from the request Origin header. The SDK does
1247
+ * not send `clientId` for this lookup because grants are origin-scoped.
1248
+ */
1249
+ async listSessionGrants(options) {
1250
+ if (!options.username && !options.accountAddress) {
437
1251
  return {
438
1252
  success: false,
439
- error: { code: "USER_CANCELLED", message: "User closed the dialog" }
1253
+ error: {
1254
+ code: "INVALID_OPTIONS",
1255
+ message: "Either username or accountAddress is required"
1256
+ }
1257
+ };
1258
+ }
1259
+ const params = new URLSearchParams();
1260
+ if (options.username) params.set("username", options.username);
1261
+ if (options.accountAddress) params.set("accountAddress", options.accountAddress);
1262
+ if (options.includeRevoked) params.set("includeRevoked", "true");
1263
+ try {
1264
+ const response = await fetch(
1265
+ `${this.config.providerUrl}/api/sessions/grants?${params.toString()}`,
1266
+ { credentials: "include" }
1267
+ );
1268
+ const data = await response.json();
1269
+ if (!response.ok) {
1270
+ return {
1271
+ success: false,
1272
+ error: {
1273
+ code: "LIST_SESSION_GRANTS_FAILED",
1274
+ message: data?.error ?? `Request failed with status ${response.status}`
1275
+ }
1276
+ };
1277
+ }
1278
+ return { success: true, grants: data.grants ?? [] };
1279
+ } catch (err) {
1280
+ return {
1281
+ success: false,
1282
+ error: {
1283
+ code: "LIST_SESSION_GRANTS_FAILED",
1284
+ message: err instanceof Error ? err.message : String(err)
1285
+ }
440
1286
  };
441
1287
  }
442
- return this.waitForConsentResponse(dialog, iframe, cleanup);
443
1288
  }
444
1289
  /**
445
1290
  * Authenticate a user with an optional challenge to sign.
@@ -448,13 +1293,13 @@ var OneAuthClient = class {
448
1293
  * challenge signing, enabling off-chain login without on-chain transactions.
449
1294
  *
450
1295
  * When a challenge is provided:
451
- * 1. User authenticates (sign in or sign up)
452
- * 2. The challenge is hashed with a domain separator
453
- * 3. User signs the hash with their passkey
454
- * 4. Returns user info + signature for server-side verification
1296
+ * 1. User authenticates (sign in or sign up) through the normal auth dialog
1297
+ * 2. User signs the challenge through the normal signing dialog
1298
+ * 3. Returns user info + signature for server-side verification
455
1299
  *
456
- * The domain separator ("\x19Passkey Signed Message:\n") ensures the signature
457
- * cannot be reused for transaction signing, preventing phishing attacks.
1300
+ * This composes the same supported provider routes as calling
1301
+ * `authWithModal()` followed by `signMessage()`, so challenge auth cannot
1302
+ * drift onto a provider route that the passkey service does not expose.
458
1303
  *
459
1304
  * @example
460
1305
  * ```typescript
@@ -473,42 +1318,130 @@ var OneAuthClient = class {
473
1318
  * ```
474
1319
  */
475
1320
  async authenticate(options) {
476
- const dialogUrl = this.getDialogUrl();
477
- const params = new URLSearchParams({
478
- mode: "iframe"
1321
+ const telemetry = this.createTelemetryOperation("authenticate", {
1322
+ hasChallenge: !!options?.challenge
479
1323
  });
480
- if (this.config.clientId) {
481
- params.set("clientId", this.config.clientId);
1324
+ const authResult = await this.authWithModal(options?.theme ? { theme: options.theme } : void 0);
1325
+ if (!authResult.success) {
1326
+ this.emitTelemetry(
1327
+ authResult.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "auth.failed",
1328
+ telemetry,
1329
+ {
1330
+ outcome: authResult.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
1331
+ ...describeTelemetryError(authResult.error)
1332
+ }
1333
+ );
1334
+ return authResult;
482
1335
  }
483
- if (options?.challenge) {
484
- params.set("challenge", options.challenge);
1336
+ if (!options?.challenge) {
1337
+ this.emitTelemetry("auth.succeeded", telemetry, {
1338
+ outcome: "success",
1339
+ attributes: { challengeSigned: false }
1340
+ });
1341
+ return authResult;
485
1342
  }
486
- const themeParams = this.getThemeParams(options?.theme);
487
- if (themeParams) {
488
- const themeParsed = new URLSearchParams(themeParams);
489
- themeParsed.forEach((value, key) => params.set(key, value));
1343
+ const accountAddress = authResult.user?.address;
1344
+ const username = authResult.user?.username;
1345
+ if (!accountAddress && !username) {
1346
+ const result2 = {
1347
+ success: false,
1348
+ user: authResult.user,
1349
+ signerType: authResult.signerType,
1350
+ error: {
1351
+ code: "AUTHENTICATE_USER_MISSING",
1352
+ message: "Authentication succeeded but no username or account address was returned for challenge signing"
1353
+ }
1354
+ };
1355
+ this.emitTelemetry("auth.failed", telemetry, {
1356
+ outcome: "failure",
1357
+ ...describeTelemetryError(result2.error)
1358
+ });
1359
+ return result2;
490
1360
  }
491
- const url = `${dialogUrl}/dialog/authenticate?${params.toString()}`;
492
- const { dialog, iframe, cleanup } = this.createModalDialog(url);
493
- return this.waitForAuthenticateResponse(dialog, iframe, cleanup);
1361
+ const signResult = await this.signMessage({
1362
+ username,
1363
+ accountAddress,
1364
+ message: options.challenge,
1365
+ description: "Verify your identity",
1366
+ theme: options.theme
1367
+ });
1368
+ if (signResult.success && signResult.signature && signResult.signedHash) {
1369
+ const result2 = {
1370
+ ...authResult,
1371
+ challenge: {
1372
+ signature: signResult.signature,
1373
+ signedHash: signResult.signedHash
1374
+ }
1375
+ };
1376
+ this.emitTelemetry("auth.succeeded", telemetry, {
1377
+ outcome: "success",
1378
+ attributes: { challengeSigned: true }
1379
+ });
1380
+ return result2;
1381
+ }
1382
+ const error = signResult.error ? { code: signResult.error.code, message: signResult.error.message } : { code: "SIGNING_FAILED", message: "Challenge signing failed" };
1383
+ const result = {
1384
+ success: false,
1385
+ user: authResult.user,
1386
+ signerType: authResult.signerType,
1387
+ error
1388
+ };
1389
+ const cancelled = error.code === "USER_CANCELLED" || error.code === "USER_REJECTED";
1390
+ this.emitTelemetry(
1391
+ cancelled ? "dialog.cancelled" : "auth.failed",
1392
+ telemetry,
1393
+ {
1394
+ outcome: cancelled ? "cancelled" : "failure",
1395
+ ...describeTelemetryError(error)
1396
+ }
1397
+ );
1398
+ return result;
494
1399
  }
495
1400
  /**
496
1401
  * Show signing in a modal overlay (iframe dialog)
497
1402
  */
498
1403
  async signWithModal(options) {
1404
+ const telemetry = this.createTelemetryOperation("sign", {
1405
+ signingMode: "transaction",
1406
+ hasTransaction: !!options.transaction,
1407
+ hasAccountAddress: !!options.accountAddress
1408
+ });
499
1409
  const dialogUrl = this.getDialogUrl();
1410
+ const params = new URLSearchParams({ mode: "iframe" });
1411
+ this.appendTelemetryParams(params, telemetry);
500
1412
  const themeParams = this.getThemeParams(options?.theme);
501
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
502
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
1413
+ if (themeParams) {
1414
+ const themeParsed = new URLSearchParams(themeParams);
1415
+ themeParsed.forEach((value, key) => params.set(key, value));
1416
+ }
1417
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
1418
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
1419
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
1420
+ hidden: requestedBlindSigning
1421
+ });
1422
+ this.emitTelemetry("dialog.opened", telemetry, {
1423
+ outcome: "started",
1424
+ attributes: { blindSigning: requestedBlindSigning }
1425
+ });
503
1426
  const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
504
1427
  mode: "iframe",
505
1428
  challenge: options.challenge,
506
1429
  username: options.username,
1430
+ accountAddress: options.accountAddress,
507
1431
  description: options.description,
508
1432
  transaction: options.transaction,
509
- metadata: options.metadata
1433
+ metadata: options.metadata,
1434
+ telemetry: this.telemetryPayload(telemetry)
1435
+ }, {
1436
+ blindSigning: requestedBlindSigning,
1437
+ reveal
510
1438
  });
511
1439
  if (!ready) {
1440
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1441
+ outcome: "cancelled",
1442
+ errorCode: "USER_REJECTED",
1443
+ errorMessage: "User closed the dialog before signing"
1444
+ });
512
1445
  return {
513
1446
  success: false,
514
1447
  error: {
@@ -517,7 +1450,21 @@ var OneAuthClient = class {
517
1450
  }
518
1451
  };
519
1452
  }
520
- return this.waitForSigningResponse(dialog, iframe, cleanup);
1453
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
1454
+ const result = await this.waitForSigningResponse(dialog, iframe, cleanup);
1455
+ if (result.success) {
1456
+ this.emitTelemetry("sign.succeeded", telemetry, { outcome: "success" });
1457
+ } else {
1458
+ this.emitTelemetry(
1459
+ result.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
1460
+ telemetry,
1461
+ {
1462
+ outcome: result.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
1463
+ ...describeTelemetryError(result.error)
1464
+ }
1465
+ );
1466
+ }
1467
+ return result;
521
1468
  }
522
1469
  /**
523
1470
  * Send an intent to the Rhinestone orchestrator
@@ -549,8 +1496,37 @@ var OneAuthClient = class {
549
1496
  * ```
550
1497
  */
551
1498
  async sendIntent(options) {
552
- const { username, accountAddress, targetChain, calls } = options;
1499
+ const { username, accountAddress, targetChain } = options;
1500
+ const calls = capCallLabels(options.calls);
1501
+ const sponsor = options.sponsor !== false;
1502
+ const telemetry = this.createTelemetryOperation("intent", {
1503
+ targetChain: targetChain ?? null,
1504
+ callsCount: calls?.length ?? 0,
1505
+ sponsor,
1506
+ blindSigning: this.shouldRequestBlindSigning(options)
1507
+ });
1508
+ if (getStoredSignerType() === "eoa") {
1509
+ this.emitTelemetry("intent.completed", telemetry, {
1510
+ outcome: "failure",
1511
+ errorCode: "E_SIGNER_UNSUPPORTED",
1512
+ errorMessage: "Cross-chain intents are not supported for EOA sessions"
1513
+ });
1514
+ return {
1515
+ success: false,
1516
+ intentId: "",
1517
+ status: "failed",
1518
+ error: {
1519
+ code: "E_SIGNER_UNSUPPORTED",
1520
+ message: "Cross-chain intents are not supported for EOA sessions. Use the connected wallet's native RPC instead."
1521
+ }
1522
+ };
1523
+ }
553
1524
  if (!username && !accountAddress) {
1525
+ this.emitTelemetry("intent.completed", telemetry, {
1526
+ outcome: "failure",
1527
+ errorCode: "INVALID_OPTIONS",
1528
+ errorMessage: "Either username or accountAddress is required"
1529
+ });
554
1530
  return {
555
1531
  success: false,
556
1532
  intentId: "",
@@ -561,14 +1537,37 @@ var OneAuthClient = class {
561
1537
  }
562
1538
  };
563
1539
  }
564
- if (!targetChain || !calls?.length) {
1540
+ const hasCalls = !!calls?.length;
1541
+ const hasTokenRequests = !!options.tokenRequests?.length;
1542
+ if (!targetChain || !hasCalls && !hasTokenRequests) {
1543
+ this.emitTelemetry("intent.completed", telemetry, {
1544
+ outcome: "failure",
1545
+ errorCode: "INVALID_OPTIONS",
1546
+ errorMessage: "targetChain and calls or tokenRequests are required"
1547
+ });
565
1548
  return {
566
1549
  success: false,
567
1550
  intentId: "",
568
1551
  status: "failed",
569
1552
  error: {
570
1553
  code: "INVALID_OPTIONS",
571
- message: "targetChain and calls are required"
1554
+ message: "targetChain and calls or tokenRequests are required"
1555
+ }
1556
+ };
1557
+ }
1558
+ if (!this.sponsorship) {
1559
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1560
+ outcome: "failure",
1561
+ errorCode: "MISSING_APP_CREDENTIALS",
1562
+ errorMessage: "No sponsorship configured on OneAuthClient"
1563
+ });
1564
+ return {
1565
+ success: false,
1566
+ intentId: "",
1567
+ status: "failed",
1568
+ error: {
1569
+ code: "MISSING_APP_CREDENTIALS",
1570
+ message: "No sponsorship configured on OneAuthClient. Every user intent must carry the app's JWT \u2014 configure `sponsorship` on the client."
572
1571
  }
573
1572
  };
574
1573
  }
@@ -577,6 +1576,111 @@ var OneAuthClient = class {
577
1576
  amount: r.amount.toString()
578
1577
  }));
579
1578
  let prepareResponse;
1579
+ const dialogUrl = this.getDialogUrl();
1580
+ const params = new URLSearchParams({ mode: "iframe" });
1581
+ this.appendTelemetryParams(params, telemetry);
1582
+ const themeParams = this.getThemeParams();
1583
+ if (themeParams) {
1584
+ const themeParsed = new URLSearchParams(themeParams);
1585
+ themeParsed.forEach((value, key) => params.set(key, value));
1586
+ }
1587
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
1588
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
1589
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
1590
+ hidden: requestedBlindSigning
1591
+ });
1592
+ this.emitTelemetry("dialog.opened", telemetry, {
1593
+ outcome: "started",
1594
+ attributes: { blindSigning: requestedBlindSigning }
1595
+ });
1596
+ const dialogOrigin = this.getDialogOrigin();
1597
+ const dialogOpenedAt = Date.now();
1598
+ let iframeReadyObserved = false;
1599
+ let stopEarlyCloseWatch = () => void 0;
1600
+ const earlyDialogClosePromise = new Promise((resolve) => {
1601
+ let settled = false;
1602
+ const stop = () => {
1603
+ if (settled) return;
1604
+ settled = true;
1605
+ window.removeEventListener("message", handleMessage);
1606
+ dialog.removeEventListener("close", handleClose);
1607
+ };
1608
+ stopEarlyCloseWatch = stop;
1609
+ const resolveClosed = (ready) => {
1610
+ stop();
1611
+ cleanup();
1612
+ resolve({ type: "dialog_closed", ready });
1613
+ };
1614
+ const handleMessage = (event) => {
1615
+ if (event.origin !== dialogOrigin) return;
1616
+ if (event.data?.type === "PASSKEY_CLOSE") {
1617
+ resolveClosed(iframeReadyObserved);
1618
+ }
1619
+ };
1620
+ const handleClose = () => {
1621
+ resolveClosed(iframeReadyObserved);
1622
+ };
1623
+ window.addEventListener("message", handleMessage);
1624
+ dialog.addEventListener("close", handleClose);
1625
+ });
1626
+ let dialogReadyDurationMs;
1627
+ const dialogReadyPromise = this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
1628
+ blindSigning: requestedBlindSigning,
1629
+ reveal
1630
+ }).then((result) => {
1631
+ dialogReadyDurationMs = Date.now() - dialogOpenedAt;
1632
+ iframeReadyObserved = result.ready;
1633
+ return result;
1634
+ });
1635
+ const dialogClosedBeforeReadyPromise = dialogReadyPromise.then(
1636
+ (result) => result.ready ? new Promise(() => void 0) : { type: "dialog_closed", ready: false }
1637
+ );
1638
+ const accessTokenStartedAt = Date.now();
1639
+ let accessTokenDurationMs;
1640
+ const accessTokenPromise = this.fetchAccessToken().then((result) => {
1641
+ accessTokenDurationMs = Date.now() - accessTokenStartedAt;
1642
+ return result;
1643
+ });
1644
+ const accessOrClose = await Promise.race([
1645
+ accessTokenPromise.then((result) => ({ type: "access_token", result })),
1646
+ dialogClosedBeforeReadyPromise,
1647
+ earlyDialogClosePromise
1648
+ ]);
1649
+ if (accessOrClose.type === "dialog_closed") {
1650
+ stopEarlyCloseWatch();
1651
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1652
+ outcome: "cancelled",
1653
+ errorCode: "USER_CANCELLED",
1654
+ errorMessage: accessOrClose.ready ? "User closed the dialog while intent setup was still loading" : "User closed the dialog before it became ready",
1655
+ attributes: {
1656
+ accessTokenDurationMs,
1657
+ dialogReadyDurationMs
1658
+ }
1659
+ });
1660
+ return {
1661
+ success: false,
1662
+ intentId: "",
1663
+ status: "failed",
1664
+ error: { code: "USER_CANCELLED", message: "User closed the dialog" }
1665
+ };
1666
+ }
1667
+ const accessTokenResult = accessOrClose.result;
1668
+ let accessTokenFailure = null;
1669
+ if (!accessTokenResult.ok) {
1670
+ const errorCode = accessTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED";
1671
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1672
+ outcome: "failure",
1673
+ errorCode,
1674
+ errorMessage: accessTokenResult.message,
1675
+ attributes: {
1676
+ accessTokenDurationMs,
1677
+ dialogReadyDurationMs,
1678
+ loadingPhase: "access_token"
1679
+ }
1680
+ });
1681
+ accessTokenFailure = { code: errorCode, message: accessTokenResult.message };
1682
+ }
1683
+ let accessToken = accessTokenResult.ok ? accessTokenResult.accessToken : "";
580
1684
  const requestBody = {
581
1685
  username,
582
1686
  accountAddress,
@@ -585,20 +1689,112 @@ var OneAuthClient = class {
585
1689
  tokenRequests: serializedTokenRequests,
586
1690
  sourceAssets: options.sourceAssets,
587
1691
  sourceChainId: options.sourceChainId,
1692
+ auth: { accessToken },
588
1693
  ...this.config.clientId && { clientId: this.config.clientId }
589
1694
  };
590
- const dialogUrl = this.getDialogUrl();
591
- const themeParams = this.getThemeParams();
592
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
593
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
594
- const dialogOrigin = this.getDialogOrigin();
595
1695
  let earlyPrepareResult = null;
596
- const preparePromise = this.prepareIntent(requestBody).then((r) => {
1696
+ let extensionTokensPromise = Promise.resolve({ ok: true, extensionTokens: [] });
1697
+ let extensionTokensDurationMs;
1698
+ this.emitTelemetry("intent.prepare.started", telemetry, {
1699
+ outcome: "started",
1700
+ targetChain,
1701
+ attributes: {
1702
+ accessTokenDurationMs,
1703
+ dialogReadyDurationMs
1704
+ }
1705
+ });
1706
+ const prepareStartedAt = Date.now();
1707
+ let prepareDurationMs;
1708
+ const runPrepare = () => this.prepareIntent(requestBody, telemetry).then((r) => {
597
1709
  earlyPrepareResult = r;
1710
+ prepareDurationMs = Date.now() - prepareStartedAt;
1711
+ if (r.success) {
1712
+ this.emitTelemetry("intent.prepare.succeeded", telemetry, {
1713
+ outcome: "success",
1714
+ targetChain,
1715
+ status: "quoted",
1716
+ attributes: {
1717
+ accessTokenDurationMs,
1718
+ dialogReadyDurationMs,
1719
+ prepareDurationMs
1720
+ }
1721
+ });
1722
+ const extensionTokensStartedAt = Date.now();
1723
+ extensionTokensPromise = this.fetchExtensionTokens(
1724
+ [r.data.intentOp],
1725
+ [sponsor]
1726
+ ).then((result) => {
1727
+ extensionTokensDurationMs = Date.now() - extensionTokensStartedAt;
1728
+ return result;
1729
+ });
1730
+ } else {
1731
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1732
+ outcome: "failure",
1733
+ targetChain,
1734
+ ...describeTelemetryError(r.error),
1735
+ attributes: {
1736
+ accessTokenDurationMs,
1737
+ dialogReadyDurationMs,
1738
+ prepareDurationMs,
1739
+ loadingPhase: "prepare"
1740
+ }
1741
+ });
1742
+ }
598
1743
  return r;
599
1744
  });
600
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
1745
+ const preparePromise = accessTokenFailure ? Promise.resolve({
1746
+ success: false,
1747
+ error: accessTokenFailure
1748
+ }).then((r) => {
1749
+ earlyPrepareResult = r;
1750
+ return r;
1751
+ }) : runPrepare();
1752
+ const retryPrepare = async () => {
1753
+ const retryTokenResult = await this.fetchAccessToken();
1754
+ if (!retryTokenResult.ok) {
1755
+ return {
1756
+ success: false,
1757
+ error: {
1758
+ code: retryTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED",
1759
+ message: retryTokenResult.message
1760
+ }
1761
+ };
1762
+ }
1763
+ accessToken = retryTokenResult.accessToken;
1764
+ requestBody.auth = { accessToken };
1765
+ return runPrepare();
1766
+ };
1767
+ const readyOrClose = await Promise.race([
1768
+ dialogReadyPromise.then((result) => ({ type: "ready", result })),
1769
+ earlyDialogClosePromise
1770
+ ]);
1771
+ if (readyOrClose.type === "dialog_closed") {
1772
+ stopEarlyCloseWatch();
1773
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1774
+ outcome: "cancelled",
1775
+ errorCode: "USER_CANCELLED",
1776
+ errorMessage: readyOrClose.ready ? "User closed the dialog while intent setup was still loading" : "User closed the dialog before it became ready",
1777
+ attributes: {
1778
+ accessTokenDurationMs,
1779
+ dialogReadyDurationMs,
1780
+ prepareDurationMs
1781
+ }
1782
+ });
1783
+ return {
1784
+ success: false,
1785
+ intentId: "",
1786
+ status: "failed",
1787
+ error: { code: "USER_CANCELLED", message: "User closed the dialog" }
1788
+ };
1789
+ }
1790
+ const dialogResult = readyOrClose.result;
601
1791
  if (!dialogResult.ready) {
1792
+ stopEarlyCloseWatch();
1793
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1794
+ outcome: "cancelled",
1795
+ errorCode: "USER_CANCELLED",
1796
+ errorMessage: "User closed the dialog before it became ready"
1797
+ });
602
1798
  return {
603
1799
  success: false,
604
1800
  intentId: "",
@@ -606,126 +1802,348 @@ var OneAuthClient = class {
606
1802
  error: { code: "USER_CANCELLED", message: "User closed the dialog" }
607
1803
  };
608
1804
  }
1805
+ const blindSigning = dialogResult.blindSigning;
1806
+ this.emitTelemetry("dialog.ready", telemetry, {
1807
+ outcome: "started",
1808
+ attributes: {
1809
+ accessTokenDurationMs,
1810
+ dialogReadyDurationMs
1811
+ }
1812
+ });
609
1813
  let currentInitPayload;
610
- if (earlyPrepareResult) {
611
- const prepareResult = earlyPrepareResult;
612
- if (!prepareResult.success) {
613
- this.sendPrepareError(iframe, prepareResult.error.message);
614
- await this.waitForDialogClose(dialog, cleanup);
1814
+ const refreshQuote = async () => {
1815
+ try {
1816
+ const refreshedTokenResult = await this.fetchAccessToken();
1817
+ const refreshToken = refreshedTokenResult.ok ? refreshedTokenResult.accessToken : accessToken;
1818
+ const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
1819
+ method: "POST",
1820
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1821
+ body: JSON.stringify({ ...requestBody, auth: { accessToken: refreshToken } }),
1822
+ credentials: "include"
1823
+ });
1824
+ if (!refreshResponse.ok) {
1825
+ console.error("[SDK] Quote refresh failed:", await refreshResponse.text());
1826
+ return null;
1827
+ }
1828
+ const refreshedData = await refreshResponse.json();
1829
+ const refreshedExtensionPromise = this.fetchExtensionTokens(
1830
+ [refreshedData.intentOp],
1831
+ [sponsor]
1832
+ );
1833
+ const extensionResult = await refreshedExtensionPromise;
1834
+ if (!extensionResult.ok) {
1835
+ console.error(
1836
+ "[SDK] Extension token refresh failed:",
1837
+ extensionResult.message
1838
+ );
1839
+ return null;
1840
+ }
1841
+ const refreshedAuth = {
1842
+ accessToken: refreshToken,
1843
+ extensionTokens: extensionResult.extensionTokens
1844
+ };
1845
+ prepareResponse = refreshedData;
1846
+ accessToken = refreshToken;
1847
+ requestBody.auth = { accessToken: refreshToken };
1848
+ extensionTokensPromise = refreshedExtensionPromise;
1849
+ currentInitPayload = {
1850
+ ...currentInitPayload,
1851
+ transaction: refreshedData.transaction,
1852
+ challenge: refreshedData.challenge,
1853
+ originMessages: refreshedData.originMessages,
1854
+ expiresAt: refreshedData.expiresAt,
1855
+ intentOp: refreshedData.intentOp,
1856
+ digestResult: refreshedData.digestResult,
1857
+ auth: refreshedAuth
1858
+ };
615
1859
  return {
616
- success: false,
617
- intentId: "",
618
- status: "failed",
619
- error: prepareResult.error
1860
+ intentOp: refreshedData.intentOp,
1861
+ expiresAt: refreshedData.expiresAt,
1862
+ challenge: refreshedData.challenge,
1863
+ originMessages: refreshedData.originMessages,
1864
+ transaction: refreshedData.transaction,
1865
+ digestResult: refreshedData.digestResult,
1866
+ // Dialog-executed intents read auth from dialog state — ship the
1867
+ // rebound bundle with the refresh so PASSKEY_REFRESH_COMPLETE
1868
+ // updates it alongside the quote.
1869
+ auth: refreshedAuth
620
1870
  };
1871
+ } catch (error) {
1872
+ console.error("[SDK] Quote refresh error:", error);
1873
+ return null;
1874
+ }
1875
+ };
1876
+ const signingResultPromise = this.waitForSigningWithRefresh(
1877
+ dialog,
1878
+ iframe,
1879
+ cleanup,
1880
+ dialogOrigin,
1881
+ refreshQuote
1882
+ );
1883
+ stopEarlyCloseWatch();
1884
+ if (earlyPrepareResult) {
1885
+ let prepareResult = earlyPrepareResult;
1886
+ for (; ; ) {
1887
+ while (!prepareResult.success) {
1888
+ this.sendPrepareError(iframe, prepareResult.error.message);
1889
+ if (blindSigning) {
1890
+ cleanup();
1891
+ return {
1892
+ success: false,
1893
+ intentId: "",
1894
+ status: "failed",
1895
+ error: prepareResult.error
1896
+ };
1897
+ }
1898
+ const next2 = await this.waitForPrepareRetryOrClose(dialog, cleanup);
1899
+ if (next2 === "closed") {
1900
+ return {
1901
+ success: false,
1902
+ intentId: "",
1903
+ status: "failed",
1904
+ error: prepareResult.error
1905
+ };
1906
+ }
1907
+ prepareResult = await retryPrepare();
1908
+ }
1909
+ prepareResponse = prepareResult.data;
1910
+ const extensionResult = await extensionTokensPromise;
1911
+ if (extensionResult.ok) {
1912
+ const sponsorshipTokens = {
1913
+ accessToken,
1914
+ extensionTokens: extensionResult.extensionTokens
1915
+ };
1916
+ currentInitPayload = {
1917
+ mode: "iframe",
1918
+ calls,
1919
+ chainId: targetChain,
1920
+ transaction: prepareResponse.transaction,
1921
+ challenge: prepareResponse.challenge,
1922
+ username,
1923
+ accountAddress: prepareResponse.accountAddress,
1924
+ originMessages: prepareResponse.originMessages,
1925
+ tokenRequests: serializedTokenRequests,
1926
+ expiresAt: prepareResponse.expiresAt,
1927
+ userId: prepareResponse.userId,
1928
+ intentOp: prepareResponse.intentOp,
1929
+ digestResult: prepareResponse.digestResult,
1930
+ tier: prepareResult.tier,
1931
+ auth: sponsorshipTokens,
1932
+ telemetry: this.telemetryPayload(telemetry)
1933
+ };
1934
+ dialogResult.sendInit(currentInitPayload);
1935
+ break;
1936
+ }
1937
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1938
+ outcome: "failure",
1939
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
1940
+ errorMessage: extensionResult.message,
1941
+ attributes: {
1942
+ accessTokenDurationMs,
1943
+ dialogReadyDurationMs,
1944
+ prepareDurationMs,
1945
+ extensionTokensDurationMs,
1946
+ loadingPhase: "extension_token"
1947
+ }
1948
+ });
1949
+ this.sendPrepareError(iframe, extensionResult.message);
1950
+ if (blindSigning) {
1951
+ cleanup();
1952
+ return {
1953
+ success: false,
1954
+ intentId: "",
1955
+ status: "failed",
1956
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
1957
+ };
1958
+ }
1959
+ const next = await this.waitForPrepareRetryOrClose(dialog, cleanup);
1960
+ if (next === "closed") {
1961
+ return {
1962
+ success: false,
1963
+ intentId: "",
1964
+ status: "failed",
1965
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
1966
+ };
1967
+ }
1968
+ prepareResult = await retryPrepare();
621
1969
  }
622
- prepareResponse = prepareResult.data;
623
- currentInitPayload = {
624
- mode: "iframe",
625
- calls,
626
- chainId: targetChain,
627
- transaction: prepareResponse.transaction,
628
- challenge: prepareResponse.challenge,
629
- username,
630
- accountAddress: prepareResponse.accountAddress,
631
- originMessages: prepareResponse.originMessages,
632
- tokenRequests: serializedTokenRequests,
633
- expiresAt: prepareResponse.expiresAt,
634
- userId: prepareResponse.userId,
635
- intentOp: prepareResponse.intentOp,
636
- digestResult: prepareResponse.digestResult,
637
- tier: prepareResult.tier
638
- };
639
- dialogResult.sendInit(currentInitPayload);
640
1970
  } else {
641
1971
  currentInitPayload = {
642
1972
  mode: "iframe",
1973
+ signingMode: "transaction",
643
1974
  calls,
644
1975
  chainId: targetChain,
645
1976
  username,
646
1977
  accountAddress: options.accountAddress,
647
- tokenRequests: serializedTokenRequests
1978
+ tokenRequests: serializedTokenRequests,
1979
+ telemetry: this.telemetryPayload(telemetry)
648
1980
  };
649
1981
  dialogResult.sendInit(currentInitPayload);
650
- const prepareResult = await preparePromise;
651
- if (!prepareResult.success) {
652
- this.sendPrepareError(iframe, prepareResult.error.message);
653
- await this.waitForDialogClose(dialog, cleanup);
654
- return {
655
- success: false,
656
- intentId: "",
657
- status: "failed",
658
- error: prepareResult.error
659
- };
1982
+ const prepareOrCancel = await Promise.race([
1983
+ preparePromise.then((prepareResult2) => ({
1984
+ type: "prepare",
1985
+ prepareResult: prepareResult2
1986
+ })),
1987
+ signingResultPromise.then((signingResult2) => ({
1988
+ type: "signing",
1989
+ signingResult: signingResult2
1990
+ }))
1991
+ ]);
1992
+ let prepareResult;
1993
+ if (prepareOrCancel.type === "signing") {
1994
+ const earlySigningResult = prepareOrCancel.signingResult;
1995
+ if (earlySigningResult.success) {
1996
+ prepareResult = await preparePromise;
1997
+ } else {
1998
+ const signingError = earlySigningResult.error;
1999
+ this.emitTelemetry(
2000
+ signingError?.code === "USER_REJECTED" ? "dialog.cancelled" : "intent.sign.failed",
2001
+ telemetry,
2002
+ {
2003
+ outcome: signingError?.code === "USER_REJECTED" ? "cancelled" : "failure",
2004
+ ...describeTelemetryError(signingError)
2005
+ }
2006
+ );
2007
+ if (blindSigning) cleanup();
2008
+ return {
2009
+ success: false,
2010
+ intentId: "",
2011
+ status: "failed",
2012
+ error: signingError
2013
+ };
2014
+ }
2015
+ } else {
2016
+ prepareResult = prepareOrCancel.prepareResult;
660
2017
  }
661
- prepareResponse = prepareResult.data;
662
- currentInitPayload = {
663
- mode: "iframe",
664
- calls,
665
- chainId: targetChain,
666
- transaction: prepareResponse.transaction,
667
- challenge: prepareResponse.challenge,
668
- username,
669
- accountAddress: prepareResponse.accountAddress,
670
- originMessages: prepareResponse.originMessages,
671
- tokenRequests: serializedTokenRequests,
672
- expiresAt: prepareResponse.expiresAt,
673
- userId: prepareResponse.userId,
674
- intentOp: prepareResponse.intentOp,
675
- digestResult: prepareResponse.digestResult,
676
- tier: prepareResult.tier
677
- };
678
- iframe.contentWindow?.postMessage(
679
- { type: "PASSKEY_INIT", ...currentInitPayload, fullViewport: true },
680
- dialogOrigin
681
- );
682
- }
683
- const handleReReady = (event) => {
684
- if (event.origin !== dialogOrigin) return;
685
- if (event.data?.type === "PASSKEY_READY") {
2018
+ for (; ; ) {
2019
+ while (!prepareResult.success) {
2020
+ this.sendPrepareError(iframe, prepareResult.error.message);
2021
+ if (blindSigning) {
2022
+ cleanup();
2023
+ return {
2024
+ success: false,
2025
+ intentId: "",
2026
+ status: "failed",
2027
+ error: prepareResult.error
2028
+ };
2029
+ }
2030
+ const next2 = await this.waitForPrepareRetryOrClose(dialog, cleanup);
2031
+ if (next2 === "closed") {
2032
+ return {
2033
+ success: false,
2034
+ intentId: "",
2035
+ status: "failed",
2036
+ error: prepareResult.error
2037
+ };
2038
+ }
2039
+ prepareResult = await retryPrepare();
2040
+ }
2041
+ prepareResponse = prepareResult.data;
2042
+ const authReady = !sponsor ? {
2043
+ accessToken,
2044
+ extensionTokens: []
2045
+ } : void 0;
2046
+ currentInitPayload = {
2047
+ mode: "iframe",
2048
+ calls,
2049
+ chainId: targetChain,
2050
+ transaction: prepareResponse.transaction,
2051
+ challenge: prepareResponse.challenge,
2052
+ username,
2053
+ accountAddress: prepareResponse.accountAddress,
2054
+ originMessages: prepareResponse.originMessages,
2055
+ tokenRequests: serializedTokenRequests,
2056
+ expiresAt: prepareResponse.expiresAt,
2057
+ userId: prepareResponse.userId,
2058
+ intentOp: prepareResponse.intentOp,
2059
+ digestResult: prepareResponse.digestResult,
2060
+ tier: prepareResult.tier,
2061
+ ...authReady && { auth: authReady },
2062
+ telemetry: this.telemetryPayload(telemetry)
2063
+ };
686
2064
  iframe.contentWindow?.postMessage(
687
- { type: "PASSKEY_INIT", ...currentInitPayload, fullViewport: true },
2065
+ { type: "PASSKEY_QUOTE_READY", ...currentInitPayload, fullViewport: true, blindSigning },
688
2066
  dialogOrigin
689
2067
  );
690
- }
691
- };
692
- window.addEventListener("message", handleReReady);
693
- const signingResult = await this.waitForSigningWithRefresh(
694
- dialog,
695
- iframe,
696
- cleanup,
697
- dialogOrigin,
698
- // Refresh callback - called when dialog requests a quote refresh
699
- async () => {
700
- try {
701
- const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
702
- method: "POST",
703
- headers: { "Content-Type": "application/json" },
704
- body: JSON.stringify(requestBody),
705
- credentials: "include"
706
- });
707
- if (!refreshResponse.ok) {
708
- console.error("[SDK] Quote refresh failed:", await refreshResponse.text());
709
- return null;
2068
+ if (!sponsor) break;
2069
+ const extensionResult = await extensionTokensPromise;
2070
+ if (extensionResult.ok) {
2071
+ const sponsorshipTokens = {
2072
+ accessToken,
2073
+ extensionTokens: extensionResult.extensionTokens
2074
+ };
2075
+ currentInitPayload = {
2076
+ ...currentInitPayload,
2077
+ auth: sponsorshipTokens
2078
+ };
2079
+ iframe.contentWindow?.postMessage(
2080
+ { type: "PASSKEY_QUOTE_READY", auth: sponsorshipTokens, fullViewport: true, blindSigning },
2081
+ dialogOrigin
2082
+ );
2083
+ break;
2084
+ }
2085
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
2086
+ outcome: "failure",
2087
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
2088
+ errorMessage: extensionResult.message,
2089
+ attributes: {
2090
+ accessTokenDurationMs,
2091
+ dialogReadyDurationMs,
2092
+ prepareDurationMs,
2093
+ extensionTokensDurationMs,
2094
+ loadingPhase: "extension_token"
710
2095
  }
711
- const refreshedData = await refreshResponse.json();
712
- prepareResponse = refreshedData;
2096
+ });
2097
+ this.sendPrepareError(iframe, extensionResult.message);
2098
+ if (blindSigning) {
2099
+ cleanup();
713
2100
  return {
714
- intentOp: refreshedData.intentOp,
715
- expiresAt: refreshedData.expiresAt,
716
- challenge: refreshedData.challenge,
717
- originMessages: refreshedData.originMessages,
718
- transaction: refreshedData.transaction,
719
- digestResult: refreshedData.digestResult
2101
+ success: false,
2102
+ intentId: "",
2103
+ status: "failed",
2104
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
720
2105
  };
721
- } catch (error) {
722
- console.error("[SDK] Quote refresh error:", error);
723
- return null;
724
2106
  }
2107
+ const next = await this.waitForPrepareRetryOrClose(dialog, cleanup);
2108
+ if (next === "closed") {
2109
+ return {
2110
+ success: false,
2111
+ intentId: "",
2112
+ status: "failed",
2113
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2114
+ };
2115
+ }
2116
+ prepareResult = await retryPrepare();
2117
+ }
2118
+ }
2119
+ const handleReReady = (event) => {
2120
+ if (event.origin !== dialogOrigin) return;
2121
+ if (event.data?.type === "PASSKEY_READY") {
2122
+ iframe.contentWindow?.postMessage(
2123
+ { type: "PASSKEY_INIT", ...currentInitPayload, fullViewport: true, blindSigning },
2124
+ dialogOrigin
2125
+ );
725
2126
  }
726
- );
2127
+ };
2128
+ window.addEventListener("message", handleReReady);
2129
+ const signingResult = await signingResultPromise;
727
2130
  window.removeEventListener("message", handleReReady);
728
2131
  if (!signingResult.success) {
2132
+ this.emitTelemetry(
2133
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "intent.sign.failed",
2134
+ telemetry,
2135
+ {
2136
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
2137
+ ...describeTelemetryError(signingResult.error),
2138
+ attributes: {
2139
+ accessTokenDurationMs,
2140
+ dialogReadyDurationMs,
2141
+ prepareDurationMs,
2142
+ extensionTokensDurationMs
2143
+ }
2144
+ }
2145
+ );
2146
+ if (blindSigning) cleanup();
729
2147
  return {
730
2148
  success: false,
731
2149
  intentId: "",
@@ -742,12 +2160,67 @@ var OneAuthClient = class {
742
2160
  intentId: signingResult.intentId,
743
2161
  status: "pending"
744
2162
  };
2163
+ this.emitTelemetry("intent.sign.succeeded", telemetry, {
2164
+ outcome: "success",
2165
+ attributes: {
2166
+ accessTokenDurationMs,
2167
+ dialogReadyDurationMs,
2168
+ prepareDurationMs,
2169
+ extensionTokensDurationMs,
2170
+ dialogExecuted: true
2171
+ }
2172
+ });
745
2173
  } else {
2174
+ const extensionResult = await extensionTokensPromise;
2175
+ if (!extensionResult.ok) {
2176
+ this.emitTelemetry("intent.sign.failed", telemetry, {
2177
+ outcome: "failure",
2178
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
2179
+ errorMessage: extensionResult.message,
2180
+ attributes: {
2181
+ accessTokenDurationMs,
2182
+ dialogReadyDurationMs,
2183
+ prepareDurationMs,
2184
+ extensionTokensDurationMs,
2185
+ loadingPhase: "extension_token"
2186
+ }
2187
+ });
2188
+ this.sendTransactionStatus(iframe, "failed");
2189
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2190
+ return {
2191
+ success: false,
2192
+ intentId: "",
2193
+ status: "failed",
2194
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2195
+ };
2196
+ }
2197
+ const extensionTokens = extensionResult.extensionTokens;
746
2198
  try {
2199
+ this.emitTelemetry("intent.sign.succeeded", telemetry, {
2200
+ outcome: "success",
2201
+ attributes: {
2202
+ accessTokenDurationMs,
2203
+ dialogReadyDurationMs,
2204
+ prepareDurationMs,
2205
+ extensionTokensDurationMs,
2206
+ dialogExecuted: false
2207
+ }
2208
+ });
2209
+ this.emitTelemetry("intent.execute.started", telemetry, {
2210
+ outcome: "started",
2211
+ targetChain: prepareResponse.targetChain,
2212
+ attributes: {
2213
+ accessTokenDurationMs,
2214
+ dialogReadyDurationMs,
2215
+ prepareDurationMs,
2216
+ extensionTokensDurationMs
2217
+ }
2218
+ });
747
2219
  const response = await fetch(`${this.config.providerUrl}/api/intent/execute`, {
748
2220
  method: "POST",
749
2221
  headers: {
750
- "Content-Type": "application/json"
2222
+ "Content-Type": "application/json",
2223
+ ...this.telemetryHeaders(telemetry)
751
2224
  },
752
2225
  body: JSON.stringify({
753
2226
  // Data from prepare response (no intentId yet - created on execute)
@@ -759,14 +2232,28 @@ var OneAuthClient = class {
759
2232
  digestResult: prepareResponse.digestResult,
760
2233
  // Signature from dialog
761
2234
  signature: signingResult.signature,
762
- passkey: signingResult.passkey
2235
+ passkey: signingResult.passkey,
763
2236
  // Include passkey info for signature encoding
2237
+ // App JWT — `accessToken` is always present. `extensionToken` is
2238
+ // only included when sponsorship was requested (sponsor !== false).
2239
+ auth: {
2240
+ accessToken,
2241
+ ...extensionTokens.length > 0 && {
2242
+ extensionToken: extensionTokens[0]
2243
+ }
2244
+ }
764
2245
  })
765
2246
  });
766
2247
  if (!response.ok) {
767
2248
  const errorData = await response.json().catch(() => ({}));
768
2249
  this.sendTransactionStatus(iframe, "failed");
769
- await this.waitForDialogClose(dialog, cleanup);
2250
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2251
+ this.emitTelemetry("intent.execute.failed", telemetry, {
2252
+ outcome: "failure",
2253
+ targetChain: prepareResponse.targetChain,
2254
+ errorCode: "EXECUTE_FAILED",
2255
+ errorMessage: errorData.error || "Failed to execute intent"
2256
+ });
770
2257
  return {
771
2258
  success: false,
772
2259
  intentId: "",
@@ -779,9 +2266,19 @@ var OneAuthClient = class {
779
2266
  };
780
2267
  }
781
2268
  executeResponse = await response.json();
2269
+ this.emitTelemetry("intent.execute.succeeded", telemetry, {
2270
+ outcome: "success",
2271
+ targetChain: prepareResponse.targetChain,
2272
+ status: executeResponse.status
2273
+ });
782
2274
  } catch (error) {
783
2275
  this.sendTransactionStatus(iframe, "failed");
784
- await this.waitForDialogClose(dialog, cleanup);
2276
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2277
+ this.emitTelemetry("intent.execute.failed", telemetry, {
2278
+ outcome: "failure",
2279
+ targetChain: prepareResponse.targetChain,
2280
+ ...describeTelemetryError(error)
2281
+ });
785
2282
  return {
786
2283
  success: false,
787
2284
  intentId: "",
@@ -798,6 +2295,10 @@ var OneAuthClient = class {
798
2295
  let finalTxHash = executeResponse.transactionHash;
799
2296
  if (finalStatus === "pending") {
800
2297
  this.sendTransactionStatus(iframe, "pending");
2298
+ this.emitTelemetry("intent.status.started", telemetry, {
2299
+ outcome: "started",
2300
+ status: finalStatus
2301
+ });
801
2302
  let userClosedEarly = false;
802
2303
  const dialogOrigin2 = this.getDialogOrigin();
803
2304
  const earlyCloseHandler = (event) => {
@@ -818,7 +2319,10 @@ var OneAuthClient = class {
818
2319
  `${this.config.providerUrl}/api/intent/status/${executeResponse.intentId}`,
819
2320
  {
820
2321
  method: "GET",
821
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
2322
+ headers: {
2323
+ ...this.telemetryHeaders(telemetry),
2324
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
2325
+ }
822
2326
  }
823
2327
  );
824
2328
  if (statusResponse.ok) {
@@ -850,6 +2354,12 @@ var OneAuthClient = class {
850
2354
  window.removeEventListener("message", earlyCloseHandler);
851
2355
  if (userClosedEarly) {
852
2356
  cleanup();
2357
+ this.emitTelemetry("intent.status.failed", telemetry, {
2358
+ outcome: "cancelled",
2359
+ status: finalStatus,
2360
+ errorCode: "USER_CANCELLED",
2361
+ errorMessage: "User closed the dialog while status polling"
2362
+ });
853
2363
  return {
854
2364
  success: false,
855
2365
  intentId: executeResponse.intentId,
@@ -872,19 +2382,25 @@ var OneAuthClient = class {
872
2382
  };
873
2383
  const isSuccessStatus = successStatuses[closeOn]?.includes(finalStatus) ?? false;
874
2384
  const displayStatus = isSuccessStatus ? "confirmed" : finalStatus;
875
- const closePromise = this.waitForDialogClose(dialog, cleanup);
2385
+ const closePromise = this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
876
2386
  this.sendTransactionStatus(iframe, displayStatus, finalTxHash);
877
2387
  await closePromise;
878
2388
  if (options.waitForHash && !finalTxHash) {
879
2389
  const hash = await this.waitForTransactionHash(executeResponse.intentId, {
880
2390
  timeoutMs: options.hashTimeoutMs,
881
2391
  intervalMs: options.hashIntervalMs
882
- });
2392
+ }, telemetry);
883
2393
  if (hash) {
884
2394
  finalTxHash = hash;
885
2395
  finalStatus = "completed";
886
2396
  } else {
887
2397
  finalStatus = "failed";
2398
+ this.emitTelemetry("intent.status.failed", telemetry, {
2399
+ outcome: "failure",
2400
+ status: finalStatus,
2401
+ errorCode: "HASH_TIMEOUT",
2402
+ errorMessage: "Timed out waiting for transaction hash"
2403
+ });
888
2404
  return {
889
2405
  success: false,
890
2406
  intentId: executeResponse.intentId,
@@ -898,6 +2414,15 @@ var OneAuthClient = class {
898
2414
  };
899
2415
  }
900
2416
  }
2417
+ this.emitTelemetry("intent.status.succeeded", telemetry, {
2418
+ outcome: isSuccessStatus ? "success" : "failure",
2419
+ status: finalStatus
2420
+ });
2421
+ this.emitTelemetry("intent.completed", telemetry, {
2422
+ outcome: isSuccessStatus ? "success" : "failure",
2423
+ status: finalStatus,
2424
+ targetChain
2425
+ });
901
2426
  return {
902
2427
  success: isSuccessStatus,
903
2428
  intentId: executeResponse.intentId,
@@ -935,7 +2460,30 @@ var OneAuthClient = class {
935
2460
  * ```
936
2461
  */
937
2462
  async sendBatchIntent(options) {
2463
+ const telemetry = this.createTelemetryOperation("batch_intent", {
2464
+ intentCount: options.intents?.length ?? 0,
2465
+ blindSigning: this.shouldRequestBlindSigning(options)
2466
+ });
2467
+ if (getStoredSignerType() === "eoa") {
2468
+ this.emitTelemetry("batch.completed", telemetry, {
2469
+ outcome: "failure",
2470
+ errorCode: "E_SIGNER_UNSUPPORTED",
2471
+ errorMessage: "Batch intents are not supported for EOA sessions"
2472
+ });
2473
+ return {
2474
+ success: false,
2475
+ results: [],
2476
+ successCount: 0,
2477
+ failureCount: 0,
2478
+ error: "Batch intents are not supported for EOA sessions. Use the connected wallet's native RPC instead."
2479
+ };
2480
+ }
938
2481
  if (!options.username && !options.accountAddress) {
2482
+ this.emitTelemetry("batch.completed", telemetry, {
2483
+ outcome: "failure",
2484
+ errorCode: "INVALID_OPTIONS",
2485
+ errorMessage: "Either username or accountAddress is required"
2486
+ });
939
2487
  return {
940
2488
  success: false,
941
2489
  results: [],
@@ -944,6 +2492,11 @@ var OneAuthClient = class {
944
2492
  };
945
2493
  }
946
2494
  if (!options.intents?.length) {
2495
+ this.emitTelemetry("batch.completed", telemetry, {
2496
+ outcome: "failure",
2497
+ errorCode: "INVALID_OPTIONS",
2498
+ errorMessage: "At least one intent is required"
2499
+ });
947
2500
  return {
948
2501
  success: false,
949
2502
  results: [],
@@ -951,9 +2504,40 @@ var OneAuthClient = class {
951
2504
  failureCount: 0
952
2505
  };
953
2506
  }
2507
+ if (!this.sponsorship) {
2508
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2509
+ outcome: "failure",
2510
+ errorCode: "MISSING_APP_CREDENTIALS",
2511
+ errorMessage: "No sponsorship configured on OneAuthClient"
2512
+ });
2513
+ return {
2514
+ success: false,
2515
+ results: [],
2516
+ successCount: 0,
2517
+ failureCount: 0,
2518
+ error: "No sponsorship configured on OneAuthClient. Every user intent must carry the app's JWT \u2014 configure `sponsorship` on the client."
2519
+ };
2520
+ }
2521
+ const sponsorFlags = options.intents.map((intent) => intent.sponsor !== false);
2522
+ const accessTokenResult = await this.fetchAccessToken();
2523
+ if (!accessTokenResult.ok) {
2524
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2525
+ outcome: "failure",
2526
+ errorCode: accessTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED",
2527
+ errorMessage: accessTokenResult.message
2528
+ });
2529
+ return {
2530
+ success: false,
2531
+ results: [],
2532
+ successCount: 0,
2533
+ failureCount: 0,
2534
+ error: accessTokenResult.message
2535
+ };
2536
+ }
2537
+ let accessToken = accessTokenResult.accessToken;
954
2538
  const serializedIntents = options.intents.map((intent) => ({
955
2539
  targetChain: intent.targetChain,
956
- calls: intent.calls,
2540
+ calls: capCallLabels(intent.calls),
957
2541
  tokenRequests: intent.tokenRequests?.map((r) => ({
958
2542
  token: r.token,
959
2543
  amount: r.amount.toString()
@@ -966,20 +2550,66 @@ var OneAuthClient = class {
966
2550
  ...options.username && { username: options.username },
967
2551
  ...options.accountAddress && { accountAddress: options.accountAddress },
968
2552
  intents: serializedIntents,
2553
+ auth: { accessToken },
969
2554
  ...this.config.clientId && { clientId: this.config.clientId }
970
2555
  };
971
2556
  const dialogUrl = this.getDialogUrl();
2557
+ const params = new URLSearchParams({ mode: "iframe" });
2558
+ this.appendTelemetryParams(params, telemetry);
972
2559
  const themeParams = this.getThemeParams();
973
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
974
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
2560
+ if (themeParams) {
2561
+ const themeParsed = new URLSearchParams(themeParams);
2562
+ themeParsed.forEach((value, key) => params.set(key, value));
2563
+ }
2564
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
2565
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
2566
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
2567
+ hidden: requestedBlindSigning
2568
+ });
2569
+ this.emitTelemetry("dialog.opened", telemetry, {
2570
+ outcome: "started",
2571
+ intentCount: options.intents.length,
2572
+ attributes: { blindSigning: requestedBlindSigning }
2573
+ });
975
2574
  const dialogOrigin = this.getDialogOrigin();
976
2575
  let earlyBatchResult = null;
977
- const preparePromise = this.prepareBatchIntent(requestBody).then((r) => {
2576
+ let batchExtensionTokensPromise = Promise.resolve({ ok: true, extensionTokens: [] });
2577
+ this.emitTelemetry("batch.prepare.started", telemetry, {
2578
+ outcome: "started",
2579
+ intentCount: options.intents.length,
2580
+ targetChains: serializedIntents.map((intent) => intent.targetChain)
2581
+ });
2582
+ const preparePromise = this.prepareBatchIntent(requestBody, telemetry).then((r) => {
978
2583
  earlyBatchResult = r;
2584
+ if (r.success) {
2585
+ this.emitTelemetry("batch.prepare.succeeded", telemetry, {
2586
+ outcome: "success",
2587
+ intentCount: r.data.intents.length,
2588
+ targetChains: serializedIntents.map((intent) => intent.targetChain)
2589
+ });
2590
+ batchExtensionTokensPromise = this.fetchExtensionTokens(
2591
+ r.data.intents.map((intent) => intent.intentOp),
2592
+ sponsorFlags
2593
+ );
2594
+ } else {
2595
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2596
+ outcome: "failure",
2597
+ errorMessage: r.error,
2598
+ intentCount: options.intents.length
2599
+ });
2600
+ }
979
2601
  return r;
980
2602
  });
981
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
2603
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
2604
+ blindSigning: requestedBlindSigning,
2605
+ reveal
2606
+ });
982
2607
  if (!dialogResult.ready) {
2608
+ this.emitTelemetry("dialog.cancelled", telemetry, {
2609
+ outcome: "cancelled",
2610
+ errorCode: "USER_CANCELLED",
2611
+ errorMessage: "User closed the dialog before it became ready"
2612
+ });
983
2613
  return {
984
2614
  success: false,
985
2615
  results: [],
@@ -987,6 +2617,8 @@ var OneAuthClient = class {
987
2617
  failureCount: 0
988
2618
  };
989
2619
  }
2620
+ const blindSigning = dialogResult.blindSigning;
2621
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
990
2622
  let currentBatchPayload;
991
2623
  const handleBatchPrepareFailure = async (prepareResult) => {
992
2624
  const failedIntents = prepareResult.failedIntents;
@@ -998,7 +2630,7 @@ var OneAuthClient = class {
998
2630
  error: { message: f.error, code: "PREPARE_FAILED" }
999
2631
  })) ?? [];
1000
2632
  this.sendPrepareError(iframe, prepareResult.error);
1001
- await this.waitForDialogClose(dialog, cleanup);
2633
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
1002
2634
  return {
1003
2635
  success: false,
1004
2636
  results: failureResults,
@@ -1007,6 +2639,17 @@ var OneAuthClient = class {
1007
2639
  error: prepareResult.error
1008
2640
  };
1009
2641
  };
2642
+ const handleSponsorshipFailure = async (message) => {
2643
+ this.sendPrepareError(iframe, message);
2644
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2645
+ return {
2646
+ success: false,
2647
+ results: [],
2648
+ successCount: 0,
2649
+ failureCount: 0,
2650
+ error: `Sponsorship token fetch failed: ${message}`
2651
+ };
2652
+ };
1010
2653
  let prepareResponse;
1011
2654
  if (earlyBatchResult) {
1012
2655
  const prepareResult = earlyBatchResult;
@@ -1014,6 +2657,14 @@ var OneAuthClient = class {
1014
2657
  return handleBatchPrepareFailure(prepareResult);
1015
2658
  }
1016
2659
  prepareResponse = prepareResult.data;
2660
+ const batchExtensionResult = await batchExtensionTokensPromise;
2661
+ if (!batchExtensionResult.ok) {
2662
+ return handleSponsorshipFailure(batchExtensionResult.message);
2663
+ }
2664
+ const batchSponsorshipTokens = {
2665
+ accessToken,
2666
+ extensionTokens: batchExtensionResult.extensionTokens
2667
+ };
1017
2668
  currentBatchPayload = {
1018
2669
  mode: "iframe",
1019
2670
  batchMode: true,
@@ -1024,7 +2675,9 @@ var OneAuthClient = class {
1024
2675
  accountAddress: prepareResponse.accountAddress,
1025
2676
  userId: prepareResponse.userId,
1026
2677
  expiresAt: prepareResponse.expiresAt,
1027
- tier: prepareResult.tier
2678
+ tier: prepareResult.tier,
2679
+ auth: batchSponsorshipTokens,
2680
+ telemetry: this.telemetryPayload(telemetry)
1028
2681
  };
1029
2682
  dialogResult.sendInit(currentBatchPayload);
1030
2683
  } else {
@@ -1038,7 +2691,8 @@ var OneAuthClient = class {
1038
2691
  // No: transaction, intentOp, expiresAt, originMessages
1039
2692
  })),
1040
2693
  username: options.username,
1041
- accountAddress: options.accountAddress
2694
+ accountAddress: options.accountAddress,
2695
+ telemetry: this.telemetryPayload(telemetry)
1042
2696
  };
1043
2697
  dialogResult.sendInit(currentBatchPayload);
1044
2698
  const prepareResult = await preparePromise;
@@ -1046,6 +2700,14 @@ var OneAuthClient = class {
1046
2700
  return handleBatchPrepareFailure(prepareResult);
1047
2701
  }
1048
2702
  prepareResponse = prepareResult.data;
2703
+ const batchExtensionResult = await batchExtensionTokensPromise;
2704
+ if (!batchExtensionResult.ok) {
2705
+ return handleSponsorshipFailure(batchExtensionResult.message);
2706
+ }
2707
+ const batchSponsorshipTokens = {
2708
+ accessToken,
2709
+ extensionTokens: batchExtensionResult.extensionTokens
2710
+ };
1049
2711
  currentBatchPayload = {
1050
2712
  mode: "iframe",
1051
2713
  batchMode: true,
@@ -1056,10 +2718,12 @@ var OneAuthClient = class {
1056
2718
  accountAddress: prepareResponse.accountAddress,
1057
2719
  userId: prepareResponse.userId,
1058
2720
  expiresAt: prepareResponse.expiresAt,
1059
- tier: prepareResult.tier
2721
+ tier: prepareResult.tier,
2722
+ auth: batchSponsorshipTokens,
2723
+ telemetry: this.telemetryPayload(telemetry)
1060
2724
  };
1061
2725
  iframe.contentWindow?.postMessage(
1062
- { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true },
2726
+ { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true, blindSigning },
1063
2727
  dialogOrigin
1064
2728
  );
1065
2729
  }
@@ -1067,7 +2731,7 @@ var OneAuthClient = class {
1067
2731
  if (event.origin !== dialogOrigin) return;
1068
2732
  if (event.data?.type === "PASSKEY_READY") {
1069
2733
  iframe.contentWindow?.postMessage(
1070
- { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true },
2734
+ { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true, blindSigning },
1071
2735
  dialogOrigin
1072
2736
  );
1073
2737
  }
@@ -1079,19 +2743,53 @@ var OneAuthClient = class {
1079
2743
  const message = event.data;
1080
2744
  if (message?.type === "PASSKEY_REFRESH_QUOTE") {
1081
2745
  try {
2746
+ const refreshedTokenResult = await this.fetchAccessToken();
2747
+ const refreshToken = refreshedTokenResult.ok ? refreshedTokenResult.accessToken : accessToken;
1082
2748
  const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
1083
2749
  method: "POST",
1084
- headers: { "Content-Type": "application/json" },
1085
- body: JSON.stringify(requestBody)
2750
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
2751
+ body: JSON.stringify({ ...requestBody, auth: { accessToken: refreshToken } })
1086
2752
  });
1087
2753
  if (refreshResponse.ok) {
1088
2754
  const refreshed = await refreshResponse.json();
2755
+ const refreshedExtensionPromise = this.fetchExtensionTokens(
2756
+ refreshed.intents.map((intent) => intent.intentOp),
2757
+ sponsorFlags
2758
+ );
2759
+ const extensionResult = await refreshedExtensionPromise;
2760
+ if (!extensionResult.ok) {
2761
+ console.error(
2762
+ "[SDK] Batch extension token refresh failed:",
2763
+ extensionResult.message
2764
+ );
2765
+ iframe.contentWindow?.postMessage({
2766
+ type: "PASSKEY_REFRESH_ERROR",
2767
+ error: "Failed to refresh batch quotes"
2768
+ }, dialogOrigin);
2769
+ return;
2770
+ }
2771
+ const refreshedAuth = {
2772
+ accessToken: refreshToken,
2773
+ extensionTokens: extensionResult.extensionTokens
2774
+ };
1089
2775
  prepareResponse = refreshed;
2776
+ accessToken = refreshToken;
2777
+ requestBody.auth = { accessToken: refreshToken };
2778
+ batchExtensionTokensPromise = refreshedExtensionPromise;
2779
+ currentBatchPayload = {
2780
+ ...currentBatchPayload,
2781
+ batchIntents: refreshed.intents,
2782
+ batchFailedIntents: refreshed.failedIntents,
2783
+ challenge: refreshed.challenge,
2784
+ expiresAt: refreshed.expiresAt,
2785
+ auth: refreshedAuth
2786
+ };
1090
2787
  iframe.contentWindow?.postMessage({
1091
2788
  type: "PASSKEY_REFRESH_COMPLETE",
1092
2789
  batchIntents: refreshed.intents,
1093
2790
  challenge: refreshed.challenge,
1094
- expiresAt: refreshed.expiresAt
2791
+ expiresAt: refreshed.expiresAt,
2792
+ auth: refreshedAuth
1095
2793
  }, dialogOrigin);
1096
2794
  } else {
1097
2795
  iframe.contentWindow?.postMessage({
@@ -1127,7 +2825,11 @@ var OneAuthClient = class {
1127
2825
  }));
1128
2826
  const allResults = [...results, ...prepareFailures].sort((a, b) => a.index - b.index);
1129
2827
  const successCount = allResults.filter((r) => r.success).length;
1130
- await this.waitForDialogClose(dialog, cleanup);
2828
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2829
+ this.emitTelemetry("batch.sign.succeeded", telemetry, {
2830
+ outcome: "success",
2831
+ intentCount: allResults.length
2832
+ });
1131
2833
  resolve({
1132
2834
  success: successCount === allResults.length,
1133
2835
  results: allResults,
@@ -1136,6 +2838,11 @@ var OneAuthClient = class {
1136
2838
  });
1137
2839
  } else {
1138
2840
  cleanup();
2841
+ this.emitTelemetry("batch.sign.failed", telemetry, {
2842
+ outcome: "failure",
2843
+ errorCode: "SIGNING_FAILED",
2844
+ errorMessage: "Batch signing failed or was cancelled"
2845
+ });
1139
2846
  resolve({
1140
2847
  success: false,
1141
2848
  results: [],
@@ -1147,6 +2854,11 @@ var OneAuthClient = class {
1147
2854
  if (message?.type === "PASSKEY_CLOSE") {
1148
2855
  window.removeEventListener("message", handleMessage);
1149
2856
  cleanup();
2857
+ this.emitTelemetry("dialog.cancelled", telemetry, {
2858
+ outcome: "cancelled",
2859
+ errorCode: "USER_CANCELLED",
2860
+ errorMessage: "User closed the batch dialog"
2861
+ });
1150
2862
  resolve({
1151
2863
  success: false,
1152
2864
  results: [],
@@ -1158,6 +2870,15 @@ var OneAuthClient = class {
1158
2870
  window.addEventListener("message", handleMessage);
1159
2871
  });
1160
2872
  window.removeEventListener("message", handleBatchReReady);
2873
+ this.emitTelemetry("batch.completed", telemetry, {
2874
+ outcome: batchResult.success ? "success" : "failure",
2875
+ intentCount: batchResult.results.length,
2876
+ attributes: {
2877
+ successCount: batchResult.successCount,
2878
+ failureCount: batchResult.failureCount
2879
+ },
2880
+ ...batchResult.error ? { errorMessage: batchResult.error } : {}
2881
+ });
1161
2882
  return batchResult;
1162
2883
  }
1163
2884
  /**
@@ -1276,6 +2997,7 @@ var OneAuthClient = class {
1276
2997
  signedHash: payload.signedHash
1277
2998
  });
1278
2999
  } else {
3000
+ cleanup();
1279
3001
  resolve({
1280
3002
  success: false,
1281
3003
  error: message.error || {
@@ -1358,6 +3080,7 @@ var OneAuthClient = class {
1358
3080
  signedHash: payload.signedHash
1359
3081
  });
1360
3082
  } else {
3083
+ cleanup();
1361
3084
  resolve({
1362
3085
  success: false,
1363
3086
  error: message.error || {
@@ -1418,6 +3141,56 @@ var OneAuthClient = class {
1418
3141
  dialog.addEventListener("close", handleClose);
1419
3142
  });
1420
3143
  }
3144
+ /**
3145
+ * Hidden blind-signing iframes have no user-visible Done or Close button.
3146
+ * Once the SDK has the result it needs, tear them down immediately instead
3147
+ * of waiting for an iframe close event that can never be clicked.
3148
+ */
3149
+ async waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup) {
3150
+ if (blindSigning) {
3151
+ cleanup();
3152
+ return;
3153
+ }
3154
+ await this.waitForDialogClose(dialog, cleanup);
3155
+ }
3156
+ /**
3157
+ * After a prepare error has been surfaced in the dialog, wait for the user
3158
+ * to either close the dialog (give up) or click "Try Again", which the
3159
+ * dialog forwards as `PASSKEY_RETRY_PREPARE`.
3160
+ *
3161
+ * On `"closed"` the dialog has been cleaned up; on `"retry"` it stays open
3162
+ * (showing its loading skeleton) while the caller re-runs prepare. Without
3163
+ * this, "Try Again" after a prepare failure was a dead end: the dialog
3164
+ * reset its local state but the SDK had already given up, so the quote
3165
+ * never arrived and the Sign button stayed disabled forever.
3166
+ */
3167
+ waitForPrepareRetryOrClose(dialog, cleanup) {
3168
+ const dialogOrigin = this.getDialogOrigin();
3169
+ return new Promise((resolve) => {
3170
+ const stop = () => {
3171
+ window.removeEventListener("message", handleMessage);
3172
+ dialog.removeEventListener("close", handleClose);
3173
+ };
3174
+ const handleMessage = (event) => {
3175
+ if (event.origin !== dialogOrigin) return;
3176
+ if (event.data?.type === "PASSKEY_RETRY_PREPARE") {
3177
+ stop();
3178
+ resolve("retry");
3179
+ } else if (event.data?.type === "PASSKEY_CLOSE") {
3180
+ stop();
3181
+ cleanup();
3182
+ resolve("closed");
3183
+ }
3184
+ };
3185
+ const handleClose = () => {
3186
+ stop();
3187
+ cleanup();
3188
+ resolve("closed");
3189
+ };
3190
+ window.addEventListener("message", handleMessage);
3191
+ dialog.addEventListener("close", handleClose);
3192
+ });
3193
+ }
1421
3194
  /**
1422
3195
  * Inject a `<link rel="preconnect">` tag for the given URL's origin.
1423
3196
  *
@@ -1463,7 +3236,7 @@ var OneAuthClient = class {
1463
3236
  * @returns `{ ready: true, sendInit }` when the iframe is ready, or
1464
3237
  * `{ ready: false }` if the dialog was closed or timed out first.
1465
3238
  */
1466
- waitForDialogReadyDeferred(dialog, iframe, cleanup) {
3239
+ waitForDialogReadyDeferred(dialog, iframe, cleanup, options = {}) {
1467
3240
  const dialogOrigin = this.getDialogOrigin();
1468
3241
  return new Promise((resolve) => {
1469
3242
  let settled = false;
@@ -1477,12 +3250,15 @@ var OneAuthClient = class {
1477
3250
  const handleMessage = (event) => {
1478
3251
  if (event.origin !== dialogOrigin) return;
1479
3252
  if (event.data?.type === "PASSKEY_READY") {
3253
+ const blindSigning = options.blindSigning === true;
1480
3254
  teardown();
1481
3255
  resolve({
1482
3256
  ready: true,
3257
+ blindSigning,
1483
3258
  sendInit: (initMessage) => {
3259
+ if (blindSigning) iframe.focus({ preventScroll: true });
1484
3260
  iframe.contentWindow?.postMessage(
1485
- { type: "PASSKEY_INIT", ...initMessage, fullViewport: true },
3261
+ { type: "PASSKEY_INIT", ...initMessage, fullViewport: true, blindSigning },
1486
3262
  dialogOrigin
1487
3263
  );
1488
3264
  }
@@ -1523,13 +3299,13 @@ var OneAuthClient = class {
1523
3299
  * @param requestBody - Serialized intent options (bigint amounts converted to
1524
3300
  * strings before this call).
1525
3301
  * @returns On success: `{ success: true, data: PrepareIntentResponse, tier }`.
1526
- * On failure: `{ success: false, error: { code, message } }`.
3302
+ * On failure: `{ success: false, error: { code, message, details? } }`.
1527
3303
  */
1528
- async prepareIntent(requestBody) {
3304
+ async prepareIntent(requestBody, telemetry) {
1529
3305
  try {
1530
3306
  const response = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
1531
3307
  method: "POST",
1532
- headers: { "Content-Type": "application/json" },
3308
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1533
3309
  body: JSON.stringify(requestBody)
1534
3310
  });
1535
3311
  if (!response.ok) {
@@ -1542,7 +3318,8 @@ var OneAuthClient = class {
1542
3318
  success: false,
1543
3319
  error: {
1544
3320
  code: errorMessage.includes("User not found") ? "USER_NOT_FOUND" : "PREPARE_FAILED",
1545
- message: errorMessage
3321
+ message: errorMessage,
3322
+ details: errorData.details ?? errorData.candidateErrors ?? errorData
1546
3323
  }
1547
3324
  };
1548
3325
  }
@@ -1575,11 +3352,11 @@ var OneAuthClient = class {
1575
3352
  * @returns On success: `{ success: true, data: PrepareBatchIntentResponse, tier }`.
1576
3353
  * On failure: `{ success: false, error, failedIntents? }`.
1577
3354
  */
1578
- async prepareBatchIntent(requestBody) {
3355
+ async prepareBatchIntent(requestBody, telemetry) {
1579
3356
  try {
1580
3357
  const response = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
1581
3358
  method: "POST",
1582
- headers: { "Content-Type": "application/json" },
3359
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1583
3360
  body: JSON.stringify(requestBody)
1584
3361
  });
1585
3362
  if (!response.ok) {
@@ -1625,15 +3402,28 @@ var OneAuthClient = class {
1625
3402
  * that hasn't completed yet.
1626
3403
  */
1627
3404
  async getIntentStatus(intentId) {
3405
+ const telemetry = this.createTelemetryOperation("status");
3406
+ this.emitTelemetry("intent.status.started", telemetry, {
3407
+ outcome: "started",
3408
+ attributes: { directStatusCheck: true }
3409
+ });
1628
3410
  try {
1629
3411
  const response = await fetch(
1630
3412
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
1631
3413
  {
1632
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3414
+ headers: {
3415
+ ...this.telemetryHeaders(telemetry),
3416
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3417
+ }
1633
3418
  }
1634
3419
  );
1635
3420
  if (!response.ok) {
1636
3421
  const errorData = await response.json().catch(() => ({}));
3422
+ this.emitTelemetry("status.failed", telemetry, {
3423
+ outcome: "failure",
3424
+ errorCode: "STATUS_FAILED",
3425
+ errorMessage: errorData.error || "Failed to get intent status"
3426
+ });
1637
3427
  return {
1638
3428
  success: false,
1639
3429
  intentId,
@@ -1645,6 +3435,10 @@ var OneAuthClient = class {
1645
3435
  };
1646
3436
  }
1647
3437
  const data = await response.json();
3438
+ this.emitTelemetry("status.succeeded", telemetry, {
3439
+ outcome: data.status === "completed" ? "success" : "started",
3440
+ status: data.status
3441
+ });
1648
3442
  return {
1649
3443
  success: data.status === "completed",
1650
3444
  intentId,
@@ -1653,6 +3447,10 @@ var OneAuthClient = class {
1653
3447
  operationId: data.operationId
1654
3448
  };
1655
3449
  } catch (error) {
3450
+ this.emitTelemetry("status.failed", telemetry, {
3451
+ outcome: "failure",
3452
+ ...describeTelemetryError(error)
3453
+ });
1656
3454
  return {
1657
3455
  success: false,
1658
3456
  intentId,
@@ -1684,6 +3482,9 @@ var OneAuthClient = class {
1684
3482
  * ```
1685
3483
  */
1686
3484
  async getIntentHistory(options) {
3485
+ const telemetry = this.createTelemetryOperation("history", {
3486
+ hasStatusFilter: !!options?.status
3487
+ });
1687
3488
  const queryParams = new URLSearchParams();
1688
3489
  if (options?.limit) queryParams.set("limit", String(options.limit));
1689
3490
  if (options?.offset) queryParams.set("offset", String(options.offset));
@@ -1692,184 +3493,183 @@ var OneAuthClient = class {
1692
3493
  if (options?.to) queryParams.set("to", options.to);
1693
3494
  const url = `${this.config.providerUrl}/api/intent/history${queryParams.toString() ? `?${queryParams}` : ""}`;
1694
3495
  const response = await fetch(url, {
1695
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {},
3496
+ headers: {
3497
+ ...this.telemetryHeaders(telemetry),
3498
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3499
+ },
1696
3500
  credentials: "include"
1697
3501
  });
1698
3502
  if (!response.ok) {
1699
3503
  const errorData = await response.json().catch(() => ({}));
3504
+ this.emitTelemetry("history.failed", telemetry, {
3505
+ outcome: "failure",
3506
+ errorCode: "HISTORY_FAILED",
3507
+ errorMessage: errorData.error || "Failed to get intent history"
3508
+ });
1700
3509
  throw new Error(errorData.error || "Failed to get intent history");
1701
3510
  }
1702
- return response.json();
3511
+ const result = await response.json();
3512
+ this.emitTelemetry("history.succeeded", telemetry, {
3513
+ outcome: "success",
3514
+ attributes: {
3515
+ total: result.total,
3516
+ returned: result.intents?.length ?? 0
3517
+ }
3518
+ });
3519
+ return result;
1703
3520
  }
1704
3521
  /**
1705
- * Send a swap intent through the Rhinestone orchestrator
1706
- *
1707
- * This is a high-level abstraction for token swaps (including cross-chain):
1708
- * 1. Resolves token symbols to addresses
1709
- * 2. Builds the swap intent with tokenRequests (output-first model)
1710
- * 3. The orchestrator's solver network finds the best route
1711
- * 4. Executes via the standard intent flow
1712
- *
1713
- * NOTE: The `amount` parameter specifies the OUTPUT amount (what the user wants to receive),
1714
- * not the input amount. The orchestrator will calculate the required input from sourceAssets.
1715
- *
1716
- * @example
1717
- * ```typescript
1718
- * // Buy 100 USDC using ETH on Base
1719
- * const result = await client.sendSwap({
1720
- * username: 'alice',
1721
- * targetChain: 8453,
1722
- * fromToken: 'ETH',
1723
- * toToken: 'USDC',
1724
- * amount: '100', // Receive 100 USDC
1725
- * });
1726
- *
1727
- * // Cross-chain: Buy 50 USDC on Base, paying with ETH from any chain
1728
- * const result = await client.sendSwap({
1729
- * username: 'alice',
1730
- * targetChain: 8453, // Base
1731
- * fromToken: 'ETH',
1732
- * toToken: 'USDC',
1733
- * amount: '50', // Receive 50 USDC
1734
- * });
1735
- * ```
3522
+ * Forward a raw EIP-1193 request to the iframe so the active EOA connector
3523
+ * can handle it (wallet-connect session or injected wallet). Opens
3524
+ * `/dialog/sign-eoa`, which looks up the wagmi connector and delegates to
3525
+ * `connector.getProvider().request({ method, params })`. The wallet's own
3526
+ * UI is the review screen; the dialog just transports the request.
1736
3527
  */
1737
- async sendSwap(options) {
1738
- try {
1739
- getChainById(options.targetChain);
1740
- } catch {
1741
- return {
1742
- success: false,
1743
- intentId: "",
1744
- status: "failed",
1745
- error: {
1746
- code: "INVALID_CHAIN",
1747
- message: `Unsupported chain: ${options.targetChain}`
1748
- }
1749
- };
1750
- }
1751
- const resolveToken = (token, label) => {
1752
- try {
1753
- const address = resolveTokenAddress(token, options.targetChain);
1754
- if (!isTokenAddressSupported(address, options.targetChain)) {
1755
- return {
1756
- error: `Unsupported ${label}: ${token} on chain ${options.targetChain}`
1757
- };
1758
- }
1759
- return { address };
1760
- } catch (error) {
1761
- return {
1762
- error: error instanceof Error ? error.message : `Unsupported ${label}: ${token} on chain ${options.targetChain}`
1763
- };
1764
- }
1765
- };
1766
- let fromTokenAddress;
1767
- if (options.fromToken) {
1768
- const fromTokenResult = resolveToken(options.fromToken, "fromToken");
1769
- if (!fromTokenResult.address) {
1770
- return {
1771
- success: false,
1772
- intentId: "",
1773
- status: "failed",
1774
- error: {
1775
- code: "INVALID_TOKEN",
1776
- message: fromTokenResult.error || `Unknown fromToken: ${options.fromToken}`
1777
- }
1778
- };
1779
- }
1780
- fromTokenAddress = fromTokenResult.address;
1781
- }
1782
- const toTokenResult = resolveToken(options.toToken, "toToken");
1783
- if (!toTokenResult.address) {
3528
+ async requestWithWallet(options) {
3529
+ const job = this.eoaSerialQueue.then(
3530
+ () => this.doRequestWithWallet(options),
3531
+ () => this.doRequestWithWallet(options)
3532
+ );
3533
+ this.eoaSerialQueue = job.then(
3534
+ () => void 0,
3535
+ () => void 0
3536
+ );
3537
+ return job;
3538
+ }
3539
+ async doRequestWithWallet(options) {
3540
+ const dialogOrigin = this.getDialogOrigin();
3541
+ const ensureResult = await this.ensureEoaDialog(options?.theme);
3542
+ if (!ensureResult.ok) {
1784
3543
  return {
1785
3544
  success: false,
1786
- intentId: "",
1787
- status: "failed",
1788
- error: {
1789
- code: "INVALID_TOKEN",
1790
- message: toTokenResult.error || `Unknown toToken: ${options.toToken}`
1791
- }
3545
+ error: { code: "USER_REJECTED", message: "User closed the dialog" }
1792
3546
  };
1793
3547
  }
1794
- const toTokenAddress = toTokenResult.address;
1795
- const formatTokenLabel = (token, fallback) => {
1796
- if (!token.startsWith("0x")) {
1797
- return token;
1798
- }
1799
- try {
1800
- return getTokenSymbol(token, options.targetChain);
1801
- } catch {
1802
- return fallback;
1803
- }
3548
+ const { dialog, iframe } = ensureResult;
3549
+ const requestId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3550
+ const hideDialog = () => {
3551
+ if (dialog.open) dialog.close();
1804
3552
  };
1805
- const toSymbol = formatTokenLabel(
1806
- options.toToken,
1807
- `${options.toToken.slice(0, 6)}...${options.toToken.slice(-4)}`
3553
+ const responsePromise = this.waitForEoaResponse(
3554
+ iframe,
3555
+ hideDialog,
3556
+ requestId,
3557
+ dialogOrigin
1808
3558
  );
1809
- const isFromNativeEth = fromTokenAddress ? fromTokenAddress === "0x0000000000000000000000000000000000000000" : false;
1810
- const isToNativeEth = toTokenAddress === "0x0000000000000000000000000000000000000000";
1811
- const KNOWN_DECIMALS = {
1812
- ETH: 18,
1813
- WETH: 18,
1814
- USDC: 6,
1815
- USDT: 6,
1816
- USDT0: 6
3559
+ const initPayload = {
3560
+ mode: "iframe",
3561
+ method: options.method,
3562
+ params: options.params,
3563
+ expectedAddress: options.expectedAddress,
3564
+ requestId
1817
3565
  };
1818
- const getDecimals = (symbol, chainId) => {
1819
- try {
1820
- const match = getSupportedTokens(chainId).find(
1821
- (t) => t.symbol.toUpperCase() === symbol.toUpperCase()
3566
+ iframe.contentWindow?.postMessage(
3567
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3568
+ dialogOrigin
3569
+ );
3570
+ const handleReReady = (event) => {
3571
+ if (event.origin !== dialogOrigin) return;
3572
+ if (event.source !== iframe.contentWindow) return;
3573
+ if (event.data?.type === "PASSKEY_READY") {
3574
+ iframe.contentWindow?.postMessage(
3575
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3576
+ dialogOrigin
1822
3577
  );
1823
- if (match) return match.decimals;
1824
- return getTokenDecimals(symbol, chainId);
1825
- } catch {
1826
- const upperSymbol = symbol.toUpperCase();
1827
- return KNOWN_DECIMALS[upperSymbol] ?? 18;
1828
3578
  }
1829
3579
  };
1830
- const toDecimals = getDecimals(options.toToken, options.targetChain);
1831
- const isBridge = options.fromToken ? options.fromToken.toUpperCase() === options.toToken.toUpperCase() : false;
1832
- const tokenRequests = [{
1833
- token: toTokenAddress,
1834
- amount: parseUnits(options.amount, toDecimals)
1835
- }];
1836
- const result = await this.sendIntent({
1837
- username: options.username,
1838
- targetChain: options.targetChain,
1839
- calls: [
1840
- {
1841
- // Minimal call - just signals to orchestrator we want the tokenRequests delivered
1842
- to: toTokenAddress,
1843
- value: "0",
1844
- // SDK provides labels so dialog shows "Buy ETH" not "Send ETH / To: 0x000..."
1845
- label: `Buy ${toSymbol}`,
1846
- sublabel: `${options.amount} ${toSymbol}`
1847
- }
1848
- ],
1849
- // Request specific output tokens - this is what actually matters for swaps
1850
- tokenRequests,
1851
- // Constrain orchestrator to use only the fromToken as input
1852
- // This ensures the swap uses the correct source token
1853
- // Use canonical symbol casing from registry (e.g. "MockUSD" not "MOCKUSD")
1854
- sourceAssets: options.sourceAssets || (options.fromToken ? [options.fromToken] : void 0),
1855
- // Pass source chain ID so orchestrator knows which chain to look for tokens on
1856
- sourceChainId: options.sourceChainId,
1857
- closeOn: options.closeOn || "preconfirmed",
1858
- waitForHash: options.waitForHash,
1859
- hashTimeoutMs: options.hashTimeoutMs,
1860
- hashIntervalMs: options.hashIntervalMs
3580
+ window.addEventListener("message", handleReReady);
3581
+ const result = await responsePromise;
3582
+ window.removeEventListener("message", handleReReady);
3583
+ hideDialog();
3584
+ return result;
3585
+ }
3586
+ /**
3587
+ * Lazily create the persistent `/dialog/sign-eoa` iframe and reveal it.
3588
+ * Subsequent calls just re-open the existing `<dialog>` so the iframe's
3589
+ * in-page state (notably the WalletConnect SignClient) is preserved
3590
+ * between transactions.
3591
+ */
3592
+ async ensureEoaDialog(theme) {
3593
+ if (this.eoaDialogState) {
3594
+ const { dialog: dialog2, iframe: iframe2 } = this.eoaDialogState;
3595
+ if (!dialog2.open) {
3596
+ dialog2.showModal();
3597
+ }
3598
+ return { ok: true, dialog: dialog2, iframe: iframe2 };
3599
+ }
3600
+ const dialogUrl = this.getDialogUrl();
3601
+ const themeParams = this.getThemeParams(theme);
3602
+ const parentOrigin = typeof window !== "undefined" ? window.location.origin : "";
3603
+ const parentOriginParam = parentOrigin ? `&parentOrigin=${encodeURIComponent(parentOrigin)}` : "";
3604
+ const signingUrl = `${dialogUrl}/dialog/sign-eoa?mode=iframe${parentOriginParam}${themeParams ? `&${themeParams}` : ""}`;
3605
+ const { dialog, iframe, destroy } = this.createModalDialog(signingUrl, {
3606
+ persistent: true
3607
+ });
3608
+ const ready = await this.waitForEoaIframeReady(iframe);
3609
+ if (!ready) {
3610
+ destroy();
3611
+ return { ok: false };
3612
+ }
3613
+ this.eoaDialogState = { dialog, iframe };
3614
+ return { ok: true, dialog, iframe };
3615
+ }
3616
+ waitForEoaIframeReady(iframe) {
3617
+ const dialogOrigin = this.getDialogOrigin();
3618
+ return new Promise((resolve) => {
3619
+ let timeoutId = null;
3620
+ const cleanup = () => {
3621
+ if (timeoutId !== null) clearTimeout(timeoutId);
3622
+ window.removeEventListener("message", handler);
3623
+ };
3624
+ const handler = (event) => {
3625
+ if (event.origin !== dialogOrigin) return;
3626
+ if (event.source !== iframe.contentWindow) return;
3627
+ if (event.data?.type === "PASSKEY_READY") {
3628
+ cleanup();
3629
+ resolve(true);
3630
+ }
3631
+ };
3632
+ window.addEventListener("message", handler);
3633
+ timeoutId = setTimeout(() => {
3634
+ cleanup();
3635
+ resolve(false);
3636
+ }, 1e4);
3637
+ });
3638
+ }
3639
+ waitForEoaResponse(iframe, cleanup, requestId, dialogOrigin) {
3640
+ return new Promise((resolve) => {
3641
+ const handleMessage = (event) => {
3642
+ if (event.origin !== dialogOrigin) return;
3643
+ if (event.source !== iframe.contentWindow) return;
3644
+ const message = event.data;
3645
+ if (message?.type === "PASSKEY_EOA_RESULT" || message?.type === "PASSKEY_EOA_CLOSE") {
3646
+ if (message.requestId !== requestId) return;
3647
+ } else {
3648
+ return;
3649
+ }
3650
+ window.removeEventListener("message", handleMessage);
3651
+ if (message.type === "PASSKEY_EOA_CLOSE") {
3652
+ cleanup();
3653
+ resolve({
3654
+ success: false,
3655
+ error: { code: "USER_REJECTED", message: "User closed the dialog" }
3656
+ });
3657
+ return;
3658
+ }
3659
+ if (message.success) {
3660
+ resolve({ success: true, result: message.data?.result });
3661
+ } else {
3662
+ resolve({
3663
+ success: false,
3664
+ error: message.error || {
3665
+ code: "EOA_REQUEST_FAILED",
3666
+ message: "Wallet request failed"
3667
+ }
3668
+ });
3669
+ }
3670
+ };
3671
+ window.addEventListener("message", handleMessage);
1861
3672
  });
1862
- return {
1863
- ...result,
1864
- quote: result.success ? {
1865
- fromToken: fromTokenAddress ?? options.fromToken,
1866
- toToken: toTokenAddress,
1867
- amountIn: options.amount,
1868
- amountOut: "",
1869
- // Filled by orchestrator quote
1870
- rate: ""
1871
- } : void 0
1872
- };
1873
3673
  }
1874
3674
  /**
1875
3675
  * Sign an arbitrary message with the user's passkey
@@ -1900,12 +3700,37 @@ var OneAuthClient = class {
1900
3700
  * ```
1901
3701
  */
1902
3702
  async signMessage(options) {
3703
+ const telemetry = this.createTelemetryOperation("sign", {
3704
+ signingMode: "message",
3705
+ hasAccountAddress: !!options.accountAddress
3706
+ });
1903
3707
  const dialogUrl = this.getDialogUrl();
3708
+ const params = new URLSearchParams({ mode: "iframe" });
3709
+ this.appendTelemetryParams(params, telemetry);
1904
3710
  const themeParams = this.getThemeParams(options?.theme);
1905
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
1906
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
1907
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
3711
+ if (themeParams) {
3712
+ const themeParsed = new URLSearchParams(themeParams);
3713
+ themeParsed.forEach((value, key) => params.set(key, value));
3714
+ }
3715
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
3716
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
3717
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
3718
+ hidden: requestedBlindSigning
3719
+ });
3720
+ this.emitTelemetry("dialog.opened", telemetry, {
3721
+ outcome: "started",
3722
+ attributes: { blindSigning: requestedBlindSigning }
3723
+ });
3724
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
3725
+ blindSigning: requestedBlindSigning,
3726
+ reveal
3727
+ });
1908
3728
  if (!dialogResult.ready) {
3729
+ this.emitTelemetry("dialog.cancelled", telemetry, {
3730
+ outcome: "cancelled",
3731
+ errorCode: "USER_REJECTED",
3732
+ errorMessage: "User closed the dialog before signing"
3733
+ });
1909
3734
  return {
1910
3735
  success: false,
1911
3736
  error: {
@@ -1921,15 +3746,18 @@ var OneAuthClient = class {
1921
3746
  username: options.username,
1922
3747
  accountAddress: options.accountAddress,
1923
3748
  description: options.description,
1924
- metadata: options.metadata
3749
+ metadata: options.metadata,
3750
+ telemetry: this.telemetryPayload(telemetry)
1925
3751
  };
1926
3752
  dialogResult.sendInit(initPayload);
3753
+ const blindSigning = dialogResult.blindSigning;
3754
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
1927
3755
  const dialogOrigin = this.getDialogOrigin();
1928
3756
  const handleReReady = (event) => {
1929
3757
  if (event.origin !== dialogOrigin) return;
1930
3758
  if (event.data?.type === "PASSKEY_READY") {
1931
3759
  iframe.contentWindow?.postMessage(
1932
- { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3760
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true, blindSigning },
1933
3761
  dialogOrigin
1934
3762
  );
1935
3763
  }
@@ -1939,6 +3767,10 @@ var OneAuthClient = class {
1939
3767
  window.removeEventListener("message", handleReReady);
1940
3768
  cleanup();
1941
3769
  if (signingResult.success) {
3770
+ this.emitTelemetry("sign.succeeded", telemetry, {
3771
+ outcome: "success",
3772
+ attributes: { signingMode: "message" }
3773
+ });
1942
3774
  return {
1943
3775
  success: true,
1944
3776
  signature: signingResult.signature,
@@ -1947,6 +3779,15 @@ var OneAuthClient = class {
1947
3779
  passkey: signingResult.passkey
1948
3780
  };
1949
3781
  }
3782
+ this.emitTelemetry(
3783
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
3784
+ telemetry,
3785
+ {
3786
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
3787
+ ...describeTelemetryError(signingResult.error),
3788
+ attributes: { signingMode: "message" }
3789
+ }
3790
+ );
1950
3791
  return {
1951
3792
  success: false,
1952
3793
  error: signingResult.error
@@ -1994,6 +3835,11 @@ var OneAuthClient = class {
1994
3835
  * ```
1995
3836
  */
1996
3837
  async signTypedData(options) {
3838
+ const telemetry = this.createTelemetryOperation("sign", {
3839
+ signingMode: "typedData",
3840
+ primaryType: options.primaryType,
3841
+ hasAccountAddress: !!options.accountAddress
3842
+ });
1997
3843
  const signedHash = hashTypedData({
1998
3844
  domain: options.domain,
1999
3845
  types: options.types,
@@ -2001,11 +3847,32 @@ var OneAuthClient = class {
2001
3847
  message: options.message
2002
3848
  });
2003
3849
  const dialogUrl = this.getDialogUrl();
3850
+ const params = new URLSearchParams({ mode: "iframe" });
3851
+ this.appendTelemetryParams(params, telemetry);
2004
3852
  const themeParams = this.getThemeParams(options?.theme);
2005
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
2006
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
2007
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
3853
+ if (themeParams) {
3854
+ const themeParsed = new URLSearchParams(themeParams);
3855
+ themeParsed.forEach((value, key) => params.set(key, value));
3856
+ }
3857
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
3858
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
3859
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
3860
+ hidden: requestedBlindSigning
3861
+ });
3862
+ this.emitTelemetry("dialog.opened", telemetry, {
3863
+ outcome: "started",
3864
+ attributes: { blindSigning: requestedBlindSigning }
3865
+ });
3866
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
3867
+ blindSigning: requestedBlindSigning,
3868
+ reveal
3869
+ });
2008
3870
  if (!dialogResult.ready) {
3871
+ this.emitTelemetry("dialog.cancelled", telemetry, {
3872
+ outcome: "cancelled",
3873
+ errorCode: "USER_REJECTED",
3874
+ errorMessage: "User closed the dialog before signing"
3875
+ });
2009
3876
  return {
2010
3877
  success: false,
2011
3878
  error: {
@@ -2026,15 +3893,18 @@ var OneAuthClient = class {
2026
3893
  challenge: signedHash,
2027
3894
  username: options.username,
2028
3895
  accountAddress: options.accountAddress,
2029
- description: options.description
3896
+ description: options.description,
3897
+ telemetry: this.telemetryPayload(telemetry)
2030
3898
  };
2031
3899
  dialogResult.sendInit(initPayload);
3900
+ const blindSigning = dialogResult.blindSigning;
3901
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
2032
3902
  const dialogOrigin = this.getDialogOrigin();
2033
3903
  const handleReReady = (event) => {
2034
3904
  if (event.origin !== dialogOrigin) return;
2035
3905
  if (event.data?.type === "PASSKEY_READY") {
2036
3906
  iframe.contentWindow?.postMessage(
2037
- { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3907
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true, blindSigning },
2038
3908
  dialogOrigin
2039
3909
  );
2040
3910
  }
@@ -2044,6 +3914,10 @@ var OneAuthClient = class {
2044
3914
  window.removeEventListener("message", handleReReady);
2045
3915
  cleanup();
2046
3916
  if (signingResult.success) {
3917
+ this.emitTelemetry("sign.succeeded", telemetry, {
3918
+ outcome: "success",
3919
+ attributes: { signingMode: "typedData", primaryType: options.primaryType }
3920
+ });
2047
3921
  return {
2048
3922
  success: true,
2049
3923
  signature: signingResult.signature,
@@ -2051,6 +3925,15 @@ var OneAuthClient = class {
2051
3925
  passkey: signingResult.passkey
2052
3926
  };
2053
3927
  }
3928
+ this.emitTelemetry(
3929
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
3930
+ telemetry,
3931
+ {
3932
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
3933
+ ...describeTelemetryError(signingResult.error),
3934
+ attributes: { signingMode: "typedData", primaryType: options.primaryType }
3935
+ }
3936
+ );
2054
3937
  return {
2055
3938
  success: false,
2056
3939
  error: signingResult.error
@@ -2122,8 +4005,66 @@ var OneAuthClient = class {
2122
4005
  options.onReady?.();
2123
4006
  };
2124
4007
  options.container.appendChild(iframe);
4008
+ this.wireViewportInfo(iframe);
4009
+ const host = new URL(dialogUrl);
4010
+ const onResize = (event) => {
4011
+ if (event.source !== iframe.contentWindow) return;
4012
+ if (event.origin !== host.origin) return;
4013
+ if (event.data?.type !== "PASSKEY_RESIZE") return;
4014
+ const h = Number(event.data.height);
4015
+ if (Number.isFinite(h) && h > 0) {
4016
+ iframe.style.height = `${h}px`;
4017
+ }
4018
+ };
4019
+ window.addEventListener("message", onResize);
2125
4020
  return iframe;
2126
4021
  }
4022
+ /**
4023
+ * Post `PASSKEY_VIEWPORT_INFO` with the parent's
4024
+ * `window.innerHeight` to the iframe whenever it signals
4025
+ * `PASSKEY_READY`, and re-post on `window.resize` (rAF-debounced)
4026
+ * so the dialog's internal max-height cap follows the user's
4027
+ * actual screen.
4028
+ *
4029
+ * Inside the iframe, `vh`/`dvh` units refer to the iframe's own
4030
+ * height (recursive when the iframe auto-sizes), so the parent is
4031
+ * the only reliable source of "what 75% of the user's screen is".
4032
+ * The dialog stores the value in `dialog-context` and falls back
4033
+ * to auto-grow when the value is absent — so it's safe to call
4034
+ * this even for embedders that don't actually cap (modal mode
4035
+ * uses `fullViewport` which has its own outer cap).
4036
+ */
4037
+ wireViewportInfo(iframe) {
4038
+ const dialogOrigin = this.getDialogOrigin();
4039
+ const post = () => {
4040
+ iframe.contentWindow?.postMessage(
4041
+ { type: "PASSKEY_VIEWPORT_INFO", height: window.innerHeight },
4042
+ dialogOrigin
4043
+ );
4044
+ };
4045
+ const handleMessage = (event) => {
4046
+ if (event.origin !== dialogOrigin) return;
4047
+ if (event.source !== iframe.contentWindow) return;
4048
+ if (event.data?.type === "PASSKEY_READY") {
4049
+ post();
4050
+ }
4051
+ };
4052
+ window.addEventListener("message", handleMessage);
4053
+ let raf = null;
4054
+ const onResize = () => {
4055
+ if (raf !== null) return;
4056
+ raf = requestAnimationFrame(() => {
4057
+ raf = null;
4058
+ post();
4059
+ });
4060
+ };
4061
+ window.addEventListener("resize", onResize);
4062
+ return () => {
4063
+ window.removeEventListener("message", handleMessage);
4064
+ window.removeEventListener("resize", onResize);
4065
+ if (raf !== null) cancelAnimationFrame(raf);
4066
+ };
4067
+ }
2127
4068
  /**
2128
4069
  * Listen for a signing result from an embedded (inline) signing iframe.
2129
4070
  *
@@ -2258,6 +4199,7 @@ var OneAuthClient = class {
2258
4199
  body: JSON.stringify({
2259
4200
  ...this.config.clientId && { clientId: this.config.clientId },
2260
4201
  username: options.username,
4202
+ accountAddress: options.accountAddress,
2261
4203
  challenge: options.challenge,
2262
4204
  description: options.description,
2263
4205
  metadata: options.metadata,
@@ -2313,7 +4255,7 @@ var OneAuthClient = class {
2313
4255
  * @returns `true` if the dialog is ready and init was sent; `false` if the
2314
4256
  * dialog was closed or timed out before becoming ready.
2315
4257
  */
2316
- waitForDialogReady(dialog, iframe, cleanup, initMessage) {
4258
+ waitForDialogReady(dialog, iframe, cleanup, initMessage, options = {}) {
2317
4259
  const dialogOrigin = this.getDialogOrigin();
2318
4260
  return new Promise((resolve) => {
2319
4261
  let settled = false;
@@ -2327,11 +4269,14 @@ var OneAuthClient = class {
2327
4269
  const handleMessage = (event) => {
2328
4270
  if (event.origin !== dialogOrigin) return;
2329
4271
  if (event.data?.type === "PASSKEY_READY") {
4272
+ const blindSigning = options.blindSigning === true;
2330
4273
  teardown();
4274
+ if (blindSigning) iframe.focus({ preventScroll: true });
2331
4275
  iframe.contentWindow?.postMessage({
2332
4276
  type: "PASSKEY_INIT",
2333
4277
  ...initMessage,
2334
- fullViewport: true
4278
+ fullViewport: true,
4279
+ blindSigning
2335
4280
  }, dialogOrigin);
2336
4281
  resolve(true);
2337
4282
  } else if (event.data?.type === "PASSKEY_CLOSE") {
@@ -2356,7 +4301,7 @@ var OneAuthClient = class {
2356
4301
  /**
2357
4302
  * Create and open a full-viewport `<dialog>` containing a passkey iframe.
2358
4303
  *
2359
- * The SDK owns only the transparent outer shell; all visible UI (backdrop,
4304
+ * The SDK owns only the hidden outer shell; all visible UI (backdrop,
2360
4305
  * card, animations, close button) is rendered by the passkey app inside the
2361
4306
  * iframe. This approach means design changes can be shipped server-side
2362
4307
  * without updating SDK consumers.
@@ -2378,34 +4323,42 @@ var OneAuthClient = class {
2378
4323
  * restores those as well.
2379
4324
  * - The iframe sandbox includes `allow-popups-to-escape-sandbox` so that
2380
4325
  * 1Password's popup UI can render outside the sandboxed context.
4326
+ * - `allow-downloads` lets the passkey iframe trigger the recovery-key
4327
+ * JSON fallback download without sending sensitive data to the parent.
2381
4328
  *
2382
4329
  * @param url - The full URL (including query params) to load in the iframe.
2383
4330
  * @returns References to the dialog, iframe, and a cleanup function.
2384
4331
  */
2385
- createModalDialog(url) {
4332
+ createModalDialog(url, options) {
2386
4333
  const dialogUrl = this.getDialogUrl();
2387
4334
  const hostUrl = new URL(dialogUrl);
2388
4335
  const urlParams = new URL(url, window.location.href).searchParams;
2389
4336
  const themeMode = urlParams.get("theme") || "light";
2390
- const accentColor = urlParams.get("accent") || "#0090ff";
2391
4337
  const isDark = themeMode === "dark" || themeMode !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches;
2392
- const bgPrimary = isDark ? "#191919" : "#fcfcfc";
2393
- const bgSecondary = isDark ? "#222222" : "#f9f9f9";
2394
- const borderColor = isDark ? "#2a2a2a" : "#e0e0e0";
2395
- const textPrimary = isDark ? "#eeeeee" : "#202020";
2396
- const textSecondary = isDark ? "#7b7b7b" : "#838383";
2397
- const bgSurface = isDark ? "#222222" : "#f0f0f0";
2398
- const ah = accentColor.replace("#", "");
2399
- const accentRgb = `${parseInt(ah.slice(0, 2), 16)},${parseInt(ah.slice(2, 4), 16)},${parseInt(ah.slice(4, 6), 16)}`;
2400
- const accentTint = isDark ? `rgba(${accentRgb},0.15)` : `rgba(${accentRgb},0.1)`;
2401
- const hostname = window.location.hostname;
4338
+ const theme = isDark ? DARK_TOKENS : LIGHT_TOKENS;
4339
+ const { bgPrimary, borderColor, textPrimary, textSecondary } = theme;
4340
+ const textFooter = FOOTER_TEXT_COLOR;
4341
+ const spinnerColor = BRAND_PURPLE;
2402
4342
  const dialog = document.createElement("dialog");
2403
4343
  dialog.dataset.passkey = "";
4344
+ if (options?.hidden) {
4345
+ dialog.dataset.passkeyHidden = "";
4346
+ }
2404
4347
  dialog.style.opacity = "1";
2405
4348
  dialog.style.background = "transparent";
2406
4349
  document.body.appendChild(dialog);
2407
4350
  const style = document.createElement("style");
2408
4351
  style.textContent = `
4352
+ /* Load Inter so the overlay's title/subtitle widths match the
4353
+ iframe (which loads Inter via next/font). Without this, host
4354
+ pages on Arial / system-ui produce wider text that wraps to two
4355
+ lines and breaks the visual match with the Figma. font-display:
4356
+ swap renders immediately with a fallback and swaps when Inter
4357
+ is ready \u2014 usually well before PASSKEY_RENDERED fires, but the
4358
+ overlay is short-lived either way so a single-frame swap is
4359
+ acceptable. */
4360
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;700&display=swap');
4361
+
2409
4362
  dialog[data-passkey] {
2410
4363
  position: fixed;
2411
4364
  top: 0;
@@ -2427,6 +4380,22 @@ var OneAuthClient = class {
2427
4380
  dialog[data-passkey]::backdrop {
2428
4381
  background: transparent !important;
2429
4382
  }
4383
+ dialog[data-passkey][data-passkey-hidden] {
4384
+ width: 1px;
4385
+ height: 1px;
4386
+ max-width: 1px;
4387
+ max-height: 1px;
4388
+ opacity: 0;
4389
+ pointer-events: none;
4390
+ }
4391
+ dialog[data-passkey][data-passkey-hidden] iframe {
4392
+ width: 1px;
4393
+ height: 1px;
4394
+ opacity: 0 !important;
4395
+ pointer-events: none;
4396
+ backdrop-filter: none;
4397
+ -webkit-backdrop-filter: none;
4398
+ }
2430
4399
  dialog[data-passkey] iframe {
2431
4400
  position: fixed;
2432
4401
  top: 0;
@@ -2447,34 +4416,56 @@ var OneAuthClient = class {
2447
4416
  width: 100%;
2448
4417
  height: 100%;
2449
4418
  display: flex;
2450
- align-items: flex-start;
4419
+ align-items: center;
2451
4420
  justify-content: center;
2452
- padding-top: 50px;
2453
4421
  background: rgba(0, 0, 0, 0.4);
2454
4422
  backdrop-filter: blur(8px);
2455
4423
  -webkit-backdrop-filter: blur(8px);
2456
4424
  z-index: 1;
4425
+ /* Purely a visual loading screen \u2014 never intercept clicks.
4426
+ If the iframe's X button or backdrop-dismiss area happens to
4427
+ be reachable while the overlay is still painting (slow CI
4428
+ runners, redirect remounts), the click goes straight through
4429
+ instead of being trapped by the SDK preload. The previous
4430
+ rAF-deferred display:none lost this race in GitHub Actions
4431
+ \u2014 dialog-close.spec.ts repeatedly hit
4432
+ "<div data-passkey-overlay> intercepts pointer events". */
4433
+ pointer-events: none;
2457
4434
  animation: _1auth-backdrop-in 0.2s ease-out;
2458
4435
  }
2459
- @media (max-width: 768px) {
2460
- dialog[data-passkey] [data-passkey-overlay] {
2461
- align-items: flex-end;
2462
- padding-top: 0;
2463
- }
4436
+ dialog[data-passkey][data-passkey-hidden] [data-passkey-overlay] {
4437
+ display: none;
2464
4438
  }
2465
4439
  dialog[data-passkey] [data-passkey-card] {
2466
- width: 340px;
2467
- overflow: hidden;
2468
- border-radius: 14px;
2469
- box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
4440
+ width: ${LOADING_MODAL_WIDTH}px;
4441
+ box-sizing: border-box;
4442
+ padding: ${LOADING_MODAL_PADDING}px;
4443
+ border-radius: ${LOADING_MODAL_RADIUS}px;
4444
+ box-shadow: inset 0 0 0 1px ${borderColor};
2470
4445
  animation: _1auth-card-in 0.2s cubic-bezier(0.32, 0.72, 0, 1);
2471
- max-height: calc(100dvh - 100px);
4446
+ max-height: 100dvh;
4447
+ overflow-y: auto;
4448
+ display: flex;
4449
+ flex-direction: column;
4450
+ gap: ${LOADING_CARD_GAP}px;
4451
+ font-family: ${LOADING_FONT_FAMILY};
4452
+ /* line-height: normal must be set on the card so the host page's
4453
+ Tailwind preflight (line-height: 1.5 on <body>) doesn't bleed
4454
+ into our card via inheritance \u2014 that's what made the SDK preload
4455
+ render taller than the iframe screen. */
4456
+ line-height: ${LOADING_LINE_HEIGHT};
4457
+ -webkit-font-smoothing: antialiased;
4458
+ -moz-osx-font-smoothing: grayscale;
2472
4459
  }
2473
- @media (max-width: 768px) {
4460
+ @media (max-width: ${LOADING_MOBILE_BREAKPOINT - 1}px) {
4461
+ dialog[data-passkey] [data-passkey-overlay] {
4462
+ align-items: flex-end;
4463
+ }
2474
4464
  dialog[data-passkey] [data-passkey-card] {
2475
4465
  width: 100%;
2476
4466
  border-bottom-left-radius: 0;
2477
4467
  border-bottom-right-radius: 0;
4468
+ padding-bottom: calc(${LOADING_MODAL_PADDING}px + env(safe-area-inset-bottom));
2478
4469
  animation: _1auth-card-slide 0.3s cubic-bezier(0.32, 0.72, 0, 1);
2479
4470
  }
2480
4471
  }
@@ -2497,13 +4488,11 @@ var OneAuthClient = class {
2497
4488
  dialog.appendChild(style);
2498
4489
  const overlay = document.createElement("div");
2499
4490
  overlay.dataset.passkeyOverlay = "";
2500
- const _sp = '<path d="M10 0.5C8.02219 0.5 6.08879 1.08649 4.4443 2.1853C2.79981 3.28412 1.51809 4.8459 0.761209 6.67316C0.00433288 8.50043 -0.1937 10.5111 0.192152 12.4509C0.578004 14.3907 1.53041 16.1725 2.92894 17.5711C4.32746 18.9696 6.10929 19.922 8.0491 20.3078C9.98891 20.6937 11.9996 20.4957 13.8268 19.7388C15.6541 18.9819 17.2159 17.7002 18.3147 16.0557C19.4135 14.4112 20 12.4778 20 10.5C20 7.84783 18.9464 5.3043 17.0711 3.42893C15.1957 1.55357 12.6522 0.5 10 0.5ZM10 17.7727C8.56159 17.7727 7.15549 17.3462 5.95949 16.547C4.7635 15.7479 3.83134 14.6121 3.28088 13.2831C2.73042 11.9542 2.5864 10.4919 2.86702 9.08116C3.14764 7.67039 3.8403 6.37451 4.85741 5.3574C5.87452 4.3403 7.17039 3.64764 8.58116 3.36702C9.99193 3.0864 11.4542 3.23042 12.7832 3.78088C14.1121 4.33133 15.2479 5.26349 16.0471 6.45949C16.8462 7.65548 17.2727 9.06159 17.2727 10.5C17.2727 12.4288 16.5065 14.2787 15.1426 15.6426C13.7787 17.0065 11.9288 17.7727 10 17.7727Z" fill="currentColor" opacity="0.3"/><path d="M10 3.22767C11.7423 3.22846 13.4276 3.8412 14.7556 4.95667C16.0837 6.07214 16.9681 7.61784 17.2512 9.31825C17.3012 9.64364 17.4662 9.94096 17.7169 10.1573C17.9677 10.3737 18.2878 10.4951 18.6205 10.5C18.8211 10.5001 19.0193 10.457 19.2012 10.3735C19.3832 10.2901 19.5445 10.1684 19.674 10.017C19.8036 9.86549 19.8981 9.68789 19.9511 9.49656C20.004 9.30523 20.0141 9.10478 19.9807 8.90918C19.5986 6.56305 18.3843 4.42821 16.5554 2.88726C14.7265 1.34631 12.4025 0.5 10 0.5C7.59751 0.5 5.27354 1.34631 3.44461 2.88726C1.61569 4.42821 0.401366 6.56305 0.0192815 8.90918C-0.0141442 9.10478 -0.00402016 9.30523 0.0489472 9.49656C0.101914 9.68789 0.196449 9.86549 0.325956 10.017C0.455463 10.1684 0.616823 10.2901 0.798778 10.3735C0.980732 10.457 1.1789 10.5001 1.37945 10.5C1.71216 10.4951 2.03235 10.3737 2.28307 10.1573C2.5338 9.94096 2.69883 9.64364 2.74882 9.31825C3.03193 7.61784 3.91633 6.07214 5.24436 4.95667C6.57239 3.8412 8.25775 3.22846 10 3.22767Z" fill="currentColor"/>';
2501
- overlay.innerHTML = `<div data-passkey-card style="background:${bgPrimary};border:1px solid ${borderColor};font-family:ui-sans-serif,system-ui,sans-serif,'Apple Color Emoji','Segoe UI Emoji'"><div style="height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:${bgSecondary};border-bottom:1px solid ${borderColor}"><div style="display:flex;align-items:center;gap:8px"><div style="display:flex;width:20px;height:20px;align-items:center;justify-content:center;border-radius:4px;background:${bgSurface}"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="${textPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg></div><span style="font-size:13px;font-weight:500;color:${textPrimary};max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${hostname}</span><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${textSecondary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg></div><div style="display:flex;align-items:center;gap:4px"><div style="display:flex;width:24px;height:24px;align-items:center;justify-content:center;border-radius:6px;color:${textSecondary}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div><button data-passkey-close style="display:flex;width:24px;height:24px;align-items:center;justify-content:center;border-radius:6px;color:${textSecondary};border:none;background:none;cursor:pointer;padding:0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button></div></div><div style="padding:12px"><div style="display:flex;align-items:center;gap:8px"><div style="display:flex;width:32px;height:32px;min-width:32px;align-items:center;justify-content:center;border-radius:50%;background:${accentTint};padding:6px"><svg style="width:100%;height:100%;animation:_1auth-spin 0.8s linear infinite;color:${accentColor}" fill="none" viewBox="0 0 20 21" xmlns="http://www.w3.org/2000/svg">${_sp}</svg></div><div style="font-weight:500;font-size:18px;color:${textPrimary}">Loading...</div></div><div style="margin-top:8px"><div style="font-size:15px;line-height:20px;color:${textPrimary}">This will only take a few moments.</div><div style="font-size:15px;line-height:20px;color:${textSecondary}">Please do not close the window.</div></div></div></div>`;
2502
- overlay.addEventListener("click", (e) => {
2503
- if (e.target === overlay) cleanup();
2504
- });
2505
- const overlayCloseBtn = overlay.querySelector("[data-passkey-close]");
2506
- if (overlayCloseBtn) overlayCloseBtn.addEventListener("click", () => cleanup());
4491
+ const spinnerCenter = SPINNER_SIZE / 2;
4492
+ const spinnerGap = SPINNER_CIRCUMFERENCE - SPINNER_ARC;
4493
+ const spinnerSvg = `<svg style="position:absolute;inset:0;width:100%;height:100%;animation:_1auth-spin 0.8s linear infinite;color:${spinnerColor}" viewBox="0 0 ${SPINNER_SIZE} ${SPINNER_SIZE}" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="${spinnerCenter}" cy="${spinnerCenter}" r="${SPINNER_RADIUS}" stroke="currentColor" opacity="0.3" stroke-width="${SPINNER_STROKE}"/><circle cx="${spinnerCenter}" cy="${spinnerCenter}" r="${SPINNER_RADIUS}" stroke="currentColor" stroke-width="${SPINNER_STROKE}" stroke-linecap="round" stroke-dasharray="${SPINNER_ARC} ${spinnerGap}" transform="rotate(-90 ${spinnerCenter} ${spinnerCenter})"/></svg>`;
4494
+ const rhinestoneLogo = `<svg width="${LOGO_WIDTH}" height="${LOGO_HEIGHT}" viewBox="0 0 72 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.4767 14.815C9.80706 15.3364 9.0433 15.7628 8.08753 15.8207C7.91561 15.8312 7.74321 15.8312 7.5713 15.8207C6.616 15.7628 5.85177 15.336 5.18212 14.815C4.56047 14.3303 3.87765 13.648 3.10071 12.871L2.9713 12.7416C2.19483 11.9647 1.51153 11.2818 1.02777 10.6602C0.505414 9.99055 0.0790608 9.22679 0.0211784 8.27102C0.0106814 8.0991 0.0106814 7.9267 0.0211784 7.75479C0.0790608 6.79949 0.505884 6.03526 1.02683 5.36561C1.51153 4.74349 2.19388 4.06114 2.97083 3.2842L3.03577 3.21926L3.10024 3.15479C3.87718 2.37784 4.56 1.69502 5.18165 1.21079C5.85177 0.689374 6.61647 0.26255 7.57177 0.205138C7.74368 0.194641 7.91608 0.194641 8.088 0.205138C9.04377 0.26255 9.80753 0.689374 10.4772 1.21079C11.0993 1.69502 11.7816 2.37784 12.5586 3.15431L12.6885 3.2842C13.4649 4.06067 14.1478 4.74349 14.632 5.36561C15.1534 6.03526 15.5802 6.79902 15.6381 7.75479C15.6485 7.92671 15.6485 8.09879 15.6381 8.27102C15.5802 9.22632 15.1534 9.99055 14.632 10.6602C14.1478 11.2818 13.4649 11.9647 12.6885 12.7416L12.6235 12.8061L12.5586 12.871C11.7821 13.6475 11.0993 14.3308 10.4772 14.8146M1.58306 8.17643C1.64518 9.20326 2.47718 10.0353 4.14212 11.7002C5.80706 13.3647 6.64 14.1971 7.66588 14.2588C7.77506 14.2654 7.88424 14.2654 7.99341 14.2588C9.0193 14.1967 9.85224 13.3647 11.5167 11.6997C13.1812 10.0348 14.0136 9.20279 14.0758 8.17596C14.0821 8.06688 14.0821 7.95752 14.0758 7.84843C14.0136 6.82255 13.1816 5.98961 11.5167 4.32514C9.85177 2.66067 9.01977 1.8282 7.99341 1.76608C7.88433 1.75974 7.77497 1.75974 7.66588 1.76608C6.63953 1.8282 5.80706 2.6602 4.14212 4.32514C2.47765 5.98961 1.64518 6.82208 1.58306 7.84843C1.57672 7.95752 1.57672 8.06735 1.58306 8.17643Z" fill="${textFooter}"/><path d="M3.65796 8.01173C3.65796 7.84138 3.7789 7.69549 3.94455 7.65597C4.78502 7.45502 5.44714 7.1402 6.0029 6.64797C6.16666 6.50271 6.32102 6.34836 6.46596 6.18491C6.95819 5.62914 7.27302 4.96702 7.47396 4.12655C7.49245 4.04577 7.53761 3.97356 7.60215 3.92157C7.66669 3.86958 7.74685 3.84084 7.82972 3.83997C8.00008 3.83997 8.14596 3.96044 8.18549 4.12655C8.3869 4.96702 8.70125 5.62914 9.19349 6.18491C9.33874 6.34836 9.4931 6.50271 9.65655 6.64797C10.2123 7.1402 10.8744 7.45502 11.7154 7.65597C11.7961 7.67455 11.8682 7.71975 11.9201 7.78428C11.972 7.84881 12.0006 7.92892 12.0015 8.01173C12.0006 8.09454 11.972 8.17465 11.9201 8.23918C11.8682 8.30371 11.7961 8.34891 11.7154 8.3675C10.8744 8.56844 10.2128 8.88326 9.65655 9.37549C9.4931 9.52075 9.33874 9.6751 9.19349 9.83855C8.70125 10.3943 8.38643 11.0564 8.18549 11.8969C8.16699 11.9777 8.12184 12.0499 8.0573 12.1019C7.99276 12.1539 7.91259 12.1826 7.82972 12.1835C7.74685 12.1826 7.66669 12.1539 7.60215 12.1019C7.53761 12.0499 7.49245 11.9777 7.47396 11.8969C7.27302 11.0564 6.95819 10.3943 6.46596 9.83855C6.32069 9.67537 6.16608 9.52076 6.0029 9.37549C5.44714 8.88326 4.78502 8.56844 3.94455 8.3675C3.86376 8.349 3.79156 8.30384 3.73957 8.23931C3.68758 8.17477 3.65883 8.0946 3.65796 8.01173Z" fill="${textFooter}"/><path d="M30.1186 4.56946C30.1186 5.08193 30.5031 5.44476 31.08 5.44476C31.6569 5.44476 32.0306 5.08193 32.0306 4.56946C32.0306 4.04711 31.6569 3.70593 31.08 3.70593C30.5031 3.70593 30.1186 4.04711 30.1186 4.56993M30.3106 6.00946V11.3422H31.8494V6.00946H30.3106ZM25.9115 11.3422H24.3732V3.87676H25.9115V6.86311C26.1572 6.35111 26.7129 5.84946 27.5035 5.84946C28.7958 5.84946 29.3835 6.68146 29.3835 8.13182V11.3422H27.8447V8.30264C27.8447 7.53464 27.5031 7.14029 26.9158 7.14029C26.2532 7.14029 25.9115 7.67346 25.9115 8.44146V11.3422ZM21.4682 11.3422H19.9304V6.00946H21.4687L21.1977 7.23299C21.1939 7.25002 21.1941 7.26767 21.1981 7.28464C21.202 7.30161 21.2098 7.31747 21.2207 7.33106C21.2316 7.34465 21.2455 7.35561 21.2612 7.36315C21.2769 7.3707 21.2941 7.37462 21.3115 7.37464C21.3657 7.37464 21.4127 7.33699 21.4268 7.28429C21.656 6.44193 22.3482 5.92429 23.0602 5.92429C23.3379 5.92429 23.5087 5.94546 23.6264 5.96664V7.38546C23.4061 7.36702 23.1853 7.35635 22.9642 7.35346C22.2913 7.35346 21.4687 7.66264 21.4687 9.34782L21.4682 11.3422ZM34.4626 11.3422H32.9242V6.00946H34.4631L34.4414 6.90546C34.6871 6.38311 35.264 5.84946 36.0546 5.84946C37.3473 5.84946 37.9346 6.68146 37.9346 8.13182V11.3422H36.3962V8.30264C36.3962 7.53464 36.0546 7.14029 35.4668 7.14029C34.8047 7.14029 34.4626 7.67346 34.4626 8.44146V11.3422Z" fill="${textFooter}"/><path fill-rule="evenodd" clip-rule="evenodd" d="M38.7355 8.68664C38.7355 10.3186 39.7506 11.513 41.4489 11.513C42.9548 11.513 43.7567 10.6918 43.9915 9.78499L42.56 9.51864C42.464 9.89182 42.0899 10.3398 41.4381 10.3398C40.7652 10.3398 40.2099 9.74217 40.1991 9.02782H44.0024C44.0344 8.89982 44.0555 8.71864 44.0555 8.47346C44.0555 6.92711 42.9016 5.83911 41.4598 5.83911C39.9534 5.83911 38.7355 7.04382 38.7355 8.68664ZM42.56 8.11064H40.2527C40.267 7.80585 40.3982 7.51829 40.6191 7.30777C40.8399 7.09724 41.1335 6.97995 41.4386 6.98029C42.0362 6.98029 42.5812 7.42829 42.56 8.11064Z" fill="${textFooter}"/><path d="M47.1016 11.513C45.8946 11.513 44.7732 11.097 44.5487 9.85982L45.9802 9.64664C46.1511 10.201 46.5355 10.4358 47.0805 10.4358C47.5826 10.4358 47.8494 10.201 47.8494 9.89182C47.8494 9.62499 47.6466 9.44382 47.1016 9.33746L46.5995 9.23064C45.5101 9.00664 44.8264 8.50546 44.8264 7.62029C44.8264 6.56429 45.7449 5.83911 47.0805 5.83911C48.3412 5.83911 49.2706 6.42546 49.3986 7.52382L47.9887 7.73746C47.9242 7.21511 47.5609 6.91629 47.0805 6.91629C46.5892 6.91629 46.3322 7.18264 46.3322 7.49229C46.3322 7.73699 46.5035 7.90829 46.9628 8.00429L47.4649 8.09982C48.7365 8.35582 49.3986 8.86782 49.3986 9.84899C49.3986 10.8626 48.3732 11.513 47.1016 11.513Z" fill="${textFooter}"/><path fill-rule="evenodd" clip-rule="evenodd" d="M54.4423 8.67582C54.4423 10.2118 55.5214 11.513 57.2197 11.513C58.9181 11.513 60.008 10.2118 60.008 8.67582C60.008 7.12946 58.9186 5.83911 57.2197 5.83911C55.5209 5.83911 54.4423 7.12946 54.4423 8.67582ZM58.4588 8.67582C58.4588 9.65699 57.8715 10.1266 57.2197 10.1266C56.5788 10.1266 55.991 9.65746 55.991 8.67582C55.991 7.70546 56.5788 7.21464 57.2197 7.21464C57.8715 7.21464 58.4588 7.69464 58.4588 8.67582Z" fill="${textFooter}"/><path d="M62.4071 11.3421H60.8692V6.00937H62.4071L62.3859 6.90536C62.6316 6.38301 63.2085 5.84937 63.9991 5.84937C65.2918 5.84937 65.8791 6.68137 65.8791 8.13172V11.3421H64.3407V8.30254C64.3407 7.53454 63.9991 7.14019 63.4113 7.14019C62.7492 7.14019 62.4071 7.67336 62.4071 8.44137V11.3421Z" fill="${textFooter}"/><path fill-rule="evenodd" clip-rule="evenodd" d="M66.68 8.68664C66.68 10.3186 67.6951 11.513 69.3934 11.513C70.8998 11.513 71.7012 10.6918 71.936 9.78499L70.5045 9.51864C70.4085 9.89182 70.0344 10.3398 69.3826 10.3398C68.7097 10.3398 68.1544 9.74217 68.1435 9.02782H71.9468C71.9788 8.89982 72 8.71864 72 8.47346C72 6.92711 70.8461 5.83911 69.4043 5.83911C67.8979 5.83911 66.68 7.04382 66.68 8.68664ZM70.5045 8.11064H68.1972C68.2111 7.80581 68.3422 7.51812 68.563 7.30754C68.7839 7.09696 69.0775 6.97973 69.3826 6.98029C69.9812 6.98029 70.5257 7.42829 70.5045 8.11064Z" fill="${textFooter}"/><path d="M52.4418 9.04852V7.26217H53.968V6.00005H52.4418V4.42029H50.9124V6.00005H49.8042V7.26217H50.9124V9.10923C50.9124 9.25511 50.9044 9.43394 50.9266 9.59958C50.9647 9.88194 51.0753 10.3384 51.4691 10.7313C51.8626 11.1243 52.32 11.2349 52.6023 11.2725C52.8216 11.3017 53.0654 11.3012 53.2197 11.3008L53.8743 11.3026V9.86923H53.256C52.8687 9.86923 52.6748 9.86923 52.5543 9.74876C52.4414 9.63582 52.4414 9.45793 52.4418 9.11488V9.04852Z" fill="${textFooter}"/></svg>`;
4495
+ overlay.innerHTML = `<div data-passkey-card style="background:${bgPrimary}"><div style="display:flex;width:100%;flex-direction:column;align-items:flex-start;gap:${LOADING_CARD_GAP}px"><div style="display:flex;width:100%;flex-direction:column;align-items:center;gap:${LOADING_BODY_GAP}px"><div style="display:flex;align-items:center;padding:${LOADING_ICON_PADDING}px"><div style="position:relative;width:${SPINNER_SIZE}px;height:${SPINNER_SIZE}px">${spinnerSvg}</div></div><div style="display:flex;width:100%;flex-direction:column;align-items:center;gap:${LOADING_TITLE_BLOCK_GAP}px;text-align:center"><p style="margin:0;font-size:${LOADING_TITLE_FONT_SIZE}px;font-weight:${LOADING_TITLE_FONT_WEIGHT};color:${textPrimary};line-height:${LOADING_LINE_HEIGHT}">${LOADING_TITLE}</p><p style="margin:0;font-size:${LOADING_SUBTITLE_FONT_SIZE}px;font-weight:${LOADING_SUBTITLE_FONT_WEIGHT};color:${textSecondary};line-height:${LOADING_LINE_HEIGHT}">${LOADING_SUBTITLE}</p></div></div><div style="display:flex;width:100%;align-items:center;justify-content:center;gap:${LOADING_FOOTER_GAP}px"><span style="font-size:${LOADING_FOOTER_FONT_SIZE}px;font-weight:${LOADING_FOOTER_FONT_WEIGHT};color:${textFooter};line-height:${LOADING_LINE_HEIGHT}">Powered by</span>` + rhinestoneLogo + `</div></div></div>`;
2507
4496
  dialog.appendChild(overlay);
2508
4497
  const iframe = document.createElement("iframe");
2509
4498
  iframe.setAttribute(
@@ -2519,32 +4508,24 @@ var OneAuthClient = class {
2519
4508
  iframe.setAttribute("tabindex", "0");
2520
4509
  iframe.setAttribute(
2521
4510
  "sandbox",
2522
- "allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
4511
+ "allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-downloads"
2523
4512
  );
2524
- iframe.setAttribute("src", url);
2525
4513
  iframe.setAttribute("title", "Passkey");
2526
4514
  iframe.style.opacity = "0";
2527
- dialog.appendChild(iframe);
2528
- const inertObserver = new MutationObserver((mutations) => {
2529
- for (const mutation of mutations) {
2530
- if (mutation.attributeName === "inert") {
2531
- dialog.removeAttribute("inert");
2532
- }
2533
- }
2534
- });
2535
- inertObserver.observe(dialog, { attributes: true });
2536
4515
  let overlayHidden = false;
2537
4516
  const hideOverlay = () => {
2538
4517
  if (overlayHidden) return;
2539
4518
  overlayHidden = true;
2540
4519
  iframe.style.opacity = "1";
4520
+ overlay.style.pointerEvents = "none";
2541
4521
  requestAnimationFrame(() => {
2542
4522
  overlay.style.display = "none";
2543
4523
  });
2544
4524
  };
4525
+ const overlayFailsafe = setTimeout(hideOverlay, 8e3);
2545
4526
  const handleMessage = (event) => {
2546
4527
  if (event.origin !== hostUrl.origin) return;
2547
- if (event.data?.type === "PASSKEY_RENDERED") {
4528
+ if (event.data?.type === "PASSKEY_RENDERED" || event.data?.type === "PASSKEY_READY") {
2548
4529
  hideOverlay();
2549
4530
  }
2550
4531
  if (event.data?.type === "PASSKEY_DISCONNECT") {
@@ -2552,18 +4533,41 @@ var OneAuthClient = class {
2552
4533
  }
2553
4534
  };
2554
4535
  window.addEventListener("message", handleMessage);
4536
+ const iframeUrl = new URL(url, window.location.href);
4537
+ iframeUrl.searchParams.set("fullViewport", "1");
4538
+ iframe.setAttribute("src", iframeUrl.toString());
4539
+ dialog.appendChild(iframe);
4540
+ const viewportInfoCleanup = this.wireViewportInfo(iframe);
4541
+ const inertObserver = new MutationObserver((mutations) => {
4542
+ for (const mutation of mutations) {
4543
+ if (mutation.attributeName === "inert") {
4544
+ dialog.removeAttribute("inert");
4545
+ }
4546
+ }
4547
+ });
4548
+ inertObserver.observe(dialog, { attributes: true });
2555
4549
  const handleEscape = (event) => {
2556
4550
  if (event.key === "Escape") {
2557
4551
  cleanup();
2558
4552
  }
2559
4553
  };
2560
4554
  document.addEventListener("keydown", handleEscape);
2561
- dialog.showModal();
4555
+ const reveal = () => {
4556
+ delete dialog.dataset.passkeyHidden;
4557
+ iframe.style.opacity = "1";
4558
+ };
4559
+ if (options?.hidden) {
4560
+ dialog.show();
4561
+ } else {
4562
+ dialog.showModal();
4563
+ }
2562
4564
  let cleanedUp = false;
2563
- const cleanup = () => {
4565
+ const destroy = () => {
2564
4566
  if (cleanedUp) return;
2565
4567
  cleanedUp = true;
4568
+ clearTimeout(overlayFailsafe);
2566
4569
  inertObserver.disconnect();
4570
+ viewportInfoCleanup();
2567
4571
  window.removeEventListener("message", handleMessage);
2568
4572
  document.removeEventListener("keydown", handleEscape);
2569
4573
  dialog.close();
@@ -2576,14 +4580,23 @@ var OneAuthClient = class {
2576
4580
  }
2577
4581
  dialog.remove();
2578
4582
  };
2579
- return { dialog, iframe, cleanup };
4583
+ const cleanup = options?.persistent ? () => {
4584
+ if (dialog.open) dialog.close();
4585
+ } : destroy;
4586
+ return { dialog, iframe, cleanup, destroy, reveal };
2580
4587
  }
2581
4588
  /**
2582
4589
  * Listen for the auth result from the modal dialog.
2583
4590
  *
2584
- * Waits for `PASSKEY_READY` before processing any other messages to avoid
2585
- * acting on stale `PASSKEY_CLOSE` events that may still be in the event queue
2586
- * from a previously closed dialog.
4591
+ * Each modal call generates a per-session nonce that the iframe must
4592
+ * echo back in `PASSKEY_CLOSE` / `PASSKEY_LOGIN_RESULT` /
4593
+ * `PASSKEY_RETRY_POPUP` messages. Without an echo, a queued CLOSE
4594
+ * from the previous iframe could be dispatched to the new modal's
4595
+ * listener (postMessage events tagged with `event.source` from a now-
4596
+ * destroyed iframe still reach the parent's message handler) and
4597
+ * silently cancel the fresh auth attempt. The nonce makes that race
4598
+ * harmless: a stale message that doesn't carry the current modal's
4599
+ * nonce is ignored.
2587
4600
  *
2588
4601
  * Also handles the `PASSKEY_RETRY_POPUP` message: sent by the dialog when a
2589
4602
  * password manager (e.g. Bitwarden) intercepts the WebAuthn call inside the
@@ -2596,25 +4609,42 @@ var OneAuthClient = class {
2596
4609
  * @param cleanup - Idempotent teardown for the modal dialog.
2597
4610
  * @returns An `AuthResult` discriminated union.
2598
4611
  */
2599
- waitForModalAuthResponse(_dialog, iframe, cleanup) {
4612
+ waitForModalAuthResponse(_dialog, iframe, cleanup, requestId) {
2600
4613
  const dialogOrigin = this.getDialogOrigin();
4614
+ const modalNonce = generateModalNonce();
2601
4615
  return new Promise((resolve) => {
2602
- let dialogReady = false;
2603
4616
  const handleMessage = (event) => {
2604
4617
  if (event.origin !== dialogOrigin) return;
2605
4618
  const data = event.data;
4619
+ const matchesSource = event.source === iframe.contentWindow;
4620
+ const matchesRequest = typeof requestId === "string" && data?.requestId === requestId;
4621
+ const matchesNonce = data?.modalNonce === modalNonce;
4622
+ const isCurrentFrameClose = data?.type === "PASSKEY_CLOSE" && matchesSource;
4623
+ if (data?.type === "PASSKEY_CLOSE" && (matchesRequest || matchesNonce || isCurrentFrameClose)) {
4624
+ window.removeEventListener("message", handleMessage);
4625
+ cleanup();
4626
+ resolve({
4627
+ success: false,
4628
+ error: {
4629
+ code: "USER_CANCELLED",
4630
+ message: "Authentication was cancelled"
4631
+ }
4632
+ });
4633
+ return;
4634
+ }
2606
4635
  if (data?.type === "PASSKEY_READY") {
2607
- dialogReady = true;
2608
4636
  iframe.contentWindow?.postMessage({
2609
4637
  type: "PASSKEY_INIT",
2610
4638
  mode: "iframe",
2611
- fullViewport: true
4639
+ fullViewport: true,
4640
+ modalNonce
2612
4641
  }, dialogOrigin);
2613
4642
  return;
2614
4643
  }
2615
- if (!dialogReady && data?.type === "PASSKEY_CLOSE") {
2616
- return;
2617
- }
4644
+ const hasModalNonce = typeof data?.modalNonce === "string";
4645
+ const isCurrentDialogMessage = matchesSource || matchesNonce || !hasModalNonce;
4646
+ if (!isCurrentDialogMessage) return;
4647
+ if (data?.modalNonce && data.modalNonce !== modalNonce) return;
2618
4648
  if (data?.type === "PASSKEY_LOGIN_RESULT") {
2619
4649
  window.removeEventListener("message", handleMessage);
2620
4650
  cleanup();
@@ -2625,7 +4655,8 @@ var OneAuthClient = class {
2625
4655
  id: data.data?.user?.id,
2626
4656
  username: data.data?.username,
2627
4657
  address: data.data?.address
2628
- }
4658
+ },
4659
+ signerType: data.data?.signerType ?? "passkey"
2629
4660
  });
2630
4661
  } else {
2631
4662
  resolve({
@@ -2638,16 +4669,6 @@ var OneAuthClient = class {
2638
4669
  cleanup();
2639
4670
  const popupUrl = data.data?.url?.replace("mode=iframe", "mode=popup") || `${this.getDialogUrl()}/dialog/auth?mode=popup${this.config.clientId ? `&clientId=${this.config.clientId}` : ""}`;
2640
4671
  this.waitForPopupAuthResponse(popupUrl).then(resolve);
2641
- } else if (data?.type === "PASSKEY_CLOSE") {
2642
- window.removeEventListener("message", handleMessage);
2643
- cleanup();
2644
- resolve({
2645
- success: false,
2646
- error: {
2647
- code: "USER_CANCELLED",
2648
- message: "Authentication was cancelled"
2649
- }
2650
- });
2651
4672
  }
2652
4673
  };
2653
4674
  window.addEventListener("message", handleMessage);
@@ -2688,7 +4709,8 @@ var OneAuthClient = class {
2688
4709
  id: data.data?.user?.id,
2689
4710
  username: data.data?.username,
2690
4711
  address: data.data?.address
2691
- }
4712
+ },
4713
+ signerType: data.data?.signerType ?? "passkey"
2692
4714
  });
2693
4715
  } else {
2694
4716
  resolve({
@@ -2712,63 +4734,6 @@ var OneAuthClient = class {
2712
4734
  window.addEventListener("message", handleMessage);
2713
4735
  });
2714
4736
  }
2715
- /**
2716
- * Listen for the authenticate result from the modal dialog.
2717
- *
2718
- * The authenticate flow combines sign-in/sign-up with optional off-chain
2719
- * challenge signing. The dialog sends `PASSKEY_AUTHENTICATE_RESULT` on
2720
- * completion with both user details and, if a challenge was requested, the
2721
- * WebAuthn signature and the hashed challenge value for server-side
2722
- * verification.
2723
- *
2724
- * @param _dialog - Unused; kept for signature consistency.
2725
- * @param _iframe - Unused; kept for signature consistency.
2726
- * @param cleanup - Idempotent teardown for the modal dialog.
2727
- * @returns An `AuthenticateResult` discriminated union.
2728
- */
2729
- waitForAuthenticateResponse(_dialog, _iframe, cleanup) {
2730
- const dialogOrigin = this.getDialogOrigin();
2731
- return new Promise((resolve) => {
2732
- const handleMessage = (event) => {
2733
- if (event.origin !== dialogOrigin) return;
2734
- const data = event.data;
2735
- if (data?.type === "PASSKEY_AUTHENTICATE_RESULT") {
2736
- window.removeEventListener("message", handleMessage);
2737
- cleanup();
2738
- if (data.success) {
2739
- resolve({
2740
- success: true,
2741
- user: {
2742
- id: data.data?.user?.id,
2743
- username: data.data?.username,
2744
- address: data.data?.accountAddress
2745
- },
2746
- challenge: data.data?.signature ? {
2747
- signature: data.data.signature,
2748
- signedHash: data.data.signedHash
2749
- } : void 0
2750
- });
2751
- } else {
2752
- resolve({
2753
- success: false,
2754
- error: data.error
2755
- });
2756
- }
2757
- } else if (data?.type === "PASSKEY_CLOSE") {
2758
- window.removeEventListener("message", handleMessage);
2759
- cleanup();
2760
- resolve({
2761
- success: false,
2762
- error: {
2763
- code: "USER_CANCELLED",
2764
- message: "Authentication was cancelled"
2765
- }
2766
- });
2767
- }
2768
- };
2769
- window.addEventListener("message", handleMessage);
2770
- });
2771
- }
2772
4737
  /**
2773
4738
  * Listen for the connect result from the connect dialog.
2774
4739
  *
@@ -2827,48 +4792,100 @@ var OneAuthClient = class {
2827
4792
  });
2828
4793
  }
2829
4794
  /**
2830
- * Listen for the consent result from the consent dialog.
2831
- *
2832
- * The consent dialog shows the user which data fields an app is requesting
2833
- * access to and lets them approve or deny. On approval, `data` contains the
2834
- * consented field values (e.g. email address, device names) and `grantedAt`
2835
- * records when consent was given.
4795
+ * Listen for grant-permission results and bridge sponsorship token requests.
2836
4796
  *
2837
- * A `PASSKEY_CLOSE` without a prior `PASSKEY_CONSENT_RESULT` is treated as an
2838
- * explicit user cancellation (distinct from denial, though both yield
2839
- * `success: false`).
4797
+ * The grant iframe owns the passkey ceremony and install intent submission.
4798
+ * Extension-token minting stays in the app origin, so the SDK exposes a
4799
+ * narrow postMessage round-trip that never gives the iframe app secrets.
2840
4800
  *
2841
4801
  * @param _dialog - Unused; kept for signature consistency.
2842
- * @param _iframe - Unused; kept for signature consistency.
4802
+ * @param iframe - The grant iframe that receives extension-token responses.
2843
4803
  * @param cleanup - Idempotent teardown for the modal dialog.
2844
- * @returns A `RequestConsentResult` discriminated union.
4804
+ * @param sponsor - Whether this grant flow should request an extension token.
4805
+ * @returns A `GrantPermissionsResult` discriminated union.
2845
4806
  */
2846
- waitForConsentResponse(_dialog, _iframe, cleanup) {
4807
+ waitForGrantPermissionResponse(dialog, iframe, cleanup, sponsor, initPayload) {
2847
4808
  const dialogOrigin = this.getDialogOrigin();
2848
4809
  return new Promise((resolve) => {
2849
- const handleMessage = (event) => {
4810
+ let readySeen = false;
4811
+ const teardown = () => {
4812
+ clearTimeout(readyTimeout);
4813
+ window.removeEventListener("message", handleMessage);
4814
+ dialog.removeEventListener("close", handleClose);
4815
+ };
4816
+ const postInit = () => {
4817
+ iframe.contentWindow?.postMessage(
4818
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
4819
+ dialogOrigin
4820
+ );
4821
+ };
4822
+ const handleClose = () => {
4823
+ teardown();
4824
+ resolve({
4825
+ success: false,
4826
+ error: {
4827
+ code: "USER_CANCELLED",
4828
+ message: "User closed the dialog"
4829
+ }
4830
+ });
4831
+ };
4832
+ const handleMessage = async (event) => {
2850
4833
  if (event.origin !== dialogOrigin) return;
2851
4834
  const data = event.data;
2852
- if (data?.type === "PASSKEY_CONSENT_RESULT") {
2853
- window.removeEventListener("message", handleMessage);
4835
+ if (data?.type === "PASSKEY_READY") {
4836
+ readySeen = true;
4837
+ clearTimeout(readyTimeout);
4838
+ postInit();
4839
+ return;
4840
+ }
4841
+ if (data?.type === "PASSKEY_GRANT_EXTENSION_TOKEN_REQUEST") {
4842
+ const requestId = data.requestId;
4843
+ const intentOp = data.intentOp;
4844
+ const postResponse = (payload) => {
4845
+ iframe.contentWindow?.postMessage(
4846
+ { type: "PASSKEY_GRANT_EXTENSION_TOKEN_RESPONSE", requestId, ...payload },
4847
+ dialogOrigin
4848
+ );
4849
+ };
4850
+ if (!requestId || !intentOp) {
4851
+ postResponse({ success: false, error: "Invalid extension token request" });
4852
+ return;
4853
+ }
4854
+ if (!sponsor) {
4855
+ postResponse({ success: true });
4856
+ return;
4857
+ }
4858
+ if (!this.sponsorship) {
4859
+ postResponse({ success: false, error: MISSING_APP_CREDENTIALS_MESSAGE });
4860
+ return;
4861
+ }
4862
+ try {
4863
+ const token = await this.sponsorship.getExtensionToken(intentOp);
4864
+ postResponse({ success: true, token });
4865
+ } catch (err) {
4866
+ postResponse({
4867
+ success: false,
4868
+ error: err instanceof Error ? err.message : String(err)
4869
+ });
4870
+ }
4871
+ return;
4872
+ }
4873
+ if (data?.type === "PASSKEY_GRANT_PERMISSION_RESULT") {
4874
+ teardown();
2854
4875
  cleanup();
2855
4876
  if (data.success) {
2856
- resolve({
2857
- success: true,
2858
- data: data.data,
2859
- grantedAt: data.data?.grantedAt
2860
- });
4877
+ resolve({ success: true, ...data.data ?? {} });
2861
4878
  } else {
2862
4879
  resolve({
2863
4880
  success: false,
2864
- error: data.error ?? {
2865
- code: "USER_REJECTED",
2866
- message: "User denied the consent request"
4881
+ error: data.error ?? data.data?.error ?? {
4882
+ code: "GRANT_PERMISSION_FAILED",
4883
+ message: "Permission grant failed"
2867
4884
  }
2868
4885
  });
2869
4886
  }
2870
4887
  } else if (data?.type === "PASSKEY_CLOSE") {
2871
- window.removeEventListener("message", handleMessage);
4888
+ teardown();
2872
4889
  cleanup();
2873
4890
  resolve({
2874
4891
  success: false,
@@ -2879,7 +4896,20 @@ var OneAuthClient = class {
2879
4896
  });
2880
4897
  }
2881
4898
  };
4899
+ const readyTimeout = setTimeout(() => {
4900
+ if (readySeen) return;
4901
+ teardown();
4902
+ cleanup();
4903
+ resolve({
4904
+ success: false,
4905
+ error: {
4906
+ code: "USER_CANCELLED",
4907
+ message: "User closed the dialog"
4908
+ }
4909
+ });
4910
+ }, 1e4);
2882
4911
  window.addEventListener("message", handleMessage);
4912
+ dialog.addEventListener("close", handleClose);
2883
4913
  });
2884
4914
  }
2885
4915
  /**
@@ -3043,6 +5073,65 @@ var OneAuthClient = class {
3043
5073
  }
3044
5074
  };
3045
5075
 
5076
+ // src/permissions.ts
5077
+ function definePermissions(config) {
5078
+ const { name, ...permission } = config;
5079
+ return {
5080
+ permissions: [permission],
5081
+ contracts: [
5082
+ {
5083
+ address: config.address,
5084
+ name,
5085
+ abi: config.abi
5086
+ }
5087
+ ]
5088
+ };
5089
+ }
5090
+
5091
+ // src/crossChainPermissions.ts
5092
+ import { isAddress } from "viem";
5093
+ function toUnixSecondsBigint(input) {
5094
+ if (typeof input === "bigint") return input;
5095
+ return BigInt(Math.floor(input.getTime() / 1e3));
5096
+ }
5097
+ function resolveTokenForChain(token, chainId) {
5098
+ return isAddress(token, { strict: false }) ? token : getTokenAddress(token, chainId);
5099
+ }
5100
+ function createCrossChainPermission(input) {
5101
+ const fromLegs = Array.isArray(input.from) ? input.from : [input.from];
5102
+ const toLegs = Array.isArray(input.to) ? input.to : [input.to];
5103
+ if (fromLegs.length === 0) {
5104
+ throw new Error("createCrossChainPermission: `from` must contain at least one leg");
5105
+ }
5106
+ if (toLegs.length === 0) {
5107
+ throw new Error("createCrossChainPermission: `to` must contain at least one leg");
5108
+ }
5109
+ const validUntil = input.validUntil !== void 0 ? toUnixSecondsBigint(input.validUntil) : void 0;
5110
+ const validAfter = input.validAfter !== void 0 ? toUnixSecondsBigint(input.validAfter) : void 0;
5111
+ if (validUntil !== void 0 && validAfter !== void 0 && validAfter > validUntil) {
5112
+ throw new Error(
5113
+ `createCrossChainPermission: validAfter (${validAfter}) is greater than validUntil (${validUntil})`
5114
+ );
5115
+ }
5116
+ return {
5117
+ from: fromLegs.map((leg) => ({
5118
+ chain: leg.chain,
5119
+ token: resolveTokenForChain(leg.token, leg.chain.id),
5120
+ maxAmount: leg.maxAmount
5121
+ })),
5122
+ to: toLegs.map((leg) => ({
5123
+ chain: leg.chain,
5124
+ token: resolveTokenForChain(leg.token, leg.chain.id),
5125
+ recipient: leg.recipient
5126
+ })),
5127
+ validUntil,
5128
+ validAfter,
5129
+ fillDeadline: input.fillDeadline,
5130
+ recipientIsAccount: !input.allowRecipientNotAccount,
5131
+ settlementLayers: input.settlementLayers
5132
+ };
5133
+ }
5134
+
3046
5135
  // src/account.ts
3047
5136
  import {
3048
5137
  bytesToString,
@@ -3068,7 +5157,8 @@ function createPasskeyAccount(client, params) {
3068
5157
  address,
3069
5158
  signMessage: async ({ message }) => {
3070
5159
  const result = await client.signMessage({
3071
- username,
5160
+ username: username || void 0,
5161
+ accountAddress: address,
3072
5162
  message: normalizeMessage(message)
3073
5163
  });
3074
5164
  if (!result.success || !result.signature) {
@@ -3103,7 +5193,8 @@ function createPasskeyAccount(client, params) {
3103
5193
  ])
3104
5194
  );
3105
5195
  const result = await client.signTypedData({
3106
- username,
5196
+ username: username || void 0,
5197
+ accountAddress: address,
3107
5198
  domain,
3108
5199
  types: normalizedTypes,
3109
5200
  primaryType: typedData.primaryType,
@@ -3117,29 +5208,37 @@ function createPasskeyAccount(client, params) {
3117
5208
  });
3118
5209
  return {
3119
5210
  ...account,
3120
- username
5211
+ ...username ? { username } : {},
5212
+ accountAddress: address
3121
5213
  };
3122
5214
  }
3123
5215
 
3124
5216
  // src/walletClient/index.ts
3125
5217
  import {
3126
5218
  createWalletClient,
3127
- hashMessage,
5219
+ hashMessage as hashMessage2,
3128
5220
  hashTypedData as hashTypedData2
3129
5221
  } from "viem";
3130
5222
  import { toAccount as toAccount2 } from "viem/accounts";
3131
5223
  function createPasskeyWalletClient(config) {
3132
- const resolvedUsername = config.username ?? config.accountAddress;
5224
+ const identity = {
5225
+ ...config.username ? { username: config.username } : {},
5226
+ accountAddress: config.accountAddress
5227
+ };
3133
5228
  const provider = new OneAuthClient({
3134
5229
  providerUrl: config.providerUrl,
3135
5230
  clientId: config.clientId,
3136
- dialogUrl: config.dialogUrl
5231
+ dialogUrl: config.dialogUrl,
5232
+ theme: config.theme,
5233
+ blind_signing: config.blind_signing,
5234
+ testnets: config.testnets,
5235
+ sponsorship: config.sponsorship
3137
5236
  });
3138
5237
  const signMessageImpl = async (message) => {
3139
- const hash = hashMessage(message);
5238
+ const hash = hashMessage2(message);
3140
5239
  const result = await provider.signWithModal({
3141
5240
  challenge: hash,
3142
- username: resolvedUsername,
5241
+ ...identity,
3143
5242
  description: "Sign message",
3144
5243
  transaction: {
3145
5244
  actions: [
@@ -3170,7 +5269,7 @@ function createPasskeyWalletClient(config) {
3170
5269
  const hash = hashCalls(calls);
3171
5270
  const result = await provider.signWithModal({
3172
5271
  challenge: hash,
3173
- username: resolvedUsername,
5272
+ ...identity,
3174
5273
  description: "Sign transaction",
3175
5274
  transaction: buildTransactionReview(calls)
3176
5275
  });
@@ -3186,7 +5285,7 @@ function createPasskeyWalletClient(config) {
3186
5285
  const hash = hashTypedData2(typedData);
3187
5286
  const result = await provider.signWithModal({
3188
5287
  challenge: hash,
3189
- username: resolvedUsername,
5288
+ ...identity,
3190
5289
  description: "Sign typed data",
3191
5290
  transaction: {
3192
5291
  actions: [
@@ -3213,10 +5312,12 @@ function createPasskeyWalletClient(config) {
3213
5312
  data: call.data || "0x",
3214
5313
  value: call.value !== void 0 ? call.value.toString() : "0",
3215
5314
  label: call.label,
3216
- sublabel: call.sublabel
5315
+ sublabel: call.sublabel,
5316
+ icon: call.icon,
5317
+ abi: call.abi
3217
5318
  }));
3218
5319
  return {
3219
- username: resolvedUsername,
5320
+ ...config.username ? { username: config.username } : {},
3220
5321
  accountAddress: config.accountAddress,
3221
5322
  targetChain,
3222
5323
  calls: intentCalls
@@ -3246,11 +5347,9 @@ function createPasskeyWalletClient(config) {
3246
5347
  value: transaction.value
3247
5348
  }
3248
5349
  ];
3249
- const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
3250
5350
  const intentPayload = await buildIntentPayload(calls, targetChain);
3251
5351
  const result = await provider.sendIntent({
3252
5352
  ...intentPayload,
3253
- closeOn,
3254
5353
  waitForHash: config.waitForHash ?? true,
3255
5354
  hashTimeoutMs: config.hashTimeoutMs,
3256
5355
  hashIntervalMs: config.hashIntervalMs
@@ -3265,14 +5364,12 @@ function createPasskeyWalletClient(config) {
3265
5364
  */
3266
5365
  async sendCalls(params) {
3267
5366
  const { calls, chainId: targetChain, tokenRequests, sourceAssets, sourceChainId } = params;
3268
- const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
3269
5367
  const intentPayload = await buildIntentPayload(calls, targetChain, { tokenRequests, sourceAssets });
3270
5368
  const result = await provider.sendIntent({
3271
5369
  ...intentPayload,
3272
5370
  tokenRequests,
3273
5371
  sourceAssets,
3274
5372
  sourceChainId,
3275
- closeOn,
3276
5373
  waitForHash: config.waitForHash ?? true,
3277
5374
  hashTimeoutMs: config.hashTimeoutMs,
3278
5375
  hashIntervalMs: config.hashIntervalMs
@@ -3303,21 +5400,36 @@ function useBatchQueue() {
3303
5400
  function generateId() {
3304
5401
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
3305
5402
  }
3306
- function getStorageKey(username) {
3307
- return username ? `1auth_batch_queue_${username}` : "1auth_batch_queue_anonymous";
5403
+ function getStorageKey(identity) {
5404
+ const key = identity?.accountAddress || identity?.username;
5405
+ return key ? `1auth_batch_queue_${key.toLowerCase()}` : "1auth_batch_queue_anonymous";
5406
+ }
5407
+ function resolveIdentity(passedIdentity, providerIdentity) {
5408
+ if (typeof passedIdentity === "string") {
5409
+ return { username: passedIdentity, accountAddress: providerIdentity.accountAddress };
5410
+ }
5411
+ return {
5412
+ username: passedIdentity?.username ?? providerIdentity.username,
5413
+ accountAddress: passedIdentity?.accountAddress ?? providerIdentity.accountAddress
5414
+ };
3308
5415
  }
3309
5416
  function BatchQueueProvider({
3310
5417
  client,
3311
5418
  username,
5419
+ accountAddress,
3312
5420
  children
3313
5421
  }) {
3314
5422
  const [queue, setQueue] = React.useState([]);
3315
5423
  const [isExpanded, setExpanded] = React.useState(false);
3316
5424
  const [isSigning, setIsSigning] = React.useState(false);
3317
5425
  const [animationTrigger, setAnimationTrigger] = React.useState(0);
5426
+ const providerIdentity = React.useMemo(
5427
+ () => ({ username, accountAddress }),
5428
+ [username, accountAddress]
5429
+ );
3318
5430
  const batchChainId = queue.length > 0 ? queue[0].targetChain : null;
3319
5431
  React.useEffect(() => {
3320
- const storageKey = getStorageKey(username);
5432
+ const storageKey = getStorageKey(providerIdentity);
3321
5433
  try {
3322
5434
  const stored = localStorage.getItem(storageKey);
3323
5435
  if (stored) {
@@ -3330,9 +5442,9 @@ function BatchQueueProvider({
3330
5442
  }
3331
5443
  } catch {
3332
5444
  }
3333
- }, [username]);
5445
+ }, [providerIdentity]);
3334
5446
  React.useEffect(() => {
3335
- const storageKey = getStorageKey(username);
5447
+ const storageKey = getStorageKey(providerIdentity);
3336
5448
  try {
3337
5449
  if (queue.length > 0) {
3338
5450
  localStorage.setItem(storageKey, JSON.stringify(queue));
@@ -3341,7 +5453,7 @@ function BatchQueueProvider({
3341
5453
  }
3342
5454
  } catch {
3343
5455
  }
3344
- }, [queue, username]);
5456
+ }, [queue, providerIdentity]);
3345
5457
  const addToBatch = React.useCallback((call, targetChain) => {
3346
5458
  if (batchChainId !== null && batchChainId !== targetChain) {
3347
5459
  return {
@@ -3366,7 +5478,7 @@ function BatchQueueProvider({
3366
5478
  setQueue([]);
3367
5479
  setExpanded(false);
3368
5480
  }, []);
3369
- const signAll = React.useCallback(async (username2) => {
5481
+ const signAll = React.useCallback(async (identity) => {
3370
5482
  if (queue.length === 0) {
3371
5483
  return {
3372
5484
  success: false,
@@ -3378,12 +5490,25 @@ function BatchQueueProvider({
3378
5490
  }
3379
5491
  };
3380
5492
  }
5493
+ const signer = resolveIdentity(identity, providerIdentity);
5494
+ if (!signer.username && !signer.accountAddress) {
5495
+ return {
5496
+ success: false,
5497
+ intentId: "",
5498
+ status: "failed",
5499
+ error: {
5500
+ code: "MISSING_IDENTITY",
5501
+ message: "Batch signing requires an accountAddress or username"
5502
+ }
5503
+ };
5504
+ }
3381
5505
  const targetChain = queue[0].targetChain;
3382
5506
  const calls = queue.map((item) => item.call);
3383
5507
  setIsSigning(true);
3384
5508
  try {
3385
5509
  const result = await client.sendIntent({
3386
- username: username2,
5510
+ username: signer.username,
5511
+ accountAddress: signer.accountAddress,
3387
5512
  targetChain,
3388
5513
  calls
3389
5514
  });
@@ -3394,7 +5519,7 @@ function BatchQueueProvider({
3394
5519
  } finally {
3395
5520
  setIsSigning(false);
3396
5521
  }
3397
- }, [queue, client, clearBatch]);
5522
+ }, [queue, client, clearBatch, providerIdentity]);
3398
5523
  const value = {
3399
5524
  queue,
3400
5525
  batchChainId,
@@ -3712,28 +5837,16 @@ function BatchQueueWidget({ onSignAll }) {
3712
5837
  ] })
3713
5838
  ] });
3714
5839
  }
3715
-
3716
- // src/verify.ts
3717
- import { keccak256, toBytes } from "viem";
3718
- var ETHEREUM_MESSAGE_PREFIX = "Ethereum Signed Message:\n";
3719
- function hashMessage2(message) {
3720
- const messageBytes = toBytes(message);
3721
- const prefixed = ETHEREUM_MESSAGE_PREFIX + messageBytes.length.toString() + message;
3722
- return keccak256(toBytes(prefixed));
3723
- }
3724
- function verifyMessageHash(message, signedHash) {
3725
- if (!signedHash) return false;
3726
- const expectedHash = hashMessage2(message);
3727
- return expectedHash.toLowerCase() === signedHash.toLowerCase();
3728
- }
3729
5840
  export {
3730
5841
  BatchQueueProvider,
3731
5842
  BatchQueueWidget,
3732
5843
  ETHEREUM_MESSAGE_PREFIX,
3733
5844
  OneAuthClient,
5845
+ createCrossChainPermission,
3734
5846
  createOneAuthProvider,
3735
5847
  createPasskeyAccount,
3736
5848
  createPasskeyWalletClient,
5849
+ definePermissions,
3737
5850
  encodeWebAuthnSignature,
3738
5851
  getAllSupportedChainsAndTokens,
3739
5852
  getChainById,
@@ -3748,7 +5861,7 @@ export {
3748
5861
  getTokenDecimals,
3749
5862
  getTokenSymbol,
3750
5863
  hashCalls,
3751
- hashMessage2 as hashMessage,
5864
+ hashMessage,
3752
5865
  isTestnet,
3753
5866
  isTokenAddressSupported,
3754
5867
  resolveTokenAddress,