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