@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.rn.mjs
CHANGED
|
@@ -1019,6 +1019,29 @@ function createLogger(level = "info", sink = console) {
|
|
|
1019
1019
|
return { error: gate("error"), warn: gate("warn"), info: gate("info"), debug: gate("debug") };
|
|
1020
1020
|
}
|
|
1021
1021
|
|
|
1022
|
+
// src/lib/logging.ts
|
|
1023
|
+
var SENSITIVE_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
1024
|
+
"email",
|
|
1025
|
+
"code",
|
|
1026
|
+
"walletAddress",
|
|
1027
|
+
"dpopJwk",
|
|
1028
|
+
"response",
|
|
1029
|
+
"refreshToken",
|
|
1030
|
+
// SEP-10 challenge envelopes: a counter-signed challenge is a live, replayable
|
|
1031
|
+
// auth credential — never log it in the clear.
|
|
1032
|
+
"signedChallengeXdr",
|
|
1033
|
+
"challengeXdr",
|
|
1034
|
+
"signedTxXdr"
|
|
1035
|
+
]);
|
|
1036
|
+
function redactBody(body) {
|
|
1037
|
+
if (!body || typeof body !== "object") return body;
|
|
1038
|
+
const out = {};
|
|
1039
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1040
|
+
out[key] = SENSITIVE_BODY_KEYS.has(key) ? "[redacted]" : value;
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1022
1045
|
// src/storage/web.ts
|
|
1023
1046
|
var LOG_PREFIX = "[PollarClient:storage]";
|
|
1024
1047
|
function createMemoryAdapter() {
|
|
@@ -1106,8 +1129,36 @@ function defaultStorage(options = {}) {
|
|
|
1106
1129
|
return createLocalStorageAdapter(options);
|
|
1107
1130
|
}
|
|
1108
1131
|
|
|
1132
|
+
// src/types.ts
|
|
1133
|
+
var AUTH_ERROR_CODES = {
|
|
1134
|
+
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
1135
|
+
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1136
|
+
SESSION_INVALID: "SESSION_INVALID",
|
|
1137
|
+
EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
|
|
1138
|
+
EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
|
|
1139
|
+
EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
|
|
1140
|
+
EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
|
|
1141
|
+
AUTH_FAILED: "AUTH_FAILED",
|
|
1142
|
+
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1143
|
+
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1144
|
+
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1145
|
+
EXTERNAL_AUTH_FAILED: "EXTERNAL_AUTH_FAILED",
|
|
1146
|
+
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1147
|
+
// Generic bucket for on-chain transaction failures; the precise reason is the
|
|
1148
|
+
// backend `code` (e.g. TX_FEE_LIMIT_EXCEEDED) carried alongside on the outcome.
|
|
1149
|
+
TX_FAILED: "TX_FAILED",
|
|
1150
|
+
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1151
|
+
};
|
|
1152
|
+
var PollarFlowError = class extends Error {
|
|
1153
|
+
constructor(message) {
|
|
1154
|
+
super(message);
|
|
1155
|
+
this.code = "INVALID_FLOW";
|
|
1156
|
+
this.name = "PollarFlowError";
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1109
1160
|
// src/version.ts
|
|
1110
|
-
var POLLAR_CORE_VERSION = "0.
|
|
1161
|
+
var POLLAR_CORE_VERSION = "0.10.0-rc.0" ;
|
|
1111
1162
|
|
|
1112
1163
|
// src/visibility/noop.ts
|
|
1113
1164
|
function createNoopVisibilityProvider() {
|
|
@@ -1160,30 +1211,6 @@ function defaultVisibilityProvider() {
|
|
|
1160
1211
|
return createNoopVisibilityProvider();
|
|
1161
1212
|
}
|
|
1162
1213
|
|
|
1163
|
-
// src/types.ts
|
|
1164
|
-
var AUTH_ERROR_CODES = {
|
|
1165
|
-
SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
|
|
1166
|
-
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1167
|
-
SESSION_INVALID: "SESSION_INVALID",
|
|
1168
|
-
EMAIL_SEND_FAILED: "EMAIL_SEND_FAILED",
|
|
1169
|
-
EMAIL_VERIFY_FAILED: "EMAIL_VERIFY_FAILED",
|
|
1170
|
-
EMAIL_CODE_EXPIRED: "EMAIL_CODE_EXPIRED",
|
|
1171
|
-
EMAIL_CODE_INVALID: "EMAIL_CODE_INVALID",
|
|
1172
|
-
AUTH_FAILED: "AUTH_FAILED",
|
|
1173
|
-
WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
|
|
1174
|
-
WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
|
|
1175
|
-
WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
|
|
1176
|
-
PASSKEY_FAILED: "PASSKEY_FAILED",
|
|
1177
|
-
UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
|
|
1178
|
-
};
|
|
1179
|
-
var PollarFlowError = class extends Error {
|
|
1180
|
-
constructor(message) {
|
|
1181
|
-
super(message);
|
|
1182
|
-
this.code = "INVALID_FLOW";
|
|
1183
|
-
this.name = "PollarFlowError";
|
|
1184
|
-
}
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
1214
|
// src/wallets/FreighterAdapter.ts
|
|
1188
1215
|
var import_freighter_api = __toESM(require_index_min());
|
|
1189
1216
|
|
|
@@ -1279,10 +1306,13 @@ function openAlbedoPopup(url) {
|
|
|
1279
1306
|
}
|
|
1280
1307
|
function waitForAlbedoPopup() {
|
|
1281
1308
|
return new Promise((resolve, reject) => {
|
|
1282
|
-
const timeout = setTimeout(
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1309
|
+
const timeout = setTimeout(
|
|
1310
|
+
() => {
|
|
1311
|
+
window.removeEventListener("message", handler);
|
|
1312
|
+
reject(new Error("Albedo response timeout"));
|
|
1313
|
+
},
|
|
1314
|
+
2 * 60 * 1e3
|
|
1315
|
+
);
|
|
1286
1316
|
function handler(event) {
|
|
1287
1317
|
if (event.origin !== window.location.origin || event.data?.type !== "ALBEDO_RESULT") return;
|
|
1288
1318
|
clearTimeout(timeout);
|
|
@@ -1436,6 +1466,10 @@ function isValidSession(value, logger = console) {
|
|
|
1436
1466
|
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.type must be internal|smart|external");
|
|
1437
1467
|
return false;
|
|
1438
1468
|
}
|
|
1469
|
+
if (w["provider"] !== void 0 && typeof w["provider"] !== "string") {
|
|
1470
|
+
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.provider must be a string if present");
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1439
1473
|
if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
|
|
1440
1474
|
logger.debug("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
|
|
1441
1475
|
return false;
|
|
@@ -1642,6 +1676,16 @@ function waitForSessionReady(args) {
|
|
|
1642
1676
|
return useStreaming ? streamUntilFound(api, clientSessionId, check, retryDelayMs ?? 200, signal, logger) : pollUntilFound(baseUrl, clientSessionId, check, retryDelayMs ?? 500, signal, logger);
|
|
1643
1677
|
}
|
|
1644
1678
|
|
|
1679
|
+
// src/client/auth/logging.ts
|
|
1680
|
+
function logApiError(logger, route, detail = {}, level = "error") {
|
|
1681
|
+
const { body, error, data } = detail;
|
|
1682
|
+
logger[level](`[PollarClient:auth] ${route} failed`, {
|
|
1683
|
+
route,
|
|
1684
|
+
...body !== void 0 ? { body: redactBody(body) } : {},
|
|
1685
|
+
cause: error ?? data
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1645
1689
|
// src/client/auth/authenticate.ts
|
|
1646
1690
|
async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
1647
1691
|
const { api, logger, basePath, useStreaming, signal, setAuthState, storeSession, clearSession } = deps;
|
|
@@ -1659,6 +1703,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1659
1703
|
} catch (err) {
|
|
1660
1704
|
if (err instanceof SessionStatusError) {
|
|
1661
1705
|
const expired = err.code === "EXPIRED_CLIENT_ID";
|
|
1706
|
+
logApiError(logger, "session status", { data: err });
|
|
1662
1707
|
setAuthState({
|
|
1663
1708
|
step: "error",
|
|
1664
1709
|
previousStep: "authenticating",
|
|
@@ -1671,14 +1716,12 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1671
1716
|
throw err;
|
|
1672
1717
|
}
|
|
1673
1718
|
const dpopJwk = await deps.getPublicJwk();
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
signal
|
|
1681
|
-
});
|
|
1719
|
+
const body = {
|
|
1720
|
+
clientSessionId,
|
|
1721
|
+
dpopJwk,
|
|
1722
|
+
...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
|
|
1723
|
+
};
|
|
1724
|
+
const { data, error } = await api.POST("/auth/login", { body, signal });
|
|
1682
1725
|
if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content, logger)) {
|
|
1683
1726
|
const sessionWallet = data.content.data?.providers?.wallet?.address;
|
|
1684
1727
|
if (expectedWallet && sessionWallet !== expectedWallet) {
|
|
@@ -1693,6 +1736,7 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1693
1736
|
}
|
|
1694
1737
|
await storeSession(data.content);
|
|
1695
1738
|
} else {
|
|
1739
|
+
if (!error) logApiError(logger, "POST /auth/login", { body, data });
|
|
1696
1740
|
setAuthState({
|
|
1697
1741
|
step: "error",
|
|
1698
1742
|
previousStep: "authenticating",
|
|
@@ -1705,10 +1749,11 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
|
|
|
1705
1749
|
|
|
1706
1750
|
// src/client/auth/deps.ts
|
|
1707
1751
|
async function createAuthSession(deps) {
|
|
1708
|
-
const { api, signal, setAuthState } = deps;
|
|
1752
|
+
const { api, logger, signal, setAuthState } = deps;
|
|
1709
1753
|
setAuthState({ step: "creating_session" });
|
|
1710
1754
|
const { data, error } = await api.POST("/auth/session", { signal });
|
|
1711
1755
|
if (error || !data?.success) {
|
|
1756
|
+
if (!error) logApiError(logger, "POST /auth/session", { data });
|
|
1712
1757
|
setAuthState({
|
|
1713
1758
|
step: "error",
|
|
1714
1759
|
previousStep: "creating_session",
|
|
@@ -1720,20 +1765,96 @@ async function createAuthSession(deps) {
|
|
|
1720
1765
|
return data.content.clientSessionId;
|
|
1721
1766
|
}
|
|
1722
1767
|
|
|
1768
|
+
// src/client/auth/errorMessages.ts
|
|
1769
|
+
var CATALOG = {
|
|
1770
|
+
// ── Smart-account deploy / sponsor wallet ──────────────────────────────────
|
|
1771
|
+
SPONSOR_NOT_FUNDED: {
|
|
1772
|
+
message: "This app can't create your wallet yet \u2014 its sponsor account isn't funded. Please contact the app's developer.",
|
|
1773
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1774
|
+
},
|
|
1775
|
+
APP_WALLET_NOT_FOUND: {
|
|
1776
|
+
message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
|
|
1777
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1778
|
+
},
|
|
1779
|
+
WALLET_NOT_FOUND: {
|
|
1780
|
+
message: "This app isn't fully set up to create wallets yet. Please contact the app's developer.",
|
|
1781
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1782
|
+
},
|
|
1783
|
+
PASSKEY_DEPLOY_FAILED: {
|
|
1784
|
+
message: "We couldn't finish creating your wallet. Please try again in a moment.",
|
|
1785
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1786
|
+
},
|
|
1787
|
+
// ── Passkey ceremony ────────────────────────────────────────────────────────
|
|
1788
|
+
PASSKEY_ALREADY_REGISTERED: {
|
|
1789
|
+
message: "A passkey is already registered for this account. Try signing in instead.",
|
|
1790
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1791
|
+
},
|
|
1792
|
+
PASSKEY_UNKNOWN_CREDENTIAL: {
|
|
1793
|
+
message: "We don't recognize this passkey. Try creating a new one.",
|
|
1794
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1795
|
+
},
|
|
1796
|
+
PASSKEY_VERIFICATION_FAILED: {
|
|
1797
|
+
message: "We couldn't verify your passkey. Please try again.",
|
|
1798
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1799
|
+
},
|
|
1800
|
+
PASSKEY_CHALLENGE_MISSING: {
|
|
1801
|
+
message: "Your passkey session expired. Please start again.",
|
|
1802
|
+
errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
|
|
1803
|
+
},
|
|
1804
|
+
// ── On-chain transaction failures (surfaced during deploy/transfer) ─────────
|
|
1805
|
+
// These map to the TX_FAILED bucket (not PASSKEY_FAILED) — the precise reason
|
|
1806
|
+
// is the entry key itself, surfaced as the raw `code` on the tx outcome.
|
|
1807
|
+
TX_INSUFFICIENT_BALANCE: {
|
|
1808
|
+
message: "Insufficient balance to complete this transaction.",
|
|
1809
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1810
|
+
},
|
|
1811
|
+
TX_INSUFFICIENT_FEE: {
|
|
1812
|
+
message: "Not enough XLM to cover the network fee. Add more XLM to your wallet and try again.",
|
|
1813
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1814
|
+
},
|
|
1815
|
+
TX_FEE_LIMIT_EXCEEDED: {
|
|
1816
|
+
message: "The transaction fee is above the allowed limit. Please try again.",
|
|
1817
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1818
|
+
},
|
|
1819
|
+
TX_CONTRACT_FAILED: {
|
|
1820
|
+
message: "The contract rejected this operation. Check the operation is allowed right now and try again.",
|
|
1821
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1822
|
+
},
|
|
1823
|
+
TX_DESTINATION_NOT_FOUND: {
|
|
1824
|
+
message: "The destination account doesn't exist on the network yet.",
|
|
1825
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1826
|
+
},
|
|
1827
|
+
TX_NO_TRUSTLINE: {
|
|
1828
|
+
message: "The destination can't receive this asset yet (no trustline).",
|
|
1829
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1830
|
+
},
|
|
1831
|
+
TX_BAD_SEQUENCE: {
|
|
1832
|
+
message: "Something went out of sync. Please try again.",
|
|
1833
|
+
errorCode: AUTH_ERROR_CODES.TX_FAILED
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
function resolveAuthError(code, fallbackMessage) {
|
|
1837
|
+
if (code && CATALOG[code]) return CATALOG[code];
|
|
1838
|
+
return { message: fallbackMessage, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED };
|
|
1839
|
+
}
|
|
1840
|
+
function extractErrorCode(error, data) {
|
|
1841
|
+
return error?.code ?? data?.code ?? void 0;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1723
1844
|
// src/client/auth/emailFlow.ts
|
|
1724
|
-
async function initEmailSession(
|
|
1725
|
-
const clientSessionId = await
|
|
1726
|
-
if (!clientSessionId) return;
|
|
1727
|
-
|
|
1845
|
+
async function initEmailSession(ctx) {
|
|
1846
|
+
const clientSessionId = await ctx.createSession();
|
|
1847
|
+
if (!clientSessionId) return null;
|
|
1848
|
+
ctx.setAuthState({ step: "entering_email", clientSessionId });
|
|
1849
|
+
return clientSessionId;
|
|
1728
1850
|
}
|
|
1729
|
-
async function sendEmailCode(email, clientSessionId,
|
|
1730
|
-
const { api, signal, setAuthState } =
|
|
1851
|
+
async function sendEmailCode(email, clientSessionId, ctx) {
|
|
1852
|
+
const { api, logger, signal, setAuthState } = ctx;
|
|
1731
1853
|
setAuthState({ step: "sending_email", email });
|
|
1732
|
-
const {
|
|
1733
|
-
|
|
1734
|
-
signal
|
|
1735
|
-
});
|
|
1854
|
+
const body = { clientSessionId, email };
|
|
1855
|
+
const { data, error } = await api.POST("/auth/email", { body, signal });
|
|
1736
1856
|
if (error || !data?.success) {
|
|
1857
|
+
if (!error) logApiError(logger, "POST /auth/email", { body, data });
|
|
1737
1858
|
setAuthState({
|
|
1738
1859
|
step: "error",
|
|
1739
1860
|
previousStep: "sending_email",
|
|
@@ -1744,19 +1865,18 @@ async function sendEmailCode(email, clientSessionId, deps) {
|
|
|
1744
1865
|
}
|
|
1745
1866
|
setAuthState({ step: "entering_code", clientSessionId, email });
|
|
1746
1867
|
}
|
|
1747
|
-
async function verifyAndAuthenticate(code, clientSessionId, email,
|
|
1748
|
-
const { api, signal, setAuthState } =
|
|
1868
|
+
async function verifyAndAuthenticate(code, clientSessionId, email, ctx) {
|
|
1869
|
+
const { api, logger, signal, setAuthState } = ctx;
|
|
1749
1870
|
setAuthState({ step: "verifying_email_code", clientSessionId, email });
|
|
1750
|
-
const {
|
|
1751
|
-
|
|
1752
|
-
signal
|
|
1753
|
-
});
|
|
1871
|
+
const body = { clientSessionId, code };
|
|
1872
|
+
const { data, error } = await api.POST("/auth/email/verify-code", { body, signal });
|
|
1754
1873
|
if (data?.code === "SDK_EMAIL_CODE_VERIFIED") {
|
|
1755
|
-
await authenticate(clientSessionId
|
|
1874
|
+
await ctx.authenticate(clientSessionId);
|
|
1756
1875
|
return;
|
|
1757
1876
|
}
|
|
1758
1877
|
const errCode = error?.error ?? data?.code;
|
|
1759
1878
|
if (errCode === "SDK_EMAIL_CODE_EXPIRED") {
|
|
1879
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1760
1880
|
setAuthState({
|
|
1761
1881
|
step: "error",
|
|
1762
1882
|
previousStep: "verifying_email_code",
|
|
@@ -1768,6 +1888,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
|
|
|
1768
1888
|
return;
|
|
1769
1889
|
}
|
|
1770
1890
|
if (errCode === "INVALID_EMAIL_CODE" || errCode === "SDK_EMAIL_CODE_INVALID") {
|
|
1891
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1771
1892
|
setAuthState({
|
|
1772
1893
|
step: "error",
|
|
1773
1894
|
previousStep: "verifying_email_code",
|
|
@@ -1778,6 +1899,7 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
|
|
|
1778
1899
|
});
|
|
1779
1900
|
return;
|
|
1780
1901
|
}
|
|
1902
|
+
if (!error) logApiError(logger, "POST /auth/email/verify-code", { body, data });
|
|
1781
1903
|
setAuthState({
|
|
1782
1904
|
step: "error",
|
|
1783
1905
|
previousStep: "verifying_email_code",
|
|
@@ -1829,8 +1951,9 @@ async function loginOAuth(provider, deps) {
|
|
|
1829
1951
|
|
|
1830
1952
|
// src/client/auth/passkeyFlow.ts
|
|
1831
1953
|
async function smartWalletFlow(deps, mode) {
|
|
1832
|
-
const { api, signal, setAuthState, passkey } = deps;
|
|
1954
|
+
const { api, logger, signal, setAuthState, passkey } = deps;
|
|
1833
1955
|
if (!passkey) {
|
|
1956
|
+
logger.error("[PollarClient:auth] passkey ceremony not configured");
|
|
1834
1957
|
setAuthState({
|
|
1835
1958
|
step: "error",
|
|
1836
1959
|
previousStep: "creating_session",
|
|
@@ -1842,38 +1965,109 @@ async function smartWalletFlow(deps, mode) {
|
|
|
1842
1965
|
const clientSessionId = await createAuthSession(deps);
|
|
1843
1966
|
if (!clientSessionId) return;
|
|
1844
1967
|
try {
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1968
|
+
const challengeBody = { clientSessionId };
|
|
1969
|
+
const { data: challengeData, error: challengeError } = await api.POST("/auth/passkey/challenge", {
|
|
1970
|
+
body: challengeBody,
|
|
1847
1971
|
signal
|
|
1848
1972
|
});
|
|
1849
1973
|
const challenge = challengeData?.content?.challenge;
|
|
1850
1974
|
if (!challengeData?.success || !challenge) {
|
|
1851
|
-
|
|
1975
|
+
if (!challengeError) logApiError(logger, "POST /auth/passkey/challenge", { body: challengeBody, data: challengeData });
|
|
1976
|
+
return failPasskey(setAuthState, extractErrorCode(challengeError, challengeData), "Failed to start passkey");
|
|
1852
1977
|
}
|
|
1853
1978
|
setAuthState({ step: "creating_passkey" });
|
|
1854
1979
|
const ceremony = await passkey({ challenge, mode });
|
|
1855
1980
|
const response = ceremony.response;
|
|
1856
1981
|
if (ceremony.kind === "register") {
|
|
1857
1982
|
setAuthState({ step: "deploying_smart_account" });
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1983
|
+
const body = { clientSessionId, response };
|
|
1984
|
+
const { data, error } = await api.POST("/auth/passkey/register", { body, signal });
|
|
1985
|
+
if (!data?.success) {
|
|
1986
|
+
if (!error) logApiError(logger, "POST /auth/passkey/register", { body, data });
|
|
1987
|
+
return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey registration failed");
|
|
1988
|
+
}
|
|
1863
1989
|
} else {
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1990
|
+
const body = { clientSessionId, response };
|
|
1991
|
+
const { data, error } = await api.POST("/auth/passkey/login", { body, signal });
|
|
1992
|
+
if (!data?.success) {
|
|
1993
|
+
if (!error) logApiError(logger, "POST /auth/passkey/login", { body, data });
|
|
1994
|
+
return failPasskey(setAuthState, extractErrorCode(error, data), "Passkey authentication failed");
|
|
1995
|
+
}
|
|
1869
1996
|
}
|
|
1870
|
-
} catch {
|
|
1871
|
-
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
logApiError(logger, "passkey ceremony", { error: err });
|
|
1999
|
+
return failPasskey(setAuthState, void 0, "Passkey login failed");
|
|
1872
2000
|
}
|
|
1873
2001
|
await authenticate(clientSessionId, deps);
|
|
1874
2002
|
}
|
|
1875
|
-
function failPasskey(setAuthState,
|
|
1876
|
-
|
|
2003
|
+
function failPasskey(setAuthState, code, fallbackMessage) {
|
|
2004
|
+
const { message, errorCode } = resolveAuthError(code, fallbackMessage);
|
|
2005
|
+
setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode });
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/client/auth/providers.ts
|
|
2009
|
+
function oauthProvider(provider) {
|
|
2010
|
+
return {
|
|
2011
|
+
id: provider,
|
|
2012
|
+
login: (ctx) => ctx.startHostedOAuth(provider)
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
function emailProvider() {
|
|
2016
|
+
return {
|
|
2017
|
+
id: "email",
|
|
2018
|
+
login: async (ctx, options) => {
|
|
2019
|
+
const email = options.email ?? "";
|
|
2020
|
+
const clientSessionId = await initEmailSession(ctx);
|
|
2021
|
+
if (clientSessionId) await sendEmailCode(email, clientSessionId, ctx);
|
|
2022
|
+
},
|
|
2023
|
+
actions: {
|
|
2024
|
+
begin: async (ctx) => {
|
|
2025
|
+
await initEmailSession(ctx);
|
|
2026
|
+
},
|
|
2027
|
+
sendCode: (ctx, payload) => {
|
|
2028
|
+
const { email, clientSessionId } = payload ?? {};
|
|
2029
|
+
return sendEmailCode(email, clientSessionId, ctx);
|
|
2030
|
+
},
|
|
2031
|
+
verifyCode: (ctx, payload) => {
|
|
2032
|
+
const { code, clientSessionId, email } = payload ?? {};
|
|
2033
|
+
return verifyAndAuthenticate(code, clientSessionId, email, ctx);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/client/auth/sep10-challenge.ts
|
|
2040
|
+
var ENVELOPE_TYPE_TX_V0 = 0;
|
|
2041
|
+
var ENVELOPE_TYPE_TX = 2;
|
|
2042
|
+
var KEY_TYPE_ED25519 = 0;
|
|
2043
|
+
var SEQ_OFFSET_V1 = 44;
|
|
2044
|
+
var SEQ_OFFSET_V0 = 40;
|
|
2045
|
+
function base64ToBytes(b64) {
|
|
2046
|
+
return base64urlDecode(b64.replace(/\+/g, "-").replace(/\//g, "_"));
|
|
2047
|
+
}
|
|
2048
|
+
function isI64Zero(view, offset) {
|
|
2049
|
+
return view.getUint32(offset, false) === 0 && view.getUint32(offset + 4, false) === 0;
|
|
2050
|
+
}
|
|
2051
|
+
function isValidSep10Challenge(challengeXdr) {
|
|
2052
|
+
try {
|
|
2053
|
+
const bytes = base64ToBytes(challengeXdr.trim());
|
|
2054
|
+
if (bytes.length < 8) return false;
|
|
2055
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2056
|
+
const envelopeType = view.getUint32(0, false);
|
|
2057
|
+
let seqOffset;
|
|
2058
|
+
if (envelopeType === ENVELOPE_TYPE_TX) {
|
|
2059
|
+
if (view.getUint32(4, false) !== KEY_TYPE_ED25519) return false;
|
|
2060
|
+
seqOffset = SEQ_OFFSET_V1;
|
|
2061
|
+
} else if (envelopeType === ENVELOPE_TYPE_TX_V0) {
|
|
2062
|
+
seqOffset = SEQ_OFFSET_V0;
|
|
2063
|
+
} else {
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
if (bytes.length < seqOffset + 8) return false;
|
|
2067
|
+
return isI64Zero(view, seqOffset);
|
|
2068
|
+
} catch {
|
|
2069
|
+
return false;
|
|
2070
|
+
}
|
|
1877
2071
|
}
|
|
1878
2072
|
|
|
1879
2073
|
// src/client/auth/walletFlow.ts
|
|
@@ -1889,8 +2083,18 @@ function withSignal(promise, signal) {
|
|
|
1889
2083
|
})
|
|
1890
2084
|
]);
|
|
1891
2085
|
}
|
|
2086
|
+
async function requestWalletChallenge(clientSessionId, walletAddress, deps) {
|
|
2087
|
+
const { api, logger, signal } = deps;
|
|
2088
|
+
const body = { clientSessionId, walletAddress };
|
|
2089
|
+
const { data, error } = await api.POST("/auth/wallet/challenge", { body, signal });
|
|
2090
|
+
if (error || !data?.success) {
|
|
2091
|
+
if (!error) logApiError(logger, "POST /auth/wallet/challenge", { body, data });
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
return data.content.challengeXdr;
|
|
2095
|
+
}
|
|
1892
2096
|
async function loginWallet(type, deps) {
|
|
1893
|
-
const { api, signal, setAuthState } = deps;
|
|
2097
|
+
const { api, logger, signal, setAuthState } = deps;
|
|
1894
2098
|
const clientSessionId = await createAuthSession(deps);
|
|
1895
2099
|
if (!clientSessionId) return;
|
|
1896
2100
|
let connectedWallet;
|
|
@@ -1905,12 +2109,36 @@ async function loginWallet(type, deps) {
|
|
|
1905
2109
|
const { address } = await withSignal(adapter.connect(), signal);
|
|
1906
2110
|
connectedWallet = address;
|
|
1907
2111
|
deps.storeWalletAdapter(adapter, type);
|
|
1908
|
-
setAuthState({ step: "
|
|
1909
|
-
const
|
|
1910
|
-
|
|
2112
|
+
setAuthState({ step: "signing_wallet_challenge", walletType: type });
|
|
2113
|
+
const challengeXdr = await requestWalletChallenge(clientSessionId, address, deps);
|
|
2114
|
+
if (!challengeXdr) {
|
|
2115
|
+
setAuthState({
|
|
2116
|
+
step: "error",
|
|
2117
|
+
previousStep: "signing_wallet_challenge",
|
|
2118
|
+
message: "Failed to obtain wallet challenge",
|
|
2119
|
+
errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
|
|
2120
|
+
});
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (!isValidSep10Challenge(challengeXdr)) {
|
|
2124
|
+
logApiError(logger, "SEP-10 challenge validation", { error: "unexpected challenge structure (sequence != 0?)" });
|
|
2125
|
+
setAuthState({
|
|
2126
|
+
step: "error",
|
|
2127
|
+
previousStep: "signing_wallet_challenge",
|
|
2128
|
+
message: "Invalid wallet challenge",
|
|
2129
|
+
errorCode: AUTH_ERROR_CODES.WALLET_AUTH_FAILED
|
|
2130
|
+
});
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const { signedTxXdr } = await withSignal(
|
|
2134
|
+
adapter.signTransaction(challengeXdr, { networkPassphrase: deps.networkPassphrase }),
|
|
1911
2135
|
signal
|
|
1912
|
-
|
|
2136
|
+
);
|
|
2137
|
+
setAuthState({ step: "authenticating_wallet" });
|
|
2138
|
+
const body = { clientSessionId, walletAddress: address, signedChallengeXdr: signedTxXdr };
|
|
2139
|
+
const { data: walletData, error: walletError } = await api.POST("/auth/wallet", { body, signal });
|
|
1913
2140
|
if (walletError || !walletData?.success) {
|
|
2141
|
+
if (!walletError) logApiError(logger, "POST /auth/wallet", { body, data: walletData });
|
|
1914
2142
|
setAuthState({
|
|
1915
2143
|
step: "error",
|
|
1916
2144
|
previousStep: "authenticating_wallet",
|
|
@@ -1919,7 +2147,8 @@ async function loginWallet(type, deps) {
|
|
|
1919
2147
|
});
|
|
1920
2148
|
return;
|
|
1921
2149
|
}
|
|
1922
|
-
} catch {
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
logApiError(logger, "wallet connect", { error: err });
|
|
1923
2152
|
setAuthState({
|
|
1924
2153
|
step: "error",
|
|
1925
2154
|
previousStep: "connecting_wallet",
|
|
@@ -1995,6 +2224,13 @@ var PollarClient = class {
|
|
|
1995
2224
|
this._loginController = null;
|
|
1996
2225
|
/** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
|
|
1997
2226
|
this._resumeController = null;
|
|
2227
|
+
/**
|
|
2228
|
+
* Registry of pluggable login strategies, keyed by provider id. Seeded with
|
|
2229
|
+
* the built-ins (`google`, `github`, `email`) and then any `config.providers`
|
|
2230
|
+
* (which can override a built-in by reusing its id). `wallet` is deliberately
|
|
2231
|
+
* absent — it keeps its own dedicated flow. See {@link PollarAuthProvider}.
|
|
2232
|
+
*/
|
|
2233
|
+
this._providers = /* @__PURE__ */ new Map();
|
|
1998
2234
|
this.apiKey = config.apiKey;
|
|
1999
2235
|
this.id = randomUUID();
|
|
2000
2236
|
this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
|
|
@@ -2016,6 +2252,12 @@ var PollarClient = class {
|
|
|
2016
2252
|
this._maxIdleMs = config.maxIdleMs;
|
|
2017
2253
|
this._openAuthUrl = config.openAuthUrl ?? defaultWebOAuthOpener;
|
|
2018
2254
|
this._oauthRedirectUri = config.oauthRedirectUri ?? (isBrowser ? window.location?.origin ?? "" : "");
|
|
2255
|
+
for (const provider of [oauthProvider("google"), oauthProvider("github"), emailProvider()]) {
|
|
2256
|
+
this._providers.set(provider.id, provider);
|
|
2257
|
+
}
|
|
2258
|
+
for (const provider of config.providers ?? []) {
|
|
2259
|
+
this._providers.set(provider.id, provider);
|
|
2260
|
+
}
|
|
2019
2261
|
this._api = createApiClient(this.basePath);
|
|
2020
2262
|
this._wireMiddlewares();
|
|
2021
2263
|
this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
|
|
@@ -2125,28 +2367,77 @@ var PollarClient = class {
|
|
|
2125
2367
|
onResponse: async ({ request, response }) => {
|
|
2126
2368
|
const newNonce = response.headers.get("DPoP-Nonce");
|
|
2127
2369
|
if (newNonce) self._dpopNonce = newNonce;
|
|
2128
|
-
if (response.status !== 401) return response;
|
|
2370
|
+
if (response.status !== 401) return self._logHttp(request, response);
|
|
2129
2371
|
const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
|
|
2130
2372
|
const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
|
|
2131
2373
|
if (request.url.includes("/auth/refresh")) {
|
|
2132
|
-
if (isNonceChallenge) return self._retryRequest(request);
|
|
2133
|
-
return response;
|
|
2374
|
+
if (isNonceChallenge) return self._logHttp(request, await self._retryRequest(request));
|
|
2375
|
+
return self._logHttp(request, response);
|
|
2134
2376
|
}
|
|
2135
2377
|
if (!isNonceChallenge) {
|
|
2136
2378
|
try {
|
|
2137
2379
|
await self.refresh();
|
|
2138
2380
|
} catch {
|
|
2139
|
-
return response;
|
|
2381
|
+
return self._logHttp(request, response);
|
|
2140
2382
|
}
|
|
2141
2383
|
const method = request.method.toUpperCase();
|
|
2142
2384
|
if (method !== "GET" && method !== "HEAD") {
|
|
2143
|
-
return response;
|
|
2385
|
+
return self._logHttp(request, response);
|
|
2144
2386
|
}
|
|
2145
2387
|
}
|
|
2146
|
-
return self._retryRequest(request);
|
|
2388
|
+
return self._logHttp(request, await self._retryRequest(request));
|
|
2147
2389
|
}
|
|
2148
2390
|
});
|
|
2149
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Logs the final outcome of an SDK API call exactly once: successes (`2xx`) at
|
|
2394
|
+
* `debug` (method + path + status, no body), failures (`4xx`/`5xx`) at `error`
|
|
2395
|
+
* with the redacted request body and the response error body. Returns the
|
|
2396
|
+
* response so it can be chained at the middleware's return points. The error
|
|
2397
|
+
* body is read off a synchronous `clone()` so it never disturbs the body the
|
|
2398
|
+
* caller consumes.
|
|
2399
|
+
*/
|
|
2400
|
+
_logHttp(request, response) {
|
|
2401
|
+
const path = this._httpPath(request.url);
|
|
2402
|
+
const label = `[PollarClient:http] ${request.method.toUpperCase()} ${path} ${response.status}`;
|
|
2403
|
+
if (response.ok) {
|
|
2404
|
+
this._log.debug(label);
|
|
2405
|
+
} else {
|
|
2406
|
+
void this._logHttpError(label, request, response.clone());
|
|
2407
|
+
}
|
|
2408
|
+
return response;
|
|
2409
|
+
}
|
|
2410
|
+
/** Reads the redacted request body + JSON response body and logs at `error`. */
|
|
2411
|
+
async _logHttpError(label, request, response) {
|
|
2412
|
+
let requestBody;
|
|
2413
|
+
const cached = this._requestBodyCache.get(request);
|
|
2414
|
+
if (cached) {
|
|
2415
|
+
try {
|
|
2416
|
+
requestBody = redactBody(JSON.parse(new TextDecoder().decode(cached)));
|
|
2417
|
+
} catch {
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
let responseBody;
|
|
2421
|
+
if ((response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
2422
|
+
try {
|
|
2423
|
+
responseBody = await response.json();
|
|
2424
|
+
} catch {
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
this._log.error(label, {
|
|
2428
|
+
...requestBody !== void 0 ? { requestBody } : {},
|
|
2429
|
+
...responseBody !== void 0 ? { responseBody } : {}
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
/** Strips origin + `/v1` version prefix from a request URL for compact logs. */
|
|
2433
|
+
_httpPath(url) {
|
|
2434
|
+
try {
|
|
2435
|
+
const { pathname } = new URL(url);
|
|
2436
|
+
return pathname.startsWith("/v1/") ? pathname.slice(3) : pathname;
|
|
2437
|
+
} catch {
|
|
2438
|
+
return url;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2150
2441
|
async _buildProofForRequest(request, accessToken) {
|
|
2151
2442
|
try {
|
|
2152
2443
|
const htu = request.url.split("?")[0].split("#")[0];
|
|
@@ -2373,28 +2664,42 @@ var PollarClient = class {
|
|
|
2373
2664
|
warnServerSide("login");
|
|
2374
2665
|
return;
|
|
2375
2666
|
}
|
|
2376
|
-
if (options.provider === "
|
|
2377
|
-
const controller = this._newController();
|
|
2378
|
-
const deps = this._flowDeps(controller.signal);
|
|
2379
|
-
if (options.provider === "google" || options.provider === "github") {
|
|
2380
|
-
loginOAuth(options.provider, {
|
|
2381
|
-
...deps,
|
|
2382
|
-
basePath: this.basePath,
|
|
2383
|
-
apiKey: this.apiKey,
|
|
2384
|
-
openAuthUrl: this._openAuthUrl,
|
|
2385
|
-
redirectUri: this._oauthRedirectUri
|
|
2386
|
-
}).catch((err) => this._handleFlowError(err));
|
|
2387
|
-
} else if (options.provider === "email") {
|
|
2388
|
-
const { email } = options;
|
|
2389
|
-
initEmailSession(deps).then(() => {
|
|
2390
|
-
if (this._authState.step === "entering_email") {
|
|
2391
|
-
return sendEmailCode(email, this._authState.clientSessionId, deps);
|
|
2392
|
-
}
|
|
2393
|
-
}).catch((err) => this._handleFlowError(err));
|
|
2394
|
-
}
|
|
2395
|
-
} else if (options.provider === "wallet") {
|
|
2667
|
+
if (options.provider === "wallet") {
|
|
2396
2668
|
this.loginWallet(options.type);
|
|
2669
|
+
return;
|
|
2397
2670
|
}
|
|
2671
|
+
const provider = this._providers.get(options.provider);
|
|
2672
|
+
if (!provider?.login) {
|
|
2673
|
+
this._setAuthState({
|
|
2674
|
+
step: "error",
|
|
2675
|
+
previousStep: this._authState.step,
|
|
2676
|
+
message: `No auth provider registered for '${options.provider}'`,
|
|
2677
|
+
errorCode: AUTH_ERROR_CODES.AUTH_FAILED
|
|
2678
|
+
});
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
const controller = this._newController();
|
|
2682
|
+
provider.login(this._providerContext(controller.signal), options).catch((err) => this._handleFlowError(err));
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Invoke a named secondary step on a registered provider (e.g. email's
|
|
2686
|
+
* `sendCode` / `verifyCode`, or a custom provider's multi-step continuation).
|
|
2687
|
+
* Reuses the in-flight login `AbortController` when one exists so the step
|
|
2688
|
+
* stays cancellable via `cancelLogin()`; otherwise starts a fresh one. The
|
|
2689
|
+
* built-in email steps also have dedicated typed methods
|
|
2690
|
+
* ({@link sendEmailCode} / {@link verifyEmailCode}) — prefer those for email.
|
|
2691
|
+
*/
|
|
2692
|
+
providerAction(provider, action, payload) {
|
|
2693
|
+
if (!isClientRuntime) {
|
|
2694
|
+
warnServerSide("providerAction");
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const fn = this._providers.get(provider)?.actions?.[action];
|
|
2698
|
+
if (!fn) {
|
|
2699
|
+
throw new PollarFlowError(`Auth provider '${provider}' has no action '${action}'`);
|
|
2700
|
+
}
|
|
2701
|
+
const signal = this._loginController?.signal ?? this._newController().signal;
|
|
2702
|
+
fn(this._providerContext(signal), payload).catch((err) => this._handleFlowError(err));
|
|
2398
2703
|
}
|
|
2399
2704
|
// ─── Email OTP flow (3 steps) ─────────────────────────────────────────────
|
|
2400
2705
|
beginEmailLogin() {
|
|
@@ -2403,7 +2708,7 @@ var PollarClient = class {
|
|
|
2403
2708
|
return;
|
|
2404
2709
|
}
|
|
2405
2710
|
const controller = this._newController();
|
|
2406
|
-
initEmailSession(this.
|
|
2711
|
+
initEmailSession(this._providerContext(controller.signal)).catch((err) => this._handleFlowError(err));
|
|
2407
2712
|
}
|
|
2408
2713
|
sendEmailCode(email) {
|
|
2409
2714
|
if (!isClientRuntime) {
|
|
@@ -2415,7 +2720,7 @@ var PollarClient = class {
|
|
|
2415
2720
|
}
|
|
2416
2721
|
const { clientSessionId } = this._authState;
|
|
2417
2722
|
const signal = this._loginController.signal;
|
|
2418
|
-
sendEmailCode(email, clientSessionId, this.
|
|
2723
|
+
sendEmailCode(email, clientSessionId, this._providerContext(signal)).catch((err) => this._handleFlowError(err));
|
|
2419
2724
|
}
|
|
2420
2725
|
verifyEmailCode(code) {
|
|
2421
2726
|
if (!isClientRuntime) {
|
|
@@ -2430,7 +2735,7 @@ var PollarClient = class {
|
|
|
2430
2735
|
const clientSessionId = state.step === "entering_code" ? state.clientSessionId : state.clientSessionId;
|
|
2431
2736
|
const email = state.step === "entering_code" ? state.email : state.email ?? "";
|
|
2432
2737
|
const controller = this._newController();
|
|
2433
|
-
verifyAndAuthenticate(code, clientSessionId, email, this.
|
|
2738
|
+
verifyAndAuthenticate(code, clientSessionId, email, this._providerContext(controller.signal)).catch(
|
|
2434
2739
|
(err) => this._handleFlowError(err)
|
|
2435
2740
|
);
|
|
2436
2741
|
}
|
|
@@ -2795,6 +3100,29 @@ var PollarClient = class {
|
|
|
2795
3100
|
getWalletType() {
|
|
2796
3101
|
return this._walletAdapter?.type ?? null;
|
|
2797
3102
|
}
|
|
3103
|
+
/**
|
|
3104
|
+
* The authenticated user's wallet as a {@link WalletInfo} discriminated union,
|
|
3105
|
+
* or `null` when there's no session (or the session carries no address yet).
|
|
3106
|
+
*
|
|
3107
|
+
* `custody` strictly determines `provider` (the mapping is 1:1 and fixed at
|
|
3108
|
+
* account creation server-side): `external` reports the connected adapter id
|
|
3109
|
+
* (`getWalletType()`), `smart` is always `'passkey'`, and `internal` reports
|
|
3110
|
+
* the login method the backend recorded (`null` for pre-provider sessions).
|
|
3111
|
+
*/
|
|
3112
|
+
getWallet() {
|
|
3113
|
+
const w = this._session?.wallet;
|
|
3114
|
+
if (!w || !w.address) return null;
|
|
3115
|
+
switch (w.type) {
|
|
3116
|
+
case "external":
|
|
3117
|
+
return { custody: "external", address: w.address, provider: this._walletAdapter?.type ?? null };
|
|
3118
|
+
case "smart":
|
|
3119
|
+
return { custody: "smart", address: w.address, provider: "passkey" };
|
|
3120
|
+
case "internal":
|
|
3121
|
+
return { custody: "internal", address: w.address, provider: w.provider ?? null };
|
|
3122
|
+
default:
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
2798
3126
|
/**
|
|
2799
3127
|
* Signs the given unsigned XDR and returns the signed XDR.
|
|
2800
3128
|
*
|
|
@@ -2848,14 +3176,16 @@ var PollarClient = class {
|
|
|
2848
3176
|
});
|
|
2849
3177
|
return { status: "signed", signedXdr, submissionToken: idempotencyKey };
|
|
2850
3178
|
}
|
|
2851
|
-
const details = error
|
|
3179
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
2852
3180
|
this._setTransactionState({
|
|
2853
3181
|
step: "error",
|
|
2854
3182
|
phase: "signing",
|
|
2855
3183
|
...buildData && { buildData },
|
|
2856
|
-
...details && { details }
|
|
3184
|
+
...details && { details },
|
|
3185
|
+
...code && { code },
|
|
3186
|
+
...message && { message }
|
|
2857
3187
|
});
|
|
2858
|
-
return { status: "error", ...details && { details } };
|
|
3188
|
+
return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
|
|
2859
3189
|
} catch (err) {
|
|
2860
3190
|
const details = err instanceof Error ? err.message : void 0;
|
|
2861
3191
|
this._setTransactionState({
|
|
@@ -2867,6 +3197,54 @@ var PollarClient = class {
|
|
|
2867
3197
|
return { status: "error", ...details && { details } };
|
|
2868
3198
|
}
|
|
2869
3199
|
}
|
|
3200
|
+
/**
|
|
3201
|
+
* Sign a single Soroban authorization entry (`SorobanAuthorizationEntry`).
|
|
3202
|
+
*
|
|
3203
|
+
* Use this when a contract is the transaction source (e.g. it sponsors the
|
|
3204
|
+
* gas and swaps the fee out of the user's token) and only needs the user's
|
|
3205
|
+
* address-credentials authorization, not a full signed envelope. The signed
|
|
3206
|
+
* entry is returned as base64 XDR for the caller to compose into its tx.
|
|
3207
|
+
*
|
|
3208
|
+
* - External wallets (Freighter/Albedo) sign the entry via the provider.
|
|
3209
|
+
* - Custodial wallets are signed by the backend, which FIRST validates the
|
|
3210
|
+
* entry's invocation tree against the app's contract/function allowlist and
|
|
3211
|
+
* caps the validity window — entries touching a non-allowlisted contract or
|
|
3212
|
+
* function, or expiring too far ahead, are rejected.
|
|
3213
|
+
*
|
|
3214
|
+
* @param entryXdr base64 XDR of the unsigned `SorobanAuthorizationEntry`.
|
|
3215
|
+
* @param options.validUntilLedger absolute ledger the signature expires at
|
|
3216
|
+
* (computed from the network's latest ledger). Ignored on the external-wallet
|
|
3217
|
+
* path, where the provider sets its own expiration.
|
|
3218
|
+
*/
|
|
3219
|
+
async signAuthEntry(entryXdr, options) {
|
|
3220
|
+
if (this._walletAdapter) {
|
|
3221
|
+
const accountToSign = this._session?.wallet?.address;
|
|
3222
|
+
try {
|
|
3223
|
+
const { signedAuthEntry } = await this._walletAdapter.signAuthEntry(
|
|
3224
|
+
entryXdr,
|
|
3225
|
+
accountToSign ? { accountToSign } : void 0
|
|
3226
|
+
);
|
|
3227
|
+
return { status: "signed", signedAuthEntry };
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3230
|
+
return { status: "error", ...details && { details } };
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
const address = this._session?.wallet?.address ?? "";
|
|
3234
|
+
try {
|
|
3235
|
+
const { data, error } = await this._api.POST("/tx/sign-auth-entry", {
|
|
3236
|
+
body: { network: this.getNetwork(), address, entryXdr, validUntilLedger: options.validUntilLedger }
|
|
3237
|
+
});
|
|
3238
|
+
if (!error && data?.success && data.content?.signedAuthEntry) {
|
|
3239
|
+
return { status: "signed", signedAuthEntry: data.content.signedAuthEntry };
|
|
3240
|
+
}
|
|
3241
|
+
const details = error?.details;
|
|
3242
|
+
return { status: "error", ...details && { details } };
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
const details = err instanceof Error ? err.message : void 0;
|
|
3245
|
+
return { status: "error", ...details && { details } };
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
2870
3248
|
/**
|
|
2871
3249
|
* Submits a signed XDR via `/tx/submit` regardless of wallet type
|
|
2872
3250
|
* (custodial or external). Routing through sdk-api gives us:
|
|
@@ -2885,6 +3263,21 @@ var PollarClient = class {
|
|
|
2885
3263
|
* `submitted` on Horizon ack (pending), `success` on ledger confirmation,
|
|
2886
3264
|
* or `error[phase: 'submitting']` on failure.
|
|
2887
3265
|
*/
|
|
3266
|
+
/**
|
|
3267
|
+
* Normalize a backend API error into { details, code, message }. `code` is the
|
|
3268
|
+
* precise backend ErrorCode (e.g. `TX_FEE_LIMIT_EXCEEDED`) for programmatic
|
|
3269
|
+
* handling; `message` is a friendly string from the error catalog; `details`
|
|
3270
|
+
* is the raw diagnostic. Lets tx flows surface a typed reason instead of an
|
|
3271
|
+
* opaque details string.
|
|
3272
|
+
*/
|
|
3273
|
+
_resolveTxApiError(error) {
|
|
3274
|
+
const e = error;
|
|
3275
|
+
const details = e?.details ?? e?.message;
|
|
3276
|
+
const code = e?.code;
|
|
3277
|
+
if (!code) return details ? { details } : {};
|
|
3278
|
+
const { message } = resolveAuthError(code, details ?? code);
|
|
3279
|
+
return { code, message, ...details && { details } };
|
|
3280
|
+
}
|
|
2888
3281
|
async submitTx(signedXdr, opts) {
|
|
2889
3282
|
const buildData = this._currentBuildData();
|
|
2890
3283
|
const outcomeExtra = buildData ? { buildData } : {};
|
|
@@ -2922,14 +3315,22 @@ var PollarClient = class {
|
|
|
2922
3315
|
...resultCode && { details: resultCode, resultCode }
|
|
2923
3316
|
};
|
|
2924
3317
|
}
|
|
2925
|
-
const details = error
|
|
3318
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
2926
3319
|
this._setTransactionState({
|
|
2927
3320
|
step: "error",
|
|
2928
3321
|
phase: "submitting",
|
|
2929
3322
|
...buildData && { buildData },
|
|
2930
|
-
...details && { details }
|
|
3323
|
+
...details && { details },
|
|
3324
|
+
...code && { code },
|
|
3325
|
+
...message && { message }
|
|
2931
3326
|
});
|
|
2932
|
-
return {
|
|
3327
|
+
return {
|
|
3328
|
+
status: "error",
|
|
3329
|
+
...outcomeExtra,
|
|
3330
|
+
...details && { details },
|
|
3331
|
+
...code && { code },
|
|
3332
|
+
...message && { message }
|
|
3333
|
+
};
|
|
2933
3334
|
} catch (err) {
|
|
2934
3335
|
const details = err instanceof Error ? err.message : void 0;
|
|
2935
3336
|
this._setTransactionState({
|
|
@@ -3016,14 +3417,22 @@ var PollarClient = class {
|
|
|
3016
3417
|
...resultCode && { details: resultCode, resultCode }
|
|
3017
3418
|
};
|
|
3018
3419
|
}
|
|
3019
|
-
const details = error
|
|
3420
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
3020
3421
|
this._setTransactionState({
|
|
3021
3422
|
step: "error",
|
|
3022
3423
|
phase: "signing-submitting",
|
|
3023
3424
|
...buildData && { buildData },
|
|
3024
|
-
...details && { details }
|
|
3425
|
+
...details && { details },
|
|
3426
|
+
...code && { code },
|
|
3427
|
+
...message && { message }
|
|
3025
3428
|
});
|
|
3026
|
-
return {
|
|
3429
|
+
return {
|
|
3430
|
+
status: "error",
|
|
3431
|
+
...outcomeExtra,
|
|
3432
|
+
...details && { details },
|
|
3433
|
+
...code && { code },
|
|
3434
|
+
...message && { message }
|
|
3435
|
+
};
|
|
3027
3436
|
} catch (err) {
|
|
3028
3437
|
const details = err instanceof Error ? err.message : void 0;
|
|
3029
3438
|
this._setTransactionState({
|
|
@@ -3098,13 +3507,15 @@ var PollarClient = class {
|
|
|
3098
3507
|
});
|
|
3099
3508
|
return { status: "error", hash, ...resultCode && { details: resultCode, resultCode } };
|
|
3100
3509
|
}
|
|
3101
|
-
const details = error
|
|
3510
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
3102
3511
|
this._setTransactionState({
|
|
3103
3512
|
step: "error",
|
|
3104
3513
|
phase: "building-signing-submitting",
|
|
3105
|
-
...details && { details }
|
|
3514
|
+
...details && { details },
|
|
3515
|
+
...code && { code },
|
|
3516
|
+
...message && { message }
|
|
3106
3517
|
});
|
|
3107
|
-
return { status: "error", ...details && { details } };
|
|
3518
|
+
return { status: "error", ...details && { details }, ...code && { code }, ...message && { message } };
|
|
3108
3519
|
} catch (err) {
|
|
3109
3520
|
const details = err instanceof Error ? err.message : void 0;
|
|
3110
3521
|
this._setTransactionState({
|
|
@@ -3217,9 +3628,22 @@ var PollarClient = class {
|
|
|
3217
3628
|
});
|
|
3218
3629
|
return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
|
|
3219
3630
|
}
|
|
3220
|
-
const details = error
|
|
3221
|
-
this._setTransactionState({
|
|
3222
|
-
|
|
3631
|
+
const { details, code, message } = this._resolveTxApiError(error);
|
|
3632
|
+
this._setTransactionState({
|
|
3633
|
+
step: "error",
|
|
3634
|
+
phase: "submitting",
|
|
3635
|
+
buildData,
|
|
3636
|
+
...details && { details },
|
|
3637
|
+
...code && { code },
|
|
3638
|
+
...message && { message }
|
|
3639
|
+
});
|
|
3640
|
+
return {
|
|
3641
|
+
status: "error",
|
|
3642
|
+
...outcomeExtra,
|
|
3643
|
+
...details && { details },
|
|
3644
|
+
...code && { code },
|
|
3645
|
+
...message && { message }
|
|
3646
|
+
};
|
|
3223
3647
|
} catch (err) {
|
|
3224
3648
|
const details = err instanceof Error ? err.message : void 0;
|
|
3225
3649
|
this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
|
|
@@ -3297,11 +3721,67 @@ var PollarClient = class {
|
|
|
3297
3721
|
this._loginController = new AbortController();
|
|
3298
3722
|
return this._loginController;
|
|
3299
3723
|
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Build the {@link AuthProviderContext} facade for one login attempt. Wraps
|
|
3726
|
+
* the internal `FlowDeps` so providers get only the curated primitives —
|
|
3727
|
+
* `createSession`, `authenticate`, `exchangeExternalToken`, `startHostedOAuth`
|
|
3728
|
+
* — while storage / wallet-adapter / key-manager internals stay private. All
|
|
3729
|
+
* legs share the same `signal`, so `cancelLogin()` aborts the whole chain.
|
|
3730
|
+
*/
|
|
3731
|
+
_providerContext(signal) {
|
|
3732
|
+
const deps = this._flowDeps(signal);
|
|
3733
|
+
return {
|
|
3734
|
+
signal,
|
|
3735
|
+
api: this._api,
|
|
3736
|
+
basePath: this.basePath,
|
|
3737
|
+
apiKey: this.apiKey,
|
|
3738
|
+
logger: this._log,
|
|
3739
|
+
setAuthState: this._setAuthState.bind(this),
|
|
3740
|
+
createSession: () => createAuthSession(deps),
|
|
3741
|
+
authenticate: (clientSessionId) => authenticate(clientSessionId, deps),
|
|
3742
|
+
requestChallenge: (clientSessionId, walletAddress) => requestWalletChallenge(clientSessionId, walletAddress, deps),
|
|
3743
|
+
exchangeExternalToken: (clientSessionId, body) => this._exchangeExternalToken(clientSessionId, body, signal),
|
|
3744
|
+
startHostedOAuth: (provider) => loginOAuth(provider, {
|
|
3745
|
+
...deps,
|
|
3746
|
+
basePath: this.basePath,
|
|
3747
|
+
apiKey: this.apiKey,
|
|
3748
|
+
openAuthUrl: this._openAuthUrl,
|
|
3749
|
+
redirectUri: this._oauthRedirectUri
|
|
3750
|
+
})
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Generic external-provider exchange leg (`POST /auth/external`). Custom
|
|
3755
|
+
* providers call this (via the context) after their own SDK has authenticated
|
|
3756
|
+
* the user and the wallet has counter-signed the SEP-10 challenge
|
|
3757
|
+
* (`{ provider, walletAddress, signedChallengeXdr }`). On success the session
|
|
3758
|
+
* is marked READY server-side and the provider should then call
|
|
3759
|
+
* `ctx.authenticate(clientSessionId)`. Returns `false` (and sets an error
|
|
3760
|
+
* state) on failure.
|
|
3761
|
+
*/
|
|
3762
|
+
async _exchangeExternalToken(clientSessionId, body, signal) {
|
|
3763
|
+
const { data, error } = await this._api.POST("/auth/external", {
|
|
3764
|
+
body: { clientSessionId, ...body },
|
|
3765
|
+
signal
|
|
3766
|
+
});
|
|
3767
|
+
if (error || !data?.success) {
|
|
3768
|
+
this._log.error("[PollarClient] External provider authentication failed", { error });
|
|
3769
|
+
this._setAuthState({
|
|
3770
|
+
step: "error",
|
|
3771
|
+
previousStep: this._authState.step,
|
|
3772
|
+
message: "External provider authentication failed",
|
|
3773
|
+
errorCode: AUTH_ERROR_CODES.EXTERNAL_AUTH_FAILED
|
|
3774
|
+
});
|
|
3775
|
+
return false;
|
|
3776
|
+
}
|
|
3777
|
+
return true;
|
|
3778
|
+
}
|
|
3300
3779
|
_flowDeps(signal) {
|
|
3301
3780
|
return {
|
|
3302
3781
|
api: this._api,
|
|
3303
3782
|
logger: this._log,
|
|
3304
3783
|
basePath: this.basePath,
|
|
3784
|
+
networkPassphrase: this._networkPassphrase(),
|
|
3305
3785
|
// SSE status streaming works on web; React Native's `fetch` has no
|
|
3306
3786
|
// readable `response.body`, so those clients poll the non-streaming
|
|
3307
3787
|
// status endpoint instead. `isBrowser` is false in RN and SSR alike.
|
|
@@ -3433,6 +3913,7 @@ var PollarClient = class {
|
|
|
3433
3913
|
async _storeSession(session) {
|
|
3434
3914
|
this._log.info("[PollarClient] Session stored");
|
|
3435
3915
|
const w = session.wallet;
|
|
3916
|
+
const wireProvider = w.provider;
|
|
3436
3917
|
const persisted = {
|
|
3437
3918
|
clientSessionId: session.clientSessionId,
|
|
3438
3919
|
userId: session.userId ?? null,
|
|
@@ -3446,6 +3927,7 @@ var PollarClient = class {
|
|
|
3446
3927
|
// persisted session speak one vocabulary while the wire stays compatible.
|
|
3447
3928
|
wallet: {
|
|
3448
3929
|
type: w.type === "custodial" ? "internal" : w.type,
|
|
3930
|
+
...wireProvider ? { provider: wireProvider } : {},
|
|
3449
3931
|
address: w.address ?? w.publicKey ?? null,
|
|
3450
3932
|
...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
|
|
3451
3933
|
...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
|