@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.mjs CHANGED
@@ -31,6 +31,10 @@ let clientConfig = {
31
31
  isServer: false,
32
32
  };
33
33
  let isInitialized = false;
34
+ // Callers awaiting getConfig() before init() completes. Resolved synchronously
35
+ // by init() — the previous 50ms setInterval poll added up to 50ms of latency
36
+ // to the first SDK operation after init for no reason.
37
+ let initWaiters = [];
34
38
  const BOUNDED_NETWORKS = {
35
39
  'bounded-staging': {
36
40
  wsApiUrl: 'wss://realtime-staging.bounded.sh',
@@ -99,23 +103,17 @@ function init(newConfig) {
99
103
  const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
100
104
  clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
101
105
  isInitialized = true;
106
+ const waiters = initWaiters;
107
+ initWaiters = [];
108
+ for (const wake of waiters)
109
+ wake();
102
110
  resolve();
103
111
  });
104
112
  }
105
113
  function waitForInitialization() {
106
- return new Promise((resolve) => {
107
- if (isInitialized) {
108
- resolve();
109
- }
110
- else {
111
- const interval = setInterval(() => {
112
- if (isInitialized) {
113
- clearInterval(interval);
114
- resolve();
115
- }
116
- }, 50);
117
- }
118
- });
114
+ if (isInitialized)
115
+ return Promise.resolve();
116
+ return new Promise((resolve) => { initWaiters.push(resolve); });
119
117
  }
