@bounded-sh/core 0.0.42 → 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
  }
@@ -3605,12 +3619,13 @@ const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use c
3605
3619
  /* SESSION MANAGER */
3606
3620
  /* ------------------------------------------------------------------ */
3607
3621
  class ServerSessionManager {
3608
- constructor(keypair) {
3622
+ constructor(keypair, sessionFactory) {
3609
3623
  /* Private cache (lives for the life of the instance) */
3610
3624
  this.session = null;
3611
3625
  /* Coalesce concurrent getSession() calls into a single in-flight request */
3612
3626
  this.pendingSession = null;
3613
3627
  this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
3628
+ this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
3614
3629
  }
3615
3630
  /**
3616
3631
  * Create a session manager for a specific keypair.
@@ -3619,10 +3634,30 @@ class ServerSessionManager {
3619
3634
  static forKeypair(keypair) {
3620
3635
  return new ServerSessionManager(keypair);
3621
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
+ }
3622
3647
  /* ---------------------------------------------- *
3623
3648
  * Session creation (instance method)
3624
3649
  * ---------------------------------------------- */
3625
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
+ }
3626
3661
  if (!this.keypair) {
3627
3662
  throw new Error(NO_AMBIENT_SERVER_SESSION);
3628
3663
  }
@@ -3726,6 +3761,133 @@ function decodeBase64Url(input) {
3726
3761
  return safeAtob(b64);
3727
3762
  }
3728
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
+
3729
3891
  async function createBearerToken(isServer) {
3730
3892
  if (isServer) {
3731
3893
  const sessionMgr = ServerSessionManager.instance;
@@ -3774,7 +3936,7 @@ async function getUserInfo(isServer) {
3774
3936
  * Returns all-null when the token can't be decoded (caller treats as no identity).
3775
3937
  */
3776
3938
  function deriveUserIdentityFromIdToken(idToken) {
3777
- const empty = { id: null, address: null, email: null };
3939
+ const empty = { id: null, address: null, email: null, evmAddress: null };
3778
3940
  if (!idToken)
3779
3941
  return empty;
3780
3942
  try {
@@ -3787,14 +3949,25 @@ function deriveUserIdentityFromIdToken(idToken) {
3787
3949
  ? walletClaim
3788
3950
  : null;
3789
3951
  const userIdClaim = payload['custom:userId'];
3790
- const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3952
+ let id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3791
3953
  ? userIdClaim
3792
3954
  : null;
3793
3955
  const emailClaim = payload['email'];
3794
3956
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
3795
3957
  ? emailClaim.toLowerCase()
3796
3958
  : null;
3797
- 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 };
3798
3971
  }
3799
3972
  catch (_a) {
3800
3973
  return empty;
@@ -4116,6 +4289,456 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4116
4289
  }
4117
4290
  }
4118
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
+
4119
4742
  var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
4120
4743
  var t = {};
4121
4744
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -4252,6 +4875,21 @@ function hashForKey$1(value) {
4252
4875
  }
4253
4876
  return h.toString(36);
4254
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
+ }
4255
4893
  function hasAuthHeader(headers) {
4256
4894
  if (!headers)
4257
4895
  return false;
@@ -4262,10 +4900,10 @@ function authValueFromHeaders(headers) {
4262
4900
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4263
4901
  }
4264
4902
  function principalFromAuthValue(authValue) {
4265
- return authValue ? `h${hashForKey$1(authValue)}` : null;
4903
+ return authValue ? `h${hashForKeyCached(authValue)}` : null;
4266
4904
  }
4267
4905
  function principalFromIdToken$1(idToken) {
4268
- return idToken ? `t${hashForKey$1(idToken)}` : null;
4906
+ return idToken ? `t${hashForKeyCached(idToken)}` : null;
4269
4907
  }
4270
4908
  function uncacheableReadKey(appId, scope) {
4271
4909
  uncacheableReadKeyCounter += 1;
@@ -5146,13 +5784,12 @@ async function runExpressionMany(many) {
5146
5784
  * collection's policy `delete` rule and broadcasts a delete to subscribers.
5147
5785
  */
5148
5786
  async function set(path, document, options) {
5149
- const result = await setMany([{ path, document }], options);
5150
- // Clear cache entries that might be affected by this update
5151
- clearCacheByPrefix(path);
5152
- 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);
5153
5790
  }
5154
5791
  async function setMany(many, options) {
5155
- var _a, _b, _c, _d, _e, _f, _g;
5792
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5156
5793
  // Returns the data that was set, or undefined if the document was already set.
5157
5794
  try {
5158
5795
  const config = await getConfig();
@@ -5208,6 +5845,9 @@ async function setMany(many, options) {
5208
5845
  // the API did nothing and gave us the required info to run this transaction.
5209
5846
  const { transactions, offchainTransaction } = setResponse.data;
5210
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
+ }
5211
5851
  if (offchainTransaction) {
5212
5852
  const transactionResult = await handleOffchainTransaction(offchainTransaction, authProvider, options);
5213
5853
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
@@ -5217,9 +5857,35 @@ async function setMany(many, options) {
5217
5857
  throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
5218
5858
  }
5219
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
+ }
5220
5868
  let transactionResult;
5221
5869
  const expectedWritePaths = documents.map(d => d.destinationPath);
5222
- 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) {
5223
5889
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5224
5890
  }
5225
5891
  else {
@@ -5243,7 +5909,7 @@ async function setMany(many, options) {
5243
5909
  else if (setResponse.data &&
5244
5910
  typeof setResponse.data === 'object' &&
5245
5911
  setResponse.data.success === true) {
5246
- 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"]);
5247
5913
  return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
5248
5914
  }
5249
5915
  else {
@@ -5343,6 +6009,14 @@ async function setMany(many, options) {
5343
6009
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5344
6010
  return transactionResult;
5345
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
+ }
5346
6020
  async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5347
6021
  var _a, _b, _c;
5348
6022
  const config = await getConfig();
@@ -6723,7 +7397,7 @@ function sendUnsubscribe(connection, subscriptionId) {
6723
7397
  * the subscription onto, since a WS connection routes to a single DO.
6724
7398
  */
6725
7399
  async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6726
- var _a, _b;
7400
+ var _a, _b, _c, _d, _e;
6727
7401
  const config = await getConfig();
6728
7402
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6729
7403
  // Use explicit appId override if provided, otherwise fall back to config
@@ -6774,19 +7448,23 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6774
7448
  const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
6775
7449
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6776
7450
  let existingSubscription;
7451
+ let specKey = '';
6777
7452
  try {
6778
- const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6779
- const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
6780
- const sortKey = subscriptionOptions.sort ? JSON.stringify(subscriptionOptions.sort) : '';
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
+ ]);
6781
7462
  for (const sub of connection.subscriptions.values()) {
6782
- const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
6783
- const subFilterKey = sub.filter ? JSON.stringify(sub.filter) : '';
6784
- const subSortKey = sub.sort ? JSON.stringify(sub.sort) : '';
6785
7463
  // H1: also require the same principal identity (compact hash AND exact
6786
7464
  // token) — a different logged-in user must not piggy-back on (and
6787
7465
  // inherit lastData from) another user's existing subscription, even
6788
7466
  // with identical path/prompt/shape/filter/sort.
6789
- if (sub.identity === identity && sub.identityToken === identityToken && 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) {
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) {
6790
7468
  existingSubscription = sub;
6791
7469
  break;
6792
7470
  }
@@ -6879,7 +7557,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6879
7557
  shape: subscriptionOptions.shape,
6880
7558
  limit: subscriptionOptions.limit,
6881
7559
  cursor: subscriptionOptions.cursor,
6882
- includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
7560
+ includeSubPaths: (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false,
6883
7561
  callbacks: [subscriptionOptions],
6884
7562
  lastData: undefined,
6885
7563
  dataRevision: 0,
@@ -6887,6 +7565,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6887
7565
  cache: responseCacheEnabled,
6888
7566
  identity,
6889
7567
  identityToken,
7568
+ specKey,
6890
7569
  };
6891
7570
  connection.subscriptions.set(subscriptionId, subscription);
6892
7571
  }
@@ -7887,5 +8566,5 @@ function defineLiveModule(mod) {
7887
8566
  return mod;
7888
8567
  }
7889
8568
 
7890
- 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 };
7891
8570
  //# sourceMappingURL=index.mjs.map