@pollar/core 0.9.0 → 0.10.0-rc.0

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.js CHANGED
@@ -1021,6 +1021,29 @@ function createLogger(level = "info", sink = console) {
1021
1021
  return { error: gate("error"), warn: gate("warn"), info: gate("info"), debug: gate("debug") };
1022
1022
  }
1023
1023
 
1024
+ // src/lib/logging.ts
1025
+ var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
1026
+ "email",
1027
+ "code",
1028
+ "walletAddress",
1029
+ "dpopJwk",
1030
+ "response",
1031
+ "refreshToken",
1032
+ // SEP-10 challenge envelopes: a counter-signed challenge is a live, replayable
1033
+ // auth credential — never log it in the clear.
1034
+ "signedChallengeXdr",
1035
+ "challengeXdr",
1036
+ "signedTxXdr"
1037
+ ]);
1038
+ function redactBody(body) {
1039
+ if (!body || typeof body !== "object") return body;
1040
+ const out = {};
1041
+ for (const [key, value] of Object.entries(body)) {
1042
+ out[key] = SENSITIVE_BODY_KEYS.has(key) ? "[redacted]" : value;
1043
+ }
1044
+ return out;
1045
+ }
1046
+
1024
1047
  // src/storage/web.ts
1025
1048
  var LOG_PREFIX = "[PollarClient:storage]";
