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