@bounded-sh/core 0.0.17 → 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.js CHANGED
@@ -30,7 +30,6 @@ let clientConfig = {
30
30
  // User configured settings
31
31
  name: '',
32
32
  logoUrl: '',
33
- apiKey: '',
34
33
  // Bounded production is the out-of-the-box default — a Bounded app needs only
35
34
  // `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
36
35
  network: 'bounded-production',
@@ -107,7 +106,7 @@ function isBoundedNetwork() {
107
106
  }
108
107
  function init(newConfig) {
109
108
  return new Promise((resolve, reject) => {
110
- if (!newConfig.apiKey && !newConfig.appId) {
109
+ if (!newConfig.appId) {
111
110
  reject(new Error('No app ID provided.'));
112
111
  return;
113
112
  }
@@ -2883,9 +2882,25 @@ async function refreshSession(refreshToken, issuer) {
2883
2882
  })();
2884
2883
  return refreshInFlight$1;
2885
2884
  }
2885
+ class SessionRevokeError extends Error {
2886
+ constructor(message, cause) {
2887
+ super(message);
2888
+ this.name = 'SessionRevokeError';
2889
+ this.cause = cause;
2890
+ }
2891
+ }
2892
+ function revokeFailureMessage(err) {
2893
+ var _a, _b;
2894
+ const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
2895
+ const statusText = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.statusText;
2896
+ const suffix = typeof status === 'number'
2897
+ ? ` (HTTP ${status}${statusText ? ` ${statusText}` : ''})`
2898
+ : '';
2899
+ return `Failed to revoke refresh token server-side${suffix}. The refresh-token family may still be active.`;
2900
+ }
2886
2901
  /**
2887
- * Revoke a session's refresh-token family server-side (logout). Best-effort: if the
2888
- * call fails the local logout still proceeds. Routes to the minting issuer.
2902
+ * Revoke a session's refresh-token family server-side (logout). Routes to the
2903
+ * minting issuer and rejects if the revoke request fails.
2889
2904
  */
2890
2905
  async function revokeSession(refreshToken, issuer) {
2891
2906
  if (!refreshToken)
@@ -2898,8 +2913,8 @@ async function revokeSession(refreshToken, issuer) {
2898
2913
  appId: config.appId,
2899
2914
  }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
2900
2915
  }
2901
- catch (_a) {
2902
- // best-effort: logout must succeed even if the revoke call doesn't
2916
+ catch (err) {
2917
+ throw new SessionRevokeError(revokeFailureMessage(err), err);
2903
2918
  }
2904
2919
  }
2905
2920
  async function signSessionCreateMessage(_signMessageFunction) {
@@ -3988,9 +4003,7 @@ async function getUserInfo(isServer) {
3988
4003
  *
3989
4004
  * Mirrors the realtime-worker auth.ts identity resolution so the client-side
3990
4005
  * `user` object matches what the backend authenticates as:
3991
- * - id = custom:userId when present, else custom:walletAddress (the fallback
3992
- * keeps wallet/SIWS tokens — which omit userId — AND legacy Better Auth
3993
- * tokens — which put the account id in custom:walletAddress — working).
4006
+ * - id = custom:userId only.
3994
4007
  * - address = custom:walletAddress only (a REAL wallet). NEVER falls back to the
3995
4008
  * identity: an opaque id is not a spendable onchain address. null for
3996
4009
  * email-only sessions.
@@ -4015,7 +4028,7 @@ function deriveUserIdentityFromIdToken(idToken) {
4015
4028
  const userIdClaim = payload['custom:userId'];
4016
4029
  const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
4017
4030
  ? userIdClaim
4018
- : address;
4031
+ : null;
4019
4032
  const emailClaim = payload['email'];
4020
4033
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
4021
4034
  ? emailClaim.toLowerCase()
@@ -4071,17 +4084,6 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
4071
4084
  await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
4072
4085
  }
4073
4086
 
4074
- var utils = /*#__PURE__*/Object.freeze({
4075
- __proto__: null,
4076
- createAuthHeader: createAuthHeader,
4077
- deriveUserIdentityFromIdToken: deriveUserIdentityFromIdToken,
4078
- getIdToken: getIdToken,
4079
- getRefreshToken: getRefreshToken,
4080
- getSessionIssuer: getSessionIssuer,
4081
- getUserInfo: getUserInfo,
4082
- updateIdTokenAndAccessToken: updateIdTokenAndAccessToken
4083
- });
4084
-
4085
4087
  const apiClient = axios.create();
4086
4088
  axiosRetry(apiClient, {
4087
4089
  retries: 2,
@@ -4146,7 +4148,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4146
4148
  const authHeader = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
4147
4149
  ? await _overrides._getAuthHeaders()
4148
4150
  : await createAuthHeader(config.isServer);
4149
- const headers = Object.assign({ "Content-Type": "application/json", "X-Public-App-Id": config.appId, "X-App-Id": config.appId }, authHeader);
4151
+ const headers = Object.assign({ "Content-Type": "application/json", "X-App-Id": config.appId }, authHeader);
4150
4152
  // Apply custom headers from _overrides
4151
4153
  if (_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers) {
4152
4154
  Object.assign(headers, _overrides.headers);
@@ -4250,6 +4252,7 @@ const pendingRequests = {};
4250
4252
  const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
4251
4253
  // Last time we cleaned up the cache
4252
4254
  let lastCacheCleanup = Date.now();
4255
+ let uncacheableReadKeyCounter = 0;
4253
4256
  /**
4254
4257
  * Return the leaf document key (last path segment) for a document path.
4255
4258
  *
@@ -4335,57 +4338,74 @@ function normalizeReadResult(responseData, pathIsDocument) {
4335
4338
  }
4336
4339
  return responseData;
4337
4340
  }
4338
- function hashForKey$2(value) {
4341
+ function hashForKey$1(value) {
4339
4342
  let h = 5381;
4340
4343
  for (let i = 0; i < value.length; i++) {
4341
4344
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
4342
4345
  }
4343
4346
  return h.toString(36);
4344
4347
  }
4348
+ function hasAuthHeader(headers) {
4349
+ if (!headers)
4350
+ return false;
4351
+ return (Object.prototype.hasOwnProperty.call(headers, 'Authorization') ||
4352
+ Object.prototype.hasOwnProperty.call(headers, 'authorization'));
4353
+ }
4345
4354
  function authValueFromHeaders(headers) {
4346
4355
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4347
4356
  }
4348
4357
  function principalFromAuthValue(authValue) {
4349
- return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
4358
+ return authValue ? `h${hashForKey$1(authValue)}` : null;
4350
4359
  }
4351
4360
  function principalFromIdToken$1(idToken) {
4352
- return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
4361
+ return idToken ? `t${hashForKey$1(idToken)}` : null;
4362
+ }
4363
+ function uncacheableReadKey(appId, scope) {
4364
+ uncacheableReadKeyCounter += 1;
4365
+ return `${appId}:${scope}-uncacheable-${uncacheableReadKeyCounter}`;
4353
4366
  }
4354
4367
  /**
4355
4368
  * SECURITY (H1): Read caches must be keyed by the caller's principal, not just
4356
4369
  * by path/filter/shape. In a shared process / SSR worker / browser login-switch,
4357
4370
  * keying by path alone lets User B receive User A's cached private read before
4358
- * any server read rule runs. This returns `appId:<principal>` for the identity a
4359
- * given read will actually authenticate as. This intentionally treats JWTs as
4360
- * opaque unverified bearer material never decoded claims and never caller
4361
- * identity hints such as `_walletAddress`. A forged token that merely claims
4362
- * another user's `sub` must miss that user's cache and go to the server.
4371
+ * any server read rule runs. This returns `appId:<principal>` for the opaque
4372
+ * auth material a given read will actually authenticate with. No-auth reads are
4373
+ * deliberately marked uncacheable instead of sharing an implicit `anon` bucket.
4374
+ * JWTs are intentionally treated as opaque unverified bearer material never
4375
+ * decoded claims and never caller identity hints such as `_walletAddress`.
4363
4376
  */
4364
4377
  async function getReadPrincipalKey(overrides) {
4365
4378
  const config = await getConfig();
4366
4379
  const appId = config.appId || '';
4367
4380
  // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4368
4381
  // caller-supplied Authorization is the real request auth when present.
4369
- const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4370
- if (directAuth) {
4371
- return `${appId}:${principalFromAuthValue(directAuth)}`;
4382
+ if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
4383
+ const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
4384
+ return principal
4385
+ ? { key: `${appId}:${principal}`, cacheable: true }
4386
+ : { key: uncacheableReadKey(appId, 'h'), cacheable: false };
4372
4387
  }
4373
4388
  // Per-request auth-header override (wallet client). Key by the exact opaque
4374
4389
  // header it produces, not decoded claims or the unverified _walletAddress hint.
4375
4390
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4376
4391
  try {
4377
4392
  const headers = await overrides._getAuthHeaders();
4378
- return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
4393
+ const principal = principalFromAuthValue(authValueFromHeaders(headers));
4394
+ return principal
4395
+ ? { key: `${appId}:o${principal}`, cacheable: true }
4396
+ : { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4379
4397
  }
4380
4398
  catch (_a) {
4381
- // If we can't resolve the override identity, use a unique-ish key so we
4382
- // never collide with (and serve) another principal's cached entry.
4383
- return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
4399
+ // If we can't resolve the override identity, do not read/write cache.
4400
+ return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4384
4401
  }
4385
4402
  }
4386
4403
  // Ambient session principal.
4387
4404
  const idToken = await getIdToken(config.isServer);
4388
- return `${appId}:${principalFromIdToken$1(idToken)}`;
4405
+ const principal = principalFromIdToken$1(idToken);
4406
+ return principal
4407
+ ? { key: `${appId}:${principal}`, cacheable: true }
4408
+ : { key: uncacheableReadKey(appId, 'a'), cacheable: false };
4389
4409
  }
4390
4410
  /**
4391
4411
  * Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
@@ -4678,18 +4698,20 @@ async function get(path, opts = {}) {
4678
4698
  // Create cache key combining path, prompt, filter, sort, includeSubPaths,
4679
4699
  // shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
4680
4700
  // so a private read cached for one user is never served to another in a
4681
- // shared process / SSR worker / browser login-switch.
4701
+ // shared process / SSR worker / browser login-switch. The cache is opt-in
4702
+ // and disabled for no-auth reads, which get an uncacheable unique key.
4682
4703
  const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
4683
4704
  const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
4684
4705
  const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
4685
- const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
4686
- const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
4687
- const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
4706
+ const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
4707
+ const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
4708
+ const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
4688
4709
  const principalKey = await getReadPrincipalKey(opts._overrides);
4689
- const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4710
+ const cacheKey = `${principalKey.key}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
4711
+ const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4690
4712
  const now = Date.now();
4691
- // Check for valid cache entry if not bypassing cache
4692
- if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4713
+ // Check for valid cache entry when the caller explicitly opted in.
4714
+ if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4693
4715
  return getCache[cacheKey].data;
4694
4716
  }
4695
4717
  // If we're bypassing cache, we should still coalesce identical requests
@@ -4731,8 +4753,8 @@ async function get(path, opts = {}) {
4731
4753
  // - collection path → `{ data, nextCursor }` preserved,
4732
4754
  // with the bare `id` (leaf doc key) attached to every returned row (Bug 1).
4733
4755
  const responseData = normalizeReadResult(response.data, pathIsDocument);
4734
- // Cache the response (unless bypassing cache)
4735
- if (!opts.bypassCache) {
4756
+ // Cache the response only when explicitly requested and principal-bound.
4757
+ if (cacheEnabled) {
4736
4758
  getCache[cacheKey] = {
4737
4759
  data: responseData,
4738
4760
  expiresAt: now + GET_CACHE_TTL
@@ -4770,6 +4792,213 @@ function cleanupExpiredCache() {
4770
4792
  });
4771
4793
  lastCacheCleanup = now;
4772
4794
  }
4795
+ const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
4796
+ const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
4797
+ const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
4798
+ const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
4799
+ const ALLOWED_SERVER_TX_PROGRAMS = new Set([
4800
+ BOUNDED_PROGRAM_MAINNET,
4801
+ BOUNDED_PROGRAM_DEVNET,
4802
+ COMPUTE_BUDGET_PROGRAM,
4803
+ SYSTEM_PROGRAM_ID,
4804
+ ]);
4805
+ const SET_DOCUMENTS_DISCRIMINATOR = '79,46,72,73,24,79,66,245';
4806
+ const SET_DOCUMENTS_V2_DISCRIMINATOR = '22,236,242,185,145,61,26,39';
4807
+ const ALLOWED_BOUNDED_SET_DISCRIMINATORS = new Set([
4808
+ SET_DOCUMENTS_DISCRIMINATOR,
4809
+ SET_DOCUMENTS_V2_DISCRIMINATOR,
4810
+ ]);
4811
+ class BorshCursor {
4812
+ constructor(data, offset, label) {
4813
+ this.data = data;
4814
+ this.offset = offset;
4815
+ this.label = label;
4816
+ }
4817
+ requireBytes(length, field) {
4818
+ if (this.offset + length > this.data.length) {
4819
+ throw new Error(`${this.label} has malformed Bounded instruction data while reading ${field}`);
4820
+ }
4821
+ }
4822
+ readU8(field) {
4823
+ this.requireBytes(1, field);
4824
+ return this.data[this.offset++];
4825
+ }
4826
+ readU32(field) {
4827
+ this.requireBytes(4, field);
4828
+ const value = this.data[this.offset] |
4829
+ (this.data[this.offset + 1] << 8) |
4830
+ (this.data[this.offset + 2] << 16) |
4831
+ (this.data[this.offset + 3] << 24);
4832
+ this.offset += 4;
4833
+ return value >>> 0;
4834
+ }
4835
+ skip(length, field) {
4836
+ this.requireBytes(length, field);
4837
+ this.offset += length;
4838
+ }
4839
+ readString(field) {
4840
+ const length = this.readU32(`${field} length`);
4841
+ this.requireBytes(length, field);
4842
+ const raw = this.data.slice(this.offset, this.offset + length);
4843
+ this.offset += length;
4844
+ return bufferExports.Buffer.from(raw).toString('utf8');
4845
+ }
4846
+ skipBytes(field) {
4847
+ const length = this.readU32(`${field} length`);
4848
+ this.skip(length, field);
4849
+ }
4850
+ isAtEnd() {
4851
+ return this.offset === this.data.length;
4852
+ }
4853
+ }
4854
+ function discriminatorKey(data) {
4855
+ return Array.from(data.slice(0, 8)).join(',');
4856
+ }
4857
+ function skipBoundedFieldValue(cursor) {
4858
+ const option = cursor.readU8('operation value option');
4859
+ if (option === 0)
4860
+ return;
4861
+ if (option !== 1) {
4862
+ throw new Error('Server transaction has malformed Bounded field value option');
4863
+ }
4864
+ const variant = cursor.readU8('operation value variant');
4865
+ switch (variant) {
4866
+ case 0: // u64Val
4867
+ case 1: // i64Val
4868
+ cursor.skip(8, 'operation numeric value');
4869
+ return;
4870
+ case 2: // boolVal
4871
+ cursor.skip(1, 'operation bool value');
4872
+ return;
4873
+ case 3: // stringVal
4874
+ cursor.readString('operation string value');
4875
+ return;
4876
+ case 4: // addressVal
4877
+ cursor.skip(32, 'operation address value');
4878
+ return;
4879
+ default:
4880
+ throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
4881
+ }
4882
+ }
4883
+ function skipBoundedFieldOperation(cursor) {
4884
+ cursor.readString('operation key');
4885
+ skipBoundedFieldValue(cursor);
4886
+ cursor.skip(1, 'operation kind');
4887
+ }
4888
+ function skipBoundedTxData(cursor, isV2) {
4889
+ cursor.readString('txData plugin function key');
4890
+ cursor.skipBytes('txData bytes');
4891
+ if (isV2) {
4892
+ cursor.skipBytes('txData raIndices');
4893
+ return;
4894
+ }
4895
+ const raIndexCount = cursor.readU32('txData raIndices length');
4896
+ cursor.skip(raIndexCount * 8, 'txData raIndices');
4897
+ }
4898
+ function normalizeOnchainPath(path) {
4899
+ let normalized = path.startsWith('/') ? path.slice(1) : path;
4900
+ if (normalized.endsWith('*') && normalized.length > 1) {
4901
+ normalized = normalized.slice(0, -1);
4902
+ }
4903
+ if (normalized.endsWith('/')) {
4904
+ normalized = normalized.slice(0, -1);
4905
+ }
4906
+ return normalized;
4907
+ }
4908
+ function parseBoundedSetDocumentsInstruction(data, label) {
4909
+ if (data.length < 8) {
4910
+ throw new Error(`${label} has malformed Bounded instruction data`);
4911
+ }
4912
+ const discriminator = discriminatorKey(data);
4913
+ if (!ALLOWED_BOUNDED_SET_DISCRIMINATORS.has(discriminator)) {
4914
+ throw new Error(`${label} contains unsupported Bounded instruction`);
4915
+ }
4916
+ const isV2 = discriminator === SET_DOCUMENTS_V2_DISCRIMINATOR;
4917
+ const cursor = new BorshCursor(data, 8, label);
4918
+ const appId = cursor.readString('appId');
4919
+ const documentPaths = [];
4920
+ const documentCount = cursor.readU32('documents length');
4921
+ for (let i = 0; i < documentCount; i++) {
4922
+ documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
4923
+ const operationCount = cursor.readU32('operations length');
4924
+ for (let j = 0; j < operationCount; j++) {
4925
+ skipBoundedFieldOperation(cursor);
4926
+ }
4927
+ }
4928
+ const deletePaths = [];
4929
+ const deleteCount = cursor.readU32('delete paths length');
4930
+ for (let i = 0; i < deleteCount; i++) {
4931
+ deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
4932
+ }
4933
+ const txDataCount = cursor.readU32('txData length');
4934
+ for (let i = 0; i < txDataCount; i++) {
4935
+ skipBoundedTxData(cursor, isV2);
4936
+ }
4937
+ const simulate = cursor.readU8('simulate');
4938
+ if (simulate !== 0 && simulate !== 1) {
4939
+ throw new Error(`${label} has malformed Bounded simulate flag`);
4940
+ }
4941
+ if (!cursor.isAtEnd()) {
4942
+ throw new Error(`${label} has trailing Bounded instruction data`);
4943
+ }
4944
+ return { appId, documentPaths, deletePaths };
4945
+ }
4946
+ function assertSamePathSet(label, expectedPaths, actualPaths) {
4947
+ const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
4948
+ const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
4949
+ const missing = [...expected].filter(path => !actual.has(path));
4950
+ const extra = [...actual].filter(path => !expected.has(path));
4951
+ if (missing.length > 0 || extra.length > 0) {
4952
+ const details = [
4953
+ missing.length ? `missing paths: ${missing.join(', ')}` : '',
4954
+ extra.length ? `unexpected paths: ${extra.join(', ')}` : '',
4955
+ ].filter(Boolean).join('; ');
4956
+ throw new Error(`${label} Bounded instruction paths do not match requested write paths (${details})`);
4957
+ }
4958
+ }
4959
+ function validateServerSuppliedVersionedTransaction(transaction, options) {
4960
+ var _a;
4961
+ const { label, expectedAppId, expectedWritePaths } = options;
4962
+ const accountKeys = transaction.message.staticAccountKeys;
4963
+ let boundedInstructionCount = 0;
4964
+ const actualWritePaths = [];
4965
+ for (const ix of transaction.message.compiledInstructions) {
4966
+ if (ix.programIdIndex >= accountKeys.length) {
4967
+ throw new Error(`${label} has program ID in lookup table (not allowed)`);
4968
+ }
4969
+ const programId = accountKeys[ix.programIdIndex].toBase58();
4970
+ if (!ALLOWED_SERVER_TX_PROGRAMS.has(programId)) {
4971
+ throw new Error(`${label} contains unauthorized program: ${programId}`);
4972
+ }
4973
+ const data = ix.data instanceof Uint8Array ? ix.data : bufferExports.Buffer.from((_a = ix.data) !== null && _a !== void 0 ? _a : []);
4974
+ if (programId === SYSTEM_PROGRAM_ID) {
4975
+ throw new Error(`${label} contains unauthorized System Program instruction`);
4976
+ }
4977
+ if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
4978
+ boundedInstructionCount += 1;
4979
+ const parsed = parseBoundedSetDocumentsInstruction(data, label);
4980
+ if (parsed.appId !== expectedAppId) {
4981
+ throw new Error(`${label} Bounded instruction appId does not match configured appId`);
4982
+ }
4983
+ actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
4984
+ }
4985
+ }
4986
+ if (boundedInstructionCount !== 1) {
4987
+ throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
4988
+ }
4989
+ assertSamePathSet(label, expectedWritePaths, actualWritePaths);
4990
+ }
4991
+ function deserializeAndValidateServerTransaction(serializedTransaction, options) {
4992
+ const txBytes = bufferExports.Buffer.from(serializedTransaction, 'base64');
4993
+ const transaction = web3_js.VersionedTransaction.deserialize(txBytes);
4994
+ validateServerSuppliedVersionedTransaction(transaction, options);
4995
+ return transaction;
4996
+ }
4997
+ function assertAllowedServerPreInstruction(ix, label) {
4998
+ if (ix.programId.equals(web3_js.SystemProgram.programId)) {
4999
+ throw new Error(`${label} contains unauthorized System Program preInstruction`);
5000
+ }
5001
+ }
4773
5002
  function classifyGetManyBatchError(error) {
4774
5003
  var _a, _b, _c;
4775
5004
  const err = error;
@@ -4814,13 +5043,14 @@ async function getMany(paths, opts = {}) {
4814
5043
  // H1: principal-scope getMany cache keys so one user's batch reads are never
4815
5044
  // served to another. Same `<appId:principal>|<path>:` shape used by get().
4816
5045
  const principalKey = await getReadPrincipalKey(opts._overrides);
5046
+ const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4817
5047
  const results = new Array(paths.length);
4818
5048
  const uncachedIndices = [];
4819
5049
  const uncachedPaths = [];
4820
5050
  for (let i = 0; i < normalizedPaths.length; i++) {
4821
5051
  const normalizedPath = normalizedPaths[i];
4822
- const cacheKey = `${principalKey}|${normalizedPath}:`;
4823
- if (!opts.bypassCache && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
5052
+ const cacheKey = `${principalKey.key}|${normalizedPath}:`;
5053
+ if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4824
5054
  results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
4825
5055
  }
4826
5056
  else {
@@ -4856,8 +5086,8 @@ async function getMany(paths, opts = {}) {
4856
5086
  ? serverResult
4857
5087
  : Object.assign(Object.assign({}, serverResult), { data: withBareId(serverResult.data) });
4858
5088
  results[originalIndex] = normalizedResult;
4859
- if (!normalizedResult.error && !opts.bypassCache) {
4860
- const cacheKey = `${principalKey}|${normalizedPath}:`;
5089
+ if (!normalizedResult.error && cacheEnabled) {
5090
+ const cacheKey = `${principalKey.key}|${normalizedPath}:`;
4861
5091
  getCache[cacheKey] = {
4862
5092
  data: normalizedResult.data,
4863
5093
  expiresAt: now + GET_CACHE_TTL
@@ -4872,7 +5102,7 @@ async function getMany(paths, opts = {}) {
4872
5102
  };
4873
5103
  }
4874
5104
  }
4875
- if (now - lastCacheCleanup > 5000) {
5105
+ if (cacheEnabled && now - lastCacheCleanup > 5000) {
4876
5106
  cleanupExpiredCache();
4877
5107
  lastCacheCleanup = now;
4878
5108
  }
@@ -5023,11 +5253,12 @@ async function setMany(many, options) {
5023
5253
  }
5024
5254
  const curTx = transactions[0];
5025
5255
  let transactionResult;
5256
+ const expectedWritePaths = documents.map(d => d.destinationPath);
5026
5257
  if (curTx.serializedTransaction) {
5027
- transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
5258
+ transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5028
5259
  }
5029
5260
  else {
5030
- transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
5261
+ transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
5031
5262
  }
5032
5263
  // Sync items after all transactions are confirmed
5033
5264
  // Wait for 1.5 seconds to ensure all transactions are confirmed
@@ -5061,7 +5292,7 @@ async function setMany(many, options) {
5061
5292
  catch (error) {
5062
5293
  throw error;
5063
5294
  }
5064
- async function handleSolanaTransaction(tx, authProvider, options) {
5295
+ async function handleSolanaTransaction(tx, authProvider, options, expectedWritePaths) {
5065
5296
  var _a, _b, _c, _d, _e;
5066
5297
  // NOTE (backwards-compat revert): a program-allowlist on server-supplied
5067
5298
  // `preInstructions` was tried here for the audit-8 SOL-drain concern, but it
@@ -5109,13 +5340,20 @@ async function setMany(many, options) {
5109
5340
  }))) !== null && _b !== void 0 ? _b : [],
5110
5341
  };
5111
5342
  const config = await getConfig();
5343
+ if (tx.signedTransaction) {
5344
+ deserializeAndValidateServerTransaction(tx.signedTransaction, {
5345
+ label: 'Server signedTransaction',
5346
+ expectedAppId: config.appId,
5347
+ expectedWritePaths,
5348
+ });
5349
+ }
5112
5350
  const solTransaction = {
5113
5351
  appId: config.appId,
5114
5352
  txArgs: [solTransactionData],
5115
5353
  lutKey: (_c = tx.lutAddress) !== null && _c !== void 0 ? _c : null,
5116
5354
  additionalLutAddresses: tx.additionalLutAddresses,
5117
5355
  network: tx.network,
5118
- preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix) => {
5356
+ preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix, index) => {
5119
5357
  var _a;
5120
5358
  const keys = (_a = ix.keys) === null || _a === void 0 ? void 0 : _a.map((k) => ({
5121
5359
  pubkey: new web3_js.PublicKey(k.pubkey),
@@ -5126,11 +5364,13 @@ async function setMany(many, options) {
5126
5364
  ? web3_js.SystemProgram.programId // prettier to use the constant
5127
5365
  : new web3_js.PublicKey(ix.programId);
5128
5366
  const data = bufferExports.Buffer.from(ix.data);
5129
- return new web3_js.TransactionInstruction({
5367
+ const instruction = new web3_js.TransactionInstruction({
5130
5368
  keys,
5131
5369
  programId,
5132
5370
  data,
5133
5371
  });
5372
+ assertAllowedServerPreInstruction(instruction, `Server preInstruction[${index}]`);
5373
+ return instruction;
5134
5374
  })) !== null && _e !== void 0 ? _e : [],
5135
5375
  // Server co-signed transaction (when CPI tx_data is present)
5136
5376
  signedTransaction: tx.signedTransaction,
@@ -5138,45 +5378,22 @@ async function setMany(many, options) {
5138
5378
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5139
5379
  return transactionResult;
5140
5380
  }
5141
- async function handlePreBuiltTransaction(tx, authProvider, options) {
5142
- var _a, _b;
5381
+ async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5382
+ var _a, _b, _c;
5143
5383
  const config = await getConfig();
5144
- const rpcUrl = config.rpcUrl || (tx.network === 'solana_devnet'
5145
- ? 'https://api.devnet.solana.com'
5146
- : 'https://api.mainnet-beta.solana.com');
5147
- const connection = new web3_js.Connection(rpcUrl, 'confirmed');
5148
- const txBytes = bufferExports.Buffer.from(tx.serializedTransaction, 'base64');
5149
- const transaction = web3_js.VersionedTransaction.deserialize(txBytes);
5150
- // Validate the transaction before signing: ensure only allowed programs
5151
- // and no unauthorized System program instructions (e.g., SOL transfers)
5152
- const BOUNDED_PROGRAM = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
5153
- const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
5154
- const SYSTEM_PROGRAM = '11111111111111111111111111111111';
5155
- const ALLOWED_PROGRAMS = new Set([BOUNDED_PROGRAM, COMPUTE_BUDGET, SYSTEM_PROGRAM]);
5156
- // System program instruction discriminators (first 4 bytes, little-endian u32)
5157
- const SYSTEM_TRANSFER = 2; // Transfer instruction index
5158
- const SYSTEM_TRANSFER_WITH_SEED = 11;
5159
- const accountKeys = transaction.message.staticAccountKeys;
5160
- for (const ix of transaction.message.compiledInstructions) {
5161
- if (ix.programIdIndex >= accountKeys.length) {
5162
- throw new Error('Pre-built transaction has program ID in lookup table (not allowed)');
5163
- }
5164
- const programId = accountKeys[ix.programIdIndex].toBase58();
5165
- if (!ALLOWED_PROGRAMS.has(programId)) {
5166
- throw new Error(`Pre-built transaction contains unauthorized program: ${programId}`);
5167
- }
5168
- // Block System program transfer instructions — a compromised DO could
5169
- // embed a SOL drain. Only allow createAccount/allocate (needed for PDA init).
5170
- if (programId === SYSTEM_PROGRAM && ix.data.length >= 4) {
5171
- const ixIndex = ix.data[0] | (ix.data[1] << 8) | (ix.data[2] << 16) | (ix.data[3] << 24);
5172
- if (ixIndex === SYSTEM_TRANSFER || ixIndex === SYSTEM_TRANSFER_WITH_SEED) {
5173
- throw new Error('Pre-built transaction contains unauthorized System transfer instruction');
5174
- }
5175
- }
5384
+ const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, {
5385
+ label: 'Pre-built transaction',
5386
+ expectedAppId: config.appId,
5387
+ expectedWritePaths,
5388
+ });
5389
+ const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
5390
+ const rpcUrl = (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
5391
+ if (shouldSubmit && !rpcUrl) {
5392
+ throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}`);
5176
5393
  }
5177
5394
  const signedTx = await authProvider.signTransaction(transaction);
5178
5395
  const rawTx = signedTx.serialize();
5179
- if ((options === null || options === void 0 ? void 0 : options.shouldSubmitTx) === false) {
5396
+ if (!shouldSubmit) {
5180
5397
  return {
5181
5398
  transactionSignature: '',
5182
5399
  signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
@@ -5184,6 +5401,7 @@ async function setMany(many, options) {
5184
5401
  gasUsed: '0',
5185
5402
  };
5186
5403
  }
5404
+ const connection = new web3_js.Connection(rpcUrl, 'confirmed');
5187
5405
  const signature = await connection.sendRawTransaction(rawTx, {
5188
5406
  skipPreflight: false,
5189
5407
  maxRetries: 3,
@@ -5196,7 +5414,7 @@ async function setMany(many, options) {
5196
5414
  return {
5197
5415
  transactionSignature: signature,
5198
5416
  signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
5199
- blockNumber: (_b = (_a = confirmation.context) === null || _a === void 0 ? void 0 : _a.slot) !== null && _b !== void 0 ? _b : 0,
5417
+ blockNumber: (_c = (_b = confirmation.context) === null || _b === void 0 ? void 0 : _b.slot) !== null && _c !== void 0 ? _c : 0,
5200
5418
  gasUsed: '0',
5201
5419
  };
5202
5420
  }
@@ -5413,7 +5631,7 @@ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
5413
5631
  const MAX_RECONNECT_DELAY_MS = 300000;
5414
5632
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
5415
5633
  const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
5416
- const MAX_AUTH_REFRESH_RETRIES = 5;
5634
+ const WS_AUTH_EXPIRED_CODE = 'auth_expired';
5417
5635
  const WS_CONFIG = {
5418
5636
  // Keep retrying indefinitely so long outages recover without page refresh.
5419
5637
  maxRetries: Infinity,
@@ -5430,11 +5648,12 @@ const WS_V2_PATH = '/ws/v2';
5430
5648
  let browserReconnectHooksAttached = false;
5431
5649
  let lastBrowserTriggeredReconnectAt = 0;
5432
5650
  let reconnectInProgress = null;
5651
+ let ambientAuthFailure = null;
5433
5652
  // ============ Helper Functions ============
5434
5653
  function generateSubscriptionId() {
5435
5654
  return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
5436
5655
  }
5437
- function hashForKey$1(value) {
5656
+ function hashForKey(value) {
5438
5657
  let h = 5381;
5439
5658
  for (let i = 0; i < value.length; i++) {
5440
5659
  h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
@@ -5446,17 +5665,17 @@ function hashForKey$1(value) {
5446
5665
  * just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
5447
5666
  * a private subscription snapshot cached for one user from being delivered to a
5448
5667
  * different user who subscribes with the same options (shared process / SSR worker
5449
- * / browser login-switch; this cache has a 5-minute TTL). When omitted (legacy
5450
- * read-only helpers) we fall back to an `anon` scope.
5668
+ * / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
5669
+ * before entries are read or written; no-auth subscriptions never populate an
5670
+ * implicit anonymous cache bucket.
5451
5671
  */
5452
5672
  function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5453
5673
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
5454
5674
  const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
5455
5675
  const limitKey = limit !== undefined ? `:l${limit}` : '';
5456
- const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
5457
- const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
5458
- const identityKey = identity || 'anon';
5459
- return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5676
+ const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
5677
+ const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
5678
+ return `${identity}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
5460
5679
  }
5461
5680
  /**
5462
5681
  * Derive an opaque identity string for the bearer material a subscription sends.
@@ -5464,7 +5683,7 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5464
5683
  * before the server has verified any claims.
5465
5684
  */
5466
5685
  function principalFromIdToken(idToken) {
5467
- return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
5686
+ return idToken ? `t${hashForKey(idToken)}` : null;
5468
5687
  }
5469
5688
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5470
5689
  // Per-subscription wallet override (server WalletClient.subscribe): key by
@@ -5473,16 +5692,21 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5473
5692
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
5474
5693
  try {
5475
5694
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
5476
- return `${effectiveAppId}:o${bearer ? principalFromIdToken(bearer) : 'anon'}`;
5695
+ const principal = principalFromIdToken(bearer);
5696
+ return principal
5697
+ ? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
5698
+ : { key: `${effectiveAppId}:oanon`, cacheable: false };
5477
5699
  }
5478
5700
  catch (_a) {
5479
- // Couldn't resolve the override identity — use a unique key so we never
5480
- // collide with another principal's cached entry.
5481
- return `${effectiveAppId}:o${principalFromIdToken(null)}-${safeBtoa(String(connectionEpoch++))}`;
5701
+ // Couldn't resolve the override identity — do not use response cache.
5702
+ return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
5482
5703
  }
5483
5704
  }
5484
5705
  const idToken = await getIdToken(isServer);
5485
- return `${effectiveAppId}:${principalFromIdToken(idToken)}`;
5706
+ const principal = principalFromIdToken(idToken);
5707
+ return principal
5708
+ ? { key: `${effectiveAppId}:${principal}`, cacheable: true }
5709
+ : { key: `${effectiveAppId}:anon`, cacheable: false };
5486
5710
  }
5487
5711
  /** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
5488
5712
  function bearerFromAuthHeaders(headers) {
@@ -5512,6 +5736,41 @@ function getTokenExpirationTime(token) {
5512
5736
  return null;
5513
5737
  }
5514
5738
  }
5739
+ function makeAuthExpiredError(message, status) {
5740
+ const err = new Error(`${WS_AUTH_EXPIRED_CODE}: ${message}`);
5741
+ err.code = WS_AUTH_EXPIRED_CODE;
5742
+ if (status !== undefined)
5743
+ err.status = status;
5744
+ return err;
5745
+ }
5746
+ function isAuthExpiredError(error) {
5747
+ return !!error && typeof error === 'object' && error.code === WS_AUTH_EXPIRED_CODE;
5748
+ }
5749
+ function normalizeAuthExpiredError(error, fallbackMessage) {
5750
+ var _a, _b;
5751
+ if (isAuthExpiredError(error))
5752
+ return error;
5753
+ 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;
5754
+ return makeAuthExpiredError(fallbackMessage, status);
5755
+ }
5756
+ function rememberAmbientAuthFailure(error) {
5757
+ ambientAuthFailure = error;
5758
+ return error;
5759
+ }
5760
+ function failConnectionAuth(connection, error) {
5761
+ connection.authFailure = error;
5762
+ connection.pendingAuthToken = null;
5763
+ connection.isConnecting = false;
5764
+ connection.isConnected = false;
5765
+ connection.isAuthenticating = false;
5766
+ for (const [, pending] of connection.pendingSubscriptions) {
5767
+ pending.reject(error);
5768
+ }
5769
+ connection.pendingSubscriptions.clear();
5770
+ for (const subscription of connection.subscriptions.values()) {
5771
+ notifyErrorCallbacks(subscription, error);
5772
+ }
5773
+ }
5515
5774
  function scheduleTokenRefresh(connection, isServer) {
5516
5775
  // Clear any existing timer
5517
5776
  if (connection.tokenRefreshTimer) {
@@ -5579,23 +5838,32 @@ async function getFreshAuthToken(isServer) {
5579
5838
  var _a, _b, _c, _d, _e;
5580
5839
  const currentToken = await getIdToken(isServer);
5581
5840
  if (!currentToken) {
5841
+ if (ambientAuthFailure) {
5842
+ throw ambientAuthFailure;
5843
+ }
5582
5844
  return null;
5583
5845
  }
5584
5846
  if (!isTokenExpired(currentToken)) {
5847
+ ambientAuthFailure = null;
5585
5848
  return currentToken;
5586
5849
  }
5850
+ if (ambientAuthFailure) {
5851
+ throw ambientAuthFailure;
5852
+ }
5587
5853
  // Token is expired — attempt refresh
5854
+ const refreshToken = await getRefreshToken(isServer);
5855
+ if (!refreshToken) {
5856
+ console.warn('[WS v2] Token expired but no refresh token available');
5857
+ throw rememberAmbientAuthFailure(makeAuthExpiredError('Authentication expired and no refresh token is available'));
5858
+ }
5588
5859
  try {
5589
- const refreshToken = await getRefreshToken(isServer);
5590
- if (!refreshToken) {
5591
- console.warn('[WS v2] Token expired but no refresh token available');
5592
- return null;
5593
- }
5594
5860
  const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
5595
5861
  if (refreshData && refreshData.idToken && refreshData.accessToken) {
5596
5862
  await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer, refreshData.refreshToken);
5863
+ ambientAuthFailure = null;
5597
5864
  return refreshData.idToken;
5598
5865
  }
5866
+ throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
5599
5867
  }
5600
5868
  catch (error) {
5601
5869
  // Log only the status — the raw axios error carries config.data (the refresh
@@ -5612,12 +5880,22 @@ async function getFreshAuthToken(isServer) {
5612
5880
  console.warn('[WS v2] Failed to clear stale session:', clearError);
5613
5881
  }
5614
5882
  }
5883
+ throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
5884
+ }
5885
+ }
5886
+ async function getConnectionAuthToken(isServer, authTokenProvider) {
5887
+ if (!authTokenProvider) {
5888
+ return getFreshAuthToken(isServer);
5889
+ }
5890
+ try {
5891
+ const token = await authTokenProvider();
5892
+ if (token)
5893
+ return token;
5894
+ throw makeAuthExpiredError('Authenticated websocket token provider returned no token');
5895
+ }
5896
+ catch (error) {
5897
+ throw normalizeAuthExpiredError(error, 'Authenticated websocket token provider failed');
5615
5898
  }
5616
- // Return null instead of the expired token to prevent infinite 401 reconnect storms.
5617
- // The server accepts unauthenticated connections; auth-required subscriptions will
5618
- // receive per-subscription errors via onError callbacks.
5619
- console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
5620
- return null;
5621
5899
  }
5622
5900
  function hasDisconnectedActiveConnections() {
5623
5901
  for (const connection of connections.values()) {
@@ -5697,8 +5975,21 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5697
5975
  const connKey = principalKey ? `${base}#id#${principalKey}` : base;
5698
5976
  let connection = connections.get(connKey);
5699
5977
  if (connection && connection.ws) {
5978
+ if (connection.authFailure) {
5979
+ throw connection.authFailure;
5980
+ }
5981
+ try {
5982
+ await getConnectionAuthToken(isServer, authTokenProvider);
5983
+ }
5984
+ catch (error) {
5985
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5986
+ failConnectionAuth(connection, authError);
5987
+ throw authError;
5988
+ }
5989
+ connection.authFailure = null;
5700
5990
  return connection;
5701
5991
  }
5992
+ let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5702
5993
  // Create new connection
5703
5994
  connection = {
5704
5995
  ws: null,
@@ -5716,6 +6007,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5716
6007
  pendingAuthToken: null,
5717
6008
  tokenRefreshTimer: null,
5718
6009
  consecutiveAuthFailures: 0,
6010
+ authFailure: null,
5719
6011
  };
5720
6012
  connections.set(connKey, connection);
5721
6013
  // URL provider for reconnection with fresh tokens
@@ -5737,27 +6029,23 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5737
6029
  // its token from the wallet's own session (self-refreshing); all others
5738
6030
  // use the ambient env/web session. The token is sent as the first WS
5739
6031
  // frame after open, never as a URL query parameter.
5740
- const authToken = connection.authTokenProvider
5741
- ? await connection.authTokenProvider().catch(() => null)
5742
- : await getFreshAuthToken(isServer);
6032
+ let authToken;
6033
+ try {
6034
+ authToken = initialAuthToken !== undefined
6035
+ ? initialAuthToken
6036
+ : await getConnectionAuthToken(isServer, connection.authTokenProvider);
6037
+ initialAuthToken = undefined;
6038
+ }
6039
+ catch (error) {
6040
+ const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
6041
+ failConnectionAuth(connection, authError);
6042
+ throw authError;
6043
+ }
5743
6044
  connection.pendingAuthToken = authToken || null;
5744
6045
  if (authToken) {
5745
6046
  // Successful token acquisition — reset failure counter
5746
6047
  connection.consecutiveAuthFailures = 0;
5747
- }
5748
- else {
5749
- // Check if user WAS authenticated (had a token that expired).
5750
- // If so, retry with exponential backoff before falling back to unauthenticated.
5751
- const expiredToken = await getIdToken(isServer);
5752
- if (expiredToken && isTokenExpired(expiredToken)) {
5753
- connection.consecutiveAuthFailures++;
5754
- if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
5755
- console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
5756
- throw new Error('Auth token refresh failed, retrying with backoff');
5757
- }
5758
- console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
5759
- }
5760
- // No token at all (never authenticated) or retries exhausted — connect without auth
6048
+ connection.authFailure = null;
5761
6049
  }
5762
6050
  return wsUrl.toString();
5763
6051
  };
@@ -5771,15 +6059,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5771
6059
  connection.isConnecting = false;
5772
6060
  connection.isConnected = true;
5773
6061
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
5774
- // fresh auth token is actually obtained (in urlProvider, line ~389) or on
5775
- // an explicit auth change (reconnectWithNewAuthV2, line ~854). Resetting
5776
- // on every 'open' event created an infinite loop: auth fails 5x → connect
5777
- // without auth → open resets counter → disconnect → auth fails 5x again →
5778
- // repeat forever, hammering /session/refresh and causing 429s.
5779
- //
5780
- // An elevated counter is safe for anonymous/guest sessions: when there's no
5781
- // token at all (getIdToken returns null), the counter is never checked —
5782
- // urlProvider skips straight to unauthenticated connection.
6062
+ // fresh auth token is actually obtained or on an explicit auth change.
5783
6063
  // Schedule periodic token freshness checks
5784
6064
  scheduleTokenRefresh(connection, isServer);
5785
6065
  if (connection.pendingAuthToken) {
@@ -5854,8 +6134,10 @@ function handleServerMessage(connection, message) {
5854
6134
  // If we already received data for this subscription, treat subscribed
5855
6135
  // as an ack only and avoid regressing to an older snapshot.
5856
6136
  if (subscription.lastData === undefined) {
5857
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
5858
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6137
+ if (subscription.cache) {
6138
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6139
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6140
+ }
5859
6141
  subscription.lastData = message.data;
5860
6142
  notifyCallbacks(subscription, message.data);
5861
6143
  }
@@ -5881,8 +6163,10 @@ function handleServerMessage(connection, message) {
5881
6163
  const subscription = connection.subscriptions.get(message.subscriptionId);
5882
6164
  if (subscription) {
5883
6165
  // Update cache
5884
- const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
5885
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6166
+ if (subscription.cache) {
6167
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6168
+ responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
6169
+ }
5886
6170
  // Store last data
5887
6171
  subscription.lastData = message.data;
5888
6172
  // Notify callbacks
@@ -6100,20 +6384,37 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6100
6384
  const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
6101
6385
  ? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
6102
6386
  : undefined;
6103
- const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6387
+ const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6388
+ const identity = identityInfo.key;
6389
+ const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
6104
6390
  const principalKey = authTokenProvider ? identity : undefined;
6105
- const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
6106
- // Deliver cached data immediately if available
6107
- const cachedEntry = responseCache.get(cacheKey);
6391
+ const cacheKey = responseCacheEnabled
6392
+ ? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
6393
+ : null;
6394
+ // Get or create connection for this routing target (room-scoped when a
6395
+ // room route is supplied by the live helper, else the app-level connection).
6396
+ let connection;
6397
+ try {
6398
+ connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
6399
+ }
6400
+ catch (error) {
6401
+ const err = error instanceof Error ? error : new Error(String(error));
6402
+ if (subscriptionOptions.onError) {
6403
+ subscriptionOptions.onError(err);
6404
+ return async () => { };
6405
+ }
6406
+ throw err;
6407
+ }
6408
+ // Deliver cached data immediately if available, but only after connection
6409
+ // auth preflight has succeeded. An expired authenticated session must receive
6410
+ // an auth error, not a stale private snapshot followed by a failed connect.
6411
+ const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
6108
6412
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
6109
6413
  setTimeout(() => {
6110
6414
  var _a;
6111
6415
  (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
6112
6416
  }, 0);
6113
6417
  }
6114
- // Get or create connection for this routing target (room-scoped when a
6115
- // room route is supplied by the live helper, else the app-level connection).
6116
- const connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
6117
6418
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6118
6419
  const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6119
6420
  const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
@@ -6132,10 +6433,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6132
6433
  }
6133
6434
  }
6134
6435
  if (existingSubscription) {
6436
+ if (responseCacheEnabled) {
6437
+ existingSubscription.cache = true;
6438
+ }
6135
6439
  // Add callback to existing subscription
6136
6440
  existingSubscription.callbacks.push(subscriptionOptions);
6137
- // Deliver last known data immediately
6138
- if (existingSubscription.lastData !== undefined && subscriptionOptions.onData) {
6441
+ // Deliver last known data immediately only for explicit, principal-bound
6442
+ // cache opt-in. Otherwise joining an existing subscription can replay a
6443
+ // stale private snapshot without a fresh server auth result.
6444
+ if (responseCacheEnabled &&
6445
+ existingSubscription.cache &&
6446
+ existingSubscription.lastData !== undefined &&
6447
+ subscriptionOptions.onData) {
6139
6448
  setTimeout(() => {
6140
6449
  var _a;
6141
6450
  (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
@@ -6159,6 +6468,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6159
6468
  includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
6160
6469
  callbacks: [subscriptionOptions],
6161
6470
  lastData: undefined,
6471
+ cache: responseCacheEnabled,
6162
6472
  identity,
6163
6473
  };
6164
6474
  connection.subscriptions.set(subscriptionId, subscription);
@@ -6290,19 +6600,21 @@ function clearCacheV2(path) {
6290
6600
  */
6291
6601
  function getCachedDataV2(path, prompt) {
6292
6602
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6293
- // H1: response caches are identity-scoped (`<identity>|<path>:...`). Resolve
6294
- // the caller's identity from an active subscription for this path (sync) so
6295
- // an authenticated user gets THEIR cached snapshot rather than the 'anon'
6296
- // bucket (which would wrongly return null). No matching sub → null (safe).
6603
+ // H1: response caches are identity-scoped (`<identity>|<path>:...`) and
6604
+ // opt-in. Resolve the caller's identity from an active cache-enabled
6605
+ // subscription for this path. No matching sub null (safe).
6297
6606
  let identity;
6298
6607
  outer: for (const connection of connections.values()) {
6299
6608
  for (const sub of connection.subscriptions.values()) {
6300
- if (sub.path === normalizedPath && sub.prompt === prompt) {
6609
+ if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
6301
6610
  identity = sub.identity;
6302
6611
  break outer;
6303
6612
  }
6304
6613
  }
6305
6614
  }
6615
+ if (!identity) {
6616
+ return null;
6617
+ }
6306
6618
  const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
6307
6619
  const cachedEntry = responseCache.get(cacheKey);
6308
6620
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
@@ -6334,6 +6646,7 @@ async function reconnectWithNewAuthV2() {
6334
6646
  }
6335
6647
  }
6336
6648
  async function doReconnectWithNewAuth() {
6649
+ ambientAuthFailure = null;
6337
6650
  // SECURITY (H1): the logged-in identity is changing (login / logout / switch).
6338
6651
  // Wipe ALL principal-scoped read caches so the new identity can never observe
6339
6652
  // data cached for the previous one. Clear both the WS response cache and the
@@ -6355,7 +6668,7 @@ async function doReconnectWithNewAuth() {
6355
6668
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
6356
6669
  }
6357
6670
  try {
6358
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
6671
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-CDLQdh7S.js'); });
6359
6672
  await reconnectRealtimeStoreWithNewAuth();
6360
6673
  }
6361
6674
  catch (error) {
@@ -6375,6 +6688,7 @@ async function doReconnectWithNewAuth() {
6375
6688
  connection.pendingUnsubscriptions.clear();
6376
6689
  // Reset auth failure counter — this is a proactive reconnect (login, token refresh)
6377
6690
  connection.consecutiveAuthFailures = 0;
6691
+ connection.authFailure = null;
6378
6692
  // Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
6379
6693
  // We use reconnect() which will close and re-open with fresh URL (including new token)
6380
6694
  try {
@@ -6385,7 +6699,7 @@ async function doReconnectWithNewAuth() {
6385
6699
  }
6386
6700
  }
6387
6701
  }
6388
- // ============ CRUD over WebSocket ============
6702
+ // ============ WebSocket request helpers ============
6389
6703
  const WS_REQUEST_TIMEOUT_MS = 30000;
6390
6704
  function generateRequestId() {
6391
6705
  return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
@@ -6401,104 +6715,6 @@ function hasActiveConnection() {
6401
6715
  }
6402
6716
  return false;
6403
6717
  }
6404
- async function waitForConnectionAuthenticated(connection) {
6405
- if (!connection.isAuthenticating || !connection.ws)
6406
- return;
6407
- const ws = connection.ws;
6408
- await new Promise((resolve, reject) => {
6409
- let timeout;
6410
- let cleanup = () => { };
6411
- const onMessage = (event) => {
6412
- try {
6413
- const message = JSON.parse(event.data);
6414
- if ((message === null || message === void 0 ? void 0 : message.type) === 'authenticated') {
6415
- cleanup();
6416
- resolve();
6417
- }
6418
- }
6419
- catch (_a) {
6420
- // Other frames are handled by the main listener.
6421
- }
6422
- };
6423
- const onClose = () => {
6424
- cleanup();
6425
- reject(new Error('WebSocket disconnected during authentication'));
6426
- };
6427
- const onError = () => {
6428
- cleanup();
6429
- reject(new Error('WebSocket authentication failed'));
6430
- };
6431
- cleanup = () => {
6432
- clearTimeout(timeout);
6433
- ws.removeEventListener('message', onMessage);
6434
- ws.removeEventListener('close', onClose);
6435
- ws.removeEventListener('error', onError);
6436
- };
6437
- timeout = setTimeout(() => {
6438
- cleanup();
6439
- reject(new Error('WebSocket authentication timeout'));
6440
- }, 10000);
6441
- if (!connection.isAuthenticating) {
6442
- cleanup();
6443
- resolve();
6444
- return;
6445
- }
6446
- ws.addEventListener('message', onMessage);
6447
- ws.addEventListener('close', onClose);
6448
- ws.addEventListener('error', onError);
6449
- });
6450
- }
6451
- async function sendRequest(msgBuilder) {
6452
- const config = await getConfig();
6453
- const appId = config.appId;
6454
- const connection = await getOrCreateConnection(appId, config.isServer);
6455
- // Wait for the connection to be open (getOrCreateConnection may return
6456
- // while still connecting).
6457
- if (!connection.isConnected && connection.ws) {
6458
- await new Promise((resolve, reject) => {
6459
- const timeout = setTimeout(() => {
6460
- var _a;
6461
- (_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
6462
- reject(new Error('WebSocket connection timeout'));
6463
- }, 10000);
6464
- const onOpen = () => { clearTimeout(timeout); resolve(); };
6465
- if (connection.isConnected) {
6466
- clearTimeout(timeout);
6467
- resolve();
6468
- return;
6469
- }
6470
- connection.ws.addEventListener('open', onOpen);
6471
- });
6472
- }
6473
- if (!connection.ws || !connection.isConnected) {
6474
- throw new Error('WebSocket connection not available');
6475
- }
6476
- await waitForConnectionAuthenticated(connection);
6477
- const requestId = generateRequestId();
6478
- const message = msgBuilder(requestId);
6479
- return new Promise((resolve, reject) => {
6480
- const timer = setTimeout(() => {
6481
- connection.pendingRequests.delete(requestId);
6482
- reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
6483
- }, WS_REQUEST_TIMEOUT_MS);
6484
- connection.pendingRequests.set(requestId, { resolve, reject, timer });
6485
- try {
6486
- connection.ws.send(JSON.stringify(message));
6487
- }
6488
- catch (error) {
6489
- connection.pendingRequests.delete(requestId);
6490
- clearTimeout(timer);
6491
- reject(error);
6492
- }
6493
- });
6494
- }
6495
- async function wsGet(path) {
6496
- return sendRequest((requestId) => ({
6497
- type: 'get',
6498
- requestId,
6499
- path,
6500
- }));
6501
- }
6502
6718
  /**
6503
6719
  * Send a live-room intent over the EXISTING per-room socket (fire-and-forget).
6504
6720
  * Returns true if it was sent over an open connection, false if there is no
@@ -6557,31 +6773,6 @@ function wsIntentReliable(appId, roomRoutePath, intent) {
6557
6773
  }
6558
6774
  });
6559
6775
  }
6560
- async function wsSet(documents) {
6561
- return sendRequest((requestId) => ({
6562
- type: 'set',
6563
- requestId,
6564
- documents,
6565
- }));
6566
- }
6567
- async function wsQuery(path, opts) {
6568
- return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
6569
- 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 } : {}))));
6570
- }
6571
- async function wsDelete(path) {
6572
- return sendRequest((requestId) => ({
6573
- type: 'delete',
6574
- requestId,
6575
- path,
6576
- }));
6577
- }
6578
- async function wsGetMany(paths) {
6579
- return sendRequest((requestId) => ({
6580
- type: 'getMany',
6581
- requestId,
6582
- paths,
6583
- }));
6584
- }
6585
6776
 
6586
6777
  /**
6587
6778
  * WebSocket Subscription Module
@@ -6762,1008 +6953,6 @@ function toMillis(seconds) {
6762
6953
  return seconds * 1000;
6763
6954
  }
6764
6955
 
6765
- // ---------------------------------------------------------------------------
6766
- // realtime-store.ts — Client-side state manager for realtime apps.
6767
- //
6768
- // Manages: WS connection, in-memory state, IDB persistence, optimistic
6769
- // writes, delta accumulation, loading states, ephemeral/durable tiers.
6770
- // ---------------------------------------------------------------------------
6771
- // ---------------------------------------------------------------------------
6772
- // IDB helpers (lazy-loaded, non-blocking)
6773
- // ---------------------------------------------------------------------------
6774
- const IDB_NAME = 'bounded-realtime';
6775
- const IDB_STORE = 'subscriptions';
6776
- const IDB_VERSION = 1;
6777
- let idbPromise = null;
6778
- function getIDB() {
6779
- if (idbPromise)
6780
- return idbPromise;
6781
- if (typeof indexedDB === 'undefined') {
6782
- return Promise.reject(new Error('IndexedDB not available'));
6783
- }
6784
- idbPromise = new Promise((resolve, reject) => {
6785
- const req = indexedDB.open(IDB_NAME, IDB_VERSION);
6786
- req.onupgradeneeded = () => {
6787
- const db = req.result;
6788
- if (!db.objectStoreNames.contains(IDB_STORE)) {
6789
- db.createObjectStore(IDB_STORE);
6790
- }
6791
- };
6792
- req.onsuccess = () => resolve(req.result);
6793
- req.onerror = () => reject(req.error);
6794
- });
6795
- return idbPromise;
6796
- }
6797
- async function idbGet(key) {
6798
- try {
6799
- const db = await getIDB();
6800
- return new Promise((resolve) => {
6801
- const tx = db.transaction(IDB_STORE, 'readonly');
6802
- const store = tx.objectStore(IDB_STORE);
6803
- const req = store.get(key);
6804
- req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
6805
- req.onerror = () => resolve(null);
6806
- });
6807
- }
6808
- catch (_a) {
6809
- return null;
6810
- }
6811
- }
6812
- async function idbSet(key, value) {
6813
- try {
6814
- const db = await getIDB();
6815
- return new Promise((resolve) => {
6816
- const tx = db.transaction(IDB_STORE, 'readwrite');
6817
- const store = tx.objectStore(IDB_STORE);
6818
- store.put(value, key);
6819
- tx.oncomplete = () => resolve();
6820
- tx.onerror = () => resolve();
6821
- });
6822
- }
6823
- catch (_a) {
6824
- // Best-effort persistence
6825
- }
6826
- }
6827
- // ---------------------------------------------------------------------------
6828
- // RealtimeStore
6829
- // ---------------------------------------------------------------------------
6830
- let nextRequestId = 1;
6831
- function hashForKey(value) {
6832
- let h = 5381;
6833
- for (let i = 0; i < value.length; i++) {
6834
- h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
6835
- }
6836
- return h.toString(36);
6837
- }
6838
- function principalFromToken(token) {
6839
- return token ? `t${hashForKey(token)}` : 'anon';
6840
- }
6841
- class RealtimeStore {
6842
- constructor() {
6843
- this.ws = null;
6844
- this.wsUrl = '';
6845
- this.appId = '';
6846
- this.subscriptions = new Map();
6847
- this.pendingRequests = new Map();
6848
- this.connectPromise = null;
6849
- this.reconnectTimer = null;
6850
- this.reconnectDelay = 1000;
6851
- this.maxReconnectDelay = 30000;
6852
- this.idbFlushTimer = null;
6853
- this.idbDirtyKeys = new Set();
6854
- this.closed = false;
6855
- this.authToken = null;
6856
- this.authPrincipalKey = 'anon';
6857
- this.authenticating = false;
6858
- this.suppressNextReconnect = false;
6859
- this.isServer = false;
6860
- this.tokenRefreshTimer = null;
6861
- // -----------------------------------------------------------------------
6862
- // WebSocket connection
6863
- // -----------------------------------------------------------------------
6864
- this.initPromise = null;
6865
- }
6866
- // -----------------------------------------------------------------------
6867
- // Initialization
6868
- // -----------------------------------------------------------------------
6869
- async init() {
6870
- const config = await getConfig();
6871
- this.appId = config.appId;
6872
- this.wsUrl = config.wsApiUrl;
6873
- this.isServer = config.isServer;
6874
- await this.refreshToken();
6875
- this.startTokenRefresh();
6876
- }
6877
- async refreshToken() {
6878
- let token = null;
6879
- try {
6880
- const { getIdToken } = await Promise.resolve().then(function () { return utils; });
6881
- token = await getIdToken(this.isServer);
6882
- }
6883
- catch ( /* no auth available */_a) { /* no auth available */ }
6884
- this.authToken = token !== null && token !== void 0 ? token : null;
6885
- this.authPrincipalKey = principalFromToken(this.authToken);
6886
- }
6887
- startTokenRefresh() {
6888
- if (this.tokenRefreshTimer)
6889
- return;
6890
- this.tokenRefreshTimer = setInterval(async () => {
6891
- const prevPrincipal = this.authPrincipalKey;
6892
- await this.refreshToken();
6893
- if (this.authPrincipalKey !== prevPrincipal) {
6894
- await this.applyAuthPrincipalChange();
6895
- if (this.subscriptions.size > 0) {
6896
- await this.ensureConnected().catch(() => {
6897
- this.setAllSubscriptionStatus('error');
6898
- });
6899
- }
6900
- }
6901
- }, 5 * 60 * 1000); // Check every 5 minutes
6902
- }
6903
- async ensureInitialized() {
6904
- if (this.appId)
6905
- return;
6906
- if (!this.initPromise)
6907
- this.initPromise = this.init();
6908
- await this.initPromise;
6909
- }
6910
- async ensureCurrentAuth() {
6911
- await this.ensureInitialized();
6912
- const prevPrincipal = this.authPrincipalKey;
6913
- await this.refreshToken();
6914
- if (this.authPrincipalKey !== prevPrincipal) {
6915
- await this.applyAuthPrincipalChange();
6916
- }
6917
- }
6918
- rekeySubscriptionsForPrincipal() {
6919
- const subs = Array.from(this.subscriptions.values());
6920
- this.subscriptions.clear();
6921
- for (const sub of subs) {
6922
- this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
6923
- }
6924
- }
6925
- async applyAuthPrincipalChange() {
6926
- if (this.idbFlushTimer) {
6927
- clearTimeout(this.idbFlushTimer);
6928
- this.idbFlushTimer = null;
6929
- }
6930
- this.idbDirtyKeys.clear();
6931
- this.rekeySubscriptionsForPrincipal();
6932
- for (const sub of this.subscriptions.values()) {
6933
- sub.docs.clear();
6934
- sub.ref.current = sub.docs;
6935
- sub.error = null;
6936
- sub.isStale = false;
6937
- let loaded = false;
6938
- if (sub.tier !== 'ephemeral') {
6939
- const cached = await idbGet(this.idbKey(sub.path));
6940
- if (cached && Array.isArray(cached)) {
6941
- for (const doc of cached) {
6942
- if (doc && doc._id)
6943
- sub.docs.set(doc._id, doc);
6944
- }
6945
- sub.ref.current = sub.docs;
6946
- loaded = sub.docs.size > 0;
6947
- }
6948
- }
6949
- sub.status = loaded ? 'cached' : 'loading';
6950
- sub.isStale = loaded;
6951
- if (loaded)
6952
- this.notifySubscription(sub);
6953
- else
6954
- this.notifyState(sub);
6955
- }
6956
- if (this.ws) {
6957
- const ws = this.ws;
6958
- this.ws = null;
6959
- this.connectPromise = null;
6960
- this.suppressNextReconnect = true;
6961
- try {
6962
- ws.close(1000, 'Auth changed');
6963
- }
6964
- catch ( /* ignore */_a) { /* ignore */ }
6965
- }
6966
- }
6967
- async ensureConnected() {
6968
- var _a;
6969
- await this.ensureCurrentAuth();
6970
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6971
- return;
6972
- if (this.connectPromise)
6973
- return this.connectPromise;
6974
- this.connectPromise = this.connect();
6975
- return this.connectPromise;
6976
- }
6977
- connect() {
6978
- return new Promise((resolve, reject) => {
6979
- if (this.closed) {
6980
- reject(new Error('Store closed'));
6981
- return;
6982
- }
6983
- const params = new URLSearchParams();
6984
- params.set('apiKey', this.appId);
6985
- const url = `${this.wsUrl}?${params.toString()}`;
6986
- const ws = new WebSocket(url);
6987
- this.ws = ws;
6988
- let authTimer = null;
6989
- const finishConnected = () => {
6990
- if (authTimer) {
6991
- clearTimeout(authTimer);
6992
- authTimer = null;
6993
- }
6994
- this.authenticating = false;
6995
- ws.removeEventListener('error', onError);
6996
- this.reconnectDelay = 1000;
6997
- this.connectPromise = null;
6998
- this.resubscribeAll();
6999
- resolve();
7000
- };
7001
- const onOpen = () => {
7002
- if (!this.authToken) {
7003
- finishConnected();
7004
- return;
7005
- }
7006
- this.authenticating = true;
7007
- authTimer = setTimeout(() => {
7008
- this.authenticating = false;
7009
- this.connectPromise = null;
7010
- try {
7011
- ws.close(1008, 'Authentication timeout');
7012
- }
7013
- catch ( /* ignore */_a) { /* ignore */ }
7014
- reject(new Error('WebSocket authentication timeout'));
7015
- }, 10000);
7016
- try {
7017
- ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));
7018
- }
7019
- catch (e) {
7020
- if (authTimer)
7021
- clearTimeout(authTimer);
7022
- this.authenticating = false;
7023
- this.connectPromise = null;
7024
- reject(e);
7025
- }
7026
- };
7027
- const onError = (e) => {
7028
- if (authTimer)
7029
- clearTimeout(authTimer);
7030
- this.authenticating = false;
7031
- ws.removeEventListener('open', onOpen);
7032
- this.connectPromise = null;
7033
- reject(new Error('WebSocket connection failed'));
7034
- };
7035
- ws.addEventListener('open', onOpen, { once: true });
7036
- ws.addEventListener('error', onError, { once: true });
7037
- ws.addEventListener('message', (event) => {
7038
- if (this.authenticating) {
7039
- try {
7040
- const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));
7041
- if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {
7042
- finishConnected();
7043
- return;
7044
- }
7045
- }
7046
- catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }
7047
- }
7048
- this.handleMessage(event.data);
7049
- });
7050
- ws.addEventListener('close', () => {
7051
- if (authTimer)
7052
- clearTimeout(authTimer);
7053
- if (this.ws !== ws) {
7054
- if (this.suppressNextReconnect)
7055
- this.suppressNextReconnect = false;
7056
- return;
7057
- }
7058
- this.authenticating = false;
7059
- this.ws = null;
7060
- this.connectPromise = null;
7061
- this.rejectAllPending('WebSocket closed');
7062
- this.setAllSubscriptionStatus('reconnecting');
7063
- if (this.suppressNextReconnect) {
7064
- this.suppressNextReconnect = false;
7065
- return;
7066
- }
7067
- this.scheduleReconnect();
7068
- });
7069
- });
7070
- }
7071
- scheduleReconnect() {
7072
- if (this.closed)
7073
- return;
7074
- if (this.reconnectTimer)
7075
- clearTimeout(this.reconnectTimer);
7076
- this.reconnectTimer = setTimeout(() => {
7077
- this.ensureConnected().catch(() => {
7078
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
7079
- this.scheduleReconnect();
7080
- });
7081
- }, this.reconnectDelay);
7082
- }
7083
- resubscribeAll() {
7084
- for (const sub of this.subscriptions.values()) {
7085
- this.sendSubscribe(sub);
7086
- }
7087
- }
7088
- // -----------------------------------------------------------------------
7089
- // Message handling
7090
- // -----------------------------------------------------------------------
7091
- handleMessage(raw) {
7092
- const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
7093
- let msg;
7094
- try {
7095
- msg = JSON.parse(text);
7096
- }
7097
- catch (_a) {
7098
- return;
7099
- }
7100
- switch (msg.type) {
7101
- case 'snapshot':
7102
- this.handleSnapshot(msg);
7103
- break;
7104
- case 'delta':
7105
- this.handleDelta(msg);
7106
- break;
7107
- case 'result':
7108
- this.handleResult(msg);
7109
- break;
7110
- case 'error':
7111
- this.handleError(msg);
7112
- break;
7113
- case 'pong':
7114
- break;
7115
- case 'authenticated':
7116
- break;
7117
- // v1 compat: handle legacy message types during transition
7118
- case 'subscribed':
7119
- this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
7120
- break;
7121
- case 'data':
7122
- // Legacy full-snapshot delta — treat as snapshot replacement
7123
- this.handleLegacyData(msg);
7124
- break;
7125
- case 'response':
7126
- this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));
7127
- break;
7128
- }
7129
- }
7130
- handleSnapshot(msg) {
7131
- var _a, _b, _c;
7132
- const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
7133
- if (!subId)
7134
- return;
7135
- const sub = this.findSubscriptionById(subId);
7136
- if (!sub)
7137
- return;
7138
- const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];
7139
- const docsArray = Array.isArray(docs) ? docs : [docs];
7140
- sub.docs.clear();
7141
- for (const doc of docsArray) {
7142
- if (doc && doc._id) {
7143
- sub.docs.set(doc._id, doc);
7144
- }
7145
- }
7146
- sub.ref.current = sub.docs;
7147
- sub.status = 'live';
7148
- sub.isStale = false;
7149
- sub.error = null;
7150
- this.notifySubscription(sub);
7151
- this.markIdbDirty(sub.path);
7152
- }
7153
- handleDelta(msg) {
7154
- var _a, _b;
7155
- const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
7156
- if (!subId)
7157
- return;
7158
- const sub = this.findSubscriptionById(subId);
7159
- if (!sub)
7160
- return;
7161
- if (sub.tier === 'ephemeral') {
7162
- // Ephemeral: just overwrite, no accumulation logic
7163
- if (msg.change === 'removed' && msg.docId) {
7164
- sub.docs.delete(msg.docId);
7165
- }
7166
- else if (msg.doc && msg.doc._id) {
7167
- sub.docs.set(msg.doc._id, msg.doc);
7168
- }
7169
- sub.ref.current = sub.docs;
7170
- if (sub.options.mode !== 'ref') {
7171
- this.notifySubscription(sub);
7172
- }
7173
- return;
7174
- }
7175
- // Durable/checkpointed: full delta handling
7176
- switch (msg.change) {
7177
- case 'added':
7178
- case 'modified':
7179
- if (msg.doc && msg.doc._id) {
7180
- sub.docs.set(msg.doc._id, msg.doc);
7181
- }
7182
- break;
7183
- case 'removed':
7184
- if (msg.docId) {
7185
- sub.docs.delete(msg.docId);
7186
- }
7187
- else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {
7188
- sub.docs.delete(msg.doc._id);
7189
- }
7190
- break;
7191
- }
7192
- sub.ref.current = sub.docs;
7193
- this.notifySubscription(sub);
7194
- this.markIdbDirty(sub.path);
7195
- }
7196
- handleLegacyData(msg) {
7197
- // Legacy v1 format: 'data' message with full snapshot or single doc
7198
- const subId = msg.subscriptionId;
7199
- if (!subId)
7200
- return;
7201
- const sub = this.findSubscriptionById(subId);
7202
- if (!sub)
7203
- return;
7204
- if (Array.isArray(msg.data)) {
7205
- // Full snapshot replacement
7206
- sub.docs.clear();
7207
- for (const doc of msg.data) {
7208
- if (doc && doc._id)
7209
- sub.docs.set(doc._id, doc);
7210
- }
7211
- }
7212
- else if (msg.data && msg.data._id) {
7213
- // Single doc update
7214
- sub.docs.set(msg.data._id, msg.data);
7215
- }
7216
- else if (msg.data === null) ;
7217
- sub.ref.current = sub.docs;
7218
- sub.status = 'live';
7219
- sub.isStale = false;
7220
- this.notifySubscription(sub);
7221
- this.markIdbDirty(sub.path);
7222
- }
7223
- handleResult(msg) {
7224
- var _a, _b, _c, _d;
7225
- const requestId = msg.requestId;
7226
- if (!requestId)
7227
- return;
7228
- const pending = this.pendingRequests.get(requestId);
7229
- if (!pending)
7230
- return;
7231
- this.pendingRequests.delete(requestId);
7232
- clearTimeout(pending.timeout);
7233
- const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);
7234
- if (ok) {
7235
- pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);
7236
- }
7237
- else {
7238
- pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));
7239
- }
7240
- }
7241
- handleError(msg) {
7242
- var _a, _b, _c;
7243
- const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
7244
- if (msg.code)
7245
- error.code = msg.code;
7246
- if (msg.subscriptionId || msg.id)
7247
- error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
7248
- const requestId = msg.requestId;
7249
- if (requestId) {
7250
- const pending = this.pendingRequests.get(requestId);
7251
- if (pending) {
7252
- this.pendingRequests.delete(requestId);
7253
- clearTimeout(pending.timeout);
7254
- pending.reject(error);
7255
- }
7256
- }
7257
- const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
7258
- if (subId) {
7259
- const sub = this.findSubscriptionById(subId);
7260
- if (sub) {
7261
- sub.status = 'error';
7262
- sub.error = error;
7263
- this.notifyState(sub);
7264
- for (const callback of Array.from(sub.errorCallbacks)) {
7265
- try {
7266
- callback(error);
7267
- }
7268
- catch ( /* swallow */_d) { /* swallow */ }
7269
- }
7270
- }
7271
- }
7272
- }
7273
- // -----------------------------------------------------------------------
7274
- // Subscribe
7275
- // -----------------------------------------------------------------------
7276
- async subscribe(path, opts = {}) {
7277
- var _a;
7278
- await this.ensureCurrentAuth();
7279
- const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
7280
- const subKey = this.getSubKey(path, opts);
7281
- let sub = this.subscriptions.get(subKey);
7282
- if (sub) {
7283
- // Existing subscription — add callback
7284
- if (opts.onData)
7285
- sub.callbacks.add(opts.onData);
7286
- if (opts.onState)
7287
- sub.stateCallbacks.add(opts.onState);
7288
- if (opts.onError)
7289
- sub.errorCallbacks.add(opts.onError);
7290
- // Immediately deliver current state
7291
- if (opts.onData && sub.docs.size > 0) {
7292
- opts.onData(this.docsToArray(sub));
7293
- }
7294
- if (opts.onState) {
7295
- opts.onState(this.getState(sub));
7296
- }
7297
- return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7298
- }
7299
- // New subscription
7300
- const subId = `sub_${nextRequestId++}`;
7301
- sub = {
7302
- id: subId,
7303
- path,
7304
- tier,
7305
- options: opts,
7306
- docs: new Map(),
7307
- status: 'idle',
7308
- isStale: false,
7309
- error: null,
7310
- callbacks: new Set(opts.onData ? [opts.onData] : []),
7311
- stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
7312
- errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
7313
- ref: { current: new Map() },
7314
- };
7315
- this.subscriptions.set(subKey, sub);
7316
- // Step 1: Load from IDB (durable/checkpointed only)
7317
- if (tier !== 'ephemeral') {
7318
- const cached = await idbGet(this.idbKey(path));
7319
- if (cached && Array.isArray(cached)) {
7320
- for (const doc of cached) {
7321
- if (doc && doc._id)
7322
- sub.docs.set(doc._id, doc);
7323
- }
7324
- sub.ref.current = sub.docs;
7325
- sub.status = 'cached';
7326
- sub.isStale = true;
7327
- this.notifySubscription(sub);
7328
- }
7329
- }
7330
- // Step 2: Connect and subscribe via WS
7331
- sub.status = sub.docs.size > 0 ? 'cached' : 'loading';
7332
- this.notifyState(sub);
7333
- try {
7334
- await this.ensureConnected();
7335
- this.sendSubscribe(sub);
7336
- }
7337
- catch (_b) {
7338
- sub.status = 'error';
7339
- sub.error = new Error('Connection failed');
7340
- this.notifyState(sub);
7341
- }
7342
- return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
7343
- }
7344
- getRef(path, opts = {}) {
7345
- var _a;
7346
- const subKey = this.getSubKey(path, opts);
7347
- const sub = this.subscriptions.get(subKey);
7348
- if (sub)
7349
- return sub.ref;
7350
- // Auto-subscribe in ref mode
7351
- const ref = { current: new Map() };
7352
- this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });
7353
- const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));
7354
- return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;
7355
- }
7356
- // -----------------------------------------------------------------------
7357
- // CRUD operations
7358
- // -----------------------------------------------------------------------
7359
- async set(path, doc) {
7360
- var _a;
7361
- await this.ensureConnected();
7362
- // Resolve operations (Increment, Time.Now) client-side for optimistic update
7363
- const resolvedDoc = this.resolveOperations(doc, path);
7364
- // Optimistic update: apply to local state immediately
7365
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7366
- const collectionPath = this.getCollectionPath(normalizedPath);
7367
- const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), {
7368
- // System timestamp field name: the Bounded worker stamps the neutral
7369
- // `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.
7370
- // Match it so the optimistic doc lines up with the server's confirmation.
7371
- [isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });
7372
- const sub = this.findSubscriptionByPath(collectionPath);
7373
- let prevDoc = null;
7374
- if (sub) {
7375
- prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
7376
- sub.docs.set(normalizedPath, optimisticDoc);
7377
- sub.ref.current = sub.docs;
7378
- this.notifySubscription(sub);
7379
- }
7380
- // Send to server
7381
- const requestId = `r_${nextRequestId++}`;
7382
- try {
7383
- const result = await this.sendRequest(requestId, {
7384
- type: 'set',
7385
- requestId,
7386
- documents: [{ destinationPath: normalizedPath, document: doc }],
7387
- });
7388
- // Replace optimistic doc with server-confirmed version
7389
- if (sub && result && typeof result === 'object') {
7390
- const serverDoc = Array.isArray(result) ? result[0] : result;
7391
- if (serverDoc && serverDoc._id) {
7392
- sub.docs.set(serverDoc._id, serverDoc);
7393
- sub.ref.current = sub.docs;
7394
- this.notifySubscription(sub);
7395
- this.markIdbDirty(collectionPath);
7396
- }
7397
- }
7398
- return Array.isArray(result) ? result[0] : result;
7399
- }
7400
- catch (err) {
7401
- // Revert optimistic update
7402
- if (sub) {
7403
- if (prevDoc) {
7404
- sub.docs.set(normalizedPath, prevDoc);
7405
- }
7406
- else {
7407
- sub.docs.delete(normalizedPath);
7408
- }
7409
- sub.ref.current = sub.docs;
7410
- this.notifySubscription(sub);
7411
- }
7412
- throw err;
7413
- }
7414
- }
7415
- async get(path) {
7416
- await this.ensureCurrentAuth();
7417
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7418
- // Check local subscriptions first
7419
- const collectionPath = this.getCollectionPath(normalizedPath);
7420
- const sub = this.findSubscriptionByPath(collectionPath);
7421
- if (sub && sub.status === 'live') {
7422
- const doc = sub.docs.get(normalizedPath);
7423
- return doc !== null && doc !== void 0 ? doc : null;
7424
- }
7425
- // One-shot WS fetch
7426
- await this.ensureConnected();
7427
- const requestId = `r_${nextRequestId++}`;
7428
- return this.sendRequest(requestId, {
7429
- type: 'get',
7430
- requestId,
7431
- path: normalizedPath,
7432
- });
7433
- }
7434
- async getMany(paths) {
7435
- await this.ensureConnected();
7436
- const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);
7437
- const requestId = `r_${nextRequestId++}`;
7438
- return this.sendRequest(requestId, {
7439
- type: 'getMany',
7440
- requestId,
7441
- paths: normalizedPaths,
7442
- });
7443
- }
7444
- async delete(path) {
7445
- var _a;
7446
- await this.ensureConnected();
7447
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7448
- // Optimistic: remove from local state
7449
- const collectionPath = this.getCollectionPath(normalizedPath);
7450
- const sub = this.findSubscriptionByPath(collectionPath);
7451
- let prevDoc = null;
7452
- if (sub) {
7453
- prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
7454
- sub.docs.delete(normalizedPath);
7455
- sub.ref.current = sub.docs;
7456
- this.notifySubscription(sub);
7457
- }
7458
- const requestId = `r_${nextRequestId++}`;
7459
- try {
7460
- await this.sendRequest(requestId, {
7461
- type: 'delete',
7462
- requestId,
7463
- path: normalizedPath,
7464
- });
7465
- if (sub)
7466
- this.markIdbDirty(collectionPath);
7467
- }
7468
- catch (err) {
7469
- // Revert
7470
- if (sub && prevDoc) {
7471
- sub.docs.set(normalizedPath, prevDoc);
7472
- sub.ref.current = sub.docs;
7473
- this.notifySubscription(sub);
7474
- }
7475
- throw err;
7476
- }
7477
- }
7478
- async query(path, opts) {
7479
- await this.ensureConnected();
7480
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7481
- const requestId = `r_${nextRequestId++}`;
7482
- 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 } : {})));
7483
- }
7484
- async count(path) {
7485
- var _a;
7486
- await this.ensureConnected();
7487
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7488
- const requestId = `r_${nextRequestId++}`;
7489
- const result = await this.sendRequest(requestId, {
7490
- type: 'count',
7491
- requestId,
7492
- path: normalizedPath,
7493
- });
7494
- return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);
7495
- }
7496
- async aggregate(path, operation, opts) {
7497
- await this.ensureConnected();
7498
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7499
- const requestId = `r_${nextRequestId++}`;
7500
- return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));
7501
- }
7502
- // -----------------------------------------------------------------------
7503
- // Helpers
7504
- // -----------------------------------------------------------------------
7505
- sendSubscribe(sub) {
7506
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
7507
- return;
7508
- const msg = {
7509
- type: 'subscribe',
7510
- subscriptionId: sub.id,
7511
- path: sub.path,
7512
- };
7513
- if (sub.options.filter)
7514
- msg.filter = sub.options.filter;
7515
- if (sub.options.includeSubPaths)
7516
- msg.includeSubPaths = true;
7517
- if (sub.options.limit)
7518
- msg.limit = sub.options.limit;
7519
- if (sub.options.prompt)
7520
- msg.prompt = sub.options.prompt;
7521
- this.ws.send(JSON.stringify(msg));
7522
- }
7523
- sendRequest(requestId, msg) {
7524
- return new Promise((resolve, reject) => {
7525
- const timeout = setTimeout(() => {
7526
- this.pendingRequests.delete(requestId);
7527
- reject(new Error('Request timed out'));
7528
- }, 30000);
7529
- this.pendingRequests.set(requestId, { resolve, reject, timeout });
7530
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7531
- this.ws.send(JSON.stringify(msg));
7532
- }
7533
- else {
7534
- this.pendingRequests.delete(requestId);
7535
- clearTimeout(timeout);
7536
- reject(new Error('WebSocket not connected'));
7537
- }
7538
- });
7539
- }
7540
- notifySubscription(sub) {
7541
- const data = this.docsToArray(sub);
7542
- const callbacks = Array.from(sub.callbacks);
7543
- for (const cb of callbacks) {
7544
- try {
7545
- cb(data);
7546
- }
7547
- catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }
7548
- }
7549
- this.notifyState(sub);
7550
- }
7551
- notifyState(sub) {
7552
- const state = this.getState(sub);
7553
- const callbacks = Array.from(sub.stateCallbacks);
7554
- for (const cb of callbacks) {
7555
- try {
7556
- cb(state);
7557
- }
7558
- catch ( /* swallow */_a) { /* swallow */ }
7559
- }
7560
- }
7561
- getState(sub) {
7562
- return {
7563
- data: this.docsToArray(sub),
7564
- status: sub.status,
7565
- isStale: sub.isStale,
7566
- error: sub.error,
7567
- };
7568
- }
7569
- docsToArray(sub) {
7570
- return Array.from(sub.docs.values());
7571
- }
7572
- findSubscriptionById(id) {
7573
- for (const sub of this.subscriptions.values()) {
7574
- if (sub.id === id)
7575
- return sub;
7576
- }
7577
- return undefined;
7578
- }
7579
- findSubscriptionByPath(collectionPath) {
7580
- for (const sub of this.subscriptions.values()) {
7581
- const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;
7582
- if (subPath === collectionPath)
7583
- return sub;
7584
- if (collectionPath.startsWith(subPath + '/'))
7585
- return sub;
7586
- }
7587
- return undefined;
7588
- }
7589
- getCollectionPath(docPath) {
7590
- const segments = docPath.split('/');
7591
- if (segments.length % 2 === 0) {
7592
- return segments.slice(0, -1).join('/');
7593
- }
7594
- return docPath;
7595
- }
7596
- getSubKey(path, opts) {
7597
- const parts = [this.appId, this.authPrincipalKey, path];
7598
- if (opts.filter)
7599
- parts.push(JSON.stringify(opts.filter));
7600
- if (opts.prompt)
7601
- parts.push(opts.prompt);
7602
- if (opts.tier)
7603
- parts.push(opts.tier);
7604
- return parts.join('::');
7605
- }
7606
- idbKey(path) {
7607
- return `${this.appId}:${this.authPrincipalKey}:${path}`;
7608
- }
7609
- markIdbDirty(path) {
7610
- const sub = this.findSubscriptionByPath(path);
7611
- if (sub && sub.tier === 'ephemeral')
7612
- return;
7613
- this.idbDirtyKeys.add(path);
7614
- if (!this.idbFlushTimer) {
7615
- this.idbFlushTimer = setTimeout(() => {
7616
- this.flushIdb();
7617
- this.idbFlushTimer = null;
7618
- }, 500);
7619
- }
7620
- }
7621
- async flushIdb() {
7622
- const keys = Array.from(this.idbDirtyKeys);
7623
- this.idbDirtyKeys.clear();
7624
- for (const path of keys) {
7625
- const sub = this.findSubscriptionByPath(path);
7626
- if (sub && sub.tier !== 'ephemeral') {
7627
- const docs = this.docsToArray(sub);
7628
- await idbSet(this.idbKey(path), docs);
7629
- }
7630
- }
7631
- }
7632
- createUnsubscribe(subKey, subId, onData, onState, onError) {
7633
- return async () => {
7634
- var _a;
7635
- const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
7636
- if (!sub)
7637
- return;
7638
- const currentSubKey = this.getSubKey(sub.path, sub.options);
7639
- if (onData)
7640
- sub.callbacks.delete(onData);
7641
- if (onState)
7642
- sub.stateCallbacks.delete(onState);
7643
- if (onError)
7644
- sub.errorCallbacks.delete(onError);
7645
- // If no more callbacks, unsubscribe entirely
7646
- if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
7647
- this.subscriptions.delete(subKey);
7648
- this.subscriptions.delete(currentSubKey);
7649
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
7650
- this.ws.send(JSON.stringify({
7651
- type: 'unsubscribe',
7652
- subscriptionId: sub.id,
7653
- }));
7654
- }
7655
- }
7656
- };
7657
- }
7658
- resolveOperations(doc, path) {
7659
- var _a;
7660
- if (!doc || typeof doc !== 'object')
7661
- return doc;
7662
- const resolved = {};
7663
- for (const [key, value] of Object.entries(doc)) {
7664
- if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {
7665
- const op = value;
7666
- if (op.operation === 'time' && op.value === 'now') {
7667
- resolved[key] = Math.floor(Date.now() / 1000);
7668
- }
7669
- else if (op.operation === 'increment') {
7670
- // For optimistic: get current value and add
7671
- const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
7672
- const collectionPath = this.getCollectionPath(normalizedPath);
7673
- const sub = this.findSubscriptionByPath(collectionPath);
7674
- const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);
7675
- const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;
7676
- resolved[key] = (typeof current === 'number' ? current : 0) + op.value;
7677
- }
7678
- else {
7679
- resolved[key] = value;
7680
- }
7681
- }
7682
- else {
7683
- resolved[key] = value;
7684
- }
7685
- }
7686
- return resolved;
7687
- }
7688
- rejectAllPending(reason) {
7689
- for (const [requestId, pending] of this.pendingRequests) {
7690
- clearTimeout(pending.timeout);
7691
- pending.reject(new Error(reason));
7692
- }
7693
- this.pendingRequests.clear();
7694
- }
7695
- setAllSubscriptionStatus(status) {
7696
- for (const sub of this.subscriptions.values()) {
7697
- sub.status = status;
7698
- this.notifyState(sub);
7699
- }
7700
- }
7701
- // -----------------------------------------------------------------------
7702
- // Lifecycle
7703
- // -----------------------------------------------------------------------
7704
- close() {
7705
- this.closed = true;
7706
- if (this.reconnectTimer)
7707
- clearTimeout(this.reconnectTimer);
7708
- if (this.idbFlushTimer)
7709
- clearTimeout(this.idbFlushTimer);
7710
- if (this.tokenRefreshTimer)
7711
- clearInterval(this.tokenRefreshTimer);
7712
- this.flushIdb();
7713
- if (this.ws) {
7714
- this.ws.close(1000, 'Store closed');
7715
- this.ws = null;
7716
- }
7717
- this.rejectAllPending('Store closed');
7718
- this.subscriptions.clear();
7719
- }
7720
- async reconnectWithNewAuth() {
7721
- if (this.closed)
7722
- return;
7723
- await this.ensureInitialized();
7724
- await this.refreshToken();
7725
- await this.applyAuthPrincipalChange();
7726
- if (this.subscriptions.size > 0) {
7727
- await this.ensureConnected().catch((error) => {
7728
- this.setAllSubscriptionStatus('error');
7729
- for (const sub of this.subscriptions.values()) {
7730
- sub.error = error instanceof Error ? error : new Error(String(error));
7731
- this.notifyState(sub);
7732
- }
7733
- });
7734
- }
7735
- }
7736
- }
7737
- // ---------------------------------------------------------------------------
7738
- // Singleton instance
7739
- // ---------------------------------------------------------------------------
7740
- let storeInstance = null;
7741
- function getRealtimeStore() {
7742
- if (!storeInstance) {
7743
- storeInstance = new RealtimeStore();
7744
- }
7745
- return storeInstance;
7746
- }
7747
- function resetRealtimeStore() {
7748
- if (storeInstance) {
7749
- storeInstance.close();
7750
- storeInstance = null;
7751
- }
7752
- }
7753
- async function reconnectRealtimeStoreWithNewAuth() {
7754
- if (storeInstance) {
7755
- await storeInstance.reconnectWithNewAuth();
7756
- }
7757
- }
7758
-
7759
- var realtimeStore = /*#__PURE__*/Object.freeze({
7760
- __proto__: null,
7761
- RealtimeStore: RealtimeStore,
7762
- getRealtimeStore: getRealtimeStore,
7763
- reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
7764
- resetRealtimeStore: resetRealtimeStore
7765
- });
7766
-
7767
6956
  // ---------------------------------------------------------------------------
