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