@pollar/core 0.8.1 → 0.9.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.rn.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { p256 } from '@noble/curves/p256';
1
+ import { p256 } from '@noble/curves/nist';
2
+ import { sha256 as sha256$1 } from '@noble/hashes/sha2';
2
3
 
3
4
  var __create = Object.create;
4
5
  var __defProp = Object.defineProperty;
@@ -184,11 +185,8 @@ function defaultKeyManager(storage, apiKey) {
184
185
  }
185
186
  return _factory(storage, apiKey);
186
187
  }
187
-
188
- // src/lib/sha256.ts
189
188
  async function sha256(data) {
190
- const buf = await crypto.subtle.digest("SHA-256", data);
191
- return new Uint8Array(buf);
189
+ return sha256$1(data);
192
190
  }
193
191
 
194
192
  // src/lib/api-key-hash.ts
@@ -267,11 +265,14 @@ async function computeJwkThumbprint(jwk) {
267
265
  const digest = await sha256(new TextEncoder().encode(canonical));
268
266
  return base64urlEncode(digest);
269
267
  }
268
+ function toBase64url(value) {
269
+ return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
270
+ }
270
271
  function canonicalEcJwk(jwk) {
271
272
  if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
272
273
  throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
273
274
  }
274
- return { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
275
+ return { kty: "EC", crv: "P-256", x: toBase64url(jwk.x), y: toBase64url(jwk.y) };
275
276
  }
276
277
 
277
278
  // src/keys/noble.ts
@@ -377,167 +378,6 @@ var NobleKeyManager = class {
377
378
  }
378
379
  };
379
380
 
380
- // src/keys/web-crypto.ts
381
- var DB_NAME = "pollar-keys";
382
- var DB_VERSION = 1;
383
- var STORE_NAME = "keys";
384
- function openDb() {
385
- return new Promise((resolve, reject) => {
386
- if (typeof indexedDB === "undefined") {
387
- reject(new Error("[PollarClient:keys] IndexedDB not available"));
388
- return;
389
- }
390
- const req = indexedDB.open(DB_NAME, DB_VERSION);
391
- req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
392
- req.onsuccess = () => resolve(req.result);
393
- req.onupgradeneeded = () => {
394
- const db = req.result;
395
- if (!db.objectStoreNames.contains(STORE_NAME)) {
396
- db.createObjectStore(STORE_NAME);
397
- }
398
- };
399
- });
400
- }
401
- function awaitTx(req) {
402
- return new Promise((resolve, reject) => {
403
- req.onsuccess = () => resolve(req.result);
404
- req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
405
- });
406
- }
407
- async function dbGet(key) {
408
- const db = await openDb();
409
- try {
410
- const tx = db.transaction(STORE_NAME, "readonly");
411
- const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
412
- return result;
413
- } finally {
414
- db.close();
415
- }
416
- }
417
- async function dbPut(key, value) {
418
- const db = await openDb();
419
- try {
420
- const tx = db.transaction(STORE_NAME, "readwrite");
421
- await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
422
- } finally {
423
- db.close();
424
- }
425
- }
426
- async function dbDelete(key) {
427
- const db = await openDb();
428
- try {
429
- const tx = db.transaction(STORE_NAME, "readwrite");
430
- await awaitTx(tx.objectStore(STORE_NAME).delete(key));
431
- } finally {
432
- db.close();
433
- }
434
- }
435
- function isCryptoKeyPair(v) {
436
- if (typeof v !== "object" || v === null) return false;
437
- const obj = v;
438
- return obj.privateKey !== void 0 && obj.publicKey !== void 0;
439
- }
440
- var WebCryptoKeyManager = class {
441
- constructor(apiKey) {
442
- this.apiKeyHash = null;
443
- this.keyPair = null;
444
- this.publicJwk = null;
445
- this.thumbprint = null;
446
- /**
447
- * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
448
- * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
449
- * failure so callers can retry, and cleared on `reset()`.
450
- */
451
- this._initPromise = null;
452
- if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
453
- throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
454
- }
455
- this.apiKey = apiKey;
456
- }
457
- /**
458
- * Idempotent and safe under concurrency. The first call kicks off the real
459
- * init; subsequent (and concurrent) calls return the same in-flight promise.
460
- * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
461
- * the manager is self-healing if `init()` was never explicitly invoked.
462
- */
463
- async init() {
464
- if (this.keyPair) return;
465
- if (!this._initPromise) {
466
- this._initPromise = this._doInit().catch((err) => {
467
- console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
468
- this._initPromise = null;
469
- throw err;
470
- });
471
- }
472
- return this._initPromise;
473
- }
474
- async _doInit() {
475
- if (!this.apiKeyHash) {
476
- this.apiKeyHash = await hashApiKey(this.apiKey);
477
- }
478
- let pair;
479
- try {
480
- pair = await dbGet(this.apiKeyHash);
481
- if (pair && !isCryptoKeyPair(pair)) pair = void 0;
482
- } catch {
483
- pair = void 0;
484
- }
485
- if (!pair) {
486
- pair = await globalThis.crypto.subtle.generateKey(
487
- { name: "ECDSA", namedCurve: "P-256" },
488
- // false → private key non-extractable; per W3C ECDSA spec the public
489
- // key is always extractable regardless of this flag.
490
- false,
491
- ["sign", "verify"]
492
- );
493
- try {
494
- await dbPut(this.apiKeyHash, pair);
495
- } catch {
496
- }
497
- }
498
- this.keyPair = pair;
499
- const exported = await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey);
500
- this.publicJwk = canonicalEcJwk(exported);
501
- this.thumbprint = await computeJwkThumbprint(this.publicJwk);
502
- }
503
- async reset() {
504
- try {
505
- if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
506
- } catch {
507
- }
508
- this.keyPair = null;
509
- this.publicJwk = null;
510
- this.thumbprint = null;
511
- this._initPromise = null;
512
- }
513
- async getPublicJwk() {
514
- if (!this.publicJwk) await this.init();
515
- if (!this.publicJwk) {
516
- throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
517
- }
518
- return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
519
- }
520
- async getThumbprint() {
521
- if (!this.thumbprint) await this.init();
522
- if (!this.thumbprint) {
523
- throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
524
- }
525
- return this.thumbprint;
526
- }
527
- async sign(payload) {
528
- if (!this.keyPair) await this.init();
529
- if (!this.keyPair) {
530
- throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
531
- }
532
- const sig = await globalThis.crypto.subtle.sign(
533
- { name: "ECDSA", hash: "SHA-256" },
534
- this.keyPair.privateKey,
535
- payload
536
- );
537
- return new Uint8Array(sig);
538
- }
539
- };
540
-
541
381
  // ../../node_modules/openapi-fetch/dist/index.mjs
