@pollar/core 0.8.2 → 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
@@ -265,11 +265,14 @@ async function computeJwkThumbprint(jwk) {
265
265
  const digest = await sha256(new TextEncoder().encode(canonical));
266
266
  return base64urlEncode(digest);
267
267
  }
268
+ function toBase64url(value) {
269
+ return value.replace(/\+/g, "-").replace(/\//g, "_").replace(/[^A-Za-z0-9_-]/g, "");
270
+ }
268
271
  function canonicalEcJwk(jwk) {
269
272
  if (jwk.kty !== "EC" || jwk.crv !== "P-256" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
270
273
  throw new Error("[PollarClient:thumbprint] Source JWK is not an EC P-256 public key");
271
274
  }
272
- 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) };
273
276
  }
274
277
 
275
278
  // src/keys/noble.ts
@@ -375,167 +378,6 @@ var NobleKeyManager = class {
375
378
  }
376
379
  };
377
380
 
378
- // src/keys/web-crypto.ts
379
- var DB_NAME = "pollar-keys";
380
- var DB_VERSION = 1;
381
- var STORE_NAME = "keys";
382
- function openDb() {
383
- return new Promise((resolve, reject) => {
384
- if (typeof indexedDB === "undefined") {
385
- reject(new Error("[PollarClient:keys] IndexedDB not available"));
386
- return;
387
- }
388
- const req = indexedDB.open(DB_NAME, DB_VERSION);
389
- req.onerror = () => reject(req.error ?? new Error("IDB open failed"));
390
- req.onsuccess = () => resolve(req.result);
391
- req.onupgradeneeded = () => {
392
- const db = req.result;
393
- if (!db.objectStoreNames.contains(STORE_NAME)) {
394
- db.createObjectStore(STORE_NAME);
395
- }
396
- };
397
- });
398
- }
399
- function awaitTx(req) {
400
- return new Promise((resolve, reject) => {
401
- req.onsuccess = () => resolve(req.result);
402
- req.onerror = () => reject(req.error ?? new Error("IDB request failed"));
403
- });
404
- }
405
- async function dbGet(key) {
406
- const db = await openDb();
407
- try {
408
- const tx = db.transaction(STORE_NAME, "readonly");
409
- const result = await awaitTx(tx.objectStore(STORE_NAME).get(key));
410
- return result;
411
- } finally {
412
- db.close();
413
- }
414
- }
415
- async function dbPut(key, value) {
416
- const db = await openDb();
417
- try {
418
- const tx = db.transaction(STORE_NAME, "readwrite");
419
- await awaitTx(tx.objectStore(STORE_NAME).put(value, key));
420
- } finally {
421
- db.close();
422
- }
423
- }
424
- async function dbDelete(key) {
425
- const db = await openDb();
426
- try {
427
- const tx = db.transaction(STORE_NAME, "readwrite");
428
- await awaitTx(tx.objectStore(STORE_NAME).delete(key));
429
- } finally {
430
- db.close();
431
- }
432
- }
433
- function isCryptoKeyPair(v) {
434
- if (typeof v !== "object" || v === null) return false;
435
- const obj = v;
436
- return obj.privateKey !== void 0 && obj.publicKey !== void 0;
437
- }
438
- var WebCryptoKeyManager = class {
439
- constructor(apiKey) {
440
- this.apiKeyHash = null;
441
- this.keyPair = null;
442
- this.publicJwk = null;
443
- this.thumbprint = null;
444
- /**
445
- * Cached in-flight init. Lets `init()` be called concurrently (or implicitly
446
- * from `getPublicJwk` / `sign`) without doing the work twice. Cleared on
447
- * failure so callers can retry, and cleared on `reset()`.
448
- */
449
- this._initPromise = null;
450
- if (typeof globalThis.crypto === "undefined" || !globalThis.crypto.subtle) {
451
- throw new Error("[PollarClient:keys] SubtleCrypto is unavailable. DPoP requires a secure context (HTTPS or localhost).");
452
- }
453
- this.apiKey = apiKey;
454
- }
455
- /**
456
- * Idempotent and safe under concurrency. The first call kicks off the real
457
- * init; subsequent (and concurrent) calls return the same in-flight promise.
458
- * Other methods (`getPublicJwk`, `getThumbprint`, `sign`) auto-await this so
459
- * the manager is self-healing if `init()` was never explicitly invoked.
460
- */
461
- async init() {
462
- if (this.keyPair) return;
463
- if (!this._initPromise) {
464
- this._initPromise = this._doInit().catch((err) => {
465
- console.error("[PollarClient:keys] WebCryptoKeyManager init failed", err);
466
- this._initPromise = null;
467
- throw err;
468
- });
469
- }
470
- return this._initPromise;
471
- }
472
- async _doInit() {
473
- if (!this.apiKeyHash) {
474
- this.apiKeyHash = await hashApiKey(this.apiKey);
475
- }
476
- let pair;
477
- try {
478
- pair = await dbGet(this.apiKeyHash);
479
- if (pair && !isCryptoKeyPair(pair)) pair = void 0;
480
- } catch {
481
- pair = void 0;
482
- }
483
- if (!pair) {
484
- pair = await globalThis.crypto.subtle.generateKey(
485
- { name: "ECDSA", namedCurve: "P-256" },
486
- // false → private key non-extractable; per W3C ECDSA spec the public
487
- // key is always extractable regardless of this flag.
488
- false,
489
- ["sign", "verify"]
490
- );
491
- try {
492
- await dbPut(this.apiKeyHash, pair);
493
- } catch {
494
- }
495
- }
496
- this.keyPair = pair;
497
- const exported = await globalThis.crypto.subtle.exportKey("jwk", pair.publicKey);
498
- this.publicJwk = canonicalEcJwk(exported);
499
- this.thumbprint = await computeJwkThumbprint(this.publicJwk);
500
- }
501
- async reset() {
502
- try {
503
- if (this.apiKeyHash) await dbDelete(this.apiKeyHash);
504
- } catch {
505
- }
506
- this.keyPair = null;
507
- this.publicJwk = null;
508
- this.thumbprint = null;
509
- this._initPromise = null;
510
- }
511
- async getPublicJwk() {
512
- if (!this.publicJwk) await this.init();
513
- if (!this.publicJwk) {
514
- throw new Error("[PollarClient:keys] Keypair initialization failed; getPublicJwk unavailable");
515
- }
516
- return { kty: this.publicJwk.kty, crv: this.publicJwk.crv, x: this.publicJwk.x, y: this.publicJwk.y };
517
- }
518
- async getThumbprint() {
519
- if (!this.thumbprint) await this.init();
520
- if (!this.thumbprint) {
521
- throw new Error("[PollarClient:keys] Keypair initialization failed; getThumbprint unavailable");
522
- }
523
- return this.thumbprint;
524
- }
525
- async sign(payload) {
526
- if (!this.keyPair) await this.init();
527
- if (!this.keyPair) {
528
- throw new Error("[PollarClient:keys] Keypair initialization failed; sign unavailable");
529
- }
530
- const sig = await globalThis.crypto.subtle.sign(
531
- { name: "ECDSA", hash: "SHA-256" },
532
- this.keyPair.privateKey,
533
- payload
534
- );
535
- return new Uint8Array(sig);
536
- }
537
- };
538
-
539
381
  // ../../node_modules/openapi-fetch/dist/index.mjs
