@pollar/core 0.5.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -169,6 +169,250 @@ var require_index_min = __commonJS({
169
169
  }
170
170
  });
171
171
 
172
+ // src/keys/factory.ts
173
+ var _factory = null;
174
+ function _setDefaultKeyManagerFactory(factory) {
175
+ _factory = factory;
176
+ }
177
+ function defaultKeyManager(storage, apiKey) {
178
+ if (!_factory) {
179
+ throw new Error(
180
+ '[PollarClient] No default KeyManager factory registered. Did you import from "@pollar/core" via a non-standard path?'
181
+ );
182
+ }
183
+ return _factory(storage, apiKey);
184
+ }
185
+
186
+ // src/lib/sha256.ts
187
+ async function sha256(data) {
188
+ const buf = await crypto.subtle.digest("SHA-256", data);
189
+ return new Uint8Array(buf);
190
+ }
191
+
192
+ // src/lib/api-key-hash.ts
193
+ async function hashApiKey(apiKey) {
194
+ const digest = await sha256(new TextEncoder().encode(apiKey));
195
+ let hex = "";
196
+ for (let i = 0; i < 4; i++) hex += digest[i].toString(16).padStart(2, "0");
197
+ return hex;
198
+ }
199
+
200
+ // src/lib/base64url.ts
201
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
202
+ (() => {
203
+ const m = /* @__PURE__ */ new Map();
204
+ for (let i = 0; i < ALPHABET.length; i++) m.set(ALPHABET[i], i);
205
+ return m;
206
+ })();
207
+ function base64urlEncode(bytes) {
208
+ let result = "";
209
+ let i = 0;
210
+ for (; i + 2 < bytes.length; i += 3) {
211
+ const b1 = bytes[i];
212
+ const b2 = bytes[i + 1];
213
+ const b3 = bytes[i + 2];
214
+ result += ALPHABET[b1 >> 2];
215
+ result += ALPHABET[(b1 & 3) << 4 | b2 >> 4];
216
+ result += ALPHABET[(b2 & 15) << 2 | b3 >> 6];
217
+ result += ALPHABET[b3 & 63];
218
+ }
219
+ if (i < bytes.length) {
220
+ const b1 = bytes[i];
221
+ if (i + 1 === bytes.length) {
222
+ result += ALPHABET[b1 >> 2];
223
+ result += ALPHABET[(b1 & 3) << 4];
224
+ } else {
225
+ const b2 = bytes[i + 1];
226
+ result += ALPHABET[b1 >> 2];
227
+ result += ALPHABET[(b1 & 3) << 4 | b2 >> 4];
228
+ result += ALPHABET[(b2 & 15) << 2];
229
+ }
230
+ }
231
+ return result;
232
+ }
233
+ function base64urlEncodeString(s) {
234
+ return base64urlEncode(new TextEncoder().encode(s));
235
+ }
236
+
237
+ // src/keys/thumbprint.ts
238
+ async function computeJwkThumbprint(jwk) {
239
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256" || !jwk.x || !jwk.y) {
240
+ throw new Error("[PollarClient:thumbprint] Expected EC P-256 JWK with x and y");
241
+ }
242
+ const canonical = `{"crv":"${jwk.crv}","kty":"${jwk.kty}","x":"${jwk.x}","y":"${jwk.y}"}`;
243
+ const digest = await sha256(new TextEncoder().encode(canonical));
244
+ return base64urlEncode(digest);
245
+ }
246
+ function canonicalEcJwk(jwk) {
247
+ if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
248
+ throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
249
+ }
250
+ return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
251
+ }
252
+
253
+ // src/keys/web-crypto.ts
254
+ var DB_NAME = "pollar-keys";
255
+ var DB_VERSION = 1;
256
+ var STORE_NAME = "keys";
257
+ function openDb() {
258
+ return new Promise((resolve, reject) => {
259
+ if (typeof indexedDB === "undefined") {
260
+ reject(new Error("[PollarClient:keys] IndexedDB not available"));
261
+ return;
262
+ }
263
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
264
+ req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
265
+ req.onsuccess = () => resolve(req.result);
266
+ req.onupgradeneeded = () => {
267
+ const db = req.result;
268
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
269
+ db.createObjectStore(STORE_NAME);
270
+ }
271
+ };
272
+ });
273
+ }
274
+ function awaitTx(req) {
275
+ return new Promise((resolve, reject) => {
276
+ req.onsuccess = () => resolve(req.result);
277
+ req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
278
+ });
279
+ }
280
+ async function dbGet(key) {
281
+ const db = await openDb();
282
+ try {
283
+ const tx = db.transaction(STORE_NAME, "readonly");
284
+ const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
285
+ return result;
286
+ } finally {
287
+ db.close();
288
+ }
289
+ }
290
+ async function dbPut(key, value) {
291
+ const db = await openDb();
292
+ try {
293
+ const tx = db.transaction(STORE_NAME, "readwrite");
294
+ await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
295
+ } finally {
296
+ db.close();
297
+ }
298
+ }
299
+ async function dbDelete(key) {
300
+ const db = await openDb();
301
+ try {
302
+ const tx = db.transaction(STORE_NAME, "readwrite");
303
+ await awaitTx(tx.objectStore(STORE_NAME).delete(key));
304
+ } finally {
305
+ db.close();
306
+ }
307
+ }
308
+ function isCryptoKeyPair(v) {
309
+ if (typeof v !== "object" || v === null) return false;
310
+ const obj = v;
311
+ return obj.privateKey !== void 0 && obj.publicKey !== void 0;
312
+ }
313
+ var WebCryptoKeyManager = class {
314
+ constructor(apiKey) {
315
+ this.apiKeyHash = null;
316
+ this.keyPair = null;
317
+ this.publicJwk = null;
318
+ this.thumbprint = null;
319
+ /**
320
+ * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
321
+ * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
322
+ * failure so callers can retry, and cleared on `reset()`.
323
+ */
324
+ this._initPromise = null;
325
+ if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
326
+ throw new Error(
327
+ "[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost)."
328
+ );
329
+ }
330
+ this.apiKey = apiKey;
331
+ }
332
+ /**
333
+ * Idempotent and safe under concurrency. The first call kicks off the real
334
+ * init; subsequent (and concurrent) calls return the same in-flight promise.
335
+ * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
336
+ * the manager is self-healing if `init()` was never explicitly invoked.
337
+ */
338
+ async init() {
339
+ if (this.keyPair) return;
340
+ if (!this._initPromise) {
341
+ this._initPromise = this._doInit().catch((err) => {
342
+ console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
343
+ this._initPromise = null;
344
+ throw err;
345
+ });
346
+ }
347
+ return this._initPromise;
348
+ }
349
+ async _doInit() {
350
+ if (!this.apiKeyHash) {
351
+ this.apiKeyHash = await hashApiKey(this.apiKey);
352
+ }
353
+ let pair;
354
+ try {
355
+ pair = await dbGet(this.apiKeyHash);
356
+ if (pair && !isCryptoKeyPair(pair)) pair = void 0;
357
+ } catch {
358
+ pair = void 0;
359
+ }
360
+ if (!pair) {
361
+ pair = await globalThis.crypto.subtle.generateKey(
362
+ { name: "ECDSA", namedCurve: "P-256" },
363
+ // false → private key non-extractable; per W3C ECDSA spec the public
364
+ // key is always extractable regardless of this flag.
365
+ false,
366
+ ["sign", "verify"]
367
+ );
368
+ try {
369
+ await dbPut(this.apiKeyHash, pair);
370
+ } catch {
371
+ }
372
+ }
373
+ this.keyPair = pair;
374
+ const exported = await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey);
375
+ this.publicJwk = canonicalEcJwk(exported);
376
+ this.thumbprint = await computeJwkThumbprint(this.publicJwk);
377
+ }
378
+ async reset() {
379
+ try {
380
+ if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
381
+ } catch {
382
+ }
383
+ this.keyPair = null;
384
+ this.publicJwk = null;
385
+ this.thumbprint = null;
386
+ this._initPromise = null;
387
+ }
388
+ async getPublicJwk() {
389
+ if (!this.publicJwk) await this.init();
390
+ if (!this.publicJwk) {
391
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
392
+ }
393
+ return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
394
+ }
395
+ async getThumbprint() {
396
+ if (!this.thumbprint) await this.init();
397
+ if (!this.thumbprint) {
398
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
399
+ }
400
+ return this.thumbprint;
401
+ }
402
+ async sign(payload) {
403
+ if (!this.keyPair) await this.init();
404
+ if (!this.keyPair) {
405
+ throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
406
+ }
407
+ const sig = await globalThis.crypto.subtle.sign(
408
+ { name: "ECDSA", hash: "SHA-256" },
409
+ this.keyPair.privateKey,
410
+ payload
411
+ );
412
+ return new Uint8Array(sig);
413
+ }
414
+ };
415
+
172
416
  // ../../node_modules/openapi-fetch/dist/index.mjs
