@zoreal/oauth2-js 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -278,6 +278,11 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
278
278
  * the provider never renders a QR for it, so
279
279
  * nobody can turn a same-device link into a
280
280
  * static code to relay.
281
+ * GET /pair/start the same-device sign-in as a NAVIGATION:
282
+ * the /pair parameters as a query, plus
283
+ * request_id (the page's own token) and
284
+ * origin; answered with a redirect to the
285
+ * pairing's universal link, inside the tap
281
286
  * GET /pair/:id/status poll: pending | claimed |
282
287
  * approved (with code) | denied | expired |
283
288
  * enrolling. Over-polling cancels the request
@@ -305,7 +310,7 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
305
310
  * auth-code mode leaves it to the RP backend.
306
311
  */
307
312
  declare const WIRE_VERSION = 1;
308
- declare const SDK_VERSION = "0.1.17";
313
+ declare const SDK_VERSION = "0.1.18";
309
314
  declare const SDK_NAME = "@zoreal/oauth2-js";
310
315
  declare const DEFAULT_ISSUER = "https://id.zoreal.com";
311
316
  /** Pending TTL is short. Poll gently; over-polling cancels the request. */
@@ -413,7 +418,15 @@ declare function startPairing(issuer: string, params: StartPairingParams, signal
413
418
  * Throws FlowAbandonedError for the human outcomes (denied, expired,
414
419
  * enrolment abandoned) and OAuthFlowError for protocol ones.
415
420
  */
416
- declare function pollUntilApproved(issuer: string, requestId: string, onState?: (state: PairingState) => void, signal?: AbortSignal): Promise<string>;
421
+ declare function pollUntilApproved(issuer: string, requestId: string, onState?: (state: PairingState) => void, signal?: AbortSignal, options?: {
422
+ /**
423
+ * Same-device navigation only. The page starts polling while the
424
+ * provider is still answering the navigation that creates the pairing,
425
+ * so a "no such pairing" answer before this instant (epoch ms) is the
426
+ * pairing not existing YET, and is read as pending.
427
+ */
428
+ tolerateUnknownUntil?: number;
429
+ }): Promise<string>;
417
430
  /**
418
431
  * The code exchange, browser-direct mode only: a public client, PKCE and no
419
432
  * secret. What comes back can only ever be the pseudonymous tier, by
package/dist/index.d.ts CHANGED
@@ -278,6 +278,11 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
278
278
  * the provider never renders a QR for it, so
279
279
  * nobody can turn a same-device link into a
280
280
  * static code to relay.
281
+ * GET /pair/start the same-device sign-in as a NAVIGATION:
282
+ * the /pair parameters as a query, plus
283
+ * request_id (the page's own token) and
284
+ * origin; answered with a redirect to the
285
+ * pairing's universal link, inside the tap
281
286
  * GET /pair/:id/status poll: pending | claimed |
282
287
  * approved (with code) | denied | expired |
283
288
  * enrolling. Over-polling cancels the request
@@ -305,7 +310,7 @@ declare function mountPairingModal(state: PairingState, options: PairingModalOpt
305
310
  * auth-code mode leaves it to the RP backend.
306
311
  */
307
312
  declare const WIRE_VERSION = 1;
308
- declare const SDK_VERSION = "0.1.17";
313
+ declare const SDK_VERSION = "0.1.18";
309
314
  declare const SDK_NAME = "@zoreal/oauth2-js";
310
315
  declare const DEFAULT_ISSUER = "https://id.zoreal.com";
311
316
  /** Pending TTL is short. Poll gently; over-polling cancels the request. */
@@ -413,7 +418,15 @@ declare function startPairing(issuer: string, params: StartPairingParams, signal
413
418
  * Throws FlowAbandonedError for the human outcomes (denied, expired,
414
419
  * enrolment abandoned) and OAuthFlowError for protocol ones.
415
420
  */
416
- declare function pollUntilApproved(issuer: string, requestId: string, onState?: (state: PairingState) => void, signal?: AbortSignal): Promise<string>;
421
+ declare function pollUntilApproved(issuer: string, requestId: string, onState?: (state: PairingState) => void, signal?: AbortSignal, options?: {
422
+ /**
423
+ * Same-device navigation only. The page starts polling while the
424
+ * provider is still answering the navigation that creates the pairing,
425
+ * so a "no such pairing" answer before this instant (epoch ms) is the
426
+ * pairing not existing YET, and is read as pending.
427
+ */
428
+ tolerateUnknownUntil?: number;
429
+ }): Promise<string>;
417
430
  /**
418
431
  * The code exchange, browser-direct mode only: a public client, PKCE and no
419
432
  * secret. What comes back can only ever be the pseudonymous tier, by
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ function unsafeClaims(idToken) {
14
14
 
15
15
  // src/wire.ts
16
16
  var WIRE_VERSION = 1;
17
- var SDK_VERSION = "0.1.17";
17
+ var SDK_VERSION = "0.1.18";
18
18
  var SDK_NAME = "@zoreal/oauth2-js";
19
19
  var DEFAULT_ISSUER = "https://id.zoreal.com";
20
20
  var POLL_INTERVAL_MS = 2e3;
@@ -63,6 +63,20 @@ async function startPairing(issuer, params, signal) {
63
63
  }
64
64
  return body;
65
65
  }
66
+ function sameDeviceStartUrl(issuer, params) {
67
+ const query = new URLSearchParams();
68
+ const all = {
69
+ ...params,
70
+ code_challenge_method: "S256",
71
+ wire_version: WIRE_VERSION,
72
+ sdk: `${SDK_NAME}/${SDK_VERSION}`
73
+ };
74
+ for (const [key, value] of Object.entries(all)) {
75
+ if (value === void 0 || value === null || value === "") continue;
76
+ query.set(key, String(value));
77
+ }
78
+ return `${issuer}/pair/start?${query.toString()}`;
79
+ }
66
80
  var sleep = (ms, signal) => new Promise((resolve, reject) => {
67
81
  if (signal?.aborted) {
68
82
  reject(new DOMException("aborted", "AbortError"));
@@ -78,12 +92,17 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
78
92
  }, ms);
79
93
  signal?.addEventListener("abort", onAbort, { once: true });
80
94
  });
81
- async function pollUntilApproved(issuer, requestId, onState, signal) {
95
+ async function pollUntilApproved(issuer, requestId, onState, signal, options = {}) {
82
96
  for (; ; ) {
83
97
  const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
84
98
  signal
85
99
  });
86
100
  const body = await parseJson(response);
101
+ if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
102
+ onState?.({ status: "pending" });
103
+ await sleep(POLL_INTERVAL_MS, signal);
104
+ continue;
105
+ }
87
106
  if (!response.ok) {
88
107
  throw new OAuthFlowError(
89
108
  body.error ?? "server_error",
@@ -1688,11 +1707,154 @@ async function challengeS256(verifier) {
1688
1707
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1689
1708
  return base64url(new Uint8Array(digest));
1690
1709
  }
1710
+ function challengeS256Sync(verifier) {
1711
+ return base64url(sha256(new TextEncoder().encode(verifier)));
1712
+ }
1713
+ var K = new Uint32Array([
1714
+ 1116352408,
1715
+ 1899447441,
1716
+ 3049323471,
1717
+ 3921009573,
1718
+ 961987163,
1719
+ 1508970993,
1720
+ 2453635748,
1721
+ 2870763221,
1722
+ 3624381080,
1723
+ 310598401,
1724
+ 607225278,
1725
+ 1426881987,
1726
+ 1925078388,
1727
+ 2162078206,
1728
+ 2614888103,
1729
+ 3248222580,
1730
+ 3835390401,
1731
+ 4022224774,
1732
+ 264347078,
1733
+ 604807628,
1734
+ 770255983,
1735
+ 1249150122,
1736
+ 1555081692,
1737
+ 1996064986,
1738
+ 2554220882,
1739
+ 2821834349,
1740
+ 2952996808,
1741
+ 3210313671,
1742
+ 3336571891,
1743
+ 3584528711,
1744
+ 113926993,
1745
+ 338241895,
1746
+ 666307205,
1747
+ 773529912,
1748
+ 1294757372,
1749
+ 1396182291,
1750
+ 1695183700,
1751
+ 1986661051,
1752
+ 2177026350,
1753
+ 2456956037,
1754
+ 2730485921,
1755
+ 2820302411,
1756
+ 3259730800,
1757
+ 3345764771,
1758
+ 3516065817,
1759
+ 3600352804,
1760
+ 4094571909,
1761
+ 275423344,
1762
+ 430227734,
1763
+ 506948616,
1764
+ 659060556,
1765
+ 883997877,
1766
+ 958139571,
1767
+ 1322822218,
1768
+ 1537002063,
1769
+ 1747873779,
1770
+ 1955562222,
1771
+ 2024104815,
1772
+ 2227730452,
1773
+ 2361852424,
1774
+ 2428436474,
1775
+ 2756734187,
1776
+ 3204031479,
1777
+ 3329325298
1778
+ ]);
1779
+ var rotr = (x, n) => x >>> n | x << 32 - n;
1780
+ function sha256(message) {
1781
+ const H = new Uint32Array([
1782
+ 1779033703,
1783
+ 3144134277,
1784
+ 1013904242,
1785
+ 2773480762,
1786
+ 1359893119,
1787
+ 2600822924,
1788
+ 528734635,
1789
+ 1541459225
1790
+ ]);
1791
+ const length = message.length;
1792
+ const padded = new Uint8Array(length + 9 + 63 >> 6 << 6);
1793
+ padded.set(message);
1794
+ padded[length] = 128;
1795
+ const view = new DataView(padded.buffer);
1796
+ const bits = length * 8;
1797
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1798
+ view.setUint32(padded.length - 4, bits >>> 0);
1799
+ const W = new Uint32Array(64);
1800
+ for (let offset = 0; offset < padded.length; offset += 64) {
1801
+ for (let i = 0; i < 16; i++) W[i] = view.getUint32(offset + i * 4);
1802
+ for (let i = 16; i < 64; i++) {
1803
+ const w15 = W[i - 15];
1804
+ const w2 = W[i - 2];
1805
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
1806
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
1807
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
1808
+ }
1809
+ let [a, b, c, d, e, f, g, h] = H;
1810
+ for (let i = 0; i < 64; i++) {
1811
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1812
+ const ch = e & f ^ ~e & g;
1813
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
1814
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1815
+ const maj = a & b ^ a & c ^ b & c;
1816
+ const t2 = S0 + maj >>> 0;
1817
+ h = g;
1818
+ g = f;
1819
+ f = e;
1820
+ e = d + t1 >>> 0;
1821
+ d = c;
1822
+ c = b;
1823
+ b = a;
1824
+ a = t1 + t2 >>> 0;
1825
+ }
1826
+ H[0] = H[0] + a >>> 0;
1827
+ H[1] = H[1] + b >>> 0;
1828
+ H[2] = H[2] + c >>> 0;
1829
+ H[3] = H[3] + d >>> 0;
1830
+ H[4] = H[4] + e >>> 0;
1831
+ H[5] = H[5] + f >>> 0;
1832
+ H[6] = H[6] + g >>> 0;
1833
+ H[7] = H[7] + h >>> 0;
1834
+ }
1835
+ const out = new Uint8Array(32);
1836
+ const outView = new DataView(out.buffer);
1837
+ for (let i = 0; i < 8; i++) outView.setUint32(i * 4, H[i]);
1838
+ return out;
1839
+ }
1691
1840
  function generateState() {
1692
1841
  const bytes = new Uint8Array(16);
1693
1842
  crypto.getRandomValues(bytes);
1694
1843
  return base64url(bytes);
1695
1844
  }
1845
+ var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1846
+ function generateRequestId() {
1847
+ let out = "";
1848
+ const bytes = new Uint8Array(64);
1849
+ while (out.length < 32) {
1850
+ crypto.getRandomValues(bytes);
1851
+ for (const byte of bytes) {
1852
+ if (byte >= 248 || out.length === 32) continue;
1853
+ out += TOKEN_ALPHABET[byte % 62];
1854
+ }
1855
+ }
1856
+ return out;
1857
+ }
1696
1858
 
1697
1859
  // src/login.ts
1698
1860
  function startLogin(options) {
@@ -1722,109 +1884,144 @@ function startLogin(options) {
1722
1884
  const state = generateState();
1723
1885
  const nonce = generateState();
1724
1886
  try {
1725
- const started = await startPairing(
1726
- issuer,
1727
- {
1887
+ let code;
1888
+ let selectBy = "device";
1889
+ if (useAppLink && typeof window !== "undefined") {
1890
+ const requestId = generateRequestId();
1891
+ const startUrl = sameDeviceStartUrl(issuer, {
1728
1892
  client_id: options.clientId,
1729
1893
  scope: options.scope ?? "openid",
1730
1894
  state,
1731
1895
  nonce,
1732
- code_challenge: await challengeS256(verifier),
1896
+ code_challenge: challengeS256Sync(verifier),
1733
1897
  redirect_uri: flow === "auth-code" ? options.redirect_uri : void 0,
1734
1898
  acr_values: Array.isArray(options.acr_values) ? options.acr_values.join(" ") : options.acr_values,
1735
1899
  max_age: options.max_age,
1736
1900
  prompt: options.prompt,
1737
1901
  locale: options.locale,
1738
- display: useAppLink ? "link" : "qr"
1739
- },
1740
- controller.signal
1741
- );
1742
- let code;
1743
- let selectBy = "device";
1744
- if ("code" in started) {
1745
- code = started.code;
1746
- selectBy = "session";
1747
- } else {
1748
- selectBy = useAppLink ? "app_link" : "qr";
1749
- const requestId = started.request_id;
1750
- const qrBase = `${issuer}/pair/${encodeURIComponent(requestId)}/qr.svg`;
1751
- const animated = !useAppLink && started.display !== "legacy";
1752
- const qrRefreshSeconds = !animated ? void 0 : typeof started.qr_refresh_seconds === "number" && started.qr_refresh_seconds > 0 ? started.qr_refresh_seconds : DEFAULT_QR_REFRESH_SECONDS;
1902
+ request_id: requestId,
1903
+ origin: window.location.origin
1904
+ });
1905
+ selectBy = "app_link";
1753
1906
  surface.requestId = requestId;
1754
- surface.pairUrl = started.pair_url;
1755
- surface.qrUrl = qrBase;
1756
- surface.appLink = useAppLink;
1907
+ surface.pairUrl = startUrl;
1908
+ surface.appLink = true;
1757
1909
  const withSurface = (s) => ({
1758
1910
  ...s,
1759
- pairUrl: surface.pairUrl,
1760
- qrUrl: surface.qrUrl,
1761
- qrRefreshSeconds,
1762
- appLink: useAppLink,
1911
+ pairUrl: startUrl,
1912
+ appLink: true,
1763
1913
  intent,
1764
1914
  cancel
1765
1915
  });
1766
- let lastPolled = { status: "pending", expiresIn: started.expires_in };
1767
- const initial = withSurface(lastPolled);
1768
- options.onState?.(initial);
1769
- if ((options.pairingUI ?? "modal") === "modal" && !useAppLink) {
1770
- modal = mountPairingModal(initial, {
1771
- onCancel: cancel,
1772
- intent,
1773
- locale: options.locale,
1774
- theme: options.theme,
1775
- timeoutMs: options.pairingTimeoutMs
1776
- });
1777
- }
1778
- if (useAppLink && typeof window !== "undefined") {
1779
- window.location.assign(started.pair_url);
1780
- }
1781
- if (qrRefreshSeconds !== void 0) {
1782
- const periodMs = qrRefreshSeconds * 1e3;
1783
- let due = Date.now() + periodMs;
1784
- let timer;
1785
- let stopped = false;
1786
- const emit = () => {
1787
- timer = void 0;
1788
- surface.qrUrl = `${qrBase}?t=${Date.now()}`;
1789
- const next = withSurface(lastPolled);
1790
- modal?.update(next);
1791
- options.onState?.(next);
1792
- if (stopped) return;
1793
- due = Date.now() + periodMs;
1794
- timer = setTimeout(emit, periodMs);
1795
- };
1796
- const onVisible = () => {
1797
- if (document.visibilityState === "visible" && timer !== void 0 && Date.now() >= due) {
1798
- clearTimeout(timer);
1799
- emit();
1800
- }
1801
- };
1802
- const hasDocument = typeof document !== "undefined";
1803
- if (hasDocument) document.addEventListener("visibilitychange", onVisible);
1804
- stopRefresh = () => {
1805
- stopped = true;
1806
- if (timer !== void 0) clearTimeout(timer);
1807
- timer = void 0;
1808
- if (hasDocument) document.removeEventListener("visibilitychange", onVisible);
1809
- stopRefresh = () => {
1810
- };
1811
- };
1812
- timer = setTimeout(emit, Math.max(0, due - Date.now()));
1813
- }
1916
+ options.onState?.(withSurface({ status: "pending" }));
1917
+ window.location.assign(startUrl);
1814
1918
  code = await pollUntilApproved(
1815
1919
  issuer,
1816
1920
  requestId,
1817
- (s) => {
1818
- lastPolled = s;
1819
- if (s.status !== "pending") stopRefresh();
1820
- const next = withSurface(s);
1821
- modal?.update(next);
1822
- options.onState?.(next);
1921
+ (s) => options.onState?.(withSurface(s)),
1922
+ controller.signal,
1923
+ { tolerateUnknownUntil: Date.now() + 15e3 }
1924
+ );
1925
+ } else {
1926
+ const started = await startPairing(
1927
+ issuer,
1928
+ {
1929
+ client_id: options.clientId,
1930
+ scope: options.scope ?? "openid",
1931
+ state,
1932
+ nonce,
1933
+ code_challenge: await challengeS256(verifier),
1934
+ redirect_uri: flow === "auth-code" ? options.redirect_uri : void 0,
1935
+ acr_values: Array.isArray(options.acr_values) ? options.acr_values.join(" ") : options.acr_values,
1936
+ max_age: options.max_age,
1937
+ prompt: options.prompt,
1938
+ locale: options.locale,
1939
+ display: "qr"
1823
1940
  },
1824
1941
  controller.signal
1825
1942
  );
1943
+ if ("code" in started) {
1944
+ code = started.code;
1945
+ selectBy = "session";
1946
+ } else {
1947
+ selectBy = "qr";
1948
+ const requestId = started.request_id;
1949
+ const qrBase = `${issuer}/pair/${encodeURIComponent(requestId)}/qr.svg`;
1950
+ const animated = started.display !== "legacy";
1951
+ const qrRefreshSeconds = !animated ? void 0 : typeof started.qr_refresh_seconds === "number" && started.qr_refresh_seconds > 0 ? started.qr_refresh_seconds : DEFAULT_QR_REFRESH_SECONDS;
1952
+ surface.requestId = requestId;
1953
+ surface.pairUrl = started.pair_url;
1954
+ surface.qrUrl = qrBase;
1955
+ surface.appLink = false;
1956
+ const withSurface = (s) => ({
1957
+ ...s,
1958
+ pairUrl: surface.pairUrl,
1959
+ qrUrl: surface.qrUrl,
1960
+ qrRefreshSeconds,
1961
+ appLink: false,
1962
+ intent,
1963
+ cancel
1964
+ });
1965
+ let lastPolled = { status: "pending", expiresIn: started.expires_in };
1966
+ const initial = withSurface(lastPolled);
1967
+ options.onState?.(initial);
1968
+ if ((options.pairingUI ?? "modal") === "modal") {
1969
+ modal = mountPairingModal(initial, {
1970
+ onCancel: cancel,
1971
+ intent,
1972
+ locale: options.locale,
1973
+ theme: options.theme,
1974
+ timeoutMs: options.pairingTimeoutMs
1975
+ });
1976
+ }
1977
+ if (qrRefreshSeconds !== void 0) {
1978
+ const periodMs = qrRefreshSeconds * 1e3;
1979
+ let due = Date.now() + periodMs;
1980
+ let timer;
1981
+ let stopped = false;
1982
+ const emit = () => {
1983
+ timer = void 0;
1984
+ surface.qrUrl = `${qrBase}?t=${Date.now()}`;
1985
+ const next = withSurface(lastPolled);
1986
+ modal?.update(next);
1987
+ options.onState?.(next);
1988
+ if (stopped) return;
1989
+ due = Date.now() + periodMs;
1990
+ timer = setTimeout(emit, periodMs);
1991
+ };
1992
+ const onVisible = () => {
1993
+ if (document.visibilityState === "visible" && timer !== void 0 && Date.now() >= due) {
1994
+ clearTimeout(timer);
1995
+ emit();
1996
+ }
1997
+ };
1998
+ const hasDocument = typeof document !== "undefined";
1999
+ if (hasDocument) document.addEventListener("visibilitychange", onVisible);
2000
+ stopRefresh = () => {
2001
+ stopped = true;
2002
+ if (timer !== void 0) clearTimeout(timer);
2003
+ timer = void 0;
2004
+ if (hasDocument) document.removeEventListener("visibilitychange", onVisible);
2005
+ stopRefresh = () => {
2006
+ };
2007
+ };
2008
+ timer = setTimeout(emit, Math.max(0, due - Date.now()));
2009
+ }
2010
+ code = await pollUntilApproved(
2011
+ issuer,
2012
+ requestId,
2013
+ (s) => {
2014
+ lastPolled = s;
2015
+ if (s.status !== "pending") stopRefresh();
2016
+ const next = withSurface(s);
2017
+ modal?.update(next);
2018
+ options.onState?.(next);
2019
+ },
2020
+ controller.signal
2021
+ );
2022
+ }
2023
+ teardown();
1826
2024
  }
1827
- teardown();
1828
2025
  if (flow === "auth-code") {
1829
2026
  const response2 = {
1830
2027
  code,