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