@rhinestone/1auth 0.6.8 → 0.6.10

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 (51) hide show
  1. package/README.md +171 -12
  2. package/dist/chunk-GUAI55LL.mjs +179 -0
  3. package/dist/chunk-GUAI55LL.mjs.map +1 -0
  4. package/dist/chunk-IHBVEU33.mjs +20 -0
  5. package/dist/chunk-IHBVEU33.mjs.map +1 -0
  6. package/dist/chunk-IIACVHR3.mjs +28 -0
  7. package/dist/chunk-IIACVHR3.mjs.map +1 -0
  8. package/dist/chunk-N6KE5CII.mjs +72 -0
  9. package/dist/chunk-N6KE5CII.mjs.map +1 -0
  10. package/dist/{chunk-SXISYG2P.mjs → chunk-THKG3FAG.mjs} +137 -255
  11. package/dist/chunk-THKG3FAG.mjs.map +1 -0
  12. package/dist/{client-BrMrhetG.d.mts → client-B_CzDa_I.d.ts} +360 -857
  13. package/dist/{client-BrMrhetG.d.ts → client-F4DnFM8d.d.mts} +360 -857
  14. package/dist/headless.d.mts +109 -0
  15. package/dist/headless.d.ts +109 -0
  16. package/dist/headless.js +467 -0
  17. package/dist/headless.js.map +1 -0
  18. package/dist/headless.mjs +382 -0
  19. package/dist/headless.mjs.map +1 -0
  20. package/dist/index.d.mts +96 -144
  21. package/dist/index.d.ts +96 -144
  22. package/dist/index.js +3212 -756
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +3010 -705
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/{provider-CDl9wYEc.d.mts → provider-Cd7Ip5L-.d.ts} +6 -5
  27. package/dist/{provider-Dgv533YQ.d.ts → provider-IvYXPMpk.d.mts} +6 -5
  28. package/dist/react.d.mts +42 -2
  29. package/dist/react.d.ts +42 -2
  30. package/dist/react.js +92 -2
  31. package/dist/react.js.map +1 -1
  32. package/dist/react.mjs +66 -1
  33. package/dist/react.mjs.map +1 -1
  34. package/dist/server.d.mts +118 -0
  35. package/dist/server.d.ts +118 -0
  36. package/dist/server.js +356 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/server.mjs +282 -0
  39. package/dist/server.mjs.map +1 -0
  40. package/dist/types-U_dwxbtS.d.mts +1488 -0
  41. package/dist/types-U_dwxbtS.d.ts +1488 -0
  42. package/dist/verify-BLgZzwmJ.d.ts +150 -0
  43. package/dist/verify-C8-a5c3K.d.mts +150 -0
  44. package/dist/wagmi.d.mts +5 -2
  45. package/dist/wagmi.d.ts +5 -2
  46. package/dist/wagmi.js +138 -43
  47. package/dist/wagmi.js.map +1 -1
  48. package/dist/wagmi.mjs +3 -1
  49. package/dist/wagmi.mjs.map +1 -1
  50. package/package.json +15 -2
  51. package/dist/chunk-SXISYG2P.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,15 @@
1
1
  import {
2
- buildTransactionReview,
3
- createOneAuthProvider,
4
- encodeWebAuthnSignature,
2
+ tokenFetchError
3
+ } from "./chunk-IIACVHR3.mjs";
4
+ import {
5
+ ETHEREUM_MESSAGE_PREFIX,
6
+ hashMessage,
7
+ verifyMessageHash
8
+ } from "./chunk-IHBVEU33.mjs";
9
+ import {
10
+ createOneAuthProvider
11
+ } from "./chunk-THKG3FAG.mjs";
12
+ import {
5
13
  getAllSupportedChainsAndTokens,
6
14
  getChainById,
7
15
  getChainExplorerUrl,
@@ -14,19 +22,280 @@ import {
14
22
  getTokenAddress,
15
23
  getTokenDecimals,
16
24
  getTokenSymbol,
17
- hashCalls,
18
25
  isTestnet,
19
26
  isTokenAddressSupported,
20
27
  resolveTokenAddress
21
- } from "./chunk-SXISYG2P.mjs";
28
+ } from "./chunk-GUAI55LL.mjs";
29
+ import {
30
+ buildTransactionReview,
31
+ encodeWebAuthnSignature,
32
+ hashCalls
33
+ } from "./chunk-N6KE5CII.mjs";
34
+
35
+ // src/client.ts
36
+ import { hashTypedData } from "viem";
37
+
38
+ // src/assets.ts
39
+ function withNetworkBucket(balance) {
40
+ return {
41
+ ...balance,
42
+ isTestnet: balance.isTestnet ?? isTestnet(balance.chainId)
43
+ };
44
+ }
45
+ function bucketAssetBalances(balances) {
46
+ const mainnetBalances = [];
47
+ const testnetBalances = [];
48
+ for (const balance of balances) {
49
+ const normalized = withNetworkBucket(balance);
50
+ if (normalized.isTestnet) {
51
+ testnetBalances.push(normalized);
52
+ } else {
53
+ mainnetBalances.push(normalized);
54
+ }
55
+ }
56
+ return {
57
+ mainnets: { balances: mainnetBalances },
58
+ testnets: { balances: testnetBalances }
59
+ };
60
+ }
61
+ function normalizeAssetsResponse(payload) {
62
+ const data = payload && typeof payload === "object" ? payload : {};
63
+ if (Array.isArray(data.balances)) {
64
+ const balances2 = data.balances.map(withNetworkBucket);
65
+ const buckets = bucketAssetBalances(balances2);
66
+ return { ...data, balances: balances2, ...buckets };
67
+ }
68
+ const balances = [];
69
+ const assets = Array.isArray(data.assets) ? data.assets : [];
70
+ for (const asset of assets) {
71
+ const symbol = asset.symbol ?? "UNKNOWN";
72
+ const decimals = asset.decimals ?? 18;
73
+ const chains = Array.isArray(asset.chains) ? asset.chains : [];
74
+ if (chains.length === 0) {
75
+ const token = asset.token ?? asset.address;
76
+ if (token) {
77
+ balances.push(withNetworkBucket({
78
+ chainId: 0,
79
+ token,
80
+ symbol,
81
+ decimals,
82
+ balance: asset.balance ?? asset.amount ?? "0",
83
+ ...asset.usdValue !== void 0 && { usdValue: asset.usdValue }
84
+ }));
85
+ }
86
+ continue;
87
+ }
88
+ for (const chain of chains) {
89
+ const chainId = chain.chainId ?? chain.chain;
90
+ const token = chain.token ?? chain.address;
91
+ if (typeof chainId !== "number" || !token) continue;
92
+ balances.push(withNetworkBucket({
93
+ chainId,
94
+ token,
95
+ symbol,
96
+ decimals: chain.decimals ?? decimals,
97
+ balance: chain.balance ?? chain.amount ?? "0",
98
+ ...asset.usdValue !== void 0 && { usdValue: asset.usdValue },
99
+ ...chain.isTestnet !== void 0 && { isTestnet: chain.isTestnet }
100
+ }));
101
+ }
102
+ }
103
+ return { ...data, balances, ...bucketAssetBalances(balances) };
104
+ }
105
+ async function fetchAssetsResponse(options) {
106
+ const portfolioUrl = new URL(
107
+ `/api/users/${encodeURIComponent(options.identifier)}/portfolio`,
108
+ options.providerUrl
109
+ );
110
+ portfolioUrl.searchParams.set("all", "true");
111
+ const response = await fetch(portfolioUrl.toString(), {
112
+ headers: {
113
+ ...options.headers,
114
+ ...options.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {},
115
+ ...options.clientId ? { "x-client-id": options.clientId } : {}
116
+ }
117
+ });
118
+ if (!response.ok) {
119
+ const data = await response.json().catch(() => ({}));
120
+ throw new Error(data.error || "Failed to get assets");
121
+ }
122
+ return normalizeAssetsResponse(await response.json());
123
+ }
124
+
125
+ // ../loading-tokens/src/index.ts
126
+ var SPINNER_SIZE = 82;
127
+ var SPINNER_STROKE = 2.4118;
128
+ var SPINNER_RADIUS = (SPINNER_SIZE - SPINNER_STROKE) / 2;
129
+ var SPINNER_CIRCUMFERENCE = 2 * Math.PI * SPINNER_RADIUS;
130
+ var SPINNER_ARC_FRACTION = 0.17;
131
+ var SPINNER_ARC = SPINNER_CIRCUMFERENCE * SPINNER_ARC_FRACTION;
132
+ var SPINNER_SPIN_DURATION_MS = 800;
133
+ var LOADING_MODAL_WIDTH = 380;
134
+ var LOADING_MODAL_PADDING = 12;
135
+ var LOADING_MODAL_RADIUS = 16;
136
+ var LOADING_BODY_GAP = 12;
137
+ var LOADING_ICON_PADDING = 12;
138
+ var LOADING_TITLE_BLOCK_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_LINE_HEIGHT = "normal";
144
+ var LOADING_FONT_FAMILY = "'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'";
145
+ var BRAND_PURPLE = "#685bff";
146
+ var LIGHT_TOKENS = {
147
+ bgPrimary: "#ffffff",
148
+ borderColor: "#f4f4f5",
149
+ textPrimary: "#27272a",
150
+ textSecondary: "#71717b"
151
+ };
152
+ var DARK_TOKENS = {
153
+ bgPrimary: "#0a0a0a",
154
+ borderColor: "#18181b",
155
+ textPrimary: "#e4e4e7",
156
+ textSecondary: "#9f9fa9"
157
+ };
158
+ var LOADING_TITLE = "Loading...";
159
+ var LOADING_SUBTITLE = "This will only take a few moments, do not close the window.";
22
160
 
23
161
  // src/client.ts
24
- import { parseUnits, hashTypedData } from "viem";
25
- var POPUP_WIDTH = 450;
162
+ var POPUP_WIDTH = LOADING_MODAL_WIDTH;
26
163
  var POPUP_HEIGHT = 600;
27
- var DEFAULT_EMBED_WIDTH = "400px";
164
+ var DEFAULT_EMBED_WIDTH = `${LOADING_MODAL_WIDTH}px`;
28
165
  var DEFAULT_EMBED_HEIGHT = "500px";
29
166
  var DEFAULT_PROVIDER_URL = "https://passkey.1auth.box";
