@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.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
|
}
|
|
@@ -1083,6 +1105,29 @@ function createLogger(level = "info", sink = console) {
|
|
|
1083
1105
|
return { error: gate("error"), warn: gate("warn"), info: gate("info"), debug: gate("debug") };
|
|
1084
1106
|
}
|
|
1085
1107
|
|
|
1108
|
+
// src/lib/logging.ts
|
|
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
|
+
]);
|
|
1122
|
+
function redactBody(body) {
|
|
1123
|
+
if (!body || typeof body !== "object") return body;
|
|
1124
|
+
const out = {};
|
|
1125
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1126
|
+
out[key] = SENSITIVE_BODY_KEYS.has(key) ? "[redacted]" : value;
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1086
1131
|
// src/storage/web.ts
|
|
1087
1132
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
1088
1133
|
function createMemoryAdapter() {
|
|
@@ -1170,8 +1215,36 @@ function defaultStorage(options = {}) {
|
|
|
1170
1215
|
return createLocalStorageAdapter(options);
|
|
1171
1216
|
}
|
|
1172
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
|
+
|
|
1173
1246
|
// src/version.ts
|
|
1174
|
-
var POLLAR_CORE_VERSION = "0.
|
|
1247
|
+
var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
|
|
1175
1248
|
|
|
1176
1249
|
// src/visibility/noop.ts
|
|
1177
1250
|
function createNoopVisibilityProvider() {
|
|
@@ -1224,30 +1297,6 @@ function defaultVisibilityProvider() {
|
|
|
1224
1297
|
return createNoopVisibilityProvider();
|
|
1225
1298
|
}
|
|
1226
1299
|
|
|
1227
|
-
// src/types.ts
|
|
1228
|
-
var AUTH_ERROR_CODES = {
|
|
1229
|
-
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
1230
|
-
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1231
|
-
SESSION_INVALID: "SESSION_INVALID",
|
|
1232
|
-
EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
|
|
1233
|
-
EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
|
|
1234
|
-
EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
|
|
1235
|
-
EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
|
|
1236
|
-
AUTH_FAILED: "AUTH_FAILED",
|
|
1237
|
-
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1238
|
-
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1239
|
-
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1240
|
-
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1241
|
-
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1242
|
-
};
|
|
1243
|
-
var PollarFlowError = class extends Error {
|
|
1244
|
-
constructor(message) {
|
|
1245
|
-
super(message);
|
|
1246
|
-
this.code = "INVALID_FLOW";
|
|
1247
|
-
this.name = "PollarFlowError";
|
|
1248
|
-
}
|
|
1249
|
-
};
|
|
1250
|
-
|
|
1251
1300
|
// src/wallets/FreighterAdapter.ts
|
|
1252
1301
|
var import_freighter_api = __toESM(require_index_min());
|
|
1253
1302
|
|
|
@@ -1343,10 +1392,13 @@ function openAlbedoPopup(url) {
|
|
|
1343
1392
|
}
|
|
1344
1393
|
function waitForAlbedoPopup() {
|
|
1345
1394
|
return new Promise((resolve, reject) => {
|
|
1346
|
-
const timeout = setTimeout(
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1395
|
+
const timeout = setTimeout(
|
|
1396
|
+
() => {
|
|
1397
|
+
window.removeEventListener("message", handler);
|
|
1398
|
+
reject(new Error("Albedo response timeout"));
|
|
1399
|
+
},
|
|
1400
|
+
2 * 60 * 1e3
|
|
1401
|
+
);
|
|
1350
1402
|
function handler(event) {
|
|
1351
1403
|
if (event.origin !== window.location.origin || event.data?.type !== "ALBEDO_RESULT") return;
|
|
1352
1404
|
clearTimeout(timeout);
|
|
@@ -1500,6 +1552,10 @@ function isValidSession(value, logger = console) {
|
|
|
1500
1552
|
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
|
|
1501
1553
|
return false;
|
|
1502
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
|
+
}
|
|
1503
1559
|
if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
|
|
1504
1560
|
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
|
|
1505
1561
|
return false;
|
|
@@ -1706,6 +1762,16 @@ function waitForSessionReady(args) {
|
|
|
1706
1762
|
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal, logger) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal, logger);
|
|
1707
1763
|
}
|
|
1708
1764
|
|
|
1765
|
+
// src/client/auth/logging.ts
|
|
1766
|
+
function logApiError(logger, route, detail = {}, level = "error") {
|
|
1767
|
+
const { body, error, data } = detail;
|
|
1768
|
+
logger[level](`[PollarClient:auth] ${route} failed`, {
|
|
1769
|
+
route,
|
|
1770
|
+
...body !== void 0 ? { body: redactBody(body) } : {},
|
|
1771
|
+
cause: error ?? data
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1709
1775
|
// src/client/auth/authenticate.ts
|
|
1710
1776
|
async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
1711
1777
|
const { api, logger, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
@@ -1723,6 +1789,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1723
1789
|
} catch (err) {
|
|
1724
1790
|
if (err instanceof SessionStatusError) {
|
|
1725
1791
|
const expired = err.code === "EXPIRED_CLIENT_ID";
|
|
1792
|
+
logApiError(logger, "session status", { data: err });
|
|
1726
1793
|
setAuthState({
|
|
1727
1794
|
step: "error",
|
|
1728
1795
|
previousStep: "authenticating",
|
|
@@ -1735,14 +1802,12 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1735
1802
|
throw err;
|
|
1736
1803
|
}
|
|
1737
1804
|
const dpopJwk = await deps.getPublicJwk();
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
signal
|
|
1745
|
-
});
|
|
1805
|
+
const body = {
|
|
1806
|
+
clientSessionId,
|
|
1807
|
+
dpopJwk,
|
|
1808
|
+
...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
|
|
1809
|
+
};
|
|
1810
|
+
const { data, error } = await api.POST("/auth/login", { body, signal });
|
|
1746
1811
|
if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content, logger)) {
|
|
1747
1812
|
const sessionWallet = data.content.data?.providers?.wallet?.address;
|
|
1748
1813
|
if (expectedWallet && sessionWallet !== expectedWallet) {
|
|
@@ -1757,6 +1822,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1757
1822
|
}
|
|
1758
1823
|
await storeSession(data.content);
|
|
1759
1824
|
} else {
|
|
1825
|
+
if (!error) logApiError(logger, "POST /auth/login", { body, data });
|
|
1760
1826
|
setAuthState({
|
|
1761
1827
|
step: "error",
|
|
1762
1828
|
previousStep: "authenticating",
|
|
@@ -1769,10 +1835,11 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1769
1835
|
|
|
1770
1836
|
// src/client/auth/deps.ts
|
|
1771
1837
|
async function createAuthSession(deps) {
|
|
1772
|
-
const { api, signal, setAuthState } = deps;
|
|
1838
|
+
const { api, logger, signal, setAuthState } = deps;
|
|
1773
1839
|
setAuthState({ step: "creating_session" });
|
|
1774
1840
|
const { data, error } = await api.POST("/auth/session", { signal });
|
|
1775
1841
|
if (error || !data?.success) {
|
|
1842
|
+
if (!error) logApiError(logger, "POST /auth/session", { data });
|
|
1776
1843
|
setAuthState({
|
|
1777
1844
|
step: "error",
|
|
1778
1845
|
previousStep: "creating_session",
|
|
@@ -1784,20 +1851,96 @@ async function createAuthSession(deps) {
|
|
|
1784
1851
|
return data.content.clientSessionId;
|
|
1785
1852
|
}
|
|
1786
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
|
+
|
|
1787
1930
|
// src/client/auth/emailFlow.ts
|
|
1788
|
-
async function initEmailSession(
|
|
1789
|
-
const clientSessionId = await
|
|
1790
|
-
if (!clientSessionId) return;
|
|
1791
|
-
|
|
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;
|
|
1792
1936
|
}
|
|
1793
|
-
async function sendEmailCode(email, clientSessionId,
|
|
1794
|
-
const { api, signal, setAuthState } =
|
|
1937
|
+
async function sendEmailCode(email, clientSessionId, ctx) {
|
|
1938
|
+
const { api, logger, signal, setAuthState } = ctx;
|
|
1795
1939
|
setAuthState({ step: "sending_email", email });
|
|
1796
|
-
const {
|
|
1797
|
-
|
|
1798
|
-
signal
|
|
1799
|
-
});
|
|
1940
|
+
const body = { clientSessionId, email };
|
|
1941
|
+
const { data, error } = await api.POST("/auth/email", { body, signal });
|
|
1800
1942
|
if (error || !data?.success) {
|
|
1943
|
+
if (!error) logApiError(logger, "POST /auth/email", { body, data });
|
|
1801
1944
|
setAuthState({
|
|
1802
1945
|
step: "error",
|
|
1803
1946
|
previousStep: "sending_email",
|
|
@@ -1808,19 +1951,18 @@ async function sendEmailCode(email, clientSessionId, deps) {
|
|
|
1808
1951
|
}
|
|
1809
1952
|
setAuthState({ step: "entering_code", clientSessionId, email });
|
|
1810
1953
|
}
|
|
1811
|
-
async function verifyAndAuthenticate(code, clientSessionId, email,
|
|
1812
|
-
const { api, signal, setAuthState } =
|
|
1954
|
+
async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
|
|
1955
|
+
const { api, logger, signal, setAuthState } = ctx;
|
|
1813
1956
|
setAuthState({ step: "verifying_email_code", clientSessionId, email });
|
|
1814
|
-
const {
|
|
1815
|
-
|
|
1816
|
-
signal
|
|
1817
|
-
});
|
|
1957
|
+
const body = { clientSessionId, code };
|
|
1958
|
+
const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
|
|
1818
1959
|
if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
|
|
1819
|
-
await authenticate(clientSessionId
|
|
1960
|
+
await ctx.authenticate(clientSessionId);
|
|
1820
1961
|
return;
|
|
1821
1962
|
}
|
|
1822
1963
|
const errCode = error?.error ?? data?.code;
|
|
1823
1964
|
if (errCode === "SDK_EMAIL_CODE_EXPIRED") {
|
|
1965
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1824
1966
|
setAuthState({
|
|
1825
1967
|
step: "error",
|
|
1826
1968
|
previousStep: "verifying_email_code",
|
|
@@ -1832,6 +1974,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
|
|
|
1832
1974
|
return;
|
|
1833
1975
|
}
|
|
1834
1976
|
if (errCode === "INVALID_EMAIL_CODE" || errCode === "SDK_EMAIL_CODE_INVALID") {
|
|
1977
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1835
1978
|
setAuthState({
|
|
1836
1979
|
step: "error",
|
|
1837
1980
|
previousStep: "verifying_email_code",
|
|
@@ -1842,6 +1985,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
|
|
|
1842
1985
|
});
|
|
1843
1986
|
return;
|
|
1844
1987
|
}
|
|
1988
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1845
1989
|
setAuthState({
|
|
1846
1990
|
step: "error",
|
|
1847
1991
|
previousStep: "verifying_email_code",
|
|
@@ -1893,8 +2037,9 @@ async function loginOAuth(provider, deps) {
|
|
|
1893
2037
|
|
|
1894
2038
|
// src/client/auth/passkeyFlow.ts
|
|
1895
2039
|
async function smartWalletFlow(deps, mode) {
|
|
1896
|
-
const { api, signal, setAuthState, passkey } = deps;
|
|
2040
|
+
const { api, logger, signal, setAuthState, passkey } = deps;
|
|
1897
2041
|
if (!passkey) {
|
|
2042
|
+
logger.error("[PollarClient:auth] passkey ceremony not configured");
|
|
1898
2043
|
setAuthState({
|
|
1899
2044
|
step: "error",
|
|
1900
2045
|
previousStep: "creating_session",
|
|
@@ -1906,38 +2051,109 @@ async function smartWalletFlow(deps, mode) {
|
|
|
1906
2051
|
const clientSessionId = await createAuthSession(deps);
|
|
1907
2052
|
if (!clientSessionId) return;
|
|
1908
2053
|
try {
|
|
1909
|
-
const
|
|
1910
|
-
|
|
2054
|
+
const challengeBody = { clientSessionId };
|
|
2055
|
+
const { data: challengeData, error: challengeError } = await api.POST("/auth/passkey/challenge", {
|
|
2056
|
+
body: challengeBody,
|
|
1911
2057
|
signal
|
|
1912
2058
|
});
|
|
1913
2059
|
const challenge = challengeData?.content?.challenge;
|
|
1914
2060
|
if (!challengeData?.success || !challenge) {
|
|
1915
|
-
|
|
2061
|
+
if (!challengeError) logApiError(logger, "POST /auth/passkey/challenge", { body: challengeBody, data: challengeData });
|
|
2062
|
+
return failPasskey(setAuthState, extractErrorCode(challengeError, challengeData), "Failed to start passkey");
|
|
1916
2063
|
}
|
|
1917
2064
|
setAuthState({ step: "creating_passkey" });
|
|
1918
2065
|
const ceremony = await passkey({ challenge, mode });
|
|
1919
2066
|
const response = ceremony.response;
|
|
1920
2067
|
if (ceremony.kind === "register") {
|
|
1921
2068
|
setAuthState({ step: "deploying_smart_account" });
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2069
|
+
const body = { clientSessionId, response };
|
|
2070
|
+
const { data, error } = await api.POST("/auth/passkey/register", { body, signal });
|
|
2071
|
+
if (!data?.success) {
|
|
2072
|
+
if (!error) logApiError(logger, "POST /auth/passkey/register", { body, data });
|
|
2073
|
+
return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey registration failed");
|
|
2074
|
+
}
|
|
1927
2075
|
} else {
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2076
|
+
const body = { clientSessionId, response };
|
|
2077
|
+
const { data, error } = await api.POST("/auth/passkey/login", { body, signal });
|
|
2078
|
+
if (!data?.success) {
|
|
2079
|
+
if (!error) logApiError(logger, "POST /auth/passkey/login", { body, data });
|
|
2080
|
+
return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey authentication failed");
|
|
2081
|
+
}
|
|
1933
2082
|
}
|
|
1934
|
-
} catch {
|
|
1935
|
-
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
logApiError(logger, "passkey ceremony", { error: err });
|
|
2085
|
+
return failPasskey(setAuthState, void 0, "Passkey login failed");
|
|
1936
2086
|
}
|
|
1937
2087
|
await authenticate(clientSessionId, deps);
|
|
1938
2088
|
}
|
|
1939
|
-
function failPasskey(setAuthState,
|
|
1940
|
-
|
|
2089
|
+
function failPasskey(setAuthState, code, fallbackMessage) {
|
|
2090
|
+
const { message, errorCode } = resolveAuthError(code, fallbackMessage);
|
|
2091
|
+
setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
|
|
2092
|
+
}
|
|
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
|
+
}
|
|
1941
2157
|
}
|
|
1942
2158
|
|
|
1943
2159
|
// src/client/auth/walletFlow.ts
|
|
@@ -1953,8 +2169,18 @@ function withSignal(promise, signal) {
|
|
|
1953
2169
|
})
|
|
1954
2170
|
]);
|
|
1955
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
|
+
}
|
|
1956
2182
|
async function loginWallet(type, deps) {
|
|
1957
|
-
const { api, signal, setAuthState } = deps;
|
|
2183
|
+
const { api, logger, signal, setAuthState } = deps;
|
|
1958
2184
|
const clientSessionId = await createAuthSession(deps);
|
|
1959
2185
|
if (!clientSessionId) return;
|
|
1960
2186
|
let connectedWallet;
|
|
@@ -1969,12 +2195,36 @@ async function loginWallet(type, deps) {
|
|
|
1969
2195
|
const { address } = await withSignal(adapter.connect(), signal);
|
|
1970
2196
|
connectedWallet = address;
|
|
1971
2197
|
deps.storeWalletAdapter(adapter, type);
|
|
1972
|
-
setAuthState({ step: "
|
|
1973
|
-
const
|
|
1974
|
-
|
|
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 }),
|
|
1975
2221
|
signal
|
|
1976
|
-
|
|
2222
|
+
);
|
|
2223
|
+
setAuthState({ step: "authenticating_wallet" });
|
|
2224
|
+
const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
|
|
2225
|
+
const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
|
|
1977
2226
|
if (walletError || !walletData?.success) {
|
|
2227
|
+
if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
|
|
1978
2228
|
setAuthState({
|
|
1979
2229
|
step: "error",
|
|
1980
2230
|
previousStep: "authenticating_wallet",
|
|
@@ -1983,7 +2233,8 @@ async function loginWallet(type, deps) {
|
|
|
1983
2233
|
});
|
|
1984
2234
|
return;
|
|
1985
2235
|
}
|
|
1986
|
-
} catch {
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
logApiError(logger, "wallet connect", { error: err });
|
|
1987
2238
|
setAuthState({
|
|
1988
2239
|
step: "error",
|
|
1989
2240
|
previousStep: "connecting_wallet",
|
|
@@ -2059,6 +2310,13 @@ var PollarClient = class {
|
|
|
2059
2310
|
this._loginController = null;
|
|
2060
2311
|
/** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
|
|
2061
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();
|
|
2062
2320
|
this.apiKey = config.apiKey;
|
|
2063
2321
|
this.id = randomUUID();
|
|
2064
2322
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
@@ -2080,6 +2338,12 @@ var PollarClient = class {
|
|
|
2080
2338
|
this._maxIdleMs = config.maxIdleMs;
|
|
2081
2339
|
this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
|
|
2082
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
|
+
}
|
|
2083
2347
|
this._api = createApiClient(this.basePath);
|
|
2084
2348
|
this._wireMiddlewares();
|
|
2085
2349
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
@@ -2189,28 +2453,77 @@ var PollarClient = class {
|
|
|
2189
2453
|
onResponse: async ({ request, response }) => {
|
|
2190
2454
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
2191
2455
|
if (newNonce) self._dpopNonce = newNonce;
|
|
2192
|
-
if (response.status !== 401) return response;
|
|
2456
|
+
if (response.status !== 401) return self._logHttp(request, response);
|
|
2193
2457
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
2194
2458
|
const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
|
|
2195
2459
|
if (request.url.includes("/auth/refresh")) {
|
|
2196
|
-
if (isNonceChallenge) return self._retryRequest(request);
|
|
2197
|
-
return response;
|
|
2460
|
+
if (isNonceChallenge) return self._logHttp(request, await self._retryRequest(request));
|
|
2461
|
+
return self._logHttp(request, response);
|
|
2198
2462
|
}
|
|
2199
2463
|
if (!isNonceChallenge) {
|
|
2200
2464
|
try {
|
|
2201
2465
|
await self.refresh();
|
|
2202
2466
|
} catch {
|
|
2203
|
-
return response;
|
|
2467
|
+
return self._logHttp(request, response);
|
|
2204
2468
|
}
|
|
2205
2469
|
const method = request.method.toUpperCase();
|
|
2206
2470
|
if (method !== "GET" && method !== "HEAD") {
|
|
2207
|
-
return response;
|
|
2471
|
+
return self._logHttp(request, response);
|
|
2208
2472
|
}
|
|
2209
2473
|
}
|
|
2210
|
-
return self._retryRequest(request);
|
|
2474
|
+
return self._logHttp(request, await self._retryRequest(request));
|
|
2211
2475
|
}
|
|
2212
2476
|
});
|
|
2213
2477
|
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Logs the final outcome of an SDK API call exactly once: successes (`2xx`) at
|
|
2480
|
+
* `debug` (method + path + status, no body), failures (`4xx`/`5xx`) at `error`
|
|
2481
|
+
* with the redacted request body and the response error body. Returns the
|
|
2482
|
+
* response so it can be chained at the middleware's return points. The error
|
|
2483
|
+
* body is read off a synchronous `clone()` so it never disturbs the body the
|
|
2484
|
+
* caller consumes.
|
|
2485
|
+
*/
|
|
2486
|
+
_logHttp(request, response) {
|
|
2487
|
+
const path = this._httpPath(request.url);
|
|
2488
|
+
const label = `[PollarClient:http] ${request.method.toUpperCase()} ${path} ${response.status}`;
|
|
2489
|
+
if (response.ok) {
|
|
2490
|
+
this._log.debug(label);
|
|
2491
|
+
} else {
|
|
2492
|
+
void this._logHttpError(label, request, response.clone());
|
|
2493
|
+
}
|
|
2494
|
+
return response;
|
|
2495
|
+
}
|
|
2496
|
+
/** Reads the redacted request body + JSON response body and logs at `error`. */
|
|
2497
|
+
async _logHttpError(label, request, response) {
|
|
2498
|
+
let requestBody;
|
|
2499
|
+
const cached = this._requestBodyCache.get(request);
|
|
2500
|
+
if (cached) {
|
|
2501
|
+
try {
|
|
2502
|
+
requestBody = redactBody(JSON.parse(new TextDecoder().decode(cached)));
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
let responseBody;
|
|
2507
|
+
if ((response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
2508
|
+
try {
|
|
2509
|
+
responseBody = await response.json();
|
|
2510
|
+
} catch {
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
this._log.error(label, {
|
|
2514
|
+
...requestBody !== void 0 ? { requestBody } : {},
|
|
2515
|
+
...responseBody !== void 0 ? { responseBody } : {}
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
/** Strips origin + `/v1` version prefix from a request URL for compact logs. */
|
|
2519
|
+
_httpPath(url) {
|
|
2520
|
+
try {
|
|
2521
|
+
const { pathname } = new URL(url);
|
|
2522
|
+
return pathname.startsWith("/v1/") ? pathname.slice(3) : pathname;
|
|
2523
|
+
} catch {
|
|
2524
|
+
return url;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2214
2527
|
async _buildProofForRequest(request, accessToken) {
|
|
2215
2528
|
try {
|
|
2216
2529
|
const htu = request.url.split("?")[0].split("#")[0];
|
|
@@ -2437,28 +2750,42 @@ var PollarClient = class {
|
|
|
2437
2750
|
warnServerSide("login");
|
|
2438
2751
|
return;
|
|
2439
2752
|
}
|
|
2440
|
-
if (options.provider === "
|
|
2441
|
-
const controller = this._newController();
|
|
2442
|
-
const deps = this._flowDeps(controller.signal);
|
|
2443
|
-
if (options.provider === "google" || options.provider === "github") {
|
|
2444
|
-
loginOAuth(options.provider, {
|
|
2445
|
-
...deps,
|
|
2446
|
-
basePath: this.basePath,
|
|
2447
|
-
apiKey: this.apiKey,
|
|
2448
|
-
openAuthUrl: this._openAuthUrl,
|
|
2449
|
-
redirectUri: this._oauthRedirectUri
|
|
2450
|
-
}).catch((err) => this._handleFlowError(err));
|
|
2451
|
-
} else if (options.provider === "email") {
|
|
2452
|
-
const { email } = options;
|
|
2453
|
-
initEmailSession(deps).then(() => {
|
|
2454
|
-
if (this._authState.step === "entering_email") {
|
|
2455
|
-
return sendEmailCode(email, this._authState.clientSessionId, deps);
|
|
2456
|
-
}
|
|
2457
|
-
}).catch((err) => this._handleFlowError(err));
|
|
2458
|
-
}
|
|
2459
|
-
} else if (options.provider === "wallet") {
|
|
2753
|
+
if (options.provider === "wallet") {
|
|
2460
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;
|
|
2461
2782
|
}
|
|
2783
|
+
const fn = this._providers.get(provider)?.actions?.[action];
|
|
2784
|
+
if (!fn) {
|
|
2785
|
+
throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
|
|
2786
|
+
}
|
|
2787
|
+
const signal = this._loginController?.signal ?? this._newController().signal;
|
|
2788
|
+
fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
|
|
2462
2789
|
}
|
|
2463
2790
|
// ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
|
|
2464
2791
|
beginEmailLogin() {
|
|
@@ -2467,7 +2794,7 @@ var PollarClient = class {
|
|
|
2467
2794
|
return;
|
|
2468
2795
|
}
|
|
2469
2796
|
const controller = this._newController();
|
|
2470
|
-
initEmailSession(this.
|
|
2797
|
+
initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2471
2798
|
}
|
|
2472
2799
|
sendEmailCode(email) {
|
|
2473
2800
|
if (!isClientRuntime) {
|
|
@@ -2479,7 +2806,7 @@ var PollarClient = class {
|
|
|
2479
2806
|
}
|
|
2480
2807
|
const { clientSessionId } = this._authState;
|
|
2481
2808
|
const signal = this._loginController.signal;
|
|
2482
|
-
sendEmailCode(email, clientSessionId, this.
|
|
2809
|
+
sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
|
|
2483
2810
|
}
|
|
2484
2811
|
verifyEmailCode(code) {
|
|
2485
2812
|
if (!isClientRuntime) {
|
|
@@ -2494,7 +2821,7 @@ var PollarClient = class {
|
|
|
2494
2821
|
const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
|
|
2495
2822
|
const email = state.step === "entering_code" ? state.email : state.email ?? "";
|
|
2496
2823
|
const controller = this._newController();
|
|
2497
|
-
verifyAndAuthenticate(code, clientSessionId, email, this.
|
|
2824
|
+
verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
|
|
2498
2825
|
(err) => this._handleFlowError(err)
|
|
2499
2826
|
);
|
|
2500
2827
|
}
|
|
@@ -2859,6 +3186,29 @@ var PollarClient = class {
|
|
|
2859
3186
|
getWalletType() {
|
|
2860
3187
|
return this._walletAdapter?.type ?? null;
|
|
2861
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
|
+
}
|
|
2862
3212
|
/**
|
|
2863
3213
|
* Signs the given unsigned XDR and returns the signed XDR.
|
|
2864
3214
|
*
|
|
@@ -2912,14 +3262,16 @@ var PollarClient = class {
|
|
|
2912
3262
|
});
|
|
2913
3263
|
return { status: "signed", signedXdr, submissionToken: idempotencyKey };
|
|
2914
3264
|
}
|
|
2915
|
-
const details = error
|
|
3265
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
2916
3266
|
this._setTransactionState({
|
|
2917
3267
|
step: "error",
|
|
2918
3268
|
phase: "signing",
|
|
2919
3269
|
...buildData && { buildData },
|
|
2920
|
-
...details && { details }
|
|
3270
|
+
...details && { details },
|
|
3271
|
+
...code && { code },
|
|
3272
|
+
...message && { message }
|
|
2921
3273
|
});
|
|
2922
|
-
return { status: "error", ...details && { details } };
|
|
3274
|
+
return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
|
|
2923
3275
|
} catch (err) {
|
|
2924
3276
|
const details = err instanceof Error ? err.message : void 0;
|
|
2925
3277
|
this._setTransactionState({
|
|
@@ -2931,6 +3283,54 @@ var PollarClient = class {
|
|
|
2931
3283
|
return { status: "error", ...details && { details } };
|
|
2932
3284
|
}
|
|
2933
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
|
+
}
|
|
2934
3334
|
/**
|
|
2935
3335
|
* Submits a signed XDR via `/tx/submit` regardless of wallet type
|
|
2936
3336
|
* (custodial or external). Routing through sdk-api gives us:
|
|
@@ -2949,6 +3349,21 @@ var PollarClient = class {
|
|
|
2949
3349
|
* `submitted` on Horizon ack (pending), `success` on ledger confirmation,
|
|
2950
3350
|
* or `error[phase: 'submitting']` on failure.
|
|
2951
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
|
+
}
|
|
2952
3367
|
async submitTx(signedXdr, opts) {
|
|
2953
3368
|
const buildData = this._currentBuildData();
|
|
2954
3369
|
const outcomeExtra = buildData ? { buildData } : {};
|
|
@@ -2986,14 +3401,22 @@ var PollarClient = class {
|
|
|
2986
3401
|
...resultCode && { details: resultCode, resultCode }
|
|
2987
3402
|
};
|
|
2988
3403
|
}
|
|
2989
|
-
const details = error
|
|
3404
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
2990
3405
|
this._setTransactionState({
|
|
2991
3406
|
step: "error",
|
|
2992
3407
|
phase: "submitting",
|
|
2993
3408
|
...buildData && { buildData },
|
|
2994
|
-
...details && { details }
|
|
3409
|
+
...details && { details },
|
|
3410
|
+
...code && { code },
|
|
3411
|
+
...message && { message }
|
|
2995
3412
|
});
|
|
2996
|
-
return {
|
|
3413
|
+
return {
|
|
3414
|
+
status: "error",
|
|
3415
|
+
...outcomeExtra,
|
|
3416
|
+
...details && { details },
|
|
3417
|
+
...code && { code },
|
|
3418
|
+
...message && { message }
|
|
3419
|
+
};
|
|
2997
3420
|
} catch (err) {
|
|
2998
3421
|
const details = err instanceof Error ? err.message : void 0;
|
|
2999
3422
|
this._setTransactionState({
|
|
@@ -3080,14 +3503,22 @@ var PollarClient = class {
|
|
|
3080
3503
|
...resultCode && { details: resultCode, resultCode }
|
|
3081
3504
|
};
|
|
3082
3505
|
}
|
|
3083
|
-
const details = error
|
|
3506
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
3084
3507
|
this._setTransactionState({
|
|
3085
3508
|
step: "error",
|
|
3086
3509
|
phase: "signing-submitting",
|
|
3087
3510
|
...buildData && { buildData },
|
|
3088
|
-
...details && { details }
|
|
3511
|
+
...details && { details },
|
|
3512
|
+
...code && { code },
|
|
3513
|
+
...message && { message }
|
|
3089
3514
|
});
|
|
3090
|
-
return {
|
|
3515
|
+
return {
|
|
3516
|
+
status: "error",
|
|
3517
|
+
...outcomeExtra,
|
|
3518
|
+
...details && { details },
|
|
3519
|
+
...code && { code },
|
|
3520
|
+
...message && { message }
|
|
3521
|
+
};
|
|
3091
3522
|
} catch (err) {
|
|
3092
3523
|
const details = err instanceof Error ? err.message : void 0;
|
|
3093
3524
|
this._setTransactionState({
|
|
@@ -3162,13 +3593,15 @@ var PollarClient = class {
|
|
|
3162
3593
|
});
|
|
3163
3594
|
return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
|
|
3164
3595
|
}
|
|
3165
|
-
const details = error
|
|
3596
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
3166
3597
|
this._setTransactionState({
|
|
3167
3598
|
step: "error",
|
|
3168
3599
|
phase: "building-signing-submitting",
|
|
3169
|
-
...details && { details }
|
|
3600
|
+
...details && { details },
|
|
3601
|
+
...code && { code },
|
|
3602
|
+
...message && { message }
|
|
3170
3603
|
});
|
|
3171
|
-
return { status: "error", ...details && { details } };
|
|
3604
|
+
return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
|
|
3172
3605
|
} catch (err) {
|
|
3173
3606
|
const details = err instanceof Error ? err.message : void 0;
|
|
3174
3607
|
this._setTransactionState({
|
|
@@ -3281,9 +3714,22 @@ var PollarClient = class {
|
|
|
3281
3714
|
});
|
|
3282
3715
|
return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
|
|
3283
3716
|
}
|
|
3284
|
-
const details = error
|
|
3285
|
-
this._setTransactionState({
|
|
3286
|
-
|
|
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
|
+
};
|
|
3287
3733
|
} catch (err) {
|
|
3288
3734
|
const details = err instanceof Error ? err.message : void 0;
|
|
3289
3735
|
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
@@ -3361,11 +3807,67 @@ var PollarClient = class {
|
|
|
3361
3807
|
this._loginController = new AbortController();
|
|
3362
3808
|
return this._loginController;
|
|
3363
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
|
+
}
|
|
3364
3865
|
_flowDeps(signal) {
|
|
3365
3866
|
return {
|
|
3366
3867
|
api: this._api,
|
|
3367
3868
|
logger: this._log,
|
|
3368
3869
|
basePath: this.basePath,
|
|
3870
|
+
networkPassphrase: this._networkPassphrase(),
|
|
3369
3871
|
// SSE status streaming works on web; React Native's `fetch` has no
|
|
3370
3872
|
// readable `response.body`, so those clients poll the non-streaming
|
|
3371
3873
|
// status endpoint instead. `isBrowser` is false in RN and SSR alike.
|
|
@@ -3497,6 +3999,7 @@ var PollarClient = class {
|
|
|
3497
3999
|
async _storeSession(session) {
|
|
3498
4000
|
this._log.info("[PollarClient] Session stored");
|
|
3499
4001
|
const w = session.wallet;
|
|
4002
|
+
const wireProvider = w.provider;
|
|
3500
4003
|
const persisted = {
|
|
3501
4004
|
clientSessionId: session.clientSessionId,
|
|
3502
4005
|
userId: session.userId ?? null,
|
|
@@ -3510,6 +4013,7 @@ var PollarClient = class {
|
|
|
3510
4013
|
// persisted session speak one vocabulary while the wire stays compatible.
|
|
3511
4014
|
wallet: {
|
|
3512
4015
|
type: w.type === "custodial" ? "internal" : w.type,
|
|
4016
|
+
...wireProvider ? { provider: wireProvider } : {},
|
|
3513
4017
|
address: w.address ?? w.publicKey ?? null,
|
|
3514
4018
|
...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
|
|
3515
4019
|
...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
|