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