@bounded-sh/core 0.0.17 → 0.0.19
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 +0 -4
- package/dist/client/functions.d.ts +2 -3
- package/dist/client/live.d.ts +2 -5
- package/dist/client/operations.d.ts +8 -4
- package/dist/client/realtime-store.d.ts +6 -0
- package/dist/client/subscription-v2.d.ts +0 -13
- package/dist/index.d.ts +1 -3
- package/dist/index.js +501 -1573
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +502 -1566
- package/dist/index.mjs.map +1 -1
- package/dist/realtime-store-Ck_VgTcv.js +21 -0
- package/dist/realtime-store-Ck_VgTcv.js.map +1 -0
- package/dist/realtime-store-D3t7PyZl.mjs +19 -0
- package/dist/realtime-store-D3t7PyZl.mjs.map +1 -0
- package/dist/types.d.ts +11 -9
- package/dist/utils/auth-api.d.ts +6 -2
- package/dist/utils/server-session-manager.d.ts +2 -2
- package/dist/utils/utils.d.ts +2 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var axios = require('axios');
|
|
4
|
-
var web3_js = require('@solana/web3.js');
|
|
5
4
|
var nacl = require('tweetnacl');
|
|
5
|
+
var web3_js = require('@solana/web3.js');
|
|
6
6
|
var anchor = require('@coral-xyz/anchor');
|
|
7
7
|
var BN = require('bn.js');
|
|
8
8
|
var ReconnectingWebSocket = require('reconnecting-websocket');
|
|
@@ -30,7 +30,6 @@ let clientConfig = {
|
|
|
30
30
|
// User configured settings
|
|
31
31
|
name: '',
|
|
32
32
|
logoUrl: '',
|
|
33
|
-
apiKey: '',
|
|
34
33
|
// Bounded production is the out-of-the-box default — a Bounded app needs only
|
|
35
34
|
// `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
|
|
36
35
|
network: 'bounded-production',
|
|
@@ -107,7 +106,7 @@ function isBoundedNetwork() {
|
|
|
107
106
|
}
|
|
108
107
|
function init(newConfig) {
|
|
109
108
|
return new Promise((resolve, reject) => {
|
|
110
|
-
if (!newConfig.
|
|
109
|
+
if (!newConfig.appId) {
|
|
111
110
|
reject(new Error('No app ID provided.'));
|
|
112
111
|
return;
|
|
113
112
|
}
|
|
@@ -2883,9 +2882,25 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
2883
2882
|
})();
|
|
2884
2883
|
return refreshInFlight$1;
|
|
2885
2884
|
}
|
|
2885
|
+
class SessionRevokeError extends Error {
|
|
2886
|
+
constructor(message, cause) {
|
|
2887
|
+
super(message);
|
|
2888
|
+
this.name = 'SessionRevokeError';
|
|
2889
|
+
this.cause = cause;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function revokeFailureMessage(err) {
|
|
2893
|
+
var _a, _b;
|
|
2894
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
2895
|
+
const statusText = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.statusText;
|
|
2896
|
+
const suffix = typeof status === 'number'
|
|
2897
|
+
? ` (HTTP ${status}${statusText ? ` ${statusText}` : ''})`
|
|
2898
|
+
: '';
|
|
2899
|
+
return `Failed to revoke refresh token server-side${suffix}. The refresh-token family may still be active.`;
|
|
2900
|
+
}
|
|
2886
2901
|
/**
|
|
2887
|
-
* Revoke a session's refresh-token family server-side (logout).
|
|
2888
|
-
*
|
|
2902
|
+
* Revoke a session's refresh-token family server-side (logout). Routes to the
|
|
2903
|
+
* minting issuer and rejects if the revoke request fails.
|
|
2889
2904
|
*/
|
|
2890
2905
|
async function revokeSession(refreshToken, issuer) {
|
|
2891
2906
|
if (!refreshToken)
|
|
@@ -2898,8 +2913,8 @@ async function revokeSession(refreshToken, issuer) {
|
|
|
2898
2913
|
appId: config.appId,
|
|
2899
2914
|
}, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
|
|
2900
2915
|
}
|
|
2901
|
-
catch (
|
|
2902
|
-
|
|
2916
|
+
catch (err) {
|
|
2917
|
+
throw new SessionRevokeError(revokeFailureMessage(err), err);
|
|
2903
2918
|
}
|
|
2904
2919
|
}
|
|
2905
2920
|
async function signSessionCreateMessage(_signMessageFunction) {
|
|
@@ -3315,231 +3330,6 @@ var sessionManager = /*#__PURE__*/Object.freeze({
|
|
|
3315
3330
|
getActiveSessionManager: getActiveSessionManager
|
|
3316
3331
|
});
|
|
3317
3332
|
|
|
3318
|
-
var safeBuffer = {exports: {}};
|
|
3319
|
-
|
|
3320
|
-
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
3321
|
-
|
|
3322
|
-
var hasRequiredSafeBuffer;
|
|
3323
|
-
|
|
3324
|
-
function requireSafeBuffer () {
|
|
3325
|
-
if (hasRequiredSafeBuffer) return safeBuffer.exports;
|
|
3326
|
-
hasRequiredSafeBuffer = 1;
|
|
3327
|
-
(function (module, exports$1) {
|
|
3328
|
-
/* eslint-disable node/no-deprecated-api */
|
|
3329
|
-
var buffer = requireBuffer();
|
|
3330
|
-
var Buffer = buffer.Buffer;
|
|
3331
|
-
|
|
3332
|
-
// alternative to using Object.keys for old browsers
|
|
3333
|
-
function copyProps (src, dst) {
|
|
3334
|
-
for (var key in src) {
|
|
3335
|
-
dst[key] = src[key];
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3338
|
-
if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) {
|
|
3339
|
-
module.exports = buffer;
|
|
3340
|
-
} else {
|
|
3341
|
-
// Copy properties from require('buffer')
|
|
3342
|
-
copyProps(buffer, exports$1);
|
|
3343
|
-
exports$1.Buffer = SafeBuffer;
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
function SafeBuffer (arg, encodingOrOffset, length) {
|
|
3347
|
-
return Buffer(arg, encodingOrOffset, length)
|
|
3348
|
-
}
|
|
3349
|
-
|
|
3350
|
-
SafeBuffer.prototype = Object.create(Buffer.prototype);
|
|
3351
|
-
|
|
3352
|
-
// Copy static methods from Buffer
|
|
3353
|
-
copyProps(Buffer, SafeBuffer);
|
|
3354
|
-
|
|
3355
|
-
SafeBuffer.from = function (arg, encodingOrOffset, length) {
|
|
3356
|
-
if (typeof arg === 'number') {
|
|
3357
|
-
throw new TypeError('Argument must not be a number')
|
|
3358
|
-
}
|
|
3359
|
-
return Buffer(arg, encodingOrOffset, length)
|
|
3360
|
-
};
|
|
3361
|
-
|
|
3362
|
-
SafeBuffer.alloc = function (size, fill, encoding) {
|
|
3363
|
-
if (typeof size !== 'number') {
|
|
3364
|
-
throw new TypeError('Argument must be a number')
|
|
3365
|
-
}
|
|
3366
|
-
var buf = Buffer(size);
|
|
3367
|
-
if (fill !== undefined) {
|
|
3368
|
-
if (typeof encoding === 'string') {
|
|
3369
|
-
buf.fill(fill, encoding);
|
|
3370
|
-
} else {
|
|
3371
|
-
buf.fill(fill);
|
|
3372
|
-
}
|
|
3373
|
-
} else {
|
|
3374
|
-
buf.fill(0);
|
|
3375
|
-
}
|
|
3376
|
-
return buf
|
|
3377
|
-
};
|
|
3378
|
-
|
|
3379
|
-
SafeBuffer.allocUnsafe = function (size) {
|
|
3380
|
-
if (typeof size !== 'number') {
|
|
3381
|
-
throw new TypeError('Argument must be a number')
|
|
3382
|
-
}
|
|
3383
|
-
return Buffer(size)
|
|
3384
|
-
};
|
|
3385
|
-
|
|
3386
|
-
SafeBuffer.allocUnsafeSlow = function (size) {
|
|
3387
|
-
if (typeof size !== 'number') {
|
|
3388
|
-
throw new TypeError('Argument must be a number')
|
|
3389
|
-
}
|
|
3390
|
-
return buffer.SlowBuffer(size)
|
|
3391
|
-
};
|
|
3392
|
-
} (safeBuffer, safeBuffer.exports));
|
|
3393
|
-
return safeBuffer.exports;
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
|
-
var src;
|
|
3397
|
-
var hasRequiredSrc;
|
|
3398
|
-
|
|
3399
|
-
function requireSrc () {
|
|
3400
|
-
if (hasRequiredSrc) return src;
|
|
3401
|
-
hasRequiredSrc = 1;
|
|
3402
|
-
// base-x encoding / decoding
|
|
3403
|
-
// Copyright (c) 2018 base-x contributors
|
|
3404
|
-
// Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp)
|
|
3405
|
-
// Distributed under the MIT software license, see the accompanying
|
|
3406
|
-
// file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
3407
|
-
// @ts-ignore
|
|
3408
|
-
var _Buffer = requireSafeBuffer().Buffer;
|
|
3409
|
-
function base (ALPHABET) {
|
|
3410
|
-
if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') }
|
|
3411
|
-
var BASE_MAP = new Uint8Array(256);
|
|
3412
|
-
for (var j = 0; j < BASE_MAP.length; j++) {
|
|
3413
|
-
BASE_MAP[j] = 255;
|
|
3414
|
-
}
|
|
3415
|
-
for (var i = 0; i < ALPHABET.length; i++) {
|
|
3416
|
-
var x = ALPHABET.charAt(i);
|
|
3417
|
-
var xc = x.charCodeAt(0);
|
|
3418
|
-
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') }
|
|
3419
|
-
BASE_MAP[xc] = i;
|
|
3420
|
-
}
|
|
3421
|
-
var BASE = ALPHABET.length;
|
|
3422
|
-
var LEADER = ALPHABET.charAt(0);
|
|
3423
|
-
var FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
|
|
3424
|
-
var iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
|
|
3425
|
-
function encode (source) {
|
|
3426
|
-
if (Array.isArray(source) || source instanceof Uint8Array) { source = _Buffer.from(source); }
|
|
3427
|
-
if (!_Buffer.isBuffer(source)) { throw new TypeError('Expected Buffer') }
|
|
3428
|
-
if (source.length === 0) { return '' }
|
|
3429
|
-
// Skip & count leading zeroes.
|
|
3430
|
-
var zeroes = 0;
|
|
3431
|
-
var length = 0;
|
|
3432
|
-
var pbegin = 0;
|
|
3433
|
-
var pend = source.length;
|
|
3434
|
-
while (pbegin !== pend && source[pbegin] === 0) {
|
|
3435
|
-
pbegin++;
|
|
3436
|
-
zeroes++;
|
|
3437
|
-
}
|
|
3438
|
-
// Allocate enough space in big-endian base58 representation.
|
|
3439
|
-
var size = ((pend - pbegin) * iFACTOR + 1) >>> 0;
|
|
3440
|
-
var b58 = new Uint8Array(size);
|
|
3441
|
-
// Process the bytes.
|
|
3442
|
-
while (pbegin !== pend) {
|
|
3443
|
-
var carry = source[pbegin];
|
|
3444
|
-
// Apply "b58 = b58 * 256 + ch".
|
|
3445
|
-
var i = 0;
|
|
3446
|
-
for (var it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
|
|
3447
|
-
carry += (256 * b58[it1]) >>> 0;
|
|
3448
|
-
b58[it1] = (carry % BASE) >>> 0;
|
|
3449
|
-
carry = (carry / BASE) >>> 0;
|
|
3450
|
-
}
|
|
3451
|
-
if (carry !== 0) { throw new Error('Non-zero carry') }
|
|
3452
|
-
length = i;
|
|
3453
|
-
pbegin++;
|
|
3454
|
-
}
|
|
3455
|
-
// Skip leading zeroes in base58 result.
|
|
3456
|
-
var it2 = size - length;
|
|
3457
|
-
while (it2 !== size && b58[it2] === 0) {
|
|
3458
|
-
it2++;
|
|
3459
|
-
}
|
|
3460
|
-
// Translate the result into a string.
|
|
3461
|
-
var str = LEADER.repeat(zeroes);
|
|
3462
|
-
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
|
|
3463
|
-
return str
|
|
3464
|
-
}
|
|
3465
|
-
function decodeUnsafe (source) {
|
|
3466
|
-
if (typeof source !== 'string') { throw new TypeError('Expected String') }
|
|
3467
|
-
if (source.length === 0) { return _Buffer.alloc(0) }
|
|
3468
|
-
var psz = 0;
|
|
3469
|
-
// Skip and count leading '1's.
|
|
3470
|
-
var zeroes = 0;
|
|
3471
|
-
var length = 0;
|
|
3472
|
-
while (source[psz] === LEADER) {
|
|
3473
|
-
zeroes++;
|
|
3474
|
-
psz++;
|
|
3475
|
-
}
|
|
3476
|
-
// Allocate enough space in big-endian base256 representation.
|
|
3477
|
-
var size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
|
|
3478
|
-
var b256 = new Uint8Array(size);
|
|
3479
|
-
// Process the characters.
|
|
3480
|
-
while (psz < source.length) {
|
|
3481
|
-
// Find code of next character
|
|
3482
|
-
var charCode = source.charCodeAt(psz);
|
|
3483
|
-
// Base map can not be indexed using char code
|
|
3484
|
-
if (charCode > 255) { return }
|
|
3485
|
-
// Decode character
|
|
3486
|
-
var carry = BASE_MAP[charCode];
|
|
3487
|
-
// Invalid character
|
|
3488
|
-
if (carry === 255) { return }
|
|
3489
|
-
var i = 0;
|
|
3490
|
-
for (var it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
|
|
3491
|
-
carry += (BASE * b256[it3]) >>> 0;
|
|
3492
|
-
b256[it3] = (carry % 256) >>> 0;
|
|
3493
|
-
carry = (carry / 256) >>> 0;
|
|
3494
|
-
}
|
|
3495
|
-
if (carry !== 0) { throw new Error('Non-zero carry') }
|
|
3496
|
-
length = i;
|
|
3497
|
-
psz++;
|
|
3498
|
-
}
|
|
3499
|
-
// Skip leading zeroes in b256.
|
|
3500
|
-
var it4 = size - length;
|
|
3501
|
-
while (it4 !== size && b256[it4] === 0) {
|
|
3502
|
-
it4++;
|
|
3503
|
-
}
|
|
3504
|
-
var vch = _Buffer.allocUnsafe(zeroes + (size - it4));
|
|
3505
|
-
vch.fill(0x00, 0, zeroes);
|
|
3506
|
-
var j = zeroes;
|
|
3507
|
-
while (it4 !== size) {
|
|
3508
|
-
vch[j++] = b256[it4++];
|
|
3509
|
-
}
|
|
3510
|
-
return vch
|
|
3511
|
-
}
|
|
3512
|
-
function decode (string) {
|
|
3513
|
-
var buffer = decodeUnsafe(string);
|
|
3514
|
-
if (buffer) { return buffer }
|
|
3515
|
-
throw new Error('Non-base' + BASE + ' character')
|
|
3516
|
-
}
|
|
3517
|
-
return {
|
|
3518
|
-
encode: encode,
|
|
3519
|
-
decodeUnsafe: decodeUnsafe,
|
|
3520
|
-
decode: decode
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
|
-
src = base;
|
|
3524
|
-
return src;
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
var bs58$1;
|
|
3528
|
-
var hasRequiredBs58;
|
|
3529
|
-
|
|
3530
|
-
function requireBs58 () {
|
|
3531
|
-
if (hasRequiredBs58) return bs58$1;
|
|
3532
|
-
hasRequiredBs58 = 1;
|
|
3533
|
-
var basex = requireSrc();
|
|
3534
|
-
var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
3535
|
-
|
|
3536
|
-
bs58$1 = basex(ALPHABET);
|
|
3537
|
-
return bs58$1;
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
|
-
var bs58Exports = requireBs58();
|
|
3541
|
-
var bs58 = /*@__PURE__*/getDefaultExportFromCjs(bs58Exports);
|
|
3542
|
-
|
|
3543
3333
|
// ─────────────────────────────────────────────────────────────
|
|
3544
3334
|
// Local implementation of getSimulationComputeUnits
|
|
3545
3335
|
// (Replaces @solana-developers/helpers to avoid Wallet import issue in browser/ESM builds)
|
|
@@ -3797,36 +3587,7 @@ async function buildSetDocumentsTransaction(connection, idl, anchorProvider, pay
|
|
|
3797
3587
|
return { tx: vTx, blockhash, lastValidBlockHeight };
|
|
3798
3588
|
}
|
|
3799
3589
|
|
|
3800
|
-
|
|
3801
|
-
/* ENV helpers */
|
|
3802
|
-
/* ------------------------------------------------------------------ */
|
|
3803
|
-
// Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI). Only consulted when no
|
|
3804
|
-
// explicit keypair was provided (createWalletClient passes one).
|
|
3805
|
-
const ENV_KEYPAIR = "BOUNDED_PRIVATE_KEY";
|
|
3806
|
-
const LEGACY_ENV_KEYPAIR = "BOUNDED_SOLANA_KEYPAIR";
|
|
3807
|
-
function loadKeypairFromEnv() {
|
|
3808
|
-
if (process.env[LEGACY_ENV_KEYPAIR]) {
|
|
3809
|
-
throw new Error(`${LEGACY_ENV_KEYPAIR} is no longer supported. Set ${ENV_KEYPAIR} instead, ` +
|
|
3810
|
-
`or pass an explicit keypair via createWalletClient({ keypair }).`);
|
|
3811
|
-
}
|
|
3812
|
-
const secret = process.env[ENV_KEYPAIR];
|
|
3813
|
-
if (!secret) {
|
|
3814
|
-
throw new Error(`No server keypair for this top-level call. The top-level get/set/subscribe/etc. use an ` +
|
|
3815
|
-
`AMBIENT session — set ${ENV_KEYPAIR} to a base-58 secret key (or JSON array) to provide one. ` +
|
|
3816
|
-
`If you already created a wallet with createWalletClient({ keypair }), call ITS methods instead ` +
|
|
3817
|
-
`(client.subscribe / client.set / client.get): that client is self-contained and deliberately does ` +
|
|
3818
|
-
`not set the ambient session, so the top-level functions can't see it.`);
|
|
3819
|
-
}
|
|
3820
|
-
try {
|
|
3821
|
-
const secretKey = secret.trim().startsWith("[")
|
|
3822
|
-
? Uint8Array.from(JSON.parse(secret))
|
|
3823
|
-
: bs58.decode(secret.trim());
|
|
3824
|
-
return web3_js.Keypair.fromSecretKey(secretKey);
|
|
3825
|
-
}
|
|
3826
|
-
catch (err) {
|
|
3827
|
-
throw new Error(`Unable to parse ${ENV_KEYPAIR}. Ensure it is valid base-58 or JSON.`);
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3590
|
+
const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use createWalletClient({ keypair }) and call that wallet client's methods.";
|
|
3830
3591
|
/* ------------------------------------------------------------------ */
|
|
3831
3592
|
/* SESSION MANAGER */
|
|
3832
3593
|
/* ------------------------------------------------------------------ */
|
|
@@ -3849,8 +3610,10 @@ class ServerSessionManager {
|
|
|
3849
3610
|
* Session creation (instance method)
|
|
3850
3611
|
* ---------------------------------------------- */
|
|
3851
3612
|
async createSession() {
|
|
3852
|
-
|
|
3853
|
-
|
|
3613
|
+
if (!this.keypair) {
|
|
3614
|
+
throw new Error(NO_AMBIENT_SERVER_SESSION);
|
|
3615
|
+
}
|
|
3616
|
+
const kp = this.keypair;
|
|
3854
3617
|
const address = kp.publicKey.toBase58();
|
|
3855
3618
|
/* fetch nonce from auth API */
|
|
3856
3619
|
const nonce = await genAuthNonce();
|
|
@@ -3912,7 +3675,7 @@ class ServerSessionManager {
|
|
|
3912
3675
|
return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.refreshToken) !== null && _b !== void 0 ? _b : null;
|
|
3913
3676
|
}
|
|
3914
3677
|
}
|
|
3915
|
-
/* The default singleton
|
|
3678
|
+
/* The default singleton exists only so top-level server calls fail closed. */
|
|
3916
3679
|
ServerSessionManager.instance = new ServerSessionManager();
|
|
3917
3680
|
|
|
3918
3681
|
/**
|
|
@@ -3988,9 +3751,7 @@ async function getUserInfo(isServer) {
|
|
|
3988
3751
|
*
|
|
3989
3752
|
* Mirrors the realtime-worker auth.ts identity resolution so the client-side
|
|
3990
3753
|
* `user` object matches what the backend authenticates as:
|
|
3991
|
-
* - id = custom:userId
|
|
3992
|
-
* keeps wallet/SIWS tokens — which omit userId — AND legacy Better Auth
|
|
3993
|
-
* tokens — which put the account id in custom:walletAddress — working).
|
|
3754
|
+
* - id = custom:userId only.
|
|
3994
3755
|
* - address = custom:walletAddress only (a REAL wallet). NEVER falls back to the
|
|
3995
3756
|
* identity: an opaque id is not a spendable onchain address. null for
|
|
3996
3757
|
* email-only sessions.
|
|
@@ -4015,7 +3776,7 @@ function deriveUserIdentityFromIdToken(idToken) {
|
|
|
4015
3776
|
const userIdClaim = payload['custom:userId'];
|
|
4016
3777
|
const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
|
|
4017
3778
|
? userIdClaim
|
|
4018
|
-
:
|
|
3779
|
+
: null;
|
|
4019
3780
|
const emailClaim = payload['email'];
|
|
4020
3781
|
const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
|
|
4021
3782
|
? emailClaim.toLowerCase()
|
|
@@ -4071,17 +3832,6 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
|
|
|
4071
3832
|
await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
|
|
4072
3833
|
}
|
|
4073
3834
|
|
|
4074
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
4075
|
-
__proto__: null,
|
|
4076
|
-
createAuthHeader: createAuthHeader,
|
|
4077
|
-
deriveUserIdentityFromIdToken: deriveUserIdentityFromIdToken,
|
|
4078
|
-
getIdToken: getIdToken,
|
|
4079
|
-
getRefreshToken: getRefreshToken,
|
|
4080
|
-
getSessionIssuer: getSessionIssuer,
|
|
4081
|
-
getUserInfo: getUserInfo,
|
|
4082
|
-
updateIdTokenAndAccessToken: updateIdTokenAndAccessToken
|
|
4083
|
-
});
|
|
4084
|
-
|
|
4085
3835
|
const apiClient = axios.create();
|
|
4086
3836
|
axiosRetry(apiClient, {
|
|
4087
3837
|
retries: 2,
|
|
@@ -4146,7 +3896,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
|
4146
3896
|
const authHeader = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
|
|
4147
3897
|
? await _overrides._getAuthHeaders()
|
|
4148
3898
|
: await createAuthHeader(config.isServer);
|
|
4149
|
-
const headers = Object.assign({ "Content-Type": "application/json", "X-
|
|
3899
|
+
const headers = Object.assign({ "Content-Type": "application/json", "X-App-Id": config.appId }, authHeader);
|
|
4150
3900
|
// Apply custom headers from _overrides
|
|
4151
3901
|
if (_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers) {
|
|
4152
3902
|
Object.assign(headers, _overrides.headers);
|
|
@@ -4250,6 +4000,7 @@ const pendingRequests = {};
|
|
|
4250
4000
|
const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
|
|
4251
4001
|
// Last time we cleaned up the cache
|
|
4252
4002
|
let lastCacheCleanup = Date.now();
|
|
4003
|
+
let uncacheableReadKeyCounter = 0;
|
|
4253
4004
|
/**
|
|
4254
4005
|
* Return the leaf document key (last path segment) for a document path.
|
|
4255
4006
|
*
|
|
@@ -4335,57 +4086,74 @@ function normalizeReadResult(responseData, pathIsDocument) {
|
|
|
4335
4086
|
}
|
|
4336
4087
|
return responseData;
|
|
4337
4088
|
}
|
|
4338
|
-
function hashForKey$
|
|
4089
|
+
function hashForKey$1(value) {
|
|
4339
4090
|
let h = 5381;
|
|
4340
4091
|
for (let i = 0; i < value.length; i++) {
|
|
4341
4092
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
4342
4093
|
}
|
|
4343
4094
|
return h.toString(36);
|
|
4344
4095
|
}
|
|
4096
|
+
function hasAuthHeader(headers) {
|
|
4097
|
+
if (!headers)
|
|
4098
|
+
return false;
|
|
4099
|
+
return (Object.prototype.hasOwnProperty.call(headers, 'Authorization') ||
|
|
4100
|
+
Object.prototype.hasOwnProperty.call(headers, 'authorization'));
|
|
4101
|
+
}
|
|
4345
4102
|
function authValueFromHeaders(headers) {
|
|
4346
4103
|
return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
|
|
4347
4104
|
}
|
|
4348
4105
|
function principalFromAuthValue(authValue) {
|
|
4349
|
-
return authValue ? `h${hashForKey$
|
|
4106
|
+
return authValue ? `h${hashForKey$1(authValue)}` : null;
|
|
4350
4107
|
}
|
|
4351
4108
|
function principalFromIdToken$1(idToken) {
|
|
4352
|
-
return idToken ? `t${hashForKey$
|
|
4109
|
+
return idToken ? `t${hashForKey$1(idToken)}` : null;
|
|
4110
|
+
}
|
|
4111
|
+
function uncacheableReadKey(appId, scope) {
|
|
4112
|
+
uncacheableReadKeyCounter += 1;
|
|
4113
|
+
return `${appId}:${scope}-uncacheable-${uncacheableReadKeyCounter}`;
|
|
4353
4114
|
}
|
|
4354
4115
|
/**
|
|
4355
4116
|
* SECURITY (H1): Read caches must be keyed by the caller's principal, not just
|
|
4356
4117
|
* by path/filter/shape. In a shared process / SSR worker / browser login-switch,
|
|
4357
4118
|
* keying by path alone lets User B receive User A's cached private read before
|
|
4358
|
-
* any server read rule runs. This returns `appId:<principal>` for the
|
|
4359
|
-
* given read will actually authenticate
|
|
4360
|
-
*
|
|
4361
|
-
*
|
|
4362
|
-
*
|
|
4119
|
+
* any server read rule runs. This returns `appId:<principal>` for the opaque
|
|
4120
|
+
* auth material a given read will actually authenticate with. No-auth reads are
|
|
4121
|
+
* deliberately marked uncacheable instead of sharing an implicit `anon` bucket.
|
|
4122
|
+
* JWTs are intentionally treated as opaque unverified bearer material — never
|
|
4123
|
+
* decoded claims and never caller identity hints such as `_walletAddress`.
|
|
4363
4124
|
*/
|
|
4364
4125
|
async function getReadPrincipalKey(overrides) {
|
|
4365
4126
|
const config = await getConfig();
|
|
4366
4127
|
const appId = config.appId || '';
|
|
4367
4128
|
// makeApiRequest applies overrides.headers AFTER its computed auth header, so
|
|
4368
4129
|
// caller-supplied Authorization is the real request auth when present.
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
return
|
|
4130
|
+
if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
|
|
4131
|
+
const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
|
|
4132
|
+
return principal
|
|
4133
|
+
? { key: `${appId}:${principal}`, cacheable: true }
|
|
4134
|
+
: { key: uncacheableReadKey(appId, 'h'), cacheable: false };
|
|
4372
4135
|
}
|
|
4373
4136
|
// Per-request auth-header override (wallet client). Key by the exact opaque
|
|
4374
4137
|
// header it produces, not decoded claims or the unverified _walletAddress hint.
|
|
4375
4138
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
4376
4139
|
try {
|
|
4377
4140
|
const headers = await overrides._getAuthHeaders();
|
|
4378
|
-
|
|
4141
|
+
const principal = principalFromAuthValue(authValueFromHeaders(headers));
|
|
4142
|
+
return principal
|
|
4143
|
+
? { key: `${appId}:o${principal}`, cacheable: true }
|
|
4144
|
+
: { key: uncacheableReadKey(appId, 'o'), cacheable: false };
|
|
4379
4145
|
}
|
|
4380
4146
|
catch (_a) {
|
|
4381
|
-
// If we can't resolve the override identity,
|
|
4382
|
-
|
|
4383
|
-
return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
|
|
4147
|
+
// If we can't resolve the override identity, do not read/write cache.
|
|
4148
|
+
return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
|
|
4384
4149
|
}
|
|
4385
4150
|
}
|
|
4386
4151
|
// Ambient session principal.
|
|
4387
4152
|
const idToken = await getIdToken(config.isServer);
|
|
4388
|
-
|
|
4153
|
+
const principal = principalFromIdToken$1(idToken);
|
|
4154
|
+
return principal
|
|
4155
|
+
? { key: `${appId}:${principal}`, cacheable: true }
|
|
4156
|
+
: { key: uncacheableReadKey(appId, 'a'), cacheable: false };
|
|
4389
4157
|
}
|
|
4390
4158
|
/**
|
|
4391
4159
|
* Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
|
|
@@ -4678,18 +4446,20 @@ async function get(path, opts = {}) {
|
|
|
4678
4446
|
// Create cache key combining path, prompt, filter, sort, includeSubPaths,
|
|
4679
4447
|
// shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
|
|
4680
4448
|
// so a private read cached for one user is never served to another in a
|
|
4681
|
-
// shared process / SSR worker / browser login-switch.
|
|
4449
|
+
// shared process / SSR worker / browser login-switch. The cache is opt-in
|
|
4450
|
+
// and disabled for no-auth reads, which get an uncacheable unique key.
|
|
4682
4451
|
const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
|
|
4683
4452
|
const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
|
|
4684
4453
|
const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
|
|
4685
|
-
const cursorKey = opts.cursor ? `:c${hashForKey$
|
|
4686
|
-
const filterKey = opts.filter ? `:f${hashForKey$
|
|
4687
|
-
const sortKey = opts.sort ? `:s${hashForKey$
|
|
4454
|
+
const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
|
|
4455
|
+
const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
|
|
4456
|
+
const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
|
|
4688
4457
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4689
|
-
const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4458
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4459
|
+
const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
|
|
4690
4460
|
const now = Date.now();
|
|
4691
|
-
// Check for valid cache entry
|
|
4692
|
-
if (
|
|
4461
|
+
// Check for valid cache entry when the caller explicitly opted in.
|
|
4462
|
+
if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
|
|
4693
4463
|
return getCache[cacheKey].data;
|
|
4694
4464
|
}
|
|
4695
4465
|
// If we're bypassing cache, we should still coalesce identical requests
|
|
@@ -4731,8 +4501,8 @@ async function get(path, opts = {}) {
|
|
|
4731
4501
|
// - collection path → `{ data, nextCursor }` preserved,
|
|
4732
4502
|
// with the bare `id` (leaf doc key) attached to every returned row (Bug 1).
|
|
4733
4503
|
const responseData = normalizeReadResult(response.data, pathIsDocument);
|
|
4734
|
-
// Cache the response
|
|
4735
|
-
if (
|
|
4504
|
+
// Cache the response only when explicitly requested and principal-bound.
|
|
4505
|
+
if (cacheEnabled) {
|
|
4736
4506
|
getCache[cacheKey] = {
|
|
4737
4507
|
data: responseData,
|
|
4738
4508
|
expiresAt: now + GET_CACHE_TTL
|
|
@@ -4770,6 +4540,213 @@ function cleanupExpiredCache() {
|
|
|
4770
4540
|
});
|
|
4771
4541
|
lastCacheCleanup = now;
|
|
4772
4542
|
}
|
|
4543
|
+
const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
|
|
4544
|
+
const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
|
|
4545
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
4546
|
+
const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
|
|
4547
|
+
const ALLOWED_SERVER_TX_PROGRAMS = new Set([
|
|
4548
|
+
BOUNDED_PROGRAM_MAINNET,
|
|
4549
|
+
BOUNDED_PROGRAM_DEVNET,
|
|
4550
|
+
COMPUTE_BUDGET_PROGRAM,
|
|
4551
|
+
SYSTEM_PROGRAM_ID,
|
|
4552
|
+
]);
|
|
4553
|
+
const SET_DOCUMENTS_DISCRIMINATOR = '79,46,72,73,24,79,66,245';
|
|
4554
|
+
const SET_DOCUMENTS_V2_DISCRIMINATOR = '22,236,242,185,145,61,26,39';
|
|
4555
|
+
const ALLOWED_BOUNDED_SET_DISCRIMINATORS = new Set([
|
|
4556
|
+
SET_DOCUMENTS_DISCRIMINATOR,
|
|
4557
|
+
SET_DOCUMENTS_V2_DISCRIMINATOR,
|
|
4558
|
+
]);
|
|
4559
|
+
class BorshCursor {
|
|
4560
|
+
constructor(data, offset, label) {
|
|
4561
|
+
this.data = data;
|
|
4562
|
+
this.offset = offset;
|
|
4563
|
+
this.label = label;
|
|
4564
|
+
}
|
|
4565
|
+
requireBytes(length, field) {
|
|
4566
|
+
if (this.offset + length > this.data.length) {
|
|
4567
|
+
throw new Error(`${this.label} has malformed Bounded instruction data while reading ${field}`);
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
readU8(field) {
|
|
4571
|
+
this.requireBytes(1, field);
|
|
4572
|
+
return this.data[this.offset++];
|
|
4573
|
+
}
|
|
4574
|
+
readU32(field) {
|
|
4575
|
+
this.requireBytes(4, field);
|
|
4576
|
+
const value = this.data[this.offset] |
|
|
4577
|
+
(this.data[this.offset + 1] << 8) |
|
|
4578
|
+
(this.data[this.offset + 2] << 16) |
|
|
4579
|
+
(this.data[this.offset + 3] << 24);
|
|
4580
|
+
this.offset += 4;
|
|
4581
|
+
return value >>> 0;
|
|
4582
|
+
}
|
|
4583
|
+
skip(length, field) {
|
|
4584
|
+
this.requireBytes(length, field);
|
|
4585
|
+
this.offset += length;
|
|
4586
|
+
}
|
|
4587
|
+
readString(field) {
|
|
4588
|
+
const length = this.readU32(`${field} length`);
|
|
4589
|
+
this.requireBytes(length, field);
|
|
4590
|
+
const raw = this.data.slice(this.offset, this.offset + length);
|
|
4591
|
+
this.offset += length;
|
|
4592
|
+
return bufferExports.Buffer.from(raw).toString('utf8');
|
|
4593
|
+
}
|
|
4594
|
+
skipBytes(field) {
|
|
4595
|
+
const length = this.readU32(`${field} length`);
|
|
4596
|
+
this.skip(length, field);
|
|
4597
|
+
}
|
|
4598
|
+
isAtEnd() {
|
|
4599
|
+
return this.offset === this.data.length;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
function discriminatorKey(data) {
|
|
4603
|
+
return Array.from(data.slice(0, 8)).join(',');
|
|
4604
|
+
}
|
|
4605
|
+
function skipBoundedFieldValue(cursor) {
|
|
4606
|
+
const option = cursor.readU8('operation value option');
|
|
4607
|
+
if (option === 0)
|
|
4608
|
+
return;
|
|
4609
|
+
if (option !== 1) {
|
|
4610
|
+
throw new Error('Server transaction has malformed Bounded field value option');
|
|
4611
|
+
}
|
|
4612
|
+
const variant = cursor.readU8('operation value variant');
|
|
4613
|
+
switch (variant) {
|
|
4614
|
+
case 0: // u64Val
|
|
4615
|
+
case 1: // i64Val
|
|
4616
|
+
cursor.skip(8, 'operation numeric value');
|
|
4617
|
+
return;
|
|
4618
|
+
case 2: // boolVal
|
|
4619
|
+
cursor.skip(1, 'operation bool value');
|
|
4620
|
+
return;
|
|
4621
|
+
case 3: // stringVal
|
|
4622
|
+
cursor.readString('operation string value');
|
|
4623
|
+
return;
|
|
4624
|
+
case 4: // addressVal
|
|
4625
|
+
cursor.skip(32, 'operation address value');
|
|
4626
|
+
return;
|
|
4627
|
+
default:
|
|
4628
|
+
throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
function skipBoundedFieldOperation(cursor) {
|
|
4632
|
+
cursor.readString('operation key');
|
|
4633
|
+
skipBoundedFieldValue(cursor);
|
|
4634
|
+
cursor.skip(1, 'operation kind');
|
|
4635
|
+
}
|
|
4636
|
+
function skipBoundedTxData(cursor, isV2) {
|
|
4637
|
+
cursor.readString('txData plugin function key');
|
|
4638
|
+
cursor.skipBytes('txData bytes');
|
|
4639
|
+
if (isV2) {
|
|
4640
|
+
cursor.skipBytes('txData raIndices');
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
const raIndexCount = cursor.readU32('txData raIndices length');
|
|
4644
|
+
cursor.skip(raIndexCount * 8, 'txData raIndices');
|
|
4645
|
+
}
|
|
4646
|
+
function normalizeOnchainPath(path) {
|
|
4647
|
+
let normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
4648
|
+
if (normalized.endsWith('*') && normalized.length > 1) {
|
|
4649
|
+
normalized = normalized.slice(0, -1);
|
|
4650
|
+
}
|
|
4651
|
+
if (normalized.endsWith('/')) {
|
|
4652
|
+
normalized = normalized.slice(0, -1);
|
|
4653
|
+
}
|
|
4654
|
+
return normalized;
|
|
4655
|
+
}
|
|
4656
|
+
function parseBoundedSetDocumentsInstruction(data, label) {
|
|
4657
|
+
if (data.length < 8) {
|
|
4658
|
+
throw new Error(`${label} has malformed Bounded instruction data`);
|
|
4659
|
+
}
|
|
4660
|
+
const discriminator = discriminatorKey(data);
|
|
4661
|
+
if (!ALLOWED_BOUNDED_SET_DISCRIMINATORS.has(discriminator)) {
|
|
4662
|
+
throw new Error(`${label} contains unsupported Bounded instruction`);
|
|
4663
|
+
}
|
|
4664
|
+
const isV2 = discriminator === SET_DOCUMENTS_V2_DISCRIMINATOR;
|
|
4665
|
+
const cursor = new BorshCursor(data, 8, label);
|
|
4666
|
+
const appId = cursor.readString('appId');
|
|
4667
|
+
const documentPaths = [];
|
|
4668
|
+
const documentCount = cursor.readU32('documents length');
|
|
4669
|
+
for (let i = 0; i < documentCount; i++) {
|
|
4670
|
+
documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
|
|
4671
|
+
const operationCount = cursor.readU32('operations length');
|
|
4672
|
+
for (let j = 0; j < operationCount; j++) {
|
|
4673
|
+
skipBoundedFieldOperation(cursor);
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
const deletePaths = [];
|
|
4677
|
+
const deleteCount = cursor.readU32('delete paths length');
|
|
4678
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
4679
|
+
deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
|
|
4680
|
+
}
|
|
4681
|
+
const txDataCount = cursor.readU32('txData length');
|
|
4682
|
+
for (let i = 0; i < txDataCount; i++) {
|
|
4683
|
+
skipBoundedTxData(cursor, isV2);
|
|
4684
|
+
}
|
|
4685
|
+
const simulate = cursor.readU8('simulate');
|
|
4686
|
+
if (simulate !== 0 && simulate !== 1) {
|
|
4687
|
+
throw new Error(`${label} has malformed Bounded simulate flag`);
|
|
4688
|
+
}
|
|
4689
|
+
if (!cursor.isAtEnd()) {
|
|
4690
|
+
throw new Error(`${label} has trailing Bounded instruction data`);
|
|
4691
|
+
}
|
|
4692
|
+
return { appId, documentPaths, deletePaths };
|
|
4693
|
+
}
|
|
4694
|
+
function assertSamePathSet(label, expectedPaths, actualPaths) {
|
|
4695
|
+
const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
4696
|
+
const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
4697
|
+
const missing = [...expected].filter(path => !actual.has(path));
|
|
4698
|
+
const extra = [...actual].filter(path => !expected.has(path));
|
|
4699
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
4700
|
+
const details = [
|
|
4701
|
+
missing.length ? `missing paths: ${missing.join(', ')}` : '',
|
|
4702
|
+
extra.length ? `unexpected paths: ${extra.join(', ')}` : '',
|
|
4703
|
+
].filter(Boolean).join('; ');
|
|
4704
|
+
throw new Error(`${label} Bounded instruction paths do not match requested write paths (${details})`);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
function validateServerSuppliedVersionedTransaction(transaction, options) {
|
|
4708
|
+
var _a;
|
|
4709
|
+
const { label, expectedAppId, expectedWritePaths } = options;
|
|
4710
|
+
const accountKeys = transaction.message.staticAccountKeys;
|
|
4711
|
+
let boundedInstructionCount = 0;
|
|
4712
|
+
const actualWritePaths = [];
|
|
4713
|
+
for (const ix of transaction.message.compiledInstructions) {
|
|
4714
|
+
if (ix.programIdIndex >= accountKeys.length) {
|
|
4715
|
+
throw new Error(`${label} has program ID in lookup table (not allowed)`);
|
|
4716
|
+
}
|
|
4717
|
+
const programId = accountKeys[ix.programIdIndex].toBase58();
|
|
4718
|
+
if (!ALLOWED_SERVER_TX_PROGRAMS.has(programId)) {
|
|
4719
|
+
throw new Error(`${label} contains unauthorized program: ${programId}`);
|
|
4720
|
+
}
|
|
4721
|
+
const data = ix.data instanceof Uint8Array ? ix.data : bufferExports.Buffer.from((_a = ix.data) !== null && _a !== void 0 ? _a : []);
|
|
4722
|
+
if (programId === SYSTEM_PROGRAM_ID) {
|
|
4723
|
+
throw new Error(`${label} contains unauthorized System Program instruction`);
|
|
4724
|
+
}
|
|
4725
|
+
if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
|
|
4726
|
+
boundedInstructionCount += 1;
|
|
4727
|
+
const parsed = parseBoundedSetDocumentsInstruction(data, label);
|
|
4728
|
+
if (parsed.appId !== expectedAppId) {
|
|
4729
|
+
throw new Error(`${label} Bounded instruction appId does not match configured appId`);
|
|
4730
|
+
}
|
|
4731
|
+
actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
if (boundedInstructionCount !== 1) {
|
|
4735
|
+
throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
|
|
4736
|
+
}
|
|
4737
|
+
assertSamePathSet(label, expectedWritePaths, actualWritePaths);
|
|
4738
|
+
}
|
|
4739
|
+
function deserializeAndValidateServerTransaction(serializedTransaction, options) {
|
|
4740
|
+
const txBytes = bufferExports.Buffer.from(serializedTransaction, 'base64');
|
|
4741
|
+
const transaction = web3_js.VersionedTransaction.deserialize(txBytes);
|
|
4742
|
+
validateServerSuppliedVersionedTransaction(transaction, options);
|
|
4743
|
+
return transaction;
|
|
4744
|
+
}
|
|
4745
|
+
function assertAllowedServerPreInstruction(ix, label) {
|
|
4746
|
+
if (ix.programId.equals(web3_js.SystemProgram.programId)) {
|
|
4747
|
+
throw new Error(`${label} contains unauthorized System Program preInstruction`);
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4773
4750
|
function classifyGetManyBatchError(error) {
|
|
4774
4751
|
var _a, _b, _c;
|
|
4775
4752
|
const err = error;
|
|
@@ -4814,13 +4791,14 @@ async function getMany(paths, opts = {}) {
|
|
|
4814
4791
|
// H1: principal-scope getMany cache keys so one user's batch reads are never
|
|
4815
4792
|
// served to another. Same `<appId:principal>|<path>:` shape used by get().
|
|
4816
4793
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4794
|
+
const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
|
|
4817
4795
|
const results = new Array(paths.length);
|
|
4818
4796
|
const uncachedIndices = [];
|
|
4819
4797
|
const uncachedPaths = [];
|
|
4820
4798
|
for (let i = 0; i < normalizedPaths.length; i++) {
|
|
4821
4799
|
const normalizedPath = normalizedPaths[i];
|
|
4822
|
-
const cacheKey = `${principalKey}|${normalizedPath}:`;
|
|
4823
|
-
if (
|
|
4800
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:`;
|
|
4801
|
+
if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
|
|
4824
4802
|
results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
|
|
4825
4803
|
}
|
|
4826
4804
|
else {
|
|
@@ -4856,8 +4834,8 @@ async function getMany(paths, opts = {}) {
|
|
|
4856
4834
|
? serverResult
|
|
4857
4835
|
: Object.assign(Object.assign({}, serverResult), { data: withBareId(serverResult.data) });
|
|
4858
4836
|
results[originalIndex] = normalizedResult;
|
|
4859
|
-
if (!normalizedResult.error &&
|
|
4860
|
-
const cacheKey = `${principalKey}|${normalizedPath}:`;
|
|
4837
|
+
if (!normalizedResult.error && cacheEnabled) {
|
|
4838
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:`;
|
|
4861
4839
|
getCache[cacheKey] = {
|
|
4862
4840
|
data: normalizedResult.data,
|
|
4863
4841
|
expiresAt: now + GET_CACHE_TTL
|
|
@@ -4872,7 +4850,7 @@ async function getMany(paths, opts = {}) {
|
|
|
4872
4850
|
};
|
|
4873
4851
|
}
|
|
4874
4852
|
}
|
|
4875
|
-
if (now - lastCacheCleanup > 5000) {
|
|
4853
|
+
if (cacheEnabled && now - lastCacheCleanup > 5000) {
|
|
4876
4854
|
cleanupExpiredCache();
|
|
4877
4855
|
lastCacheCleanup = now;
|
|
4878
4856
|
}
|
|
@@ -5023,11 +5001,12 @@ async function setMany(many, options) {
|
|
|
5023
5001
|
}
|
|
5024
5002
|
const curTx = transactions[0];
|
|
5025
5003
|
let transactionResult;
|
|
5004
|
+
const expectedWritePaths = documents.map(d => d.destinationPath);
|
|
5026
5005
|
if (curTx.serializedTransaction) {
|
|
5027
|
-
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
|
|
5006
|
+
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
5028
5007
|
}
|
|
5029
5008
|
else {
|
|
5030
|
-
transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
|
|
5009
|
+
transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
5031
5010
|
}
|
|
5032
5011
|
// Sync items after all transactions are confirmed
|
|
5033
5012
|
// Wait for 1.5 seconds to ensure all transactions are confirmed
|
|
@@ -5061,7 +5040,7 @@ async function setMany(many, options) {
|
|
|
5061
5040
|
catch (error) {
|
|
5062
5041
|
throw error;
|
|
5063
5042
|
}
|
|
5064
|
-
async function handleSolanaTransaction(tx, authProvider, options) {
|
|
5043
|
+
async function handleSolanaTransaction(tx, authProvider, options, expectedWritePaths) {
|
|
5065
5044
|
var _a, _b, _c, _d, _e;
|
|
5066
5045
|
// NOTE (backwards-compat revert): a program-allowlist on server-supplied
|
|
5067
5046
|
// `preInstructions` was tried here for the audit-8 SOL-drain concern, but it
|
|
@@ -5109,13 +5088,20 @@ async function setMany(many, options) {
|
|
|
5109
5088
|
}))) !== null && _b !== void 0 ? _b : [],
|
|
5110
5089
|
};
|
|
5111
5090
|
const config = await getConfig();
|
|
5091
|
+
if (tx.signedTransaction) {
|
|
5092
|
+
deserializeAndValidateServerTransaction(tx.signedTransaction, {
|
|
5093
|
+
label: 'Server signedTransaction',
|
|
5094
|
+
expectedAppId: config.appId,
|
|
5095
|
+
expectedWritePaths,
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5112
5098
|
const solTransaction = {
|
|
5113
5099
|
appId: config.appId,
|
|
5114
5100
|
txArgs: [solTransactionData],
|
|
5115
5101
|
lutKey: (_c = tx.lutAddress) !== null && _c !== void 0 ? _c : null,
|
|
5116
5102
|
additionalLutAddresses: tx.additionalLutAddresses,
|
|
5117
5103
|
network: tx.network,
|
|
5118
|
-
preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix) => {
|
|
5104
|
+
preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix, index) => {
|
|
5119
5105
|
var _a;
|
|
5120
5106
|
const keys = (_a = ix.keys) === null || _a === void 0 ? void 0 : _a.map((k) => ({
|
|
5121
5107
|
pubkey: new web3_js.PublicKey(k.pubkey),
|
|
@@ -5126,11 +5112,13 @@ async function setMany(many, options) {
|
|
|
5126
5112
|
? web3_js.SystemProgram.programId // prettier to use the constant
|
|
5127
5113
|
: new web3_js.PublicKey(ix.programId);
|
|
5128
5114
|
const data = bufferExports.Buffer.from(ix.data);
|
|
5129
|
-
|
|
5115
|
+
const instruction = new web3_js.TransactionInstruction({
|
|
5130
5116
|
keys,
|
|
5131
5117
|
programId,
|
|
5132
5118
|
data,
|
|
5133
5119
|
});
|
|
5120
|
+
assertAllowedServerPreInstruction(instruction, `Server preInstruction[${index}]`);
|
|
5121
|
+
return instruction;
|
|
5134
5122
|
})) !== null && _e !== void 0 ? _e : [],
|
|
5135
5123
|
// Server co-signed transaction (when CPI tx_data is present)
|
|
5136
5124
|
signedTransaction: tx.signedTransaction,
|
|
@@ -5138,45 +5126,22 @@ async function setMany(many, options) {
|
|
|
5138
5126
|
const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
|
|
5139
5127
|
return transactionResult;
|
|
5140
5128
|
}
|
|
5141
|
-
async function handlePreBuiltTransaction(tx, authProvider, options) {
|
|
5142
|
-
var _a, _b;
|
|
5129
|
+
async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
|
|
5130
|
+
var _a, _b, _c;
|
|
5143
5131
|
const config = await getConfig();
|
|
5144
|
-
const
|
|
5145
|
-
|
|
5146
|
-
:
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
const
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
|
|
5154
|
-
const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
5155
|
-
const ALLOWED_PROGRAMS = new Set([BOUNDED_PROGRAM, COMPUTE_BUDGET, SYSTEM_PROGRAM]);
|
|
5156
|
-
// System program instruction discriminators (first 4 bytes, little-endian u32)
|
|
5157
|
-
const SYSTEM_TRANSFER = 2; // Transfer instruction index
|
|
5158
|
-
const SYSTEM_TRANSFER_WITH_SEED = 11;
|
|
5159
|
-
const accountKeys = transaction.message.staticAccountKeys;
|
|
5160
|
-
for (const ix of transaction.message.compiledInstructions) {
|
|
5161
|
-
if (ix.programIdIndex >= accountKeys.length) {
|
|
5162
|
-
throw new Error('Pre-built transaction has program ID in lookup table (not allowed)');
|
|
5163
|
-
}
|
|
5164
|
-
const programId = accountKeys[ix.programIdIndex].toBase58();
|
|
5165
|
-
if (!ALLOWED_PROGRAMS.has(programId)) {
|
|
5166
|
-
throw new Error(`Pre-built transaction contains unauthorized program: ${programId}`);
|
|
5167
|
-
}
|
|
5168
|
-
// Block System program transfer instructions — a compromised DO could
|
|
5169
|
-
// embed a SOL drain. Only allow createAccount/allocate (needed for PDA init).
|
|
5170
|
-
if (programId === SYSTEM_PROGRAM && ix.data.length >= 4) {
|
|
5171
|
-
const ixIndex = ix.data[0] | (ix.data[1] << 8) | (ix.data[2] << 16) | (ix.data[3] << 24);
|
|
5172
|
-
if (ixIndex === SYSTEM_TRANSFER || ixIndex === SYSTEM_TRANSFER_WITH_SEED) {
|
|
5173
|
-
throw new Error('Pre-built transaction contains unauthorized System transfer instruction');
|
|
5174
|
-
}
|
|
5175
|
-
}
|
|
5132
|
+
const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, {
|
|
5133
|
+
label: 'Pre-built transaction',
|
|
5134
|
+
expectedAppId: config.appId,
|
|
5135
|
+
expectedWritePaths,
|
|
5136
|
+
});
|
|
5137
|
+
const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
|
|
5138
|
+
const rpcUrl = (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
|
|
5139
|
+
if (shouldSubmit && !rpcUrl) {
|
|
5140
|
+
throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}`);
|
|
5176
5141
|
}
|
|
5177
5142
|
const signedTx = await authProvider.signTransaction(transaction);
|
|
5178
5143
|
const rawTx = signedTx.serialize();
|
|
5179
|
-
if (
|
|
5144
|
+
if (!shouldSubmit) {
|
|
5180
5145
|
return {
|
|
5181
5146
|
transactionSignature: '',
|
|
5182
5147
|
signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
|
|
@@ -5184,6 +5149,7 @@ async function setMany(many, options) {
|
|
|
5184
5149
|
gasUsed: '0',
|
|
5185
5150
|
};
|
|
5186
5151
|
}
|
|
5152
|
+
const connection = new web3_js.Connection(rpcUrl, 'confirmed');
|
|
5187
5153
|
const signature = await connection.sendRawTransaction(rawTx, {
|
|
5188
5154
|
skipPreflight: false,
|
|
5189
5155
|
maxRetries: 3,
|
|
@@ -5196,7 +5162,7 @@ async function setMany(many, options) {
|
|
|
5196
5162
|
return {
|
|
5197
5163
|
transactionSignature: signature,
|
|
5198
5164
|
signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
|
|
5199
|
-
blockNumber: (
|
|
5165
|
+
blockNumber: (_c = (_b = confirmation.context) === null || _b === void 0 ? void 0 : _b.slot) !== null && _c !== void 0 ? _c : 0,
|
|
5200
5166
|
gasUsed: '0',
|
|
5201
5167
|
};
|
|
5202
5168
|
}
|
|
@@ -5413,7 +5379,7 @@ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
|
5413
5379
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
5414
5380
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
5415
5381
|
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
5416
|
-
const
|
|
5382
|
+
const WS_AUTH_EXPIRED_CODE = 'auth_expired';
|
|
5417
5383
|
const WS_CONFIG = {
|
|
5418
5384
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
5419
5385
|
maxRetries: Infinity,
|
|
@@ -5430,11 +5396,12 @@ const WS_V2_PATH = '/ws/v2';
|
|
|
5430
5396
|
let browserReconnectHooksAttached = false;
|
|
5431
5397
|
let lastBrowserTriggeredReconnectAt = 0;
|
|
5432
5398
|
let reconnectInProgress = null;
|
|
5399
|
+
let ambientAuthFailure = null;
|
|
5433
5400
|
// ============ Helper Functions ============
|
|
5434
5401
|
function generateSubscriptionId() {
|
|
5435
5402
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5436
5403
|
}
|
|
5437
|
-
function hashForKey
|
|
5404
|
+
function hashForKey(value) {
|
|
5438
5405
|
let h = 5381;
|
|
5439
5406
|
for (let i = 0; i < value.length; i++) {
|
|
5440
5407
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
@@ -5446,17 +5413,17 @@ function hashForKey$1(value) {
|
|
|
5446
5413
|
* just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
|
|
5447
5414
|
* a private subscription snapshot cached for one user from being delivered to a
|
|
5448
5415
|
* different user who subscribes with the same options (shared process / SSR worker
|
|
5449
|
-
* / browser login-switch; this cache has a 5-minute TTL).
|
|
5450
|
-
* read
|
|
5416
|
+
* / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
|
|
5417
|
+
* before entries are read or written; no-auth subscriptions never populate an
|
|
5418
|
+
* implicit anonymous cache bucket.
|
|
5451
5419
|
*/
|
|
5452
5420
|
function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
5453
5421
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
5454
5422
|
const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
|
|
5455
5423
|
const limitKey = limit !== undefined ? `:l${limit}` : '';
|
|
5456
|
-
const cursorKey = cursor ? `:c${hashForKey
|
|
5457
|
-
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey
|
|
5458
|
-
|
|
5459
|
-
return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5424
|
+
const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
|
|
5425
|
+
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
|
|
5426
|
+
return `${identity}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5460
5427
|
}
|
|
5461
5428
|
/**
|
|
5462
5429
|
* Derive an opaque identity string for the bearer material a subscription sends.
|
|
@@ -5464,7 +5431,7 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
|
5464
5431
|
* before the server has verified any claims.
|
|
5465
5432
|
*/
|
|
5466
5433
|
function principalFromIdToken(idToken) {
|
|
5467
|
-
return idToken ? `t${hashForKey
|
|
5434
|
+
return idToken ? `t${hashForKey(idToken)}` : null;
|
|
5468
5435
|
}
|
|
5469
5436
|
async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
5470
5437
|
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
@@ -5473,16 +5440,21 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
|
5473
5440
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
5474
5441
|
try {
|
|
5475
5442
|
const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
|
|
5476
|
-
|
|
5443
|
+
const principal = principalFromIdToken(bearer);
|
|
5444
|
+
return principal
|
|
5445
|
+
? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
|
|
5446
|
+
: { key: `${effectiveAppId}:oanon`, cacheable: false };
|
|
5477
5447
|
}
|
|
5478
5448
|
catch (_a) {
|
|
5479
|
-
// Couldn't resolve the override identity —
|
|
5480
|
-
|
|
5481
|
-
return `${effectiveAppId}:o${principalFromIdToken(null)}-${safeBtoa(String(connectionEpoch++))}`;
|
|
5449
|
+
// Couldn't resolve the override identity — do not use response cache.
|
|
5450
|
+
return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
|
|
5482
5451
|
}
|
|
5483
5452
|
}
|
|
5484
5453
|
const idToken = await getIdToken(isServer);
|
|
5485
|
-
|
|
5454
|
+
const principal = principalFromIdToken(idToken);
|
|
5455
|
+
return principal
|
|
5456
|
+
? { key: `${effectiveAppId}:${principal}`, cacheable: true }
|
|
5457
|
+
: { key: `${effectiveAppId}:anon`, cacheable: false };
|
|
5486
5458
|
}
|
|
5487
5459
|
/** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
|
|
5488
5460
|
function bearerFromAuthHeaders(headers) {
|
|
@@ -5512,6 +5484,41 @@ function getTokenExpirationTime(token) {
|
|
|
5512
5484
|
return null;
|
|
5513
5485
|
}
|
|
5514
5486
|
}
|
|
5487
|
+
function makeAuthExpiredError(message, status) {
|
|
5488
|
+
const err = new Error(`${WS_AUTH_EXPIRED_CODE}: ${message}`);
|
|
5489
|
+
err.code = WS_AUTH_EXPIRED_CODE;
|
|
5490
|
+
if (status !== undefined)
|
|
5491
|
+
err.status = status;
|
|
5492
|
+
return err;
|
|
5493
|
+
}
|
|
5494
|
+
function isAuthExpiredError(error) {
|
|
5495
|
+
return !!error && typeof error === 'object' && error.code === WS_AUTH_EXPIRED_CODE;
|
|
5496
|
+
}
|
|
5497
|
+
function normalizeAuthExpiredError(error, fallbackMessage) {
|
|
5498
|
+
var _a, _b;
|
|
5499
|
+
if (isAuthExpiredError(error))
|
|
5500
|
+
return error;
|
|
5501
|
+
const status = (_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : error === null || error === void 0 ? void 0 : error.code;
|
|
5502
|
+
return makeAuthExpiredError(fallbackMessage, status);
|
|
5503
|
+
}
|
|
5504
|
+
function rememberAmbientAuthFailure(error) {
|
|
5505
|
+
ambientAuthFailure = error;
|
|
5506
|
+
return error;
|
|
5507
|
+
}
|
|
5508
|
+
function failConnectionAuth(connection, error) {
|
|
5509
|
+
connection.authFailure = error;
|
|
5510
|
+
connection.pendingAuthToken = null;
|
|
5511
|
+
connection.isConnecting = false;
|
|
5512
|
+
connection.isConnected = false;
|
|
5513
|
+
connection.isAuthenticating = false;
|
|
5514
|
+
for (const [, pending] of connection.pendingSubscriptions) {
|
|
5515
|
+
pending.reject(error);
|
|
5516
|
+
}
|
|
5517
|
+
connection.pendingSubscriptions.clear();
|
|
5518
|
+
for (const subscription of connection.subscriptions.values()) {
|
|
5519
|
+
notifyErrorCallbacks(subscription, error);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5515
5522
|
function scheduleTokenRefresh(connection, isServer) {
|
|
5516
5523
|
// Clear any existing timer
|
|
5517
5524
|
if (connection.tokenRefreshTimer) {
|
|
@@ -5579,23 +5586,32 @@ async function getFreshAuthToken(isServer) {
|
|
|
5579
5586
|
var _a, _b, _c, _d, _e;
|
|
5580
5587
|
const currentToken = await getIdToken(isServer);
|
|
5581
5588
|
if (!currentToken) {
|
|
5589
|
+
if (ambientAuthFailure) {
|
|
5590
|
+
throw ambientAuthFailure;
|
|
5591
|
+
}
|
|
5582
5592
|
return null;
|
|
5583
5593
|
}
|
|
5584
5594
|
if (!isTokenExpired(currentToken)) {
|
|
5595
|
+
ambientAuthFailure = null;
|
|
5585
5596
|
return currentToken;
|
|
5586
5597
|
}
|
|
5598
|
+
if (ambientAuthFailure) {
|
|
5599
|
+
throw ambientAuthFailure;
|
|
5600
|
+
}
|
|
5587
5601
|
// Token is expired — attempt refresh
|
|
5602
|
+
const refreshToken = await getRefreshToken(isServer);
|
|
5603
|
+
if (!refreshToken) {
|
|
5604
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
5605
|
+
throw rememberAmbientAuthFailure(makeAuthExpiredError('Authentication expired and no refresh token is available'));
|
|
5606
|
+
}
|
|
5588
5607
|
try {
|
|
5589
|
-
const refreshToken = await getRefreshToken(isServer);
|
|
5590
|
-
if (!refreshToken) {
|
|
5591
|
-
console.warn('[WS v2] Token expired but no refresh token available');
|
|
5592
|
-
return null;
|
|
5593
|
-
}
|
|
5594
5608
|
const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
|
|
5595
5609
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
5596
5610
|
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer, refreshData.refreshToken);
|
|
5611
|
+
ambientAuthFailure = null;
|
|
5597
5612
|
return refreshData.idToken;
|
|
5598
5613
|
}
|
|
5614
|
+
throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
|
|
5599
5615
|
}
|
|
5600
5616
|
catch (error) {
|
|
5601
5617
|
// Log only the status — the raw axios error carries config.data (the refresh
|
|
@@ -5612,12 +5628,22 @@ async function getFreshAuthToken(isServer) {
|
|
|
5612
5628
|
console.warn('[WS v2] Failed to clear stale session:', clearError);
|
|
5613
5629
|
}
|
|
5614
5630
|
}
|
|
5631
|
+
throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
async function getConnectionAuthToken(isServer, authTokenProvider) {
|
|
5635
|
+
if (!authTokenProvider) {
|
|
5636
|
+
return getFreshAuthToken(isServer);
|
|
5637
|
+
}
|
|
5638
|
+
try {
|
|
5639
|
+
const token = await authTokenProvider();
|
|
5640
|
+
if (token)
|
|
5641
|
+
return token;
|
|
5642
|
+
throw makeAuthExpiredError('Authenticated websocket token provider returned no token');
|
|
5643
|
+
}
|
|
5644
|
+
catch (error) {
|
|
5645
|
+
throw normalizeAuthExpiredError(error, 'Authenticated websocket token provider failed');
|
|
5615
5646
|
}
|
|
5616
|
-
// Return null instead of the expired token to prevent infinite 401 reconnect storms.
|
|
5617
|
-
// The server accepts unauthenticated connections; auth-required subscriptions will
|
|
5618
|
-
// receive per-subscription errors via onError callbacks.
|
|
5619
|
-
console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
|
|
5620
|
-
return null;
|
|
5621
5647
|
}
|
|
5622
5648
|
function hasDisconnectedActiveConnections() {
|
|
5623
5649
|
for (const connection of connections.values()) {
|
|
@@ -5692,13 +5718,26 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5692
5718
|
const roomKey = roomKeyFromRoutePath(routePath);
|
|
5693
5719
|
// A wallet-scoped subscription (server WalletClient) gets its OWN connection
|
|
5694
5720
|
// keyed by the wallet identity, so its WS authenticates as that wallet and
|
|
5695
|
-
// never shares a connection (or token) with
|
|
5721
|
+
// never shares a connection (or token) with any top-level server caller.
|
|
5696
5722
|
const base = roomKey ? `${appId}#room#${roomKey}` : appId;
|
|
5697
5723
|
const connKey = principalKey ? `${base}#id#${principalKey}` : base;
|
|
5698
5724
|
let connection = connections.get(connKey);
|
|
5699
5725
|
if (connection && connection.ws) {
|
|
5726
|
+
if (connection.authFailure) {
|
|
5727
|
+
throw connection.authFailure;
|
|
5728
|
+
}
|
|
5729
|
+
try {
|
|
5730
|
+
await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5731
|
+
}
|
|
5732
|
+
catch (error) {
|
|
5733
|
+
const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
|
|
5734
|
+
failConnectionAuth(connection, authError);
|
|
5735
|
+
throw authError;
|
|
5736
|
+
}
|
|
5737
|
+
connection.authFailure = null;
|
|
5700
5738
|
return connection;
|
|
5701
5739
|
}
|
|
5740
|
+
let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5702
5741
|
// Create new connection
|
|
5703
5742
|
connection = {
|
|
5704
5743
|
ws: null,
|
|
@@ -5716,6 +5755,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5716
5755
|
pendingAuthToken: null,
|
|
5717
5756
|
tokenRefreshTimer: null,
|
|
5718
5757
|
consecutiveAuthFailures: 0,
|
|
5758
|
+
authFailure: null,
|
|
5719
5759
|
};
|
|
5720
5760
|
connections.set(connKey, connection);
|
|
5721
5761
|
// URL provider for reconnection with fresh tokens
|
|
@@ -5735,29 +5775,25 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5735
5775
|
}
|
|
5736
5776
|
// Resolve auth token if available. A wallet-scoped connection resolves
|
|
5737
5777
|
// its token from the wallet's own session (self-refreshing); all others
|
|
5738
|
-
// use the
|
|
5778
|
+
// use the browser session or fail closed for top-level server calls. The token is sent as the first WS
|
|
5739
5779
|
// frame after open, never as a URL query parameter.
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5780
|
+
let authToken;
|
|
5781
|
+
try {
|
|
5782
|
+
authToken = initialAuthToken !== undefined
|
|
5783
|
+
? initialAuthToken
|
|
5784
|
+
: await getConnectionAuthToken(isServer, connection.authTokenProvider);
|
|
5785
|
+
initialAuthToken = undefined;
|
|
5786
|
+
}
|
|
5787
|
+
catch (error) {
|
|
5788
|
+
const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
|
|
5789
|
+
failConnectionAuth(connection, authError);
|
|
5790
|
+
throw authError;
|
|
5791
|
+
}
|
|
5743
5792
|
connection.pendingAuthToken = authToken || null;
|
|
5744
5793
|
if (authToken) {
|
|
5745
5794
|
// Successful token acquisition — reset failure counter
|
|
5746
5795
|
connection.consecutiveAuthFailures = 0;
|
|
5747
|
-
|
|
5748
|
-
else {
|
|
5749
|
-
// Check if user WAS authenticated (had a token that expired).
|
|
5750
|
-
// If so, retry with exponential backoff before falling back to unauthenticated.
|
|
5751
|
-
const expiredToken = await getIdToken(isServer);
|
|
5752
|
-
if (expiredToken && isTokenExpired(expiredToken)) {
|
|
5753
|
-
connection.consecutiveAuthFailures++;
|
|
5754
|
-
if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
|
|
5755
|
-
console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
|
|
5756
|
-
throw new Error('Auth token refresh failed, retrying with backoff');
|
|
5757
|
-
}
|
|
5758
|
-
console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
|
|
5759
|
-
}
|
|
5760
|
-
// No token at all (never authenticated) or retries exhausted — connect without auth
|
|
5796
|
+
connection.authFailure = null;
|
|
5761
5797
|
}
|
|
5762
5798
|
return wsUrl.toString();
|
|
5763
5799
|
};
|
|
@@ -5771,15 +5807,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5771
5807
|
connection.isConnecting = false;
|
|
5772
5808
|
connection.isConnected = true;
|
|
5773
5809
|
// NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
|
|
5774
|
-
// fresh auth token is actually obtained
|
|
5775
|
-
// an explicit auth change (reconnectWithNewAuthV2, line ~854). Resetting
|
|
5776
|
-
// on every 'open' event created an infinite loop: auth fails 5x → connect
|
|
5777
|
-
// without auth → open resets counter → disconnect → auth fails 5x again →
|
|
5778
|
-
// repeat forever, hammering /session/refresh and causing 429s.
|
|
5779
|
-
//
|
|
5780
|
-
// An elevated counter is safe for anonymous/guest sessions: when there's no
|
|
5781
|
-
// token at all (getIdToken returns null), the counter is never checked —
|
|
5782
|
-
// urlProvider skips straight to unauthenticated connection.
|
|
5810
|
+
// fresh auth token is actually obtained or on an explicit auth change.
|
|
5783
5811
|
// Schedule periodic token freshness checks
|
|
5784
5812
|
scheduleTokenRefresh(connection, isServer);
|
|
5785
5813
|
if (connection.pendingAuthToken) {
|
|
@@ -5854,8 +5882,10 @@ function handleServerMessage(connection, message) {
|
|
|
5854
5882
|
// If we already received data for this subscription, treat subscribed
|
|
5855
5883
|
// as an ack only and avoid regressing to an older snapshot.
|
|
5856
5884
|
if (subscription.lastData === undefined) {
|
|
5857
|
-
|
|
5858
|
-
|
|
5885
|
+
if (subscription.cache) {
|
|
5886
|
+
const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
|
|
5887
|
+
responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
|
|
5888
|
+
}
|
|
5859
5889
|
subscription.lastData = message.data;
|
|
5860
5890
|
notifyCallbacks(subscription, message.data);
|
|
5861
5891
|
}
|
|
@@ -5881,8 +5911,10 @@ function handleServerMessage(connection, message) {
|
|
|
5881
5911
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
5882
5912
|
if (subscription) {
|
|
5883
5913
|
// Update cache
|
|
5884
|
-
|
|
5885
|
-
|
|
5914
|
+
if (subscription.cache) {
|
|
5915
|
+
const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
|
|
5916
|
+
responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
|
|
5917
|
+
}
|
|
5886
5918
|
// Store last data
|
|
5887
5919
|
subscription.lastData = message.data;
|
|
5888
5920
|
// Notify callbacks
|
|
@@ -6094,26 +6126,43 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6094
6126
|
// subscribes with identical options (shared process / SSR / login-switch).
|
|
6095
6127
|
// Per-subscription wallet override (server WalletClient.subscribe). When set,
|
|
6096
6128
|
// the connection authenticates + caches under the wallet's identity instead of
|
|
6097
|
-
//
|
|
6098
|
-
//
|
|
6129
|
+
// any top-level server caller — so a `createWalletClient` caller can
|
|
6130
|
+
// subscribe through its explicit wallet session.
|
|
6099
6131
|
const overrides = subscriptionOptions._overrides;
|
|
6100
6132
|
const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
|
|
6101
6133
|
? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
|
|
6102
6134
|
: undefined;
|
|
6103
|
-
const
|
|
6135
|
+
const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
|
|
6136
|
+
const identity = identityInfo.key;
|
|
6137
|
+
const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
|
|
6104
6138
|
const principalKey = authTokenProvider ? identity : undefined;
|
|
6105
|
-
const cacheKey =
|
|
6106
|
-
|
|
6107
|
-
|
|
6139
|
+
const cacheKey = responseCacheEnabled
|
|
6140
|
+
? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
|
|
6141
|
+
: null;
|
|
6142
|
+
// Get or create connection for this routing target (room-scoped when a
|
|
6143
|
+
// room route is supplied by the live helper, else the app-level connection).
|
|
6144
|
+
let connection;
|
|
6145
|
+
try {
|
|
6146
|
+
connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
|
|
6147
|
+
}
|
|
6148
|
+
catch (error) {
|
|
6149
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
6150
|
+
if (subscriptionOptions.onError) {
|
|
6151
|
+
subscriptionOptions.onError(err);
|
|
6152
|
+
return async () => { };
|
|
6153
|
+
}
|
|
6154
|
+
throw err;
|
|
6155
|
+
}
|
|
6156
|
+
// Deliver cached data immediately if available, but only after connection
|
|
6157
|
+
// auth preflight has succeeded. An expired authenticated session must receive
|
|
6158
|
+
// an auth error, not a stale private snapshot followed by a failed connect.
|
|
6159
|
+
const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
|
|
6108
6160
|
if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
|
|
6109
6161
|
setTimeout(() => {
|
|
6110
6162
|
var _a;
|
|
6111
6163
|
(_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
|
|
6112
6164
|
}, 0);
|
|
6113
6165
|
}
|
|
6114
|
-
// Get or create connection for this routing target (room-scoped when a
|
|
6115
|
-
// room route is supplied by the live helper, else the app-level connection).
|
|
6116
|
-
const connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
|
|
6117
6166
|
// Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
|
|
6118
6167
|
const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
|
|
6119
6168
|
const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
|
|
@@ -6132,10 +6181,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6132
6181
|
}
|
|
6133
6182
|
}
|
|
6134
6183
|
if (existingSubscription) {
|
|
6184
|
+
if (responseCacheEnabled) {
|
|
6185
|
+
existingSubscription.cache = true;
|
|
6186
|
+
}
|
|
6135
6187
|
// Add callback to existing subscription
|
|
6136
6188
|
existingSubscription.callbacks.push(subscriptionOptions);
|
|
6137
|
-
// Deliver last known data immediately
|
|
6138
|
-
|
|
6189
|
+
// Deliver last known data immediately only for explicit, principal-bound
|
|
6190
|
+
// cache opt-in. Otherwise joining an existing subscription can replay a
|
|
6191
|
+
// stale private snapshot without a fresh server auth result.
|
|
6192
|
+
if (responseCacheEnabled &&
|
|
6193
|
+
existingSubscription.cache &&
|
|
6194
|
+
existingSubscription.lastData !== undefined &&
|
|
6195
|
+
subscriptionOptions.onData) {
|
|
6139
6196
|
setTimeout(() => {
|
|
6140
6197
|
var _a;
|
|
6141
6198
|
(_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
|
|
@@ -6159,6 +6216,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6159
6216
|
includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
|
|
6160
6217
|
callbacks: [subscriptionOptions],
|
|
6161
6218
|
lastData: undefined,
|
|
6219
|
+
cache: responseCacheEnabled,
|
|
6162
6220
|
identity,
|
|
6163
6221
|
};
|
|
6164
6222
|
connection.subscriptions.set(subscriptionId, subscription);
|
|
@@ -6290,19 +6348,21 @@ function clearCacheV2(path) {
|
|
|
6290
6348
|
*/
|
|
6291
6349
|
function getCachedDataV2(path, prompt) {
|
|
6292
6350
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6293
|
-
// H1: response caches are identity-scoped (`<identity>|<path>:...`)
|
|
6294
|
-
// the caller's identity from an active
|
|
6295
|
-
//
|
|
6296
|
-
// bucket (which would wrongly return null). No matching sub → null (safe).
|
|
6351
|
+
// H1: response caches are identity-scoped (`<identity>|<path>:...`) and
|
|
6352
|
+
// opt-in. Resolve the caller's identity from an active cache-enabled
|
|
6353
|
+
// subscription for this path. No matching sub → null (safe).
|
|
6297
6354
|
let identity;
|
|
6298
6355
|
outer: for (const connection of connections.values()) {
|
|
6299
6356
|
for (const sub of connection.subscriptions.values()) {
|
|
6300
|
-
if (sub.path === normalizedPath && sub.prompt === prompt) {
|
|
6357
|
+
if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
|
|
6301
6358
|
identity = sub.identity;
|
|
6302
6359
|
break outer;
|
|
6303
6360
|
}
|
|
6304
6361
|
}
|
|
6305
6362
|
}
|
|
6363
|
+
if (!identity) {
|
|
6364
|
+
return null;
|
|
6365
|
+
}
|
|
6306
6366
|
const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
|
|
6307
6367
|
const cachedEntry = responseCache.get(cacheKey);
|
|
6308
6368
|
if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
|
|
@@ -6334,6 +6394,7 @@ async function reconnectWithNewAuthV2() {
|
|
|
6334
6394
|
}
|
|
6335
6395
|
}
|
|
6336
6396
|
async function doReconnectWithNewAuth() {
|
|
6397
|
+
ambientAuthFailure = null;
|
|
6337
6398
|
// SECURITY (H1): the logged-in identity is changing (login / logout / switch).
|
|
6338
6399
|
// Wipe ALL principal-scoped read caches so the new identity can never observe
|
|
6339
6400
|
// data cached for the previous one. Clear both the WS response cache and the
|
|
@@ -6355,7 +6416,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6355
6416
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6356
6417
|
}
|
|
6357
6418
|
try {
|
|
6358
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return
|
|
6419
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-Ck_VgTcv.js'); });
|
|
6359
6420
|
await reconnectRealtimeStoreWithNewAuth();
|
|
6360
6421
|
}
|
|
6361
6422
|
catch (error) {
|
|
@@ -6375,6 +6436,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6375
6436
|
connection.pendingUnsubscriptions.clear();
|
|
6376
6437
|
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
6377
6438
|
connection.consecutiveAuthFailures = 0;
|
|
6439
|
+
connection.authFailure = null;
|
|
6378
6440
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
6379
6441
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
6380
6442
|
try {
|
|
@@ -6385,7 +6447,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6385
6447
|
}
|
|
6386
6448
|
}
|
|
6387
6449
|
}
|
|
6388
|
-
// ============
|
|
6450
|
+
// ============ WebSocket request helpers ============
|
|
6389
6451
|
const WS_REQUEST_TIMEOUT_MS = 30000;
|
|
6390
6452
|
function generateRequestId() {
|
|
6391
6453
|
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -6401,104 +6463,6 @@ function hasActiveConnection() {
|
|
|
6401
6463
|
}
|
|
6402
6464
|
return false;
|
|
6403
6465
|
}
|
|
6404
|
-
async function waitForConnectionAuthenticated(connection) {
|
|
6405
|
-
if (!connection.isAuthenticating || !connection.ws)
|
|
6406
|
-
return;
|
|
6407
|
-
const ws = connection.ws;
|
|
6408
|
-
await new Promise((resolve, reject) => {
|
|
6409
|
-
let timeout;
|
|
6410
|
-
let cleanup = () => { };
|
|
6411
|
-
const onMessage = (event) => {
|
|
6412
|
-
try {
|
|
6413
|
-
const message = JSON.parse(event.data);
|
|
6414
|
-
if ((message === null || message === void 0 ? void 0 : message.type) === 'authenticated') {
|
|
6415
|
-
cleanup();
|
|
6416
|
-
resolve();
|
|
6417
|
-
}
|
|
6418
|
-
}
|
|
6419
|
-
catch (_a) {
|
|
6420
|
-
// Other frames are handled by the main listener.
|
|
6421
|
-
}
|
|
6422
|
-
};
|
|
6423
|
-
const onClose = () => {
|
|
6424
|
-
cleanup();
|
|
6425
|
-
reject(new Error('WebSocket disconnected during authentication'));
|
|
6426
|
-
};
|
|
6427
|
-
const onError = () => {
|
|
6428
|
-
cleanup();
|
|
6429
|
-
reject(new Error('WebSocket authentication failed'));
|
|
6430
|
-
};
|
|
6431
|
-
cleanup = () => {
|
|
6432
|
-
clearTimeout(timeout);
|
|
6433
|
-
ws.removeEventListener('message', onMessage);
|
|
6434
|
-
ws.removeEventListener('close', onClose);
|
|
6435
|
-
ws.removeEventListener('error', onError);
|
|
6436
|
-
};
|
|
6437
|
-
timeout = setTimeout(() => {
|
|
6438
|
-
cleanup();
|
|
6439
|
-
reject(new Error('WebSocket authentication timeout'));
|
|
6440
|
-
}, 10000);
|
|
6441
|
-
if (!connection.isAuthenticating) {
|
|
6442
|
-
cleanup();
|
|
6443
|
-
resolve();
|
|
6444
|
-
return;
|
|
6445
|
-
}
|
|
6446
|
-
ws.addEventListener('message', onMessage);
|
|
6447
|
-
ws.addEventListener('close', onClose);
|
|
6448
|
-
ws.addEventListener('error', onError);
|
|
6449
|
-
});
|
|
6450
|
-
}
|
|
6451
|
-
async function sendRequest(msgBuilder) {
|
|
6452
|
-
const config = await getConfig();
|
|
6453
|
-
const appId = config.appId;
|
|
6454
|
-
const connection = await getOrCreateConnection(appId, config.isServer);
|
|
6455
|
-
// Wait for the connection to be open (getOrCreateConnection may return
|
|
6456
|
-
// while still connecting).
|
|
6457
|
-
if (!connection.isConnected && connection.ws) {
|
|
6458
|
-
await new Promise((resolve, reject) => {
|
|
6459
|
-
const timeout = setTimeout(() => {
|
|
6460
|
-
var _a;
|
|
6461
|
-
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
|
|
6462
|
-
reject(new Error('WebSocket connection timeout'));
|
|
6463
|
-
}, 10000);
|
|
6464
|
-
const onOpen = () => { clearTimeout(timeout); resolve(); };
|
|
6465
|
-
if (connection.isConnected) {
|
|
6466
|
-
clearTimeout(timeout);
|
|
6467
|
-
resolve();
|
|
6468
|
-
return;
|
|
6469
|
-
}
|
|
6470
|
-
connection.ws.addEventListener('open', onOpen);
|
|
6471
|
-
});
|
|
6472
|
-
}
|
|
6473
|
-
if (!connection.ws || !connection.isConnected) {
|
|
6474
|
-
throw new Error('WebSocket connection not available');
|
|
6475
|
-
}
|
|
6476
|
-
await waitForConnectionAuthenticated(connection);
|
|
6477
|
-
const requestId = generateRequestId();
|
|
6478
|
-
const message = msgBuilder(requestId);
|
|
6479
|
-
return new Promise((resolve, reject) => {
|
|
6480
|
-
const timer = setTimeout(() => {
|
|
6481
|
-
connection.pendingRequests.delete(requestId);
|
|
6482
|
-
reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
|
|
6483
|
-
}, WS_REQUEST_TIMEOUT_MS);
|
|
6484
|
-
connection.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
6485
|
-
try {
|
|
6486
|
-
connection.ws.send(JSON.stringify(message));
|
|
6487
|
-
}
|
|
6488
|
-
catch (error) {
|
|
6489
|
-
connection.pendingRequests.delete(requestId);
|
|
6490
|
-
clearTimeout(timer);
|
|
6491
|
-
reject(error);
|
|
6492
|
-
}
|
|
6493
|
-
});
|
|
6494
|
-
}
|
|
6495
|
-
async function wsGet(path) {
|
|
6496
|
-
return sendRequest((requestId) => ({
|
|
6497
|
-
type: 'get',
|
|
6498
|
-
requestId,
|
|
6499
|
-
path,
|
|
6500
|
-
}));
|
|
6501
|
-
}
|
|
6502
6466
|
/**
|
|
6503
6467
|
* Send a live-room intent over the EXISTING per-room socket (fire-and-forget).
|
|
6504
6468
|
* Returns true if it was sent over an open connection, false if there is no
|
|
@@ -6557,31 +6521,6 @@ function wsIntentReliable(appId, roomRoutePath, intent) {
|
|
|
6557
6521
|
}
|
|
6558
6522
|
});
|
|
6559
6523
|
}
|
|
6560
|
-
async function wsSet(documents) {
|
|
6561
|
-
return sendRequest((requestId) => ({
|
|
6562
|
-
type: 'set',
|
|
6563
|
-
requestId,
|
|
6564
|
-
documents,
|
|
6565
|
-
}));
|
|
6566
|
-
}
|
|
6567
|
-
async function wsQuery(path, opts) {
|
|
6568
|
-
return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
|
|
6569
|
-
path }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: opts.includeSubPaths } : {}))));
|
|
6570
|
-
}
|
|
6571
|
-
async function wsDelete(path) {
|
|
6572
|
-
return sendRequest((requestId) => ({
|
|
6573
|
-
type: 'delete',
|
|
6574
|
-
requestId,
|
|
6575
|
-
path,
|
|
6576
|
-
}));
|
|
6577
|
-
}
|
|
6578
|
-
async function wsGetMany(paths) {
|
|
6579
|
-
return sendRequest((requestId) => ({
|
|
6580
|
-
type: 'getMany',
|
|
6581
|
-
requestId,
|
|
6582
|
-
paths,
|
|
6583
|
-
}));
|
|
6584
|
-
}
|
|
6585
6524
|
|
|
6586
6525
|
/**
|
|
6587
6526
|
* WebSocket Subscription Module
|
|
@@ -6762,1008 +6701,6 @@ function toMillis(seconds) {
|
|
|
6762
6701
|
return seconds * 1000;
|
|
6763
6702
|
}
|
|
6764
6703
|
|
|
6765
|
-
// ---------------------------------------------------------------------------
|
|
6766
|
-
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
6767
|
-
//
|
|
6768
|
-
// Manages: WS connection, in-memory state, IDB persistence, optimistic
|
|
6769
|
-
// writes, delta accumulation, loading states, ephemeral/durable tiers.
|
|
6770
|
-
// ---------------------------------------------------------------------------
|
|
6771
|
-
// ---------------------------------------------------------------------------
|
|
6772
|
-
// IDB helpers (lazy-loaded, non-blocking)
|
|
6773
|
-
// ---------------------------------------------------------------------------
|
|
6774
|
-
const IDB_NAME = 'bounded-realtime';
|
|
6775
|
-
const IDB_STORE = 'subscriptions';
|
|
6776
|
-
const IDB_VERSION = 1;
|
|
6777
|
-
let idbPromise = null;
|
|
6778
|
-
function getIDB() {
|
|
6779
|
-
if (idbPromise)
|
|
6780
|
-
return idbPromise;
|
|
6781
|
-
if (typeof indexedDB === 'undefined') {
|
|
6782
|
-
return Promise.reject(new Error('IndexedDB not available'));
|
|
6783
|
-
}
|
|
6784
|
-
idbPromise = new Promise((resolve, reject) => {
|
|
6785
|
-
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
6786
|
-
req.onupgradeneeded = () => {
|
|
6787
|
-
const db = req.result;
|
|
6788
|
-
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
6789
|
-
db.createObjectStore(IDB_STORE);
|
|
6790
|
-
}
|
|
6791
|
-
};
|
|
6792
|
-
req.onsuccess = () => resolve(req.result);
|
|
6793
|
-
req.onerror = () => reject(req.error);
|
|
6794
|
-
});
|
|
6795
|
-
return idbPromise;
|
|
6796
|
-
}
|
|
6797
|
-
async function idbGet(key) {
|
|
6798
|
-
try {
|
|
6799
|
-
const db = await getIDB();
|
|
6800
|
-
return new Promise((resolve) => {
|
|
6801
|
-
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
6802
|
-
const store = tx.objectStore(IDB_STORE);
|
|
6803
|
-
const req = store.get(key);
|
|
6804
|
-
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
6805
|
-
req.onerror = () => resolve(null);
|
|
6806
|
-
});
|
|
6807
|
-
}
|
|
6808
|
-
catch (_a) {
|
|
6809
|
-
return null;
|
|
6810
|
-
}
|
|
6811
|
-
}
|
|
6812
|
-
async function idbSet(key, value) {
|
|
6813
|
-
try {
|
|
6814
|
-
const db = await getIDB();
|
|
6815
|
-
return new Promise((resolve) => {
|
|
6816
|
-
const tx = db.transaction(IDB_STORE, 'readwrite');
|
|
6817
|
-
const store = tx.objectStore(IDB_STORE);
|
|
6818
|
-
store.put(value, key);
|
|
6819
|
-
tx.oncomplete = () => resolve();
|
|
6820
|
-
tx.onerror = () => resolve();
|
|
6821
|
-
});
|
|
6822
|
-
}
|
|
6823
|
-
catch (_a) {
|
|
6824
|
-
// Best-effort persistence
|
|
6825
|
-
}
|
|
6826
|
-
}
|
|
6827
|
-
// ---------------------------------------------------------------------------
|
|
6828
|
-
// RealtimeStore
|
|
6829
|
-
// ---------------------------------------------------------------------------
|
|
6830
|
-
let nextRequestId = 1;
|
|
6831
|
-
function hashForKey(value) {
|
|
6832
|
-
let h = 5381;
|
|
6833
|
-
for (let i = 0; i < value.length; i++) {
|
|
6834
|
-
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
6835
|
-
}
|
|
6836
|
-
return h.toString(36);
|
|
6837
|
-
}
|
|
6838
|
-
function principalFromToken(token) {
|
|
6839
|
-
return token ? `t${hashForKey(token)}` : 'anon';
|
|
6840
|
-
}
|
|
6841
|
-
class RealtimeStore {
|
|
6842
|
-
constructor() {
|
|
6843
|
-
this.ws = null;
|
|
6844
|
-
this.wsUrl = '';
|
|
6845
|
-
this.appId = '';
|
|
6846
|
-
this.subscriptions = new Map();
|
|
6847
|
-
this.pendingRequests = new Map();
|
|
6848
|
-
this.connectPromise = null;
|
|
6849
|
-
this.reconnectTimer = null;
|
|
6850
|
-
this.reconnectDelay = 1000;
|
|
6851
|
-
this.maxReconnectDelay = 30000;
|
|
6852
|
-
this.idbFlushTimer = null;
|
|
6853
|
-
this.idbDirtyKeys = new Set();
|
|
6854
|
-
this.closed = false;
|
|
6855
|
-
this.authToken = null;
|
|
6856
|
-
this.authPrincipalKey = 'anon';
|
|
6857
|
-
this.authenticating = false;
|
|
6858
|
-
this.suppressNextReconnect = false;
|
|
6859
|
-
this.isServer = false;
|
|
6860
|
-
this.tokenRefreshTimer = null;
|
|
6861
|
-
// -----------------------------------------------------------------------
|
|
6862
|
-
// WebSocket connection
|
|
6863
|
-
// -----------------------------------------------------------------------
|
|
6864
|
-
this.initPromise = null;
|
|
6865
|
-
}
|
|
6866
|
-
// -----------------------------------------------------------------------
|
|
6867
|
-
// Initialization
|
|
6868
|
-
// -----------------------------------------------------------------------
|
|
6869
|
-
async init() {
|
|
6870
|
-
const config = await getConfig();
|
|
6871
|
-
this.appId = config.appId;
|
|
6872
|
-
this.wsUrl = config.wsApiUrl;
|
|
6873
|
-
this.isServer = config.isServer;
|
|
6874
|
-
await this.refreshToken();
|
|
6875
|
-
this.startTokenRefresh();
|
|
6876
|
-
}
|
|
6877
|
-
async refreshToken() {
|
|
6878
|
-
let token = null;
|
|
6879
|
-
try {
|
|
6880
|
-
const { getIdToken } = await Promise.resolve().then(function () { return utils; });
|
|
6881
|
-
token = await getIdToken(this.isServer);
|
|
6882
|
-
}
|
|
6883
|
-
catch ( /* no auth available */_a) { /* no auth available */ }
|
|
6884
|
-
this.authToken = token !== null && token !== void 0 ? token : null;
|
|
6885
|
-
this.authPrincipalKey = principalFromToken(this.authToken);
|
|
6886
|
-
}
|
|
6887
|
-
startTokenRefresh() {
|
|
6888
|
-
if (this.tokenRefreshTimer)
|
|
6889
|
-
return;
|
|
6890
|
-
this.tokenRefreshTimer = setInterval(async () => {
|
|
6891
|
-
const prevPrincipal = this.authPrincipalKey;
|
|
6892
|
-
await this.refreshToken();
|
|
6893
|
-
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6894
|
-
await this.applyAuthPrincipalChange();
|
|
6895
|
-
if (this.subscriptions.size > 0) {
|
|
6896
|
-
await this.ensureConnected().catch(() => {
|
|
6897
|
-
this.setAllSubscriptionStatus('error');
|
|
6898
|
-
});
|
|
6899
|
-
}
|
|
6900
|
-
}
|
|
6901
|
-
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
6902
|
-
}
|
|
6903
|
-
async ensureInitialized() {
|
|
6904
|
-
if (this.appId)
|
|
6905
|
-
return;
|
|
6906
|
-
if (!this.initPromise)
|
|
6907
|
-
this.initPromise = this.init();
|
|
6908
|
-
await this.initPromise;
|
|
6909
|
-
}
|
|
6910
|
-
async ensureCurrentAuth() {
|
|
6911
|
-
await this.ensureInitialized();
|
|
6912
|
-
const prevPrincipal = this.authPrincipalKey;
|
|
6913
|
-
await this.refreshToken();
|
|
6914
|
-
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6915
|
-
await this.applyAuthPrincipalChange();
|
|
6916
|
-
}
|
|
6917
|
-
}
|
|
6918
|
-
rekeySubscriptionsForPrincipal() {
|
|
6919
|
-
const subs = Array.from(this.subscriptions.values());
|
|
6920
|
-
this.subscriptions.clear();
|
|
6921
|
-
for (const sub of subs) {
|
|
6922
|
-
this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
|
|
6923
|
-
}
|
|
6924
|
-
}
|
|
6925
|
-
async applyAuthPrincipalChange() {
|
|
6926
|
-
if (this.idbFlushTimer) {
|
|
6927
|
-
clearTimeout(this.idbFlushTimer);
|
|
6928
|
-
this.idbFlushTimer = null;
|
|
6929
|
-
}
|
|
6930
|
-
this.idbDirtyKeys.clear();
|
|
6931
|
-
this.rekeySubscriptionsForPrincipal();
|
|
6932
|
-
for (const sub of this.subscriptions.values()) {
|
|
6933
|
-
sub.docs.clear();
|
|
6934
|
-
sub.ref.current = sub.docs;
|
|
6935
|
-
sub.error = null;
|
|
6936
|
-
sub.isStale = false;
|
|
6937
|
-
let loaded = false;
|
|
6938
|
-
if (sub.tier !== 'ephemeral') {
|
|
6939
|
-
const cached = await idbGet(this.idbKey(sub.path));
|
|
6940
|
-
if (cached && Array.isArray(cached)) {
|
|
6941
|
-
for (const doc of cached) {
|
|
6942
|
-
if (doc && doc._id)
|
|
6943
|
-
sub.docs.set(doc._id, doc);
|
|
6944
|
-
}
|
|
6945
|
-
sub.ref.current = sub.docs;
|
|
6946
|
-
loaded = sub.docs.size > 0;
|
|
6947
|
-
}
|
|
6948
|
-
}
|
|
6949
|
-
sub.status = loaded ? 'cached' : 'loading';
|
|
6950
|
-
sub.isStale = loaded;
|
|
6951
|
-
if (loaded)
|
|
6952
|
-
this.notifySubscription(sub);
|
|
6953
|
-
else
|
|
6954
|
-
this.notifyState(sub);
|
|
6955
|
-
}
|
|
6956
|
-
if (this.ws) {
|
|
6957
|
-
const ws = this.ws;
|
|
6958
|
-
this.ws = null;
|
|
6959
|
-
this.connectPromise = null;
|
|
6960
|
-
this.suppressNextReconnect = true;
|
|
6961
|
-
try {
|
|
6962
|
-
ws.close(1000, 'Auth changed');
|
|
6963
|
-
}
|
|
6964
|
-
catch ( /* ignore */_a) { /* ignore */ }
|
|
6965
|
-
}
|
|
6966
|
-
}
|
|
6967
|
-
async ensureConnected() {
|
|
6968
|
-
var _a;
|
|
6969
|
-
await this.ensureCurrentAuth();
|
|
6970
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
|
|
6971
|
-
return;
|
|
6972
|
-
if (this.connectPromise)
|
|
6973
|
-
return this.connectPromise;
|
|
6974
|
-
this.connectPromise = this.connect();
|
|
6975
|
-
return this.connectPromise;
|
|
6976
|
-
}
|
|
6977
|
-
connect() {
|
|
6978
|
-
return new Promise((resolve, reject) => {
|
|
6979
|
-
if (this.closed) {
|
|
6980
|
-
reject(new Error('Store closed'));
|
|
6981
|
-
return;
|
|
6982
|
-
}
|
|
6983
|
-
const params = new URLSearchParams();
|
|
6984
|
-
params.set('apiKey', this.appId);
|
|
6985
|
-
const url = `${this.wsUrl}?${params.toString()}`;
|
|
6986
|
-
const ws = new WebSocket(url);
|
|
6987
|
-
this.ws = ws;
|
|
6988
|
-
let authTimer = null;
|
|
6989
|
-
const finishConnected = () => {
|
|
6990
|
-
if (authTimer) {
|
|
6991
|
-
clearTimeout(authTimer);
|
|
6992
|
-
authTimer = null;
|
|
6993
|
-
}
|
|
6994
|
-
this.authenticating = false;
|
|
6995
|
-
ws.removeEventListener('error', onError);
|
|
6996
|
-
this.reconnectDelay = 1000;
|
|
6997
|
-
this.connectPromise = null;
|
|
6998
|
-
this.resubscribeAll();
|
|
6999
|
-
resolve();
|
|
7000
|
-
};
|
|
7001
|
-
const onOpen = () => {
|
|
7002
|
-
if (!this.authToken) {
|
|
7003
|
-
finishConnected();
|
|
7004
|
-
return;
|
|
7005
|
-
}
|
|
7006
|
-
this.authenticating = true;
|
|
7007
|
-
authTimer = setTimeout(() => {
|
|
7008
|
-
this.authenticating = false;
|
|
7009
|
-
this.connectPromise = null;
|
|
7010
|
-
try {
|
|
7011
|
-
ws.close(1008, 'Authentication timeout');
|
|
7012
|
-
}
|
|
7013
|
-
catch ( /* ignore */_a) { /* ignore */ }
|
|
7014
|
-
reject(new Error('WebSocket authentication timeout'));
|
|
7015
|
-
}, 10000);
|
|
7016
|
-
try {
|
|
7017
|
-
ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));
|
|
7018
|
-
}
|
|
7019
|
-
catch (e) {
|
|
7020
|
-
if (authTimer)
|
|
7021
|
-
clearTimeout(authTimer);
|
|
7022
|
-
this.authenticating = false;
|
|
7023
|
-
this.connectPromise = null;
|
|
7024
|
-
reject(e);
|
|
7025
|
-
}
|
|
7026
|
-
};
|
|
7027
|
-
const onError = (e) => {
|
|
7028
|
-
if (authTimer)
|
|
7029
|
-
clearTimeout(authTimer);
|
|
7030
|
-
this.authenticating = false;
|
|
7031
|
-
ws.removeEventListener('open', onOpen);
|
|
7032
|
-
this.connectPromise = null;
|
|
7033
|
-
reject(new Error('WebSocket connection failed'));
|
|
7034
|
-
};
|
|
7035
|
-
ws.addEventListener('open', onOpen, { once: true });
|
|
7036
|
-
ws.addEventListener('error', onError, { once: true });
|
|
7037
|
-
ws.addEventListener('message', (event) => {
|
|
7038
|
-
if (this.authenticating) {
|
|
7039
|
-
try {
|
|
7040
|
-
const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));
|
|
7041
|
-
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {
|
|
7042
|
-
finishConnected();
|
|
7043
|
-
return;
|
|
7044
|
-
}
|
|
7045
|
-
}
|
|
7046
|
-
catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }
|
|
7047
|
-
}
|
|
7048
|
-
this.handleMessage(event.data);
|
|
7049
|
-
});
|
|
7050
|
-
ws.addEventListener('close', () => {
|
|
7051
|
-
if (authTimer)
|
|
7052
|
-
clearTimeout(authTimer);
|
|
7053
|
-
if (this.ws !== ws) {
|
|
7054
|
-
if (this.suppressNextReconnect)
|
|
7055
|
-
this.suppressNextReconnect = false;
|
|
7056
|
-
return;
|
|
7057
|
-
}
|
|
7058
|
-
this.authenticating = false;
|
|
7059
|
-
this.ws = null;
|
|
7060
|
-
this.connectPromise = null;
|
|
7061
|
-
this.rejectAllPending('WebSocket closed');
|
|
7062
|
-
this.setAllSubscriptionStatus('reconnecting');
|
|
7063
|
-
if (this.suppressNextReconnect) {
|
|
7064
|
-
this.suppressNextReconnect = false;
|
|
7065
|
-
return;
|
|
7066
|
-
}
|
|
7067
|
-
this.scheduleReconnect();
|
|
7068
|
-
});
|
|
7069
|
-
});
|
|
7070
|
-
}
|
|
7071
|
-
scheduleReconnect() {
|
|
7072
|
-
if (this.closed)
|
|
7073
|
-
return;
|
|
7074
|
-
if (this.reconnectTimer)
|
|
7075
|
-
clearTimeout(this.reconnectTimer);
|
|
7076
|
-
this.reconnectTimer = setTimeout(() => {
|
|
7077
|
-
this.ensureConnected().catch(() => {
|
|
7078
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
7079
|
-
this.scheduleReconnect();
|
|
7080
|
-
});
|
|
7081
|
-
}, this.reconnectDelay);
|
|
7082
|
-
}
|
|
7083
|
-
resubscribeAll() {
|
|
7084
|
-
for (const sub of this.subscriptions.values()) {
|
|
7085
|
-
this.sendSubscribe(sub);
|
|
7086
|
-
}
|
|
7087
|
-
}
|
|
7088
|
-
// -----------------------------------------------------------------------
|
|
7089
|
-
// Message handling
|
|
7090
|
-
// -----------------------------------------------------------------------
|
|
7091
|
-
handleMessage(raw) {
|
|
7092
|
-
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
7093
|
-
let msg;
|
|
7094
|
-
try {
|
|
7095
|
-
msg = JSON.parse(text);
|
|
7096
|
-
}
|
|
7097
|
-
catch (_a) {
|
|
7098
|
-
return;
|
|
7099
|
-
}
|
|
7100
|
-
switch (msg.type) {
|
|
7101
|
-
case 'snapshot':
|
|
7102
|
-
this.handleSnapshot(msg);
|
|
7103
|
-
break;
|
|
7104
|
-
case 'delta':
|
|
7105
|
-
this.handleDelta(msg);
|
|
7106
|
-
break;
|
|
7107
|
-
case 'result':
|
|
7108
|
-
this.handleResult(msg);
|
|
7109
|
-
break;
|
|
7110
|
-
case 'error':
|
|
7111
|
-
this.handleError(msg);
|
|
7112
|
-
break;
|
|
7113
|
-
case 'pong':
|
|
7114
|
-
break;
|
|
7115
|
-
case 'authenticated':
|
|
7116
|
-
break;
|
|
7117
|
-
// v1 compat: handle legacy message types during transition
|
|
7118
|
-
case 'subscribed':
|
|
7119
|
-
this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
|
|
7120
|
-
break;
|
|
7121
|
-
case 'data':
|
|
7122
|
-
// Legacy full-snapshot delta — treat as snapshot replacement
|
|
7123
|
-
this.handleLegacyData(msg);
|
|
7124
|
-
break;
|
|
7125
|
-
case 'response':
|
|
7126
|
-
this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));
|
|
7127
|
-
break;
|
|
7128
|
-
}
|
|
7129
|
-
}
|
|
7130
|
-
handleSnapshot(msg) {
|
|
7131
|
-
var _a, _b, _c;
|
|
7132
|
-
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
7133
|
-
if (!subId)
|
|
7134
|
-
return;
|
|
7135
|
-
const sub = this.findSubscriptionById(subId);
|
|
7136
|
-
if (!sub)
|
|
7137
|
-
return;
|
|
7138
|
-
const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];
|
|
7139
|
-
const docsArray = Array.isArray(docs) ? docs : [docs];
|
|
7140
|
-
sub.docs.clear();
|
|
7141
|
-
for (const doc of docsArray) {
|
|
7142
|
-
if (doc && doc._id) {
|
|
7143
|
-
sub.docs.set(doc._id, doc);
|
|
7144
|
-
}
|
|
7145
|
-
}
|
|
7146
|
-
sub.ref.current = sub.docs;
|
|
7147
|
-
sub.status = 'live';
|
|
7148
|
-
sub.isStale = false;
|
|
7149
|
-
sub.error = null;
|
|
7150
|
-
this.notifySubscription(sub);
|
|
7151
|
-
this.markIdbDirty(sub.path);
|
|
7152
|
-
}
|
|
7153
|
-
handleDelta(msg) {
|
|
7154
|
-
var _a, _b;
|
|
7155
|
-
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
7156
|
-
if (!subId)
|
|
7157
|
-
return;
|
|
7158
|
-
const sub = this.findSubscriptionById(subId);
|
|
7159
|
-
if (!sub)
|
|
7160
|
-
return;
|
|
7161
|
-
if (sub.tier === 'ephemeral') {
|
|
7162
|
-
// Ephemeral: just overwrite, no accumulation logic
|
|
7163
|
-
if (msg.change === 'removed' && msg.docId) {
|
|
7164
|
-
sub.docs.delete(msg.docId);
|
|
7165
|
-
}
|
|
7166
|
-
else if (msg.doc && msg.doc._id) {
|
|
7167
|
-
sub.docs.set(msg.doc._id, msg.doc);
|
|
7168
|
-
}
|
|
7169
|
-
sub.ref.current = sub.docs;
|
|
7170
|
-
if (sub.options.mode !== 'ref') {
|
|
7171
|
-
this.notifySubscription(sub);
|
|
7172
|
-
}
|
|
7173
|
-
return;
|
|
7174
|
-
}
|
|
7175
|
-
// Durable/checkpointed: full delta handling
|
|
7176
|
-
switch (msg.change) {
|
|
7177
|
-
case 'added':
|
|
7178
|
-
case 'modified':
|
|
7179
|
-
if (msg.doc && msg.doc._id) {
|
|
7180
|
-
sub.docs.set(msg.doc._id, msg.doc);
|
|
7181
|
-
}
|
|
7182
|
-
break;
|
|
7183
|
-
case 'removed':
|
|
7184
|
-
if (msg.docId) {
|
|
7185
|
-
sub.docs.delete(msg.docId);
|
|
7186
|
-
}
|
|
7187
|
-
else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {
|
|
7188
|
-
sub.docs.delete(msg.doc._id);
|
|
7189
|
-
}
|
|
7190
|
-
break;
|
|
7191
|
-
}
|
|
7192
|
-
sub.ref.current = sub.docs;
|
|
7193
|
-
this.notifySubscription(sub);
|
|
7194
|
-
this.markIdbDirty(sub.path);
|
|
7195
|
-
}
|
|
7196
|
-
handleLegacyData(msg) {
|
|
7197
|
-
// Legacy v1 format: 'data' message with full snapshot or single doc
|
|
7198
|
-
const subId = msg.subscriptionId;
|
|
7199
|
-
if (!subId)
|
|
7200
|
-
return;
|
|
7201
|
-
const sub = this.findSubscriptionById(subId);
|
|
7202
|
-
if (!sub)
|
|
7203
|
-
return;
|
|
7204
|
-
if (Array.isArray(msg.data)) {
|
|
7205
|
-
// Full snapshot replacement
|
|
7206
|
-
sub.docs.clear();
|
|
7207
|
-
for (const doc of msg.data) {
|
|
7208
|
-
if (doc && doc._id)
|
|
7209
|
-
sub.docs.set(doc._id, doc);
|
|
7210
|
-
}
|
|
7211
|
-
}
|
|
7212
|
-
else if (msg.data && msg.data._id) {
|
|
7213
|
-
// Single doc update
|
|
7214
|
-
sub.docs.set(msg.data._id, msg.data);
|
|
7215
|
-
}
|
|
7216
|
-
else if (msg.data === null) ;
|
|
7217
|
-
sub.ref.current = sub.docs;
|
|
7218
|
-
sub.status = 'live';
|
|
7219
|
-
sub.isStale = false;
|
|
7220
|
-
this.notifySubscription(sub);
|
|
7221
|
-
this.markIdbDirty(sub.path);
|
|
7222
|
-
}
|
|
7223
|
-
handleResult(msg) {
|
|
7224
|
-
var _a, _b, _c, _d;
|
|
7225
|
-
const requestId = msg.requestId;
|
|
7226
|
-
if (!requestId)
|
|
7227
|
-
return;
|
|
7228
|
-
const pending = this.pendingRequests.get(requestId);
|
|
7229
|
-
if (!pending)
|
|
7230
|
-
return;
|
|
7231
|
-
this.pendingRequests.delete(requestId);
|
|
7232
|
-
clearTimeout(pending.timeout);
|
|
7233
|
-
const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);
|
|
7234
|
-
if (ok) {
|
|
7235
|
-
pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);
|
|
7236
|
-
}
|
|
7237
|
-
else {
|
|
7238
|
-
pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));
|
|
7239
|
-
}
|
|
7240
|
-
}
|
|
7241
|
-
handleError(msg) {
|
|
7242
|
-
var _a, _b, _c;
|
|
7243
|
-
const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
|
|
7244
|
-
if (msg.code)
|
|
7245
|
-
error.code = msg.code;
|
|
7246
|
-
if (msg.subscriptionId || msg.id)
|
|
7247
|
-
error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
|
|
7248
|
-
const requestId = msg.requestId;
|
|
7249
|
-
if (requestId) {
|
|
7250
|
-
const pending = this.pendingRequests.get(requestId);
|
|
7251
|
-
if (pending) {
|
|
7252
|
-
this.pendingRequests.delete(requestId);
|
|
7253
|
-
clearTimeout(pending.timeout);
|
|
7254
|
-
pending.reject(error);
|
|
7255
|
-
}
|
|
7256
|
-
}
|
|
7257
|
-
const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
|
|
7258
|
-
if (subId) {
|
|
7259
|
-
const sub = this.findSubscriptionById(subId);
|
|
7260
|
-
if (sub) {
|
|
7261
|
-
sub.status = 'error';
|
|
7262
|
-
sub.error = error;
|
|
7263
|
-
this.notifyState(sub);
|
|
7264
|
-
for (const callback of Array.from(sub.errorCallbacks)) {
|
|
7265
|
-
try {
|
|
7266
|
-
callback(error);
|
|
7267
|
-
}
|
|
7268
|
-
catch ( /* swallow */_d) { /* swallow */ }
|
|
7269
|
-
}
|
|
7270
|
-
}
|
|
7271
|
-
}
|
|
7272
|
-
}
|
|
7273
|
-
// -----------------------------------------------------------------------
|
|
7274
|
-
// Subscribe
|
|
7275
|
-
// -----------------------------------------------------------------------
|
|
7276
|
-
async subscribe(path, opts = {}) {
|
|
7277
|
-
var _a;
|
|
7278
|
-
await this.ensureCurrentAuth();
|
|
7279
|
-
const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
|
|
7280
|
-
const subKey = this.getSubKey(path, opts);
|
|
7281
|
-
let sub = this.subscriptions.get(subKey);
|
|
7282
|
-
if (sub) {
|
|
7283
|
-
// Existing subscription — add callback
|
|
7284
|
-
if (opts.onData)
|
|
7285
|
-
sub.callbacks.add(opts.onData);
|
|
7286
|
-
if (opts.onState)
|
|
7287
|
-
sub.stateCallbacks.add(opts.onState);
|
|
7288
|
-
if (opts.onError)
|
|
7289
|
-
sub.errorCallbacks.add(opts.onError);
|
|
7290
|
-
// Immediately deliver current state
|
|
7291
|
-
if (opts.onData && sub.docs.size > 0) {
|
|
7292
|
-
opts.onData(this.docsToArray(sub));
|
|
7293
|
-
}
|
|
7294
|
-
if (opts.onState) {
|
|
7295
|
-
opts.onState(this.getState(sub));
|
|
7296
|
-
}
|
|
7297
|
-
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7298
|
-
}
|
|
7299
|
-
// New subscription
|
|
7300
|
-
const subId = `sub_${nextRequestId++}`;
|
|
7301
|
-
sub = {
|
|
7302
|
-
id: subId,
|
|
7303
|
-
path,
|
|
7304
|
-
tier,
|
|
7305
|
-
options: opts,
|
|
7306
|
-
docs: new Map(),
|
|
7307
|
-
status: 'idle',
|
|
7308
|
-
isStale: false,
|
|
7309
|
-
error: null,
|
|
7310
|
-
callbacks: new Set(opts.onData ? [opts.onData] : []),
|
|
7311
|
-
stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
|
|
7312
|
-
errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
|
|
7313
|
-
ref: { current: new Map() },
|
|
7314
|
-
};
|
|
7315
|
-
this.subscriptions.set(subKey, sub);
|
|
7316
|
-
// Step 1: Load from IDB (durable/checkpointed only)
|
|
7317
|
-
if (tier !== 'ephemeral') {
|
|
7318
|
-
const cached = await idbGet(this.idbKey(path));
|
|
7319
|
-
if (cached && Array.isArray(cached)) {
|
|
7320
|
-
for (const doc of cached) {
|
|
7321
|
-
if (doc && doc._id)
|
|
7322
|
-
sub.docs.set(doc._id, doc);
|
|
7323
|
-
}
|
|
7324
|
-
sub.ref.current = sub.docs;
|
|
7325
|
-
sub.status = 'cached';
|
|
7326
|
-
sub.isStale = true;
|
|
7327
|
-
this.notifySubscription(sub);
|
|
7328
|
-
}
|
|
7329
|
-
}
|
|
7330
|
-
// Step 2: Connect and subscribe via WS
|
|
7331
|
-
sub.status = sub.docs.size > 0 ? 'cached' : 'loading';
|
|
7332
|
-
this.notifyState(sub);
|
|
7333
|
-
try {
|
|
7334
|
-
await this.ensureConnected();
|
|
7335
|
-
this.sendSubscribe(sub);
|
|
7336
|
-
}
|
|
7337
|
-
catch (_b) {
|
|
7338
|
-
sub.status = 'error';
|
|
7339
|
-
sub.error = new Error('Connection failed');
|
|
7340
|
-
this.notifyState(sub);
|
|
7341
|
-
}
|
|
7342
|
-
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7343
|
-
}
|
|
7344
|
-
getRef(path, opts = {}) {
|
|
7345
|
-
var _a;
|
|
7346
|
-
const subKey = this.getSubKey(path, opts);
|
|
7347
|
-
const sub = this.subscriptions.get(subKey);
|
|
7348
|
-
if (sub)
|
|
7349
|
-
return sub.ref;
|
|
7350
|
-
// Auto-subscribe in ref mode
|
|
7351
|
-
const ref = { current: new Map() };
|
|
7352
|
-
this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });
|
|
7353
|
-
const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));
|
|
7354
|
-
return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;
|
|
7355
|
-
}
|
|
7356
|
-
// -----------------------------------------------------------------------
|
|
7357
|
-
// CRUD operations
|
|
7358
|
-
// -----------------------------------------------------------------------
|
|
7359
|
-
async set(path, doc) {
|
|
7360
|
-
var _a;
|
|
7361
|
-
await this.ensureConnected();
|
|
7362
|
-
// Resolve operations (Increment, Time.Now) client-side for optimistic update
|
|
7363
|
-
const resolvedDoc = this.resolveOperations(doc, path);
|
|
7364
|
-
// Optimistic update: apply to local state immediately
|
|
7365
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7366
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7367
|
-
const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), {
|
|
7368
|
-
// System timestamp field name: the Bounded worker stamps the neutral
|
|
7369
|
-
// `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.
|
|
7370
|
-
// Match it so the optimistic doc lines up with the server's confirmation.
|
|
7371
|
-
[isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });
|
|
7372
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7373
|
-
let prevDoc = null;
|
|
7374
|
-
if (sub) {
|
|
7375
|
-
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
7376
|
-
sub.docs.set(normalizedPath, optimisticDoc);
|
|
7377
|
-
sub.ref.current = sub.docs;
|
|
7378
|
-
this.notifySubscription(sub);
|
|
7379
|
-
}
|
|
7380
|
-
// Send to server
|
|
7381
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7382
|
-
try {
|
|
7383
|
-
const result = await this.sendRequest(requestId, {
|
|
7384
|
-
type: 'set',
|
|
7385
|
-
requestId,
|
|
7386
|
-
documents: [{ destinationPath: normalizedPath, document: doc }],
|
|
7387
|
-
});
|
|
7388
|
-
// Replace optimistic doc with server-confirmed version
|
|
7389
|
-
if (sub && result && typeof result === 'object') {
|
|
7390
|
-
const serverDoc = Array.isArray(result) ? result[0] : result;
|
|
7391
|
-
if (serverDoc && serverDoc._id) {
|
|
7392
|
-
sub.docs.set(serverDoc._id, serverDoc);
|
|
7393
|
-
sub.ref.current = sub.docs;
|
|
7394
|
-
this.notifySubscription(sub);
|
|
7395
|
-
this.markIdbDirty(collectionPath);
|
|
7396
|
-
}
|
|
7397
|
-
}
|
|
7398
|
-
return Array.isArray(result) ? result[0] : result;
|
|
7399
|
-
}
|
|
7400
|
-
catch (err) {
|
|
7401
|
-
// Revert optimistic update
|
|
7402
|
-
if (sub) {
|
|
7403
|
-
if (prevDoc) {
|
|
7404
|
-
sub.docs.set(normalizedPath, prevDoc);
|
|
7405
|
-
}
|
|
7406
|
-
else {
|
|
7407
|
-
sub.docs.delete(normalizedPath);
|
|
7408
|
-
}
|
|
7409
|
-
sub.ref.current = sub.docs;
|
|
7410
|
-
this.notifySubscription(sub);
|
|
7411
|
-
}
|
|
7412
|
-
throw err;
|
|
7413
|
-
}
|
|
7414
|
-
}
|
|
7415
|
-
async get(path) {
|
|
7416
|
-
await this.ensureCurrentAuth();
|
|
7417
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7418
|
-
// Check local subscriptions first
|
|
7419
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7420
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7421
|
-
if (sub && sub.status === 'live') {
|
|
7422
|
-
const doc = sub.docs.get(normalizedPath);
|
|
7423
|
-
return doc !== null && doc !== void 0 ? doc : null;
|
|
7424
|
-
}
|
|
7425
|
-
// One-shot WS fetch
|
|
7426
|
-
await this.ensureConnected();
|
|
7427
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7428
|
-
return this.sendRequest(requestId, {
|
|
7429
|
-
type: 'get',
|
|
7430
|
-
requestId,
|
|
7431
|
-
path: normalizedPath,
|
|
7432
|
-
});
|
|
7433
|
-
}
|
|
7434
|
-
async getMany(paths) {
|
|
7435
|
-
await this.ensureConnected();
|
|
7436
|
-
const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);
|
|
7437
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7438
|
-
return this.sendRequest(requestId, {
|
|
7439
|
-
type: 'getMany',
|
|
7440
|
-
requestId,
|
|
7441
|
-
paths: normalizedPaths,
|
|
7442
|
-
});
|
|
7443
|
-
}
|
|
7444
|
-
async delete(path) {
|
|
7445
|
-
var _a;
|
|
7446
|
-
await this.ensureConnected();
|
|
7447
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7448
|
-
// Optimistic: remove from local state
|
|
7449
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7450
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7451
|
-
let prevDoc = null;
|
|
7452
|
-
if (sub) {
|
|
7453
|
-
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
7454
|
-
sub.docs.delete(normalizedPath);
|
|
7455
|
-
sub.ref.current = sub.docs;
|
|
7456
|
-
this.notifySubscription(sub);
|
|
7457
|
-
}
|
|
7458
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7459
|
-
try {
|
|
7460
|
-
await this.sendRequest(requestId, {
|
|
7461
|
-
type: 'delete',
|
|
7462
|
-
requestId,
|
|
7463
|
-
path: normalizedPath,
|
|
7464
|
-
});
|
|
7465
|
-
if (sub)
|
|
7466
|
-
this.markIdbDirty(collectionPath);
|
|
7467
|
-
}
|
|
7468
|
-
catch (err) {
|
|
7469
|
-
// Revert
|
|
7470
|
-
if (sub && prevDoc) {
|
|
7471
|
-
sub.docs.set(normalizedPath, prevDoc);
|
|
7472
|
-
sub.ref.current = sub.docs;
|
|
7473
|
-
this.notifySubscription(sub);
|
|
7474
|
-
}
|
|
7475
|
-
throw err;
|
|
7476
|
-
}
|
|
7477
|
-
}
|
|
7478
|
-
async query(path, opts) {
|
|
7479
|
-
await this.ensureConnected();
|
|
7480
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7481
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7482
|
-
return this.sendRequest(requestId, Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId, path: normalizedPath }, ((opts === null || opts === void 0 ? void 0 : opts.filter) ? { filter: opts.filter } : {})), ((opts === null || opts === void 0 ? void 0 : opts.sort) ? { sort: opts.sort } : {})), ((opts === null || opts === void 0 ? void 0 : opts.limit) !== undefined ? { limit: opts.limit } : {})), ((opts === null || opts === void 0 ? void 0 : opts.includeSubPaths) ? { includeSubPaths: true } : {})));
|
|
7483
|
-
}
|
|
7484
|
-
async count(path) {
|
|
7485
|
-
var _a;
|
|
7486
|
-
await this.ensureConnected();
|
|
7487
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7488
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7489
|
-
const result = await this.sendRequest(requestId, {
|
|
7490
|
-
type: 'count',
|
|
7491
|
-
requestId,
|
|
7492
|
-
path: normalizedPath,
|
|
7493
|
-
});
|
|
7494
|
-
return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);
|
|
7495
|
-
}
|
|
7496
|
-
async aggregate(path, operation, opts) {
|
|
7497
|
-
await this.ensureConnected();
|
|
7498
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7499
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7500
|
-
return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));
|
|
7501
|
-
}
|
|
7502
|
-
// -----------------------------------------------------------------------
|
|
7503
|
-
// Helpers
|
|
7504
|
-
// -----------------------------------------------------------------------
|
|
7505
|
-
sendSubscribe(sub) {
|
|
7506
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
7507
|
-
return;
|
|
7508
|
-
const msg = {
|
|
7509
|
-
type: 'subscribe',
|
|
7510
|
-
subscriptionId: sub.id,
|
|
7511
|
-
path: sub.path,
|
|
7512
|
-
};
|
|
7513
|
-
if (sub.options.filter)
|
|
7514
|
-
msg.filter = sub.options.filter;
|
|
7515
|
-
if (sub.options.includeSubPaths)
|
|
7516
|
-
msg.includeSubPaths = true;
|
|
7517
|
-
if (sub.options.limit)
|
|
7518
|
-
msg.limit = sub.options.limit;
|
|
7519
|
-
if (sub.options.prompt)
|
|
7520
|
-
msg.prompt = sub.options.prompt;
|
|
7521
|
-
this.ws.send(JSON.stringify(msg));
|
|
7522
|
-
}
|
|
7523
|
-
sendRequest(requestId, msg) {
|
|
7524
|
-
return new Promise((resolve, reject) => {
|
|
7525
|
-
const timeout = setTimeout(() => {
|
|
7526
|
-
this.pendingRequests.delete(requestId);
|
|
7527
|
-
reject(new Error('Request timed out'));
|
|
7528
|
-
}, 30000);
|
|
7529
|
-
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
7530
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7531
|
-
this.ws.send(JSON.stringify(msg));
|
|
7532
|
-
}
|
|
7533
|
-
else {
|
|
7534
|
-
this.pendingRequests.delete(requestId);
|
|
7535
|
-
clearTimeout(timeout);
|
|
7536
|
-
reject(new Error('WebSocket not connected'));
|
|
7537
|
-
}
|
|
7538
|
-
});
|
|
7539
|
-
}
|
|
7540
|
-
notifySubscription(sub) {
|
|
7541
|
-
const data = this.docsToArray(sub);
|
|
7542
|
-
const callbacks = Array.from(sub.callbacks);
|
|
7543
|
-
for (const cb of callbacks) {
|
|
7544
|
-
try {
|
|
7545
|
-
cb(data);
|
|
7546
|
-
}
|
|
7547
|
-
catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }
|
|
7548
|
-
}
|
|
7549
|
-
this.notifyState(sub);
|
|
7550
|
-
}
|
|
7551
|
-
notifyState(sub) {
|
|
7552
|
-
const state = this.getState(sub);
|
|
7553
|
-
const callbacks = Array.from(sub.stateCallbacks);
|
|
7554
|
-
for (const cb of callbacks) {
|
|
7555
|
-
try {
|
|
7556
|
-
cb(state);
|
|
7557
|
-
}
|
|
7558
|
-
catch ( /* swallow */_a) { /* swallow */ }
|
|
7559
|
-
}
|
|
7560
|
-
}
|
|
7561
|
-
getState(sub) {
|
|
7562
|
-
return {
|
|
7563
|
-
data: this.docsToArray(sub),
|
|
7564
|
-
status: sub.status,
|
|
7565
|
-
isStale: sub.isStale,
|
|
7566
|
-
error: sub.error,
|
|
7567
|
-
};
|
|
7568
|
-
}
|
|
7569
|
-
docsToArray(sub) {
|
|
7570
|
-
return Array.from(sub.docs.values());
|
|
7571
|
-
}
|
|
7572
|
-
findSubscriptionById(id) {
|
|
7573
|
-
for (const sub of this.subscriptions.values()) {
|
|
7574
|
-
if (sub.id === id)
|
|
7575
|
-
return sub;
|
|
7576
|
-
}
|
|
7577
|
-
return undefined;
|
|
7578
|
-
}
|
|
7579
|
-
findSubscriptionByPath(collectionPath) {
|
|
7580
|
-
for (const sub of this.subscriptions.values()) {
|
|
7581
|
-
const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;
|
|
7582
|
-
if (subPath === collectionPath)
|
|
7583
|
-
return sub;
|
|
7584
|
-
if (collectionPath.startsWith(subPath + '/'))
|
|
7585
|
-
return sub;
|
|
7586
|
-
}
|
|
7587
|
-
return undefined;
|
|
7588
|
-
}
|
|
7589
|
-
getCollectionPath(docPath) {
|
|
7590
|
-
const segments = docPath.split('/');
|
|
7591
|
-
if (segments.length % 2 === 0) {
|
|
7592
|
-
return segments.slice(0, -1).join('/');
|
|
7593
|
-
}
|
|
7594
|
-
return docPath;
|
|
7595
|
-
}
|
|
7596
|
-
getSubKey(path, opts) {
|
|
7597
|
-
const parts = [this.appId, this.authPrincipalKey, path];
|
|
7598
|
-
if (opts.filter)
|
|
7599
|
-
parts.push(JSON.stringify(opts.filter));
|
|
7600
|
-
if (opts.prompt)
|
|
7601
|
-
parts.push(opts.prompt);
|
|
7602
|
-
if (opts.tier)
|
|
7603
|
-
parts.push(opts.tier);
|
|
7604
|
-
return parts.join('::');
|
|
7605
|
-
}
|
|
7606
|
-
idbKey(path) {
|
|
7607
|
-
return `${this.appId}:${this.authPrincipalKey}:${path}`;
|
|
7608
|
-
}
|
|
7609
|
-
markIdbDirty(path) {
|
|
7610
|
-
const sub = this.findSubscriptionByPath(path);
|
|
7611
|
-
if (sub && sub.tier === 'ephemeral')
|
|
7612
|
-
return;
|
|
7613
|
-
this.idbDirtyKeys.add(path);
|
|
7614
|
-
if (!this.idbFlushTimer) {
|
|
7615
|
-
this.idbFlushTimer = setTimeout(() => {
|
|
7616
|
-
this.flushIdb();
|
|
7617
|
-
this.idbFlushTimer = null;
|
|
7618
|
-
}, 500);
|
|
7619
|
-
}
|
|
7620
|
-
}
|
|
7621
|
-
async flushIdb() {
|
|
7622
|
-
const keys = Array.from(this.idbDirtyKeys);
|
|
7623
|
-
this.idbDirtyKeys.clear();
|
|
7624
|
-
for (const path of keys) {
|
|
7625
|
-
const sub = this.findSubscriptionByPath(path);
|
|
7626
|
-
if (sub && sub.tier !== 'ephemeral') {
|
|
7627
|
-
const docs = this.docsToArray(sub);
|
|
7628
|
-
await idbSet(this.idbKey(path), docs);
|
|
7629
|
-
}
|
|
7630
|
-
}
|
|
7631
|
-
}
|
|
7632
|
-
createUnsubscribe(subKey, subId, onData, onState, onError) {
|
|
7633
|
-
return async () => {
|
|
7634
|
-
var _a;
|
|
7635
|
-
const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
|
|
7636
|
-
if (!sub)
|
|
7637
|
-
return;
|
|
7638
|
-
const currentSubKey = this.getSubKey(sub.path, sub.options);
|
|
7639
|
-
if (onData)
|
|
7640
|
-
sub.callbacks.delete(onData);
|
|
7641
|
-
if (onState)
|
|
7642
|
-
sub.stateCallbacks.delete(onState);
|
|
7643
|
-
if (onError)
|
|
7644
|
-
sub.errorCallbacks.delete(onError);
|
|
7645
|
-
// If no more callbacks, unsubscribe entirely
|
|
7646
|
-
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
|
|
7647
|
-
this.subscriptions.delete(subKey);
|
|
7648
|
-
this.subscriptions.delete(currentSubKey);
|
|
7649
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7650
|
-
this.ws.send(JSON.stringify({
|
|
7651
|
-
type: 'unsubscribe',
|
|
7652
|
-
subscriptionId: sub.id,
|
|
7653
|
-
}));
|
|
7654
|
-
}
|
|
7655
|
-
}
|
|
7656
|
-
};
|
|
7657
|
-
}
|
|
7658
|
-
resolveOperations(doc, path) {
|
|
7659
|
-
var _a;
|
|
7660
|
-
if (!doc || typeof doc !== 'object')
|
|
7661
|
-
return doc;
|
|
7662
|
-
const resolved = {};
|
|
7663
|
-
for (const [key, value] of Object.entries(doc)) {
|
|
7664
|
-
if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {
|
|
7665
|
-
const op = value;
|
|
7666
|
-
if (op.operation === 'time' && op.value === 'now') {
|
|
7667
|
-
resolved[key] = Math.floor(Date.now() / 1000);
|
|
7668
|
-
}
|
|
7669
|
-
else if (op.operation === 'increment') {
|
|
7670
|
-
// For optimistic: get current value and add
|
|
7671
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7672
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7673
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7674
|
-
const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);
|
|
7675
|
-
const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;
|
|
7676
|
-
resolved[key] = (typeof current === 'number' ? current : 0) + op.value;
|
|
7677
|
-
}
|
|
7678
|
-
else {
|
|
7679
|
-
resolved[key] = value;
|
|
7680
|
-
}
|
|
7681
|
-
}
|
|
7682
|
-
else {
|
|
7683
|
-
resolved[key] = value;
|
|
7684
|
-
}
|
|
7685
|
-
}
|
|
7686
|
-
return resolved;
|
|
7687
|
-
}
|
|
7688
|
-
rejectAllPending(reason) {
|
|
7689
|
-
for (const [requestId, pending] of this.pendingRequests) {
|
|
7690
|
-
clearTimeout(pending.timeout);
|
|
7691
|
-
pending.reject(new Error(reason));
|
|
7692
|
-
}
|
|
7693
|
-
this.pendingRequests.clear();
|
|
7694
|
-
}
|
|
7695
|
-
setAllSubscriptionStatus(status) {
|
|
7696
|
-
for (const sub of this.subscriptions.values()) {
|
|
7697
|
-
sub.status = status;
|
|
7698
|
-
this.notifyState(sub);
|
|
7699
|
-
}
|
|
7700
|
-
}
|
|
7701
|
-
// -----------------------------------------------------------------------
|
|
7702
|
-
// Lifecycle
|
|
7703
|
-
// -----------------------------------------------------------------------
|
|
7704
|
-
close() {
|
|
7705
|
-
this.closed = true;
|
|
7706
|
-
if (this.reconnectTimer)
|
|
7707
|
-
clearTimeout(this.reconnectTimer);
|
|
7708
|
-
if (this.idbFlushTimer)
|
|
7709
|
-
clearTimeout(this.idbFlushTimer);
|
|
7710
|
-
if (this.tokenRefreshTimer)
|
|
7711
|
-
clearInterval(this.tokenRefreshTimer);
|
|
7712
|
-
this.flushIdb();
|
|
7713
|
-
if (this.ws) {
|
|
7714
|
-
this.ws.close(1000, 'Store closed');
|
|
7715
|
-
this.ws = null;
|
|
7716
|
-
}
|
|
7717
|
-
this.rejectAllPending('Store closed');
|
|
7718
|
-
this.subscriptions.clear();
|
|
7719
|
-
}
|
|
7720
|
-
async reconnectWithNewAuth() {
|
|
7721
|
-
if (this.closed)
|
|
7722
|
-
return;
|
|
7723
|
-
await this.ensureInitialized();
|
|
7724
|
-
await this.refreshToken();
|
|
7725
|
-
await this.applyAuthPrincipalChange();
|
|
7726
|
-
if (this.subscriptions.size > 0) {
|
|
7727
|
-
await this.ensureConnected().catch((error) => {
|
|
7728
|
-
this.setAllSubscriptionStatus('error');
|
|
7729
|
-
for (const sub of this.subscriptions.values()) {
|
|
7730
|
-
sub.error = error instanceof Error ? error : new Error(String(error));
|
|
7731
|
-
this.notifyState(sub);
|
|
7732
|
-
}
|
|
7733
|
-
});
|
|
7734
|
-
}
|
|
7735
|
-
}
|
|
7736
|
-
}
|
|
7737
|
-
// ---------------------------------------------------------------------------
|
|
7738
|
-
// Singleton instance
|
|
7739
|
-
// ---------------------------------------------------------------------------
|
|
7740
|
-
let storeInstance = null;
|
|
7741
|
-
function getRealtimeStore() {
|
|
7742
|
-
if (!storeInstance) {
|
|
7743
|
-
storeInstance = new RealtimeStore();
|
|
7744
|
-
}
|
|
7745
|
-
return storeInstance;
|
|
7746
|
-
}
|
|
7747
|
-
function resetRealtimeStore() {
|
|
7748
|
-
if (storeInstance) {
|
|
7749
|
-
storeInstance.close();
|
|
7750
|
-
storeInstance = null;
|
|
7751
|
-
}
|
|
7752
|
-
}
|
|
7753
|
-
async function reconnectRealtimeStoreWithNewAuth() {
|
|
7754
|
-
if (storeInstance) {
|
|
7755
|
-
await storeInstance.reconnectWithNewAuth();
|
|
7756
|
-
}
|
|
7757
|
-
}
|
|
7758
|
-
|
|
7759
|
-
var realtimeStore = /*#__PURE__*/Object.freeze({
|
|
7760
|
-
__proto__: null,
|
|
7761
|
-
RealtimeStore: RealtimeStore,
|
|
7762
|
-
getRealtimeStore: getRealtimeStore,
|
|
7763
|
-
reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
|
|
7764
|
-
resetRealtimeStore: resetRealtimeStore
|
|
7765
|
-
});
|
|
7766
|
-
|
|
7767
6704
|
// ---------------------------------------------------------------------------
|
|
7768
6705
|
// functions.ts -- Bounded Functions client (the imperative escape hatch).
|
|
7769
6706
|
//
|
|
@@ -7819,11 +6756,12 @@ async function invoke(name, args = {}, opts = {}) {
|
|
|
7819
6756
|
const base = (config.functionsUrl || DEFAULT_FUNCTIONS_URL).replace(/\/$/, '');
|
|
7820
6757
|
// Attach the caller's session token automatically (same token as data calls).
|
|
7821
6758
|
// A wallet-scoped call (server WalletClient.invoke) resolves the token from the
|
|
7822
|
-
// wallet's own session;
|
|
6759
|
+
// wallet's own session; browser calls use the active web session. Top-level
|
|
6760
|
+
// server calls fail closed because there is no ambient server signer.
|
|
7823
6761
|
const authHeader = ((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders)
|
|
7824
6762
|
? await opts._overrides._getAuthHeaders()
|
|
7825
6763
|
: await createAuthHeader(config.isServer);
|
|
7826
|
-
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId
|
|
6764
|
+
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId }, ((_b = stripAuthHeaders(opts.headers)) !== null && _b !== void 0 ? _b : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
|
|
7827
6765
|
const controller = new AbortController();
|
|
7828
6766
|
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
7829
6767
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -7892,8 +6830,8 @@ const functions = { invoke };
|
|
|
7892
6830
|
// Subscribing to your view: a per-player view doc lives at
|
|
7893
6831
|
// `<roomPath>/view/<myUserId>` (the policy declares
|
|
7894
6832
|
// `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
|
|
7895
|
-
//
|
|
7896
|
-
//
|
|
6833
|
+
// View paths key only by @user.id; wallet-address aliases are intentionally not
|
|
6834
|
+
// accepted by this helper.
|
|
7897
6835
|
// ---------------------------------------------------------------------------
|
|
7898
6836
|
class LiveIntentError extends Error {
|
|
7899
6837
|
constructor(message, statusCode, details) {
|
|
@@ -7987,7 +6925,7 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7987
6925
|
const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
|
|
7988
6926
|
const buildHeaders = async () => {
|
|
7989
6927
|
const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
|
|
7990
|
-
return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId
|
|
6928
|
+
return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
|
|
7991
6929
|
};
|
|
7992
6930
|
const controller = new AbortController();
|
|
7993
6931
|
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
@@ -8048,7 +6986,7 @@ async function status(roomPath, opts = {}) {
|
|
|
8048
6986
|
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
8049
6987
|
const config = await getConfig();
|
|
8050
6988
|
const base = realtimeHttpBase(config.wsApiUrl);
|
|
8051
|
-
const headers = Object.assign({ 'X-App-Id': config.appId
|
|
6989
|
+
const headers = Object.assign({ 'X-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
|
|
8052
6990
|
const controller = new AbortController();
|
|
8053
6991
|
const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
|
|
8054
6992
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -8090,29 +7028,27 @@ async function status(roomPath, opts = {}) {
|
|
|
8090
7028
|
* subscribe('<roomPath>/view/<myUserId>', { onData, onError })
|
|
8091
7029
|
*
|
|
8092
7030
|
* The view id defaults to the logged-in user's @user.id (from the session token
|
|
8093
|
-
* claims); pass `opts.userId` to override.
|
|
8094
|
-
*
|
|
8095
|
-
* function (a Promise<() => Promise<void>>, same as `subscribe`).
|
|
7031
|
+
* claims); pass `opts.userId` to override. Returns the unsubscribe function
|
|
7032
|
+
* (a Promise<() => Promise<void>>, same as `subscribe`).
|
|
8096
7033
|
*
|
|
8097
7034
|
* Note: this is a browser-first helper (the WS subscription manager is
|
|
8098
7035
|
* browser-oriented). Server consumers should use `live.intent`.
|
|
8099
7036
|
*/
|
|
8100
7037
|
async function subscribeView(roomPath, opts) {
|
|
8101
|
-
var _a, _b, _c;
|
|
8102
7038
|
if (!roomPath || typeof roomPath !== 'string') {
|
|
8103
7039
|
throw new LiveIntentError('A room path is required');
|
|
8104
7040
|
}
|
|
8105
7041
|
if (!opts || typeof opts.onData !== 'function') {
|
|
8106
7042
|
throw new LiveIntentError('subscribeView requires an onData callback');
|
|
8107
7043
|
}
|
|
8108
|
-
let viewUserId =
|
|
7044
|
+
let viewUserId = opts.userId;
|
|
8109
7045
|
if (!viewUserId) {
|
|
8110
7046
|
const config = await getConfig();
|
|
8111
7047
|
const info = await getUserInfo(config.isServer);
|
|
8112
7048
|
// getUserInfo returns the RAW idToken payload. The universal live view key
|
|
8113
|
-
// is @user.id (`custom:userId`); wallet-address keyed
|
|
8114
|
-
//
|
|
8115
|
-
viewUserId =
|
|
7049
|
+
// is @user.id (`custom:userId`); wallet-address keyed compatibility aliases
|
|
7050
|
+
// are intentionally not accepted.
|
|
7051
|
+
viewUserId = info === null || info === void 0 ? void 0 : info['custom:userId'];
|
|
8116
7052
|
}
|
|
8117
7053
|
if (!viewUserId || typeof viewUserId !== 'string') {
|
|
8118
7054
|
throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
|
|
@@ -8187,7 +7123,6 @@ exports.FunctionInvokeError = FunctionInvokeError;
|
|
|
8187
7123
|
exports.InsufficientBalanceError = InsufficientBalanceError;
|
|
8188
7124
|
exports.LiveIntentError = LiveIntentError;
|
|
8189
7125
|
exports.ReactNativeSessionManager = ReactNativeSessionManager;
|
|
8190
|
-
exports.RealtimeStore = RealtimeStore;
|
|
8191
7126
|
exports.ServerSessionManager = ServerSessionManager;
|
|
8192
7127
|
exports.WebSessionManager = WebSessionManager;
|
|
8193
7128
|
exports.aggregate = aggregate;
|
|
@@ -8210,7 +7145,6 @@ exports.getConfig = getConfig;
|
|
|
8210
7145
|
exports.getFiles = getFiles;
|
|
8211
7146
|
exports.getIdToken = getIdToken;
|
|
8212
7147
|
exports.getMany = getMany;
|
|
8213
|
-
exports.getRealtimeStore = getRealtimeStore;
|
|
8214
7148
|
exports.getWebhookKeysUrl = getWebhookKeysUrl;
|
|
8215
7149
|
exports.hasActiveConnection = hasActiveConnection;
|
|
8216
7150
|
exports.increment = increment;
|
|
@@ -8224,7 +7158,6 @@ exports.now = now;
|
|
|
8224
7158
|
exports.queryAggregate = queryAggregate;
|
|
8225
7159
|
exports.reconnectWithNewAuth = reconnectWithNewAuth;
|
|
8226
7160
|
exports.refreshSession = refreshSession;
|
|
8227
|
-
exports.resetRealtimeStore = resetRealtimeStore;
|
|
8228
7161
|
exports.revokeSession = revokeSession;
|
|
8229
7162
|
exports.runExpression = runExpression;
|
|
8230
7163
|
exports.runExpressionMany = runExpressionMany;
|
|
@@ -8244,9 +7177,4 @@ exports.subscribeLiveView = subscribeView;
|
|
|
8244
7177
|
exports.toMillis = toMillis;
|
|
8245
7178
|
exports.toSeconds = toSeconds;
|
|
8246
7179
|
exports.withEffects = withEffects;
|
|
8247
|
-
exports.wsDelete = wsDelete;
|
|
8248
|
-
exports.wsGet = wsGet;
|
|
8249
|
-
exports.wsGetMany = wsGetMany;
|
|
8250
|
-
exports.wsQuery = wsQuery;
|
|
8251
|
-
exports.wsSet = wsSet;
|
|
8252
7180
|
//# sourceMappingURL=index.js.map
|