@zoreal/oauth2-react 0.2.16 → 0.2.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.js CHANGED
@@ -5,7 +5,7 @@ import { createContext, useContext, useMemo, useState as useState2 } from "react
5
5
 
6
6
  // src/wire.ts
7
7
  var WIRE_VERSION = 1;
8
- var SDK_VERSION = "0.2.16";
8
+ var SDK_VERSION = "0.2.18";
9
9
  var DEFAULT_ISSUER = "https://id.zoreal.com";
10
10
  var POLL_INTERVAL_MS = 2e3;
11
11
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1655,6 +1655,20 @@ async function startPairing(issuer, params) {
1655
1655
  }
1656
1656
  return body;
1657
1657
  }
1658
+ function sameDeviceStartUrl(issuer, params) {
1659
+ const query = new URLSearchParams();
1660
+ const all = {
1661
+ ...params,
1662
+ code_challenge_method: "S256",
1663
+ wire_version: WIRE_VERSION,
1664
+ sdk: `@zoreal/oauth2-react/${SDK_VERSION}`
1665
+ };
1666
+ for (const [key, value] of Object.entries(all)) {
1667
+ if (value === void 0 || value === null || value === "") continue;
1668
+ query.set(key, String(value));
1669
+ }
1670
+ return `${issuer}/pair/start?${query.toString()}`;
1671
+ }
1658
1672
  function qrRefreshSecondsOf(started) {
1659
1673
  const seconds = started.qr_refresh_seconds;
1660
1674
  return typeof seconds === "number" && seconds > 0 ? seconds : DEFAULT_QR_REFRESH_SECONDS;
@@ -1677,6 +1691,8 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
1677
1691
  }, ms);
1678
1692
  signal?.addEventListener("abort", onAbort, { once: true });
1679
1693
  });
1694
+ var SETTLE_MS = 1500;
1695
+ var NETWORK_FAILURES_TOLERATED = 4;
1680
1696
  async function pollUntilApproved(issuer, requestId, onState, signal, options = {}) {
1681
1697
  let last = { status: "pending" };
1682
1698
  const emit = (state) => {
@@ -1705,12 +1721,33 @@ async function pollUntilApproved(issuer, requestId, onState, signal, options = {
1705
1721
  })().catch(() => {
1706
1722
  });
1707
1723
  };
1724
+ const settlingUntil = options.tolerateUnknownUntil ?? 0;
1725
+ let networkFailures = 0;
1708
1726
  try {
1727
+ if (settlingUntil > Date.now()) await sleep(SETTLE_MS, signal);
1709
1728
  for (; ; ) {
1710
- const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
1711
- signal
1712
- });
1729
+ let response;
1730
+ try {
1731
+ response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
1732
+ signal
1733
+ });
1734
+ networkFailures = 0;
1735
+ } catch (e) {
1736
+ if (e instanceof DOMException && e.name === "AbortError") throw e;
1737
+ networkFailures += 1;
1738
+ if (settlingUntil > Date.now() || networkFailures <= NETWORK_FAILURES_TOLERATED) {
1739
+ emit({ ...last, status: last.status });
1740
+ await sleep(POLL_INTERVAL_MS, signal);
1741
+ continue;
1742
+ }
1743
+ throw e;
1744
+ }
1713
1745
  const body = await parseJson(response);