167
+ function currentWindowOrigin() {
168
+ if (typeof window === "undefined") return null;
169
+ const origin = window.location?.origin;
170
+ return typeof origin === "string" && origin.length > 0 ? origin : null;
171
+ }
172
+ function appendParentOriginParam(params) {
173
+ const origin = currentWindowOrigin();
174
+ if (origin) params.set("parentOrigin", origin);
175
+ }
176
+ var MAX_LABEL_LEN = 20;
177
+ var DEFAULT_BACKDROP_COLOR = "#000000";
178
+ var DEFAULT_BACKDROP_OPACITY = 0.4;
179
+ var DEFAULT_BACKDROP_BLUR = 8;
180
+ var HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
181
+ function resolveBackdropStyle(params) {
182
+ const rawColor = params.get("backdropColor");
183
+ const hex = (rawColor && HEX_COLOR_RE.test(rawColor) ? rawColor : DEFAULT_BACKDROP_COLOR).slice(1);
184
+ const r = parseInt(hex.slice(0, 2), 16);
185
+ const g = parseInt(hex.slice(2, 4), 16);
186
+ const b = parseInt(hex.slice(4, 6), 16);
187
+ const clampNumber = (key, min, max, fallback) => {
188
+ const raw = params.get(key);
189
+ if (!raw) return fallback;
190
+ const n = Number(raw);
191
+ return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
192
+ };
193
+ const opacity = clampNumber("backdropOpacity", 0, 1, DEFAULT_BACKDROP_OPACITY);
194
+ const blur = clampNumber("backdropBlur", 0, 40, DEFAULT_BACKDROP_BLUR);
195
+ return { background: `rgba(${r}, ${g}, ${b}, ${opacity})`, blur };
196
+ }
197
+ function generateModalNonce() {
198
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
199
+ return crypto.randomUUID();
200
+ }
201
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
202
+ }
203
+ function generateTelemetryOperationId() {
204
+ return `1auth-${generateModalNonce()}`;
205
+ }
206
+ function cleanTelemetryAttributes(attributes) {
207
+ if (!attributes) return void 0;
208
+ const clean = {};
209
+ for (const [key, value] of Object.entries(attributes)) {
210
+ if (value !== void 0) clean[key] = value;
211
+ }
212
+ return Object.keys(clean).length > 0 ? clean : void 0;
213
+ }
214
+ function describeTelemetryError(error) {
215
+ if (!error) return {};
216
+ if (error instanceof Error) {
217
+ return { errorCode: error.name, errorMessage: error.message };
218
+ }
219
+ if (typeof error === "object") {
220
+ const err = error;
221
+ return {
222
+ errorCode: typeof err.code === "string" ? err.code : void 0,
223
+ errorMessage: typeof err.message === "string" ? err.message : String(error)
224
+ };
225
+ }
226
+ return { errorMessage: String(error) };
227
+ }
228
+ function capCallLabels(calls) {
229
+ if (!calls) return calls;
230
+ return calls.map((c) => {
231
+ const cappedLabel = c.label && c.label.length > MAX_LABEL_LEN ? c.label.slice(0, MAX_LABEL_LEN - 1) + "\u2026" : c.label;
232
+ const cappedSublabel = c.sublabel && c.sublabel.length > MAX_LABEL_LEN ? c.sublabel.slice(0, MAX_LABEL_LEN - 1) + "\u2026" : c.sublabel;
233
+ if (cappedLabel === c.label && cappedSublabel === c.sublabel) return c;
234
+ return { ...c, label: cappedLabel, sublabel: cappedSublabel };
235
+ });
236
+ }
237
+ function getStoredSignerType() {
238
+ if (typeof window === "undefined") return "passkey";
239
+ try {
240
+ const raw = window.localStorage.getItem("1auth-user");
241
+ if (!raw) return "passkey";
242
+ const parsed = JSON.parse(raw);
243
+ return parsed?.signerType === "eoa" ? "eoa" : "passkey";
244
+ } catch {
245
+ return "passkey";
246
+ }
247
+ }
248
+ var DEFAULT_BLIND_SIGNING = true;
249
+ 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.";
250
+ function normalizeSponsorship(config) {
251
+ if (!config) return null;
252
+ if ("accessToken" in config && "getExtensionToken" in config) {
253
+ return config;
254
+ }
255
+ const { accessTokenUrl, extensionTokenUrl } = config;
256
+ return {
257
+ accessToken: async () => {
258
+ const response = await fetch(accessTokenUrl, { credentials: "include" });
259
+ if (!response.ok) {
260
+ throw await tokenFetchError("access token", response);
261
+ }
262
+ const data = await response.json();
263
+ if (!data.token) {
264
+ throw new Error("Access token response missing `token` field");
265
+ }
266
+ return data.token;
267
+ },
268
+ getExtensionToken: async (intentOp) => {
269
+ const response = await fetch(extensionTokenUrl, {
270
+ method: "POST",
271
+ credentials: "include",
272
+ headers: { "Content-Type": "application/json" },
273
+ body: JSON.stringify({ intentOp })
274
+ });
275
+ if (!response.ok) {
276
+ throw await tokenFetchError("extension token", response);
277
+ }
278
+ const data = await response.json();
279
+ if (!data.token) {
280
+ throw new Error("Extension token response missing `token` field");
281
+ }
282
+ return data.token;
283
+ }
284
+ };
285
+ }
286
+ function grantPermissionReplacer(_key, value) {
287
+ return typeof value === "bigint" ? value.toString() : value;
288
+ }
289
+ function cloneGrantPayload(value) {
290
+ return JSON.parse(JSON.stringify(value, grantPermissionReplacer));
291
+ }
292
+ function normalizeGrantTargetChains(options) {
293
+ const chainCandidates = options.targetChains ?? (options.targetChain !== void 0 ? [options.targetChain] : []);
294
+ return Array.from(new Set(chainCandidates));
295
+ }
296
+ function normalizeGrantSourceChains(options) {
297
+ return Array.from(new Set(options.sourceChains ?? []));
298
+ }
30
299
  var OneAuthClient = class {
31
300
  /**
32
301
  * Create a new OneAuthClient.
@@ -40,15 +309,246 @@ var OneAuthClient = class {
40
309
  * clientId, theme, and redirect settings.
41
310
  */
42
311
  constructor(config) {
312
+ // Persistent state for EOA forwarding via `/dialog/sign-eoa`. The iframe
313
+ // is created lazily on the first `requestWithWallet` call and reused for
314
+ // every subsequent call so its WalletConnect SignClient survives between
315
+ // transactions — otherwise each call would spin up a fresh SignClient
316
+ // that restores stale relay state from localStorage, producing
317
+ // "Restore will override" warnings and "emitting session_request:N
318
+ // without any listeners" errors on the second and later txs.
319
+ this.eoaDialogState = null;
320
+ // Hidden iframe that warms the dialog ahead of the first real open (see
321
+ // `prewarm()`). The passkey app runs on a long-lived server (not serverless),
322
+ // so there is no backend cold-start to hide — what stays expensive is the
323
+ // *browser-side* cost of a freshly created cross-origin iframe: downloading
324
+ // the dialog HTML + JS bundle, parsing it, hydrating React, and loading the
325
+ // font. Loading that bundle once into a parked hidden iframe lets the real
326
+ // open hit the HTTP cache (Next.js chunks/fonts are immutable-cached) over an
327
+ // already-warm connection, so the SDK preload overlay is shown only briefly.
328
+ this.prewarmState = null;
329
+ // Serialises concurrent `requestWithWallet` calls — they would otherwise
330
+ // both try to drive the same iframe and cross-resolve each other.
331
+ this.eoaSerialQueue = Promise.resolve();
43
332
  const providerUrl = config.providerUrl || DEFAULT_PROVIDER_URL;
44
333
  const dialogUrl = config.dialogUrl || providerUrl;
45
334
  this.config = { ...config, providerUrl, dialogUrl };
46
335
  this.theme = this.config.theme || {};
336
+ this.sponsorship = normalizeSponsorship(config.sponsorship);
47
337
  if (typeof document !== "undefined") {
48
338
  this.injectPreconnect(providerUrl);
49
339
  if (dialogUrl !== providerUrl) {
50
340
  this.injectPreconnect(dialogUrl);
51
341
  }
342
+ if (this.config.prewarm) {
343
+ const warm = () => void this.prewarm();
344
+ const ric = window.requestIdleCallback;
345
+ if (typeof ric === "function") {
346
+ ric(warm, { timeout: 2e3 });
347
+ } else {
348
+ setTimeout(warm, 200);
349
+ }
350
+ }
351
+ }
352
+ const initOperation = this.createTelemetryOperation("client", {
353
+ hasClientId: !!this.config.clientId,
354
+ hasDialogUrlOverride: dialogUrl !== providerUrl
355
+ });
356
+ this.emitTelemetry("client.init", initOperation, { outcome: "started" });
357
+ }
358
+ /**
359
+ * Update the sponsorship configuration at runtime. Pass `undefined` to
360
+ * disable sponsorship.
361
+ */
362
+ setSponsorship(sponsorship) {
363
+ this.sponsorship = normalizeSponsorship(sponsorship);
364
+ }
365
+ /**
366
+ * Whether the caller opted into SDK telemetry propagation or callbacks.
367
+ */
368
+ shouldUseTelemetry() {
369
+ return !!this.config.telemetry && this.config.telemetry.enabled !== false;
370
+ }
371
+ /**
372
+ * Resolve the host app's current trace context. Callback failures are
373
+ * ignored because observability can never be allowed to break signing.
374
+ */
375
+ getTelemetryTraceContext() {
376
+ const traceContext = this.config.telemetry?.traceContext;
377
+ if (!traceContext || !this.shouldUseTelemetry()) return void 0;
378
+ try {
379
+ return typeof traceContext === "function" ? traceContext() : traceContext;
380
+ } catch {
381
+ return void 0;
382
+ }
383
+ }
384
+ /**
385
+ * Resolve host-provided SDK event attributes and remove undefined fields.
386
+ */
387
+ getTelemetryAttributes() {
388
+ const attributes = this.config.telemetry?.attributes;
389
+ if (!attributes || !this.shouldUseTelemetry()) return void 0;
390
+ try {
391
+ const resolved = typeof attributes === "function" ? attributes() : attributes;
392
+ return cleanTelemetryAttributes(resolved);
393
+ } catch {
394
+ return void 0;
395
+ }
396
+ }
397
+ /**
398
+ * Create per-operation telemetry context shared by callbacks, HTTP headers,
399
+ * dialog URL params, and PASSKEY_INIT messages.
400
+ */
401
+ createTelemetryOperation(flow, attributes) {
402
+ return {
403
+ operationId: generateTelemetryOperationId(),
404
+ flow,
405
+ startedAt: Date.now(),
406
+ trace: this.getTelemetryTraceContext(),
407
+ attributes: cleanTelemetryAttributes({
408
+ ...this.getTelemetryAttributes(),
409
+ ...attributes
410
+ })
411
+ };
412
+ }
413
+ /**
414
+ * Emit a telemetry event to the host app. The callback is intentionally
415
+ * fire-and-forget and exception-safe so app telemetry cannot affect users.
416
+ */
417
+ emitTelemetry(name, operation, patch = {}) {
418
+ const onEvent = this.config.telemetry?.onEvent;
419
+ if (!onEvent || !this.shouldUseTelemetry()) return;
420
+ const attributes = cleanTelemetryAttributes({
421
+ ...operation.attributes,
422
+ ...patch.attributes
423
+ });
424
+ const event = {
425
+ name,
426
+ operationId: operation.operationId,
427
+ flow: operation.flow,
428
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
429
+ durationMs: patch.durationMs ?? Date.now() - operation.startedAt,
430
+ clientId: this.config.clientId,
431
+ providerUrl: this.config.providerUrl,
432
+ dialogUrl: this.getDialogUrl(),
433
+ trace: operation.trace,
434
+ ...patch,
435
+ attributes
436
+ };
437
+ try {
438
+ onEvent(event);
439
+ } catch {
440
+ }
441
+ }
442
+ /**
443
+ * Attach SDK correlation and optional W3C trace context to dialog URLs.
444
+ */
445
+ appendTelemetryParams(params, operation) {
446
+ if (!this.shouldUseTelemetry()) return;
447
+ params.set("sdkOperationId", operation.operationId);
448
+ params.set("sdkTelemetryFlow", operation.flow);
449
+ if (operation.trace?.traceparent) {
450
+ params.set("traceparent", operation.trace.traceparent);
451
+ }
452
+ if (operation.trace?.tracestate) {
453
+ params.set("tracestate", operation.trace.tracestate);
454
+ }
455
+ if (operation.trace?.traceId) {
456
+ params.set("sdkTraceId", operation.trace.traceId);
457
+ }
458
+ if (operation.trace?.spanId) {
459
+ params.set("sdkSpanId", operation.trace.spanId);
460
+ }
461
+ }
462
+ /**
463
+ * Add SDK correlation headers to passkey API calls. These headers are also
464
+ * valid W3C trace propagation inputs when the host app supplies traceparent.
465
+ */
466
+ telemetryHeaders(operation) {
467
+ if (!operation || !this.shouldUseTelemetry()) return {};
468
+ return {
469
+ "x-1auth-sdk-operation-id": operation.operationId,
470
+ "x-1auth-sdk-flow": operation.flow,
471
+ ...operation.trace?.traceparent ? { traceparent: operation.trace.traceparent } : {},
472
+ ...operation.trace?.tracestate ? { tracestate: operation.trace.tracestate } : {}
473
+ };
474
+ }
475
+ /**
476
+ * Embed telemetry context in PASSKEY_INIT payloads for dialog-side API calls.
477
+ */
478
+ telemetryPayload(operation) {
479
+ if (!this.shouldUseTelemetry()) return void 0;
480
+ return {
481
+ operationId: operation.operationId,
482
+ flow: operation.flow,
483
+ ...operation.trace?.traceparent ? { traceparent: operation.trace.traceparent } : {},
484
+ ...operation.trace?.tracestate ? { tracestate: operation.trace.tracestate } : {},
485
+ ...operation.trace?.traceId ? { traceId: operation.trace.traceId } : {},
486
+ ...operation.trace?.spanId ? { spanId: operation.trace.spanId } : {}
487
+ };
488
+ }
489
+ /**
490
+ * Determine whether this signing call should request invisible dialog mode.
491
+ * Resolution order: per-call option wins over the constructor config, which
492
+ * wins over the platform-wide {@link DEFAULT_BLIND_SIGNING}. This lets an app
493
+ * flip the global default for itself while still overriding individual
494
+ * high-risk calls — e.g. default-blind but force the review UI for a large
495
+ * transfer, or default-visible but blind-sign selected low-risk signatures.
496
+ */
497
+ shouldRequestBlindSigning(options) {
498
+ return options?.blind_signing ?? this.config.blind_signing ?? DEFAULT_BLIND_SIGNING;
499
+ }
500
+ /**
501
+ * Fetch the app's access token (JWT). Called up front, before
502
+ * `/api/intent/prepare`, so the passkey server can authenticate the
503
+ * orchestrator quote call with the app's JWT instead of a service-level
504
+ * API key.
505
+ *
506
+ * Does not depend on `intentOp` — can run in parallel with anything that
507
+ * also doesn't depend on the quote.
508
+ */
509
+ async fetchAccessToken() {
510
+ if (!this.sponsorship) {
511
+ return {
512
+ ok: false,
513
+ code: "MISSING_APP_CREDENTIALS",
514
+ message: MISSING_APP_CREDENTIALS_MESSAGE
515
+ };
516
+ }
517
+ try {
518
+ const accessToken = await this.sponsorship.accessToken();
519
+ return { ok: true, accessToken };
520
+ } catch (err) {
521
+ return {
522
+ ok: false,
523
+ message: err instanceof Error ? err.message : String(err)
524
+ };
525
+ }
526
+ }
527
+ /**
528
+ * Fetch one extension token per intent, aligned with `sponsorFlags`.
529
+ * Returns an empty array when no intent is sponsored. Fired after
530
+ * `/prepare` so each token can be bound to its quote's `intentOp`, and
531
+ * overlaps the dialog / WebAuthn ceremony for no added serial latency.
532
+ */
533
+ async fetchExtensionTokens(intentOps, sponsorFlags) {
534
+ if (!this.sponsorship) {
535
+ return { ok: false, message: MISSING_APP_CREDENTIALS_MESSAGE };
536
+ }
537
+ const anySponsored = sponsorFlags.some(Boolean);
538
+ if (!anySponsored) return { ok: true, extensionTokens: [] };
539
+ try {
540
+ const sponsorship = this.sponsorship;
541
+ const tokens = await Promise.all(
542
+ intentOps.map(
543
+ (op, i) => sponsorFlags[i] ? sponsorship.getExtensionToken(op) : Promise.resolve("")
544
+ )
545
+ );
546
+ return { ok: true, extensionTokens: tokens };
547
+ } catch (err) {
548
+ return {
549
+ ok: false,
550
+ message: err instanceof Error ? err.message : String(err)
551
+ };
52
552
  }
53
553
  }
54
554
  /**
@@ -75,8 +575,23 @@ var OneAuthClient = class {
75
575
  if (theme.mode) {
76
576
  params.set("theme", theme.mode);
77
577
  }
78
- if (theme.accent) {
79
- params.set("accent", theme.accent);
578
+ const instancePrimary = this.theme.primaryColor ?? this.theme.accent;
579
+ const overridePrimary = overrideTheme?.primaryColor ?? overrideTheme?.accent;
580
+ const primary = overridePrimary ?? instancePrimary;
581
+ if (primary) {
582
+ params.set("accent", primary);
583
+ }
584
+ const backdrop = theme.backdrop;
585
+ if (backdrop) {
586
+ if (backdrop.color) {
587
+ params.set("backdropColor", backdrop.color);
588
+ }
589
+ if (backdrop.opacity !== void 0) {
590
+ params.set("backdropOpacity", String(backdrop.opacity));
591
+ }
592
+ if (backdrop.blur !== void 0) {
593
+ params.set("backdropBlur", String(backdrop.blur));
594
+ }
80
595
  }
81
596
  return params.toString();
82
597
  }
@@ -126,6 +641,60 @@ var OneAuthClient = class {
126
641
  getClientId() {
127
642
  return this.config.clientId;
128
643
  }
644
+ /**
645
+ * Whether this client operates on testnet chains only.
646
+ */
647
+ getTestnets() {
648
+ return this.config.testnets ?? false;
649
+ }
650
+ /**
651
+ * Get a unified token portfolio for a 1auth account across mainnets and
652
+ * testnets.
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * const assets = await client.getAssets({
657
+ * accountAddress: "0x1111111111111111111111111111111111111111",
658
+ * });
659
+ * console.log(assets.mainnets.balances, assets.testnets.balances);
660
+ * ```
661
+ */
662
+ async getAssets(options) {
663
+ if (!options.accountAddress) {
664
+ throw new Error("getAssets requires accountAddress");
665
+ }
666
+ const telemetry = this.createTelemetryOperation("assets", {
667
+ hasAccountAddress: !!options.accountAddress
668
+ });
669
+ try {
670
+ const accessTokenResult = await this.fetchAccessToken();
671
+ if (!accessTokenResult.ok) {
672
+ throw new Error(accessTokenResult.message);
673
+ }
674
+ const result = await fetchAssetsResponse({
675
+ providerUrl: this.config.providerUrl,
676
+ identifier: options.accountAddress,
677
+ clientId: this.config.clientId,
678
+ accessToken: accessTokenResult.accessToken,
679
+ headers: this.telemetryHeaders(telemetry)
680
+ });
681
+ this.emitTelemetry("assets.succeeded", telemetry, {
682
+ outcome: "success",
683
+ attributes: {
684
+ balanceCount: result.balances.length,
685
+ mainnetBalanceCount: result.mainnets.balances.length,
686
+ testnetBalanceCount: result.testnets.balances.length
687
+ }
688
+ });
689
+ return result;
690
+ } catch (error) {
691
+ this.emitTelemetry("assets.failed", telemetry, {
692
+ outcome: "failure",
693
+ ...describeTelemetryError(error)
694
+ });
695
+ throw error;
696
+ }
697
+ }
129
698
  /**
130
699
  * Poll the intent status endpoint until a transaction hash appears or the
131
700
  * intent reaches a terminal failure state.
@@ -144,7 +713,7 @@ var OneAuthClient = class {
144
713
  * @returns The transaction hash string, or `undefined` if the deadline was
145
714
  * reached or the intent failed/expired before a hash was produced.
146
715
  */
147
- async waitForTransactionHash(intentId, options = {}) {
716
+ async waitForTransactionHash(intentId, options = {}, telemetry) {
148
717
  const timeoutMs = options.timeoutMs ?? 12e4;
149
718
  const intervalMs = options.intervalMs ?? 2e3;
150
719
  const deadline = Date.now() + timeoutMs;
@@ -153,7 +722,10 @@ var OneAuthClient = class {
153
722
  const response = await fetch(
154
723
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
155
724
  {
156
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
725
+ headers: {
726
+ ...this.telemetryHeaders(telemetry),
727
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
728
+ }
157
729
  }
158
730
  );
159
731
  if (response.ok) {
@@ -181,6 +753,8 @@ var OneAuthClient = class {
181
753
  * @param options.username - Pre-fill the username field
182
754
  * @param options.theme - Override the theme for this modal invocation
183
755
  * @param options.oauthEnabled - Set to false to hide OAuth sign-in buttons
756
+ * @param options.flow - Force the login or account-creation route instead
757
+ * of the default auto-detect entry route
184
758
  * @returns {@link AuthResult} with user details and typed address
185
759
  *
186
760
  * @example
@@ -196,9 +770,54 @@ var OneAuthClient = class {
196
770
  * ```
197
771
  */
198
772
  async authWithModal(options) {
773
+ return this.openAuthModal(options);
774
+ }
775
+ /**
776
+ * Open the sign-in modal directly.
777
+ *
778
+ * This bypasses the `/dialog/auth` auto-router and targets the dedicated
779
+ * returning-user flow. Use it when the embedding app already knows the user
780
+ * is creating a login session, for example after an app-level "Log in"
781
+ * button. The result intentionally matches {@link authWithModal}.
782
+ *
783
+ * @param options.username - Optional account hint for the sign-in ceremony
784
+ * @param options.theme - Override the theme for this modal invocation
785
+ * @returns {@link AuthResult} with user details and typed address
786
+ */
787
+ async loginWithModal(options) {
788
+ return this.openAuthModal({ ...options, flow: "login" });
789
+ }
790
+ /**
791
+ * Open the account-creation modal directly.
792
+ *
793
+ * This bypasses the `/dialog/auth` auto-router and targets the dedicated
794
+ * sign-up flow. Account creation still happens server-side in the passkey
795
+ * service; the SDK only opens the dialog and receives the postMessage result.
796
+ *
797
+ * @param options.theme - Override the theme for this modal invocation
798
+ * @param options.oauthEnabled - Set to false to hide OAuth sign-in buttons
799
+ * @returns {@link AuthResult} with the newly authenticated account details
800
+ */
801
+ async createAccountWithModal(options) {
802
+ return this.openAuthModal({ ...options, flow: "create-account" });
803
+ }
804
+ /**
805
+ * Open one of the passkey authentication modal routes and wait for the
806
+ * login-result message.
807
+ *
808
+ * Keeping URL construction in one helper prevents the explicit login/signup
809
+ * methods from drifting away from the legacy `authWithModal` behavior.
810
+ */
811
+ async openAuthModal(options) {
812
+ const telemetry = this.createTelemetryOperation("auth", {
813
+ flow: options?.flow ?? "auto",
814
+ hasUsernameHint: !!options?.username
815
+ });
199
816
  const dialogUrl = this.getDialogUrl();
817
+ const requestId = this.createDialogRequestId();
200
818
  const params = new URLSearchParams({
201
- mode: "iframe"
819
+ mode: "iframe",
820
+ requestId
202
821
  });
203
822
  if (this.config.clientId) {
204
823
  params.set("clientId", this.config.clientId);
@@ -209,14 +828,58 @@ var OneAuthClient = class {
209
828
  if (options?.oauthEnabled === false) {
210
829
  params.set("oauth", "0");
211
830
  }
831
+ if (options?.flow === "create-account") {
832
+ params.set("switch", "1");
833
+ }
834
+ appendParentOriginParam(params);
835
+ this.appendTelemetryParams(params, telemetry);
212
836
  const themeParams = this.getThemeParams(options?.theme);
213
837
  if (themeParams) {
214
838
  const themeParsed = new URLSearchParams(themeParams);
215
839
  themeParsed.forEach((value, key) => params.set(key, value));
216
840
  }
217
- const url = `${dialogUrl}/dialog/auth?${params.toString()}`;
841
+ const authPath = this.getAuthDialogPath(options?.flow);
842
+ const url = `${dialogUrl}${authPath}?${params.toString()}`;
218
843
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
219
- return this.waitForModalAuthResponse(dialog, iframe, cleanup);
844
+ this.emitTelemetry("dialog.opened", telemetry, { outcome: "started" });
845
+ const result = await this.waitForModalAuthResponse(dialog, iframe, cleanup, requestId);
846
+ if (result.success) {
847
+ this.emitTelemetry("auth.succeeded", telemetry, {
848
+ outcome: "success",
849
+ attributes: { signerType: result.signerType ?? "passkey" }
850
+ });
851
+ } else {
852
+ this.emitTelemetry(
853
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "auth.failed",
854
+ telemetry,
855
+ {
856
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
857
+ ...describeTelemetryError(result.error)
858
+ }
859
+ );
860
+ }
861
+ return result;
862
+ }
863
+ /**
864
+ * Generate a per-dialog nonce used to correlate iframe close messages with
865
+ * the SDK modal that opened them. Chromium can report a different
866
+ * `MessageEvent.source` WindowProxy after the `/dialog/auth` redirect, so
867
+ * auth close handling needs a stable in-band identifier without accepting
868
+ * stale close messages from earlier dialogs.
869
+ */
870
+ createDialogRequestId() {
871
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
872
+ return crypto.randomUUID();
873
+ }
874
+ return `dialog-${Date.now()}-${Math.random().toString(36).slice(2)}`;
875
+ }
876
+ /**
877
+ * Resolve the passkey app route for a requested auth flow.
878
+ */
879
+ getAuthDialogPath(flow = "auto") {
880
+ if (flow === "login") return "/dialog/auth/signin";
881
+ if (flow === "create-account") return "/dialog/auth/signup";
882
+ return "/dialog/auth";
220
883
  }
221
884
  /**
222
885
  * Open the connect dialog (lightweight connection without passkey auth).
@@ -241,6 +904,7 @@ var OneAuthClient = class {
241
904
  * ```
242
905
  */
243
906
  async connectWithModal(options) {
907
+ const telemetry = this.createTelemetryOperation("connect");
244
908
  const dialogUrl = this.getDialogUrl();
245
909
  const params = new URLSearchParams({
246
910
  mode: "iframe"
@@ -248,6 +912,8 @@ var OneAuthClient = class {
248
912
  if (this.config.clientId) {
249
913
  params.set("clientId", this.config.clientId);
250
914
  }
915
+ appendParentOriginParam(params);
916
+ this.appendTelemetryParams(params, telemetry);
251
917
  const themeParams = this.getThemeParams(options?.theme);
252
918
  if (themeParams) {
253
919
  const themeParsed = new URLSearchParams(themeParams);
@@ -255,17 +921,104 @@ var OneAuthClient = class {
255
921
  }
256
922
  const url = `${dialogUrl}/dialog/connect?${params.toString()}`;
257
923
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
924
+ this.emitTelemetry("dialog.opened", telemetry, { outcome: "started" });
258
925
  const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
259
- mode: "iframe"
926
+ mode: "iframe",
927
+ telemetry: this.telemetryPayload(telemetry)
260
928
  });
261
929
  if (!ready) {
930
+ this.emitTelemetry("dialog.cancelled", telemetry, {
931
+ outcome: "cancelled",
932
+ errorCode: "USER_CANCELLED",
933
+ errorMessage: "Connection was cancelled before the dialog became ready"
934
+ });
262
935
  return {
263
936
  success: false,
264
937
  action: "cancel",
265
938
  error: { code: "USER_CANCELLED", message: "Connection was cancelled" }
266
939
  };
267
940
  }
268
- return this.waitForConnectResponse(dialog, iframe, cleanup);
941
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
942
+ const result = await this.waitForConnectResponse(dialog, iframe, cleanup);
943
+ if (result.success) {
944
+ this.emitTelemetry("connect.succeeded", telemetry, {
945
+ outcome: "success",
946
+ attributes: {
947
+ autoConnected: result.autoConnected === true,
948
+ signerType: result.signerType ?? "passkey"
949
+ }
950
+ });
951
+ } else {
952
+ this.emitTelemetry(
953
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "connect.failed",
954
+ telemetry,
955
+ {
956
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
957
+ ...describeTelemetryError(result.error),
958
+ attributes: { action: result.action }
959
+ }
960
+ );
961
+ }
962
+ return result;
963
+ }
964
+ /**
965
+ * High-level connect entry point — the method most integrators want.
966
+ *
967
+ * Tries the lightweight {@link connectWithModal} first (no passkey ceremony
968
+ * for returning users on the same browser), and transparently falls back to
969
+ * {@link authWithModal} when the connect dialog reports it has no stored
970
+ * credentials for this origin (`action: "switch"`). First-time visitors get
971
+ * one passkey ceremony to register; subsequent connects on the same browser
972
+ * resolve with no biometric prompt — and silently when the user has enabled
973
+ * "Connect automatically".
974
+ *
975
+ * The return shape is the same {@link ConnectResult} discriminated union as
976
+ * {@link connectWithModal}, so callers can treat both paths uniformly. When
977
+ * the auth-modal fallback runs, its successful result is normalized into a
978
+ * `ConnectResult` (no `id` field — the embedder origin must not see it).
979
+ *
980
+ * Cancellation: if the user cancels either dialog, the result is
981
+ * `{ success: false, action: "cancel", error: { code: "USER_CANCELLED", … } }`.
982
+ *
983
+ * @param options - Forwarded to whichever underlying modal is shown.
984
+ * `username` and `oauthEnabled` only apply to the auth-modal fallback path.
985
+ * @returns A {@link ConnectResult} discriminated union.
986
+ *
987
+ * @example
988
+ * ```typescript
989
+ * const result = await client.connect();
990
+ * if (result.success) {
991
+ * console.log("Connected as", result.user?.username ?? result.user?.address);
992
+ * }
993
+ * ```
994
+ */
995
+ async connect(options) {
996
+ const connectResult = await this.connectWithModal(
997
+ options?.theme ? { theme: options.theme } : void 0
998
+ );
999
+ if (connectResult.success) return connectResult;
1000
+ if (connectResult.action === "switch") {
1001
+ const authResult = await this.authWithModal(options);
1002
+ if (authResult.success && authResult.user) {
1003
+ return {
1004
+ success: true,
1005
+ user: {
1006
+ username: authResult.user.username,
1007
+ address: authResult.user.address
1008
+ },
1009
+ autoConnected: false
1010
+ };
1011
+ }
1012
+ return {
1013
+ success: false,
1014
+ action: "cancel",
1015
+ error: authResult.error ?? {
1016
+ code: "USER_CANCELLED",
1017
+ message: "Connection was cancelled"
1018
+ }
1019
+ };
1020
+ }
1021
+ return connectResult;
269
1022
  }
270
1023
  /**
271
1024
  * Open the account management dialog.
@@ -290,10 +1043,13 @@ var OneAuthClient = class {
290
1043
  themeParsed.forEach((value, key) => params.set(key, value));
291
1044
  }
292
1045
  const url = `${dialogUrl}/dialog/account?${params.toString()}`;
1046
+ const accessTokenResult = await this.fetchAccessToken();
1047
+ const initMessage = { mode: "iframe" };
1048
+ if (accessTokenResult.ok) {
1049
+ initMessage.auth = { accessToken: accessTokenResult.accessToken };
1050
+ }
293
1051
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
294
- const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
295
- mode: "iframe"
296
- });
1052
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, initMessage);
297
1053
  if (!ready) return;
298
1054
  const dialogOrigin = this.getDialogOrigin();
299
1055
  return new Promise((resolve) => {
@@ -315,131 +1071,275 @@ var OneAuthClient = class {
315
1071
  });
316
1072
  }
317
1073
  /**
318
- * Check if a user has already granted consent for the requested fields.
319
- * This is a read-only check — no dialog is shown.
1074
+ * Open the account-recovery backup flow directly.
320
1075
  *
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
- * ```
1076
+ * Deep-links into the "Backup your account" flow inside the account dialog
1077
+ * (rather than the account overview): the user verifies with their passkey,
1078
+ * then saves a recovery passphrase and downloads an encrypted backup file.
1079
+ * Use this to prompt users to secure their account after sign-up.
1080
+ *
1081
+ * Requires an authenticated user (call `authWithModal()` / `connect()`
1082
+ * first) — the dialog reads the signed-in user from the passkey origin.
1083
+ *
1084
+ * Resolves `{ completed: true }` when the user finishes the backup, or
1085
+ * `{ completed: false }` if they dismiss it — so callers can mark an
1086
+ * onboarding step done or re-prompt later.
331
1087
  */
332
- async checkConsent(options) {
333
- const clientId = options.clientId ?? this.config.clientId;
334
- if (!clientId) {
335
- return {
336
- hasConsent: false
337
- };
1088
+ async setupRecovery(options) {
1089
+ const dialogUrl = this.getDialogUrl();
1090
+ const params = new URLSearchParams({
1091
+ mode: "iframe",
1092
+ // Deep-link the account dialog straight into the backup nudge instead
1093
+ // of the account overview.
1094
+ action: "backup"
1095
+ });
1096
+ const themeParams = this.getThemeParams(options?.theme);
1097
+ if (themeParams) {
1098
+ const themeParsed = new URLSearchParams(themeParams);
1099
+ themeParsed.forEach((value, key) => params.set(key, value));
338
1100
  }
339
- const username = options.username ?? options.accountAddress;
340
- if (!username) {
341
- return {
342
- hasConsent: false
1101
+ const url = `${dialogUrl}/dialog/account?${params.toString()}`;
1102
+ const { dialog, iframe, cleanup } = this.createModalDialog(url);
1103
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
1104
+ mode: "iframe"
1105
+ });
1106
+ if (!ready) return { completed: false };
1107
+ const dialogOrigin = this.getDialogOrigin();
1108
+ return new Promise((resolve) => {
1109
+ let completed = false;
1110
+ const finish = () => {
1111
+ window.removeEventListener("message", handleMessage);
1112
+ dialog.removeEventListener("close", handleClose);
1113
+ cleanup();
1114
+ resolve({ completed });
343
1115
  };
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
1116
+ const handleMessage = (event) => {
1117
+ if (event.origin !== dialogOrigin) return;
1118
+ const type = event.data?.type;
1119
+ if (type === "PASSKEY_RECOVERY_RESULT") {
1120
+ completed = Boolean(event.data?.data?.completed ?? event.data?.success);
1121
+ return;
1122
+ }
1123
+ if (type === "PASSKEY_CLOSE" || type === "PASSKEY_DISCONNECT") {
1124
+ finish();
1125
+ }
366
1126
  };
367
- } catch {
368
- return { hasConsent: false };
369
- }
1127
+ const handleClose = () => {
1128
+ window.removeEventListener("message", handleMessage);
1129
+ resolve({ completed });
1130
+ };
1131
+ window.addEventListener("message", handleMessage);
1132
+ dialog.addEventListener("close", handleClose);
1133
+ });
1134
+ }
1135
+ /**
1136
+ * Check if a user has already granted consent for the requested fields.
1137
+ *
1138
+ * @deprecated Data-sharing consent is disabled.
1139
+ *
1140
+ * Always returns `{ hasConsent: false }`.
1141
+ */
1142
+ async checkConsent(options) {
1143
+ void options;
1144
+ return { hasConsent: false };
370
1145
  }
371
1146
  /**
372
1147
  * Request consent from the user to share their data.
373
1148
  *
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.
1149
+ * @deprecated Data-sharing consent is disabled.
376
1150
  *
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
- * ```
1151
+ * Always returns an `INVALID_REQUEST` result.
388
1152
  */
389
1153
  async requestConsent(options) {
390
- const clientId = options.clientId ?? this.config.clientId;
391
- if (!clientId) {
1154
+ void options;
1155
+ return {
1156
+ success: false,
1157
+ error: {
1158
+ code: "INVALID_REQUEST",
1159
+ message: "Data-sharing consent is disabled"
1160
+ }
1161
+ };
1162
+ }
1163
+ /**
1164
+ * Open the dedicated SmartSession permission grant dialog.
1165
+ *
1166
+ * The app owns the session key material and sends only the public
1167
+ * `sessionKeyAddress`. 1auth renders the permission review, collects the
1168
+ * owner's passkey authorization, submits the install/enable intent, and
1169
+ * returns a persistable session handle.
1170
+ */
1171
+ async grantPermissions(options) {
1172
+ const telemetry = this.createTelemetryOperation("grant_permission", {
1173
+ targetChains: normalizeGrantTargetChains(options).join(","),
1174
+ sponsor: options.sponsor !== false
1175
+ });
1176
+ if (!options.username && !options.accountAddress) {
1177
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1178
+ outcome: "failure",
1179
+ errorCode: "INVALID_OPTIONS",
1180
+ errorMessage: "Either username or accountAddress is required"
1181
+ });
392
1182
  return {
393
1183
  success: false,
394
- error: { code: "INVALID_REQUEST", message: "clientId is required (set in config or options)" }
1184
+ error: {
1185
+ code: "INVALID_OPTIONS",
1186
+ message: "Either username or accountAddress is required"
1187
+ }
395
1188
  };
396
1189
  }
397
- const username = options.username ?? options.accountAddress;
398
- if (!username) {
1190
+ const targetChains = normalizeGrantTargetChains(options);
1191
+ const sourceChains = normalizeGrantSourceChains(options);
1192
+ if (targetChains.length === 0 || !options.sessionKeyAddress || !options.permissions?.length) {
1193
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1194
+ outcome: "failure",
1195
+ errorCode: "INVALID_OPTIONS",
1196
+ errorMessage: "targetChains, sessionKeyAddress, and permissions are required"
1197
+ });
399
1198
  return {
400
1199
  success: false,
401
- error: { code: "INVALID_REQUEST", message: "username or accountAddress is required" }
1200
+ error: {
1201
+ code: "INVALID_OPTIONS",
1202
+ message: "targetChains, sessionKeyAddress, and permissions are required"
1203
+ }
402
1204
  };
403
1205
  }
404
- if (!options.fields || options.fields.length === 0) {
1206
+ if (!this.sponsorship) {
1207
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1208
+ outcome: "failure",
1209
+ errorCode: "MISSING_APP_CREDENTIALS",
1210
+ errorMessage: MISSING_APP_CREDENTIALS_MESSAGE
1211
+ });
405
1212
  return {
406
1213
  success: false,
407
- error: { code: "INVALID_REQUEST", message: "At least one field is required" }
1214
+ error: {
1215
+ code: "MISSING_APP_CREDENTIALS",
1216
+ message: MISSING_APP_CREDENTIALS_MESSAGE
1217
+ }
408
1218
  };
409
1219
  }
410
- const existing = await this.checkConsent({ ...options, clientId });
411
- if (existing.hasConsent && existing.data) {
1220
+ const accessTokenResult = await this.fetchAccessToken();
1221
+ if (!accessTokenResult.ok) {
1222
+ this.emitTelemetry("grant_permission.failed", telemetry, {
1223
+ outcome: "failure",
1224
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
1225
+ errorMessage: accessTokenResult.message
1226
+ });
412
1227
  return {
413
- success: true,
414
- data: existing.data,
415
- grantedAt: existing.grantedAt,
416
- cached: true
1228
+ success: false,
1229
+ error: {
1230
+ code: "SPONSORSHIP_FETCH_FAILED",
1231
+ message: accessTokenResult.message
1232
+ }
417
1233
  };
418
1234
  }
419
1235
  const dialogUrl = this.getDialogUrl();
420
- const params = new URLSearchParams({
421
- mode: "iframe",
422
- username,
423
- clientId,
424
- fields: options.fields.join(",")
425
- });
1236
+ const params = new URLSearchParams({ mode: "iframe" });
1237
+ appendParentOriginParam(params);
1238
+ this.appendTelemetryParams(params, telemetry);
426
1239
  const themeParams = this.getThemeParams(options.theme);
427
1240
  if (themeParams) {
428
1241
  const themeParsed = new URLSearchParams(themeParams);
429
1242
  themeParsed.forEach((value, key) => params.set(key, value));
430
1243
  }
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"
1244
+ const grantUrl = `${dialogUrl}/dialog/grant-permission?${params.toString()}`;
1245
+ const { dialog, iframe, cleanup } = this.createModalDialog(grantUrl);
1246
+ const initPayload = {
1247
+ mode: "iframe",
1248
+ clientId: this.config.clientId,
1249
+ grant: cloneGrantPayload({
1250
+ username: options.username,
1251
+ accountAddress: options.accountAddress,
1252
+ targetChain: targetChains[0],
1253
+ targetChains,
1254
+ sourceChains,
1255
+ sessionKeyAddress: options.sessionKeyAddress,
1256
+ permissions: options.permissions,
1257
+ crossChainPermits: options.crossChainPermits,
1258
+ validUntil: options.validUntil,
1259
+ validAfter: options.validAfter,
1260
+ maxUses: options.maxUses,
1261
+ contracts: options.contracts
1262
+ }),
1263
+ auth: { accessToken: accessTokenResult.accessToken },
1264
+ sponsor: options.sponsor !== false,
1265
+ telemetry: this.telemetryPayload(telemetry)
1266
+ };
1267
+ this.emitTelemetry("dialog.opened", telemetry, {
1268
+ outcome: "started",
1269
+ targetChains
435
1270
  });
436
- if (!ready) {
1271
+ const result = await this.waitForGrantPermissionResponse(
1272
+ dialog,
1273
+ iframe,
1274
+ cleanup,
1275
+ options.sponsor !== false,
1276
+ initPayload
1277
+ );
1278
+ if (result.success) {
1279
+ this.emitTelemetry("grant_permission.succeeded", telemetry, {
1280
+ outcome: "success",
1281
+ targetChains,
1282
+ status: result.status ? String(result.status) : void 0
1283
+ });
1284
+ await this.waitForDialogClose(dialog, cleanup);
1285
+ } else {
1286
+ this.emitTelemetry(
1287
+ result.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "grant_permission.failed",
1288
+ telemetry,
1289
+ {
1290
+ outcome: result.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
1291
+ targetChains,
1292
+ ...describeTelemetryError(result.error)
1293
+ }
1294
+ );
1295
+ }
1296
+ return result;
1297
+ }
1298
+ /**
1299
+ * List SmartSession grants previously granted to the current browser origin.
1300
+ *
1301
+ * 1auth derives the app identity from the request Origin header. The SDK does
1302
+ * not send `clientId` for this lookup because grants are origin-scoped.
1303
+ */
1304
+ async listSessionGrants(options) {
1305
+ if (!options.username && !options.accountAddress) {
437
1306
  return {
438
1307
  success: false,
439
- error: { code: "USER_CANCELLED", message: "User closed the dialog" }
1308
+ error: {
1309
+ code: "INVALID_OPTIONS",
1310
+ message: "Either username or accountAddress is required"
1311
+ }
1312
+ };
1313
+ }
1314
+ const params = new URLSearchParams();
1315
+ if (options.username) params.set("username", options.username);
1316
+ if (options.accountAddress) params.set("accountAddress", options.accountAddress);
1317
+ if (options.includeRevoked) params.set("includeRevoked", "true");
1318
+ try {
1319
+ const response = await fetch(
1320
+ `${this.config.providerUrl}/api/sessions/grants?${params.toString()}`,
1321
+ { credentials: "include" }
1322
+ );
1323
+ const data = await response.json();
1324
+ if (!response.ok) {
1325
+ return {
1326
+ success: false,
1327
+ error: {
1328
+ code: "LIST_SESSION_GRANTS_FAILED",
1329
+ message: data?.error ?? `Request failed with status ${response.status}`
1330
+ }
1331
+ };
1332
+ }
1333
+ return { success: true, grants: data.grants ?? [] };
1334
+ } catch (err) {
1335
+ return {
1336
+ success: false,
1337
+ error: {
1338
+ code: "LIST_SESSION_GRANTS_FAILED",
1339
+ message: err instanceof Error ? err.message : String(err)
1340
+ }
440
1341
  };
441
1342
  }
442
- return this.waitForConsentResponse(dialog, iframe, cleanup);
443
1343
  }
444
1344
  /**
445
1345
  * Authenticate a user with an optional challenge to sign.
@@ -448,13 +1348,13 @@ var OneAuthClient = class {
448
1348
  * challenge signing, enabling off-chain login without on-chain transactions.
449
1349
  *
450
1350
  * 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
1351
+ * 1. User authenticates (sign in or sign up) through the normal auth dialog
1352
+ * 2. User signs the challenge through the normal signing dialog
1353
+ * 3. Returns user info + signature for server-side verification
455
1354
  *
456
- * The domain separator ("\x19Passkey Signed Message:\n") ensures the signature
457
- * cannot be reused for transaction signing, preventing phishing attacks.
1355
+ * This composes the same supported provider routes as calling
1356
+ * `authWithModal()` followed by `signMessage()`, so challenge auth cannot
1357
+ * drift onto a provider route that the passkey service does not expose.
458
1358
  *
459
1359
  * @example
460
1360
  * ```typescript
@@ -473,42 +1373,131 @@ var OneAuthClient = class {
473
1373
  * ```
474
1374
  */
475
1375
  async authenticate(options) {
476
- const dialogUrl = this.getDialogUrl();
477
- const params = new URLSearchParams({
478
- mode: "iframe"
1376
+ const telemetry = this.createTelemetryOperation("authenticate", {
1377
+ hasChallenge: !!options?.challenge
479
1378
  });
480
- if (this.config.clientId) {
481
- params.set("clientId", this.config.clientId);
1379
+ const authResult = await this.authWithModal(options?.theme ? { theme: options.theme } : void 0);
1380
+ if (!authResult.success) {
1381
+ this.emitTelemetry(
1382
+ authResult.error?.code === "USER_CANCELLED" ? "dialog.cancelled" : "auth.failed",
1383
+ telemetry,
1384
+ {
1385
+ outcome: authResult.error?.code === "USER_CANCELLED" ? "cancelled" : "failure",
1386
+ ...describeTelemetryError(authResult.error)
1387
+ }
1388
+ );
1389
+ return authResult;
482
1390
  }
483
- if (options?.challenge) {
484
- params.set("challenge", options.challenge);
1391
+ if (!options?.challenge) {
1392
+ this.emitTelemetry("auth.succeeded", telemetry, {
1393
+ outcome: "success",
1394
+ attributes: { challengeSigned: false }
1395
+ });
1396
+ return authResult;
485
1397
  }
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));
1398
+ const accountAddress = authResult.user?.address;
1399
+ const username = authResult.user?.username;
1400
+ if (!accountAddress && !username) {
1401
+ const result2 = {
1402
+ success: false,
1403
+ user: authResult.user,
1404
+ signerType: authResult.signerType,
1405
+ error: {
1406
+ code: "AUTHENTICATE_USER_MISSING",
1407
+ message: "Authentication succeeded but no username or account address was returned for challenge signing"
1408
+ }
1409
+ };
1410
+ this.emitTelemetry("auth.failed", telemetry, {
1411
+ outcome: "failure",
1412
+ ...describeTelemetryError(result2.error)
1413
+ });
1414
+ return result2;
490
1415
  }
491
- const url = `${dialogUrl}/dialog/authenticate?${params.toString()}`;
492
- const { dialog, iframe, cleanup } = this.createModalDialog(url);
493
- return this.waitForAuthenticateResponse(dialog, iframe, cleanup);
1416
+ const signResult = await this.signMessage({
1417
+ username,
1418
+ accountAddress,
1419
+ message: options.challenge,
1420
+ description: "Verify your identity",
1421
+ theme: options.theme
1422
+ });
1423
+ if (signResult.success && signResult.signature && signResult.signedHash) {
1424
+ const result2 = {
1425
+ ...authResult,
1426
+ challenge: {
1427
+ signature: signResult.signature,
1428
+ signedHash: signResult.signedHash
1429
+ }
1430
+ };
1431
+ this.emitTelemetry("auth.succeeded", telemetry, {
1432
+ outcome: "success",
1433
+ attributes: { challengeSigned: true }
1434
+ });
1435
+ return result2;
1436
+ }
1437
+ const error = signResult.error ? { code: signResult.error.code, message: signResult.error.message } : { code: "SIGNING_FAILED", message: "Challenge signing failed" };
1438
+ const result = {
1439
+ success: false,
1440
+ user: authResult.user,
1441
+ signerType: authResult.signerType,
1442
+ error
1443
+ };
1444
+ const cancelled = error.code === "USER_CANCELLED" || error.code === "USER_REJECTED";
1445
+ this.emitTelemetry(
1446
+ cancelled ? "dialog.cancelled" : "auth.failed",
1447
+ telemetry,
1448
+ {
1449
+ outcome: cancelled ? "cancelled" : "failure",
1450
+ ...describeTelemetryError(error)
1451
+ }
1452
+ );
1453
+ return result;
494
1454
  }
495
1455
  /**
496
1456
  * Show signing in a modal overlay (iframe dialog)
497
1457
  */
498
1458
  async signWithModal(options) {
1459
+ const telemetry = this.createTelemetryOperation("sign", {
1460
+ signingMode: "transaction",
1461
+ hasTransaction: !!options.transaction,
1462
+ hasAccountAddress: !!options.accountAddress
1463
+ });
499
1464
  const dialogUrl = this.getDialogUrl();
1465
+ const params = new URLSearchParams({ mode: "iframe" });
1466
+ appendParentOriginParam(params);
1467
+ this.appendTelemetryParams(params, telemetry);
500
1468
  const themeParams = this.getThemeParams(options?.theme);
501
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
502
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
1469
+ if (themeParams) {
1470
+ const themeParsed = new URLSearchParams(themeParams);
1471
+ themeParsed.forEach((value, key) => params.set(key, value));
1472
+ }
1473
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
1474
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
1475
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
1476
+ hidden: requestedBlindSigning
1477
+ });
1478
+ this.emitTelemetry("dialog.opened", telemetry, {
1479
+ outcome: "started",
1480
+ attributes: { blindSigning: requestedBlindSigning }
1481
+ });
503
1482
  const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
504
1483
  mode: "iframe",
505
1484
  challenge: options.challenge,
506
1485
  username: options.username,
1486
+ accountAddress: options.accountAddress,
507
1487
  description: options.description,
508
1488
  transaction: options.transaction,
509
- metadata: options.metadata
1489
+ metadata: options.metadata,
1490
+ telemetry: this.telemetryPayload(telemetry)
1491
+ }, {
1492
+ blindSigning: requestedBlindSigning,
1493
+ reveal
510
1494
  });
511
1495
  if (!ready) {
1496
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1497
+ outcome: "cancelled",
1498
+ errorCode: "USER_REJECTED",
1499
+ errorMessage: "User closed the dialog before signing"
1500
+ });
512
1501
  return {
513
1502
  success: false,
514
1503
  error: {
@@ -517,7 +1506,21 @@ var OneAuthClient = class {
517
1506
  }
518
1507
  };
519
1508
  }
520
- return this.waitForSigningResponse(dialog, iframe, cleanup);
1509
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
1510
+ const result = await this.waitForSigningResponse(dialog, iframe, cleanup);
1511
+ if (result.success) {
1512
+ this.emitTelemetry("sign.succeeded", telemetry, { outcome: "success" });
1513
+ } else {
1514
+ this.emitTelemetry(
1515
+ result.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
1516
+ telemetry,
1517
+ {
1518
+ outcome: result.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
1519
+ ...describeTelemetryError(result.error)
1520
+ }
1521
+ );
1522
+ }
1523
+ return result;
521
1524
  }
522
1525
  /**
523
1526
  * Send an intent to the Rhinestone orchestrator
@@ -549,8 +1552,37 @@ var OneAuthClient = class {
549
1552
  * ```
550
1553
  */
551
1554
  async sendIntent(options) {
552
- const { username, accountAddress, targetChain, calls } = options;
1555
+ const { username, accountAddress, targetChain } = options;
1556
+ const calls = capCallLabels(options.calls);
1557
+ const sponsor = options.sponsor !== false;
1558
+ const telemetry = this.createTelemetryOperation("intent", {
1559
+ targetChain: targetChain ?? null,
1560
+ callsCount: calls?.length ?? 0,
1561
+ sponsor,
1562
+ blindSigning: this.shouldRequestBlindSigning(options)
1563
+ });
1564
+ if (getStoredSignerType() === "eoa") {
1565
+ this.emitTelemetry("intent.completed", telemetry, {
1566
+ outcome: "failure",
1567
+ errorCode: "E_SIGNER_UNSUPPORTED",
1568
+ errorMessage: "Cross-chain intents are not supported for EOA sessions"
1569
+ });
1570
+ return {
1571
+ success: false,
1572
+ intentId: "",
1573
+ status: "failed",
1574
+ error: {
1575
+ code: "E_SIGNER_UNSUPPORTED",
1576
+ message: "Cross-chain intents are not supported for EOA sessions. Use the connected wallet's native RPC instead."
1577
+ }
1578
+ };
1579
+ }
553
1580
  if (!username && !accountAddress) {
1581
+ this.emitTelemetry("intent.completed", telemetry, {
1582
+ outcome: "failure",
1583
+ errorCode: "INVALID_OPTIONS",
1584
+ errorMessage: "Either username or accountAddress is required"
1585
+ });
554
1586
  return {
555
1587
  success: false,
556
1588
  intentId: "",
@@ -561,44 +1593,265 @@ var OneAuthClient = class {
561
1593
  }
562
1594
  };
563
1595
  }
564
- if (!targetChain || !calls?.length) {
1596
+ const hasCalls = !!calls?.length;
1597
+ const hasTokenRequests = !!options.tokenRequests?.length;
1598
+ if (!targetChain || !hasCalls && !hasTokenRequests) {
1599
+ this.emitTelemetry("intent.completed", telemetry, {
1600
+ outcome: "failure",
1601
+ errorCode: "INVALID_OPTIONS",
1602
+ errorMessage: "targetChain and calls or tokenRequests are required"
1603
+ });
1604
+ return {
1605
+ success: false,
1606
+ intentId: "",
1607
+ status: "failed",
1608
+ error: {
1609
+ code: "INVALID_OPTIONS",
1610
+ message: "targetChain and calls or tokenRequests are required"
1611
+ }
1612
+ };
1613
+ }
1614
+ if (!this.sponsorship) {
1615
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1616
+ outcome: "failure",
1617
+ errorCode: "MISSING_APP_CREDENTIALS",
1618
+ errorMessage: "No sponsorship configured on OneAuthClient"
1619
+ });
1620
+ return {
1621
+ success: false,
1622
+ intentId: "",
1623
+ status: "failed",
1624
+ error: {
1625
+ code: "MISSING_APP_CREDENTIALS",
1626
+ message: "No sponsorship configured on OneAuthClient. Every user intent must carry the app's JWT \u2014 configure `sponsorship` on the client."
1627
+ }
1628
+ };
1629
+ }
1630
+ const serializedTokenRequests = options.tokenRequests?.map((r) => ({
1631
+ token: r.token,
1632
+ amount: r.amount.toString()
1633
+ }));
1634
+ let prepareResponse;
1635
+ const dialogUrl = this.getDialogUrl();
1636
+ const params = new URLSearchParams({ mode: "iframe" });
1637
+ appendParentOriginParam(params);
1638
+ this.appendTelemetryParams(params, telemetry);
1639
+ const themeParams = this.getThemeParams();
1640
+ if (themeParams) {
1641
+ const themeParsed = new URLSearchParams(themeParams);
1642
+ themeParsed.forEach((value, key) => params.set(key, value));
1643
+ }
1644
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
1645
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
1646
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
1647
+ hidden: requestedBlindSigning
1648
+ });
1649
+ this.emitTelemetry("dialog.opened", telemetry, {
1650
+ outcome: "started",
1651
+ attributes: { blindSigning: requestedBlindSigning }
1652
+ });
1653
+ const dialogOrigin = this.getDialogOrigin();
1654
+ const dialogOpenedAt = Date.now();
1655
+ let iframeReadyObserved = false;
1656
+ let stopEarlyCloseWatch = () => void 0;
1657
+ const earlyDialogClosePromise = new Promise((resolve) => {
1658
+ let settled = false;
1659
+ const stop = () => {
1660
+ if (settled) return;
1661
+ settled = true;
1662
+ window.removeEventListener("message", handleMessage);
1663
+ dialog.removeEventListener("close", handleClose);
1664
+ };
1665
+ stopEarlyCloseWatch = stop;
1666
+ const resolveClosed = (ready) => {
1667
+ stop();
1668
+ cleanup();
1669
+ resolve({ type: "dialog_closed", ready });
1670
+ };
1671
+ const handleMessage = (event) => {
1672
+ if (event.origin !== dialogOrigin) return;
1673
+ if (event.data?.type === "PASSKEY_CLOSE") {
1674
+ resolveClosed(iframeReadyObserved);
1675
+ }
1676
+ };
1677
+ const handleClose = () => {
1678
+ resolveClosed(iframeReadyObserved);
1679
+ };
1680
+ window.addEventListener("message", handleMessage);
1681
+ dialog.addEventListener("close", handleClose);
1682
+ });
1683
+ let dialogReadyDurationMs;
1684
+ const dialogReadyPromise = this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
1685
+ blindSigning: requestedBlindSigning,
1686
+ reveal
1687
+ }).then((result) => {
1688
+ dialogReadyDurationMs = Date.now() - dialogOpenedAt;
1689
+ iframeReadyObserved = result.ready;
1690
+ return result;
1691
+ });
1692
+ const dialogClosedBeforeReadyPromise = dialogReadyPromise.then(
1693
+ (result) => result.ready ? new Promise(() => void 0) : { type: "dialog_closed", ready: false }
1694
+ );
1695
+ const accessTokenStartedAt = Date.now();
1696
+ let accessTokenDurationMs;
1697
+ const accessTokenPromise = this.fetchAccessToken().then((result) => {
1698
+ accessTokenDurationMs = Date.now() - accessTokenStartedAt;
1699
+ return result;
1700
+ });
1701
+ const accessOrClose = await Promise.race([
1702
+ accessTokenPromise.then((result) => ({ type: "access_token", result })),
1703
+ dialogClosedBeforeReadyPromise,
1704
+ earlyDialogClosePromise
1705
+ ]);
1706
+ if (accessOrClose.type === "dialog_closed") {
1707
+ stopEarlyCloseWatch();
1708
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1709
+ outcome: "cancelled",
1710
+ errorCode: "USER_CANCELLED",
1711
+ errorMessage: accessOrClose.ready ? "User closed the dialog while intent setup was still loading" : "User closed the dialog before it became ready",
1712
+ attributes: {
1713
+ accessTokenDurationMs,
1714
+ dialogReadyDurationMs
1715
+ }
1716
+ });
1717
+ return {
1718
+ success: false,
1719
+ intentId: "",
1720
+ status: "failed",
1721
+ error: { code: "USER_CANCELLED", message: "User closed the dialog" }
1722
+ };
1723
+ }
1724
+ const accessTokenResult = accessOrClose.result;
1725
+ let accessTokenFailure = null;
1726
+ if (!accessTokenResult.ok) {
1727
+ const errorCode = accessTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED";
1728
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1729
+ outcome: "failure",
1730
+ errorCode,
1731
+ errorMessage: accessTokenResult.message,
1732
+ attributes: {
1733
+ accessTokenDurationMs,
1734
+ dialogReadyDurationMs,
1735
+ loadingPhase: "access_token"
1736
+ }
1737
+ });
1738
+ accessTokenFailure = { code: errorCode, message: accessTokenResult.message };
1739
+ }
1740
+ let accessToken = accessTokenResult.ok ? accessTokenResult.accessToken : "";
1741
+ const requestBody = {
1742
+ username,
1743
+ accountAddress,
1744
+ targetChain,
1745
+ calls,
1746
+ tokenRequests: serializedTokenRequests,
1747
+ sourceAssets: options.sourceAssets,
1748
+ sourceChainId: options.sourceChainId,
1749
+ auth: { accessToken },
1750
+ ...this.config.clientId && { clientId: this.config.clientId }
1751
+ };
1752
+ let earlyPrepareResult = null;
1753
+ let extensionTokensPromise = Promise.resolve({ ok: true, extensionTokens: [] });
1754
+ let extensionTokensDurationMs;
1755
+ this.emitTelemetry("intent.prepare.started", telemetry, {
1756
+ outcome: "started",
1757
+ targetChain,
1758
+ attributes: {
1759
+ accessTokenDurationMs,
1760
+ dialogReadyDurationMs
1761
+ }
1762
+ });
1763
+ const prepareStartedAt = Date.now();
1764
+ let prepareDurationMs;
1765
+ const runPrepare = () => this.prepareIntent(requestBody, telemetry).then((r) => {
1766
+ earlyPrepareResult = r;
1767
+ prepareDurationMs = Date.now() - prepareStartedAt;
1768
+ if (r.success) {
1769
+ this.emitTelemetry("intent.prepare.succeeded", telemetry, {
1770
+ outcome: "success",
1771
+ targetChain,
1772
+ status: "quoted",
1773
+ attributes: {
1774
+ accessTokenDurationMs,
1775
+ dialogReadyDurationMs,
1776
+ prepareDurationMs
1777
+ }
1778
+ });
1779
+ const extensionTokensStartedAt = Date.now();
1780
+ extensionTokensPromise = this.fetchExtensionTokens(
1781
+ [r.data.intentOp],
1782
+ [sponsor]
1783
+ ).then((result) => {
1784
+ extensionTokensDurationMs = Date.now() - extensionTokensStartedAt;
1785
+ return result;
1786
+ });
1787
+ } else {
1788
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
1789
+ outcome: "failure",
1790
+ targetChain,
1791
+ ...describeTelemetryError(r.error),
1792
+ attributes: {
1793
+ accessTokenDurationMs,
1794
+ dialogReadyDurationMs,
1795
+ prepareDurationMs,
1796
+ loadingPhase: "prepare"
1797
+ }
1798
+ });
1799
+ }
1800
+ return r;
1801
+ });
1802
+ const preparePromise = accessTokenFailure ? Promise.resolve({
1803
+ success: false,
1804
+ error: accessTokenFailure
1805
+ }).then((r) => {
1806
+ earlyPrepareResult = r;
1807
+ return r;
1808
+ }) : runPrepare();
1809
+ const retryPrepare = async () => {
1810
+ const retryTokenResult = await this.fetchAccessToken();
1811
+ if (!retryTokenResult.ok) {
1812
+ return {
1813
+ success: false,
1814
+ error: {
1815
+ code: retryTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED",
1816
+ message: retryTokenResult.message
1817
+ }
1818
+ };
1819
+ }
1820
+ accessToken = retryTokenResult.accessToken;
1821
+ requestBody.auth = { accessToken };
1822
+ return runPrepare();
1823
+ };
1824
+ const readyOrClose = await Promise.race([
1825
+ dialogReadyPromise.then((result) => ({ type: "ready", result })),
1826
+ earlyDialogClosePromise
1827
+ ]);
1828
+ if (readyOrClose.type === "dialog_closed") {
1829
+ stopEarlyCloseWatch();
1830
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1831
+ outcome: "cancelled",
1832
+ errorCode: "USER_CANCELLED",
1833
+ errorMessage: readyOrClose.ready ? "User closed the dialog while intent setup was still loading" : "User closed the dialog before it became ready",
1834
+ attributes: {
1835
+ accessTokenDurationMs,
1836
+ dialogReadyDurationMs,
1837
+ prepareDurationMs
1838
+ }
1839
+ });
565
1840
  return {
566
1841
  success: false,
567
1842
  intentId: "",
568
1843
  status: "failed",
569
- error: {
570
- code: "INVALID_OPTIONS",
571
- message: "targetChain and calls are required"
572
- }
1844
+ error: { code: "USER_CANCELLED", message: "User closed the dialog" }
573
1845
  };
