@bounded-sh/core 0.0.16 → 0.0.18

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
@@ -10,7 +10,6 @@ let clientConfig = {
10
10
  // User configured settings
11
11
  name: '',
12
12
  logoUrl: '',
13
- apiKey: '',
14
13
  // Bounded production is the out-of-the-box default — a Bounded app needs only
15
14
  // `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
16
15
  network: 'bounded-production',
@@ -87,7 +86,7 @@ function isBoundedNetwork() {
87
86
  }
88
87
  function init(newConfig) {
89
88
  return new Promise((resolve, reject) => {
90
- if (!newConfig.apiKey && !newConfig.appId) {
89
+ if (!newConfig.appId) {
91
90
  reject(new Error('No app ID provided.'));
92
91
  return;
93
92
  }
@@ -2863,9 +2862,25 @@ async function refreshSession(refreshToken, issuer) {
2863
2862
  })();
2864
2863
  return refreshInFlight$1;
2865
2864
  }
2865
+ class SessionRevokeError extends Error {
2866
+ constructor(message, cause) {
2867
+ super(message);
2868
+ this.name = 'SessionRevokeError';
2869
+ this.cause = cause;
2870
+ }
2871
+ }
2872
+ function revokeFailureMessage(err) {
2873
+ var _a, _b;
2874
+ const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
2875
+ const statusText = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.statusText;
2876
+ const suffix = typeof status === 'number'
2877
+ ? ` (HTTP ${status}${statusText ? ` ${statusText}` : ''})`
2878
+ : '';
2879
+ return `Failed to revoke refresh token server-side${suffix}. The refresh-token family may still be active.`;
2880
+ }
2866
2881
  /**
2867
- * Revoke a session's refresh-token family server-side (logout). Best-effort: if the
2868
- * call fails the local logout still proceeds. Routes to the minting issuer.
2882
+ * Revoke a session's refresh-token family server-side (logout). Routes to the
2883
+ * minting issuer and rejects if the revoke request fails.
2869
2884
  */
2870
2885
  async function revokeSession(refreshToken, issuer) {
2871
2886
  if (!refreshToken)
@@ -2878,8 +2893,8 @@ async function revokeSession(refreshToken, issuer) {
2878
2893
  appId: config.appId,
2879
2894
  }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
2880
2895
  }
2881
- catch (_a) {
2882
- // best-effort: logout must succeed even if the revoke call doesn't
2896
+ catch (err) {
2897
+ throw new SessionRevokeError(revokeFailureMessage(err), err);
2883
2898
  }
2884
2899
  }
2885
2900
  async function signSessionCreateMessage(_signMessageFunction) {
@@ -3968,9 +3983,7 @@ async function getUserInfo(isServer) {
3968
3983
  *
3969
3984
  * Mirrors the realtime-worker auth.ts identity resolution so the client-side
3970
3985
  * `user` object matches what the backend authenticates as:
3971
- * - id = custom:userId when present, else custom:walletAddress (the fallback
3972
- * keeps wallet/SIWS tokens — which omit userId — AND legacy Better Auth
3973
- * tokens — which put the account id in custom:walletAddress — working).
3986
+ * - id = custom:userId only.
3974
3987
  * - address = custom:walletAddress only (a REAL wallet). NEVER falls back to the
3975
3988
  * identity: an opaque id is not a spendable onchain address. null for
3976
3989
  * email-only sessions.
@@ -3995,7 +4008,7 @@ function deriveUserIdentityFromIdToken(idToken) {
3995
4008
  const userIdClaim = payload['custom:userId'];
3996
4009
  const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3997
4010
  ? userIdClaim
3998
- : address;
4011
+ : null;
3999
4012
  const emailClaim = payload['email'];
4000
4013
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
4001
4014
  ? emailClaim.toLowerCase()
@@ -4051,17 +4064,6 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
4051
4064
  await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
4052
4065
  }
4053
4066
 
4054
- var utils = /*#__PURE__*/Object.freeze({
4055
- __proto__: null,
4056
- createAuthHeader: createAuthHeader,
4057
- deriveUserIdentityFromIdToken: deriveUserIdentityFromIdToken,
4058
- getIdToken: getIdToken,
4059
- getRefreshToken: getRefreshToken,
4060
- getSessionIssuer: getSessionIssuer,
4061
- getUserInfo: getUserInfo,
4062
- updateIdTokenAndAccessToken: updateIdTokenAndAccessToken
4063
- });
4064
-
4065
4067
  const apiClient = axios.create();
4066
4068
  axiosRetry(apiClient, {
4067
4069
  retries: 2,
@@ -4126,7 +4128,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4126
4128
  const authHeader = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
4127
4129
  ? await _overrides._getAuthHeaders()
4128
4130
  : await createAuthHeader(config.isServer);
4129
- const headers = Object.assign({ "Content-Type": "application/json", "X-Public-App-Id": config.appId, "X-App-Id": config.appId }, authHeader);
4131
+ const headers = Object.assign({ "Content-Type": "application/json", "X-App-Id": config.appId }, authHeader);
4130
4132
  // Apply custom headers from _overrides
4131
4133
  if (_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers) {
4132
4134
  Object.assign(headers, _overrides.headers);
@@ -4230,6 +4232,7 @@ const pendingRequests = {};
4230
4232
  const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
4231
4233
  // Last time we cleaned up the cache
4232
4234
  let lastCacheCleanup = Date.now();
4235
+ let uncacheableReadKeyCounter = 0;
4233
4236
  /**
4234
4237
  * Return the leaf document key (last path segment) for a document path.
4235
4238
  *
@@ -4315,57 +4318,74 @@ function normalizeReadResult(responseData, pathIsDocument) {
4315
4318
  }
4316
4319
  return responseData;
4317
4320
  }
4318
- function hashForKey$2(value) {
4321
+ function hashForKey$1(value) {
4319
4322
  let h = 5381;
4320
4323
  for (let i = 0; i < value.length; i++) {
4321
4324
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4322
4325
  }
4323
4326
  return h.toString(36);
4324
4327
  }
4328
+ function hasAuthHeader(headers) {
4329
+ if (!headers)
4330
+ return false;
4331
+ return (Object.prototype.hasOwnProperty.call(headers, 'Authorization') ||
4332
+ Object.prototype.hasOwnProperty.call(headers, 'authorization'));
4333
+ }
4325
4334
  function authValueFromHeaders(headers) {
4326
4335
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4327
4336
  }
4328
4337
  function principalFromAuthValue(authValue) {
4329
- return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
4338
+ return authValue ? `h${hashForKey$1(authValue)}` : null;
4330
4339
  }
4331
4340
  function principalFromIdToken$1(idToken) {
4332
- return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4341
+ return idToken ? `t${hashForKey$1(idToken)}` : null;
4342
+ }
4343
+ function uncacheableReadKey(appId, scope) {
4344
+ uncacheableReadKeyCounter += 1;
4345
+ return `${appId}:${scope}-uncacheable-${uncacheableReadKeyCounter}`;
4333
4346
  }
4334
4347
  /**
4335
4348
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4336
4349
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4337
4350
  * keying by path alone lets User B receive User A's cached private read before
4338
- * any server read rule runs. This returns `appId:<principal>` for the identity a
4339
- * given read will actually authenticate as. This intentionally treats JWTs as
4340
- * opaque unverified bearer material never decoded claims and never caller
4341
- * identity hints such as `_walletAddress`. A forged token that merely claims
4342
- * another user's `sub` must miss that user's cache and go to the server.
4351
+ * any server read rule runs. This returns `appId:<principal>` for the opaque
4352
+ * auth material a given read will actually authenticate with. No-auth reads are
4353
+ * deliberately marked uncacheable instead of sharing an implicit `anon` bucket.
4354
+ * JWTs are intentionally treated as opaque unverified bearer material never
4355
+ * decoded claims and never caller identity hints such as `_walletAddress`.
4343
4356
  */
4344
4357
  async function getReadPrincipalKey(overrides) {
4345
4358
  const config = await getConfig();
4346
4359
  const appId = config.appId || '';
4347
4360
  // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4348
4361
  // caller-supplied Authorization is the real request auth when present.
4349
- const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4350
- if (directAuth) {
4351
- return `${appId}:${principalFromAuthValue(directAuth)}`;
4362
+ if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
4363
+ const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
4364
+ return principal
4365
+ ? { key: `${appId}:${principal}`, cacheable: true }
4366
+ : { key: uncacheableReadKey(appId, 'h'), cacheable: false };
4352
4367
  }
4353
4368
  // Per-request auth-header override (wallet client). Key by the exact opaque
4354
4369
  // header it produces, not decoded claims or the unverified _walletAddress hint.
4355
4370
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4356
4371
  try {
4357
4372
  const headers = await overrides._getAuthHeaders();
4358
- return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4373
+ const principal = principalFromAuthValue(authValueFromHeaders(headers));
4374
+ return principal
4375
+ ? { key: `${appId}:o${principal}`, cacheable: true }
4376
+ : { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4359
4377
  }
4360
4378
  catch (_a) {
4361
- // If we can't resolve the override identity, use a unique-ish key so we
4362
- // never collide with (and serve) another principal's cached entry.
4363
- return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4379
+ // If we can't resolve the override identity, do not read/write cache.
4380
+ return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4364
4381
  }
4365
4382
  }
4366
4383
  // Ambient session principal.
4367
4384
  const idToken = await getIdToken(config.isServer);
4368
- return `${appId}:${principalFromIdToken$1(idToken)}`;
4385
+ const principal = principalFromIdToken$1(idToken);
4386
+ return principal
4387
+ ? { key: `${appId}:${principal}`, cacheable: true }
4388
+ : { key: uncacheableReadKey(appId, 'a'), cacheable: false };
4369
4389
  }
4370
4390
  /**
4371
4391
  * Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
@@ -4658,18 +4678,20 @@ async function get(path, opts = {}) {
4658
4678
  // Create cache key combining path, prompt, filter, sort, includeSubPaths,
4659
4679
  // shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
4660
4680
  // so a private read cached for one user is never served to another in a
4661
- // shared process / SSR worker / browser login-switch.
4681
+ // shared process / SSR worker / browser login-switch. The cache is opt-in
4682
+ // and disabled for no-auth reads, which get an uncacheable unique key.
4662
4683
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4663
4684
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4664
4685
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4665
- const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
4666
- const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
4667
- const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
4686
+ const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4687
+ const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4688
+ const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
4668
4689
  const principalKey = await getReadPrincipalKey(opts._overrides);
4669
- const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4690
+ const cacheKey = `${principalKey.key}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4691
+ const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4670
4692
  const now = Date.now();
4671
- // Check for valid cache entry if not bypassing cache
4672
- if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4693
+ // Check for valid cache entry when the caller explicitly opted in.
4694
+ if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4673
4695
  return getCache[cacheKey].data;
4674
4696
  }
4675
4697
  // If we're bypassing cache, we should still coalesce identical requests
@@ -4711,8 +4733,8 @@ async function get(path, opts = {}) {
4711
4733
  // - collection path → `{ data, nextCursor }` preserved,
4712
4734
  // with the bare `id` (leaf doc key) attached to every returned row (Bug 1).
4713
4735
  const responseData = normalizeReadResult(response.data, pathIsDocument);
4714
- // Cache the response (unless bypassing cache)
4715
- if (!opts.bypassCache) {
4736
+ // Cache the response only when explicitly requested and principal-bound.
4737
+ if (cacheEnabled) {
4716
4738
  getCache[cacheKey] = {
4717
4739
  data: responseData,
4718
4740
  expiresAt: now + GET_CACHE_TTL
@@ -4750,6 +4772,213 @@ function cleanupExpiredCache() {
4750
4772
  });
4751
4773
  lastCacheCleanup = now;
4752
4774
  }
4775
+ const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
4776
+ const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
4777
+ const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
4778
+ const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
4779
+ const ALLOWED_SERVER_TX_PROGRAMS = new Set([
4780
+ BOUNDED_PROGRAM_MAINNET,
4781
+ BOUNDED_PROGRAM_DEVNET,
4782
+ COMPUTE_BUDGET_PROGRAM,
4783
+ SYSTEM_PROGRAM_ID,
4784
+ ]);
4785
+ const SET_DOCUMENTS_DISCRIMINATOR = '79,46,72,73,24,79,66,245';
4786
+ const SET_DOCUMENTS_V2_DISCRIMINATOR = '22,236,242,185,145,61,26,39';
4787
+ const ALLOWED_BOUNDED_SET_DISCRIMINATORS = new Set([
4788
+ SET_DOCUMENTS_DISCRIMINATOR,
4789
+ SET_DOCUMENTS_V2_DISCRIMINATOR,
4790
+ ]);
4791
+ class BorshCursor {
4792
+ constructor(data, offset, label) {
4793
+ this.data = data;
4794
+ this.offset = offset;
4795
+ this.label = label;
4796
+ }
4797
+ requireBytes(length, field) {
4798
+ if (this.offset + length > this.data.length) {
4799
+ throw new Error(`${this.label} has malformed Bounded instruction data while reading ${field}`);
4800
+ }
4801
+ }
4802
+ readU8(field) {
4803
+ this.requireBytes(1, field);
4804
+ return this.data[this.offset++];
4805
+ }
4806
+ readU32(field) {
4807
+ this.requireBytes(4, field);
4808
+ const value = this.data[this.offset] |
4809
+ (this.data[this.offset + 1] << 8) |
4810
+ (this.data[this.offset + 2] << 16) |
4811
+ (this.data[this.offset + 3] << 24);
4812
+ this.offset += 4;
4813
+ return value >>> 0;
4814
+ }
4815
+ skip(length, field) {
4816
+ this.requireBytes(length, field);
4817
+ this.offset += length;
4818
+ }
4819
+ readString(field) {
4820
+ const length = this.readU32(`${field} length`);
4821
+ this.requireBytes(length, field);
4822
+ const raw = this.data.slice(this.offset, this.offset + length);
4823
+ this.offset += length;
4824
+ return bufferExports.Buffer.from(raw).toString('utf8');
4825
+ }
4826
+ skipBytes(field) {
4827
+ const length = this.readU32(`${field} length`);
4828
+ this.skip(length, field);
4829
+ }
4830
+ isAtEnd() {
4831
+ return this.offset === this.data.length;
4832
+ }
4833
+ }
4834
+ function discriminatorKey(data) {
4835
+ return Array.from(data.slice(0, 8)).join(',');
4836
+ }
4837
+ function skipBoundedFieldValue(cursor) {
4838
+ const option = cursor.readU8('operation value option');
4839
+ if (option === 0)
4840
+ return;
4841
+ if (option !== 1) {
4842
+ throw new Error('Server transaction has malformed Bounded field value option');
4843
+ }
4844
+ const variant = cursor.readU8('operation value variant');
4845
+ switch (variant) {
4846
+ case 0: // u64Val
4847
+ case 1: // i64Val
4848
+ cursor.skip(8, 'operation numeric value');
4849
+ return;
4850
+ case 2: // boolVal
4851
+ cursor.skip(1, 'operation bool value');
4852
+ return;
4853
+ case 3: // stringVal
4854
+ cursor.readString('operation string value');
4855
+ return;
4856
+ case 4: // addressVal
4857
+ cursor.skip(32, 'operation address value');
4858
+ return;
4859
+ default:
4860
+ throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
4861
+ }
4862
+ }
4863
+ function skipBoundedFieldOperation(cursor) {
4864
+ cursor.readString('operation key');
4865
+ skipBoundedFieldValue(cursor);
4866
+ cursor.skip(1, 'operation kind');
4867
+ }
4868
+ function skipBoundedTxData(cursor, isV2) {
4869
+ cursor.readString('txData plugin function key');
4870
+ cursor.skipBytes('txData bytes');
4871
+ if (isV2) {
4872
+ cursor.skipBytes('txData raIndices');
4873
+ return;
4874
+ }
4875
+ const raIndexCount = cursor.readU32('txData raIndices length');
4876
+ cursor.skip(raIndexCount * 8, 'txData raIndices');
4877
+ }
4878
+ function normalizeOnchainPath(path) {
4879
+ let normalized = path.startsWith('/') ? path.slice(1) : path;
4880
+ if (normalized.endsWith('*') && normalized.length > 1) {
4881
+ normalized = normalized.slice(0, -1);
4882
+ }
4883
+ if (normalized.endsWith('/')) {
4884
+ normalized = normalized.slice(0, -1);
4885
+ }
4886
+ return normalized;
4887
+ }
4888
+ function parseBoundedSetDocumentsInstruction(data, label) {
4889
+ if (data.length < 8) {
4890
+ throw new Error(`${label} has malformed Bounded instruction data`);
4891
+ }
4892
+ const discriminator = discriminatorKey(data);
4893
+ if (!ALLOWED_BOUNDED_SET_DISCRIMINATORS.has(discriminator)) {
4894
+ throw new Error(`${label} contains unsupported Bounded instruction`);
4895
+ }
4896
+ const isV2 = discriminator === SET_DOCUMENTS_V2_DISCRIMINATOR;
4897
+ const cursor = new BorshCursor(data, 8, label);
4898
+ const appId = cursor.readString('appId');
4899
+ const documentPaths = [];
4900
+ const documentCount = cursor.readU32('documents length');
4901
+ for (let i = 0; i < documentCount; i++) {
4902
+ documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
4903
+ const operationCount = cursor.readU32('operations length');
4904
+ for (let j = 0; j < operationCount; j++) {
4905
+ skipBoundedFieldOperation(cursor);
4906
+ }
4907
+ }
4908
+ const deletePaths = [];
4909
+ const deleteCount = cursor.readU32('delete paths length');
4910
+ for (let i = 0; i < deleteCount; i++) {
4911
+ deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
4912
+ }
4913
+ const txDataCount = cursor.readU32('txData length');
4914
+ for (let i = 0; i < txDataCount; i++) {
4915
+ skipBoundedTxData(cursor, isV2);
4916
+ }
4917
+ const simulate = cursor.readU8('simulate');
4918
+ if (simulate !== 0 && simulate !== 1) {
4919
+ throw new Error(`${label} has malformed Bounded simulate flag`);
4920
+ }
4921
+ if (!cursor.isAtEnd()) {
4922
+ throw new Error(`${label} has trailing Bounded instruction data`);
4923
+ }
4924
+ return { appId, documentPaths, deletePaths };
4925
+ }
4926
+ function assertSamePathSet(label, expectedPaths, actualPaths) {
4927
+ const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
4928
+ const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
4929
+ const missing = [...expected].filter(path => !actual.has(path));
4930
+ const extra = [...actual].filter(path => !expected.has(path));
4931
+ if (missing.length > 0 || extra.length > 0) {
4932
+ const details = [
4933
+ missing.length ? `missing paths: ${missing.join(', ')}` : '',
4934
+ extra.length ? `unexpected paths: ${extra.join(', ')}` : '',
4935
+ ].filter(Boolean).join('; ');
4936
+ throw new Error(`${label} Bounded instruction paths do not match requested write paths (${details})`);
4937
+ }
4938
+ }
4939
+ function validateServerSuppliedVersionedTransaction(transaction, options) {
4940
+ var _a;
4941
+ const { label, expectedAppId, expectedWritePaths } = options;
4942
+ const accountKeys = transaction.message.staticAccountKeys;
4943
+ let boundedInstructionCount = 0;
4944
+ const actualWritePaths = [];
4945
+ for (const ix of transaction.message.compiledInstructions) {
4946
+ if (ix.programIdIndex >= accountKeys.length) {
4947
+ throw new Error(`${label} has program ID in lookup table (not allowed)`);
4948
+ }
4949
+ const programId = accountKeys[ix.programIdIndex].toBase58();
4950
+ if (!ALLOWED_SERVER_TX_PROGRAMS.has(programId)) {
4951
+ throw new Error(`${label} contains unauthorized program: ${programId}`);
4952
+ }
4953
+ const data = ix.data instanceof Uint8Array ? ix.data : bufferExports.Buffer.from((_a = ix.data) !== null && _a !== void 0 ? _a : []);
4954
+ if (programId === SYSTEM_PROGRAM_ID) {
4955
+ throw new Error(`${label} contains unauthorized System Program instruction`);
4956
+ }
4957
+ if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
4958
+ boundedInstructionCount += 1;
4959
+ const parsed = parseBoundedSetDocumentsInstruction(data, label);
4960
+ if (parsed.appId !== expectedAppId) {
4961
+ throw new Error(`${label} Bounded instruction appId does not match configured appId`);
4962
+ }
4963
+ actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
4964
+ }
4965
+ }
4966
+ if (boundedInstructionCount !== 1) {
4967
+ throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
4968
+ }
4969
+ assertSamePathSet(label, expectedWritePaths, actualWritePaths);
4970
+ }
4971
+ function deserializeAndValidateServerTransaction(serializedTransaction, options) {
4972
+ const txBytes = bufferExports.Buffer.from(serializedTransaction, 'base64');
4973
+ const transaction = VersionedTransaction.deserialize(txBytes);
4974
+ validateServerSuppliedVersionedTransaction(transaction, options);
4975
+ return transaction;
4976
+ }
4977
+ function assertAllowedServerPreInstruction(ix, label) {
4978
+ if (ix.programId.equals(SystemProgram.programId)) {
4979
+ throw new Error(`${label} contains unauthorized System Program preInstruction`);
4980
+ }
4981
+ }
4753
4982
  function classifyGetManyBatchError(error) {
4754
4983
  var _a, _b, _c;
4755
4984
  const err = error;
@@ -4794,13 +5023,14 @@ async function getMany(paths, opts = {}) {
4794
5023
  // H1: principal-scope getMany cache keys so one user's batch reads are never
4795
5024
  // served to another. Same `<appId:principal>|<path>:` shape used by get().
4796
5025
  const principalKey = await getReadPrincipalKey(opts._overrides);
5026
+ const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4797
5027
  const results = new Array(paths.length);
4798
5028
  const uncachedIndices = [];
4799
5029
  const uncachedPaths = [];
4800
5030
  for (let i = 0; i < normalizedPaths.length; i++) {
4801
5031
  const normalizedPath = normalizedPaths[i];
4802
- const cacheKey = `${principalKey}|${normalizedPath}:`;
4803
- if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
5032
+ const cacheKey = `${principalKey.key}|${normalizedPath}:`;
5033
+ if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4804
5034
  results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
4805
5035
  }
4806
5036
  else {
@@ -4836,8 +5066,8 @@ async function getMany(paths, opts = {}) {
4836
5066
  ? serverResult
4837
5067
  : Object.assign(Object.assign({}, serverResult), { data: withBareId(serverResult.data) });
4838
5068
  results[originalIndex] = normalizedResult;
4839
- if (!normalizedResult.error && !opts.bypassCache) {
4840
- const cacheKey = `${principalKey}|${normalizedPath}:`;
5069
+ if (!normalizedResult.error && cacheEnabled) {
5070
+ const cacheKey = `${principalKey.key}|${normalizedPath}:`;
4841
5071
  getCache[cacheKey] = {
4842
5072
  data: normalizedResult.data,
4843
5073
  expiresAt: now + GET_CACHE_TTL
@@ -4852,7 +5082,7 @@ async function getMany(paths, opts = {}) {
4852
5082
  };
4853
5083
  }
4854
5084
  }
4855
- if (now - lastCacheCleanup > 5000) {
5085
+ if (cacheEnabled && now - lastCacheCleanup > 5000) {
4856
5086
  cleanupExpiredCache();
4857
5087
  lastCacheCleanup = now;
4858
5088
  }
@@ -5003,11 +5233,12 @@ async function setMany(many, options) {
5003
5233
  }
5004
5234
  const curTx = transactions[0];
5005
5235
  let transactionResult;
5236
+ const expectedWritePaths = documents.map(d => d.destinationPath);
5006
5237
  if (curTx.serializedTransaction) {
5007
- transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
5238
+ transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5008
5239
  }
5009
5240
  else {
5010
- transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
5241
+ transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
5011
5242
  }
5012
5243
  // Sync items after all transactions are confirmed
5013
5244
  // Wait for 1.5 seconds to ensure all transactions are confirmed
@@ -5041,7 +5272,7 @@ async function setMany(many, options) {
5041
5272
  catch (error) {
5042
5273
  throw error;
5043
5274
  }
5044
- async function handleSolanaTransaction(tx, authProvider, options) {
5275
+ async function handleSolanaTransaction(tx, authProvider, options, expectedWritePaths) {
5045
5276
  var _a, _b, _c, _d, _e;
5046
5277
  // NOTE (backwards-compat revert): a program-allowlist on server-supplied
5047
5278
  // `preInstructions` was tried here for the audit-8 SOL-drain concern, but it
@@ -5089,13 +5320,20 @@ async function setMany(many, options) {
5089
5320
  }))) !== null && _b !== void 0 ? _b : [],
5090
5321
  };
5091
5322
  const config = await getConfig();
5323
+ if (tx.signedTransaction) {
5324
+ deserializeAndValidateServerTransaction(tx.signedTransaction, {
5325
+ label: 'Server signedTransaction',
5326
+ expectedAppId: config.appId,
5327
+ expectedWritePaths,
5328
+ });
5329
+ }
5092
5330
  const solTransaction = {
5093
5331
  appId: config.appId,
5094
5332
  txArgs: [solTransactionData],
5095
5333
  lutKey: (_c = tx.lutAddress) !== null && _c !== void 0 ? _c : null,
5096
5334
  additionalLutAddresses: tx.additionalLutAddresses,
5097
5335
  network: tx.network,
5098
- preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix) => {
5336
+ preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix, index) => {
5099
5337
  var _a;
5100
5338
  const keys = (_a = ix.keys) === null || _a === void 0 ? void 0 : _a.map((k) => ({
5101
5339
  pubkey: new PublicKey(k.pubkey),
@@ -5106,11 +5344,13 @@ async function setMany(many, options) {
5106
5344
  ? SystemProgram.programId // prettier to use the constant
5107
5345
  : new PublicKey(ix.programId);
5108
5346
  const data = bufferExports.Buffer.from(ix.data);
5109
- return new TransactionInstruction({
5347
+ const instruction = new TransactionInstruction({
5110
5348
  keys,
5111
5349
  programId,
5112
5350
  data,
5113
5351
  });
5352
+ assertAllowedServerPreInstruction(instruction, `Server preInstruction[${index}]`);
5353
+ return instruction;
5114
5354
  })) !== null && _e !== void 0 ? _e : [],
5115
5355
  // Server co-signed transaction (when CPI tx_data is present)
5116
5356
  signedTransaction: tx.signedTransaction,
@@ -5118,45 +5358,22 @@ async function setMany(many, options) {
5118
5358
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5119
5359
  return transactionResult;
5120
5360
  }
5121
- async function handlePreBuiltTransaction(tx, authProvider, options) {
5122
- var _a, _b;
5361
+ async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5362
+ var _a, _b, _c;
5123
5363
  const config = await getConfig();
5124
- const rpcUrl = config.rpcUrl || (tx.network === 'solana_devnet'
5125
- ? 'https://api.devnet.solana.com'
5126
- : 'https://api.mainnet-beta.solana.com');
5127
- const connection = new Connection(rpcUrl, 'confirmed');
5128
- const txBytes = bufferExports.Buffer.from(tx.serializedTransaction, 'base64');
5129
- const transaction = VersionedTransaction.deserialize(txBytes);
5130
- // Validate the transaction before signing: ensure only allowed programs
5131
- // and no unauthorized System program instructions (e.g., SOL transfers)
5132
- const BOUNDED_PROGRAM = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
5133
- const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
5134
- const SYSTEM_PROGRAM = '11111111111111111111111111111111';
5135
- const ALLOWED_PROGRAMS = new Set([BOUNDED_PROGRAM, COMPUTE_BUDGET, SYSTEM_PROGRAM]);
5136
- // System program instruction discriminators (first 4 bytes, little-endian u32)
5137
- const SYSTEM_TRANSFER = 2; // Transfer instruction index
5138
- const SYSTEM_TRANSFER_WITH_SEED = 11;
5139
- const accountKeys = transaction.message.staticAccountKeys;
5140
- for (const ix of transaction.message.compiledInstructions) {
5141
- if (ix.programIdIndex >= accountKeys.length) {
5142
- throw new Error('Pre-built transaction has program ID in lookup table (not allowed)');
5143
- }
5144
- const programId = accountKeys[ix.programIdIndex].toBase58();
5145
- if (!ALLOWED_PROGRAMS.has(programId)) {
5146
- throw new Error(`Pre-built transaction contains unauthorized program: ${programId}`);
5147
- }
5148
- // Block System program transfer instructions — a compromised DO could
5149
- // embed a SOL drain. Only allow createAccount/allocate (needed for PDA init).
5150
- if (programId === SYSTEM_PROGRAM && ix.data.length >= 4) {
5151
- const ixIndex = ix.data[0] | (ix.data[1] << 8) | (ix.data[2] << 16) | (ix.data[3] << 24);
5152
- if (ixIndex === SYSTEM_TRANSFER || ixIndex === SYSTEM_TRANSFER_WITH_SEED) {
5153
- throw new Error('Pre-built transaction contains unauthorized System transfer instruction');
5154
- }
5155
- }
5364
+ const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, {
5365
+ label: 'Pre-built transaction',
5366
+ expectedAppId: config.appId,
5367
+ expectedWritePaths,
5368
+ });
5369
+ const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
5370
+ const rpcUrl = (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
5371
+ if (shouldSubmit && !rpcUrl) {
5372
+ throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}`);
5156
5373
  }
5157
5374
  const signedTx = await authProvider.signTransaction(transaction);
5158
5375
  const rawTx = signedTx.serialize();
5159
- if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
5376
+ if (!shouldSubmit) {
5160
5377
  return {
5161
5378
  transactionSignature: '',
5162
5379
  signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
@@ -5164,6 +5381,7 @@ async function setMany(many, options) {
5164
5381
  gasUsed: '0',
5165
5382
  };
5166
5383
  }
5384
+ const connection = new Connection(rpcUrl, 'confirmed');
5167
5385
  const signature = await connection.sendRawTransaction(rawTx, {
5168
5386
  skipPreflight: false,
5169
5387
  maxRetries: 3,
@@ -5176,7 +5394,7 @@ async function setMany(many, options) {
5176
5394
  return {
5177
5395
  transactionSignature: signature,
5178
5396
  signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
5179
- blockNumber: (_b = (_a = confirmation.context) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0,
5397
+ blockNumber: (_c = (_b = confirmation.context) === null || _b === void 0 ? void 0 : _b.slot) !== null && _c !== void 0 ? _c : 0,
5180
5398
  gasUsed: '0',
5181
5399
  };
5182
5400
  }
@@ -5393,7 +5611,7 @@ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
5393
5611
  const MAX_RECONNECT_DELAY_MS = 300000;
5394
5612
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
5395
5613
  const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
5396
- const MAX_AUTH_REFRESH_RETRIES = 5;
5614
+ const WS_AUTH_EXPIRED_CODE = 'auth_expired';
5397
5615
  const WS_CONFIG = {
5398
5616
  // Keep retrying indefinitely so long outages recover without page refresh.
5399
5617
  maxRetries: Infinity,
@@ -5410,11 +5628,12 @@ const WS_V2_PATH = '/ws/v2';
5410
5628
  let browserReconnectHooksAttached = false;
5411
5629
  let lastBrowserTriggeredReconnectAt = 0;
5412
5630
  let reconnectInProgress = null;
5631
+ let ambientAuthFailure = null;
5413
5632
  // ============ Helper Functions ============
5414
5633
  function generateSubscriptionId() {
5415
5634
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5416
5635
  }
5417
- function hashForKey$1(value) {
5636
+ function hashForKey(value) {
5418
5637
  let h = 5381;
5419
5638
  for (let i = 0; i < value.length; i++) {
5420
5639
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5426,17 +5645,17 @@ function hashForKey$1(value) {
5426
5645
  * just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
5427
5646
  * a private subscription snapshot cached for one user from being delivered to a
5428
5647
  * different user who subscribes with the same options (shared process / SSR worker
5429
- * / browser login-switch; this cache has a 5-minute TTL). When omitted (legacy
5430
- * read-only helpers) we fall back to an `anon` scope.
5648
+ * / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
5649
+ * before entries are read or written; no-auth subscriptions never populate an
5650
+ * implicit anonymous cache bucket.
5431
5651
  */
5432
5652
  function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5433
5653
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5434
5654
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5435
5655
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5436
- const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
5437
- const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
5438
- const identityKey = identity || 'anon';
5439
- return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5656
+ const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5657
+ const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
5658
+ return `${identity}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5440
5659
  }
5441
5660
  /**
5442
5661
  * Derive an opaque identity string for the bearer material a subscription sends.
@@ -5444,7 +5663,7 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5444
5663
  * before the server has verified any claims.
5445
5664
  */
5446
5665
  function principalFromIdToken(idToken) {
5447
- return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5666
+ return idToken ? `t${hashForKey(idToken)}` : null;
5448
5667
  }
5449
5668
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5450
5669
  // Per-subscription wallet override (server WalletClient.subscribe): key by
@@ -5453,16 +5672,21 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5453
5672
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5454
5673
  try {
5455
5674
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
5456
- return `${effectiveAppId}:o${bearer ? principalFromIdToken(bearer) : 'anon'}`;
5675
+ const principal = principalFromIdToken(bearer);
5676
+ return principal
5677
+ ? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
5678
+ : { key: `${effectiveAppId}:oanon`, cacheable: false };
5457
5679
  }
5458
5680
  catch (_a) {
5459
- // Couldn't resolve the override identity — use a unique key so we never
5460
- // collide with another principal's cached entry.
5461
- return `${effectiveAppId}:o${principalFromIdToken(null)}-${safeBtoa(String(connectionEpoch++))}`;
5681
+ // Couldn't resolve the override identity — do not use response cache.
5682
+ return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
5462
5683
  }
5463
5684
  }
5464
5685
  const idToken = await getIdToken(isServer);
5465
- return `${effectiveAppId}:${principalFromIdToken(idToken)}`;
5686
+ const principal = principalFromIdToken(idToken);
5687
+ return principal
5688
+ ? { key: `${effectiveAppId}:${principal}`, cacheable: true }
5689
+ : { key: `${effectiveAppId}:anon`, cacheable: false };
5466
5690
  }
5467
5691
  /** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
5468
5692
  function bearerFromAuthHeaders(headers) {
@@ -5492,6 +5716,41 @@ function getTokenExpirationTime(token) {
5492
5716
  return null;
5493
5717
  }
5494
5718
  }
5719
+ function makeAuthExpiredError(message, status) {
5720
+ const err = new Error(`${WS_AUTH_EXPIRED_CODE}: ${message}`);
5721
+ err.code = WS_AUTH_EXPIRED_CODE;
5722
+ if (status !== undefined)
5723
+ err.status = status;
5724
+ return err;
5725
+ }
5726
+ function isAuthExpiredError(error) {
5727
+ return !!error && typeof error === 'object' && error.code === WS_AUTH_EXPIRED_CODE;
5728
+ }
5729
+ function normalizeAuthExpiredError(error, fallbackMessage) {
5730
+ var _a, _b;
5731
+ if (isAuthExpiredError(error))
5732
+ return error;
5733
+ const status = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.code;
5734
+ return makeAuthExpiredError(fallbackMessage, status);
5735
+ }
5736
+ function rememberAmbientAuthFailure(error) {
5737
+ ambientAuthFailure = error;
5738
+ return error;
5739
+ }
5740
+ function failConnectionAuth(connection, error) {
5741
+ connection.authFailure = error;
5742
+ connection.pendingAuthToken = null;
5743
+ connection.isConnecting = false;
5744
+ connection.isConnected = false;
5745
+ connection.isAuthenticating = false;
5746
+ for (const [, pending] of connection.pendingSubscriptions) {
5747
+ pending.reject(error);
5748
+ }
5749
+ connection.pendingSubscriptions.clear();
5750
+ for (const subscription of connection.subscriptions.values()) {
5751
+ notifyErrorCallbacks(subscription, error);
5752
+ }
5753
+ }
5495
5754
  function scheduleTokenRefresh(connection, isServer) {
5496
5755
  // Clear any existing timer
5497
5756
  if (connection.tokenRefreshTimer) {
@@ -5559,23 +5818,32 @@ async function getFreshAuthToken(isServer) {
5559
5818
  var _a, _b, _c, _d, _e;
5560
5819
  const currentToken = await getIdToken(isServer);
5561
5820
  if (!currentToken) {
5821
+ if (ambientAuthFailure) {
5822
+ throw ambientAuthFailure;
5823
+ }
5562
5824
  return null;
5563
5825
  }
5564
5826
  if (!isTokenExpired(currentToken)) {
5827
+ ambientAuthFailure = null;
5565
5828
  return currentToken;
5566
5829
  }
5830
+ if (ambientAuthFailure) {
5831
+ throw ambientAuthFailure;
5832
+ }
5567
5833
  // Token is expired — attempt refresh
5834
+ const refreshToken = await getRefreshToken(isServer);
5835
+ if (!refreshToken) {
5836
+ console.warn('[WS v2] Token expired but no refresh token available');
5837
+ throw rememberAmbientAuthFailure(makeAuthExpiredError('Authentication expired and no refresh token is available'));
5838
+ }
5568
5839
  try {
5569
- const refreshToken = await getRefreshToken(isServer);
5570
- if (!refreshToken) {
5571
- console.warn('[WS v2] Token expired but no refresh token available');
5572
- return null;
5573
- }
5574
5840
  const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
5575
5841
  if (refreshData && refreshData.idToken && refreshData.accessToken) {
5576
5842
  await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer, refreshData.refreshToken);
5843
+ ambientAuthFailure = null;
5577
5844
  return refreshData.idToken;
5578
5845
  }
5846
+ throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
5579
5847
  }
5580
5848
  catch (error) {
5581
5849
  // Log only the status — the raw axios error carries config.data (the refresh
@@ -5592,12 +5860,22 @@ async function getFreshAuthToken(isServer) {
5592
5860
  console.warn('[WS v2] Failed to clear stale session:', clearError);
5593
5861
  }
5594
5862
  }
5863
+ throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
5864
+ }
5865
+ }
5866
+ async function getConnectionAuthToken(isServer, authTokenProvider) {
5867
+ if (!authTokenProvider) {
5868
+ return getFreshAuthToken(isServer);
5869
+ }
5870
+ try {
5871
+ const token = await authTokenProvider();
5872
+ if (token)
5873
+ return token;
5874
+ throw makeAuthExpiredError('Authenticated websocket token provider returned no token');
5875
+ }
5876
+ catch (error) {
5877
+ throw normalizeAuthExpiredError(error, 'Authenticated websocket token provider failed');
5595
5878
  }
5596
- // Return null instead of the expired token to prevent infinite 401 reconnect storms.
5597
- // The server accepts unauthenticated connections; auth-required subscriptions will
5598
- // receive per-subscription errors via onError callbacks.
5599
- console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
5600
- return null;
5601
5879
  }
5602
5880
  function hasDisconnectedActiveConnections() {
5603
5881
  for (const connection of connections.values()) {
@@ -5677,8 +5955,21 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5677
5955
  const connKey = principalKey ? `${base}#id#${principalKey}` : base;
5678
5956
  let connection = connections.get(connKey);
5679
5957
  if (connection && connection.ws) {
5958
+ if (connection.authFailure) {
5959
+ throw connection.authFailure;
5960
+ }
5961
+ try {
5962
+ await getConnectionAuthToken(isServer, authTokenProvider);
5963
+ }
5964
+ catch (error) {
5965
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5966
+ failConnectionAuth(connection, authError);
5967
+ throw authError;
5968
+ }
5969
+ connection.authFailure = null;
5680
5970
  return connection;
5681
5971
  }
5972
+ let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5682
5973
  // Create new connection
5683
5974
  connection = {
5684
5975
  ws: null,
@@ -5696,6 +5987,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5696
5987
  pendingAuthToken: null,
5697
5988
  tokenRefreshTimer: null,
5698
5989
  consecutiveAuthFailures: 0,
5990
+ authFailure: null,
5699
5991
  };
5700
5992
  connections.set(connKey, connection);
5701
5993
  // URL provider for reconnection with fresh tokens
@@ -5717,27 +6009,23 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5717
6009
  // its token from the wallet's own session (self-refreshing); all others
5718
6010
  // use the ambient env/web session. The token is sent as the first WS
5719
6011
  // frame after open, never as a URL query parameter.
5720
- const authToken = connection.authTokenProvider
5721
- ? await connection.authTokenProvider().catch(() => null)
5722
- : await getFreshAuthToken(isServer);
6012
+ let authToken;
6013
+ try {
6014
+ authToken = initialAuthToken !== undefined
6015
+ ? initialAuthToken
6016
+ : await getConnectionAuthToken(isServer, connection.authTokenProvider);
6017
+ initialAuthToken = undefined;
6018
+ }
6019
+ catch (error) {
6020
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
6021
+ failConnectionAuth(connection, authError);
6022
+ throw authError;
6023
+ }
5723
6024
  connection.pendingAuthToken = authToken || null;
5724
6025
  if (authToken) {
5725
6026
  // Successful token acquisition — reset failure counter
5726
6027
  connection.consecutiveAuthFailures = 0;
5727
- }
5728
- else {
5729
- // Check if user WAS authenticated (had a token that expired).
5730
- // If so, retry with exponential backoff before falling back to unauthenticated.
5731
- const expiredToken = await getIdToken(isServer);
5732
- if (expiredToken && isTokenExpired(expiredToken)) {
5733
- connection.consecutiveAuthFailures++;
5734
- if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
5735
- console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
5736
- throw new Error('Auth token refresh failed, retrying with backoff');
5737
- }
5738
- console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
5739
- }
5740
- // No token at all (never authenticated) or retries exhausted — connect without auth
6028
+ connection.authFailure = null;
5741
6029
  }
5742
6030
  return wsUrl.toString();
5743
6031
  };
@@ -5751,15 +6039,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5751
6039
  connection.isConnecting = false;
5752
6040
  connection.isConnected = true;
5753
6041
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
5754
- // fresh auth token is actually obtained (in urlProvider, line ~389) or on
5755
- // an explicit auth change (reconnectWithNewAuthV2, line ~854). Resetting
5756
- // on every 'open' event created an infinite loop: auth fails 5x → connect
5757
- // without auth → open resets counter → disconnect → auth fails 5x again →
5758
- // repeat forever, hammering /session/refresh and causing 429s.
5759
- //
5760
- // An elevated counter is safe for anonymous/guest sessions: when there's no
5761
- // token at all (getIdToken returns null), the counter is never checked —
5762
- // urlProvider skips straight to unauthenticated connection.
6042
+ // fresh auth token is actually obtained or on an explicit auth change.
5763
6043
  // Schedule periodic token freshness checks
5764
6044
  scheduleTokenRefresh(connection, isServer);
5765
6045
  if (connection.pendingAuthToken) {
@@ -5834,8 +6114,10 @@ function handleServerMessage(connection, message) {
5834
6114
  // If we already received data for this subscription, treat subscribed
5835
6115
  // as an ack only and avoid regressing to an older snapshot.
5836
6116
  if (subscription.lastData === undefined) {
5837
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
5838
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6117
+ if (subscription.cache) {
6118
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6119
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6120
+ }
5839
6121
  subscription.lastData = message.data;
5840
6122
  notifyCallbacks(subscription, message.data);
5841
6123
  }
@@ -5861,8 +6143,10 @@ function handleServerMessage(connection, message) {
5861
6143
  const subscription = connection.subscriptions.get(message.subscriptionId);
5862
6144
  if (subscription) {
5863
6145
  // Update cache
5864
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
5865
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6146
+ if (subscription.cache) {
6147
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6148
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6149
+ }
5866
6150
  // Store last data
5867
6151
  subscription.lastData = message.data;
5868
6152
  // Notify callbacks
@@ -6080,20 +6364,37 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6080
6364
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6081
6365
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6082
6366
  : undefined;
6083
- const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6367
+ const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6368
+ const identity = identityInfo.key;
6369
+ const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
6084
6370
  const principalKey = authTokenProvider ? identity : undefined;
6085
- const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6086
- // Deliver cached data immediately if available
6087
- const cachedEntry = responseCache.get(cacheKey);
6371
+ const cacheKey = responseCacheEnabled
6372
+ ? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
6373
+ : null;
6374
+ // Get or create connection for this routing target (room-scoped when a
6375
+ // room route is supplied by the live helper, else the app-level connection).
6376
+ let connection;
6377
+ try {
6378
+ connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
6379
+ }
6380
+ catch (error) {
6381
+ const err = error instanceof Error ? error : new Error(String(error));
6382
+ if (subscriptionOptions.onError) {
6383
+ subscriptionOptions.onError(err);
6384
+ return async () => { };
6385
+ }
6386
+ throw err;
6387
+ }
6388
+ // Deliver cached data immediately if available, but only after connection
6389
+ // auth preflight has succeeded. An expired authenticated session must receive
6390
+ // an auth error, not a stale private snapshot followed by a failed connect.
6391
+ const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
6088
6392
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
6089
6393
  setTimeout(() => {
6090
6394
  var _a;
6091
6395
  (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
6092
6396
  }, 0);
6093
6397
  }
6094
- // Get or create connection for this routing target (room-scoped when a
6095
- // room route is supplied by the live helper, else the app-level connection).
6096
- const connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
6097
6398
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6098
6399
  const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6099
6400
  const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
@@ -6112,10 +6413,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6112
6413
  }
6113
6414
  }
6114
6415
  if (existingSubscription) {
6416
+ if (responseCacheEnabled) {
6417
+ existingSubscription.cache = true;
6418
+ }
6115
6419
  // Add callback to existing subscription
6116
6420
  existingSubscription.callbacks.push(subscriptionOptions);
6117
- // Deliver last known data immediately
6118
- if (existingSubscription.lastData !== undefined && subscriptionOptions.onData) {
6421
+ // Deliver last known data immediately only for explicit, principal-bound
6422
+ // cache opt-in. Otherwise joining an existing subscription can replay a
6423
+ // stale private snapshot without a fresh server auth result.
6424
+ if (responseCacheEnabled &&
6425
+ existingSubscription.cache &&
6426
+ existingSubscription.lastData !== undefined &&
6427
+ subscriptionOptions.onData) {
6119
6428
  setTimeout(() => {
6120
6429
  var _a;
6121
6430
  (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
@@ -6139,6 +6448,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6139
6448
  includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
6140
6449
  callbacks: [subscriptionOptions],
6141
6450
  lastData: undefined,
6451
+ cache: responseCacheEnabled,
6142
6452
  identity,
6143
6453
  };
6144
6454
  connection.subscriptions.set(subscriptionId, subscription);
@@ -6270,19 +6580,21 @@ function clearCacheV2(path) {
6270
6580
  */
6271
6581
  function getCachedDataV2(path, prompt) {
6272
6582
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6273
- // H1: response caches are identity-scoped (`<identity>|<path>:...`). Resolve
6274
- // the caller's identity from an active subscription for this path (sync) so
6275
- // an authenticated user gets THEIR cached snapshot rather than the 'anon'
6276
- // bucket (which would wrongly return null). No matching sub → null (safe).
6583
+ // H1: response caches are identity-scoped (`<identity>|<path>:...`) and
6584
+ // opt-in. Resolve the caller's identity from an active cache-enabled
6585
+ // subscription for this path. No matching sub null (safe).
6277
6586
  let identity;
6278
6587
  outer: for (const connection of connections.values()) {
6279
6588
  for (const sub of connection.subscriptions.values()) {
6280
- if (sub.path === normalizedPath && sub.prompt === prompt) {
6589
+ if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
6281
6590
  identity = sub.identity;
6282
6591
  break outer;
6283
6592
  }
6284
6593
  }
6285
6594
  }
6595
+ if (!identity) {
6596
+ return null;
6597
+ }
6286
6598
  const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
6287
6599
  const cachedEntry = responseCache.get(cacheKey);
6288
6600
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
@@ -6314,6 +6626,7 @@ async function reconnectWithNewAuthV2() {
6314
6626
  }
6315
6627
  }
6316
6628
  async function doReconnectWithNewAuth() {
6629
+ ambientAuthFailure = null;
6317
6630
  // SECURITY (H1): the logged-in identity is changing (login / logout / switch).
6318
6631
  // Wipe ALL principal-scoped read caches so the new identity can never observe
6319
6632
  // data cached for the previous one. Clear both the WS response cache and the
@@ -6335,7 +6648,7 @@ async function doReconnectWithNewAuth() {
6335
6648
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6336
6649
  }
6337
6650
  try {
6338
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
6651
+ const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-DVnh5nQ8.mjs');
6339
6652
  await reconnectRealtimeStoreWithNewAuth();
6340
6653
  }
6341
6654
  catch (error) {
@@ -6355,6 +6668,7 @@ async function doReconnectWithNewAuth() {
6355
6668
  connection.pendingUnsubscriptions.clear();
6356
6669
  // Reset auth failure counter — this is a proactive reconnect (login, token refresh)
6357
6670
  connection.consecutiveAuthFailures = 0;
6671
+ connection.authFailure = null;
6358
6672
  // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
6359
6673
  // We use reconnect() which will close and re-open with fresh URL (including new token)
6360
6674
  try {
@@ -6365,7 +6679,7 @@ async function doReconnectWithNewAuth() {
6365
6679
  }
6366
6680
  }
6367
6681
  }
6368
- // ============ CRUD over WebSocket ============
6682
+ // ============ WebSocket request helpers ============
6369
6683
  const WS_REQUEST_TIMEOUT_MS = 30000;
6370
6684
  function generateRequestId() {
6371
6685
  return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -6381,104 +6695,6 @@ function hasActiveConnection() {
6381
6695
  }
6382
6696
  return false;
6383
6697
  }
6384
- async function waitForConnectionAuthenticated(connection) {
6385
- if (!connection.isAuthenticating || !connection.ws)
6386
- return;
6387
- const ws = connection.ws;
6388
- await new Promise((resolve, reject) => {
6389
- let timeout;
6390
- let cleanup = () => { };
6391
- const onMessage = (event) => {
6392
- try {
6393
- const message = JSON.parse(event.data);
6394
- if ((message === null || message === void 0 ? void 0 : message.type) === 'authenticated') {
6395
- cleanup();
6396
- resolve();
6397
- }
6398
- }
6399
- catch (_a) {
6400
- // Other frames are handled by the main listener.
6401
- }
6402
- };
6403
- const onClose = () => {
6404
- cleanup();
6405
- reject(new Error('WebSocket disconnected during authentication'));
6406
- };
6407
- const onError = () => {
6408
- cleanup();
6409
- reject(new Error('WebSocket authentication failed'));
6410
- };
6411
- cleanup = () => {
6412
- clearTimeout(timeout);
6413
- ws.removeEventListener('message', onMessage);
6414
- ws.removeEventListener('close', onClose);
6415
- ws.removeEventListener('error', onError);
6416
- };
6417
- timeout = setTimeout(() => {
6418
- cleanup();
6419
- reject(new Error('WebSocket authentication timeout'));
6420
- }, 10000);
6421
- if (!connection.isAuthenticating) {
6422
- cleanup();
6423
- resolve();
6424
- return;
6425
- }
6426
- ws.addEventListener('message', onMessage);
6427
- ws.addEventListener('close', onClose);
6428
- ws.addEventListener('error', onError);
6429
- });
6430
- }
6431
- async function sendRequest(msgBuilder) {
6432
- const config = await getConfig();
6433
- const appId = config.appId;
6434
- const connection = await getOrCreateConnection(appId, config.isServer);
6435
- // Wait for the connection to be open (getOrCreateConnection may return
6436
- // while still connecting).
6437
- if (!connection.isConnected && connection.ws) {
6438
- await new Promise((resolve, reject) => {
6439
- const timeout = setTimeout(() => {
6440
- var _a;
6441
- (_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
6442
- reject(new Error('WebSocket connection timeout'));
6443
- }, 10000);
6444
- const onOpen = () => { clearTimeout(timeout); resolve(); };
6445
- if (connection.isConnected) {
6446
- clearTimeout(timeout);
6447
- resolve();
6448
- return;
6449
- }
6450
- connection.ws.addEventListener('open', onOpen);
6451
- });
6452
- }
6453
- if (!connection.ws || !connection.isConnected) {
6454
- throw new Error('WebSocket connection not available');
6455
- }
6456
- await waitForConnectionAuthenticated(connection);
6457
- const requestId = generateRequestId();
6458
- const message = msgBuilder(requestId);
6459
- return new Promise((resolve, reject) => {
6460
- const timer = setTimeout(() => {
6461
- connection.pendingRequests.delete(requestId);
6462
- reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
6463
- }, WS_REQUEST_TIMEOUT_MS);
6464
- connection.pendingRequests.set(requestId, { resolve, reject, timer });
6465
- try {
6466
- connection.ws.send(JSON.stringify(message));
6467
- }
6468
- catch (error) {
6469
- connection.pendingRequests.delete(requestId);
6470
- clearTimeout(timer);
6471
- reject(error);
6472
- }
6473
- });
6474
- }
6475
- async function wsGet(path) {
6476
- return sendRequest((requestId) => ({
6477
- type: 'get',
6478
- requestId,
6479
- path,
6480
- }));
6481
- }
6482
6698
  /**
6483
6699
  * Send a live-room intent over the EXISTING per-room socket (fire-and-forget).
6484
6700
  * Returns true if it was sent over an open connection, false if there is no
@@ -6537,31 +6753,6 @@ function wsIntentReliable(appId, roomRoutePath, intent) {
6537
6753
  }
6538
6754
  });
6539
6755
  }
6540
- async function wsSet(documents) {
6541
- return sendRequest((requestId) => ({
6542
- type: 'set',
6543
- requestId,
6544
- documents,
6545
- }));
6546
- }
6547
- async function wsQuery(path, opts) {
6548
- return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
6549
- 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 } : {}))));
6550
- }
6551
- async function wsDelete(path) {
6552
- return sendRequest((requestId) => ({
6553
- type: 'delete',
6554
- requestId,
6555
- path,
6556
- }));
6557
- }
6558
- async function wsGetMany(paths) {
6559
- return sendRequest((requestId) => ({
6560
- type: 'getMany',
6561
- requestId,
6562
- paths,
6563
- }));
6564
- }
6565
6756
 
6566
6757
  /**
6567
6758
  * WebSocket Subscription Module
@@ -6742,1008 +6933,6 @@ function toMillis(seconds) {
6742
6933
  return seconds * 1000;
6743
6934
  }
6744
6935
 
6745
- // ---------------------------------------------------------------------------
6746
- // realtime-store.ts — Client-side state manager for realtime apps.
6747
- //
6748
- // Manages: WS connection, in-memory state, IDB persistence, optimistic
6749
- // writes, delta accumulation, loading states, ephemeral/durable tiers.
6750
- // ---------------------------------------------------------------------------
6751
- // ---------------------------------------------------------------------------
6752
- // IDB helpers (lazy-loaded, non-blocking)
6753
- // ---------------------------------------------------------------------------
6754
- const IDB_NAME = 'bounded-realtime';
6755
- const IDB_STORE = 'subscriptions';
6756
- const IDB_VERSION = 1;
6757
- let idbPromise = null;
6758
- function getIDB() {
6759
- if (idbPromise)
6760
- return idbPromise;
6761
- if (typeof indexedDB === 'undefined') {
6762
- return Promise.reject(new Error('IndexedDB not available'));
6763
- }
6764
- idbPromise = new Promise((resolve, reject) => {
6765
- const req = indexedDB.open(IDB_NAME, IDB_VERSION);
6766
- req.onupgradeneeded = () => {
6767
- const db = req.result;
6768
- if (!db.objectStoreNames.contains(IDB_STORE)) {
6769
- db.createObjectStore(IDB_STORE);
6770
- }
6771
- };
6772
- req.onsuccess = () => resolve(req.result);
6773
- req.onerror = () => reject(req.error);
6774
- });
6775
- return idbPromise;
6776
- }
6777
- async function idbGet(key) {
6778
- try {
6779
- const db = await getIDB();
6780
- return new Promise((resolve) => {
6781
- const tx = db.transaction(IDB_STORE, 'readonly');
6782
- const store = tx.objectStore(IDB_STORE);
6783
- const req = store.get(key);
6784
- req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
6785
- req.onerror = () => resolve(null);
6786
- });
6787
- }
6788
- catch (_a) {
6789
- return null;
6790
- }
6791
- }
6792
- async function idbSet(key, value) {
6793
- try {
6794
- const db = await getIDB();
6795
- return new Promise((resolve) => {
6796
- const tx = db.transaction(IDB_STORE, 'readwrite');
6797
- const store = tx.objectStore(IDB_STORE);
6798
- store.put(value, key);
6799
- tx.oncomplete = () => resolve();
6800
- tx.onerror = () => resolve();
6801
- });
6802
- }
6803
- catch (_a) {
6804
- // Best-effort persistence
6805
- }
6806
- }
6807
- // ---------------------------------------------------------------------------
6808
- // RealtimeStore
6809
- // ---------------------------------------------------------------------------
6810
- let nextRequestId = 1;
6811
- function hashForKey(value) {
6812
- let h = 5381;
6813
- for (let i = 0; i < value.length; i++) {
6814
- h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
6815
- }
6816
- return h.toString(36);
6817
- }
6818
- function principalFromToken(token) {
6819
- return token ? `t${hashForKey(token)}` : 'anon';
6820
- }
6821
- class RealtimeStore {
6822
- constructor() {
6823
- this.ws = null;
6824
- this.wsUrl = '';
6825
- this.appId = '';
6826
- this.subscriptions = new Map();
6827
- this.pendingRequests = new Map();
6828
- this.connectPromise = null;
6829
- this.reconnectTimer = null;
6830
- this.reconnectDelay = 1000;
6831
- this.maxReconnectDelay = 30000;
6832
- this.idbFlushTimer = null;
6833
- this.idbDirtyKeys = new Set();
6834
- this.closed = false;
6835
- this.authToken = null;
6836
- this.authPrincipalKey = 'anon';
6837
- this.authenticating = false;
6838
- this.suppressNextReconnect = false;
6839
- this.isServer = false;
6840
- this.tokenRefreshTimer = null;
6841
- // -----------------------------------------------------------------------
6842
- // WebSocket connection
6843
- // -----------------------------------------------------------------------
6844
- this.initPromise = null;
6845
- }
6846
- // -----------------------------------------------------------------------
6847
- // Initialization
6848
- // -----------------------------------------------------------------------
6849
- async init() {
6850
- const config = await getConfig();
6851
- this.appId = config.appId;
6852
- this.wsUrl = config.wsApiUrl;
6853
- this.isServer = config.isServer;
6854
- await this.refreshToken();
6855
- this.startTokenRefresh();
6856
- }
6857
- async refreshToken() {
6858
- let token = null;
6859
- try {
6860
- const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6861
- token = await getIdToken(this.isServer);
6862
- }
6863
- catch ( /* no auth available */_a) { /* no auth available */ }
6864
- this.authToken = token !== null && token !== void 0 ? token : null;
6865
- this.authPrincipalKey = principalFromToken(this.authToken);
6866
- }
6867
- startTokenRefresh() {
6868
- if (this.tokenRefreshTimer)
6869
- return;
6870
- this.tokenRefreshTimer = setInterval(async () => {
6871
- const prevPrincipal = this.authPrincipalKey;
6872
- await this.refreshToken();
6873
- if (this.authPrincipalKey !== prevPrincipal) {
6874
- await this.applyAuthPrincipalChange();
6875
- if (this.subscriptions.size > 0) {
6876
- await this.ensureConnected().catch(() => {
6877
- this.setAllSubscriptionStatus('error');
6878
- });
6879
- }
6880
- }
6881
- }, 5 * 60 * 1000); // Check every 5 minutes
6882
- }
6883
- async ensureInitialized() {
6884
- if (this.appId)
6885
- return;
6886
- if (!this.initPromise)
6887
- this.initPromise = this.init();
6888
- await this.initPromise;
6889
- }
6890
- async ensureCurrentAuth() {
6891
- await this.ensureInitialized();
6892
- const prevPrincipal = this.authPrincipalKey;
6893
- await this.refreshToken();
6894
- if (this.authPrincipalKey !== prevPrincipal) {
6895
- await this.applyAuthPrincipalChange();
6896
- }
6897
- }
6898
- rekeySubscriptionsForPrincipal() {
6899
- const subs = Array.from(this.subscriptions.values());
6900
- this.subscriptions.clear();
6901
- for (const sub of subs) {
6902
- this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
6903
- }
6904
- }
6905
- async applyAuthPrincipalChange() {
6906
- if (this.idbFlushTimer) {
6907
- clearTimeout(this.idbFlushTimer);
6908
- this.idbFlushTimer = null;
6909
- }
6910
- this.idbDirtyKeys.clear();
6911
- this.rekeySubscriptionsForPrincipal();
6912
- for (const sub of this.subscriptions.values()) {
6913
- sub.docs.clear();
6914
- sub.ref.current = sub.docs;
6915
- sub.error = null;
6916
- sub.isStale = false;
6917
- let loaded = false;
6918
- if (sub.tier !== 'ephemeral') {
6919
- const cached = await idbGet(this.idbKey(sub.path));
6920
- if (cached && Array.isArray(cached)) {
6921
- for (const doc of cached) {
6922
- if (doc && doc._id)
6923
- sub.docs.set(doc._id, doc);
6924
- }
6925
- sub.ref.current = sub.docs;
6926
- loaded = sub.docs.size > 0;
6927
- }
6928
- }
6929
- sub.status = loaded ? 'cached' : 'loading';
6930
- sub.isStale = loaded;
6931
- if (loaded)
6932
- this.notifySubscription(sub);
6933
- else
6934
- this.notifyState(sub);
6935
- }
6936
- if (this.ws) {
6937
- const ws = this.ws;
6938
- this.ws = null;
6939
- this.connectPromise = null;
6940
- this.suppressNextReconnect = true;
6941
- try {
6942
- ws.close(1000, 'Auth changed');
6943
- }
6944
- catch ( /* ignore */_a) { /* ignore */ }
6945
- }
6946
- }
6947
- async ensureConnected() {
6948
- var _a;
6949
- await this.ensureCurrentAuth();
6950
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6951
- return;
6952
- if (this.connectPromise)
6953
- return this.connectPromise;
6954
- this.connectPromise = this.connect();
6955
- return this.connectPromise;
6956
- }
6957
- connect() {
6958
- return new Promise((resolve, reject) => {
6959
- if (this.closed) {
6960
- reject(new Error('Store closed'));
6961
- return;
6962
- }
6963
- const params = new URLSearchParams();
6964
- params.set('apiKey', this.appId);
6965
- const url = `${this.wsUrl}?${params.toString()}`;
6966
- const ws = new WebSocket(url);
6967
- this.ws = ws;
6968
- let authTimer = null;
6969
- const finishConnected = () => {
6970
- if (authTimer) {
6971
- clearTimeout(authTimer);
6972
- authTimer = null;
6973
- }
6974
- this.authenticating = false;
6975
- ws.removeEventListener('error', onError);
6976
- this.reconnectDelay = 1000;
6977
- this.connectPromise = null;
6978
- this.resubscribeAll();
6979
- resolve();
6980
- };
6981
- const onOpen = () => {
6982
- if (!this.authToken) {
6983
- finishConnected();
6984
- return;
6985
- }
6986
- this.authenticating = true;
6987
- authTimer = setTimeout(() => {
6988
- this.authenticating = false;
6989
- this.connectPromise = null;
6990
- try {
6991
- ws.close(1008, 'Authentication timeout');
6992
- }
6993
- catch ( /* ignore */_a) { /* ignore */ }
6994
- reject(new Error('WebSocket authentication timeout'));
6995
- }, 10000);
6996
- try {
6997
- ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));
6998
- }
6999
- catch (e) {
7000
- if (authTimer)
7001
- clearTimeout(authTimer);
7002
- this.authenticating = false;
7003
- this.connectPromise = null;
7004
- reject(e);
7005
- }
7006
- };
7007
- const onError = (e) => {
7008
- if (authTimer)
7009
- clearTimeout(authTimer);
7010
- this.authenticating = false;
7011
- ws.removeEventListener('open', onOpen);
7012
- this.connectPromise = null;
7013
- reject(new Error('WebSocket connection failed'));
7014
- };
7015
- ws.addEventListener('open', onOpen, { once: true });
7016
- ws.addEventListener('error', onError, { once: true });
7017
- ws.addEventListener('message', (event) => {
7018
- if (this.authenticating) {
7019
- try {
7020
- const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));
7021
- if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {
7022
- finishConnected();
7023
- return;
7024
- }
7025
- }
7026
- catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }
7027
- }
7028
- this.handleMessage(event.data);
7029
- });
7030
- ws.addEventListener('close', () => {
7031
- if (authTimer)
7032
- clearTimeout(authTimer);
7033
- if (this.ws !== ws) {
7034
- if (this.suppressNextReconnect)
7035
- this.suppressNextReconnect = false;
7036
- return;
7037
- }
7038
- this.authenticating = false;
7039
- this.ws = null;
7040
- this.connectPromise = null;
7041
- this.rejectAllPending('WebSocket closed');
7042
- this.setAllSubscriptionStatus('reconnecting');
7043
- if (this.suppressNextReconnect) {
7044
- this.suppressNextReconnect = false;
7045
- return;
7046
- }
7047
- this.scheduleReconnect();
7048
- });
7049
- });
7050
- }
7051
- scheduleReconnect() {
7052
- if (this.closed)
7053
- return;
7054
- if (this.reconnectTimer)
7055
- clearTimeout(this.reconnectTimer);
7056
- this.reconnectTimer = setTimeout(() => {
7057
- this.ensureConnected().catch(() => {
7058
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
7059
- this.scheduleReconnect();
7060
- });
7061
- }, this.reconnectDelay);
7062
- }
7063
- resubscribeAll() {
7064
- for (const sub of this.subscriptions.values()) {
7065
- this.sendSubscribe(sub);
7066
- }
7067
- }
7068
- // -----------------------------------------------------------------------
7069
- // Message handling
7070
- // -----------------------------------------------------------------------
7071
- handleMessage(raw) {
7072
- const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
7073
- let msg;
7074
- try {
7075
- msg = JSON.parse(text);
7076
- }
7077
- catch (_a) {
7078
- return;
7079
- }
7080
- switch (msg.type) {
7081
- case 'snapshot':
7082
- this.handleSnapshot(msg);
7083
- break;
7084
- case 'delta':
7085
- this.handleDelta(msg);
7086
- break;
7087
- case 'result':
7088
- this.handleResult(msg);
7089
- break;
7090
- case 'error':
7091
- this.handleError(msg);
7092
- break;
7093
- case 'pong':
7094
- break;
7095
- case 'authenticated':
7096
- break;
7097
- // v1 compat: handle legacy message types during transition
7098
- case 'subscribed':
7099
- this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
7100
- break;
7101
- case 'data':
7102
- // Legacy full-snapshot delta — treat as snapshot replacement
7103
- this.handleLegacyData(msg);
7104
- break;
7105
- case 'response':
7106
- this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));
7107
- break;
7108
- }
7109
- }
7110
- handleSnapshot(msg) {
7111
- var _a, _b, _c;
7112
- const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
7113
- if (!subId)
7114
- return;
7115
- const sub = this.findSubscriptionById(subId);
7116
- if (!sub)
7117
- return;
7118
- const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];
7119
- const docsArray = Array.isArray(docs) ? docs : [docs];
7120
- sub.docs.clear();
7121
- for (const doc of docsArray) {
7122
- if (doc && doc._id) {
7123
- sub.docs.set(doc._id, doc);
7124
- }
7125
- }
7126
- sub.ref.current = sub.docs;
7127
- sub.status = 'live';
7128
- sub.isStale = false;
7129
- sub.error = null;
7130
- this.notifySubscription(sub);
7131
- this.markIdbDirty(sub.path);
7132
- }
7133
- handleDelta(msg) {
7134
- var _a, _b;
7135
- const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
7136
- if (!subId)
7137
- return;
7138
- const sub = this.findSubscriptionById(subId);
7139
- if (!sub)
7140
- return;
7141
- if (sub.tier === 'ephemeral') {
7142
- // Ephemeral: just overwrite, no accumulation logic
7143
- if (msg.change === 'removed' && msg.docId) {
7144
- sub.docs.delete(msg.docId);
7145
- }
7146
- else if (msg.doc && msg.doc._id) {
7147
- sub.docs.set(msg.doc._id, msg.doc);
7148
- }
7149
- sub.ref.current = sub.docs;
7150
- if (sub.options.mode !== 'ref') {
7151
- this.notifySubscription(sub);
7152
- }
7153
- return;
7154
- }
7155
- // Durable/checkpointed: full delta handling
7156
- switch (msg.change) {
7157
- case 'added':
7158
- case 'modified':
7159
- if (msg.doc && msg.doc._id) {
7160
- sub.docs.set(msg.doc._id, msg.doc);
7161
- }
7162
- break;
7163
- case 'removed':
7164
- if (msg.docId) {
7165
- sub.docs.delete(msg.docId);
7166
- }
7167
- else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {
7168
- sub.docs.delete(msg.doc._id);
7169
- }
7170
- break;
7171
- }
7172
- sub.ref.current = sub.docs;
7173
- this.notifySubscription(sub);
7174
- this.markIdbDirty(sub.path);
7175
- }
7176
- handleLegacyData(msg) {
7177
- // Legacy v1 format: 'data' message with full snapshot or single doc
7178
- const subId = msg.subscriptionId;
7179
- if (!subId)
7180
- return;
7181
- const sub = this.findSubscriptionById(subId);
7182
- if (!sub)
7183
- return;
7184
- if (Array.isArray(msg.data)) {
7185
- // Full snapshot replacement
7186
- sub.docs.clear();
7187
- for (const doc of msg.data) {
7188
- if (doc && doc._id)
7189
- sub.docs.set(doc._id, doc);
7190
- }
7191
- }
7192
- else if (msg.data && msg.data._id) {
7193
- // Single doc update
7194
- sub.docs.set(msg.data._id, msg.data);
7195
- }
7196
- else if (msg.data === null) ;
7197
- sub.ref.current = sub.docs;
7198
- sub.status = 'live';
7199
- sub.isStale = false;
7200
- this.notifySubscription(sub);
7201
- this.markIdbDirty(sub.path);
7202
- }
7203
- handleResult(msg) {
7204
- var _a, _b, _c, _d;
7205
- const requestId = msg.requestId;
7206
- if (!requestId)
7207
- return;
7208
- const pending = this.pendingRequests.get(requestId);
7209
- if (!pending)
7210
- return;
7211
- this.pendingRequests.delete(requestId);
7212
- clearTimeout(pending.timeout);
7213
- const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);
7214
- if (ok) {
7215
- pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);
7216
- }
7217
- else {
7218
- pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));
7219
- }
7220
- }
7221
- handleError(msg) {
7222
- var _a, _b, _c;
7223
- const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
7224
- if (msg.code)
7225
- error.code = msg.code;
7226
- if (msg.subscriptionId || msg.id)
7227
- error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
7228
- const requestId = msg.requestId;
7229
- if (requestId) {
7230
- const pending = this.pendingRequests.get(requestId);
7231
- if (pending) {
7232
- this.pendingRequests.delete(requestId);
7233
- clearTimeout(pending.timeout);
7234
- pending.reject(error);
7235
- }
7236
- }
7237
- const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
7238
- if (subId) {
7239
- const sub = this.findSubscriptionById(subId);
7240
- if (sub) {
7241
- sub.status = 'error';
7242
- sub.error = error;
7243
- this.notifyState(sub);
7244
- for (const callback of Array.from(sub.errorCallbacks)) {
7245
- try {
7246
- callback(error);
7247
- }
7248
- catch ( /* swallow */_d) { /* swallow */ }
7249
- }
7250
- }
7251
- }
7252
- }
7253
- // -----------------------------------------------------------------------
7254
- // Subscribe
7255
- // -----------------------------------------------------------------------
7256
- async subscribe(path, opts = {}) {
7257
- var _a;
7258
- await this.ensureCurrentAuth();
7259
- const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7260
- const subKey = this.getSubKey(path, opts);
7261
- let sub = this.subscriptions.get(subKey);
7262
- if (sub) {
7263
- // Existing subscription — add callback
7264
- if (opts.onData)
7265
- sub.callbacks.add(opts.onData);
7266
- if (opts.onState)
7267
- sub.stateCallbacks.add(opts.onState);
7268
- if (opts.onError)
7269
- sub.errorCallbacks.add(opts.onError);
7270
- // Immediately deliver current state
7271
- if (opts.onData && sub.docs.size > 0) {
7272
- opts.onData(this.docsToArray(sub));
7273
- }
7274
- if (opts.onState) {
7275
- opts.onState(this.getState(sub));
7276
- }
7277
- return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7278
- }
7279
- // New subscription
7280
- const subId = `sub_${nextRequestId++}`;
7281
- sub = {
7282
- id: subId,
7283
- path,
7284
- tier,
7285
- options: opts,
7286
- docs: new Map(),
7287
- status: 'idle',
7288
- isStale: false,
7289
- error: null,
7290
- callbacks: new Set(opts.onData ? [opts.onData] : []),
7291
- stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7292
- errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7293
- ref: { current: new Map() },
7294
- };
7295
- this.subscriptions.set(subKey, sub);
7296
- // Step 1: Load from IDB (durable/checkpointed only)
7297
- if (tier !== 'ephemeral') {
7298
- const cached = await idbGet(this.idbKey(path));
7299
- if (cached && Array.isArray(cached)) {
7300
- for (const doc of cached) {
7301
- if (doc && doc._id)
7302
- sub.docs.set(doc._id, doc);
7303
- }
7304
- sub.ref.current = sub.docs;
7305
- sub.status = 'cached';
7306
- sub.isStale = true;
7307
- this.notifySubscription(sub);
7308
- }
7309
- }
7310
- // Step 2: Connect and subscribe via WS
7311
- sub.status = sub.docs.size > 0 ? 'cached' : 'loading';
7312
- this.notifyState(sub);
7313
- try {
7314
- await this.ensureConnected();
7315
- this.sendSubscribe(sub);
7316
- }
7317
- catch (_b) {
7318
- sub.status = 'error';
7319
- sub.error = new Error('Connection failed');
7320
- this.notifyState(sub);
7321
- }
7322
- return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7323
- }
7324
- getRef(path, opts = {}) {
7325
- var _a;
7326
- const subKey = this.getSubKey(path, opts);
7327
- const sub = this.subscriptions.get(subKey);
7328
- if (sub)
7329
- return sub.ref;
7330
- // Auto-subscribe in ref mode
7331
- const ref = { current: new Map() };
7332
- this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });
7333
- const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));
7334
- return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;
7335
- }
7336
- // -----------------------------------------------------------------------
7337
- // CRUD operations
7338
- // -----------------------------------------------------------------------
7339
- async set(path, doc) {
7340
- var _a;
7341
- await this.ensureConnected();
7342
- // Resolve operations (Increment, Time.Now) client-side for optimistic update
7343
- const resolvedDoc = this.resolveOperations(doc, path);
7344
- // Optimistic update: apply to local state immediately
7345
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7346
- const collectionPath = this.getCollectionPath(normalizedPath);
7347
- const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), {
7348
- // System timestamp field name: the Bounded worker stamps the neutral
7349
- // `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.
7350
- // Match it so the optimistic doc lines up with the server's confirmation.
7351
- [isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });
7352
- const sub = this.findSubscriptionByPath(collectionPath);
7353
- let prevDoc = null;
7354
- if (sub) {
7355
- prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
7356
- sub.docs.set(normalizedPath, optimisticDoc);
7357
- sub.ref.current = sub.docs;
7358
- this.notifySubscription(sub);
7359
- }
7360
- // Send to server
7361
- const requestId = `r_${nextRequestId++}`;
7362
- try {
7363
- const result = await this.sendRequest(requestId, {
7364
- type: 'set',
7365
- requestId,
7366
- documents: [{ destinationPath: normalizedPath, document: doc }],
7367
- });
7368
- // Replace optimistic doc with server-confirmed version
7369
- if (sub && result && typeof result === 'object') {
7370
- const serverDoc = Array.isArray(result) ? result[0] : result;
7371
- if (serverDoc && serverDoc._id) {
7372
- sub.docs.set(serverDoc._id, serverDoc);
7373
- sub.ref.current = sub.docs;
7374
- this.notifySubscription(sub);
7375
- this.markIdbDirty(collectionPath);
7376
- }
7377
- }
7378
- return Array.isArray(result) ? result[0] : result;
7379
- }
7380
- catch (err) {
7381
- // Revert optimistic update
7382
- if (sub) {
7383
- if (prevDoc) {
7384
- sub.docs.set(normalizedPath, prevDoc);
7385
- }
7386
- else {
7387
- sub.docs.delete(normalizedPath);
7388
- }
7389
- sub.ref.current = sub.docs;
7390
- this.notifySubscription(sub);
7391
- }
7392
- throw err;
7393
- }
7394
- }
7395
- async get(path) {
7396
- await this.ensureCurrentAuth();
7397
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7398
- // Check local subscriptions first
7399
- const collectionPath = this.getCollectionPath(normalizedPath);
7400
- const sub = this.findSubscriptionByPath(collectionPath);
7401
- if (sub && sub.status === 'live') {
7402
- const doc = sub.docs.get(normalizedPath);
7403
- return doc !== null && doc !== void 0 ? doc : null;
7404
- }
7405
- // One-shot WS fetch
7406
- await this.ensureConnected();
7407
- const requestId = `r_${nextRequestId++}`;
7408
- return this.sendRequest(requestId, {
7409
- type: 'get',
7410
- requestId,
7411
- path: normalizedPath,
7412
- });
7413
- }
7414
- async getMany(paths) {
7415
- await this.ensureConnected();
7416
- const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);
7417
- const requestId = `r_${nextRequestId++}`;
7418
- return this.sendRequest(requestId, {
7419
- type: 'getMany',
7420
- requestId,
7421
- paths: normalizedPaths,
7422
- });
7423
- }
7424
- async delete(path) {
7425
- var _a;
7426
- await this.ensureConnected();
7427
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7428
- // Optimistic: remove from local state
7429
- const collectionPath = this.getCollectionPath(normalizedPath);
7430
- const sub = this.findSubscriptionByPath(collectionPath);
7431
- let prevDoc = null;
7432
- if (sub) {
7433
- prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
7434
- sub.docs.delete(normalizedPath);
7435
- sub.ref.current = sub.docs;
7436
- this.notifySubscription(sub);
7437
- }
7438
- const requestId = `r_${nextRequestId++}`;
7439
- try {
7440
- await this.sendRequest(requestId, {
7441
- type: 'delete',
7442
- requestId,
7443
- path: normalizedPath,
7444
- });
7445
- if (sub)
7446
- this.markIdbDirty(collectionPath);
7447
- }
7448
- catch (err) {
7449
- // Revert
7450
- if (sub && prevDoc) {
7451
- sub.docs.set(normalizedPath, prevDoc);
7452
- sub.ref.current = sub.docs;
7453
- this.notifySubscription(sub);
7454
- }
7455
- throw err;
7456
- }
7457
- }
7458
- async query(path, opts) {
7459
- await this.ensureConnected();
7460
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7461
- const requestId = `r_${nextRequestId++}`;
7462
- return this.sendRequest(requestId, Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId, path: normalizedPath }, ((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: true } : {})));
7463
- }
7464
- async count(path) {
7465
- var _a;
7466
- await this.ensureConnected();
7467
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7468
- const requestId = `r_${nextRequestId++}`;
7469
- const result = await this.sendRequest(requestId, {
7470
- type: 'count',
7471
- requestId,
7472
- path: normalizedPath,
7473
- });
7474
- return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);
7475
- }
7476
- async aggregate(path, operation, opts) {
7477
- await this.ensureConnected();
7478
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7479
- const requestId = `r_${nextRequestId++}`;
7480
- return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));
7481
- }
7482
- // -----------------------------------------------------------------------
7483
- // Helpers
7484
- // -----------------------------------------------------------------------
7485
- sendSubscribe(sub) {
7486
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
7487
- return;
7488
- const msg = {
7489
- type: 'subscribe',
7490
- subscriptionId: sub.id,
7491
- path: sub.path,
7492
- };
7493
- if (sub.options.filter)
7494
- msg.filter = sub.options.filter;
7495
- if (sub.options.includeSubPaths)
7496
- msg.includeSubPaths = true;
7497
- if (sub.options.limit)
7498
- msg.limit = sub.options.limit;
7499
- if (sub.options.prompt)
7500
- msg.prompt = sub.options.prompt;
7501
- this.ws.send(JSON.stringify(msg));
7502
- }
7503
- sendRequest(requestId, msg) {
7504
- return new Promise((resolve, reject) => {
7505
- const timeout = setTimeout(() => {
7506
- this.pendingRequests.delete(requestId);
7507
- reject(new Error('Request timed out'));
7508
- }, 30000);
7509
- this.pendingRequests.set(requestId, { resolve, reject, timeout });
7510
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7511
- this.ws.send(JSON.stringify(msg));
7512
- }
7513
- else {
7514
- this.pendingRequests.delete(requestId);
7515
- clearTimeout(timeout);
7516
- reject(new Error('WebSocket not connected'));
7517
- }
7518
- });
7519
- }
7520
- notifySubscription(sub) {
7521
- const data = this.docsToArray(sub);
7522
- const callbacks = Array.from(sub.callbacks);
7523
- for (const cb of callbacks) {
7524
- try {
7525
- cb(data);
7526
- }
7527
- catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }
7528
- }
7529
- this.notifyState(sub);
7530
- }
7531
- notifyState(sub) {
7532
- const state = this.getState(sub);
7533
- const callbacks = Array.from(sub.stateCallbacks);
7534
- for (const cb of callbacks) {
7535
- try {
7536
- cb(state);
7537
- }
7538
- catch ( /* swallow */_a) { /* swallow */ }
7539
- }
7540
- }
7541
- getState(sub) {
7542
- return {
7543
- data: this.docsToArray(sub),
7544
- status: sub.status,
7545
- isStale: sub.isStale,
7546
- error: sub.error,
7547
- };
7548
- }
7549
- docsToArray(sub) {
7550
- return Array.from(sub.docs.values());
7551
- }
7552
- findSubscriptionById(id) {
7553
- for (const sub of this.subscriptions.values()) {
7554
- if (sub.id === id)
7555
- return sub;
7556
- }
7557
- return undefined;
7558
- }
7559
- findSubscriptionByPath(collectionPath) {
7560
- for (const sub of this.subscriptions.values()) {
7561
- const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;
7562
- if (subPath === collectionPath)
7563
- return sub;
7564
- if (collectionPath.startsWith(subPath + '/'))
7565
- return sub;
7566
- }
7567
- return undefined;
7568
- }
7569
- getCollectionPath(docPath) {
7570
- const segments = docPath.split('/');
7571
- if (segments.length % 2 === 0) {
7572
- return segments.slice(0, -1).join('/');
7573
- }
7574
- return docPath;
7575
- }
7576
- getSubKey(path, opts) {
7577
- const parts = [this.appId, this.authPrincipalKey, path];
7578
- if (opts.filter)
7579
- parts.push(JSON.stringify(opts.filter));
7580
- if (opts.prompt)
7581
- parts.push(opts.prompt);
7582
- if (opts.tier)
7583
- parts.push(opts.tier);
7584
- return parts.join('::');
7585
- }
7586
- idbKey(path) {
7587
- return `${this.appId}:${this.authPrincipalKey}:${path}`;
7588
- }
7589
- markIdbDirty(path) {
7590
- const sub = this.findSubscriptionByPath(path);
7591
- if (sub && sub.tier === 'ephemeral')
7592
- return;
7593
- this.idbDirtyKeys.add(path);
7594
- if (!this.idbFlushTimer) {
7595
- this.idbFlushTimer = setTimeout(() => {
7596
- this.flushIdb();
7597
- this.idbFlushTimer = null;
7598
- }, 500);
7599
- }
7600
- }
7601
- async flushIdb() {
7602
- const keys = Array.from(this.idbDirtyKeys);
7603
- this.idbDirtyKeys.clear();
7604
- for (const path of keys) {
7605
- const sub = this.findSubscriptionByPath(path);
7606
- if (sub && sub.tier !== 'ephemeral') {
7607
- const docs = this.docsToArray(sub);
7608
- await idbSet(this.idbKey(path), docs);
7609
- }
7610
- }
7611
- }
7612
- createUnsubscribe(subKey, subId, onData, onState, onError) {
7613
- return async () => {
7614
- var _a;
7615
- const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
7616
- if (!sub)
7617
- return;
7618
- const currentSubKey = this.getSubKey(sub.path, sub.options);
7619
- if (onData)
7620
- sub.callbacks.delete(onData);
7621
- if (onState)
7622
- sub.stateCallbacks.delete(onState);
7623
- if (onError)
7624
- sub.errorCallbacks.delete(onError);
7625
- // If no more callbacks, unsubscribe entirely
7626
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7627
- this.subscriptions.delete(subKey);
7628
- this.subscriptions.delete(currentSubKey);
7629
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7630
- this.ws.send(JSON.stringify({
7631
- type: 'unsubscribe',
7632
- subscriptionId: sub.id,
7633
- }));
7634
- }
7635
- }
7636
- };
7637
- }
7638
- resolveOperations(doc, path) {
7639
- var _a;
7640
- if (!doc || typeof doc !== 'object')
7641
- return doc;
7642
- const resolved = {};
7643
- for (const [key, value] of Object.entries(doc)) {
7644
- if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {
7645
- const op = value;
7646
- if (op.operation === 'time' && op.value === 'now') {
7647
- resolved[key] = Math.floor(Date.now() / 1000);
7648
- }
7649
- else if (op.operation === 'increment') {
7650
- // For optimistic: get current value and add
7651
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7652
- const collectionPath = this.getCollectionPath(normalizedPath);
7653
- const sub = this.findSubscriptionByPath(collectionPath);
7654
- const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);
7655
- const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;
7656
- resolved[key] = (typeof current === 'number' ? current : 0) + op.value;
7657
- }
7658
- else {
7659
- resolved[key] = value;
7660
- }
7661
- }
7662
- else {
7663
- resolved[key] = value;
7664
- }
7665
- }
7666
- return resolved;
7667
- }
7668
- rejectAllPending(reason) {
7669
- for (const [requestId, pending] of this.pendingRequests) {
7670
- clearTimeout(pending.timeout);
7671
- pending.reject(new Error(reason));
7672
- }
7673
- this.pendingRequests.clear();
7674
- }
7675
- setAllSubscriptionStatus(status) {
7676
- for (const sub of this.subscriptions.values()) {
7677
- sub.status = status;
7678
- this.notifyState(sub);
7679
- }
7680
- }
7681
- // -----------------------------------------------------------------------
7682
- // Lifecycle
7683
- // -----------------------------------------------------------------------
7684
- close() {
7685
- this.closed = true;
7686
- if (this.reconnectTimer)
7687
- clearTimeout(this.reconnectTimer);
7688
- if (this.idbFlushTimer)
7689
- clearTimeout(this.idbFlushTimer);
7690
- if (this.tokenRefreshTimer)
7691
- clearInterval(this.tokenRefreshTimer);
7692
- this.flushIdb();
7693
- if (this.ws) {
7694
- this.ws.close(1000, 'Store closed');
7695
- this.ws = null;
7696
- }
7697
- this.rejectAllPending('Store closed');
7698
- this.subscriptions.clear();
7699
- }
7700
- async reconnectWithNewAuth() {
7701
- if (this.closed)
7702
- return;
7703
- await this.ensureInitialized();
7704
- await this.refreshToken();
7705
- await this.applyAuthPrincipalChange();
7706
- if (this.subscriptions.size > 0) {
7707
- await this.ensureConnected().catch((error) => {
7708
- this.setAllSubscriptionStatus('error');
7709
- for (const sub of this.subscriptions.values()) {
7710
- sub.error = error instanceof Error ? error : new Error(String(error));
7711
- this.notifyState(sub);
7712
- }
7713
- });
7714
- }
7715
- }
7716
- }
7717
- // ---------------------------------------------------------------------------
7718
- // Singleton instance
7719
- // ---------------------------------------------------------------------------
7720
- let storeInstance = null;
7721
- function getRealtimeStore() {
7722
- if (!storeInstance) {
7723
- storeInstance = new RealtimeStore();
7724
- }
7725
- return storeInstance;
7726
- }
7727
- function resetRealtimeStore() {
7728
- if (storeInstance) {
7729
- storeInstance.close();
7730
- storeInstance = null;
7731
- }
7732
- }
7733
- async function reconnectRealtimeStoreWithNewAuth() {
7734
- if (storeInstance) {
7735
- await storeInstance.reconnectWithNewAuth();
7736
- }
7737
- }
7738
-
7739
- var realtimeStore = /*#__PURE__*/Object.freeze({
7740
- __proto__: null,
7741
- RealtimeStore: RealtimeStore,
7742
- getRealtimeStore: getRealtimeStore,
7743
- reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
7744
- resetRealtimeStore: resetRealtimeStore
7745
- });
7746
-
7747
6936
  // ---------------------------------------------------------------------------