173
417
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
174
418
  var supportsRequestInitExt = () => {
@@ -717,6 +961,68 @@ async function pollRampTransaction(api, txId, { intervalMs = 5e3, timeoutMs = 6e
717
961
  throw new Error("Ramp transaction polling timed out");
718
962
  }
719
963
 
964
+ // src/dpop.ts
965
+ async function buildProof(args, keyManager) {
966
+ const jwk = await keyManager.getPublicJwk();
967
+ const header = {
968
+ typ: "dpop+jwt",
969
+ alg: "ES256",
970
+ jwk
971
+ };
972
+ const payload = {
973
+ jti: generateJti(),
974
+ htm: args.htm.toUpperCase(),
975
+ htu: normalizeHtu(args.htu),
976
+ iat: Math.floor(Date.now() / 1e3)
977
+ };
978
+ if (args.accessToken !== void 0 && args.accessToken !== "") {
979
+ payload.ath = base64urlEncode(await sha256(new TextEncoder().encode(args.accessToken)));
980
+ }
981
+ if (args.nonce !== void 0 && args.nonce !== "") {
982
+ payload.nonce = args.nonce;
983
+ }
984
+ const encodedHeader = base64urlEncodeString(JSON.stringify(header));
985
+ const encodedPayload = base64urlEncodeString(JSON.stringify(payload));
986
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
987
+ const signature = await keyManager.sign(new TextEncoder().encode(signingInput));
988
+ const encodedSignature = base64urlEncode(signature);
989
+ return `${signingInput}.${encodedSignature}`;
990
+ }
991
+ function normalizeHtu(rawUrl) {
992
+ let url;
993
+ try {
994
+ url = new URL(rawUrl);
995
+ } catch {
996
+ return rawUrl.split("#")[0].split("?")[0];
997
+ }
998
+ const scheme = url.protocol.toLowerCase();
999
+ const host = url.hostname.toLowerCase();
1000
+ let port = url.port;
1001
+ if (scheme === "https:" && port === "443" || scheme === "http:" && port === "80") {
1002
+ port = "";
1003
+ }
1004
+ const portPart = port ? `:${port}` : "";
1005
+ return `${scheme}//${host}${portPart}${url.pathname}`;
1006
+ }
1007
+ function generateJti() {
1008
+ const c = globalThis.crypto;
1009
+ if (c && typeof c.randomUUID === "function") {
1010
+ return c.randomUUID();
1011
+ }
1012
+ if (c && typeof c.getRandomValues === "function") {
1013
+ const bytes = new Uint8Array(16);
1014
+ c.getRandomValues(bytes);
1015
+ bytes[6] = bytes[6] & 15 | 64;
1016
+ bytes[8] = bytes[8] & 63 | 128;
1017
+ const hex = [];
1018
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
1019
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
1020
+ }
1021
+ throw new Error(
1022
+ "[PollarClient:dpop] No secure random source available (crypto.randomUUID / crypto.getRandomValues). DPoP requires a secure context (HTTPS) or, in React Native, the `react-native-get-random-values` polyfill."
1023
+ );
1024
+ }
1025
+
720
1026
  // src/stellar/StellarClient.ts
721
1027
  var HORIZON_URLS = {
722
1028
  mainnet: "https://horizon.stellar.org",
@@ -745,6 +1051,93 @@ var StellarClient = class {
745
1051
  }
746
1052
  };
747
1053
 
1054
+ // src/storage/web.ts
1055
+ var LOG_PREFIX = "[PollarClient:storage]";
1056
+ function createMemoryAdapter() {
1057
+ const store = /* @__PURE__ */ new Map();
1058
+ return {
1059
+ async get(key) {
1060
+ const value = store.get(key);
1061
+ return value === void 0 ? null : value;
1062
+ },
1063
+ async set(key, value) {
1064
+ store.set(key, value);
1065
+ },
1066
+ async remove(key) {
1067
+ store.delete(key);
1068
+ }
1069
+ };
1070
+ }
1071
+ function createLocalStorageAdapter(options = {}) {
1072
+ const fallback = createMemoryAdapter();
1073
+ let degraded = false;
1074
+ function degrade(reason, error) {
1075
+ if (degraded) return;
1076
+ degraded = true;
1077
+ console.warn(`${LOG_PREFIX} localStorage unavailable (${reason}); degrading to in-memory storage`);
1078
+ options.onDegrade?.(reason, error);
1079
+ }
1080
+ return {
1081
+ async get(key) {
1082
+ if (degraded) return fallback.get(key);
1083
+ try {
1084
+ return globalThis.localStorage.getItem(key);
1085
+ } catch (error) {
1086
+ degrade("read-failed", error);
1087
+ return fallback.get(key);
1088
+ }
1089
+ },
1090
+ async set(key, value) {
1091
+ if (degraded) return fallback.set(key, value);
1092
+ try {
1093
+ globalThis.localStorage.setItem(key, value);
1094
+ } catch (error) {
1095
+ const reason = isQuotaError(error) ? "quota-exceeded" : "write-failed";
1096
+ degrade(reason, error);
1097
+ await fallback.set(key, value);
1098
+ }
1099
+ },
1100
+ async remove(key) {
1101
+ if (degraded) return fallback.remove(key);
1102
+ try {
1103
+ globalThis.localStorage.removeItem(key);
1104
+ } catch (error) {
1105
+ degrade("remove-failed", error);
1106
+ await fallback.remove(key);
1107
+ }
1108
+ }
1109
+ };
1110
+ }
1111
+ function isQuotaError(error) {
1112
+ if (typeof error !== "object" || error === null) return false;
1113
+ const name = error.name;
1114
+ const code = error.code;
1115
+ return name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED" || code === 22 || code === 1014;
1116
+ }
1117
+
1118
+ // src/storage/autodetect.ts
1119
+ var PROBE_KEY = "__pollar_storage_probe__";
1120
+ function defaultStorage(options = {}) {
1121
+ if (typeof globalThis === "undefined" || typeof globalThis.localStorage === "undefined") {
1122
+ options.onDegrade?.("unavailable");
1123
+ return createMemoryAdapter();
1124
+ }
1125
+ try {
1126
+ const probeValue = String(Date.now());
1127
+ globalThis.localStorage.setItem(PROBE_KEY, probeValue);
1128
+ const read = globalThis.localStorage.getItem(PROBE_KEY);
1129
+ globalThis.localStorage.removeItem(PROBE_KEY);
1130
+ if (read !== probeValue) {
1131
+ options.onDegrade?.("probe-failed");
1132
+ return createMemoryAdapter();
1133
+ }
1134
+ } catch (error) {
1135
+ options.onDegrade?.("probe-failed", error);
1136
+ return createMemoryAdapter();
1137
+ }
1138
+ return createLocalStorageAdapter(options);
1139
+ }
1140
+
748
1141
  // src/types.ts
749
1142
  var AUTH_ERROR_CODES = {
750
1143
  SESSION_CREATE_FAILED: "SESSION_CREATE_FAILED",
@@ -933,24 +1326,42 @@ var AlbedoAdapter = class {
933
1326
  };
934
1327
 
935
1328
  // src/client/session.ts
936
- var STORAGE_KEY = "pollar:session";
937
- var WALLET_TYPE_KEY = "pollar:walletType";
1329
+ var SESSION_SUFFIX = ":session";
1330
+ var WALLET_TYPE_SUFFIX = ":walletType";
1331
+ function sessionStorageKey(apiKeyHash) {
1332
+ return `pollar:${apiKeyHash}${SESSION_SUFFIX}`;
1333
+ }
1334
+ function walletTypeStorageKey(apiKeyHash) {
1335
+ return `pollar:${apiKeyHash}${WALLET_TYPE_SUFFIX}`;
1336
+ }
1337
+ var MAX_ACCESS_TOKEN = 4096;
1338
+ var MAX_REFRESH_TOKEN = 4096;
1339
+ var MAX_USER_ID = 64;
1340
+ var MAX_CLIENT_SESSION_ID = 64;
1341
+ var MAX_STATUS = 64;
1342
+ var MAX_WALLET_PUBLIC_KEY = 128;
1343
+ var MAX_WALLET_TYPE = 32;
1344
+ function isBoundedString(v, max, allowEmpty = false) {
1345
+ if (typeof v !== "string") return false;
1346
+ if (!allowEmpty && v.length === 0) return false;
1347
+ return v.length <= max;
1348
+ }
938
1349
  function isValidSession(value) {
939
1350
  if (typeof value !== "object" || value === null) {
940
1351
  console.warn("[PollarClient:session] Invalid session \u2014 value is not an object");
941
1352
  return false;
942
1353
  }
943
1354
  const s = value;
944
- if (typeof s["clientSessionId"] !== "string" || s["clientSessionId"] === "") {
945
- console.warn("[PollarClient:session] Invalid session \u2014 clientSessionId missing or empty");
1355
+ if (!isBoundedString(s["clientSessionId"], MAX_CLIENT_SESSION_ID)) {
1356
+ console.warn("[PollarClient:session] Invalid session \u2014 clientSessionId missing/empty/too long");
946
1357
  return false;
947
1358
  }
948
- if (s["userId"] !== null && typeof s["userId"] !== "string") {
949
- console.warn("[PollarClient:session] Invalid session \u2014 userId must be string or null, got:", typeof s["userId"]);
1359
+ if (s["userId"] !== null && !isBoundedString(s["userId"], MAX_USER_ID)) {
1360
+ console.warn("[PollarClient:session] Invalid session \u2014 userId must be string|null");
950
1361
  return false;
951
1362
  }
952
- if (typeof s["status"] !== "string") {
953
- console.warn("[PollarClient:session] Invalid session \u2014 status must be string, got:", typeof s["status"]);
1363
+ if (!isBoundedString(s["status"], MAX_STATUS)) {
1364
+ console.warn("[PollarClient:session] Invalid session \u2014 status must be string");
954
1365
  return false;
955
1366
  }
956
1367
  const token = s["token"];
@@ -959,12 +1370,12 @@ function isValidSession(value) {
959
1370
  return false;
960
1371
  }
961
1372
  const t = token;
962
- if (typeof t["accessToken"] !== "string" || t["accessToken"] === "") {
963
- console.warn("[PollarClient:session] Invalid session \u2014 token.accessToken missing or empty");
1373
+ if (!isBoundedString(t["accessToken"], MAX_ACCESS_TOKEN)) {
1374
+ console.warn("[PollarClient:session] Invalid session \u2014 token.accessToken missing/empty/too long");
964
1375
  return false;
965
1376
  }
966
- if (typeof t["refreshToken"] !== "string" || t["refreshToken"] === "") {
967
- console.warn("[PollarClient:session] Invalid session \u2014 token.refreshToken missing or empty");
1377
+ if (!isBoundedString(t["refreshToken"], MAX_REFRESH_TOKEN)) {
1378
+ console.warn("[PollarClient:session] Invalid session \u2014 token.refreshToken missing/empty/too long");
968
1379
  return false;
969
1380
  }
970
1381
  if (typeof t["expiresAt"] !== "number" || !Number.isFinite(t["expiresAt"])) {
@@ -977,12 +1388,12 @@ function isValidSession(value) {
977
1388
  return false;
978
1389
  }
979
1390
  const u = user;
980
- if (u["id"] !== void 0 && typeof u["id"] !== "string") {
981
- console.warn("[PollarClient:session] Invalid session \u2014 user.id must be string if present, got:", typeof u["id"]);
1391
+ if (u["id"] !== void 0 && !isBoundedString(u["id"], MAX_USER_ID)) {
1392
+ console.warn("[PollarClient:session] Invalid session \u2014 user.id must be string if present");
982
1393
  return false;
983
1394
  }
984
1395
  if (typeof u["ready"] !== "boolean") {
985
- console.warn("[PollarClient:session] Invalid session \u2014 user.ready must be boolean, got:", typeof u["ready"]);
1396
+ console.warn("[PollarClient:session] Invalid session \u2014 user.ready must be boolean");
986
1397
  return false;
987
1398
  }
988
1399
  const wallet = s["wallet"];
@@ -991,11 +1402,8 @@ function isValidSession(value) {
991
1402
  return false;
992
1403
  }
993
1404
  const w = wallet;
994
- if (w["publicKey"] !== null && typeof w["publicKey"] !== "string") {
995
- console.warn(
996
- "[PollarClient:session] Invalid session \u2014 wallet.publicKey must be string or null, got:",
997
- typeof w["publicKey"]
998
- );
1405
+ if (w["publicKey"] !== null && !isBoundedString(w["publicKey"], MAX_WALLET_PUBLIC_KEY)) {
1406
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.publicKey must be string|null");
999
1407
  return false;
1000
1408
  }
1001
1409
  if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
@@ -1006,78 +1414,43 @@ function isValidSession(value) {
1006
1414
  console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
1007
1415
  return false;
1008
1416
  }
1009
- const data = s["data"];
1010
- if (typeof data !== "object" || data === null) {
1011
- console.warn("[PollarClient:session] Invalid session \u2014 data missing or not an object");
1012
- return false;
1013
- }
1014
- const d = data;
1015
- for (const field of ["mail", "first_name", "last_name", "avatar"]) {
1016
- if (typeof d[field] !== "string") {
1017
- console.warn(`[PollarClient:session] Invalid session \u2014 data.${field} must be string, got:`, typeof d[field]);
1018
- return false;
1019
- }
1020
- }
1021
- const providers = d["providers"];
1022
- if (typeof providers !== "object" || providers === null) {
1023
- console.warn("[PollarClient:session] Invalid session \u2014 data.providers missing or not an object");
1024
- return false;
1025
- }
1026
- const p = providers;
1027
- const providerInnerField = { email: "address", google: "id", github: "id", wallet: "address" };
1028
- for (const [field, innerField] of Object.entries(providerInnerField)) {
1029
- const v = p[field];
1030
- if (v === null) continue;
1031
- if (typeof v !== "object") {
1032
- console.warn(`[PollarClient:session] Invalid session \u2014 data.providers.${field} must be object or null, got:`, typeof v);
1033
- return false;
1034
- }
1035
- const vObj = v;
1036
- if (typeof vObj[innerField] !== "string" || vObj[innerField] === "") {
1037
- console.warn(`[PollarClient:session] Invalid session \u2014 data.providers.${field}.${innerField} must be a non-empty string`);
1038
- return false;
1039
- }
1040
- }
1041
1417
  return true;
1042
1418
  }
1043
- function readStorage() {
1044
- const raw = localStorage.getItem(STORAGE_KEY);
1045
- if (!raw) {
1046
- return null;
1047
- }
1419
+ async function readStorage(storage, apiKeyHash) {
1420
+ const raw = await storage.get(sessionStorageKey(apiKeyHash));
1421
+ if (!raw) return null;
1048
1422
  try {
1049
1423
  const session = JSON.parse(raw);
1050
1424
  if (!isValidSession(session)) {
1051
- localStorage.removeItem(STORAGE_KEY);
1425
+ await storage.remove(sessionStorageKey(apiKeyHash));
1052
1426
  console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
1053
1427
  return null;
1054
1428
  }
1055
1429
  if (session.token.expiresAt * 1e3 < Date.now()) {
1056
- localStorage.removeItem(STORAGE_KEY);
1057
- console.warn("[PollarClient:session] Session token has expired \u2014 clearing storage");
1058
- return null;
1430
+ return session;
1059
1431
  }
1060
1432
  return session;
1061
1433
  } catch (error) {
1062
1434
  console.error("[PollarClient:session] Failed to parse session from storage", error);
1063
- localStorage.removeItem(STORAGE_KEY);
1435
+ await storage.remove(sessionStorageKey(apiKeyHash));
1064
1436
  return null;
1065
1437
  }
1066
1438
  }
1067
- function writeStorage(session) {
1068
- localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
1069
- console.info("[PollarClient:session] Session written to storage");
1439
+ async function writeStorage(storage, apiKeyHash, session) {
1440
+ await storage.set(sessionStorageKey(apiKeyHash), JSON.stringify(session));
1070
1441
  }
1071
- function removeStorage() {
1072
- localStorage.removeItem(STORAGE_KEY);
1073
- localStorage.removeItem(WALLET_TYPE_KEY);
1074
- console.info("[PollarClient:session] Session removed from storage");
1442
+ async function removeStorage(storage, apiKeyHash) {
1443
+ await storage.remove(sessionStorageKey(apiKeyHash));
1444
+ await storage.remove(walletTypeStorageKey(apiKeyHash));
1075
1445
  }
1076
- function writeWalletType(type) {
1077
- localStorage.setItem(WALLET_TYPE_KEY, type);
1446
+ async function writeWalletType(storage, apiKeyHash, type) {
1447
+ if (type.length > MAX_WALLET_TYPE) {
1448
+ throw new Error(`[PollarClient:session] walletType too long: ${type.length} > ${MAX_WALLET_TYPE}`);
1449
+ }
1450
+ await storage.set(walletTypeStorageKey(apiKeyHash), type);
1078
1451
  }
1079
- function readWalletType() {
1080
- return localStorage.getItem(WALLET_TYPE_KEY);
1452
+ async function readWalletType(storage, apiKeyHash) {
1453
+ return storage.get(walletTypeStorageKey(apiKeyHash));
1081
1454
  }
1082
1455
 
1083
1456
  // src/client/stream.ts
@@ -1094,7 +1467,14 @@ function abortableDelay(ms, signal) {
1094
1467
  );
1095
1468
  });
1096
1469
  }
1470
+ var MAX_BACKOFF_MS = 5e3;
1097
1471
  async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200, signal) {
1472
+ let backoff = retryDelayMs;
1473
+ const sleep = async (ms) => {
1474
+ if (ms <= 0) return;
1475
+ if (signal) await abortableDelay(ms, signal);
1476
+ else await new Promise((r) => setTimeout(r, ms));
1477
+ };
1098
1478
  while (true) {
1099
1479
  signal?.throwIfAborted();
1100
1480
  let data, error;
@@ -1109,13 +1489,14 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1109
1489
  console.warn(e);
1110
1490
  }
1111
1491
  if (error || !data) {
1112
- if (signal) await abortableDelay(retryDelayMs, signal);
1113
- else await new Promise((r) => setTimeout(r, retryDelayMs));
1492
+ await sleep(backoff);
1493
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1114
1494
  continue;
1115
1495
  }
1116
1496
  const reader = data.getReader();
1117
1497
  const decoder = new TextDecoder();
1118
1498
  let streamDone = false;
1499
+ let sawAnyChunk = false;
1119
1500
  try {
1120
1501
  while (true) {
1121
1502
  signal?.throwIfAborted();
@@ -1124,6 +1505,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1124
1505
  streamDone = true;
1125
1506
  break;
1126
1507
  }
1508
+ sawAnyChunk = true;
1127
1509
  const chunk = decoder.decode(value);
1128
1510
  for (const message of chunk.split("\n\n").filter(Boolean)) {
1129
1511
  const dataLine = message.split("\n").find((l) => l.startsWith("data:"));
@@ -1143,11 +1525,10 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1143
1525
  } finally {
1144
1526
  reader.releaseLock();
1145
1527
  }
1146
- const delay = streamDone ? retryDelayMs : 0;
1147
- if (delay) {
1148
- if (signal) await abortableDelay(delay, signal);
1149
- else await new Promise((r) => setTimeout(r, delay));
1150
- }
1528
+ if (sawAnyChunk) backoff = retryDelayMs;
1529
+ else backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
1530
+ const delay = streamDone ? backoff : 0;
1531
+ if (delay) await sleep(delay);
1151
1532
  }
1152
1533
  }
1153
1534
 
@@ -1156,8 +1537,13 @@ async function authenticate(clientSessionId, deps, expectedWallet) {
1156
1537
  const { api, signal, setAuthState, storeSession, clearSession } = deps;
1157
1538
  setAuthState({ step: "authenticating" });
1158
1539
  await streamUntilFound(api, clientSessionId, (data2) => data2?.status === "READY", 200, signal);
1540
+ const dpopJwk = await deps.getPublicJwk();
1159
1541
  const { data, error } = await api.POST("/auth/login", {
1160
- body: { clientSessionId },
1542
+ body: {
1543
+ clientSessionId,
1544
+ dpopJwk,
1545
+ ...deps.deviceLabel ? { deviceLabel: deps.deviceLabel } : {}
1546
+ },
1161
1547
  signal
1162
1548
  });
1163
1549
  if (data?.code === "SDK_LOGIN_SUCCESS" && isValidSession(data?.content)) {
@@ -1267,9 +1653,17 @@ async function verifyAndAuthenticate(code, clientSessionId, email, deps) {
1267
1653
  }
1268
1654
 
1269
1655
  // src/client/auth/oauthFlow.ts
1656
+ function severOpener(popup) {
1657
+ if (!popup) return;
1658
+ try {
1659
+ popup.opener = null;
1660
+ } catch {
1661
+ }
1662
+ }
1270
1663
  async function loginOAuth(provider, deps) {
1271
1664
  const { setAuthState, basePath, apiKey } = deps;
1272
1665
  const popup = window.open("about:blank", "_blank");
1666
+ severOpener(popup);
1273
1667
  const clientSessionId = await createAuthSession(deps);
1274
1668
  if (!clientSessionId) {
1275
1669
  popup?.close();
@@ -1282,8 +1676,9 @@ async function loginOAuth(provider, deps) {
1282
1676
  url.searchParams.set("redirect_uri", window.location.origin);
1283
1677
  if (popup) {
1284
1678
  popup.location.href = url.toString();
1679
+ severOpener(popup);
1285
1680
  } else {
1286
- window.open(url.toString(), "_blank");
1681
+ window.open(url.toString(), "_blank", "noopener,noreferrer");
1287
1682
  }
1288
1683
  await authenticate(clientSessionId, deps);
1289
1684
  }
@@ -1308,7 +1703,7 @@ async function loginWallet(type, deps) {
1308
1703
  let connectedWallet;
1309
1704
  try {
1310
1705
  setAuthState({ step: "connecting_wallet", walletType: type });
1311
- const adapter = type === "freighter" /* FREIGHTER */ ? new FreighterAdapter() : new AlbedoAdapter();
1706
+ const adapter = await deps.resolveWalletAdapter(type);
1312
1707
  const available = await withSignal(adapter.isAvailable(), signal);
1313
1708
  if (!available) {
1314
1709
  setAuthState({ step: "wallet_not_installed", walletType: type });
@@ -1345,6 +1740,7 @@ async function loginWallet(type, deps) {
1345
1740
 
1346
1741
  // src/client/client.ts
1347
1742
  var isBrowser = typeof window !== "undefined" && typeof localStorage !== "undefined";
1743
+ var RETRIED_HEADER = "X-Pollar-Retried";
1348
1744
  function warnServerSide(method) {
1349
1745
  console.warn(
1350
1746
  `[PollarClient] ${method}() called server-side \u2014 browser APIs unavailable. Use PollarClient only in Client Components.`
@@ -1352,7 +1748,19 @@ function warnServerSide(method) {
1352
1748
  }
1353
1749
  var PollarClient = class {
1354
1750
  constructor(config) {
1751
+ /**
1752
+ * Per-API-key storage namespace. Computed asynchronously inside
1753
+ * `_initialize()` because SHA-256 lives behind `crypto.subtle.digest`.
1754
+ * Accessing `apiKeyHash` before `await client.ready()` throws.
1755
+ */
1756
+ this._apiKeyHash = null;
1355
1757
  this._session = null;
1758
+ this._profile = null;
1759
+ /** Last `DPoP-Nonce` we saw from a server response. Carried into the next proof. */
1760
+ this._dpopNonce = null;
1761
+ /** Singleton in-flight refresh — concurrent 401s coalesce into one /auth/refresh call. */
1762
+ this._refreshPromise = null;
1763
+ this._storageEventHandler = null;
1356
1764
  this._transactionState = null;
1357
1765
  this._transactionStateListeners = /* @__PURE__ */ new Set();
1358
1766
  this._txHistoryState = { step: "idle" };
@@ -1368,33 +1776,197 @@ var PollarClient = class {
1368
1776
  this.apiKey = config.apiKey;
1369
1777
  this.id = crypto.randomUUID();
1370
1778
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
1779
+ this._storage = config.storage ?? defaultStorage(config.onStorageDegrade ? { onDegrade: config.onStorageDegrade } : void 0);
1780
+ this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
1781
+ this._walletAdapterResolver = config.walletAdapter ?? null;
1782
+ this._deviceLabel = config.deviceLabel;
1371
1783
  this._api = createApiClient(this.basePath);
1784
+ this._wireMiddlewares();
1785
+ this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1786
+ if (!isBrowser) {
1787
+ warnServerSide("constructor");
1788
+ this._initialized = Promise.resolve();
1789
+ return;
1790
+ }
1791
+ console.info(`[PollarClient] Initialized \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`);
1792
+ this._initialized = this._initialize();
1793
+ }
1794
+ /**
1795
+ * Short SHA-256-derived namespace for this client's persisted state.
1796
+ * Available after `await client.ready()` (or any awaited method); throws
1797
+ * if read before initialization completes.
1798
+ */
1799
+ get apiKeyHash() {
1800
+ if (this._apiKeyHash === null) {
1801
+ throw new Error("[PollarClient] apiKeyHash is not available until client.ready() resolves");
1802
+ }
1803
+ return this._apiKeyHash;
1804
+ }
1805
+ /** Awaitable handle for the initial keypair + session restore. */
1806
+ ready() {
1807
+ return this._initialized;
1808
+ }
1809
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
1810
+ async _initialize() {
1811
+ this._apiKeyHash = await hashApiKey(this.apiKey);
1812
+ if (typeof window !== "undefined") {
1813
+ const sessionKey = sessionStorageKey(this._apiKeyHash);
1814
+ const handler = (e) => {
1815
+ if (e.key === sessionKey) {
1816
+ this._restoreSession().catch((err) => console.error("[PollarClient] Cross-tab restore failed", err));
1817
+ }
1818
+ };
1819
+ window.addEventListener("storage", handler);
1820
+ this._storageEventHandler = handler;
1821
+ }
1822
+ try {
1823
+ await this._keyManager.init();
1824
+ } catch (err) {
1825
+ console.warn("[PollarClient] KeyManager init failed; DPoP unavailable for this session", err);
1826
+ }
1827
+ await this._restoreSession();
1828
+ }
1829
+ /** Detach the cross-tab storage listener and abort any in-flight login. */
1830
+ destroy() {
1831
+ if (this._storageEventHandler && typeof window !== "undefined") {
1832
+ window.removeEventListener("storage", this._storageEventHandler);
1833
+ this._storageEventHandler = null;
1834
+ }
1835
+ this._loginController?.abort();
1836
+ this._loginController = null;
1837
+ }
1838
+ // ─── Middlewares (DPoP + auto-refresh) ────────────────────────────────────
1839
+ _wireMiddlewares() {
1372
1840
  const self = this;
1373
1841
  this._api.use({
1374
- onRequest({ request }) {
1375
- request.headers.set("x-pollar-api-key", config.apiKey);
1842
+ onRequest: async ({ request }) => {
1843
+ request.headers.set("x-pollar-api-key", self.apiKey);
1844
+ await self._initialized;
1845
+ const isRefresh = request.url.includes("/auth/refresh");
1846
+ if (!isRefresh && self._refreshPromise) await self._refreshPromise;
1847
+ if (isRefresh) {
1848
+ const refreshProof = await self._buildProofForRequest(request, void 0);
1849
+ if (refreshProof) request.headers.set("DPoP", refreshProof);
1850
+ return request;
1851
+ }
1376
1852
  const accessToken = self._session?.token?.accessToken;
1377
- if (accessToken) {
1853
+ if (!accessToken) return request;
1854
+ const proof = await self._buildProofForRequest(request, accessToken);
1855
+ if (proof) {
1856
+ request.headers.set("Authorization", `DPoP ${accessToken}`);
1857
+ request.headers.set("DPoP", proof);
1858
+ } else {
1378
1859
  request.headers.set("Authorization", `Bearer ${accessToken}`);
1379
1860
  }
1380
1861
  return request;
1862
+ },
1863
+ onResponse: async ({ request, response }) => {
1864
+ const newNonce = response.headers.get("DPoP-Nonce");
1865
+ if (newNonce) self._dpopNonce = newNonce;
1866
+ if (response.status !== 401) return response;
1867
+ if (request.headers.get(RETRIED_HEADER)) return response;
1868
+ if (request.url.includes("/auth/refresh")) return response;
1869
+ const wwwAuth = response.headers.get("WWW-Authenticate") ?? "";
1870
+ const isNonceChallenge = wwwAuth.includes("use_dpop_nonce");
1871
+ if (!isNonceChallenge) {
1872
+ try {
1873
+ await self.refresh();
1874
+ } catch {
1875
+ return response;
1876
+ }
1877
+ }
1878
+ return self._retryRequest(request);
1381
1879
  }
1382
1880
  });
1383
- this._networkState = { step: "connected", network: config.stellarNetwork ?? "testnet" };
1384
- if (!isBrowser) {
1385
- warnServerSide("constructor");
1386
- this._session = null;
1387
- return;
1881
+ }
1882
+ async _buildProofForRequest(request, accessToken) {
1883
+ try {
1884
+ const htu = request.url.split("?")[0].split("#")[0];
1885
+ return await buildProof(
1886
+ {
1887
+ htm: request.method,
1888
+ htu,
1889
+ ...accessToken ? { accessToken } : {},
1890
+ ...this._dpopNonce !== null ? { nonce: this._dpopNonce } : {}
1891
+ },
1892
+ this._keyManager
1893
+ );
1894
+ } catch (err) {
1895
+ console.warn("[PollarClient] DPoP proof build failed", err);
1896
+ return null;
1388
1897
  }
1389
- console.info(`[PollarClient] Initialized \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`);
1390
- this._restoreSession();
1391
- window.addEventListener("storage", (e) => {
1392
- if (e.key === STORAGE_KEY) {
1393
- const prev = this._session;
1394
- console.info(`[PollarClient] Storage event \u2014 session ${this._session ? "updated" : prev ? "cleared" : "unchanged"}`);
1395
- this._restoreSession();
1898
+ }
1899
+ async _retryRequest(originalRequest) {
1900
+ const clone = originalRequest.clone();
1901
+ clone.headers.set(RETRIED_HEADER, "1");
1902
+ const accessToken = this._session?.token?.accessToken;
1903
+ if (accessToken) {
1904
+ const proof = await this._buildProofForRequest(clone, accessToken);
1905
+ if (proof) {
1906
+ clone.headers.set("Authorization", `DPoP ${accessToken}`);
1907
+ clone.headers.set("DPoP", proof);
1908
+ } else {
1909
+ clone.headers.set("Authorization", `Bearer ${accessToken}`);
1396
1910
  }
1911
+ }
1912
+ return fetch(clone);
1913
+ }
1914
+ // ─── Refresh (race-safe singleton) ───────────────────────────────────────
1915
+ /**
1916
+ * Coalesce concurrent refresh attempts. The first caller does the work;
1917
+ * everyone else awaits the same promise and sees the new tokens.
1918
+ */
1919
+ refresh() {
1920
+ if (this._refreshPromise) return this._refreshPromise;
1921
+ this._refreshPromise = this._doRefresh().finally(() => {
1922
+ this._refreshPromise = null;
1397
1923
  });
1924
+ return this._refreshPromise;
1925
+ }
1926
+ async _doRefresh() {
1927
+ const refreshToken = this._session?.token?.refreshToken;
1928
+ if (!refreshToken) {
1929
+ console.warn("[PollarClient] Refresh skipped: no refresh token in session");
1930
+ await this._clearSession();
1931
+ throw new Error("No refresh token available");
1932
+ }
1933
+ let data;
1934
+ let error;
1935
+ try {
1936
+ const response = await this._api.POST("/auth/refresh", { body: { refreshToken } });
1937
+ data = response.data;
1938
+ error = response.error;
1939
+ } catch (err) {
1940
+ console.error("[PollarClient] /auth/refresh request threw", err);
1941
+ await this._clearSession();
1942
+ throw err;
1943
+ }
1944
+ if (error || !data) {
1945
+ console.warn("[PollarClient] /auth/refresh returned error", { error });
1946
+ await this._clearSession();
1947
+ throw new Error("Refresh failed");
1948
+ }
1949
+ const successData = data;
1950
+ if (!successData.success || !successData.content?.token) {
1951
+ console.warn("[PollarClient] /auth/refresh response malformed", successData);
1952
+ await this._clearSession();
1953
+ throw new Error("Refresh response malformed");
1954
+ }
1955
+ const newToken = successData.content.token;
1956
+ if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
1957
+ console.warn("[PollarClient] /auth/refresh token shape invalid", newToken);
1958
+ await this._clearSession();
1959
+ throw new Error("Refresh response token shape invalid");
1960
+ }
1961
+ if (this._session) {
1962
+ try {
1963
+ this._session = { ...this._session, token: newToken };
1964
+ await writeStorage(this._storage, this.apiKeyHash, this._session);
1965
+ console.info("[PollarClient] Tokens refreshed");
1966
+ } catch (err) {
1967
+ console.error("[PollarClient] Failed to persist refreshed session", err);
1968
+ }
1969
+ }
1398
1970
  }
1399
1971
  // ─── Auth state ──────────────────────────────────────────────────────────────
1400
1972
  getAuthState() {
@@ -1405,6 +1977,10 @@ var PollarClient = class {
1405
1977
  cb(this._authState);
1406
1978
  return () => this._authStateListeners.delete(cb);
1407
1979
  }
1980
+ /** PII (email, names, avatar, providers). Held in memory only — never persisted. */
1981
+ getUserProfile() {
1982
+ return this._profile;
1983
+ }
1408
1984
  // ─── Login (unified entry point) ─────────────────────────────────────────
1409
1985
  login(options) {
1410
1986
  if (!isBrowser) {
@@ -1486,13 +2062,80 @@ var PollarClient = class {
1486
2062
  this._setAuthState({ step: "idle" });
1487
2063
  }
1488
2064
  // ─── Logout ───────────────────────────────────────────────────────────────
1489
- logout() {
2065
+ /**
2066
+ * Revoke the current session server-side, then clear local storage.
2067
+ *
2068
+ * Server revocation is best-effort: if the POST fails (offline, server
2069
+ * down), local state is wiped regardless. The orphan refresh token then
2070
+ * remains unused until its natural expiry. The in-flight access token
2071
+ * stays valid until its own TTL elapses (≤10 min for DPoP-bound tokens).
2072
+ *
2073
+ * Pass `everywhere: true` to revoke every active session for this user
2074
+ * across all devices.
2075
+ */
2076
+ async logout(options = {}) {
1490
2077
  if (!isBrowser) {
1491
2078
  warnServerSide("logout");
1492
2079
  return;
1493
2080
  }
1494
- console.info("[PollarClient] Logout requested");
1495
- this._clearSession();
2081
+ console.info("[PollarClient] Logout requested", { everywhere: !!options.everywhere });
2082
+ if (this._session?.token?.accessToken) {
2083
+ try {
2084
+ await this._api.POST("/auth/logout", {
2085
+ body: options.everywhere ? { everywhere: true } : {}
2086
+ });
2087
+ } catch (err) {
2088
+ console.warn("[PollarClient] Server logout failed (continuing with local clear)", err);
2089
+ }
2090
+ }
2091
+ try {
2092
+ await this._clearSession();
2093
+ } catch (err) {
2094
+ console.warn("[PollarClient] Local logout cleanup failed", err);
2095
+ }
2096
+ }
2097
+ /** Convenience: revoke every active session for this user (all devices). */
2098
+ logoutEverywhere() {
2099
+ return this.logout({ everywhere: true });
2100
+ }
2101
+ /**
2102
+ * List active sessions for the authenticated user. Returns one entry per
2103
+ * refresh-token family with the metadata captured at issuance time. The
2104
+ * `current` flag identifies which entry corresponds to this client.
2105
+ */
2106
+ async listSessions() {
2107
+ if (!isBrowser) {
2108
+ warnServerSide("listSessions");
2109
+ return [];
2110
+ }
2111
+ if (!this._session?.token?.accessToken) {
2112
+ throw new Error("[PollarClient] listSessions requires an authenticated session");
2113
+ }
2114
+ const { data, error } = await this._api.GET("/auth/sessions");
2115
+ if (error || !data?.success) {
2116
+ throw new Error("[PollarClient] Failed to list sessions");
2117
+ }
2118
+ return data.content.sessions;
2119
+ }
2120
+ /**
2121
+ * Revoke a specific refresh-token family (a single device session). Use
2122
+ * `listSessions` to enumerate the familyIds. Revoking the current session
2123
+ * does NOT clear local state — call `logout()` for that case.
2124
+ */
2125
+ async revokeSession(familyId) {
2126
+ if (!isBrowser) {
2127
+ warnServerSide("revokeSession");
2128
+ return;
2129
+ }
2130
+ if (!this._session?.token?.accessToken) {
2131
+ throw new Error("[PollarClient] revokeSession requires an authenticated session");
2132
+ }
2133
+ const { error } = await this._api.DELETE("/auth/sessions/{familyId}", {
2134
+ params: { path: { familyId } }
2135
+ });
2136
+ if (error) {
2137
+ throw new Error("[PollarClient] Failed to revoke session");
2138
+ }
1496
2139
  }
1497
2140
  // ─── Network ──────────────────────────────────────────────────────────────
1498
2141
  getNetwork() {
@@ -1601,10 +2244,9 @@ var PollarClient = class {
1601
2244
  async signAndSubmitTx(unsignedXdr) {
1602
2245
  const state = this._transactionState;
1603
2246
  const buildData = state?.step === "built" ? state.buildData : state?.step === "error" ? state.buildData : void 0;
1604
- const isBuiltFlow = !!buildData;
1605
2247
  const stateExtra = buildData ? { buildData } : { external: true };
1606
2248
  this._setTransactionState({ step: "signing", ...stateExtra });
1607
- const accountToSign = isBuiltFlow ? this._session?.wallet?.publicKey : this._session?.data?.providers?.wallet?.address ?? this._session?.wallet?.publicKey;
2249
+ const accountToSign = this._session?.wallet?.publicKey;
1608
2250
  if (this._walletAdapter) {
1609
2251
  try {
1610
2252
  const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
@@ -1689,13 +2331,11 @@ var PollarClient = class {
1689
2331
  for (const cb of this._walletBalanceStateListeners) cb(next);
1690
2332
  }
1691
2333
  // ─── Private ──────────────────────────────────────────────────────────────
1692
- /** Creates a new AbortController, cancelling any existing flow first. */
1693
2334
  _newController() {
1694
2335
  this._loginController?.abort();
1695
2336
  this._loginController = new AbortController();
1696
2337
  return this._loginController;
1697
2338
  }
1698
- /** Builds the deps object passed to flow functions via bind pattern. */
1699
2339
  _flowDeps(signal) {
1700
2340
  return {
1701
2341
  api: this._api,
@@ -1703,12 +2343,31 @@ var PollarClient = class {
1703
2343
  setAuthState: this._setAuthState.bind(this),
1704
2344
  storeSession: this._storeSession.bind(this),
1705
2345
  clearSession: this._clearSession.bind(this),
1706
- storeWalletAdapter: (adapter, type) => {
2346
+ getPublicJwk: () => this._keyManager.getPublicJwk(),
2347
+ resolveWalletAdapter: (id) => this._resolveWalletAdapter(id),
2348
+ storeWalletAdapter: async (adapter, id) => {
1707
2349
  this._walletAdapter = adapter;
1708
- writeWalletType(type);
1709
- }
2350
+ await writeWalletType(this._storage, this.apiKeyHash, id);
2351
+ },
2352
+ ...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
1710
2353
  };
1711
2354
  }
2355
+ /**
2356
+ * Resolves a wallet adapter for the requested id. Uses the consumer's
2357
+ * injected `walletAdapter` resolver when present; otherwise falls back to
2358
+ * the built-in `FreighterAdapter` / `AlbedoAdapter`. Throws if the id is
2359
+ * unknown and no resolver is configured.
2360
+ */
2361
+ async _resolveWalletAdapter(id) {
2362
+ if (this._walletAdapterResolver) {
2363
+ return Promise.resolve(this._walletAdapterResolver(id));
2364
+ }
2365
+ if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
2366
+ if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
2367
+ throw new Error(
2368
+ `[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
2369
+ );
2370
+ }
1712
2371
  _handleFlowError(error) {
1713
2372
  if (error instanceof Error && error.name === "AbortError") {
1714
2373
  console.info("[PollarClient] Login cancelled");
@@ -1723,34 +2382,58 @@ var PollarClient = class {
1723
2382
  errorCode: AUTH_ERROR_CODES.UNEXPECTED_ERROR
1724
2383
  });
1725
2384
  }
1726
- _restoreSession() {
1727
- this._session = readStorage();
2385
+ async _restoreSession() {
2386
+ this._session = await readStorage(this._storage, this.apiKeyHash);
1728
2387
  if (this._session) {
1729
- this._authState = { step: "authenticated", session: this._session };
1730
- if (this._session.data?.providers?.wallet?.address) {
1731
- const storedType = readWalletType();
1732
- if (storedType === "freighter" /* FREIGHTER */) {
1733
- this._walletAdapter = new FreighterAdapter();
1734
- } else if (storedType === "albedo" /* ALBEDO */) {
1735
- this._walletAdapter = new AlbedoAdapter();
2388
+ const storedType = await readWalletType(this._storage, this.apiKeyHash);
2389
+ if (storedType) {
2390
+ try {
2391
+ this._walletAdapter = await this._resolveWalletAdapter(storedType);
2392
+ } catch (err) {
2393
+ console.warn("[PollarClient] Could not restore wallet adapter for stored id", { id: storedType, err });
1736
2394
  }
1737
2395
  }
1738
2396
  console.info("[PollarClient] Session restored from storage");
2397
+ this._setAuthState({ step: "authenticated", session: this._session });
1739
2398
  } else {
1740
2399
  console.info("[PollarClient] No session in storage");
1741
2400
  }
1742
2401
  }
1743
- _storeSession(session) {
1744
- console.info(`[PollarClient] Session stored \u2014 user: ${session.userId ?? "anonymous"}`);
1745
- this._session = session;
1746
- writeStorage(session);
1747
- this._setAuthState({ step: "authenticated", session });
2402
+ async _storeSession(session) {
2403
+ console.info("[PollarClient] Session stored");
2404
+ const persisted = {
2405
+ clientSessionId: session.clientSessionId,
2406
+ userId: session.userId ?? null,
2407
+ status: session.status,
2408
+ token: session.token,
2409
+ user: session.user,
2410
+ wallet: session.wallet
2411
+ };
2412
+ this._session = persisted;
2413
+ if (session.data) {
2414
+ this._profile = {
2415
+ mail: session.data.mail,
2416
+ first_name: session.data.first_name,
2417
+ last_name: session.data.last_name,
2418
+ avatar: session.data.avatar,
2419
+ providers: session.data.providers
2420
+ };
2421
+ }
2422
+ await writeStorage(this._storage, this.apiKeyHash, persisted);
2423
+ this._setAuthState({ step: "authenticated", session: persisted });
1748
2424
  }
1749
- _clearSession() {
2425
+ async _clearSession() {
1750
2426
  console.info("[PollarClient] Session cleared");
1751
2427
  this._session = null;
2428
+ this._profile = null;
1752
2429
  this._walletAdapter = null;
1753
- removeStorage();
2430
+ this._dpopNonce = null;
2431
+ try {
2432
+ await this._keyManager.reset();
2433
+ } catch (err) {
2434
+ console.warn("[PollarClient] KeyManager reset failed during clearSession", err);
2435
+ }
2436
+ await removeStorage(this._storage, this.apiKeyHash);
1754
2437
  this._transactionState = null;
1755
2438
  this._setAuthState({ step: "idle" });
1756
2439
  }
@@ -1775,6 +2458,9 @@ var PollarClient = class {
1775
2458
  }
1776
2459
  };
1777
2460
 
1778
- export { AUTH_ERROR_CODES, AlbedoAdapter, FreighterAdapter, PollarClient, StellarClient, WalletType, createOffRamp, createOnRamp, getKycProviders, getKycStatus, getRampTransaction, getRampsQuote, isValidSession, pollKycStatus, pollRampTransaction, resolveKyc, startKyc };
2461
+ // src/index.ts
2462
+ _setDefaultKeyManagerFactory((_storage, apiKey) => new WebCryptoKeyManager(apiKey));
2463
+
2464
+ export { AUTH_ERROR_CODES, AlbedoAdapter, FreighterAdapter, PollarClient, StellarClient, WalletType, WebCryptoKeyManager, buildProof, canonicalEcJwk, computeJwkThumbprint, createLocalStorageAdapter, createMemoryAdapter, createOffRamp, createOnRamp, defaultKeyManager, defaultStorage, getKycProviders, getKycStatus, getRampTransaction, getRampsQuote, isValidSession, normalizeHtu, pollKycStatus, pollRampTransaction, resolveKyc, startKyc };
1779
2465
  //# sourceMappingURL=index.mjs.map
1780
2466
  //# sourceMappingURL=index.mjs.map