@bounded-sh/core 0.0.41 → 0.0.44

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
@@ -51,6 +51,10 @@ let clientConfig = {
51
51
  isServer: false,
52
52
  };
53
53
  let isInitialized = false;
54
+ // Callers awaiting getConfig() before init() completes. Resolved synchronously
55
+ // by init() — the previous 50ms setInterval poll added up to 50ms of latency
56
+ // to the first SDK operation after init for no reason.
57
+ let initWaiters = [];
54
58
  const BOUNDED_NETWORKS = {
55
59
  'bounded-staging': {
56
60
  wsApiUrl: 'wss://realtime-staging.bounded.sh',
@@ -119,23 +123,17 @@ function init(newConfig) {
119
123
  const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
120
124
  clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
121
125
  isInitialized = true;
126
+ const waiters = initWaiters;
127
+ initWaiters = [];
128
+ for (const wake of waiters)
129
+ wake();
122
130
  resolve();
123
131
  });
124
132
  }
125
133
  function waitForInitialization() {
126
- return new Promise((resolve) => {
127
- if (isInitialized) {
128
- resolve();
129
- }
130
- else {
131
- const interval = setInterval(() => {
132
- if (isInitialized) {
133
- clearInterval(interval);
134
- resolve();
135
- }
136
- }, 50);
137
- }
138
- });
134
+ if (isInitialized)
135
+ return Promise.resolve();
136
+ return new Promise((resolve) => { initWaiters.push(resolve); });
139
137
  }
