@pollar/core 0.8.0 → 0.8.1

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.rn.mjs CHANGED
@@ -1106,6 +1106,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
1106
1106
  throw new Error("Ramp transaction polling timed out");
1107
1107
  }
1108
1108
 
1109
+ // src/lib/random-uuid.ts
1110
+ function randomUUID() {
1111
+ const c = globalThis.crypto;
1112
+ if (c && typeof c.randomUUID === "function") {
1113
+ return c.randomUUID();
1114
+ }
1115
+ if (c && typeof c.getRandomValues === "function") {
1116
+ const bytes = new Uint8Array(16);
1117
+ c.getRandomValues(bytes);
1118
+ bytes[6] = bytes[6] & 15 | 64;
1119
+ bytes[8] = bytes[8] & 63 | 128;
1120
+ const hex = [];
1121
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1122
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1123
+ }
1124
+ throw new Error(
1125
+ "[PollarClient] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1126
+ );
1127
+ }
1128
+
1109
1129
  // src/dpop.ts
1110
1130
  async function buildProof(args, keyManager) {
1111
1131
  const jwk = await keyManager.getPublicJwk();
@@ -1115,7 +1135,7 @@ async function buildProof(args, keyManager) {
1115
1135
  jwk
1116
1136
  };
1117
1137
  const payload = {
1118
- jti: generateJti(),
1138
+ jti: randomUUID(),
1119
1139
  htm: args.htm.toUpperCase(),
1120
1140
  htu: normalizeHtu(args.htu),
1121
1141
  iat: Math.floor(Date.now() / 1e3)
@@ -1149,24 +1169,6 @@ function normalizeHtu(rawUrl) {
1149
1169
  const portPart = port ? `:${port}` : "";
1150
1170
  return `${scheme}//${host}${portPart}${url.pathname}`;
1151
1171
  }
1152
- function generateJti() {
1153
- const c = globalThis.crypto;
1154
- if (c && typeof c.randomUUID === "function") {
1155
- return c.randomUUID();
1156
- }
1157
- if (c && typeof c.getRandomValues === "function") {
1158
- const bytes = new Uint8Array(16);
1159
- c.getRandomValues(bytes);
1160
- bytes[6] = bytes[6] & 15 | 64;
1161
- bytes[8] = bytes[8] & 63 | 128;
1162
- const hex = [];
1163
- for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1164
- return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1165
- }
1166
- throw new Error(
1167
- "[PollarClient:dpop] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1168
- );
1169
- }
1170
1172
 
1171
1173
  // src/storage/web.ts
1172
1174
  var LOG_PREFIX = "[PollarClient:storage]";
@@ -1309,6 +1311,8 @@ function defaultVisibilityProvider() {
1309
1311
  // src/types.ts
1310
1312
  var AUTH_ERROR_CODES = {
1311
1313
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1314
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1315
+ SESSION_INVALID: "SESSION_INVALID",
1312
1316
  EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1313
1317
  EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1314
1318
  EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
@@ -1622,7 +1626,32 @@ async function readWalletType(storage, apiKeyHash) {
1622
1626
  return storage.get(walletTypeStorageKey(apiKeyHash));
1623
1627
  }
1624
1628
 
1629
+ // src/lib/abort.ts
1630
+ function abortError() {
1631
+ if (typeof DOMException !== "undefined") {
1632
+ return new DOMException("Aborted", "AbortError");
1633
+ }
1634
+ const err = new Error("Aborted");
1635
+ err.name = "AbortError";
1636
+ return err;
1637
+ }
1638
+ function throwIfAborted(signal) {
1639
+ if (signal?.aborted) throw abortError();
1640
+ }
1641
+
1625
1642
  // src/client/stream.ts
1643
+ var SessionStatusError = class extends Error {
1644
+ constructor(code) {
1645
+ super(`[PollarClient] Session status terminal: ${code}`);
1646
+ this.code = code;
1647
+ this.name = "SessionStatusError";
1648
+ }
1649
+ };
1650
+ function terminalStatusCode(parsed) {
1651
+ const err = parsed?.error;
1652
+ if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
1653
+ return null;
1654
+ }
1626
1655
  function abortableDelay(ms, signal) {
1627
1656
  return new Promise((resolve, reject) => {
1628
1657
  const t = setTimeout(resolve, ms);
@@ -1630,7 +1659,7 @@ function abortableDelay(ms, signal) {
1630
1659
  "abort",
1631
1660
  () => {
1632
1661
  clearTimeout(t);
1633
- reject(new DOMException("Aborted", "AbortError"));
1662
+ reject(abortError());
1634
1663
  },
1635
1664
  { once: true }
1636
1665
  );
@@ -1645,7 +1674,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1645
1674
  else await new Promise((r) => setTimeout(r, ms));
1646
1675
  };
1647
1676
  while (true) {
1648
- signal?.throwIfAborted();
1677
+ throwIfAborted(signal);
1649
1678
  let data, error;
1650
1679
  try {
1651
1680
  ({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
@@ -1668,7 +1697,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1668
1697
  let sawAnyChunk = false;
1669
1698
  try {
1670
1699
  while (true) {
1671
- signal?.throwIfAborted();
1700
+ throwIfAborted(signal);
1672
1701
  const { done, value } = await reader.read();
1673
1702
  if (done) {
1674
1703
  streamDone = true;
@@ -1679,17 +1708,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1679
1708
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1680
1709
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
1681
1710
  if (!dataLine) continue;
1711
+ let parsed;
1682
1712
  try {
1683
- const parsed = JSON.parse(dataLine.slice("data:".length).trim());
1684
- if (check(parsed)) {
1685
- return parsed;
1686
- }
1713
+ parsed = JSON.parse(dataLine.slice("data:".length).trim());
1687
1714
  } catch {
1715
+ continue;
1716
+ }
1717
+ const terminal = terminalStatusCode(parsed);
1718
+ if (terminal) throw new SessionStatusError(terminal);
1719
+ if (check(parsed)) {
1720
+ return parsed;
1688
1721
  }
1689
1722
  }
1690
1723
  }
1691
1724
  } catch (e) {
1692
1725
  if (e instanceof Error && e.name === "AbortError") throw e;
1726
+ if (e instanceof SessionStatusError) throw e;
1693
1727
  console.warn(e);
1694
1728
  } finally {
1695
1729
  reader.releaseLock();
@@ -1700,12 +1734,72 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1700
1734
  if (delay) await sleep(delay);
1701
1735
  }
1702
1736
  }
1737
+ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
1738
+ const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
1739
+ let backoff = intervalMs;
1740
+ const sleep = async (ms) => {
1741
+ if (ms <= 0) return;
1742
+ if (signal) await abortableDelay(ms, signal);
1743
+ else await new Promise((r) => setTimeout(r, ms));
1744
+ };
1745
+ while (true) {
1746
+ throwIfAborted(signal);
1747
+ let envelope = null;
1748
+ let httpStatus = 0;
1749
+ try {
1750
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
1751
+ httpStatus = response.status;
1752
+ envelope = await response.json().catch(() => null);
1753
+ } catch (e) {
1754
+ if (e instanceof Error && e.name === "AbortError") throw e;
1755
+ console.warn(e);
1756
+ }
1757
+ if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1758
+ throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
1759
+ }
1760
+ if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
1761
+ throw new SessionStatusError("EXPIRED_CLIENT_ID");
1762
+ }
1763
+ if (envelope?.success && envelope.content && check(envelope.content)) {
1764
+ return envelope.content;
1765
+ }
1766
+ if (envelope) backoff = intervalMs;
1767
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1768
+ await sleep(backoff);
1769
+ }
1770
+ }
1771
+ function waitForSessionReady(args) {
1772
+ const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
1773
+ return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
1774
+ }
1703
1775
 
1704
1776
  // src/client/auth/authenticate.ts
1705
1777
  async function authenticate(clientSessionId, deps, expectedWallet) {
1706
- const { api, signal, setAuthState, storeSession, clearSession } = deps;
1778
+ const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
1707
1779
  setAuthState({ step: "authenticating" });
1708
- await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1780
+ try {
1781
+ await waitForSessionReady({
1782
+ api,
1783
+ baseUrl: basePath,
1784
+ clientSessionId,
1785
+ check: (data2) => data2?.status === "READY",
1786
+ useStreaming,
1787
+ signal
1788
+ });
1789
+ } catch (err) {
1790
+ if (err instanceof SessionStatusError) {
1791
+ const expired = err.code === "EXPIRED_CLIENT_ID";
1792
+ setAuthState({
1793
+ step: "error",
1794
+ previousStep: "authenticating",
1795
+ message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
1796
+ errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
1797
+ });
1798
+ await clearSession();
1799
+ return;
1800
+ }
1801
+ throw err;
1802
+ }
1709
1803
  const dpopJwk = await deps.getPublicJwk();
1710
1804
  const { data } = await api.POST("/auth/login", {
1711
1805
  body: {
@@ -1829,26 +1923,36 @@ function severOpener(popup) {
1829
1923
  } catch {
1830
1924
  }
1831
1925
  }
1832
- async function loginOAuth(provider, deps) {
1833
- const { setAuthState, basePath, apiKey } = deps;
1834
- const popup = window.open("about:blank", "_blank");
1926
+ var defaultWebOAuthOpener = async ({ getUrl }) => {
1927
+ const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
1835
1928
  severOpener(popup);
1836
- const clientSessionId = await createAuthSession(deps);
1837
- if (!clientSessionId) {
1929
+ const url = await getUrl();
1930
+ if (!url) {
1838
1931
  popup?.close();
1839
1932
  return;
1840
1933
  }
1841
- setAuthState({ step: "opening_oauth", provider });
1842
- const url = new URL(`${basePath}/auth/${provider}`);
1843
- url.searchParams.set("api_key", apiKey);
1844
- url.searchParams.set("client_session_id", clientSessionId);
1845
- url.searchParams.set("redirect_uri", window.location.origin);
1846
1934
  if (popup) {
1847
- popup.location.href = url.toString();
1935
+ popup.location.href = url;
1848
1936
  severOpener(popup);
1849
- } else {
1850
- window.open(url.toString(), "_blank", "noopener,noreferrer");
1937
+ } else if (typeof window !== "undefined") {
1938
+ window.open(url, "_blank", "noopener,noreferrer");
1851
1939
  }
1940
+ };
1941
+ async function loginOAuth(provider, deps) {
1942
+ const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
1943
+ let clientSessionId = null;
1944
+ const getUrl = async () => {
1945
+ clientSessionId = await createAuthSession(deps);
1946
+ if (!clientSessionId) return null;
1947
+ setAuthState({ step: "opening_oauth", provider });
1948
+ const url = new URL(`${basePath}/auth/${provider}`);
1949
+ url.searchParams.set("api_key", apiKey);
1950
+ url.searchParams.set("client_session_id", clientSessionId);
1951
+ url.searchParams.set("redirect_uri", redirectUri);
1952
+ return url.toString();
1953
+ };
1954
+ await openAuthUrl({ provider, getUrl, redirectUri, signal });
1955
+ if (!clientSessionId) return;
1852
1956
  await authenticate(clientSessionId, deps);
1853
1957
  }
1854
1958
 
@@ -1858,10 +1962,10 @@ function withSignal(promise, signal) {
1858
1962
  promise,
1859
1963
  new Promise((_, reject) => {
1860
1964
  if (signal.aborted) {
1861
- reject(new DOMException("Aborted", "AbortError"));
1965
+ reject(abortError());
1862
1966
  return;
1863
1967
  }
1864
- signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
1968
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
1865
1969
  })
1866
1970
  ]);
1867
1971
  }
@@ -1909,6 +2013,8 @@ async function loginWallet(type, deps) {
1909
2013
 
1910
2014
  // src/client/client.ts
1911
2015
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
2016
+ var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
2017
+ var isClientRuntime = isBrowser || isReactNative;
1912
2018
  var REFRESH_SKEW_SECONDS = 60;
1913
2019
  function warnServerSide(method) {
1914
2020
  console.warn(
@@ -1964,7 +2070,7 @@ var PollarClient = class {
1964
2070
  this._walletAdapter = null;
1965
2071
  this._loginController = null;
1966
2072
  this.apiKey = config.apiKey;
1967
- this.id = crypto.randomUUID();
2073
+ this.id = randomUUID();
1968
2074
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1969
2075
  this._storage = config.storage ?? defaultStorage({
1970
2076
  onDegrade: (reason, error) => {
@@ -1978,10 +2084,12 @@ var PollarClient = class {
1978
2084
  this._deviceLabel = config.deviceLabel;
1979
2085
  this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1980
2086
  this._maxIdleMs = config.maxIdleMs;
2087
+ this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2088
+ this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
1981
2089
  this._api = createApiClient(this.basePath);
1982
2090
  this._wireMiddlewares();
1983
2091
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1984
- if (!isBrowser) {
2092
+ if (!isClientRuntime) {
1985
2093
  warnServerSide("constructor");
1986
2094
  this._initialized = Promise.resolve();
1987
2095
  return;
@@ -2007,7 +2115,7 @@ var PollarClient = class {
2007
2115
  // ─── Lifecycle ────────────────────────────────────────────────────────────
2008
2116
  async _initialize() {
2009
2117
  this._apiKeyHash = await hashApiKey(this.apiKey);
2010
- if (typeof window !== "undefined") {
2118
+ if (isBrowser) {
2011
2119
  const sessionKey = sessionStorageKey(this._apiKeyHash);
2012
2120
  const handler = (e) => {
2013
2121
  if (e.key === sessionKey) {
@@ -2029,7 +2137,7 @@ var PollarClient = class {
2029
2137
  }
2030
2138
  /** Detach the cross-tab storage listener and abort any in-flight login. */
2031
2139
  destroy() {
2032
- if (this._storageEventHandler && typeof window !== "undefined") {
2140
+ if (this._storageEventHandler && isBrowser) {
2033
2141
  window.removeEventListener("storage", this._storageEventHandler);
2034
2142
  this._storageEventHandler = null;
2035
2143
  }
@@ -2312,7 +2420,7 @@ var PollarClient = class {
2312
2420
  }
2313
2421
  // ─── Login (unified entry point) ─────────────────────────────────────────
2314
2422
  login(options) {
2315
- if (!isBrowser) {
2423
+ if (!isClientRuntime) {
2316
2424
  warnServerSide("login");
2317
2425
  return;
2318
2426
  }
@@ -2323,7 +2431,9 @@ var PollarClient = class {
2323
2431
  loginOAuth(options.provider, {
2324
2432
  ...deps,
2325
2433
  basePath: this.basePath,
2326
- apiKey: this.apiKey
2434
+ apiKey: this.apiKey,
2435
+ openAuthUrl: this._openAuthUrl,
2436
+ redirectUri: this._oauthRedirectUri
2327
2437
  }).catch((err) => this._handleFlowError(err));
2328
2438
  } else if (options.provider === "email") {
2329
2439
  const { email } = options;
@@ -2339,7 +2449,7 @@ var PollarClient = class {
2339
2449
  }
2340
2450
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2341
2451
  beginEmailLogin() {
2342
- if (!isBrowser) {
2452
+ if (!isClientRuntime) {
2343
2453
  warnServerSide("beginEmailLogin");
2344
2454
  return;
2345
2455
  }
@@ -2347,7 +2457,7 @@ var PollarClient = class {
2347
2457
  initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2348
2458
  }
2349
2459
  sendEmailCode(email) {
2350
- if (!isBrowser) {
2460
+ if (!isClientRuntime) {
2351
2461
  warnServerSide("sendEmailCode");
2352
2462
  return;
2353
2463
  }
@@ -2359,7 +2469,7 @@ var PollarClient = class {
2359
2469
  sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2360
2470
  }
2361
2471
  verifyEmailCode(code) {
2362
- if (!isBrowser) {
2472
+ if (!isClientRuntime) {
2363
2473
  warnServerSide("verifyEmailCode");
2364
2474
  return;
2365
2475
  }
@@ -2377,7 +2487,7 @@ var PollarClient = class {
2377
2487
  }
2378
2488
  // ─── Wallet flow (single call) ────────────────────────────────────────────
2379
2489
  loginWallet(type) {
2380
- if (!isBrowser) {
2490
+ if (!isClientRuntime) {
2381
2491
  warnServerSide("loginWallet");
2382
2492
  return;
2383
2493
  }
@@ -2403,7 +2513,7 @@ var PollarClient = class {
2403
2513
  * across all devices.
2404
2514
  */
2405
2515
  async logout(options = {}) {
2406
- if (!isBrowser) {
2516
+ if (!isClientRuntime) {
2407
2517
  warnServerSide("logout");
2408
2518
  return;
2409
2519
  }
@@ -2433,7 +2543,7 @@ var PollarClient = class {
2433
2543
  * `current` flag identifies which entry corresponds to this client.
2434
2544
  */
2435
2545
  async listSessions() {
2436
- if (!isBrowser) {
2546
+ if (!isClientRuntime) {
2437
2547
  warnServerSide("listSessions");
2438
2548
  return [];
2439
2549
  }
@@ -2452,7 +2562,7 @@ var PollarClient = class {
2452
2562
  * does NOT clear local state — call `logout()` for that case.
2453
2563
  */
2454
2564
  async revokeSession(familyId) {
2455
- if (!isBrowser) {
2565
+ if (!isClientRuntime) {
2456
2566
  warnServerSide("revokeSession");
2457
2567
  return;
2458
2568
  }
@@ -2950,6 +3060,11 @@ var PollarClient = class {
2950
3060
  _flowDeps(signal) {
2951
3061
  return {
2952
3062
  api: this._api,
3063
+ basePath: this.basePath,
3064
+ // SSE status streaming works on web; React Native's `fetch` has no
3065
+ // readable `response.body`, so those clients poll the non-streaming
3066
+ // status endpoint instead. `isBrowser` is false in RN and SSR alike.
3067
+ useStreaming: isBrowser,
2953
3068
  signal,
2954
3069
  setAuthState: this._setAuthState.bind(this),
2955
3070
  storeSession: this._storeSession.bind(this),