7748
6937
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
7749
6938
  //
@@ -7803,7 +6992,7 @@ async function invoke(name, args = {}, opts = {}) {
7803
6992
  const authHeader = ((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders)
7804
6993
  ? await opts._overrides._getAuthHeaders()
7805
6994
  : await createAuthHeader(config.isServer);
7806
- const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_b = stripAuthHeaders(opts.headers)) !== null && _b !== void 0 ? _b : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
6995
+ const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId }, ((_b = stripAuthHeaders(opts.headers)) !== null && _b !== void 0 ? _b : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
7807
6996
  const controller = new AbortController();
7808
6997
  const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7809
6998
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -7872,8 +7061,8 @@ const functions = { invoke };
7872
7061
  // Subscribing to your view: a per-player view doc lives at
7873
7062
  // `<roomPath>/view/<myUserId>` (the policy declares
7874
7063
  // `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
7875
- // Wallet-address keyed view paths remain supported through opts.address for
7876
- // older policies, but new live rooms should key views by @user.id.
7064
+ // View paths key only by @user.id; wallet-address aliases are intentionally not
7065
+ // accepted by this helper.
7877
7066
  // ---------------------------------------------------------------------------
7878
7067
  class LiveIntentError extends Error {
7879
7068
  constructor(message, statusCode, details) {
@@ -7967,7 +7156,7 @@ async function intent(roomPath, intent, opts = {}) {
7967
7156
  const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
7968
7157
  const buildHeaders = async () => {
7969
7158
  const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
7970
- return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
7159
+ return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
7971
7160
  };
7972
7161
  const controller = new AbortController();
7973
7162
  const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
@@ -8028,7 +7217,7 @@ async function status(roomPath, opts = {}) {
8028
7217
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
8029
7218
  const config = await getConfig();
8030
7219
  const base = realtimeHttpBase(config.wsApiUrl);
8031
- const headers = Object.assign({ 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
7220
+ const headers = Object.assign({ 'X-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
8032
7221
  const controller = new AbortController();
8033
7222
  const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
8034
7223
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -8070,29 +7259,27 @@ async function status(roomPath, opts = {}) {
8070
7259
  * subscribe('<roomPath>/view/<myUserId>', { onData, onError })
8071
7260
  *
8072
7261
  * The view id defaults to the logged-in user's @user.id (from the session token
8073
- * claims); pass `opts.userId` to override. `opts.address` is kept as a legacy
8074
- * alias for older wallet-address keyed policies. Returns the unsubscribe
8075
- * function (a Promise<() => Promise<void>>, same as `subscribe`).
7262
+ * claims); pass `opts.userId` to override. Returns the unsubscribe function
7263
+ * (a Promise<() => Promise<void>>, same as `subscribe`).
8076
7264
  *
8077
7265
  * Note: this is a browser-first helper (the WS subscription manager is
8078
7266
  * browser-oriented). Server consumers should use `live.intent`.
8079
7267
  */
8080
7268
  async function subscribeView(roomPath, opts) {
8081
- var _a, _b, _c;
8082
7269
  if (!roomPath || typeof roomPath !== 'string') {
8083
7270
  throw new LiveIntentError('A room path is required');
8084
7271
  }
8085
7272
  if (!opts || typeof opts.onData !== 'function') {
8086
7273
  throw new LiveIntentError('subscribeView requires an onData callback');
8087
7274
  }
8088
- let viewUserId = (_a = opts.userId) !== null && _a !== void 0 ? _a : opts.address;
7275
+ let viewUserId = opts.userId;
8089
7276
  if (!viewUserId) {
8090
7277
  const config = await getConfig();
8091
7278
  const info = await getUserInfo(config.isServer);
8092
7279
  // getUserInfo returns the RAW idToken payload. The universal live view key
8093
- // is @user.id (`custom:userId`); wallet-address keyed views are still
8094
- // resolved as a compatibility fallback for older policies.
8095
- viewUserId = (_c = (_b = info === null || info === void 0 ? void 0 : info['custom:userId']) !== null && _b !== void 0 ? _b : info === null || info === void 0 ? void 0 : info['custom:walletAddress']) !== null && _c !== void 0 ? _c : info === null || info === void 0 ? void 0 : info.address;
7280
+ // is @user.id (`custom:userId`); wallet-address keyed compatibility aliases
7281
+ // are intentionally not accepted.
7282
+ viewUserId = info === null || info === void 0 ? void 0 : info['custom:userId'];
8096
7283
  }
8097
7284
  if (!viewUserId || typeof viewUserId !== 'string') {
8098
7285
  throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
@@ -8162,5 +7349,5 @@ function defineLiveModule(mod) {
8162
7349
  return mod;
8163
7350
  }
8164
7351
 
8165
- export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, RealtimeStore, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getRealtimeStore, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, resetRealtimeStore, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects, wsDelete, wsGet, wsGetMany, wsQuery, wsSet };
7352
+ export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects };
8166
7353
  //# sourceMappingURL=index.mjs.map