@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/CHANGELOG.md +28 -0
- package/README.md +120 -117
- package/dist/index.js +203 -684
- package/dist/index.js.map +4 -4
- package/dist/react.js +227 -616
- package/dist/react.js.map +4 -4
- package/dist/styles.css +5 -49
- package/dist/types/core.d.ts +4 -38
- package/dist/types/index.d.ts +3 -7
- package/dist/types/modal.d.ts +3 -21
- package/dist/types/react.d.ts +4 -15
- package/dist/types/types.d.ts +35 -62
- package/dist/types/view.d.ts +6 -13
- package/dist/types/vue.d.ts +5 -16
- package/dist/vue.js +231 -656
- package/dist/vue.js.map +4 -4
- package/docs/README.en.md +41 -40
- package/docs/README.ja.md +41 -40
- package/docs/README.zh-CN.md +41 -40
- package/examples/react-client.tsx +9 -21
- package/examples/vue-client.vue +9 -26
- package/package.json +2 -2
- package/dist/types/passcode.d.ts +0 -29
package/dist/react.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
// src/react.tsx
|
|
5
|
-
import {
|
|
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
|
|
1652
|
-
|
|
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
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
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
|
-
|
|
1660
|
+
payload = JSON.parse(ticket.qr);
|
|
1661
|
+
origin = new URL(String(payload.origin));
|
|
1677
1662
|
} catch {
|
|
1678
|
-
|
|
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
|
-
|
|
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,
|
|
2267
|
-
const
|
|
2268
|
-
const
|
|
2269
|
-
const
|
|
2270
|
-
const
|
|
2271
|
-
const
|
|
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" },
|
|
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
|
|
2132
|
+
if (input) void options.submitCode(input.value);
|
|
2307
2133
|
} },
|
|
2308
|
-
node("label", { for: "authlink-
|
|
2134
|
+
node("label", { for: "authlink-passcode" }, l.otpCode),
|
|
2309
2135
|
node("input", {
|
|
2310
|
-
id: "authlink-
|
|
2311
|
-
name:
|
|
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
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
"
|
|
2355
|
-
"
|
|
2356
|
-
|
|
2357
|
-
"aria-
|
|
2358
|
-
|
|
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
|
|
2194
|
+
function element(value) {
|
|
2363
2195
|
if (typeof value === "string") return document.createTextNode(value);
|
|
2364
|
-
|
|
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(
|
|
2201
|
+
result.append(...value.children.map(element));
|
|
2371
2202
|
return result;
|
|
2372
2203
|
}
|
|
2373
|
-
function
|
|
2204
|
+
function open(options, mode) {
|
|
2374
2205
|
if (typeof document === "undefined" || !document.body) throw new AuthLinkError("BROWSER_REQUIRED");
|
|
2375
|
-
|
|
2376
|
-
const
|
|
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,
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
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
|
-
|
|
2236
|
+
options.onError?.(value);
|
|
2610
2237
|
} catch {
|
|
2611
|
-
throw new AuthLinkError("INVALID_TICKET");
|
|
2612
2238
|
}
|
|
2613
|
-
|
|
2614
|
-
}
|
|
2615
|
-
const
|
|
2616
|
-
|
|
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
|
|
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 =
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
|
|
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
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
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
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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
|
|
2660
|
-
if (closed
|
|
2661
|
-
if (
|
|
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 =
|
|
2322
|
+
error = null;
|
|
2323
|
+
const current = new AbortController();
|
|
2324
|
+
abort = current;
|
|
2673
2325
|
render();
|
|
2674
2326
|
try {
|
|
2675
|
-
const
|
|
2676
|
-
if (
|
|
2677
|
-
|
|
2678
|
-
|
|
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 && !
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
-
|
|
2690
|
-
|
|
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
|
-
|
|
2378
|
+
try {
|
|
2379
|
+
dialog.showModal();
|
|
2380
|
+
} catch (cause) {
|
|
2381
|
+
dispose(false);
|
|
2382
|
+
throw cause;
|
|
2383
|
+
}
|
|
2701
2384
|
content.querySelector("input")?.focus();
|
|
2702
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
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
|
|
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
|
|
2797
|
-
const hasRefresh = Boolean(
|
|
2411
|
+
const binding = bind ? options : void 0;
|
|
2412
|
+
const hasRefresh = Boolean(binding?.onRefresh);
|
|
2798
2413
|
useEffect(() => {
|
|
2799
|
-
const
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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
|
-
|
|
2812
|
-
|
|
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
|
-
}, [
|
|
2435
|
+
}, [bind, options.expiresAt, binding?.qr, binding?.authLinkOrigin, hasRefresh]);
|
|
2820
2436
|
useEffect(() => {
|
|
2821
|
-
modal.current?.setLocale(
|
|
2822
|
-
}, [
|
|
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
|
|
2829
|
-
return /* @__PURE__ */ jsx(
|
|
2441
|
+
function AuthLinkVerify(props) {
|
|
2442
|
+
return /* @__PURE__ */ jsx(Prompt, { options: props, bind: false });
|
|
2830
2443
|
}
|
|
2831
|
-
function
|
|
2832
|
-
return /* @__PURE__ */ jsx(
|
|
2444
|
+
function AuthLinkBind(props) {
|
|
2445
|
+
return /* @__PURE__ */ jsx(Prompt, { options: props, bind: true });
|
|
2833
2446
|
}
|
|
2834
2447
|
export {
|
|
2835
2448
|
AuthLinkBind,
|
|
2836
|
-
|
|
2837
|
-
AuthLinkPasscode,
|
|
2838
|
-
useAuthLink
|
|
2449
|
+
AuthLinkVerify
|
|
2839
2450
|
};
|
|
2840
2451
|
//# sourceMappingURL=react.js.map
|