@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.js CHANGED
@@ -51,6 +51,10 @@ let clientConfig = {
51
51
  isServer: false,
52
52
  };
53
53
  let isInitialized = false;
54
+ // Callers awaiting getConfig() before init() completes. Resolved synchronously
55
+ // by init() — the previous 50ms setInterval poll added up to 50ms of latency
56
+ // to the first SDK operation after init for no reason.
57
+ let initWaiters = [];
54
58
  const BOUNDED_NETWORKS = {
55
59
  'bounded-staging': {
56
60
  wsApiUrl: 'wss://realtime-staging.bounded.sh',
@@ -119,23 +123,17 @@ function init(newConfig) {
119
123
  const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
120
124
  clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
121
125
  isInitialized = true;
126
+ const waiters = initWaiters;
127
+ initWaiters = [];
128
+ for (const wake of waiters)
129
+ wake();
122
130
  resolve();
123
131
  });
124
132
  }
125
133
  function waitForInitialization() {
126
- return new Promise((resolve) => {
127
- if (isInitialized) {
128
- resolve();
129
- }
130
- else {
131
- const interval = setInterval(() => {
132
- if (isInitialized) {
133
- clearInterval(interval);
134
- resolve();
135
- }
136
- }, 50);
137
- }
138
- });
134
+ if (isInitialized)
135
+ return Promise.resolve();
136
+ return new Promise((resolve) => { initWaiters.push(resolve); });
139
137
  }
