@pooflabs/core 0.0.47 → 0.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -376,6 +376,28 @@ class WebSessionManager {
376
376
  static async storeSession(address, accessToken, idToken, refreshToken) {
377
377
  if (typeof window === "undefined")
378
378
  return;
379
+ // JWT-wallet binding: refuse to store a session whose idToken is bound
380
+ // to a different wallet than `address`. Prevents races that would otherwise
381
+ // leave localStorage with mismatched address/token state.
382
+ try {
383
+ const payloadB64 = idToken.split(".")[1];
384
+ if (payloadB64) {
385
+ const payload = JSON.parse(this.decodeBase64Url(payloadB64));
386
+ const tokenWallet = payload["custom:walletAddress"];
387
+ if (tokenWallet && tokenWallet !== address) {
388
+ throw new Error(`[WebSessionManager] Refusing to store session: address (${address}) does not match idToken custom:walletAddress (${tokenWallet})`);
389
+ }
390
+ if (!tokenWallet) {
391
+ console.warn("[WebSessionManager] storeSession: idToken has no custom:walletAddress claim — writing without validation");
392
+ }
393
+ }
394
+ }
395
+ catch (err) {
396
+ if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string" && err.message.includes("Refusing to store session")) {
397
+ throw err;
398
+ }
399
+ console.warn("[WebSessionManager] storeSession: failed to decode idToken for validation:", err);
400
+ }
379
401
  const config = await getConfig();
380
402
  const currentAppId = config.appId;
381
403
  localStorage.setItem(this.TAROBASE_SESSION_STORAGE_KEY, JSON.stringify({
@@ -4722,6 +4744,7 @@ async function getOrCreateConnection(appId, isServer) {
4722
4744
  subscriptions: new Map(),
4723
4745
  pendingSubscriptions: new Map(),
4724
4746
  pendingUnsubscriptions: new Map(),
4747
+ pendingRequests: new Map(),
4725
4748
  isConnecting: false,
4726
4749
  isConnected: false,
4727
4750
  appId,
@@ -4820,11 +4843,17 @@ async function getOrCreateConnection(appId, isServer) {
4820
4843
  clearInterval(connection.tokenRefreshTimer);
4821
4844
  connection.tokenRefreshTimer = null;
4822
4845
  }
4846
+ // Reject all pending WS CRUD requests on disconnect (fail fast)
4847
+ for (const [id, pending] of connection.pendingRequests) {
4848
+ clearTimeout(pending.timer);
4849
+ pending.reject(new Error('WebSocket disconnected'));
4850
+ }
4851
+ connection.pendingRequests.clear();
4823
4852
  });
4824
4853
  return connection;
4825
4854
  }
4826
4855
  function handleServerMessage(connection, message) {
4827
- var _a;
4856
+ var _a, _b;
4828
4857
  switch (message.type) {
4829
4858
  case 'subscribed': {
4830
4859
  const subscription = connection.subscriptions.get(message.subscriptionId);
@@ -4869,8 +4898,45 @@ function handleServerMessage(connection, message) {
4869
4898
  }
4870
4899
  break;
4871
4900
  }
4901
+ case 'response': {
4902
+ const pendingReq = connection.pendingRequests.get(message.requestId);
4903
+ if (pendingReq) {
4904
+ connection.pendingRequests.delete(message.requestId);
4905
+ clearTimeout(pendingReq.timer);
4906
+ if (message.status >= 400) {
4907
+ pendingReq.reject(new Error(`Request failed with status ${message.status}`));
4908
+ }
4909
+ else {
4910
+ pendingReq.resolve(message.data);
4911
+ }
4912
+ }
4913
+ break;
4914
+ }
4915
+ case 'setResponse': {
4916
+ const pendingSet = connection.pendingRequests.get(message.requestId);
4917
+ if (pendingSet) {
4918
+ connection.pendingRequests.delete(message.requestId);
4919
+ clearTimeout(pendingSet.timer);
4920
+ if (message.statusCode >= 400) {
4921
+ pendingSet.reject(new Error(((_a = message.body) === null || _a === void 0 ? void 0 : _a.message) || `Set failed with status ${message.statusCode}`));
4922
+ }
4923
+ else {
4924
+ pendingSet.resolve(message.body);
4925
+ }
4926
+ }
4927
+ break;
4928
+ }
4872
4929
  case 'error': {
4873
4930
  console.error('[WS v2] Server error:', message.code, message.message);
4931
+ // Handle CRUD request errors (requestId present)
4932
+ if (message.requestId) {
4933
+ const pendingReq = connection.pendingRequests.get(message.requestId);
4934
+ if (pendingReq) {
4935
+ connection.pendingRequests.delete(message.requestId);
4936
+ clearTimeout(pendingReq.timer);
4937
+ pendingReq.reject(new Error(`${message.code}: ${message.message}`));
4938
+ }
4939
+ }
4874
4940
  if (message.subscriptionId) {
4875
4941
  // Reject pending subscription if this is a subscription error
4876
4942
  const pending = connection.pendingSubscriptions.get(message.subscriptionId);
@@ -4882,7 +4948,7 @@ function handleServerMessage(connection, message) {
4882
4948
  const subscription = connection.subscriptions.get(message.subscriptionId);
4883
4949
  if (subscription) {
4884
4950
  for (const callback of subscription.callbacks) {
4885
- (_a = callback.onError) === null || _a === void 0 ? void 0 : _a.call(callback, new Error(`${message.code}: ${message.message}`));
4951
+ (_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, new Error(`${message.code}: ${message.message}`));
4886
4952
  }
4887
4953
  }
4888
4954
  }
@@ -5171,6 +5237,97 @@ async function doReconnectWithNewAuth() {
5171
5237
  }
5172
5238
  }
5173
5239
  }
5240
+ // ============ CRUD over WebSocket ============
5241
+ const WS_REQUEST_TIMEOUT_MS = 30000;
5242
+ function generateRequestId() {
5243
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5244
+ }
5245
+ /**
5246
+ * Returns true if there is an active v2 WebSocket connection open.
5247
+ */
5248
+ function hasActiveConnection() {
5249
+ for (const connection of connections.values()) {
5250
+ if (connection.ws && connection.isConnected) {
5251
+ return true;
5252
+ }
5253
+ }
5254
+ return false;
5255
+ }
5256
+ async function sendRequest(msgBuilder) {
5257
+ const config = await getConfig();
5258
+ const appId = config.appId;
5259
+ const connection = await getOrCreateConnection(appId, config.isServer);
5260
+ // Wait for the connection to be open (getOrCreateConnection may return
5261
+ // while still connecting).
5262
+ if (!connection.isConnected && connection.ws) {
5263
+ await new Promise((resolve, reject) => {
5264
+ const timeout = setTimeout(() => {
5265
+ var _a;
5266
+ (_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
5267
+ reject(new Error('WebSocket connection timeout'));
5268
+ }, 10000);
5269
+ const onOpen = () => { clearTimeout(timeout); resolve(); };
5270
+ if (connection.isConnected) {
5271
+ clearTimeout(timeout);
5272
+ resolve();
5273
+ return;
5274
+ }
5275
+ connection.ws.addEventListener('open', onOpen);
5276
+ });
5277
+ }
5278
+ if (!connection.ws || !connection.isConnected) {
5279
+ throw new Error('WebSocket connection not available');
5280
+ }
5281
+ const requestId = generateRequestId();
5282
+ const message = msgBuilder(requestId);
5283
+ return new Promise((resolve, reject) => {
5284
+ const timer = setTimeout(() => {
5285
+ connection.pendingRequests.delete(requestId);
5286
+ reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
5287
+ }, WS_REQUEST_TIMEOUT_MS);
5288
+ connection.pendingRequests.set(requestId, { resolve, reject, timer });
5289
+ try {
5290
+ connection.ws.send(JSON.stringify(message));
5291
+ }
5292
+ catch (error) {
5293
+ connection.pendingRequests.delete(requestId);
5294
+ clearTimeout(timer);
5295
+ reject(error);
5296
+ }
5297
+ });
5298
+ }
5299
+ async function wsGet(path) {
5300
+ return sendRequest((requestId) => ({
5301
+ type: 'get',
5302
+ requestId,
5303
+ path,
5304
+ }));
5305
+ }
5306
+ async function wsSet(documents) {
5307
+ return sendRequest((requestId) => ({
5308
+ type: 'set',
5309
+ requestId,
5310
+ documents,
5311
+ }));
5312
+ }
5313
+ async function wsQuery(path, opts) {
5314
+ return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
5315
+ path }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: opts.includeSubPaths } : {}))));
5316
+ }
5317
+ async function wsDelete(path) {
5318
+ return sendRequest((requestId) => ({
5319
+ type: 'delete',
5320
+ requestId,
5321
+ path,
5322
+ }));
5323
+ }
5324
+ async function wsGetMany(paths) {
5325
+ return sendRequest((requestId) => ({
5326
+ type: 'getMany',
5327
+ requestId,
5328
+ paths,
5329
+ }));
5330
+ }
5174
5331
 
