@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.js CHANGED
@@ -981,6 +981,26 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
981
981
  throw new Error("Ramp transaction polling timed out");
982
982
  }
983
983
 
984
+ // src/lib/random-uuid.ts
985
+ function randomUUID() {
986
+ const c = globalThis.crypto;
987
+ if (c && typeof c.randomUUID === "function") {
988
+ return c.randomUUID();
989
+ }
990
+ if (c && typeof c.getRandomValues === "function") {
991
+ const bytes = new Uint8Array(16);
992
+ c.getRandomValues(bytes);
993
+ bytes[6] = bytes[6] & 15 | 64;
994
+ bytes[8] = bytes[8] & 63 | 128;
995
+ const hex = [];
996
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
997
+ 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("")}`;
998
+ }
999
+ throw new Error(
1000
+ "[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."
1001
+ );
1002
+ }
1003
+
984
1004
  // src/dpop.ts
985
1005
  async function buildProof(args, keyManager) {
986
1006
  const jwk = await keyManager.getPublicJwk();
@@ -990,7 +1010,7 @@ async function buildProof(args, keyManager) {
990
1010
  jwk
991
1011
  };
992
1012
  const payload = {
993
- jti: generateJti(),
1013
+ jti: randomUUID(),
994
1014
  htm: args.htm.toUpperCase(),
995
1015
  htu: normalizeHtu(args.htu),
996
1016
  iat: Math.floor(Date.now() / 1e3)
@@ -1024,24 +1044,6 @@ function normalizeHtu(rawUrl) {
1024
1044
  const portPart = port ? `:${port}` : "";
1025
1045
  return `${scheme}//${host}${portPart}${url.pathname}`;
1026
1046
  }