540
382
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
541
383
  var supportsRequestInitExt = () => {
@@ -1255,6 +1097,9 @@ function defaultStorage(options = {}) {
1255
1097
  return createLocalStorageAdapter(options);
1256
1098
  }
1257
1099
 
1100
+ // src/version.ts
1101
+ var POLLAR_CORE_VERSION = "0.9.0-rc.0" ;
1102
+
1258
1103
  // src/visibility/noop.ts
1259
1104
  function createNoopVisibilityProvider() {
1260
1105
  return {
@@ -1319,6 +1164,7 @@ var AUTH_ERROR_CODES = {
1319
1164
  WALLET_CONNECT_FAILED: "WALLET_CONNECT_FAILED",
1320
1165
  WALLET_AUTH_FAILED: "WALLET_AUTH_FAILED",
1321
1166
  WALLET_RESOLVER_TIMEOUT: "WALLET_RESOLVER_TIMEOUT",
1167
+ PASSKEY_FAILED: "PASSKEY_FAILED",
1322
1168
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR"
1323
1169
  };
1324
1170
  var PollarFlowError = class extends Error {
@@ -1364,7 +1210,7 @@ var FreighterAdapter = class {
1364
1210
  if (!userInfo?.publicKey) {
1365
1211
  throw new Error("Failed to get user information from Freighter");
1366
1212
  }
1367
- return { address: userInfo.publicKey, publicKey: userInfo.publicKey };
1213
+ return { address: userInfo.publicKey };
1368
1214
  }
1369
1215
  async disconnect() {
1370
1216
  }
@@ -1402,6 +1248,19 @@ var FreighterAdapter = class {
1402
1248
  };
1403
1249
 
1404
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
+ }
1405
1264
  function openAlbedoPopup(url) {
1406
1265
  const popup = window.open(url, "albedo", "width=420,height=720,resizable=yes,scrollbars=yes");
1407
1266
  if (!popup) {
@@ -1440,7 +1299,13 @@ function waitForAlbedoResult() {
1440
1299
  });
1441
1300
  }
1442
1301
  var AlbedoAdapter = class {
1443
- 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;
1444
1309
  this.type = "albedo" /* ALBEDO */;
1445
1310
  }
1446
1311
  async isAvailable() {
@@ -1450,7 +1315,7 @@ var AlbedoAdapter = class {
1450
1315
  const url = new URL("https://albedo.link");
1451
1316
  url.searchParams.set("intent", "public-key");
1452
1317
  url.searchParams.set("app_name", "Pollar");
1453
- url.searchParams.set("network", "testnet");
1318
+ url.searchParams.set("network", this.network);
1454
1319
  url.searchParams.set("callback", `${window.location.origin}/albedo-callback`);
1455
1320
  url.searchParams.set("origin", window.location.origin);
1456
1321
  openAlbedoPopup(url.toString());
@@ -1458,7 +1323,7 @@ var AlbedoAdapter = class {
1458
1323
  if (!result.pubkey) {
1459
1324
  throw new Error("Albedo connection rejected");
1460
1325
  }
1461
- return { address: result.pubkey, publicKey: result.pubkey };
1326
+ return { address: result.pubkey };
1462
1327
  }
1463
1328
  async disconnect() {
1464
1329
  }
@@ -1468,12 +1333,12 @@ var AlbedoAdapter = class {
1468
1333
  async getNetwork() {
1469
1334
  throw new Error("Albedo does not expose network");
1470
1335
  }
1471
- async signTransaction(xdr, _options) {
1336
+ async signTransaction(xdr, options) {
1472
1337
  const url = new URL("https://albedo.link");
1473
1338
  url.searchParams.set("intent", "tx");
1474
1339
  url.searchParams.set("xdr", xdr);
1475
1340
  url.searchParams.set("app_name", "Pollar");
1476
- url.searchParams.set("network", "testnet");
1341
+ url.searchParams.set("network", albedoNetwork(options, this.network));
1477
1342
  url.searchParams.set("callback", window.location.href);
1478
1343
  url.searchParams.set("origin", window.location.origin);
1479
1344
  window.location.href = url.toString();
@@ -1486,7 +1351,7 @@ var AlbedoAdapter = class {
1486
1351
  url.searchParams.set("intent", "sign-auth-entry");
1487
1352
  url.searchParams.set("xdr", entryXdr);
1488
1353
  url.searchParams.set("app_name", "Pollar");
1489
- url.searchParams.set("network", "testnet");
1354
+ url.searchParams.set("network", this.network);
1490
1355
  url.searchParams.set("callback", window.location.href);
1491
1356
  url.searchParams.set("origin", window.location.origin);
1492
1357
  window.location.href = url.toString();
@@ -1573,8 +1438,12 @@ function isValidSession(value) {
1573
1438
  return false;
1574
1439
  }
1575
1440
  const w = wallet;
1576
- if (w["publicKey"] !== null && !isBoundedString(w["publicKey"], MAX_WALLET_PUBLIC_KEY)) {
1577
- 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");
1578
1447
  return false;
1579
1448
  }
1580
1449
  if (w["existsOnStellar"] !== void 0 && typeof w["existsOnStellar"] !== "boolean") {
@@ -1585,6 +1454,10 @@ function isValidSession(value) {
1585
1454
  console.warn("[PollarClient:session] Invalid session \u2014 wallet.createdAt must be a finite number if present");
1586
1455
  return false;
1587
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
+ }
1588
1461
  return true;
1589
1462
  }
1590
1463
  async function readStorage(storage, apiKeyHash) {
@@ -1592,6 +1465,12 @@ async function readStorage(storage, apiKeyHash) {
1592
1465
  if (!raw) return null;
1593
1466
  try {
1594
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
+ }
1595
1474
  if (!isValidSession(session)) {
1596
1475
  await storage.remove(sessionStorageKey(apiKeyHash));
1597
1476
  console.warn("[PollarClient:session] Stored session is invalid \u2014 clearing storage");
@@ -1682,7 +1561,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1682
1561
  }));
1683
1562
  } catch (e) {
1684
1563
  if (e instanceof Error && e.name === "AbortError") throw e;
1685
- console.warn(e);
1564
+ console.warn("[PollarClient:stream] session-status request failed; will retry", e);
1686
1565
  }
1687
1566
  if (error || !data) {
1688
1567
  await sleep(backoff);
@@ -1722,7 +1601,7 @@ async function streamUntilFound(api, clientSessionId, check, retryDelayMs = 200,
1722
1601
  } catch (e) {
1723
1602
  if (e instanceof Error && e.name === "AbortError") throw e;
1724
1603
  if (e instanceof SessionStatusError) throw e;
1725
- console.warn(e);
1604
+ console.warn("[PollarClient:stream] session-status stream read failed; will retry", e);
1726
1605
  } finally {
1727
1606
  reader.releaseLock();
1728
1607
  }
@@ -1750,7 +1629,7 @@ async function pollUntilFound(baseUrl, clientSessionId, check, intervalMs = 500,
1750
1629
  envelope = await response.json().catch(() => null);
1751
1630
  } catch (e) {
1752
1631
  if (e instanceof Error && e.name === "AbortError") throw e;
1753
- console.warn(e);
1632
+ console.warn("[PollarClient:stream] session-status poll failed; will retry", e);
1754
1633
  }
1755
1634
  if (httpStatus === 404 || envelope?.code === "INVALID_CLIENT_SESSION_ID") {
1756
1635
  throw new SessionStatusError("INVALID_CLIENT_SESSION_ID");
@@ -1954,6 +1833,55 @@ async function loginOAuth(provider, deps) {
1954
1833
  await authenticate(clientSessionId, deps);
1955
1834
  }
1956
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
+
1957
1885
  // src/client/auth/walletFlow.ts
1958
1886
  function withSignal(promise, signal) {
1959
1887
  return Promise.race([
@@ -1980,12 +1908,12 @@ async function loginWallet(type, deps) {
1980
1908
  setAuthState({ step: "wallet_not_installed", walletType: type });
1981
1909
  return;
1982
1910
  }
1983
- const { publicKey } = await withSignal(adapter.connect(), signal);
1984
- connectedWallet = publicKey;
1911
+ const { address } = await withSignal(adapter.connect(), signal);
1912
+ connectedWallet = address;
1985
1913
  deps.storeWalletAdapter(adapter, type);
1986
1914
  setAuthState({ step: "authenticating_wallet" });
1987
1915
  const { data: walletData, error: walletError } = await api.POST("/auth/wallet", {
1988
- body: { clientSessionId, walletAddress: publicKey },
1916
+ body: { clientSessionId, walletAddress: address },
1989
1917
  signal
1990
1918
  });
1991
1919
  if (walletError || !walletData?.success) {
@@ -2050,8 +1978,12 @@ var PollarClient = class {
2050
1978
  this._transactionStateListeners = /* @__PURE__ */ new Set();
2051
1979
  this._txHistoryState = { step: "idle" };
2052
1980
  this._txHistoryStateListeners = /* @__PURE__ */ new Set();
1981
+ this._sessionsState = { step: "idle" };
1982
+ this._sessionsStateListeners = /* @__PURE__ */ new Set();
2053
1983
  this._walletBalanceState = { step: "idle" };
2054
1984
  this._walletBalanceStateListeners = /* @__PURE__ */ new Set();
1985
+ this._enabledAssetsState = { step: "idle" };
1986
+ this._enabledAssetsStateListeners = /* @__PURE__ */ new Set();
2055
1987
  this._authState = { step: "idle" };
2056
1988
  this._authStateListeners = /* @__PURE__ */ new Set();
2057
1989
  this._networkState = { step: "idle" };
@@ -2067,6 +1999,8 @@ var PollarClient = class {
2067
1999
  this._storageDegradeListeners = /* @__PURE__ */ new Set();
2068
2000
  this._walletAdapter = null;
2069
2001
  this._loginController = null;
2002
+ /** Aborts an in-flight `/auth/session/resume` on destroy() or re-trigger. */
2003
+ this._resumeController = null;
2070
2004
  this.apiKey = config.apiKey;
2071
2005
  this.id = randomUUID();
2072
2006
  this.basePath = `${config.baseUrl || "https://sdk.api.pollar.xyz"}/v1`;
@@ -2079,6 +2013,8 @@ var PollarClient = class {
2079
2013
  this._keyManager = config.keyManager ?? defaultKeyManager(this._storage, config.apiKey);
2080
2014
  this._walletAdapterResolver = config.walletAdapter ?? null;
2081
2015
  this._walletResolverTimeoutMs = config.walletResolverTimeoutMs ?? 5e3;
2016
+ this._passkey = config.passkey ?? null;
2017
+ this._passkeySign = config.passkeySign ?? null;
2082
2018
  this._deviceLabel = config.deviceLabel;
2083
2019
  this._visibilityProvider = config.visibilityProvider ?? defaultVisibilityProvider();
2084
2020
  this._maxIdleMs = config.maxIdleMs;
@@ -2092,7 +2028,9 @@ var PollarClient = class {
2092
2028
  this._initialized = Promise.resolve();
2093
2029
  return;
2094
2030
  }
2095
- 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
+ );
2096
2034
  this._initialized = this._initialize();
2097
2035
  }
2098
2036
  /**
@@ -2130,7 +2068,11 @@ var PollarClient = class {
2130
2068
  }
2131
2069
  await this._restoreSession();
2132
2070
  this._visibilityUnsubscribe = this._visibilityProvider.onChange((visible) => {
2133
- 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
+ }
2134
2076
  });
2135
2077
  }
2136
2078
  /** Detach the cross-tab storage listener and abort any in-flight login. */
@@ -2141,6 +2083,8 @@ var PollarClient = class {
2141
2083
  }
2142
2084
  this._loginController?.abort();
2143
2085
  this._loginController = null;
2086
+ this._resumeController?.abort();
2087
+ this._resumeController = null;
2144
2088
  this._clearRefreshTimer();
2145
2089
  if (this._visibilityUnsubscribe) {
2146
2090
  this._visibilityUnsubscribe();
@@ -2155,7 +2099,9 @@ var PollarClient = class {
2155
2099
  request.headers.set("x-pollar-api-key", self.apiKey);
2156
2100
  self._lastRequestAt = Date.now();
2157
2101
  await self._initialized;
2158
- if (request.body !== null) {
2102
+ const cacheMethod = request.method.toUpperCase();
2103
+ const cacheBodyAllowed = cacheMethod !== "GET" && cacheMethod !== "HEAD";
2104
+ if (cacheBodyAllowed && request.body != null) {
2159
2105
  try {
2160
2106
  self._requestBodyCache.set(request, await request.clone().arrayBuffer());
2161
2107
  } catch (err) {
@@ -2242,7 +2188,9 @@ var PollarClient = class {
2242
2188
  }
2243
2189
  }
2244
2190
  }
2245
- 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;
2246
2194
  const retried = new Request(originalRequest.url, {
2247
2195
  method: originalRequest.method,
2248
2196
  headers,
@@ -2286,19 +2234,26 @@ var PollarClient = class {
2286
2234
  throw err;
2287
2235
  }
2288
2236
  if (error || !data) {
2289
- console.warn("[PollarClient] /auth/refresh returned error", { error });
2237
+ console.error("[PollarClient] /auth/refresh returned error", { error });
2290
2238
  await this._clearSession();
2291
2239
  throw new Error("Refresh failed");
2292
2240
  }
2293
2241
  const successData = data;
2294
2242
  if (!successData.success || !successData.content?.token) {
2295
- 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
+ });
2296
2247
  await this._clearSession();
2297
2248
  throw new Error("Refresh response malformed");
2298
2249
  }
2299
2250
  const newToken = successData.content.token;
2300
2251
  if (typeof newToken.accessToken !== "string" || typeof newToken.refreshToken !== "string" || typeof newToken.expiresAt !== "number") {
2301
- 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
+ });
2302
2257
  await this._clearSession();
2303
2258
  throw new Error("Refresh response token shape invalid");
2304
2259
  }
@@ -2492,6 +2447,19 @@ var PollarClient = class {
2492
2447
  const controller = this._newController();
2493
2448
  loginWallet(type, this._flowDeps(controller.signal)).catch((err) => this._handleFlowError(err));
2494
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
+ }
2495
2463
  // ─── Cancel ───────────────────────────────────────────────────────────────
2496
2464
  cancelLogin() {
2497
2465
  this._loginController?.abort();
@@ -2554,6 +2522,29 @@ var PollarClient = class {
2554
2522
  }
2555
2523
  return data.content.sessions;
2556
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
+ }
2557
2548
  /**
2558
2549
  * Revoke a specific refresh-token family (a single device session). Use
2559
2550
  * `listSessions` to enumerate the familyIds. Revoking the current session
@@ -2630,16 +2621,19 @@ var PollarClient = class {
2630
2621
  cb(this._walletBalanceState);
2631
2622
  return () => this._walletBalanceStateListeners.delete(cb);
2632
2623
  }
2633
- async refreshBalance(publicKey) {
2634
- const pk = publicKey ?? this._session?.wallet?.publicKey;
2635
- 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) {
2636
2631
  this._setWalletBalanceState({ step: "error", message: "No wallet connected" });
2637
2632
  return;
2638
2633
  }
2639
2634
  this._setWalletBalanceState({ step: "loading" });
2640
2635
  try {
2641
- const network = this.getNetwork();
2642
- const { data, error } = await this._api.GET("/wallet/balance", { params: { query: { publicKey: pk, network } } });
2636
+ const { data, error } = await this._api.GET("/wallet/balance");
2643
2637
  if (!error && data?.success && data.content) {
2644
2638
  this._setWalletBalanceState({ step: "loaded", data: data.content });
2645
2639
  } else {
@@ -2649,6 +2643,53 @@ var PollarClient = class {
2649
2643
  this._setWalletBalanceState({ step: "error", message: "Failed to load balance" });
2650
2644
  }
2651
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
+ }
2652
2693
  // ─── Transactions ─────────────────────────────────────────────────────────
2653
2694
  /**
2654
2695
  * Builds an unsigned XDR. Drives `_setTransactionState` for modal-style UIs
@@ -2656,14 +2697,14 @@ var PollarClient = class {
2656
2697
  * inspect the result without subscribing to state changes.
2657
2698
  */
2658
2699
  async buildTx(operation, params, options) {
2659
- if (!this._session?.wallet?.publicKey) {
2700
+ if (!this._session?.wallet?.address) {
2660
2701
  const details = "No wallet connected";
2661
2702
  this._setTransactionState({ step: "error", phase: "building", details });
2662
2703
  return { status: "error", details };
2663
2704
  }
2664
2705
  const body = {
2665
2706
  network: this.getNetwork(),
2666
- publicKey: this._session.wallet.publicKey,
2707
+ address: this._session.wallet.address,
2667
2708
  operation,
2668
2709
  params,
2669
2710
  options: options ?? {}
@@ -2704,7 +2745,7 @@ var PollarClient = class {
2704
2745
  const buildData = this._currentBuildData();
2705
2746
  this._setTransactionState({ step: "signing", ...buildData && { buildData } });
2706
2747
  if (this._walletAdapter) {
2707
- const accountToSign = this._session?.wallet?.publicKey;
2748
+ const accountToSign = this._session?.wallet?.address;
2708
2749
  const signOpts = accountToSign ? { networkPassphrase: this._networkPassphrase(), accountToSign } : { networkPassphrase: this._networkPassphrase() };
2709
2750
  try {
2710
2751
  const { signedTxXdr } = await this._walletAdapter.signTransaction(unsignedXdr, signOpts);
@@ -2725,10 +2766,10 @@ var PollarClient = class {
2725
2766
  return { status: "error", ...details && { details } };
2726
2767
  }
2727
2768
  }
2728
- const publicKey = this._session?.wallet?.publicKey ?? "";
2769
+ const address = this._session?.wallet?.address ?? "";
2729
2770
  try {
2730
2771
  const { data, error } = await this._api.POST("/tx/sign", {
2731
- body: { network: this.getNetwork(), publicKey, unsignedXdr }
2772
+ body: { network: this.getNetwork(), address, unsignedXdr }
2732
2773
  });
2733
2774
  if (!error && data?.success && data.content?.signedXdr) {
2734
2775
  const { signedXdr, idempotencyKey } = data.content;
@@ -2781,12 +2822,12 @@ var PollarClient = class {
2781
2822
  const buildData = this._currentBuildData();
2782
2823
  const outcomeExtra = buildData ? { buildData } : {};
2783
2824
  this._setTransactionState({ step: "submitting", signedXdr, ...buildData && { buildData } });
2784
- const publicKey = this._session?.wallet?.publicKey ?? "";
2825
+ const address = this._session?.wallet?.address ?? "";
2785
2826
  try {
2786
2827
  const { data, error } = await this._api.POST("/tx/submit", {
2787
2828
  body: {
2788
2829
  network: this.getNetwork(),
2789
- publicKey,
2830
+ address,
2790
2831
  signedXdr,
2791
2832
  ...opts?.submissionToken && { idempotencyKey: opts.submissionToken }
2792
2833
  }
@@ -2846,6 +2887,19 @@ var PollarClient = class {
2846
2887
  * `success` (ledger-confirmed), or `error[phase: 'signing-submitting']`.
2847
2888
  */
2848
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
+ }
2849
2903
  if (this._walletAdapter) {
2850
2904
  const signed = await this.signTx(unsignedXdr);
2851
2905
  if (signed.status === "error") {
@@ -2863,7 +2917,7 @@ var PollarClient = class {
2863
2917
  this._setTransactionState({ step: "signing-submitting", ...buildData && { buildData } });
2864
2918
  const body = {
2865
2919
  network: this.getNetwork(),
2866
- publicKey: this._session?.wallet?.publicKey ?? "",
2920
+ address: this._session?.wallet?.address ?? "",
2867
2921
  unsignedXdr
2868
2922
  };
2869
2923
  try {
@@ -2932,14 +2986,20 @@ var PollarClient = class {
2932
2986
  * `signTx`, and `submitTx` separately instead.
2933
2987
  */
2934
2988
  async buildAndSignAndSubmitTx(operation, params, options) {
2989
+ if (this._session?.wallet?.type === "smart") {
2990
+ return this._runSmartTx(operation, params, options);
2991
+ }
2935
2992
  if (this._walletAdapter) {
2936
2993
  const built = await this.buildTx(operation, params, options);
2937
2994
  if (built.status === "error") {
2938
2995
  return { status: "error", ...built.details && { details: built.details } };
2939
2996
  }
2997
+ if (!built.buildData.unsignedXdr) {
2998
+ return { status: "error", details: "build returned no unsigned transaction" };
2999
+ }
2940
3000
  return this.signAndSubmitTx(built.buildData.unsignedXdr);
2941
3001
  }
2942
- if (!this._session?.wallet?.publicKey) {
3002
+ if (!this._session?.wallet?.address) {
2943
3003
  this._setTransactionState({ step: "error", phase: "building-signing-submitting", details: "No wallet connected" });
2944
3004
  return { status: "error", details: "No wallet connected" };
2945
3005
  }
@@ -2948,7 +3008,7 @@ var PollarClient = class {
2948
3008
  const { data, error } = await this._api.POST("/tx/build-sign-submit", {
2949
3009
  body: {
2950
3010
  network: this.getNetwork(),
2951
- publicKey: this._session.wallet.publicKey,
3011
+ address: this._session.wallet.address,
2952
3012
  operation,
2953
3013
  params,
2954
3014
  options: options ?? {}
@@ -2992,6 +3052,113 @@ var PollarClient = class {
2992
3052
  async runTx(operation, params, options) {
2993
3053
  return this.buildAndSignAndSubmitTx(operation, params, options);
2994
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
+ }
2995
3162
  // ─── App config ───────────────────────────────────────────────────────────
2996
3163
  async getAppConfig() {
2997
3164
  try {
@@ -3045,10 +3212,18 @@ var PollarClient = class {
3045
3212
  this._txHistoryState = next;
3046
3213
  for (const cb of this._txHistoryStateListeners) cb(next);
3047
3214
  }
3215
+ _setSessionsState(next) {
3216
+ this._sessionsState = next;
3217
+ for (const cb of this._sessionsStateListeners) cb(next);
3218
+ }
3048
3219
  _setWalletBalanceState(next) {
3049
3220
  this._walletBalanceState = next;
3050
3221
  for (const cb of this._walletBalanceStateListeners) cb(next);
3051
3222
  }
3223
+ _setEnabledAssetsState(next) {
3224
+ this._enabledAssetsState = next;
3225
+ for (const cb of this._enabledAssetsStateListeners) cb(next);
3226
+ }
3052
3227
  // ─── Private ──────────────────────────────────────────────────────────────
3053
3228
  _newController() {
3054
3229
  this._loginController?.abort();
@@ -3073,6 +3248,7 @@ var PollarClient = class {
3073
3248
  this._walletAdapter = adapter;
3074
3249
  await writeWalletType(this._storage, this.apiKeyHash, id);
3075
3250
  },
3251
+ ...this._passkey ? { passkey: this._passkey } : {},
3076
3252
  ...this._deviceLabel ? { deviceLabel: this._deviceLabel } : {}
3077
3253
  };
3078
3254
  }
@@ -3102,7 +3278,7 @@ var PollarClient = class {
3102
3278
  }
3103
3279
  }
3104
3280
  if (id === "freighter" /* FREIGHTER */) return new FreighterAdapter();
3105
- if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter();
3281
+ if (id === "albedo" /* ALBEDO */) return new AlbedoAdapter(this.getNetwork() === "mainnet" ? "public" : "testnet");
3106
3282
  throw new Error(
3107
3283
  `[PollarClient] No wallet adapter configured for "${id}". Pass a walletAdapter resolver in PollarClientConfig.`
3108
3284
  );
@@ -3143,21 +3319,66 @@ var PollarClient = class {
3143
3319
  }
3144
3320
  }
3145
3321
  console.info("[PollarClient] Session restored from storage");
3146
- this._setAuthState({ step: "authenticated", session: this._session });
3322
+ this._setAuthState({ step: "authenticated", session: this._session, verified: false });
3147
3323
  this._scheduleNextRefresh();
3324
+ void this._resume();
3148
3325
  } else {
3149
3326
  console.info("[PollarClient] No session in storage");
3150
3327
  }
3151
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
+ }
3152
3362
  async _storeSession(session) {
3153
3363
  console.info("[PollarClient] Session stored");
3364
+ const w = session.wallet;
3154
3365
  const persisted = {
3155
3366
  clientSessionId: session.clientSessionId,
3156
3367
  userId: session.userId ?? null,
3157
3368
  status: session.status,
3158
3369
  token: session.token,
3159
3370
  user: session.user,
3160
- 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
+ }
3161
3382
  };
3162
3383
  this._session = persisted;
3163
3384
  if (session.data) {
@@ -3170,7 +3391,7 @@ var PollarClient = class {
3170
3391
  };
3171
3392
  }
3172
3393
  await writeStorage(this._storage, this.apiKeyHash, persisted);
3173
- this._setAuthState({ step: "authenticated", session: persisted });
3394
+ this._setAuthState({ step: "authenticated", session: persisted, verified: true });
3174
3395
  this._scheduleNextRefresh();
3175
3396
  }
3176
3397
  async _clearSession() {
@@ -3222,6 +3443,196 @@ var PollarClient = class {
3222
3443
  }
3223
3444
  };
3224
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
+
3225
3636
  // src/stellar/StellarClient.ts
3226
3637
  var HORIZON_URLS = {
3227
3638
  mainnet: "https://horizon.stellar.org",
@@ -3252,13 +3663,9 @@ var StellarClient = class {
3252
3663
 
3253
3664
  // src/index.rn.ts
3254
3665
  _setDefaultKeyManagerFactory((storage, apiKey) => {
3255
- const subtle = globalThis.crypto?.subtle;
3256
- if (subtle && typeof subtle.generateKey === "function" && typeof subtle.sign === "function") {
3257
- return new WebCryptoKeyManager(apiKey);
3258
- }
3259
3666
  return new NobleKeyManager(storage, apiKey);
3260
3667
  });
3261
3668
 
3262
- 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 };
3263
3670
  //# sourceMappingURL=index.rn.mjs.map
3264
3671
  //# sourceMappingURL=index.rn.mjs.map