@pollar/core 0.8.0 → 0.8.2

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