1026
1049
  function createMemoryAdapter() {
@@ -1108,8 +1131,36 @@ function defaultStorage(options = {}) {
1108
1131
  return createLocalStorageAdapter(options);
1109
1132
  }
1110
1133
 
1134
+ // src/types.ts
1135
+ var AUTH_ERROR_CODES = {
1136
+ SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1137
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1138
+ SESSION_INVALID: "SESSION_INVALID",
1139
+ EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1140
+ EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1141
+ EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1142
+ EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1143
+ AUTH_FAILED: "AUTH_FAILED",
1144
+ WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1145
+ WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1146
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1147
+ EXTERNAL_AUTH_FAILED: "EXTERNAL_AUTH_FAILED",
1148
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1149
+ // Generic bucket for on-chain transaction failures; the precise reason is the
1150
+ // backend `code` (e.g. TX_FEE_LIMIT_EXCEEDED) carried alongside on the outcome.
1151
+ TX_FAILED: "TX_FAILED",
1152
+ UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1153
+ };
1154
+ var PollarFlowError = class extends Error {
1155
+ constructor(message) {
1156
+ super(message);
1157
+ this.code = "INVALID_FLOW";
1158
+ this.name = "PollarFlowError";
1159
+ }
1160
+ };
1161
+
1111
1162
  // src/version.ts
1112
- var POLLAR_CORE_VERSION = "0.9.0" ;
1163
+ var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
1113
1164
 
1114
1165
  // src/visibility/noop.ts
1115
1166
  function createNoopVisibilityProvider() {
@@ -1162,30 +1213,6 @@ function defaultVisibilityProvider() {
1162
1213
  return createNoopVisibilityProvider();
1163
1214
  }
1164
1215
 
1165
- // src/types.ts
1166
- var AUTH_ERROR_CODES = {
1167
- SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1168
- SESSION_EXPIRED: "SESSION_EXPIRED",
1169
- SESSION_INVALID: "SESSION_INVALID",
1170
- EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1171
- EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1172
- EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1173
- EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1174
- AUTH_FAILED: "AUTH_FAILED",
1175
- WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1176
- WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1177
- WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1178
- PASSKEY_FAILED: "PASSKEY_FAILED",
1179
- UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1180
- };
1181
- var PollarFlowError = class extends Error {
1182
- constructor(message) {
1183
- super(message);
1184
- this.code = "INVALID_FLOW";
1185
- this.name = "PollarFlowError";
1186
- }
1187
- };
1188
-
1189
1216
  // src/wallets/FreighterAdapter.ts
1190
1217
  var import_freighter_api = __toESM(require_index_min());
1191
1218
 
@@ -1281,10 +1308,13 @@ function openAlbedoPopup(url) {
1281
1308
  }
1282
1309
  function waitForAlbedoPopup() {
1283
1310
  return new Promise((resolve, reject) => {
1284
- const timeout = setTimeout(() => {
1285
- window.removeEventListener("message", handler);
1286
- reject(new Error("Albedo response timeout"));
1287
- }, 2 * 60 * 1e3);
1311
+ const timeout = setTimeout(
1312
+ () => {
1313
+ window.removeEventListener("message", handler);
1314
+ reject(new Error("Albedo response timeout"));
1315
+ },
1316
+ 2 * 60 * 1e3
1317
+ );
1288
1318
  function handler(event) {
1289
1319
  if (event.origin !== window.location.origin || event.data?.type !== "ALBEDO_RESULT") return;
1290
1320
  clearTimeout(timeout);
@@ -1438,6 +1468,10 @@ function isValidSession(value, logger = console) {
1438
1468
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
1439
1469
  return false;
1440
1470
  }
1471
+ if (w["provider"] !== void 0 && typeof w["provider"] !== "string") {
1472
+ logger.debug("[PollarClient:session] Invalid session \u2014 wallet.provider must be a string if present");
1473
+ return false;
1474
+ }
1441
1475
  if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1442
1476
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1443
1477
  return false;
@@ -1644,6 +1678,16 @@ function waitForSessionReady(args) {
1644
1678
  return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal, logger) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal, logger);
1645
1679
  }
1646
1680
 
1681
+ // src/client/auth/logging.ts
1682
+ function logApiError(logger, route, detail = {}, level = "error") {
1683
+ const { body, error, data } = detail;
1684
+ logger[level](`[PollarClient:auth] ${route} failed`, {
1685
+ route,
1686
+ ...body !== void 0 ? { body: redactBody(body) } : {},
1687
+ cause: error ?? data
1688
+ });
1689
+ }
1690
+
1647
1691
  // src/client/auth/authenticate.ts
1648
1692
  async function authenticate(clientSessionId, deps, expectedWallet) {
1649
1693
  const { api, logger, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
@@ -1661,6 +1705,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1661
1705
  } catch (err) {
1662
1706
  if (err instanceof SessionStatusError) {
1663
1707
  const expired = err.code === "EXPIRED_CLIENT_ID";
1708
+ logApiError(logger, "session status", { data: err });
1664
1709
  setAuthState({
1665
1710
  step: "error",
1666
1711
  previousStep: "authenticating",
@@ -1673,14 +1718,12 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1673
1718
  throw err;
1674
1719
  }
1675
1720
  const dpopJwk = await deps.getPublicJwk();
1676
- const { data } = await api.POST("/auth/login", {
1677
- body: {
1678
- clientSessionId,
1679
- dpopJwk,
1680
- ...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
1681
- },
1682
- signal
1683
- });
1721
+ const body = {
1722
+ clientSessionId,
1723
+ dpopJwk,
1724
+ ...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
1725
+ };
1726
+ const { data, error } = await api.POST("/auth/login", { body, signal });
1684
1727
  if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content, logger)) {
1685
1728
  const sessionWallet = data.content.data?.providers?.wallet?.address;
1686
1729
  if (expectedWallet && sessionWallet !== expectedWallet) {
@@ -1695,6 +1738,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1695
1738
  }
1696
1739
  await storeSession(data.content);
1697
1740
  } else {
1741
+ if (!error) logApiError(logger, "POST /auth/login", { body, data });
1698
1742
  setAuthState({
1699
1743
  step: "error",
1700
1744
  previousStep: "authenticating",
@@ -1707,10 +1751,11 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1707
1751
 
1708
1752
  // src/client/auth/deps.ts
1709
1753
  async function createAuthSession(deps) {
1710
- const { api, signal, setAuthState } = deps;
1754
+ const { api, logger, signal, setAuthState } = deps;
1711
1755
  setAuthState({ step: "creating_session" });
1712
1756
  const { data, error } = await api.POST("/auth/session", { signal });
1713
1757
  if (error || !data?.success) {
1758
+ if (!error) logApiError(logger, "POST /auth/session", { data });
1714
1759
  setAuthState({
1715
1760
  step: "error",
1716
1761
  previousStep: "creating_session",
@@ -1722,20 +1767,96 @@ async function createAuthSession(deps) {
1722
1767
  return data.content.clientSessionId;
1723
1768
  }
1724
1769
 
1770
+ // src/client/auth/errorMessages.ts
1771
+ var CATALOG = {
1772
+ // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1773
+ SPONSOR_NOT_FUNDED: {
1774
+ message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1775
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1776
+ },
1777
+ APP_WALLET_NOT_FOUND: {
1778
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1779
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1780
+ },
1781
+ WALLET_NOT_FOUND: {
1782
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1783
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1784
+ },
1785
+ PASSKEY_DEPLOY_FAILED: {
1786
+ message: "We couldn't finish creating your wallet. Please try again in a moment.",
1787
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1788
+ },
1789
+ // ── Passkey ceremony ────────────────────────────────────────────────────────
1790
+ PASSKEY_ALREADY_REGISTERED: {
1791
+ message: "A passkey is already registered for this account. Try signing in instead.",
1792
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1793
+ },
1794
+ PASSKEY_UNKNOWN_CREDENTIAL: {
1795
+ message: "We don't recognize this passkey. Try creating a new one.",
1796
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1797
+ },
1798
+ PASSKEY_VERIFICATION_FAILED: {
1799
+ message: "We couldn't verify your passkey. Please try again.",
1800
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1801
+ },
1802
+ PASSKEY_CHALLENGE_MISSING: {
1803
+ message: "Your passkey session expired. Please start again.",
1804
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1805
+ },
1806
+ // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1807
+ // These map to the TX_FAILED bucket (not PASSKEY_FAILED) — the precise reason
1808
+ // is the entry key itself, surfaced as the raw `code` on the tx outcome.
1809
+ TX_INSUFFICIENT_BALANCE: {
1810
+ message: "Insufficient balance to complete this transaction.",
1811
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1812
+ },
1813
+ TX_INSUFFICIENT_FEE: {
1814
+ message: "Not enough XLM to cover the network fee. Add more XLM to your wallet and try again.",
1815
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1816
+ },
1817
+ TX_FEE_LIMIT_EXCEEDED: {
1818
+ message: "The transaction fee is above the allowed limit. Please try again.",
1819
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1820
+ },
1821
+ TX_CONTRACT_FAILED: {
1822
+ message: "The contract rejected this operation. Check the operation is allowed right now and try again.",
1823
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1824
+ },
1825
+ TX_DESTINATION_NOT_FOUND: {
1826
+ message: "The destination account doesn't exist on the network yet.",
1827
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1828
+ },
1829
+ TX_NO_TRUSTLINE: {
1830
+ message: "The destination can't receive this asset yet (no trustline).",
1831
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1832
+ },
1833
+ TX_BAD_SEQUENCE: {
1834
+ message: "Something went out of sync. Please try again.",
1835
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1836
+ }
1837
+ };
1838
+ function resolveAuthError(code, fallbackMessage) {
1839
+ if (code && CATALOG[code]) return CATALOG[code];
1840
+ return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1841
+ }
1842
+ function extractErrorCode(error, data) {
1843
+ return error?.code ?? data?.code ?? void 0;
1844
+ }
1845
+
1725
1846
  // src/client/auth/emailFlow.ts
1726
- async function initEmailSession(deps) {
1727
- const clientSessionId = await createAuthSession(deps);
1728
- if (!clientSessionId) return;
1729
- deps.setAuthState({ step: "entering_email", clientSessionId });
1847
+ async function initEmailSession(ctx) {
1848
+ const clientSessionId = await ctx.createSession();
1849
+ if (!clientSessionId) return null;
1850
+ ctx.setAuthState({ step: "entering_email", clientSessionId });
1851
+ return clientSessionId;
1730
1852
  }
1731
- async function sendEmailCode(email, clientSessionId, deps) {
1732
- const { api, signal, setAuthState } = deps;
1853
+ async function sendEmailCode(email, clientSessionId, ctx) {
1854
+ const { api, logger, signal, setAuthState } = ctx;
1733
1855
  setAuthState({ step: "sending_email", email });
1734
- const { data, error } = await api.POST("/auth/email", {
1735
- body: { clientSessionId, email },
1736
- signal
1737
- });
1856
+ const body = { clientSessionId, email };
1857
+ const { data, error } = await api.POST("/auth/email", { body, signal });
1738
1858
  if (error || !data?.success) {
1859
+ if (!error) logApiError(logger, "POST /auth/email", { body, data });
1739
1860
  setAuthState({
1740
1861
  step: "error",
1741
1862
  previousStep: "sending_email",
@@ -1746,19 +1867,18 @@ async function sendEmailCode(email, clientSessionId, deps) {
1746
1867
  }
1747
1868
  setAuthState({ step: "entering_code", clientSessionId, email });
1748
1869
  }
1749
- async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1750
- const { api, signal, setAuthState } = deps;
1870
+ async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
1871
+ const { api, logger, signal, setAuthState } = ctx;
1751
1872
  setAuthState({ step: "verifying_email_code", clientSessionId, email });
1752
- const { data, error } = await api.POST("/auth/email/verify-code", {
1753
- body: { clientSessionId, code },
1754
- signal
1755
- });
1873
+ const body = { clientSessionId, code };
1874
+ const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
1756
1875
  if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
1757
- await authenticate(clientSessionId, deps);
1876
+ await ctx.authenticate(clientSessionId);
1758
1877
  return;
1759
1878
  }
1760
1879
  const errCode = error?.error ?? data?.code;
1761
1880
  if (errCode === "SDK_EMAIL_CODE_EXPIRED") {
1881
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1762
1882
  setAuthState({
1763
1883
  step: "error",
1764
1884
  previousStep: "verifying_email_code",
@@ -1770,6 +1890,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1770
1890
  return;
1771
1891
  }
1772
1892
  if (errCode === "INVALID_EMAIL_CODE" || errCode === "SDK_EMAIL_CODE_INVALID") {
1893
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1773
1894
  setAuthState({
1774
1895
  step: "error",
1775
1896
  previousStep: "verifying_email_code",
@@ -1780,6 +1901,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1780
1901
  });
1781
1902
  return;
1782
1903
  }
1904
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1783
1905
  setAuthState({
1784
1906
  step: "error",
1785
1907
  previousStep: "verifying_email_code",
@@ -1831,8 +1953,9 @@ async function loginOAuth(provider, deps) {
1831
1953
 
1832
1954
  // src/client/auth/passkeyFlow.ts
1833
1955
  async function smartWalletFlow(deps, mode) {
1834
- const { api, signal, setAuthState, passkey } = deps;
1956
+ const { api, logger, signal, setAuthState, passkey } = deps;
1835
1957
  if (!passkey) {
1958
+ logger.error("[PollarClient:auth] passkey ceremony not configured");
1836
1959
  setAuthState({
1837
1960
  step: "error",
1838
1961
  previousStep: "creating_session",
@@ -1844,38 +1967,109 @@ async function smartWalletFlow(deps, mode) {
1844
1967
  const clientSessionId = await createAuthSession(deps);
1845
1968
  if (!clientSessionId) return;
1846
1969
  try {
1847
- const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
1848
- body: { clientSessionId },
1970
+ const challengeBody = { clientSessionId };
1971
+ const { data: challengeData, error: challengeError } = await api.POST("/auth/passkey/challenge", {
1972
+ body: challengeBody,
1849
1973
  signal
1850
1974
  });
1851
1975
  const challenge = challengeData?.content?.challenge;
1852
1976
  if (!challengeData?.success || !challenge) {
1853
- return failPasskey(setAuthState, "Failed to start passkey");
1977
+ if (!challengeError) logApiError(logger, "POST /auth/passkey/challenge", { body: challengeBody, data: challengeData });
1978
+ return failPasskey(setAuthState, extractErrorCode(challengeError, challengeData), "Failed to start passkey");
1854
1979
  }
1855
1980
  setAuthState({ step: "creating_passkey" });
1856
1981
  const ceremony = await passkey({ challenge, mode });
1857
1982
  const response = ceremony.response;
1858
1983
  if (ceremony.kind === "register") {
1859
1984
  setAuthState({ step: "deploying_smart_account" });
1860
- const { data } = await api.POST("/auth/passkey/register", {
1861
- body: { clientSessionId, response },
1862
- signal
1863
- });
1864
- if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
1985
+ const body = { clientSessionId, response };
1986
+ const { data, error } = await api.POST("/auth/passkey/register", { body, signal });
1987
+ if (!data?.success) {
1988
+ if (!error) logApiError(logger, "POST /auth/passkey/register", { body, data });
1989
+ return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey registration failed");
1990
+ }
1865
1991
  } else {
1866
- const { data } = await api.POST("/auth/passkey/login", {
1867
- body: { clientSessionId, response },
1868
- signal
1869
- });
1870
- if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
1992
+ const body = { clientSessionId, response };
1993
+ const { data, error } = await api.POST("/auth/passkey/login", { body, signal });
1994
+ if (!data?.success) {
1995
+ if (!error) logApiError(logger, "POST /auth/passkey/login", { body, data });
1996
+ return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey authentication failed");
1997
+ }
1871
1998
  }
1872
- } catch {
1873
- return failPasskey(setAuthState, "Passkey login failed");
1999
+ } catch (err) {
2000
+ logApiError(logger, "passkey ceremony", { error: err });
2001
+ return failPasskey(setAuthState, void 0, "Passkey login failed");
1874
2002
  }
1875
2003
  await authenticate(clientSessionId, deps);
1876
2004
  }
1877
- function failPasskey(setAuthState, message) {
1878
- setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
2005
+ function failPasskey(setAuthState, code, fallbackMessage) {
2006
+ const { message, errorCode } = resolveAuthError(code, fallbackMessage);
2007
+ setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
2008
+ }
2009
+
2010
+ // src/client/auth/providers.ts
2011
+ function oauthProvider(provider) {
2012
+ return {
2013
+ id: provider,
2014
+ login: (ctx) => ctx.startHostedOAuth(provider)
2015
+ };
2016
+ }
2017
+ function emailProvider() {
2018
+ return {
2019
+ id: "email",
2020
+ login: async (ctx, options) => {
2021
+ const email = options.email ?? "";
2022
+ const clientSessionId = await initEmailSession(ctx);
2023
+ if (clientSessionId) await sendEmailCode(email, clientSessionId, ctx);
2024
+ },
2025
+ actions: {
2026
+ begin: async (ctx) => {
2027
+ await initEmailSession(ctx);
2028
+ },
2029
+ sendCode: (ctx, payload) => {
2030
+ const { email, clientSessionId } = payload ?? {};
2031
+ return sendEmailCode(email, clientSessionId, ctx);
2032
+ },
2033
+ verifyCode: (ctx, payload) => {
2034
+ const { code, clientSessionId, email } = payload ?? {};
2035
+ return verifyAndAuthenticate(code, clientSessionId, email, ctx);
2036
+ }
2037
+ }
2038
+ };
2039
+ }
2040
+
2041
+ // src/client/auth/sep10-challenge.ts
2042
+ var ENVELOPE_TYPE_TX_V0 = 0;
2043
+ var ENVELOPE_TYPE_TX = 2;
2044
+ var KEY_TYPE_ED25519 = 0;
2045
+ var SEQ_OFFSET_V1 = 44;
2046
+ var SEQ_OFFSET_V0 = 40;
2047
+ function base64ToBytes(b64) {
2048
+ return base64urlDecode(b64.replace(/\+/g, "-").replace(/\//g, "_"));
2049
+ }
2050
+ function isI64Zero(view, offset) {
2051
+ return view.getUint32(offset, false) === 0 && view.getUint32(offset + 4, false) === 0;
2052
+ }
2053
+ function isValidSep10Challenge(challengeXdr) {
2054
+ try {
2055
+ const bytes = base64ToBytes(challengeXdr.trim());
2056
+ if (bytes.length < 8) return false;
2057
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2058
+ const envelopeType = view.getUint32(0, false);
2059
+ let seqOffset;
2060
+ if (envelopeType === ENVELOPE_TYPE_TX) {
2061
+ if (view.getUint32(4, false) !== KEY_TYPE_ED25519) return false;
2062
+ seqOffset = SEQ_OFFSET_V1;
2063
+ } else if (envelopeType === ENVELOPE_TYPE_TX_V0) {
2064
+ seqOffset = SEQ_OFFSET_V0;
2065
+ } else {
2066
+ return false;
2067
+ }
2068
+ if (bytes.length < seqOffset + 8) return false;
2069
+ return isI64Zero(view, seqOffset);
2070
+ } catch {
2071
+ return false;
2072
+ }
1879
2073
  }
1880
2074
 
1881
2075
  // src/client/auth/walletFlow.ts
@@ -1891,8 +2085,18 @@ function withSignal(promise, signal) {
1891
2085
  })
1892
2086
  ]);
1893
2087
  }
2088
+ async function requestWalletChallenge(clientSessionId, walletAddress, deps) {
2089
+ const { api, logger, signal } = deps;
2090
+ const body = { clientSessionId, walletAddress };
2091
+ const { data, error } = await api.POST("/auth/wallet/challenge", { body, signal });
2092
+ if (error || !data?.success) {
2093
+ if (!error) logApiError(logger, "POST /auth/wallet/challenge", { body, data });
2094
+ return null;
2095
+ }
2096
+ return data.content.challengeXdr;
2097
+ }
1894
2098
  async function loginWallet(type, deps) {
1895
- const { api, signal, setAuthState } = deps;
2099
+ const { api, logger, signal, setAuthState } = deps;
1896
2100
  const clientSessionId = await createAuthSession(deps);
1897
2101
  if (!clientSessionId) return;
1898
2102
  let connectedWallet;
@@ -1907,12 +2111,36 @@ async function loginWallet(type, deps) {
1907
2111
  const { address } = await withSignal(adapter.connect(), signal);
1908
2112
  connectedWallet = address;
1909
2113
  deps.storeWalletAdapter(adapter, type);
1910
- setAuthState({ step: "authenticating_wallet" });
1911
- const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
1912
- body: { clientSessionId, walletAddress: address },
2114
+ setAuthState({ step: "signing_wallet_challenge", walletType: type });
2115
+ const challengeXdr = await requestWalletChallenge(clientSessionId, address, deps);
2116
+ if (!challengeXdr) {
2117
+ setAuthState({
2118
+ step: "error",
2119
+ previousStep: "signing_wallet_challenge",
2120
+ message: "Failed to obtain wallet challenge",
2121
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2122
+ });
2123
+ return;
2124
+ }
2125
+ if (!isValidSep10Challenge(challengeXdr)) {
2126
+ logApiError(logger, "SEP-10 challenge validation", { error: "unexpected challenge structure (sequence != 0?)" });
2127
+ setAuthState({
2128
+ step: "error",
2129
+ previousStep: "signing_wallet_challenge",
2130
+ message: "Invalid wallet challenge",
2131
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2132
+ });
2133
+ return;
2134
+ }
2135
+ const { signedTxXdr } = await withSignal(
2136
+ adapter.signTransaction(challengeXdr, { networkPassphrase: deps.networkPassphrase }),
1913
2137
  signal
1914
- });
2138
+ );
2139
+ setAuthState({ step: "authenticating_wallet" });
2140
+ const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
2141
+ const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
1915
2142
  if (walletError || !walletData?.success) {
2143
+ if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
1916
2144
  setAuthState({
1917
2145
  step: "error",
1918
2146
  previousStep: "authenticating_wallet",
@@ -1921,7 +2149,8 @@ async function loginWallet(type, deps) {
1921
2149
  });
1922
2150
  return;
1923
2151
  }
1924
- } catch {
2152
+ } catch (err) {
2153
+ logApiError(logger, "wallet connect", { error: err });
1925
2154
  setAuthState({
1926
2155
  step: "error",
1927
2156
  previousStep: "connecting_wallet",
@@ -1997,6 +2226,13 @@ var PollarClient = class {
1997
2226
  this._loginController = null;
1998
2227
  /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
1999
2228
  this._resumeController = null;
2229
+ /**
2230
+ * Registry of pluggable login strategies, keyed by provider id. Seeded with
2231
+ * the built-ins (`google`, `github`, `email`) and then any `config.providers`
2232
+ * (which can override a built-in by reusing its id). `wallet` is deliberately
2233
+ * absent — it keeps its own dedicated flow. See {@link PollarAuthProvider}.
2234
+ */
2235
+ this._providers = /* @__PURE__ */ new Map();
2000
2236
  this.apiKey = config.apiKey;
2001
2237
  this.id = randomUUID();
2002
2238
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2018,6 +2254,12 @@ var PollarClient = class {
2018
2254
  this._maxIdleMs = config.maxIdleMs;
2019
2255
  this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2020
2256
  this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location?.origin ?? "" : "");
2257
+ for (const provider of [oauthProvider("google"), oauthProvider("github"), emailProvider()]) {
2258
+ this._providers.set(provider.id, provider);
2259
+ }
2260
+ for (const provider of config.providers ?? []) {
2261
+ this._providers.set(provider.id, provider);
2262
+ }
2021
2263
  this._api = createApiClient(this.basePath);
2022
2264
  this._wireMiddlewares();
2023
2265
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -2127,28 +2369,77 @@ var PollarClient = class {
2127
2369
  onResponse: async ({ request, response }) => {
2128
2370
  const newNonce = response.headers.get("DPoP-Nonce");
2129
2371
  if (newNonce) self._dpopNonce = newNonce;
2130
- if (response.status !== 401) return response;
2372
+ if (response.status !== 401) return self._logHttp(request, response);
2131
2373
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
2132
2374
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2133
2375
  if (request.url.includes("/auth/refresh")) {
2134
- if (isNonceChallenge) return self._retryRequest(request);
2135
- return response;
2376
+ if (isNonceChallenge) return self._logHttp(request, await self._retryRequest(request));
2377
+ return self._logHttp(request, response);
2136
2378
  }
2137
2379
  if (!isNonceChallenge) {
2138
2380
  try {
2139
2381
  await self.refresh();
2140
2382
  } catch {
2141
- return response;
2383
+ return self._logHttp(request, response);
2142
2384
  }
2143
2385
  const method = request.method.toUpperCase();
2144
2386
  if (method !== "GET" && method !== "HEAD") {
2145
- return response;
2387
+ return self._logHttp(request, response);
2146
2388
  }
2147
2389
  }
2148
- return self._retryRequest(request);
2390
+ return self._logHttp(request, await self._retryRequest(request));
2149
2391
  }
2150
2392
  });
2151
2393
  }
2394
+ /**
2395
+ * Logs the final outcome of an SDK API call exactly once: successes (`2xx`) at
2396
+ * `debug` (method + path + status, no body), failures (`4xx`/`5xx`) at `error`
2397
+ * with the redacted request body and the response error body. Returns the
2398
+ * response so it can be chained at the middleware's return points. The error
2399
+ * body is read off a synchronous `clone()` so it never disturbs the body the
2400
+ * caller consumes.
2401
+ */
2402
+ _logHttp(request, response) {
2403
+ const path = this._httpPath(request.url);
2404
+ const label = `[PollarClient:http] ${request.method.toUpperCase()} ${path} ${response.status}`;
2405
+ if (response.ok) {
2406
+ this._log.debug(label);
2407
+ } else {
2408
+ void this._logHttpError(label, request, response.clone());
2409
+ }
2410
+ return response;
2411
+ }
2412
+ /** Reads the redacted request body + JSON response body and logs at `error`. */
2413
+ async _logHttpError(label, request, response) {
2414
+ let requestBody;
2415
+ const cached = this._requestBodyCache.get(request);
2416
+ if (cached) {
2417
+ try {
2418
+ requestBody = redactBody(JSON.parse(new TextDecoder().decode(cached)));
2419
+ } catch {
2420
+ }
2421
+ }
2422
+ let responseBody;
2423
+ if ((response.headers.get("content-type") ?? "").includes("application/json")) {
2424
+ try {
2425
+ responseBody = await response.json();
2426
+ } catch {
2427
+ }
2428
+ }
2429
+ this._log.error(label, {
2430
+ ...requestBody !== void 0 ? { requestBody } : {},
2431
+ ...responseBody !== void 0 ? { responseBody } : {}
2432
+ });
2433
+ }
2434
+ /** Strips origin + `/v1` version prefix from a request URL for compact logs. */
2435
+ _httpPath(url) {
2436
+ try {
2437
+ const { pathname } = new URL(url);
2438
+ return pathname.startsWith("/v1/") ? pathname.slice(3) : pathname;
2439
+ } catch {
2440
+ return url;
2441
+ }
2442
+ }
2152
2443
  async _buildProofForRequest(request, accessToken) {
2153
2444
  try {
2154
2445
  const htu = request.url.split("?")[0].split("#")[0];
@@ -2375,28 +2666,42 @@ var PollarClient = class {
2375
2666
  warnServerSide("login");
2376
2667
  return;
2377
2668
  }
2378
- if (options.provider === "google" || options.provider === "github" || options.provider === "email") {
2379
- const controller = this._newController();
2380
- const deps = this._flowDeps(controller.signal);
2381
- if (options.provider === "google" || options.provider === "github") {
2382
- loginOAuth(options.provider, {
2383
- ...deps,
2384
- basePath: this.basePath,
2385
- apiKey: this.apiKey,
2386
- openAuthUrl: this._openAuthUrl,
2387
- redirectUri: this._oauthRedirectUri
2388
- }).catch((err) => this._handleFlowError(err));
2389
- } else if (options.provider === "email") {
2390
- const { email } = options;
2391
- initEmailSession(deps).then(() => {
2392
- if (this._authState.step === "entering_email") {
2393
- return sendEmailCode(email, this._authState.clientSessionId, deps);
2394
- }
2395
- }).catch((err) => this._handleFlowError(err));
2396
- }
2397
- } else if (options.provider === "wallet") {
2669
+ if (options.provider === "wallet") {
2398
2670
  this.loginWallet(options.type);
2671
+ return;
2399
2672
  }
2673
+ const provider = this._providers.get(options.provider);
2674
+ if (!provider?.login) {
2675
+ this._setAuthState({
2676
+ step: "error",
2677
+ previousStep: this._authState.step,
2678
+ message: `No auth provider registered for '${options.provider}'`,
2679
+ errorCode: AUTH_ERROR_CODES.AUTH_FAILED
2680
+ });
2681
+ return;
2682
+ }
2683
+ const controller = this._newController();
2684
+ provider.login(this._providerContext(controller.signal), options).catch((err) => this._handleFlowError(err));
2685
+ }
2686
+ /**
2687
+ * Invoke a named secondary step on a registered provider (e.g. email's
2688
+ * `sendCode` / `verifyCode`, or a custom provider's multi-step continuation).
2689
+ * Reuses the in-flight login `AbortController` when one exists so the step
2690
+ * stays cancellable via `cancelLogin()`; otherwise starts a fresh one. The
2691
+ * built-in email steps also have dedicated typed methods
2692
+ * ({@link sendEmailCode} / {@link verifyEmailCode}) — prefer those for email.
2693
+ */
2694
+ providerAction(provider, action, payload) {
2695
+ if (!isClientRuntime) {
2696
+ warnServerSide("providerAction");
2697
+ return;
2698
+ }
2699
+ const fn = this._providers.get(provider)?.actions?.[action];
2700
+ if (!fn) {
2701
+ throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
2702
+ }
2703
+ const signal = this._loginController?.signal ?? this._newController().signal;
2704
+ fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
2400
2705
  }
2401
2706
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2402
2707
  beginEmailLogin() {
@@ -2405,7 +2710,7 @@ var PollarClient = class {
2405
2710
  return;
2406
2711
  }
2407
2712
  const controller = this._newController();
2408
- initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2713
+ initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
2409
2714
  }
2410
2715
  sendEmailCode(email) {
2411
2716
  if (!isClientRuntime) {
@@ -2417,7 +2722,7 @@ var PollarClient = class {
2417
2722
  }
2418
2723
  const { clientSessionId } = this._authState;
2419
2724
  const signal = this._loginController.signal;
2420
- sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2725
+ sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
2421
2726
  }
2422
2727
  verifyEmailCode(code) {
2423
2728
  if (!isClientRuntime) {
@@ -2432,7 +2737,7 @@ var PollarClient = class {
2432
2737
  const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
2433
2738
  const email = state.step === "entering_code" ? state.email : state.email ?? "";
2434
2739
  const controller = this._newController();
2435
- verifyAndAuthenticate(code, clientSessionId, email, this._flowDeps(controller.signal)).catch(
2740
+ verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
2436
2741
  (err) => this._handleFlowError(err)
2437
2742
  );
2438
2743
  }
@@ -2797,6 +3102,29 @@ var PollarClient = class {
2797
3102
  getWalletType() {
2798
3103
  return this._walletAdapter?.type ?? null;
2799
3104
  }
3105
+ /**
3106
+ * The authenticated user's wallet as a {@link WalletInfo} discriminated union,
3107
+ * or `null` when there's no session (or the session carries no address yet).
3108
+ *
3109
+ * `custody` strictly determines `provider` (the mapping is 1:1 and fixed at
3110
+ * account creation server-side): `external` reports the connected adapter id
3111
+ * (`getWalletType()`), `smart` is always `'passkey'`, and `internal` reports
3112
+ * the login method the backend recorded (`null` for pre-provider sessions).
3113
+ */
3114
+ getWallet() {
3115
+ const w = this._session?.wallet;
3116
+ if (!w || !w.address) return null;
3117
+ switch (w.type) {
3118
+ case "external":
3119
+ return { custody: "external", address: w.address, provider: this._walletAdapter?.type ?? null };
3120
+ case "smart":
3121
+ return { custody: "smart", address: w.address, provider: "passkey" };
3122
+ case "internal":
3123
+ return { custody: "internal", address: w.address, provider: w.provider ?? null };
3124
+ default:
3125
+ return null;
3126
+ }
3127
+ }
2800
3128
  /**
2801
3129
  * Signs the given unsigned XDR and returns the signed XDR.
2802
3130
  *
@@ -2850,14 +3178,16 @@ var PollarClient = class {
2850
3178
  });
2851
3179
  return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2852
3180
  }
2853
- const details = error?.details;
3181
+ const { details, code, message } = this._resolveTxApiError(error);
2854
3182
  this._setTransactionState({
2855
3183
  step: "error",
2856
3184
  phase: "signing",
2857
3185
  ...buildData && { buildData },
2858
- ...details && { details }
3186
+ ...details && { details },
3187
+ ...code && { code },
3188
+ ...message && { message }
2859
3189
  });
2860
- return { status: "error", ...details && { details } };
3190
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
2861
3191
  } catch (err) {
2862
3192
  const details = err instanceof Error ? err.message : void 0;
2863
3193
  this._setTransactionState({
@@ -2869,6 +3199,54 @@ var PollarClient = class {
2869
3199
  return { status: "error", ...details && { details } };
2870
3200
  }
2871
3201
  }
3202
+ /**
3203
+ * Sign a single Soroban authorization entry (`SorobanAuthorizationEntry`).
3204
+ *
3205
+ * Use this when a contract is the transaction source (e.g. it sponsors the
3206
+ * gas and swaps the fee out of the user's token) and only needs the user's
3207
+ * address-credentials authorization, not a full signed envelope. The signed
3208
+ * entry is returned as base64 XDR for the caller to compose into its tx.
3209
+ *
3210
+ * - External wallets (Freighter/Albedo) sign the entry via the provider.
3211
+ * - Custodial wallets are signed by the backend, which FIRST validates the
3212
+ * entry's invocation tree against the app's contract/function allowlist and
3213
+ * caps the validity window — entries touching a non-allowlisted contract or
3214
+ * function, or expiring too far ahead, are rejected.
3215
+ *
3216
+ * @param entryXdr base64 XDR of the unsigned `SorobanAuthorizationEntry`.
3217
+ * @param options.validUntilLedger absolute ledger the signature expires at
3218
+ * (computed from the network's latest ledger). Ignored on the external-wallet
3219
+ * path, where the provider sets its own expiration.
3220
+ */
3221
+ async signAuthEntry(entryXdr, options) {
3222
+ if (this._walletAdapter) {
3223
+ const accountToSign = this._session?.wallet?.address;
3224
+ try {
3225
+ const { signedAuthEntry } = await this._walletAdapter.signAuthEntry(
3226
+ entryXdr,
3227
+ accountToSign ? { accountToSign } : void 0
3228
+ );
3229
+ return { status: "signed", signedAuthEntry };
3230
+ } catch (err) {
3231
+ const details = err instanceof Error ? err.message : void 0;
3232
+ return { status: "error", ...details && { details } };
3233
+ }
3234
+ }
3235
+ const address = this._session?.wallet?.address ?? "";
3236
+ try {
3237
+ const { data, error } = await this._api.POST("/tx/sign-auth-entry", {
3238
+ body: { network: this.getNetwork(), address, entryXdr, validUntilLedger: options.validUntilLedger }
3239
+ });
3240
+ if (!error && data?.success && data.content?.signedAuthEntry) {
3241
+ return { status: "signed", signedAuthEntry: data.content.signedAuthEntry };
3242
+ }
3243
+ const details = error?.details;
3244
+ return { status: "error", ...details && { details } };
3245
+ } catch (err) {
3246
+ const details = err instanceof Error ? err.message : void 0;
3247
+ return { status: "error", ...details && { details } };
3248
+ }
3249
+ }
2872
3250
  /**
2873
3251
  * Submits a signed XDR via `/tx/submit` regardless of wallet type
2874
3252
  * (custodial or external). Routing through sdk-api gives us:
@@ -2887,6 +3265,21 @@ var PollarClient = class {
2887
3265
  * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2888
3266
  * or `error[phase: 'submitting']` on failure.
2889
3267
  */
3268
+ /**
3269
+ * Normalize a backend API error into { details, code, message }. `code` is the
3270
+ * precise backend ErrorCode (e.g. `TX_FEE_LIMIT_EXCEEDED`) for programmatic
3271
+ * handling; `message` is a friendly string from the error catalog; `details`
3272
+ * is the raw diagnostic. Lets tx flows surface a typed reason instead of an
3273
+ * opaque details string.
3274
+ */
3275
+ _resolveTxApiError(error) {
3276
+ const e = error;
3277
+ const details = e?.details ?? e?.message;
3278
+ const code = e?.code;
3279
+ if (!code) return details ? { details } : {};
3280
+ const { message } = resolveAuthError(code, details ?? code);
3281
+ return { code, message, ...details && { details } };
3282
+ }
2890
3283
  async submitTx(signedXdr, opts) {
2891
3284
  const buildData = this._currentBuildData();
2892
3285
  const outcomeExtra = buildData ? { buildData } : {};
@@ -2924,14 +3317,22 @@ var PollarClient = class {
2924
3317
  ...resultCode && { details: resultCode, resultCode }
2925
3318
  };
2926
3319
  }
2927
- const details = error?.details;
3320
+ const { details, code, message } = this._resolveTxApiError(error);
2928
3321
  this._setTransactionState({
2929
3322
  step: "error",
2930
3323
  phase: "submitting",
2931
3324
  ...buildData && { buildData },
2932
- ...details && { details }
3325
+ ...details && { details },
3326
+ ...code && { code },
3327
+ ...message && { message }
2933
3328
  });
2934
- return { status: "error", ...outcomeExtra, ...details && { details } };
3329
+ return {
3330
+ status: "error",
3331
+ ...outcomeExtra,
3332
+ ...details && { details },
3333
+ ...code && { code },
3334
+ ...message && { message }
3335
+ };
2935
3336
  } catch (err) {
2936
3337
  const details = err instanceof Error ? err.message : void 0;
2937
3338
  this._setTransactionState({
@@ -3018,14 +3419,22 @@ var PollarClient = class {
3018
3419
  ...resultCode && { details: resultCode, resultCode }
3019
3420
  };
3020
3421
  }
3021
- const details = error?.details;
3422
+ const { details, code, message } = this._resolveTxApiError(error);
3022
3423
  this._setTransactionState({
3023
3424
  step: "error",
3024
3425
  phase: "signing-submitting",
3025
3426
  ...buildData && { buildData },
3026
- ...details && { details }
3427
+ ...details && { details },
3428
+ ...code && { code },
3429
+ ...message && { message }
3027
3430
  });
3028
- return { status: "error", ...outcomeExtra, ...details && { details } };
3431
+ return {
3432
+ status: "error",
3433
+ ...outcomeExtra,
3434
+ ...details && { details },
3435
+ ...code && { code },
3436
+ ...message && { message }
3437
+ };
3029
3438
  } catch (err) {
3030
3439
  const details = err instanceof Error ? err.message : void 0;
3031
3440
  this._setTransactionState({
@@ -3100,13 +3509,15 @@ var PollarClient = class {
3100
3509
  });
3101
3510
  return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
3102
3511
  }
3103
- const details = error?.details;
3512
+ const { details, code, message } = this._resolveTxApiError(error);
3104
3513
  this._setTransactionState({
3105
3514
  step: "error",
3106
3515
  phase: "building-signing-submitting",
3107
- ...details && { details }
3516
+ ...details && { details },
3517
+ ...code && { code },
3518
+ ...message && { message }
3108
3519
  });
3109
- return { status: "error", ...details && { details } };
3520
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3110
3521
  } catch (err) {
3111
3522
  const details = err instanceof Error ? err.message : void 0;
3112
3523
  this._setTransactionState({
@@ -3219,9 +3630,22 @@ var PollarClient = class {
3219
3630
  });
3220
3631
  return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3221
3632
  }
3222
- const details = error?.details;
3223
- this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3224
- return { status: "error", ...outcomeExtra, ...details && { details } };
3633
+ const { details, code, message } = this._resolveTxApiError(error);
3634
+ this._setTransactionState({
3635
+ step: "error",
3636
+ phase: "submitting",
3637
+ buildData,
3638
+ ...details && { details },
3639
+ ...code && { code },
3640
+ ...message && { message }
3641
+ });
3642
+ return {
3643
+ status: "error",
3644
+ ...outcomeExtra,
3645
+ ...details && { details },
3646
+ ...code && { code },
3647
+ ...message && { message }
3648
+ };
3225
3649
  } catch (err) {
3226
3650
  const details = err instanceof Error ? err.message : void 0;
3227
3651
  this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
@@ -3299,11 +3723,67 @@ var PollarClient = class {
3299
3723
  this._loginController = new AbortController();
3300
3724
  return this._loginController;
3301
3725
  }
3726
+ /**
3727
+ * Build the {@link AuthProviderContext} facade for one login attempt. Wraps
3728
+ * the internal `FlowDeps` so providers get only the curated primitives —
3729
+ * `createSession`, `authenticate`, `exchangeExternalToken`, `startHostedOAuth`
3730
+ * — while storage / wallet-adapter / key-manager internals stay private. All
3731
+ * legs share the same `signal`, so `cancelLogin()` aborts the whole chain.
3732
+ */
3733
+ _providerContext(signal) {
3734
+ const deps = this._flowDeps(signal);
3735
+ return {
3736
+ signal,
3737
+ api: this._api,
3738
+ basePath: this.basePath,
3739
+ apiKey: this.apiKey,
3740
+ logger: this._log,
3741
+ setAuthState: this._setAuthState.bind(this),
3742
+ createSession: () => createAuthSession(deps),
3743
+ authenticate: (clientSessionId) => authenticate(clientSessionId, deps),
3744
+ requestChallenge: (clientSessionId, walletAddress) => requestWalletChallenge(clientSessionId, walletAddress, deps),
3745
+ exchangeExternalToken: (clientSessionId, body) => this._exchangeExternalToken(clientSessionId, body, signal),
3746
+ startHostedOAuth: (provider) => loginOAuth(provider, {
3747
+ ...deps,
3748
+ basePath: this.basePath,
3749
+ apiKey: this.apiKey,
3750
+ openAuthUrl: this._openAuthUrl,
3751
+ redirectUri: this._oauthRedirectUri
3752
+ })
3753
+ };
3754
+ }
3755
+ /**
3756
+ * Generic external-provider exchange leg (`POST /auth/external`). Custom
3757
+ * providers call this (via the context) after their own SDK has authenticated
3758
+ * the user and the wallet has counter-signed the SEP-10 challenge
3759
+ * (`{ provider, walletAddress, signedChallengeXdr }`). On success the session
3760
+ * is marked READY server-side and the provider should then call
3761
+ * `ctx.authenticate(clientSessionId)`. Returns `false` (and sets an error
3762
+ * state) on failure.
3763
+ */
3764
+ async _exchangeExternalToken(clientSessionId, body, signal) {
3765
+ const { data, error } = await this._api.POST("/auth/external", {
3766
+ body: { clientSessionId, ...body },
3767
+ signal
3768
+ });
3769
+ if (error || !data?.success) {
3770
+ this._log.error("[PollarClient] External provider authentication failed", { error });
3771
+ this._setAuthState({
3772
+ step: "error",
3773
+ previousStep: this._authState.step,
3774
+ message: "External provider authentication failed",
3775
+ errorCode: AUTH_ERROR_CODES.EXTERNAL_AUTH_FAILED
3776
+ });
3777
+ return false;
3778
+ }
3779
+ return true;
3780
+ }
3302
3781
  _flowDeps(signal) {
3303
3782
  return {
3304
3783
  api: this._api,
3305
3784
  logger: this._log,
3306
3785
  basePath: this.basePath,
3786
+ networkPassphrase: this._networkPassphrase(),
3307
3787
  // SSE status streaming works on web; React Native's `fetch` has no
3308
3788
  // readable `response.body`, so those clients poll the non-streaming
3309
3789
  // status endpoint instead. `isBrowser` is false in RN and SSR alike.
@@ -3435,6 +3915,7 @@ var PollarClient = class {
3435
3915
  async _storeSession(session) {
3436
3916
  this._log.info("[PollarClient] Session stored");
3437
3917
  const w = session.wallet;
3918
+ const wireProvider = w.provider;
3438
3919
  const persisted = {
3439
3920
  clientSessionId: session.clientSessionId,
3440
3921
  userId: session.userId ?? null,
@@ -3448,6 +3929,7 @@ var PollarClient = class {
3448
3929
  // persisted session speak one vocabulary while the wire stays compatible.
3449
3930
  wallet: {
3450
3931
  type: w.type === "custodial" ? "internal" : w.type,
3932
+ ...wireProvider ? { provider: wireProvider } : {},
3451
3933
  address: w.address ?? w.publicKey ?? null,
3452
3934
  ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3453
3935
  ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},