7768
6957
  // functions.ts -- Bounded Functions client (the imperative escape hatch).
7769
6958
  //
@@ -7823,7 +7012,7 @@ async function invoke(name, args = {}, opts = {}) {
7823
7012
  const authHeader = ((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders)
7824
7013
  ? await opts._overrides._getAuthHeaders()
7825
7014
  : await createAuthHeader(config.isServer);
7826
- 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 : {}));
7015
+ 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 : {}));
7827
7016
  const controller = new AbortController();
7828
7017
  const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7829
7018
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -7892,8 +7081,8 @@ const functions = { invoke };
7892
7081
  // Subscribing to your view: a per-player view doc lives at
7893
7082
  // `<roomPath>/view/<myUserId>` (the policy declares
7894
7083
  // `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
7895
- // Wallet-address keyed view paths remain supported through opts.address for
7896
- // older policies, but new live rooms should key views by @user.id.
7084
+ // View paths key only by @user.id; wallet-address aliases are intentionally not
7085
+ // accepted by this helper.
7897
7086
  // ---------------------------------------------------------------------------
7898
7087
  class LiveIntentError extends Error {
7899
7088
  constructor(message, statusCode, details) {
@@ -7987,7 +7176,7 @@ async function intent(roomPath, intent, opts = {}) {
7987
7176
  const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
7988
7177
  const buildHeaders = async () => {
7989
7178
  const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
7990
- 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 : {}));
7179
+ 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 : {}));
7991
7180
  };