574
1846
  }
575
- const serializedTokenRequests = options.tokenRequests?.map((r) => ({
576
- token: r.token,
577
- amount: r.amount.toString()
578
- }));
579
- let prepareResponse;
580
- const requestBody = {
581
- username,
582
- accountAddress,
583
- targetChain,
584
- calls,
585
- tokenRequests: serializedTokenRequests,
586
- sourceAssets: options.sourceAssets,
587
- sourceChainId: options.sourceChainId,
588
- ...this.config.clientId && { clientId: this.config.clientId }
589
- };
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
- let earlyPrepareResult = null;
596
- const preparePromise = this.prepareIntent(requestBody).then((r) => {
597
- earlyPrepareResult = r;
598
- return r;
599
- });
600
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
1847
+ const dialogResult = readyOrClose.result;
601
1848
  if (!dialogResult.ready) {
1849
+ stopEarlyCloseWatch();
1850
+ this.emitTelemetry("dialog.cancelled", telemetry, {
1851
+ outcome: "cancelled",
1852
+ errorCode: "USER_CANCELLED",
1853
+ errorMessage: "User closed the dialog before it became ready"
1854
+ });
602
1855
  return {
603
1856
  success: false,
604
1857
  intentId: "",
@@ -606,126 +1859,355 @@ var OneAuthClient = class {
606
1859
  error: { code: "USER_CANCELLED", message: "User closed the dialog" }
607
1860
  };
608
1861
  }
1862
+ const blindSigning = dialogResult.blindSigning;
1863
+ this.emitTelemetry("dialog.ready", telemetry, {
1864
+ outcome: "started",
1865
+ attributes: {
1866
+ accessTokenDurationMs,
1867
+ dialogReadyDurationMs
1868
+ }
1869
+ });
609
1870
  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);
