@auth-link/web 1.1.0 → 3.0.0

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/dist/react.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use client";
3
3
 
4
4
  // src/react.tsx
5
- import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useEffect, useRef } from "react";
6
6
 
7
7
  // node_modules/qrcode-generator/dist/qrcode.mjs
8
8
  var qrcode = function(typeNumber, errorCorrectionLevel) {
@@ -1639,195 +1639,36 @@ var qrcode_default = qrcode;
1639
1639
  var stringToBytes = qrcode.stringToBytes;
1640
1640
 
1641
1641
  // src/core.ts
1642
- var token = /^[A-Za-z0-9_-]{43}$/;
1643
1642
  var AuthLinkError = class extends Error {
1643
+ code;
1644
1644
  constructor(code) {
1645
1645
  super(code);
1646
- this.code = code;
1647
1646
  this.name = "AuthLinkError";
1647
+ this.code = code;
1648
1648
  }
1649
- code;
1650
1649
  };
1651
- function fail(code) {
1652
- throw new AuthLinkError(code);
1650
+ function deadline(value) {
1651
+ const at = typeof value === "string" ? Date.parse(value) : NaN;
1652
+ if (!Number.isFinite(at) || at <= Date.now()) throw new AuthLinkError("INVALID_EXPIRY");
1653
+ return at;
1653
1654
  }
1654
- function object(value) {
1655
- if (!value || typeof value !== "object" || Array.isArray(value)) fail("INVALID_RESPONSE");
1656
- return value;
1657
- }
1658
- function validateDisplayCode(value) {
1659
- const m = object(value);
1660
- const span = Date.parse(String(m.validUntil)) - Date.parse(String(m.serverTime));
1661
- if (typeof m.value !== "string" || !/^\d{6}$/.test(m.value) || m.periodSeconds !== 30 || !Number.isFinite(span) || span <= 0 || span > 3e4 || "seed" in m || "secret" in m || "key" in m) fail("INVALID_DISPLAY_CODE");
1662
- return { value: m.value, serverTime: String(m.serverTime), validUntil: String(m.validUntil), periodSeconds: 30 };
1663
- }
1664
- function validateTicket(value, expectedMode, trustedOrigin) {
1665
- const m = object(value);
1666
- if ("pairing" in m || "seed" in m || "apiKey" in m) fail("SERVER_SECRET_FORBIDDEN");
1667
- if (typeof m.requestId !== "string" || !token.test(m.requestId) || !["bind", "confirm"].includes(String(m.purpose)) || expectedMode && m.purpose !== expectedMode || typeof m.qr !== "string" || m.qr.length > 4096 || typeof m.pairingCode !== "string" || !/^\d{6}$/.test(m.pairingCode)) fail("INVALID_TICKET");
1668
- let qr;
1669
- try {
1670
- qr = object(JSON.parse(m.qr));
1671
- } catch {
1672
- return fail("INVALID_QR");
1673
- }
1674
- let origin;
1655
+ function bindingImage(ticket, expectedOrigin) {
1656
+ if (typeof ticket.qr !== "string" || ticket.qr.length > 4096) throw new AuthLinkError("INVALID_QR");
1657
+ if (deadline(ticket.expiresAt) > Date.now() + 36e5) throw new AuthLinkError("INVALID_EXPIRY");
1658
+ let payload, origin;
1675
1659
  try {
1676
- origin = new URL(String(qr.origin));
1660
+ payload = JSON.parse(ticket.qr);
1661
+ origin = new URL(String(payload.origin));
1677
1662
  } catch {
1678
- return fail("INVALID_QR");
1679
- }
1680
- if (qr.v !== 1 || qr.requestId !== m.requestId || typeof qr.token !== "string" || !token.test(qr.token) || origin.protocol !== "https:" || origin.origin !== qr.origin || origin.origin.length > 512 || origin.username || origin.password || trustedOrigin && origin.origin !== trustedOrigin) fail("UNTRUSTED_QR");
1681
- const displayCode = m.displayCode === void 0 ? void 0 : validateDisplayCode(m.displayCode);
1682
- const expiry = Date.parse(String(m.expiresAt));
1683
- const now = displayCode ? Date.parse(displayCode.serverTime) : Date.now();
1684
- if (!Number.isFinite(expiry) || expiry > now + 36e4) fail("INVALID_EXPIRY");
1685
- return {
1686
- requestId: m.requestId,
1687
- purpose: m.purpose,
1688
- qr: m.qr,
1689
- pairingCode: m.pairingCode,
1690
- expiresAt: String(m.expiresAt),
1691
- ...displayCode ? { displayCode } : {}
1692
- };
1693
- }
1694
- function qrImage(ticket) {
1695
- const value = validateTicket(ticket);
1696
- const payload = JSON.parse(value.qr);
1697
- const qr = qrcode_default(0, "M");
1698
- qr.addData(JSON.stringify({ v: 1, origin: payload.origin, requestId: payload.requestId, token: payload.token }), "Byte");
1699
- qr.make();
1700
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(qr.createSvgTag({ cellSize: 4, margin: 16, scalable: true }))}`;
1701
- }
1702
- function initialSnapshot() {
1703
- return { status: "idle", code: null, secondsRemaining: 0, codeSecondsRemaining: 0, error: null };
1704
- }
1705
- function abortable(promise, signal) {
1706
- signal.throwIfAborted();
1707
- let cancel;
1708
- const abort = new Promise((_, reject) => {
1709
- cancel = () => reject(signal.reason);
1710
- signal.addEventListener("abort", cancel, { once: true });
1711
- });
1712
- return Promise.race([promise, abort]).finally(() => signal.removeEventListener("abort", cancel));
1713
- }
1714
- async function observeRequest(input, options) {
1715
- const { client, signal, onUpdate } = options;
1716
- let ticket;
1717
- try {
1718
- ticket = validateTicket(input, options.expectedMode);
1719
- } catch (error) {
1720
- if (!signal.aborted) options.onError?.(error);
1721
- return;
1722
- }
1723
- const expiry = Date.parse(ticket.expiresAt);
1724
- const deadline = performance.now() + Math.max(0, expiry - Date.now());
1725
- let ended = false, busy = false;
1726
- let done;
1727
- const finished = new Promise((resolve) => {
1728
- done = resolve;
1729
- });
1730
- const controller = new AbortController();
1731
- const cancel = () => {
1732
- ended = true;
1733
- controller.abort();
1734
- done();
1735
- };
1736
- signal.addEventListener("abort", cancel, { once: true });
1737
- let snapshot = initialSnapshot();
1738
- const remaining = () => Math.max(0, (deadline - performance.now()) / 1e3);
1739
- const tick = () => {
1740
- if (ended || signal.aborted) return;
1741
- if (remaining() <= 0) {
1742
- ended = true;
1743
- controller.abort();
1744
- onUpdate({ ...snapshot, status: "expired", code: null, submitting: false });
1745
- void Promise.resolve().then(() => options.onExpired?.()).catch(() => options.onError?.(new AuthLinkError("CALLBACK_FAILED")));
1746
- done();
1747
- return;
1748
- }
1749
- snapshot = {
1750
- ...snapshot,
1751
- status: "pending",
1752
- code: ticket.pairingCode,
1753
- secondsRemaining: Math.ceil(remaining()),
1754
- codeSecondsRemaining: Math.ceil(remaining()),
1755
- submitting: busy
1756
- };
1757
- onUpdate(snapshot);
1758
- };
1759
- const submit = async (confirmationCode) => {
1760
- if (ended || signal.aborted || busy) return;
1761
- if (!/^[0-9]{6}$/.test(confirmationCode)) {
1762
- snapshot = { ...snapshot, error: new AuthLinkError("CONFIRMATION_CODE_REQUIRED") };
1763
- tick();
1764
- return;
1765
- }
1766
- if (remaining() <= 0) {
1767
- tick();
1768
- return;
1769
- }
1770
- busy = true;
1771
- snapshot = { ...snapshot, error: null };
1772
- tick();
1773
- try {
1774
- const value = object(await abortable(client.result(ticket, controller.signal, confirmationCode), controller.signal));
1775
- if (ended || signal.aborted) return;
1776
- if (value.requestId !== ticket.requestId) fail("REQUEST_MISMATCH");
1777
- if (value.status === "confirmed") {
1778
- if (!token.test(String(value.verificationId)) || value.authenticationPolicy !== "device-presence.v1" || value.expiresAt !== ticket.expiresAt || value.purpose !== ticket.purpose || remaining() <= 0 || "action" in value || "transactionHash" in value) fail("INVALID_CONFIRMATION");
1779
- ended = true;
1780
- onUpdate({ ...snapshot, status: "confirmed", code: null, submitting: false });
1781
- try {
1782
- await options.onConfirmed?.(value);
1783
- } catch {
1784
- options.onError?.(new AuthLinkError("CALLBACK_FAILED"));
1785
- }
1786
- done();
1787
- } else if (value.status === "rejected" || value.status === "expired") {
1788
- ended = true;
1789
- onUpdate({ ...snapshot, status: value.status, code: null, submitting: false });
1790
- try {
1791
- if (value.status === "rejected") await options.onRejected?.({ status: "rejected", code: "REJECTED", requestId: ticket.requestId, purpose: ticket.purpose });
1792
- else await options.onExpired?.();
1793
- } catch {
1794
- options.onError?.(new AuthLinkError("CALLBACK_FAILED"));
1795
- } finally {
1796
- done();
1797
- }
1798
- } else {
1799
- fail("CONFIRMATION_NOT_READY");
1800
- }
1801
- } catch (error) {
1802
- if (!ended && !signal.aborted) {
1803
- const raw = error instanceof Error ? error : new AuthLinkError("NETWORK_ERROR");
1804
- const code = raw.message.replace(/^AUTHLINK_/, "");
1805
- const failure = ["CONFIRMATION_CODE_LOCKED", "CONFIRMATION_CODE_INVALID", "APPROVAL_REQUIRED", "REQUEST_EXPIRED"].includes(code) ? new AuthLinkError(code) : raw;
1806
- snapshot = { ...snapshot, error: failure };
1807
- if (code === "CONFIRMATION_CODE_LOCKED" || code === "REQUEST_EXPIRED") {
1808
- ended = true;
1809
- onUpdate({ ...snapshot, code: null, submitting: false, status: code === "REQUEST_EXPIRED" ? "expired" : "error" });
1810
- if (code === "REQUEST_EXPIRED") void Promise.resolve().then(() => options.onExpired?.()).catch(() => options.onError?.(new AuthLinkError("CALLBACK_FAILED")));
1811
- else options.onError?.(failure);
1812
- done();
1813
- } else options.onError?.(failure);
1814
- }
1815
- } finally {
1816
- busy = false;
1817
- tick();
1818
- }
1819
- };
1820
- tick();
1821
- options.onReady?.(submit);
1822
- const timer = setInterval(tick, 1e3);
1823
- if (signal.aborted) cancel();
1824
- try {
1825
- await finished;
1826
- } finally {
1827
- clearInterval(timer);
1828
- controller.abort();
1829
- signal.removeEventListener("abort", cancel);
1663
+ throw new AuthLinkError("INVALID_QR");
1830
1664
  }
1665
+ const token = /^[A-Za-z0-9_-]{43}$/;
1666
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname);
1667
+ if (payload.v !== 1 || typeof payload.requestId !== "string" || !token.test(payload.requestId) || typeof payload.token !== "string" || !token.test(payload.token) || origin.origin !== payload.origin || origin.protocol !== "https:" && !(origin.protocol === "http:" && loopback) || expectedOrigin !== void 0 && expectedOrigin !== origin.origin) throw new AuthLinkError("INVALID_QR");
1668
+ const image = qrcode_default(0, "M");
1669
+ image.addData(JSON.stringify({ v: 1, origin: origin.origin, requestId: payload.requestId, token: payload.token }), "Byte");
1670
+ image.make();
1671
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(image.createSvgTag({ cellSize: 4, margin: 16, scalable: true }))}`;
1831
1672
  }
1832
1673
 
1833
1674
  // src/locales/en.json
@@ -2259,35 +2100,20 @@ function getMessages(locale) {
2259
2100
 
2260
2101
  // src/view.ts
2261
2102
  var node = (tag, props, ...children) => ({ tag, props, children });
2262
- function resolveVariant(variant) {
2263
- return variant === "B" || variant === "C" ? variant : "A";
2264
- }
2103
+ var digits = (value) => value.replace(/[^0-9]/g, "");
2265
2104
  function widgetView(options) {
2266
- const { snapshot: s, mode, retry, refresh } = options;
2267
- const locale = resolveLocale(options.locale), variant = resolveVariant(options.variant);
2268
- const l = getMessages(locale);
2269
- const pending = s.status === "pending", hasQr = pending && (mode === "bind" || Boolean(options.showQr)) && Boolean(options.qr);
2270
- const status = { idle: l.loading, pending: options.passcodeMode ? l.otpWaiting : l.waiting, confirmed: l.confirmed, rejected: l.rejected, expired: l.expired, error: l.error }[s.status];
2271
- const expiring = pending && s.secondsRemaining <= 30;
2272
- const expiryTime = `00:${String(Math.max(0, Math.ceil(s.secondsRemaining))).padStart(2, "0")}`;
2273
- const statusNode = () => node(
2274
- "p",
2275
- {
2276
- class: "authlink-status",
2277
- role: s.status === "error" ? "alert" : "status",
2278
- "data-expiring": String(expiring)
2279
- },
2280
- node("span", { class: "authlink-status-dot", "aria-hidden": "true" }),
2281
- expiring ? l.expiring : status,
2282
- ...expiring ? [node("span", { class: "authlink-expiry", "aria-hidden": "true" }, ` \xB7 ${expiryTime}`)] : []
2283
- );
2284
- const cancel = () => options.close ? [node("button", {
2285
- type: "button",
2286
- class: "authlink-cancel",
2287
- "data-authlink-focus": "cancel",
2288
- onClick: options.close
2289
- }, l.cancel)] : [];
2105
+ const { snapshot: s, mode } = options, locale = resolveLocale(options.locale), l = getMessages(locale);
2106
+ const pending = s.status === "pending", hasQr = pending && mode === "bind" && Boolean(options.qr);
2107
+ const expiring = pending && s.secondsRemaining !== void 0 && s.secondsRemaining <= 30;
2108
+ const status = s.status === "pending" ? l.otpWaiting : l[s.status];
2109
+ const remaining = s.secondsRemaining ?? 0;
2110
+ const expiryTime = `${String(Math.floor(remaining / 60)).padStart(2, "0")}:${String(remaining % 60).padStart(2, "0")}`;
2290
2111
  const children = [node("h3", { class: "authlink-title" }, l[mode])];
2112
+ const errorNode = s.error ? node(
2113
+ "p",
2114
+ { class: "authlink-input-error", role: "alert" },
2115
+ ["PASSCODE_LOCKED", "RATE_LIMITED", "AUTHLINK_PASSCODE_LOCKED"].includes(s.error.message) ? l.codeLocked : l.otpError
2116
+ ) : void 0;
2291
2117
  if (hasQr) children.push(node("p", { class: "authlink-subtitle" }, l.scan));
2292
2118
  if (pending) {
2293
2119
  const qr = hasQr ? [node(
@@ -2297,18 +2123,18 @@ function widgetView(options) {
2297
2123
  node("figcaption", { class: "authlink-qr-caption" }, l.scan)
2298
2124
  )] : [];
2299
2125
  const detail = [
2300
- node("p", { class: "authlink-help" }, options.passcodeMode ? l.otpHelp : l.help),
2126
+ node("p", { class: "authlink-help" }, l.otpHelp),
2301
2127
  node(
2302
2128
  "form",
2303
2129
  { class: "authlink-code-form", onSubmit: (event) => {
2304
2130
  event.preventDefault();
2305
2131
  const input = event.currentTarget.querySelector("input");
2306
- if (input) void options.submitCode?.(input.value);
2132
+ if (input) void options.submitCode(input.value);
2307
2133
  } },
2308
- node("label", { for: "authlink-confirmation-code" }, options.passcodeMode ? l.otpCode : l.confirmationCode),
2134
+ node("label", { for: "authlink-passcode" }, l.otpCode),
2309
2135
  node("input", {
2310
- id: "authlink-confirmation-code",
2311
- name: options.passcodeMode ? "passcode" : "confirmationCode",
2136
+ id: "authlink-passcode",
2137
+ name: "passcode",
2312
2138
  type: "text",
2313
2139
  inputmode: "numeric",
2314
2140
  autocomplete: "one-time-code",
@@ -2316,272 +2142,74 @@ function widgetView(options) {
2316
2142
  maxlength: 6,
2317
2143
  minlength: 6,
2318
2144
  required: true,
2319
- "data-authlink-focus": "confirmation-code",
2320
- value: options.inputCode ?? "",
2321
- onInput: (event) => options.setCode?.(event.target.value)
2145
+ ...s.submitting ? { disabled: true } : {},
2146
+ "data-authlink-focus": "passcode",
2147
+ value: options.inputCode,
2148
+ onInput: (event) => {
2149
+ const input = event.target, original = input.value;
2150
+ const start = input.selectionStart ?? original.length, end = input.selectionEnd ?? start;
2151
+ const code = digits(original).slice(0, 6);
2152
+ if (original !== code) {
2153
+ input.value = code;
2154
+ input.setSelectionRange(Math.min(6, digits(original.slice(0, start)).length), Math.min(6, digits(original.slice(0, end)).length));
2155
+ }
2156
+ options.setCode(code);
2157
+ },
2158
+ onPaste: (event) => {
2159
+ if (!event.clipboardData) return;
2160
+ event.preventDefault();
2161
+ const input = event.target, pasted = digits(event.clipboardData.getData("text"));
2162
+ if (!pasted) return;
2163
+ const start = input.selectionStart ?? input.value.length, end = input.selectionEnd ?? start;
2164
+ const before = input.value.slice(0, start), after = input.value.slice(end);
2165
+ const insert = pasted.slice(0, Math.max(0, 6 - before.length - after.length));
2166
+ input.value = before + insert + after;
2167
+ input.setSelectionRange(before.length + insert.length, before.length + insert.length);
2168
+ options.setCode(input.value);
2169
+ }
2322
2170
  }),
2323
- node(
2324
- "button",
2325
- { type: "submit", class: "authlink-button", ...s.submitting ? { disabled: true } : {} },
2326
- s.submitting ? l.loading : l.submitCode
2327
- )
2328
- ),
2329
- ...s.error ? [node("p", { class: "authlink-input-error", role: "alert" }, options.passcodeMode ? s.error.message === "OTP_NOT_CONFIGURED" ? l.otpSetup : l.otpError : l.codeError)] : []
2171
+ node("button", { type: "submit", class: "authlink-button", ...s.submitting ? { disabled: true } : {} }, s.submitting ? l.loading : l.submitCode)
2172
+ )
2330
2173
  ];
2331
- children.push(node("div", { class: "authlink-body" }, ...qr, node("div", { class: "authlink-details" }, ...detail)));
2332
- if (variant === "A") children.push(node("div", { class: "authlink-footer" }, statusNode()));
2333
- if (variant === "B") children.push(node("div", { class: "authlink-footer" }, statusNode(), ...cancel()));
2334
- if (variant === "C") children.push(node("div", { class: "authlink-footer" }, statusNode(), ...cancel()));
2335
- } else {
2336
- children.push(statusNode());
2337
- if (s.error?.message === "CONFIRMATION_CODE_LOCKED") children.push(node("p", { class: "authlink-input-error", role: "alert" }, l.codeLocked));
2338
- if (s.status === "error" && s.error?.message !== "CONFIRMATION_CODE_LOCKED") children.push(node("button", {
2339
- type: "button",
2340
- class: "authlink-button",
2341
- "data-authlink-focus": "retry",
2342
- onClick: retry
2343
- }, l.retry));
2344
- if (refresh && ["expired", "rejected", "error"].includes(s.status)) children.push(node("button", {
2345
- type: "button",
2346
- class: "authlink-button",
2347
- "data-authlink-focus": "refresh",
2348
- onClick: refresh
2349
- }, l.refresh));
2174
+ children.push(node("div", { class: "authlink-body" }, ...qr, node("div", { class: "authlink-details" }, ...detail, ...errorNode ? [errorNode] : [])));
2350
2175
  }
2351
- return node("section", {
2352
- lang: locale,
2353
- class: `authlink-widget ${options.className ?? ""}`.trim(),
2354
- "data-status": s.status,
2355
- "data-variant": variant,
2356
- "data-has-qr": String(hasQr),
2357
- "aria-label": l[mode]
2358
- }, ...children);
2176
+ if (!pending && errorNode) children.push(errorNode);
2177
+ children.push(node("div", { class: "authlink-footer" }, node(
2178
+ "p",
2179
+ { class: "authlink-status", role: "status", "data-expiring": String(expiring) },
2180
+ node("span", { class: "authlink-status-dot", "aria-hidden": "true" }),
2181
+ expiring ? l.expiring : status,
2182
+ ...expiring ? [node("span", { class: "authlink-expiry", "aria-hidden": "true" }, ` \xB7 ${expiryTime}`)] : []
2183
+ )));
2184
+ if (s.status === "expired" && options.refresh) children.push(node("button", {
2185
+ type: "button",
2186
+ class: "authlink-button",
2187
+ ...s.submitting ? { disabled: true } : {},
2188
+ onClick: options.refresh
2189
+ }, s.submitting ? l.loading : l.refresh));
2190
+ return node("section", { lang: locale, class: "authlink-widget", "data-status": s.status, "data-has-qr": String(hasQr), "aria-label": l[mode] }, ...children);
2359
2191
  }
2360
2192
 
2361
2193
  // src/modal.ts
2362
- function element(value, svg = false) {
2194
+ function element(value) {
2363
2195
  if (typeof value === "string") return document.createTextNode(value);
2364
- svg ||= value.tag === "svg";
2365
- const result = svg ? document.createElementNS("http://www.w3.org/2000/svg", value.tag) : document.createElement(value.tag);
2196
+ const result = document.createElement(value.tag);
2366
2197
  for (const [key, v] of Object.entries(value.props)) {
2367
2198
  if (key.startsWith("on") && typeof v === "function") result.addEventListener(key.slice(2).toLowerCase(), v);
2368
2199
  else if (v !== void 0 && v !== null) result.setAttribute(key, String(v));
2369
2200
  }
2370
- result.append(...value.children.map((child) => element(child, svg)));
2201
+ result.append(...value.children.map(element));
2371
2202
  return result;
2372
2203
  }
2373
- function openAuthLink(options) {
2204
+ function open(options, mode) {
2374
2205
  if (typeof document === "undefined" || !document.body) throw new AuthLinkError("BROWSER_REQUIRED");
2375
- const mode = options.mode ?? options.ticket.purpose;
2376
- const ticket = validateTicket(options.ticket, mode);
2206
+ if (typeof options.submit !== "function") throw new AuthLinkError("SUBMIT_REQUIRED");
2207
+ const binding = mode === "bind" ? options : void 0;
2208
+ let image = binding ? bindingImage(binding, binding.authLinkOrigin) : void 0;
2209
+ let expires = options.expiresAt === void 0 ? void 0 : deadline(options.expiresAt);
2377
2210
  const dialog = document.createElement("dialog");
2378
2211
  if (typeof dialog.showModal !== "function") throw new AuthLinkError("DIALOG_UNSUPPORTED");
2379
2212
  const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2380
- dialog.className = `authlink-dialog ${options.className ?? ""}`.trim();
2381
- dialog.setAttribute("aria-modal", "true");
2382
- const header = document.createElement("div");
2383
- header.className = "authlink-header";
2384
- const brand = document.createElement("span");
2385
- brand.className = "authlink-brand";
2386
- brand.textContent = "AuthLink";
2387
- brand.tabIndex = -1;
2388
- const closeButton = document.createElement("button");
2389
- closeButton.type = "button";
2390
- closeButton.className = "authlink-close";
2391
- const closeIcon = document.createElement("span");
2392
- closeIcon.className = "authlink-close-icon";
2393
- closeIcon.setAttribute("aria-hidden", "true");
2394
- closeButton.append(closeIcon);
2395
- const content = document.createElement("div");
2396
- content.className = "authlink-modal-content";
2397
- header.append(brand, closeButton);
2398
- dialog.append(header, content);
2399
- let locale = options.locale, variant = resolveVariant(options.variant), snapshot = initialSnapshot(), closed = false;
2400
- let abort;
2401
- let inputCode = "";
2402
- let submitCode;
2403
- const image = qrImage(ticket);
2404
- let progressFill, progressFrame, progressDeadline = 0;
2405
- const motion = window.matchMedia?.("(prefers-reduced-motion: reduce)");
2406
- const stopProgress = () => {
2407
- if (progressFrame !== void 0) window.cancelAnimationFrame(progressFrame);
2408
- progressFrame = void 0;
2409
- };
2410
- const drawProgress = () => {
2411
- stopProgress();
2412
- if (closed || !progressFill?.isConnected) return;
2413
- const remaining = Math.max(0, progressDeadline - performance.now());
2414
- progressFill.style.transform = `scaleX(${Math.min(1, remaining / 3e4)})`;
2415
- if (remaining > 0 && !motion?.matches && typeof window.requestAnimationFrame === "function") {
2416
- progressFrame = window.requestAnimationFrame(drawProgress);
2417
- }
2418
- };
2419
- const syncProgress = (remainingMs) => {
2420
- progressDeadline = performance.now() + remainingMs;
2421
- drawProgress();
2422
- };
2423
- motion?.addEventListener?.("change", drawProgress);
2424
- const render = () => {
2425
- if (closed) return;
2426
- const language = resolveLocale(locale), messages2 = getMessages(language);
2427
- dialog.lang = language;
2428
- dialog.setAttribute("aria-label", messages2[mode]);
2429
- dialog.dataset.variant = variant;
2430
- dialog.dataset.mode = mode;
2431
- dialog.dataset.hasQr = String(snapshot.status === "pending" && (mode === "bind" || Boolean(options.showQr)));
2432
- closeButton.setAttribute("aria-label", messages2.close);
2433
- const focused = document.activeElement;
2434
- const focusKey = focused instanceof HTMLElement && content.contains(focused) ? focused.dataset.authlinkFocus : void 0;
2435
- const next = element(widgetView({
2436
- mode,
2437
- locale,
2438
- variant,
2439
- snapshot,
2440
- qr: image,
2441
- showQr: options.showQr,
2442
- inputCode,
2443
- setCode: (code) => {
2444
- inputCode = code;
2445
- },
2446
- submitCode: (code) => submitCode?.(code) ?? Promise.resolve(),
2447
- retry,
2448
- refresh: options.onRefresh,
2449
- close: () => dispose(true)
2450
- }));
2451
- const oldCode = content.querySelector(".authlink-code");
2452
- const nextCode = next.querySelector(".authlink-code");
2453
- const codeChanged = Boolean(oldCode && nextCode && snapshot.code && oldCode.textContent !== nextCode.textContent);
2454
- if (oldCode && nextCode && oldCode.textContent === nextCode.textContent) {
2455
- oldCode.setAttribute("aria-label", nextCode.getAttribute("aria-label"));
2456
- nextCode.replaceWith(oldCode);
2457
- }
2458
- const oldProgress = content.querySelector(".authlink-progress");
2459
- const nextProgress = next.querySelector(".authlink-progress");
2460
- if (oldProgress && nextProgress) {
2461
- oldProgress.setAttribute("aria-valuenow", nextProgress.getAttribute("aria-valuenow"));
2462
- oldProgress.setAttribute("aria-label", nextProgress.getAttribute("aria-label"));
2463
- nextProgress.replaceWith(oldProgress);
2464
- }
2465
- const oldInput = content.querySelector("input[name=confirmationCode]");
2466
- const nextInput = next.querySelector("input[name=confirmationCode]");
2467
- if (oldInput && nextInput) nextInput.replaceWith(oldInput);
2468
- content.replaceChildren(next);
2469
- progressFill = content.querySelector(".authlink-progress-fill") ?? void 0;
2470
- drawProgress();
2471
- if (codeChanged && !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
2472
- nextCode?.animate?.([{ opacity: 0.5 }, { opacity: 1 }], { duration: 180, easing: "ease-out" });
2473
- }
2474
- if (focusKey) (Array.from(content.querySelectorAll("[data-authlink-focus]")).find((item) => item.dataset.authlinkFocus === focusKey) ?? brand).focus({ preventScroll: true });
2475
- };
2476
- const dispose = (notify) => {
2477
- if (closed) return;
2478
- closed = true;
2479
- abort?.abort();
2480
- stopProgress();
2481
- progressFill = void 0;
2482
- motion?.removeEventListener?.("change", drawProgress);
2483
- if (dialog.open) dialog.close();
2484
- dialog.remove();
2485
- if (!document.querySelector("dialog.authlink-dialog[open]")) {
2486
- if (document.body.style.overflow === "hidden") document.body.style.overflow = document.body.dataset.authlinkOverflow ?? "";
2487
- delete document.body.dataset.authlinkOverflow;
2488
- }
2489
- if (previousFocus?.isConnected) previousFocus.focus();
2490
- if (notify) options.onClose?.();
2491
- };
2492
- const delivered = /* @__PURE__ */ new Set();
2493
- const once = (status, callback) => {
2494
- if (delivered.has(status)) return;
2495
- delivered.add(status);
2496
- return callback();
2497
- };
2498
- const retry = () => {
2499
- if (closed) return;
2500
- abort?.abort();
2501
- abort = new AbortController();
2502
- snapshot = initialSnapshot();
2503
- render();
2504
- void observeRequest(ticket, {
2505
- client: options.client,
2506
- intervalMs: options.intervalMs,
2507
- expectedMode: mode,
2508
- signal: abort.signal,
2509
- onReady: (submit) => {
2510
- submitCode = submit;
2511
- },
2512
- onCodeTiming: syncProgress,
2513
- onUpdate: (value) => {
2514
- snapshot = value;
2515
- if (value.status !== "rejected") render();
2516
- },
2517
- onConfirmed: (value) => once("confirmed", () => {
2518
- dispose(false);
2519
- try {
2520
- return Promise.resolve(options.onConfirmed?.(value)).catch(() => options.onError?.(new AuthLinkError("CALLBACK_FAILED")));
2521
- } catch {
2522
- options.onError?.(new AuthLinkError("CALLBACK_FAILED"));
2523
- }
2524
- }),
2525
- onRejected: (value) => once("rejected", () => {
2526
- dispose(false);
2527
- try {
2528
- return Promise.resolve(options.onRejected?.(value)).catch(() => options.onError?.(new AuthLinkError("CALLBACK_FAILED")));
2529
- } catch {
2530
- options.onError?.(new AuthLinkError("CALLBACK_FAILED"));
2531
- }
2532
- }),
2533
- onExpired: () => once("expired", () => options.onExpired?.()),
2534
- onError: options.onError
2535
- });
2536
- };
2537
- closeButton.addEventListener("click", () => dispose(true));
2538
- dialog.addEventListener("cancel", (event) => {
2539
- event.preventDefault();
2540
- dispose(true);
2541
- });
2542
- dialog.addEventListener("close", () => dispose(true));
2543
- render();
2544
- document.body.append(dialog);
2545
- try {
2546
- dialog.showModal();
2547
- } catch (error) {
2548
- dialog.remove();
2549
- throw error;
2550
- }
2551
- brand.focus({ preventScroll: true });
2552
- if (document.body.dataset.authlinkOverflow === void 0) document.body.dataset.authlinkOverflow = document.body.style.overflow;
2553
- document.body.style.overflow = "hidden";
2554
- retry();
2555
- return {
2556
- close: () => dispose(true),
2557
- destroy: () => dispose(false),
2558
- setLocale: (value) => {
2559
- locale = value;
2560
- render();
2561
- },
2562
- setVariant: (value) => {
2563
- variant = resolveVariant(value);
2564
- render();
2565
- },
2566
- retry
2567
- };
2568
- }
2569
-
2570
- // src/passcode.ts
2571
- function validateOTPResult(value) {
2572
- if (!value || typeof value !== "object") throw new AuthLinkError("INVALID_RESPONSE");
2573
- const result = value;
2574
- const expiry = typeof result.expiresAt === "string" ? Date.parse(result.expiresAt) : NaN;
2575
- if (result.status !== "confirmed" || result.authenticationPolicy !== "device-totp.v1" || typeof result.verificationId !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(result.verificationId) || !Number.isFinite(expiry) || expiry <= Date.now() || expiry > Date.now() + 9e4 || "requestId" in result || "state" in result || "businessId" in result) throw new AuthLinkError("INVALID_CONFIRMATION");
2576
- return result;
2577
- }
2578
- function openPasscode(options) {
2579
- if (typeof document === "undefined" || !document.body) throw new AuthLinkError("BROWSER_REQUIRED");
2580
- const deadline = Date.parse(options.expiresAt);
2581
- if (!Number.isFinite(deadline) || deadline <= Date.now() || deadline > Date.now() + 3e5) throw new AuthLinkError("INVALID_EXPIRY");
2582
- const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2583
- const dialog = document.createElement("dialog");
2584
- if (typeof dialog.showModal !== "function") throw new AuthLinkError("DIALOG_UNSUPPORTED");
2585
2213
  dialog.className = "authlink-dialog";
2586
2214
  dialog.setAttribute("aria-modal", "true");
2587
2215
  const header = document.createElement("div");
@@ -2600,94 +2228,143 @@ function openPasscode(options) {
2600
2228
  content.className = "authlink-modal-content";
2601
2229
  header.append(brand, close);
2602
2230
  dialog.append(header, content);
2603
- let locale = options.locale, variant = resolveVariant(options.variant), closed = false, busy = false, code = "", error;
2604
- const mode = options.bindingQr ? "bind" : "confirm";
2605
- let image;
2606
- if (options.bindingQr) {
2607
- let payload;
2231
+ let locale = options.locale, closed = false, busy = false, code = "", error = null;
2232
+ let abort, timer, expiredDelivered = false;
2233
+ const notifyError = (cause) => {
2234
+ const value = cause instanceof Error ? cause : new AuthLinkError("VERIFICATION_FAILED");
2608
2235
  try {
2609
- payload = JSON.parse(options.bindingQr);
2236
+ options.onError?.(value);
2610
2237
  } catch {
2611
- throw new AuthLinkError("INVALID_TICKET");
2612
2238
  }
2613
- image = qrImage(validateTicket({ requestId: payload.requestId, purpose: "bind", qr: options.bindingQr, pairingCode: "000000", expiresAt: options.expiresAt }, "bind", options.authLinkOrigin));
2614
- }
2615
- const abort = new AbortController();
2616
- let timer;
2239
+ return value;
2240
+ };
2241
+ const callback = (fn) => {
2242
+ try {
2243
+ void Promise.resolve(fn?.()).catch(() => notifyError(new AuthLinkError("CALLBACK_FAILED")));
2244
+ } catch {
2245
+ notifyError(new AuthLinkError("CALLBACK_FAILED"));
2246
+ }
2247
+ };
2617
2248
  const dispose = (notify) => {
2618
2249
  if (closed) return;
2619
2250
  closed = true;
2620
- abort.abort();
2251
+ abort?.abort();
2621
2252
  clearInterval(timer);
2253
+ if (dialog.open) dialog.close();
2622
2254
  dialog.remove();
2623
- previousFocus?.focus();
2624
- if (notify) options.onClose?.();
2255
+ if (previousFocus?.isConnected) previousFocus.focus();
2256
+ if (notify) callback(options.onClose);
2625
2257
  };
2258
+ const expired = () => expires !== void 0 && Date.now() >= expires;
2626
2259
  const render = () => {
2627
2260
  if (closed) return;
2628
2261
  const l = getMessages(locale);
2629
2262
  dialog.lang = resolveLocale(locale);
2630
- dialog.dataset.variant = variant;
2631
2263
  dialog.dataset.mode = mode;
2632
- dialog.dataset.hasQr = String(Boolean(image));
2264
+ dialog.dataset.hasQr = String(Boolean(image) && !expired());
2633
2265
  dialog.setAttribute("aria-label", l[mode]);
2634
2266
  close.setAttribute("aria-label", l.close);
2635
- const focused = content.contains(document.activeElement);
2636
- const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1e3));
2637
- const snapshot = { status: remaining ? "pending" : "expired", code: null, secondsRemaining: remaining, codeSecondsRemaining: remaining, submitting: busy, error: error ?? null };
2638
- content.replaceChildren(element(widgetView({
2267
+ const focused = document.activeElement;
2268
+ const focusKey = focused instanceof HTMLElement && content.contains(focused) ? focused.dataset.authlinkFocus : void 0;
2269
+ const oldInput = content.querySelector("input");
2270
+ const next = element(widgetView({
2639
2271
  mode,
2640
2272
  locale,
2641
- variant,
2642
- passcodeMode: true,
2643
- snapshot,
2273
+ snapshot: {
2274
+ status: expired() ? "expired" : "pending",
2275
+ submitting: busy,
2276
+ error,
2277
+ secondsRemaining: expires === void 0 ? void 0 : Math.max(0, Math.ceil((expires - Date.now()) / 1e3))
2278
+ },
2644
2279
  qr: image,
2645
- showQr: Boolean(image),
2646
2280
  inputCode: code,
2647
2281
  setCode: (value) => {
2648
2282
  code = value;
2649
2283
  },
2650
2284
  submitCode: submit,
2651
- retry: () => {
2652
- error = void 0;
2653
- render();
2654
- },
2655
- close: () => dispose(true)
2656
- })));
2657
- if (focused) content.querySelector("input")?.focus();
2285
+ refresh: binding?.onRefresh ? refresh : void 0
2286
+ }));
2287
+ const nextInput = next.querySelector("input");
2288
+ if (oldInput && nextInput) {
2289
+ oldInput.disabled = busy;
2290
+ if (oldInput.value !== code) oldInput.value = code;
2291
+ nextInput.replaceWith(oldInput);
2292
+ }
2293
+ content.replaceChildren(next);
2294
+ if (focusKey) content.querySelector(`[data-authlink-focus="${focusKey}"]`)?.focus({ preventScroll: true });
2658
2295
  };
2659
- const submit = async (value) => {
2660
- if (closed || busy) return;
2661
- if (Date.now() >= deadline) {
2296
+ const tick = () => {
2297
+ if (closed) return;
2298
+ if (expired() && !expiredDelivered) {
2299
+ expiredDelivered = true;
2300
+ abort?.abort();
2301
+ busy = false;
2662
2302
  render();
2303
+ callback(options.onExpired);
2304
+ } else if (expires !== void 0) render();
2305
+ };
2306
+ const startTimer = () => {
2307
+ clearInterval(timer);
2308
+ if (expires !== void 0) timer = setInterval(tick, 1e3);
2309
+ };
2310
+ const submit = async (value) => {
2311
+ if (closed || busy || expired()) {
2312
+ tick();
2663
2313
  return;
2664
2314
  }
2665
2315
  if (!/^[0-9]{6}$/.test(value)) {
2666
- error = new AuthLinkError("PASSCODE_INVALID");
2316
+ error = notifyError(new AuthLinkError("PASSCODE_INVALID"));
2667
2317
  render();
2668
2318
  return;
2669
2319
  }
2670
2320
  code = value;
2671
2321
  busy = true;
2672
- error = void 0;
2322
+ error = null;
2323
+ const current = new AbortController();
2324
+ abort = current;
2673
2325
  render();
2674
2326
  try {
2675
- const result = validateOTPResult(await options.client.verify(value, abort.signal));
2676
- if (closed || abort.signal.aborted || Date.now() >= deadline) return;
2677
- try {
2678
- await options.onVerified?.(result);
2679
- } catch {
2680
- options.onError?.(new AuthLinkError("CALLBACK_FAILED"));
2681
- }
2327
+ const pending = options.submit(value, { signal: current.signal });
2328
+ if (!pending || typeof pending.then !== "function") throw new AuthLinkError("SUBMIT_MUST_RETURN_PROMISE");
2329
+ await pending;
2330
+ if (closed || current.signal.aborted || abort !== current) return;
2682
2331
  dispose(false);
2332
+ callback(options.onSuccess);
2683
2333
  } catch (cause) {
2684
- if (!closed && !abort.signal.aborted) {
2685
- error = cause instanceof Error ? cause : new AuthLinkError("NETWORK_ERROR");
2686
- options.onError?.(error);
2334
+ if (!closed && !current.signal.aborted && abort === current) error = notifyError(cause);
2335
+ } finally {
2336
+ if (!closed && abort === current) {
2337
+ busy = false;
2338
+ render();
2687
2339
  }
2340
+ }
2341
+ };
2342
+ const refresh = async () => {
2343
+ if (closed || busy || !binding?.onRefresh || !expired()) return;
2344
+ busy = true;
2345
+ error = null;
2346
+ expiredDelivered = true;
2347
+ clearInterval(timer);
2348
+ const current = new AbortController();
2349
+ abort?.abort();
2350
+ abort = current;
2351
+ render();
2352
+ try {
2353
+ const ticket = await binding.onRefresh({ signal: current.signal });
2354
+ if (closed || current.signal.aborted || abort !== current) return;
2355
+ const nextImage = bindingImage(ticket, binding.authLinkOrigin), nextDeadline = deadline(ticket.expiresAt);
2356
+ image = nextImage;
2357
+ expires = nextDeadline;
2358
+ code = "";
2359
+ expiredDelivered = false;
2360
+ startTimer();
2361
+ } catch (cause) {
2362
+ if (!closed && !current.signal.aborted && abort === current) error = notifyError(cause);
2688
2363
  } finally {
2689
- busy = false;
2690
- render();
2364
+ if (!closed && abort === current) {
2365
+ busy = false;
2366
+ render();
2367
+ }
2691
2368
  }
2692
2369
  };
2693
2370
  close.addEventListener("click", () => dispose(true));
@@ -2695,18 +2372,17 @@ function openPasscode(options) {
2695
2372
  event.preventDefault();
2696
2373
  dispose(true);
2697
2374
  });
2375
+ dialog.addEventListener("close", () => dispose(true));
2698
2376
  document.body.append(dialog);
2699
2377
  render();
2700
- dialog.showModal();
2378
+ try {
2379
+ dialog.showModal();
2380
+ } catch (cause) {
2381
+ dispose(false);
2382
+ throw cause;
2383
+ }
2701
2384
  content.querySelector("input")?.focus();
2702
- timer = setInterval(() => {
2703
- if (closed) return;
2704
- if (Date.now() >= deadline) {
2705
- render();
2706
- clearInterval(timer);
2707
- void Promise.resolve(options.onExpired?.()).catch(() => options.onError?.(new AuthLinkError("CALLBACK_FAILED")));
2708
- }
2709
- }, 1e3);
2385
+ startTimer();
2710
2386
  return {
2711
2387
  close: () => dispose(true),
2712
2388
  destroy: () => dispose(false),
@@ -2714,127 +2390,62 @@ function openPasscode(options) {
2714
2390
  locale = value;
2715
2391
  render();
2716
2392
  },
2717
- setVariant: (value) => {
2718
- variant = resolveVariant(value);
2719
- render();
2720
- },
2721
2393
  retry: () => {
2722
- error = void 0;
2723
- render();
2394
+ if (!closed && !busy) {
2395
+ error = null;
2396
+ tick();
2397
+ render();
2398
+ }
2724
2399
  }
2725
2400
  };
2726
2401
  }
2402
+ var openVerify = (options) => open(options, "confirm");
2403
+ var openBind = (options) => open(options, "bind");
2727
2404
 
2728
2405
  // src/react.tsx
2729
2406
  import { jsx } from "react/jsx-runtime";
2730
- function AuthLinkPasscode(props) {
2731
- const latest = useRef(props);
2732
- latest.current = props;
2733
- const modal = useRef(void 0);
2734
- useEffect(() => {
2735
- const handle = openPasscode({
2736
- ...props,
2737
- onVerified: (value) => latest.current.onVerified?.(value),
2738
- onExpired: () => latest.current.onExpired?.(),
2739
- onClose: () => latest.current.onClose?.(),
2740
- onError: (value) => latest.current.onError?.(value)
2741
- });
2742
- modal.current = handle;
2743
- return () => {
2744
- handle.destroy();
2745
- if (modal.current === handle) modal.current = void 0;
2746
- };
2747
- }, [props.client, props.expiresAt, props.bindingQr, props.authLinkOrigin]);
2748
- useEffect(() => {
2749
- modal.current?.setLocale(props.locale);
2750
- }, [props.locale]);
2751
- useEffect(() => {
2752
- modal.current?.setVariant(props.variant);
2753
- }, [props.variant]);
2754
- return null;
2755
- }
2756
- function useAuthLink(options) {
2407
+ function Prompt({ options, bind }) {
2757
2408
  const latest = useRef(options);
2758
2409
  latest.current = options;
2759
- const submit = useRef(void 0);
2760
- const delivered = useRef(/* @__PURE__ */ new Set());
2761
- const [snapshot, setSnapshot] = useState(initialSnapshot);
2762
- const [attempt, setAttempt] = useState(0);
2763
- const retry = useCallback(() => setAttempt((value) => value + 1), []);
2764
- const { ticket, client, intervalMs, mode } = options;
2765
- useEffect(() => {
2766
- const abort = new AbortController();
2767
- setSnapshot(initialSnapshot());
2768
- const once = (status, callback) => {
2769
- const key = `${ticket.requestId}:${status}`;
2770
- if (delivered.current.has(key)) return;
2771
- delivered.current.add(key);
2772
- return callback();
2773
- };
2774
- void observeRequest(ticket, {
2775
- client,
2776
- intervalMs,
2777
- expectedMode: mode,
2778
- signal: abort.signal,
2779
- onReady: (fn) => {
2780
- submit.current = fn;
2781
- },
2782
- onUpdate: setSnapshot,
2783
- onConfirmed: (result) => once("confirmed", () => latest.current.onConfirmed?.(result)),
2784
- onRejected: (result) => once("rejected", () => latest.current.onRejected?.(result)),
2785
- onExpired: () => once("expired", () => latest.current.onExpired?.()),
2786
- onError: (error) => latest.current.onError?.(error)
2787
- });
2788
- return () => abort.abort();
2789
- }, [ticket, client, intervalMs, mode, attempt]);
2790
- return { snapshot, retry, submitCode: (code) => submit.current?.(code) ?? Promise.resolve() };
2791
- }
2792
- function Widget(props) {
2793
- const latest = useRef(props);
2794
- latest.current = props;
2795
2410
  const modal = useRef(void 0);
2796
- const delivered = useRef(/* @__PURE__ */ new Set());
2797
- const hasRefresh = Boolean(props.onRefresh);
2411
+ const binding = bind ? options : void 0;
2412
+ const hasRefresh = Boolean(binding?.onRefresh);
2798
2413
  useEffect(() => {
2799
- const once = (status, callback) => {
2800
- const key = `${props.ticket.requestId}:${status}`;
2801
- if (delivered.current.has(key)) return;
2802
- delivered.current.add(key);
2803
- return callback();
2804
- };
2805
- const handle = openAuthLink({
2806
- ...props,
2807
- onConfirmed: (result) => once("confirmed", () => latest.current.onConfirmed?.(result)),
2808
- onRejected: (result) => once("rejected", () => latest.current.onRejected?.(result)),
2809
- onExpired: () => once("expired", () => latest.current.onExpired?.()),
2414
+ const common = {
2415
+ expiresAt: options.expiresAt,
2416
+ locale: latest.current.locale,
2417
+ submit: (code, context) => latest.current.submit(code, context),
2418
+ onSuccess: () => latest.current.onSuccess?.(),
2419
+ onClose: () => latest.current.onClose?.(),
2810
2420
  onError: (error) => latest.current.onError?.(error),
2811
- onRefresh: hasRefresh ? () => latest.current.onRefresh?.() : void 0,
2812
- onClose: () => latest.current.onClose?.()
2813
- });
2421
+ onExpired: () => latest.current.onExpired?.()
2422
+ };
2423
+ const handle = bind ? openBind({
2424
+ ...common,
2425
+ qr: binding.qr,
2426
+ expiresAt: binding.expiresAt,
2427
+ authLinkOrigin: binding.authLinkOrigin,
2428
+ onRefresh: hasRefresh ? (context) => latest.current.onRefresh(context) : void 0
2429
+ }) : openVerify(common);
2814
2430
  modal.current = handle;
2815
2431
  return () => {
2816
2432
  handle.destroy();
2817
2433
  if (modal.current === handle) modal.current = void 0;
2818
2434
  };
2819
- }, [props.ticket, props.client, props.intervalMs, props.mode, props.showQr, props.className, hasRefresh]);
2435
+ }, [bind, options.expiresAt, binding?.qr, binding?.authLinkOrigin, hasRefresh]);
2820
2436
  useEffect(() => {
2821
- modal.current?.setLocale(props.locale);
2822
- }, [props.locale]);
2823
- useEffect(() => {
2824
- modal.current?.setVariant(props.variant);
2825
- }, [props.variant]);
2437
+ modal.current?.setLocale(options.locale);
2438
+ }, [options.locale]);
2826
2439
  return null;
2827
2440
  }
2828
- function AuthLinkBind(props) {
2829
- return /* @__PURE__ */ jsx(Widget, { ...props, mode: "bind" });
2441
+ function AuthLinkVerify(props) {
2442
+ return /* @__PURE__ */ jsx(Prompt, { options: props, bind: false });
2830
2443
  }
2831
- function AuthLinkConfirm(props) {
2832
- return /* @__PURE__ */ jsx(Widget, { ...props, mode: "confirm" });
2444
+ function AuthLinkBind(props) {
2445
+ return /* @__PURE__ */ jsx(Prompt, { options: props, bind: true });
2833
2446
  }
2834
2447
  export {
2835
2448
  AuthLinkBind,
2836
- AuthLinkConfirm,
2837
- AuthLinkPasscode,
2838
- useAuthLink
2449
+ AuthLinkVerify
2839
2450
  };
2840
2451
  //# sourceMappingURL=react.js.map