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