1871
+ const refreshQuote = async () => {
1872
+ try {
1873
+ const refreshedTokenResult = await this.fetchAccessToken();
1874
+ const refreshToken = refreshedTokenResult.ok ? refreshedTokenResult.accessToken : accessToken;
1875
+ const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
1876
+ method: "POST",
1877
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1878
+ body: JSON.stringify({ ...requestBody, auth: { accessToken: refreshToken } }),
1879
+ credentials: "include"
1880
+ });
1881
+ if (!refreshResponse.ok) {
1882
+ console.error("[SDK] Quote refresh failed:", await refreshResponse.text());
1883
+ return null;
1884
+ }
1885
+ const refreshedData = await refreshResponse.json();
1886
+ const refreshedExtensionPromise = this.fetchExtensionTokens(
1887
+ [refreshedData.intentOp],
1888
+ [sponsor]
1889
+ );
1890
+ const extensionResult = await refreshedExtensionPromise;
1891
+ if (!extensionResult.ok) {
1892
+ console.error(
1893
+ "[SDK] Extension token refresh failed:",
1894
+ extensionResult.message
1895
+ );
1896
+ return null;
1897
+ }
1898
+ const refreshedAuth = {
1899
+ accessToken: refreshToken,
1900
+ extensionTokens: extensionResult.extensionTokens
1901
+ };
1902
+ prepareResponse = refreshedData;
1903
+ accessToken = refreshToken;
1904
+ requestBody.auth = { accessToken: refreshToken };
1905
+ extensionTokensPromise = refreshedExtensionPromise;
1906
+ currentInitPayload = {
1907
+ ...currentInitPayload,
1908
+ transaction: refreshedData.transaction,
1909
+ challenge: refreshedData.challenge,
1910
+ originMessages: refreshedData.originMessages,
1911
+ expiresAt: refreshedData.expiresAt,
1912
+ intentOp: refreshedData.intentOp,
1913
+ digestResult: refreshedData.digestResult,
1914
+ // A refreshed quote mints a fresh binding — it MUST replace the old
1915
+ // one, or execute would verify the new intentOp against a stale
1916
+ // binding and reject (once enforce mode is on).
1917
+ binding: refreshedData.binding,
1918
+ auth: refreshedAuth
1919
+ };
615
1920
  return {
616
- success: false,
617
- intentId: "",
618
- status: "failed",
619
- error: prepareResult.error
1921
+ intentOp: refreshedData.intentOp,
1922
+ expiresAt: refreshedData.expiresAt,
1923
+ challenge: refreshedData.challenge,
1924
+ originMessages: refreshedData.originMessages,
1925
+ transaction: refreshedData.transaction,
1926
+ digestResult: refreshedData.digestResult,
1927
+ binding: refreshedData.binding,
1928
+ // Dialog-executed intents read auth from dialog state — ship the
1929
+ // rebound bundle with the refresh so PASSKEY_REFRESH_COMPLETE
1930
+ // updates it alongside the quote.
1931
+ auth: refreshedAuth
620
1932
  };
1933
+ } catch (error) {
1934
+ console.error("[SDK] Quote refresh error:", error);
1935
+ return null;
1936
+ }
1937
+ };
1938
+ const signingResultPromise = this.waitForSigningWithRefresh(
1939
+ dialog,
1940
+ iframe,
1941
+ cleanup,
1942
+ dialogOrigin,
1943
+ refreshQuote
1944
+ );
1945
+ stopEarlyCloseWatch();
1946
+ if (earlyPrepareResult) {
1947
+ let prepareResult = earlyPrepareResult;
1948
+ for (; ; ) {
1949
+ while (!prepareResult.success) {
1950
+ this.sendPrepareError(iframe, prepareResult.error.message);
1951
+ if (blindSigning) {
1952
+ cleanup();
1953
+ return {
1954
+ success: false,
1955
+ intentId: "",
1956
+ status: "failed",
1957
+ error: prepareResult.error
1958
+ };
1959
+ }
1960
+ const next2 = await this.waitForPrepareRetryOrClose(dialog, cleanup);
1961
+ if (next2 === "closed") {
1962
+ return {
1963
+ success: false,
1964
+ intentId: "",
1965
+ status: "failed",
1966
+ error: prepareResult.error
1967
+ };
1968
+ }
1969
+ prepareResult = await retryPrepare();
1970
+ }
1971
+ prepareResponse = prepareResult.data;
1972
+ const extensionResult = await extensionTokensPromise;
1973
+ if (extensionResult.ok) {
1974
+ const sponsorshipTokens = {
1975
+ accessToken,
1976
+ extensionTokens: extensionResult.extensionTokens
1977
+ };
1978
+ currentInitPayload = {
1979
+ mode: "iframe",
1980
+ calls,
1981
+ chainId: targetChain,
1982
+ transaction: prepareResponse.transaction,
1983
+ challenge: prepareResponse.challenge,
1984
+ username,
1985
+ accountAddress: prepareResponse.accountAddress,
1986
+ originMessages: prepareResponse.originMessages,
1987
+ tokenRequests: serializedTokenRequests,
1988
+ expiresAt: prepareResponse.expiresAt,
1989
+ userId: prepareResponse.userId,
1990
+ intentOp: prepareResponse.intentOp,
1991
+ digestResult: prepareResponse.digestResult,
1992
+ binding: prepareResponse.binding,
1993
+ tier: prepareResult.tier,
1994
+ auth: sponsorshipTokens,
1995
+ telemetry: this.telemetryPayload(telemetry)
1996
+ };
1997
+ dialogResult.sendInit(currentInitPayload);
1998
+ break;
1999
+ }
2000
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
2001
+ outcome: "failure",
2002
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
2003
+ errorMessage: extensionResult.message,
2004
+ attributes: {
2005
+ accessTokenDurationMs,
2006
+ dialogReadyDurationMs,
2007
+ prepareDurationMs,
2008
+ extensionTokensDurationMs,
2009
+ loadingPhase: "extension_token"
2010
+ }
2011
+ });
2012
+ this.sendPrepareError(iframe, extensionResult.message);
2013
+ if (blindSigning) {
2014
+ cleanup();
2015
+ return {
2016
+ success: false,
2017
+ intentId: "",
2018
+ status: "failed",
2019
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2020
+ };
2021
+ }
2022
+ const next = await this.waitForPrepareRetryOrClose(dialog, cleanup);
2023
+ if (next === "closed") {
2024
+ return {
2025
+ success: false,
2026
+ intentId: "",
2027
+ status: "failed",
2028
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2029
+ };
2030
+ }
2031
+ prepareResult = await retryPrepare();
621
2032
  }
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
2033
  } else {
641
2034
  currentInitPayload = {
642
2035
  mode: "iframe",
2036
+ signingMode: "transaction",
643
2037
  calls,
644
2038
  chainId: targetChain,
645
2039
  username,
646
2040
  accountAddress: options.accountAddress,
647
- tokenRequests: serializedTokenRequests
2041
+ tokenRequests: serializedTokenRequests,
2042
+ telemetry: this.telemetryPayload(telemetry)
648
2043
  };
649
2044
  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
2045
+ const prepareOrCancel = await Promise.race([
2046
+ preparePromise.then((prepareResult2) => ({
2047
+ type: "prepare",
2048
+ prepareResult: prepareResult2
2049
+ })),
2050
+ signingResultPromise.then((signingResult2) => ({
2051
+ type: "signing",
2052
+ signingResult: signingResult2
2053
+ }))
2054
+ ]);
2055
+ let prepareResult;
2056
+ if (prepareOrCancel.type === "signing") {
2057
+ const earlySigningResult = prepareOrCancel.signingResult;
2058
+ if (earlySigningResult.success) {
2059
+ prepareResult = await preparePromise;
2060
+ } else {
2061
+ const signingError = earlySigningResult.error;
2062
+ this.emitTelemetry(
2063
+ signingError?.code === "USER_REJECTED" ? "dialog.cancelled" : "intent.sign.failed",
2064
+ telemetry,
2065
+ {
2066
+ outcome: signingError?.code === "USER_REJECTED" ? "cancelled" : "failure",
2067
+ ...describeTelemetryError(signingError)
2068
+ }
2069
+ );
2070
+ if (blindSigning) cleanup();
2071
+ return {
2072
+ success: false,
2073
+ intentId: "",
2074
+ status: "failed",
2075
+ error: signingError
2076
+ };
2077
+ }
2078
+ } else {
2079
+ prepareResult = prepareOrCancel.prepareResult;
2080
+ }
2081
+ for (; ; ) {
2082
+ while (!prepareResult.success) {
2083
+ this.sendPrepareError(iframe, prepareResult.error.message);
2084
+ if (blindSigning) {
2085
+ cleanup();
2086
+ return {
2087
+ success: false,
2088
+ intentId: "",
2089
+ status: "failed",
2090
+ error: prepareResult.error
2091
+ };
2092
+ }
2093
+ const next2 = await this.waitForPrepareRetryOrClose(dialog, cleanup);
2094
+ if (next2 === "closed") {
2095
+ return {
2096
+ success: false,
2097
+ intentId: "",
2098
+ status: "failed",
2099
+ error: prepareResult.error
2100
+ };
2101
+ }
2102
+ prepareResult = await retryPrepare();
2103
+ }
2104
+ prepareResponse = prepareResult.data;
2105
+ const authReady = !sponsor ? {
2106
+ accessToken,
2107
+ extensionTokens: []
2108
+ } : void 0;
2109
+ currentInitPayload = {
2110
+ mode: "iframe",
2111
+ calls,
2112
+ chainId: targetChain,
2113
+ transaction: prepareResponse.transaction,
2114
+ challenge: prepareResponse.challenge,
2115
+ username,
2116
+ accountAddress: prepareResponse.accountAddress,
2117
+ originMessages: prepareResponse.originMessages,
2118
+ tokenRequests: serializedTokenRequests,
2119
+ expiresAt: prepareResponse.expiresAt,
2120
+ userId: prepareResponse.userId,
2121
+ intentOp: prepareResponse.intentOp,
2122
+ digestResult: prepareResponse.digestResult,
2123
+ binding: prepareResponse.binding,
2124
+ tier: prepareResult.tier,
2125
+ ...authReady && { auth: authReady },
2126
+ telemetry: this.telemetryPayload(telemetry)
659
2127
  };
2128
+ iframe.contentWindow?.postMessage(
2129
+ { type: "PASSKEY_QUOTE_READY", ...currentInitPayload, fullViewport: true, blindSigning },
2130
+ dialogOrigin
2131
+ );
2132
+ if (!sponsor) break;
2133
+ const extensionResult = await extensionTokensPromise;
2134
+ if (extensionResult.ok) {
2135
+ const sponsorshipTokens = {
2136
+ accessToken,
2137
+ extensionTokens: extensionResult.extensionTokens
2138
+ };
2139
+ currentInitPayload = {
2140
+ ...currentInitPayload,
2141
+ auth: sponsorshipTokens
2142
+ };
2143
+ iframe.contentWindow?.postMessage(
2144
+ { type: "PASSKEY_QUOTE_READY", auth: sponsorshipTokens, fullViewport: true, blindSigning },
2145
+ dialogOrigin
2146
+ );
2147
+ break;
2148
+ }
2149
+ this.emitTelemetry("intent.prepare.failed", telemetry, {
2150
+ outcome: "failure",
2151
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
2152
+ errorMessage: extensionResult.message,
2153
+ attributes: {
2154
+ accessTokenDurationMs,
2155
+ dialogReadyDurationMs,
2156
+ prepareDurationMs,
2157
+ extensionTokensDurationMs,
2158
+ loadingPhase: "extension_token"
2159
+ }
2160
+ });
2161
+ this.sendPrepareError(iframe, extensionResult.message);
2162
+ if (blindSigning) {
2163
+ cleanup();
2164
+ return {
2165
+ success: false,
2166
+ intentId: "",
2167
+ status: "failed",
2168
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2169
+ };
2170
+ }
2171
+ const next = await this.waitForPrepareRetryOrClose(dialog, cleanup);
2172
+ if (next === "closed") {
2173
+ return {
2174
+ success: false,
2175
+ intentId: "",
2176
+ status: "failed",
2177
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2178
+ };
2179
+ }
2180
+ prepareResult = await retryPrepare();
660
2181
  }
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
2182
  }
683
2183
  const handleReReady = (event) => {
684
2184
  if (event.origin !== dialogOrigin) return;
685
2185
  if (event.data?.type === "PASSKEY_READY") {
686
2186
  iframe.contentWindow?.postMessage(
687
- { type: "PASSKEY_INIT", ...currentInitPayload, fullViewport: true },
2187
+ { type: "PASSKEY_INIT", ...currentInitPayload, fullViewport: true, blindSigning },
688
2188
  dialogOrigin
689
2189
  );
690
2190
  }
691
2191
  };
692
2192
  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;
710
- }
711
- const refreshedData = await refreshResponse.json();
712
- prepareResponse = refreshedData;
713
- 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
720
- };
721
- } catch (error) {
722
- console.error("[SDK] Quote refresh error:", error);
723
- return null;
724
- }
725
- }
726
- );
2193
+ const signingResult = await signingResultPromise;
727
2194
  window.removeEventListener("message", handleReReady);
