@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/client/config.d.ts +20 -1
- package/dist/client/operations.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +726 -40
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +720 -41
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +29 -5
- package/dist/utils/auth-api.d.ts +10 -1
- package/dist/utils/evm/evm-transaction-validation.d.ts +46 -0
- package/dist/utils/evm/evm-utils.d.ts +40 -0
- package/dist/utils/server-session-manager.d.ts +7 -0
- package/dist/utils/utils.d.ts +3 -1
- package/package.json +3 -3
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
2805
|
-
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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${
|
|
4903
|
+
return authValue ? `h${hashForKeyCached(authValue)}` : null;
|
|
4266
4904
|
}
|
|
4267
4905
|
function principalFromIdToken$1(idToken) {
|
|
4268
|
-
return idToken ? `t${
|
|
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
|
-
|
|
5150
|
-
//
|
|
5151
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
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 &&
|
|
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: (
|
|
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
|