542
382
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
543
383
  var supportsRequestInitExt = () => {
@@ -1257,6 +1097,9 @@ function defaultStorage(options = {}) {
1257
1097
  return createLocalStorageAdapter(options);
1258
1098
  }
1259
1099
 
1100
+ // src/version.ts
1101
+ var POLLAR_CORE_VERSION = "0.9.0-rc.0" ;
1102
+
1260
1103
  // src/visibility/noop.ts
1261
1104
  function createNoopVisibilityProvider() {
1262
1105
  return {
@@ -1321,6 +1164,7 @@ var AUTH_ERROR_CODES = {
1321
1164
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1322
1165
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1323
1166
  WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1167
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1324
1168
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1325
1169
  };
1326
1170
  var PollarFlowError = class extends Error {
@@ -1366,7 +1210,7 @@ var FreighterAdapter = class {
1366
1210
  if (!userInfo?.publicKey) {
1367
1211
  throw new Error("Failed to get user information from Freighter");
1368
1212
  }
1369
- return { address: userInfo.publicKey, publicKey: userInfo.publicKey };
1213
+ return { address: userInfo.publicKey };
1370
1214
  }
1371
1215
  async disconnect() {
1372
1216
  }
@@ -1404,6 +1248,19 @@ var FreighterAdapter = class {
1404
1248
  };
1405
1249
 
1406
1250
  // src/wallets/AlbedoAdapter.ts
1251
+ var PUBLIC_PASSPHRASE = "Public Global Stellar Network ; September 2015";
1252
+ var TESTNET_PASSPHRASE = "Test SDF Network ; September 2015";
1253
+ function albedoNetwork(options, fallback) {
1254
+ switch (options?.networkPassphrase) {
1255
+ case PUBLIC_PASSPHRASE:
1256
+ return "public";
1257
+ case TESTNET_PASSPHRASE:
1258
+ return "testnet";
1259
+ }
1260
+ if (options?.network === "public" || options?.network === "mainnet") return "public";
1261
+ if (options?.network === "testnet") return "testnet";
1262
+ return fallback;
1263
+ }
1407
1264
  function openAlbedoPopup(url) {
1408
1265
  const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
1409
1266
  if (!popup) {
@@ -1442,7 +1299,13 @@ function waitForAlbedoResult() {
1442
1299
  });
1443
1300
  }
1444
1301
  var AlbedoAdapter = class {
1445
- constructor() {
1302
+ /**
1303
+ * Network used for `connect` and `signAuthEntry` (which carry no per-call
1304
+ * network) and as the fallback for `signTransaction`. Defaults to `'testnet'`
1305
+ * to preserve the previous behavior when constructed with no argument.
1306
+ */
1307
+ constructor(network = "testnet") {
1308
+ this.network = network;
1446
1309
  this.type = "albedo" /* ALBEDO */;
1447
1310
  }
1448
1311
  async isAvailable() {
@@ -1452,7 +1315,7 @@ var AlbedoAdapter = class {
1452
1315
  const url = new URL("https://albedo.link");
1453
1316
  url.searchParams.set("intent", "public-key");
1454
1317
  url.searchParams.set("app_name", "Pollar");
1455
- url.searchParams.set("network", "testnet");
1318
+ url.searchParams.set("network", this.network);
1456
1319
  url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
1457
1320
  url.searchParams.set("origin", window.location.origin);
1458
1321
  openAlbedoPopup(url.toString());
@@ -1460,7 +1323,7 @@ var AlbedoAdapter = class {
1460
1323
  if (!result.pubkey) {
1461
1324
  throw new Error("Albedo connection rejected");
1462
1325
  }
1463
- return { address: result.pubkey, publicKey: result.pubkey };
1326
+ return { address: result.pubkey };
1464
1327
  }
1465
1328
  async disconnect() {
1466
1329
  }
@@ -1470,12 +1333,12 @@ var AlbedoAdapter = class {
1470
1333
  async getNetwork() {
1471
1334
  throw new Error("Albedo does not expose network");
1472
1335
  }
1473
- async signTransaction(xdr, _options) {
1336
+ async signTransaction(xdr, options) {
1474
1337
  const url = new URL("https://albedo.link");
1475
1338
  url.searchParams.set("intent", "tx");
1476
1339
  url.searchParams.set("xdr", xdr);
1477
1340
  url.searchParams.set("app_name", "Pollar");
1478
- url.searchParams.set("network", "testnet");
1341
+ url.searchParams.set("network", albedoNetwork(options, this.network));
1479
1342
  url.searchParams.set("callback", window.location.href);
1480
1343
  url.searchParams.set("origin", window.location.origin);
1481
1344
  window.location.href = url.toString();
@@ -1488,7 +1351,7 @@ var AlbedoAdapter = class {
1488
1351
  url.searchParams.set("intent", "sign-auth-entry");
1489
1352
  url.searchParams.set("xdr", entryXdr);
1490
1353
  url.searchParams.set("app_name", "Pollar");
1491
- url.searchParams.set("network", "testnet");
1354
+ url.searchParams.set("network", this.network);
1492
1355
  url.searchParams.set("callback", window.location.href);
1493
1356
  url.searchParams.set("origin", window.location.origin);
1494
1357
  window.location.href = url.toString();
@@ -1575,8 +1438,12 @@ function isValidSession(value) {
1575
1438
  return false;
1576
1439
  }
1577
1440
  const w = wallet;
1578
- if (w["publicKey"] !== null && !isBoundedString(w["publicKey"], MAX_WALLET_PUBLIC_KEY)) {
1579
- console.warn("[PollarClient:session] Invalid session \u2014 wallet.publicKey must be string|null");
1441
+ if (w["type"] !== "custodial" && w["type"] !== "smart" && w["type"] !== "external") {
1442
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.type must be custodial|smart|external");
1443
+ return false;
1444
+ }
1445
+ if (w["address"] !== null && !isBoundedString(w["address"], MAX_WALLET_PUBLIC_KEY)) {
1446
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.address must be string|null");
1580
1447
  return false;
1581
1448
  }
1582
1449
  if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
@@ -1587,6 +1454,10 @@ function isValidSession(value) {
1587
1454
  console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
1588
1455
  return false;
1589
1456
  }
1457
+ if (w["linkedAt"] !== void 0 && (typeof w["linkedAt"] !== "number" || !Number.isFinite(w["linkedAt"]))) {
1458
+ console.warn("[PollarClient:session] Invalid session \u2014 wallet.linkedAt must be a finite number if present");
1459
+ return false;
1460
+ }
1590
1461
  return true;
1591
1462
  }
1592
1463
  async function readStorage(storage, apiKeyHash) {
@@ -1594,6 +1465,12 @@ async function readStorage(storage, apiKeyHash) {
1594
1465
  if (!raw) return null;
1595
1466
  try {
1596
1467
  const session = JSON.parse(raw);
1468
+ if (typeof session === "object" && session !== null) {
1469
+ const w = session.wallet;
1470
+ if (w && w["address"] == null && typeof w["publicKey"] === "string") {
1471
+ w["address"] = w["publicKey"];
1472
+ }
1473
+ }
1597
1474
  if (!isValidSession(session)) {
1598
1475
  await storage.remove(sessionStorageKey(apiKeyHash));
1599
1476
  console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
@@ -1684,7 +1561,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1684
1561
  }));
1685
1562
  } catch (e) {
1686
1563
  if (e instanceof Error && e.name === "AbortError") throw e;
1687
- console.warn(e);
1564
+ console.warn("[PollarClient:stream] session-status request failed; will retry", e);
1688
1565
  }
1689
1566
  if (error || !data) {
1690
1567
  await sleep(backoff);
@@ -1724,7 +1601,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1724
1601
  } catch (e) {
1725
1602
  if (e instanceof Error && e.name === "AbortError") throw e;
1726
1603
  if (e instanceof SessionStatusError) throw e;
1727
- console.warn(e);
1604
+ console.warn("[PollarClient:stream] session-status stream read failed; will retry", e);
1728
1605
  } finally {
1729
1606
  reader.releaseLock();
1730
1607
  }
@@ -1752,7 +1629,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
1752
1629
  envelope = await response.json().catch(() => null);
1753
1630
  } catch (e) {
1754
1631
  if (e instanceof Error && e.name === "AbortError") throw e;
1755
- console.warn(e);
1632
+ console.warn("[PollarClient:stream] session-status poll failed; will retry", e);
1756
1633
  }
1757
1634
  if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1758
1635
  throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
@@ -1956,6 +1833,55 @@ async function loginOAuth(provider, deps) {
1956
1833
  await authenticate(clientSessionId, deps);
1957
1834
  }
1958
1835
 
1836
+ // src/client/auth/passkeyFlow.ts
1837
+ async function loginSmartWallet(deps) {
1838
+ const { api, signal, setAuthState, passkey } = deps;
1839
+ if (!passkey) {
1840
+ setAuthState({
1841
+ step: "error",
1842
+ previousStep: "creating_session",
1843
+ message: "Passkey support is not configured",
1844
+ errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED
1845
+ });
1846
+ return;
1847
+ }
1848
+ const clientSessionId = await createAuthSession(deps);
1849
+ if (!clientSessionId) return;
1850
+ try {
1851
+ const { data: challengeData } = await api.POST("/auth/passkey/challenge", {
1852
+ body: { clientSessionId },
1853
+ signal
1854
+ });
1855
+ const challenge = challengeData?.content?.challenge;
1856
+ if (!challengeData?.success || !challenge) {
1857
+ return failPasskey(setAuthState, "Failed to start passkey");
1858
+ }
1859
+ setAuthState({ step: "creating_passkey" });
1860
+ const ceremony = await passkey({ challenge });
1861
+ const response = ceremony.response;
1862
+ if (ceremony.kind === "register") {
1863
+ setAuthState({ step: "deploying_smart_account" });
1864
+ const { data } = await api.POST("/auth/passkey/register", {
1865
+ body: { clientSessionId, response },
1866
+ signal
1867
+ });
1868
+ if (!data?.success) return failPasskey(setAuthState, "Passkey registration failed");
1869
+ } else {
1870
+ const { data } = await api.POST("/auth/passkey/login", {
1871
+ body: { clientSessionId, response },
1872
+ signal
1873
+ });
1874
+ if (!data?.success) return failPasskey(setAuthState, "Passkey authentication failed");
1875
+ }
1876
+ } catch {
1877
+ return failPasskey(setAuthState, "Passkey login failed");
1878
+ }
1879
+ await authenticate(clientSessionId, deps);
1880
+ }
1881
+ function failPasskey(setAuthState, message) {
1882
+ setAuthState({ step: "error", previousStep: "creating_passkey", message, errorCode: AUTH_ERROR_CODES.PASSKEY_FAILED });
1883
+ }
1884
+
1959
1885
  // src/client/auth/walletFlow.ts
1960
1886
  function withSignal(promise, signal) {
1961
1887
  return Promise.race([
@@ -1982,12 +1908,12 @@ async function loginWallet(type, deps) {
1982
1908
  setAuthState({ step: "wallet_not_installed", walletType: type });
1983
1909
  return;
1984
1910
  }
1985
- const { publicKey } = await withSignal(adapter.connect(), signal);
1986
- connectedWallet = publicKey;
1911
+ const { address } = await withSignal(adapter.connect(), signal);
1912
+ connectedWallet = address;
1987
1913
  deps.storeWalletAdapter(adapter, type);
1988
1914
  setAuthState({ step: "authenticating_wallet" });
1989
1915
  const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
1990
- body: { clientSessionId, walletAddress: publicKey },
1916
+ body: { clientSessionId, walletAddress: address },
1991
1917
  signal
1992
1918
  });
1993
1919
  if (walletError || !walletData?.success) {
@@ -2052,8 +1978,12 @@ var PollarClient = class {
2052
1978
  this._transactionStateListeners = /* @__PURE__ */ new Set();
2053
1979
  this._txHistoryState = { step: "idle" };
2054
1980
  this._txHistoryStateListeners = /* @__PURE__ */ new Set();
1981
+ this._sessionsState = { step: "idle" };
1982
+ this._sessionsStateListeners = /* @__PURE__ */ new Set();
2055
1983
  this._walletBalanceState = { step: "idle" };
2056
1984
  this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
1985
+ this._enabledAssetsState = { step: "idle" };
1986
+ this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
2057
1987
  this._authState = { step: "idle" };
2058
1988
  this._authStateListeners = /* @__PURE__ */ new Set();
2059
1989
  this._networkState = { step: "idle" };
@@ -2069,6 +1999,8 @@ var PollarClient = class {
2069
1999
  this._storageDegradeListeners = /* @__PURE__ */ new Set();
2070
2000
  this._walletAdapter = null;
2071
2001
  this._loginController = null;
2002
+ /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2003
+ this._resumeController = null;
2072
2004
  this.apiKey = config.apiKey;
2073
2005
  this.id = randomUUID();
2074
2006
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2081,6 +2013,8 @@ var PollarClient = class {
2081
2013
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
2082
2014
  this._walletAdapterResolver = config.walletAdapter ?? null;
2083
2015
  this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
2016
+ this._passkey = config.passkey ?? null;
2017
+ this._passkeySign = config.passkeySign ?? null;
2084
2018
  this._deviceLabel = config.deviceLabel;
2085
2019
  this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
2086
2020
  this._maxIdleMs = config.maxIdleMs;
@@ -2094,7 +2028,9 @@ var PollarClient = class {
2094
2028
  this._initialized = Promise.resolve();
2095
2029
  return;
2096
2030
  }
2097
- console.info(`[PollarClient] Initialized \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`);
2031
+ console.info(
2032
+ `[PollarClient] Initialized v${POLLAR_CORE_VERSION} \u2014 endpoint: ${this.basePath}, network: ${this._networkState.network}`
2033
+ );
2098
2034
  this._initialized = this._initialize();
2099
2035
  }
2100
2036
  /**
@@ -2132,7 +2068,11 @@ var PollarClient = class {
2132
2068
  }
2133
2069
  await this._restoreSession();
2134
2070
  this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2135
- if (visible) void this._maybeProactiveRefresh();
2071
+ if (!visible) return;
2072
+ void this._maybeProactiveRefresh();
2073
+ if (this._authState.step === "authenticated" && !this._authState.verified) {
2074
+ void this._resume();
2075
+ }
2136
2076
  });
2137
2077
  }
2138
2078
  /** Detach the cross-tab storage listener and abort any in-flight login. */
@@ -2143,6 +2083,8 @@ var PollarClient = class {
2143
2083
  }
2144
2084
  this._loginController?.abort();
2145
2085
  this._loginController = null;
2086
+ this._resumeController?.abort();
2087
+ this._resumeController = null;
2146
2088
  this._clearRefreshTimer();
2147
2089
  if (this._visibilityUnsubscribe) {
2148
2090
  this._visibilityUnsubscribe();
@@ -2157,7 +2099,9 @@ var PollarClient = class {
2157
2099
  request.headers.set("x-pollar-api-key", self.apiKey);
2158
2100
  self._lastRequestAt = Date.now();
2159
2101
  await self._initialized;
2160
- if (request.body !== null) {
2102
+ const cacheMethod = request.method.toUpperCase();
2103
+ const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
2104
+ if (cacheBodyAllowed && request.body != null) {
2161
2105
  try {
2162
2106
  self._requestBodyCache.set(request, await request.clone().arrayBuffer());
2163
2107
  } catch (err) {
@@ -2244,7 +2188,9 @@ var PollarClient = class {
2244
2188
  }
2245
2189
  }
2246
2190
  }
2247
- const cachedBody = this._requestBodyCache.get(originalRequest);
2191
+ const retryMethod = originalRequest.method.toUpperCase();
2192
+ const retryBodyAllowed = retryMethod !== "GET" && retryMethod !== "HEAD";
2193
+ const cachedBody = retryBodyAllowed ? this._requestBodyCache.get(originalRequest) : void 0;
2248
2194
  const retried = new Request(originalRequest.url, {
2249
2195
  method: originalRequest.method,
2250
2196
  headers,
@@ -2288,19 +2234,26 @@ var PollarClient = class {
2288
2234
  throw err;
2289
2235
  }
2290
2236
  if (error || !data) {
2291
- console.warn("[PollarClient] /auth/refresh returned error", { error });
2237
+ console.error("[PollarClient] /auth/refresh returned error", { error });
2292
2238
  await this._clearSession();
2293
2239
  throw new Error("Refresh failed");
2294
2240
  }
2295
2241
  const successData = data;
2296
2242
  if (!successData.success || !successData.content?.token) {
2297
- console.warn("[PollarClient] /auth/refresh response malformed", successData);
2243
+ console.error("[PollarClient] /auth/refresh response malformed", {
2244
+ success: successData.success,
2245
+ hasToken: !!successData.content?.token
2246
+ });
2298
2247
  await this._clearSession();
2299
2248
  throw new Error("Refresh response malformed");
2300
2249
  }
2301
2250
  const newToken = successData.content.token;
2302
2251
  if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
2303
- console.warn("[PollarClient] /auth/refresh token shape invalid", newToken);
2252
+ console.error("[PollarClient] /auth/refresh token shape invalid", {
2253
+ accessToken: typeof newToken.accessToken,
2254
+ refreshToken: typeof newToken.refreshToken,
2255
+ expiresAt: typeof newToken.expiresAt
2256
+ });
2304
2257
  await this._clearSession();
2305
2258
  throw new Error("Refresh response token shape invalid");
2306
2259
  }
@@ -2494,6 +2447,19 @@ var PollarClient = class {
2494
2447
  const controller = this._newController();
2495
2448
  loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2496
2449
  }
2450
+ /**
2451
+ * "Smart Wallet" login: runs the passkey (WebAuthn) ceremony and, for a new
2452
+ * user, creates a sponsored smart-account C-address. Requires the `passkey`
2453
+ * ceremony to be configured (e.g. via `@pollar/react`).
2454
+ */
2455
+ loginSmartWallet() {
2456
+ if (!isClientRuntime) {
2457
+ warnServerSide("loginSmartWallet");
2458
+ return;
2459
+ }
2460
+ const controller = this._newController();
2461
+ loginSmartWallet(this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2462
+ }
2497
2463
  // ─── Cancel ───────────────────────────────────────────────────────────────
2498
2464
  cancelLogin() {
2499
2465
  this._loginController?.abort();
@@ -2556,6 +2522,29 @@ var PollarClient = class {
2556
2522
  }
2557
2523
  return data.content.sessions;
2558
2524
  }
2525
+ getSessionsState() {
2526
+ return this._sessionsState;
2527
+ }
2528
+ onSessionsStateChange(cb) {
2529
+ this._sessionsStateListeners.add(cb);
2530
+ cb(this._sessionsState);
2531
+ return () => this._sessionsStateListeners.delete(cb);
2532
+ }
2533
+ /**
2534
+ * Fire-and-forget variant of {@link listSessions} that drives the observable
2535
+ * `SessionsState` store instead of returning the array. UI layers subscribe
2536
+ * via `onSessionsStateChange` and stay pure readers — mirrors `fetchTxHistory`.
2537
+ */
2538
+ async fetchSessions() {
2539
+ this._setSessionsState({ step: "loading" });
2540
+ try {
2541
+ const sessions = await this.listSessions();
2542
+ this._setSessionsState({ step: "loaded", sessions });
2543
+ } catch (err) {
2544
+ const message = err instanceof Error ? err.message : "Failed to load sessions";
2545
+ this._setSessionsState({ step: "error", message });
2546
+ }
2547
+ }
2559
2548
  /**
2560
2549
  * Revoke a specific refresh-token family (a single device session). Use
2561
2550
  * `listSessions` to enumerate the familyIds. Revoking the current session
@@ -2632,16 +2621,19 @@ var PollarClient = class {
2632
2621
  cb(this._walletBalanceState);
2633
2622
  return () => this._walletBalanceStateListeners.delete(cb);
2634
2623
  }
2635
- async refreshBalance(publicKey) {
2636
- const pk = publicKey ?? this._session?.wallet?.publicKey;
2637
- if (!pk) {
2624
+ /**
2625
+ * Refreshes the balances of the authenticated user's OWN wallet. The wallet
2626
+ * and network are resolved server-side from the session — no arguments. Drives
2627
+ * `walletBalanceState`. For an arbitrary wallet, use {@link getWalletBalance}.
2628
+ */
2629
+ async refreshBalance() {
2630
+ if (!this._session?.wallet?.address) {
2638
2631
  this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
2639
2632
  return;
2640
2633
  }
2641
2634
  this._setWalletBalanceState({ step: "loading" });
2642
2635
  try {
2643
- const network = this.getNetwork();
2644
- const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
2636
+ const { data, error } = await this._api.GET("/wallet/balance");
2645
2637
  if (!error && data?.success && data.content) {
2646
2638
  this._setWalletBalanceState({ step: "loaded", data: data.content });
2647
2639
  } else {
@@ -2651,6 +2643,53 @@ var PollarClient = class {
2651
2643
  this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
2652
2644
  }
2653
2645
  }
2646
+ /**
2647
+ * General-purpose balance lookup for ANY wallet on ANY network — not scoped
2648
+ * to this application. Enumerates the account's real on-chain holdings via
2649
+ * Horizon (server-side) and returns the data directly (no reactive state).
2650
+ * `network` defaults to the client's current network.
2651
+ */
2652
+ async getWalletBalance(publicKey, network) {
2653
+ const { data, error } = await this._api.GET("/wallet/{publicKey}/balance", {
2654
+ params: { path: { publicKey }, query: { network: network ?? this.getNetwork() } }
2655
+ });
2656
+ if (error || !data?.success || !data.content) {
2657
+ throw new Error("[PollarClient] Failed to load wallet balance");
2658
+ }
2659
+ return data.content;
2660
+ }
2661
+ // ─── Enabled assets ───────────────────────────────────────────────────────
2662
+ getEnabledAssetsState() {
2663
+ return this._enabledAssetsState;
2664
+ }
2665
+ onEnabledAssetsStateChange(cb) {
2666
+ this._enabledAssetsStateListeners.add(cb);
2667
+ cb(this._enabledAssetsState);
2668
+ return () => this._enabledAssetsStateListeners.delete(cb);
2669
+ }
2670
+ /**
2671
+ * Loads the application's enabled assets paired with the authenticated
2672
+ * wallet's on-chain trustline state — so the SDK knows which trustlines still
2673
+ * need to be added. Wallet and network are resolved server-side from the
2674
+ * session. Drives `enabledAssetsState`; mirrors {@link refreshBalance}.
2675
+ */
2676
+ async refreshAssets() {
2677
+ if (!this._session?.wallet?.address) {
2678
+ this._setEnabledAssetsState({ step: "error", message: "No wallet connected" });
2679
+ return;
2680
+ }
2681
+ this._setEnabledAssetsState({ step: "loading" });
2682
+ try {
2683
+ const { data, error } = await this._api.GET("/wallet/assets");
2684
+ if (!error && data?.success && data.content) {
2685
+ this._setEnabledAssetsState({ step: "loaded", data: data.content });
2686
+ } else {
2687
+ this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
2688
+ }
2689
+ } catch {
2690
+ this._setEnabledAssetsState({ step: "error", message: "Failed to load assets" });
2691
+ }
2692
+ }
2654
2693
  // ─── Transactions ─────────────────────────────────────────────────────────
2655
2694
  /**
2656
2695
  * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
@@ -2658,14 +2697,14 @@ var PollarClient = class {
2658
2697
  * inspect the result without subscribing to state changes.
2659
2698
  */
2660
2699
  async buildTx(operation, params, options) {
2661
- if (!this._session?.wallet?.publicKey) {
2700
+ if (!this._session?.wallet?.address) {
2662
2701
  const details = "No wallet connected";
2663
2702
  this._setTransactionState({ step: "error", phase: "building", details });
2664
2703
  return { status: "error", details };
2665
2704
  }
2666
2705
  const body = {
2667
2706
  network: this.getNetwork(),
2668
- publicKey: this._session.wallet.publicKey,
2707
+ address: this._session.wallet.address,
2669
2708
  operation,
2670
2709
  params,
2671
2710
  options: options ?? {}
@@ -2706,7 +2745,7 @@ var PollarClient = class {
2706
2745
  const buildData = this._currentBuildData();
2707
2746
  this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2708
2747
  if (this._walletAdapter) {
2709
- const accountToSign = this._session?.wallet?.publicKey;
2748
+ const accountToSign = this._session?.wallet?.address;
2710
2749
  const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2711
2750
  try {
2712
2751
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
@@ -2727,10 +2766,10 @@ var PollarClient = class {
2727
2766
  return { status: "error", ...details && { details } };
2728
2767
  }
2729
2768
  }
2730
- const publicKey = this._session?.wallet?.publicKey ?? "";
2769
+ const address = this._session?.wallet?.address ?? "";
2731
2770
  try {
2732
2771
  const { data, error } = await this._api.POST("/tx/sign", {
2733
- body: { network: this.getNetwork(), publicKey, unsignedXdr }
2772
+ body: { network: this.getNetwork(), address, unsignedXdr }
2734
2773
  });
2735
2774
  if (!error && data?.success && data.content?.signedXdr) {
2736
2775
  const { signedXdr, idempotencyKey } = data.content;
@@ -2783,12 +2822,12 @@ var PollarClient = class {
2783
2822
  const buildData = this._currentBuildData();
2784
2823
  const outcomeExtra = buildData ? { buildData } : {};
2785
2824
  this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2786
- const publicKey = this._session?.wallet?.publicKey ?? "";
2825
+ const address = this._session?.wallet?.address ?? "";
2787
2826
  try {
2788
2827
  const { data, error } = await this._api.POST("/tx/submit", {
2789
2828
  body: {
2790
2829
  network: this.getNetwork(),
2791
- publicKey,
2830
+ address,
2792
2831
  signedXdr,
2793
2832
  ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2794
2833
  }
@@ -2848,6 +2887,19 @@ var PollarClient = class {
2848
2887
  * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2849
2888
  */
2850
2889
  async signAndSubmitTx(unsignedXdr) {
2890
+ if (this._session?.wallet?.type === "smart") {
2891
+ const buildData2 = this._currentBuildData();
2892
+ if (!buildData2?.smart) {
2893
+ const details = "no prepared smart transaction; call buildTx first";
2894
+ this._setTransactionState({ step: "error", phase: "signing", details });
2895
+ return { status: "error", details };
2896
+ }
2897
+ return this._signSubmitSmart(buildData2);
2898
+ }
2899
+ if (!unsignedXdr) {
2900
+ this._setTransactionState({ step: "error", phase: "signing", details: "missing unsigned transaction" });
2901
+ return { status: "error", details: "missing unsigned transaction" };
2902
+ }
2851
2903
  if (this._walletAdapter) {
2852
2904
  const signed = await this.signTx(unsignedXdr);
2853
2905
  if (signed.status === "error") {
@@ -2865,7 +2917,7 @@ var PollarClient = class {
2865
2917
  this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2866
2918
  const body = {
2867
2919
  network: this.getNetwork(),
2868
- publicKey: this._session?.wallet?.publicKey ?? "",
2920
+ address: this._session?.wallet?.address ?? "",
2869
2921
  unsignedXdr
2870
2922
  };
2871
2923
  try {
@@ -2934,14 +2986,20 @@ var PollarClient = class {
2934
2986
  * `signTx`, and `submitTx` separately instead.
2935
2987
  */
2936
2988
  async buildAndSignAndSubmitTx(operation, params, options) {
2989
+ if (this._session?.wallet?.type === "smart") {
2990
+ return this._runSmartTx(operation, params, options);
2991
+ }
2937
2992
  if (this._walletAdapter) {
2938
2993
  const built = await this.buildTx(operation, params, options);
2939
2994
  if (built.status === "error") {
2940
2995
  return { status: "error", ...built.details && { details: built.details } };
2941
2996
  }
2997
+ if (!built.buildData.unsignedXdr) {
2998
+ return { status: "error", details: "build returned no unsigned transaction" };
2999
+ }
2942
3000
  return this.signAndSubmitTx(built.buildData.unsignedXdr);
2943
3001
  }
2944
- if (!this._session?.wallet?.publicKey) {
3002
+ if (!this._session?.wallet?.address) {
2945
3003
  this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2946
3004
  return { status: "error", details: "No wallet connected" };
2947
3005
  }
@@ -2950,7 +3008,7 @@ var PollarClient = class {
2950
3008
  const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2951
3009
  body: {
2952
3010
  network: this.getNetwork(),
2953
- publicKey: this._session.wallet.publicKey,
3011
+ address: this._session.wallet.address,
2954
3012
  operation,
2955
3013
  params,
2956
3014
  options: options ?? {}
@@ -2994,6 +3052,113 @@ var PollarClient = class {
2994
3052
  async runTx(operation, params, options) {
2995
3053
  return this.buildAndSignAndSubmitTx(operation, params, options);
2996
3054
  }
3055
+ /**
3056
+ * Smart-wallet (passkey / C-address) transaction: build (server prepares the
3057
+ * SAC transfer + returns the auth digest) → sign the digest with the passkey
3058
+ * → submit (server assembles the signed auth entry and broadcasts; the
3059
+ * sponsor pays the fee). State machine: building → built → signing →
3060
+ * submitting → success.
3061
+ */
3062
+ async _runSmartTx(operation, params, options) {
3063
+ const address = this._session?.wallet?.address;
3064
+ if (!address) {
3065
+ this._setTransactionState({ step: "error", phase: "building", details: "No wallet connected" });
3066
+ return { status: "error", details: "No wallet connected" };
3067
+ }
3068
+ if (!this._passkeySign) {
3069
+ const details = "Passkey signer not configured";
3070
+ this._setTransactionState({ step: "error", phase: "signing", details });
3071
+ return { status: "error", details };
3072
+ }
3073
+ this._setTransactionState({ step: "building" });
3074
+ let buildData;
3075
+ try {
3076
+ const body = {
3077
+ network: this.getNetwork(),
3078
+ address,
3079
+ operation,
3080
+ params,
3081
+ options: options ?? {}
3082
+ };
3083
+ const { data, error } = await this._api.POST("/tx/build", { body });
3084
+ if (error || !data?.success || !data.content?.smart) {
3085
+ const details = error?.details ?? "Failed to build transaction";
3086
+ this._setTransactionState({ step: "error", phase: "building", details });
3087
+ return { status: "error", details };
3088
+ }
3089
+ buildData = data.content;
3090
+ } catch (err) {
3091
+ const details = err instanceof Error ? err.message : void 0;
3092
+ this._setTransactionState({ step: "error", phase: "building", ...details && { details } });
3093
+ return { status: "error", ...details && { details } };
3094
+ }
3095
+ this._setTransactionState({ step: "built", buildData });
3096
+ return this._signSubmitSmart(buildData);
3097
+ }
3098
+ /**
3099
+ * Steps 2–3 of the smart-wallet flow: sign the prepared auth digest with the
3100
+ * passkey, then submit. Shared by `_runSmartTx` (atomic) and `signAndSubmitTx`
3101
+ * (split flow, when a smart build is already on the state machine).
3102
+ */
3103
+ async _signSubmitSmart(buildData) {
3104
+ const address = this._session?.wallet?.address;
3105
+ const smart = buildData.smart;
3106
+ if (!address || !smart) {
3107
+ const details = "no prepared smart transaction";
3108
+ this._setTransactionState({ step: "error", phase: "signing", buildData, details });
3109
+ return { status: "error", buildData, details };
3110
+ }
3111
+ if (!this._passkeySign) {
3112
+ const details = "Passkey signer not configured";
3113
+ this._setTransactionState({ step: "error", phase: "signing", buildData, details });
3114
+ return { status: "error", buildData, details };
3115
+ }
3116
+ this._setTransactionState({ step: "signing", buildData });
3117
+ let assertion;
3118
+ try {
3119
+ assertion = await this._passkeySign({ credentialId: smart.credentialId, challenge: smart.digest });
3120
+ } catch (err) {
3121
+ const details = err instanceof Error ? err.message : void 0;
3122
+ this._setTransactionState({ step: "error", phase: "signing", buildData, ...details && { details } });
3123
+ return { status: "error", buildData, ...details && { details } };
3124
+ }
3125
+ this._setTransactionState({ step: "submitting", buildData });
3126
+ const outcomeExtra = { buildData };
3127
+ try {
3128
+ const { data, error } = await this._api.POST("/tx/submit", {
3129
+ body: {
3130
+ network: this.getNetwork(),
3131
+ address,
3132
+ smart: { entryXdr: smart.entryXdr, funcXdr: smart.funcXdr, assertion }
3133
+ }
3134
+ });
3135
+ if (!error && data?.success && data.content) {
3136
+ const { hash, status: backendStatus, resultCode } = data.content;
3137
+ if (backendStatus === "SUCCESS") {
3138
+ this._setTransactionState({ step: "success", hash, buildData });
3139
+ return { status: "success", hash, ...outcomeExtra };
3140
+ }
3141
+ if (backendStatus === "PENDING") {
3142
+ this._setTransactionState({ step: "submitted", hash, buildData });
3143
+ return { status: "pending", hash, ...outcomeExtra };
3144
+ }
3145
+ this._setTransactionState({
3146
+ step: "error",
3147
+ phase: "submitting",
3148
+ buildData,
3149
+ ...resultCode && { details: resultCode }
3150
+ });
3151
+ return { status: "error", hash, ...outcomeExtra, ...resultCode && { details: resultCode, resultCode } };
3152
+ }
3153
+ const details = error?.details;
3154
+ this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3155
+ return { status: "error", ...outcomeExtra, ...details && { details } };
3156
+ } catch (err) {
3157
+ const details = err instanceof Error ? err.message : void 0;
3158
+ this._setTransactionState({ step: "error", phase: "submitting", buildData, ...details && { details } });
3159
+ return { status: "error", ...outcomeExtra, ...details && { details } };
3160
+ }
3161
+ }
2997
3162
  // ─── App config ───────────────────────────────────────────────────────────
2998
3163
  async getAppConfig() {
2999
3164
  try {
@@ -3047,10 +3212,18 @@ var PollarClient = class {
3047
3212
  this._txHistoryState = next;
3048
3213
  for (const cb of this._txHistoryStateListeners) cb(next);
3049
3214
  }
3215
+ _setSessionsState(next) {
3216
+ this._sessionsState = next;
3217
+ for (const cb of this._sessionsStateListeners) cb(next);
3218
+ }
3050
3219
  _setWalletBalanceState(next) {
3051
3220
  this._walletBalanceState = next;
3052
3221
  for (const cb of this._walletBalanceStateListeners) cb(next);
3053
3222
  }
3223
+ _setEnabledAssetsState(next) {
3224
+ this._enabledAssetsState = next;
3225
+ for (const cb of this._enabledAssetsStateListeners) cb(next);
3226
+ }
3054
3227
  // ─── Private ──────────────────────────────────────────────────────────────
3055
3228
  _newController() {
3056
3229
  this._loginController?.abort();
@@ -3075,6 +3248,7 @@ var PollarClient = class {
3075
3248
  this._walletAdapter = adapter;
3076
3249
  await writeWalletType(this._storage, this.apiKeyHash, id);
3077
3250
  },
3251
+ ...this._passkey ? { passkey: this._passkey } : {},
3078
3252
  ...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
3079
3253
  };
3080
3254
  }
@@ -3104,7 +3278,7 @@ var PollarClient = class {
3104
3278
  }
3105
3279
  }
3106
3280
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
3107
- if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
3281
+ if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
3108
3282
  throw new Error(
3109
3283
  `[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
3110
3284
  );
@@ -3145,21 +3319,66 @@ var PollarClient = class {
3145
3319
  }
3146
3320
  }
3147
3321
  console.info("[PollarClient] Session restored from storage");
3148
- this._setAuthState({ step: "authenticated", session: this._session });
3322
+ this._setAuthState({ step: "authenticated", session: this._session, verified: false });
3149
3323
  this._scheduleNextRefresh();
3324
+ void this._resume();
3150
3325
  } else {
3151
3326
  console.info("[PollarClient] No session in storage");
3152
3327
  }
3153
3328
  }
3329
+ /**
3330
+ * Validate the restored session against the server and repopulate the
3331
+ * in-memory profile (PII is never persisted, so it's null after a cold
3332
+ * reload). Goes through the normal authed client, so it coalesces with any
3333
+ * in-flight refresh (onRequest awaits `_refreshPromise`) and, being a GET,
3334
+ * is auto-retried after a 401-triggered refresh.
3335
+ *
3336
+ * - 200 → store profile, mark the session `verified`.
3337
+ * - 401 → the refresh-on-401 path already ran; if the family was
3338
+ * revoked, refresh failed and `_clearSession()` took us to
3339
+ * idle. Nothing to do here — don't double-handle.
3340
+ * - network error → stay optimistic (do NOT log out); revalidated later on
3341
+ * `visibilitychange` or first use.
3342
+ */
3343
+ async _resume() {
3344
+ if (!this._session) return;
3345
+ this._resumeController?.abort();
3346
+ const controller = new AbortController();
3347
+ this._resumeController = controller;
3348
+ try {
3349
+ const { data, error } = await this._api.GET("/auth/session/resume", { signal: controller.signal });
3350
+ if (error || !data) return;
3351
+ const content = data.content;
3352
+ if (!content || !this._session) return;
3353
+ this._profile = { ...content };
3354
+ this._setAuthState({ step: "authenticated", session: this._session, verified: true });
3355
+ } catch (err) {
3356
+ if (err?.name === "AbortError") return;
3357
+ console.warn("[PollarClient] resume failed (network); will retry", err);
3358
+ } finally {
3359
+ if (this._resumeController === controller) this._resumeController = null;
3360
+ }
3361
+ }
3154
3362
  async _storeSession(session) {
3155
3363
  console.info("[PollarClient] Session stored");
3364
+ const w = session.wallet;
3156
3365
  const persisted = {
3157
3366
  clientSessionId: session.clientSessionId,
3158
3367
  userId: session.userId ?? null,
3159
3368
  status: session.status,
3160
3369
  token: session.token,
3161
3370
  user: session.user,
3162
- wallet: session.wallet
3371
+ // The wire response still carries the legacy `publicKey` alias (kept for
3372
+ // older SDKs); the persisted session standardizes on `address` only.
3373
+ wallet: {
3374
+ type: w.type,
3375
+ address: w.address ?? w.publicKey ?? null,
3376
+ ...w.existsOnStellar !== void 0 ? { existsOnStellar: w.existsOnStellar } : {},
3377
+ ...w.createdAt !== void 0 ? { createdAt: w.createdAt } : {},
3378
+ ...w.linkedAt !== void 0 ? { linkedAt: w.linkedAt } : {},
3379
+ ...w.network !== void 0 ? { network: w.network } : {},
3380
+ ...w.deployTxHash !== void 0 ? { deployTxHash: w.deployTxHash } : {}
3381
+ }
3163
3382
  };
3164
3383
  this._session = persisted;
3165
3384
  if (session.data) {
@@ -3172,7 +3391,7 @@ var PollarClient = class {
3172
3391
  };
3173
3392
  }
3174
3393
  await writeStorage(this._storage, this.apiKeyHash, persisted);
3175
- this._setAuthState({ step: "authenticated", session: persisted });
3394
+ this._setAuthState({ step: "authenticated", session: persisted, verified: true });
3176
3395
  this._scheduleNextRefresh();
3177
3396
  }
3178
3397
  async _clearSession() {
@@ -3224,6 +3443,196 @@ var PollarClient = class {
3224
3443
  }
3225
3444
  };
3226
3445
 
3446
+ // src/keys/web-crypto.ts
3447
+ var DB_NAME = "pollar-keys";
3448
+ var DB_VERSION = 1;
3449
+ var STORE_NAME = "keys";
3450
+ function openDb() {
3451
+ return new Promise((resolve, reject) => {
3452
+ if (typeof indexedDB === "undefined") {
3453
+ reject(new Error("[PollarClient:keys] IndexedDB not available"));
3454
+ return;
3455
+ }
3456
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
3457
+ req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB open failed"));
3458
+ req.onsuccess = () => resolve(req.result);
3459
+ req.onupgradeneeded = () => {
3460
+ const db = req.result;
3461
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
3462
+ db.createObjectStore(STORE_NAME);
3463
+ }
3464
+ };
3465
+ });
3466
+ }
3467
+ function awaitTx(req) {
3468
+ return new Promise((resolve, reject) => {
3469
+ req.onsuccess = () => resolve(req.result);
3470
+ req.onerror = () => reject(req.error ?? new Error("[PollarClient:keys] IDB request failed"));
3471
+ });
3472
+ }
3473
+ async function dbGet(key) {
3474
+ const db = await openDb();
3475
+ try {
3476
+ const tx = db.transaction(STORE_NAME, "readonly");
3477
+ const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
3478
+ return result;
3479
+ } finally {
3480
+ db.close();
3481
+ }
3482
+ }
3483
+ async function dbPut(key, value) {
3484
+ const db = await openDb();
3485
+ try {
3486
+ const tx = db.transaction(STORE_NAME, "readwrite");
3487
+ await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
3488
+ } finally {
3489
+ db.close();
3490
+ }
3491
+ }
3492
+ async function dbDelete(key) {
3493
+ const db = await openDb();
3494
+ try {
3495
+ const tx = db.transaction(STORE_NAME, "readwrite");
3496
+ await awaitTx(tx.objectStore(STORE_NAME).delete(key));
3497
+ } finally {
3498
+ db.close();
3499
+ }
3500
+ }
3501
+ function isCryptoKeyPair(v) {
3502
+ if (typeof v !== "object" || v === null) return false;
3503
+ const obj = v;
3504
+ return obj.privateKey !== void 0 && obj.publicKey !== void 0;
3505
+ }
3506
+ var WebCryptoKeyManager = class {
3507
+ constructor(apiKey) {
3508
+ this.apiKeyHash = null;
3509
+ this.keyPair = null;
3510
+ this.publicJwk = null;
3511
+ this.thumbprint = null;
3512
+ /**
3513
+ * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
3514
+ * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
3515
+ * failure so callers can retry, and cleared on `reset()`.
3516
+ */
3517
+ this._initPromise = null;
3518
+ if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
3519
+ throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
3520
+ }
3521
+ this.apiKey = apiKey;
3522
+ }
3523
+ /**
3524
+ * Idempotent and safe under concurrency. The first call kicks off the real
3525
+ * init; subsequent (and concurrent) calls return the same in-flight promise.
3526
+ * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
3527
+ * the manager is self-healing if `init()` was never explicitly invoked.
3528
+ */
3529
+ async init() {
3530
+ if (this.keyPair) return;
3531
+ if (!this._initPromise) {
3532
+ this._initPromise = this._doInit().catch((err) => {
3533
+ console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
3534
+ this._initPromise = null;
3535
+ throw err;
3536
+ });
3537
+ }
3538
+ return this._initPromise;
3539
+ }
3540
+ async _doInit() {
3541
+ if (!this.apiKeyHash) {
3542
+ this.apiKeyHash = await hashApiKey(this.apiKey);
3543
+ }
3544
+ let pair;
3545
+ try {
3546
+ pair = await dbGet(this.apiKeyHash);
3547
+ if (pair && !isCryptoKeyPair(pair)) pair = void 0;
3548
+ } catch {
3549
+ pair = void 0;
3550
+ }
3551
+ if (!pair) {
3552
+ pair = await globalThis.crypto.subtle.generateKey(
3553
+ { name: "ECDSA", namedCurve: "P-256" },
3554
+ // false → private key non-extractable; per W3C ECDSA spec the public
3555
+ // key is always extractable regardless of this flag.
3556
+ false,
3557
+ ["sign", "verify"]
3558
+ );
3559
+ try {
3560
+ await dbPut(this.apiKeyHash, pair);
3561
+ } catch {
3562
+ }
3563
+ }
3564
+ this.keyPair = pair;
3565
+ this.publicJwk = await this._exportPublicJwk(pair.publicKey);
3566
+ this.thumbprint = await computeJwkThumbprint(this.publicJwk);
3567
+ }
3568
+ /**
3569
+ * Derive the public JWK from a `CryptoKey`. Prefers the `'raw'` export (the
3570
+ * 65-byte uncompressed point `0x04 || X(32) || Y(32)`) and base64url-encodes
3571
+ * the coordinates ourselves — that sidesteps polyfills whose `exportKey('jwk')`
3572
+ * emits non-base64url `x`/`y` (standard base64, `=` padding, or — as seen with
3573
+ * `react-native-quick-crypto` — a stray `.`). Real browsers and most polyfills
3574
+ * support `'raw'` for public EC keys.
3575
+ *
3576
+ * Falls back to the `'jwk'` export (normalized via `canonicalEcJwk`) if `'raw'`
3577
+ * is unsupported or returns an unexpected shape, so this can't regress on a
3578
+ * runtime that only implements the JWK path. Both routes yield identical
3579
+ * coordinate bytes, so the `cnf.jkt` thumbprint is unchanged either way.
3580
+ */
3581
+ async _exportPublicJwk(publicKey) {
3582
+ try {
3583
+ const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", publicKey));
3584
+ if (raw.length !== 65 || raw[0] !== 4) {
3585
+ throw new Error(`[PollarClient:keys] Unexpected raw EC point (len=${raw.length}, tag=${raw[0]})`);
3586
+ }
3587
+ return {
3588
+ kty: "EC",
3589
+ crv: "P-256",
3590
+ x: base64urlEncode(raw.slice(1, 33)),
3591
+ y: base64urlEncode(raw.slice(33, 65))
3592
+ };
3593
+ } catch {
3594
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
3595
+ return canonicalEcJwk(jwk);
3596
+ }
3597
+ }
3598
+ async reset() {
3599
+ try {
3600
+ if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
3601
+ } catch {
3602
+ }
3603
+ this.keyPair = null;
3604
+ this.publicJwk = null;
3605
+ this.thumbprint = null;
3606
+ this._initPromise = null;
3607
+ }
3608
+ async getPublicJwk() {
3609
+ if (!this.publicJwk) await this.init();
3610
+ if (!this.publicJwk) {
3611
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
3612
+ }
3613
+ return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
3614
+ }
3615
+ async getThumbprint() {
3616
+ if (!this.thumbprint) await this.init();
3617
+ if (!this.thumbprint) {
3618
+ throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
3619
+ }
3620
+ return this.thumbprint;
3621
+ }
3622
+ async sign(payload) {
3623
+ if (!this.keyPair) await this.init();
3624
+ if (!this.keyPair) {
3625
+ throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
3626
+ }
3627
+ const sig = await globalThis.crypto.subtle.sign(
3628
+ { name: "ECDSA", hash: "SHA-256" },
3629
+ this.keyPair.privateKey,
3630
+ payload
3631
+ );
3632
+ return new Uint8Array(sig);
3633
+ }
3634
+ };
3635
+
3227
3636
  // src/stellar/StellarClient.ts
3228
3637
  var HORIZON_URLS = {
3229
3638
  mainnet: "https://horizon.stellar.org",
@@ -3254,13 +3663,9 @@ var StellarClient = class {
3254
3663
 
3255
3664
  // src/index.rn.ts
3256
3665
  _setDefaultKeyManagerFactory((storage, apiKey) => {
3257
- const subtle = globalThis.crypto?.subtle;
3258
- if (subtle && typeof subtle.generateKey === "function" && typeof subtle.sign === "function") {
3259
- return new WebCryptoKeyManager(apiKey);
3260
- }
3261
3666
  return new NobleKeyManager(storage, apiKey);
3262
3667
  });
3263
3668
 
3264
- export { AUTH_ERROR_CODES, AlbedoAdapter, FreighterAdapter, NobleKeyManager, PollarClient, StellarClient, WalletType, WebCryptoKeyManager, buildProof, canonicalEcJwk, claimDistributionRule, computeJwkThumbprint, createLocalStorageAdapter, createMemoryAdapter, createOffRamp, createOnRamp, defaultKeyManager, defaultStorage, getKycProviders, getKycStatus, getRampTransaction, getRampsQuote, isValidSession, listDistributionRules, normalizeHtu, pollKycStatus, pollRampTransaction, resolveKyc, startKyc };
3669
+ export { AUTH_ERROR_CODES, AlbedoAdapter, FreighterAdapter, NobleKeyManager, POLLAR_CORE_VERSION, PollarClient, StellarClient, WalletType, WebCryptoKeyManager, buildProof, canonicalEcJwk, claimDistributionRule, computeJwkThumbprint, createLocalStorageAdapter, createMemoryAdapter, createOffRamp, createOnRamp, defaultKeyManager, defaultStorage, getKycProviders, getKycStatus, getRampTransaction, getRampsQuote, isValidSession, listDistributionRules, normalizeHtu, pollKycStatus, pollRampTransaction, resolveKyc, startKyc };
3265
3670
  //# sourceMappingURL=index.rn.mjs.map
3266
3671
  //# sourceMappingURL=index.rn.mjs.map