728
2195
  if (!signingResult.success) {
2196
+ this.emitTelemetry(
2197
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "intent.sign.failed",
2198
+ telemetry,
2199
+ {
2200
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
2201
+ ...describeTelemetryError(signingResult.error),
2202
+ attributes: {
2203
+ accessTokenDurationMs,
2204
+ dialogReadyDurationMs,
2205
+ prepareDurationMs,
2206
+ extensionTokensDurationMs
2207
+ }
2208
+ }
2209
+ );
2210
+ if (blindSigning) cleanup();
729
2211
  return {
730
2212
  success: false,
731
2213
  intentId: "",
@@ -742,12 +2224,67 @@ var OneAuthClient = class {
742
2224
  intentId: signingResult.intentId,
743
2225
  status: "pending"
744
2226
  };
2227
+ this.emitTelemetry("intent.sign.succeeded", telemetry, {
2228
+ outcome: "success",
2229
+ attributes: {
2230
+ accessTokenDurationMs,
2231
+ dialogReadyDurationMs,
2232
+ prepareDurationMs,
2233
+ extensionTokensDurationMs,
2234
+ dialogExecuted: true
2235
+ }
2236
+ });
745
2237
  } else {
2238
+ const extensionResult = await extensionTokensPromise;
2239
+ if (!extensionResult.ok) {
2240
+ this.emitTelemetry("intent.sign.failed", telemetry, {
2241
+ outcome: "failure",
2242
+ errorCode: "SPONSORSHIP_FETCH_FAILED",
2243
+ errorMessage: extensionResult.message,
2244
+ attributes: {
2245
+ accessTokenDurationMs,
2246
+ dialogReadyDurationMs,
2247
+ prepareDurationMs,
2248
+ extensionTokensDurationMs,
2249
+ loadingPhase: "extension_token"
2250
+ }
2251
+ });
2252
+ this.sendTransactionStatus(iframe, "failed");
2253
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2254
+ return {
2255
+ success: false,
2256
+ intentId: "",
2257
+ status: "failed",
2258
+ error: { code: "SPONSORSHIP_FETCH_FAILED", message: extensionResult.message }
2259
+ };
2260
+ }
2261
+ const extensionTokens = extensionResult.extensionTokens;
746
2262
  try {
2263
+ this.emitTelemetry("intent.sign.succeeded", telemetry, {
2264
+ outcome: "success",
2265
+ attributes: {
2266
+ accessTokenDurationMs,
2267
+ dialogReadyDurationMs,
2268
+ prepareDurationMs,
2269
+ extensionTokensDurationMs,
2270
+ dialogExecuted: false
2271
+ }
2272
+ });
2273
+ this.emitTelemetry("intent.execute.started", telemetry, {
2274
+ outcome: "started",
2275
+ targetChain: prepareResponse.targetChain,
2276
+ attributes: {
2277
+ accessTokenDurationMs,
2278
+ dialogReadyDurationMs,
2279
+ prepareDurationMs,
2280
+ extensionTokensDurationMs
2281
+ }
2282
+ });
747
2283
  const response = await fetch(`${this.config.providerUrl}/api/intent/execute`, {
748
2284
  method: "POST",
749
2285
  headers: {
750
- "Content-Type": "application/json"
2286
+ "Content-Type": "application/json",
2287
+ ...this.telemetryHeaders(telemetry)
751
2288
  },
752
2289
  body: JSON.stringify({
753
2290
  // Data from prepare response (no intentId yet - created on execute)
@@ -757,16 +2294,32 @@ var OneAuthClient = class {
757
2294
  calls: prepareResponse.calls,
758
2295
  expiresAt: prepareResponse.expiresAt,
759
2296
  digestResult: prepareResponse.digestResult,
2297
+ // Opaque prepare->execute binding, forwarded unchanged.
2298
+ binding: prepareResponse.binding,
760
2299
  // Signature from dialog
761
2300
  signature: signingResult.signature,
762
- passkey: signingResult.passkey
2301
+ passkey: signingResult.passkey,
763
2302
  // Include passkey info for signature encoding
2303
+ // App JWT — `accessToken` is always present. `extensionToken` is
2304
+ // only included when sponsorship was requested (sponsor !== false).
2305
+ auth: {
2306
+ accessToken,
2307
+ ...extensionTokens.length > 0 && {
2308
+ extensionToken: extensionTokens[0]
2309
+ }
2310
+ }
764
2311
  })
765
2312
  });
766
2313
  if (!response.ok) {
767
2314
  const errorData = await response.json().catch(() => ({}));
768
2315
  this.sendTransactionStatus(iframe, "failed");
769
- await this.waitForDialogClose(dialog, cleanup);
2316
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2317
+ this.emitTelemetry("intent.execute.failed", telemetry, {
2318
+ outcome: "failure",
2319
+ targetChain: prepareResponse.targetChain,
2320
+ errorCode: "EXECUTE_FAILED",
2321
+ errorMessage: errorData.error || "Failed to execute intent"
2322
+ });
770
2323
  return {
771
2324
  success: false,
772
2325
  intentId: "",
@@ -779,9 +2332,19 @@ var OneAuthClient = class {
779
2332
  };
780
2333
  }
781
2334
  executeResponse = await response.json();
2335
+ this.emitTelemetry("intent.execute.succeeded", telemetry, {
2336
+ outcome: "success",
2337
+ targetChain: prepareResponse.targetChain,
2338
+ status: executeResponse.status
2339
+ });
782
2340
  } catch (error) {
783
2341
  this.sendTransactionStatus(iframe, "failed");
784
- await this.waitForDialogClose(dialog, cleanup);
2342
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2343
+ this.emitTelemetry("intent.execute.failed", telemetry, {
2344
+ outcome: "failure",
2345
+ targetChain: prepareResponse.targetChain,
2346
+ ...describeTelemetryError(error)
2347
+ });
785
2348
  return {
786
2349
  success: false,
787
2350
  intentId: "",
@@ -798,6 +2361,10 @@ var OneAuthClient = class {
798
2361
  let finalTxHash = executeResponse.transactionHash;
799
2362
  if (finalStatus === "pending") {
800
2363
  this.sendTransactionStatus(iframe, "pending");
2364
+ this.emitTelemetry("intent.status.started", telemetry, {
2365
+ outcome: "started",
2366
+ status: finalStatus
2367
+ });
801
2368
  let userClosedEarly = false;
802
2369
  const dialogOrigin2 = this.getDialogOrigin();
803
2370
  const earlyCloseHandler = (event) => {
@@ -818,7 +2385,10 @@ var OneAuthClient = class {
818
2385
  `${this.config.providerUrl}/api/intent/status/${executeResponse.intentId}`,
819
2386
  {
820
2387
  method: "GET",
821
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
2388
+ headers: {
2389
+ ...this.telemetryHeaders(telemetry),
2390
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
2391
+ }
822
2392
  }
823
2393
  );
824
2394
  if (statusResponse.ok) {
@@ -850,6 +2420,12 @@ var OneAuthClient = class {
850
2420
  window.removeEventListener("message", earlyCloseHandler);
851
2421
  if (userClosedEarly) {
852
2422
  cleanup();
2423
+ this.emitTelemetry("intent.status.failed", telemetry, {
2424
+ outcome: "cancelled",
2425
+ status: finalStatus,
2426
+ errorCode: "USER_CANCELLED",
2427
+ errorMessage: "User closed the dialog while status polling"
2428
+ });
853
2429
  return {
854
2430
  success: false,
855
2431
  intentId: executeResponse.intentId,
@@ -872,19 +2448,25 @@ var OneAuthClient = class {
872
2448
  };
873
2449
  const isSuccessStatus = successStatuses[closeOn]?.includes(finalStatus) ?? false;
874
2450
  const displayStatus = isSuccessStatus ? "confirmed" : finalStatus;
875
- const closePromise = this.waitForDialogClose(dialog, cleanup);
2451
+ const closePromise = this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
876
2452
  this.sendTransactionStatus(iframe, displayStatus, finalTxHash);
877
2453
  await closePromise;
878
2454
  if (options.waitForHash && !finalTxHash) {
879
2455
  const hash = await this.waitForTransactionHash(executeResponse.intentId, {
880
2456
  timeoutMs: options.hashTimeoutMs,
881
2457
  intervalMs: options.hashIntervalMs
882
- });
2458
+ }, telemetry);
883
2459
  if (hash) {
884
2460
  finalTxHash = hash;
885
2461
  finalStatus = "completed";
886
2462
  } else {
887
2463
  finalStatus = "failed";
2464
+ this.emitTelemetry("intent.status.failed", telemetry, {
2465
+ outcome: "failure",
2466
+ status: finalStatus,
2467
+ errorCode: "HASH_TIMEOUT",
2468
+ errorMessage: "Timed out waiting for transaction hash"
2469
+ });
888
2470
  return {
889
2471
  success: false,
890
2472
  intentId: executeResponse.intentId,
@@ -898,6 +2480,15 @@ var OneAuthClient = class {
898
2480
  };
899
2481
  }
900
2482
  }
2483
+ this.emitTelemetry("intent.status.succeeded", telemetry, {
2484
+ outcome: isSuccessStatus ? "success" : "failure",
2485
+ status: finalStatus
2486
+ });
2487
+ this.emitTelemetry("intent.completed", telemetry, {
2488
+ outcome: isSuccessStatus ? "success" : "failure",
2489
+ status: finalStatus,
2490
+ targetChain
2491
+ });
901
2492
  return {
902
2493
  success: isSuccessStatus,
903
2494
  intentId: executeResponse.intentId,
@@ -935,7 +2526,30 @@ var OneAuthClient = class {
935
2526
  * ```
936
2527
  */
937
2528
  async sendBatchIntent(options) {
2529
+ const telemetry = this.createTelemetryOperation("batch_intent", {
2530
+ intentCount: options.intents?.length ?? 0,
2531
+ blindSigning: this.shouldRequestBlindSigning(options)
2532
+ });
2533
+ if (getStoredSignerType() === "eoa") {
2534
+ this.emitTelemetry("batch.completed", telemetry, {
2535
+ outcome: "failure",
2536
+ errorCode: "E_SIGNER_UNSUPPORTED",
2537
+ errorMessage: "Batch intents are not supported for EOA sessions"
2538
+ });
2539
+ return {
2540
+ success: false,
2541
+ results: [],
2542
+ successCount: 0,
2543
+ failureCount: 0,
2544
+ error: "Batch intents are not supported for EOA sessions. Use the connected wallet's native RPC instead."
2545
+ };
2546
+ }
938
2547
  if (!options.username && !options.accountAddress) {
2548
+ this.emitTelemetry("batch.completed", telemetry, {
2549
+ outcome: "failure",
2550
+ errorCode: "INVALID_OPTIONS",
2551
+ errorMessage: "Either username or accountAddress is required"
2552
+ });
939
2553
  return {
940
2554
  success: false,
941
2555
  results: [],
@@ -944,6 +2558,11 @@ var OneAuthClient = class {
944
2558
  };
945
2559
  }
946
2560
  if (!options.intents?.length) {
2561
+ this.emitTelemetry("batch.completed", telemetry, {
2562
+ outcome: "failure",
2563
+ errorCode: "INVALID_OPTIONS",
2564
+ errorMessage: "At least one intent is required"
2565
+ });
947
2566
  return {
948
2567
  success: false,
949
2568
  results: [],
@@ -951,9 +2570,40 @@ var OneAuthClient = class {
951
2570
  failureCount: 0
952
2571
  };
953
2572
  }
2573
+ if (!this.sponsorship) {
2574
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2575
+ outcome: "failure",
2576
+ errorCode: "MISSING_APP_CREDENTIALS",
2577
+ errorMessage: "No sponsorship configured on OneAuthClient"
2578
+ });
2579
+ return {
2580
+ success: false,
2581
+ results: [],
2582
+ successCount: 0,
2583
+ failureCount: 0,
2584
+ error: "No sponsorship configured on OneAuthClient. Every user intent must carry the app's JWT \u2014 configure `sponsorship` on the client."
2585
+ };
2586
+ }
2587
+ const sponsorFlags = options.intents.map((intent) => intent.sponsor !== false);
2588
+ const accessTokenResult = await this.fetchAccessToken();
2589
+ if (!accessTokenResult.ok) {
2590
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2591
+ outcome: "failure",
2592
+ errorCode: accessTokenResult.code ?? "SPONSORSHIP_FETCH_FAILED",
2593
+ errorMessage: accessTokenResult.message
2594
+ });
2595
+ return {
2596
+ success: false,
2597
+ results: [],
2598
+ successCount: 0,
2599
+ failureCount: 0,
2600
+ error: accessTokenResult.message
2601
+ };
2602
+ }
2603
+ let accessToken = accessTokenResult.accessToken;
954
2604
  const serializedIntents = options.intents.map((intent) => ({
955
2605
  targetChain: intent.targetChain,
956
- calls: intent.calls,
2606
+ calls: capCallLabels(intent.calls),
957
2607
  tokenRequests: intent.tokenRequests?.map((r) => ({
958
2608
  token: r.token,
959
2609
  amount: r.amount.toString()
@@ -966,20 +2616,67 @@ var OneAuthClient = class {
966
2616
  ...options.username && { username: options.username },
967
2617
  ...options.accountAddress && { accountAddress: options.accountAddress },
968
2618
  intents: serializedIntents,
2619
+ auth: { accessToken },
969
2620
  ...this.config.clientId && { clientId: this.config.clientId }
970
2621
  };
971
2622
  const dialogUrl = this.getDialogUrl();
2623
+ const params = new URLSearchParams({ mode: "iframe" });
2624
+ appendParentOriginParam(params);
2625
+ this.appendTelemetryParams(params, telemetry);
972
2626
  const themeParams = this.getThemeParams();
973
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
974
- const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
2627
+ if (themeParams) {
2628
+ const themeParsed = new URLSearchParams(themeParams);
2629
+ themeParsed.forEach((value, key) => params.set(key, value));
2630
+ }
2631
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
2632
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
2633
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
2634
+ hidden: requestedBlindSigning
2635
+ });
2636
+ this.emitTelemetry("dialog.opened", telemetry, {
2637
+ outcome: "started",
2638
+ intentCount: options.intents.length,
2639
+ attributes: { blindSigning: requestedBlindSigning }
2640
+ });
975
2641
  const dialogOrigin = this.getDialogOrigin();
976
2642
  let earlyBatchResult = null;
977
- const preparePromise = this.prepareBatchIntent(requestBody).then((r) => {
2643
+ let batchExtensionTokensPromise = Promise.resolve({ ok: true, extensionTokens: [] });
2644
+ this.emitTelemetry("batch.prepare.started", telemetry, {
2645
+ outcome: "started",
2646
+ intentCount: options.intents.length,
2647
+ targetChains: serializedIntents.map((intent) => intent.targetChain)
2648
+ });
2649
+ const preparePromise = this.prepareBatchIntent(requestBody, telemetry).then((r) => {
978
2650
  earlyBatchResult = r;
2651
+ if (r.success) {
2652
+ this.emitTelemetry("batch.prepare.succeeded", telemetry, {
2653
+ outcome: "success",
2654
+ intentCount: r.data.intents.length,
2655
+ targetChains: serializedIntents.map((intent) => intent.targetChain)
2656
+ });
2657
+ batchExtensionTokensPromise = this.fetchExtensionTokens(
2658
+ r.data.intents.map((intent) => intent.intentOp),
2659
+ sponsorFlags
2660
+ );
2661
+ } else {
2662
+ this.emitTelemetry("batch.prepare.failed", telemetry, {
2663
+ outcome: "failure",
2664
+ errorMessage: r.error,
2665
+ intentCount: options.intents.length
2666
+ });
2667
+ }
979
2668
  return r;
980
2669
  });
981
- const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup);
2670
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
2671
+ blindSigning: requestedBlindSigning,
2672
+ reveal
2673
+ });
982
2674
  if (!dialogResult.ready) {
2675
+ this.emitTelemetry("dialog.cancelled", telemetry, {
2676
+ outcome: "cancelled",
2677
+ errorCode: "USER_CANCELLED",
2678
+ errorMessage: "User closed the dialog before it became ready"
2679
+ });
983
2680
  return {
984
2681
  success: false,
985
2682
  results: [],
@@ -987,6 +2684,8 @@ var OneAuthClient = class {
987
2684
  failureCount: 0
988
2685
  };
989
2686
  }
2687
+ const blindSigning = dialogResult.blindSigning;
2688
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
990
2689
  let currentBatchPayload;
991
2690
  const handleBatchPrepareFailure = async (prepareResult) => {
992
2691
  const failedIntents = prepareResult.failedIntents;
@@ -998,7 +2697,7 @@ var OneAuthClient = class {
998
2697
  error: { message: f.error, code: "PREPARE_FAILED" }
999
2698
  })) ?? [];
1000
2699
  this.sendPrepareError(iframe, prepareResult.error);
1001
- await this.waitForDialogClose(dialog, cleanup);
2700
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
1002
2701
  return {
1003
2702
  success: false,
1004
2703
  results: failureResults,
@@ -1007,6 +2706,17 @@ var OneAuthClient = class {
1007
2706
  error: prepareResult.error
1008
2707
  };
1009
2708
  };
2709
+ const handleSponsorshipFailure = async (message) => {
2710
+ this.sendPrepareError(iframe, message);
2711
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2712
+ return {
2713
+ success: false,
2714
+ results: [],
2715
+ successCount: 0,
2716
+ failureCount: 0,
2717
+ error: `Sponsorship token fetch failed: ${message}`
2718
+ };
2719
+ };
1010
2720
  let prepareResponse;
1011
2721
  if (earlyBatchResult) {
1012
2722
  const prepareResult = earlyBatchResult;
@@ -1014,17 +2724,28 @@ var OneAuthClient = class {
1014
2724
  return handleBatchPrepareFailure(prepareResult);
1015
2725
  }
1016
2726
  prepareResponse = prepareResult.data;
2727
+ const batchExtensionResult = await batchExtensionTokensPromise;
2728
+ if (!batchExtensionResult.ok) {
2729
+ return handleSponsorshipFailure(batchExtensionResult.message);
2730
+ }
2731
+ const batchSponsorshipTokens = {
2732
+ accessToken,
2733
+ extensionTokens: batchExtensionResult.extensionTokens
2734
+ };
1017
2735
  currentBatchPayload = {
1018
2736
  mode: "iframe",
1019
2737
  batchMode: true,
1020
2738
  batchIntents: prepareResponse.intents,
1021
2739
  batchFailedIntents: prepareResponse.failedIntents,
1022
2740
  challenge: prepareResponse.challenge,
2741
+ binding: prepareResponse.binding,
1023
2742
  username: options.username,
1024
2743
  accountAddress: prepareResponse.accountAddress,
1025
2744
  userId: prepareResponse.userId,
1026
2745
  expiresAt: prepareResponse.expiresAt,
1027
- tier: prepareResult.tier
2746
+ tier: prepareResult.tier,
2747
+ auth: batchSponsorshipTokens,
2748
+ telemetry: this.telemetryPayload(telemetry)
1028
2749
  };
1029
2750
  dialogResult.sendInit(currentBatchPayload);
1030
2751
  } else {
@@ -1038,7 +2759,8 @@ var OneAuthClient = class {
1038
2759
  // No: transaction, intentOp, expiresAt, originMessages
1039
2760
  })),
1040
2761
  username: options.username,
1041
- accountAddress: options.accountAddress
2762
+ accountAddress: options.accountAddress,
2763
+ telemetry: this.telemetryPayload(telemetry)
1042
2764
  };
1043
2765
  dialogResult.sendInit(currentBatchPayload);
1044
2766
  const prepareResult = await preparePromise;
@@ -1046,20 +2768,31 @@ var OneAuthClient = class {
1046
2768
  return handleBatchPrepareFailure(prepareResult);
1047
2769
  }
1048
2770
  prepareResponse = prepareResult.data;
2771
+ const batchExtensionResult = await batchExtensionTokensPromise;
2772
+ if (!batchExtensionResult.ok) {
2773
+ return handleSponsorshipFailure(batchExtensionResult.message);
2774
+ }
2775
+ const batchSponsorshipTokens = {
2776
+ accessToken,
2777
+ extensionTokens: batchExtensionResult.extensionTokens
2778
+ };
1049
2779
  currentBatchPayload = {
1050
2780
  mode: "iframe",
1051
2781
  batchMode: true,
1052
2782
  batchIntents: prepareResponse.intents,
1053
2783
  batchFailedIntents: prepareResponse.failedIntents,
1054
2784
  challenge: prepareResponse.challenge,
2785
+ binding: prepareResponse.binding,
1055
2786
  username: options.username,
1056
2787
  accountAddress: prepareResponse.accountAddress,
1057
2788
  userId: prepareResponse.userId,
1058
2789
  expiresAt: prepareResponse.expiresAt,
1059
- tier: prepareResult.tier
2790
+ tier: prepareResult.tier,
2791
+ auth: batchSponsorshipTokens,
2792
+ telemetry: this.telemetryPayload(telemetry)
1060
2793
  };
1061
2794
  iframe.contentWindow?.postMessage(
1062
- { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true },
2795
+ { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true, blindSigning },
1063
2796
  dialogOrigin
1064
2797
  );
1065
2798
  }
@@ -1067,7 +2800,7 @@ var OneAuthClient = class {
1067
2800
  if (event.origin !== dialogOrigin) return;
1068
2801
  if (event.data?.type === "PASSKEY_READY") {
1069
2802
  iframe.contentWindow?.postMessage(
1070
- { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true },
2803
+ { type: "PASSKEY_INIT", ...currentBatchPayload, fullViewport: true, blindSigning },
1071
2804
  dialogOrigin
1072
2805
  );
1073
2806
  }
@@ -1079,19 +2812,55 @@ var OneAuthClient = class {
1079
2812
  const message = event.data;
1080
2813
  if (message?.type === "PASSKEY_REFRESH_QUOTE") {
1081
2814
  try {
2815
+ const refreshedTokenResult = await this.fetchAccessToken();
2816
+ const refreshToken = refreshedTokenResult.ok ? refreshedTokenResult.accessToken : accessToken;
1082
2817
  const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
1083
2818
  method: "POST",
1084
- headers: { "Content-Type": "application/json" },
1085
- body: JSON.stringify(requestBody)
2819
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
2820
+ body: JSON.stringify({ ...requestBody, auth: { accessToken: refreshToken } })
1086
2821
  });
1087
2822
  if (refreshResponse.ok) {
1088
2823
  const refreshed = await refreshResponse.json();
2824
+ const refreshedExtensionPromise = this.fetchExtensionTokens(
2825
+ refreshed.intents.map((intent) => intent.intentOp),
2826
+ sponsorFlags
2827
+ );
2828
+ const extensionResult = await refreshedExtensionPromise;
2829
+ if (!extensionResult.ok) {
2830
+ console.error(
2831
+ "[SDK] Batch extension token refresh failed:",
2832
+ extensionResult.message
2833
+ );
2834
+ iframe.contentWindow?.postMessage({
2835
+ type: "PASSKEY_REFRESH_ERROR",
2836
+ error: "Failed to refresh batch quotes"
2837
+ }, dialogOrigin);
2838
+ return;
2839
+ }
2840
+ const refreshedAuth = {
2841
+ accessToken: refreshToken,
2842
+ extensionTokens: extensionResult.extensionTokens
2843
+ };
1089
2844
  prepareResponse = refreshed;
2845
+ accessToken = refreshToken;
2846
+ requestBody.auth = { accessToken: refreshToken };
2847
+ batchExtensionTokensPromise = refreshedExtensionPromise;
2848
+ currentBatchPayload = {
2849
+ ...currentBatchPayload,
2850
+ batchIntents: refreshed.intents,
2851
+ batchFailedIntents: refreshed.failedIntents,
2852
+ challenge: refreshed.challenge,
2853
+ binding: refreshed.binding,
2854
+ expiresAt: refreshed.expiresAt,
2855
+ auth: refreshedAuth
2856
+ };
1090
2857
  iframe.contentWindow?.postMessage({
1091
2858
  type: "PASSKEY_REFRESH_COMPLETE",
1092
2859
  batchIntents: refreshed.intents,
1093
2860
  challenge: refreshed.challenge,
1094
- expiresAt: refreshed.expiresAt
2861
+ binding: refreshed.binding,
2862
+ expiresAt: refreshed.expiresAt,
2863
+ auth: refreshedAuth
1095
2864
  }, dialogOrigin);
1096
2865
  } else {
1097
2866
  iframe.contentWindow?.postMessage({
@@ -1127,7 +2896,11 @@ var OneAuthClient = class {
1127
2896
  }));
1128
2897
  const allResults = [...results, ...prepareFailures].sort((a, b) => a.index - b.index);
1129
2898
  const successCount = allResults.filter((r) => r.success).length;
1130
- await this.waitForDialogClose(dialog, cleanup);
2899
+ await this.waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup);
2900
+ this.emitTelemetry("batch.sign.succeeded", telemetry, {
2901
+ outcome: "success",
2902
+ intentCount: allResults.length
2903
+ });
1131
2904
  resolve({
1132
2905
  success: successCount === allResults.length,
1133
2906
  results: allResults,
@@ -1136,6 +2909,11 @@ var OneAuthClient = class {
1136
2909
  });
1137
2910
  } else {
1138
2911
  cleanup();
2912
+ this.emitTelemetry("batch.sign.failed", telemetry, {
2913
+ outcome: "failure",
2914
+ errorCode: "SIGNING_FAILED",
2915
+ errorMessage: "Batch signing failed or was cancelled"
2916
+ });
1139
2917
  resolve({
1140
2918
  success: false,
1141
2919
  results: [],
@@ -1147,6 +2925,11 @@ var OneAuthClient = class {
1147
2925
  if (message?.type === "PASSKEY_CLOSE") {
1148
2926
  window.removeEventListener("message", handleMessage);
1149
2927
  cleanup();
2928
+ this.emitTelemetry("dialog.cancelled", telemetry, {
2929
+ outcome: "cancelled",
2930
+ errorCode: "USER_CANCELLED",
2931
+ errorMessage: "User closed the batch dialog"
2932
+ });
1150
2933
  resolve({
1151
2934
  success: false,
1152
2935
  results: [],
@@ -1158,6 +2941,15 @@ var OneAuthClient = class {
1158
2941
  window.addEventListener("message", handleMessage);
1159
2942
  });
1160
2943
  window.removeEventListener("message", handleBatchReReady);
2944
+ this.emitTelemetry("batch.completed", telemetry, {
2945
+ outcome: batchResult.success ? "success" : "failure",
2946
+ intentCount: batchResult.results.length,
2947
+ attributes: {
2948
+ successCount: batchResult.successCount,
2949
+ failureCount: batchResult.failureCount
2950
+ },
2951
+ ...batchResult.error ? { errorMessage: batchResult.error } : {}
2952
+ });
1161
2953
  return batchResult;
1162
2954
  }
1163
2955
  /**
@@ -1276,6 +3068,7 @@ var OneAuthClient = class {
1276
3068
  signedHash: payload.signedHash
1277
3069
  });
1278
3070
  } else {
3071
+ cleanup();
1279
3072
  resolve({
1280
3073
  success: false,
1281
3074
  error: message.error || {
@@ -1358,6 +3151,7 @@ var OneAuthClient = class {
1358
3151
  signedHash: payload.signedHash
1359
3152
  });
1360
3153
  } else {
3154
+ cleanup();
1361
3155
  resolve({
1362
3156
  success: false,
1363
3157
  error: message.error || {
@@ -1418,6 +3212,56 @@ var OneAuthClient = class {
1418
3212
  dialog.addEventListener("close", handleClose);
1419
3213
  });
1420
3214
  }
3215
+ /**
3216
+ * Hidden blind-signing iframes have no user-visible Done or Close button.
3217
+ * Once the SDK has the result it needs, tear them down immediately instead
3218
+ * of waiting for an iframe close event that can never be clicked.
3219
+ */
3220
+ async waitForDialogCloseUnlessBlind(blindSigning, dialog, cleanup) {
3221
+ if (blindSigning) {
3222
+ cleanup();
3223
+ return;
3224
+ }
3225
+ await this.waitForDialogClose(dialog, cleanup);
3226
+ }
3227
+ /**
3228
+ * After a prepare error has been surfaced in the dialog, wait for the user
3229
+ * to either close the dialog (give up) or click "Try Again", which the
3230
+ * dialog forwards as `PASSKEY_RETRY_PREPARE`.
3231
+ *
3232
+ * On `"closed"` the dialog has been cleaned up; on `"retry"` it stays open
3233
+ * (showing its loading skeleton) while the caller re-runs prepare. Without
3234
+ * this, "Try Again" after a prepare failure was a dead end: the dialog
3235
+ * reset its local state but the SDK had already given up, so the quote
3236
+ * never arrived and the Sign button stayed disabled forever.
3237
+ */
3238
+ waitForPrepareRetryOrClose(dialog, cleanup) {
3239
+ const dialogOrigin = this.getDialogOrigin();
3240
+ return new Promise((resolve) => {
3241
+ const stop = () => {
3242
+ window.removeEventListener("message", handleMessage);
3243
+ dialog.removeEventListener("close", handleClose);
3244
+ };
3245
+ const handleMessage = (event) => {
3246
+ if (event.origin !== dialogOrigin) return;
3247
+ if (event.data?.type === "PASSKEY_RETRY_PREPARE") {
3248
+ stop();
3249
+ resolve("retry");
3250
+ } else if (event.data?.type === "PASSKEY_CLOSE") {
3251
+ stop();
3252
+ cleanup();
3253
+ resolve("closed");
3254
+ }
3255
+ };
3256
+ const handleClose = () => {
3257
+ stop();
3258
+ cleanup();
3259
+ resolve("closed");
3260
+ };
3261
+ window.addEventListener("message", handleMessage);
3262
+ dialog.addEventListener("close", handleClose);
3263
+ });
3264
+ }
1421
3265
  /**
1422
3266
  * Inject a `<link rel="preconnect">` tag for the given URL's origin.
1423
3267
  *
@@ -1443,6 +3287,82 @@ var OneAuthClient = class {
1443
3287
  } catch {
1444
3288
  }
1445
3289
  }
3290
+ /**
3291
+ * Warm the dialog ahead of the first user interaction.
3292
+ *
3293
+ * `preconnect` (run in the constructor) only warms DNS/TLS. This goes
3294
+ * further: it loads the dialog into a hidden, off-screen iframe so the
3295
+ * browser downloads and parses the dialog HTML + JS bundle and the font, and
3296
+ * keeps the cross-origin connection alive. When the user later opens a real
3297
+ * auth/sign dialog, the fresh iframe serves those bytes from cache over the
3298
+ * warm connection and paints far sooner — so the SDK preload overlay is shown
3299
+ * only briefly (or imperceptibly) instead of covering a full cold load.
3300
+ *
3301
+ * It loads a dedicated `/dialog/warm` route, NOT a real flow route. That
3302
+ * matters for two reasons:
3303
+ * - **No side effects.** The real `/dialog/auth` route runs the signup
3304
+ * flow on mount (identity probe → `POST /api/auth/register?step=start`),
3305
+ * which writes an `AuthChallenge` row and counts against the register
3306
+ * rate limiter. Warming must never create backend auth state for a user
3307
+ * who hasn't acted, so the warm route renders nothing and runs no flow.
3308
+ * - **No message cross-talk.** The warm route posts no `PASSKEY_READY` /
3309
+ * `PASSKEY_RENDERED`, so a slow warm can never satisfy a visible modal's
3310
+ * readiness listener and drive it to `PASSKEY_INIT` at the wrong moment.
3311
+ * The warm route still pulls the shared Next.js framework + main + dialog
3312
+ * layout chunks (the bulk of every flow's bundle), so the real open is fast.
3313
+ *
3314
+ * Best called on a *likely-intent* signal — `pointerenter`/`focus` of your
3315
+ * "Sign in" / "Pay" button — rather than on page load, so visitors who never
3316
+ * authenticate don't pay for a cross-origin iframe. Pass `prewarm: true` in
3317
+ * the client config to have the SDK schedule this once on an idle callback.
3318
+ *
3319
+ * Idempotent: repeated calls return the same in-flight/settled promise.
3320
+ * Resolves `true` once the hidden iframe's document has loaded, `false` on
3321
+ * timeout or failure. Never throws and never blocks a real dialog open — a
3322
+ * failed prewarm just means the next open does a normal (cold) load.
3323
+ *
3324
+ * @returns Whether the dialog became warm.
3325
+ */
3326
+ async prewarm() {
3327
+ if (typeof document === "undefined") return false;
3328
+ if (this.prewarmState) return this.prewarmState.ready;
3329
+ const warmUrl = `${this.getDialogUrl()}/dialog/warm`;
3330
+ const iframe = document.createElement("iframe");
3331
+ iframe.setAttribute("aria-hidden", "true");
3332
+ iframe.setAttribute("tabindex", "-1");
3333
+ iframe.title = "1auth dialog prewarm";
3334
+ iframe.style.cssText = "position:fixed;left:-9999px;top:0;width:1px;height:1px;opacity:0;border:0;pointer-events:none;";
3335
+ const ready = new Promise((resolve) => {
3336
+ let settled = false;
3337
+ const finish = (value) => {
3338
+ if (settled) return;
3339
+ settled = true;
3340
+ clearTimeout(timer);
3341
+ resolve(value);
3342
+ };
3343
+ iframe.onload = () => finish(true);
3344
+ iframe.onerror = () => finish(false);
3345
+ const timer = setTimeout(() => finish(false), 1e4);
3346
+ });
3347
+ iframe.src = warmUrl;
3348
+ document.body.appendChild(iframe);
3349
+ this.prewarmState = { iframe, ready };
3350
+ return ready;
3351
+ }
3352
+ /**
3353
+ * Tear down the hidden prewarm iframe created by {@link prewarm}.
3354
+ *
3355
+ * The HTTP cache that prewarm populated survives teardown, so a subsequent
3356
+ * open is still bundle-warm; this only releases the parked frame (and its
3357
+ * keep-alive connection). Call it when auth is no longer likely on the
3358
+ * current view. Safe to call when nothing is warmed.
3359
+ */
3360
+ destroyPrewarm() {
3361
+ const state = this.prewarmState;
3362
+ if (!state) return;
3363
+ this.prewarmState = null;
3364
+ state.iframe.remove();
3365
+ }
1446
3366
  /**
1447
3367
  * Wait for the dialog iframe to signal ready, but defer sending `PASSKEY_INIT`
1448
3368
  * until the caller decides the time is right.
@@ -1463,7 +3383,7 @@ var OneAuthClient = class {
1463
3383
  * @returns `{ ready: true, sendInit }` when the iframe is ready, or
1464
3384
  * `{ ready: false }` if the dialog was closed or timed out first.
1465
3385
  */
1466
- waitForDialogReadyDeferred(dialog, iframe, cleanup) {
3386
+ waitForDialogReadyDeferred(dialog, iframe, cleanup, options = {}) {
1467
3387
  const dialogOrigin = this.getDialogOrigin();
1468
3388
  return new Promise((resolve) => {
1469
3389
  let settled = false;
@@ -1477,12 +3397,15 @@ var OneAuthClient = class {
1477
3397
  const handleMessage = (event) => {
1478
3398
  if (event.origin !== dialogOrigin) return;
1479
3399
  if (event.data?.type === "PASSKEY_READY") {
3400
+ const blindSigning = options.blindSigning === true;
1480
3401
  teardown();
1481
3402
  resolve({
1482
3403
  ready: true,
3404
+ blindSigning,
1483
3405
  sendInit: (initMessage) => {
3406
+ if (blindSigning) iframe.focus({ preventScroll: true });
1484
3407
  iframe.contentWindow?.postMessage(
1485
- { type: "PASSKEY_INIT", ...initMessage, fullViewport: true },
3408
+ { type: "PASSKEY_INIT", ...initMessage, fullViewport: true, blindSigning },
1486
3409
  dialogOrigin
1487
3410
  );
1488
3411
  }
@@ -1523,13 +3446,13 @@ var OneAuthClient = class {
1523
3446
  * @param requestBody - Serialized intent options (bigint amounts converted to
1524
3447
  * strings before this call).
1525
3448
  * @returns On success: `{ success: true, data: PrepareIntentResponse, tier }`.
1526
- * On failure: `{ success: false, error: { code, message } }`.
3449
+ * On failure: `{ success: false, error: { code, message, details? } }`.
1527
3450
  */
1528
- async prepareIntent(requestBody) {
3451
+ async prepareIntent(requestBody, telemetry) {
1529
3452
  try {
1530
3453
  const response = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
1531
3454
  method: "POST",
1532
- headers: { "Content-Type": "application/json" },
3455
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1533
3456
  body: JSON.stringify(requestBody)
1534
3457
  });
1535
3458
  if (!response.ok) {
@@ -1542,7 +3465,8 @@ var OneAuthClient = class {
1542
3465
  success: false,
1543
3466
  error: {
1544
3467
  code: errorMessage.includes("User not found") ? "USER_NOT_FOUND" : "PREPARE_FAILED",
1545
- message: errorMessage
3468
+ message: errorMessage,
3469
+ details: errorData.details ?? errorData.candidateErrors ?? errorData
1546
3470
  }
1547
3471
  };
1548
3472
  }
@@ -1575,11 +3499,11 @@ var OneAuthClient = class {
1575
3499
  * @returns On success: `{ success: true, data: PrepareBatchIntentResponse, tier }`.
1576
3500
  * On failure: `{ success: false, error, failedIntents? }`.
1577
3501
  */
1578
- async prepareBatchIntent(requestBody) {
3502
+ async prepareBatchIntent(requestBody, telemetry) {
1579
3503
  try {
1580
3504
  const response = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
1581
3505
  method: "POST",
1582
- headers: { "Content-Type": "application/json" },
3506
+ headers: { "Content-Type": "application/json", ...this.telemetryHeaders(telemetry) },
1583
3507
  body: JSON.stringify(requestBody)
1584
3508
  });
1585
3509
  if (!response.ok) {
@@ -1625,15 +3549,28 @@ var OneAuthClient = class {
1625
3549
  * that hasn't completed yet.
1626
3550
  */
1627
3551
  async getIntentStatus(intentId) {
3552
+ const telemetry = this.createTelemetryOperation("status");
3553
+ this.emitTelemetry("intent.status.started", telemetry, {
3554
+ outcome: "started",
3555
+ attributes: { directStatusCheck: true }
3556
+ });
1628
3557
  try {
1629
3558
  const response = await fetch(
1630
3559
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
1631
3560
  {
1632
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3561
+ headers: {
3562
+ ...this.telemetryHeaders(telemetry),
3563
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3564
+ }
1633
3565
  }
1634
3566
  );
1635
3567
  if (!response.ok) {
1636
3568
  const errorData = await response.json().catch(() => ({}));
3569
+ this.emitTelemetry("status.failed", telemetry, {
3570
+ outcome: "failure",
3571
+ errorCode: "STATUS_FAILED",
3572
+ errorMessage: errorData.error || "Failed to get intent status"
3573
+ });
1637
3574
  return {
1638
3575
  success: false,
1639
3576
  intentId,
@@ -1645,6 +3582,10 @@ var OneAuthClient = class {
1645
3582
  };
1646
3583
  }
1647
3584
  const data = await response.json();
3585
+ this.emitTelemetry("status.succeeded", telemetry, {
3586
+ outcome: data.status === "completed" ? "success" : "started",
3587
+ status: data.status
3588
+ });
1648
3589
  return {
1649
3590
  success: data.status === "completed",
1650
3591
  intentId,
@@ -1653,6 +3594,10 @@ var OneAuthClient = class {
1653
3594
  operationId: data.operationId
1654
3595
  };
1655
3596
  } catch (error) {
3597
+ this.emitTelemetry("status.failed", telemetry, {
3598
+ outcome: "failure",
3599
+ ...describeTelemetryError(error)
3600
+ });
1656
3601
  return {
1657
3602
  success: false,
1658
3603
  intentId,
@@ -1684,6 +3629,9 @@ var OneAuthClient = class {
1684
3629
  * ```
1685
3630
  */
1686
3631
  async getIntentHistory(options) {
3632
+ const telemetry = this.createTelemetryOperation("history", {
3633
+ hasStatusFilter: !!options?.status
3634
+ });
1687
3635
  const queryParams = new URLSearchParams();
1688
3636
  if (options?.limit) queryParams.set("limit", String(options.limit));
1689
3637
  if (options?.offset) queryParams.set("offset", String(options.offset));
@@ -1692,184 +3640,188 @@ var OneAuthClient = class {
1692
3640
  if (options?.to) queryParams.set("to", options.to);
1693
3641
  const url = `${this.config.providerUrl}/api/intent/history${queryParams.toString() ? `?${queryParams}` : ""}`;
1694
3642
  const response = await fetch(url, {
1695
- headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {},
3643
+ headers: {
3644
+ ...this.telemetryHeaders(telemetry),
3645
+ ...this.config.clientId ? { "x-client-id": this.config.clientId } : {}
3646
+ },
1696
3647
  credentials: "include"
1697
3648
  });
1698
3649
  if (!response.ok) {
1699
3650
  const errorData = await response.json().catch(() => ({}));
3651
+ this.emitTelemetry("history.failed", telemetry, {
3652
+ outcome: "failure",
3653
+ errorCode: "HISTORY_FAILED",
3654
+ errorMessage: errorData.error || "Failed to get intent history"
3655
+ });
1700
3656
  throw new Error(errorData.error || "Failed to get intent history");
1701
3657
  }
1702
- return response.json();
3658
+ const result = await response.json();
3659
+ this.emitTelemetry("history.succeeded", telemetry, {
3660
+ outcome: "success",
3661
+ attributes: {
3662
+ total: result.total,
3663
+ returned: result.intents?.length ?? 0
3664
+ }
3665
+ });
3666
+ return result;
1703
3667
  }
1704
3668
  /**
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
- * ```
3669
+ * Forward a raw EIP-1193 request to the iframe so the active EOA connector
3670
+ * can handle it (wallet-connect session or injected wallet). Opens
3671
+ * `/dialog/sign-eoa`, which looks up the wagmi connector and delegates to
3672
+ * `connector.getProvider().request({ method, params })`. The wallet's own
3673
+ * UI is the review screen; the dialog just transports the request.
1736
3674
  */
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) {
3675
+ async requestWithWallet(options) {
3676
+ const job = this.eoaSerialQueue.then(
3677
+ () => this.doRequestWithWallet(options),
3678
+ () => this.doRequestWithWallet(options)
3679
+ );
3680
+ this.eoaSerialQueue = job.then(
3681
+ () => void 0,
3682
+ () => void 0
3683
+ );
3684
+ return job;
3685
+ }
3686
+ async doRequestWithWallet(options) {
3687
+ const dialogOrigin = this.getDialogOrigin();
3688
+ const ensureResult = await this.ensureEoaDialog(options?.theme);
3689
+ if (!ensureResult.ok) {
1784
3690
  return {
1785
3691
  success: false,
1786
- intentId: "",
1787
- status: "failed",
1788
- error: {
1789
- code: "INVALID_TOKEN",
1790
- message: toTokenResult.error || `Unknown toToken: ${options.toToken}`
1791
- }
3692
+ error: { code: "USER_REJECTED", message: "User closed the dialog" }
1792
3693
  };
1793
3694
  }
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
- }
3695
+ const { dialog, iframe } = ensureResult;
3696
+ const requestId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3697
+ const hideDialog = () => {
3698
+ if (dialog.open) dialog.close();
1804
3699
  };
1805
- const toSymbol = formatTokenLabel(
1806
- options.toToken,
1807
- `${options.toToken.slice(0, 6)}...${options.toToken.slice(-4)}`
3700
+ const responsePromise = this.waitForEoaResponse(
3701
+ iframe,
3702
+ hideDialog,
3703
+ requestId,
3704
+ dialogOrigin
1808
3705
  );
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
3706
+ const initPayload = {
3707
+ mode: "iframe",
3708
+ method: options.method,
3709
+ params: options.params,
3710
+ expectedAddress: options.expectedAddress,
3711
+ requestId
1817
3712
  };
1818
- const getDecimals = (symbol, chainId) => {
1819
- try {
1820
- const match = getSupportedTokens(chainId).find(
1821
- (t) => t.symbol.toUpperCase() === symbol.toUpperCase()
3713
+ iframe.contentWindow?.postMessage(
3714
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3715
+ dialogOrigin
3716
+ );
3717
+ const handleReReady = (event) => {
3718
+ if (event.origin !== dialogOrigin) return;
3719
+ if (event.source !== iframe.contentWindow) return;
3720
+ if (event.data?.type === "PASSKEY_READY") {
3721
+ iframe.contentWindow?.postMessage(
3722
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3723
+ dialogOrigin
1822
3724
  );
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
3725
  }
1829
3726
  };
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
3727
+ window.addEventListener("message", handleReReady);
3728
+ const result = await responsePromise;
3729
+ window.removeEventListener("message", handleReReady);
3730
+ hideDialog();
3731
+ return result;
3732
+ }
3733
+ /**
3734
+ * Lazily create the persistent `/dialog/sign-eoa` iframe and reveal it.
3735
+ * Subsequent calls just re-open the existing `<dialog>` so the iframe's
3736
+ * in-page state (notably the WalletConnect SignClient) is preserved
3737
+ * between transactions.
3738
+ */
3739
+ async ensureEoaDialog(theme) {
3740
+ if (this.eoaDialogState) {
3741
+ const { dialog: dialog2, iframe: iframe2 } = this.eoaDialogState;
3742
+ if (!dialog2.open) {
3743
+ dialog2.showModal();
3744
+ }
3745
+ return { ok: true, dialog: dialog2, iframe: iframe2 };
3746
+ }
3747
+ const dialogUrl = this.getDialogUrl();
3748
+ const params = new URLSearchParams({ mode: "iframe" });
3749
+ appendParentOriginParam(params);
3750
+ const themeParams = this.getThemeParams(theme);
3751
+ if (themeParams) {
3752
+ new URLSearchParams(themeParams).forEach((value, key) => {
3753
+ params.set(key, value);
3754
+ });
3755
+ }
3756
+ const signingUrl = `${dialogUrl}/dialog/sign-eoa?${params.toString()}`;
3757
+ const { dialog, iframe, destroy } = this.createModalDialog(signingUrl, {
3758
+ persistent: true
3759
+ });
3760
+ const ready = await this.waitForEoaIframeReady(iframe);
3761
+ if (!ready) {
3762
+ destroy();
3763
+ return { ok: false };
3764
+ }
3765
+ this.eoaDialogState = { dialog, iframe };
3766
+ return { ok: true, dialog, iframe };
3767
+ }
3768
+ waitForEoaIframeReady(iframe) {
3769
+ const dialogOrigin = this.getDialogOrigin();
3770
+ return new Promise((resolve) => {
3771
+ let timeoutId = null;
3772
+ const cleanup = () => {
3773
+ if (timeoutId !== null) clearTimeout(timeoutId);
3774
+ window.removeEventListener("message", handler);
3775
+ };
3776
+ const handler = (event) => {
3777
+ if (event.origin !== dialogOrigin) return;
3778
+ if (event.source !== iframe.contentWindow) return;
3779
+ if (event.data?.type === "PASSKEY_READY") {
3780
+ cleanup();
3781
+ resolve(true);
3782
+ }
3783
+ };
3784
+ window.addEventListener("message", handler);
3785
+ timeoutId = setTimeout(() => {
3786
+ cleanup();
3787
+ resolve(false);
3788
+ }, 1e4);
3789
+ });
3790
+ }
3791
+ waitForEoaResponse(iframe, cleanup, requestId, dialogOrigin) {
3792
+ return new Promise((resolve) => {
3793
+ const handleMessage = (event) => {
3794
+ if (event.origin !== dialogOrigin) return;
3795
+ if (event.source !== iframe.contentWindow) return;
3796
+ const message = event.data;
3797
+ if (message?.type === "PASSKEY_EOA_RESULT" || message?.type === "PASSKEY_EOA_CLOSE") {
3798
+ if (message.requestId !== requestId) return;
3799
+ } else {
3800
+ return;
3801
+ }
3802
+ window.removeEventListener("message", handleMessage);
3803
+ if (message.type === "PASSKEY_EOA_CLOSE") {
3804
+ cleanup();
3805
+ resolve({
3806
+ success: false,
3807
+ error: { code: "USER_REJECTED", message: "User closed the dialog" }
3808
+ });
3809
+ return;
3810
+ }
3811
+ if (message.success) {
3812
+ resolve({ success: true, result: message.data?.result });
3813
+ } else {
3814
+ resolve({
3815
+ success: false,
3816
+ error: message.error || {
3817
+ code: "EOA_REQUEST_FAILED",
3818
+ message: "Wallet request failed"
3819
+ }
3820
+ });
3821
+ }
3822
+ };
3823
+ window.addEventListener("message", handleMessage);
1861
3824
  });
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
3825
  }
1874
3826
  /**
1875
3827
  * Sign an arbitrary message with the user's passkey
@@ -1900,12 +3852,38 @@ var OneAuthClient = class {
1900
3852
  * ```
1901
3853
  */
1902
3854
  async signMessage(options) {
3855
+ const telemetry = this.createTelemetryOperation("sign", {
3856
+ signingMode: "message",
3857
+ hasAccountAddress: !!options.accountAddress
3858
+ });
1903
3859
  const dialogUrl = this.getDialogUrl();
3860
+ const params = new URLSearchParams({ mode: "iframe" });
3861
+ appendParentOriginParam(params);
3862
+ this.appendTelemetryParams(params, telemetry);
1904
3863
  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);
3864
+ if (themeParams) {
3865
+ const themeParsed = new URLSearchParams(themeParams);
3866
+ themeParsed.forEach((value, key) => params.set(key, value));
3867
+ }
3868
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
3869
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
3870
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
3871
+ hidden: requestedBlindSigning
3872
+ });
3873
+ this.emitTelemetry("dialog.opened", telemetry, {
3874
+ outcome: "started",
3875
+ attributes: { blindSigning: requestedBlindSigning }
3876
+ });
3877
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
3878
+ blindSigning: requestedBlindSigning,
3879
+ reveal
3880
+ });
1908
3881
  if (!dialogResult.ready) {
3882
+ this.emitTelemetry("dialog.cancelled", telemetry, {
3883
+ outcome: "cancelled",
3884
+ errorCode: "USER_REJECTED",
3885
+ errorMessage: "User closed the dialog before signing"
3886
+ });
1909
3887
  return {
1910
3888
  success: false,
1911
3889
  error: {
@@ -1921,15 +3899,18 @@ var OneAuthClient = class {
1921
3899
  username: options.username,
1922
3900
  accountAddress: options.accountAddress,
1923
3901
  description: options.description,
1924
- metadata: options.metadata
3902
+ metadata: options.metadata,
3903
+ telemetry: this.telemetryPayload(telemetry)
1925
3904
  };
1926
3905
  dialogResult.sendInit(initPayload);
3906
+ const blindSigning = dialogResult.blindSigning;
3907
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
1927
3908
  const dialogOrigin = this.getDialogOrigin();
1928
3909
  const handleReReady = (event) => {
1929
3910
  if (event.origin !== dialogOrigin) return;
1930
3911
  if (event.data?.type === "PASSKEY_READY") {
1931
3912
  iframe.contentWindow?.postMessage(
1932
- { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
3913
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true, blindSigning },
1933
3914
  dialogOrigin
1934
3915
  );
1935
3916
  }
@@ -1939,6 +3920,10 @@ var OneAuthClient = class {
1939
3920
  window.removeEventListener("message", handleReReady);
1940
3921
  cleanup();
1941
3922
  if (signingResult.success) {
3923
+ this.emitTelemetry("sign.succeeded", telemetry, {
3924
+ outcome: "success",
3925
+ attributes: { signingMode: "message" }
3926
+ });
1942
3927
  return {
1943
3928
  success: true,
1944
3929
  signature: signingResult.signature,
@@ -1947,6 +3932,15 @@ var OneAuthClient = class {
1947
3932
  passkey: signingResult.passkey
1948
3933
  };
1949
3934
  }
3935
+ this.emitTelemetry(
3936
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
3937
+ telemetry,
3938
+ {
3939
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
3940
+ ...describeTelemetryError(signingResult.error),
3941
+ attributes: { signingMode: "message" }
3942
+ }
3943
+ );
1950
3944
  return {
1951
3945
  success: false,
1952
3946
  error: signingResult.error
@@ -1994,6 +3988,11 @@ var OneAuthClient = class {
1994
3988
  * ```
1995
3989
  */
1996
3990
  async signTypedData(options) {
3991
+ const telemetry = this.createTelemetryOperation("sign", {
3992
+ signingMode: "typedData",
3993
+ primaryType: options.primaryType,
3994
+ hasAccountAddress: !!options.accountAddress
3995
+ });
1997
3996
  const signedHash = hashTypedData({
1998
3997
  domain: options.domain,
1999
3998
  types: options.types,
@@ -2001,11 +4000,33 @@ var OneAuthClient = class {
2001
4000
  message: options.message
2002
4001
  });
2003
4002
  const dialogUrl = this.getDialogUrl();
4003
+ const params = new URLSearchParams({ mode: "iframe" });
4004
+ appendParentOriginParam(params);
4005
+ this.appendTelemetryParams(params, telemetry);
2004
4006
  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);
4007
+ if (themeParams) {
4008
+ const themeParsed = new URLSearchParams(themeParams);
4009
+ themeParsed.forEach((value, key) => params.set(key, value));
4010
+ }
4011
+ const signingUrl = `${dialogUrl}/dialog/sign?${params.toString()}`;
4012
+ const requestedBlindSigning = this.shouldRequestBlindSigning(options);
4013
+ const { dialog, iframe, cleanup, reveal } = this.createModalDialog(signingUrl, {
4014
+ hidden: requestedBlindSigning
4015
+ });
4016
+ this.emitTelemetry("dialog.opened", telemetry, {
4017
+ outcome: "started",
4018
+ attributes: { blindSigning: requestedBlindSigning }
4019
+ });
4020
+ const dialogResult = await this.waitForDialogReadyDeferred(dialog, iframe, cleanup, {
4021
+ blindSigning: requestedBlindSigning,
4022
+ reveal
4023
+ });
2008
4024
  if (!dialogResult.ready) {
4025
+ this.emitTelemetry("dialog.cancelled", telemetry, {
4026
+ outcome: "cancelled",
4027
+ errorCode: "USER_REJECTED",
4028
+ errorMessage: "User closed the dialog before signing"
4029
+ });
2009
4030
  return {
2010
4031
  success: false,
2011
4032
  error: {
@@ -2026,15 +4047,18 @@ var OneAuthClient = class {
2026
4047
  challenge: signedHash,
2027
4048
  username: options.username,
2028
4049
  accountAddress: options.accountAddress,
2029
- description: options.description
4050
+ description: options.description,
4051
+ telemetry: this.telemetryPayload(telemetry)
2030
4052
  };
2031
4053
  dialogResult.sendInit(initPayload);
4054
+ const blindSigning = dialogResult.blindSigning;
4055
+ this.emitTelemetry("dialog.ready", telemetry, { outcome: "started" });
2032
4056
  const dialogOrigin = this.getDialogOrigin();
2033
4057
  const handleReReady = (event) => {
2034
4058
  if (event.origin !== dialogOrigin) return;
2035
4059
  if (event.data?.type === "PASSKEY_READY") {
2036
4060
  iframe.contentWindow?.postMessage(
2037
- { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
4061
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true, blindSigning },
2038
4062
  dialogOrigin
2039
4063
  );
2040
4064
  }
@@ -2044,6 +4068,10 @@ var OneAuthClient = class {
2044
4068
  window.removeEventListener("message", handleReReady);
2045
4069
  cleanup();
2046
4070
  if (signingResult.success) {
4071
+ this.emitTelemetry("sign.succeeded", telemetry, {
4072
+ outcome: "success",
4073
+ attributes: { signingMode: "typedData", primaryType: options.primaryType }
4074
+ });
2047
4075
  return {
2048
4076
  success: true,
2049
4077
  signature: signingResult.signature,
@@ -2051,6 +4079,15 @@ var OneAuthClient = class {
2051
4079
  passkey: signingResult.passkey
2052
4080
  };
2053
4081
  }
4082
+ this.emitTelemetry(
4083
+ signingResult.error?.code === "USER_REJECTED" ? "dialog.cancelled" : "sign.failed",
4084
+ telemetry,
4085
+ {
4086
+ outcome: signingResult.error?.code === "USER_REJECTED" ? "cancelled" : "failure",
4087
+ ...describeTelemetryError(signingResult.error),
4088
+ attributes: { signingMode: "typedData", primaryType: options.primaryType }
4089
+ }
4090
+ );
2054
4091
  return {
2055
4092
  success: false,
2056
4093
  error: signingResult.error
@@ -2121,8 +4158,66 @@ var OneAuthClient = class {
2121
4158
  iframe.onload = () => {
2122
4159
  options.onReady?.();
2123
4160
  };
2124
- options.container.appendChild(iframe);
2125
- return iframe;
4161
+ options.container.appendChild(iframe);
4162
+ this.wireViewportInfo(iframe);
4163
+ const host = new URL(dialogUrl);
4164
+ const onResize = (event) => {
4165
+ if (event.source !== iframe.contentWindow) return;
4166
+ if (event.origin !== host.origin) return;
4167
+ if (event.data?.type !== "PASSKEY_RESIZE") return;
4168
+ const h = Number(event.data.height);
4169
+ if (Number.isFinite(h) && h > 0) {
4170
+ iframe.style.height = `${h}px`;
4171
+ }
4172
+ };
4173
+ window.addEventListener("message", onResize);
4174
+ return iframe;
4175
+ }
4176
+ /**
4177
+ * Post `PASSKEY_VIEWPORT_INFO` with the parent's
4178
+ * `window.innerHeight` to the iframe whenever it signals
4179
+ * `PASSKEY_READY`, and re-post on `window.resize` (rAF-debounced)
4180
+ * so the dialog's internal max-height cap follows the user's
4181
+ * actual screen.
4182
+ *
4183
+ * Inside the iframe, `vh`/`dvh` units refer to the iframe's own
4184
+ * height (recursive when the iframe auto-sizes), so the parent is
4185
+ * the only reliable source of "what 75% of the user's screen is".
4186
+ * The dialog stores the value in `dialog-context` and falls back
4187
+ * to auto-grow when the value is absent — so it's safe to call
4188
+ * this even for embedders that don't actually cap (modal mode
4189
+ * uses `fullViewport` which has its own outer cap).
4190
+ */
4191
+ wireViewportInfo(iframe) {
4192
+ const dialogOrigin = this.getDialogOrigin();
4193
+ const post = () => {
4194
+ iframe.contentWindow?.postMessage(
4195
+ { type: "PASSKEY_VIEWPORT_INFO", height: window.innerHeight },
4196
+ dialogOrigin
4197
+ );
4198
+ };
4199
+ const handleMessage = (event) => {
4200
+ if (event.origin !== dialogOrigin) return;
4201
+ if (event.source !== iframe.contentWindow) return;
4202
+ if (event.data?.type === "PASSKEY_READY") {
4203
+ post();
4204
+ }
4205
+ };
4206
+ window.addEventListener("message", handleMessage);
4207
+ let raf = null;
4208
+ const onResize = () => {
4209
+ if (raf !== null) return;
4210
+ raf = requestAnimationFrame(() => {
4211
+ raf = null;
4212
+ post();
4213
+ });
4214
+ };
4215
+ window.addEventListener("resize", onResize);
4216
+ return () => {
4217
+ window.removeEventListener("message", handleMessage);
4218
+ window.removeEventListener("resize", onResize);
4219
+ if (raf !== null) cancelAnimationFrame(raf);
4220
+ };
2126
4221
  }
2127
4222
  /**
2128
4223
  * Listen for a signing result from an embedded (inline) signing iframe.
@@ -2258,6 +4353,7 @@ var OneAuthClient = class {
2258
4353
  body: JSON.stringify({
2259
4354
  ...this.config.clientId && { clientId: this.config.clientId },
2260
4355
  username: options.username,
4356
+ accountAddress: options.accountAddress,
2261
4357
  challenge: options.challenge,
2262
4358
  description: options.description,
2263
4359
  metadata: options.metadata,
@@ -2313,7 +4409,7 @@ var OneAuthClient = class {
2313
4409
  * @returns `true` if the dialog is ready and init was sent; `false` if the
2314
4410
  * dialog was closed or timed out before becoming ready.
2315
4411
  */
2316
- waitForDialogReady(dialog, iframe, cleanup, initMessage) {
4412
+ waitForDialogReady(dialog, iframe, cleanup, initMessage, options = {}) {
2317
4413
  const dialogOrigin = this.getDialogOrigin();
2318
4414
  return new Promise((resolve) => {
2319
4415
  let settled = false;
@@ -2327,11 +4423,14 @@ var OneAuthClient = class {
2327
4423
  const handleMessage = (event) => {
2328
4424
  if (event.origin !== dialogOrigin) return;
2329
4425
  if (event.data?.type === "PASSKEY_READY") {
4426
+ const blindSigning = options.blindSigning === true;
2330
4427
  teardown();
4428
+ if (blindSigning) iframe.focus({ preventScroll: true });
2331
4429
  iframe.contentWindow?.postMessage({
2332
4430
  type: "PASSKEY_INIT",
2333
4431
  ...initMessage,
2334
- fullViewport: true
4432
+ fullViewport: true,
4433
+ blindSigning
2335
4434
  }, dialogOrigin);
2336
4435
  resolve(true);
2337
4436
  } else if (event.data?.type === "PASSKEY_CLOSE") {
@@ -2356,17 +4455,17 @@ var OneAuthClient = class {
2356
4455
  /**
2357
4456
  * Create and open a full-viewport `<dialog>` containing a passkey iframe.
2358
4457
  *
2359
- * The SDK owns only the transparent outer shell; all visible UI (backdrop,
2360
- * card, animations, close button) is rendered by the passkey app inside the
2361
- * iframe. This approach means design changes can be shipped server-side
2362
- * without updating SDK consumers.
4458
+ * The SDK owns the browser `<dialog>` plus a minimal generic preload shell
4459
+ * that appears immediately on click. The passkey iframe owns all branded and
4460
+ * origin-specific UI (TitleBar, trust icon, action cards); the shell is
4461
+ * removed as soon as the iframe reports that its centered card has painted.
2363
4462
  *
2364
4463
  * Lifecycle:
2365
- * 1. A themed "Loading…" overlay is injected and shown immediately while the
2366
- * iframe loads, matching the exact visual structure of the passkey app's
2367
- * TitleBar + IndeterminateLoader so the transition is seamless.
2368
- * 2. When the iframe sends `PASSKEY_RENDERED`, the overlay fades out and
2369
- * the iframe becomes fully visible.
4464
+ * 1. A themed generic preload shell is injected and shown immediately
4465
+ * while the iframe loads. It intentionally contains no TitleBar or
4466
+ * origin chrome so that UI has a single implementation in apps/passkey.
4467
+ * 2. When the iframe sends `PASSKEY_RENDERED`, the overlay is removed and
4468
+ * the iframe becomes fully visible in the same frame.
2370
4469
  * 3. The returned `cleanup` function tears down all event listeners,
2371
4470
  * disconnects the MutationObserver, closes the `<dialog>`, and removes
2372
4471
  * it from the DOM. It is idempotent — safe to call multiple times.
@@ -2378,29 +4477,25 @@ var OneAuthClient = class {
2378
4477
  * restores those as well.
2379
4478
  * - The iframe sandbox includes `allow-popups-to-escape-sandbox` so that
2380
4479
  * 1Password's popup UI can render outside the sandboxed context.
4480
+ * - `allow-downloads` lets the passkey iframe trigger the recovery-key
4481
+ * JSON fallback download without sending sensitive data to the parent.
2381
4482
  *
2382
4483
  * @param url - The full URL (including query params) to load in the iframe.
2383
4484
  * @returns References to the dialog, iframe, and a cleanup function.
2384
4485
  */
2385
- createModalDialog(url) {
4486
+ createModalDialog(url, options) {
2386
4487
  const dialogUrl = this.getDialogUrl();
2387
4488
  const hostUrl = new URL(dialogUrl);
2388
4489
  const urlParams = new URL(url, window.location.href).searchParams;
4490
+ const { background: backdropBackground, blur: backdropBlur } = resolveBackdropStyle(urlParams);
2389
4491
  const themeMode = urlParams.get("theme") || "light";
2390
- const accentColor = urlParams.get("accent") || "#0090ff";
2391
4492
  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;
4493
+ const theme = isDark ? DARK_TOKENS : LIGHT_TOKENS;
2402
4494
  const dialog = document.createElement("dialog");
2403
4495
  dialog.dataset.passkey = "";
4496
+ if (options?.hidden) {
4497
+ dialog.dataset.passkeyHidden = "";
4498
+ }
2404
4499
  dialog.style.opacity = "1";
2405
4500
  dialog.style.background = "transparent";
2406
4501
  document.body.appendChild(dialog);
@@ -2427,83 +4522,159 @@ var OneAuthClient = class {
2427
4522
  dialog[data-passkey]::backdrop {
2428
4523
  background: transparent !important;
2429
4524
  }
2430
- dialog[data-passkey] iframe {
2431
- position: fixed;
2432
- top: 0;
2433
- left: 0;
2434
- width: 100%;
2435
- height: 100%;
2436
- border: 0;
2437
- background-color: transparent;
2438
- color-scheme: normal;
2439
- pointer-events: auto;
2440
- backdrop-filter: blur(8px);
2441
- -webkit-backdrop-filter: blur(8px);
4525
+ dialog[data-passkey][data-passkey-hidden] {
4526
+ width: 1px;
4527
+ height: 1px;
4528
+ max-width: 1px;
4529
+ max-height: 1px;
4530
+ opacity: 0;
4531
+ pointer-events: none;
4532
+ }
4533
+ dialog[data-passkey][data-passkey-hidden] iframe {
4534
+ width: 1px;
4535
+ height: 1px;
4536
+ opacity: 0 !important;
4537
+ pointer-events: none;
4538
+ backdrop-filter: none;
4539
+ -webkit-backdrop-filter: none;
2442
4540
  }
2443
4541
  dialog[data-passkey] [data-passkey-overlay] {
2444
4542
  position: fixed;
2445
- top: 0;
2446
- left: 0;
2447
- width: 100%;
2448
- height: 100%;
4543
+ inset: 0;
2449
4544
  display: flex;
2450
- align-items: flex-start;
4545
+ align-items: center;
2451
4546
  justify-content: center;
2452
- padding-top: 50px;
2453
- background: rgba(0, 0, 0, 0.4);
2454
- backdrop-filter: blur(8px);
2455
- -webkit-backdrop-filter: blur(8px);
4547
+ background: ${backdropBackground};
4548
+ backdrop-filter: blur(${backdropBlur}px);
4549
+ -webkit-backdrop-filter: blur(${backdropBlur}px);
2456
4550
  z-index: 1;
2457
- animation: _1auth-backdrop-in 0.2s ease-out;
4551
+ pointer-events: none;
2458
4552
  }
2459
- @media (max-width: 768px) {
2460
- dialog[data-passkey] [data-passkey-overlay] {
2461
- align-items: flex-end;
2462
- padding-top: 0;
2463
- }
4553
+ dialog[data-passkey][data-passkey-hidden] [data-passkey-overlay] {
4554
+ display: none;
2464
4555
  }
2465
- dialog[data-passkey] [data-passkey-card] {
2466
- width: 340px;
4556
+ dialog[data-passkey] [data-passkey-preload-card] {
4557
+ width: ${LOADING_MODAL_WIDTH}px;
4558
+ max-width: 100%;
2467
4559
  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);
2470
- animation: _1auth-card-in 0.2s cubic-bezier(0.32, 0.72, 0, 1);
2471
- max-height: calc(100dvh - 100px);
4560
+ border-radius: ${LOADING_MODAL_RADIUS}px;
4561
+ background: ${theme.bgPrimary};
4562
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.24), 0 2px 12px rgba(0, 0, 0, 0.12);
4563
+ font-family: ${LOADING_FONT_FAMILY};
4564
+ line-height: ${LOADING_LINE_HEIGHT};
4565
+ -webkit-font-smoothing: antialiased;
4566
+ -moz-osx-font-smoothing: grayscale;
2472
4567
  }
2473
- @media (max-width: 768px) {
2474
- dialog[data-passkey] [data-passkey-card] {
2475
- width: 100%;
2476
- border-bottom-left-radius: 0;
2477
- border-bottom-right-radius: 0;
2478
- animation: _1auth-card-slide 0.3s cubic-bezier(0.32, 0.72, 0, 1);
2479
- }
4568
+ dialog[data-passkey] [data-passkey-preload-body] {
4569
+ box-sizing: border-box;
4570
+ display: flex;
4571
+ width: 100%;
4572
+ flex-direction: column;
4573
+ align-items: center;
4574
+ gap: ${LOADING_BODY_GAP}px;
4575
+ padding: ${LOADING_MODAL_PADDING}px;
4576
+ border-radius: ${LOADING_MODAL_RADIUS}px;
4577
+ background: ${theme.bgPrimary};
4578
+ }
4579
+ dialog[data-passkey] [data-passkey-preload-spinner-wrap] {
4580
+ display: flex;
4581
+ align-items: center;
4582
+ padding: ${LOADING_ICON_PADDING}px;
2480
4583
  }
2481
- @keyframes _1auth-backdrop-in {
2482
- from { opacity: 0; } to { opacity: 1; }
4584
+ dialog[data-passkey] [data-passkey-preload-spinner] {
4585
+ position: relative;
4586
+ width: ${SPINNER_SIZE}px;
4587
+ height: ${SPINNER_SIZE}px;
2483
4588
  }
2484
- @keyframes _1auth-card-in {
2485
- from { opacity: 0; transform: scale(0.96) translateY(8px); }
2486
- to { opacity: 1; transform: scale(1) translateY(0); }
4589
+ dialog[data-passkey] [data-passkey-preload-spinner] svg {
4590
+ position: absolute;
4591
+ inset: 0;
4592
+ width: 100%;
4593
+ height: 100%;
4594
+ color: ${BRAND_PURPLE};
4595
+ animation: _1auth-spin ${SPINNER_SPIN_DURATION_MS}ms linear infinite;
4596
+ }
4597
+ dialog[data-passkey] [data-passkey-preload-copy] {
4598
+ display: flex;
4599
+ width: 100%;
4600
+ flex-direction: column;
4601
+ align-items: center;
4602
+ gap: ${LOADING_TITLE_BLOCK_GAP}px;
4603
+ text-align: center;
4604
+ }
4605
+ dialog[data-passkey] [data-passkey-preload-title] {
4606
+ margin: 0;
4607
+ color: ${theme.textPrimary};
4608
+ font-size: ${LOADING_TITLE_FONT_SIZE}px;
4609
+ font-weight: ${LOADING_TITLE_FONT_WEIGHT};
4610
+ line-height: ${LOADING_LINE_HEIGHT};
4611
+ }
4612
+ dialog[data-passkey] [data-passkey-preload-subtitle] {
4613
+ margin: 0;
4614
+ color: ${theme.textSecondary};
4615
+ font-size: ${LOADING_SUBTITLE_FONT_SIZE}px;
4616
+ font-weight: ${LOADING_SUBTITLE_FONT_WEIGHT};
4617
+ line-height: ${LOADING_LINE_HEIGHT};
2487
4618
  }
2488
- @keyframes _1auth-card-slide {
2489
- from { transform: translate3d(0, 100%, 0); }
2490
- to { transform: translate3d(0, 0, 0); }
4619
+ @media (max-width: 639px) {
4620
+ dialog[data-passkey] [data-passkey-overlay] {
4621
+ align-items: flex-end;
4622
+ }
4623
+ dialog[data-passkey] [data-passkey-preload-card] {
4624
+ width: 100%;
4625
+ border-bottom-right-radius: 0;
4626
+ border-bottom-left-radius: 0;
4627
+ }
4628
+ dialog[data-passkey] [data-passkey-preload-body] {
4629
+ padding-bottom: calc(${LOADING_MODAL_PADDING}px + env(safe-area-inset-bottom));
4630
+ }
2491
4631
  }
2492
4632
  @keyframes _1auth-spin {
2493
4633
  from { transform: rotate(0deg); }
2494
4634
  to { transform: rotate(360deg); }
2495
4635
  }
4636
+ dialog[data-passkey] iframe {
4637
+ position: fixed;
4638
+ top: 0;
4639
+ left: 0;
4640
+ width: 100%;
4641
+ height: 100%;
4642
+ border: 0;
4643
+ background-color: transparent;
4644
+ color-scheme: normal;
4645
+ pointer-events: auto;
4646
+ backdrop-filter: blur(${backdropBlur}px);
4647
+ -webkit-backdrop-filter: blur(${backdropBlur}px);
4648
+ transition: none;
4649
+ }
2496
4650
  `;
2497
4651
  dialog.appendChild(style);
2498
4652
  const overlay = document.createElement("div");
2499
4653
  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());
4654
+ const preloadCard = document.createElement("div");
4655
+ preloadCard.dataset.passkeyPreloadCard = "";
4656
+ const preloadBody = document.createElement("div");
4657
+ preloadBody.dataset.passkeyPreloadBody = "";
4658
+ const spinnerWrap = document.createElement("div");
4659
+ spinnerWrap.dataset.passkeyPreloadSpinnerWrap = "";
4660
+ const spinner = document.createElement("div");
4661
+ spinner.dataset.passkeyPreloadSpinner = "";
4662
+ const spinnerCenter = SPINNER_SIZE / 2;
4663
+ const spinnerGap = SPINNER_CIRCUMFERENCE - SPINNER_ARC;
4664
+ spinner.innerHTML = `<svg viewBox="0 0 ${SPINNER_SIZE} ${SPINNER_SIZE}" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><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>`;
4665
+ spinnerWrap.appendChild(spinner);
4666
+ const preloadCopy = document.createElement("div");
4667
+ preloadCopy.dataset.passkeyPreloadCopy = "";
4668
+ const preloadTitle = document.createElement("p");
4669
+ preloadTitle.dataset.passkeyPreloadTitle = "";
4670
+ preloadTitle.textContent = LOADING_TITLE;
4671
+ const preloadSubtitle = document.createElement("p");
4672
+ preloadSubtitle.dataset.passkeyPreloadSubtitle = "";
4673
+ preloadSubtitle.textContent = LOADING_SUBTITLE;
4674
+ preloadCopy.append(preloadTitle, preloadSubtitle);
4675
+ preloadBody.append(spinnerWrap, preloadCopy);
4676
+ preloadCard.appendChild(preloadBody);
4677
+ overlay.appendChild(preloadCard);
2507
4678
  dialog.appendChild(overlay);
2508
4679
  const iframe = document.createElement("iframe");
2509
4680
  iframe.setAttribute(
@@ -2519,51 +4690,75 @@ var OneAuthClient = class {
2519
4690
  iframe.setAttribute("tabindex", "0");
2520
4691
  iframe.setAttribute(
2521
4692
  "sandbox",
2522
- "allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"
4693
+ "allow-forms allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-downloads"
2523
4694
  );
2524
- iframe.setAttribute("src", url);
2525
4695
  iframe.setAttribute("title", "Passkey");
2526
4696
  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
- let overlayHidden = false;
2537
- const hideOverlay = () => {
2538
- if (overlayHidden) return;
2539
- overlayHidden = true;
4697
+ let revealed = false;
4698
+ let readyFailsafe;
4699
+ const revealIframe = () => {
4700
+ if (revealed) return;
4701
+ revealed = true;
4702
+ clearTimeout(readyFailsafe);
2540
4703
  iframe.style.opacity = "1";
2541
- requestAnimationFrame(() => {
2542
- overlay.style.display = "none";
2543
- });
4704
+ overlay.style.display = "none";
2544
4705
  };
4706
+ const revealFailsafe = setTimeout(revealIframe, 8e3);
2545
4707
  const handleMessage = (event) => {
2546
4708
  if (event.origin !== hostUrl.origin) return;
4709
+ if (event.source !== iframe.contentWindow) return;
2547
4710
  if (event.data?.type === "PASSKEY_RENDERED") {
2548
- hideOverlay();
4711
+ revealIframe();
4712
+ } else if (event.data?.type === "PASSKEY_READY" && !revealed && readyFailsafe === void 0) {
4713
+ readyFailsafe = setTimeout(revealIframe, 1e3);
2549
4714
  }
2550
4715
  if (event.data?.type === "PASSKEY_DISCONNECT") {
2551
4716
  localStorage.removeItem("1auth-user");
2552
4717
  }
2553
4718
  };
2554
4719
  window.addEventListener("message", handleMessage);
4720
+ const iframeUrl = new URL(url, window.location.href);
4721
+ iframeUrl.searchParams.set("fullViewport", "1");
4722
+ iframe.setAttribute("src", iframeUrl.toString());
4723
+ dialog.appendChild(iframe);
4724
+ const viewportInfoCleanup = this.wireViewportInfo(iframe);
4725
+ const inertObserver = new MutationObserver((mutations) => {
4726
+ for (const mutation of mutations) {
4727
+ if (mutation.attributeName === "inert") {
4728
+ dialog.removeAttribute("inert");
4729
+ }
4730
+ }
4731
+ });
4732
+ inertObserver.observe(dialog, { attributes: true });
2555
4733
  const handleEscape = (event) => {
2556
4734
  if (event.key === "Escape") {
2557
4735
  cleanup();
2558
4736
  }
2559
4737
  };
2560
4738
  document.addEventListener("keydown", handleEscape);
2561
- dialog.showModal();
4739
+ const reveal = () => {
4740
+ delete dialog.dataset.passkeyHidden;
4741
+ if (!revealed) {
4742
+ revealed = true;
4743
+ clearTimeout(readyFailsafe);
4744
+ clearTimeout(revealFailsafe);
4745
+ }
4746
+ iframe.style.opacity = "1";
4747
+ overlay.style.display = "none";
4748
+ };
4749
+ if (options?.hidden) {
4750
+ dialog.show();
4751
+ } else {
4752
+ dialog.showModal();
4753
+ }
2562
4754
  let cleanedUp = false;
2563
- const cleanup = () => {
4755
+ const destroy = () => {
2564
4756
  if (cleanedUp) return;
2565
4757
  cleanedUp = true;
4758
+ clearTimeout(revealFailsafe);
4759
+ clearTimeout(readyFailsafe);
2566
4760
  inertObserver.disconnect();
4761
+ viewportInfoCleanup();
2567
4762
  window.removeEventListener("message", handleMessage);
2568
4763
  document.removeEventListener("keydown", handleEscape);
2569
4764
  dialog.close();
@@ -2576,14 +4771,23 @@ var OneAuthClient = class {
2576
4771
  }
2577
4772
  dialog.remove();
2578
4773
  };
2579
- return { dialog, iframe, cleanup };
4774
+ const cleanup = options?.persistent ? () => {
4775
+ if (dialog.open) dialog.close();
4776
+ } : destroy;
4777
+ return { dialog, iframe, cleanup, destroy, reveal };
2580
4778
  }
2581
4779
  /**
2582
4780
  * Listen for the auth result from the modal dialog.
2583
4781
  *
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.
4782
+ * Each modal call generates a per-session nonce that the iframe must
4783
+ * echo back in `PASSKEY_CLOSE` / `PASSKEY_LOGIN_RESULT` /
4784
+ * `PASSKEY_RETRY_POPUP` messages. Without an echo, a queued CLOSE
4785
+ * from the previous iframe could be dispatched to the new modal's
4786
+ * listener (postMessage events tagged with `event.source` from a now-
4787
+ * destroyed iframe still reach the parent's message handler) and
4788
+ * silently cancel the fresh auth attempt. The nonce makes that race
4789
+ * harmless: a stale message that doesn't carry the current modal's
4790
+ * nonce is ignored.
2587
4791
  *
2588
4792
  * Also handles the `PASSKEY_RETRY_POPUP` message: sent by the dialog when a
2589
4793
  * password manager (e.g. Bitwarden) intercepts the WebAuthn call inside the
@@ -2596,25 +4800,42 @@ var OneAuthClient = class {
2596
4800
  * @param cleanup - Idempotent teardown for the modal dialog.
2597
4801
  * @returns An `AuthResult` discriminated union.
2598
4802
  */
2599
- waitForModalAuthResponse(_dialog, iframe, cleanup) {
4803
+ waitForModalAuthResponse(_dialog, iframe, cleanup, requestId) {
2600
4804
  const dialogOrigin = this.getDialogOrigin();
4805
+ const modalNonce = generateModalNonce();
2601
4806
  return new Promise((resolve) => {
2602
- let dialogReady = false;
2603
4807
  const handleMessage = (event) => {
2604
4808
  if (event.origin !== dialogOrigin) return;
2605
4809
  const data = event.data;
4810
+ const matchesSource = event.source === iframe.contentWindow;
4811
+ const matchesRequest = typeof requestId === "string" && data?.requestId === requestId;
4812
+ const matchesNonce = data?.modalNonce === modalNonce;
4813
+ const isCurrentFrameClose = data?.type === "PASSKEY_CLOSE" && matchesSource;
4814
+ if (data?.type === "PASSKEY_CLOSE" && (matchesRequest || matchesNonce || isCurrentFrameClose)) {
4815
+ window.removeEventListener("message", handleMessage);
4816
+ cleanup();
4817
+ resolve({
4818
+ success: false,
4819
+ error: {
4820
+ code: "USER_CANCELLED",
4821
+ message: "Authentication was cancelled"
4822
+ }
4823
+ });
4824
+ return;
4825
+ }
2606
4826
  if (data?.type === "PASSKEY_READY") {
2607
- dialogReady = true;
2608
4827
  iframe.contentWindow?.postMessage({
2609
4828
  type: "PASSKEY_INIT",
2610
4829
  mode: "iframe",
2611
- fullViewport: true
4830
+ fullViewport: true,
4831
+ modalNonce
2612
4832
  }, dialogOrigin);
2613
4833
  return;
2614
4834
  }
2615
- if (!dialogReady && data?.type === "PASSKEY_CLOSE") {
2616
- return;
2617
- }
4835
+ const hasModalNonce = typeof data?.modalNonce === "string";
4836
+ const isCurrentDialogMessage = matchesSource || matchesNonce || !hasModalNonce;
4837
+ if (!isCurrentDialogMessage) return;
4838
+ if (data?.modalNonce && data.modalNonce !== modalNonce) return;
2618
4839
  if (data?.type === "PASSKEY_LOGIN_RESULT") {
2619
4840
  window.removeEventListener("message", handleMessage);
2620
4841
  cleanup();
@@ -2625,7 +4846,8 @@ var OneAuthClient = class {
2625
4846
  id: data.data?.user?.id,
2626
4847
  username: data.data?.username,
2627
4848
  address: data.data?.address
2628
- }
4849
+ },
4850
+ signerType: data.data?.signerType ?? "passkey"
2629
4851
  });
2630
4852
  } else {
2631
4853
  resolve({
@@ -2638,16 +4860,6 @@ var OneAuthClient = class {
2638
4860
  cleanup();
2639
4861
  const popupUrl = data.data?.url?.replace("mode=iframe", "mode=popup") || `${this.getDialogUrl()}/dialog/auth?mode=popup${this.config.clientId ? `&clientId=${this.config.clientId}` : ""}`;
2640
4862
  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
4863
  }
2652
4864
  };
2653
4865
  window.addEventListener("message", handleMessage);
@@ -2688,7 +4900,8 @@ var OneAuthClient = class {
2688
4900
  id: data.data?.user?.id,
2689
4901
  username: data.data?.username,
2690
4902
  address: data.data?.address
2691
- }
4903
+ },
4904
+ signerType: data.data?.signerType ?? "passkey"
2692
4905
  });
2693
4906
  } else {
2694
4907
  resolve({
@@ -2712,63 +4925,6 @@ var OneAuthClient = class {
2712
4925
  window.addEventListener("message", handleMessage);
2713
4926
  });
2714
4927
  }
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
4928
  /**
2773
4929
  * Listen for the connect result from the connect dialog.
2774
4930
  *
@@ -2827,48 +4983,101 @@ var OneAuthClient = class {
2827
4983
  });
2828
4984
  }
2829
4985
  /**
2830
- * Listen for the consent result from the consent dialog.
4986
+ * Listen for grant-permission results and bridge sponsorship token requests.
2831
4987
  *
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.
2836
- *
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`).
4988
+ * The grant iframe owns the passkey ceremony and install intent submission.
4989
+ * Extension-token minting stays in the app origin, so the SDK exposes a
4990
+ * narrow postMessage round-trip that never gives the iframe app secrets.
2840
4991
  *
2841
4992
  * @param _dialog - Unused; kept for signature consistency.
2842
- * @param _iframe - Unused; kept for signature consistency.
4993
+ * @param iframe - The grant iframe that receives extension-token responses.
2843
4994
  * @param cleanup - Idempotent teardown for the modal dialog.
2844
- * @returns A `RequestConsentResult` discriminated union.
4995
+ * @param sponsor - Whether this grant flow should request an extension token.
4996
+ * @returns A `GrantPermissionsResult` discriminated union.
2845
4997
  */
2846
- waitForConsentResponse(_dialog, _iframe, cleanup) {
4998
+ waitForGrantPermissionResponse(dialog, iframe, cleanup, sponsor, initPayload) {
2847
4999
  const dialogOrigin = this.getDialogOrigin();
2848
5000
  return new Promise((resolve) => {
2849
- const handleMessage = (event) => {
5001
+ let readySeen = false;
5002
+ const teardown = () => {
5003
+ clearTimeout(readyTimeout);
5004
+ window.removeEventListener("message", handleMessage);
5005
+ dialog.removeEventListener("close", handleClose);
5006
+ };
5007
+ const postInit = () => {
5008
+ iframe.contentWindow?.postMessage(
5009
+ { type: "PASSKEY_INIT", ...initPayload, fullViewport: true },
5010
+ dialogOrigin
5011
+ );
5012
+ };
5013
+ const handleClose = () => {
5014
+ teardown();
5015
+ resolve({
5016
+ success: false,
5017
+ error: {
5018
+ code: "USER_CANCELLED",
5019
+ message: "User closed the dialog"
5020
+ }
5021
+ });
5022
+ };
5023
+ const handleMessage = async (event) => {
2850
5024
  if (event.origin !== dialogOrigin) return;
2851
5025
  const data = event.data;
2852
- if (data?.type === "PASSKEY_CONSENT_RESULT") {
2853
- window.removeEventListener("message", handleMessage);
2854
- cleanup();
2855
- if (data.success) {
2856
- resolve({
2857
- success: true,
2858
- data: data.data,
2859
- grantedAt: data.data?.grantedAt
2860
- });
2861
- } else {
2862
- resolve({
5026
+ if (data?.type === "PASSKEY_READY") {
5027
+ readySeen = true;
5028
+ clearTimeout(readyTimeout);
5029
+ postInit();
5030
+ return;
5031
+ }
5032
+ if (data?.type === "PASSKEY_GRANT_EXTENSION_TOKEN_REQUEST") {
5033
+ const requestId = data.requestId;
5034
+ const intentOp = data.intentOp;
5035
+ const postResponse = (payload) => {
5036
+ iframe.contentWindow?.postMessage(
5037
+ { type: "PASSKEY_GRANT_EXTENSION_TOKEN_RESPONSE", requestId, ...payload },
5038
+ dialogOrigin
5039
+ );
5040
+ };
5041
+ if (!requestId || !intentOp) {
5042
+ postResponse({ success: false, error: "Invalid extension token request" });
5043
+ return;
5044
+ }
5045
+ if (!sponsor) {
5046
+ postResponse({ success: true });
5047
+ return;
5048
+ }
5049
+ if (!this.sponsorship) {
5050
+ postResponse({ success: false, error: MISSING_APP_CREDENTIALS_MESSAGE });
5051
+ return;
5052
+ }
5053
+ try {
5054
+ const token = await this.sponsorship.getExtensionToken(intentOp);
5055
+ postResponse({ success: true, token });
5056
+ } catch (err) {
5057
+ postResponse({
2863
5058
  success: false,
2864
- error: data.error ?? {
2865
- code: "USER_REJECTED",
2866
- message: "User denied the consent request"
2867
- }
5059
+ error: err instanceof Error ? err.message : String(err)
2868
5060
  });
2869
5061
  }
5062
+ return;
5063
+ }
5064
+ if (data?.type === "PASSKEY_GRANT_PERMISSION_RESULT") {
5065
+ if (data.success) {
5066
+ teardown();
5067
+ resolve({ success: true, ...data.data ?? {} });
5068
+ return;
5069
+ }
5070
+ teardown();
5071
+ cleanup();
5072
+ resolve({
5073
+ success: false,
5074
+ error: data.error ?? data.data?.error ?? {
5075
+ code: "GRANT_PERMISSION_FAILED",
5076
+ message: "Permission grant failed"
5077
+ }
5078
+ });
2870
5079
  } else if (data?.type === "PASSKEY_CLOSE") {
2871
- window.removeEventListener("message", handleMessage);
5080
+ teardown();
2872
5081
  cleanup();
2873
5082
  resolve({
2874
5083
  success: false,
@@ -2879,7 +5088,20 @@ var OneAuthClient = class {
2879
5088
  });
2880
5089
  }
2881
5090
  };
5091
+ const readyTimeout = setTimeout(() => {
5092
+ if (readySeen) return;
5093
+ teardown();
5094
+ cleanup();
5095
+ resolve({
5096
+ success: false,
5097
+ error: {
5098
+ code: "USER_CANCELLED",
5099
+ message: "User closed the dialog"
5100
+ }
5101
+ });
5102
+ }, 1e4);
2882
5103
  window.addEventListener("message", handleMessage);
5104
+ dialog.addEventListener("close", handleClose);
2883
5105
  });
2884
5106
  }
2885
5107
  /**
@@ -3043,6 +5265,65 @@ var OneAuthClient = class {
3043
5265
  }
3044
5266
  };
3045
5267
 
5268
+ // src/permissions.ts
5269
+ function definePermissions(config) {
5270
+ const { name, ...permission } = config;
5271
+ return {
5272
+ permissions: [permission],
5273
+ contracts: [
5274
+ {
5275
+ address: config.address,
5276
+ name,
5277
+ abi: config.abi
5278
+ }
5279
+ ]
5280
+ };
5281
+ }
5282
+
5283
+ // src/crossChainPermissions.ts
5284
+ import { isAddress } from "viem";
5285
+ function toUnixSecondsBigint(input) {
5286
+ if (typeof input === "bigint") return input;
5287
+ return BigInt(Math.floor(input.getTime() / 1e3));
5288
+ }
5289
+ function resolveTokenForChain(token, chainId) {
5290
+ return isAddress(token, { strict: false }) ? token : getTokenAddress(token, chainId);
5291
+ }
5292
+ function createCrossChainPermission(input) {
5293
+ const fromLegs = Array.isArray(input.from) ? input.from : [input.from];
5294
+ const toLegs = Array.isArray(input.to) ? input.to : [input.to];
5295
+ if (fromLegs.length === 0) {
5296
+ throw new Error("createCrossChainPermission: `from` must contain at least one leg");
5297
+ }
5298
+ if (toLegs.length === 0) {
5299
+ throw new Error("createCrossChainPermission: `to` must contain at least one leg");
5300
+ }
5301
+ const validUntil = input.validUntil !== void 0 ? toUnixSecondsBigint(input.validUntil) : void 0;
5302
+ const validAfter = input.validAfter !== void 0 ? toUnixSecondsBigint(input.validAfter) : void 0;
5303
+ if (validUntil !== void 0 && validAfter !== void 0 && validAfter > validUntil) {
5304
+ throw new Error(
5305
+ `createCrossChainPermission: validAfter (${validAfter}) is greater than validUntil (${validUntil})`
5306
+ );
5307
+ }
5308
+ return {
5309
+ from: fromLegs.map((leg) => ({
5310
+ chain: leg.chain,
5311
+ token: resolveTokenForChain(leg.token, leg.chain.id),
5312
+ maxAmount: leg.maxAmount
5313
+ })),
5314
+ to: toLegs.map((leg) => ({
5315
+ chain: leg.chain,
5316
+ token: resolveTokenForChain(leg.token, leg.chain.id),
5317
+ recipient: leg.recipient
5318
+ })),
5319
+ validUntil,
5320
+ validAfter,
5321
+ fillDeadline: input.fillDeadline,
5322
+ recipientIsAccount: !input.allowRecipientNotAccount,
5323
+ settlementLayers: input.settlementLayers
5324
+ };
5325
+ }
5326
+
3046
5327
  // src/account.ts
3047
5328
  import {
3048
5329
  bytesToString,
@@ -3068,7 +5349,8 @@ function createPasskeyAccount(client, params) {
3068
5349
  address,
3069
5350
  signMessage: async ({ message }) => {
3070
5351
  const result = await client.signMessage({
3071
- username,
5352
+ username: username || void 0,
5353
+ accountAddress: address,
3072
5354
  message: normalizeMessage(message)
3073
5355
  });
3074
5356
  if (!result.success || !result.signature) {
@@ -3103,7 +5385,8 @@ function createPasskeyAccount(client, params) {
3103
5385
  ])
3104
5386
  );
3105
5387
  const result = await client.signTypedData({
3106
- username,
5388
+ username: username || void 0,
5389
+ accountAddress: address,
3107
5390
  domain,
3108
5391
  types: normalizedTypes,
3109
5392
  primaryType: typedData.primaryType,
@@ -3117,29 +5400,37 @@ function createPasskeyAccount(client, params) {
3117
5400
  });
3118
5401
  return {
3119
5402
  ...account,
3120
- username
5403
+ ...username ? { username } : {},
5404
+ accountAddress: address
3121
5405
  };
3122
5406
  }
3123
5407
 
3124
5408
  // src/walletClient/index.ts
3125
5409
  import {
3126
5410
  createWalletClient,
3127
- hashMessage,
5411
+ hashMessage as hashMessage2,
3128
5412
  hashTypedData as hashTypedData2
3129
5413
  } from "viem";
3130
5414
  import { toAccount as toAccount2 } from "viem/accounts";
3131
5415
  function createPasskeyWalletClient(config) {
3132
- const resolvedUsername = config.username ?? config.accountAddress;
5416
+ const identity = {
5417
+ ...config.username ? { username: config.username } : {},
5418
+ accountAddress: config.accountAddress
5419
+ };
3133
5420
  const provider = new OneAuthClient({
3134
5421
  providerUrl: config.providerUrl,
3135
5422
  clientId: config.clientId,
3136
- dialogUrl: config.dialogUrl
5423
+ dialogUrl: config.dialogUrl,
5424
+ theme: config.theme,
5425
+ blind_signing: config.blind_signing,
5426
+ testnets: config.testnets,
5427
+ sponsorship: config.sponsorship
3137
5428
  });
3138
5429
  const signMessageImpl = async (message) => {
3139
- const hash = hashMessage(message);
5430
+ const hash = hashMessage2(message);
3140
5431
  const result = await provider.signWithModal({
3141
5432
  challenge: hash,
3142
- username: resolvedUsername,
5433
+ ...identity,
3143
5434
  description: "Sign message",
3144
5435
  transaction: {
3145
5436
  actions: [
@@ -3170,7 +5461,7 @@ function createPasskeyWalletClient(config) {
3170
5461
  const hash = hashCalls(calls);
3171
5462
  const result = await provider.signWithModal({
3172
5463
  challenge: hash,
3173
- username: resolvedUsername,
5464
+ ...identity,
3174
5465
  description: "Sign transaction",
3175
5466
  transaction: buildTransactionReview(calls)
3176
5467
  });
@@ -3186,7 +5477,7 @@ function createPasskeyWalletClient(config) {
3186
5477
  const hash = hashTypedData2(typedData);
3187
5478
  const result = await provider.signWithModal({
3188
5479
  challenge: hash,
3189
- username: resolvedUsername,
5480
+ ...identity,
3190
5481
  description: "Sign typed data",
3191
5482
  transaction: {
3192
5483
  actions: [
@@ -3213,10 +5504,12 @@ function createPasskeyWalletClient(config) {
3213
5504
  data: call.data || "0x",
3214
5505
  value: call.value !== void 0 ? call.value.toString() : "0",
3215
5506
  label: call.label,
3216
- sublabel: call.sublabel
5507
+ sublabel: call.sublabel,
5508
+ icon: call.icon,
5509
+ abi: call.abi
3217
5510
  }));
3218
5511
  return {
3219
- username: resolvedUsername,
5512
+ ...config.username ? { username: config.username } : {},
3220
5513
  accountAddress: config.accountAddress,
3221
5514
  targetChain,
3222
5515
  calls: intentCalls
@@ -3246,11 +5539,9 @@ function createPasskeyWalletClient(config) {
3246
5539
  value: transaction.value
3247
5540
  }
3248
5541
  ];
3249
- const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
3250
5542
  const intentPayload = await buildIntentPayload(calls, targetChain);
3251
5543
  const result = await provider.sendIntent({
3252
5544
  ...intentPayload,
3253
- closeOn,
3254
5545
  waitForHash: config.waitForHash ?? true,
3255
5546
  hashTimeoutMs: config.hashTimeoutMs,
3256
5547
  hashIntervalMs: config.hashIntervalMs
@@ -3265,14 +5556,12 @@ function createPasskeyWalletClient(config) {
3265
5556
  */
3266
5557
  async sendCalls(params) {
3267
5558
  const { calls, chainId: targetChain, tokenRequests, sourceAssets, sourceChainId } = params;
3268
- const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
3269
5559
  const intentPayload = await buildIntentPayload(calls, targetChain, { tokenRequests, sourceAssets });
3270
5560
  const result = await provider.sendIntent({
3271
5561
  ...intentPayload,
3272
5562
  tokenRequests,
3273
5563
  sourceAssets,
3274
5564
  sourceChainId,
3275
- closeOn,
3276
5565
  waitForHash: config.waitForHash ?? true,
3277
5566
  hashTimeoutMs: config.hashTimeoutMs,
3278
5567
  hashIntervalMs: config.hashIntervalMs
@@ -3303,21 +5592,36 @@ function useBatchQueue() {
3303
5592
  function generateId() {
3304
5593
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
3305
5594
  }
3306
- function getStorageKey(username) {
3307
- return username ? `1auth_batch_queue_${username}` : "1auth_batch_queue_anonymous";
5595
+ function getStorageKey(identity) {
5596
+ const key = identity?.accountAddress || identity?.username;
5597
+ return key ? `1auth_batch_queue_${key.toLowerCase()}` : "1auth_batch_queue_anonymous";
5598
+ }
5599
+ function resolveIdentity(passedIdentity, providerIdentity) {
5600
+ if (typeof passedIdentity === "string") {
5601
+ return { username: passedIdentity, accountAddress: providerIdentity.accountAddress };
5602
+ }
5603
+ return {
5604
+ username: passedIdentity?.username ?? providerIdentity.username,
5605
+ accountAddress: passedIdentity?.accountAddress ?? providerIdentity.accountAddress
5606
+ };
3308
5607
  }
3309
5608
  function BatchQueueProvider({
3310
5609
  client,
3311
5610
  username,
5611
+ accountAddress,
3312
5612
  children
3313
5613
  }) {
3314
5614
  const [queue, setQueue] = React.useState([]);
3315
5615
  const [isExpanded, setExpanded] = React.useState(false);
3316
5616
  const [isSigning, setIsSigning] = React.useState(false);
3317
5617
  const [animationTrigger, setAnimationTrigger] = React.useState(0);
5618
+ const providerIdentity = React.useMemo(
5619
+ () => ({ username, accountAddress }),
5620
+ [username, accountAddress]
5621
+ );
3318
5622
  const batchChainId = queue.length > 0 ? queue[0].targetChain : null;
3319
5623
  React.useEffect(() => {
3320
- const storageKey = getStorageKey(username);
5624
+ const storageKey = getStorageKey(providerIdentity);
3321
5625
  try {
3322
5626
  const stored = localStorage.getItem(storageKey);
3323
5627
  if (stored) {
@@ -3330,9 +5634,9 @@ function BatchQueueProvider({
3330
5634
  }
3331
5635
  } catch {
3332
5636
  }
3333
- }, [username]);
5637
+ }, [providerIdentity]);
3334
5638
  React.useEffect(() => {
3335
- const storageKey = getStorageKey(username);
5639
+ const storageKey = getStorageKey(providerIdentity);
3336
5640
  try {
3337
5641
  if (queue.length > 0) {
3338
5642
  localStorage.setItem(storageKey, JSON.stringify(queue));
@@ -3341,7 +5645,7 @@ function BatchQueueProvider({
3341
5645
  }
3342
5646
  } catch {
3343
5647
  }
3344
- }, [queue, username]);
5648
+ }, [queue, providerIdentity]);
3345
5649
  const addToBatch = React.useCallback((call, targetChain) => {
3346
5650
  if (batchChainId !== null && batchChainId !== targetChain) {
3347
5651
  return {
@@ -3366,7 +5670,7 @@ function BatchQueueProvider({
3366
5670
  setQueue([]);
3367
5671
  setExpanded(false);
3368
5672
  }, []);
3369
- const signAll = React.useCallback(async (username2) => {
5673
+ const signAll = React.useCallback(async (identity) => {
3370
5674
  if (queue.length === 0) {
3371
5675
  return {
3372
5676
  success: false,
@@ -3378,12 +5682,25 @@ function BatchQueueProvider({
3378
5682
  }
3379
5683
  };
3380
5684
  }
5685
+ const signer = resolveIdentity(identity, providerIdentity);
5686
+ if (!signer.username && !signer.accountAddress) {
5687
+ return {
5688
+ success: false,
5689
+ intentId: "",
5690
+ status: "failed",
5691
+ error: {
5692
+ code: "MISSING_IDENTITY",
5693
+ message: "Batch signing requires an accountAddress or username"
5694
+ }
5695
+ };
5696
+ }
3381
5697
  const targetChain = queue[0].targetChain;
3382
5698
  const calls = queue.map((item) => item.call);
3383
5699
  setIsSigning(true);
3384
5700
  try {
3385
5701
  const result = await client.sendIntent({
3386
- username: username2,
5702
+ username: signer.username,
5703
+ accountAddress: signer.accountAddress,
3387
5704
  targetChain,
3388
5705
  calls
3389
5706
  });
@@ -3394,7 +5711,7 @@ function BatchQueueProvider({
3394
5711
  } finally {
3395
5712
  setIsSigning(false);
3396
5713
  }
3397
- }, [queue, client, clearBatch]);
5714
+ }, [queue, client, clearBatch, providerIdentity]);
3398
5715
  const value = {
3399
5716
  queue,
3400
5717
  batchChainId,
@@ -3712,28 +6029,16 @@ function BatchQueueWidget({ onSignAll }) {
3712
6029
  ] })
3713
6030
  ] });
3714
6031
  }
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
6032
  export {
3730
6033
  BatchQueueProvider,
3731
6034
  BatchQueueWidget,
3732
6035
  ETHEREUM_MESSAGE_PREFIX,
3733
6036
  OneAuthClient,
6037
+ createCrossChainPermission,
3734
6038
  createOneAuthProvider,
3735
6039
  createPasskeyAccount,
3736
6040
  createPasskeyWalletClient,
6041
+ definePermissions,
3737
6042
  encodeWebAuthnSignature,
3738
6043
  getAllSupportedChainsAndTokens,
3739
6044
  getChainById,
@@ -3748,7 +6053,7 @@ export {
3748
6053
  getTokenDecimals,
3749
6054
  getTokenSymbol,
3750
6055
  hashCalls,
3751
- hashMessage2 as hashMessage,
6056
+ hashMessage,
3752
6057
  isTestnet,
3753
6058
  isTokenAddressSupported,
3754
6059
  resolveTokenAddress,