1027
- function generateJti() {
1028
- const c = globalThis.crypto;
1029
- if (c && typeof c.randomUUID === "function") {
1030
- return c.randomUUID();
1031
- }
1032
- if (c && typeof c.getRandomValues === "function") {
1033
- const bytes = new Uint8Array(16);
1034
- c.getRandomValues(bytes);
1035
- bytes[6] = bytes[6] & 15 | 64;
1036
- bytes[8] = bytes[8] & 63 | 128;
1037
- const hex = [];
1038
- for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1039
- 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("")}`;
1040
- }
1041
- throw new Error(
1042
- "[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."
1043
- );
1044
- }
1045
1047
 
1046
1048
  // src/storage/web.ts
1047
1049
  var LOG_PREFIX = "[PollarClient:storage]";
@@ -1184,6 +1186,8 @@ function defaultVisibilityProvider() {
1184
1186
  // src/types.ts
1185
1187
  var AUTH_ERROR_CODES = {
1186
1188
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1189
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1190
+ SESSION_INVALID: "SESSION_INVALID",
1187
1191
  EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1188
1192
  EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1189
1193
  EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
@@ -1497,7 +1501,32 @@ async function readWalletType(storage, apiKeyHash) {
1497
1501
  return storage.get(walletTypeStorageKey(apiKeyHash));
1498
1502
  }
1499
1503
 
1504
+ // src/lib/abort.ts
1505
+ function abortError() {
1506
+ if (typeof DOMException !== "undefined") {
1507
+ return new DOMException("Aborted", "AbortError");
1508
+ }
1509
+ const err = new Error("Aborted");
1510
+ err.name = "AbortError";
1511
+ return err;
1512
+ }
1513
+ function throwIfAborted(signal) {
1514
+ if (signal?.aborted) throw abortError();
1515
+ }
1516
+
1500
1517
  // src/client/stream.ts
1518
+ var SessionStatusError = class extends Error {
1519
+ constructor(code) {
1520
+ super(`[PollarClient] Session status terminal: ${code}`);
1521
+ this.code = code;
1522
+ this.name = "SessionStatusError";
1523
+ }
1524
+ };
1525
+ function terminalStatusCode(parsed) {
1526
+ const err = parsed?.error;
1527
+ if (err === "INVALID_CLIENT_SESSION_ID" || err === "EXPIRED_CLIENT_ID") return err;
1528
+ return null;
1529
+ }
1501
1530
  function abortableDelay(ms, signal) {
1502
1531
  return new Promise((resolve, reject) => {
1503
1532
  const t = setTimeout(resolve, ms);
@@ -1505,7 +1534,7 @@ function abortableDelay(ms, signal) {
1505
1534
  "abort",
1506
1535
  () => {
1507
1536
  clearTimeout(t);
1508
- reject(new DOMException("Aborted", "AbortError"));
1537
+ reject(abortError());
1509
1538
  },
1510
1539
  { once: true }
1511
1540
  );
@@ -1520,7 +1549,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1520
1549
  else await new Promise((r) => setTimeout(r, ms));
1521
1550
  };
1522
1551
  while (true) {
1523
- signal?.throwIfAborted();
1552
+ throwIfAborted(signal);
1524
1553
  let data, error;
1525
1554
  try {
1526
1555
  ({ data, error } = await api.GET("/auth/session/status/{clientSessionId}", {
@@ -1543,7 +1572,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1543
1572
  let sawAnyChunk = false;
1544
1573
  try {
1545
1574
  while (true) {
1546
- signal?.throwIfAborted();
1575
+ throwIfAborted(signal);
1547
1576
  const { done, value } = await reader.read();
1548
1577
  if (done) {
1549
1578
  streamDone = true;
@@ -1554,17 +1583,22 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1554
1583
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1555
1584
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
1556
1585
  if (!dataLine) continue;
1586
+ let parsed;
1557
1587
  try {
1558
- const parsed = JSON.parse(dataLine.slice("data:".length).trim());
1559
- if (check(parsed)) {
1560
- return parsed;
1561
- }
1588
+ parsed = JSON.parse(dataLine.slice("data:".length).trim());
1562
1589
  } catch {
1590
+ continue;
1591
+ }
1592
+ const terminal = terminalStatusCode(parsed);
1593
+ if (terminal) throw new SessionStatusError(terminal);
1594
+ if (check(parsed)) {
1595
+ return parsed;
1563
1596
  }
1564
1597
  }
1565
1598
  }
1566
1599
  } catch (e) {
1567
1600
  if (e instanceof Error && e.name === "AbortError") throw e;
1601
+ if (e instanceof SessionStatusError) throw e;
1568
1602
  console.warn(e);
1569
1603
  } finally {
1570
1604
  reader.releaseLock();
@@ -1575,12 +1609,72 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1575
1609
  if (delay) await sleep(delay);
1576
1610
  }
1577
1611
  }
1612
+ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500, signal) {
1613
+ const url = `${baseUrl}/auth/session/status/${encodeURIComponent(clientSessionId)}/poll`;
1614
+ let backoff = intervalMs;
1615
+ const sleep = async (ms) => {
1616
+ if (ms <= 0) return;
1617
+ if (signal) await abortableDelay(ms, signal);
1618
+ else await new Promise((r) => setTimeout(r, ms));
1619
+ };
1620
+ while (true) {
1621
+ throwIfAborted(signal);
1622
+ let envelope = null;
1623
+ let httpStatus = 0;
1624
+ try {
1625
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: signal ?? null });
1626
+ httpStatus = response.status;
1627
+ envelope = await response.json().catch(() => null);
1628
+ } catch (e) {
1629
+ if (e instanceof Error && e.name === "AbortError") throw e;
1630
+ console.warn(e);
1631
+ }
1632
+ if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1633
+ throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
1634
+ }
1635
+ if (httpStatus === 410 || envelope?.code === "EXPIRED_CLIENT_ID") {
1636
+ throw new SessionStatusError("EXPIRED_CLIENT_ID");
1637
+ }
1638
+ if (envelope?.success && envelope.content && check(envelope.content)) {
1639
+ return envelope.content;
1640
+ }
1641
+ if (envelope) backoff = intervalMs;
1642
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1643
+ await sleep(backoff);
1644
+ }
1645
+ }
1646
+ function waitForSessionReady(args) {
1647
+ const { api, baseUrl, clientSessionId, check, useStreaming, retryDelayMs, signal } = args;
1648
+ return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal);
1649
+ }
1578
1650
 
1579
1651
  // src/client/auth/authenticate.ts
1580
1652
  async function authenticate(clientSessionId, deps, expectedWallet) {
1581
- const { api, signal, setAuthState, storeSession, clearSession } = deps;
1653
+ const { api, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
1582
1654
  setAuthState({ step: "authenticating" });
1583
- await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1655
+ try {
1656
+ await waitForSessionReady({
1657
+ api,
1658
+ baseUrl: basePath,
1659
+ clientSessionId,
1660
+ check: (data2) => data2?.status === "READY",
1661
+ useStreaming,
1662
+ signal
1663
+ });
1664
+ } catch (err) {
1665
+ if (err instanceof SessionStatusError) {
1666
+ const expired = err.code === "EXPIRED_CLIENT_ID";
1667
+ setAuthState({
1668
+ step: "error",
1669
+ previousStep: "authenticating",
1670
+ message: expired ? "Login session expired \u2014 please try again" : "Login session is no longer valid \u2014 please try again",
1671
+ errorCode: expired ? AUTH_ERROR_CODES.SESSION_EXPIRED : AUTH_ERROR_CODES.SESSION_INVALID
1672
+ });
1673
+ await clearSession();
1674
+ return;
1675
+ }
1676
+ throw err;
1677
+ }
1584
1678
  const dpopJwk = await deps.getPublicJwk();
1585
1679
  const { data } = await api.POST("/auth/login", {
1586
1680
  body: {
@@ -1704,26 +1798,36 @@ function severOpener(popup) {
1704
1798
  } catch {
1705
1799
  }
1706
1800
  }
1707
- async function loginOAuth(provider, deps) {
1708
- const { setAuthState, basePath, apiKey } = deps;
1709
- const popup = window.open("about:blank", "_blank");
1801
+ var defaultWebOAuthOpener = async ({ getUrl }) => {
1802
+ const popup = typeof window !== "undefined" ? window.open("about:blank", "_blank") : null;
1710
1803
  severOpener(popup);
1711
- const clientSessionId = await createAuthSession(deps);
1712
- if (!clientSessionId) {
1804
+ const url = await getUrl();
1805
+ if (!url) {
1713
1806
  popup?.close();
1714
1807
  return;
1715
1808
  }
1716
- setAuthState({ step: "opening_oauth", provider });
1717
- const url = new URL(`${basePath}/auth/${provider}`);
1718
- url.searchParams.set("api_key", apiKey);
1719
- url.searchParams.set("client_session_id", clientSessionId);
1720
- url.searchParams.set("redirect_uri", window.location.origin);
1721
1809
  if (popup) {
1722
- popup.location.href = url.toString();
1810
+ popup.location.href = url;
1723
1811
  severOpener(popup);
1724
- } else {
1725
- window.open(url.toString(), "_blank", "noopener,noreferrer");
1812
+ } else if (typeof window !== "undefined") {
1813
+ window.open(url, "_blank", "noopener,noreferrer");
1726
1814
  }
1815
+ };
1816
+ async function loginOAuth(provider, deps) {
1817
+ const { setAuthState, basePath, apiKey, openAuthUrl, redirectUri, signal } = deps;
1818
+ let clientSessionId = null;
1819
+ const getUrl = async () => {
1820
+ clientSessionId = await createAuthSession(deps);
1821
+ if (!clientSessionId) return null;
1822
+ setAuthState({ step: "opening_oauth", provider });
1823
+ const url = new URL(`${basePath}/auth/${provider}`);
1824
+ url.searchParams.set("api_key", apiKey);
1825
+ url.searchParams.set("client_session_id", clientSessionId);
1826
+ url.searchParams.set("redirect_uri", redirectUri);
1827
+ return url.toString();
1828
+ };
1829
+ await openAuthUrl({ provider, getUrl, redirectUri, signal });
1830
+ if (!clientSessionId) return;
1727
1831
  await authenticate(clientSessionId, deps);
1728
1832
  }
1729
1833
 
@@ -1733,10 +1837,10 @@ function withSignal(promise, signal) {
1733
1837
  promise,
1734
1838
  new Promise((_, reject) => {
1735
1839
  if (signal.aborted) {
1736
- reject(new DOMException("Aborted", "AbortError"));
1840
+ reject(abortError());
1737
1841
  return;
1738
1842
  }
1739
- signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
1843
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
1740
1844
  })
1741
1845
  ]);
1742
1846
  }
@@ -1784,6 +1888,8 @@ async function loginWallet(type, deps) {
1784
1888
 
1785
1889
  // src/client/client.ts
1786
1890
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1891
+ var isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative";
1892
+ var isClientRuntime = isBrowser || isReactNative;
1787
1893
  var REFRESH_SKEW_SECONDS = 60;
1788
1894
  function warnServerSide(method) {
1789
1895
  console.warn(
@@ -1839,7 +1945,7 @@ var PollarClient = class {
1839
1945
  this._walletAdapter = null;
1840
1946
  this._loginController = null;
1841
1947
  this.apiKey = config.apiKey;
1842
- this.id = crypto.randomUUID();
1948
+ this.id = randomUUID();
1843
1949
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1844
1950
  this._storage = config.storage ?? defaultStorage({
1845
1951
  onDegrade: (reason, error) => {
@@ -1853,10 +1959,12 @@ var PollarClient = class {
1853
1959
  this._deviceLabel = config.deviceLabel;
1854
1960
  this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
1855
1961
  this._maxIdleMs = config.maxIdleMs;
1962
+ this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
1963
+ this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location.origin : "");
1856
1964
  this._api = createApiClient(this.basePath);
1857
1965
  this._wireMiddlewares();
1858
1966
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1859
- if (!isBrowser) {
1967
+ if (!isClientRuntime) {
1860
1968
  warnServerSide("constructor");
1861
1969
  this._initialized = Promise.resolve();
1862
1970
  return;
@@ -1882,7 +1990,7 @@ var PollarClient = class {
1882
1990
  // ─── Lifecycle ────────────────────────────────────────────────────────────
1883
1991
  async _initialize() {
1884
1992
  this._apiKeyHash = await hashApiKey(this.apiKey);
1885
- if (typeof window !== "undefined") {
1993
+ if (isBrowser) {
1886
1994
  const sessionKey = sessionStorageKey(this._apiKeyHash);
1887
1995
  const handler = (e) => {
1888
1996
  if (e.key === sessionKey) {
@@ -1904,7 +2012,7 @@ var PollarClient = class {
1904
2012
  }
1905
2013
  /** Detach the cross-tab storage listener and abort any in-flight login. */
1906
2014
  destroy() {
1907
- if (this._storageEventHandler && typeof window !== "undefined") {
2015
+ if (this._storageEventHandler && isBrowser) {
1908
2016
  window.removeEventListener("storage", this._storageEventHandler);
1909
2017
  this._storageEventHandler = null;
1910
2018
  }
@@ -2187,7 +2295,7 @@ var PollarClient = class {
2187
2295
  }
2188
2296
  // ─── Login (unified entry point) ─────────────────────────────────────────
2189
2297
  login(options) {
2190
- if (!isBrowser) {
2298
+ if (!isClientRuntime) {
2191
2299
  warnServerSide("login");
2192
2300
  return;
2193
2301
  }
@@ -2198,7 +2306,9 @@ var PollarClient = class {
2198
2306
  loginOAuth(options.provider, {
2199
2307
  ...deps,
2200
2308
  basePath: this.basePath,
2201
- apiKey: this.apiKey
2309
+ apiKey: this.apiKey,
2310
+ openAuthUrl: this._openAuthUrl,
2311
+ redirectUri: this._oauthRedirectUri
2202
2312
  }).catch((err) => this._handleFlowError(err));
2203
2313
  } else if (options.provider === "email") {
2204
2314
  const { email } = options;
@@ -2214,7 +2324,7 @@ var PollarClient = class {
2214
2324
  }
2215
2325
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2216
2326
  beginEmailLogin() {
2217
- if (!isBrowser) {
2327
+ if (!isClientRuntime) {
2218
2328
  warnServerSide("beginEmailLogin");
2219
2329
  return;
2220
2330
  }
@@ -2222,7 +2332,7 @@ var PollarClient = class {
2222
2332
  initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2223
2333
  }
2224
2334
  sendEmailCode(email) {
2225
- if (!isBrowser) {
2335
+ if (!isClientRuntime) {
2226
2336
  warnServerSide("sendEmailCode");
2227
2337
  return;
2228
2338
  }
@@ -2234,7 +2344,7 @@ var PollarClient = class {
2234
2344
  sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2235
2345
  }
2236
2346
  verifyEmailCode(code) {
2237
- if (!isBrowser) {
2347
+ if (!isClientRuntime) {
2238
2348
  warnServerSide("verifyEmailCode");
2239
2349
  return;
2240
2350
  }
@@ -2252,7 +2362,7 @@ var PollarClient = class {
2252
2362
  }
2253
2363
  // ─── Wallet flow (single call) ────────────────────────────────────────────
2254
2364
  loginWallet(type) {
2255
- if (!isBrowser) {
2365
+ if (!isClientRuntime) {
2256
2366
  warnServerSide("loginWallet");
2257
2367
  return;
2258
2368
  }
@@ -2278,7 +2388,7 @@ var PollarClient = class {
2278
2388
  * across all devices.
2279
2389
  */
2280
2390
  async logout(options = {}) {
2281
- if (!isBrowser) {
2391
+ if (!isClientRuntime) {
2282
2392
  warnServerSide("logout");
2283
2393
  return;
2284
2394
  }
@@ -2308,7 +2418,7 @@ var PollarClient = class {
2308
2418
  * `current` flag identifies which entry corresponds to this client.
2309
2419
  */
2310
2420
  async listSessions() {
2311
- if (!isBrowser) {
2421
+ if (!isClientRuntime) {
2312
2422
  warnServerSide("listSessions");
2313
2423
  return [];
2314
2424
  }
@@ -2327,7 +2437,7 @@ var PollarClient = class {
2327
2437
  * does NOT clear local state — call `logout()` for that case.
2328
2438
  */
2329
2439
  async revokeSession(familyId) {
2330
- if (!isBrowser) {
2440
+ if (!isClientRuntime) {
2331
2441
  warnServerSide("revokeSession");
2332
2442
  return;
2333
2443
  }
@@ -2825,6 +2935,11 @@ var PollarClient = class {
2825
2935
  _flowDeps(signal) {
2826
2936
  return {
2827
2937
  api: this._api,
2938
+ basePath: this.basePath,
2939
+ // SSE status streaming works on web; React Native's `fetch` has no
2940
+ // readable `response.body`, so those clients poll the non-streaming
2941
+ // status endpoint instead. `isBrowser` is false in RN and SSR alike.
2942
+ useStreaming: isBrowser,
2828
2943
  signal,
2829
2944
  setAuthState: this._setAuthState.bind(this),
2830
2945
  storeSession: this._storeSession.bind(this),