@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.mjs CHANGED
@@ -187,7 +187,7 @@ function defaultKeyManager(storage, apiKey) {
187
187
 
188
188
  // src/lib/base64url.ts
189
189
  var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
190
- (() => {
190
+ var REVERSE = (() => {
191
191
  const m = /* @__PURE__ */ new Map();
192
192
  for (let i = 0; i < ALPHABET.length; i++) m.set(ALPHABET[i], i);
193
193
  return m;
@@ -218,6 +218,28 @@ function base64urlEncode(bytes) {
218
218
  }
219
219
  return result;
220
220
  }
221
+ function base64urlDecode(input) {
222
+ const clean = input.replace(/=+$/, "");
223
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
224
+ let byteIdx = 0;
225
+ for (let i = 0; i < clean.length; i += 4) {
226
+ const c1 = REVERSE.get(clean[i]);
227
+ const c2 = REVERSE.get(clean[i + 1]);
228
+ const c3 = i + 2 < clean.length ? REVERSE.get(clean[i + 2]) : void 0;
229
+ const c4 = i + 3 < clean.length ? REVERSE.get(clean[i + 3]) : void 0;
230
+ if (c1 === void 0 || c2 === void 0) {
231
+ throw new Error("[PollarClient] Invalid base64url input");
232
+ }
233
+ out[byteIdx++] = c1 << 2 | c2 >> 4;
234
+ if (c3 !== void 0) {
235
+ out[byteIdx++] = (c2 & 15) << 4 | c3 >> 2;
236
+ if (c4 !== void 0) {
237
+ out[byteIdx++] = (c3 & 3) << 6 | c4;
238
+ }
239
+ }
240
+ }
241
+ return out.slice(0, byteIdx);
242
+ }
221
243
  function base64urlEncodeString(s) {
222
244
  return base64urlEncode(new TextEncoder().encode(s));
223
245
  }
@@ -1084,7 +1106,19 @@ function createLogger(level = "info", sink = console) {
1084
1106
  }
1085
1107
 
1086
1108
  // src/lib/logging.ts
1087
- var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set(["email", "code", "walletAddress", "dpopJwk", "response", "refreshToken"]);
1109
+ var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
1110
+ "email",
1111
+ "code",
1112
+ "walletAddress",
1113
+ "dpopJwk",
1114
+ "response",
1115
+ "refreshToken",
1116
+ // SEP-10 challenge envelopes: a counter-signed challenge is a live, replayable
1117
+ // auth credential — never log it in the clear.
1118
+ "signedChallengeXdr",
1119
+ "challengeXdr",
1120
+ "signedTxXdr"
1121
+ ]);
1088
1122
  function redactBody(body) {
1089
1123
  if (!body || typeof body !== "object") return body;
1090
1124
  const out = {};
@@ -1181,8 +1215,36 @@ function defaultStorage(options = {}) {
1181
1215
  return createLocalStorageAdapter(options);
1182
1216
  }
1183
1217
 
1218
+ // src/types.ts
1219
+ var AUTH_ERROR_CODES = {
1220
+ SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1221
+ SESSION_EXPIRED: "SESSION_EXPIRED",
1222
+ SESSION_INVALID: "SESSION_INVALID",
1223
+ EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1224
+ EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1225
+ EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1226
+ EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1227
+ AUTH_FAILED: "AUTH_FAILED",
1228
+ WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1229
+ WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1230
+ WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1231
+ EXTERNAL_AUTH_FAILED: "EXTERNAL_AUTH_FAILED",
1232
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1233
+ // Generic bucket for on-chain transaction failures; the precise reason is the
1234
+ // backend `code` (e.g. TX_FEE_LIMIT_EXCEEDED) carried alongside on the outcome.
1235
+ TX_FAILED: "TX_FAILED",
1236
+ UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1237
+ };
1238
+ var PollarFlowError = class extends Error {
1239
+ constructor(message) {
1240
+ super(message);
1241
+ this.code = "INVALID_FLOW";
1242
+ this.name = "PollarFlowError";
1243
+ }
1244
+ };
1245
+
1184
1246
  // src/version.ts
1185
- var POLLAR_CORE_VERSION = "0.9.1-rc.0" ;
1247
+ var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
1186
1248
 
1187
1249
  // src/visibility/noop.ts
1188
1250
  function createNoopVisibilityProvider() {
@@ -1235,30 +1297,6 @@ function defaultVisibilityProvider() {
1235
1297
  return createNoopVisibilityProvider();
1236
1298
  }
1237
1299
 
1238
- // src/types.ts
1239
- var AUTH_ERROR_CODES = {
1240
- SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
1241
- SESSION_EXPIRED: "SESSION_EXPIRED",
1242
- SESSION_INVALID: "SESSION_INVALID",
1243
- EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
1244
- EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
1245
- EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
1246
- EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
1247
- AUTH_FAILED: "AUTH_FAILED",
1248
- WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1249
- WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1250
- WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1251
- PASSKEY_FAILED: "PASSKEY_FAILED",
1252
- UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1253
- };
1254
- var PollarFlowError = class extends Error {
1255
- constructor(message) {
1256
- super(message);
1257
- this.code = "INVALID_FLOW";
1258
- this.name = "PollarFlowError";
1259
- }
1260
- };
1261
-
1262
1300
  // src/wallets/FreighterAdapter.ts
1263
1301
  var import_freighter_api = __toESM(require_index_min());
1264
1302
 
@@ -1514,6 +1552,10 @@ function isValidSession(value, logger = console) {
1514
1552
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
1515
1553
  return false;
1516
1554
  }
1555
+ if (w["provider"] !== void 0 && typeof w["provider"] !== "string") {
1556
+ logger.debug("[PollarClient:session] Invalid session \u2014 wallet.provider must be a string if present");
1557
+ return false;
1558
+ }
1517
1559
  if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1518
1560
  logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1519
1561
  return false;
@@ -1809,14 +1851,91 @@ async function createAuthSession(deps) {
1809
1851
  return data.content.clientSessionId;
1810
1852
  }
1811
1853
 
1854
+ // src/client/auth/errorMessages.ts
1855
+ var CATALOG = {
1856
+ // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1857
+ SPONSOR_NOT_FUNDED: {
1858
+ message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1859
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1860
+ },
1861
+ APP_WALLET_NOT_FOUND: {
1862
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1863
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1864
+ },
1865
+ WALLET_NOT_FOUND: {
1866
+ message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1867
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1868
+ },
1869
+ PASSKEY_DEPLOY_FAILED: {
1870
+ message: "We couldn't finish creating your wallet. Please try again in a moment.",
1871
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1872
+ },
1873
+ // ── Passkey ceremony ────────────────────────────────────────────────────────
1874
+ PASSKEY_ALREADY_REGISTERED: {
1875
+ message: "A passkey is already registered for this account. Try signing in instead.",
1876
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1877
+ },
1878
+ PASSKEY_UNKNOWN_CREDENTIAL: {
1879
+ message: "We don't recognize this passkey. Try creating a new one.",
1880
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1881
+ },
1882
+ PASSKEY_VERIFICATION_FAILED: {
1883
+ message: "We couldn't verify your passkey. Please try again.",
1884
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1885
+ },
1886
+ PASSKEY_CHALLENGE_MISSING: {
1887
+ message: "Your passkey session expired. Please start again.",
1888
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1889
+ },
1890
+ // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1891
+ // These map to the TX_FAILED bucket (not PASSKEY_FAILED) — the precise reason
1892
+ // is the entry key itself, surfaced as the raw `code` on the tx outcome.
1893
+ TX_INSUFFICIENT_BALANCE: {
1894
+ message: "Insufficient balance to complete this transaction.",
1895
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1896
+ },
1897
+ TX_INSUFFICIENT_FEE: {
1898
+ message: "Not enough XLM to cover the network fee. Add more XLM to your wallet and try again.",
1899
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1900
+ },
1901
+ TX_FEE_LIMIT_EXCEEDED: {
1902
+ message: "The transaction fee is above the allowed limit. Please try again.",
1903
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1904
+ },
1905
+ TX_CONTRACT_FAILED: {
1906
+ message: "The contract rejected this operation. Check the operation is allowed right now and try again.",
1907
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1908
+ },
1909
+ TX_DESTINATION_NOT_FOUND: {
1910
+ message: "The destination account doesn't exist on the network yet.",
1911
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1912
+ },
1913
+ TX_NO_TRUSTLINE: {
1914
+ message: "The destination can't receive this asset yet (no trustline).",
1915
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1916
+ },
1917
+ TX_BAD_SEQUENCE: {
1918
+ message: "Something went out of sync. Please try again.",
1919
+ errorCode: AUTH_ERROR_CODES.TX_FAILED
1920
+ }
1921
+ };
1922
+ function resolveAuthError(code, fallbackMessage) {
1923
+ if (code && CATALOG[code]) return CATALOG[code];
1924
+ return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1925
+ }
1926
+ function extractErrorCode(error, data) {
1927
+ return error?.code ?? data?.code ?? void 0;
1928
+ }
1929
+
1812
1930
  // src/client/auth/emailFlow.ts
1813
- async function initEmailSession(deps) {
1814
- const clientSessionId = await createAuthSession(deps);
1815
- if (!clientSessionId) return;
1816
- deps.setAuthState({ step: "entering_email", clientSessionId });
1931
+ async function initEmailSession(ctx) {
1932
+ const clientSessionId = await ctx.createSession();
1933
+ if (!clientSessionId) return null;
1934
+ ctx.setAuthState({ step: "entering_email", clientSessionId });
1935
+ return clientSessionId;
1817
1936
  }
1818
- async function sendEmailCode(email, clientSessionId, deps) {
1819
- const { api, logger, signal, setAuthState } = deps;
1937
+ async function sendEmailCode(email, clientSessionId, ctx) {
1938
+ const { api, logger, signal, setAuthState } = ctx;
1820
1939
  setAuthState({ step: "sending_email", email });
1821
1940
  const body = { clientSessionId, email };
1822
1941
  const { data, error } = await api.POST("/auth/email", { body, signal });
@@ -1832,13 +1951,13 @@ async function sendEmailCode(email, clientSessionId, deps) {
1832
1951
  }
1833
1952
  setAuthState({ step: "entering_code", clientSessionId, email });
1834
1953
  }
1835
- async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1836
- const { api, logger, signal, setAuthState } = deps;
1954
+ async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
1955
+ const { api, logger, signal, setAuthState } = ctx;
1837
1956
  setAuthState({ step: "verifying_email_code", clientSessionId, email });
1838
1957
  const body = { clientSessionId, code };
1839
1958
  const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
1840
1959
  if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
1841
- await authenticate(clientSessionId, deps);
1960
+ await ctx.authenticate(clientSessionId);
1842
1961
  return;
1843
1962
  }
1844
1963
  const errCode = error?.error ?? data?.code;
@@ -1916,68 +2035,6 @@ async function loginOAuth(provider, deps) {
1916
2035
  await authenticate(clientSessionId, deps);
1917
2036
  }
1918
2037
 
1919
- // src/client/auth/errorMessages.ts
1920
- var CATALOG = {
1921
- // ── Smart-account deploy / sponsor wallet ──────────────────────────────────
1922
- SPONSOR_NOT_FUNDED: {
1923
- message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
1924
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1925
- },
1926
- APP_WALLET_NOT_FOUND: {
1927
- message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1928
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1929
- },
1930
- WALLET_NOT_FOUND: {
1931
- message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
1932
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1933
- },
1934
- PASSKEY_DEPLOY_FAILED: {
1935
- message: "We couldn't finish creating your wallet. Please try again in a moment.",
1936
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1937
- },
1938
- // ── Passkey ceremony ────────────────────────────────────────────────────────
1939
- PASSKEY_ALREADY_REGISTERED: {
1940
- message: "A passkey is already registered for this account. Try signing in instead.",
1941
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1942
- },
1943
- PASSKEY_UNKNOWN_CREDENTIAL: {
1944
- message: "We don't recognize this passkey. Try creating a new one.",
1945
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1946
- },
1947
- PASSKEY_VERIFICATION_FAILED: {
1948
- message: "We couldn't verify your passkey. Please try again.",
1949
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1950
- },
1951
- PASSKEY_CHALLENGE_MISSING: {
1952
- message: "Your passkey session expired. Please start again.",
1953
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1954
- },
1955
- // ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
1956
- TX_INSUFFICIENT_BALANCE: {
1957
- message: "Insufficient balance to complete this transaction.",
1958
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1959
- },
1960
- TX_DESTINATION_NOT_FOUND: {
1961
- message: "The destination account doesn't exist on the network yet.",
1962
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1963
- },
1964
- TX_NO_TRUSTLINE: {
1965
- message: "The destination can't receive this asset yet (no trustline).",
1966
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1967
- },
1968
- TX_BAD_SEQUENCE: {
1969
- message: "Something went out of sync. Please try again.",
1970
- errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1971
- }
1972
- };
1973
- function resolveAuthError(code, fallbackMessage) {
1974
- if (code && CATALOG[code]) return CATALOG[code];
1975
- return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
1976
- }
1977
- function extractErrorCode(error, data) {
1978
- return error?.code ?? data?.code ?? void 0;
1979
- }
1980
-
1981
2038
  // src/client/auth/passkeyFlow.ts
1982
2039
  async function smartWalletFlow(deps, mode) {
1983
2040
  const { api, logger, signal, setAuthState, passkey } = deps;
@@ -2034,6 +2091,71 @@ function failPasskey(setAuthState, code, fallbackMessage) {
2034
2091
  setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
2035
2092
  }
2036
2093
 
2094
+ // src/client/auth/providers.ts
2095
+ function oauthProvider(provider) {
2096
+ return {
2097
+ id: provider,
2098
+ login: (ctx) => ctx.startHostedOAuth(provider)
2099
+ };
2100
+ }
2101
+ function emailProvider() {
2102
+ return {
2103
+ id: "email",
2104
+ login: async (ctx, options) => {
2105
+ const email = options.email ?? "";
2106
+ const clientSessionId = await initEmailSession(ctx);
2107
+ if (clientSessionId) await sendEmailCode(email, clientSessionId, ctx);
2108
+ },
2109
+ actions: {
2110
+ begin: async (ctx) => {
2111
+ await initEmailSession(ctx);
2112
+ },
2113
+ sendCode: (ctx, payload) => {
2114
+ const { email, clientSessionId } = payload ?? {};
2115
+ return sendEmailCode(email, clientSessionId, ctx);
2116
+ },
2117
+ verifyCode: (ctx, payload) => {
2118
+ const { code, clientSessionId, email } = payload ?? {};
2119
+ return verifyAndAuthenticate(code, clientSessionId, email, ctx);
2120
+ }
2121
+ }
2122
+ };
2123
+ }
2124
+
2125
+ // src/client/auth/sep10-challenge.ts
2126
+ var ENVELOPE_TYPE_TX_V0 = 0;
2127
+ var ENVELOPE_TYPE_TX = 2;
2128
+ var KEY_TYPE_ED25519 = 0;
2129
+ var SEQ_OFFSET_V1 = 44;
2130
+ var SEQ_OFFSET_V0 = 40;
2131
+ function base64ToBytes(b64) {
2132
+ return base64urlDecode(b64.replace(/\+/g, "-").replace(/\//g, "_"));
2133
+ }
2134
+ function isI64Zero(view, offset) {
2135
+ return view.getUint32(offset, false) === 0 && view.getUint32(offset + 4, false) === 0;
2136
+ }
2137
+ function isValidSep10Challenge(challengeXdr) {
2138
+ try {
2139
+ const bytes = base64ToBytes(challengeXdr.trim());
2140
+ if (bytes.length < 8) return false;
2141
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2142
+ const envelopeType = view.getUint32(0, false);
2143
+ let seqOffset;
2144
+ if (envelopeType === ENVELOPE_TYPE_TX) {
2145
+ if (view.getUint32(4, false) !== KEY_TYPE_ED25519) return false;
2146
+ seqOffset = SEQ_OFFSET_V1;
2147
+ } else if (envelopeType === ENVELOPE_TYPE_TX_V0) {
2148
+ seqOffset = SEQ_OFFSET_V0;
2149
+ } else {
2150
+ return false;
2151
+ }
2152
+ if (bytes.length < seqOffset + 8) return false;
2153
+ return isI64Zero(view, seqOffset);
2154
+ } catch {
2155
+ return false;
2156
+ }
2157
+ }
2158
+
2037
2159
  // src/client/auth/walletFlow.ts
2038
2160
  function withSignal(promise, signal) {
2039
2161
  return Promise.race([
@@ -2047,6 +2169,16 @@ function withSignal(promise, signal) {
2047
2169
  })
2048
2170
  ]);
2049
2171
  }
2172
+ async function requestWalletChallenge(clientSessionId, walletAddress, deps) {
2173
+ const { api, logger, signal } = deps;
2174
+ const body = { clientSessionId, walletAddress };
2175
+ const { data, error } = await api.POST("/auth/wallet/challenge", { body, signal });
2176
+ if (error || !data?.success) {
2177
+ if (!error) logApiError(logger, "POST /auth/wallet/challenge", { body, data });
2178
+ return null;
2179
+ }
2180
+ return data.content.challengeXdr;
2181
+ }
2050
2182
  async function loginWallet(type, deps) {
2051
2183
  const { api, logger, signal, setAuthState } = deps;
2052
2184
  const clientSessionId = await createAuthSession(deps);
@@ -2063,8 +2195,33 @@ async function loginWallet(type, deps) {
2063
2195
  const { address } = await withSignal(adapter.connect(), signal);
2064
2196
  connectedWallet = address;
2065
2197
  deps.storeWalletAdapter(adapter, type);
2198
+ setAuthState({ step: "signing_wallet_challenge", walletType: type });
2199
+ const challengeXdr = await requestWalletChallenge(clientSessionId, address, deps);
2200
+ if (!challengeXdr) {
2201
+ setAuthState({
2202
+ step: "error",
2203
+ previousStep: "signing_wallet_challenge",
2204
+ message: "Failed to obtain wallet challenge",
2205
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2206
+ });
2207
+ return;
2208
+ }
2209
+ if (!isValidSep10Challenge(challengeXdr)) {
2210
+ logApiError(logger, "SEP-10 challenge validation", { error: "unexpected challenge structure (sequence != 0?)" });
2211
+ setAuthState({
2212
+ step: "error",
2213
+ previousStep: "signing_wallet_challenge",
2214
+ message: "Invalid wallet challenge",
2215
+ errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
2216
+ });
2217
+ return;
2218
+ }
2219
+ const { signedTxXdr } = await withSignal(
2220
+ adapter.signTransaction(challengeXdr, { networkPassphrase: deps.networkPassphrase }),
2221
+ signal
2222
+ );
2066
2223
  setAuthState({ step: "authenticating_wallet" });
2067
- const body = { clientSessionId, walletAddress: address };
2224
+ const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
2068
2225
  const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
2069
2226
  if (walletError || !walletData?.success) {
2070
2227
  if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
@@ -2153,6 +2310,13 @@ var PollarClient = class {
2153
2310
  this._loginController = null;
2154
2311
  /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2155
2312
  this._resumeController = null;
2313
+ /**
2314
+ * Registry of pluggable login strategies, keyed by provider id. Seeded with
2315
+ * the built-ins (`google`, `github`, `email`) and then any `config.providers`
2316
+ * (which can override a built-in by reusing its id). `wallet` is deliberately
2317
+ * absent — it keeps its own dedicated flow. See {@link PollarAuthProvider}.
2318
+ */
2319
+ this._providers = /* @__PURE__ */ new Map();
2156
2320
  this.apiKey = config.apiKey;
2157
2321
  this.id = randomUUID();
2158
2322
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2174,6 +2338,12 @@ var PollarClient = class {
2174
2338
  this._maxIdleMs = config.maxIdleMs;
2175
2339
  this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
2176
2340
  this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location?.origin ?? "" : "");
2341
+ for (const provider of [oauthProvider("google"), oauthProvider("github"), emailProvider()]) {
2342
+ this._providers.set(provider.id, provider);
2343
+ }
2344
+ for (const provider of config.providers ?? []) {
2345
+ this._providers.set(provider.id, provider);
2346
+ }
2177
2347
  this._api = createApiClient(this.basePath);
2178
2348
  this._wireMiddlewares();
2179
2349
  this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
@@ -2580,28 +2750,42 @@ var PollarClient = class {
2580
2750
  warnServerSide("login");
2581
2751
  return;
2582
2752
  }
2583
- if (options.provider === "google" || options.provider === "github" || options.provider === "email") {
2584
- const controller = this._newController();
2585
- const deps = this._flowDeps(controller.signal);
2586
- if (options.provider === "google" || options.provider === "github") {
2587
- loginOAuth(options.provider, {
2588
- ...deps,
2589
- basePath: this.basePath,
2590
- apiKey: this.apiKey,
2591
- openAuthUrl: this._openAuthUrl,
2592
- redirectUri: this._oauthRedirectUri
2593
- }).catch((err) => this._handleFlowError(err));
2594
- } else if (options.provider === "email") {
2595
- const { email } = options;
2596
- initEmailSession(deps).then(() => {
2597
- if (this._authState.step === "entering_email") {
2598
- return sendEmailCode(email, this._authState.clientSessionId, deps);
2599
- }
2600
- }).catch((err) => this._handleFlowError(err));
2601
- }
2602
- } else if (options.provider === "wallet") {
2753
+ if (options.provider === "wallet") {
2603
2754
  this.loginWallet(options.type);
2755
+ return;
2756
+ }
2757
+ const provider = this._providers.get(options.provider);
2758
+ if (!provider?.login) {
2759
+ this._setAuthState({
2760
+ step: "error",
2761
+ previousStep: this._authState.step,
2762
+ message: `No auth provider registered for '${options.provider}'`,
2763
+ errorCode: AUTH_ERROR_CODES.AUTH_FAILED
2764
+ });
2765
+ return;
2766
+ }
2767
+ const controller = this._newController();
2768
+ provider.login(this._providerContext(controller.signal), options).catch((err) => this._handleFlowError(err));
2769
+ }
2770
+ /**
2771
+ * Invoke a named secondary step on a registered provider (e.g. email's
2772
+ * `sendCode` / `verifyCode`, or a custom provider's multi-step continuation).
2773
+ * Reuses the in-flight login `AbortController` when one exists so the step
2774
+ * stays cancellable via `cancelLogin()`; otherwise starts a fresh one. The
2775
+ * built-in email steps also have dedicated typed methods
2776
+ * ({@link sendEmailCode} / {@link verifyEmailCode}) — prefer those for email.
2777
+ */
2778
+ providerAction(provider, action, payload) {
2779
+ if (!isClientRuntime) {
2780
+ warnServerSide("providerAction");
2781
+ return;
2782
+ }
2783
+ const fn = this._providers.get(provider)?.actions?.[action];
2784
+ if (!fn) {
2785
+ throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
2604
2786
  }
2787
+ const signal = this._loginController?.signal ?? this._newController().signal;
2788
+ fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
2605
2789
  }
2606
2790
  // ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
2607
2791
  beginEmailLogin() {
@@ -2610,7 +2794,7 @@ var PollarClient = class {
2610
2794
  return;
2611
2795
  }
2612
2796
  const controller = this._newController();
2613
- initEmailSession(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2797
+ initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
2614
2798
  }
2615
2799
  sendEmailCode(email) {
2616
2800
  if (!isClientRuntime) {
@@ -2622,7 +2806,7 @@ var PollarClient = class {
2622
2806
  }
2623
2807
  const { clientSessionId } = this._authState;
2624
2808
  const signal = this._loginController.signal;
2625
- sendEmailCode(email, clientSessionId, this._flowDeps(signal)).catch((err) => this._handleFlowError(err));
2809
+ sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
2626
2810
  }
2627
2811
  verifyEmailCode(code) {
2628
2812
  if (!isClientRuntime) {
@@ -2637,7 +2821,7 @@ var PollarClient = class {
2637
2821
  const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
2638
2822
  const email = state.step === "entering_code" ? state.email : state.email ?? "";
2639
2823
  const controller = this._newController();
2640
- verifyAndAuthenticate(code, clientSessionId, email, this._flowDeps(controller.signal)).catch(
2824
+ verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
2641
2825
  (err) => this._handleFlowError(err)
2642
2826
  );
2643
2827
  }
@@ -3002,6 +3186,29 @@ var PollarClient = class {
3002
3186
  getWalletType() {
3003
3187
  return this._walletAdapter?.type ?? null;
3004
3188
  }
3189
+ /**
3190
+ * The authenticated user's wallet as a {@link WalletInfo} discriminated union,
3191
+ * or `null` when there's no session (or the session carries no address yet).
3192
+ *
3193
+ * `custody` strictly determines `provider` (the mapping is 1:1 and fixed at
3194
+ * account creation server-side): `external` reports the connected adapter id
3195
+ * (`getWalletType()`), `smart` is always `'passkey'`, and `internal` reports
3196
+ * the login method the backend recorded (`null` for pre-provider sessions).
3197
+ */
3198
+ getWallet() {
3199
+ const w = this._session?.wallet;
3200
+ if (!w || !w.address) return null;
3201
+ switch (w.type) {
3202
+ case "external":
3203
+ return { custody: "external", address: w.address, provider: this._walletAdapter?.type ?? null };
3204
+ case "smart":
3205
+ return { custody: "smart", address: w.address, provider: "passkey" };
3206
+ case "internal":
3207
+ return { custody: "internal", address: w.address, provider: w.provider ?? null };
3208
+ default:
3209
+ return null;
3210
+ }
3211
+ }
3005
3212
  /**
3006
3213
  * Signs the given unsigned XDR and returns the signed XDR.
3007
3214
  *
@@ -3055,14 +3262,16 @@ var PollarClient = class {
3055
3262
  });
3056
3263
  return { status: "signed", signedXdr, submissionToken: idempotencyKey };
3057
3264
  }
3058
- const details = error?.details;
3265
+ const { details, code, message } = this._resolveTxApiError(error);
3059
3266
  this._setTransactionState({
3060
3267
  step: "error",
3061
3268
  phase: "signing",
3062
3269
  ...buildData && { buildData },
3063
- ...details && { details }
3270
+ ...details && { details },
3271
+ ...code && { code },
3272
+ ...message && { message }
3064
3273
  });
3065
- return { status: "error", ...details && { details } };
3274
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3066
3275
  } catch (err) {
3067
3276
  const details = err instanceof Error ? err.message : void 0;
3068
3277
  this._setTransactionState({
@@ -3074,6 +3283,54 @@ var PollarClient = class {
3074
3283
  return { status: "error", ...details && { details } };
3075
3284
  }
3076
3285
  }
3286
+ /**
3287
+ * Sign a single Soroban authorization entry (`SorobanAuthorizationEntry`).
3288
+ *
3289
+ * Use this when a contract is the transaction source (e.g. it sponsors the
3290
+ * gas and swaps the fee out of the user's token) and only needs the user's
3291
+ * address-credentials authorization, not a full signed envelope. The signed
3292
+ * entry is returned as base64 XDR for the caller to compose into its tx.
3293
+ *
3294
+ * - External wallets (Freighter/Albedo) sign the entry via the provider.
3295
+ * - Custodial wallets are signed by the backend, which FIRST validates the
3296
+ * entry's invocation tree against the app's contract/function allowlist and
3297
+ * caps the validity window — entries touching a non-allowlisted contract or
3298
+ * function, or expiring too far ahead, are rejected.
3299
+ *
3300
+ * @param entryXdr base64 XDR of the unsigned `SorobanAuthorizationEntry`.
3301
+ * @param options.validUntilLedger absolute ledger the signature expires at
3302
+ * (computed from the network's latest ledger). Ignored on the external-wallet
3303
+ * path, where the provider sets its own expiration.
3304
+ */
3305
+ async signAuthEntry(entryXdr, options) {
3306
+ if (this._walletAdapter) {
3307
+ const accountToSign = this._session?.wallet?.address;
3308
+ try {
3309
+ const { signedAuthEntry } = await this._walletAdapter.signAuthEntry(
3310
+ entryXdr,
3311
+ accountToSign ? { accountToSign } : void 0
3312
+ );
3313
+ return { status: "signed", signedAuthEntry };
3314
+ } catch (err) {
3315
+ const details = err instanceof Error ? err.message : void 0;
3316
+ return { status: "error", ...details && { details } };
3317
+ }
3318
+ }
3319
+ const address = this._session?.wallet?.address ?? "";
3320
+ try {
3321
+ const { data, error } = await this._api.POST("/tx/sign-auth-entry", {
3322
+ body: { network: this.getNetwork(), address, entryXdr, validUntilLedger: options.validUntilLedger }
3323
+ });
3324
+ if (!error && data?.success && data.content?.signedAuthEntry) {
3325
+ return { status: "signed", signedAuthEntry: data.content.signedAuthEntry };
3326
+ }
3327
+ const details = error?.details;
3328
+ return { status: "error", ...details && { details } };
3329
+ } catch (err) {
3330
+ const details = err instanceof Error ? err.message : void 0;
3331
+ return { status: "error", ...details && { details } };
3332
+ }
3333
+ }
3077
3334
  /**
3078
3335
  * Submits a signed XDR via `/tx/submit` regardless of wallet type
3079
3336
  * (custodial or external). Routing through sdk-api gives us:
@@ -3092,6 +3349,21 @@ var PollarClient = class {
3092
3349
  * `submitted` on Horizon ack (pending), `success` on ledger confirmation,
3093
3350
  * or `error[phase: 'submitting']` on failure.
3094
3351
  */
3352
+ /**
3353
+ * Normalize a backend API error into { details, code, message }. `code` is the
3354
+ * precise backend ErrorCode (e.g. `TX_FEE_LIMIT_EXCEEDED`) for programmatic
3355
+ * handling; `message` is a friendly string from the error catalog; `details`
3356
+ * is the raw diagnostic. Lets tx flows surface a typed reason instead of an
3357
+ * opaque details string.
3358
+ */
3359
+ _resolveTxApiError(error) {
3360
+ const e = error;
3361
+ const details = e?.details ?? e?.message;
3362
+ const code = e?.code;
3363
+ if (!code) return details ? { details } : {};
3364
+ const { message } = resolveAuthError(code, details ?? code);
3365
+ return { code, message, ...details && { details } };
3366
+ }
3095
3367
  async submitTx(signedXdr, opts) {
3096
3368
  const buildData = this._currentBuildData();
3097
3369
  const outcomeExtra = buildData ? { buildData } : {};
@@ -3129,14 +3401,22 @@ var PollarClient = class {
3129
3401
  ...resultCode && { details: resultCode, resultCode }
3130
3402
  };
3131
3403
  }
3132
- const details = error?.details;
3404
+ const { details, code, message } = this._resolveTxApiError(error);
3133
3405
  this._setTransactionState({
3134
3406
  step: "error",
3135
3407
  phase: "submitting",
3136
3408
  ...buildData && { buildData },
3137
- ...details && { details }
3409
+ ...details && { details },
3410
+ ...code && { code },
3411
+ ...message && { message }
3138
3412
  });
3139
- return { status: "error", ...outcomeExtra, ...details && { details } };
3413
+ return {
3414
+ status: "error",
3415
+ ...outcomeExtra,
3416
+ ...details && { details },
3417
+ ...code && { code },
3418
+ ...message && { message }
3419
+ };
3140
3420
  } catch (err) {
3141
3421
  const details = err instanceof Error ? err.message : void 0;
3142
3422
  this._setTransactionState({
@@ -3223,14 +3503,22 @@ var PollarClient = class {
3223
3503
  ...resultCode && { details: resultCode, resultCode }
3224
3504
  };
3225
3505
  }
3226
- const details = error?.details;
3506
+ const { details, code, message } = this._resolveTxApiError(error);
3227
3507
  this._setTransactionState({
3228
3508
  step: "error",
3229
3509
  phase: "signing-submitting",
3230
3510
  ...buildData && { buildData },
3231
- ...details && { details }
3511
+ ...details && { details },
3512
+ ...code && { code },
3513
+ ...message && { message }
3232
3514
  });
3233
- return { status: "error", ...outcomeExtra, ...details && { details } };
3515
+ return {
3516
+ status: "error",
3517
+ ...outcomeExtra,
3518
+ ...details && { details },
3519
+ ...code && { code },
3520
+ ...message && { message }
3521
+ };
3234
3522
  } catch (err) {
3235
3523
  const details = err instanceof Error ? err.message : void 0;
3236
3524
  this._setTransactionState({
@@ -3305,13 +3593,15 @@ var PollarClient = class {
3305
3593
  });
3306
3594
  return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
3307
3595
  }
3308
- const details = error?.details;
3596
+ const { details, code, message } = this._resolveTxApiError(error);
3309
3597
  this._setTransactionState({
3310
3598
  step: "error",
3311
3599
  phase: "building-signing-submitting",
3312
- ...details && { details }
3600
+ ...details && { details },
3601
+ ...code && { code },
3602
+ ...message && { message }
3313
3603
  });
3314
- return { status: "error", ...details && { details } };
3604
+ return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
3315
3605
  } catch (err) {
3316
3606
  const details = err instanceof Error ? err.message : void 0;
3317
3607
  this._setTransactionState({
@@ -3424,9 +3714,22 @@ var PollarClient = class {
3424
3714
  });
3425
3715
  return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3426
3716
  }
3427
- const details = error?.details;
3428
- this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3429
- return { status: "error", ...outcomeExtra, ...details && { details } };
3717
+ const { details, code, message } = this._resolveTxApiError(error);
3718
+ this._setTransactionState({
3719
+ step: "error",
3720
+ phase: "submitting",
3721
+ buildData,
3722
+ ...details && { details },
3723
+ ...code && { code },
3724
+ ...message && { message }
3725
+ });
3726
+ return {
3727
+ status: "error",
3728
+ ...outcomeExtra,
3729
+ ...details && { details },
3730
+ ...code && { code },
3731
+ ...message && { message }
3732
+ };
3430
3733
  } catch (err) {
3431
3734
  const details = err instanceof Error ? err.message : void 0;
3432
3735
  this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
@@ -3504,11 +3807,67 @@ var PollarClient = class {
3504
3807
  this._loginController = new AbortController();
3505
3808
  return this._loginController;
3506
3809
  }
3810
+ /**
3811
+ * Build the {@link AuthProviderContext} facade for one login attempt. Wraps
3812
+ * the internal `FlowDeps` so providers get only the curated primitives —
3813
+ * `createSession`, `authenticate`, `exchangeExternalToken`, `startHostedOAuth`
3814
+ * — while storage / wallet-adapter / key-manager internals stay private. All
3815
+ * legs share the same `signal`, so `cancelLogin()` aborts the whole chain.
3816
+ */
3817
+ _providerContext(signal) {
3818
+ const deps = this._flowDeps(signal);
3819
+ return {
3820
+ signal,
3821
+ api: this._api,
3822
+ basePath: this.basePath,
3823
+ apiKey: this.apiKey,
3824
+ logger: this._log,
3825
+ setAuthState: this._setAuthState.bind(this),
3826
+ createSession: () => createAuthSession(deps),
3827
+ authenticate: (clientSessionId) => authenticate(clientSessionId, deps),
3828
+ requestChallenge: (clientSessionId, walletAddress) => requestWalletChallenge(clientSessionId, walletAddress, deps),
3829
+ exchangeExternalToken: (clientSessionId, body) => this._exchangeExternalToken(clientSessionId, body, signal),
3830
+ startHostedOAuth: (provider) => loginOAuth(provider, {
3831
+ ...deps,
3832
+ basePath: this.basePath,
3833
+ apiKey: this.apiKey,
3834
+ openAuthUrl: this._openAuthUrl,
3835
+ redirectUri: this._oauthRedirectUri
3836
+ })
3837
+ };
3838
+ }
3839
+ /**
3840
+ * Generic external-provider exchange leg (`POST /auth/external`). Custom
3841
+ * providers call this (via the context) after their own SDK has authenticated
3842
+ * the user and the wallet has counter-signed the SEP-10 challenge
3843
+ * (`{ provider, walletAddress, signedChallengeXdr }`). On success the session
3844
+ * is marked READY server-side and the provider should then call
3845
+ * `ctx.authenticate(clientSessionId)`. Returns `false` (and sets an error
3846
+ * state) on failure.
3847
+ */
3848
+ async _exchangeExternalToken(clientSessionId, body, signal) {
3849
+ const { data, error } = await this._api.POST("/auth/external", {
3850
+ body: { clientSessionId, ...body },
3851
+ signal
3852
+ });
3853
+ if (error || !data?.success) {
3854
+ this._log.error("[PollarClient] External provider authentication failed", { error });
3855
+ this._setAuthState({
3856
+ step: "error",
3857
+ previousStep: this._authState.step,
3858
+ message: "External provider authentication failed",
3859
+ errorCode: AUTH_ERROR_CODES.EXTERNAL_AUTH_FAILED
3860
+ });
3861
+ return false;
3862
+ }
3863
+ return true;
3864
+ }
3507
3865
  _flowDeps(signal) {
3508
3866
  return {
3509
3867
  api: this._api,
3510
3868
  logger: this._log,
3511
3869
  basePath: this.basePath,
3870
+ networkPassphrase: this._networkPassphrase(),
3512
3871
  // SSE status streaming works on web; React Native's `fetch` has no
3513
3872
  // readable `response.body`, so those clients poll the non-streaming
3514
3873
  // status endpoint instead. `isBrowser` is false in RN and SSR alike.
@@ -3640,6 +3999,7 @@ var PollarClient = class {
3640
3999
  async _storeSession(session) {
3641
4000
  this._log.info("[PollarClient] Session stored");
3642
4001
  const w = session.wallet;
4002
+ const wireProvider = w.provider;
3643
4003
  const persisted = {
3644
4004
  clientSessionId: session.clientSessionId,
3645
4005
  userId: session.userId ?? null,
@@ -3653,6 +4013,7 @@ var PollarClient = class {
3653
4013
  // persisted session speak one vocabulary while the wire stays compatible.
3654
4014
  wallet: {
3655
4015
  type: w.type === "custodial" ? "internal" : w.type,
4016
+ ...wireProvider ? { provider: wireProvider } : {},
3656
4017
  address: w.address ?? w.publicKey ?? null,
3657
4018
  ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3658
4019
  ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},