@zoreal/oauth2-react 0.2.17 → 0.2.19

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.17";
8
+ var SDK_VERSION = "0.2.19";
9
9
  var DEFAULT_ISSUER = "https://id.zoreal.com";
10
10
  var POLL_INTERVAL_MS = 2e3;
11
11
  var POLL_INTERVAL_ENROLLING_MS = 5e3;
@@ -1600,6 +1600,76 @@ import { useMemo as useMemo2, useState as useState4 } from "react";
1600
1600
  // src/useZorealLogin.ts
1601
1601
  import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
1602
1602
 
1603
+ // src/return.ts
1604
+ var PREFIX2 = "zoreal:oauth2:return:";
1605
+ var DONE = "zoreal:oauth2:done:";
1606
+ var MAX_AGE_MS = 10 * 60 * 1e3;
1607
+ function storage() {
1608
+ try {
1609
+ return typeof localStorage === "undefined" ? null : localStorage;
1610
+ } catch {
1611
+ return null;
1612
+ }
1613
+ }
1614
+ function saveReturnFlow(flow) {
1615
+ try {
1616
+ storage()?.setItem(PREFIX2 + flow.requestId, JSON.stringify(flow));
1617
+ } catch {
1618
+ }
1619
+ }
1620
+ function peekReturnFlow(requestId) {
1621
+ const store = storage();
1622
+ if (!store) return null;
1623
+ const raw = store.getItem(PREFIX2 + requestId);
1624
+ if (!raw) return null;
1625
+ try {
1626
+ const flow = JSON.parse(raw);
1627
+ if (flow.v !== 1 || flow.requestId !== requestId) return null;
1628
+ if (Date.now() - flow.createdAt > MAX_AGE_MS) return null;
1629
+ return flow;
1630
+ } catch {
1631
+ return null;
1632
+ }
1633
+ }
1634
+ function forgetReturnFlow(requestId) {
1635
+ try {
1636
+ storage()?.removeItem(PREFIX2 + requestId);
1637
+ } catch {
1638
+ }
1639
+ if (pending === requestId) pending = null;
1640
+ }
1641
+ function markReturnDone(requestId) {
1642
+ try {
1643
+ const store = storage();
1644
+ store?.setItem(DONE + requestId, String(Date.now()));
1645
+ store?.removeItem(PREFIX2 + requestId);
1646
+ } catch {
1647
+ }
1648
+ }
1649
+ function isReturnDone(requestId) {
1650
+ return storage()?.getItem(DONE + requestId) !== null && storage()?.getItem(DONE + requestId) !== void 0;
1651
+ }
1652
+ function returnToUrl() {
1653
+ if (typeof window === "undefined") return void 0;
1654
+ const { href } = window.location;
1655
+ const hash = href.indexOf("#");
1656
+ return hash === -1 ? href : href.slice(0, hash);
1657
+ }
1658
+ var RETURN_MARK = /(?:^|[#&])zoreal_return=([A-Za-z0-9]{32})(?:&|$)/;
1659
+ var pending = null;
1660
+ function pendingReturnId() {
1661
+ if (pending) return pending;
1662
+ if (typeof window === "undefined") return null;
1663
+ const match = RETURN_MARK.exec(window.location.hash);
1664
+ if (!match) return null;
1665
+ pending = match[1];
1666
+ try {
1667
+ window.history.replaceState(window.history.state, "", returnToUrl());
1668
+ } catch {
1669
+ }
1670
+ return pending;
1671
+ }
1672
+
1603
1673
  // src/jwt.ts
1604
1674
  function unsafeClaims(idToken) {
1605
1675
  try {
@@ -1691,6 +1761,8 @@ var sleep = (ms, signal) => new Promise((resolve, reject) => {
1691
1761
  }, ms);
1692
1762
  signal?.addEventListener("abort", onAbort, { once: true });
1693
1763
  });
1764
+ var SETTLE_MS = 1500;
1765
+ var NETWORK_FAILURES_TOLERATED = 4;
1694
1766
  async function pollUntilApproved(issuer, requestId, onState, signal, options = {}) {
1695
1767
  let last = { status: "pending" };
1696
1768
  const emit = (state) => {
@@ -1719,11 +1791,27 @@ async function pollUntilApproved(issuer, requestId, onState, signal, options = {
1719
1791
  })().catch(() => {
1720
1792
  });
1721
1793
  };
1794
+ const settlingUntil = options.tolerateUnknownUntil ?? 0;
1795
+ let networkFailures = 0;
1722
1796
  try {
1797
+ if (settlingUntil > Date.now()) await sleep(SETTLE_MS, signal);
1723
1798
  for (; ; ) {
1724
- const response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
1725
- signal
1726
- });
1799
+ let response;
1800
+ try {
1801
+ response = await fetch(`${issuer}/pair/${encodeURIComponent(requestId)}/status`, {
1802
+ signal
1803
+ });
1804
+ networkFailures = 0;
1805
+ } catch (e) {
1806
+ if (e instanceof DOMException && e.name === "AbortError") throw e;
1807
+ networkFailures += 1;
1808
+ if (settlingUntil > Date.now() || networkFailures <= NETWORK_FAILURES_TOLERATED) {
1809
+ emit({ ...last, status: last.status });
1810
+ await sleep(POLL_INTERVAL_MS, signal);
1811
+ continue;
1812
+ }
1813
+ throw e;
1814
+ }
1727
1815
  const body = await parseJson(response);
1728
1816
  if (response.status === 404 && (options.tolerateUnknownUntil ?? 0) > Date.now()) {
1729
1817
  emit({ status: "pending" });
@@ -1961,6 +2049,7 @@ function generateRequestId() {
1961
2049
  }
1962
2050
 
1963
2051
  // src/useZorealLogin.ts
2052
+ var resumedReturns = /* @__PURE__ */ new Set();
1964
2053
  function useZorealFlow(options) {
1965
2054
  const { clientId, issuer, locale } = useZorealOAuth();
1966
2055
  const [pairing, setPairing] = useState3(null);
@@ -1970,6 +2059,7 @@ function useZorealFlow(options) {
1970
2059
  const abortRef = useRef2(null);
1971
2060
  const optionsRef = useRef2(options);
1972
2061
  optionsRef.current = options;
2062
+ const closedByPerson = useRef2(false);
1973
2063
  useEffect2(
1974
2064
  () => () => {
1975
2065
  abortRef.current?.abort();
@@ -1977,6 +2067,61 @@ function useZorealFlow(options) {
1977
2067
  },
1978
2068
  []
1979
2069
  );
2070
+ useEffect2(() => {
2071
+ const id = pendingReturnId();
2072
+ if (!id || resumedReturns.has(id)) return;
2073
+ const saved = peekReturnFlow(id);
2074
+ if (!saved || saved.clientId !== clientId) return;
2075
+ forgetReturnFlow(id);
2076
+ resumedReturns.add(id);
2077
+ const controller = new AbortController();
2078
+ abortRef.current = controller;
2079
+ void (async () => {
2080
+ const opts = optionsRef.current;
2081
+ try {
2082
+ const code = await pollUntilApproved(issuer, id, void 0, controller.signal, {
2083
+ tolerateUnknownUntil: Date.now() + 5e3
2084
+ });
2085
+ if (saved.flow === "auth-code") {
2086
+ opts.onCode?.({
2087
+ code,
2088
+ scope: saved.scope,
2089
+ app_state: saved.appState,
2090
+ code_verifier: saved.verifier,
2091
+ nonce: saved.nonce
2092
+ });
2093
+ } else {
2094
+ const tokens = await exchangeCode(issuer, {
2095
+ code,
2096
+ code_verifier: saved.verifier,
2097
+ client_id: clientId
2098
+ });
2099
+ const claims = unsafeClaims(tokens.id_token);
2100
+ opts.onCredential?.({
2101
+ credential: tokens.id_token,
2102
+ clientId,
2103
+ select_by: "app_link",
2104
+ acr: claims.acr ?? "zoreal.device"
2105
+ });
2106
+ }
2107
+ markReturnDone(id);
2108
+ } catch (e) {
2109
+ if (e instanceof DOMException && e.name === "AbortError") return;
2110
+ if (e instanceof FlowAbandonedError) {
2111
+ opts.onNonOAuthError?.(e.reason);
2112
+ return;
2113
+ }
2114
+ if (e instanceof OAuthFlowError) {
2115
+ opts.onError?.({ error: e.error, description: e.description });
2116
+ return;
2117
+ }
2118
+ opts.onNonOAuthError?.({
2119
+ type: "unknown",
2120
+ description: e instanceof Error ? e.message : String(e)
2121
+ });
2122
+ }
2123
+ })();
2124
+ }, [clientId, issuer]);
1980
2125
  const login = useCallback(() => {
1981
2126
  const opts = optionsRef.current;
1982
2127
  const run = async () => {
@@ -1993,8 +2138,23 @@ function useZorealFlow(options) {
1993
2138
  try {
1994
2139
  let code;
1995
2140
  let selectBy = "device";
2141
+ let returnId = null;
1996
2142
  if (useAppLink) {
1997
2143
  const requestId = generateRequestId();
2144
+ saveReturnFlow({
2145
+ v: 1,
2146
+ issuer,
2147
+ clientId,
2148
+ flow,
2149
+ verifier,
2150
+ nonce,
2151
+ state,
2152
+ scope: opts.scope ?? "openid",
2153
+ appState: opts.app_state,
2154
+ requestId,
2155
+ createdAt: Date.now()
2156
+ });
2157
+ returnId = requestId;
1998
2158
  const startUrl = sameDeviceStartUrl(issuer, {
1999
2159
  client_id: clientId,
2000
2160
  scope: opts.scope ?? "openid",
@@ -2007,10 +2167,12 @@ function useZorealFlow(options) {
2007
2167
  prompt: opts.prompt,
2008
2168
  locale,
2009
2169
  request_id: requestId,
2010
- origin: window.location.origin
2170
+ origin: window.location.origin,
2171
+ return_to: returnToUrl()
2011
2172
  });
2012
2173
  selectBy = "app_link";
2013
2174
  const cancel = () => {
2175
+ closedByPerson.current = true;
2014
2176
  controller.abort();
2015
2177
  setPairing(null);
2016
2178
  };
@@ -2037,6 +2199,9 @@ function useZorealFlow(options) {
2037
2199
  controller.signal,
2038
2200
  { tolerateUnknownUntil: Date.now() + 15e3 }
2039
2201
  );
2202
+ if (isReturnDone(requestId)) {
2203
+ throw new DOMException("aborted", "AbortError");
2204
+ }
2040
2205
  } else {
2041
2206
  const started = await startPairing(issuer, {
2042
2207
  client_id: clientId,
@@ -2058,6 +2223,7 @@ function useZorealFlow(options) {
2058
2223
  selectBy = "qr";
2059
2224
  const qrRefreshSeconds = qrRefreshSecondsOf(started);
2060
2225
  const cancel = () => {
2226
+ closedByPerson.current = true;
2061
2227
  controller.abort();
2062
2228
  setPairing(null);
2063
2229
  publishRef.current?.(null);
@@ -2107,6 +2273,7 @@ function useZorealFlow(options) {
2107
2273
  }
2108
2274
  setPairing(null);
2109
2275
  publishRef.current?.(null);
2276
+ if (returnId) markReturnDone(returnId);
2110
2277
  if (flow === "auth-code") {
2111
2278
  opts.onCode?.({
2112
2279
  code,
@@ -2133,7 +2300,16 @@ function useZorealFlow(options) {
2133
2300
  } catch (e) {
2134
2301
  setPairing(null);
2135
2302
  publishRef.current?.(null);
2136
- if (e instanceof DOMException && e.name === "AbortError") return;
2303
+ if (e instanceof DOMException && e.name === "AbortError") {
2304
+ if (closedByPerson.current) {
2305
+ closedByPerson.current = false;
2306
+ opts.onNonOAuthError?.({
2307
+ type: "popup_closed",
2308
+ description: "the sign-in dialog was closed before the holder approved"
2309
+ });
2310
+ }
2311
+ return;
2312
+ }
2137
2313
  if (e instanceof FlowAbandonedError) {
2138
2314
  opts.onNonOAuthError?.(e.reason);
2139
2315
  return;