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