120
118
  async function getConfig() {
121
119
  await waitForInitialization();
@@ -2801,8 +2799,24 @@ async function genAuthNonce() {
2801
2799
  // stamps an is_anonymous claim. Without this a guest would be indistinguishable
2802
2800
  // from 'phantom' and couldn't be gated or flagged.
2803
2801
  const SIGNATURE_AUTH_METHODS = new Set(['phantom', 'mobile-wallet-adapter', 'guest']);
2804
- async function createSessionWithSignature(address, message, signature) {
2805
- var _a, _b;
2802
+ // Maps a client authMethod to the /session issuer's signature discriminator when
2803
+ // they differ. EVM WALLET LOGIN (config.authMethod 'evm-wallet') exchanges a SIWE
2804
+ // (EIP-4361 personal_sign, secp256k1) signature, which the bounded-auth issuer
2805
+ // routes on authMethod:"siwe" (index.ts /session → parseSiweMessage/recoverSiweSigner)
2806
+ // and mints under custom:evmWalletAddress. The EVM provider also passes 'siwe'
2807
+ // explicitly (authMethodOverride), so this map is the belt-and-suspenders path.
2808
+ const SESSION_METHOD_ALIASES = { 'evm-wallet': 'siwe' };
2809
+ /**
2810
+ * Exchange a wallet signature for a Bounded session at the /session issuer.
2811
+ *
2812
+ * @param authMethodOverride the exact /session discriminator to send (e.g. 'siwe'
2813
+ * for the SIWE/EVM path). When omitted the method is derived from config: an
2814
+ * aliased method (evm-wallet → siwe) maps through SESSION_METHOD_ALIASES, a
2815
+ * Solana wallet/guest method passes through verbatim, and anything else
2816
+ * defaults to the canonical 'phantom' the issuer accepts for a signature.
2817
+ */
2818
+ async function createSessionWithSignature(address, message, signature, authMethodOverride) {
2819
+ var _a, _b, _c;
2806
2820
  const client = await getAxiosAuthClient();
2807
2821
  const config = await getConfig();
2808
2822
  const appId = config.appId;
@@ -2811,7 +2825,7 @@ async function createSessionWithSignature(address, message, signature) {
2811
2825
  // canonical Solana wallet method 'phantom' that every /session issuer accepts
2812
2826
  // for a signature exchange. This is what makes the server SDK + guest device
2813
2827
  // keypair work under the email-default config.
2814
- const authMethod = SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom';
2828
+ 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');
2815
2829
  try {
2816
2830
  const response = await client.post('/session', {
2817
2831
  appId,
@@ -2826,11 +2840,11 @@ async function createSessionWithSignature(address, message, signature) {
2826
2840
  // Surface the issuer's actionable message (e.g. "Anonymous auth is not enabled for
2827
2841
  // this app. Add \"auth\": { \"anonymous\": true } to policy.json and redeploy.")
2828
2842
  // instead of axios's generic "Request failed with status code 403".
2829
- const d = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
2843
+ const d = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data;
2830
2844
  if (d && (d.message || d.error)) {
2831
2845
  const e = new Error(String(d.message || d.error));
2832
2846
  e.code = d.error;
2833
- e.status = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
2847
+ e.status = (_c = err === null || err === void 0 ? void 0 : err.response) === null || _c === void 0 ? void 0 : _c.status;
2834
2848
  e.hint = d.hint;
2835
2849
  throw e;
2836
2850
  }
@@ -2909,7 +2923,14 @@ async function revokeSession(refreshToken, issuer) {
2909
2923
  await axios.post(`${base}/session/revoke`, {
2910
2924
  refreshToken,
2911
2925
  appId: config.appId,
2912
- }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
2926
+ }, {
2927
+ headers: { 'Content-Type': 'application/json' },
2928
+ timeout: 15000,
2929
+ // Preserve the transport error for programmatic diagnostics while
2930
+ // ensuring AxiosError.toJSON() cannot serialize the live token body
2931
+ // into logs, telemetry, UI state, or crash reports.
2932
+ redact: ['data'],
2933
+ });
2913
2934
  }
2914
2935
  catch (err) {
2915
2936
  throw new SessionRevokeError(revokeFailureMessage(err), err);
@@ -3598,12 +3619,13 @@ const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use c
3598
3619
  /* SESSION MANAGER */
3599
3620
  /* ------------------------------------------------------------------ */
3600
3621
  class ServerSessionManager {
3601
- constructor(keypair) {
3622
+ constructor(keypair, sessionFactory) {
3602
3623
  /* Private cache (lives for the life of the instance) */
3603
3624
  this.session = null;
3604
3625
  /* Coalesce concurrent getSession() calls into a single in-flight request */
3605
3626
  this.pendingSession = null;
3606
3627
  this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
3628
+ this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
3607
3629
  }
3608
3630
  /**
3609
3631
  * Create a session manager for a specific keypair.
@@ -3612,10 +3634,30 @@ class ServerSessionManager {
3612
3634
  static forKeypair(keypair) {
3613
3635
  return new ServerSessionManager(keypair);
3614
3636
  }
3637
+ /**
3638
+ * Create an isolated server session manager for a non-Solana signer (currently
3639
+ * EVM/SIWE). The factory must perform the signature exchange and return the
3640
+ * canonical signer address with its tokens; no signer state becomes global.
3641
+ */
3642
+ static forSessionFactory(factory) {
3643
+ if (typeof factory !== 'function')
3644
+ throw new Error('A server session factory is required');
3645
+ return new ServerSessionManager(undefined, factory);
3646
+ }
3615
3647
  /* ---------------------------------------------- *
3616
3648
  * Session creation (instance method)
3617
3649
  * ---------------------------------------------- */
3618
3650
  async createSession() {
3651
+ if (this.sessionFactory) {
3652
+ const session = await this.sessionFactory();
3653
+ if (!session || typeof session.address !== 'string' || !session.address ||
3654
+ typeof session.idToken !== 'string' || !session.idToken ||
3655
+ typeof session.accessToken !== 'string' || !session.accessToken ||
3656
+ typeof session.refreshToken !== 'string' || !session.refreshToken) {
3657
+ throw new Error('Server signer session factory returned an invalid session');
3658
+ }
3659
+ return session;
3660
+ }
3619
3661
  if (!this.keypair) {
3620
3662
  throw new Error(NO_AMBIENT_SERVER_SESSION);
3621
3663
  }
@@ -3719,6 +3761,133 @@ function decodeBase64Url(input) {
3719
3761
  return safeAtob(b64);
3720
3762
  }
3721
3763
 
3764
+ // evm-utils.ts — the EVM twin of sol/sol-utils.ts's genSolanaMessage. It builds
3765
+ // the canonical EIP-4361 (Sign-In with Ethereum, SIWE) prompt the bounded-auth
3766
+ // issuer verifies, and normalizes EVM addresses to the single canonical form the
3767
+ // identity layer matches on.
3768
+ //
3769
+ // The SIWE message MUST match bounded-auth's parser byte-for-byte
3770
+ // (packages/cdk/cloudflare/bounded-auth/src/verify-siwe.ts → parseSiweMessage):
3771
+ // <domain> wants you to sign in with your Ethereum account:
3772
+ // <address>
3773
+ // <blank>
3774
+ // <statement>
3775
+ // <blank>
3776
+ // URI: <uri>
3777
+ // Version: 1
3778
+ // Chain ID: <chainId>
3779
+ // Nonce: <nonce>
3780
+ // Issued At: <ISO8601>
3781
+ // Expiration Time: <ISO8601>
3782
+ // Resources:
3783
+ // - bounded:app:<appId>
3784
+ //
3785
+ // Bindings the verifier enforces (so we produce them exactly): the address is
3786
+ // lowercased (identity match is case-sensitive, so lowercase-at-boundary is
3787
+ // mandatory), the single `bounded:app:<appId>` resource binds the signature to
3788
+ // THIS app (a signature for one app must not mint a session for another — the
3789
+ // SIWE analog of genSolanaMessage's `App ID:` line), and Expiration Time is
3790
+ // REQUIRED and must fall within the issuer's 10-minute max validity window.
3791
+ /** Default statement — kept identical to genSolanaMessage's for a consistent prompt. */
3792
+ const DEFAULT_STATEMENT = 'Sign this message to authenticate with our application.';
3793
+ /** Match bounded-auth's MAX_AUTH_WINDOW_MS (10 min); we default to a 5-min window. */
3794
+ const DEFAULT_VALIDITY_MS = 5 * 60 * 1000;
3795
+ /**
3796
+ * Build the canonical EIP-4361 SIWE message the bounded-auth issuer verifies.
3797
+ *
3798
+ * @param address the connected EVM account (lowercased into the prompt).
3799
+ * @param nonce single-use nonce from genAuthNonce().
3800
+ * @param appId the target app id (bound as the sole `bounded:app:<appId>` resource).
3801
+ * @param domain RFC-3986 authority of the requesting site (e.g. window.location.host).
3802
+ * @param uri the requesting URI; its origin is bound to the domain and actual browser Origin.
3803
+ */
3804
+ function genSiweMessage(address, nonce, appId, domain, uri, options = {}) {
3805
+ var _a, _b, _c, _d, _e;
3806
+ const addr = normalizeEvmAddress(address);
3807
+ if (!addr)
3808
+ throw new Error('SIWE address must be a 20-byte 0x-prefixed EVM address');
3809
+ const statement = (_a = options.statement) !== null && _a !== void 0 ? _a : DEFAULT_STATEMENT;
3810
+ const version = (_b = options.version) !== null && _b !== void 0 ? _b : '1';
3811
+ const chainId = (_c = options.chainId) !== null && _c !== void 0 ? _c : 1;
3812
+ if (statement !== DEFAULT_STATEMENT)
3813
+ throw new Error('SIWE statement must use the canonical Bounded authentication text');
3814
+ if (version !== '1')
3815
+ throw new Error('SIWE version must be 1');
3816
+ if (!Number.isSafeInteger(chainId) || chainId <= 0)
3817
+ throw new Error('SIWE chainId must be a positive safe integer');
3818
+ if (!/^[A-Za-z0-9]{8,}$/.test(nonce))
3819
+ throw new Error('SIWE nonce must be at least 8 alphanumeric characters');
3820
+ if (!appId || /[\r\n]/.test(appId))
3821
+ throw new Error('SIWE appId is required and must be single-line');
3822
+ if (!domain || domain !== domain.trim() || /[\s/@?#\\]/.test(domain))
3823
+ throw new Error('SIWE domain must be an RFC-3986 authority');
3824
+ try {
3825
+ const parsedAuthority = new URL(`https://${domain}`);
3826
+ if (parsedAuthority.username || parsedAuthority.password || parsedAuthority.pathname !== '/' ||
3827
+ parsedAuthority.search || parsedAuthority.hash || parsedAuthority.host.toLowerCase() !== domain) {
3828
+ throw new Error('non-canonical authority');
3829
+ }
3830
+ }
3831
+ catch (_f) {
3832
+ throw new Error('SIWE domain must be a canonical lowercase RFC-3986 authority');
3833
+ }
3834
+ let parsedUri;
3835
+ try {
3836
+ parsedUri = new URL(uri);
3837
+ }
3838
+ catch (_g) {
3839
+ throw new Error('SIWE URI must be an absolute URL');
3840
+ }
3841
+ const local = parsedUri.hostname === 'localhost' || parsedUri.hostname === '127.0.0.1' || parsedUri.hostname === '[::1]';
3842
+ if (parsedUri.protocol !== 'https:' && !(local && parsedUri.protocol === 'http:')) {
3843
+ throw new Error('SIWE URI must use HTTPS (HTTP is allowed only for localhost)');
3844
+ }
3845
+ if (parsedUri.username || parsedUri.password || parsedUri.hash || parsedUri.host.toLowerCase() !== domain.toLowerCase()) {
3846
+ throw new Error('SIWE URI authority must match the SIWE domain');
3847
+ }
3848
+ const issuedAtDate = (_d = options.issuedAt) !== null && _d !== void 0 ? _d : new Date();
3849
+ const expirationDate = (_e = options.expirationTime) !== null && _e !== void 0 ? _e : new Date(issuedAtDate.getTime() + DEFAULT_VALIDITY_MS);
3850
+ if (!Number.isFinite(issuedAtDate.getTime()) || !Number.isFinite(expirationDate.getTime()) || expirationDate <= issuedAtDate) {
3851
+ throw new Error('SIWE validity window is invalid');
3852
+ }
3853
+ if (expirationDate.getTime() - issuedAtDate.getTime() > 10 * 60 * 1000) {
3854
+ throw new Error('SIWE validity window exceeds 10 minutes');
3855
+ }
3856
+ const issuedAt = issuedAtDate.toISOString();
3857
+ const expirationTime = expirationDate.toISOString();
3858
+ return (`${domain} wants you to sign in with your Ethereum account:\n${addr}\n\n` +
3859
+ `${statement}\n\n` +
3860
+ `URI: ${parsedUri.toString()}\n` +
3861
+ `Version: ${version}\n` +
3862
+ `Chain ID: ${chainId}\n` +
3863
+ `Nonce: ${nonce}\n` +
3864
+ `Issued At: ${issuedAt}\n` +
3865
+ `Expiration Time: ${expirationTime}\n` +
3866
+ `Resources:\n` +
3867
+ `- bounded:app:${appId}`);
3868
+ }
3869
+ /**
3870
+ * Lowercase an EVM address to the canonical identity form. EVM addresses are
3871
+ * case-insensitive on-chain but the identity layer (@user.evmAddress) matches
3872
+ * case-sensitively, so every boundary normalizes to lowercase. Does not validate
3873
+ * shape — use normalizeEvmAddress() when the input is untrusted.
3874
+ */
3875
+ function toLowerEvmAddress(address) {
3876
+ return address.trim().toLowerCase();
3877
+ }
3878
+ /**
3879
+ * Normalize an untrusted EVM address to canonical lowercase 0x-hex, or null when
3880
+ * it isn't a well-formed 20-byte hex address. Mirrors the realtime-worker's
3881
+ * normalizeEvmAddress (auth.ts) EXACTLY so a client-derived @user.evmAddress and
3882
+ * the server-authenticated one are byte-identical (fail-closed on malformed).
3883
+ */
3884
+ function normalizeEvmAddress(raw) {
3885
+ if (typeof raw !== 'string')
3886
+ return null;
3887
+ const v = raw.trim().toLowerCase();
3888
+ return /^0x[0-9a-f]{40}$/.test(v) ? v : null;
3889
+ }
3890
+
3722
3891
  async function createBearerToken(isServer) {
3723
3892
  if (isServer) {
3724
3893
  const sessionMgr = ServerSessionManager.instance;
@@ -3767,7 +3936,7 @@ async function getUserInfo(isServer) {
3767
3936
  * Returns all-null when the token can't be decoded (caller treats as no identity).
3768
3937
  */
3769
3938
  function deriveUserIdentityFromIdToken(idToken) {
3770
- const empty = { id: null, address: null, email: null };
3939
+ const empty = { id: null, address: null, email: null, evmAddress: null };
3771
3940
  if (!idToken)
3772
3941
  return empty;
3773
3942
  try {
@@ -3780,14 +3949,25 @@ function deriveUserIdentityFromIdToken(idToken) {
3780
3949
  ? walletClaim
3781
3950
  : null;
3782
3951
  const userIdClaim = payload['custom:userId'];
3783
- const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3952
+ let id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3784
3953
  ? userIdClaim
3785
3954
  : null;
3786
3955
  const emailClaim = payload['email'];
3787
3956
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
3788
3957
  ? emailClaim.toLowerCase()
3789
3958
  : null;
3790
- return { id, address, email };
3959
+ // EVM wallet — carried on the `custom:evmWalletAddress` claim (an EVM login
3960
+ // OMITS custom:walletAddress). Normalized to canonical lowercase 0x-hex the
3961
+ // SAME way the realtime-worker does, so the client @user.evmAddress and the
3962
+ // server-authenticated one are byte-identical (fail-closed on malformed).
3963
+ const evmAddress = normalizeEvmAddress(payload['custom:evmWalletAddress']);
3964
+ // A SIWE identity uses its EVM address as custom:userId. Normalize legacy
3965
+ // mixed-case tokens at this boundary too so the principal and
3966
+ // @user.evmAddress cannot diverge solely by hex casing. Opaque human-login
3967
+ // ids remain byte-for-byte unchanged even when they have a linked wallet.
3968
+ if (evmAddress && id && normalizeEvmAddress(id) === evmAddress)
3969
+ id = evmAddress;
3970
+ return { id, address, email, evmAddress };
3791
3971
  }
3792
3972
  catch (_a) {
3793
3973
  return empty;
@@ -4109,6 +4289,456 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4109
4289
  }
4110
4290
  }
4111
4291
 
4292
+ /**
4293
+ * Selectors derived from the canonical BoundedRuntime signatures (BoundedTypes.sol):
4294
+ *
4295
+ * cast sig 'setDocuments(string,(string[],bool,(string,uint8,uint8,bytes)[])[])'
4296
+ * => 0x0827b094
4297
+ * cast sig 'setDocumentsAttested(string,(string[],bool,(string,uint8,uint8,bytes)[])[],(bytes32,uint256,uint256,uint8,bytes32,bytes32))'
4298
+ * => 0x87b111c4
4299
+ *
4300
+ * Keep the signatures beside the constants: changing either Solidity tuple is an
4301
+ * ABI break and must deliberately update these values and the Foundry goldens.
4302
+ */
4303
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = '0x0827b094';
4304
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = '0x87b111c4';
4305
+ const SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR.slice(2);
4306
+ const SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR.slice(2);
4307
+ const MAX_CALLDATA_BYTES = 1024 * 1024;
4308
+ const MAX_ARRAY_ITEMS = 2048;
4309
+ const BIGINT_ZERO = BigInt(0);
4310
+ const BIGINT_ONE = BigInt(1);
4311
+ const BIGINT_EIGHT = BigInt(8);
4312
+ const SECP256K1_N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
4313
+ const SECP256K1_HALF_N = BigInt('0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0');
4314
+ function hexBytes(raw) {
4315
+ if (typeof raw !== 'string' || !/^0x[0-9a-fA-F]*$/.test(raw) || raw.length % 2 !== 0) {
4316
+ throw new Error('EVM transaction calldata must be even-length 0x hex');
4317
+ }
4318
+ const hex = raw.slice(2);
4319
+ if (hex.length / 2 > MAX_CALLDATA_BYTES)
4320
+ throw new Error('EVM transaction calldata exceeds the client safety limit');
4321
+ const out = new Uint8Array(hex.length / 2);
4322
+ for (let i = 0; i < out.length; i++)
4323
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
4324
+ return out;
4325
+ }
4326
+ class AbiReader {
4327
+ constructor(bytes) {
4328
+ this.bytes = bytes;
4329
+ this.maxRead = 0;
4330
+ this.dynamicPayloadBytes = 0;
4331
+ this.utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
4332
+ }
4333
+ range(offset, length, label) {
4334
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > this.bytes.length) {
4335
+ throw new Error(`Malformed EVM setDocuments calldata (${label})`);
4336
+ }
4337
+ this.maxRead = Math.max(this.maxRead, offset + length);
4338
+ }
4339
+ word(offset, label) {
4340
+ this.range(offset, 32, label);
4341
+ let value = BIGINT_ZERO;
4342
+ for (let i = 0; i < 32; i++)
4343
+ value = (value << BIGINT_EIGHT) | BigInt(this.bytes[offset + i]);
4344
+ return value;
4345
+ }
4346
+ numberWord(offset, label, max = Number.MAX_SAFE_INTEGER) {
4347
+ const value = this.word(offset, label);
4348
+ if (value > BigInt(max))
4349
+ throw new Error(`Malformed EVM setDocuments calldata (${label} too large)`);
4350
+ return Number(value);
4351
+ }
4352
+ offset(base, wordOffset, min, label) {
4353
+ const relative = this.numberWord(wordOffset, `${label} offset`, this.bytes.length);
4354
+ if (relative < min || relative % 32 !== 0)
4355
+ throw new Error(`Malformed EVM setDocuments calldata (${label} offset)`);
4356
+ const absolute = base + relative;
4357
+ this.range(absolute, 0, label);
4358
+ return absolute;
4359
+ }
4360
+ dynamicBytes(at, label) {
4361
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4362
+ this.dynamicPayloadBytes += length;
4363
+ // ABI offsets may alias. Cap the cumulative decoded payload so a malicious
4364
+ // 1 MB tail referenced by thousands of fields cannot amplify into a
4365
+ // multi-gigabyte allocation/UTF-8 decode in the wallet process.
4366
+ if (this.dynamicPayloadBytes > MAX_CALLDATA_BYTES) {
4367
+ throw new Error('Malformed EVM setDocuments calldata (dynamic payload amplification)');
4368
+ }
4369
+ const start = at + 32;
4370
+ const padded = Math.ceil(length / 32) * 32;
4371
+ this.range(start, padded, label);
4372
+ for (let i = start + length; i < start + padded; i++) {
4373
+ if (this.bytes[i] !== 0)
4374
+ throw new Error(`Malformed EVM setDocuments calldata (${label} padding)`);
4375
+ }
4376
+ return this.bytes.slice(start, start + length);
4377
+ }
4378
+ dynamicEnd(at, label) {
4379
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4380
+ const end = at + 32 + Math.ceil(length / 32) * 32;
4381
+ this.range(at + 32, Math.ceil(length / 32) * 32, label);
4382
+ return end;
4383
+ }
4384
+ string(at, label) {
4385
+ try {
4386
+ return this.utf8.decode(this.dynamicBytes(at, label));
4387
+ }
4388
+ catch (_a) {
4389
+ throw new Error(`Malformed EVM setDocuments calldata (${label} utf8)`);
4390
+ }
4391
+ }
4392
+ finish() {
4393
+ if (this.maxRead !== this.bytes.length)
4394
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4395
+ }
4396
+ }
4397
+ function assertCanonicalOffset(actual, expected, label) {
4398
+ if (actual !== expected)
4399
+ throw new Error(`Malformed EVM setDocuments calldata (${label} non-canonical offset)`);
4400
+ }
4401
+ function decodeStringArray(reader, at, label) {
4402
+ const count = reader.numberWord(at, `${label} count`, MAX_ARRAY_ITEMS);
4403
+ const head = at + 32;
4404
+ const min = count * 32;
4405
+ const out = [];
4406
+ let expectedTail = head + min;
4407
+ for (let i = 0; i < count; i++) {
4408
+ const itemAt = reader.offset(head, head + i * 32, min, `${label}[${i}]`);
4409
+ assertCanonicalOffset(itemAt, expectedTail, `${label}[${i}]`);
4410
+ out.push(reader.string(itemAt, `${label}[${i}]`));
4411
+ expectedTail = reader.dynamicEnd(itemAt, `${label}[${i}]`);
4412
+ }
4413
+ return { value: out, end: expectedTail };
4414
+ }
4415
+ function decodeFields(reader, at) {
4416
+ const count = reader.numberWord(at, 'fields count', MAX_ARRAY_ITEMS);
4417
+ const head = at + 32;
4418
+ const min = count * 32;
4419
+ const out = [];
4420
+ let expectedTail = head + min;
4421
+ for (let i = 0; i < count; i++) {
4422
+ const tuple = reader.offset(head, head + i * 32, min, `field[${i}]`);
4423
+ assertCanonicalOffset(tuple, expectedTail, `field[${i}]`);
4424
+ const nameAt = reader.offset(tuple, tuple, 128, `field[${i}].name`);
4425
+ assertCanonicalOffset(nameAt, tuple + 128, `field[${i}].name`);
4426
+ const name = reader.string(nameAt, `field[${i}].name`);
4427
+ const nameEnd = reader.dynamicEnd(nameAt, `field[${i}].name`);
4428
+ const ftype = reader.numberWord(tuple + 32, `field[${i}].ftype`, 255);
4429
+ const op = reader.numberWord(tuple + 64, `field[${i}].op`, 255);
4430
+ const valueAt = reader.offset(tuple, tuple + 96, 128, `field[${i}].value`);
4431
+ assertCanonicalOffset(valueAt, nameEnd, `field[${i}].value`);
4432
+ const value = reader.dynamicBytes(valueAt, `field[${i}].value`);
4433
+ expectedTail = reader.dynamicEnd(valueAt, `field[${i}].value`);
4434
+ out.push({ name, ftype, op, value });
4435
+ }
4436
+ return { value: out, end: expectedTail };
4437
+ }
4438
+ function decodeRuntimeWrite(rawData) {
4439
+ const bytes = hexBytes(rawData);
4440
+ if (bytes.length < 68)
4441
+ throw new Error('Malformed EVM setDocuments calldata');
4442
+ const selector = Array.from(bytes.slice(0, 4)).map((b) => b.toString(16).padStart(2, '0')).join('');
4443
+ const kind = selector === SET_DOCUMENTS_SELECTOR
4444
+ ? 'setDocuments'
4445
+ : selector === SET_DOCUMENTS_ATTESTED_SELECTOR
4446
+ ? 'setDocumentsAttested'
4447
+ : null;
4448
+ if (!kind)
4449
+ throw new Error('EVM transaction is not a supported BoundedRuntime document write');
4450
+ const reader = new AbiReader(bytes);
4451
+ reader.maxRead = 4;
4452
+ const args = 4;
4453
+ // ExternalCallAttestation is a fully-static six-word tuple and is therefore
4454
+ // expanded inline into the top-level head: 2 dynamic offsets + 6 words.
4455
+ const headBytes = kind === 'setDocumentsAttested' ? 256 : 64;
4456
+ const appAt = reader.offset(args, args, headBytes, 'appId');
4457
+ assertCanonicalOffset(appAt, args + headBytes, 'appId');
4458
+ const opsAt = reader.offset(args, args + 32, headBytes, 'ops');
4459
+ const appId = reader.string(appAt, 'appId');
4460
+ const appEnd = reader.dynamicEnd(appAt, 'appId');
4461
+ assertCanonicalOffset(opsAt, appEnd, 'ops');
4462
+ const count = reader.numberWord(opsAt, 'ops count', MAX_ARRAY_ITEMS);
4463
+ const head = opsAt + 32;
4464
+ const min = count * 32;
4465
+ const ops = [];
4466
+ let expectedTail = head + min;
4467
+ for (let i = 0; i < count; i++) {
4468
+ const tuple = reader.offset(head, head + i * 32, min, `op[${i}]`);
4469
+ assertCanonicalOffset(tuple, expectedTail, `op[${i}]`);
4470
+ const pathAt = reader.offset(tuple, tuple, 96, `op[${i}].path`);
4471
+ assertCanonicalOffset(pathAt, tuple + 96, `op[${i}].path`);
4472
+ const deleteWord = reader.numberWord(tuple + 32, `op[${i}].isDelete`, 1);
4473
+ const fieldsAt = reader.offset(tuple, tuple + 64, 96, `op[${i}].fields`);
4474
+ const path = decodeStringArray(reader, pathAt, `op[${i}].path`);
4475
+ assertCanonicalOffset(fieldsAt, path.end, `op[${i}].fields`);
4476
+ const fields = decodeFields(reader, fieldsAt);
4477
+ expectedTail = fields.end;
4478
+ ops.push({
4479
+ pathSegments: path.value,
4480
+ isDelete: deleteWord === 1,
4481
+ fields: fields.value,
4482
+ });
4483
+ }
4484
+ let attestation;
4485
+ if (kind === 'setDocumentsAttested') {
4486
+ const callsHash = reader.word(args + 64, 'attestation.callsHash');
4487
+ const nonce = reader.word(args + 96, 'attestation.nonce');
4488
+ const deadline = reader.word(args + 128, 'attestation.deadline');
4489
+ const v = reader.numberWord(args + 160, 'attestation.v', 255);
4490
+ const r = reader.word(args + 192, 'attestation.r');
4491
+ const s = reader.word(args + 224, 'attestation.s');
4492
+ if (v !== 27 && v !== 28)
4493
+ throw new Error('Malformed EVM setDocuments calldata (attestation.v must be 27 or 28)');
4494
+ if (r === BIGINT_ZERO || r >= SECP256K1_N)
4495
+ throw new Error('Malformed EVM setDocuments calldata (attestation.r is not canonical)');
4496
+ if (s === BIGINT_ZERO || s > SECP256K1_HALF_N)
4497
+ throw new Error('Malformed EVM setDocuments calldata (attestation.s is not canonical low-s)');
4498
+ attestation = { callsHash, nonce, deadline, v, r, s };
4499
+ }
4500
+ if (expectedTail !== bytes.length)
4501
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4502
+ reader.finish();
4503
+ return Object.assign({ kind, appId, ops }, (attestation ? { attestation } : {}));
4504
+ }
4505
+ /**
4506
+ * Fail closed unless calldata is a byte-for-byte canonical ABI encoding of one
4507
+ * supported BoundedRuntime write. This is the defense used by direct signer
4508
+ * surfaces; the higher-level write validator below additionally binds app/ops.
4509
+ */
4510
+ function assertCanonicalBoundedRuntimeWriteCalldata(rawData) {
4511
+ decodeRuntimeWrite(rawData);
4512
+ }
4513
+ const MAX_EVM_DOCUMENT_PATH_UTF8_BYTES = 1024;
4514
+ function canonicalUtf8ByteLength(value) {
4515
+ let bytes = 0;
4516
+ for (let i = 0; i < value.length; i++) {
4517
+ const unit = value.charCodeAt(i);
4518
+ if (unit <= 0x7f)
4519
+ bytes += 1;
4520
+ else if (unit <= 0x7ff)
4521
+ bytes += 2;
4522
+ else if (unit >= 0xd800 && unit <= 0xdbff) {
4523
+ const low = value.charCodeAt(++i);
4524
+ if (!(low >= 0xdc00 && low <= 0xdfff))
4525
+ throw new Error(`Invalid EVM write path: ${value}`);
4526
+ bytes += 4;
4527
+ }
4528
+ else if (unit >= 0xdc00 && unit <= 0xdfff) {
4529
+ throw new Error(`Invalid EVM write path: ${value}`);
4530
+ }
4531
+ else
4532
+ bytes += 3;
4533
+ }
4534
+ return bytes;
4535
+ }
4536
+ function normalizedPathSegments(path) {
4537
+ if (typeof path !== 'string' || path.length === 0 ||
4538
+ canonicalUtf8ByteLength(path) > MAX_EVM_DOCUMENT_PATH_UTF8_BYTES) {
4539
+ throw new Error(`Invalid EVM write path: ${path}`);
4540
+ }
4541
+ const segments = path.split('/');
4542
+ if (segments.length < 2 || segments.length % 2 !== 0 ||
4543
+ segments[0].startsWith('__') ||
4544
+ segments.some((s) => !s || s === '.' || s === '..' || /[\\\u0000-\u001f\u007f]/.test(s))) {
4545
+ throw new Error(`Invalid EVM write path: ${path}`);
4546
+ }
4547
+ return segments;
4548
+ }
4549
+ function bytesBigInt(bytes, signed) {
4550
+ if (bytes.length !== 32)
4551
+ throw new Error('EVM field value is not one ABI word');
4552
+ let value = BIGINT_ZERO;
4553
+ for (const b of bytes)
4554
+ value = (value << BIGINT_EIGHT) | BigInt(b);
4555
+ if (signed && (value & (BIGINT_ONE << BigInt(255))) !== BIGINT_ZERO)
4556
+ value -= BIGINT_ONE << BigInt(256);
4557
+ return value;
4558
+ }
4559
+ function decodedAbiString(bytes) {
4560
+ const reader = new AbiReader(bytes);
4561
+ const at = reader.offset(0, 0, 32, 'field string');
4562
+ const value = reader.string(at, 'field string');
4563
+ reader.finish();
4564
+ return value;
4565
+ }
4566
+ function assertSetValue(field, expected) {
4567
+ if (field.op !== 0)
4568
+ throw new Error(`EVM field ${field.name} changes the requested SET action`);
4569
+ if (typeof expected === 'boolean') {
4570
+ if (field.ftype !== 3 || bytesBigInt(field.value, false) !== (expected ? BIGINT_ONE : BIGINT_ZERO))
4571
+ throw new Error(`EVM field ${field.name} value mismatch`);
4572
+ return;
4573
+ }
4574
+ if (typeof expected === 'number') {
4575
+ if (!Number.isFinite(expected))
4576
+ throw new Error(`EVM field ${field.name} is not finite`);
4577
+ if (Number.isInteger(expected)) {
4578
+ if (!Number.isSafeInteger(expected)) {
4579
+ throw new Error(`EVM field ${field.name} integer must be a safe number or canonical decimal string`);
4580
+ }
4581
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4582
+ if (actual === null || actual !== BigInt(expected))
4583
+ throw new Error(`EVM field ${field.name} value mismatch`);
4584
+ return;
4585
+ }
4586
+ if (field.ftype !== 4 || decodedAbiString(field.value) !== expected.toString())
4587
+ throw new Error(`EVM field ${field.name} value mismatch`);
4588
+ return;
4589
+ }
4590
+ if (typeof expected === 'bigint') {
4591
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4592
+ if (actual === null || actual !== expected)
4593
+ throw new Error(`EVM field ${field.name} value mismatch`);
4594
+ return;
4595
+ }
4596
+ if (typeof expected === 'string') {
4597
+ if (field.ftype === 4 && decodedAbiString(field.value) === expected)
4598
+ return;
4599
+ if ((field.ftype === 1 || field.ftype === 2) && expected.length <= 20 && /^-?(?:0|[1-9][0-9]*)$/.test(expected)) {
4600
+ const exact = BigInt(expected);
4601
+ if (exact >= BigInt(Number.MIN_SAFE_INTEGER) && exact <= BigInt(Number.MAX_SAFE_INTEGER)) {
4602
+ throw new Error(`EVM field ${field.name} safe integer must be supplied as a number`);
4603
+ }
4604
+ const inRange = field.ftype === 1
4605
+ ? exact >= BIGINT_ZERO && exact <= ((BIGINT_ONE << BigInt(64)) - BIGINT_ONE)
4606
+ : exact >= -(BIGINT_ONE << BigInt(63)) && exact <= ((BIGINT_ONE << BigInt(63)) - BIGINT_ONE);
4607
+ const actual = bytesBigInt(field.value, field.ftype === 2);
4608
+ if (inRange && actual === exact)
4609
+ return;
4610
+ throw new Error(`EVM field ${field.name} value mismatch`);
4611
+ }
4612
+ if (field.ftype === 5 && normalizeEvmAddress(expected)) {
4613
+ const word = field.value;
4614
+ if (word.length === 32 && word.slice(0, 12).every((b) => b === 0)) {
4615
+ const actual = `0x${Array.from(word.slice(12)).map((b) => b.toString(16).padStart(2, '0')).join('')}`;
4616
+ if (actual === normalizeEvmAddress(expected))
4617
+ return;
4618
+ }
4619
+ }
4620
+ throw new Error(`EVM field ${field.name} value mismatch`);
4621
+ }
4622
+ if (expected === undefined || expected === null) {
4623
+ throw new Error(`EVM field ${field.name} cannot be represented safely on EVM; use a document delete or omit the field`);
4624
+ }
4625
+ const json = JSON.stringify(expected);
4626
+ if (typeof json !== 'string' || field.ftype !== 4 || decodedAbiString(field.value) !== json) {
4627
+ throw new Error(`EVM field ${field.name} value mismatch`);
4628
+ }
4629
+ }
4630
+ function assertField(field, expected) {
4631
+ if (expected === null) {
4632
+ if (field.op !== 2 || field.ftype < 1 || field.ftype > 5 || field.value.length !== 0) {
4633
+ throw new Error(`EVM field ${field.name} deletion mismatch`);
4634
+ }
4635
+ return;
4636
+ }
4637
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
4638
+ const operation = expected.operation;
4639
+ if (operation === 'increment') {
4640
+ const delta = expected.value;
4641
+ const actual = field.ftype === 1 || field.ftype === 2
4642
+ ? bytesBigInt(field.value, true)
4643
+ : null;
4644
+ if (!Number.isSafeInteger(delta) || field.op !== 3 || actual === null || actual !== BigInt(delta)) {
4645
+ throw new Error(`EVM field ${field.name} increment mismatch`);
4646
+ }
4647
+ return;
4648
+ }
4649
+ if (operation === 'time' && expected.value === 'now') {
4650
+ if (field.op !== 4 || (field.ftype !== 1 && field.ftype !== 2) || field.value.length !== 0) {
4651
+ throw new Error(`EVM field ${field.name} timestamp mismatch`);
4652
+ }
4653
+ return;
4654
+ }
4655
+ }
4656
+ assertSetValue(field, expected);
4657
+ }
4658
+ function assertOp(decoded, path, document) {
4659
+ const expectedSegments = normalizedPathSegments(path);
4660
+ if (decoded.pathSegments.length !== expectedSegments.length || decoded.pathSegments.some((s, i) => s !== expectedSegments[i])) {
4661
+ throw new Error(`EVM transaction path does not match requested write: ${path}`);
4662
+ }
4663
+ if (decoded.isDelete !== (document === null))
4664
+ throw new Error(`EVM transaction action does not match requested write: ${path}`);
4665
+ if (document === null) {
4666
+ if (decoded.fields.length !== 0)
4667
+ throw new Error(`EVM delete unexpectedly carries fields: ${path}`);
4668
+ return;
4669
+ }
4670
+ if (!document || typeof document !== 'object' || Array.isArray(document))
4671
+ throw new Error(`EVM document must be a plain object or null: ${path}`);
4672
+ const expectedEntries = Object.entries(document);
4673
+ if (expectedEntries.some(([name]) => name.startsWith('_')))
4674
+ throw new Error(`Reserved EVM field reached transaction validation: ${path}`);
4675
+ const expected = new Map(expectedEntries);
4676
+ const seen = new Set();
4677
+ for (const field of decoded.fields) {
4678
+ if (!field.name || seen.has(field.name) || !expected.has(field.name))
4679
+ throw new Error(`EVM transaction contains unexpected or duplicate field ${field.name}`);
4680
+ seen.add(field.name);
4681
+ assertField(field, expected.get(field.name));
4682
+ }
4683
+ const missing = [...expected.keys()].filter((name) => !seen.has(name));
4684
+ if (missing.length > 0)
4685
+ throw new Error(`EVM transaction omits requested fields: ${missing.join(', ')}`);
4686
+ }
4687
+ function zeroWei(raw) {
4688
+ if (raw === undefined || raw === null || raw === '')
4689
+ return true;
4690
+ if (typeof raw !== 'string' && typeof raw !== 'number')
4691
+ return false;
4692
+ try {
4693
+ return BigInt(raw) === BIGINT_ZERO;
4694
+ }
4695
+ catch (_a) {
4696
+ return false;
4697
+ }
4698
+ }
4699
+ /** Verify the untrusted 202 payload is exactly the governed write the app asked for. */
4700
+ function assertEvmTransactionMatchesWrite(transaction, context) {
4701
+ var _a, _b;
4702
+ if (!transaction || !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0) {
4703
+ throw new Error('EVM transaction chainId is required');
4704
+ }
4705
+ const chainId = transaction.chainId;
4706
+ const policy = context.policy;
4707
+ if (!policy)
4708
+ throw new Error('EVM transaction signing requires an explicit runtime/chain policy');
4709
+ if (policy.chainId !== undefined && policy.chainId !== chainId)
4710
+ throw new Error(`EVM transaction chain ${chainId} does not match configured chain ${policy.chainId}`);
4711
+ 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);
4712
+ const normalizedRuntime = normalizeEvmAddress(expectedRuntime);
4713
+ if (!normalizedRuntime)
4714
+ throw new Error(`No trusted BoundedRuntime address configured for EVM chain ${chainId}`);
4715
+ if (normalizeEvmAddress(transaction.to) !== normalizedRuntime)
4716
+ throw new Error('EVM transaction target is not the configured BoundedRuntime');
4717
+ if (!zeroWei(transaction.value))
4718
+ throw new Error('BoundedRuntime.setDocuments must not transfer native value');
4719
+ if (transaction.nonce !== undefined)
4720
+ throw new Error('Server-supplied EVM nonce is not accepted');
4721
+ const decoded = decodeRuntimeWrite(transaction.data);
4722
+ if (decoded.appId !== context.appId)
4723
+ throw new Error('EVM transaction appId does not match configured appId');
4724
+ if (decoded.ops.length !== context.documents.length)
4725
+ throw new Error('EVM transaction operation count does not match requested writes');
4726
+ // The worker's contract ABI groups upserts first and document deletes second.
4727
+ const expectedOps = [
4728
+ ...context.documents.filter((entry) => entry.document !== null),
4729
+ ...context.documents.filter((entry) => entry.document === null),
4730
+ ];
4731
+ const paths = new Set();
4732
+ for (let i = 0; i < expectedOps.length; i++) {
4733
+ const expected = expectedOps[i];
4734
+ const canonicalPath = normalizedPathSegments(expected.destinationPath).join('/');
4735
+ if (paths.has(canonicalPath))
4736
+ throw new Error(`Duplicate EVM write path is not allowed: ${canonicalPath}`);
4737
+ paths.add(canonicalPath);
4738
+ assertOp(decoded.ops[i], canonicalPath, expected.document);
4739
+ }
4740
+ }
4741
+
4112
4742
  var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
4113
4743
  var t = {};
4114
4744
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -4135,12 +4765,21 @@ class InsufficientBalanceError extends Error {
4135
4765
  this.deficitSol = deficitSol;
4136
4766
  }
4137
4767
  }
4138
- // Cache for get responses
4139
4768
  const getCache = {};
4140
- // Track in-flight requests to coalesce multiple identical requests
4141
4769
  const pendingRequests = {};
4142
4770
  // Default cache TTL (500ms = 0.5 seconds)
4143
4771
  const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
4772
+ function getReadCacheEntry(cacheKey, principalValue) {
4773
+ var _a;
4774
+ return (_a = getCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.find(entry => entry.principalValue === principalValue);
4775
+ }
4776
+ function setReadCacheEntry(cacheKey, principalValue, data, expiresAt) {
4777
+ var _a;
4778
+ const bucket = (_a = getCache[cacheKey]) !== null && _a !== void 0 ? _a : [];
4779
+ const reusableEntries = bucket.filter(entry => (entry.principalValue !== principalValue && entry.expiresAt >= Date.now()));
4780
+ reusableEntries.push({ data, expiresAt, principalValue });
4781
+ getCache[cacheKey] = reusableEntries;
4782
+ }
4144
4783
  // Last time we cleaned up the cache
4145
4784
  let lastCacheCleanup = Date.now();
4146
4785
  let uncacheableReadKeyCounter = 0;
@@ -4236,6 +4875,21 @@ function hashForKey$1(value) {
4236
4875
  }
4237
4876
  return h.toString(36);
4238
4877
  }
4878
+ // Memoize the compact principal hash per exact credential string. Every read
4879
+ // hashed the entire (~1KB) bearer/idToken on every call; a session presents
4880
+ // the same handful of credentials for its lifetime. Bounded size — a rotation
4881
+ // simply inserts the new token and stale entries fall off on clear.
4882
+ const principalHashCache = new Map();
4883
+ function hashForKeyCached(value) {
4884
+ const hit = principalHashCache.get(value);
4885
+ if (hit !== undefined)
4886
+ return hit;
4887
+ const h = hashForKey$1(value);
4888
+ if (principalHashCache.size >= 50)
4889
+ principalHashCache.clear();
4890
+ principalHashCache.set(value, h);
4891
+ return h;
4892
+ }
4239
4893
  function hasAuthHeader(headers) {
4240
4894
  if (!headers)
4241
4895
  return false;
@@ -4246,10 +4900,10 @@ function authValueFromHeaders(headers) {
4246
4900
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4247
4901
  }
4248
4902
  function principalFromAuthValue(authValue) {
4249
- return authValue ? `h${hashForKey$1(authValue)}` : null;
4903
+ return authValue ? `h${hashForKeyCached(authValue)}` : null;
4250
4904
  }
4251
4905
  function principalFromIdToken$1(idToken) {
4252
- return idToken ? `t${hashForKey$1(idToken)}` : null;
4906
+ return idToken ? `t${hashForKeyCached(idToken)}` : null;
4253
4907
  }
4254
4908
  function uncacheableReadKey(appId, scope) {
4255
4909
  uncacheableReadKeyCounter += 1;
@@ -4271,32 +4925,34 @@ async function getReadPrincipalKey(overrides) {
4271
4925
  // makeApiRequest applies overrides.headers AFTER its computed auth header, so
4272
4926
  // caller-supplied Authorization is the real request auth when present.
4273
4927
  if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
4274
- const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
4928
+ const authValue = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
4929
+ const principal = principalFromAuthValue(authValue);
4275
4930
  return principal
4276
- ? { key: `${appId}:${principal}`, cacheable: true }
4277
- : { key: uncacheableReadKey(appId, 'h'), cacheable: false };
4931
+ ? { key: `${appId}:${principal}`, cacheable: true, principalValue: authValue }
4932
+ : { key: uncacheableReadKey(appId, 'h'), cacheable: false, principalValue: null };
4278
4933
  }
4279
4934
  // Per-request auth-header override (wallet client). Key by the exact opaque
4280
4935
  // header it produces, not decoded claims or the unverified _walletAddress hint.
4281
4936
  if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
4282
4937
  try {
4283
4938
  const headers = await overrides._getAuthHeaders();
4284
- const principal = principalFromAuthValue(authValueFromHeaders(headers));
4939
+ const authValue = authValueFromHeaders(headers);
4940
+ const principal = principalFromAuthValue(authValue);
4285
4941
  return principal
4286
- ? { key: `${appId}:o${principal}`, cacheable: true }
4287
- : { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4942
+ ? { key: `${appId}:o${principal}`, cacheable: true, principalValue: authValue }
4943
+ : { key: uncacheableReadKey(appId, 'o'), cacheable: false, principalValue: null };
4288
4944
  }
4289
4945
  catch (_a) {
4290
4946
  // If we can't resolve the override identity, do not read/write cache.
4291
- return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
4947
+ return { key: uncacheableReadKey(appId, 'o'), cacheable: false, principalValue: null };
4292
4948
  }
4293
4949
  }
4294
4950
  // Ambient session principal.
4295
4951
  const idToken = await getIdToken(config.isServer);
4296
4952
  const principal = principalFromIdToken$1(idToken);
4297
4953
  return principal
4298
- ? { key: `${appId}:${principal}`, cacheable: true }
4299
- : { key: uncacheableReadKey(appId, 'a'), cacheable: false };
4954
+ ? { key: `${appId}:${principal}`, cacheable: true, principalValue: idToken !== null && idToken !== void 0 ? idToken : null }
4955
+ : { key: uncacheableReadKey(appId, 'a'), cacheable: false, principalValue: null };
4300
4956
  }
4301
4957
  /**
4302
4958
  * Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
@@ -4577,6 +5233,7 @@ async function search(path, query, opts = {}) {
4577
5233
  return response.data;
4578
5234
  }
4579
5235
  async function get(path, opts = {}) {
5236
+ var _a, _b;
4580
5237
  try {
4581
5238
  let normalizedPath = path.startsWith("/") ? path.slice(1) : path;
4582
5239
  // Remove last '*' if it exists in the path
@@ -4587,7 +5244,8 @@ async function get(path, opts = {}) {
4587
5244
  throw new Error("Invalid path provided.");
4588
5245
  }
4589
5246
  // Create cache key combining path, prompt, filter, sort, includeSubPaths,
4590
- // shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
5247
+ // shape, limit, cursor — and (H1) the caller's appId + compact principal
5248
+ // bucket. Exact opaque credential equality is checked inside that bucket,
4591
5249
  // so a private read cached for one user is never served to another in a
4592
5250
  // shared process / SSR worker / browser login-switch. The cache is opt-in
4593
5251
  // and disabled for no-auth reads, which get an uncacheable unique key.
@@ -4602,20 +5260,25 @@ async function get(path, opts = {}) {
4602
5260
  const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
4603
5261
  const now = Date.now();
4604
5262
  // Check for valid cache entry when the caller explicitly opted in.
4605
- if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4606
- return getCache[cacheKey].data;
5263
+ const cachedEntry = getReadCacheEntry(cacheKey, principalKey.principalValue);
5264
+ if (cacheEnabled &&
5265
+ cachedEntry &&
5266
+ now < cachedEntry.expiresAt) {
5267
+ return cachedEntry.data;
4607
5268
  }
4608
5269
  // If we're bypassing cache, we should still coalesce identical requests
4609
5270
  // but need a different key to avoid interfering with regular requests
4610
5271
  const requestKey = opts.bypassCache ? `bypass:${cacheKey}` : cacheKey;
4611
- // Check if there's already an in-flight request for this key
4612
- // Fix for TS error - use hasOwnProperty to explicitly check if key exists
4613
- if (Object.prototype.hasOwnProperty.call(pendingRequests, requestKey)) {
4614
- // Return the existing promise to coalesce requests
4615
- return pendingRequests[requestKey];
5272
+ // The compact principal hash is only an index bucket. Coalesce only when
5273
+ // the exact opaque credential also matches, so collisions stay isolated.
5274
+ const existingPending = (_a = pendingRequests[requestKey]) === null || _a === void 0 ? void 0 : _a.find(entry => entry.principalValue === principalKey.principalValue);
5275
+ if (existingPending) {
5276
+ return existingPending.promise;
4616
5277
  }
4617
5278
  // Create a new request promise and store it
4618
- const requestPromise = (async () => {
5279
+ let requestPromise;
5280
+ requestPromise = (async () => {
5281
+ var _a;
4619
5282
  try {
4620
5283
  const pathIsDocument = normalizedPath.split("/").length % 2 === 0;
4621
5284
  // Cache miss or bypass - proceed with HTTP API request
@@ -4646,10 +5309,7 @@ async function get(path, opts = {}) {
4646
5309
  const responseData = normalizeReadResult(response.data, pathIsDocument);
4647
5310
  // Cache the response only when explicitly requested and principal-bound.
4648
5311
  if (cacheEnabled) {
4649
- getCache[cacheKey] = {
4650
- data: responseData,
4651
- expiresAt: now + GET_CACHE_TTL
4652
- };
5312
+ setReadCacheEntry(cacheKey, principalKey.principalValue, responseData, now + GET_CACHE_TTL);
4653
5313
  // Periodically clean up expired cache entries (every 5 seconds)
4654
5314
  if (now - lastCacheCleanup > 5000) {
4655
5315
  cleanupExpiredCache();
@@ -4660,12 +5320,21 @@ async function get(path, opts = {}) {
4660
5320
  return responseData;
4661
5321
  }
4662
5322
  finally {
4663
- // Remove this request from pendingRequests regardless of success/failure
4664
- delete pendingRequests[requestKey];
5323
+ // Remove only this exact request. A colliding principal can legitimately
5324
+ // have another request in the same compact-hash bucket.
5325
+ const bucket = pendingRequests[requestKey];
5326
+ const index = (_a = bucket === null || bucket === void 0 ? void 0 : bucket.findIndex(entry => entry.promise === requestPromise)) !== null && _a !== void 0 ? _a : -1;
5327
+ if (index >= 0)
5328
+ bucket.splice(index, 1);
5329
+ if ((bucket === null || bucket === void 0 ? void 0 : bucket.length) === 0)
5330
+ delete pendingRequests[requestKey];
4665
5331
  }
4666
5332
  })();
4667
- // Store the promise so other calls can join it
4668
- pendingRequests[requestKey] = requestPromise;
5333
+ // Store the promise so only callers with the exact same credential join it.
5334
+ ((_b = pendingRequests[requestKey]) !== null && _b !== void 0 ? _b : (pendingRequests[requestKey] = [])).push({
5335
+ principalValue: principalKey.principalValue,
5336
+ promise: requestPromise,
5337
+ });
4669
5338
  // Return the promise
4670
5339
  return requestPromise;
4671
5340
  }
@@ -4677,9 +5346,12 @@ async function get(path, opts = {}) {
4677
5346
  function cleanupExpiredCache() {
4678
5347
  const now = Date.now();
4679
5348
  Object.keys(getCache).forEach(key => {
4680
- if (getCache[key] && getCache[key].expiresAt < now) {
5349
+ var _a, _b;
5350
+ const activeEntries = (_b = (_a = getCache[key]) === null || _a === void 0 ? void 0 : _a.filter(entry => entry.expiresAt >= now)) !== null && _b !== void 0 ? _b : [];
5351
+ if (activeEntries.length > 0)
5352
+ getCache[key] = activeEntries;
5353
+ else
4681
5354
  delete getCache[key];
4682
- }
4683
5355
  });
4684
5356
  lastCacheCleanup = now;
4685
5357
  }
@@ -4941,8 +5613,11 @@ async function getMany(paths, opts = {}) {
4941
5613
  for (let i = 0; i < normalizedPaths.length; i++) {
4942
5614
  const normalizedPath = normalizedPaths[i];
4943
5615
  const cacheKey = `${principalKey.key}|${normalizedPath}:`;
4944
- if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
4945
- results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
5616
+ const cachedEntry = getReadCacheEntry(cacheKey, principalKey.principalValue);
5617
+ if (cacheEnabled &&
5618
+ cachedEntry &&
5619
+ now < cachedEntry.expiresAt) {
5620
+ results[i] = { path: normalizedPath, data: cachedEntry.data };
4946
5621
  }
4947
5622
  else {
4948
5623
  uncachedIndices.push(i);
@@ -4979,10 +5654,7 @@ async function getMany(paths, opts = {}) {
4979
5654
  results[originalIndex] = normalizedResult;
4980
5655
  if (!normalizedResult.error && cacheEnabled) {
4981
5656
  const cacheKey = `${principalKey.key}|${normalizedPath}:`;
4982
- getCache[cacheKey] = {
4983
- data: normalizedResult.data,
4984
- expiresAt: now + GET_CACHE_TTL
4985
- };
5657
+ setReadCacheEntry(cacheKey, principalKey.principalValue, normalizedResult.data, now + GET_CACHE_TTL);
4986
5658
  }
4987
5659
  }
4988
5660
  else {
@@ -5112,13 +5784,12 @@ async function runExpressionMany(many) {
5112
5784
  * collection's policy `delete` rule and broadcasts a delete to subscribers.
5113
5785
  */
5114
5786
  async function set(path, document, options) {
5115
- const result = await setMany([{ path, document }], options);
5116
- // Clear cache entries that might be affected by this update
5117
- clearCacheByPrefix(path);
5118
- return result;
5787
+ // setMany already invalidates the read cache for the affected document and
5788
+ // collection paths — no second sweep needed here.
5789
+ return setMany([{ path, document }], options);
5119
5790
  }
5120
5791
  async function setMany(many, options) {
5121
- var _a, _b, _c, _d, _e, _f, _g;
5792
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5122
5793
  // Returns the data that was set, or undefined if the document was already set.
5123
5794
  try {
5124
5795
  const config = await getConfig();
@@ -5174,6 +5845,9 @@ async function setMany(many, options) {
5174
5845
  // the API did nothing and gave us the required info to run this transaction.
5175
5846
  const { transactions, offchainTransaction } = setResponse.data;
5176
5847
  // Handle offchain transaction flow (for 'offchain' protocol apps)
5848
+ if (offchainTransaction && Array.isArray(transactions) && transactions.length > 0) {
5849
+ throw new Error('Ambiguous write response mixes offchain and onchain transaction targets');
5850
+ }
5177
5851
  if (offchainTransaction) {
5178
5852
  const transactionResult = await handleOffchainTransaction(offchainTransaction, authProvider, options);
5179
5853
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
@@ -5183,9 +5857,35 @@ async function setMany(many, options) {
5183
5857
  throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
5184
5858
  }
5185
5859
  const curTx = transactions[0];
5860
+ const hasEvmTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.evmTransaction) != null;
5861
+ const hasSerializedSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.serializedTransaction) != null;
5862
+ const hasBuiltSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.transactionArgs) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.remainingAccounts) != null ||
5863
+ (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;
5864
+ if ((hasEvmTarget && (hasSerializedSolanaTarget || hasBuiltSolanaTarget)) ||
5865
+ (hasSerializedSolanaTarget && hasBuiltSolanaTarget)) {
5866
+ throw new Error('Ambiguous on-chain write response mixes EVM and/or Solana transaction target formats');
5867
+ }
5186
5868
  let transactionResult;
5187
5869
  const expectedWritePaths = documents.map(d => d.destinationPath);
5188
- if (curTx.serializedTransaction) {
5870
+ // EVM onchain write: the worker returns a PRE-BUILT EIP-1559 tx on
5871
+ // `transactions[0].evmTransaction` (the EVM twin of Solana's pre-built
5872
+ // `serializedTransaction`). WORKER 202 CONTRACT (field name + shape the
5873
+ // worker side must emit to match):
5874
+ // { transactions: [ { evmTransaction: {
5875
+ // to, data, value?, chainId, gas?, maxFeePerGas?, maxPriorityFeePerGas?, nonce?
5876
+ // } } ] }
5877
+ // We hand it to the EVM auth provider's runTransaction(evmTx, undefined, options)
5878
+ // (eth_sendTransaction — the wallet signs + submits). syncItems below is
5879
+ // identical to the Solana path.
5880
+ if (curTx.evmTransaction) {
5881
+ assertEvmTransactionMatchesWrite(curTx.evmTransaction, {
5882
+ appId: config.appId,
5883
+ documents,
5884
+ 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,
5885
+ });
5886
+ transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
5887
+ }
5888
+ else if (curTx.serializedTransaction) {
5189
5889
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5190
5890
  }
5191
5891
  else {
@@ -5209,7 +5909,7 @@ async function setMany(many, options) {
5209
5909
  else if (setResponse.data &&
5210
5910
  typeof setResponse.data === 'object' &&
5211
5911
  setResponse.data.success === true) {
5212
- const _h = setResponse.data, { success: _success } = _h, rest = __rest$1(_h, ["success"]);
5912
+ const _l = setResponse.data, { success: _success } = _l, rest = __rest$1(_l, ["success"]);
5213
5913
  return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
5214
5914
  }
5215
5915
  else {
@@ -5309,6 +6009,14 @@ async function setMany(many, options) {
5309
6009
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5310
6010
  return transactionResult;
5311
6011
  }
6012
+ // EVM onchain write: hand the worker's pre-built EIP-1559 tx to the EVM auth
6013
+ // provider, which eth_sendTransaction-s it (the wallet signs + submits in one
6014
+ // step). Mirrors handlePreBuiltTransaction's shape but routes through the EVM
6015
+ // slot of runTransaction (Solana slot undefined). shouldSubmitTx is honored by
6016
+ // the provider itself.
6017
+ async function handleEvmTransaction(evmTransaction, authProvider, options) {
6018
+ return authProvider.runTransaction(evmTransaction, undefined, options);
6019
+ }
5312
6020
  async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5313
6021
  var _a, _b, _c;
5314
6022
  const config = await getConfig();
@@ -5549,10 +6257,37 @@ var operations = /*#__PURE__*/Object.freeze({
5549
6257
  // ============ Global State ============
5550
6258
  // One connection per routing target (appId, or appId#room#<collection>/<roomId>)
5551
6259
  const connections = new Map();
6260
+ // Coalesce concurrent first-use/auth-preflight calls for the same routing target.
6261
+ // React commonly mounts several subscriptions in one render; without this map,
6262
+ // every call can pass the empty `connections` check, await auth, and create an
6263
+ // orphan ReconnectingWebSocket before the first call publishes its connection.
6264
+ // Keyed by the identity-bucket lease key (see connectionLeaseKey) so two distinct
6265
+ // wallet tokens never share one in-flight socket even under a compact-hash collision.
6266
+ const connectionFlights = new Map();
6267
+ // A subscriber holds a routing-target lease from connection acquisition until
6268
+ // its callback/subscription is registered. The last old-route unsubscribe must
6269
+ // not close a connection already captured by a new route awaiting auth. Keyed by
6270
+ // the same identity-bucket lease key as connectionFlights.
6271
+ const connectionAcquisitions = new Map();
6272
+ // Compact identity hashes remain useful bounded-size indexes, but collisions
6273
+ // share a bucket instead of sharing authority. Each exact bearer keeps its own
6274
+ // cache entry so collision isolation does not evict another principal's cache.
5552
6275
  const responseCache = new Map();
5553
6276
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
5554
6277
  const TOKEN_REFRESH_BUFFER = 10 * 60 * 1000; // Refresh token 10 minutes before expiry
5555
6278
  const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000; // Check token freshness every 5 minutes
6279
+ function getResponseCacheEntry(cacheKey, identityToken) {
6280
+ var _a;
6281
+ return (_a = responseCache.get(cacheKey)) === null || _a === void 0 ? void 0 : _a.find(entry => entry.identityToken === identityToken);
6282
+ }
6283
+ function setResponseCacheEntry(cacheKey, identityToken, data) {
6284
+ var _a;
6285
+ const now = Date.now();
6286
+ const bucket = (_a = responseCache.get(cacheKey)) !== null && _a !== void 0 ? _a : [];
6287
+ const reusableEntries = bucket.filter(entry => (entry.identityToken !== identityToken && now - entry.timestamp < CACHE_TTL));
6288
+ reusableEntries.push({ data, timestamp: now, identityToken });
6289
+ responseCache.set(cacheKey, reusableEntries);
6290
+ }
5556
6291
  // ============ WebSocket Config ============
5557
6292
  const BASE_MIN_RECONNECT_DELAY_MS = 1000;
5558
6293
  const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
@@ -5560,6 +6295,10 @@ const MAX_RECONNECT_DELAY_MS = 300000;
5560
6295
  const RECONNECT_DELAY_GROW_FACTOR = 1.8;
5561
6296
  const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
5562
6297
  const WS_AUTH_EXPIRED_CODE = 'auth_expired';
6298
+ // React runs old-route effect cleanup before new-route effects. Keep an
6299
+ // otherwise-idle socket for one short handoff window so the new route can
6300
+ // cancel teardown and reuse it; genuinely idle sockets still close promptly.
6301
+ const IDLE_CONNECTION_CLOSE_GRACE_MS = 1000;
5563
6302
  const WS_CONFIG = {
5564
6303
  // Keep retrying indefinitely so long outages recover without page refresh.
5565
6304
  maxRetries: Infinity,
@@ -5590,12 +6329,12 @@ function hashForKey(value) {
5590
6329
  }
5591
6330
  /**
5592
6331
  * SECURITY (H1): Response-cache keys must be scoped to the caller's identity, not
5593
- * just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
5594
- * a private subscription snapshot cached for one user from being delivered to a
5595
- * different user who subscribes with the same options (shared process / SSR worker
5596
- * / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
5597
- * before entries are read or written; no-auth subscriptions never populate an
5598
- * implicit anonymous cache bucket.
6332
+ * just the path/filter/shape. The compact `identity` prefix selects a bucket;
6333
+ * exact opaque bearer equality is required inside it before reuse. Together they
6334
+ * keep a private snapshot from being delivered to a different user with the same
6335
+ * options (shared process / SSR worker / browser login-switch; this cache has a
6336
+ * 5-minute TTL). Callers must opt in before entries are read or written; no-auth
6337
+ * subscriptions never populate an implicit anonymous cache bucket.
5599
6338
  */
5600
6339
  function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5601
6340
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
@@ -5633,16 +6372,16 @@ function livePrincipalFromIdToken(idToken) {
5633
6372
  }
5634
6373
  function authSnapshotFromToken(idToken) {
5635
6374
  return {
5636
- tokenFingerprint: principalFromIdToken(idToken),
6375
+ tokenValue: idToken !== null && idToken !== void 0 ? idToken : null,
5637
6376
  principal: livePrincipalFromIdToken(idToken),
5638
6377
  };
5639
6378
  }
5640
6379
  function connectionMatchesAuthToken(connection, idToken) {
5641
6380
  const current = authSnapshotFromToken(idToken);
5642
- if (!current.tokenFingerprint) {
5643
- return connection.authTokenFingerprint === null && connection.authPrincipal === null;
6381
+ if (!current.tokenValue) {
6382
+ return connection.authTokenValue === null && connection.authPrincipal === null;
5644
6383
  }
5645
- if (connection.authTokenFingerprint && connection.authTokenFingerprint === current.tokenFingerprint) {
6384
+ if (connection.authTokenValue && connection.authTokenValue === current.tokenValue) {
5646
6385
  return true;
5647
6386
  }
5648
6387
  return !!connection.authPrincipal && !!current.principal && connection.authPrincipal === current.principal;
@@ -5651,7 +6390,7 @@ async function canSendAuthenticatedLiveIntent(connection, isServer) {
5651
6390
  const currentToken = await getIdToken(isServer);
5652
6391
  if (!currentToken)
5653
6392
  return false;
5654
- if (!connection.authTokenFingerprint && !connection.authPrincipal)
6393
+ if (!connection.authTokenValue && !connection.authPrincipal)
5655
6394
  return false;
5656
6395
  return connectionMatchesAuthToken(connection, currentToken);
5657
6396
  }
@@ -5664,19 +6403,19 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5664
6403
  const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
5665
6404
  const principal = principalFromIdToken(bearer);
5666
6405
  return principal
5667
- ? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
5668
- : { key: `${effectiveAppId}:oanon`, cacheable: false };
6406
+ ? { key: `${effectiveAppId}:o${principal}`, cacheable: true, token: bearer }
6407
+ : { key: `${effectiveAppId}:oanon`, cacheable: false, token: null };
5669
6408
  }
5670
6409
  catch (_a) {
5671
6410
  // Couldn't resolve the override identity — do not use response cache.
5672
- return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
6411
+ return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false, token: null };
5673
6412
  }
5674
6413
  }
5675
6414
  const idToken = await getIdToken(isServer);
5676
6415
  const principal = principalFromIdToken(idToken);
5677
6416
  return principal
5678
- ? { key: `${effectiveAppId}:${principal}`, cacheable: true }
5679
- : { key: `${effectiveAppId}:anon`, cacheable: false };
6417
+ ? { key: `${effectiveAppId}:${principal}`, cacheable: true, token: idToken !== null && idToken !== void 0 ? idToken : null }
6418
+ : { key: `${effectiveAppId}:anon`, cacheable: false, token: null };
5680
6419
  }
5681
6420
  /** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
5682
6421
  function bearerFromAuthHeaders(headers) {
@@ -5732,7 +6471,7 @@ function failConnectionAuth(connection, error) {
5732
6471
  connection.authFailure = error;
5733
6472
  connection.pendingAuthToken = null;
5734
6473
  connection.pendingAuthSnapshot = null;
5735
- connection.authTokenFingerprint = null;
6474
+ connection.authTokenValue = null;
5736
6475
  connection.authPrincipal = null;
5737
6476
  connection.isConnecting = false;
5738
6477
  connection.isConnected = false;
@@ -5743,6 +6482,7 @@ function failConnectionAuth(connection, error) {
5743
6482
  connection.pendingSubscriptions.clear();
5744
6483
  for (const subscription of connection.subscriptions.values()) {
5745
6484
  notifyErrorCallbacks(subscription, error);
6485
+ rejectSubscriptionRefresh(subscription, error);
5746
6486
  }
5747
6487
  }
5748
6488
  function scheduleTokenRefresh(connection, isServer) {
@@ -5935,7 +6675,136 @@ function replaySubscriptions(connection) {
5935
6675
  sendSubscribe(connection, sub);
5936
6676
  }
5937
6677
  }
5938
- async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey) {
6678
+ /**
6679
+ * Derive the stable identity-bucket lease key used to coalesce (singleflight)
6680
+ * and refcount (acquisition / idle-close) connection use. Ambient app/room
6681
+ * connections key by their routing target; wallet-scoped connections pin the
6682
+ * exact bearer so two distinct tokens never share a flight or a lease, even
6683
+ * when their compact `#id#<principalKey>` hashes collide.
6684
+ */
6685
+ function connectionLeaseKey(appId, routePath, principalKey, principalToken) {
6686
+ const roomKey = roomKeyFromRoutePath(routePath);
6687
+ const base = roomKey ? `${appId}#room#${roomKey}` : appId;
6688
+ if (!principalKey)
6689
+ return base;
6690
+ const bucket = `${base}#id#${principalKey}`;
6691
+ return principalToken ? `${bucket}#tok#${principalToken}` : bucket;
6692
+ }
6693
+ function cancelIdleConnectionClose(connection) {
6694
+ if (!connection.idleCloseTimer)
6695
+ return;
6696
+ clearTimeout(connection.idleCloseTimer);
6697
+ connection.idleCloseTimer = null;
6698
+ }
6699
+ function closeConnectionNow(connection) {
6700
+ if (connections.get(connection.key) !== connection)
6701
+ return;
6702
+ if (!connection.ws)
6703
+ return;
6704
+ cancelIdleConnectionClose(connection);
6705
+ if (connection.tokenRefreshTimer) {
6706
+ clearInterval(connection.tokenRefreshTimer);
6707
+ connection.tokenRefreshTimer = null;
6708
+ }
6709
+ const ws = connection.ws;
6710
+ connection.ws = null;
6711
+ connection.isConnecting = false;
6712
+ connection.isConnected = false;
6713
+ connection.isAuthenticating = false;
6714
+ const closeError = new Error('WebSocket connection closed');
6715
+ for (const subscription of connection.subscriptions.values()) {
6716
+ rejectSubscriptionRefresh(subscription, closeError);
6717
+ }
6718
+ connections.delete(connection.key);
6719
+ try {
6720
+ ws.close();
6721
+ }
6722
+ catch ( /* already closing */_a) { /* already closing */ }
6723
+ }
6724
+ function scheduleConnectionCloseIfIdle(connection) {
6725
+ var _a;
6726
+ if (connections.get(connection.key) !== connection)
6727
+ return;
6728
+ if (connection.subscriptions.size > 0)
6729
+ return;
6730
+ if (((_a = connectionAcquisitions.get(connection.acquisitionKey)) !== null && _a !== void 0 ? _a : 0) > 0)
6731
+ return;
6732
+ if (!connection.ws || connection.idleCloseTimer)
6733
+ return;
6734
+ connection.idleCloseTimer = setTimeout(() => {
6735
+ var _a;
6736
+ connection.idleCloseTimer = null;
6737
+ if (connection.subscriptions.size > 0)
6738
+ return;
6739
+ if (((_a = connectionAcquisitions.get(connection.acquisitionKey)) !== null && _a !== void 0 ? _a : 0) > 0)
6740
+ return;
6741
+ closeConnectionNow(connection);
6742
+ }, IDLE_CONNECTION_CLOSE_GRACE_MS);
6743
+ }
6744
+ /**
6745
+ * Acquire a connection under a routing-target lease. The lease is refcounted on
6746
+ * the identity-bucket key and held across the caller's auth preflight so a
6747
+ * concurrent old-route teardown cannot close a socket a new route is about to
6748
+ * reuse. The lease is released once the caller has registered its
6749
+ * subscription/callback.
6750
+ */
6751
+ async function acquireConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken) {
6752
+ var _a;
6753
+ const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken);
6754
+ connectionAcquisitions.set(leaseKey, ((_a = connectionAcquisitions.get(leaseKey)) !== null && _a !== void 0 ? _a : 0) + 1);
6755
+ let released = false;
6756
+ let releaseTimer = null;
6757
+ const release = (connection) => {
6758
+ var _a;
6759
+ if (released)
6760
+ return;
6761
+ released = true;
6762
+ if (releaseTimer) {
6763
+ clearTimeout(releaseTimer);
6764
+ releaseTimer = null;
6765
+ }
6766
+ const remaining = ((_a = connectionAcquisitions.get(leaseKey)) !== null && _a !== void 0 ? _a : 1) - 1;
6767
+ if (remaining > 0)
6768
+ connectionAcquisitions.set(leaseKey, remaining);
6769
+ else
6770
+ connectionAcquisitions.delete(leaseKey);
6771
+ if (connection)
6772
+ scheduleConnectionCloseIfIdle(connection);
6773
+ };
6774
+ try {
6775
+ const connection = await getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken);
6776
+ const releaseConnection = () => release(connection);
6777
+ // Registration is synchronous after the caller's await. This fail-safe
6778
+ // releases on the next task even if an option getter/callback throws
6779
+ // before subscribeV2 reaches its explicit release path.
6780
+ releaseTimer = setTimeout(releaseConnection, 0);
6781
+ return { connection, release: releaseConnection };
6782
+ }
6783
+ catch (error) {
6784
+ release();
6785
+ throw error;
6786
+ }
6787
+ }
6788
+ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken) {
6789
+ // Singleflight: concurrent first-use of the same routing target shares one
6790
+ // in-flight connection rather than each creating an orphan socket. Keyed by
6791
+ // the identity-bucket lease key so distinct wallet tokens never coalesce.
6792
+ const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken);
6793
+ const activeFlight = connectionFlights.get(leaseKey);
6794
+ if (activeFlight)
6795
+ return activeFlight;
6796
+ const flight = getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey);
6797
+ connectionFlights.set(leaseKey, flight);
6798
+ try {
6799
+ return await flight;
6800
+ }
6801
+ finally {
6802
+ if (connectionFlights.get(leaseKey) === flight) {
6803
+ connectionFlights.delete(leaseKey);
6804
+ }
6805
+ }
6806
+ }
6807
+ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey) {
5939
6808
  attachBrowserReconnectHooksOnce();
5940
6809
  // A per-room subscription gets its OWN connection routed to the room DO; all
5941
6810
  // other subscriptions share the app-level connection (project DO). The worker
@@ -5946,9 +6815,38 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5946
6815
  // keyed by the wallet identity, so its WS authenticates as that wallet and
5947
6816
  // never shares a connection (or token) with any top-level server caller.
5948
6817
  const base = roomKey ? `${appId}#room#${roomKey}` : appId;
5949
- const connKey = principalKey ? `${base}#id#${principalKey}` : base;
5950
- let connection = connections.get(connKey);
6818
+ const identityBucketKey = principalKey
6819
+ ? `${base}#id#${principalKey}`
6820
+ : base;
6821
+ let connKey = identityBucketKey;
6822
+ let connection;
6823
+ if (principalKey && principalToken) {
6824
+ // The 31-bit hash selects a small bucket only. Exact token equality is
6825
+ // authoritative, and a rare collision gets an independent map slot.
6826
+ for (const [candidateKey, candidate] of connections) {
6827
+ if ((candidateKey === identityBucketKey || candidateKey.startsWith(`${identityBucketKey}#collision#`)) &&
6828
+ candidate.connectionIdentityToken === principalToken) {
6829
+ connKey = candidateKey;
6830
+ connection = candidate;
6831
+ break;
6832
+ }
6833
+ }
6834
+ if (!connection && connections.has(connKey)) {
6835
+ let collisionIndex = 1;
6836
+ while (connections.has(`${identityBucketKey}#collision#${collisionIndex}`))
6837
+ collisionIndex += 1;
6838
+ connKey = `${identityBucketKey}#collision#${collisionIndex}`;
6839
+ }
6840
+ }
6841
+ else {
6842
+ connection = connections.get(connKey);
6843
+ }
5951
6844
  if (connection && connection.ws) {
6845
+ // We may reuse this connection; cancel any pending idle-close so it is
6846
+ // not torn down during the auth preflight below. The held acquisition
6847
+ // lease also guards this window, but cancelling here avoids a needless
6848
+ // grace-timer round-trip on the common reuse path.
6849
+ cancelIdleConnectionClose(connection);
5952
6850
  if (connection.authFailure) {
5953
6851
  throw connection.authFailure;
5954
6852
  }
@@ -5964,19 +6862,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5964
6862
  if (connection.isConnected &&
5965
6863
  !connection.isAuthenticating &&
5966
6864
  !connectionMatchesAuthToken(connection, currentAuthToken)) {
5967
- if (connection.tokenRefreshTimer) {
5968
- clearInterval(connection.tokenRefreshTimer);
5969
- connection.tokenRefreshTimer = null;
5970
- }
5971
- try {
5972
- connection.ws.close();
5973
- }
5974
- catch (_a) {
5975
- // Already closing.
5976
- }
5977
- connection.ws = null;
5978
- connection.isConnected = false;
5979
- connections.delete(connection.key);
6865
+ closeConnectionNow(connection);
5980
6866
  }
5981
6867
  else {
5982
6868
  connection.authFailure = null;
@@ -5997,13 +6883,16 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5997
6883
  isAuthenticating: false,
5998
6884
  appId,
5999
6885
  key: connKey,
6886
+ acquisitionKey: leaseKey,
6000
6887
  routePath: roomKey ? routePath : undefined,
6001
6888
  authTokenProvider,
6889
+ connectionIdentityToken: principalToken !== null && principalToken !== void 0 ? principalToken : null,
6002
6890
  pendingAuthToken: null,
6003
6891
  pendingAuthSnapshot: null,
6004
- authTokenFingerprint: null,
6892
+ authTokenValue: null,
6005
6893
  authPrincipal: null,
6006
6894
  tokenRefreshTimer: null,
6895
+ idleCloseTimer: null,
6007
6896
  consecutiveAuthFailures: 0,
6008
6897
  authFailure: null,
6009
6898
  };
@@ -6080,7 +6969,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
6080
6969
  }
6081
6970
  else {
6082
6971
  connection.isAuthenticating = false;
6083
- connection.authTokenFingerprint = null;
6972
+ connection.authTokenValue = null;
6084
6973
  connection.authPrincipal = null;
6085
6974
  replaySubscriptions(connection);
6086
6975
  }
@@ -6098,10 +6987,14 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
6098
6987
  // Handle errors
6099
6988
  ws.addEventListener('error', (event) => {
6100
6989
  console.error('[WS v2] WebSocket error:', event);
6990
+ const wsError = new Error('WebSocket error');
6101
6991
  for (const [, pending] of connection.pendingSubscriptions) {
6102
- pending.reject(new Error('WebSocket error'));
6992
+ pending.reject(wsError);
6103
6993
  }
6104
6994
  connection.pendingSubscriptions.clear();
6995
+ for (const subscription of connection.subscriptions.values()) {
6996
+ rejectSubscriptionRefresh(subscription, wsError);
6997
+ }
6105
6998
  });
6106
6999
  // Handle close
6107
7000
  ws.addEventListener('close', () => {
@@ -6125,7 +7018,7 @@ function handleServerMessage(connection, message) {
6125
7018
  switch (message.type) {
6126
7019
  case 'authenticated': {
6127
7020
  connection.isAuthenticating = false;
6128
- connection.authTokenFingerprint = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenFingerprint) !== null && _b !== void 0 ? _b : null;
7021
+ connection.authTokenValue = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenValue) !== null && _b !== void 0 ? _b : null;
6129
7022
  connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
6130
7023
  replaySubscriptions(connection);
6131
7024
  break;
@@ -6133,15 +7026,49 @@ function handleServerMessage(connection, message) {
6133
7026
  case 'subscribed': {
6134
7027
  const subscription = connection.subscriptions.get(message.subscriptionId);
6135
7028
  if (subscription) {
7029
+ const refresh = subscription.refresh;
6136
7030
  // A data update can race ahead of subscribed during reconnect.
6137
7031
  // If we already received data for this subscription, treat subscribed
6138
7032
  // as an ack only and avoid regressing to an older snapshot.
6139
- if (subscription.lastData === undefined) {
7033
+ if (refresh) {
7034
+ const hadBaseline = subscription.lastData !== undefined;
7035
+ if (subscription.dataRevision === refresh.dataRevisionAtStart) {
7036
+ if (subscription.cache) {
7037
+ const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
7038
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
7039
+ }
7040
+ subscription.lastData = message.data;
7041
+ subscription.dataRevision += 1;
7042
+ if (hadBaseline) {
7043
+ const callbacks = Array.from(refresh.callbacks)
7044
+ .filter((callback) => subscription.callbacks.includes(callback));
7045
+ notifyCallbackOptions(callbacks, message.data);
7046
+ }
7047
+ else {
7048
+ // The refresh joined an original registration or a
7049
+ // reconnect replay before its first baseline arrived.
7050
+ // Every active callback needs that initial snapshot.
7051
+ notifyCallbacks(subscription, message.data);
7052
+ }
7053
+ }
7054
+ else if (refresh.callbacks.size > 0 && subscription.lastData !== undefined) {
7055
+ // A newer authorized data frame arrived after this refresh
7056
+ // began. Do not regress lastData to the older subscribed
7057
+ // snapshot; use that newer frame for callbacks that joined
7058
+ // after it was broadcast.
7059
+ const callbacks = Array.from(refresh.callbacks)
7060
+ .filter((callback) => subscription.callbacks.includes(callback));
7061
+ notifyCallbackOptions(callbacks, subscription.lastData);
7062
+ }
7063
+ resolveSubscriptionRefresh(subscription);
7064
+ }
7065
+ else if (subscription.lastData === undefined) {
6140
7066
  if (subscription.cache) {
6141
7067
  const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6142
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
7068
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
6143
7069
  }
6144
7070
  subscription.lastData = message.data;
7071
+ subscription.dataRevision += 1;
6145
7072
  notifyCallbacks(subscription, message.data);
6146
7073
  }
6147
7074
  }
@@ -6168,12 +7095,20 @@ function handleServerMessage(connection, message) {
6168
7095
  // Update cache
6169
7096
  if (subscription.cache) {
6170
7097
  const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
6171
- responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
7098
+ setResponseCacheEntry(cacheKey, subscription.identityToken, message.data);
6172
7099
  }
6173
7100
  // Store last data
6174
7101
  subscription.lastData = message.data;
7102
+ subscription.dataRevision += 1;
6175
7103
  // Notify callbacks
6176
7104
  notifyCallbacks(subscription, message.data);
7105
+ // A data frame is itself a full, freshly authorized snapshot.
7106
+ // It satisfies every joined callback even if the duplicate
7107
+ // subscribed ACK was lost; resolving here prevents a false
7108
+ // refresh timeout ten seconds after the UI already rendered.
7109
+ if (subscription.refresh) {
7110
+ resolveSubscriptionRefresh(subscription);
7111
+ }
6177
7112
  }
6178
7113
  break;
6179
7114
  }
@@ -6260,6 +7195,7 @@ function handleServerMessage(connection, message) {
6260
7195
  const subscription = connection.subscriptions.get(message.subscriptionId);
6261
7196
  if (subscription) {
6262
7197
  notifyErrorCallbacks(subscription, wsError);
7198
+ rejectSubscriptionRefresh(subscription, wsError);
6263
7199
  }
6264
7200
  }
6265
7201
  break;
@@ -6296,13 +7232,10 @@ function addIdsToSubscriptionData(data) {
6296
7232
  return data.map(withSubscriptionId);
6297
7233
  return withSubscriptionId(data);
6298
7234
  }
6299
- function notifyCallbacks(subscription, data) {
7235
+ function notifyCallbackOptions(callbacks, data) {
6300
7236
  var _a;
6301
7237
  // Attach the bare `id` (leaf doc key) to every delivered row (Bug 1).
6302
7238
  const decorated = addIdsToSubscriptionData(data);
6303
- // Snapshot the callbacks array so that unsubscriptions during
6304
- // notification don't cause callbacks to be skipped.
6305
- const callbacks = subscription.callbacks.slice();
6306
7239
  for (const callback of callbacks) {
6307
7240
  try {
6308
7241
  (_a = callback.onData) === null || _a === void 0 ? void 0 : _a.call(callback, decorated);
@@ -6312,23 +7245,33 @@ function notifyCallbacks(subscription, data) {
6312
7245
  }
6313
7246
  }
6314
7247
  }
7248
+ function notifyCallbacks(subscription, data) {
7249
+ // Snapshot the callbacks array so that unsubscriptions during
7250
+ // notification don't cause callbacks to be skipped.
7251
+ notifyCallbackOptions(subscription.callbacks.slice(), data);
7252
+ }
7253
+ function notifyErrorCallback(callback, error) {
7254
+ let delivered = false;
7255
+ try {
7256
+ const onError = callback.onError;
7257
+ if (!onError)
7258
+ return false;
7259
+ onError(error);
7260
+ delivered = true;
7261
+ }
7262
+ catch (callbackError) {
7263
+ console.error('[WS v2] Error in subscription error callback:', callbackError);
7264
+ }
7265
+ return delivered;
7266
+ }
6315
7267
  function notifyErrorCallbacks(subscription, error) {
6316
7268
  let delivered = false;
6317
7269
  const callbacks = subscription.callbacks.slice();
6318
7270
  for (const callback of callbacks) {
6319
- if (!callback.onError)
6320
- continue;
6321
- delivered = true;
6322
- try {
6323
- callback.onError(error);
6324
- }
6325
- catch (callbackError) {
6326
- console.error('[WS v2] Error in subscription error callback:', callbackError);
6327
- }
7271
+ delivered = notifyErrorCallback(callback, error) || delivered;
6328
7272
  }
6329
- if (delivered) {
7273
+ if (delivered)
6330
7274
  error.__boundedDeliveredToOnError = true;
6331
- }
6332
7275
  return delivered;
6333
7276
  }
6334
7277
  // WebSocket readyState constants
@@ -6359,6 +7302,71 @@ function sendSubscribe(connection, subscription) {
6359
7302
  console.error('[WS v2] Error sending subscribe message:', error);
6360
7303
  }
6361
7304
  }
7305
+ function resolveSubscriptionRefresh(subscription) {
7306
+ const refresh = subscription.refresh;
7307
+ if (!refresh)
7308
+ return;
7309
+ subscription.refresh = null;
7310
+ if (refresh.timer)
7311
+ clearTimeout(refresh.timer);
7312
+ refresh.callbacks.clear();
7313
+ refresh.resolve();
7314
+ }
7315
+ function rejectSubscriptionRefresh(subscription, error) {
7316
+ const refresh = subscription.refresh;
7317
+ if (!refresh)
7318
+ return;
7319
+ subscription.refresh = null;
7320
+ if (refresh.timer)
7321
+ clearTimeout(refresh.timer);
7322
+ refresh.callbacks.clear();
7323
+ refresh.reject(error);
7324
+ }
7325
+ /**
7326
+ * A callback joining an already-registered non-cache subscription cannot be
7327
+ * seeded from `lastData`: that snapshot may predate the callback's auth
7328
+ * preflight. Re-send the same subscription id so the server re-runs read auth
7329
+ * and returns a fresh baseline, without allocating another websocket or
7330
+ * replacing the logical client subscription.
7331
+ */
7332
+ function requestFreshSubscriptionSnapshot(connection, subscription, callback) {
7333
+ let refresh = subscription.refresh;
7334
+ if (!refresh) {
7335
+ let resolve;
7336
+ let reject;
7337
+ const promise = new Promise((resolvePromise, rejectPromise) => {
7338
+ resolve = resolvePromise;
7339
+ reject = rejectPromise;
7340
+ });
7341
+ refresh = {
7342
+ promise,
7343
+ resolve,
7344
+ reject,
7345
+ timer: null,
7346
+ callbacks: new Set(),
7347
+ dataRevisionAtStart: subscription.dataRevision,
7348
+ };
7349
+ subscription.refresh = refresh;
7350
+ refresh.timer = setTimeout(() => {
7351
+ if (subscription.refresh !== refresh)
7352
+ return;
7353
+ rejectSubscriptionRefresh(subscription, new Error('Subscription timeout'));
7354
+ }, 10000);
7355
+ }
7356
+ // Register before sendSubscribe: test/browser transports may synchronously
7357
+ // dispatch the subscribed response from send().
7358
+ refresh.callbacks.add(callback);
7359
+ // Every newly joined callback gets an idempotent same-id resend when the
7360
+ // authenticated socket is ready. Do not suppress this because an initial
7361
+ // confirmation or an earlier refresh is pending: that pending frame may
7362
+ // have been lost or displaced, and sharing it is exactly how an entire
7363
+ // route remount can inherit one doomed timeout. When connecting or
7364
+ // authenticating, replaySubscriptions will send after auth completes.
7365
+ if (connection.isConnected && !connection.isAuthenticating) {
7366
+ sendSubscribe(connection, subscription);
7367
+ }
7368
+ return refresh.promise;
7369
+ }
6362
7370
  function sendUnsubscribe(connection, subscriptionId) {
6363
7371
  if (!connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
6364
7372
  return;
@@ -6389,7 +7397,7 @@ function sendUnsubscribe(connection, subscriptionId) {
6389
7397
  * the subscription onto, since a WS connection routes to a single DO.
6390
7398
  */
6391
7399
  async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6392
- var _a, _b;
7400
+ var _a, _b, _c, _d, _e;
6393
7401
  const config = await getConfig();
6394
7402
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6395
7403
  // Use explicit appId override if provided, otherwise fall back to config
@@ -6407,16 +7415,27 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6407
7415
  : undefined;
6408
7416
  const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
6409
7417
  const identity = identityInfo.key;
7418
+ const identityToken = identityInfo.token;
6410
7419
  const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
7420
+ // Preserve the existing compact connection-key shape, but use the exact
7421
+ // token as a secondary in-memory equality check within that hash bucket.
6411
7422
  const principalKey = authTokenProvider ? identity : undefined;
7423
+ const principalToken = authTokenProvider
7424
+ ? (identityToken !== null && identityToken !== void 0 ? identityToken : identity)
7425
+ : undefined;
6412
7426
  const cacheKey = responseCacheEnabled
6413
7427
  ? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
6414
7428
  : null;
6415
- // Get or create connection for this routing target (room-scoped when a
6416
- // room route is supplied by the live helper, else the app-level connection).
7429
+ // Acquire a connection for this routing target (room-scoped when a room route
7430
+ // is supplied by the live helper, else the app-level connection). The lease is
7431
+ // held across auth preflight and released once this call registers below, so a
7432
+ // concurrent old-route teardown cannot close a socket we are about to reuse.
6417
7433
  let connection;
7434
+ let releaseConnection;
6418
7435
  try {
6419
- connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
7436
+ const acquisition = await acquireConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey, principalToken);
7437
+ connection = acquisition.connection;
7438
+ releaseConnection = acquisition.release;
6420
7439
  }
6421
7440
  catch (error) {
6422
7441
  const err = error instanceof Error ? error : new Error(String(error));
@@ -6426,73 +7445,135 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6426
7445
  }
6427
7446
  throw err;
6428
7447
  }
6429
- // Deliver cached data immediately if available, but only after connection
6430
- // auth preflight has succeeded. An expired authenticated session must receive
6431
- // an auth error, not a stale private snapshot followed by a failed connect.
6432
- const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
6433
- if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
6434
- setTimeout(() => {
6435
- var _a;
6436
- (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
6437
- }, 0);
6438
- }
7448
+ const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
6439
7449
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6440
- const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6441
- const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
6442
- const sortKey = subscriptionOptions.sort ? JSON.stringify(subscriptionOptions.sort) : '';
6443
7450
  let existingSubscription;
6444
- for (const sub of connection.subscriptions.values()) {
6445
- const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
6446
- const subFilterKey = sub.filter ? JSON.stringify(sub.filter) : '';
6447
- const subSortKey = sub.sort ? JSON.stringify(sub.sort) : '';
6448
- // H1: also require the same principal identity — a different logged-in
6449
- // user must not piggy-back on (and inherit lastData from) another user's
6450
- // existing subscription, even with identical path/prompt/shape/filter/sort.
6451
- 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) {
6452
- existingSubscription = sub;
6453
- break;
7451
+ let specKey = '';
7452
+ try {
7453
+ // Stringify THIS call's specs once; existing subscriptions carry their
7454
+ // precomputed specKey, so the scan is a string compare per entry
7455
+ // (previously each new subscribe re-stringified every existing
7456
+ // subscription's shape/filter/sort — O(N²) with many live hooks).
7457
+ specKey = JSON.stringify([
7458
+ (_a = subscriptionOptions.shape) !== null && _a !== void 0 ? _a : null,
7459
+ (_b = subscriptionOptions.filter) !== null && _b !== void 0 ? _b : null,
7460
+ (_c = subscriptionOptions.sort) !== null && _c !== void 0 ? _c : null,
7461
+ ]);
7462
+ for (const sub of connection.subscriptions.values()) {
7463
+ // H1: also require the same principal identity (compact hash AND exact
7464
+ // token) — a different logged-in user must not piggy-back on (and
7465
+ // inherit lastData from) another user's existing subscription, even
7466
+ // with identical path/prompt/shape/filter/sort.
7467
+ 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) {
7468
+ existingSubscription = sub;
7469
+ break;
7470
+ }
6454
7471
  }
6455
7472
  }
7473
+ catch (error) {
7474
+ releaseConnection();
7475
+ throw error;
7476
+ }
6456
7477
  if (existingSubscription) {
7478
+ let replayData = undefined;
7479
+ let replayOnData;
7480
+ if (responseCacheEnabled) {
7481
+ // Read callback getters before mutating the shared subscription. If
7482
+ // a caller supplied a throwing getter, the acquisition fail-safe can
7483
+ // release cleanly without leaving a ghost callback behind.
7484
+ replayOnData = subscriptionOptions.onData;
7485
+ replayData = existingSubscription.lastData !== undefined
7486
+ ? existingSubscription.lastData
7487
+ : cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL
7488
+ ? cachedEntry.data
7489
+ : undefined;
7490
+ }
6457
7491
  if (responseCacheEnabled) {
6458
7492
  existingSubscription.cache = true;
6459
7493
  }
6460
7494
  // Add callback to existing subscription
6461
7495
  existingSubscription.callbacks.push(subscriptionOptions);
6462
- // Deliver last known data immediately only for explicit, principal-bound
6463
- // cache opt-in. Otherwise joining an existing subscription can replay a
6464
- // stale private snapshot without a fresh server auth result.
6465
- if (responseCacheEnabled &&
6466
- existingSubscription.cache &&
6467
- existingSubscription.lastData !== undefined &&
6468
- subscriptionOptions.onData) {
6469
- setTimeout(() => {
6470
- var _a;
6471
- (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
6472
- }, 0);
7496
+ let refreshPromise = null;
7497
+ if (responseCacheEnabled) {
7498
+ // Explicit, principal-bound cache opt-in may replay exactly one
7499
+ // snapshot. Prefer the active subscription's lastData, falling back
7500
+ // to the short-lived response cache while a reconnect is in flight.
7501
+ if (replayData !== undefined && replayOnData) {
7502
+ setTimeout(() => {
7503
+ replayOnData === null || replayOnData === void 0 ? void 0 : replayOnData(addIdsToSubscriptionData(replayData));
7504
+ }, 0);
7505
+ }
7506
+ }
7507
+ else {
7508
+ // Default (non-cache) subscriptions require a fresh authorized
7509
+ // baseline for every newly joined callback. Reuse the same logical
7510
+ // subscription id and socket, but make the server re-run the read.
7511
+ refreshPromise = requestFreshSubscriptionSnapshot(connection, existingSubscription, subscriptionOptions);
7512
+ }
7513
+ releaseConnection();
7514
+ if (refreshPromise) {
7515
+ try {
7516
+ await refreshPromise;
7517
+ }
7518
+ catch (error) {
7519
+ const err = error instanceof Error ? error : new Error(String(error));
7520
+ let deliveredLocally = false;
7521
+ if (!err.__boundedDeliveredToOnError) {
7522
+ deliveredLocally = notifyErrorCallback(subscriptionOptions, err);
7523
+ }
7524
+ if (!subscriptionOptions.onError) {
7525
+ await removeCallbackFromSubscription(connection, existingSubscription.subscriptionId, subscriptionOptions);
7526
+ throw err;
7527
+ }
7528
+ if (!err.__boundedDeliveredToOnError && !deliveredLocally) {
7529
+ console.warn('[WS v2] Subscription refresh failed, keeping for reconnect recovery:', error);
7530
+ }
7531
+ }
6473
7532
  }
6474
7533
  return async () => {
6475
7534
  await removeCallbackFromSubscription(connection, existingSubscription.subscriptionId, subscriptionOptions);
6476
7535
  };
6477
7536
  }
7537
+ // Deliver cached data immediately for a new logical subscription, but only
7538
+ // after connection auth preflight has succeeded. An expired authenticated
7539
+ // session must receive an auth error, not a stale private snapshot followed
7540
+ // by a failed connect.
7541
+ if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
7542
+ setTimeout(() => {
7543
+ var _a;
7544
+ (_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
7545
+ }, 0);
7546
+ }
6478
7547
  // Create new subscription
6479
7548
  const subscriptionId = generateSubscriptionId();
6480
- const subscription = {
6481
- subscriptionId,
6482
- path: normalizedPath,
6483
- filter: subscriptionOptions.filter,
6484
- sort: subscriptionOptions.sort,
6485
- prompt: subscriptionOptions.prompt,
6486
- shape: subscriptionOptions.shape,
6487
- limit: subscriptionOptions.limit,
6488
- cursor: subscriptionOptions.cursor,
6489
- includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
6490
- callbacks: [subscriptionOptions],
6491
- lastData: undefined,
6492
- cache: responseCacheEnabled,
6493
- identity,
6494
- };
6495
- connection.subscriptions.set(subscriptionId, subscription);
7549
+ let subscription;
7550
+ try {
7551
+ subscription = {
7552
+ subscriptionId,
7553
+ path: normalizedPath,
7554
+ filter: subscriptionOptions.filter,
7555
+ sort: subscriptionOptions.sort,
7556
+ prompt: subscriptionOptions.prompt,
7557
+ shape: subscriptionOptions.shape,
7558
+ limit: subscriptionOptions.limit,
7559
+ cursor: subscriptionOptions.cursor,
7560
+ includeSubPaths: (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false,
7561
+ callbacks: [subscriptionOptions],
7562
+ lastData: undefined,
7563
+ dataRevision: 0,
7564
+ refresh: null,
7565
+ cache: responseCacheEnabled,
7566
+ identity,
7567
+ identityToken,
7568
+ specKey,
7569
+ };
7570
+ connection.subscriptions.set(subscriptionId, subscription);
7571
+ }
7572
+ catch (error) {
7573
+ releaseConnection();
7574
+ throw error;
7575
+ }
7576
+ releaseConnection();
6496
7577
  // Send subscribe message if connected
6497
7578
  if (connection.isConnected && !connection.isAuthenticating) {
6498
7579
  // Create a promise to wait for subscription confirmation
@@ -6512,15 +7593,23 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6512
7593
  }
6513
7594
  catch (error) {
6514
7595
  const err = error instanceof Error ? error : new Error(String(error));
7596
+ let deliveredLocally = false;
6515
7597
  if (!err.__boundedDeliveredToOnError) {
6516
- notifyErrorCallbacks(subscription, err);
7598
+ // A local confirmation timeout belongs to the caller that
7599
+ // originated it. Joined callbacks may already have a viable
7600
+ // same-id refresh and must not be poisoned by that old attempt.
7601
+ deliveredLocally = notifyErrorCallback(subscriptionOptions, err);
6517
7602
  }
6518
7603
  if (!subscriptionOptions.onError) {
6519
7604
  connection.pendingSubscriptions.delete(subscriptionId);
6520
- connection.subscriptions.delete(subscriptionId);
7605
+ // This registration may have acquired joined callbacks while
7606
+ // its confirmation was pending. Remove only the originating
7607
+ // callback; deleting the whole logical subscription would
7608
+ // orphan those remounted consumers and their refresh flight.
7609
+ await removeCallbackFromSubscription(connection, subscriptionId, subscriptionOptions);
6521
7610
  throw err;
6522
7611
  }
6523
- if (!err.__boundedDeliveredToOnError) {
7612
+ if (!err.__boundedDeliveredToOnError && !deliveredLocally) {
6524
7613
  console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
6525
7614
  }
6526
7615
  }
@@ -6531,12 +7620,14 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6531
7620
  };
6532
7621
  }
6533
7622
  async function removeCallbackFromSubscription(connection, subscriptionId, callback) {
7623
+ var _a;
6534
7624
  const subscription = connection.subscriptions.get(subscriptionId);
6535
7625
  if (!subscription) {
6536
7626
  return;
6537
7627
  }
6538
7628
  // Remove this callback
6539
7629
  subscription.callbacks = subscription.callbacks.filter(cb => cb !== callback);
7630
+ (_a = subscription.refresh) === null || _a === void 0 ? void 0 : _a.callbacks.delete(callback);
6540
7631
  // If there are still callbacks, don't unsubscribe
6541
7632
  if (subscription.callbacks.length > 0) {
6542
7633
  return;
@@ -6558,17 +7649,9 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6558
7649
  sendUnsubscribe(connection, subscriptionId);
6559
7650
  await unsubscribePromise;
6560
7651
  }
6561
- // If no more subscriptions, close connection
6562
- if (connection.subscriptions.size === 0 && connection.ws) {
6563
- // Clear token refresh timer
6564
- if (connection.tokenRefreshTimer) {
6565
- clearInterval(connection.tokenRefreshTimer);
6566
- connection.tokenRefreshTimer = null;
6567
- }
6568
- connection.ws.close();
6569
- connection.ws = null;
6570
- connections.delete(connection.key);
6571
- }
7652
+ // If no more subscriptions, let the connection close after a short idle
7653
+ // grace so an immediately-following route remount can reclaim the socket.
7654
+ scheduleConnectionCloseIfIdle(connection);
6572
7655
  }
6573
7656
  /**
6574
7657
  * Close all v2 subscriptions.
@@ -6576,6 +7659,11 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6576
7659
  async function closeAllSubscriptionsV2() {
6577
7660
  const closePromises = [];
6578
7661
  for (const [appId, connection] of connections) {
7662
+ cancelIdleConnectionClose(connection);
7663
+ const closeError = new Error('Subscriptions closed');
7664
+ for (const subscription of connection.subscriptions.values()) {
7665
+ rejectSubscriptionRefresh(subscription, closeError);
7666
+ }
6579
7667
  if (connection.tokenRefreshTimer) {
6580
7668
  clearInterval(connection.tokenRefreshTimer);
6581
7669
  connection.tokenRefreshTimer = null;
@@ -6584,15 +7672,18 @@ async function closeAllSubscriptionsV2() {
6584
7672
  const ws = connection.ws;
6585
7673
  connection.ws = null;
6586
7674
  connection.isConnected = false;
6587
- closePromises.push(new Promise((resolve) => {
6588
- ws.addEventListener('close', () => {
6589
- resolve();
6590
- });
6591
- ws.close();
6592
- }));
7675
+ if (ws.readyState !== WS_READY_STATE_CLOSED) {
7676
+ closePromises.push(new Promise((resolve) => {
7677
+ ws.addEventListener('close', () => {
7678
+ resolve();
7679
+ });
7680
+ ws.close();
7681
+ }));
7682
+ }
6593
7683
  }
6594
7684
  connections.delete(appId);
6595
7685
  }
7686
+ connectionAcquisitions.clear();
6596
7687
  await Promise.all(closePromises);
6597
7688
  }
6598
7689
  /**
@@ -6625,10 +7716,12 @@ function getCachedDataV2(path, prompt) {
6625
7716
  // opt-in. Resolve the caller's identity from an active cache-enabled
6626
7717
  // subscription for this path. No matching sub → null (safe).
6627
7718
  let identity;
7719
+ let identityToken;
6628
7720
  outer: for (const connection of connections.values()) {
6629
7721
  for (const sub of connection.subscriptions.values()) {
6630
7722
  if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
6631
7723
  identity = sub.identity;
7724
+ identityToken = sub.identityToken;
6632
7725
  break outer;
6633
7726
  }
6634
7727
  }
@@ -6637,7 +7730,7 @@ function getCachedDataV2(path, prompt) {
6637
7730
  return null;
6638
7731
  }
6639
7732
  const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
6640
- const cachedEntry = responseCache.get(cacheKey);
7733
+ const cachedEntry = getResponseCacheEntry(cacheKey, identityToken !== null && identityToken !== void 0 ? identityToken : null);
6641
7734
  if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
6642
7735
  return cachedEntry.data;
6643
7736
  }
@@ -6699,10 +7792,15 @@ async function doReconnectWithNewAuth() {
6699
7792
  if (!connection.ws) {
6700
7793
  continue;
6701
7794
  }
7795
+ cancelIdleConnectionClose(connection);
7796
+ const reconnectError = new Error('Connection reconnecting due to auth change');
6702
7797
  for (const [, pending] of connection.pendingSubscriptions) {
6703
- pending.reject(new Error('Connection reconnecting due to auth change'));
7798
+ pending.reject(reconnectError);
6704
7799
  }
6705
7800
  connection.pendingSubscriptions.clear();
7801
+ for (const subscription of connection.subscriptions.values()) {
7802
+ rejectSubscriptionRefresh(subscription, reconnectError);
7803
+ }
6706
7804
  for (const [, pending] of connection.pendingUnsubscriptions) {
6707
7805
  pending.resolve();
6708
7806
  }
@@ -6762,10 +7860,10 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6762
7860
  const currentToken = await getIdToken(isServer).catch(() => null);
6763
7861
  if (!currentToken) {
6764
7862
  // Mid-refresh: the socket principal still governs server-side.
6765
- if (!connection.authTokenFingerprint && !connection.authPrincipal)
7863
+ if (!connection.authTokenValue && !connection.authPrincipal)
6766
7864
  return false;
6767
7865
  }
6768
- else if (!connection.authTokenFingerprint && !connection.authPrincipal) {
7866
+ else if (!connection.authTokenValue && !connection.authPrincipal) {
6769
7867
  // Anonymous socket + authenticated user: do not attribute this user's
6770
7868
  // input to the anon principal. Reauth the socket; caller may fall back.
6771
7869
  void maybeReauthRoomConnection(connection);
@@ -6785,7 +7883,7 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6785
7883
  }
6786
7884
  }
6787
7885
  // Debounced background reauth: when the client's current token no longer
6788
- // matches the connection's auth fingerprint (token rotation), refresh the
7886
+ // matches the connection's authenticated identity (token rotation), refresh the
6789
7887
  // socket auth ONCE rather than degrading every subsequent send.
6790
7888
  const reauthInFlight = new WeakSet();
6791
7889
  async function maybeReauthRoomConnection(connection, isServer) {
@@ -7468,5 +8566,5 @@ function defineLiveModule(mod) {
7468
8566
  return mod;
7469
8567
  }
7470
8568
 
7471
- export { BoundedDeclineError, DECLINE_EVENT_NAME, EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveDecline, deriveUserIdentityFromIdToken, dispatchDeclineEvent, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects };
8569
+ export { BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR, BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR, BoundedDeclineError, DECLINE_EVENT_NAME, EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, assertCanonicalBoundedRuntimeWriteCalldata, assertEvmTransactionMatchesWrite, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveDecline, deriveUserIdentityFromIdToken, dispatchDeclineEvent, docId, functions, genAuthNonce, genSiweMessage, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, normalizeEvmAddress, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toLowerEvmAddress, toMillis, toSeconds, withEffects };
7472
8570
  //# sourceMappingURL=index.mjs.map