@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.mjs CHANGED
@@ -187,7 +187,7 @@ function defaultKeyManager(storage, apiKey) {
187
187
 
188
188
  // src/lib/base64url.ts
189
189
  var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
190
- (() => {
190
+ var REVERSE = (() => {
191
191
  const m = /* @__PURE__ */ new Map();
192
192
  for (let i = 0; i < ALPHABET.length; i++) m.set(ALPHABET[i], i);
193
193
  return m;
@@ -218,6 +218,28 @@ function base64urlEncode(bytes) {
218
218
  }
219
219
  return result;
220
220
  }
221
+ function base64urlDecode(input) {
222
+ const clean = input.replace(/=+$/, "");
223
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
224
+ let byteIdx = 0;
225
+ for (let i = 0; i < clean.length; i += 4) {
226
+ const c1 = REVERSE.get(clean[i]);
227
+ const c2 = REVERSE.get(clean[i + 1]);
228
+ const c3 = i + 2 < clean.length ? REVERSE.get(clean[i + 2]) : void 0;
229
+ const c4 = i + 3 < clean.length ? REVERSE.get(clean[i + 3]) : void 0;
230
+ if (c1 === void 0 || c2 === void 0) {
231
+ throw new Error("[PollarClient] Invalid base64url input");
232
+ }
233
+ out[byteIdx++] = c1 << 2 | c2 >> 4;
234
+ if (c3 !== void 0) {
235
+ out[byteIdx++] = (c2 & 15) << 4 | c3 >> 2;
236
+ if (c4 !== void 0) {
237
+ out[byteIdx++] = (c3 & 3) << 6 | c4;
238
+ }
239
+ }
240
+ }
241
+ return out.slice(0, byteIdx);
242
+ }
221
243
  function base64urlEncodeString(s) {
222
244
  return base64urlEncode(new TextEncoder().encode(s));
223
245
  }
@@ -1083,6 +1105,29 @@ function createLogger(level = "info", sink = console) {
1083
1105
  return { error: gate("error"), warn: gate("warn"), info: gate("info"), debug: gate("debug") };
1084
1106
  }
1085
1107
 
1108
+ // src/lib/logging.ts
1109
+ var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
1110
+ "email",
1111
+ "code",
1112
+ "walletAddress",
1113
+ "dpopJwk",
1114
+ "response",
1115
+ "refreshToken",
1116
+ // SEP-10 challenge envelopes: a counter-signed challenge is a live, replayable
1117
+ // auth credential — never log it in the clear.
1118
+ "signedChallengeXdr",
1119
+ "challengeXdr",
1120
+ "signedTxXdr"
1121
+ ]);
1122
+ function redactBody(body) {
1123
+ if (!body || typeof body !== "object") return body;
1124
+ const out = {};
1125
+ for (const [key, value] of Object.entries(body)) {
1126
+ out[key] = SENSITIVE_BODY_KEYS.has(key) ? "[redacted]" : value;
1127
+ }
1128
+ return out;
1129
+ }
1130
+
1086
1131
  // src/storage/web.ts
1087
1132
  var LOG_PREFIX = "[PollarClient:storage]";
1088
1133
  function createMemoryAdapter() {
@@ -1170,8 +1215,36 @@ function defaultStorage(options = {}) {
1170
1215
  return createLocalStorageAdapter(options);
1171
1216
  }
1172
1217
 
1218
+ // src/types.ts
1219
+ var AUTH_ERROR_CODES = {
1220
+ SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1221
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1222
+ SESSION_INVALID: "SESSION_INVALID",
1223
+ EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1224
+ EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1225
+ EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1226
+ EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1227
+ AUTH_FAILED: "AUTH_FAILED",
1228
+ WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1229
+ WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1230
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1231
+ EXTERNAL_AUTH_FAILED: "EXTERNAL_AUTH_FAILED",
1232
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1233
+ // Generic bucket for on-chain transaction failures; the precise reason is the
1234
+ // backend `code` (e.g. TX_FEE_LIMIT_EXCEEDED) carried alongside on the outcome.
1235
+ TX_FAILED: "TX_FAILED",
1236
+ UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1237
+ };
1238
+ var PollarFlowError = class extends Error {
1239
+ constructor(message) {
1240
+ super(message);
1241
+ this.code = "INVALID_FLOW";
1242
+ this.name = "PollarFlowError";
1243
+ }
1244
+ };
1245
+
1173
1246
  // src/version.ts
1174
- var POLLAR_CORE_VERSION = "0.9.0" ;
1247
+ var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
1175
1248
 
1176
1249
  // src/visibility/noop.ts
1177
1250
  function createNoopVisibilityProvider() {
@@ -1224,30 +1297,6 @@ function defaultVisibilityProvider() {
1224
1297
  return createNoopVisibilityProvider();
1225
1298
  }
1226
1299
 
1227
- // src/types.ts
1228
- var AUTH_ERROR_CODES = {
1229
- SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1230
- SESSION_EXPIRED: "SESSION_EXPIRED",
1231
- SESSION_INVALID: "SESSION_INVALID",
1232
- EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1233
- EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1234
- EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1235
- EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1236
- AUTH_FAILED: "AUTH_FAILED",
1237
- WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1238
- WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1239
- WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1240
- PASSKEY_FAILED: "PASSKEY_FAILED",
1241
- UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1242
- };
1243
- var PollarFlowError = class extends Error {
1244
- constructor(message) {
1245
- super(message);
1246
- this.code = "INVALID_FLOW";
1247
- this.name = "PollarFlowError";
1248
- }
1249
- };
1250
-
1251
1300
  // src/wallets/FreighterAdapter.ts
1252
1301
  var import_freighter_api = __toESM(require_index_min());
1253
1302
 
@@ -1343,10 +1392,13 @@ function openAlbedoPopup(url) {
1343
1392
  }
1344
1393
  function waitForAlbedoPopup() {
1345
1394
  return new Promise((resolve, reject) => {
1346
- const timeout = setTimeout(() => {
1347
- window.removeEventListener("message", handler);
1348
- reject(new Error("Albedo response timeout"));
1349
- }, 2 * 60 * 1e3);
1395
+ const timeout = setTimeout(
1396
+ () => {
1397
+ window.removeEventListener("message", handler);
1398
+ reject(new Error("Albedo response timeout"));
1399
+ },
1400
+ 2 * 60 * 1e3
1401
+ );
1350
1402
  function handler(event) {
1351
1403
  if (event.origin !== window.location.origin || event.data?.type !== "ALBEDO_RESULT") return;
1352
1404
  clearTimeout(timeout);
@@ -1500,6 +1552,10 @@ function isValidSession(value, logger = console) {
1500
1552
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
1501
1553
  return false;
1502
1554
  }
1555
+ if (w["provider"] !== void 0 && typeof w["provider"] !== "string") {
1556
+ logger.debug("[PollarClient:session] Invalid session \u2014 wallet.provider must be a string if present");
1557
+ return false;
1558
+ }
1503
1559
  if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1504
1560
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1505
1561
  return false;
@@ -1706,6 +1762,16 @@ function waitForSessionReady(args) {
1706
1762
  return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal, logger) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal, logger);
1707
1763
  }
1708
1764
 
1765
+ // src/client/auth/logging.ts
1766
+ function logApiError(logger, route, detail = {}, level = "error") {
1767
+ const { body, error, data } = detail;
1768
+ logger[level](`[PollarClient:auth] ${route} failed`, {
1769
+ route,
1770
+ ...body !== void 0 ? { body: redactBody(body) } : {},
1771
+ cause: error ?? data
1772
+ });
1773
+ }
1774
+
1709
1775
  // src/client/auth/authenticate.ts
1710
1776
  async function authenticate(clientSessionId, deps, expectedWallet) {
1711
1777
  const { api, logger, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
@@ -1723,6 +1789,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1723
1789
  } catch (err) {
1724
1790
  if (err instanceof SessionStatusError) {
1725
1791
  const expired = err.code === "EXPIRED_CLIENT_ID";
1792
+ logApiError(logger, "session status", { data: err });
1726
1793
  setAuthState({
1727
1794
  step: "error",
1728
1795
  previousStep: "authenticating",
@@ -1735,14 +1802,12 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1735
1802
  throw err;
1736
1803
  }
1737
1804
  const dpopJwk = await deps.getPublicJwk();
1738
- const { data } = await api.POST("/auth/login", {
1739
- body: {
1740
- clientSessionId,
1741
- dpopJwk,
1742
- ...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
1743
- },
1744
- signal
1745
- });
1805
+ const body = {
1806
+ clientSessionId,
1807
+ dpopJwk,
1808
+ ...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
1809
+ };
1810
+ const { data, error } = await api.POST("/auth/login", { body, signal });
1746
1811
  if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content, logger)) {
1747
1812
  const sessionWallet = data.content.data?.providers?.wallet?.address;
1748
1813
  if (expectedWallet && sessionWallet !== expectedWallet) {
@@ -1757,6 +1822,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1757
1822
  }
1758
1823
  await storeSession(data.content);
1759
1824
  } else {
1825
+ if (!error) logApiError(logger, "POST /auth/login", { body, data });
1760
1826
  setAuthState({
1761
1827
  step: "error",
1762
1828
  previousStep: "authenticating",
@@ -1769,10 +1835,11 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1769
1835
 
1770
1836
  // src/client/auth/deps.ts
1771
1837
  async function createAuthSession(deps) {
1772
- const { api, signal, setAuthState } = deps;
1838
+ const { api, logger, signal, setAuthState } = deps;
1773
1839
  setAuthState({ step: "creating_session" });
1774
1840
  const { data, error } = await api.POST("/auth/session", { signal });
1775
1841
  if (error || !data?.success) {
1842
+ if (!error) logApiError(logger, "POST /auth/session", { data });
1776
1843
  setAuthState({
1777
1844
  step: "error",
1778
1845
  previousStep: "creating_session",
@@ -1784,20 +1851,96 @@ async function createAuthSession(deps) {
1784
1851
  return data.content.clientSessionId;
1785
1852
  }
1786
1853
 
1854
+ // src/client/auth/errorMessages.ts
1855
+ var CATALOG = {
1856
+ // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1857
+ SPONSOR_NOT_FUNDED: {
1858
+ message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1859
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1860
+ },
1861
+ APP_WALLET_NOT_FOUND: {
1862
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1863
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1864
+ },
1865
+ WALLET_NOT_FOUND: {
1866
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1867
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1868
+ },
1869
+ PASSKEY_DEPLOY_FAILED: {
1870
+ message: "We couldn't finish creating your wallet. Please try again in a moment.",
1871
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1872
+ },
1873
+ // ── Passkey ceremony ────────────────────────────────────────────────────────
1874
+ PASSKEY_ALREADY_REGISTERED: {
1875
+ message: "A passkey is already registered for this account. Try signing in instead.",
1876
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1877
+ },
1878
+ PASSKEY_UNKNOWN_CREDENTIAL: {
1879
+ message: "We don't recognize this passkey. Try creating a new one.",
1880
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1881
+ },
1882
+ PASSKEY_VERIFICATION_FAILED: {
1883
+ message: "We couldn't verify your passkey. Please try again.",
1884
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1885
+ },
1886
+ PASSKEY_CHALLENGE_MISSING: {
1887
+ message: "Your passkey session expired. Please start again.",
1888
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1889
+ },
1890
+ // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1891
+ // These map to the TX_FAILED bucket (not PASSKEY_FAILED) — the precise reason
1892
+ // is the entry key itself, surfaced as the raw `code` on the tx outcome.
1893
+ TX_INSUFFICIENT_BALANCE: {
1894
+ message: "Insufficient balance to complete this transaction.",
1895
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1896
+ },
1897
+ TX_INSUFFICIENT_FEE: {
1898
+ message: "Not enough XLM to cover the network fee. Add more XLM to your wallet and try again.",
1899
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1900
+ },
1901
+ TX_FEE_LIMIT_EXCEEDED: {
1902
+ message: "The transaction fee is above the allowed limit. Please try again.",
1903
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1904
+ },
1905
+ TX_CONTRACT_FAILED: {
1906
+ message: "The contract rejected this operation. Check the operation is allowed right now and try again.",
1907
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1908
+ },
1909
+ TX_DESTINATION_NOT_FOUND: {
1910
+ message: "The destination account doesn't exist on the network yet.",
1911
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1912
+ },
1913
+ TX_NO_TRUSTLINE: {
1914
+ message: "The destination can't receive this asset yet (no trustline).",
1915
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1916
+ },
1917
+ TX_BAD_SEQUENCE: {
1918
+ message: "Something went out of sync. Please try again.",
1919
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1920
+ }
1921
+ };
1922
+ function resolveAuthError(code, fallbackMessage) {
1923
+ if (code && CATALOG[code]) return CATALOG[code];
1924
+ return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1925
+ }
1926
+ function extractErrorCode(error, data) {
1927
+ return error?.code ?? data?.code ?? void 0;
1928
+ }
1929
+
1787
1930
  // src/client/auth/emailFlow.ts
1788
- async function initEmailSession(deps) {
1789
- const clientSessionId = await createAuthSession(deps);
1790
- if (!clientSessionId) return;
1791
- deps.setAuthState({ step: "entering_email", clientSessionId });
1931
+ async function initEmailSession(ctx) {
1932
+ const clientSessionId = await ctx.createSession();
1933
+ if (!clientSessionId) return null;
1934
+ ctx.setAuthState({ step: "entering_email", clientSessionId });
1935
+ return clientSessionId;
1792
1936
  }
1793
- async function sendEmailCode(email, clientSessionId, deps) {
1794
- const { api, signal, setAuthState } = deps;
1937
+ async function sendEmailCode(email, clientSessionId, ctx) {
1938
+ const { api, logger, signal, setAuthState } = ctx;
1795
1939
  setAuthState({ step: "sending_email", email });
1796
- const { data, error } = await api.POST("/auth/email", {
1797
- body: { clientSessionId, email },
1798
- signal
1799
- });
1940
+ const body = { clientSessionId, email };
1941
+ const { data, error } = await api.POST("/auth/email", { body, signal });
1800
1942
  if (error || !data?.success) {
1943
+ if (!error) logApiError(logger, "POST /auth/email", { body, data });
1801
1944
  setAuthState({
1802
1945
  step: "error",
1803
1946
  previousStep: "sending_email",
@@ -1808,19 +1951,18 @@ async function sendEmailCode(email, clientSessionId, deps) {
1808
1951
  }
1809
1952
  setAuthState({ step: "entering_code", clientSessionId, email });
1810
1953
  }
1811
- async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1812
- const { api, signal, setAuthState } = deps;
1954
+ async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
1955
+ const { api, logger, signal, setAuthState } = ctx;
1813
1956
  setAuthState({ step: "verifying_email_code", clientSessionId, email });
1814
- const { data, error } = await api.POST("/auth/email/verify-code", {
1815
- body: { clientSessionId, code },
1816
- signal
1817
- });
1957
+ const body = { clientSessionId, code };
1958
+ const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
1818
1959
  if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
1819
- await authenticate(clientSessionId, deps);
1960
+ await ctx.authenticate(clientSessionId);
1820
1961
  return;
1821
1962
  }
1822
1963
  const errCode = error?.error ?? data?.code;
1823
1964
  if (errCode === "SDK_EMAIL_CODE_EXPIRED") {
1965
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1824
1966
  setAuthState({
1825
1967
  step: "error",
1826
1968
  previousStep: "verifying_email_code",
@@ -1832,6 +1974,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1832
1974
  return;
1833
1975
  }
1834
1976
  if (errCode === "INVALID_EMAIL_CODE" || errCode === "SDK_EMAIL_CODE_INVALID") {
1977
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1835
1978
  setAuthState({
1836
1979
  step: "error",
1837
1980
  previousStep: "verifying_email_code",
@@ -1842,6 +1985,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1842
1985
  });
1843
1986
  return;
1844
1987
  }
1988
+ if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
1845
1989
  setAuthState({
1846
1990
  step: "error",
1847
1991
  previousStep: "verifying_email_code",
@@ -1893,8 +2037,9 @@ async function loginOAuth(provider, deps) {
1893
2037
 
1894
2038
  // src/client/auth/passkeyFlow.ts
1895
2039
  async function smartWalletFlow(deps, mode) {
1896
- const { api, signal, setAuthState, passkey } = deps;
2040
+ const { api, logger, signal, setAuthState, passkey } = deps;
1897
2041
  if (!passkey) {
2042
+ logger.error("[PollarClient:auth] passkey ceremony not configured");
1898
2043
  setAuthState({
1899
2044
  step: "error",
1900
2045
  previousStep: "creating_session",
@@ -1906,38 +2051,109 @@ async function smartWalletFlow(deps, mode) {
1906
2051
  const clientSessionId = await createAuthSession(deps);
1907
2052
  if (!clientSessionId) return;
1908
2053
  try {
1909
- const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
1910
- body: { clientSessionId },
2054
+ const challengeBody = { clientSessionId };
2055
+ const { data: challengeData, error: challengeError } = await api.POST("/auth/passkey/challenge", {
2056
+ body: challengeBody,
1911
2057
  signal
1912
2058
  });
1913
2059
  const challenge = challengeData?.content?.challenge;
1914
2060
  if (!challengeData?.success || !challenge) {
1915
- return failPasskey(setAuthState, "Failed to start passkey");
2061
+ if (!challengeError) logApiError(logger, "POST /auth/passkey/challenge", { body: challengeBody, data: challengeData });
2062
+ return failPasskey(setAuthState, extractErrorCode(challengeError, challengeData), "Failed to start passkey");
1916
2063
  }
1917
2064
  setAuthState({ step: "creating_passkey" });
1918
2065
  const ceremony = await passkey({ challenge, mode });
1919
2066
  const response = ceremony.response;
1920
2067
  if (ceremony.kind === "register") {
1921
2068
  setAuthState({ step: "deploying_smart_account" });
1922
- const { data } = await api.POST("/auth/passkey/register", {
1923
- body: { clientSessionId, response },
1924
- signal
1925
- });
1926
- if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
2069
+ const body = { clientSessionId, response };
2070
+ const { data, error } = await api.POST("/auth/passkey/register", { body, signal });
2071
+ if (!data?.success) {
2072
+ if (!error) logApiError(logger, "POST /auth/passkey/register", { body, data });
2073
+ return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey registration failed");
2074
+ }
1927
2075
  } else {
1928
- const { data } = await api.POST("/auth/passkey/login", {
1929
- body: { clientSessionId, response },
1930
- signal
1931
- });
1932
- if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
2076
+ const body = { clientSessionId, response };
2077
+ const { data, error } = await api.POST("/auth/passkey/login", { body, signal });
2078
+ if (!data?.success) {
2079
+ if (!error) logApiError(logger, "POST /auth/passkey/login", { body, data });
2080
+ return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey authentication failed");
2081
+ }
1933
2082
  }
1934
- } catch {
1935
- return failPasskey(setAuthState, "Passkey login failed");
2083
+ } catch (err) {
2084
+ logApiError(logger, "passkey ceremony", { error: err });
2085
+ return failPasskey(setAuthState, void 0, "Passkey login failed");
1936
2086
  }
1937
2087
  await authenticate(clientSessionId, deps);
1938
2088
  }
1939
- function failPasskey(setAuthState, message) {
1940
- setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
2089
+ function failPasskey(setAuthState, code, fallbackMessage) {
2090
+ const { message, errorCode } = resolveAuthError(code, fallbackMessage);
2091
+ setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
2092
+ }
2093
+
2094
+ // src/client/auth/providers.ts
2095
+ function oauthProvider(provider) {
2096
+ return {
2097
+ id: provider,
2098
+ login: (ctx) => ctx.startHostedOAuth(provider)
2099
+ };
2100
+ }
2101
+ function emailProvider() {
2102
+ return {
2103
+ id: "email",
2104
+ login: async (ctx, options) => {
2105
+ const email = options.email ?? "";
2106
+ const clientSessionId = await initEmailSession(ctx);
2107
+ if (clientSessionId) await sendEmailCode(email, clientSessionId, ctx);
2108
+ },
2109
+ actions: {
2110
+ begin: async (ctx) => {
2111
+ await initEmailSession(ctx);
2112
+ },
2113
+ sendCode: (ctx, payload) => {
2114
+ const { email, clientSessionId } = payload ?? {};
2115
+ return sendEmailCode(email, clientSessionId, ctx);
2116
+ },
2117
+ verifyCode: (ctx, payload) => {
2118
+ const { code, clientSessionId, email } = payload ?? {};
2119
+ return verifyAndAuthenticate(code, clientSessionId, email, ctx);
2120
+ }
2121
+ }
2122
+ };
2123
+ }
2124
+
2125
+ // src/client/auth/sep10-challenge.ts
2126
+ var ENVELOPE_TYPE_TX_V0 = 0;
2127
+ var ENVELOPE_TYPE_TX = 2;
2128
+ var KEY_TYPE_ED25519 = 0;
2129
+ var SEQ_OFFSET_V1 = 44;
2130
+ var SEQ_OFFSET_V0 = 40;
2131
+ function base64ToBytes(b64) {
2132
+ return base64urlDecode(b64.replace(/\+/g, "-").replace(/\//g, "_"));
2133
+ }
2134
+ function isI64Zero(view, offset) {
2135
+ return view.getUint32(offset, false) === 0 && view.getUint32(offset + 4, false) === 0;
2136
+ }
2137
+ function isValidSep10Challenge(challengeXdr) {
2138
+ try {
2139
+ const bytes = base64ToBytes(challengeXdr.trim());
2140
+ if (bytes.length < 8) return false;
2141
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2142
+ const envelopeType = view.getUint32(0, false);
2143
+ let seqOffset;
2144
+ if (envelopeType === ENVELOPE_TYPE_TX) {
2145
+ if (view.getUint32(4, false) !== KEY_TYPE_ED25519) return false;
2146
+ seqOffset = SEQ_OFFSET_V1;
2147
+ } else if (envelopeType === ENVELOPE_TYPE_TX_V0) {
2148
+ seqOffset = SEQ_OFFSET_V0;
2149
+ } else {
2150
+ return false;
2151
+ }
2152
+ if (bytes.length < seqOffset + 8) return false;
2153
+ return isI64Zero(view, seqOffset);
2154
+ } catch {
2155
+ return false;
2156
+ }
1941
2157
  }
1942
2158
 
1943
2159
  // src/client/auth/walletFlow.ts
@@ -1953,8 +2169,18 @@ function withSignal(promise, signal) {
1953
2169
  })
1954
2170
  ]);
1955
2171
  }
2172
+ async function requestWalletChallenge(clientSessionId, walletAddress, deps) {
2173
+ const { api, logger, signal } = deps;
2174
+ const body = { clientSessionId, walletAddress };
2175
+ const { data, error } = await api.POST("/auth/wallet/challenge", { body, signal });
2176
+ if (error || !data?.success) {
2177
+ if (!error) logApiError(logger, "POST /auth/wallet/challenge", { body, data });
2178
+ return null;
2179
+ }
2180
+ return data.content.challengeXdr;
2181
+ }
1956
2182
  async function loginWallet(type, deps) {
1957
- const { api, signal, setAuthState } = deps;
2183
+ const { api, logger, signal, setAuthState } = deps;
1958
2184
  const clientSessionId = await createAuthSession(deps);
1959
2185
  if (!clientSessionId) return;
1960
2186
  let connectedWallet;
@@ -1969,12 +2195,36 @@ async function loginWallet(type, deps) {
1969
2195
  const { address } = await withSignal(adapter.connect(), signal);
1970
2196
  connectedWallet = address;
1971
2197
  deps.storeWalletAdapter(adapter, type);
1972
- setAuthState({ step: "authenticating_wallet" });
1973
- const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
1974
- body: { clientSessionId, walletAddress: address },
2198
+ setAuthState({ step: "signing_wallet_challenge", walletType: type });
2199
+ const challengeXdr = await requestWalletChallenge(clientSessionId, address, deps);
2200
+ if (!challengeXdr) {
2201
+ setAuthState({
2202
+ step: "error",
2203
+ previousStep: "signing_wallet_challenge",
2204
+ message: "Failed to obtain wallet challenge",
2205
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2206
+ });
2207
+ return;
2208
+ }
2209
+ if (!isValidSep10Challenge(challengeXdr)) {
2210
+ logApiError(logger, "SEP-10 challenge validation", { error: "unexpected challenge structure (sequence != 0?)" });
2211
+ setAuthState({
2212
+ step: "error",
2213
+ previousStep: "signing_wallet_challenge",
2214
+ message: "Invalid wallet challenge",
2215
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2216
+ });
2217
+ return;
2218
+ }
2219
+ const { signedTxXdr } = await withSignal(
2220
+ adapter.signTransaction(challengeXdr, { networkPassphrase: deps.networkPassphrase }),
1975
2221
  signal
1976
- });
2222
+ );
2223
+ setAuthState({ step: "authenticating_wallet" });
2224
+ const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
2225
+ const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
1977
2226
  if (walletError || !walletData?.success) {
2227
+ if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
1978
2228
  setAuthState({
1979
2229
  step: "error",
1980
2230
  previousStep: "authenticating_wallet",
@@ -1983,7 +2233,8 @@ async function loginWallet(type, deps) {
1983
2233
  });
1984
2234
  return;
1985
2235
  }
1986
- } catch {
2236
+ } catch (err) {
2237
+ logApiError(logger, "wallet connect", { error: err });
1987
2238
  setAuthState({
1988
2239
  step: "error",
1989
2240
  previousStep: "connecting_wallet",
@@ -2059,6 +2310,13 @@ var PollarClient = class {
2059
2310
  this._loginController = null;
2060
2311
  /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2061
2312
  this._resumeController = null;
2313
+ /**
2314
+ * Registry of pluggable login strategies, keyed by provider id. Seeded with
2315
+ * the built-ins (`google`, `github`, `email`) and then any `config.providers`
2316
+ * (which can override a built-in by reusing its id). `wallet` is deliberately
2317
+ * absent — it keeps its own dedicated flow. See {@link PollarAuthProvider}.
2318
+ */
2319
+ this._providers = /* @__PURE__ */ new Map();
2062
2320
  this.apiKey = config.apiKey;
2063
2321
  this.id = randomUUID();
2064
2322
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2080,6 +2338,12 @@ var PollarClient = class {
2080
2338
  this._maxIdleMs = config.maxIdleMs;
2081
2339
  this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2082
2340
  this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location?.origin ?? "" : "");
2341
+ for (const provider of [oauthProvider("google"), oauthProvider("github"), emailProvider()]) {
2342
+ this._providers.set(provider.id, provider);
2343
+ }
2344
+ for (const provider of config.providers ?? []) {
2345
+ this._providers.set(provider.id, provider);
2346
+ }
2083
2347
  this._api = createApiClient(this.basePath);
2084
2348
  this._wireMiddlewares();
2085
2349
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -2189,28 +2453,77 @@ var PollarClient = class {
2189
2453
  onResponse: async ({ request, response }) => {
2190
2454
  const newNonce = response.headers.get("DPoP-Nonce");
2191
2455
  if (newNonce) self._dpopNonce = newNonce;
2192
- if (response.status !== 401) return response;
2456
+ if (response.status !== 401) return self._logHttp(request, response);
2193
2457
  const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
2194
2458
  const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
2195
2459
  if (request.url.includes("/auth/refresh")) {
2196
- if (isNonceChallenge) return self._retryRequest(request);
2197
- return response;
2460
+ if (isNonceChallenge) return self._logHttp(request, await self._retryRequest(request));
2461
+ return self._logHttp(request, response);
2198
2462
  }
2199
2463
  if (!isNonceChallenge) {
2200
2464
  try {
2201
2465
  await self.refresh();
2202
2466
  } catch {
2203
- return response;
2467
+ return self._logHttp(request, response);
2204
2468
  }
2205
2469
  const method = request.method.toUpperCase();
2206
2470
  if (method !== "GET" && method !== "HEAD") {
2207
- return response;
2471
+ return self._logHttp(request, response);
2208
2472
  }
2209
2473
  }
2210
- return self._retryRequest(request);
2474
+ return self._logHttp(request, await self._retryRequest(request));
2211
2475
  }
2212
2476
  });
2213
2477
  }
2478
+ /**
2479
+ * Logs the final outcome of an SDK API call exactly once: successes (`2xx`) at
2480
+ * `debug` (method + path + status, no body), failures (`4xx`/`5xx`) at `error`
2481
+ * with the redacted request body and the response error body. Returns the
2482
+ * response so it can be chained at the middleware's return points. The error
2483
+ * body is read off a synchronous `clone()` so it never disturbs the body the
2484
+ * caller consumes.
2485
+ */
2486
+ _logHttp(request, response) {
2487
+ const path = this._httpPath(request.url);
2488
+ const label = `[PollarClient:http] ${request.method.toUpperCase()} ${path} ${response.status}`;
2489
+ if (response.ok) {
2490
+ this._log.debug(label);
2491
+ } else {
2492
+ void this._logHttpError(label, request, response.clone());
2493
+ }
2494
+ return response;
2495
+ }
2496
+ /** Reads the redacted request body + JSON response body and logs at `error`. */
2497
+ async _logHttpError(label, request, response) {
2498
+ let requestBody;
2499
+ const cached = this._requestBodyCache.get(request);
2500
+ if (cached) {
2501
+ try {
2502
+ requestBody = redactBody(JSON.parse(new TextDecoder().decode(cached)));
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ let responseBody;
2507
+ if ((response.headers.get("content-type") ?? "").includes("application/json")) {
2508
+ try {
2509
+ responseBody = await response.json();
2510
+ } catch {
2511
+ }
2512
+ }
2513
+ this._log.error(label, {
2514
+ ...requestBody !== void 0 ? { requestBody } : {},
2515
+ ...responseBody !== void 0 ? { responseBody } : {}
2516
+ });
2517
+ }
2518
+ /** Strips origin + `/v1` version prefix from a request URL for compact logs. */
2519
+ _httpPath(url) {
2520
+ try {
2521
+ const { pathname } = new URL(url);
2522
+ return pathname.startsWith("/v1/") ? pathname.slice(3) : pathname;
2523
+ } catch {
2524
+ return url;
2525
+ }
2526
+ }
2214
2527
  async _buildProofForRequest(request, accessToken) {
2215
2528
  try {
2216
2529
  const htu = request.url.split("?")[0].split("#")[0];
@@ -2437,28 +2750,42 @@ var PollarClient = class {
2437
2750
  warnServerSide("login");
2438
2751
  return;
2439
2752
  }
2440
- if (options.provider === "google" || options.provider === "github" || options.provider === "email") {
2441
- const controller = this._newController();
2442
- const deps = this._flowDeps(controller.signal);
2443
- if (options.provider === "google" || options.provider === "github") {
2444
- loginOAuth(options.provider, {
2445
- ...deps,
2446
- basePath: this.basePath,
2447
- apiKey: this.apiKey,
2448
- openAuthUrl: this._openAuthUrl,
2449
- redirectUri: this._oauthRedirectUri
2450
- }).catch((err) => this._handleFlowError(err));
2451
- } else if (options.provider === "email") {
2452
- const { email } = options;
2453
- initEmailSession(deps).then(() => {
2454
- if (this._authState.step === "entering_email") {
2455
- return sendEmailCode(email, this._authState.clientSessionId, deps);
2456
- }
2457
- }).catch((err) => this._handleFlowError(err));
2458
- }
2459
- } else if (options.provider === "wallet") {
2753
+ if (options.provider === "wallet") {
2460
2754
  this.loginWallet(options.type);
2755
+ return;
2756
+ }
2757
+ const provider = this._providers.get(options.provider);
2758
+ if (!provider?.login) {
2759
+ this._setAuthState({
2760
+ step: "error",
2761
+ previousStep: this._authState.step,
2762
+ message: `No auth provider registered for '${options.provider}'`,
2763
+ errorCode: AUTH_ERROR_CODES.AUTH_FAILED
2764
+ });
2765
+ return;
2766
+ }
2767
+ const controller = this._newController();
2768
+ provider.login(this._providerContext(controller.signal), options).catch((err) => this._handleFlowError(err));
2769
+ }
2770
+ /**
2771
+ * Invoke a named secondary step on a registered provider (e.g. email's
2772
+ * `sendCode` / `verifyCode`, or a custom provider's multi-step continuation).
2773
+ * Reuses the in-flight login `AbortController` when one exists so the step
2774
+ * stays cancellable via `cancelLogin()`; otherwise starts a fresh one. The
2775
+ * built-in email steps also have dedicated typed methods
2776
+ * ({@link sendEmailCode} / {@link verifyEmailCode}) — prefer those for email.
2777
+ */
2778
+ providerAction(provider, action, payload) {
2779
+ if (!isClientRuntime) {
2780
+ warnServerSide("providerAction");
2781
+ return;
2461
2782
  }
2783
+ const fn = this._providers.get(provider)?.actions?.[action];
2784
+ if (!fn) {
2785
+ throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
2786
+ }
2787
+ const signal = this._loginController?.signal ?? this._newController().signal;
2788
+ fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
2462
2789
  }
2463
2790
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2464
2791
  beginEmailLogin() {
@@ -2467,7 +2794,7 @@ var PollarClient = class {
2467
2794
  return;
2468
2795
  }
2469
2796
  const controller = this._newController();
2470
- initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2797
+ initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
2471
2798
  }
2472
2799
  sendEmailCode(email) {
2473
2800
  if (!isClientRuntime) {
@@ -2479,7 +2806,7 @@ var PollarClient = class {
2479
2806
  }
2480
2807
  const { clientSessionId } = this._authState;
2481
2808
  const signal = this._loginController.signal;
2482
- sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2809
+ sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
2483
2810
  }
2484
2811
  verifyEmailCode(code) {
2485
2812
  if (!isClientRuntime) {
@@ -2494,7 +2821,7 @@ var PollarClient = class {
2494
2821
  const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
2495
2822
  const email = state.step === "entering_code" ? state.email : state.email ?? "";
2496
2823
  const controller = this._newController();
2497
- verifyAndAuthenticate(code, clientSessionId, email, this._flowDeps(controller.signal)).catch(
2824
+ verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
2498
2825
  (err) => this._handleFlowError(err)
2499
2826
  );
2500
2827
  }
@@ -2859,6 +3186,29 @@ var PollarClient = class {
2859
3186
  getWalletType() {
2860
3187
  return this._walletAdapter?.type ?? null;
2861
3188
  }
3189
+ /**
3190
+ * The authenticated user's wallet as a {@link WalletInfo} discriminated union,
3191
+ * or `null` when there's no session (or the session carries no address yet).
3192
+ *
3193
+ * `custody` strictly determines `provider` (the mapping is 1:1 and fixed at
3194
+ * account creation server-side): `external` reports the connected adapter id
3195
+ * (`getWalletType()`), `smart` is always `'passkey'`, and `internal` reports
3196
+ * the login method the backend recorded (`null` for pre-provider sessions).
3197
+ */
3198
+ getWallet() {
3199
+ const w = this._session?.wallet;
3200
+ if (!w || !w.address) return null;
3201
+ switch (w.type) {
3202
+ case "external":
3203
+ return { custody: "external", address: w.address, provider: this._walletAdapter?.type ?? null };
3204
+ case "smart":
3205
+ return { custody: "smart", address: w.address, provider: "passkey" };
3206
+ case "internal":
3207
+ return { custody: "internal", address: w.address, provider: w.provider ?? null };
3208
+ default:
3209
+ return null;
3210
+ }
3211
+ }
2862
3212
  /**
2863
3213
  * Signs the given unsigned XDR and returns the signed XDR.
2864
3214
  *
@@ -2912,14 +3262,16 @@ var PollarClient = class {
2912
3262
  });
2913
3263
  return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2914
3264
  }
2915
- const details = error?.details;
3265
+ const { details, code, message } = this._resolveTxApiError(error);
2916
3266
  this._setTransactionState({
2917
3267
  step: "error",
2918
3268
  phase: "signing",
2919
3269
  ...buildData && { buildData },
2920
- ...details && { details }
3270
+ ...details && { details },
3271
+ ...code && { code },
3272
+ ...message && { message }
2921
3273
  });
2922
- return { status: "error", ...details && { details } };
3274
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
2923
3275
  } catch (err) {
2924
3276
  const details = err instanceof Error ? err.message : void 0;
2925
3277
  this._setTransactionState({
@@ -2931,6 +3283,54 @@ var PollarClient = class {
2931
3283
  return { status: "error", ...details && { details } };
2932
3284
  }
2933
3285
  }
3286
+ /**
3287
+ * Sign a single Soroban authorization entry (`SorobanAuthorizationEntry`).
3288
+ *
3289
+ * Use this when a contract is the transaction source (e.g. it sponsors the
3290
+ * gas and swaps the fee out of the user's token) and only needs the user's
3291
+ * address-credentials authorization, not a full signed envelope. The signed
3292
+ * entry is returned as base64 XDR for the caller to compose into its tx.
3293
+ *
3294
+ * - External wallets (Freighter/Albedo) sign the entry via the provider.
3295
+ * - Custodial wallets are signed by the backend, which FIRST validates the
3296
+ * entry's invocation tree against the app's contract/function allowlist and
3297
+ * caps the validity window — entries touching a non-allowlisted contract or
3298
+ * function, or expiring too far ahead, are rejected.
3299
+ *
3300
+ * @param entryXdr base64 XDR of the unsigned `SorobanAuthorizationEntry`.
3301
+ * @param options.validUntilLedger absolute ledger the signature expires at
3302
+ * (computed from the network's latest ledger). Ignored on the external-wallet
3303
+ * path, where the provider sets its own expiration.
3304
+ */
3305
+ async signAuthEntry(entryXdr, options) {
3306
+ if (this._walletAdapter) {
3307
+ const accountToSign = this._session?.wallet?.address;
3308
+ try {
3309
+ const { signedAuthEntry } = await this._walletAdapter.signAuthEntry(
3310
+ entryXdr,
3311
+ accountToSign ? { accountToSign } : void 0
3312
+ );
3313
+ return { status: "signed", signedAuthEntry };
3314
+ } catch (err) {
3315
+ const details = err instanceof Error ? err.message : void 0;
3316
+ return { status: "error", ...details && { details } };
3317
+ }
3318
+ }
3319
+ const address = this._session?.wallet?.address ?? "";
3320
+ try {
3321
+ const { data, error } = await this._api.POST("/tx/sign-auth-entry", {
3322
+ body: { network: this.getNetwork(), address, entryXdr, validUntilLedger: options.validUntilLedger }
3323
+ });
3324
+ if (!error && data?.success && data.content?.signedAuthEntry) {
3325
+ return { status: "signed", signedAuthEntry: data.content.signedAuthEntry };
3326
+ }
3327
+ const details = error?.details;
3328
+ return { status: "error", ...details && { details } };
3329
+ } catch (err) {
3330
+ const details = err instanceof Error ? err.message : void 0;
3331
+ return { status: "error", ...details && { details } };
3332
+ }
3333
+ }
2934
3334
  /**
2935
3335
  * Submits a signed XDR via `/tx/submit` regardless of wallet type
2936
3336
  * (custodial or external). Routing through sdk-api gives us:
@@ -2949,6 +3349,21 @@ var PollarClient = class {
2949
3349
  * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
2950
3350
  * or `error[phase: 'submitting']` on failure.
2951
3351
  */
3352
+ /**
3353
+ * Normalize a backend API error into { details, code, message }. `code` is the
3354
+ * precise backend ErrorCode (e.g. `TX_FEE_LIMIT_EXCEEDED`) for programmatic
3355
+ * handling; `message` is a friendly string from the error catalog; `details`
3356
+ * is the raw diagnostic. Lets tx flows surface a typed reason instead of an
3357
+ * opaque details string.
3358
+ */
3359
+ _resolveTxApiError(error) {
3360
+ const e = error;
3361
+ const details = e?.details ?? e?.message;
3362
+ const code = e?.code;
3363
+ if (!code) return details ? { details } : {};
3364
+ const { message } = resolveAuthError(code, details ?? code);
3365
+ return { code, message, ...details && { details } };
3366
+ }
2952
3367
  async submitTx(signedXdr, opts) {
2953
3368
  const buildData = this._currentBuildData();
2954
3369
  const outcomeExtra = buildData ? { buildData } : {};
@@ -2986,14 +3401,22 @@ var PollarClient = class {
2986
3401
  ...resultCode && { details: resultCode, resultCode }
2987
3402
  };
2988
3403
  }
2989
- const details = error?.details;
3404
+ const { details, code, message } = this._resolveTxApiError(error);
2990
3405
  this._setTransactionState({
2991
3406
  step: "error",
2992
3407
  phase: "submitting",
2993
3408
  ...buildData && { buildData },
2994
- ...details && { details }
3409
+ ...details && { details },
3410
+ ...code && { code },
3411
+ ...message && { message }
2995
3412
  });
2996
- return { status: "error", ...outcomeExtra, ...details && { details } };
3413
+ return {
3414
+ status: "error",
3415
+ ...outcomeExtra,
3416
+ ...details && { details },
3417
+ ...code && { code },
3418
+ ...message && { message }
3419
+ };
2997
3420
  } catch (err) {
2998
3421
  const details = err instanceof Error ? err.message : void 0;
2999
3422
  this._setTransactionState({
@@ -3080,14 +3503,22 @@ var PollarClient = class {
3080
3503
  ...resultCode && { details: resultCode, resultCode }
3081
3504
  };
3082
3505
  }
3083
- const details = error?.details;
3506
+ const { details, code, message } = this._resolveTxApiError(error);
3084
3507
  this._setTransactionState({
3085
3508
  step: "error",
3086
3509
  phase: "signing-submitting",
3087
3510
  ...buildData && { buildData },
3088
- ...details && { details }
3511
+ ...details && { details },
3512
+ ...code && { code },
3513
+ ...message && { message }
3089
3514
  });
3090
- return { status: "error", ...outcomeExtra, ...details && { details } };
3515
+ return {
3516
+ status: "error",
3517
+ ...outcomeExtra,
3518
+ ...details && { details },
3519
+ ...code && { code },
3520
+ ...message && { message }
3521
+ };
3091
3522
  } catch (err) {
3092
3523
  const details = err instanceof Error ? err.message : void 0;
3093
3524
  this._setTransactionState({
@@ -3162,13 +3593,15 @@ var PollarClient = class {
3162
3593
  });
3163
3594
  return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
3164
3595
  }
3165
- const details = error?.details;
3596
+ const { details, code, message } = this._resolveTxApiError(error);
3166
3597
  this._setTransactionState({
3167
3598
  step: "error",
3168
3599
  phase: "building-signing-submitting",
3169
- ...details && { details }
3600
+ ...details && { details },
3601
+ ...code && { code },
3602
+ ...message && { message }
3170
3603
  });
3171
- return { status: "error", ...details && { details } };
3604
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3172
3605
  } catch (err) {
3173
3606
  const details = err instanceof Error ? err.message : void 0;
3174
3607
  this._setTransactionState({
@@ -3281,9 +3714,22 @@ var PollarClient = class {
3281
3714
  });
3282
3715
  return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3283
3716
  }
3284
- const details = error?.details;
3285
- this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3286
- return { status: "error", ...outcomeExtra, ...details && { details } };
3717
+ const { details, code, message } = this._resolveTxApiError(error);
3718
+ this._setTransactionState({
3719
+ step: "error",
3720
+ phase: "submitting",
3721
+ buildData,
3722
+ ...details && { details },
3723
+ ...code && { code },
3724
+ ...message && { message }
3725
+ });
3726
+ return {
3727
+ status: "error",
3728
+ ...outcomeExtra,
3729
+ ...details && { details },
3730
+ ...code && { code },
3731
+ ...message && { message }
3732
+ };
3287
3733
  } catch (err) {
3288
3734
  const details = err instanceof Error ? err.message : void 0;
3289
3735
  this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
@@ -3361,11 +3807,67 @@ var PollarClient = class {
3361
3807
  this._loginController = new AbortController();
3362
3808
  return this._loginController;
3363
3809
  }
3810
+ /**
3811
+ * Build the {@link AuthProviderContext} facade for one login attempt. Wraps
3812
+ * the internal `FlowDeps` so providers get only the curated primitives —
3813
+ * `createSession`, `authenticate`, `exchangeExternalToken`, `startHostedOAuth`
3814
+ * — while storage / wallet-adapter / key-manager internals stay private. All
3815
+ * legs share the same `signal`, so `cancelLogin()` aborts the whole chain.
3816
+ */
3817
+ _providerContext(signal) {
3818
+ const deps = this._flowDeps(signal);
3819
+ return {
3820
+ signal,
3821
+ api: this._api,
3822
+ basePath: this.basePath,
3823
+ apiKey: this.apiKey,
3824
+ logger: this._log,
3825
+ setAuthState: this._setAuthState.bind(this),
3826
+ createSession: () => createAuthSession(deps),
3827
+ authenticate: (clientSessionId) => authenticate(clientSessionId, deps),
3828
+ requestChallenge: (clientSessionId, walletAddress) => requestWalletChallenge(clientSessionId, walletAddress, deps),
3829
+ exchangeExternalToken: (clientSessionId, body) => this._exchangeExternalToken(clientSessionId, body, signal),
3830
+ startHostedOAuth: (provider) => loginOAuth(provider, {
3831
+ ...deps,
3832
+ basePath: this.basePath,
3833
+ apiKey: this.apiKey,
3834
+ openAuthUrl: this._openAuthUrl,
3835
+ redirectUri: this._oauthRedirectUri
3836
+ })
3837
+ };
3838
+ }
3839
+ /**
3840
+ * Generic external-provider exchange leg (`POST /auth/external`). Custom
3841
+ * providers call this (via the context) after their own SDK has authenticated
3842
+ * the user and the wallet has counter-signed the SEP-10 challenge
3843
+ * (`{ provider, walletAddress, signedChallengeXdr }`). On success the session
3844
+ * is marked READY server-side and the provider should then call
3845
+ * `ctx.authenticate(clientSessionId)`. Returns `false` (and sets an error
3846
+ * state) on failure.
3847
+ */
3848
+ async _exchangeExternalToken(clientSessionId, body, signal) {
3849
+ const { data, error } = await this._api.POST("/auth/external", {
3850
+ body: { clientSessionId, ...body },
3851
+ signal
3852
+ });
3853
+ if (error || !data?.success) {
3854
+ this._log.error("[PollarClient] External provider authentication failed", { error });
3855
+ this._setAuthState({
3856
+ step: "error",
3857
+ previousStep: this._authState.step,
3858
+ message: "External provider authentication failed",
3859
+ errorCode: AUTH_ERROR_CODES.EXTERNAL_AUTH_FAILED
3860
+ });
3861
+ return false;
3862
+ }
3863
+ return true;
3864
+ }
3364
3865
  _flowDeps(signal) {
3365
3866
  return {
3366
3867
  api: this._api,
3367
3868
  logger: this._log,
3368
3869
  basePath: this.basePath,
3870
+ networkPassphrase: this._networkPassphrase(),
3369
3871
  // SSE status streaming works on web; React Native's `fetch` has no
3370
3872
  // readable `response.body`, so those clients poll the non-streaming
3371
3873
  // status endpoint instead. `isBrowser` is false in RN and SSR alike.
@@ -3497,6 +3999,7 @@ var PollarClient = class {
3497
3999
  async _storeSession(session) {
3498
4000
  this._log.info("[PollarClient] Session stored");
3499
4001
  const w = session.wallet;
4002
+ const wireProvider = w.provider;
3500
4003
  const persisted = {
3501
4004
  clientSessionId: session.clientSessionId,
3502
4005
  userId: session.userId ?? null,
@@ -3510,6 +4013,7 @@ var PollarClient = class {
3510
4013
  // persisted session speak one vocabulary while the wire stays compatible.
3511
4014
  wallet: {
3512
4015
  type: w.type === "custodial" ? "internal" : w.type,
4016
+ ...wireProvider ? { provider: wireProvider } : {},
3513
4017
  address: w.address ?? w.publicKey ?? null,
3514
4018
  ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3515
4019
  ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},