@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.
- package/README.md +171 -12
- package/dist/chunk-GUAI55LL.mjs +179 -0
- package/dist/chunk-GUAI55LL.mjs.map +1 -0
- package/dist/chunk-IHBVEU33.mjs +20 -0
- package/dist/chunk-IHBVEU33.mjs.map +1 -0
- package/dist/chunk-IIACVHR3.mjs +28 -0
- package/dist/chunk-IIACVHR3.mjs.map +1 -0
- package/dist/chunk-N6KE5CII.mjs +72 -0
- package/dist/chunk-N6KE5CII.mjs.map +1 -0
- package/dist/{chunk-SXISYG2P.mjs → chunk-THKG3FAG.mjs} +137 -255
- package/dist/chunk-THKG3FAG.mjs.map +1 -0
- package/dist/{client-BrMrhetG.d.mts → client-B_CzDa_I.d.ts} +360 -857
- package/dist/{client-BrMrhetG.d.ts → client-F4DnFM8d.d.mts} +360 -857
- package/dist/headless.d.mts +109 -0
- package/dist/headless.d.ts +109 -0
- package/dist/headless.js +467 -0
- package/dist/headless.js.map +1 -0
- package/dist/headless.mjs +382 -0
- package/dist/headless.mjs.map +1 -0
- package/dist/index.d.mts +96 -144
- package/dist/index.d.ts +96 -144
- package/dist/index.js +3212 -756
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3010 -705
- package/dist/index.mjs.map +1 -1
- package/dist/{provider-CDl9wYEc.d.mts → provider-Cd7Ip5L-.d.ts} +6 -5
- package/dist/{provider-Dgv533YQ.d.ts → provider-IvYXPMpk.d.mts} +6 -5
- package/dist/react.d.mts +42 -2
- package/dist/react.d.ts +42 -2
- package/dist/react.js +92 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +66 -1
- package/dist/react.mjs.map +1 -1
- package/dist/server.d.mts +118 -0
- package/dist/server.d.ts +118 -0
- package/dist/server.js +356 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +282 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-U_dwxbtS.d.mts +1488 -0
- package/dist/types-U_dwxbtS.d.ts +1488 -0
- package/dist/verify-BLgZzwmJ.d.ts +150 -0
- package/dist/verify-C8-a5c3K.d.mts +150 -0
- package/dist/wagmi.d.mts +5 -2
- package/dist/wagmi.d.ts +5 -2
- package/dist/wagmi.js +138 -43
- package/dist/wagmi.js.map +1 -1
- package/dist/wagmi.mjs +3 -1
- package/dist/wagmi.mjs.map +1 -1
- package/package.json +15 -2
- package/dist/chunk-SXISYG2P.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
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
|
-
|
|
25
|
-
var POPUP_WIDTH = 450;
|
|
162
|
+
var POPUP_WIDTH = LOADING_MODAL_WIDTH;
|
|
26
163
|
var POPUP_HEIGHT = 600;
|
|
27
|
-
var DEFAULT_EMBED_WIDTH =
|
|
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
|
-
|
|
79
|
-
|
|
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:
|
|
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
|
|
841
|
+
const authPath = this.getAuthDialogPath(options?.flow);
|
|
842
|
+
const url = `${dialogUrl}${authPath}?${params.toString()}`;
|
|
218
843
|
const { dialog, iframe, cleanup } = this.createModalDialog(url);
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
319
|
-
* This is a read-only check — no dialog is shown.
|
|
1074
|
+
* Open the account-recovery backup flow directly.
|
|
320
1075
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
391
|
-
|
|
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: {
|
|
1184
|
+
error: {
|
|
1185
|
+
code: "INVALID_OPTIONS",
|
|
1186
|
+
message: "Either username or accountAddress is required"
|
|
1187
|
+
}
|
|
395
1188
|
};
|
|
396
1189
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
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: {
|
|
1200
|
+
error: {
|
|
1201
|
+
code: "INVALID_OPTIONS",
|
|
1202
|
+
message: "targetChains, sessionKeyAddress, and permissions are required"
|
|
1203
|
+
}
|
|
402
1204
|
};
|
|
403
1205
|
}
|
|
404
|
-
if (!
|
|
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: {
|
|
1214
|
+
error: {
|
|
1215
|
+
code: "MISSING_APP_CREDENTIALS",
|
|
1216
|
+
message: MISSING_APP_CREDENTIALS_MESSAGE
|
|
1217
|
+
}
|
|
408
1218
|
};
|
|
409
1219
|
}
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
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:
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
|
432
|
-
const { dialog, iframe, cleanup } = this.createModalDialog(
|
|
433
|
-
const
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
453
|
-
* 3.
|
|
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
|
-
*
|
|
457
|
-
*
|
|
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
|
|
477
|
-
|
|
478
|
-
mode: "iframe"
|
|
1376
|
+
const telemetry = this.createTelemetryOperation("authenticate", {
|
|
1377
|
+
hasChallenge: !!options?.challenge
|
|
479
1378
|
});
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
await this.
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
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
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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
|
-
|
|
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
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
|
1806
|
-
|
|
1807
|
-
|
|
3700
|
+
const responsePromise = this.waitForEoaResponse(
|
|
3701
|
+
iframe,
|
|
3702
|
+
hideDialog,
|
|
3703
|
+
requestId,
|
|
3704
|
+
dialogOrigin
|
|
1808
3705
|
);
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
-
|
|
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
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
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
|
|
2366
|
-
* iframe loads
|
|
2367
|
-
*
|
|
2368
|
-
* 2. When the iframe sends `PASSKEY_RENDERED`, the overlay
|
|
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
|
|
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]
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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
|
-
|
|
2446
|
-
left: 0;
|
|
2447
|
-
width: 100%;
|
|
2448
|
-
height: 100%;
|
|
4543
|
+
inset: 0;
|
|
2449
4544
|
display: flex;
|
|
2450
|
-
align-items:
|
|
4545
|
+
align-items: center;
|
|
2451
4546
|
justify-content: center;
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
backdrop-filter: blur(
|
|
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
|
-
|
|
4551
|
+
pointer-events: none;
|
|
2458
4552
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
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:
|
|
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:
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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
|
-
|
|
2482
|
-
|
|
4584
|
+
dialog[data-passkey] [data-passkey-preload-spinner] {
|
|
4585
|
+
position: relative;
|
|
4586
|
+
width: ${SPINNER_SIZE}px;
|
|
4587
|
+
height: ${SPINNER_SIZE}px;
|
|
2483
4588
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
@
|
|
2489
|
-
|
|
2490
|
-
|
|
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
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
*
|
|
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
|
-
|
|
2616
|
-
|
|
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
|
|
4986
|
+
* Listen for grant-permission results and bridge sponsorship token requests.
|
|
2831
4987
|
*
|
|
2832
|
-
* The
|
|
2833
|
-
*
|
|
2834
|
-
*
|
|
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
|
|
4993
|
+
* @param iframe - The grant iframe that receives extension-token responses.
|
|
2843
4994
|
* @param cleanup - Idempotent teardown for the modal dialog.
|
|
2844
|
-
* @
|
|
4995
|
+
* @param sponsor - Whether this grant flow should request an extension token.
|
|
4996
|
+
* @returns A `GrantPermissionsResult` discriminated union.
|
|
2845
4997
|
*/
|
|
2846
|
-
|
|
4998
|
+
waitForGrantPermissionResponse(dialog, iframe, cleanup, sponsor, initPayload) {
|
|
2847
4999
|
const dialogOrigin = this.getDialogOrigin();
|
|
2848
5000
|
return new Promise((resolve) => {
|
|
2849
|
-
|
|
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 === "
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
5430
|
+
const hash = hashMessage2(message);
|
|
3140
5431
|
const result = await provider.signWithModal({
|
|
3141
5432
|
challenge: hash,
|
|
3142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
3307
|
-
|
|
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(
|
|
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
|
-
}, [
|
|
5637
|
+
}, [providerIdentity]);
|
|
3334
5638
|
React.useEffect(() => {
|
|
3335
|
-
const storageKey = getStorageKey(
|
|
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,
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
6056
|
+
hashMessage,
|
|
3752
6057
|
isTestnet,
|
|
3753
6058
|
isTokenAddressSupported,
|
|
3754
6059
|
resolveTokenAddress,
|