7992
7181
  const controller = new AbortController();
7993
7182
  const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
@@ -8048,7 +7237,7 @@ async function status(roomPath, opts = {}) {
8048
7237
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
8049
7238
  const config = await getConfig();
8050
7239
  const base = realtimeHttpBase(config.wsApiUrl);
8051
- const headers = Object.assign({ 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
7240
+ const headers = Object.assign({ 'X-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
8052
7241
  const controller = new AbortController();
8053
7242
  const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
8054
7243
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -8090,29 +7279,27 @@ async function status(roomPath, opts = {}) {
8090
7279
  * subscribe('<roomPath>/view/<myUserId>', { onData, onError })
8091
7280
  *
8092
7281
  * The view id defaults to the logged-in user's @user.id (from the session token
8093
- * claims); pass `opts.userId` to override. `opts.address` is kept as a legacy
8094
- * alias for older wallet-address keyed policies. Returns the unsubscribe
8095
- * function (a Promise<() => Promise<void>>, same as `subscribe`).
7282
+ * claims); pass `opts.userId` to override. Returns the unsubscribe function
7283
+ * (a Promise<() => Promise<void>>, same as `subscribe`).
8096
7284
  *
8097
7285
  * Note: this is a browser-first helper (the WS subscription manager is
8098
7286
  * browser-oriented). Server consumers should use `live.intent`.
8099
7287
  */
8100
7288
  async function subscribeView(roomPath, opts) {
8101
- var _a, _b, _c;
8102
7289
  if (!roomPath || typeof roomPath !== 'string') {
8103
7290
  throw new LiveIntentError('A room path is required');
8104
7291
  }
8105
7292
  if (!opts || typeof opts.onData !== 'function') {
8106
7293
  throw new LiveIntentError('subscribeView requires an onData callback');
8107
7294
  }
8108
- let viewUserId = (_a = opts.userId) !== null && _a !== void 0 ? _a : opts.address;
7295
+ let viewUserId = opts.userId;
8109
7296
  if (!viewUserId) {
8110
7297
  const config = await getConfig();
8111
7298
  const info = await getUserInfo(config.isServer);
8112
7299
  // getUserInfo returns the RAW idToken payload. The universal live view key
8113
- // is @user.id (`custom:userId`); wallet-address keyed views are still
8114
- // resolved as a compatibility fallback for older policies.
8115
- 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;
7300
+ // is @user.id (`custom:userId`); wallet-address keyed compatibility aliases
7301
+ // are intentionally not accepted.
7302
+ viewUserId = info === null || info === void 0 ? void 0 : info['custom:userId'];
8116
7303
  }
8117
7304
  if (!viewUserId || typeof viewUserId !== 'string') {
8118
7305
  throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
@@ -8187,7 +7374,6 @@ exports.FunctionInvokeError = FunctionInvokeError;
8187
7374
  exports.InsufficientBalanceError = InsufficientBalanceError;
8188
7375
  exports.LiveIntentError = LiveIntentError;
8189
7376
  exports.ReactNativeSessionManager = ReactNativeSessionManager;
8190
- exports.RealtimeStore = RealtimeStore;
8191
7377
  exports.ServerSessionManager = ServerSessionManager;
8192
7378
  exports.WebSessionManager = WebSessionManager;
8193
7379
  exports.aggregate = aggregate;
@@ -8210,7 +7396,6 @@ exports.getConfig = getConfig;
8210
7396
  exports.getFiles = getFiles;
8211
7397
  exports.getIdToken = getIdToken;
8212
7398
  exports.getMany = getMany;
8213
- exports.getRealtimeStore = getRealtimeStore;
8214
7399
  exports.getWebhookKeysUrl = getWebhookKeysUrl;
8215
7400
  exports.hasActiveConnection = hasActiveConnection;
8216
7401
  exports.increment = increment;
@@ -8224,7 +7409,6 @@ exports.now = now;
8224
7409
  exports.queryAggregate = queryAggregate;
8225
7410
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
8226
7411
  exports.refreshSession = refreshSession;
8227
- exports.resetRealtimeStore = resetRealtimeStore;
8228
7412
  exports.revokeSession = revokeSession;
8229
7413
  exports.runExpression = runExpression;
8230
7414
  exports.runExpressionMany = runExpressionMany;
@@ -8244,9 +7428,4 @@ exports.subscribeLiveView = subscribeView;
8244
7428
  exports.toMillis = toMillis;
8245
7429
  exports.toSeconds = toSeconds;
8246
7430
  exports.withEffects = withEffects;
8247
- exports.wsDelete = wsDelete;
8248
- exports.wsGet = wsGet;
8249
- exports.wsGetMany = wsGetMany;
8250
- exports.wsQuery = wsQuery;
8251
- exports.wsSet = wsSet;
8252
7431
  //# sourceMappingURL=index.js.map