140
138
  async function getConfig() {
141
139
  await waitForInitialization();
@@ -2821,8 +2819,24 @@ async function genAuthNonce() {
2821
2819
  // stamps an is_anonymous claim. Without this a guest would be indistinguishable
2822
2820
  // from 'phantom' and couldn't be gated or flagged.
2823
2821
  const SIGNATURE_AUTH_METHODS = new Set(['phantom', 'mobile-wallet-adapter', 'guest']);
2824
- async function createSessionWithSignature(address, message, signature) {
2825
- var _a, _b;
2822
+ // Maps a client authMethod to the /session issuer's signature discriminator when
2823
+ // they differ. EVM WALLET LOGIN (config.authMethod 'evm-wallet') exchanges a SIWE
2824
+ // (EIP-4361 personal_sign, secp256k1) signature, which the bounded-auth issuer
2825
+ // routes on authMethod:"siwe" (index.ts /session → parseSiweMessage/recoverSiweSigner)
2826
+ // and mints under custom:evmWalletAddress. The EVM provider also passes 'siwe'
2827
+ // explicitly (authMethodOverride), so this map is the belt-and-suspenders path.
2828
+ const SESSION_METHOD_ALIASES = { 'evm-wallet': 'siwe' };
2829
+ /**
2830
+ * Exchange a wallet signature for a Bounded session at the /session issuer.
2831
+ *
2832
+ * @param authMethodOverride the exact /session discriminator to send (e.g. 'siwe'
2833
+ * for the SIWE/EVM path). When omitted the method is derived from config: an
2834
+ * aliased method (evm-wallet → siwe) maps through SESSION_METHOD_ALIASES, a
2835
+ * Solana wallet/guest method passes through verbatim, and anything else
2836
+ * defaults to the canonical 'phantom' the issuer accepts for a signature.
2837
+ */
2838
+ async function createSessionWithSignature(address, message, signature, authMethodOverride) {
2839
+ var _a, _b, _c;
2826
2840
  const client = await getAxiosAuthClient();
2827
2841
  const config = await getConfig();
2828
2842
  const appId = config.appId;
@@ -2831,7 +2845,7 @@ async function createSessionWithSignature(address, message, signature) {
2831
2845
  // canonical Solana wallet method 'phantom' that every /session issuer accepts
2832
2846
  // for a signature exchange. This is what makes the server SDK + guest device
2833
2847
  // keypair work under the email-default config.
2834
- const authMethod = SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom';
2848
+ const authMethod = (_a = authMethodOverride !== null && authMethodOverride !== void 0 ? authMethodOverride : SESSION_METHOD_ALIASES[config.authMethod]) !== null && _a !== void 0 ? _a : (SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom');
2835
2849
  try {
2836
2850
  const response = await client.post('/session', {
2837
2851
  appId,
@@ -2846,11 +2860,11 @@ async function createSessionWithSignature(address, message, signature) {
2846
2860
  // Surface the issuer's actionable message (e.g. "Anonymous auth is not enabled for
2847
2861
  // this app. Add \"auth\": { \"anonymous\": true } to policy.json and redeploy.")
2848
2862
  // instead of axios's generic "Request failed with status code 403".
2849
- const d = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
2863
+ const d = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data;
2850
2864
  if (d && (d.message || d.error)) {
2851
2865
  const e = new Error(String(d.message || d.error));
2852
2866
  e.code = d.error;
2853
- e.status = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
2867
+ e.status = (_c = err === null || err === void 0 ? void 0 : err.response) === null || _c === void 0 ? void 0 : _c.status;
2854
2868
  e.hint = d.hint;
2855
2869
  throw e;
2856
2870
  }
@@ -2929,7 +2943,14 @@ async function revokeSession(refreshToken, issuer) {
2929
2943
  await axios.post(`${base}/session/revoke`, {
2930
2944
  refreshToken,
2931
2945
  appId: config.appId,
2932
- }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
2946
+ }, {
2947
+ headers: { 'Content-Type': 'application/json' },
2948
+ timeout: 15000,
2949
+ // Preserve the transport error for programmatic diagnostics while
2950
+ // ensuring AxiosError.toJSON() cannot serialize the live token body
2951
+ // into logs, telemetry, UI state, or crash reports.
2952
+ redact: ['data'],
2953
+ });
2933
2954
  }
2934
2955
  catch (err) {
2935
2956
  throw new SessionRevokeError(revokeFailureMessage(err), err);
@@ -3618,12 +3639,13 @@ const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use c
3618
3639
  /* SESSION MANAGER */
3619
3640
  /* ------------------------------------------------------------------ */
3620
3641
  class ServerSessionManager {
3621
- constructor(keypair) {
3642
+ constructor(keypair, sessionFactory) {
3622
3643
  /* Private cache (lives for the life of the instance) */
3623
3644
  this.session = null;
3624
3645
  /* Coalesce concurrent getSession() calls into a single in-flight request */
3625
3646
  this.pendingSession = null;
3626
3647
  this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
3648
+ this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
3627
3649
  }
3628
3650
  /**
3629
3651
  * Create a session manager for a specific keypair.
@@ -3632,10 +3654,30 @@ class ServerSessionManager {
3632
3654
  static forKeypair(keypair) {
3633
3655
  return new ServerSessionManager(keypair);
3634
3656
  }
3657
+ /**
3658
+ * Create an isolated server session manager for a non-Solana signer (currently
3659
+ * EVM/SIWE). The factory must perform the signature exchange and return the
3660
+ * canonical signer address with its tokens; no signer state becomes global.
3661
+ */
3662
+ static forSessionFactory(factory) {
3663
+ if (typeof factory !== 'function')
3664
+ throw new Error('A server session factory is required');
3665
+ return new ServerSessionManager(undefined, factory);
3666
+ }
3635
3667
  /* ---------------------------------------------- *
3636
3668
  * Session creation (instance method)
3637
3669
  * ---------------------------------------------- */
3638
3670
  async createSession() {
3671
+ if (this.sessionFactory) {
3672
+ const session = await this.sessionFactory();
3673
+ if (!session || typeof session.address !== 'string' || !session.address ||
3674
+ typeof session.idToken !== 'string' || !session.idToken ||
3675
+ typeof session.accessToken !== 'string' || !session.accessToken ||
3676
+ typeof session.refreshToken !== 'string' || !session.refreshToken) {
3677
+ throw new Error('Server signer session factory returned an invalid session');
3678
+ }
3679
+ return session;
3680
+ }
3639
3681
  if (!this.keypair) {
3640
3682
  throw new Error(NO_AMBIENT_SERVER_SESSION);
3641
3683
  }
@@ -3739,6 +3781,133 @@ function decodeBase64Url(input) {
3739
3781
  return safeAtob(b64);
3740
3782
  }
3741
3783
 
3784
+ // evm-utils.ts — the EVM twin of sol/sol-utils.ts's genSolanaMessage. It builds
3785
+ // the canonical EIP-4361 (Sign-In with Ethereum, SIWE) prompt the bounded-auth
3786
+ // issuer verifies, and normalizes EVM addresses to the single canonical form the
3787
+ // identity layer matches on.
3788
+ //
3789
+ // The SIWE message MUST match bounded-auth's parser byte-for-byte
3790
+ // (packages/cdk/cloudflare/bounded-auth/src/verify-siwe.ts → parseSiweMessage):
3791
+ // <domain> wants you to sign in with your Ethereum account:
3792
+ // <address>
3793
+ // <blank>
3794
+ // <statement>
3795
+ // <blank>
3796
+ // URI: <uri>
3797
+ // Version: 1
3798
+ // Chain ID: <chainId>
3799
+ // Nonce: <nonce>
3800
+ // Issued At: <ISO8601>
3801
+ // Expiration Time: <ISO8601>
3802
+ // Resources:
3803
+ // - bounded:app:<appId>
3804
+ //
3805
+ // Bindings the verifier enforces (so we produce them exactly): the address is
3806
+ // lowercased (identity match is case-sensitive, so lowercase-at-boundary is
3807
+ // mandatory), the single `bounded:app:<appId>` resource binds the signature to
3808
+ // THIS app (a signature for one app must not mint a session for another — the
3809
+ // SIWE analog of genSolanaMessage's `App ID:` line), and Expiration Time is
3810
+ // REQUIRED and must fall within the issuer's 10-minute max validity window.
3811
+ /** Default statement — kept identical to genSolanaMessage's for a consistent prompt. */
3812
+ const DEFAULT_STATEMENT = 'Sign this message to authenticate with our application.';
3813
+ /** Match bounded-auth's MAX_AUTH_WINDOW_MS (10 min); we default to a 5-min window. */
3814
+ const DEFAULT_VALIDITY_MS = 5 * 60 * 1000;
3815
+ /**
3816
+ * Build the canonical EIP-4361 SIWE message the bounded-auth issuer verifies.
3817
+ *
3818
+ * @param address the connected EVM account (lowercased into the prompt).
3819
+ * @param nonce single-use nonce from genAuthNonce().
3820
+ * @param appId the target app id (bound as the sole `bounded:app:<appId>` resource).
3821
+ * @param domain RFC-3986 authority of the requesting site (e.g. window.location.host).
3822
+ * @param uri the requesting URI; its origin is bound to the domain and actual browser Origin.
3823
+ */
3824
+ function genSiweMessage(address, nonce, appId, domain, uri, options = {}) {
3825
+ var _a, _b, _c, _d, _e;
3826
+ const addr = normalizeEvmAddress(address);
3827
+ if (!addr)
3828
+ throw new Error('SIWE address must be a 20-byte 0x-prefixed EVM address');
3829
+ const statement = (_a = options.statement) !== null && _a !== void 0 ? _a : DEFAULT_STATEMENT;
3830
+ const version = (_b = options.version) !== null && _b !== void 0 ? _b : '1';
3831
+ const chainId = (_c = options.chainId) !== null && _c !== void 0 ? _c : 1;
3832
+ if (statement !== DEFAULT_STATEMENT)
3833
+ throw new Error('SIWE statement must use the canonical Bounded authentication text');
3834
+ if (version !== '1')
3835
+ throw new Error('SIWE version must be 1');
3836
+ if (!Number.isSafeInteger(chainId) || chainId <= 0)
3837
+ throw new Error('SIWE chainId must be a positive safe integer');
3838
+ if (!/^[A-Za-z0-9]{8,}$/.test(nonce))
3839
+ throw new Error('SIWE nonce must be at least 8 alphanumeric characters');
3840
+ if (!appId || /[\r\n]/.test(appId))
3841
+ throw new Error('SIWE appId is required and must be single-line');
3842
+ if (!domain || domain !== domain.trim() || /[\s/@?#\\]/.test(domain))
3843
+ throw new Error('SIWE domain must be an RFC-3986 authority');
3844
+ try {
3845
+ const parsedAuthority = new URL(`https://${domain}`);
3846
+ if (parsedAuthority.username || parsedAuthority.password || parsedAuthority.pathname !== '/' ||
3847
+ parsedAuthority.search || parsedAuthority.hash || parsedAuthority.host.toLowerCase() !== domain) {
3848
+ throw new Error('non-canonical authority');
3849
+ }
3850
+ }
3851
+ catch (_f) {
3852
+ throw new Error('SIWE domain must be a canonical lowercase RFC-3986 authority');
3853
+ }
3854
+ let parsedUri;
3855
+ try {
3856
+ parsedUri = new URL(uri);
3857
+ }
3858
+ catch (_g) {
3859
+ throw new Error('SIWE URI must be an absolute URL');
3860
+ }
3861
+ const local = parsedUri.hostname === 'localhost' || parsedUri.hostname === '127.0.0.1' || parsedUri.hostname === '[::1]';
3862
+ if (parsedUri.protocol !== 'https:' && !(local && parsedUri.protocol === 'http:')) {
3863
+ throw new Error('SIWE URI must use HTTPS (HTTP is allowed only for localhost)');
3864
+ }
3865
+ if (parsedUri.username || parsedUri.password || parsedUri.hash || parsedUri.host.toLowerCase() !== domain.toLowerCase()) {
3866
+ throw new Error('SIWE URI authority must match the SIWE domain');
3867
+ }
3868
+ const issuedAtDate = (_d = options.issuedAt) !== null && _d !== void 0 ? _d : new Date();
3869
+ const expirationDate = (_e = options.expirationTime) !== null && _e !== void 0 ? _e : new Date(issuedAtDate.getTime() + DEFAULT_VALIDITY_MS);
3870
+ if (!Number.isFinite(issuedAtDate.getTime()) || !Number.isFinite(expirationDate.getTime()) || expirationDate <= issuedAtDate) {
3871
+ throw new Error('SIWE validity window is invalid');
3872
+ }
3873
+ if (expirationDate.getTime() - issuedAtDate.getTime() > 10 * 60 * 1000) {
3874
+ throw new Error('SIWE validity window exceeds 10 minutes');
3875
+ }
3876
+ const issuedAt = issuedAtDate.toISOString();
3877
+ const expirationTime = expirationDate.toISOString();
3878
+ return (`${domain} wants you to sign in with your Ethereum account:\n${addr}\n\n` +
3879
+ `${statement}\n\n` +
3880
+ `URI: ${parsedUri.toString()}\n` +
3881
+ `Version: ${version}\n` +
3882
+ `Chain ID: ${chainId}\n` +
3883
+ `Nonce: ${nonce}\n` +
3884
+ `Issued At: ${issuedAt}\n` +
3885
+ `Expiration Time: ${expirationTime}\n` +
3886
+ `Resources:\n` +
3887
+ `- bounded:app:${appId}`);
3888
+ }
3889
+ /**
3890
+ * Lowercase an EVM address to the canonical identity form. EVM addresses are
3891
+ * case-insensitive on-chain but the identity layer (@user.evmAddress) matches
3892
+ * case-sensitively, so every boundary normalizes to lowercase. Does not validate
3893
+ * shape — use normalizeEvmAddress() when the input is untrusted.
3894
+ */
3895
+ function toLowerEvmAddress(address) {
3896
+ return address.trim().toLowerCase();
3897
+ }
3898
+ /**
3899
+ * Normalize an untrusted EVM address to canonical lowercase 0x-hex, or null when
3900
+ * it isn't a well-formed 20-byte hex address. Mirrors the realtime-worker's
3901
+ * normalizeEvmAddress (auth.ts) EXACTLY so a client-derived @user.evmAddress and
3902
+ * the server-authenticated one are byte-identical (fail-closed on malformed).
3903
+ */
3904
+ function normalizeEvmAddress(raw) {
3905
+ if (typeof raw !== 'string')
3906
+ return null;
3907
+ const v = raw.trim().toLowerCase();
3908
+ return /^0x[0-9a-f]{40}$/.test(v) ? v : null;
3909
+ }
3910
+
3742
3911
  async function createBearerToken(isServer) {
3743
3912
  if (isServer) {
3744
3913
  const sessionMgr = ServerSessionManager.instance;
@@ -3787,7 +3956,7 @@ async function getUserInfo(isServer) {
3787
3956
  * Returns all-null when the token can't be decoded (caller treats as no identity).
3788
3957
  */
3789
3958
  function deriveUserIdentityFromIdToken(idToken) {
3790
- const empty = { id: null, address: null, email: null };
3959
+ const empty = { id: null, address: null, email: null, evmAddress: null };
3791
3960
  if (!idToken)
3792
3961
  return empty;
3793
3962
  try {
@@ -3800,14 +3969,25 @@ function deriveUserIdentityFromIdToken(idToken) {
3800
3969
  ? walletClaim
3801
3970
  : null;
3802
3971
  const userIdClaim = payload['custom:userId'];
3803
- const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3972
+ let id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3804
3973
  ? userIdClaim
3805
3974
  : null;
3806
3975
  const emailClaim = payload['email'];
3807
3976
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
3808
3977
  ? emailClaim.toLowerCase()
3809
3978
  : null;
3810
- return { id, address, email };
3979
+ // EVM wallet — carried on the `custom:evmWalletAddress` claim (an EVM login
3980
+ // OMITS custom:walletAddress). Normalized to canonical lowercase 0x-hex the
3981
+ // SAME way the realtime-worker does, so the client @user.evmAddress and the
3982
+ // server-authenticated one are byte-identical (fail-closed on malformed).
3983
+ const evmAddress = normalizeEvmAddress(payload['custom:evmWalletAddress']);
3984
+ // A SIWE identity uses its EVM address as custom:userId. Normalize legacy
3985
+ // mixed-case tokens at this boundary too so the principal and
3986
+ // @user.evmAddress cannot diverge solely by hex casing. Opaque human-login
3987
+ // ids remain byte-for-byte unchanged even when they have a linked wallet.
3988
+ if (evmAddress && id && normalizeEvmAddress(id) === evmAddress)
3989
+ id = evmAddress;
3990
+ return { id, address, email, evmAddress };
3811
3991
  }
3812
3992
  catch (_a) {
3813
3993
  return empty;
@@ -4129,6 +4309,456 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4129
4309
  }
4130
4310
  }
4131
4311
 
4312
+ /**
4313
+ * Selectors derived from the canonical BoundedRuntime signatures (BoundedTypes.sol):
4314
+ *
4315
+ * cast sig 'setDocuments(string,(string[],bool,(string,uint8,uint8,bytes)[])[])'
4316
+ * => 0x0827b094
4317
+ * cast sig 'setDocumentsAttested(string,(string[],bool,(string,uint8,uint8,bytes)[])[],(bytes32,uint256,uint256,uint8,bytes32,bytes32))'
4318
+ * => 0x87b111c4
4319
+ *
4320
+ * Keep the signatures beside the constants: changing either Solidity tuple is an
4321
+ * ABI break and must deliberately update these values and the Foundry goldens.
4322
+ */
4323
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = '0x0827b094';
4324
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = '0x87b111c4';
4325
+ const SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR.slice(2);
4326
+ const SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR.slice(2);
4327
+ const MAX_CALLDATA_BYTES = 1024 * 1024;
4328
+ const MAX_ARRAY_ITEMS = 2048;
4329
+ const BIGINT_ZERO = BigInt(0);
4330
+ const BIGINT_ONE = BigInt(1);
4331
+ const BIGINT_EIGHT = BigInt(8);
4332
+ const SECP256K1_N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
4333
+ const SECP256K1_HALF_N = BigInt('0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0');
4334
+ function hexBytes(raw) {
4335
+ if (typeof raw !== 'string' || !/^0x[0-9a-fA-F]*$/.test(raw) || raw.length % 2 !== 0) {
4336
+ throw new Error('EVM transaction calldata must be even-length 0x hex');
4337
+ }
4338
+ const hex = raw.slice(2);
4339
+ if (hex.length / 2 > MAX_CALLDATA_BYTES)
4340
+ throw new Error('EVM transaction calldata exceeds the client safety limit');
4341
+ const out = new Uint8Array(hex.length / 2);
4342
+ for (let i = 0; i < out.length; i++)
4343
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
4344
+ return out;
4345
+ }
4346
+ class AbiReader {
4347
+ constructor(bytes) {
4348
+ this.bytes = bytes;
4349
+ this.maxRead = 0;
4350
+ this.dynamicPayloadBytes = 0;
4351
+ this.utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
4352
+ }
4353
+ range(offset, length, label) {
4354
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > this.bytes.length) {
4355
+ throw new Error(`Malformed EVM setDocuments calldata (${label})`);
4356
+ }
4357
+ this.maxRead = Math.max(this.maxRead, offset + length);
4358
+ }
4359
+ word(offset, label) {
4360
+ this.range(offset, 32, label);
4361
+ let value = BIGINT_ZERO;
4362
+ for (let i = 0; i < 32; i++)
4363
+ value = (value << BIGINT_EIGHT) | BigInt(this.bytes[offset + i]);
4364
+ return value;
4365
+ }
4366
+ numberWord(offset, label, max = Number.MAX_SAFE_INTEGER) {
4367
+ const value = this.word(offset, label);
4368
+ if (value > BigInt(max))
4369
+ throw new Error(`Malformed EVM setDocuments calldata (${label} too large)`);
4370
+ return Number(value);
4371
+ }
4372
+ offset(base, wordOffset, min, label) {
4373
+ const relative = this.numberWord(wordOffset, `${label} offset`, this.bytes.length);
4374
+ if (relative < min || relative % 32 !== 0)
4375
+ throw new Error(`Malformed EVM setDocuments calldata (${label} offset)`);
4376
+ const absolute = base + relative;
4377
+ this.range(absolute, 0, label);
4378
+ return absolute;
4379
+ }
4380
+ dynamicBytes(at, label) {
4381
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4382
+ this.dynamicPayloadBytes += length;
4383
+ // ABI offsets may alias. Cap the cumulative decoded payload so a malicious
4384
+ // 1 MB tail referenced by thousands of fields cannot amplify into a
4385
+ // multi-gigabyte allocation/UTF-8 decode in the wallet process.
4386
+ if (this.dynamicPayloadBytes > MAX_CALLDATA_BYTES) {
4387
+ throw new Error('Malformed EVM setDocuments calldata (dynamic payload amplification)');
4388
+ }
4389
+ const start = at + 32;
4390
+ const padded = Math.ceil(length / 32) * 32;
4391
+ this.range(start, padded, label);
4392
+ for (let i = start + length; i < start + padded; i++) {
4393
+ if (this.bytes[i] !== 0)
4394
+ throw new Error(`Malformed EVM setDocuments calldata (${label} padding)`);
4395
+ }
4396
+ return this.bytes.slice(start, start + length);
4397
+ }
4398
+ dynamicEnd(at, label) {
4399
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4400
+ const end = at + 32 + Math.ceil(length / 32) * 32;
4401
+ this.range(at + 32, Math.ceil(length / 32) * 32, label);
4402
+ return end;
4403
+ }
4404
+ string(at, label) {
4405
+ try {
4406
+ return this.utf8.decode(this.dynamicBytes(at, label));
4407
+ }
4408
+ catch (_a) {
4409
+ throw new Error(`Malformed EVM setDocuments calldata (${label} utf8)`);
4410
+ }
4411
+ }
4412
+ finish() {
4413
+ if (this.maxRead !== this.bytes.length)
4414
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4415
+ }
4416
+ }
4417
+ function assertCanonicalOffset(actual, expected, label) {
4418
+ if (actual !== expected)
4419
+ throw new Error(`Malformed EVM setDocuments calldata (${label} non-canonical offset)`);
4420
+ }
4421
+ function decodeStringArray(reader, at, label) {
4422
+ const count = reader.numberWord(at, `${label} count`, MAX_ARRAY_ITEMS);
4423
+ const head = at + 32;
4424
+ const min = count * 32;
4425
+ const out = [];
4426
+ let expectedTail = head + min;
4427
+ for (let i = 0; i < count; i++) {
4428
+ const itemAt = reader.offset(head, head + i * 32, min, `${label}[${i}]`);
4429
+ assertCanonicalOffset(itemAt, expectedTail, `${label}[${i}]`);
4430
+ out.push(reader.string(itemAt, `${label}[${i}]`));
4431
+ expectedTail = reader.dynamicEnd(itemAt, `${label}[${i}]`);
4432
+ }
4433
+ return { value: out, end: expectedTail };
4434
+ }
4435
+ function decodeFields(reader, at) {
4436
+ const count = reader.numberWord(at, 'fields count', MAX_ARRAY_ITEMS);
4437
+ const head = at + 32;
4438
+ const min = count * 32;
4439
+ const out = [];
4440
+ let expectedTail = head + min;
4441
+ for (let i = 0; i < count; i++) {
4442
+ const tuple = reader.offset(head, head + i * 32, min, `field[${i}]`);
4443
+ assertCanonicalOffset(tuple, expectedTail, `field[${i}]`);
4444
+ const nameAt = reader.offset(tuple, tuple, 128, `field[${i}].name`);
4445
+ assertCanonicalOffset(nameAt, tuple + 128, `field[${i}].name`);
4446
+ const name = reader.string(nameAt, `field[${i}].name`);
4447
+ const nameEnd = reader.dynamicEnd(nameAt, `field[${i}].name`);
4448
+ const ftype = reader.numberWord(tuple + 32, `field[${i}].ftype`, 255);
4449
+ const op = reader.numberWord(tuple + 64, `field[${i}].op`, 255);
4450
+ const valueAt = reader.offset(tuple, tuple + 96, 128, `field[${i}].value`);
4451
+ assertCanonicalOffset(valueAt, nameEnd, `field[${i}].value`);
4452
+ const value = reader.dynamicBytes(valueAt, `field[${i}].value`);
4453
+ expectedTail = reader.dynamicEnd(valueAt, `field[${i}].value`);
4454
+ out.push({ name, ftype, op, value });
4455
+ }
4456
+ return { value: out, end: expectedTail };
4457
+ }
4458
+ function decodeRuntimeWrite(rawData) {
4459
+ const bytes = hexBytes(rawData);
4460
+ if (bytes.length < 68)
4461
+ throw new Error('Malformed EVM setDocuments calldata');
4462
+ const selector = Array.from(bytes.slice(0, 4)).map((b) => b.toString(16).padStart(2, '0')).join('');
4463
+ const kind = selector === SET_DOCUMENTS_SELECTOR
4464
+ ? 'setDocuments'
4465
+ : selector === SET_DOCUMENTS_ATTESTED_SELECTOR
4466
+ ? 'setDocumentsAttested'
4467
+ : null;
4468
+ if (!kind)
4469
+ throw new Error('EVM transaction is not a supported BoundedRuntime document write');
4470
+ const reader = new AbiReader(bytes);
4471
+ reader.maxRead = 4;
4472
+ const args = 4;
4473
+ // ExternalCallAttestation is a fully-static six-word tuple and is therefore
4474
+ // expanded inline into the top-level head: 2 dynamic offsets + 6 words.
4475
+ const headBytes = kind === 'setDocumentsAttested' ? 256 : 64;
4476
+ const appAt = reader.offset(args, args, headBytes, 'appId');
4477
+ assertCanonicalOffset(appAt, args + headBytes, 'appId');
4478
+ const opsAt = reader.offset(args, args + 32, headBytes, 'ops');
4479
+ const appId = reader.string(appAt, 'appId');
4480
+ const appEnd = reader.dynamicEnd(appAt, 'appId');
4481
+ assertCanonicalOffset(opsAt, appEnd, 'ops');
4482
+ const count = reader.numberWord(opsAt, 'ops count', MAX_ARRAY_ITEMS);
4483
+ const head = opsAt + 32;
4484
+ const min = count * 32;
4485
+ const ops = [];
4486
+ let expectedTail = head + min;
4487
+ for (let i = 0; i < count; i++) {
4488
+ const tuple = reader.offset(head, head + i * 32, min, `op[${i}]`);
4489
+ assertCanonicalOffset(tuple, expectedTail, `op[${i}]`);
4490
+ const pathAt = reader.offset(tuple, tuple, 96, `op[${i}].path`);
4491
+ assertCanonicalOffset(pathAt, tuple + 96, `op[${i}].path`);
4492
+ const deleteWord = reader.numberWord(tuple + 32, `op[${i}].isDelete`, 1);
4493
+ const fieldsAt = reader.offset(tuple, tuple + 64, 96, `op[${i}].fields`);
4494
+ const path = decodeStringArray(reader, pathAt, `op[${i}].path`);
4495
+ assertCanonicalOffset(fieldsAt, path.end, `op[${i}].fields`);
4496
+ const fields = decodeFields(reader, fieldsAt);
4497
+ expectedTail = fields.end;
4498
+ ops.push({
4499
+ pathSegments: path.value,
4500
+ isDelete: deleteWord === 1,
4501
+ fields: fields.value,
4502
+ });
4503
+ }
4504
+ let attestation;
4505
+ if (kind === 'setDocumentsAttested') {
4506
+ const callsHash = reader.word(args + 64, 'attestation.callsHash');
4507
+ const nonce = reader.word(args + 96, 'attestation.nonce');
4508
+ const deadline = reader.word(args + 128, 'attestation.deadline');
4509
+ const v = reader.numberWord(args + 160, 'attestation.v', 255);
4510
+ const r = reader.word(args + 192, 'attestation.r');
4511
+ const s = reader.word(args + 224, 'attestation.s');
4512
+ if (v !== 27 && v !== 28)
4513
+ throw new Error('Malformed EVM setDocuments calldata (attestation.v must be 27 or 28)');
4514
+ if (r === BIGINT_ZERO || r >= SECP256K1_N)
4515
+ throw new Error('Malformed EVM setDocuments calldata (attestation.r is not canonical)');
4516
+ if (s === BIGINT_ZERO || s > SECP256K1_HALF_N)
4517
+ throw new Error('Malformed EVM setDocuments calldata (attestation.s is not canonical low-s)');
4518
+ attestation = { callsHash, nonce, deadline, v, r, s };
4519
+ }
4520
+ if (expectedTail !== bytes.length)
4521
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4522
+ reader.finish();
4523
+ return Object.assign({ kind, appId, ops }, (attestation ? { attestation } : {}));
4524
+ }
4525
+ /**
4526
+ * Fail closed unless calldata is a byte-for-byte canonical ABI encoding of one
4527
+ * supported BoundedRuntime write. This is the defense used by direct signer
4528
+ * surfaces; the higher-level write validator below additionally binds app/ops.
4529
+ */
4530
+ function assertCanonicalBoundedRuntimeWriteCalldata(rawData) {
4531
+ decodeRuntimeWrite(rawData);
4532
+ }
4533
+ const MAX_EVM_DOCUMENT_PATH_UTF8_BYTES = 1024;
4534
+ function canonicalUtf8ByteLength(value) {
4535
+ let bytes = 0;
4536
+ for (let i = 0; i < value.length; i++) {
4537
+ const unit = value.charCodeAt(i);
4538
+ if (unit <= 0x7f)
4539
+ bytes += 1;
4540
+ else if (unit <= 0x7ff)
4541
+ bytes += 2;
4542
+ else if (unit >= 0xd800 && unit <= 0xdbff) {
4543
+ const low = value.charCodeAt(++i);
4544
+ if (!(low >= 0xdc00 && low <= 0xdfff))
4545
+ throw new Error(`Invalid EVM write path: ${value}`);
4546
+ bytes += 4;
4547
+ }
4548
+ else if (unit >= 0xdc00 && unit <= 0xdfff) {
4549
+ throw new Error(`Invalid EVM write path: ${value}`);
4550
+ }
4551
+ else
4552
+ bytes += 3;
4553
+ }
4554
+ return bytes;
4555
+ }
4556
+ function normalizedPathSegments(path) {
4557
+ if (typeof path !== 'string' || path.length === 0 ||
4558
+ canonicalUtf8ByteLength(path) > MAX_EVM_DOCUMENT_PATH_UTF8_BYTES) {
4559
+ throw new Error(`Invalid EVM write path: ${path}`);
4560
+ }
4561
+ const segments = path.split('/');
4562
+ if (segments.length < 2 || segments.length % 2 !== 0 ||
4563
+ segments[0].startsWith('__') ||
4564
+ segments.some((s) => !s || s === '.' || s === '..' || /[\\\u0000-\u001f\u007f]/.test(s))) {
4565
+ throw new Error(`Invalid EVM write path: ${path}`);
4566
+ }
4567
+ return segments;
4568
+ }
4569
+ function bytesBigInt(bytes, signed) {
4570
+ if (bytes.length !== 32)
4571
+ throw new Error('EVM field value is not one ABI word');
4572
+ let value = BIGINT_ZERO;
4573
+ for (const b of bytes)
4574
+ value = (value << BIGINT_EIGHT) | BigInt(b);
4575
+ if (signed && (value & (BIGINT_ONE << BigInt(255))) !== BIGINT_ZERO)
4576
+ value -= BIGINT_ONE << BigInt(256);
4577
+ return value;
4578
+ }
4579
+ function decodedAbiString(bytes) {
4580
+ const reader = new AbiReader(bytes);
4581
+ const at = reader.offset(0, 0, 32, 'field string');
4582
+ const value = reader.string(at, 'field string');
4583
+ reader.finish();
4584
+ return value;
4585
+ }
4586
+ function assertSetValue(field, expected) {
4587
+ if (field.op !== 0)
4588
+ throw new Error(`EVM field ${field.name} changes the requested SET action`);
4589
+ if (typeof expected === 'boolean') {
4590
+ if (field.ftype !== 3 || bytesBigInt(field.value, false) !== (expected ? BIGINT_ONE : BIGINT_ZERO))
4591
+ throw new Error(`EVM field ${field.name} value mismatch`);
4592
+ return;
4593
+ }
4594
+ if (typeof expected === 'number') {
4595
+ if (!Number.isFinite(expected))
4596
+ throw new Error(`EVM field ${field.name} is not finite`);
4597
+ if (Number.isInteger(expected)) {
4598
+ if (!Number.isSafeInteger(expected)) {
4599
+ throw new Error(`EVM field ${field.name} integer must be a safe number or canonical decimal string`);
4600
+ }
4601
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4602
+ if (actual === null || actual !== BigInt(expected))
4603
+ throw new Error(`EVM field ${field.name} value mismatch`);
4604
+ return;
4605
+ }
4606
+ if (field.ftype !== 4 || decodedAbiString(field.value) !== expected.toString())
4607
+ throw new Error(`EVM field ${field.name} value mismatch`);
4608
+ return;
4609
+ }
4610
+ if (typeof expected === 'bigint') {
4611
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4612
+ if (actual === null || actual !== expected)
4613
+ throw new Error(`EVM field ${field.name} value mismatch`);
4614
+ return;
4615
+ }
4616
+ if (typeof expected === 'string') {
4617
+ if (field.ftype === 4 && decodedAbiString(field.value) === expected)
4618
+ return;
4619
+ if ((field.ftype === 1 || field.ftype === 2) && expected.length <= 20 && /^-?(?:0|[1-9][0-9]*)$/.test(expected)) {
4620
+ const exact = BigInt(expected);
4621
+ if (exact >= BigInt(Number.MIN_SAFE_INTEGER) && exact <= BigInt(Number.MAX_SAFE_INTEGER)) {
4622
+ throw new Error(`EVM field ${field.name} safe integer must be supplied as a number`);
4623
+ }
4624
+ const inRange = field.ftype === 1
4625
+ ? exact >= BIGINT_ZERO && exact <= ((BIGINT_ONE << BigInt(64)) - BIGINT_ONE)
4626
+ : exact >= -(BIGINT_ONE << BigInt(63)) && exact <= ((BIGINT_ONE << BigInt(63)) - BIGINT_ONE);
4627
+ const actual = bytesBigInt(field.value, field.ftype === 2);
4628
+ if (inRange && actual === exact)
4629
+ return;
4630
+ throw new Error(`EVM field ${field.name} value mismatch`);
4631
+ }
4632
+ if (field.ftype === 5 && normalizeEvmAddress(expected)) {
4633
+ const word = field.value;
4634
+ if (word.length === 32 && word.slice(0, 12).every((b) => b === 0)) {
4635
+ const actual = `0x${Array.from(word.slice(12)).map((b) => b.toString(16).padStart(2, '0')).join('')}`;
4636
+ if (actual === normalizeEvmAddress(expected))
4637
+ return;
4638
+ }
4639
+ }
4640
+ throw new Error(`EVM field ${field.name} value mismatch`);
4641
+ }
4642
+ if (expected === undefined || expected === null) {
4643
+ throw new Error(`EVM field ${field.name} cannot be represented safely on EVM; use a document delete or omit the field`);
4644
+ }
4645
+ const json = JSON.stringify(expected);
4646
+ if (typeof json !== 'string' || field.ftype !== 4 || decodedAbiString(field.value) !== json) {
4647
+ throw new Error(`EVM field ${field.name} value mismatch`);
4648
+ }
4649
+ }
4650
+ function assertField(field, expected) {
4651
+ if (expected === null) {
4652
+ if (field.op !== 2 || field.ftype < 1 || field.ftype > 5 || field.value.length !== 0) {
4653
+ throw new Error(`EVM field ${field.name} deletion mismatch`);
4654
+ }
4655
+ return;
4656
+ }
4657
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
4658
+ const operation = expected.operation;
4659
+ if (operation === 'increment') {
4660
+ const delta = expected.value;
4661
+ const actual = field.ftype === 1 || field.ftype === 2
4662
+ ? bytesBigInt(field.value, true)
4663
+ : null;
4664
+ if (!Number.isSafeInteger(delta) || field.op !== 3 || actual === null || actual !== BigInt(delta)) {
4665
+ throw new Error(`EVM field ${field.name} increment mismatch`);
4666
+ }
4667
+ return;
4668
+ }
4669
+ if (operation === 'time' && expected.value === 'now') {
4670
+ if (field.op !== 4 || (field.ftype !== 1 && field.ftype !== 2) || field.value.length !== 0) {
4671
+ throw new Error(`EVM field ${field.name} timestamp mismatch`);
4672
+ }
4673
+ return;
4674
+ }
4675
+ }
4676
+ assertSetValue(field, expected);
4677
+ }
4678
+ function assertOp(decoded, path, document) {
4679
+ const expectedSegments = normalizedPathSegments(path);
4680
+ if (decoded.pathSegments.length !== expectedSegments.length || decoded.pathSegments.some((s, i) => s !== expectedSegments[i])) {
4681
+ throw new Error(`EVM transaction path does not match requested write: ${path}`);
4682
+ }
4683
+ if (decoded.isDelete !== (document === null))
4684
+ throw new Error(`EVM transaction action does not match requested write: ${path}`);
4685
+ if (document === null) {
4686
+ if (decoded.fields.length !== 0)
4687
+ throw new Error(`EVM delete unexpectedly carries fields: ${path}`);
4688
+ return;
4689
+ }
4690
+ if (!document || typeof document !== 'object' || Array.isArray(document))
4691
+ throw new Error(`EVM document must be a plain object or null: ${path}`);
4692
+ const expectedEntries = Object.entries(document);
4693
+ if (expectedEntries.some(([name]) => name.startsWith('_')))
4694
+ throw new Error(`Reserved EVM field reached transaction validation: ${path}`);
4695
+ const expected = new Map(expectedEntries);
4696
+ const seen = new Set();
4697
+ for (const field of decoded.fields) {
4698
+ if (!field.name || seen.has(field.name) || !expected.has(field.name))
4699
+ throw new Error(`EVM transaction contains unexpected or duplicate field ${field.name}`);
4700
+ seen.add(field.name);
4701
+ assertField(field, expected.get(field.name));
4702
+ }
4703
+ const missing = [...expected.keys()].filter((name) => !seen.has(name));
4704
+ if (missing.length > 0)
4705
+ throw new Error(`EVM transaction omits requested fields: ${missing.join(', ')}`);
4706
+ }
4707
+ function zeroWei(raw) {
4708
+ if (raw === undefined || raw === null || raw === '')
4709
+ return true;
4710
+ if (typeof raw !== 'string' && typeof raw !== 'number')
4711
+ return false;
4712
+ try {
4713
+ return BigInt(raw) === BIGINT_ZERO;
4714
+ }
4715
+ catch (_a) {
4716
+ return false;
4717
+ }
4718
+ }
4719
+ /** Verify the untrusted 202 payload is exactly the governed write the app asked for. */
4720
+ function assertEvmTransactionMatchesWrite(transaction, context) {
4721
+ var _a, _b;
4722
+ if (!transaction || !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0) {
4723
+ throw new Error('EVM transaction chainId is required');
4724
+ }
4725
+ const chainId = transaction.chainId;
4726
+ const policy = context.policy;
4727
+ if (!policy)
4728
+ throw new Error('EVM transaction signing requires an explicit runtime/chain policy');
4729
+ if (policy.chainId !== undefined && policy.chainId !== chainId)
4730
+ throw new Error(`EVM transaction chain ${chainId} does not match configured chain ${policy.chainId}`);
4731
+ const expectedRuntime = (_b = (_a = policy.runtimeAddresses) === null || _a === void 0 ? void 0 : _a[String(chainId)]) !== null && _b !== void 0 ? _b : (policy.chainId === chainId ? policy.runtimeAddress : undefined);
4732
+ const normalizedRuntime = normalizeEvmAddress(expectedRuntime);
4733
+ if (!normalizedRuntime)
4734
+ throw new Error(`No trusted BoundedRuntime address configured for EVM chain ${chainId}`);
4735
+ if (normalizeEvmAddress(transaction.to) !== normalizedRuntime)
4736
+ throw new Error('EVM transaction target is not the configured BoundedRuntime');
4737
+ if (!zeroWei(transaction.value))
4738
+ throw new Error('BoundedRuntime.setDocuments must not transfer native value');
4739
+ if (transaction.nonce !== undefined)
4740
+ throw new Error('Server-supplied EVM nonce is not accepted');
4741
+ const decoded = decodeRuntimeWrite(transaction.data);
4742
+ if (decoded.appId !== context.appId)
4743
+ throw new Error('EVM transaction appId does not match configured appId');
4744
+ if (decoded.ops.length !== context.documents.length)
4745
+ throw new Error('EVM transaction operation count does not match requested writes');
4746
+ // The worker's contract ABI groups upserts first and document deletes second.
4747
+ const expectedOps = [
4748
+ ...context.documents.filter((entry) => entry.document !== null),
4749
+ ...context.documents.filter((entry) => entry.document === null),
4750
+ ];
4751
+ const paths = new Set();
4752
+ for (let i = 0; i < expectedOps.length; i++) {
4753
+ const expected = expectedOps[i];
4754
+ const canonicalPath = normalizedPathSegments(expected.destinationPath).join('/');
4755
+ if (paths.has(canonicalPath))
4756
+ throw new Error(`Duplicate EVM write path is not allowed: ${canonicalPath}`);
4757
+ paths.add(canonicalPath);
4758
+ assertOp(decoded.ops[i], canonicalPath, expected.document);
4759
+ }
4760
+ }
4761
+
4132
4762
  var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
4133
4763
  var t = {};
4134
4764
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -4155,12 +4785,21 @@ class InsufficientBalanceError extends Error {
4155
4785
  this.deficitSol = deficitSol;
4156
4786
  }
4157
4787
  }
4158
- // Cache for get responses
4159
4788
  const getCache = {};
4160
- // Track in-flight requests to coalesce multiple identical requests
4161
4789
  const pendingRequests = {};
4162
4790
  // Default cache TTL (500ms = 0.5 seconds)
4163
4791
  const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
4792
+ function getReadCacheEntry(cacheKey, principalValue) {
4793
+ var _a;
4794
+ return (_a = getCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.find(entry => entry.principalValue === principalValue);
4795
+ }
4796
+ function setReadCacheEntry(cacheKey, principalValue, data, expiresAt) {
4797
+ var _a;
4798
+ const bucket = (_a = getCache[cacheKey]) !== null && _a !== void 0 ? _a : [];
4799
+ const reusableEntries = bucket.filter(entry => (entry.principalValue !== principalValue && entry.expiresAt >= Date.now()));
4800
+ reusableEntries.push({ data, expiresAt, principalValue });
4801
+ getCache[cacheKey] = reusableEntries;
4802
+ }
4164
4803
  // Last time we cleaned up the cache
4165
4804
  let lastCacheCleanup = Date.now();
4166
4805
  let uncacheableReadKeyCounter = 0;
@@ -4256,6 +4895,21 @@ function hashForKey$1(value) {
4256
4895
  }
4257
4896
  return h.toString(36);
4258
4897
  }
4898
+ // Memoize the compact principal hash per exact credential string. Every read
4899
+ // hashed the entire (~1KB) bearer/idToken on every call; a session presents
4900
+ // the same handful of credentials for its lifetime. Bounded size — a rotation
4901
+ // simply inserts the new token and stale entries fall off on clear.
4902
+ const principalHashCache = new Map();
4903
+ function hashForKeyCached(value) {
4904
+ const hit = principalHashCache.get(value);
4905
+ if (hit !== undefined)
4906
+ return hit;
4907
+ const h = hashForKey$1(value);
4908
+ if (principalHashCache.size >= 50)
4909
+ principalHashCache.clear();
4910
+ principalHashCache.set(value, h);
4911
+ return h;
4912
+ }
4259
4913
  function hasAuthHeader(headers) {
4260
4914
  if (!headers)
4261
4915
  return false;
@@ -4266,10 +4920,10 @@ function authValueFromHeaders(headers) {
4266
4920
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4267
4921
  }
4268
4922
  function principalFromAuthValue(authValue) {
4269
- return authValue ? `h${hashForKey$1(authValue)}` : null;
4923
+ return authValue ? `h${hashForKeyCached(authValue)}` : null;
4270
4924
  }
4271
4925
  function principalFromIdToken$1(idToken) {
4272
- return idToken ? `t${hashForKey$1(idToken)}` : null;
4926
+ return idToken ? `t${hashForKeyCached(idToken)}` : null;
4273
4927
  }
4274
4928
  function uncacheableReadKey(appId, scope) {
4275
4929
  uncacheableReadKeyCounter += 1;
@@ -4291,32 +4945,34 @@ async function getReadPrincipalKey(overrides) {
4291
4945
  // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4292
4946
  // caller-supplied Authorization is the real request auth when present.
4293
4947
  if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
4294
- const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
4948
+ const authValue = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4949
+ const principal = principalFromAuthValue(authValue);
4295
4950
  return principal
4296
- ? { key: `${appId}:${principal}`, cacheable: true }
4297
- : { key: uncacheableReadKey(appId, 'h'), cacheable: false };
4951
+ ? { key: `${appId}:${principal}`, cacheable: true, principalValue: authValue }
4952
+ : { key: uncacheableReadKey(appId, 'h'), cacheable: false, principalValue: null };
4298
4953
  }
4299
4954
  // Per-request auth-header override (wallet client). Key by the exact opaque
4300
4955
  // header it produces, not decoded claims or the unverified _walletAddress hint.
4301
4956
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4302
4957
  try {
4303
4958
  const headers = await overrides._getAuthHeaders();
4304
- const principal = principalFromAuthValue(authValueFromHeaders(headers));
4959
+ const authValue = authValueFromHeaders(headers);
4960
+ const principal = principalFromAuthValue(authValue);
4305
4961
  return principal
4306
- ? { key: `${appId}:o${principal}`, cacheable: true }
4307
- : { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4962
+ ? { key: `${appId}:o${principal}`, cacheable: true, principalValue: authValue }
4963
+ : { key: uncacheableReadKey(appId, 'o'), cacheable: false, principalValue: null };
4308
4964
  }
4309
4965
  catch (_a) {
4310
4966
  // If we can't resolve the override identity, do not read/write cache.
4311
- return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4967
+ return { key: uncacheableReadKey(appId, 'o'), cacheable: false, principalValue: null };
4312
4968
  }
4313
4969
  }
4314
4970
  // Ambient session principal.
4315
4971
  const idToken = await getIdToken(config.isServer);
4316
4972
  const principal = principalFromIdToken$1(idToken);
4317
4973
  return principal
4318
- ? { key: `${appId}:${principal}`, cacheable: true }
4319
- : { key: uncacheableReadKey(appId, 'a'), cacheable: false };
4974
+ ? { key: `${appId}:${principal}`, cacheable: true, principalValue: idToken !== null && idToken !== void 0 ? idToken : null }
4975
+ : { key: uncacheableReadKey(appId, 'a'), cacheable: false, principalValue: null };
4320
4976
  }
4321
4977
  /**
4322
4978
  * Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
@@ -4597,6 +5253,7 @@ async function search(path, query, opts = {}) {
4597
5253
  return response.data;
4598
5254
  }
4599
5255
  async function get(path, opts = {}) {
5256
+ var _a, _b;
4600
5257
  try {
4601
5258
  let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
4602
5259
  // Remove last '*' if it exists in the path
@@ -4607,7 +5264,8 @@ async function get(path, opts = {}) {
4607
5264
  throw new Error("Invalid path provided.");
4608
5265
  }
4609
5266
  // Create cache key combining path, prompt, filter, sort, includeSubPaths,
4610
- // shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
5267
+ // shape, limit, cursor — and (H1) the caller's appId + compact principal
5268
+ // bucket. Exact opaque credential equality is checked inside that bucket,
4611
5269
  // so a private read cached for one user is never served to another in a
4612
5270
  // shared process / SSR worker / browser login-switch. The cache is opt-in
4613
5271
  // and disabled for no-auth reads, which get an uncacheable unique key.
@@ -4622,20 +5280,25 @@ async function get(path, opts = {}) {
4622
5280
  const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4623
5281
  const now = Date.now();
4624
5282
  // Check for valid cache entry when the caller explicitly opted in.
4625
- if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4626
- return getCache[cacheKey].data;
5283
+ const cachedEntry = getReadCacheEntry(cacheKey, principalKey.principalValue);
5284
+ if (cacheEnabled &&
5285
+ cachedEntry &&
5286
+ now < cachedEntry.expiresAt) {
5287
+ return cachedEntry.data;
4627
5288
  }
4628
5289
  // If we're bypassing cache, we should still coalesce identical requests
4629
5290
  // but need a different key to avoid interfering with regular requests
4630
5291
  const requestKey = opts.bypassCache ? `bypass:${cacheKey}` : cacheKey;
4631
- // Check if there's already an in-flight request for this key
4632
- // Fix for TS error - use hasOwnProperty to explicitly check if key exists
4633
- if (Object.prototype.hasOwnProperty.call(pendingRequests, requestKey)) {
4634
- // Return the existing promise to coalesce requests
4635
- return pendingRequests[requestKey];
5292
+ // The compact principal hash is only an index bucket. Coalesce only when
5293
+ // the exact opaque credential also matches, so collisions stay isolated.
5294
+ const existingPending = (_a = pendingRequests[requestKey]) === null || _a === void 0 ? void 0 : _a.find(entry => entry.principalValue === principalKey.principalValue);
5295
+ if (existingPending) {
5296
+ return existingPending.promise;
4636
5297
  }
4637
5298
  // Create a new request promise and store it
4638
- const requestPromise = (async () => {
5299
+ let requestPromise;
5300
+ requestPromise = (async () => {
5301
+ var _a;
4639
5302
  try {
4640
5303
  const pathIsDocument = normalizedPath.split("/").length % 2 === 0;
4641
5304
  // Cache miss or bypass - proceed with HTTP API request
@@ -4666,10 +5329,7 @@ async function get(path, opts = {}) {
4666
5329
  const responseData = normalizeReadResult(response.data, pathIsDocument);
4667
5330
  // Cache the response only when explicitly requested and principal-bound.
4668
5331
  if (cacheEnabled) {
4669
- getCache[cacheKey] = {
4670
- data: responseData,
4671
- expiresAt: now + GET_CACHE_TTL
4672
- };
5332
+ setReadCacheEntry(cacheKey, principalKey.principalValue, responseData, now + GET_CACHE_TTL);
4673
5333
  // Periodically clean up expired cache entries (every 5 seconds)
4674
5334
  if (now - lastCacheCleanup > 5000) {
4675
5335
  cleanupExpiredCache();
@@ -4680,12 +5340,21 @@ async function get(path, opts = {}) {
4680
5340
  return responseData;
4681
5341
  }
4682
5342
  finally {
4683
- // Remove this request from pendingRequests regardless of success/failure
4684
- delete pendingRequests[requestKey];
5343
+ // Remove only this exact request. A colliding principal can legitimately
5344
+ // have another request in the same compact-hash bucket.
5345
+ const bucket = pendingRequests[requestKey];
5346
+ const index = (_a = bucket === null || bucket === void 0 ? void 0 : bucket.findIndex(entry => entry.promise === requestPromise)) !== null && _a !== void 0 ? _a : -1;
5347
+ if (index >= 0)
5348
+ bucket.splice(index, 1);
5349
+ if ((bucket === null || bucket === void 0 ? void 0 : bucket.length) === 0)
5350
+ delete pendingRequests[requestKey];
4685
5351
  }
4686
5352
  })();
4687
- // Store the promise so other calls can join it
4688
- pendingRequests[requestKey] = requestPromise;
5353
+ // Store the promise so only callers with the exact same credential join it.
5354
+ ((_b = pendingRequests[requestKey]) !== null && _b !== void 0 ? _b : (pendingRequests[requestKey] = [])).push({
5355
+ principalValue: principalKey.principalValue,
5356
+ promise: requestPromise,
5357
+ });
4689
5358
  // Return the promise
4690
5359
  return requestPromise;
4691
5360
  }
@@ -4697,9 +5366,12 @@ async function get(path, opts = {}) {
4697
5366
  function cleanupExpiredCache() {
4698
5367
  const now = Date.now();
4699
5368
  Object.keys(getCache).forEach(key => {
4700
- if (getCache[key] && getCache[key].expiresAt < now) {
5369
+ var _a, _b;
5370
+ const activeEntries = (_b = (_a = getCache[key]) === null || _a === void 0 ? void 0 : _a.filter(entry => entry.expiresAt >= now)) !== null && _b !== void 0 ? _b : [];
5371
+ if (activeEntries.length > 0)
5372
+ getCache[key] = activeEntries;
5373
+ else
4701
5374
  delete getCache[key];
4702
- }
4703
5375
  });
4704
5376
  lastCacheCleanup = now;
4705
5377
  }
@@ -4961,8 +5633,11 @@ async function getMany(paths, opts = {}) {
4961
5633
  for (let i = 0; i < normalizedPaths.length; i++) {
4962
5634
  const normalizedPath = normalizedPaths[i];
4963
5635
  const cacheKey = `${principalKey.key}|${normalizedPath}:`;
4964
- if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4965
- results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
5636
+ const cachedEntry = getReadCacheEntry(cacheKey, principalKey.principalValue);
5637
+ if (cacheEnabled &&
5638
+ cachedEntry &&
5639
+ now < cachedEntry.expiresAt) {
5640
+ results[i] = { path: normalizedPath, data: cachedEntry.data };
4966
5641
  }
4967
5642
  else {
4968
5643
  uncachedIndices.push(i);
@@ -4999,10 +5674,7 @@ async function getMany(paths, opts = {}) {
4999
5674
  results[originalIndex] = normalizedResult;
5000
5675
  if (!normalizedResult.error && cacheEnabled) {
5001
5676
  const cacheKey = `${principalKey.key}|${normalizedPath}:`;
5002
- getCache[cacheKey] = {
5003
- data: normalizedResult.data,
5004
- expiresAt: now + GET_CACHE_TTL
5005
- };
5677
+ setReadCacheEntry(cacheKey, principalKey.principalValue, normalizedResult.data, now + GET_CACHE_TTL);
5006
5678
  }
5007
5679
  }
5008
5680
  else {
@@ -5132,13 +5804,12 @@ async function runExpressionMany(many) {
5132
5804
  * collection's policy `delete` rule and broadcasts a delete to subscribers.
5133
5805
  */
5134
5806
  async function set(path, document, options) {
5135
- const result = await setMany([{ path, document }], options);
5136
- // Clear cache entries that might be affected by this update
5137
- clearCacheByPrefix(path);
5138
- return result;
5807
+ // setMany already invalidates the read cache for the affected document and
5808
+ // collection paths — no second sweep needed here.
5809
+ return setMany([{ path, document }], options);
5139
5810
  }
5140
5811
  async function setMany(many, options) {
5141
- var _a, _b, _c, _d, _e, _f, _g;
5812
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5142
5813
  // Returns the data that was set, or undefined if the document was already set.
5143
5814
  try {
5144
5815
  const config = await getConfig();
@@ -5194,6 +5865,9 @@ async function setMany(many, options) {
5194
5865
  // the API did nothing and gave us the required info to run this transaction.
5195
5866
  const { transactions, offchainTransaction } = setResponse.data;
5196
5867
  // Handle offchain transaction flow (for 'offchain' protocol apps)
5868
+ if (offchainTransaction && Array.isArray(transactions) && transactions.length > 0) {
5869
+ throw new Error('Ambiguous write response mixes offchain and onchain transaction targets');
5870
+ }
5197
5871
  if (offchainTransaction) {
5198
5872
  const transactionResult = await handleOffchainTransaction(offchainTransaction, authProvider, options);
5199
5873
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
@@ -5203,9 +5877,35 @@ async function setMany(many, options) {
5203
5877
  throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
5204
5878
  }
5205
5879
  const curTx = transactions[0];
5880
+ const hasEvmTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.evmTransaction) != null;
5881
+ const hasSerializedSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.serializedTransaction) != null;
5882
+ const hasBuiltSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.transactionArgs) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.remainingAccounts) != null ||
5883
+ (curTx === null || curTx === void 0 ? void 0 : curTx.idl) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.txData) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.preInstructions) != null;
5884
+ if ((hasEvmTarget && (hasSerializedSolanaTarget || hasBuiltSolanaTarget)) ||
5885
+ (hasSerializedSolanaTarget && hasBuiltSolanaTarget)) {
5886
+ throw new Error('Ambiguous on-chain write response mixes EVM and/or Solana transaction target formats');
5887
+ }
5206
5888
  let transactionResult;
5207
5889
  const expectedWritePaths = documents.map(d => d.destinationPath);
5208
- if (curTx.serializedTransaction) {
5890
+ // EVM onchain write: the worker returns a PRE-BUILT EIP-1559 tx on
5891
+ // `transactions[0].evmTransaction` (the EVM twin of Solana's pre-built
5892
+ // `serializedTransaction`). WORKER 202 CONTRACT (field name + shape the
5893
+ // worker side must emit to match):
5894
+ // { transactions: [ { evmTransaction: {
5895
+ // to, data, value?, chainId, gas?, maxFeePerGas?, maxPriorityFeePerGas?, nonce?
5896
+ // } } ] }
5897
+ // We hand it to the EVM auth provider's runTransaction(evmTx, undefined, options)
5898
+ // (eth_sendTransaction — the wallet signs + submits). syncItems below is
5899
+ // identical to the Solana path.
5900
+ if (curTx.evmTransaction) {
5901
+ assertEvmTransactionMatchesWrite(curTx.evmTransaction, {
5902
+ appId: config.appId,
5903
+ documents,
5904
+ policy: (_k = (_j = (_h = options === null || options === void 0 ? void 0 : options._overrides) === null || _h === void 0 ? void 0 : _h._evmTransactionPolicy) !== null && _j !== void 0 ? _j : config.evmWalletConfig) !== null && _k !== void 0 ? _k : null,
5905
+ });
5906
+ transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
5907
+ }
5908
+ else if (curTx.serializedTransaction) {
5209
5909
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5210
5910
  }
5211
5911
  else {
@@ -5229,7 +5929,7 @@ async function setMany(many, options) {
5229
5929
  else if (setResponse.data &&
5230
5930
  typeof setResponse.data === 'object' &&
5231
5931
  setResponse.data.success === true) {
5232
- const _h = setResponse.data, { success: _success } = _h, rest = __rest$1(_h, ["success"]);
5932
+ const _l = setResponse.data, { success: _success } = _l, rest = __rest$1(_l, ["success"]);
5233
5933
  return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
5234
5934
  }
5235
5935
  else {
@@ -5329,6 +6029,14 @@ async function setMany(many, options) {
5329
6029
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5330
6030
  return transactionResult;
5331
6031
  }
6032
+ // EVM onchain write: hand the worker's pre-built EIP-1559 tx to the EVM auth
6033
+ // provider, which eth_sendTransaction-s it (the wallet signs + submits in one
6034
+ // step). Mirrors handlePreBuiltTransaction's shape but routes through the EVM
6035
+ // slot of runTransaction (Solana slot undefined). shouldSubmitTx is honored by
6036
+ // the provider itself.
6037
+ async function handleEvmTransaction(evmTransaction, authProvider, options) {
6038
+ return authProvider.runTransaction(evmTransaction, undefined, options);
6039
+ }
5332
6040
  async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5333
6041
  var _a, _b, _c;
5334
6042
  const config = await getConfig();
@@ -5569,10 +6277,37 @@ var operations = /*#__PURE__*/Object.freeze({
5569
6277
  // ============ Global State ============
5570
6278
  // One connection per routing target (appId, or appId#room#<collection>/<roomId>)
5571
6279
  const connections = new Map();
6280
+ // Coalesce concurrent first-use/auth-preflight calls for the same routing target.
6281
+ // React commonly mounts several subscriptions in one render; without this map,
6282
+ // every call can pass the empty `connections` check, await auth, and create an
6283
+ // orphan ReconnectingWebSocket before the first call publishes its connection.
6284
+ // Keyed by the identity-bucket lease key (see connectionLeaseKey) so two distinct
6285
+ // wallet tokens never share one in-flight socket even under a compact-hash collision.
6286
+ const connectionFlights = new Map();
6287
+ // A subscriber holds a routing-target lease from connection acquisition until
6288
+ // its callback/subscription is registered. The last old-route unsubscribe must
6289
+ // not close a connection already captured by a new route awaiting auth. Keyed by
6290
+ // the same identity-bucket lease key as connectionFlights.
6291
+ const connectionAcquisitions = new Map();
6292
+ // Compact identity hashes remain useful bounded-size indexes, but collisions
6293
+ // share a bucket instead of sharing authority. Each exact bearer keeps its own
6294
+ // cache entry so collision isolation does not evict another principal's cache.
5572
6295
  const responseCache = new Map();
5573
6296
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
5574
6297
  const TOKEN_REFRESH_BUFFER = 10 * 60 * 1000; // Refresh token 10 minutes before expiry
5575
6298
  const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; // Check token freshness every 5 minutes
6299
+ function getResponseCacheEntry(cacheKey, identityToken) {
6300
+ var _a;
6301
+ return (_a = responseCache.get(cacheKey)) === null || _a === void 0 ? void 0 : _a.find(entry => entry.identityToken === identityToken);
6302
+ }
6303
+ function setResponseCacheEntry(cacheKey, identityToken, data) {
6304
+ var _a;
6305
+ const now = Date.now();
6306
+ const bucket = (_a = responseCache.get(cacheKey)) !== null && _a !== void 0 ? _a : [];
6307
+ const reusableEntries = bucket.filter(entry => (entry.identityToken !== identityToken && now - entry.timestamp < CACHE_TTL));
6308
+ reusableEntries.push({ data, timestamp: now, identityToken });
6309
+ responseCache.set(cacheKey, reusableEntries);
6310
+ }
5576
6311
  // ============ WebSocket Config ============
5577
6312
  const BASE_MIN_RECONNECT_DELAY_MS = 1000;
5578
6313
  const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
@@ -5580,6 +6315,10 @@ const MAX_RECONNECT_DELAY_MS = 300000;
5580
6315
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
5581
6316
  const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
5582
6317
  const WS_AUTH_EXPIRED_CODE = 'auth_expired';
6318
+ // React runs old-route effect cleanup before new-route effects. Keep an
6319
+ // otherwise-idle socket for one short handoff window so the new route can
6320
+ // cancel teardown and reuse it; genuinely idle sockets still close promptly.
6321
+ const IDLE_CONNECTION_CLOSE_GRACE_MS = 1000;
5583
6322
  const WS_CONFIG = {
5584
6323
  // Keep retrying indefinitely so long outages recover without page refresh.
5585
6324
  maxRetries: Infinity,
@@ -5610,12 +6349,12 @@ function hashForKey(value) {
5610
6349
  }
5611
6350
  /**
5612
6351
  * SECURITY (H1): Response-cache keys must be scoped to the caller's identity, not
5613
- * just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
5614
- * a private subscription snapshot cached for one user from being delivered to a
5615
- * different user who subscribes with the same options (shared process / SSR worker
5616
- * / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
5617
- * before entries are read or written; no-auth subscriptions never populate an
5618
- * implicit anonymous cache bucket.
6352
+ * just the path/filter/shape. The compact `identity` prefix selects a bucket;
6353
+ * exact opaque bearer equality is required inside it before reuse. Together they
6354
+ * keep a private snapshot from being delivered to a different user with the same
6355
+ * options (shared process / SSR worker / browser login-switch; this cache has a
6356
+ * 5-minute TTL). Callers must opt in before entries are read or written; no-auth
6357
+ * subscriptions never populate an implicit anonymous cache bucket.
5619
6358
  */
5620
6359
  function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5621
6360
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
@@ -5653,16 +6392,16 @@ function livePrincipalFromIdToken(idToken) {
5653
6392
  }
5654
6393
  function authSnapshotFromToken(idToken) {
5655
6394
  return {
5656
- tokenFingerprint: principalFromIdToken(idToken),
6395
+ tokenValue: idToken !== null && idToken !== void 0 ? idToken : null,
5657
6396
  principal: livePrincipalFromIdToken(idToken),
5658
6397
  };
5659
6398
  }
5660
6399
  function connectionMatchesAuthToken(connection, idToken) {
5661
6400
  const current = authSnapshotFromToken(idToken);
5662
- if (!current.tokenFingerprint) {
5663
- return connection.authTokenFingerprint === null && connection.authPrincipal === null;
6401
+ if (!current.tokenValue) {
6402
+ return connection.authTokenValue === null && connection.authPrincipal === null;
5664
6403
  }
5665
- if (connection.authTokenFingerprint && connection.authTokenFingerprint === current.tokenFingerprint) {
6404
+ if (connection.authTokenValue && connection.authTokenValue === current.tokenValue) {
5666
6405
  return true;
5667
6406
  }
5668
6407
  return !!connection.authPrincipal && !!current.principal && connection.authPrincipal === current.principal;
@@ -5671,7 +6410,7 @@ async function canSendAuthenticatedLiveIntent(connection, isServer) {
5671
6410
  const currentToken = await getIdToken(isServer);
5672
6411
  if (!currentToken)
5673
6412
  return false;
5674
- if (!connection.authTokenFingerprint && !connection.authPrincipal)
6413
+ if (!connection.authTokenValue && !connection.authPrincipal)
5675
6414
  return false;
5676
6415
  return connectionMatchesAuthToken(connection, currentToken);
5677
6416
  }
@@ -5684,19 +6423,19 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5684
6423
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
5685
6424
  const principal = principalFromIdToken(bearer);
5686
6425
  return principal
5687
- ? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
5688
- : { key: `${effectiveAppId}:oanon`, cacheable: false };
6426
+ ? { key: `${effectiveAppId}:o${principal}`, cacheable: true, token: bearer }
6427
+ : { key: `${effectiveAppId}:oanon`, cacheable: false, token: null };
5689
6428
  }
5690
6429
  catch (_a) {
5691
6430
  // Couldn't resolve the override identity — do not use response cache.
5692
- return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
6431
+ return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false, token: null };
5693
6432
  }
5694
6433
  }
5695
6434
  const idToken = await getIdToken(isServer);
5696
6435
  const principal = principalFromIdToken(idToken);
5697
6436
  return principal
5698
- ? { key: `${effectiveAppId}:${principal}`, cacheable: true }
5699
- : { key: `${effectiveAppId}:anon`, cacheable: false };
6437
+ ? { key: `${effectiveAppId}:${principal}`, cacheable: true, token: idToken !== null && idToken !== void 0 ? idToken : null }
6438
+ : { key: `${effectiveAppId}:anon`, cacheable: false, token: null };
5700
6439
  }
5701
6440
  /** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
5702
6441
  function bearerFromAuthHeaders(headers) {
@@ -5752,7 +6491,7 @@ function failConnectionAuth(connection, error) {
5752
6491
  connection.authFailure = error;
5753
6492
  connection.pendingAuthToken = null;
5754
6493
  connection.pendingAuthSnapshot = null;
5755
- connection.authTokenFingerprint = null;
6494
+ connection.authTokenValue = null;
5756
6495
  connection.authPrincipal = null;
5757
6496
  connection.isConnecting = false;
5758
6497
  connection.isConnected = false;
@@ -5763,6 +6502,7 @@ function failConnectionAuth(connection, error) {
5763
6502
  connection.pendingSubscriptions.clear();
5764
6503
  for (const subscription of connection.subscriptions.values()) {
5765
6504
  notifyErrorCallbacks(subscription, error);
6505
+ rejectSubscriptionRefresh(subscription, error);
5766
6506
  }
5767
6507
  }
5768
6508
  function scheduleTokenRefresh(connection, isServer) {
@@ -5955,7 +6695,136 @@ function replaySubscriptions(connection) {
5955
6695
  sendSubscribe(connection, sub);
5956
6696
  }
5957
6697
  }
5958
- async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey) {
6698
+ /**
6699
+ * Derive the stable identity-bucket lease key used to coalesce (singleflight)
6700
+ * and refcount (acquisition / idle-close) connection use. Ambient app/room
6701
+ * connections key by their routing target; wallet-scoped connections pin the
6702
+ * exact bearer so two distinct tokens never share a flight or a lease, even
6703
+ * when their compact `#id#<principalKey>` hashes collide.
6704
+ */
6705
+ function connectionLeaseKey(appId, routePath, principalKey, principalToken) {
6706
+ const roomKey = roomKeyFromRoutePath(routePath);
6707
+ const base = roomKey ? `${appId}#room#${roomKey}` : appId;
6708
+ if (!principalKey)
6709
+ return base;
6710
+ const bucket = `${base}#id#${principalKey}`;
6711
+ return principalToken ? `${bucket}#tok#${principalToken}` : bucket;
6712
+ }
6713
+ function cancelIdleConnectionClose(connection) {
6714
+ if (!connection.idleCloseTimer)
6715
+ return;
6716
+ clearTimeout(connection.idleCloseTimer);
6717
+ connection.idleCloseTimer = null;
6718
+ }
6719
+ function closeConnectionNow(connection) {
6720
+ if (connections.get(connection.key) !== connection)
6721
+ return;
6722
+ if (!connection.ws)
6723
+ return;
6724
+ cancelIdleConnectionClose(connection);
6725
+ if (connection.tokenRefreshTimer) {
6726
+ clearInterval(connection.tokenRefreshTimer);
6727
+ connection.tokenRefreshTimer = null;
6728
+ }
6729
+ const ws = connection.ws;
6730
+ connection.ws = null;
6731
+ connection.isConnecting = false;
6732
+ connection.isConnected = false;
6733
+ connection.isAuthenticating = false;
6734
+ const closeError = new Error('WebSocket connection closed');
6735
+ for (const subscription of connection.subscriptions.values()) {
6736
+ rejectSubscriptionRefresh(subscription, closeError);
6737
+ }
6738
+ connections.delete(connection.key);
6739
+ try {
6740
+ ws.close();
6741
+ }
6742
+ catch ( /* already closing */_a) { /* already closing */ }
6743
+ }
6744
+ function scheduleConnectionCloseIfIdle(connection) {
6745
+ var _a;
6746
+ if (connections.get(connection.key) !== connection)
6747
+ return;
6748
+ if (connection.subscriptions.size > 0)
6749
+ return;
6750
+ if (((_a = connectionAcquisitions.get(connection.acquisitionKey)) !== null && _a !== void 0 ? _a : 0) > 0)
6751
+ return;
6752
+ if (!connection.ws || connection.idleCloseTimer)
6753
+ return;
6754
+ connection.idleCloseTimer = setTimeout(() => {
6755
+ var _a;
6756
+ connection.idleCloseTimer = null;
6757
+ if (connection.subscriptions.size > 0)
6758
+ return;
6759
+ if (((_a = connectionAcquisitions.get(connection.acquisitionKey)) !== null && _a !== void 0 ? _a : 0) > 0)
6760
+ return;
6761
+ closeConnectionNow(connection);
6762
+ }, IDLE_CONNECTION_CLOSE_GRACE_MS);
6763
+ }
6764
+ /**
6765
+ * Acquire a connection under a routing-target lease. The lease is refcounted on
6766
+ * the identity-bucket key and held across the caller's auth preflight so a
6767
+ * concurrent old-route teardown cannot close a socket a new route is about to
6768
+ * reuse. The lease is released once the caller has registered its
6769
+ * subscription/callback.
6770
+ */
6771
+ async function acquireConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken) {
6772
+ var _a;
6773
+ const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken);
6774
+ connectionAcquisitions.set(leaseKey, ((_a = connectionAcquisitions.get(leaseKey)) !== null && _a !== void 0 ? _a : 0) + 1);
6775
+ let released = false;
6776
+ let releaseTimer = null;
6777
+ const release = (connection) => {
6778
+ var _a;
6779
+ if (released)
6780
+ return;
6781
+ released = true;
6782
+ if (releaseTimer) {
6783
+ clearTimeout(releaseTimer);
6784
+ releaseTimer = null;
6785
+ }
6786
+ const remaining = ((_a = connectionAcquisitions.get(leaseKey)) !== null && _a !== void 0 ? _a : 1) - 1;
6787
+ if (remaining > 0)
6788
+ connectionAcquisitions.set(leaseKey, remaining);
6789
+ else
6790
+ connectionAcquisitions.delete(leaseKey);
6791
+ if (connection)
6792
+ scheduleConnectionCloseIfIdle(connection);
6793
+ };
6794
+ try {
6795
+ const connection = await getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken);
6796
+ const releaseConnection = () => release(connection);
6797
+ // Registration is synchronous after the caller's await. This fail-safe
6798
+ // releases on the next task even if an option getter/callback throws
6799
+ // before subscribeV2 reaches its explicit release path.
6800
+ releaseTimer = setTimeout(releaseConnection, 0);
6801
+ return { connection, release: releaseConnection };
6802
+ }
6803
+ catch (error) {
6804
+ release();
6805
+ throw error;
6806
+ }
6807
+ }
6808
+ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken) {
6809
+ // Singleflight: concurrent first-use of the same routing target shares one
6810
+ // in-flight connection rather than each creating an orphan socket. Keyed by
6811
+ // the identity-bucket lease key so distinct wallet tokens never coalesce.
6812
+ const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken);
6813
+ const activeFlight = connectionFlights.get(leaseKey);
6814
+ if (activeFlight)
6815
+ return activeFlight;
6816
+ const flight = getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey);
6817
+ connectionFlights.set(leaseKey, flight);
6818
+ try {
6819
+ return await flight;
6820
+ }
6821
+ finally {
6822
+ if (connectionFlights.get(leaseKey) === flight) {
6823
+ connectionFlights.delete(leaseKey);
6824
+ }
6825
+ }
6826
+ }
6827
+ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey) {
5959
6828
  attachBrowserReconnectHooksOnce();
5960
6829
  // A per-room subscription gets its OWN connection routed to the room DO; all
5961
6830
  // other subscriptions share the app-level connection (project DO). The worker
@@ -5966,9 +6835,38 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5966
6835
  // keyed by the wallet identity, so its WS authenticates as that wallet and
5967
6836
  // never shares a connection (or token) with any top-level server caller.
5968
6837
  const base = roomKey ? `${appId}#room#${roomKey}` : appId;
5969
- const connKey = principalKey ? `${base}#id#${principalKey}` : base;
5970
- let connection = connections.get(connKey);
6838
+ const identityBucketKey = principalKey
6839
+ ? `${base}#id#${principalKey}`
6840
+ : base;
6841
+ let connKey = identityBucketKey;
6842
+ let connection;
6843
+ if (principalKey && principalToken) {
6844
+ // The 31-bit hash selects a small bucket only. Exact token equality is
6845
+ // authoritative, and a rare collision gets an independent map slot.
6846
+ for (const [candidateKey, candidate] of connections) {
6847
+ if ((candidateKey === identityBucketKey || candidateKey.startsWith(`${identityBucketKey}#collision#`)) &&
6848
+ candidate.connectionIdentityToken === principalToken) {
6849
+ connKey = candidateKey;
6850
+ connection = candidate;
6851
+ break;
6852
+ }
6853
+ }
6854
+ if (!connection && connections.has(connKey)) {
6855
+ let collisionIndex = 1;
6856
+ while (connections.has(`${identityBucketKey}#collision#${collisionIndex}`))
6857
+ collisionIndex += 1;
6858
+ connKey = `${identityBucketKey}#collision#${collisionIndex}`;
6859
+ }
6860
+ }
6861
+ else {
6862
+ connection = connections.get(connKey);
6863
+ }
5971
6864
  if (connection && connection.ws) {
6865
+ // We may reuse this connection; cancel any pending idle-close so it is
6866
+ // not torn down during the auth preflight below. The held acquisition
6867
+ // lease also guards this window, but cancelling here avoids a needless
6868
+ // grace-timer round-trip on the common reuse path.
6869
+ cancelIdleConnectionClose(connection);
5972
6870
  if (connection.authFailure) {
5973
6871
  throw connection.authFailure;
5974
6872
  }
@@ -5984,19 +6882,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5984
6882
  if (connection.isConnected &&
5985
6883
  !connection.isAuthenticating &&
5986
6884
  !connectionMatchesAuthToken(connection, currentAuthToken)) {
5987
- if (connection.tokenRefreshTimer) {
5988
- clearInterval(connection.tokenRefreshTimer);
5989
- connection.tokenRefreshTimer = null;
5990
- }
5991
- try {
5992
- connection.ws.close();
5993
- }
5994
- catch (_a) {
5995
- // Already closing.
5996
- }
5997
- connection.ws = null;
5998
- connection.isConnected = false;
5999
- connections.delete(connection.key);
6885
+ closeConnectionNow(connection);
6000
6886
  }
6001
6887
  else {
6002
6888
  connection.authFailure = null;
@@ -6017,13 +6903,16 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
6017
6903
  isAuthenticating: false,
6018
6904
  appId,
6019
6905
  key: connKey,
6906
+ acquisitionKey: leaseKey,
6020
6907
  routePath: roomKey ? routePath : undefined,
6021
6908
  authTokenProvider,
6909
+ connectionIdentityToken: principalToken !== null && principalToken !== void 0 ? principalToken : null,
6022
6910
  pendingAuthToken: null,
6023
6911
  pendingAuthSnapshot: null,
6024
- authTokenFingerprint: null,
6912
+ authTokenValue: null,
6025
6913
  authPrincipal: null,
6026
6914
  tokenRefreshTimer: null,
6915
+ idleCloseTimer: null,
6027
6916
  consecutiveAuthFailures: 0,
6028
6917
  authFailure: null,
6029
6918
  };
@@ -6100,7 +6989,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
6100
6989
  }
6101
6990
  else {
6102
6991
  connection.isAuthenticating = false;
6103
- connection.authTokenFingerprint = null;
6992
+ connection.authTokenValue = null;
6104
6993
  connection.authPrincipal = null;
6105
6994
  replaySubscriptions(connection);
6106
6995
  }
@@ -6118,10 +7007,14 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
6118
7007
  // Handle errors
6119
7008
  ws.addEventListener('error', (event) => {
6120
7009
  console.error('[WS v2] WebSocket error:', event);
7010
+ const wsError = new Error('WebSocket error');
6121
7011
  for (const [, pending] of connection.pendingSubscriptions) {
6122
- pending.reject(new Error('WebSocket error'));
7012
+ pending.reject(wsError);
6123
7013
  }
6124
7014
  connection.pendingSubscriptions.clear();
7015
+ for (const subscription of connection.subscriptions.values()) {
7016
+ rejectSubscriptionRefresh(subscription, wsError);
7017
+ }
6125
7018
  });
6126
7019
  // Handle close
6127
7020
  ws.addEventListener('close', () => {
@@ -6145,7 +7038,7 @@ function handleServerMessage(connection, message) {
6145
7038
  switch (message.type) {
6146
7039
  case 'authenticated': {
6147
7040
  connection.isAuthenticating = false;
6148
- connection.authTokenFingerprint = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenFingerprint) !== null && _b !== void 0 ? _b : null;
7041
+ connection.authTokenValue = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenValue) !== null && _b !== void 0 ? _b : null;
6149
7042
  connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
6150
7043
  replaySubscriptions(connection);
6151
7044
  break;
@@ -6153,15 +7046,49 @@ function handleServerMessage(connection, message) {
6153
7046
  case 'subscribed': {
6154
7047
  const subscription = connection.subscriptions.get(message.subscriptionId);
6155
7048
  if (subscription) {
7049
+ const refresh = subscription.refresh;
6156
7050
  // A data update can race ahead of subscribed during reconnect.
6157
7051
  // If we already received data for this subscription, treat subscribed
6158
7052
  // as an ack only and avoid regressing to an older snapshot.
6159
- if (subscription.lastData === undefined) {
7053
+ if (refresh) {
7054
+ const hadBaseline = subscription.lastData !== undefined;
7055
+ if (subscription.dataRevision === refresh.dataRevisionAtStart) {
7056
+ if (subscription.cache) {
7057
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
7058
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
7059
+ }
7060
+ subscription.lastData = message.data;
7061
+ subscription.dataRevision += 1;
7062
+ if (hadBaseline) {
7063
+ const callbacks = Array.from(refresh.callbacks)
7064
+ .filter((callback) => subscription.callbacks.includes(callback));
7065
+ notifyCallbackOptions(callbacks, message.data);
7066
+ }
7067
+ else {
7068
+ // The refresh joined an original registration or a
7069
+ // reconnect replay before its first baseline arrived.
7070
+ // Every active callback needs that initial snapshot.
7071
+ notifyCallbacks(subscription, message.data);
7072
+ }
7073
+ }
7074
+ else if (refresh.callbacks.size > 0 && subscription.lastData !== undefined) {
7075
+ // A newer authorized data frame arrived after this refresh
7076
+ // began. Do not regress lastData to the older subscribed
7077
+ // snapshot; use that newer frame for callbacks that joined
7078
+ // after it was broadcast.
7079
+ const callbacks = Array.from(refresh.callbacks)
7080
+ .filter((callback) => subscription.callbacks.includes(callback));
7081
+ notifyCallbackOptions(callbacks, subscription.lastData);
7082
+ }
7083
+ resolveSubscriptionRefresh(subscription);
7084
+ }
7085
+ else if (subscription.lastData === undefined) {
6160
7086
  if (subscription.cache) {
6161
7087
  const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6162
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
7088
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
6163
7089
  }
6164
7090
  subscription.lastData = message.data;
7091
+ subscription.dataRevision += 1;
6165
7092
  notifyCallbacks(subscription, message.data);
6166
7093
  }
6167
7094
  }
@@ -6188,12 +7115,20 @@ function handleServerMessage(connection, message) {
6188
7115
  // Update cache
6189
7116
  if (subscription.cache) {
6190
7117
  const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6191
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
7118
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
6192
7119
  }
6193
7120
  // Store last data
6194
7121
  subscription.lastData = message.data;
7122
+ subscription.dataRevision += 1;
6195
7123
  // Notify callbacks
6196
7124
  notifyCallbacks(subscription, message.data);
7125
+ // A data frame is itself a full, freshly authorized snapshot.
7126
+ // It satisfies every joined callback even if the duplicate
7127
+ // subscribed ACK was lost; resolving here prevents a false
7128
+ // refresh timeout ten seconds after the UI already rendered.
7129
+ if (subscription.refresh) {
7130
+ resolveSubscriptionRefresh(subscription);
7131
+ }
6197
7132
  }
6198
7133
  break;
6199
7134
  }
@@ -6280,6 +7215,7 @@ function handleServerMessage(connection, message) {
6280
7215
  const subscription = connection.subscriptions.get(message.subscriptionId);
6281
7216
  if (subscription) {
6282
7217
  notifyErrorCallbacks(subscription, wsError);
7218
+ rejectSubscriptionRefresh(subscription, wsError);
6283
7219
  }
6284
7220
  }
6285
7221
  break;
@@ -6316,13 +7252,10 @@ function addIdsToSubscriptionData(data) {
6316
7252
  return data.map(withSubscriptionId);
6317
7253
  return withSubscriptionId(data);
6318
7254
  }
6319
- function notifyCallbacks(subscription, data) {
7255
+ function notifyCallbackOptions(callbacks, data) {
6320
7256
  var _a;
6321
7257
  // Attach the bare `id` (leaf doc key) to every delivered row (Bug 1).
6322
7258
  const decorated = addIdsToSubscriptionData(data);
6323
- // Snapshot the callbacks array so that unsubscriptions during
6324
- // notification don't cause callbacks to be skipped.
6325
- const callbacks = subscription.callbacks.slice();
6326
7259
  for (const callback of callbacks) {
6327
7260
  try {
6328
7261
  (_a = callback.onData) === null || _a === void 0 ? void 0 : _a.call(callback, decorated);
@@ -6332,23 +7265,33 @@ function notifyCallbacks(subscription, data) {
6332
7265
  }
6333
7266
  }
6334
7267
  }
7268
+ function notifyCallbacks(subscription, data) {
7269
+ // Snapshot the callbacks array so that unsubscriptions during
7270
+ // notification don't cause callbacks to be skipped.
7271
+ notifyCallbackOptions(subscription.callbacks.slice(), data);
7272
+ }
7273
+ function notifyErrorCallback(callback, error) {
7274
+ let delivered = false;
7275
+ try {
7276
+ const onError = callback.onError;
7277
+ if (!onError)
7278
+ return false;
7279
+ onError(error);
7280
+ delivered = true;
7281
+ }
7282
+ catch (callbackError) {
7283
+ console.error('[WS v2] Error in subscription error callback:', callbackError);
7284
+ }
7285
+ return delivered;
7286
+ }
6335
7287
  function notifyErrorCallbacks(subscription, error) {
6336
7288
  let delivered = false;
6337
7289
  const callbacks = subscription.callbacks.slice();
6338
7290
  for (const callback of callbacks) {
6339
- if (!callback.onError)
6340
- continue;
6341
- delivered = true;
6342
- try {
6343
- callback.onError(error);
6344
- }
6345
- catch (callbackError) {
6346
- console.error('[WS v2] Error in subscription error callback:', callbackError);
6347
- }
7291
+ delivered = notifyErrorCallback(callback, error) || delivered;
6348
7292
  }
6349
- if (delivered) {
7293
+ if (delivered)
6350
7294
  error.__boundedDeliveredToOnError = true;
6351
- }
6352
7295
  return delivered;
6353
7296
  }
6354
7297
  // WebSocket readyState constants
@@ -6379,6 +7322,71 @@ function sendSubscribe(connection, subscription) {
6379
7322
  console.error('[WS v2] Error sending subscribe message:', error);
6380
7323
  }
6381
7324
  }
7325
+ function resolveSubscriptionRefresh(subscription) {
7326
+ const refresh = subscription.refresh;
7327
+ if (!refresh)
7328
+ return;
7329
+ subscription.refresh = null;
7330
+ if (refresh.timer)
7331
+ clearTimeout(refresh.timer);
7332
+ refresh.callbacks.clear();
7333
+ refresh.resolve();
7334
+ }
7335
+ function rejectSubscriptionRefresh(subscription, error) {
7336
+ const refresh = subscription.refresh;
7337
+ if (!refresh)
7338
+ return;
7339
+ subscription.refresh = null;
7340
+ if (refresh.timer)
7341
+ clearTimeout(refresh.timer);
7342
+ refresh.callbacks.clear();
7343
+ refresh.reject(error);
7344
+ }
7345
+ /**
7346
+ * A callback joining an already-registered non-cache subscription cannot be
7347
+ * seeded from `lastData`: that snapshot may predate the callback's auth
7348
+ * preflight. Re-send the same subscription id so the server re-runs read auth
7349
+ * and returns a fresh baseline, without allocating another websocket or
7350
+ * replacing the logical client subscription.
7351
+ */
7352
+ function requestFreshSubscriptionSnapshot(connection, subscription, callback) {
7353
+ let refresh = subscription.refresh;
7354
+ if (!refresh) {
7355
+ let resolve;
7356
+ let reject;
7357
+ const promise = new Promise((resolvePromise, rejectPromise) => {
7358
+ resolve = resolvePromise;
7359
+ reject = rejectPromise;
7360
+ });
7361
+ refresh = {
7362
+ promise,
7363
+ resolve,
7364
+ reject,
7365
+ timer: null,
7366
+ callbacks: new Set(),
7367
+ dataRevisionAtStart: subscription.dataRevision,
7368
+ };
7369
+ subscription.refresh = refresh;
7370
+ refresh.timer = setTimeout(() => {
7371
+ if (subscription.refresh !== refresh)
7372
+ return;
7373
+ rejectSubscriptionRefresh(subscription, new Error('Subscription timeout'));
7374
+ }, 10000);
7375
+ }
7376
+ // Register before sendSubscribe: test/browser transports may synchronously
7377
+ // dispatch the subscribed response from send().
7378
+ refresh.callbacks.add(callback);
7379
+ // Every newly joined callback gets an idempotent same-id resend when the
7380
+ // authenticated socket is ready. Do not suppress this because an initial
7381
+ // confirmation or an earlier refresh is pending: that pending frame may
7382
+ // have been lost or displaced, and sharing it is exactly how an entire
7383
+ // route remount can inherit one doomed timeout. When connecting or
7384
+ // authenticating, replaySubscriptions will send after auth completes.
7385
+ if (connection.isConnected && !connection.isAuthenticating) {
7386
+ sendSubscribe(connection, subscription);
7387
+ }
7388
+ return refresh.promise;
7389
+ }
6382
7390
  function sendUnsubscribe(connection, subscriptionId) {
6383
7391
  if (!connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
6384
7392
  return;
@@ -6409,7 +7417,7 @@ function sendUnsubscribe(connection, subscriptionId) {
6409
7417
  * the subscription onto, since a WS connection routes to a single DO.
6410
7418
  */
6411
7419
  async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6412
- var _a, _b;
7420
+ var _a, _b, _c, _d, _e;
6413
7421
  const config = await getConfig();
6414
7422
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6415
7423
  // Use explicit appId override if provided, otherwise fall back to config
@@ -6427,16 +7435,27 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6427
7435
  : undefined;
6428
7436
  const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6429
7437
  const identity = identityInfo.key;
7438
+ const identityToken = identityInfo.token;
6430
7439
  const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
7440
+ // Preserve the existing compact connection-key shape, but use the exact
7441
+ // token as a secondary in-memory equality check within that hash bucket.
6431
7442
  const principalKey = authTokenProvider ? identity : undefined;
7443
+ const principalToken = authTokenProvider
7444
+ ? (identityToken !== null && identityToken !== void 0 ? identityToken : identity)
7445
+ : undefined;
6432
7446
  const cacheKey = responseCacheEnabled
6433
7447
  ? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
6434
7448
  : null;
6435
- // Get or create connection for this routing target (room-scoped when a
6436
- // room route is supplied by the live helper, else the app-level connection).
7449
+ // Acquire a connection for this routing target (room-scoped when a room route
7450
+ // is supplied by the live helper, else the app-level connection). The lease is
7451
+ // held across auth preflight and released once this call registers below, so a
7452
+ // concurrent old-route teardown cannot close a socket we are about to reuse.
6437
7453
  let connection;
7454
+ let releaseConnection;
6438
7455
  try {
6439
- connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
7456
+ const acquisition = await acquireConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey, principalToken);
7457
+ connection = acquisition.connection;
7458
+ releaseConnection = acquisition.release;
6440
7459
  }
6441
7460
  catch (error) {
6442
7461
  const err = error instanceof Error ? error : new Error(String(error));
@@ -6446,73 +7465,135 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6446
7465
  }
6447
7466
  throw err;
6448
7467
  }
6449
- // Deliver cached data immediately if available, but only after connection
6450
- // auth preflight has succeeded. An expired authenticated session must receive
6451
- // an auth error, not a stale private snapshot followed by a failed connect.
6452
- const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
6453
- if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
6454
- setTimeout(() => {
6455
- var _a;
6456
- (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
6457
- }, 0);
6458
- }
7468
+ const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
6459
7469
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6460
- const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6461
- const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
6462
- const sortKey = subscriptionOptions.sort ? JSON.stringify(subscriptionOptions.sort) : '';
6463
7470
  let existingSubscription;
6464
- for (const sub of connection.subscriptions.values()) {
6465
- const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
6466
- const subFilterKey = sub.filter ? JSON.stringify(sub.filter) : '';
6467
- const subSortKey = sub.sort ? JSON.stringify(sub.sort) : '';
6468
- // H1: also require the same principal identity — a different logged-in
6469
- // user must not piggy-back on (and inherit lastData from) another user's
6470
- // existing subscription, even with identical path/prompt/shape/filter/sort.
6471
- if (sub.identity === identity && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && subShapeKey === shapeKey && subFilterKey === filterKey && subSortKey === sortKey && sub.includeSubPaths === ((_a = subscriptionOptions.includeSubPaths) !== null && _a !== void 0 ? _a : false) && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
6472
- existingSubscription = sub;
6473
- break;
7471
+ let specKey = '';
7472
+ try {
7473
+ // Stringify THIS call's specs once; existing subscriptions carry their
7474
+ // precomputed specKey, so the scan is a string compare per entry
7475
+ // (previously each new subscribe re-stringified every existing
7476
+ // subscription's shape/filter/sort — O(N²) with many live hooks).
7477
+ specKey = JSON.stringify([
7478
+ (_a = subscriptionOptions.shape) !== null && _a !== void 0 ? _a : null,
7479
+ (_b = subscriptionOptions.filter) !== null && _b !== void 0 ? _b : null,
7480
+ (_c = subscriptionOptions.sort) !== null && _c !== void 0 ? _c : null,
7481
+ ]);
7482
+ for (const sub of connection.subscriptions.values()) {
7483
+ // H1: also require the same principal identity (compact hash AND exact
7484
+ // token) — a different logged-in user must not piggy-back on (and
7485
+ // inherit lastData from) another user's existing subscription, even
7486
+ // with identical path/prompt/shape/filter/sort.
7487
+ if (sub.identity === identity && sub.identityToken === identityToken && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && sub.specKey === specKey && sub.includeSubPaths === ((_d = subscriptionOptions.includeSubPaths) !== null && _d !== void 0 ? _d : false) && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
7488
+ existingSubscription = sub;
7489
+ break;
7490
+ }
6474
7491
  }
6475
7492
  }
7493
+ catch (error) {
7494
+ releaseConnection();
7495
+ throw error;
7496
+ }
6476
7497
  if (existingSubscription) {
7498
+ let replayData = undefined;
7499
+ let replayOnData;
7500
+ if (responseCacheEnabled) {
7501
+ // Read callback getters before mutating the shared subscription. If
7502
+ // a caller supplied a throwing getter, the acquisition fail-safe can
7503
+ // release cleanly without leaving a ghost callback behind.
7504
+ replayOnData = subscriptionOptions.onData;
7505
+ replayData = existingSubscription.lastData !== undefined
7506
+ ? existingSubscription.lastData
7507
+ : cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL
7508
+ ? cachedEntry.data
7509
+ : undefined;
7510
+ }
6477
7511
  if (responseCacheEnabled) {
6478
7512
  existingSubscription.cache = true;
6479
7513
  }
6480
7514
  // Add callback to existing subscription
6481
7515
  existingSubscription.callbacks.push(subscriptionOptions);
6482
- // Deliver last known data immediately only for explicit, principal-bound
6483
- // cache opt-in. Otherwise joining an existing subscription can replay a
6484
- // stale private snapshot without a fresh server auth result.
6485
- if (responseCacheEnabled &&
6486
- existingSubscription.cache &&
6487
- existingSubscription.lastData !== undefined &&
6488
- subscriptionOptions.onData) {
6489
- setTimeout(() => {
6490
- var _a;
6491
- (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
6492
- }, 0);
7516
+ let refreshPromise = null;
7517
+ if (responseCacheEnabled) {
7518
+ // Explicit, principal-bound cache opt-in may replay exactly one
7519
+ // snapshot. Prefer the active subscription's lastData, falling back
7520
+ // to the short-lived response cache while a reconnect is in flight.
7521
+ if (replayData !== undefined && replayOnData) {
7522
+ setTimeout(() => {
7523
+ replayOnData === null || replayOnData === void 0 ? void 0 : replayOnData(addIdsToSubscriptionData(replayData));
7524
+ }, 0);
7525
+ }
7526
+ }
7527
+ else {
7528
+ // Default (non-cache) subscriptions require a fresh authorized
7529
+ // baseline for every newly joined callback. Reuse the same logical
7530
+ // subscription id and socket, but make the server re-run the read.
7531
+ refreshPromise = requestFreshSubscriptionSnapshot(connection, existingSubscription, subscriptionOptions);
7532
+ }
7533
+ releaseConnection();
7534
+ if (refreshPromise) {
7535
+ try {
7536
+ await refreshPromise;
7537
+ }
7538
+ catch (error) {
7539
+ const err = error instanceof Error ? error : new Error(String(error));
7540
+ let deliveredLocally = false;
7541
+ if (!err.__boundedDeliveredToOnError) {
7542
+ deliveredLocally = notifyErrorCallback(subscriptionOptions, err);
7543
+ }
7544
+ if (!subscriptionOptions.onError) {
7545
+ await removeCallbackFromSubscription(connection, existingSubscription.subscriptionId, subscriptionOptions);
7546
+ throw err;
7547
+ }
7548
+ if (!err.__boundedDeliveredToOnError && !deliveredLocally) {
7549
+ console.warn('[WS v2] Subscription refresh failed, keeping for reconnect recovery:', error);
7550
+ }
7551
+ }
6493
7552
  }
6494
7553
  return async () => {
6495
7554
  await removeCallbackFromSubscription(connection, existingSubscription.subscriptionId, subscriptionOptions);
6496
7555
  };
6497
7556
  }
7557
+ // Deliver cached data immediately for a new logical subscription, but only
7558
+ // after connection auth preflight has succeeded. An expired authenticated
7559
+ // session must receive an auth error, not a stale private snapshot followed
7560
+ // by a failed connect.
7561
+ if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
7562
+ setTimeout(() => {
7563
+ var _a;
7564
+ (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
7565
+ }, 0);
7566
+ }
6498
7567
  // Create new subscription
6499
7568
  const subscriptionId = generateSubscriptionId();
6500
- const subscription = {
6501
- subscriptionId,
6502
- path: normalizedPath,
6503
- filter: subscriptionOptions.filter,
6504
- sort: subscriptionOptions.sort,
6505
- prompt: subscriptionOptions.prompt,
6506
- shape: subscriptionOptions.shape,
6507
- limit: subscriptionOptions.limit,
6508
- cursor: subscriptionOptions.cursor,
6509
- includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
6510
- callbacks: [subscriptionOptions],
6511
- lastData: undefined,
6512
- cache: responseCacheEnabled,
6513
- identity,
6514
- };
6515
- connection.subscriptions.set(subscriptionId, subscription);
7569
+ let subscription;
7570
+ try {
7571
+ subscription = {
7572
+ subscriptionId,
7573
+ path: normalizedPath,
7574
+ filter: subscriptionOptions.filter,
7575
+ sort: subscriptionOptions.sort,
7576
+ prompt: subscriptionOptions.prompt,
7577
+ shape: subscriptionOptions.shape,
7578
+ limit: subscriptionOptions.limit,
7579
+ cursor: subscriptionOptions.cursor,
7580
+ includeSubPaths: (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false,
7581
+ callbacks: [subscriptionOptions],
7582
+ lastData: undefined,
7583
+ dataRevision: 0,
7584
+ refresh: null,
7585
+ cache: responseCacheEnabled,
7586
+ identity,
7587
+ identityToken,
7588
+ specKey,
7589
+ };
7590
+ connection.subscriptions.set(subscriptionId, subscription);
7591
+ }
7592
+ catch (error) {
7593
+ releaseConnection();
7594
+ throw error;
7595
+ }
7596
+ releaseConnection();
6516
7597
  // Send subscribe message if connected
6517
7598
  if (connection.isConnected && !connection.isAuthenticating) {
6518
7599
  // Create a promise to wait for subscription confirmation
@@ -6532,15 +7613,23 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6532
7613
  }
6533
7614
  catch (error) {
6534
7615
  const err = error instanceof Error ? error : new Error(String(error));
7616
+ let deliveredLocally = false;
6535
7617
  if (!err.__boundedDeliveredToOnError) {
6536
- notifyErrorCallbacks(subscription, err);
7618
+ // A local confirmation timeout belongs to the caller that
7619
+ // originated it. Joined callbacks may already have a viable
7620
+ // same-id refresh and must not be poisoned by that old attempt.
7621
+ deliveredLocally = notifyErrorCallback(subscriptionOptions, err);
6537
7622
  }
6538
7623
  if (!subscriptionOptions.onError) {
6539
7624
  connection.pendingSubscriptions.delete(subscriptionId);
6540
- connection.subscriptions.delete(subscriptionId);
7625
+ // This registration may have acquired joined callbacks while
7626
+ // its confirmation was pending. Remove only the originating
7627
+ // callback; deleting the whole logical subscription would
7628
+ // orphan those remounted consumers and their refresh flight.
7629
+ await removeCallbackFromSubscription(connection, subscriptionId, subscriptionOptions);
6541
7630
  throw err;
6542
7631
  }
6543
- if (!err.__boundedDeliveredToOnError) {
7632
+ if (!err.__boundedDeliveredToOnError && !deliveredLocally) {
6544
7633
  console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6545
7634
  }
6546
7635
  }
@@ -6551,12 +7640,14 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6551
7640
  };
6552
7641
  }
6553
7642
  async function removeCallbackFromSubscription(connection, subscriptionId, callback) {
7643
+ var _a;
6554
7644
  const subscription = connection.subscriptions.get(subscriptionId);
6555
7645
  if (!subscription) {
6556
7646
  return;
6557
7647
  }
6558
7648
  // Remove this callback
6559
7649
  subscription.callbacks = subscription.callbacks.filter(cb => cb !== callback);
7650
+ (_a = subscription.refresh) === null || _a === void 0 ? void 0 : _a.callbacks.delete(callback);
6560
7651
  // If there are still callbacks, don't unsubscribe
6561
7652
  if (subscription.callbacks.length > 0) {
6562
7653
  return;
@@ -6578,17 +7669,9 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6578
7669
  sendUnsubscribe(connection, subscriptionId);
6579
7670
  await unsubscribePromise;
6580
7671
  }
6581
- // If no more subscriptions, close connection
6582
- if (connection.subscriptions.size === 0 && connection.ws) {
6583
- // Clear token refresh timer
6584
- if (connection.tokenRefreshTimer) {
6585
- clearInterval(connection.tokenRefreshTimer);
6586
- connection.tokenRefreshTimer = null;
6587
- }
6588
- connection.ws.close();
6589
- connection.ws = null;
6590
- connections.delete(connection.key);
6591
- }
7672
+ // If no more subscriptions, let the connection close after a short idle
7673
+ // grace so an immediately-following route remount can reclaim the socket.
7674
+ scheduleConnectionCloseIfIdle(connection);
6592
7675
  }
6593
7676
  /**
6594
7677
  * Close all v2 subscriptions.
@@ -6596,6 +7679,11 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6596
7679
  async function closeAllSubscriptionsV2() {
6597
7680
  const closePromises = [];
6598
7681
  for (const [appId, connection] of connections) {
7682
+ cancelIdleConnectionClose(connection);
7683
+ const closeError = new Error('Subscriptions closed');
7684
+ for (const subscription of connection.subscriptions.values()) {
7685
+ rejectSubscriptionRefresh(subscription, closeError);
7686
+ }
6599
7687
  if (connection.tokenRefreshTimer) {
6600
7688
  clearInterval(connection.tokenRefreshTimer);
6601
7689
  connection.tokenRefreshTimer = null;
@@ -6604,15 +7692,18 @@ async function closeAllSubscriptionsV2() {
6604
7692
  const ws = connection.ws;
6605
7693
  connection.ws = null;
6606
7694
  connection.isConnected = false;
6607
- closePromises.push(new Promise((resolve) => {
6608
- ws.addEventListener('close', () => {
6609
- resolve();
6610
- });
6611
- ws.close();
6612
- }));
7695
+ if (ws.readyState !== WS_READY_STATE_CLOSED) {
7696
+ closePromises.push(new Promise((resolve) => {
7697
+ ws.addEventListener('close', () => {
7698
+ resolve();
7699
+ });
7700
+ ws.close();
7701
+ }));
7702
+ }
6613
7703
  }
6614
7704
  connections.delete(appId);
6615
7705
  }
7706
+ connectionAcquisitions.clear();
6616
7707
  await Promise.all(closePromises);
6617
7708
  }
6618
7709
  /**
@@ -6645,10 +7736,12 @@ function getCachedDataV2(path, prompt) {
6645
7736
  // opt-in. Resolve the caller's identity from an active cache-enabled
6646
7737
  // subscription for this path. No matching sub → null (safe).
6647
7738
  let identity;
7739
+ let identityToken;
6648
7740
  outer: for (const connection of connections.values()) {
6649
7741
  for (const sub of connection.subscriptions.values()) {
6650
7742
  if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
6651
7743
  identity = sub.identity;
7744
+ identityToken = sub.identityToken;
6652
7745
  break outer;
6653
7746
  }
6654
7747
  }
@@ -6657,7 +7750,7 @@ function getCachedDataV2(path, prompt) {
6657
7750
  return null;
6658
7751
  }
6659
7752
  const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
6660
- const cachedEntry = responseCache.get(cacheKey);
7753
+ const cachedEntry = getResponseCacheEntry(cacheKey, identityToken !== null && identityToken !== void 0 ? identityToken : null);
6661
7754
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
6662
7755
  return cachedEntry.data;
6663
7756
  }
@@ -6719,10 +7812,15 @@ async function doReconnectWithNewAuth() {
6719
7812
  if (!connection.ws) {
6720
7813
  continue;
6721
7814
  }
7815
+ cancelIdleConnectionClose(connection);
7816
+ const reconnectError = new Error('Connection reconnecting due to auth change');
6722
7817
  for (const [, pending] of connection.pendingSubscriptions) {
6723
- pending.reject(new Error('Connection reconnecting due to auth change'));
7818
+ pending.reject(reconnectError);
6724
7819
  }
6725
7820
  connection.pendingSubscriptions.clear();
7821
+ for (const subscription of connection.subscriptions.values()) {
7822
+ rejectSubscriptionRefresh(subscription, reconnectError);
7823
+ }
6726
7824
  for (const [, pending] of connection.pendingUnsubscriptions) {
6727
7825
  pending.resolve();
6728
7826
  }
@@ -6782,10 +7880,10 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6782
7880
  const currentToken = await getIdToken(isServer).catch(() => null);
6783
7881
  if (!currentToken) {
6784
7882
  // Mid-refresh: the socket principal still governs server-side.
6785
- if (!connection.authTokenFingerprint && !connection.authPrincipal)
7883
+ if (!connection.authTokenValue && !connection.authPrincipal)
6786
7884
  return false;
6787
7885
  }
6788
- else if (!connection.authTokenFingerprint && !connection.authPrincipal) {
7886
+ else if (!connection.authTokenValue && !connection.authPrincipal) {
6789
7887
  // Anonymous socket + authenticated user: do not attribute this user's
6790
7888
  // input to the anon principal. Reauth the socket; caller may fall back.
6791
7889
  void maybeReauthRoomConnection(connection);
@@ -6805,7 +7903,7 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6805
7903
  }
6806
7904
  }
6807
7905
  // Debounced background reauth: when the client's current token no longer
6808
- // matches the connection's auth fingerprint (token rotation), refresh the
7906
+ // matches the connection's authenticated identity (token rotation), refresh the
6809
7907
  // socket auth ONCE rather than degrading every subsequent send.
6810
7908
  const reauthInFlight = new WeakSet();
6811
7909
  async function maybeReauthRoomConnection(connection, isServer) {
@@ -7488,6 +8586,8 @@ function defineLiveModule(mod) {
7488
8586
  return mod;
7489
8587
  }
7490
8588
 
8589
+ exports.BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR;
8590
+ exports.BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR;
7491
8591
  exports.BoundedDeclineError = BoundedDeclineError;
7492
8592
  exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
7493
8593
  exports.EFFECT_INTENT_ADDRESS = EFFECT_INTENT_ADDRESS;
@@ -7498,6 +8598,8 @@ exports.ReactNativeSessionManager = ReactNativeSessionManager;
7498
8598
  exports.ServerSessionManager = ServerSessionManager;
7499
8599
  exports.WebSessionManager = WebSessionManager;
7500
8600
  exports.aggregate = aggregate;
8601
+ exports.assertCanonicalBoundedRuntimeWriteCalldata = assertCanonicalBoundedRuntimeWriteCalldata;
8602
+ exports.assertEvmTransactionMatchesWrite = assertEvmTransactionMatchesWrite;
7501
8603
  exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
7502
8604
  exports.clearCache = clearCache;
7503
8605
  exports.closeAllSubscriptions = closeAllSubscriptions;
@@ -7511,6 +8613,7 @@ exports.dispatchDeclineEvent = dispatchDeclineEvent;
7511
8613
  exports.docId = docId;
7512
8614
  exports.functions = functions;
7513
8615
  exports.genAuthNonce = genAuthNonce;
8616
+ exports.genSiweMessage = genSiweMessage;
7514
8617
  exports.genSolanaMessage = genSolanaMessage;
7515
8618
  exports.get = get;
7516
8619
  exports.getActiveSessionManager = getActiveSessionManager;
@@ -7528,6 +8631,7 @@ exports.isEffectResult = isEffectResult;
7528
8631
  exports.live = live;
7529
8632
  exports.liveIntent = intent;
7530
8633
  exports.liveStatus = status;
8634
+ exports.normalizeEvmAddress = normalizeEvmAddress;
7531
8635
  exports.now = now;
7532
8636
  exports.queryAggregate = queryAggregate;
7533
8637
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
@@ -7548,6 +8652,7 @@ exports.signSessionCreateMessage = signSessionCreateMessage;
7548
8652
  exports.signTransaction = signTransaction;
7549
8653
  exports.subscribe = subscribe;
7550
8654
  exports.subscribeLiveView = subscribeView;
8655
+ exports.toLowerEvmAddress = toLowerEvmAddress;
7551
8656
  exports.toMillis = toMillis;
7552
8657
  exports.toSeconds = toSeconds;
7553
8658
  exports.withEffects = withEffects;