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