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