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