140
138
  async function getConfig() {
141
139
  await waitForInitialization();
@@ -2821,8 +2819,24 @@ async function genAuthNonce() {
2821
2819
  // stamps an is_anonymous claim. Without this a guest would be indistinguishable
2822
2820
  // from 'phantom' and couldn't be gated or flagged.
2823
2821
  const SIGNATURE_AUTH_METHODS = new Set(['phantom', 'mobile-wallet-adapter', 'guest']);
2824
- async function createSessionWithSignature(address, message, signature) {
2825
- var _a, _b;
2822
+ // Maps a client authMethod to the /session issuer's signature discriminator when
2823
+ // they differ. EVM WALLET LOGIN (config.authMethod 'evm-wallet') exchanges a SIWE
2824
+ // (EIP-4361 personal_sign, secp256k1) signature, which the bounded-auth issuer
2825
+ // routes on authMethod:"siwe" (index.ts /session → parseSiweMessage/recoverSiweSigner)
2826
+ // and mints under custom:evmWalletAddress. The EVM provider also passes 'siwe'
2827
+ // explicitly (authMethodOverride), so this map is the belt-and-suspenders path.
2828
+ const SESSION_METHOD_ALIASES = { 'evm-wallet': 'siwe' };
2829
+ /**
2830
+ * Exchange a wallet signature for a Bounded session at the /session issuer.
2831
+ *
2832
+ * @param authMethodOverride the exact /session discriminator to send (e.g. 'siwe'
2833
+ * for the SIWE/EVM path). When omitted the method is derived from config: an
2834
+ * aliased method (evm-wallet → siwe) maps through SESSION_METHOD_ALIASES, a
2835
+ * Solana wallet/guest method passes through verbatim, and anything else
2836
+ * defaults to the canonical 'phantom' the issuer accepts for a signature.
2837
+ */
2838
+ async function createSessionWithSignature(address, message, signature, authMethodOverride) {
2839
+ var _a, _b, _c;
2826
2840
  const client = await getAxiosAuthClient();
2827
2841
  const config = await getConfig();
2828
2842
  const appId = config.appId;
@@ -2831,7 +2845,7 @@ async function createSessionWithSignature(address, message, signature) {
2831
2845
  // canonical Solana wallet method 'phantom' that every /session issuer accepts
2832
2846
  // for a signature exchange. This is what makes the server SDK + guest device
2833
2847
  // keypair work under the email-default config.
2834
- const authMethod = SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom';
2848
+ const authMethod = (_a = authMethodOverride !== null && authMethodOverride !== void 0 ? authMethodOverride : SESSION_METHOD_ALIASES[config.authMethod]) !== null && _a !== void 0 ? _a : (SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom');
2835
2849
  try {
2836
2850
  const response = await client.post('/session', {
2837
2851
  appId,
@@ -2846,11 +2860,11 @@ async function createSessionWithSignature(address, message, signature) {
2846
2860
  // Surface the issuer's actionable message (e.g. "Anonymous auth is not enabled for
2847
2861
  // this app. Add \"auth\": { \"anonymous\": true } to policy.json and redeploy.")
2848
2862
  // instead of axios's generic "Request failed with status code 403".
2849
- const d = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
2863
+ const d = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.data;
2850
2864
  if (d && (d.message || d.error)) {
2851
2865
  const e = new Error(String(d.message || d.error));
2852
2866
  e.code = d.error;
2853
- e.status = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
2867
+ e.status = (_c = err === null || err === void 0 ? void 0 : err.response) === null || _c === void 0 ? void 0 : _c.status;
2854
2868
  e.hint = d.hint;
2855
2869
  throw e;
2856
2870
  }
@@ -3625,12 +3639,13 @@ const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use c
3625
3639
  /* SESSION MANAGER */
3626
3640
  /* ------------------------------------------------------------------ */
3627
3641
  class ServerSessionManager {
3628
- constructor(keypair) {
3642
+ constructor(keypair, sessionFactory) {
3629
3643
  /* Private cache (lives for the life of the instance) */
3630
3644
  this.session = null;
3631
3645
  /* Coalesce concurrent getSession() calls into a single in-flight request */
3632
3646
  this.pendingSession = null;
3633
3647
  this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
3648
+ this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
3634
3649
  }
3635
3650
  /**
3636
3651
  * Create a session manager for a specific keypair.
@@ -3639,10 +3654,30 @@ class ServerSessionManager {
3639
3654
  static forKeypair(keypair) {
3640
3655
  return new ServerSessionManager(keypair);
3641
3656
  }
3657
+ /**
3658
+ * Create an isolated server session manager for a non-Solana signer (currently
3659
+ * EVM/SIWE). The factory must perform the signature exchange and return the
3660
+ * canonical signer address with its tokens; no signer state becomes global.
3661
+ */
3662
+ static forSessionFactory(factory) {
3663
+ if (typeof factory !== 'function')
3664
+ throw new Error('A server session factory is required');
3665
+ return new ServerSessionManager(undefined, factory);
3666
+ }
3642
3667
  /* ---------------------------------------------- *
3643
3668
  * Session creation (instance method)
3644
3669
  * ---------------------------------------------- */
3645
3670
  async createSession() {
3671
+ if (this.sessionFactory) {
3672
+ const session = await this.sessionFactory();
3673
+ if (!session || typeof session.address !== 'string' || !session.address ||
3674
+ typeof session.idToken !== 'string' || !session.idToken ||
3675
+ typeof session.accessToken !== 'string' || !session.accessToken ||
3676
+ typeof session.refreshToken !== 'string' || !session.refreshToken) {
3677
+ throw new Error('Server signer session factory returned an invalid session');
3678
+ }
3679
+ return session;
3680
+ }
3646
3681
  if (!this.keypair) {
3647
3682
  throw new Error(NO_AMBIENT_SERVER_SESSION);
3648
3683
  }
@@ -3746,6 +3781,133 @@ function decodeBase64Url(input) {
3746
3781
  return safeAtob(b64);
3747
3782
  }
3748
3783
 
3784
+ // evm-utils.ts — the EVM twin of sol/sol-utils.ts's genSolanaMessage. It builds
3785
+ // the canonical EIP-4361 (Sign-In with Ethereum, SIWE) prompt the bounded-auth
3786
+ // issuer verifies, and normalizes EVM addresses to the single canonical form the
3787
+ // identity layer matches on.
3788
+ //
3789
+ // The SIWE message MUST match bounded-auth's parser byte-for-byte
3790
+ // (packages/cdk/cloudflare/bounded-auth/src/verify-siwe.ts → parseSiweMessage):
3791
+ // <domain> wants you to sign in with your Ethereum account:
3792
+ // <address>
3793
+ // <blank>
3794
+ // <statement>
3795
+ // <blank>
3796
+ // URI: <uri>
3797
+ // Version: 1
3798
+ // Chain ID: <chainId>
3799
+ // Nonce: <nonce>
3800
+ // Issued At: <ISO8601>
3801
+ // Expiration Time: <ISO8601>
3802
+ // Resources:
3803
+ // - bounded:app:<appId>
3804
+ //
3805
+ // Bindings the verifier enforces (so we produce them exactly): the address is
3806
+ // lowercased (identity match is case-sensitive, so lowercase-at-boundary is
3807
+ // mandatory), the single `bounded:app:<appId>` resource binds the signature to
3808
+ // THIS app (a signature for one app must not mint a session for another — the
3809
+ // SIWE analog of genSolanaMessage's `App ID:` line), and Expiration Time is
3810
+ // REQUIRED and must fall within the issuer's 10-minute max validity window.
3811
+ /** Default statement — kept identical to genSolanaMessage's for a consistent prompt. */
3812
+ const DEFAULT_STATEMENT = 'Sign this message to authenticate with our application.';
3813
+ /** Match bounded-auth's MAX_AUTH_WINDOW_MS (10 min); we default to a 5-min window. */
3814
+ const DEFAULT_VALIDITY_MS = 5 * 60 * 1000;
3815
+ /**
3816
+ * Build the canonical EIP-4361 SIWE message the bounded-auth issuer verifies.
3817
+ *
3818
+ * @param address the connected EVM account (lowercased into the prompt).
3819
+ * @param nonce single-use nonce from genAuthNonce().
3820
+ * @param appId the target app id (bound as the sole `bounded:app:<appId>` resource).
3821
+ * @param domain RFC-3986 authority of the requesting site (e.g. window.location.host).
3822
+ * @param uri the requesting URI; its origin is bound to the domain and actual browser Origin.
3823
+ */
3824
+ function genSiweMessage(address, nonce, appId, domain, uri, options = {}) {
3825
+ var _a, _b, _c, _d, _e;
3826
+ const addr = normalizeEvmAddress(address);
3827
+ if (!addr)
3828
+ throw new Error('SIWE address must be a 20-byte 0x-prefixed EVM address');
3829
+ const statement = (_a = options.statement) !== null && _a !== void 0 ? _a : DEFAULT_STATEMENT;
3830
+ const version = (_b = options.version) !== null && _b !== void 0 ? _b : '1';
3831
+ const chainId = (_c = options.chainId) !== null && _c !== void 0 ? _c : 1;
3832
+ if (statement !== DEFAULT_STATEMENT)
3833
+ throw new Error('SIWE statement must use the canonical Bounded authentication text');
3834
+ if (version !== '1')
3835
+ throw new Error('SIWE version must be 1');
3836
+ if (!Number.isSafeInteger(chainId) || chainId <= 0)
3837
+ throw new Error('SIWE chainId must be a positive safe integer');
3838
+ if (!/^[A-Za-z0-9]{8,}$/.test(nonce))
3839
+ throw new Error('SIWE nonce must be at least 8 alphanumeric characters');
3840
+ if (!appId || /[\r\n]/.test(appId))
3841
+ throw new Error('SIWE appId is required and must be single-line');
3842
+ if (!domain || domain !== domain.trim() || /[\s/@?#\\]/.test(domain))
3843
+ throw new Error('SIWE domain must be an RFC-3986 authority');
3844
+ try {
3845
+ const parsedAuthority = new URL(`https://${domain}`);
3846
+ if (parsedAuthority.username || parsedAuthority.password || parsedAuthority.pathname !== '/' ||
3847
+ parsedAuthority.search || parsedAuthority.hash || parsedAuthority.host.toLowerCase() !== domain) {
3848
+ throw new Error('non-canonical authority');
3849
+ }
3850
+ }
3851
+ catch (_f) {
3852
+ throw new Error('SIWE domain must be a canonical lowercase RFC-3986 authority');
3853
+ }
3854
+ let parsedUri;
3855
+ try {
3856
+ parsedUri = new URL(uri);
3857
+ }
3858
+ catch (_g) {
3859
+ throw new Error('SIWE URI must be an absolute URL');
3860
+ }
3861
+ const local = parsedUri.hostname === 'localhost' || parsedUri.hostname === '127.0.0.1' || parsedUri.hostname === '[::1]';
3862
+ if (parsedUri.protocol !== 'https:' && !(local && parsedUri.protocol === 'http:')) {
3863
+ throw new Error('SIWE URI must use HTTPS (HTTP is allowed only for localhost)');
3864
+ }
3865
+ if (parsedUri.username || parsedUri.password || parsedUri.hash || parsedUri.host.toLowerCase() !== domain.toLowerCase()) {
3866
+ throw new Error('SIWE URI authority must match the SIWE domain');
3867
+ }
3868
+ const issuedAtDate = (_d = options.issuedAt) !== null && _d !== void 0 ? _d : new Date();
3869
+ const expirationDate = (_e = options.expirationTime) !== null && _e !== void 0 ? _e : new Date(issuedAtDate.getTime() + DEFAULT_VALIDITY_MS);
3870
+ if (!Number.isFinite(issuedAtDate.getTime()) || !Number.isFinite(expirationDate.getTime()) || expirationDate <= issuedAtDate) {
3871
+ throw new Error('SIWE validity window is invalid');
3872
+ }
3873
+ if (expirationDate.getTime() - issuedAtDate.getTime() > 10 * 60 * 1000) {
3874
+ throw new Error('SIWE validity window exceeds 10 minutes');
3875
+ }
3876
+ const issuedAt = issuedAtDate.toISOString();
3877
+ const expirationTime = expirationDate.toISOString();
3878
+ return (`${domain} wants you to sign in with your Ethereum account:\n${addr}\n\n` +
3879
+ `${statement}\n\n` +
3880
+ `URI: ${parsedUri.toString()}\n` +
3881
+ `Version: ${version}\n` +
3882
+ `Chain ID: ${chainId}\n` +
3883
+ `Nonce: ${nonce}\n` +
3884
+ `Issued At: ${issuedAt}\n` +
3885
+ `Expiration Time: ${expirationTime}\n` +
3886
+ `Resources:\n` +
3887
+ `- bounded:app:${appId}`);
3888
+ }
3889
+ /**
3890
+ * Lowercase an EVM address to the canonical identity form. EVM addresses are
3891
+ * case-insensitive on-chain but the identity layer (@user.evmAddress) matches
3892
+ * case-sensitively, so every boundary normalizes to lowercase. Does not validate
3893
+ * shape — use normalizeEvmAddress() when the input is untrusted.
3894
+ */
3895
+ function toLowerEvmAddress(address) {
3896
+ return address.trim().toLowerCase();
3897
+ }
3898
+ /**
3899
+ * Normalize an untrusted EVM address to canonical lowercase 0x-hex, or null when
3900
+ * it isn't a well-formed 20-byte hex address. Mirrors the realtime-worker's
3901
+ * normalizeEvmAddress (auth.ts) EXACTLY so a client-derived @user.evmAddress and
3902
+ * the server-authenticated one are byte-identical (fail-closed on malformed).
3903
+ */
3904
+ function normalizeEvmAddress(raw) {
3905
+ if (typeof raw !== 'string')
3906
+ return null;
3907
+ const v = raw.trim().toLowerCase();
3908
+ return /^0x[0-9a-f]{40}$/.test(v) ? v : null;
3909
+ }
3910
+
3749
3911
  async function createBearerToken(isServer) {
3750
3912
  if (isServer) {
3751
3913
  const sessionMgr = ServerSessionManager.instance;
@@ -3794,7 +3956,7 @@ async function getUserInfo(isServer) {
3794
3956
  * Returns all-null when the token can't be decoded (caller treats as no identity).
3795
3957
  */
3796
3958
  function deriveUserIdentityFromIdToken(idToken) {
3797
- const empty = { id: null, address: null, email: null };
3959
+ const empty = { id: null, address: null, email: null, evmAddress: null };
3798
3960
  if (!idToken)
3799
3961
  return empty;
3800
3962
  try {
@@ -3807,14 +3969,25 @@ function deriveUserIdentityFromIdToken(idToken) {
3807
3969
  ? walletClaim
3808
3970
  : null;
3809
3971
  const userIdClaim = payload['custom:userId'];
3810
- const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3972
+ let id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
3811
3973
  ? userIdClaim
3812
3974
  : null;
3813
3975
  const emailClaim = payload['email'];
3814
3976
  const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
3815
3977
  ? emailClaim.toLowerCase()
3816
3978
  : null;
3817
- return { id, address, email };
3979
+ // EVM wallet — carried on the `custom:evmWalletAddress` claim (an EVM login
3980
+ // OMITS custom:walletAddress). Normalized to canonical lowercase 0x-hex the
3981
+ // SAME way the realtime-worker does, so the client @user.evmAddress and the
3982
+ // server-authenticated one are byte-identical (fail-closed on malformed).
3983
+ const evmAddress = normalizeEvmAddress(payload['custom:evmWalletAddress']);
3984
+ // A SIWE identity uses its EVM address as custom:userId. Normalize legacy
3985
+ // mixed-case tokens at this boundary too so the principal and
3986
+ // @user.evmAddress cannot diverge solely by hex casing. Opaque human-login
3987
+ // ids remain byte-for-byte unchanged even when they have a linked wallet.
3988
+ if (evmAddress && id && normalizeEvmAddress(id) === evmAddress)
3989
+ id = evmAddress;
3990
+ return { id, address, email, evmAddress };
3818
3991
  }
3819
3992
  catch (_a) {
3820
3993
  return empty;
@@ -4136,6 +4309,456 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4136
4309
  }
4137
4310
  }
4138
4311
 
4312
+ /**
4313
+ * Selectors derived from the canonical BoundedRuntime signatures (BoundedTypes.sol):
4314
+ *
4315
+ * cast sig 'setDocuments(string,(string[],bool,(string,uint8,uint8,bytes)[])[])'
4316
+ * => 0x0827b094
4317
+ * cast sig 'setDocumentsAttested(string,(string[],bool,(string,uint8,uint8,bytes)[])[],(bytes32,uint256,uint256,uint8,bytes32,bytes32))'
4318
+ * => 0x87b111c4
4319
+ *
4320
+ * Keep the signatures beside the constants: changing either Solidity tuple is an
4321
+ * ABI break and must deliberately update these values and the Foundry goldens.
4322
+ */
4323
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = '0x0827b094';
4324
+ const BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = '0x87b111c4';
4325
+ const SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR.slice(2);
4326
+ const SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR.slice(2);
4327
+ const MAX_CALLDATA_BYTES = 1024 * 1024;
4328
+ const MAX_ARRAY_ITEMS = 2048;
4329
+ const BIGINT_ZERO = BigInt(0);
4330
+ const BIGINT_ONE = BigInt(1);
4331
+ const BIGINT_EIGHT = BigInt(8);
4332
+ const SECP256K1_N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
4333
+ const SECP256K1_HALF_N = BigInt('0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0');
4334
+ function hexBytes(raw) {
4335
+ if (typeof raw !== 'string' || !/^0x[0-9a-fA-F]*$/.test(raw) || raw.length % 2 !== 0) {
4336
+ throw new Error('EVM transaction calldata must be even-length 0x hex');
4337
+ }
4338
+ const hex = raw.slice(2);
4339
+ if (hex.length / 2 > MAX_CALLDATA_BYTES)
4340
+ throw new Error('EVM transaction calldata exceeds the client safety limit');
4341
+ const out = new Uint8Array(hex.length / 2);
4342
+ for (let i = 0; i < out.length; i++)
4343
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
4344
+ return out;
4345
+ }
4346
+ class AbiReader {
4347
+ constructor(bytes) {
4348
+ this.bytes = bytes;
4349
+ this.maxRead = 0;
4350
+ this.dynamicPayloadBytes = 0;
4351
+ this.utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: false });
4352
+ }
4353
+ range(offset, length, label) {
4354
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > this.bytes.length) {
4355
+ throw new Error(`Malformed EVM setDocuments calldata (${label})`);
4356
+ }
4357
+ this.maxRead = Math.max(this.maxRead, offset + length);
4358
+ }
4359
+ word(offset, label) {
4360
+ this.range(offset, 32, label);
4361
+ let value = BIGINT_ZERO;
4362
+ for (let i = 0; i < 32; i++)
4363
+ value = (value << BIGINT_EIGHT) | BigInt(this.bytes[offset + i]);
4364
+ return value;
4365
+ }
4366
+ numberWord(offset, label, max = Number.MAX_SAFE_INTEGER) {
4367
+ const value = this.word(offset, label);
4368
+ if (value > BigInt(max))
4369
+ throw new Error(`Malformed EVM setDocuments calldata (${label} too large)`);
4370
+ return Number(value);
4371
+ }
4372
+ offset(base, wordOffset, min, label) {
4373
+ const relative = this.numberWord(wordOffset, `${label} offset`, this.bytes.length);
4374
+ if (relative < min || relative % 32 !== 0)
4375
+ throw new Error(`Malformed EVM setDocuments calldata (${label} offset)`);
4376
+ const absolute = base + relative;
4377
+ this.range(absolute, 0, label);
4378
+ return absolute;
4379
+ }
4380
+ dynamicBytes(at, label) {
4381
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4382
+ this.dynamicPayloadBytes += length;
4383
+ // ABI offsets may alias. Cap the cumulative decoded payload so a malicious
4384
+ // 1 MB tail referenced by thousands of fields cannot amplify into a
4385
+ // multi-gigabyte allocation/UTF-8 decode in the wallet process.
4386
+ if (this.dynamicPayloadBytes > MAX_CALLDATA_BYTES) {
4387
+ throw new Error('Malformed EVM setDocuments calldata (dynamic payload amplification)');
4388
+ }
4389
+ const start = at + 32;
4390
+ const padded = Math.ceil(length / 32) * 32;
4391
+ this.range(start, padded, label);
4392
+ for (let i = start + length; i < start + padded; i++) {
4393
+ if (this.bytes[i] !== 0)
4394
+ throw new Error(`Malformed EVM setDocuments calldata (${label} padding)`);
4395
+ }
4396
+ return this.bytes.slice(start, start + length);
4397
+ }
4398
+ dynamicEnd(at, label) {
4399
+ const length = this.numberWord(at, `${label} length`, MAX_CALLDATA_BYTES);
4400
+ const end = at + 32 + Math.ceil(length / 32) * 32;
4401
+ this.range(at + 32, Math.ceil(length / 32) * 32, label);
4402
+ return end;
4403
+ }
4404
+ string(at, label) {
4405
+ try {
4406
+ return this.utf8.decode(this.dynamicBytes(at, label));
4407
+ }
4408
+ catch (_a) {
4409
+ throw new Error(`Malformed EVM setDocuments calldata (${label} utf8)`);
4410
+ }
4411
+ }
4412
+ finish() {
4413
+ if (this.maxRead !== this.bytes.length)
4414
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4415
+ }
4416
+ }
4417
+ function assertCanonicalOffset(actual, expected, label) {
4418
+ if (actual !== expected)
4419
+ throw new Error(`Malformed EVM setDocuments calldata (${label} non-canonical offset)`);
4420
+ }
4421
+ function decodeStringArray(reader, at, label) {
4422
+ const count = reader.numberWord(at, `${label} count`, MAX_ARRAY_ITEMS);
4423
+ const head = at + 32;
4424
+ const min = count * 32;
4425
+ const out = [];
4426
+ let expectedTail = head + min;
4427
+ for (let i = 0; i < count; i++) {
4428
+ const itemAt = reader.offset(head, head + i * 32, min, `${label}[${i}]`);
4429
+ assertCanonicalOffset(itemAt, expectedTail, `${label}[${i}]`);
4430
+ out.push(reader.string(itemAt, `${label}[${i}]`));
4431
+ expectedTail = reader.dynamicEnd(itemAt, `${label}[${i}]`);
4432
+ }
4433
+ return { value: out, end: expectedTail };
4434
+ }
4435
+ function decodeFields(reader, at) {
4436
+ const count = reader.numberWord(at, 'fields count', MAX_ARRAY_ITEMS);
4437
+ const head = at + 32;
4438
+ const min = count * 32;
4439
+ const out = [];
4440
+ let expectedTail = head + min;
4441
+ for (let i = 0; i < count; i++) {
4442
+ const tuple = reader.offset(head, head + i * 32, min, `field[${i}]`);
4443
+ assertCanonicalOffset(tuple, expectedTail, `field[${i}]`);
4444
+ const nameAt = reader.offset(tuple, tuple, 128, `field[${i}].name`);
4445
+ assertCanonicalOffset(nameAt, tuple + 128, `field[${i}].name`);
4446
+ const name = reader.string(nameAt, `field[${i}].name`);
4447
+ const nameEnd = reader.dynamicEnd(nameAt, `field[${i}].name`);
4448
+ const ftype = reader.numberWord(tuple + 32, `field[${i}].ftype`, 255);
4449
+ const op = reader.numberWord(tuple + 64, `field[${i}].op`, 255);
4450
+ const valueAt = reader.offset(tuple, tuple + 96, 128, `field[${i}].value`);
4451
+ assertCanonicalOffset(valueAt, nameEnd, `field[${i}].value`);
4452
+ const value = reader.dynamicBytes(valueAt, `field[${i}].value`);
4453
+ expectedTail = reader.dynamicEnd(valueAt, `field[${i}].value`);
4454
+ out.push({ name, ftype, op, value });
4455
+ }
4456
+ return { value: out, end: expectedTail };
4457
+ }
4458
+ function decodeRuntimeWrite(rawData) {
4459
+ const bytes = hexBytes(rawData);
4460
+ if (bytes.length < 68)
4461
+ throw new Error('Malformed EVM setDocuments calldata');
4462
+ const selector = Array.from(bytes.slice(0, 4)).map((b) => b.toString(16).padStart(2, '0')).join('');
4463
+ const kind = selector === SET_DOCUMENTS_SELECTOR
4464
+ ? 'setDocuments'
4465
+ : selector === SET_DOCUMENTS_ATTESTED_SELECTOR
4466
+ ? 'setDocumentsAttested'
4467
+ : null;
4468
+ if (!kind)
4469
+ throw new Error('EVM transaction is not a supported BoundedRuntime document write');
4470
+ const reader = new AbiReader(bytes);
4471
+ reader.maxRead = 4;
4472
+ const args = 4;
4473
+ // ExternalCallAttestation is a fully-static six-word tuple and is therefore
4474
+ // expanded inline into the top-level head: 2 dynamic offsets + 6 words.
4475
+ const headBytes = kind === 'setDocumentsAttested' ? 256 : 64;
4476
+ const appAt = reader.offset(args, args, headBytes, 'appId');
4477
+ assertCanonicalOffset(appAt, args + headBytes, 'appId');
4478
+ const opsAt = reader.offset(args, args + 32, headBytes, 'ops');
4479
+ const appId = reader.string(appAt, 'appId');
4480
+ const appEnd = reader.dynamicEnd(appAt, 'appId');
4481
+ assertCanonicalOffset(opsAt, appEnd, 'ops');
4482
+ const count = reader.numberWord(opsAt, 'ops count', MAX_ARRAY_ITEMS);
4483
+ const head = opsAt + 32;
4484
+ const min = count * 32;
4485
+ const ops = [];
4486
+ let expectedTail = head + min;
4487
+ for (let i = 0; i < count; i++) {
4488
+ const tuple = reader.offset(head, head + i * 32, min, `op[${i}]`);
4489
+ assertCanonicalOffset(tuple, expectedTail, `op[${i}]`);
4490
+ const pathAt = reader.offset(tuple, tuple, 96, `op[${i}].path`);
4491
+ assertCanonicalOffset(pathAt, tuple + 96, `op[${i}].path`);
4492
+ const deleteWord = reader.numberWord(tuple + 32, `op[${i}].isDelete`, 1);
4493
+ const fieldsAt = reader.offset(tuple, tuple + 64, 96, `op[${i}].fields`);
4494
+ const path = decodeStringArray(reader, pathAt, `op[${i}].path`);
4495
+ assertCanonicalOffset(fieldsAt, path.end, `op[${i}].fields`);
4496
+ const fields = decodeFields(reader, fieldsAt);
4497
+ expectedTail = fields.end;
4498
+ ops.push({
4499
+ pathSegments: path.value,
4500
+ isDelete: deleteWord === 1,
4501
+ fields: fields.value,
4502
+ });
4503
+ }
4504
+ let attestation;
4505
+ if (kind === 'setDocumentsAttested') {
4506
+ const callsHash = reader.word(args + 64, 'attestation.callsHash');
4507
+ const nonce = reader.word(args + 96, 'attestation.nonce');
4508
+ const deadline = reader.word(args + 128, 'attestation.deadline');
4509
+ const v = reader.numberWord(args + 160, 'attestation.v', 255);
4510
+ const r = reader.word(args + 192, 'attestation.r');
4511
+ const s = reader.word(args + 224, 'attestation.s');
4512
+ if (v !== 27 && v !== 28)
4513
+ throw new Error('Malformed EVM setDocuments calldata (attestation.v must be 27 or 28)');
4514
+ if (r === BIGINT_ZERO || r >= SECP256K1_N)
4515
+ throw new Error('Malformed EVM setDocuments calldata (attestation.r is not canonical)');
4516
+ if (s === BIGINT_ZERO || s > SECP256K1_HALF_N)
4517
+ throw new Error('Malformed EVM setDocuments calldata (attestation.s is not canonical low-s)');
4518
+ attestation = { callsHash, nonce, deadline, v, r, s };
4519
+ }
4520
+ if (expectedTail !== bytes.length)
4521
+ throw new Error('Malformed EVM setDocuments calldata (unreferenced trailing bytes)');
4522
+ reader.finish();
4523
+ return Object.assign({ kind, appId, ops }, (attestation ? { attestation } : {}));
4524
+ }
4525
+ /**
4526
+ * Fail closed unless calldata is a byte-for-byte canonical ABI encoding of one
4527
+ * supported BoundedRuntime write. This is the defense used by direct signer
4528
+ * surfaces; the higher-level write validator below additionally binds app/ops.
4529
+ */
4530
+ function assertCanonicalBoundedRuntimeWriteCalldata(rawData) {
4531
+ decodeRuntimeWrite(rawData);
4532
+ }
4533
+ const MAX_EVM_DOCUMENT_PATH_UTF8_BYTES = 1024;
4534
+ function canonicalUtf8ByteLength(value) {
4535
+ let bytes = 0;
4536
+ for (let i = 0; i < value.length; i++) {
4537
+ const unit = value.charCodeAt(i);
4538
+ if (unit <= 0x7f)
4539
+ bytes += 1;
4540
+ else if (unit <= 0x7ff)
4541
+ bytes += 2;
4542
+ else if (unit >= 0xd800 && unit <= 0xdbff) {
4543
+ const low = value.charCodeAt(++i);
4544
+ if (!(low >= 0xdc00 && low <= 0xdfff))
4545
+ throw new Error(`Invalid EVM write path: ${value}`);
4546
+ bytes += 4;
4547
+ }
4548
+ else if (unit >= 0xdc00 && unit <= 0xdfff) {
4549
+ throw new Error(`Invalid EVM write path: ${value}`);
4550
+ }
4551
+ else
4552
+ bytes += 3;
4553
+ }
4554
+ return bytes;
4555
+ }
4556
+ function normalizedPathSegments(path) {
4557
+ if (typeof path !== 'string' || path.length === 0 ||
4558
+ canonicalUtf8ByteLength(path) > MAX_EVM_DOCUMENT_PATH_UTF8_BYTES) {
4559
+ throw new Error(`Invalid EVM write path: ${path}`);
4560
+ }
4561
+ const segments = path.split('/');
4562
+ if (segments.length < 2 || segments.length % 2 !== 0 ||
4563
+ segments[0].startsWith('__') ||
4564
+ segments.some((s) => !s || s === '.' || s === '..' || /[\\\u0000-\u001f\u007f]/.test(s))) {
4565
+ throw new Error(`Invalid EVM write path: ${path}`);
4566
+ }
4567
+ return segments;
4568
+ }
4569
+ function bytesBigInt(bytes, signed) {
4570
+ if (bytes.length !== 32)
4571
+ throw new Error('EVM field value is not one ABI word');
4572
+ let value = BIGINT_ZERO;
4573
+ for (const b of bytes)
4574
+ value = (value << BIGINT_EIGHT) | BigInt(b);
4575
+ if (signed && (value & (BIGINT_ONE << BigInt(255))) !== BIGINT_ZERO)
4576
+ value -= BIGINT_ONE << BigInt(256);
4577
+ return value;
4578
+ }
4579
+ function decodedAbiString(bytes) {
4580
+ const reader = new AbiReader(bytes);
4581
+ const at = reader.offset(0, 0, 32, 'field string');
4582
+ const value = reader.string(at, 'field string');
4583
+ reader.finish();
4584
+ return value;
4585
+ }
4586
+ function assertSetValue(field, expected) {
4587
+ if (field.op !== 0)
4588
+ throw new Error(`EVM field ${field.name} changes the requested SET action`);
4589
+ if (typeof expected === 'boolean') {
4590
+ if (field.ftype !== 3 || bytesBigInt(field.value, false) !== (expected ? BIGINT_ONE : BIGINT_ZERO))
4591
+ throw new Error(`EVM field ${field.name} value mismatch`);
4592
+ return;
4593
+ }
4594
+ if (typeof expected === 'number') {
4595
+ if (!Number.isFinite(expected))
4596
+ throw new Error(`EVM field ${field.name} is not finite`);
4597
+ if (Number.isInteger(expected)) {
4598
+ if (!Number.isSafeInteger(expected)) {
4599
+ throw new Error(`EVM field ${field.name} integer must be a safe number or canonical decimal string`);
4600
+ }
4601
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4602
+ if (actual === null || actual !== BigInt(expected))
4603
+ throw new Error(`EVM field ${field.name} value mismatch`);
4604
+ return;
4605
+ }
4606
+ if (field.ftype !== 4 || decodedAbiString(field.value) !== expected.toString())
4607
+ throw new Error(`EVM field ${field.name} value mismatch`);
4608
+ return;
4609
+ }
4610
+ if (typeof expected === 'bigint') {
4611
+ const actual = field.ftype === 1 ? bytesBigInt(field.value, false) : field.ftype === 2 ? bytesBigInt(field.value, true) : null;
4612
+ if (actual === null || actual !== expected)
4613
+ throw new Error(`EVM field ${field.name} value mismatch`);
4614
+ return;
4615
+ }
4616
+ if (typeof expected === 'string') {
4617
+ if (field.ftype === 4 && decodedAbiString(field.value) === expected)
4618
+ return;
4619
+ if ((field.ftype === 1 || field.ftype === 2) && expected.length <= 20 && /^-?(?:0|[1-9][0-9]*)$/.test(expected)) {
4620
+ const exact = BigInt(expected);
4621
+ if (exact >= BigInt(Number.MIN_SAFE_INTEGER) && exact <= BigInt(Number.MAX_SAFE_INTEGER)) {
4622
+ throw new Error(`EVM field ${field.name} safe integer must be supplied as a number`);
4623
+ }
4624
+ const inRange = field.ftype === 1
4625
+ ? exact >= BIGINT_ZERO && exact <= ((BIGINT_ONE << BigInt(64)) - BIGINT_ONE)
4626
+ : exact >= -(BIGINT_ONE << BigInt(63)) && exact <= ((BIGINT_ONE << BigInt(63)) - BIGINT_ONE);
4627
+ const actual = bytesBigInt(field.value, field.ftype === 2);
4628
+ if (inRange && actual === exact)
4629
+ return;
4630
+ throw new Error(`EVM field ${field.name} value mismatch`);
4631
+ }
4632
+ if (field.ftype === 5 && normalizeEvmAddress(expected)) {
4633
+ const word = field.value;
4634
+ if (word.length === 32 && word.slice(0, 12).every((b) => b === 0)) {
4635
+ const actual = `0x${Array.from(word.slice(12)).map((b) => b.toString(16).padStart(2, '0')).join('')}`;
4636
+ if (actual === normalizeEvmAddress(expected))
4637
+ return;
4638
+ }
4639
+ }
4640
+ throw new Error(`EVM field ${field.name} value mismatch`);
4641
+ }
4642
+ if (expected === undefined || expected === null) {
4643
+ throw new Error(`EVM field ${field.name} cannot be represented safely on EVM; use a document delete or omit the field`);
4644
+ }
4645
+ const json = JSON.stringify(expected);
4646
+ if (typeof json !== 'string' || field.ftype !== 4 || decodedAbiString(field.value) !== json) {
4647
+ throw new Error(`EVM field ${field.name} value mismatch`);
4648
+ }
4649
+ }
4650
+ function assertField(field, expected) {
4651
+ if (expected === null) {
4652
+ if (field.op !== 2 || field.ftype < 1 || field.ftype > 5 || field.value.length !== 0) {
4653
+ throw new Error(`EVM field ${field.name} deletion mismatch`);
4654
+ }
4655
+ return;
4656
+ }
4657
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
4658
+ const operation = expected.operation;
4659
+ if (operation === 'increment') {
4660
+ const delta = expected.value;
4661
+ const actual = field.ftype === 1 || field.ftype === 2
4662
+ ? bytesBigInt(field.value, true)
4663
+ : null;
4664
+ if (!Number.isSafeInteger(delta) || field.op !== 3 || actual === null || actual !== BigInt(delta)) {
4665
+ throw new Error(`EVM field ${field.name} increment mismatch`);
4666
+ }
4667
+ return;
4668
+ }
4669
+ if (operation === 'time' && expected.value === 'now') {
4670
+ if (field.op !== 4 || (field.ftype !== 1 && field.ftype !== 2) || field.value.length !== 0) {
4671
+ throw new Error(`EVM field ${field.name} timestamp mismatch`);
4672
+ }
4673
+ return;
4674
+ }
4675
+ }
4676
+ assertSetValue(field, expected);
4677
+ }
4678
+ function assertOp(decoded, path, document) {
4679
+ const expectedSegments = normalizedPathSegments(path);
4680
+ if (decoded.pathSegments.length !== expectedSegments.length || decoded.pathSegments.some((s, i) => s !== expectedSegments[i])) {
4681
+ throw new Error(`EVM transaction path does not match requested write: ${path}`);
4682
+ }
4683
+ if (decoded.isDelete !== (document === null))
4684
+ throw new Error(`EVM transaction action does not match requested write: ${path}`);
4685
+ if (document === null) {
4686
+ if (decoded.fields.length !== 0)
4687
+ throw new Error(`EVM delete unexpectedly carries fields: ${path}`);
4688
+ return;
4689
+ }
4690
+ if (!document || typeof document !== 'object' || Array.isArray(document))
4691
+ throw new Error(`EVM document must be a plain object or null: ${path}`);
4692
+ const expectedEntries = Object.entries(document);
4693
+ if (expectedEntries.some(([name]) => name.startsWith('_')))
4694
+ throw new Error(`Reserved EVM field reached transaction validation: ${path}`);
4695
+ const expected = new Map(expectedEntries);
4696
+ const seen = new Set();
4697
+ for (const field of decoded.fields) {
4698
+ if (!field.name || seen.has(field.name) || !expected.has(field.name))
4699
+ throw new Error(`EVM transaction contains unexpected or duplicate field ${field.name}`);
4700
+ seen.add(field.name);
4701
+ assertField(field, expected.get(field.name));
4702
+ }
4703
+ const missing = [...expected.keys()].filter((name) => !seen.has(name));
4704
+ if (missing.length > 0)
4705
+ throw new Error(`EVM transaction omits requested fields: ${missing.join(', ')}`);
4706
+ }
4707
+ function zeroWei(raw) {
4708
+ if (raw === undefined || raw === null || raw === '')
4709
+ return true;
4710
+ if (typeof raw !== 'string' && typeof raw !== 'number')
4711
+ return false;
4712
+ try {
4713
+ return BigInt(raw) === BIGINT_ZERO;
4714
+ }
4715
+ catch (_a) {
4716
+ return false;
4717
+ }
4718
+ }
4719
+ /** Verify the untrusted 202 payload is exactly the governed write the app asked for. */
4720
+ function assertEvmTransactionMatchesWrite(transaction, context) {
4721
+ var _a, _b;
4722
+ if (!transaction || !Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0) {
4723
+ throw new Error('EVM transaction chainId is required');
4724
+ }
4725
+ const chainId = transaction.chainId;
4726
+ const policy = context.policy;
4727
+ if (!policy)
4728
+ throw new Error('EVM transaction signing requires an explicit runtime/chain policy');
4729
+ if (policy.chainId !== undefined && policy.chainId !== chainId)
4730
+ throw new Error(`EVM transaction chain ${chainId} does not match configured chain ${policy.chainId}`);
4731
+ const expectedRuntime = (_b = (_a = policy.runtimeAddresses) === null || _a === void 0 ? void 0 : _a[String(chainId)]) !== null && _b !== void 0 ? _b : (policy.chainId === chainId ? policy.runtimeAddress : undefined);
4732
+ const normalizedRuntime = normalizeEvmAddress(expectedRuntime);
4733
+ if (!normalizedRuntime)
4734
+ throw new Error(`No trusted BoundedRuntime address configured for EVM chain ${chainId}`);
4735
+ if (normalizeEvmAddress(transaction.to) !== normalizedRuntime)
4736
+ throw new Error('EVM transaction target is not the configured BoundedRuntime');
4737
+ if (!zeroWei(transaction.value))
4738
+ throw new Error('BoundedRuntime.setDocuments must not transfer native value');
4739
+ if (transaction.nonce !== undefined)
4740
+ throw new Error('Server-supplied EVM nonce is not accepted');
4741
+ const decoded = decodeRuntimeWrite(transaction.data);
4742
+ if (decoded.appId !== context.appId)
4743
+ throw new Error('EVM transaction appId does not match configured appId');
4744
+ if (decoded.ops.length !== context.documents.length)
4745
+ throw new Error('EVM transaction operation count does not match requested writes');
4746
+ // The worker's contract ABI groups upserts first and document deletes second.
4747
+ const expectedOps = [
4748
+ ...context.documents.filter((entry) => entry.document !== null),
4749
+ ...context.documents.filter((entry) => entry.document === null),
4750
+ ];
4751
+ const paths = new Set();
4752
+ for (let i = 0; i < expectedOps.length; i++) {
4753
+ const expected = expectedOps[i];
4754
+ const canonicalPath = normalizedPathSegments(expected.destinationPath).join('/');
4755
+ if (paths.has(canonicalPath))
4756
+ throw new Error(`Duplicate EVM write path is not allowed: ${canonicalPath}`);
4757
+ paths.add(canonicalPath);
4758
+ assertOp(decoded.ops[i], canonicalPath, expected.document);
4759
+ }
4760
+ }
4761
+
4139
4762
  var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
4140
4763
  var t = {};
4141
4764
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -4272,6 +4895,21 @@ function hashForKey$1(value) {
4272
4895
  }
4273
4896
  return h.toString(36);
4274
4897
  }
4898
+ // Memoize the compact principal hash per exact credential string. Every read
4899
+ // hashed the entire (~1KB) bearer/idToken on every call; a session presents
4900
+ // the same handful of credentials for its lifetime. Bounded size — a rotation
4901
+ // simply inserts the new token and stale entries fall off on clear.
4902
+ const principalHashCache = new Map();
4903
+ function hashForKeyCached(value) {
4904
+ const hit = principalHashCache.get(value);
4905
+ if (hit !== undefined)
4906
+ return hit;
4907
+ const h = hashForKey$1(value);
4908
+ if (principalHashCache.size >= 50)
4909
+ principalHashCache.clear();
4910
+ principalHashCache.set(value, h);
4911
+ return h;
4912
+ }
4275
4913
  function hasAuthHeader(headers) {
4276
4914
  if (!headers)
4277
4915
  return false;
@@ -4282,10 +4920,10 @@ function authValueFromHeaders(headers) {
4282
4920
  return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
4283
4921
  }
4284
4922
  function principalFromAuthValue(authValue) {
4285
- return authValue ? `h${hashForKey$1(authValue)}` : null;
4923
+ return authValue ? `h${hashForKeyCached(authValue)}` : null;
4286
4924
  }
4287
4925
  function principalFromIdToken$1(idToken) {
4288
- return idToken ? `t${hashForKey$1(idToken)}` : null;
4926
+ return idToken ? `t${hashForKeyCached(idToken)}` : null;
4289
4927
  }
4290
4928
  function uncacheableReadKey(appId, scope) {
4291
4929
  uncacheableReadKeyCounter += 1;
@@ -5166,13 +5804,12 @@ async function runExpressionMany(many) {
5166
5804
  * collection's policy `delete` rule and broadcasts a delete to subscribers.
5167
5805
  */
5168
5806
  async function set(path, document, options) {
5169
- const result = await setMany([{ path, document }], options);
5170
- // Clear cache entries that might be affected by this update
5171
- clearCacheByPrefix(path);
5172
- return result;
5807
+ // setMany already invalidates the read cache for the affected document and
5808
+ // collection paths — no second sweep needed here.
5809
+ return setMany([{ path, document }], options);
5173
5810
  }
5174
5811
  async function setMany(many, options) {
5175
- var _a, _b, _c, _d, _e, _f, _g;
5812
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5176
5813
  // Returns the data that was set, or undefined if the document was already set.
5177
5814
  try {
5178
5815
  const config = await getConfig();
@@ -5228,6 +5865,9 @@ async function setMany(many, options) {
5228
5865
  // the API did nothing and gave us the required info to run this transaction.
5229
5866
  const { transactions, offchainTransaction } = setResponse.data;
5230
5867
  // Handle offchain transaction flow (for 'offchain' protocol apps)
5868
+ if (offchainTransaction && Array.isArray(transactions) && transactions.length > 0) {
5869
+ throw new Error('Ambiguous write response mixes offchain and onchain transaction targets');
5870
+ }
5231
5871
  if (offchainTransaction) {
5232
5872
  const transactionResult = await handleOffchainTransaction(offchainTransaction, authProvider, options);
5233
5873
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
@@ -5237,9 +5877,35 @@ async function setMany(many, options) {
5237
5877
  throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
5238
5878
  }
5239
5879
  const curTx = transactions[0];
5880
+ const hasEvmTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.evmTransaction) != null;
5881
+ const hasSerializedSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.serializedTransaction) != null;
5882
+ const hasBuiltSolanaTarget = (curTx === null || curTx === void 0 ? void 0 : curTx.transactionArgs) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.remainingAccounts) != null ||
5883
+ (curTx === null || curTx === void 0 ? void 0 : curTx.idl) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.txData) != null || (curTx === null || curTx === void 0 ? void 0 : curTx.preInstructions) != null;
5884
+ if ((hasEvmTarget && (hasSerializedSolanaTarget || hasBuiltSolanaTarget)) ||
5885
+ (hasSerializedSolanaTarget && hasBuiltSolanaTarget)) {
5886
+ throw new Error('Ambiguous on-chain write response mixes EVM and/or Solana transaction target formats');
5887
+ }
5240
5888
  let transactionResult;
5241
5889
  const expectedWritePaths = documents.map(d => d.destinationPath);
5242
- if (curTx.serializedTransaction) {
5890
+ // EVM onchain write: the worker returns a PRE-BUILT EIP-1559 tx on
5891
+ // `transactions[0].evmTransaction` (the EVM twin of Solana's pre-built
5892
+ // `serializedTransaction`). WORKER 202 CONTRACT (field name + shape the
5893
+ // worker side must emit to match):
5894
+ // { transactions: [ { evmTransaction: {
5895
+ // to, data, value?, chainId, gas?, maxFeePerGas?, maxPriorityFeePerGas?, nonce?
5896
+ // } } ] }
5897
+ // We hand it to the EVM auth provider's runTransaction(evmTx, undefined, options)
5898
+ // (eth_sendTransaction — the wallet signs + submits). syncItems below is
5899
+ // identical to the Solana path.
5900
+ if (curTx.evmTransaction) {
5901
+ assertEvmTransactionMatchesWrite(curTx.evmTransaction, {
5902
+ appId: config.appId,
5903
+ documents,
5904
+ policy: (_k = (_j = (_h = options === null || options === void 0 ? void 0 : options._overrides) === null || _h === void 0 ? void 0 : _h._evmTransactionPolicy) !== null && _j !== void 0 ? _j : config.evmWalletConfig) !== null && _k !== void 0 ? _k : null,
5905
+ });
5906
+ transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
5907
+ }
5908
+ else if (curTx.serializedTransaction) {
5243
5909
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
5244
5910
  }
5245
5911
  else {
@@ -5263,7 +5929,7 @@ async function setMany(many, options) {
5263
5929
  else if (setResponse.data &&
5264
5930
  typeof setResponse.data === 'object' &&
5265
5931
  setResponse.data.success === true) {
5266
- const _h = setResponse.data, { success: _success } = _h, rest = __rest$1(_h, ["success"]);
5932
+ const _l = setResponse.data, { success: _success } = _l, rest = __rest$1(_l, ["success"]);
5267
5933
  return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
5268
5934
  }
5269
5935
  else {
@@ -5363,6 +6029,14 @@ async function setMany(many, options) {
5363
6029
  const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
5364
6030
  return transactionResult;
5365
6031
  }
6032
+ // EVM onchain write: hand the worker's pre-built EIP-1559 tx to the EVM auth
6033
+ // provider, which eth_sendTransaction-s it (the wallet signs + submits in one
6034
+ // step). Mirrors handlePreBuiltTransaction's shape but routes through the EVM
6035
+ // slot of runTransaction (Solana slot undefined). shouldSubmitTx is honored by
6036
+ // the provider itself.
6037
+ async function handleEvmTransaction(evmTransaction, authProvider, options) {
6038
+ return authProvider.runTransaction(evmTransaction, undefined, options);
6039
+ }
5366
6040
  async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
5367
6041
  var _a, _b, _c;
5368
6042
  const config = await getConfig();
@@ -6743,7 +7417,7 @@ function sendUnsubscribe(connection, subscriptionId) {
6743
7417
  * the subscription onto, since a WS connection routes to a single DO.
6744
7418
  */
6745
7419
  async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6746
- var _a, _b;
7420
+ var _a, _b, _c, _d, _e;
6747
7421
  const config = await getConfig();
6748
7422
  const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
6749
7423
  // Use explicit appId override if provided, otherwise fall back to config
@@ -6794,19 +7468,23 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6794
7468
  const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
6795
7469
  // Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
6796
7470
  let existingSubscription;
7471
+ let specKey = '';
6797
7472
  try {
6798
- const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
6799
- const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
6800
- const sortKey = subscriptionOptions.sort ? JSON.stringify(subscriptionOptions.sort) : '';
7473
+ // Stringify THIS call's specs once; existing subscriptions carry their
7474
+ // precomputed specKey, so the scan is a string compare per entry
7475
+ // (previously each new subscribe re-stringified every existing
7476
+ // subscription's shape/filter/sort — O(N²) with many live hooks).
7477
+ specKey = JSON.stringify([
7478
+ (_a = subscriptionOptions.shape) !== null && _a !== void 0 ? _a : null,
7479
+ (_b = subscriptionOptions.filter) !== null && _b !== void 0 ? _b : null,
7480
+ (_c = subscriptionOptions.sort) !== null && _c !== void 0 ? _c : null,
7481
+ ]);
6801
7482
  for (const sub of connection.subscriptions.values()) {
6802
- const subShapeKey = sub.shape ? JSON.stringify(sub.shape) : '';
6803
- const subFilterKey = sub.filter ? JSON.stringify(sub.filter) : '';
6804
- const subSortKey = sub.sort ? JSON.stringify(sub.sort) : '';
6805
7483
  // H1: also require the same principal identity (compact hash AND exact
6806
7484
  // token) — a different logged-in user must not piggy-back on (and
6807
7485
  // inherit lastData from) another user's existing subscription, even
6808
7486
  // with identical path/prompt/shape/filter/sort.
6809
- 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) {
7487
+ if (sub.identity === identity && sub.identityToken === identityToken && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && sub.specKey === specKey && sub.includeSubPaths === ((_d = subscriptionOptions.includeSubPaths) !== null && _d !== void 0 ? _d : false) && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
6810
7488
  existingSubscription = sub;
6811
7489
  break;
6812
7490
  }
@@ -6899,7 +7577,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6899
7577
  shape: subscriptionOptions.shape,
6900
7578
  limit: subscriptionOptions.limit,
6901
7579
  cursor: subscriptionOptions.cursor,
6902
- includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
7580
+ includeSubPaths: (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false,
6903
7581
  callbacks: [subscriptionOptions],
6904
7582
  lastData: undefined,
6905
7583
  dataRevision: 0,
@@ -6907,6 +7585,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6907
7585
  cache: responseCacheEnabled,
6908
7586
  identity,
6909
7587
  identityToken,
7588
+ specKey,
6910
7589
  };
6911
7590
  connection.subscriptions.set(subscriptionId, subscription);
6912
7591
  }
@@ -7907,6 +8586,8 @@ function defineLiveModule(mod) {
7907
8586
  return mod;
7908
8587
  }
7909
8588
 
8589
+ exports.BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_ATTESTED_SELECTOR;
8590
+ exports.BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR = BOUNDED_RUNTIME_SET_DOCUMENTS_SELECTOR;
7910
8591
  exports.BoundedDeclineError = BoundedDeclineError;
7911
8592
  exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
7912
8593
  exports.EFFECT_INTENT_ADDRESS = EFFECT_INTENT_ADDRESS;
@@ -7917,6 +8598,8 @@ exports.ReactNativeSessionManager = ReactNativeSessionManager;
7917
8598
  exports.ServerSessionManager = ServerSessionManager;
7918
8599
  exports.WebSessionManager = WebSessionManager;
7919
8600
  exports.aggregate = aggregate;
8601
+ exports.assertCanonicalBoundedRuntimeWriteCalldata = assertCanonicalBoundedRuntimeWriteCalldata;
8602
+ exports.assertEvmTransactionMatchesWrite = assertEvmTransactionMatchesWrite;
7920
8603
  exports.buildSetDocumentsTransaction = buildSetDocumentsTransaction;
7921
8604
  exports.clearCache = clearCache;
7922
8605
  exports.closeAllSubscriptions = closeAllSubscriptions;
@@ -7930,6 +8613,7 @@ exports.dispatchDeclineEvent = dispatchDeclineEvent;
7930
8613
  exports.docId = docId;
7931
8614
  exports.functions = functions;
7932
8615
  exports.genAuthNonce = genAuthNonce;
8616
+ exports.genSiweMessage = genSiweMessage;
7933
8617
  exports.genSolanaMessage = genSolanaMessage;
7934
8618
  exports.get = get;
7935
8619
  exports.getActiveSessionManager = getActiveSessionManager;
@@ -7947,6 +8631,7 @@ exports.isEffectResult = isEffectResult;
7947
8631
  exports.live = live;
7948
8632
  exports.liveIntent = intent;
7949
8633
  exports.liveStatus = status;
8634
+ exports.normalizeEvmAddress = normalizeEvmAddress;
7950
8635
  exports.now = now;
7951
8636
  exports.queryAggregate = queryAggregate;
7952
8637
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
@@ -7967,6 +8652,7 @@ exports.signSessionCreateMessage = signSessionCreateMessage;
7967
8652
  exports.signTransaction = signTransaction;
7968
8653
  exports.subscribe = subscribe;
7969
8654
  exports.subscribeLiveView = subscribeView;
8655
+ exports.toLowerEvmAddress = toLowerEvmAddress;
7970
8656
  exports.toMillis = toMillis;
7971
8657
  exports.toSeconds = toSeconds;
7972
8658
  exports.withEffects = withEffects;