1746
+ if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
1747
+ emit({ status: "pending" });
1748
+ await sleep(POLL_INTERVAL_MS, signal);
1749
+ continue;
1750
+ }
1714
1751
  if (!response.ok) {
1715
1752
  throw new OAuthFlowError(
1716
1753
  body.error ?? "server_error",
@@ -1792,11 +1829,154 @@ async function challengeS256(verifier) {
1792
1829
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1793
1830
  return base64url(new Uint8Array(digest));
1794
1831
  }
1832
+ function challengeS256Sync(verifier) {
1833
+ return base64url(sha256(new TextEncoder().encode(verifier)));
1834
+ }
1835
+ var K = new Uint32Array([
1836
+ 1116352408,
1837
+ 1899447441,
1838
+ 3049323471,
1839
+ 3921009573,
1840
+ 961987163,
1841
+ 1508970993,
1842
+ 2453635748,
1843
+ 2870763221,
1844
+ 3624381080,
1845
+ 310598401,
1846
+ 607225278,
1847
+ 1426881987,
1848
+ 1925078388,
1849
+ 2162078206,
1850
+ 2614888103,
1851
+ 3248222580,
1852
+ 3835390401,
1853
+ 4022224774,
1854
+ 264347078,
1855
+ 604807628,
1856
+ 770255983,
1857
+ 1249150122,
1858
+ 1555081692,
1859
+ 1996064986,
1860
+ 2554220882,
1861
+ 2821834349,
1862
+ 2952996808,
1863
+ 3210313671,
1864
+ 3336571891,
1865
+ 3584528711,
1866
+ 113926993,
1867
+ 338241895,
1868
+ 666307205,
1869
+ 773529912,
1870
+ 1294757372,
1871
+ 1396182291,
1872
+ 1695183700,
1873
+ 1986661051,
1874
+ 2177026350,
1875
+ 2456956037,
1876
+ 2730485921,
1877
+ 2820302411,
1878
+ 3259730800,
1879
+ 3345764771,
1880
+ 3516065817,
1881
+ 3600352804,
1882
+ 4094571909,
1883
+ 275423344,
1884
+ 430227734,
1885
+ 506948616,
1886
+ 659060556,
1887
+ 883997877,
1888
+ 958139571,
1889
+ 1322822218,
1890
+ 1537002063,
1891
+ 1747873779,
1892
+ 1955562222,
1893
+ 2024104815,
1894
+ 2227730452,
1895
+ 2361852424,
1896
+ 2428436474,
1897
+ 2756734187,
1898
+ 3204031479,
1899
+ 3329325298
1900
+ ]);
1901
+ var rotr = (x, n) => x >>> n | x << 32 - n;
1902
+ function sha256(message) {
1903
+ const H = new Uint32Array([
1904
+ 1779033703,
1905
+ 3144134277,
1906
+ 1013904242,
1907
+ 2773480762,
1908
+ 1359893119,
1909
+ 2600822924,
1910
+ 528734635,
1911
+ 1541459225
1912
+ ]);
1913
+ const length = message.length;
1914
+ const padded = new Uint8Array(length + 9 + 63 >> 6 << 6);
1915
+ padded.set(message);
1916
+ padded[length] = 128;
1917
+ const view = new DataView(padded.buffer);
1918
+ const bits = length * 8;
1919
+ view.setUint32(padded.length - 8, Math.floor(bits / 4294967296));
1920
+ view.setUint32(padded.length - 4, bits >>> 0);
1921
+ const W = new Uint32Array(64);
1922
+ for (let offset = 0; offset < padded.length; offset += 64) {
1923
+ for (let i = 0; i < 16; i++) W[i] = view.getUint32(offset + i * 4);
1924
+ for (let i = 16; i < 64; i++) {
1925
+ const w15 = W[i - 15];
1926
+ const w2 = W[i - 2];
1927
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
1928
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
1929
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
1930
+ }
1931
+ let [a, b, c, d, e, f, g, h] = H;
1932
+ for (let i = 0; i < 64; i++) {
1933
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1934
+ const ch = e & f ^ ~e & g;
1935
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
1936
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1937
+ const maj = a & b ^ a & c ^ b & c;
1938
+ const t2 = S0 + maj >>> 0;
1939
+ h = g;
1940
+ g = f;
1941
+ f = e;
1942
+ e = d + t1 >>> 0;
1943
+ d = c;
1944
+ c = b;
1945
+ b = a;
1946
+ a = t1 + t2 >>> 0;
1947
+ }
1948
+ H[0] = H[0] + a >>> 0;
1949
+ H[1] = H[1] + b >>> 0;
1950
+ H[2] = H[2] + c >>> 0;
1951
+ H[3] = H[3] + d >>> 0;
1952
+ H[4] = H[4] + e >>> 0;
1953
+ H[5] = H[5] + f >>> 0;
1954
+ H[6] = H[6] + g >>> 0;
1955
+ H[7] = H[7] + h >>> 0;
1956
+ }
1957
+ const out = new Uint8Array(32);
1958
+ const outView = new DataView(out.buffer);
1959
+ for (let i = 0; i < 8; i++) outView.setUint32(i * 4, H[i]);
1960
+ return out;
1961
+ }
1795
1962
  function generateState() {
1796
1963
  const bytes = new Uint8Array(16);
1797
1964
  crypto.getRandomValues(bytes);
1798
1965
  return base64url(bytes);
1799
1966
  }
1967
+ var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1968
+ function generateRequestId() {
1969
+ let out = "";
1970
+ const bytes = new Uint8Array(64);
1971
+ while (out.length < 32) {
1972
+ crypto.getRandomValues(bytes);
1973
+ for (const byte of bytes) {
1974
+ if (byte >= 248 || out.length === 32) continue;
1975
+ out += TOKEN_ALPHABET[byte % 62];
1976
+ }
1977
+ }
1978
+ return out;
1979
+ }
1800
1980
 
1801
1981
  // src/useZorealLogin.ts
1802
1982
  function useZorealFlow(options) {
@@ -1829,76 +2009,119 @@ function useZorealFlow(options) {
1829
2009
  const useAppLink = display === "link";
1830
2010
  const intent = resolveIntent(opts.intent, opts.scope, opts.acr_values);
1831
2011
  try {
1832
- const started = await startPairing(issuer, {
1833
- client_id: clientId,
1834
- scope: opts.scope ?? "openid",
1835
- state,
1836
- nonce,
1837
- code_challenge: await challengeS256(verifier),
1838
- redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
1839
- acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
1840
- max_age: opts.max_age,
1841
- prompt: opts.prompt,
1842
- locale,
1843
- display
1844
- });
1845
2012
  let code;
1846
2013
  let selectBy = "device";
1847
- if ("code" in started) {
1848
- code = started.code;
1849
- selectBy = "session";
1850
- } else {
1851
- selectBy = useAppLink ? "app_link" : "qr";
1852
- const qrRefreshSeconds = qrRefreshSecondsOf(started);
2014
+ if (useAppLink) {
2015
+ const requestId = generateRequestId();
2016
+ const startUrl = sameDeviceStartUrl(issuer, {
2017
+ client_id: clientId,
2018
+ scope: opts.scope ?? "openid",
2019
+ state,
2020
+ nonce,
2021
+ code_challenge: challengeS256Sync(verifier),
2022
+ redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
2023
+ acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
2024
+ max_age: opts.max_age,
2025
+ prompt: opts.prompt,
2026
+ locale,
2027
+ request_id: requestId,
2028
+ origin: window.location.origin
2029
+ });
2030
+ selectBy = "app_link";
1853
2031
  const cancel = () => {
1854
2032
  controller.abort();
1855
2033
  setPairing(null);
1856
- publishRef.current?.(null);
1857
2034
  };
1858
- const surface = {
1859
- pairUrl: started.pair_url,
1860
- appLink: useAppLink,
1861
- intent,
1862
- cancel,
1863
- // The app link has no QR and therefore no cadence to report.
1864
- ...useAppLink ? null : { qrRefreshSeconds }
1865
- };
1866
- let qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;
2035
+ const surface = { pairUrl: startUrl, appLink: true, intent, cancel };
1867
2036
  const active = {
1868
- requestId: started.request_id,
1869
- pairUrl: surface.pairUrl,
1870
- qrUrl,
1871
- state: { status: "pending", expiresIn: started.expires_in, qrUrl, ...surface },
1872
- appLink: useAppLink,
2037
+ requestId,
2038
+ pairUrl: startUrl,
2039
+ qrUrl: "",
2040
+ state: { status: "pending", ...surface },
2041
+ appLink: true,
1873
2042
  cancel
1874
2043
  };
1875
2044
  setPairing(active);
1876
- if (!useAppLink) {
1877
- publishRef.current?.({ state: active.state, qrUrl, intent, cancel });
1878
- }
1879
2045
  opts.onPairingStateChange?.(active.state);
1880
- if (useAppLink) {
1881
- window.location.assign(started.pair_url);
1882
- }
2046
+ window.location.assign(startUrl);
1883
2047
  code = await pollUntilApproved(
1884
2048
  issuer,
1885
- started.request_id,
2049
+ requestId,
1886
2050
  (s) => {
1887
- if (s.qrUrl) qrUrl = s.qrUrl;
1888
- const enriched = { ...s, ...surface, qrUrl };
1889
- setPairing(
1890
- (p) => p && p.requestId === started.request_id ? { ...p, qrUrl, state: enriched } : p
1891
- );
1892
- if (!useAppLink) {
1893
- publishRef.current?.({ state: enriched, qrUrl, intent, cancel });
1894
- }
2051
+ const enriched = { ...s, ...surface };
2052
+ setPairing((p) => p && p.requestId === requestId ? { ...p, state: enriched } : p);
1895
2053
  opts.onPairingStateChange?.(enriched);
1896
2054
  },
1897
2055
  controller.signal,
1898
- // The app link has no QR to animate, and the provider answers 404
1899
- // for one, so the frames are asked for only on the QR surface.
1900
- { qrRefreshSeconds: useAppLink ? void 0 : qrRefreshSeconds }
2056
+ { tolerateUnknownUntil: Date.now() + 15e3 }
1901
2057
  );
2058
+ } else {
2059
+ const started = await startPairing(issuer, {
2060
+ client_id: clientId,
2061
+ scope: opts.scope ?? "openid",
2062
+ state,
2063
+ nonce,
2064
+ code_challenge: await challengeS256(verifier),
2065
+ redirect_uri: flow === "auth-code" ? opts.redirect_uri : void 0,
2066
+ acr_values: Array.isArray(opts.acr_values) ? opts.acr_values.join(" ") : opts.acr_values,
2067
+ max_age: opts.max_age,
2068
+ prompt: opts.prompt,
2069
+ locale,
2070
+ display: "qr"
2071
+ });
2072
+ if ("code" in started) {
2073
+ code = started.code;
2074
+ selectBy = "session";
2075
+ } else {
2076
+ selectBy = "qr";
2077
+ const qrRefreshSeconds = qrRefreshSecondsOf(started);
2078
+ const cancel = () => {
2079
+ controller.abort();
2080
+ setPairing(null);
2081
+ publishRef.current?.(null);
2082
+ };
2083
+ const surface = {
2084
+ pairUrl: started.pair_url,
2085
+ appLink: false,
2086
+ intent,
2087
+ cancel,
2088
+ qrRefreshSeconds
2089
+ };
2090
+ let qrUrl = `${issuer}/pair/${encodeURIComponent(started.request_id)}/qr.svg`;
2091
+ const active = {
2092
+ requestId: started.request_id,
2093
+ pairUrl: surface.pairUrl,
2094
+ qrUrl,
2095
+ state: { status: "pending", expiresIn: started.expires_in, qrUrl, ...surface },
2096
+ appLink: false,
2097
+ cancel
2098
+ };
2099
+ setPairing(active);
2100
+ if (!useAppLink) {
2101
+ publishRef.current?.({ state: active.state, qrUrl, intent, cancel });
2102
+ }
2103
+ opts.onPairingStateChange?.(active.state);
2104
+ if (useAppLink) {
2105
+ window.location.assign(started.pair_url);
2106
+ }
2107
+ code = await pollUntilApproved(
2108
+ issuer,
2109
+ started.request_id,
2110
+ (s) => {
2111
+ if (s.qrUrl) qrUrl = s.qrUrl;
2112
+ const enriched = { ...s, ...surface, qrUrl };
2113
+ setPairing(
2114
+ (p) => p && p.requestId === started.request_id ? { ...p, qrUrl, state: enriched } : p
2115
+ );
2116
+ if (!useAppLink) {
2117
+ publishRef.current?.({ state: enriched, qrUrl, intent, cancel });
2118
+ }
2119
+ opts.onPairingStateChange?.(enriched);
2120
+ },
2121
+ controller.signal,
2122
+ { qrRefreshSeconds }
2123
+ );
2124
+ }
1902
2125
  }
1903
2126
  setPairing(null);
1904
2127
  publishRef.current?.(null);