@pollar/core 0.9.1-rc.0 → 0.10.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.rn.js CHANGED
@@ -1022,7 +1022,19 @@ function createLogger(level = "info", sink = console) {
1022
1022
  }
1023
1023
 
1024
1024
  // src/lib/logging.ts
1025
- var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set(["email", "code", "walletAddress", "dpopJwk", "response", "refreshToken"]);
1025
+ var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
1026
+ "email",
1027
+ "code",
1028
+ "walletAddress",
1029
+ "dpopJwk",
1030
+ "response",
1031
+ "refreshToken",
1032
+ // SEP-10 challenge envelopes: a counter-signed challenge is a live, replayable
1033
+ // auth credential — never log it in the clear.
1034
+ "signedChallengeXdr",
1035
+ "challengeXdr",
1036
+ "signedTxXdr"
1037
+ ]);
1026
1038
  function redactBody(body) {
1027
1039
  if (!body || typeof body !== "object") return body;
1028
1040
  const out = {};
@@ -1119,8 +1131,36 @@ function defaultStorage(options = {}) {
1119
1131
  return createLocalStorageAdapter(options);
1120
1132
  }
1121
1133
 
1134
+ // src/types.ts
1135
+ var AUTH_ERROR_CODES = {
1136
+ SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1137
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1138
+ SESSION_INVALID: "SESSION_INVALID",
1139
+ EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1140
+ EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1141
+ EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1142
+ EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1143
+ AUTH_FAILED: "AUTH_FAILED",
1144
+ WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1145
+ WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1146
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1147
+ EXTERNAL_AUTH_FAILED: "EXTERNAL_AUTH_FAILED",
1148
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1149
+ // Generic bucket for on-chain transaction failures; the precise reason is the
1150
+ // backend `code` (e.g. TX_FEE_LIMIT_EXCEEDED) carried alongside on the outcome.
1151
+ TX_FAILED: "TX_FAILED",
1152
+ UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1153
+ };
1154
+ var PollarFlowError = class extends Error {
1155
+ constructor(message) {
1156
+ super(message);
1157
+ this.code = "INVALID_FLOW";
1158
+ this.name = "PollarFlowError";
1159
+ }
1160
+ };
1161
+
1122
1162
  // src/version.ts
1123
- var POLLAR_CORE_VERSION = "0.9.1-rc.0" ;
1163
+ var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
1124
1164
 
1125
1165
  // src/visibility/noop.ts
1126
1166
  function createNoopVisibilityProvider() {
@@ -1173,30 +1213,6 @@ function defaultVisibilityProvider() {
1173
1213
  return createNoopVisibilityProvider();
1174
1214
  }
1175
1215
 
1176
- // src/types.ts
1177
- var AUTH_ERROR_CODES = {
1178
- SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1179
- SESSION_EXPIRED: "SESSION_EXPIRED",
1180
- SESSION_INVALID: "SESSION_INVALID",
1181
- EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1182
- EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1183
- EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1184
- EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1185
- AUTH_FAILED: "AUTH_FAILED",
1186
- WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1187
- WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1188
- WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1189
- PASSKEY_FAILED: "PASSKEY_FAILED",
1190
- UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1191
- };
1192
- var PollarFlowError = class extends Error {
1193
- constructor(message) {
1194
- super(message);
1195
- this.code = "INVALID_FLOW";
1196
- this.name = "PollarFlowError";
1197
- }
1198
- };
1199
-
1200
1216
  // src/wallets/FreighterAdapter.ts
1201
1217
  var import_freighter_api = __toESM(require_index_min());
1202
1218
 
@@ -1452,6 +1468,10 @@ function isValidSession(value, logger = console) {
1452
1468
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
1453
1469
  return false;
1454
1470
  }
1471
+ if (w["provider"] !== void 0 && typeof w["provider"] !== "string") {
1472
+ logger.debug("[PollarClient:session] Invalid session \u2014 wallet.provider must be a string if present");
1473
+ return false;
1474
+ }
1455
1475
  if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1456
1476
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1457
1477
  return false;
@@ -1747,14 +1767,91 @@ async function createAuthSession(deps) {
1747
1767
  return data.content.clientSessionId;
1748
1768
  }
1749
1769
 
1770
+ // src/client/auth/errorMessages.ts
1771
+ var CATALOG = {
1772
+ // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1773
+ SPONSOR_NOT_FUNDED: {
1774
+ message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1775
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1776
+ },
1777
+ APP_WALLET_NOT_FOUND: {
1778
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1779
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1780
+ },
1781
+ WALLET_NOT_FOUND: {
1782
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1783
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1784
+ },
1785
+ PASSKEY_DEPLOY_FAILED: {
1786
+ message: "We couldn't finish creating your wallet. Please try again in a moment.",
1787
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1788
+ },
1789
+ // ── Passkey ceremony ────────────────────────────────────────────────────────
1790
+ PASSKEY_ALREADY_REGISTERED: {
1791
+ message: "A passkey is already registered for this account. Try signing in instead.",
1792
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1793
+ },
1794
+ PASSKEY_UNKNOWN_CREDENTIAL: {
1795
+ message: "We don't recognize this passkey. Try creating a new one.",
1796
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1797
+ },
1798
+ PASSKEY_VERIFICATION_FAILED: {
1799
+ message: "We couldn't verify your passkey. Please try again.",
1800
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1801
+ },
1802
+ PASSKEY_CHALLENGE_MISSING: {
1803
+ message: "Your passkey session expired. Please start again.",
1804
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1805
+ },
1806
+ // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1807
+ // These map to the TX_FAILED bucket (not PASSKEY_FAILED) — the precise reason
1808
+ // is the entry key itself, surfaced as the raw `code` on the tx outcome.
1809
+ TX_INSUFFICIENT_BALANCE: {
1810
+ message: "Insufficient balance to complete this transaction.",
1811
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1812
+ },
1813
+ TX_INSUFFICIENT_FEE: {
1814
+ message: "Not enough XLM to cover the network fee. Add more XLM to your wallet and try again.",
1815
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1816
+ },
1817
+ TX_FEE_LIMIT_EXCEEDED: {
1818
+ message: "The transaction fee is above the allowed limit. Please try again.",
1819
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1820
+ },
1821
+ TX_CONTRACT_FAILED: {
1822
+ message: "The contract rejected this operation. Check the operation is allowed right now and try again.",
1823
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1824
+ },
1825
+ TX_DESTINATION_NOT_FOUND: {
1826
+ message: "The destination account doesn't exist on the network yet.",
1827
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1828
+ },
1829
+ TX_NO_TRUSTLINE: {
1830
+ message: "The destination can't receive this asset yet (no trustline).",
1831
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1832
+ },
1833
+ TX_BAD_SEQUENCE: {
1834
+ message: "Something went out of sync. Please try again.",
1835
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1836
+ }
1837
+ };
1838
+ function resolveAuthError(code, fallbackMessage) {
1839
+ if (code && CATALOG[code]) return CATALOG[code];
1840
+ return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1841
+ }
1842
+ function extractErrorCode(error, data) {
1843
+ return error?.code ?? data?.code ?? void 0;
1844
+ }
1845
+
1750
1846
  // src/client/auth/emailFlow.ts
1751
- async function initEmailSession(deps) {
1752
- const clientSessionId = await createAuthSession(deps);
1753
- if (!clientSessionId) return;
1754
- deps.setAuthState({ step: "entering_email", clientSessionId });
1847
+ async function initEmailSession(ctx) {
1848
+ const clientSessionId = await ctx.createSession();
1849
+ if (!clientSessionId) return null;
1850
+ ctx.setAuthState({ step: "entering_email", clientSessionId });
1851
+ return clientSessionId;
1755
1852
  }
1756
- async function sendEmailCode(email, clientSessionId, deps) {
1757
- const { api, logger, signal, setAuthState } = deps;
1853
+ async function sendEmailCode(email, clientSessionId, ctx) {
1854
+ const { api, logger, signal, setAuthState } = ctx;
1758
1855
  setAuthState({ step: "sending_email", email });
1759
1856
  const body = { clientSessionId, email };
1760
1857
  const { data, error } = await api.POST("/auth/email", { body, signal });
@@ -1770,13 +1867,13 @@ async function sendEmailCode(email, clientSessionId, deps) {
1770
1867
  }
1771
1868
  setAuthState({ step: "entering_code", clientSessionId, email });
1772
1869
  }
1773
- async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1774
- const { api, logger, signal, setAuthState } = deps;
1870
+ async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
1871
+ const { api, logger, signal, setAuthState } = ctx;
1775
1872
  setAuthState({ step: "verifying_email_code", clientSessionId, email });
1776
1873
  const body = { clientSessionId, code };
1777
1874
  const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
1778
1875
  if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
1779
- await authenticate(clientSessionId, deps);
1876
+ await ctx.authenticate(clientSessionId);
1780
1877
  return;
1781
1878
  }
1782
1879
  const errCode = error?.error ?? data?.code;
@@ -1854,68 +1951,6 @@ async function loginOAuth(provider, deps) {
1854
1951
  await authenticate(clientSessionId, deps);
1855
1952
  }
1856
1953
 
1857
- // src/client/auth/errorMessages.ts
1858
- var CATALOG = {
1859
- // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1860
- SPONSOR_NOT_FUNDED: {
1861
- message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1862
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1863
- },
1864
- APP_WALLET_NOT_FOUND: {
1865
- message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1866
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1867
- },
1868
- WALLET_NOT_FOUND: {
1869
- message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1870
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1871
- },
1872
- PASSKEY_DEPLOY_FAILED: {
1873
- message: "We couldn't finish creating your wallet. Please try again in a moment.",
1874
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1875
- },
1876
- // ── Passkey ceremony ────────────────────────────────────────────────────────
1877
- PASSKEY_ALREADY_REGISTERED: {
1878
- message: "A passkey is already registered for this account. Try signing in instead.",
1879
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1880
- },
1881
- PASSKEY_UNKNOWN_CREDENTIAL: {
1882
- message: "We don't recognize this passkey. Try creating a new one.",
1883
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1884
- },
1885
- PASSKEY_VERIFICATION_FAILED: {
1886
- message: "We couldn't verify your passkey. Please try again.",
1887
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1888
- },
1889
- PASSKEY_CHALLENGE_MISSING: {
1890
- message: "Your passkey session expired. Please start again.",
1891
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1892
- },
1893
- // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1894
- TX_INSUFFICIENT_BALANCE: {
1895
- message: "Insufficient balance to complete this transaction.",
1896
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1897
- },
1898
- TX_DESTINATION_NOT_FOUND: {
1899
- message: "The destination account doesn't exist on the network yet.",
1900
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1901
- },
1902
- TX_NO_TRUSTLINE: {
1903
- message: "The destination can't receive this asset yet (no trustline).",
1904
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1905
- },
1906
- TX_BAD_SEQUENCE: {
1907
- message: "Something went out of sync. Please try again.",
1908
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1909
- }
1910
- };
1911
- function resolveAuthError(code, fallbackMessage) {
1912
- if (code && CATALOG[code]) return CATALOG[code];
1913
- return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1914
- }
1915
- function extractErrorCode(error, data) {
1916
- return error?.code ?? data?.code ?? void 0;
1917
- }
1918
-
1919
1954
  // src/client/auth/passkeyFlow.ts
1920
1955
  async function smartWalletFlow(deps, mode) {
1921
1956
  const { api, logger, signal, setAuthState, passkey } = deps;
@@ -1972,6 +2007,71 @@ function failPasskey(setAuthState, code, fallbackMessage) {
1972
2007
  setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
1973
2008
  }
1974
2009
 
2010
+ // src/client/auth/providers.ts
2011
+ function oauthProvider(provider) {
2012
+ return {
2013
+ id: provider,
2014
+ login: (ctx) => ctx.startHostedOAuth(provider)
2015
+ };
2016
+ }
2017
+ function emailProvider() {
2018
+ return {
2019
+ id: "email",
2020
+ login: async (ctx, options) => {
2021
+ const email = options.email ?? "";
2022
+ const clientSessionId = await initEmailSession(ctx);
2023
+ if (clientSessionId) await sendEmailCode(email, clientSessionId, ctx);
2024
+ },
2025
+ actions: {
2026
+ begin: async (ctx) => {
2027
+ await initEmailSession(ctx);
2028
+ },
2029
+ sendCode: (ctx, payload) => {
2030
+ const { email, clientSessionId } = payload ?? {};
2031
+ return sendEmailCode(email, clientSessionId, ctx);
2032
+ },
2033
+ verifyCode: (ctx, payload) => {
2034
+ const { code, clientSessionId, email } = payload ?? {};
2035
+ return verifyAndAuthenticate(code, clientSessionId, email, ctx);
2036
+ }
2037
+ }
2038
+ };
2039
+ }
2040
+
2041
+ // src/client/auth/sep10-challenge.ts
2042
+ var ENVELOPE_TYPE_TX_V0 = 0;
2043
+ var ENVELOPE_TYPE_TX = 2;
2044
+ var KEY_TYPE_ED25519 = 0;
2045
+ var SEQ_OFFSET_V1 = 44;
2046
+ var SEQ_OFFSET_V0 = 40;
2047
+ function base64ToBytes(b64) {
2048
+ return base64urlDecode(b64.replace(/\+/g, "-").replace(/\//g, "_"));
2049
+ }
2050
+ function isI64Zero(view, offset) {
2051
+ return view.getUint32(offset, false) === 0 && view.getUint32(offset + 4, false) === 0;
2052
+ }
2053
+ function isValidSep10Challenge(challengeXdr) {
2054
+ try {
2055
+ const bytes = base64ToBytes(challengeXdr.trim());
2056
+ if (bytes.length < 8) return false;
2057
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2058
+ const envelopeType = view.getUint32(0, false);
2059
+ let seqOffset;
2060
+ if (envelopeType === ENVELOPE_TYPE_TX) {
2061
+ if (view.getUint32(4, false) !== KEY_TYPE_ED25519) return false;
2062
+ seqOffset = SEQ_OFFSET_V1;
2063
+ } else if (envelopeType === ENVELOPE_TYPE_TX_V0) {
2064
+ seqOffset = SEQ_OFFSET_V0;
2065
+ } else {
2066
+ return false;
2067
+ }
2068
+ if (bytes.length < seqOffset + 8) return false;
2069
+ return isI64Zero(view, seqOffset);
2070
+ } catch {
2071
+ return false;
2072
+ }
2073
+ }
2074
+
1975
2075
  // src/client/auth/walletFlow.ts
1976
2076
  function withSignal(promise, signal) {
1977
2077
  return Promise.race([
@@ -1985,6 +2085,16 @@ function withSignal(promise, signal) {
1985
2085
  })
1986
2086
  ]);
1987
2087
  }
2088
+ async function requestWalletChallenge(clientSessionId, walletAddress, deps) {
2089
+ const { api, logger, signal } = deps;
2090
+ const body = { clientSessionId, walletAddress };
2091
+ const { data, error } = await api.POST("/auth/wallet/challenge", { body, signal });
2092
+ if (error || !data?.success) {
2093
+ if (!error) logApiError(logger, "POST /auth/wallet/challenge", { body, data });
2094
+ return null;
2095
+ }
2096
+ return data.content.challengeXdr;
2097
+ }
1988
2098
  async function loginWallet(type, deps) {
1989
2099
  const { api, logger, signal, setAuthState } = deps;
1990
2100
  const clientSessionId = await createAuthSession(deps);
@@ -2001,8 +2111,33 @@ async function loginWallet(type, deps) {
2001
2111
  const { address } = await withSignal(adapter.connect(), signal);
2002
2112
  connectedWallet = address;
2003
2113
  deps.storeWalletAdapter(adapter, type);
2114
+ setAuthState({ step: "signing_wallet_challenge", walletType: type });
2115
+ const challengeXdr = await requestWalletChallenge(clientSessionId, address, deps);
2116
+ if (!challengeXdr) {
2117
+ setAuthState({
2118
+ step: "error",
2119
+ previousStep: "signing_wallet_challenge",
2120
+ message: "Failed to obtain wallet challenge",
2121
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2122
+ });
2123
+ return;
2124
+ }
2125
+ if (!isValidSep10Challenge(challengeXdr)) {
2126
+ logApiError(logger, "SEP-10 challenge validation", { error: "unexpected challenge structure (sequence != 0?)" });
2127
+ setAuthState({
2128
+ step: "error",
2129
+ previousStep: "signing_wallet_challenge",
2130
+ message: "Invalid wallet challenge",
2131
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2132
+ });
2133
+ return;
2134
+ }
2135
+ const { signedTxXdr } = await withSignal(
2136
+ adapter.signTransaction(challengeXdr, { networkPassphrase: deps.networkPassphrase }),
2137
+ signal
2138
+ );
2004
2139
  setAuthState({ step: "authenticating_wallet" });
2005
- const body = { clientSessionId, walletAddress: address };
2140
+ const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
2006
2141
  const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
2007
2142
  if (walletError || !walletData?.success) {
2008
2143
  if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
@@ -2091,6 +2226,13 @@ var PollarClient = class {
2091
2226
  this._loginController = null;
2092
2227
  /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2093
2228
  this._resumeController = null;
2229
+ /**
2230
+ * Registry of pluggable login strategies, keyed by provider id. Seeded with
2231
+ * the built-ins (`google`, `github`, `email`) and then any `config.providers`
2232
+ * (which can override a built-in by reusing its id). `wallet` is deliberately
2233
+ * absent — it keeps its own dedicated flow. See {@link PollarAuthProvider}.
2234
+ */
2235
+ this._providers = /* @__PURE__ */ new Map();
2094
2236
  this.apiKey = config.apiKey;
2095
2237
  this.id = randomUUID();
2096
2238
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2112,6 +2254,12 @@ var PollarClient = class {
2112
2254
  this._maxIdleMs = config.maxIdleMs;
2113
2255
  this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2114
2256
  this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location?.origin ?? "" : "");
2257
+ for (const provider of [oauthProvider("google"), oauthProvider("github"), emailProvider()]) {
2258
+ this._providers.set(provider.id, provider);
2259
+ }
2260
+ for (const provider of config.providers ?? []) {
2261
+ this._providers.set(provider.id, provider);
2262
+ }
2115
2263
  this._api = createApiClient(this.basePath);
2116
2264
  this._wireMiddlewares();
2117
2265
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -2518,28 +2666,42 @@ var PollarClient = class {
2518
2666
  warnServerSide("login");
2519
2667
  return;
2520
2668
  }
2521
- if (options.provider === "google" || options.provider === "github" || options.provider === "email") {
2522
- const controller = this._newController();
2523
- const deps = this._flowDeps(controller.signal);
2524
- if (options.provider === "google" || options.provider === "github") {
2525
- loginOAuth(options.provider, {
2526
- ...deps,
2527
- basePath: this.basePath,
2528
- apiKey: this.apiKey,
2529
- openAuthUrl: this._openAuthUrl,
2530
- redirectUri: this._oauthRedirectUri
2531
- }).catch((err) => this._handleFlowError(err));
2532
- } else if (options.provider === "email") {
2533
- const { email } = options;
2534
- initEmailSession(deps).then(() => {
2535
- if (this._authState.step === "entering_email") {
2536
- return sendEmailCode(email, this._authState.clientSessionId, deps);
2537
- }
2538
- }).catch((err) => this._handleFlowError(err));
2539
- }
2540
- } else if (options.provider === "wallet") {
2669
+ if (options.provider === "wallet") {
2541
2670
  this.loginWallet(options.type);
2671
+ return;
2672
+ }
2673
+ const provider = this._providers.get(options.provider);
2674
+ if (!provider?.login) {
2675
+ this._setAuthState({
2676
+ step: "error",
2677
+ previousStep: this._authState.step,
2678
+ message: `No auth provider registered for '${options.provider}'`,
2679
+ errorCode: AUTH_ERROR_CODES.AUTH_FAILED
2680
+ });
2681
+ return;
2682
+ }
2683
+ const controller = this._newController();
2684
+ provider.login(this._providerContext(controller.signal), options).catch((err) => this._handleFlowError(err));
2685
+ }
2686
+ /**
2687
+ * Invoke a named secondary step on a registered provider (e.g. email's
2688
+ * `sendCode` / `verifyCode`, or a custom provider's multi-step continuation).
2689
+ * Reuses the in-flight login `AbortController` when one exists so the step
2690
+ * stays cancellable via `cancelLogin()`; otherwise starts a fresh one. The
2691
+ * built-in email steps also have dedicated typed methods
2692
+ * ({@link sendEmailCode} / {@link verifyEmailCode}) — prefer those for email.
2693
+ */
2694
+ providerAction(provider, action, payload) {
2695
+ if (!isClientRuntime) {
2696
+ warnServerSide("providerAction");
2697
+ return;
2542
2698
  }
2699
+ const fn = this._providers.get(provider)?.actions?.[action];
2700
+ if (!fn) {
2701
+ throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
2702
+ }
2703
+ const signal = this._loginController?.signal ?? this._newController().signal;
2704
+ fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
2543
2705
  }
2544
2706
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2545
2707
  beginEmailLogin() {
@@ -2548,7 +2710,7 @@ var PollarClient = class {
2548
2710
  return;
2549
2711
  }
2550
2712
  const controller = this._newController();
2551
- initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2713
+ initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
2552
2714
  }
2553
2715
  sendEmailCode(email) {
2554
2716
  if (!isClientRuntime) {
@@ -2560,7 +2722,7 @@ var PollarClient = class {
2560
2722
  }
2561
2723
  const { clientSessionId } = this._authState;
2562
2724
  const signal = this._loginController.signal;
2563
- sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2725
+ sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
2564
2726
  }
2565
2727
  verifyEmailCode(code) {
2566
2728
  if (!isClientRuntime) {
@@ -2575,7 +2737,7 @@ var PollarClient = class {
2575
2737
  const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
2576
2738
  const email = state.step === "entering_code" ? state.email : state.email ?? "";
2577
2739
  const controller = this._newController();
2578
- verifyAndAuthenticate(code, clientSessionId, email, this._flowDeps(controller.signal)).catch(
2740
+ verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
2579
2741
  (err) => this._handleFlowError(err)
2580
2742
  );
2581
2743
  }
@@ -2940,6 +3102,29 @@ var PollarClient = class {
2940
3102
  getWalletType() {
2941
3103
  return this._walletAdapter?.type ?? null;
2942
3104
  }
3105
+ /**
3106
+ * The authenticated user's wallet as a {@link WalletInfo} discriminated union,
3107
+ * or `null` when there's no session (or the session carries no address yet).
3108
+ *
3109
+ * `custody` strictly determines `provider` (the mapping is 1:1 and fixed at
3110
+ * account creation server-side): `external` reports the connected adapter id
3111
+ * (`getWalletType()`), `smart` is always `'passkey'`, and `internal` reports
3112
+ * the login method the backend recorded (`null` for pre-provider sessions).
3113
+ */
3114
+ getWallet() {
3115
+ const w = this._session?.wallet;
3116
+ if (!w || !w.address) return null;
3117
+ switch (w.type) {
3118
+ case "external":
3119
+ return { custody: "external", address: w.address, provider: this._walletAdapter?.type ?? null };
3120
+ case "smart":
3121
+ return { custody: "smart", address: w.address, provider: "passkey" };
3122
+ case "internal":
3123
+ return { custody: "internal", address: w.address, provider: w.provider ?? null };
3124
+ default:
3125
+ return null;
3126
+ }
3127
+ }
2943
3128
  /**
2944
3129
  * Signs the given unsigned XDR and returns the signed XDR.
2945
3130
  *
@@ -2993,14 +3178,16 @@ var PollarClient = class {
2993
3178
  });
2994
3179
  return { status: "signed", signedXdr, submissionToken: idempotencyKey };
2995
3180
  }
2996
- const details = error?.details;
3181
+ const { details, code, message } = this._resolveTxApiError(error);
2997
3182
  this._setTransactionState({
2998
3183
  step: "error",
2999
3184
  phase: "signing",
3000
3185
  ...buildData && { buildData },
3001
- ...details && { details }
3186
+ ...details && { details },
3187
+ ...code && { code },
3188
+ ...message && { message }
3002
3189
  });
3003
- return { status: "error", ...details && { details } };
3190
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3004
3191
  } catch (err) {
3005
3192
  const details = err instanceof Error ? err.message : void 0;
3006
3193
  this._setTransactionState({
@@ -3012,6 +3199,54 @@ var PollarClient = class {
3012
3199
  return { status: "error", ...details && { details } };
3013
3200
  }
3014
3201
  }
3202
+ /**
3203
+ * Sign a single Soroban authorization entry (`SorobanAuthorizationEntry`).
3204
+ *
3205
+ * Use this when a contract is the transaction source (e.g. it sponsors the
3206
+ * gas and swaps the fee out of the user's token) and only needs the user's
3207
+ * address-credentials authorization, not a full signed envelope. The signed
3208
+ * entry is returned as base64 XDR for the caller to compose into its tx.
3209
+ *
3210
+ * - External wallets (Freighter/Albedo) sign the entry via the provider.
3211
+ * - Custodial wallets are signed by the backend, which FIRST validates the
3212
+ * entry's invocation tree against the app's contract/function allowlist and
3213
+ * caps the validity window — entries touching a non-allowlisted contract or
3214
+ * function, or expiring too far ahead, are rejected.
3215
+ *
3216
+ * @param entryXdr base64 XDR of the unsigned `SorobanAuthorizationEntry`.
3217
+ * @param options.validUntilLedger absolute ledger the signature expires at
3218
+ * (computed from the network's latest ledger). Ignored on the external-wallet
3219
+ * path, where the provider sets its own expiration.
3220
+ */
3221
+ async signAuthEntry(entryXdr, options) {
3222
+ if (this._walletAdapter) {
3223
+ const accountToSign = this._session?.wallet?.address;
3224
+ try {
3225
+ const { signedAuthEntry } = await this._walletAdapter.signAuthEntry(
3226
+ entryXdr,
3227
+ accountToSign ? { accountToSign } : void 0
3228
+ );
3229
+ return { status: "signed", signedAuthEntry };
3230
+ } catch (err) {
3231
+ const details = err instanceof Error ? err.message : void 0;
3232
+ return { status: "error", ...details && { details } };
3233
+ }
3234
+ }
3235
+ const address = this._session?.wallet?.address ?? "";
3236
+ try {
3237
+ const { data, error } = await this._api.POST("/tx/sign-auth-entry", {
3238
+ body: { network: this.getNetwork(), address, entryXdr, validUntilLedger: options.validUntilLedger }
3239
+ });
3240
+ if (!error && data?.success && data.content?.signedAuthEntry) {
3241
+ return { status: "signed", signedAuthEntry: data.content.signedAuthEntry };
3242
+ }
3243
+ const details = error?.details;
3244
+ return { status: "error", ...details && { details } };
3245
+ } catch (err) {
3246
+ const details = err instanceof Error ? err.message : void 0;
3247
+ return { status: "error", ...details && { details } };
3248
+ }
3249
+ }
3015
3250
  /**
3016
3251
  * Submits a signed XDR via `/tx/submit` regardless of wallet type
3017
3252
  * (custodial or external). Routing through sdk-api gives us:
@@ -3030,6 +3265,21 @@ var PollarClient = class {
3030
3265
  * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
3031
3266
  * or `error[phase: 'submitting']` on failure.
3032
3267
  */
3268
+ /**
3269
+ * Normalize a backend API error into { details, code, message }. `code` is the
3270
+ * precise backend ErrorCode (e.g. `TX_FEE_LIMIT_EXCEEDED`) for programmatic
3271
+ * handling; `message` is a friendly string from the error catalog; `details`
3272
+ * is the raw diagnostic. Lets tx flows surface a typed reason instead of an
3273
+ * opaque details string.
3274
+ */
3275
+ _resolveTxApiError(error) {
3276
+ const e = error;
3277
+ const details = e?.details ?? e?.message;
3278
+ const code = e?.code;
3279
+ if (!code) return details ? { details } : {};
3280
+ const { message } = resolveAuthError(code, details ?? code);
3281
+ return { code, message, ...details && { details } };
3282
+ }
3033
3283
  async submitTx(signedXdr, opts) {
3034
3284
  const buildData = this._currentBuildData();
3035
3285
  const outcomeExtra = buildData ? { buildData } : {};
@@ -3067,14 +3317,22 @@ var PollarClient = class {
3067
3317
  ...resultCode && { details: resultCode, resultCode }
3068
3318
  };
3069
3319
  }
3070
- const details = error?.details;
3320
+ const { details, code, message } = this._resolveTxApiError(error);
3071
3321
  this._setTransactionState({
3072
3322
  step: "error",
3073
3323
  phase: "submitting",
3074
3324
  ...buildData && { buildData },
3075
- ...details && { details }
3325
+ ...details && { details },
3326
+ ...code && { code },
3327
+ ...message && { message }
3076
3328
  });
3077
- return { status: "error", ...outcomeExtra, ...details && { details } };
3329
+ return {
3330
+ status: "error",
3331
+ ...outcomeExtra,
3332
+ ...details && { details },
3333
+ ...code && { code },
3334
+ ...message && { message }
3335
+ };
3078
3336
  } catch (err) {
3079
3337
  const details = err instanceof Error ? err.message : void 0;
3080
3338
  this._setTransactionState({
@@ -3161,14 +3419,22 @@ var PollarClient = class {
3161
3419
  ...resultCode && { details: resultCode, resultCode }
3162
3420
  };
3163
3421
  }
3164
- const details = error?.details;
3422
+ const { details, code, message } = this._resolveTxApiError(error);
3165
3423
  this._setTransactionState({
3166
3424
  step: "error",
3167
3425
  phase: "signing-submitting",
3168
3426
  ...buildData && { buildData },
3169
- ...details && { details }
3427
+ ...details && { details },
3428
+ ...code && { code },
3429
+ ...message && { message }
3170
3430
  });
3171
- return { status: "error", ...outcomeExtra, ...details && { details } };
3431
+ return {
3432
+ status: "error",
3433
+ ...outcomeExtra,
3434
+ ...details && { details },
3435
+ ...code && { code },
3436
+ ...message && { message }
3437
+ };
3172
3438
  } catch (err) {
3173
3439
  const details = err instanceof Error ? err.message : void 0;
3174
3440
  this._setTransactionState({
@@ -3243,13 +3509,15 @@ var PollarClient = class {
3243
3509
  });
3244
3510
  return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
3245
3511
  }
3246
- const details = error?.details;
3512
+ const { details, code, message } = this._resolveTxApiError(error);
3247
3513
  this._setTransactionState({
3248
3514
  step: "error",
3249
3515
  phase: "building-signing-submitting",
3250
- ...details && { details }
3516
+ ...details && { details },
3517
+ ...code && { code },
3518
+ ...message && { message }
3251
3519
  });
3252
- return { status: "error", ...details && { details } };
3520
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3253
3521
  } catch (err) {
3254
3522
  const details = err instanceof Error ? err.message : void 0;
3255
3523
  this._setTransactionState({
@@ -3362,9 +3630,22 @@ var PollarClient = class {
3362
3630
  });
3363
3631
  return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3364
3632
  }
3365
- const details = error?.details;
3366
- this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3367
- return { status: "error", ...outcomeExtra, ...details && { details } };
3633
+ const { details, code, message } = this._resolveTxApiError(error);
3634
+ this._setTransactionState({
3635
+ step: "error",
3636
+ phase: "submitting",
3637
+ buildData,
3638
+ ...details && { details },
3639
+ ...code && { code },
3640
+ ...message && { message }
3641
+ });
3642
+ return {
3643
+ status: "error",
3644
+ ...outcomeExtra,
3645
+ ...details && { details },
3646
+ ...code && { code },
3647
+ ...message && { message }
3648
+ };
3368
3649
  } catch (err) {
3369
3650
  const details = err instanceof Error ? err.message : void 0;
3370
3651
  this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
@@ -3442,11 +3723,67 @@ var PollarClient = class {
3442
3723
  this._loginController = new AbortController();
3443
3724
  return this._loginController;
3444
3725
  }
3726
+ /**
3727
+ * Build the {@link AuthProviderContext} facade for one login attempt. Wraps
3728
+ * the internal `FlowDeps` so providers get only the curated primitives —
3729
+ * `createSession`, `authenticate`, `exchangeExternalToken`, `startHostedOAuth`
3730
+ * — while storage / wallet-adapter / key-manager internals stay private. All
3731
+ * legs share the same `signal`, so `cancelLogin()` aborts the whole chain.
3732
+ */
3733
+ _providerContext(signal) {
3734
+ const deps = this._flowDeps(signal);
3735
+ return {
3736
+ signal,
3737
+ api: this._api,
3738
+ basePath: this.basePath,
3739
+ apiKey: this.apiKey,
3740
+ logger: this._log,
3741
+ setAuthState: this._setAuthState.bind(this),
3742
+ createSession: () => createAuthSession(deps),
3743
+ authenticate: (clientSessionId) => authenticate(clientSessionId, deps),
3744
+ requestChallenge: (clientSessionId, walletAddress) => requestWalletChallenge(clientSessionId, walletAddress, deps),
3745
+ exchangeExternalToken: (clientSessionId, body) => this._exchangeExternalToken(clientSessionId, body, signal),
3746
+ startHostedOAuth: (provider) => loginOAuth(provider, {
3747
+ ...deps,
3748
+ basePath: this.basePath,
3749
+ apiKey: this.apiKey,
3750
+ openAuthUrl: this._openAuthUrl,
3751
+ redirectUri: this._oauthRedirectUri
3752
+ })
3753
+ };
3754
+ }
3755
+ /**
3756
+ * Generic external-provider exchange leg (`POST /auth/external`). Custom
3757
+ * providers call this (via the context) after their own SDK has authenticated
3758
+ * the user and the wallet has counter-signed the SEP-10 challenge
3759
+ * (`{ provider, walletAddress, signedChallengeXdr }`). On success the session
3760
+ * is marked READY server-side and the provider should then call
3761
+ * `ctx.authenticate(clientSessionId)`. Returns `false` (and sets an error
3762
+ * state) on failure.
3763
+ */
3764
+ async _exchangeExternalToken(clientSessionId, body, signal) {
3765
+ const { data, error } = await this._api.POST("/auth/external", {
3766
+ body: { clientSessionId, ...body },
3767
+ signal
3768
+ });
3769
+ if (error || !data?.success) {
3770
+ this._log.error("[PollarClient] External provider authentication failed", { error });
3771
+ this._setAuthState({
3772
+ step: "error",
3773
+ previousStep: this._authState.step,
3774
+ message: "External provider authentication failed",
3775
+ errorCode: AUTH_ERROR_CODES.EXTERNAL_AUTH_FAILED
3776
+ });
3777
+ return false;
3778
+ }
3779
+ return true;
3780
+ }
3445
3781
  _flowDeps(signal) {
3446
3782
  return {
3447
3783
  api: this._api,
3448
3784
  logger: this._log,
3449
3785
  basePath: this.basePath,
3786
+ networkPassphrase: this._networkPassphrase(),
3450
3787
  // SSE status streaming works on web; React Native's `fetch` has no
3451
3788
  // readable `response.body`, so those clients poll the non-streaming
3452
3789
  // status endpoint instead. `isBrowser` is false in RN and SSR alike.
@@ -3578,6 +3915,7 @@ var PollarClient = class {
3578
3915
  async _storeSession(session) {
3579
3916
  this._log.info("[PollarClient] Session stored");
3580
3917
  const w = session.wallet;
3918
+ const wireProvider = w.provider;
3581
3919
  const persisted = {
3582
3920
  clientSessionId: session.clientSessionId,
3583
3921
  userId: session.userId ?? null,
@@ -3591,6 +3929,7 @@ var PollarClient = class {
3591
3929
  // persisted session speak one vocabulary while the wire stays compatible.
3592
3930
  wallet: {
3593
3931
  type: w.type === "custodial" ? "internal" : w.type,
3932
+ ...wireProvider ? { provider: wireProvider } : {},
3594
3933
  address: w.address ?? w.publicKey ?? null,
3595
3934
  ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3596
3935
  ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},