5175
5332
  /**
5176
5333
  * WebSocket Subscription Module
@@ -5294,6 +5451,28 @@ class ReactNativeSessionManager {
5294
5451
  /* STORE */
5295
5452
  /* ------------------------------------------------------------------ */
5296
5453
  static async storeSession(address, accessToken, idToken, refreshToken) {
5454
+ // JWT-wallet binding: refuse to store a session whose idToken is bound
5455
+ // to a different wallet than `address`. Prevents races that would otherwise
5456
+ // leave storage with mismatched address/token state.
5457
+ try {
5458
+ const payloadB64 = idToken.split(".")[1];
5459
+ if (payloadB64) {
5460
+ const payload = JSON.parse(this.decodeBase64Url(payloadB64));
5461
+ const tokenWallet = payload["custom:walletAddress"];
5462
+ if (tokenWallet && tokenWallet !== address) {
5463
+ throw new Error(`[ReactNativeSessionManager] Refusing to store session: address (${address}) does not match idToken custom:walletAddress (${tokenWallet})`);
5464
+ }
5465
+ if (!tokenWallet) {
5466
+ console.warn("[ReactNativeSessionManager] storeSession: idToken has no custom:walletAddress claim — writing without validation");
5467
+ }
5468
+ }
5469
+ }
5470
+ catch (err) {
5471
+ if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string" && err.message.includes("Refusing to store session")) {
5472
+ throw err;
5473
+ }
5474
+ console.warn("[ReactNativeSessionManager] storeSession: failed to decode idToken for validation:", err);
5475
+ }
5297
5476
  const config = await getConfig();
5298
5477
  const currentAppId = config.appId;
5299
5478
  this.getStorage().setItem(this.TAROBASE_SESSION_STORAGE_KEY, JSON.stringify({
@@ -5403,5 +5582,5 @@ class ReactNativeSessionManager {
5403
5582
  }
5404
5583
  ReactNativeSessionManager.TAROBASE_SESSION_STORAGE_KEY = "tarobase_session_storage";
5405
5584
 
5406
- export { InsufficientBalanceError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithPrivy, createSessionWithSignature, genAuthNonce, genSolanaMessage, get, getCachedData, getConfig, getFiles, getIdToken, getMany, init, reconnectWithNewAuth, refreshSession, runExpression, runExpressionMany, runQuery, runQueryMany, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe };
5585
+ export { InsufficientBalanceError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithPrivy, createSessionWithSignature, genAuthNonce, genSolanaMessage, get, getCachedData, getConfig, getFiles, getIdToken, getMany, hasActiveConnection, init, reconnectWithNewAuth, refreshSession, runExpression, runExpressionMany, runQuery, runQueryMany, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, wsDelete, wsGet, wsGetMany, wsQuery, wsSet };
5407
5586
  //# sourceMappingURL=index.mjs.map