@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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { ComputeBudgetProgram, PublicKey, VersionedTransaction, TransactionMessage, Keypair, SystemProgram, TransactionInstruction, Connection } from '@solana/web3.js';
|
|
3
2
|
import nacl from 'tweetnacl';
|
|
3
|
+
import { ComputeBudgetProgram, PublicKey, VersionedTransaction, TransactionMessage, SystemProgram, TransactionInstruction, Connection } from '@solana/web3.js';
|
|
4
4
|
import * as anchor from '@coral-xyz/anchor';
|
|
5
5
|
import { Program } from '@coral-xyz/anchor';
|
|
6
6
|
import BN from 'bn.js';
|
|
@@ -10,7 +10,6 @@ let clientConfig = {
|
|
|
10
10
|
// User configured settings
|
|
11
11
|
name: '',
|
|
12
12
|
logoUrl: '',
|
|
13
|
-
apiKey: '',
|
|
14
13
|
// Bounded production is the out-of-the-box default — a Bounded app needs only
|
|
15
14
|
// `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
|
|
16
15
|
network: 'bounded-production',
|
|
@@ -87,7 +86,7 @@ function isBoundedNetwork() {
|
|
|
87
86
|
}
|
|
88
87
|
function init(newConfig) {
|
|
89
88
|
return new Promise((resolve, reject) => {
|
|
90
|
-
if (!newConfig.
|
|
89
|
+
if (!newConfig.appId) {
|
|
91
90
|
reject(new Error('No app ID provided.'));
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
@@ -2863,9 +2862,25 @@ async function refreshSession(refreshToken, issuer) {
|
|
|
2863
2862
|
})();
|
|
2864
2863
|
return refreshInFlight$1;
|
|
2865
2864
|
}
|
|
2865
|
+
class SessionRevokeError extends Error {
|
|
2866
|
+
constructor(message, cause) {
|
|
2867
|
+
super(message);
|
|
2868
|
+
this.name = 'SessionRevokeError';
|
|
2869
|
+
this.cause = cause;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
function revokeFailureMessage(err) {
|
|
2873
|
+
var _a, _b;
|
|
2874
|
+
const status = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status;
|
|
2875
|
+
const statusText = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.statusText;
|
|
2876
|
+
const suffix = typeof status === 'number'
|
|
2877
|
+
? ` (HTTP ${status}${statusText ? ` ${statusText}` : ''})`
|
|
2878
|
+
: '';
|
|
2879
|
+
return `Failed to revoke refresh token server-side${suffix}. The refresh-token family may still be active.`;
|
|
2880
|
+
}
|
|
2866
2881
|
/**
|
|
2867
|
-
* Revoke a session's refresh-token family server-side (logout).
|
|
2868
|
-
*
|
|
2882
|
+
* Revoke a session's refresh-token family server-side (logout). Routes to the
|
|
2883
|
+
* minting issuer and rejects if the revoke request fails.
|
|
2869
2884
|
*/
|
|
2870
2885
|
async function revokeSession(refreshToken, issuer) {
|
|
2871
2886
|
if (!refreshToken)
|
|
@@ -2878,8 +2893,8 @@ async function revokeSession(refreshToken, issuer) {
|
|
|
2878
2893
|
appId: config.appId,
|
|
2879
2894
|
}, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
|
|
2880
2895
|
}
|
|
2881
|
-
catch (
|
|
2882
|
-
|
|
2896
|
+
catch (err) {
|
|
2897
|
+
throw new SessionRevokeError(revokeFailureMessage(err), err);
|
|
2883
2898
|
}
|
|
2884
2899
|
}
|
|
2885
2900
|
async function signSessionCreateMessage(_signMessageFunction) {
|
|
@@ -3295,231 +3310,6 @@ var sessionManager = /*#__PURE__*/Object.freeze({
|
|
|
3295
3310
|
getActiveSessionManager: getActiveSessionManager
|
|
3296
3311
|
});
|
|
3297
3312
|
|
|
3298
|
-
var safeBuffer = {exports: {}};
|
|
3299
|
-
|
|
3300
|
-
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
3301
|
-
|
|
3302
|
-
var hasRequiredSafeBuffer;
|
|
3303
|
-
|
|
3304
|
-
function requireSafeBuffer () {
|
|
3305
|
-
if (hasRequiredSafeBuffer) return safeBuffer.exports;
|
|
3306
|
-
hasRequiredSafeBuffer = 1;
|
|
3307
|
-
(function (module, exports$1) {
|
|
3308
|
-
/* eslint-disable node/no-deprecated-api */
|
|
3309
|
-
var buffer = requireBuffer();
|
|
3310
|
-
var Buffer = buffer.Buffer;
|
|
3311
|
-
|
|
3312
|
-
// alternative to using Object.keys for old browsers
|
|
3313
|
-
function copyProps (src, dst) {
|
|
3314
|
-
for (var key in src) {
|
|
3315
|
-
dst[key] = src[key];
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) {
|
|
3319
|
-
module.exports = buffer;
|
|
3320
|
-
} else {
|
|
3321
|
-
// Copy properties from require('buffer')
|
|
3322
|
-
copyProps(buffer, exports$1);
|
|
3323
|
-
exports$1.Buffer = SafeBuffer;
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
function SafeBuffer (arg, encodingOrOffset, length) {
|
|
3327
|
-
return Buffer(arg, encodingOrOffset, length)
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
|
-
SafeBuffer.prototype = Object.create(Buffer.prototype);
|
|
3331
|
-
|
|
3332
|
-
// Copy static methods from Buffer
|
|
3333
|
-
copyProps(Buffer, SafeBuffer);
|
|
3334
|
-
|
|
3335
|
-
SafeBuffer.from = function (arg, encodingOrOffset, length) {
|
|
3336
|
-
if (typeof arg === 'number') {
|
|
3337
|
-
throw new TypeError('Argument must not be a number')
|
|
3338
|
-
}
|
|
3339
|
-
return Buffer(arg, encodingOrOffset, length)
|
|
3340
|
-
};
|
|
3341
|
-
|
|
3342
|
-
SafeBuffer.alloc = function (size, fill, encoding) {
|
|
3343
|
-
if (typeof size !== 'number') {
|
|
3344
|
-
throw new TypeError('Argument must be a number')
|
|
3345
|
-
}
|
|
3346
|
-
var buf = Buffer(size);
|
|
3347
|
-
if (fill !== undefined) {
|
|
3348
|
-
if (typeof encoding === 'string') {
|
|
3349
|
-
buf.fill(fill, encoding);
|
|
3350
|
-
} else {
|
|
3351
|
-
buf.fill(fill);
|
|
3352
|
-
}
|
|
3353
|
-
} else {
|
|
3354
|
-
buf.fill(0);
|
|
3355
|
-
}
|
|
3356
|
-
return buf
|
|
3357
|
-
};
|
|
3358
|
-
|
|
3359
|
-
SafeBuffer.allocUnsafe = function (size) {
|
|
3360
|
-
if (typeof size !== 'number') {
|
|
3361
|
-
throw new TypeError('Argument must be a number')
|
|
3362
|
-
}
|
|
3363
|
-
return Buffer(size)
|
|
3364
|
-
};
|
|
3365
|
-
|
|
3366
|
-
SafeBuffer.allocUnsafeSlow = function (size) {
|
|
3367
|
-
if (typeof size !== 'number') {
|
|
3368
|
-
throw new TypeError('Argument must be a number')
|
|
3369
|
-
}
|
|
3370
|
-
return buffer.SlowBuffer(size)
|
|
3371
|
-
};
|
|
3372
|
-
} (safeBuffer, safeBuffer.exports));
|
|
3373
|
-
return safeBuffer.exports;
|
|
3374
|
-
}
|
|
3375
|
-
|
|
3376
|
-
var src;
|
|
3377
|
-
var hasRequiredSrc;
|
|
3378
|
-
|
|
3379
|
-
function requireSrc () {
|
|
3380
|
-
if (hasRequiredSrc) return src;
|
|
3381
|
-
hasRequiredSrc = 1;
|
|
3382
|
-
// base-x encoding / decoding
|
|
3383
|
-
// Copyright (c) 2018 base-x contributors
|
|
3384
|
-
// Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp)
|
|
3385
|
-
// Distributed under the MIT software license, see the accompanying
|
|
3386
|
-
// file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
3387
|
-
// @ts-ignore
|
|
3388
|
-
var _Buffer = requireSafeBuffer().Buffer;
|
|
3389
|
-
function base (ALPHABET) {
|
|
3390
|
-
if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') }
|
|
3391
|
-
var BASE_MAP = new Uint8Array(256);
|
|
3392
|
-
for (var j = 0; j < BASE_MAP.length; j++) {
|
|
3393
|
-
BASE_MAP[j] = 255;
|
|
3394
|
-
}
|
|
3395
|
-
for (var i = 0; i < ALPHABET.length; i++) {
|
|
3396
|
-
var x = ALPHABET.charAt(i);
|
|
3397
|
-
var xc = x.charCodeAt(0);
|
|
3398
|
-
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') }
|
|
3399
|
-
BASE_MAP[xc] = i;
|
|
3400
|
-
}
|
|
3401
|
-
var BASE = ALPHABET.length;
|
|
3402
|
-
var LEADER = ALPHABET.charAt(0);
|
|
3403
|
-
var FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
|
|
3404
|
-
var iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
|
|
3405
|
-
function encode (source) {
|
|
3406
|
-
if (Array.isArray(source) || source instanceof Uint8Array) { source = _Buffer.from(source); }
|
|
3407
|
-
if (!_Buffer.isBuffer(source)) { throw new TypeError('Expected Buffer') }
|
|
3408
|
-
if (source.length === 0) { return '' }
|
|
3409
|
-
// Skip & count leading zeroes.
|
|
3410
|
-
var zeroes = 0;
|
|
3411
|
-
var length = 0;
|
|
3412
|
-
var pbegin = 0;
|
|
3413
|
-
var pend = source.length;
|
|
3414
|
-
while (pbegin !== pend && source[pbegin] === 0) {
|
|
3415
|
-
pbegin++;
|
|
3416
|
-
zeroes++;
|
|
3417
|
-
}
|
|
3418
|
-
// Allocate enough space in big-endian base58 representation.
|
|
3419
|
-
var size = ((pend - pbegin) * iFACTOR + 1) >>> 0;
|
|
3420
|
-
var b58 = new Uint8Array(size);
|
|
3421
|
-
// Process the bytes.
|
|
3422
|
-
while (pbegin !== pend) {
|
|
3423
|
-
var carry = source[pbegin];
|
|
3424
|
-
// Apply "b58 = b58 * 256 + ch".
|
|
3425
|
-
var i = 0;
|
|
3426
|
-
for (var it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
|
|
3427
|
-
carry += (256 * b58[it1]) >>> 0;
|
|
3428
|
-
b58[it1] = (carry % BASE) >>> 0;
|
|
3429
|
-
carry = (carry / BASE) >>> 0;
|
|
3430
|
-
}
|
|
3431
|
-
if (carry !== 0) { throw new Error('Non-zero carry') }
|
|
3432
|
-
length = i;
|
|
3433
|
-
pbegin++;
|
|
3434
|
-
}
|
|
3435
|
-
// Skip leading zeroes in base58 result.
|
|
3436
|
-
var it2 = size - length;
|
|
3437
|
-
while (it2 !== size && b58[it2] === 0) {
|
|
3438
|
-
it2++;
|
|
3439
|
-
}
|
|
3440
|
-
// Translate the result into a string.
|
|
3441
|
-
var str = LEADER.repeat(zeroes);
|
|
3442
|
-
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
|
|
3443
|
-
return str
|
|
3444
|
-
}
|
|
3445
|
-
function decodeUnsafe (source) {
|
|
3446
|
-
if (typeof source !== 'string') { throw new TypeError('Expected String') }
|
|
3447
|
-
if (source.length === 0) { return _Buffer.alloc(0) }
|
|
3448
|
-
var psz = 0;
|
|
3449
|
-
// Skip and count leading '1's.
|
|
3450
|
-
var zeroes = 0;
|
|
3451
|
-
var length = 0;
|
|
3452
|
-
while (source[psz] === LEADER) {
|
|
3453
|
-
zeroes++;
|
|
3454
|
-
psz++;
|
|
3455
|
-
}
|
|
3456
|
-
// Allocate enough space in big-endian base256 representation.
|
|
3457
|
-
var size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
|
|
3458
|
-
var b256 = new Uint8Array(size);
|
|
3459
|
-
// Process the characters.
|
|
3460
|
-
while (psz < source.length) {
|
|
3461
|
-
// Find code of next character
|
|
3462
|
-
var charCode = source.charCodeAt(psz);
|
|
3463
|
-
// Base map can not be indexed using char code
|
|
3464
|
-
if (charCode > 255) { return }
|
|
3465
|
-
// Decode character
|
|
3466
|
-
var carry = BASE_MAP[charCode];
|
|
3467
|
-
// Invalid character
|
|
3468
|
-
if (carry === 255) { return }
|
|
3469
|
-
var i = 0;
|
|
3470
|
-
for (var it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
|
|
3471
|
-
carry += (BASE * b256[it3]) >>> 0;
|
|
3472
|
-
b256[it3] = (carry % 256) >>> 0;
|
|
3473
|
-
carry = (carry / 256) >>> 0;
|
|
3474
|
-
}
|
|
3475
|
-
if (carry !== 0) { throw new Error('Non-zero carry') }
|
|
3476
|
-
length = i;
|
|
3477
|
-
psz++;
|
|
3478
|
-
}
|
|
3479
|
-
// Skip leading zeroes in b256.
|
|
3480
|
-
var it4 = size - length;
|
|
3481
|
-
while (it4 !== size && b256[it4] === 0) {
|
|
3482
|
-
it4++;
|
|
3483
|
-
}
|
|
3484
|
-
var vch = _Buffer.allocUnsafe(zeroes + (size - it4));
|
|
3485
|
-
vch.fill(0x00, 0, zeroes);
|
|
3486
|
-
var j = zeroes;
|
|
3487
|
-
while (it4 !== size) {
|
|
3488
|
-
vch[j++] = b256[it4++];
|
|
3489
|
-
}
|
|
3490
|
-
return vch
|
|
3491
|
-
}
|
|
3492
|
-
function decode (string) {
|
|
3493
|
-
var buffer = decodeUnsafe(string);
|
|
3494
|
-
if (buffer) { return buffer }
|
|
3495
|
-
throw new Error('Non-base' + BASE + ' character')
|
|
3496
|
-
}
|
|
3497
|
-
return {
|
|
3498
|
-
encode: encode,
|
|
3499
|
-
decodeUnsafe: decodeUnsafe,
|
|
3500
|
-
decode: decode
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
src = base;
|
|
3504
|
-
return src;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
var bs58$1;
|
|
3508
|
-
var hasRequiredBs58;
|
|
3509
|
-
|
|
3510
|
-
function requireBs58 () {
|
|
3511
|
-
if (hasRequiredBs58) return bs58$1;
|
|
3512
|
-
hasRequiredBs58 = 1;
|
|
3513
|
-
var basex = requireSrc();
|
|
3514
|
-
var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
3515
|
-
|
|
3516
|
-
bs58$1 = basex(ALPHABET);
|
|
3517
|
-
return bs58$1;
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
var bs58Exports = requireBs58();
|
|
3521
|
-
var bs58 = /*@__PURE__*/getDefaultExportFromCjs(bs58Exports);
|
|
3522
|
-
|
|
3523
3313
|
// ─────────────────────────────────────────────────────────────
|
|
3524
3314
|
// Local implementation of getSimulationComputeUnits
|
|
3525
3315
|
// (Replaces @solana-developers/helpers to avoid Wallet import issue in browser/ESM builds)
|
|
@@ -3777,36 +3567,7 @@ async function buildSetDocumentsTransaction(connection, idl, anchorProvider, pay
|
|
|
3777
3567
|
return { tx: vTx, blockhash, lastValidBlockHeight };
|
|
3778
3568
|
}
|
|
3779
3569
|
|
|
3780
|
-
|
|
3781
|
-
/* ENV helpers */
|
|
3782
|
-
/* ------------------------------------------------------------------ */
|
|
3783
|
-
// Canonical `BOUNDED_PRIVATE_KEY` (matches the CLI). Only consulted when no
|
|
3784
|
-
// explicit keypair was provided (createWalletClient passes one).
|
|
3785
|
-
const ENV_KEYPAIR = "BOUNDED_PRIVATE_KEY";
|
|
3786
|
-
const LEGACY_ENV_KEYPAIR = "BOUNDED_SOLANA_KEYPAIR";
|
|
3787
|
-
function loadKeypairFromEnv() {
|
|
3788
|
-
if (process.env[LEGACY_ENV_KEYPAIR]) {
|
|
3789
|
-
throw new Error(`${LEGACY_ENV_KEYPAIR} is no longer supported. Set ${ENV_KEYPAIR} instead, ` +
|
|
3790
|
-
`or pass an explicit keypair via createWalletClient({ keypair }).`);
|
|
3791
|
-
}
|
|
3792
|
-
const secret = process.env[ENV_KEYPAIR];
|
|
3793
|
-
if (!secret) {
|
|
3794
|
-
throw new Error(`No server keypair for this top-level call. The top-level get/set/subscribe/etc. use an ` +
|
|
3795
|
-
`AMBIENT session — set ${ENV_KEYPAIR} to a base-58 secret key (or JSON array) to provide one. ` +
|
|
3796
|
-
`If you already created a wallet with createWalletClient({ keypair }), call ITS methods instead ` +
|
|
3797
|
-
`(client.subscribe / client.set / client.get): that client is self-contained and deliberately does ` +
|
|
3798
|
-
`not set the ambient session, so the top-level functions can't see it.`);
|
|
3799
|
-
}
|
|
3800
|
-
try {
|
|
3801
|
-
const secretKey = secret.trim().startsWith("[")
|
|
3802
|
-
? Uint8Array.from(JSON.parse(secret))
|
|
3803
|
-
: bs58.decode(secret.trim());
|
|
3804
|
-
return Keypair.fromSecretKey(secretKey);
|
|
3805
|
-
}
|
|
3806
|
-
catch (err) {
|
|
3807
|
-
throw new Error(`Unable to parse ${ENV_KEYPAIR}. Ensure it is valid base-58 or JSON.`);
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3570
|
+
const NO_AMBIENT_SERVER_SESSION = "Server sessions are not process-global. Use createWalletClient({ keypair }) and call that wallet client's methods.";
|
|
3810
3571
|
/* ------------------------------------------------------------------ */
|
|
3811
3572
|
/* SESSION MANAGER */
|
|
3812
3573
|
/* ------------------------------------------------------------------ */
|
|
@@ -3829,8 +3590,10 @@ class ServerSessionManager {
|
|
|
3829
3590
|
* Session creation (instance method)
|
|
3830
3591
|
* ---------------------------------------------- */
|
|
3831
3592
|
async createSession() {
|
|
3832
|
-
|
|
3833
|
-
|
|
3593
|
+
if (!this.keypair) {
|
|
3594
|
+
throw new Error(NO_AMBIENT_SERVER_SESSION);
|
|
3595
|
+
}
|
|
3596
|
+
const kp = this.keypair;
|
|
3834
3597
|
const address = kp.publicKey.toBase58();
|
|
3835
3598
|
/* fetch nonce from auth API */
|
|
3836
3599
|
const nonce = await genAuthNonce();
|
|
@@ -3892,7 +3655,7 @@ class ServerSessionManager {
|
|
|
3892
3655
|
return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.refreshToken) !== null && _b !== void 0 ? _b : null;
|
|
3893
3656
|
}
|
|
3894
3657
|
}
|
|
3895
|
-
/* The default singleton
|
|
3658
|
+
/* The default singleton exists only so top-level server calls fail closed. */
|
|
3896
3659
|
ServerSessionManager.instance = new ServerSessionManager();
|
|
3897
3660
|
|
|
3898
3661
|
/**
|
|
@@ -3968,9 +3731,7 @@ async function getUserInfo(isServer) {
|
|
|
3968
3731
|
*
|
|
3969
3732
|
* Mirrors the realtime-worker auth.ts identity resolution so the client-side
|
|
3970
3733
|
* `user` object matches what the backend authenticates as:
|
|
3971
|
-
* - id = custom:userId
|
|
3972
|
-
* keeps wallet/SIWS tokens — which omit userId — AND legacy Better Auth
|
|
3973
|
-
* tokens — which put the account id in custom:walletAddress — working).
|
|
3734
|
+
* - id = custom:userId only.
|
|
3974
3735
|
* - address = custom:walletAddress only (a REAL wallet). NEVER falls back to the
|
|
3975
3736
|
* identity: an opaque id is not a spendable onchain address. null for
|
|
3976
3737
|
* email-only sessions.
|
|
@@ -3995,7 +3756,7 @@ function deriveUserIdentityFromIdToken(idToken) {
|
|
|
3995
3756
|
const userIdClaim = payload['custom:userId'];
|
|
3996
3757
|
const id = (typeof userIdClaim === 'string' && userIdClaim.length > 0)
|
|
3997
3758
|
? userIdClaim
|
|
3998
|
-
:
|
|
3759
|
+
: null;
|
|
3999
3760
|
const emailClaim = payload['email'];
|
|
4000
3761
|
const email = (typeof emailClaim === 'string' && emailClaim.length > 0)
|
|
4001
3762
|
? emailClaim.toLowerCase()
|
|
@@ -4051,17 +3812,6 @@ async function updateIdTokenAndAccessToken(idToken, accessToken, isServer = fals
|
|
|
4051
3812
|
await getActiveSessionManager().updateIdTokenAndAccessToken(idToken, accessToken, refreshToken);
|
|
4052
3813
|
}
|
|
4053
3814
|
|
|
4054
|
-
var utils = /*#__PURE__*/Object.freeze({
|
|
4055
|
-
__proto__: null,
|
|
4056
|
-
createAuthHeader: createAuthHeader,
|
|
4057
|
-
deriveUserIdentityFromIdToken: deriveUserIdentityFromIdToken,
|
|
4058
|
-
getIdToken: getIdToken,
|
|
4059
|
-
getRefreshToken: getRefreshToken,
|
|
4060
|
-
getSessionIssuer: getSessionIssuer,
|
|
4061
|
-
getUserInfo: getUserInfo,
|
|
4062
|
-
updateIdTokenAndAccessToken: updateIdTokenAndAccessToken
|
|
4063
|
-
});
|
|
4064
|
-
|
|
4065
3815
|
const apiClient = axios.create();
|
|
4066
3816
|
axiosRetry(apiClient, {
|
|
4067
3817
|
retries: 2,
|
|
@@ -4126,7 +3876,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
|
|
|
4126
3876
|
const authHeader = (_overrides === null || _overrides === void 0 ? void 0 : _overrides._getAuthHeaders)
|
|
4127
3877
|
? await _overrides._getAuthHeaders()
|
|
4128
3878
|
: await createAuthHeader(config.isServer);
|
|
4129
|
-
const headers = Object.assign({ "Content-Type": "application/json", "X-
|
|
3879
|
+
const headers = Object.assign({ "Content-Type": "application/json", "X-App-Id": config.appId }, authHeader);
|
|
4130
3880
|
// Apply custom headers from _overrides
|
|
4131
3881
|
if (_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers) {
|
|
4132
3882
|
Object.assign(headers, _overrides.headers);
|
|
@@ -4230,6 +3980,7 @@ const pendingRequests = {};
|
|
|
4230
3980
|
const GET_CACHE_TTL = 500; // Adjust this value as needed (in milliseconds)
|
|
4231
3981
|
// Last time we cleaned up the cache
|
|
4232
3982
|
let lastCacheCleanup = Date.now();
|
|
3983
|
+
let uncacheableReadKeyCounter = 0;
|
|
4233
3984
|
/**
|
|
4234
3985
|
* Return the leaf document key (last path segment) for a document path.
|
|
4235
3986
|
*
|
|
@@ -4315,57 +4066,74 @@ function normalizeReadResult(responseData, pathIsDocument) {
|
|
|
4315
4066
|
}
|
|
4316
4067
|
return responseData;
|
|
4317
4068
|
}
|
|
4318
|
-
function hashForKey$
|
|
4069
|
+
function hashForKey$1(value) {
|
|
4319
4070
|
let h = 5381;
|
|
4320
4071
|
for (let i = 0; i < value.length; i++) {
|
|
4321
4072
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
4322
4073
|
}
|
|
4323
4074
|
return h.toString(36);
|
|
4324
4075
|
}
|
|
4076
|
+
function hasAuthHeader(headers) {
|
|
4077
|
+
if (!headers)
|
|
4078
|
+
return false;
|
|
4079
|
+
return (Object.prototype.hasOwnProperty.call(headers, 'Authorization') ||
|
|
4080
|
+
Object.prototype.hasOwnProperty.call(headers, 'authorization'));
|
|
4081
|
+
}
|
|
4325
4082
|
function authValueFromHeaders(headers) {
|
|
4326
4083
|
return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
|
|
4327
4084
|
}
|
|
4328
4085
|
function principalFromAuthValue(authValue) {
|
|
4329
|
-
return authValue ? `h${hashForKey$
|
|
4086
|
+
return authValue ? `h${hashForKey$1(authValue)}` : null;
|
|
4330
4087
|
}
|
|
4331
4088
|
function principalFromIdToken$1(idToken) {
|
|
4332
|
-
return idToken ? `t${hashForKey$
|
|
4089
|
+
return idToken ? `t${hashForKey$1(idToken)}` : null;
|
|
4090
|
+
}
|
|
4091
|
+
function uncacheableReadKey(appId, scope) {
|
|
4092
|
+
uncacheableReadKeyCounter += 1;
|
|
4093
|
+
return `${appId}:${scope}-uncacheable-${uncacheableReadKeyCounter}`;
|
|
4333
4094
|
}
|
|
4334
4095
|
/**
|
|
4335
4096
|
* SECURITY (H1): Read caches must be keyed by the caller's principal, not just
|
|
4336
4097
|
* by path/filter/shape. In a shared process / SSR worker / browser login-switch,
|
|
4337
4098
|
* keying by path alone lets User B receive User A's cached private read before
|
|
4338
|
-
* any server read rule runs. This returns `appId:<principal>` for the
|
|
4339
|
-
* given read will actually authenticate
|
|
4340
|
-
*
|
|
4341
|
-
*
|
|
4342
|
-
*
|
|
4099
|
+
* any server read rule runs. This returns `appId:<principal>` for the opaque
|
|
4100
|
+
* auth material a given read will actually authenticate with. No-auth reads are
|
|
4101
|
+
* deliberately marked uncacheable instead of sharing an implicit `anon` bucket.
|
|
4102
|
+
* JWTs are intentionally treated as opaque unverified bearer material — never
|
|
4103
|
+
* decoded claims and never caller identity hints such as `_walletAddress`.
|
|
4343
4104
|
*/
|
|
4344
4105
|
async function getReadPrincipalKey(overrides) {
|
|
4345
4106
|
const config = await getConfig();
|
|
4346
4107
|
const appId = config.appId || '';
|
|
4347
4108
|
// makeApiRequest applies overrides.headers AFTER its computed auth header, so
|
|
4348
4109
|
// caller-supplied Authorization is the real request auth when present.
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
return
|
|
4110
|
+
if (hasAuthHeader(overrides === null || overrides === void 0 ? void 0 : overrides.headers)) {
|
|
4111
|
+
const principal = principalFromAuthValue(authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers));
|
|
4112
|
+
return principal
|
|
4113
|
+
? { key: `${appId}:${principal}`, cacheable: true }
|
|
4114
|
+
: { key: uncacheableReadKey(appId, 'h'), cacheable: false };
|
|
4352
4115
|
}
|
|
4353
4116
|
// Per-request auth-header override (wallet client). Key by the exact opaque
|
|
4354
4117
|
// header it produces, not decoded claims or the unverified _walletAddress hint.
|
|
4355
4118
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
4356
4119
|
try {
|
|
4357
4120
|
const headers = await overrides._getAuthHeaders();
|
|
4358
|
-
|
|
4121
|
+
const principal = principalFromAuthValue(authValueFromHeaders(headers));
|
|
4122
|
+
return principal
|
|
4123
|
+
? { key: `${appId}:o${principal}`, cacheable: true }
|
|
4124
|
+
: { key: uncacheableReadKey(appId, 'o'), cacheable: false };
|
|
4359
4125
|
}
|
|
4360
4126
|
catch (_a) {
|
|
4361
|
-
// If we can't resolve the override identity,
|
|
4362
|
-
|
|
4363
|
-
return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
|
|
4127
|
+
// If we can't resolve the override identity, do not read/write cache.
|
|
4128
|
+
return { key: uncacheableReadKey(appId, 'o'), cacheable: false };
|
|
4364
4129
|
}
|
|
4365
4130
|
}
|
|
4366
4131
|
// Ambient session principal.
|
|
4367
4132
|
const idToken = await getIdToken(config.isServer);
|
|
4368
|
-
|
|
4133
|
+
const principal = principalFromIdToken$1(idToken);
|
|
4134
|
+
return principal
|
|
4135
|
+
? { key: `${appId}:${principal}`, cacheable: true }
|
|
4136
|
+
: { key: uncacheableReadKey(appId, 'a'), cacheable: false };
|
|
4369
4137
|
}
|
|
4370
4138
|
/**
|
|
4371
4139
|
* Validates that a field name is a safe identifier (alphanumeric, underscores, dots for nested paths).
|
|
@@ -4658,18 +4426,20 @@ async function get(path, opts = {}) {
|
|
|
4658
4426
|
// Create cache key combining path, prompt, filter, sort, includeSubPaths,
|
|
4659
4427
|
// shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
|
|
4660
4428
|
// so a private read cached for one user is never served to another in a
|
|
4661
|
-
// shared process / SSR worker / browser login-switch.
|
|
4429
|
+
// shared process / SSR worker / browser login-switch. The cache is opt-in
|
|
4430
|
+
// and disabled for no-auth reads, which get an uncacheable unique key.
|
|
4662
4431
|
const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
|
|
4663
4432
|
const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
|
|
4664
4433
|
const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
|
|
4665
|
-
const cursorKey = opts.cursor ? `:c${hashForKey$
|
|
4666
|
-
const filterKey = opts.filter ? `:f${hashForKey$
|
|
4667
|
-
const sortKey = opts.sort ? `:s${hashForKey$
|
|
4434
|
+
const cursorKey = opts.cursor ? `:c${hashForKey$1(opts.cursor)}` : '';
|
|
4435
|
+
const filterKey = opts.filter ? `:f${hashForKey$1(JSON.stringify(opts.filter))}` : '';
|
|
4436
|
+
const sortKey = opts.sort ? `:s${hashForKey$1(JSON.stringify(opts.sort))}` : '';
|
|
4668
4437
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4669
|
-
const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4438
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4439
|
+
const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
|
|
4670
4440
|
const now = Date.now();
|
|
4671
|
-
// Check for valid cache entry
|
|
4672
|
-
if (
|
|
4441
|
+
// Check for valid cache entry when the caller explicitly opted in.
|
|
4442
|
+
if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
|
|
4673
4443
|
return getCache[cacheKey].data;
|
|
4674
4444
|
}
|
|
4675
4445
|
// If we're bypassing cache, we should still coalesce identical requests
|
|
@@ -4711,8 +4481,8 @@ async function get(path, opts = {}) {
|
|
|
4711
4481
|
// - collection path → `{ data, nextCursor }` preserved,
|
|
4712
4482
|
// with the bare `id` (leaf doc key) attached to every returned row (Bug 1).
|
|
4713
4483
|
const responseData = normalizeReadResult(response.data, pathIsDocument);
|
|
4714
|
-
// Cache the response
|
|
4715
|
-
if (
|
|
4484
|
+
// Cache the response only when explicitly requested and principal-bound.
|
|
4485
|
+
if (cacheEnabled) {
|
|
4716
4486
|
getCache[cacheKey] = {
|
|
4717
4487
|
data: responseData,
|
|
4718
4488
|
expiresAt: now + GET_CACHE_TTL
|
|
@@ -4750,6 +4520,213 @@ function cleanupExpiredCache() {
|
|
|
4750
4520
|
});
|
|
4751
4521
|
lastCacheCleanup = now;
|
|
4752
4522
|
}
|
|
4523
|
+
const BOUNDED_PROGRAM_MAINNET = 'poof4b5pk1L9tmThvBmaABjcyjfhFGbMbQP5BXk2QZp';
|
|
4524
|
+
const BOUNDED_PROGRAM_DEVNET = 'taro6CvKqwrYrDc16ufYgzQ2NZcyyVKStffbtudrhRu';
|
|
4525
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
4526
|
+
const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
|
|
4527
|
+
const ALLOWED_SERVER_TX_PROGRAMS = new Set([
|
|
4528
|
+
BOUNDED_PROGRAM_MAINNET,
|
|
4529
|
+
BOUNDED_PROGRAM_DEVNET,
|
|
4530
|
+
COMPUTE_BUDGET_PROGRAM,
|
|
4531
|
+
SYSTEM_PROGRAM_ID,
|
|
4532
|
+
]);
|
|
4533
|
+
const SET_DOCUMENTS_DISCRIMINATOR = '79,46,72,73,24,79,66,245';
|
|
4534
|
+
const SET_DOCUMENTS_V2_DISCRIMINATOR = '22,236,242,185,145,61,26,39';
|
|
4535
|
+
const ALLOWED_BOUNDED_SET_DISCRIMINATORS = new Set([
|
|
4536
|
+
SET_DOCUMENTS_DISCRIMINATOR,
|
|
4537
|
+
SET_DOCUMENTS_V2_DISCRIMINATOR,
|
|
4538
|
+
]);
|
|
4539
|
+
class BorshCursor {
|
|
4540
|
+
constructor(data, offset, label) {
|
|
4541
|
+
this.data = data;
|
|
4542
|
+
this.offset = offset;
|
|
4543
|
+
this.label = label;
|
|
4544
|
+
}
|
|
4545
|
+
requireBytes(length, field) {
|
|
4546
|
+
if (this.offset + length > this.data.length) {
|
|
4547
|
+
throw new Error(`${this.label} has malformed Bounded instruction data while reading ${field}`);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
readU8(field) {
|
|
4551
|
+
this.requireBytes(1, field);
|
|
4552
|
+
return this.data[this.offset++];
|
|
4553
|
+
}
|
|
4554
|
+
readU32(field) {
|
|
4555
|
+
this.requireBytes(4, field);
|
|
4556
|
+
const value = this.data[this.offset] |
|
|
4557
|
+
(this.data[this.offset + 1] << 8) |
|
|
4558
|
+
(this.data[this.offset + 2] << 16) |
|
|
4559
|
+
(this.data[this.offset + 3] << 24);
|
|
4560
|
+
this.offset += 4;
|
|
4561
|
+
return value >>> 0;
|
|
4562
|
+
}
|
|
4563
|
+
skip(length, field) {
|
|
4564
|
+
this.requireBytes(length, field);
|
|
4565
|
+
this.offset += length;
|
|
4566
|
+
}
|
|
4567
|
+
readString(field) {
|
|
4568
|
+
const length = this.readU32(`${field} length`);
|
|
4569
|
+
this.requireBytes(length, field);
|
|
4570
|
+
const raw = this.data.slice(this.offset, this.offset + length);
|
|
4571
|
+
this.offset += length;
|
|
4572
|
+
return bufferExports.Buffer.from(raw).toString('utf8');
|
|
4573
|
+
}
|
|
4574
|
+
skipBytes(field) {
|
|
4575
|
+
const length = this.readU32(`${field} length`);
|
|
4576
|
+
this.skip(length, field);
|
|
4577
|
+
}
|
|
4578
|
+
isAtEnd() {
|
|
4579
|
+
return this.offset === this.data.length;
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
function discriminatorKey(data) {
|
|
4583
|
+
return Array.from(data.slice(0, 8)).join(',');
|
|
4584
|
+
}
|
|
4585
|
+
function skipBoundedFieldValue(cursor) {
|
|
4586
|
+
const option = cursor.readU8('operation value option');
|
|
4587
|
+
if (option === 0)
|
|
4588
|
+
return;
|
|
4589
|
+
if (option !== 1) {
|
|
4590
|
+
throw new Error('Server transaction has malformed Bounded field value option');
|
|
4591
|
+
}
|
|
4592
|
+
const variant = cursor.readU8('operation value variant');
|
|
4593
|
+
switch (variant) {
|
|
4594
|
+
case 0: // u64Val
|
|
4595
|
+
case 1: // i64Val
|
|
4596
|
+
cursor.skip(8, 'operation numeric value');
|
|
4597
|
+
return;
|
|
4598
|
+
case 2: // boolVal
|
|
4599
|
+
cursor.skip(1, 'operation bool value');
|
|
4600
|
+
return;
|
|
4601
|
+
case 3: // stringVal
|
|
4602
|
+
cursor.readString('operation string value');
|
|
4603
|
+
return;
|
|
4604
|
+
case 4: // addressVal
|
|
4605
|
+
cursor.skip(32, 'operation address value');
|
|
4606
|
+
return;
|
|
4607
|
+
default:
|
|
4608
|
+
throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
function skipBoundedFieldOperation(cursor) {
|
|
4612
|
+
cursor.readString('operation key');
|
|
4613
|
+
skipBoundedFieldValue(cursor);
|
|
4614
|
+
cursor.skip(1, 'operation kind');
|
|
4615
|
+
}
|
|
4616
|
+
function skipBoundedTxData(cursor, isV2) {
|
|
4617
|
+
cursor.readString('txData plugin function key');
|
|
4618
|
+
cursor.skipBytes('txData bytes');
|
|
4619
|
+
if (isV2) {
|
|
4620
|
+
cursor.skipBytes('txData raIndices');
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
const raIndexCount = cursor.readU32('txData raIndices length');
|
|
4624
|
+
cursor.skip(raIndexCount * 8, 'txData raIndices');
|
|
4625
|
+
}
|
|
4626
|
+
function normalizeOnchainPath(path) {
|
|
4627
|
+
let normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
4628
|
+
if (normalized.endsWith('*') && normalized.length > 1) {
|
|
4629
|
+
normalized = normalized.slice(0, -1);
|
|
4630
|
+
}
|
|
4631
|
+
if (normalized.endsWith('/')) {
|
|
4632
|
+
normalized = normalized.slice(0, -1);
|
|
4633
|
+
}
|
|
4634
|
+
return normalized;
|
|
4635
|
+
}
|
|
4636
|
+
function parseBoundedSetDocumentsInstruction(data, label) {
|
|
4637
|
+
if (data.length < 8) {
|
|
4638
|
+
throw new Error(`${label} has malformed Bounded instruction data`);
|
|
4639
|
+
}
|
|
4640
|
+
const discriminator = discriminatorKey(data);
|
|
4641
|
+
if (!ALLOWED_BOUNDED_SET_DISCRIMINATORS.has(discriminator)) {
|
|
4642
|
+
throw new Error(`${label} contains unsupported Bounded instruction`);
|
|
4643
|
+
}
|
|
4644
|
+
const isV2 = discriminator === SET_DOCUMENTS_V2_DISCRIMINATOR;
|
|
4645
|
+
const cursor = new BorshCursor(data, 8, label);
|
|
4646
|
+
const appId = cursor.readString('appId');
|
|
4647
|
+
const documentPaths = [];
|
|
4648
|
+
const documentCount = cursor.readU32('documents length');
|
|
4649
|
+
for (let i = 0; i < documentCount; i++) {
|
|
4650
|
+
documentPaths.push(normalizeOnchainPath(cursor.readString('document path')));
|
|
4651
|
+
const operationCount = cursor.readU32('operations length');
|
|
4652
|
+
for (let j = 0; j < operationCount; j++) {
|
|
4653
|
+
skipBoundedFieldOperation(cursor);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
const deletePaths = [];
|
|
4657
|
+
const deleteCount = cursor.readU32('delete paths length');
|
|
4658
|
+
for (let i = 0; i < deleteCount; i++) {
|
|
4659
|
+
deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
|
|
4660
|
+
}
|
|
4661
|
+
const txDataCount = cursor.readU32('txData length');
|
|
4662
|
+
for (let i = 0; i < txDataCount; i++) {
|
|
4663
|
+
skipBoundedTxData(cursor, isV2);
|
|
4664
|
+
}
|
|
4665
|
+
const simulate = cursor.readU8('simulate');
|
|
4666
|
+
if (simulate !== 0 && simulate !== 1) {
|
|
4667
|
+
throw new Error(`${label} has malformed Bounded simulate flag`);
|
|
4668
|
+
}
|
|
4669
|
+
if (!cursor.isAtEnd()) {
|
|
4670
|
+
throw new Error(`${label} has trailing Bounded instruction data`);
|
|
4671
|
+
}
|
|
4672
|
+
return { appId, documentPaths, deletePaths };
|
|
4673
|
+
}
|
|
4674
|
+
function assertSamePathSet(label, expectedPaths, actualPaths) {
|
|
4675
|
+
const expected = new Set(expectedPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
4676
|
+
const actual = new Set(actualPaths.map(normalizeOnchainPath).filter(Boolean));
|
|
4677
|
+
const missing = [...expected].filter(path => !actual.has(path));
|
|
4678
|
+
const extra = [...actual].filter(path => !expected.has(path));
|
|
4679
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
4680
|
+
const details = [
|
|
4681
|
+
missing.length ? `missing paths: ${missing.join(', ')}` : '',
|
|
4682
|
+
extra.length ? `unexpected paths: ${extra.join(', ')}` : '',
|
|
4683
|
+
].filter(Boolean).join('; ');
|
|
4684
|
+
throw new Error(`${label} Bounded instruction paths do not match requested write paths (${details})`);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
function validateServerSuppliedVersionedTransaction(transaction, options) {
|
|
4688
|
+
var _a;
|
|
4689
|
+
const { label, expectedAppId, expectedWritePaths } = options;
|
|
4690
|
+
const accountKeys = transaction.message.staticAccountKeys;
|
|
4691
|
+
let boundedInstructionCount = 0;
|
|
4692
|
+
const actualWritePaths = [];
|
|
4693
|
+
for (const ix of transaction.message.compiledInstructions) {
|
|
4694
|
+
if (ix.programIdIndex >= accountKeys.length) {
|
|
4695
|
+
throw new Error(`${label} has program ID in lookup table (not allowed)`);
|
|
4696
|
+
}
|
|
4697
|
+
const programId = accountKeys[ix.programIdIndex].toBase58();
|
|
4698
|
+
if (!ALLOWED_SERVER_TX_PROGRAMS.has(programId)) {
|
|
4699
|
+
throw new Error(`${label} contains unauthorized program: ${programId}`);
|
|
4700
|
+
}
|
|
4701
|
+
const data = ix.data instanceof Uint8Array ? ix.data : bufferExports.Buffer.from((_a = ix.data) !== null && _a !== void 0 ? _a : []);
|
|
4702
|
+
if (programId === SYSTEM_PROGRAM_ID) {
|
|
4703
|
+
throw new Error(`${label} contains unauthorized System Program instruction`);
|
|
4704
|
+
}
|
|
4705
|
+
if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
|
|
4706
|
+
boundedInstructionCount += 1;
|
|
4707
|
+
const parsed = parseBoundedSetDocumentsInstruction(data, label);
|
|
4708
|
+
if (parsed.appId !== expectedAppId) {
|
|
4709
|
+
throw new Error(`${label} Bounded instruction appId does not match configured appId`);
|
|
4710
|
+
}
|
|
4711
|
+
actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
if (boundedInstructionCount !== 1) {
|
|
4715
|
+
throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
|
|
4716
|
+
}
|
|
4717
|
+
assertSamePathSet(label, expectedWritePaths, actualWritePaths);
|
|
4718
|
+
}
|
|
4719
|
+
function deserializeAndValidateServerTransaction(serializedTransaction, options) {
|
|
4720
|
+
const txBytes = bufferExports.Buffer.from(serializedTransaction, 'base64');
|
|
4721
|
+
const transaction = VersionedTransaction.deserialize(txBytes);
|
|
4722
|
+
validateServerSuppliedVersionedTransaction(transaction, options);
|
|
4723
|
+
return transaction;
|
|
4724
|
+
}
|
|
4725
|
+
function assertAllowedServerPreInstruction(ix, label) {
|
|
4726
|
+
if (ix.programId.equals(SystemProgram.programId)) {
|
|
4727
|
+
throw new Error(`${label} contains unauthorized System Program preInstruction`);
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4753
4730
|
function classifyGetManyBatchError(error) {
|
|
4754
4731
|
var _a, _b, _c;
|
|
4755
4732
|
const err = error;
|
|
@@ -4794,13 +4771,14 @@ async function getMany(paths, opts = {}) {
|
|
|
4794
4771
|
// H1: principal-scope getMany cache keys so one user's batch reads are never
|
|
4795
4772
|
// served to another. Same `<appId:principal>|<path>:` shape used by get().
|
|
4796
4773
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4774
|
+
const cacheEnabled = opts.cache === true && !opts.bypassCache && principalKey.cacheable;
|
|
4797
4775
|
const results = new Array(paths.length);
|
|
4798
4776
|
const uncachedIndices = [];
|
|
4799
4777
|
const uncachedPaths = [];
|
|
4800
4778
|
for (let i = 0; i < normalizedPaths.length; i++) {
|
|
4801
4779
|
const normalizedPath = normalizedPaths[i];
|
|
4802
|
-
const cacheKey = `${principalKey}|${normalizedPath}:`;
|
|
4803
|
-
if (
|
|
4780
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:`;
|
|
4781
|
+
if (cacheEnabled && getCache[cacheKey] && now < getCache[cacheKey].expiresAt) {
|
|
4804
4782
|
results[i] = { path: normalizedPath, data: getCache[cacheKey].data };
|
|
4805
4783
|
}
|
|
4806
4784
|
else {
|
|
@@ -4836,8 +4814,8 @@ async function getMany(paths, opts = {}) {
|
|
|
4836
4814
|
? serverResult
|
|
4837
4815
|
: Object.assign(Object.assign({}, serverResult), { data: withBareId(serverResult.data) });
|
|
4838
4816
|
results[originalIndex] = normalizedResult;
|
|
4839
|
-
if (!normalizedResult.error &&
|
|
4840
|
-
const cacheKey = `${principalKey}|${normalizedPath}:`;
|
|
4817
|
+
if (!normalizedResult.error && cacheEnabled) {
|
|
4818
|
+
const cacheKey = `${principalKey.key}|${normalizedPath}:`;
|
|
4841
4819
|
getCache[cacheKey] = {
|
|
4842
4820
|
data: normalizedResult.data,
|
|
4843
4821
|
expiresAt: now + GET_CACHE_TTL
|
|
@@ -4852,7 +4830,7 @@ async function getMany(paths, opts = {}) {
|
|
|
4852
4830
|
};
|
|
4853
4831
|
}
|
|
4854
4832
|
}
|
|
4855
|
-
if (now - lastCacheCleanup > 5000) {
|
|
4833
|
+
if (cacheEnabled && now - lastCacheCleanup > 5000) {
|
|
4856
4834
|
cleanupExpiredCache();
|
|
4857
4835
|
lastCacheCleanup = now;
|
|
4858
4836
|
}
|
|
@@ -5003,11 +4981,12 @@ async function setMany(many, options) {
|
|
|
5003
4981
|
}
|
|
5004
4982
|
const curTx = transactions[0];
|
|
5005
4983
|
let transactionResult;
|
|
4984
|
+
const expectedWritePaths = documents.map(d => d.destinationPath);
|
|
5006
4985
|
if (curTx.serializedTransaction) {
|
|
5007
|
-
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
|
|
4986
|
+
transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
5008
4987
|
}
|
|
5009
4988
|
else {
|
|
5010
|
-
transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
|
|
4989
|
+
transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
|
|
5011
4990
|
}
|
|
5012
4991
|
// Sync items after all transactions are confirmed
|
|
5013
4992
|
// Wait for 1.5 seconds to ensure all transactions are confirmed
|
|
@@ -5041,7 +5020,7 @@ async function setMany(many, options) {
|
|
|
5041
5020
|
catch (error) {
|
|
5042
5021
|
throw error;
|
|
5043
5022
|
}
|
|
5044
|
-
async function handleSolanaTransaction(tx, authProvider, options) {
|
|
5023
|
+
async function handleSolanaTransaction(tx, authProvider, options, expectedWritePaths) {
|
|
5045
5024
|
var _a, _b, _c, _d, _e;
|
|
5046
5025
|
// NOTE (backwards-compat revert): a program-allowlist on server-supplied
|
|
5047
5026
|
// `preInstructions` was tried here for the audit-8 SOL-drain concern, but it
|
|
@@ -5089,13 +5068,20 @@ async function setMany(many, options) {
|
|
|
5089
5068
|
}))) !== null && _b !== void 0 ? _b : [],
|
|
5090
5069
|
};
|
|
5091
5070
|
const config = await getConfig();
|
|
5071
|
+
if (tx.signedTransaction) {
|
|
5072
|
+
deserializeAndValidateServerTransaction(tx.signedTransaction, {
|
|
5073
|
+
label: 'Server signedTransaction',
|
|
5074
|
+
expectedAppId: config.appId,
|
|
5075
|
+
expectedWritePaths,
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5092
5078
|
const solTransaction = {
|
|
5093
5079
|
appId: config.appId,
|
|
5094
5080
|
txArgs: [solTransactionData],
|
|
5095
5081
|
lutKey: (_c = tx.lutAddress) !== null && _c !== void 0 ? _c : null,
|
|
5096
5082
|
additionalLutAddresses: tx.additionalLutAddresses,
|
|
5097
5083
|
network: tx.network,
|
|
5098
|
-
preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix) => {
|
|
5084
|
+
preInstructions: (_e = (_d = tx.preInstructions) === null || _d === void 0 ? void 0 : _d.map((ix, index) => {
|
|
5099
5085
|
var _a;
|
|
5100
5086
|
const keys = (_a = ix.keys) === null || _a === void 0 ? void 0 : _a.map((k) => ({
|
|
5101
5087
|
pubkey: new PublicKey(k.pubkey),
|
|
@@ -5106,11 +5092,13 @@ async function setMany(many, options) {
|
|
|
5106
5092
|
? SystemProgram.programId // prettier to use the constant
|
|
5107
5093
|
: new PublicKey(ix.programId);
|
|
5108
5094
|
const data = bufferExports.Buffer.from(ix.data);
|
|
5109
|
-
|
|
5095
|
+
const instruction = new TransactionInstruction({
|
|
5110
5096
|
keys,
|
|
5111
5097
|
programId,
|
|
5112
5098
|
data,
|
|
5113
5099
|
});
|
|
5100
|
+
assertAllowedServerPreInstruction(instruction, `Server preInstruction[${index}]`);
|
|
5101
|
+
return instruction;
|
|
5114
5102
|
})) !== null && _e !== void 0 ? _e : [],
|
|
5115
5103
|
// Server co-signed transaction (when CPI tx_data is present)
|
|
5116
5104
|
signedTransaction: tx.signedTransaction,
|
|
@@ -5118,45 +5106,22 @@ async function setMany(many, options) {
|
|
|
5118
5106
|
const transactionResult = await authProvider.runTransaction(undefined, solTransaction, options);
|
|
5119
5107
|
return transactionResult;
|
|
5120
5108
|
}
|
|
5121
|
-
async function handlePreBuiltTransaction(tx, authProvider, options) {
|
|
5122
|
-
var _a, _b;
|
|
5109
|
+
async function handlePreBuiltTransaction(tx, authProvider, options, expectedWritePaths) {
|
|
5110
|
+
var _a, _b, _c;
|
|
5123
5111
|
const config = await getConfig();
|
|
5124
|
-
const
|
|
5125
|
-
|
|
5126
|
-
:
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
const
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
const COMPUTE_BUDGET = 'ComputeBudget111111111111111111111111111111';
|
|
5134
|
-
const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
5135
|
-
const ALLOWED_PROGRAMS = new Set([BOUNDED_PROGRAM, COMPUTE_BUDGET, SYSTEM_PROGRAM]);
|
|
5136
|
-
// System program instruction discriminators (first 4 bytes, little-endian u32)
|
|
5137
|
-
const SYSTEM_TRANSFER = 2; // Transfer instruction index
|
|
5138
|
-
const SYSTEM_TRANSFER_WITH_SEED = 11;
|
|
5139
|
-
const accountKeys = transaction.message.staticAccountKeys;
|
|
5140
|
-
for (const ix of transaction.message.compiledInstructions) {
|
|
5141
|
-
if (ix.programIdIndex >= accountKeys.length) {
|
|
5142
|
-
throw new Error('Pre-built transaction has program ID in lookup table (not allowed)');
|
|
5143
|
-
}
|
|
5144
|
-
const programId = accountKeys[ix.programIdIndex].toBase58();
|
|
5145
|
-
if (!ALLOWED_PROGRAMS.has(programId)) {
|
|
5146
|
-
throw new Error(`Pre-built transaction contains unauthorized program: ${programId}`);
|
|
5147
|
-
}
|
|
5148
|
-
// Block System program transfer instructions — a compromised DO could
|
|
5149
|
-
// embed a SOL drain. Only allow createAccount/allocate (needed for PDA init).
|
|
5150
|
-
if (programId === SYSTEM_PROGRAM && ix.data.length >= 4) {
|
|
5151
|
-
const ixIndex = ix.data[0] | (ix.data[1] << 8) | (ix.data[2] << 16) | (ix.data[3] << 24);
|
|
5152
|
-
if (ixIndex === SYSTEM_TRANSFER || ixIndex === SYSTEM_TRANSFER_WITH_SEED) {
|
|
5153
|
-
throw new Error('Pre-built transaction contains unauthorized System transfer instruction');
|
|
5154
|
-
}
|
|
5155
|
-
}
|
|
5112
|
+
const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, {
|
|
5113
|
+
label: 'Pre-built transaction',
|
|
5114
|
+
expectedAppId: config.appId,
|
|
5115
|
+
expectedWritePaths,
|
|
5116
|
+
});
|
|
5117
|
+
const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
|
|
5118
|
+
const rpcUrl = (_a = config.rpcUrl) === null || _a === void 0 ? void 0 : _a.trim();
|
|
5119
|
+
if (shouldSubmit && !rpcUrl) {
|
|
5120
|
+
throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}`);
|
|
5156
5121
|
}
|
|
5157
5122
|
const signedTx = await authProvider.signTransaction(transaction);
|
|
5158
5123
|
const rawTx = signedTx.serialize();
|
|
5159
|
-
if (
|
|
5124
|
+
if (!shouldSubmit) {
|
|
5160
5125
|
return {
|
|
5161
5126
|
transactionSignature: '',
|
|
5162
5127
|
signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
|
|
@@ -5164,6 +5129,7 @@ async function setMany(many, options) {
|
|
|
5164
5129
|
gasUsed: '0',
|
|
5165
5130
|
};
|
|
5166
5131
|
}
|
|
5132
|
+
const connection = new Connection(rpcUrl, 'confirmed');
|
|
5167
5133
|
const signature = await connection.sendRawTransaction(rawTx, {
|
|
5168
5134
|
skipPreflight: false,
|
|
5169
5135
|
maxRetries: 3,
|
|
@@ -5176,7 +5142,7 @@ async function setMany(many, options) {
|
|
|
5176
5142
|
return {
|
|
5177
5143
|
transactionSignature: signature,
|
|
5178
5144
|
signedTransaction: bufferExports.Buffer.from(rawTx).toString('base64'),
|
|
5179
|
-
blockNumber: (
|
|
5145
|
+
blockNumber: (_c = (_b = confirmation.context) === null || _b === void 0 ? void 0 : _b.slot) !== null && _c !== void 0 ? _c : 0,
|
|
5180
5146
|
gasUsed: '0',
|
|
5181
5147
|
};
|
|
5182
5148
|
}
|
|
@@ -5393,7 +5359,7 @@ const MIN_RECONNECT_DELAY_JITTER_MS = 1000;
|
|
|
5393
5359
|
const MAX_RECONNECT_DELAY_MS = 300000;
|
|
5394
5360
|
const RECONNECT_DELAY_GROW_FACTOR = 1.8;
|
|
5395
5361
|
const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
5396
|
-
const
|
|
5362
|
+
const WS_AUTH_EXPIRED_CODE = 'auth_expired';
|
|
5397
5363
|
const WS_CONFIG = {
|
|
5398
5364
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
5399
5365
|
maxRetries: Infinity,
|
|
@@ -5410,11 +5376,12 @@ const WS_V2_PATH = '/ws/v2';
|
|
|
5410
5376
|
let browserReconnectHooksAttached = false;
|
|
5411
5377
|
let lastBrowserTriggeredReconnectAt = 0;
|
|
5412
5378
|
let reconnectInProgress = null;
|
|
5379
|
+
let ambientAuthFailure = null;
|
|
5413
5380
|
// ============ Helper Functions ============
|
|
5414
5381
|
function generateSubscriptionId() {
|
|
5415
5382
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5416
5383
|
}
|
|
5417
|
-
function hashForKey
|
|
5384
|
+
function hashForKey(value) {
|
|
5418
5385
|
let h = 5381;
|
|
5419
5386
|
for (let i = 0; i < value.length; i++) {
|
|
5420
5387
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
@@ -5426,17 +5393,17 @@ function hashForKey$1(value) {
|
|
|
5426
5393
|
* just the path/filter/shape. The `identity` prefix (`<appId>:<principal>`) keeps
|
|
5427
5394
|
* a private subscription snapshot cached for one user from being delivered to a
|
|
5428
5395
|
* different user who subscribes with the same options (shared process / SSR worker
|
|
5429
|
-
* / browser login-switch; this cache has a 5-minute TTL).
|
|
5430
|
-
* read
|
|
5396
|
+
* / browser login-switch; this cache has a 5-minute TTL). Callers must opt in
|
|
5397
|
+
* before entries are read or written; no-auth subscriptions never populate an
|
|
5398
|
+
* implicit anonymous cache bucket.
|
|
5431
5399
|
*/
|
|
5432
5400
|
function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
5433
5401
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
5434
5402
|
const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
|
|
5435
5403
|
const limitKey = limit !== undefined ? `:l${limit}` : '';
|
|
5436
|
-
const cursorKey = cursor ? `:c${hashForKey
|
|
5437
|
-
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey
|
|
5438
|
-
|
|
5439
|
-
return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5404
|
+
const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
|
|
5405
|
+
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
|
|
5406
|
+
return `${identity}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5440
5407
|
}
|
|
5441
5408
|
/**
|
|
5442
5409
|
* Derive an opaque identity string for the bearer material a subscription sends.
|
|
@@ -5444,7 +5411,7 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
|
5444
5411
|
* before the server has verified any claims.
|
|
5445
5412
|
*/
|
|
5446
5413
|
function principalFromIdToken(idToken) {
|
|
5447
|
-
return idToken ? `t${hashForKey
|
|
5414
|
+
return idToken ? `t${hashForKey(idToken)}` : null;
|
|
5448
5415
|
}
|
|
5449
5416
|
async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
5450
5417
|
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
@@ -5453,16 +5420,21 @@ async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
|
5453
5420
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
5454
5421
|
try {
|
|
5455
5422
|
const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
|
|
5456
|
-
|
|
5423
|
+
const principal = principalFromIdToken(bearer);
|
|
5424
|
+
return principal
|
|
5425
|
+
? { key: `${effectiveAppId}:o${principal}`, cacheable: true }
|
|
5426
|
+
: { key: `${effectiveAppId}:oanon`, cacheable: false };
|
|
5457
5427
|
}
|
|
5458
5428
|
catch (_a) {
|
|
5459
|
-
// Couldn't resolve the override identity —
|
|
5460
|
-
|
|
5461
|
-
return `${effectiveAppId}:o${principalFromIdToken(null)}-${safeBtoa(String(connectionEpoch++))}`;
|
|
5429
|
+
// Couldn't resolve the override identity — do not use response cache.
|
|
5430
|
+
return { key: `${effectiveAppId}:o-uncacheable-${safeBtoa(String(connectionEpoch++))}`, cacheable: false };
|
|
5462
5431
|
}
|
|
5463
5432
|
}
|
|
5464
5433
|
const idToken = await getIdToken(isServer);
|
|
5465
|
-
|
|
5434
|
+
const principal = principalFromIdToken(idToken);
|
|
5435
|
+
return principal
|
|
5436
|
+
? { key: `${effectiveAppId}:${principal}`, cacheable: true }
|
|
5437
|
+
: { key: `${effectiveAppId}:anon`, cacheable: false };
|
|
5466
5438
|
}
|
|
5467
5439
|
/** Extract the bare bearer token from a `{ Authorization: 'Bearer <jwt>' }` map. */
|
|
5468
5440
|
function bearerFromAuthHeaders(headers) {
|
|
@@ -5492,6 +5464,41 @@ function getTokenExpirationTime(token) {
|
|
|
5492
5464
|
return null;
|
|
5493
5465
|
}
|
|
5494
5466
|
}
|
|
5467
|
+
function makeAuthExpiredError(message, status) {
|
|
5468
|
+
const err = new Error(`${WS_AUTH_EXPIRED_CODE}: ${message}`);
|
|
5469
|
+
err.code = WS_AUTH_EXPIRED_CODE;
|
|
5470
|
+
if (status !== undefined)
|
|
5471
|
+
err.status = status;
|
|
5472
|
+
return err;
|
|
5473
|
+
}
|
|
5474
|
+
function isAuthExpiredError(error) {
|
|
5475
|
+
return !!error && typeof error === 'object' && error.code === WS_AUTH_EXPIRED_CODE;
|
|
5476
|
+
}
|
|
5477
|
+
function normalizeAuthExpiredError(error, fallbackMessage) {
|
|
5478
|
+
var _a, _b;
|
|
5479
|
+
if (isAuthExpiredError(error))
|
|
5480
|
+
return error;
|
|
5481
|
+
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;
|
|
5482
|
+
return makeAuthExpiredError(fallbackMessage, status);
|
|
5483
|
+
}
|
|
5484
|
+
function rememberAmbientAuthFailure(error) {
|
|
5485
|
+
ambientAuthFailure = error;
|
|
5486
|
+
return error;
|
|
5487
|
+
}
|
|
5488
|
+
function failConnectionAuth(connection, error) {
|
|
5489
|
+
connection.authFailure = error;
|
|
5490
|
+
connection.pendingAuthToken = null;
|
|
5491
|
+
connection.isConnecting = false;
|
|
5492
|
+
connection.isConnected = false;
|
|
5493
|
+
connection.isAuthenticating = false;
|
|
5494
|
+
for (const [, pending] of connection.pendingSubscriptions) {
|
|
5495
|
+
pending.reject(error);
|
|
5496
|
+
}
|
|
5497
|
+
connection.pendingSubscriptions.clear();
|
|
5498
|
+
for (const subscription of connection.subscriptions.values()) {
|
|
5499
|
+
notifyErrorCallbacks(subscription, error);
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5495
5502
|
function scheduleTokenRefresh(connection, isServer) {
|
|
5496
5503
|
// Clear any existing timer
|
|
5497
5504
|
if (connection.tokenRefreshTimer) {
|
|
@@ -5559,23 +5566,32 @@ async function getFreshAuthToken(isServer) {
|
|
|
5559
5566
|
var _a, _b, _c, _d, _e;
|
|
5560
5567
|
const currentToken = await getIdToken(isServer);
|
|
5561
5568
|
if (!currentToken) {
|
|
5569
|
+
if (ambientAuthFailure) {
|
|
5570
|
+
throw ambientAuthFailure;
|
|
5571
|
+
}
|
|
5562
5572
|
return null;
|
|
5563
5573
|
}
|
|
5564
5574
|
if (!isTokenExpired(currentToken)) {
|
|
5575
|
+
ambientAuthFailure = null;
|
|
5565
5576
|
return currentToken;
|
|
5566
5577
|
}
|
|
5578
|
+
if (ambientAuthFailure) {
|
|
5579
|
+
throw ambientAuthFailure;
|
|
5580
|
+
}
|
|
5567
5581
|
// Token is expired — attempt refresh
|
|
5582
|
+
const refreshToken = await getRefreshToken(isServer);
|
|
5583
|
+
if (!refreshToken) {
|
|
5584
|
+
console.warn('[WS v2] Token expired but no refresh token available');
|
|
5585
|
+
throw rememberAmbientAuthFailure(makeAuthExpiredError('Authentication expired and no refresh token is available'));
|
|
5586
|
+
}
|
|
5568
5587
|
try {
|
|
5569
|
-
const refreshToken = await getRefreshToken(isServer);
|
|
5570
|
-
if (!refreshToken) {
|
|
5571
|
-
console.warn('[WS v2] Token expired but no refresh token available');
|
|
5572
|
-
return null;
|
|
5573
|
-
}
|
|
5574
5588
|
const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
|
|
5575
5589
|
if (refreshData && refreshData.idToken && refreshData.accessToken) {
|
|
5576
5590
|
await updateIdTokenAndAccessToken(refreshData.idToken, refreshData.accessToken, isServer, refreshData.refreshToken);
|
|
5591
|
+
ambientAuthFailure = null;
|
|
5577
5592
|
return refreshData.idToken;
|
|
5578
5593
|
}
|
|
5594
|
+
throw makeAuthExpiredError('Authentication refresh returned an incomplete session');
|
|
5579
5595
|
}
|
|
5580
5596
|
catch (error) {
|
|
5581
5597
|
// Log only the status — the raw axios error carries config.data (the refresh
|
|
@@ -5592,12 +5608,22 @@ async function getFreshAuthToken(isServer) {
|
|
|
5592
5608
|
console.warn('[WS v2] Failed to clear stale session:', clearError);
|
|
5593
5609
|
}
|
|
5594
5610
|
}
|
|
5611
|
+
throw rememberAmbientAuthFailure(normalizeAuthExpiredError(error, 'Authentication expired and refresh failed'));
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
async function getConnectionAuthToken(isServer, authTokenProvider) {
|
|
5615
|
+
if (!authTokenProvider) {
|
|
5616
|
+
return getFreshAuthToken(isServer);
|
|
5617
|
+
}
|
|
5618
|
+
try {
|
|
5619
|
+
const token = await authTokenProvider();
|
|
5620
|
+
if (token)
|
|
5621
|
+
return token;
|
|
5622
|
+
throw makeAuthExpiredError('Authenticated websocket token provider returned no token');
|
|
5623
|
+
}
|
|
5624
|
+
catch (error) {
|
|
5625
|
+
throw normalizeAuthExpiredError(error, 'Authenticated websocket token provider failed');
|
|
5595
5626
|
}
|
|
5596
|
-
// Return null instead of the expired token to prevent infinite 401 reconnect storms.
|
|
5597
|
-
// The server accepts unauthenticated connections; auth-required subscriptions will
|
|
5598
|
-
// receive per-subscription errors via onError callbacks.
|
|
5599
|
-
console.warn('[WS v2] Token refresh failed, connecting without auth to prevent reconnect storm');
|
|
5600
|
-
return null;
|
|
5601
5627
|
}
|
|
5602
5628
|
function hasDisconnectedActiveConnections() {
|
|
5603
5629
|
for (const connection of connections.values()) {
|
|
@@ -5672,13 +5698,26 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5672
5698
|
const roomKey = roomKeyFromRoutePath(routePath);
|
|
5673
5699
|
// A wallet-scoped subscription (server WalletClient) gets its OWN connection
|
|
5674
5700
|
// keyed by the wallet identity, so its WS authenticates as that wallet and
|
|
5675
|
-
// never shares a connection (or token) with
|
|
5701
|
+
// never shares a connection (or token) with any top-level server caller.
|
|
5676
5702
|
const base = roomKey ? `${appId}#room#${roomKey}` : appId;
|
|
5677
5703
|
const connKey = principalKey ? `${base}#id#${principalKey}` : base;
|
|
5678
5704
|
let connection = connections.get(connKey);
|
|
5679
5705
|
if (connection && connection.ws) {
|
|
5706
|
+
if (connection.authFailure) {
|
|
5707
|
+
throw connection.authFailure;
|
|
5708
|
+
}
|
|
5709
|
+
try {
|
|
5710
|
+
await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5711
|
+
}
|
|
5712
|
+
catch (error) {
|
|
5713
|
+
const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
|
|
5714
|
+
failConnectionAuth(connection, authError);
|
|
5715
|
+
throw authError;
|
|
5716
|
+
}
|
|
5717
|
+
connection.authFailure = null;
|
|
5680
5718
|
return connection;
|
|
5681
5719
|
}
|
|
5720
|
+
let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
|
|
5682
5721
|
// Create new connection
|
|
5683
5722
|
connection = {
|
|
5684
5723
|
ws: null,
|
|
@@ -5696,6 +5735,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5696
5735
|
pendingAuthToken: null,
|
|
5697
5736
|
tokenRefreshTimer: null,
|
|
5698
5737
|
consecutiveAuthFailures: 0,
|
|
5738
|
+
authFailure: null,
|
|
5699
5739
|
};
|
|
5700
5740
|
connections.set(connKey, connection);
|
|
5701
5741
|
// URL provider for reconnection with fresh tokens
|
|
@@ -5715,29 +5755,25 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5715
5755
|
}
|
|
5716
5756
|
// Resolve auth token if available. A wallet-scoped connection resolves
|
|
5717
5757
|
// its token from the wallet's own session (self-refreshing); all others
|
|
5718
|
-
// use the
|
|
5758
|
+
// use the browser session or fail closed for top-level server calls. The token is sent as the first WS
|
|
5719
5759
|
// frame after open, never as a URL query parameter.
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5760
|
+
let authToken;
|
|
5761
|
+
try {
|
|
5762
|
+
authToken = initialAuthToken !== undefined
|
|
5763
|
+
? initialAuthToken
|
|
5764
|
+
: await getConnectionAuthToken(isServer, connection.authTokenProvider);
|
|
5765
|
+
initialAuthToken = undefined;
|
|
5766
|
+
}
|
|
5767
|
+
catch (error) {
|
|
5768
|
+
const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
|
|
5769
|
+
failConnectionAuth(connection, authError);
|
|
5770
|
+
throw authError;
|
|
5771
|
+
}
|
|
5723
5772
|
connection.pendingAuthToken = authToken || null;
|
|
5724
5773
|
if (authToken) {
|
|
5725
5774
|
// Successful token acquisition — reset failure counter
|
|
5726
5775
|
connection.consecutiveAuthFailures = 0;
|
|
5727
|
-
|
|
5728
|
-
else {
|
|
5729
|
-
// Check if user WAS authenticated (had a token that expired).
|
|
5730
|
-
// If so, retry with exponential backoff before falling back to unauthenticated.
|
|
5731
|
-
const expiredToken = await getIdToken(isServer);
|
|
5732
|
-
if (expiredToken && isTokenExpired(expiredToken)) {
|
|
5733
|
-
connection.consecutiveAuthFailures++;
|
|
5734
|
-
if (connection.consecutiveAuthFailures <= MAX_AUTH_REFRESH_RETRIES) {
|
|
5735
|
-
console.warn(`[WS v2] Auth refresh failed (attempt ${connection.consecutiveAuthFailures}/${MAX_AUTH_REFRESH_RETRIES}), retrying with backoff`);
|
|
5736
|
-
throw new Error('Auth token refresh failed, retrying with backoff');
|
|
5737
|
-
}
|
|
5738
|
-
console.warn('[WS v2] Auth refresh retries exhausted, falling back to unauthenticated connection');
|
|
5739
|
-
}
|
|
5740
|
-
// No token at all (never authenticated) or retries exhausted — connect without auth
|
|
5776
|
+
connection.authFailure = null;
|
|
5741
5777
|
}
|
|
5742
5778
|
return wsUrl.toString();
|
|
5743
5779
|
};
|
|
@@ -5751,15 +5787,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5751
5787
|
connection.isConnecting = false;
|
|
5752
5788
|
connection.isConnected = true;
|
|
5753
5789
|
// NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
|
|
5754
|
-
// fresh auth token is actually obtained
|
|
5755
|
-
// an explicit auth change (reconnectWithNewAuthV2, line ~854). Resetting
|
|
5756
|
-
// on every 'open' event created an infinite loop: auth fails 5x → connect
|
|
5757
|
-
// without auth → open resets counter → disconnect → auth fails 5x again →
|
|
5758
|
-
// repeat forever, hammering /session/refresh and causing 429s.
|
|
5759
|
-
//
|
|
5760
|
-
// An elevated counter is safe for anonymous/guest sessions: when there's no
|
|
5761
|
-
// token at all (getIdToken returns null), the counter is never checked —
|
|
5762
|
-
// urlProvider skips straight to unauthenticated connection.
|
|
5790
|
+
// fresh auth token is actually obtained or on an explicit auth change.
|
|
5763
5791
|
// Schedule periodic token freshness checks
|
|
5764
5792
|
scheduleTokenRefresh(connection, isServer);
|
|
5765
5793
|
if (connection.pendingAuthToken) {
|
|
@@ -5834,8 +5862,10 @@ function handleServerMessage(connection, message) {
|
|
|
5834
5862
|
// If we already received data for this subscription, treat subscribed
|
|
5835
5863
|
// as an ack only and avoid regressing to an older snapshot.
|
|
5836
5864
|
if (subscription.lastData === undefined) {
|
|
5837
|
-
|
|
5838
|
-
|
|
5865
|
+
if (subscription.cache) {
|
|
5866
|
+
const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
|
|
5867
|
+
responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
|
|
5868
|
+
}
|
|
5839
5869
|
subscription.lastData = message.data;
|
|
5840
5870
|
notifyCallbacks(subscription, message.data);
|
|
5841
5871
|
}
|
|
@@ -5861,8 +5891,10 @@ function handleServerMessage(connection, message) {
|
|
|
5861
5891
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
5862
5892
|
if (subscription) {
|
|
5863
5893
|
// Update cache
|
|
5864
|
-
|
|
5865
|
-
|
|
5894
|
+
if (subscription.cache) {
|
|
5895
|
+
const cacheKey = getCacheKey(subscription.path, subscription.prompt, subscription.shape, subscription.limit, subscription.cursor, subscription.filter, subscription.identity);
|
|
5896
|
+
responseCache.set(cacheKey, { data: message.data, timestamp: Date.now() });
|
|
5897
|
+
}
|
|
5866
5898
|
// Store last data
|
|
5867
5899
|
subscription.lastData = message.data;
|
|
5868
5900
|
// Notify callbacks
|
|
@@ -6074,26 +6106,43 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6074
6106
|
// subscribes with identical options (shared process / SSR / login-switch).
|
|
6075
6107
|
// Per-subscription wallet override (server WalletClient.subscribe). When set,
|
|
6076
6108
|
// the connection authenticates + caches under the wallet's identity instead of
|
|
6077
|
-
//
|
|
6078
|
-
//
|
|
6109
|
+
// any top-level server caller — so a `createWalletClient` caller can
|
|
6110
|
+
// subscribe through its explicit wallet session.
|
|
6079
6111
|
const overrides = subscriptionOptions._overrides;
|
|
6080
6112
|
const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
|
|
6081
6113
|
? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
|
|
6082
6114
|
: undefined;
|
|
6083
|
-
const
|
|
6115
|
+
const identityInfo = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
|
|
6116
|
+
const identity = identityInfo.key;
|
|
6117
|
+
const responseCacheEnabled = subscriptionOptions.cache === true && identityInfo.cacheable;
|
|
6084
6118
|
const principalKey = authTokenProvider ? identity : undefined;
|
|
6085
|
-
const cacheKey =
|
|
6086
|
-
|
|
6087
|
-
|
|
6119
|
+
const cacheKey = responseCacheEnabled
|
|
6120
|
+
? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
|
|
6121
|
+
: null;
|
|
6122
|
+
// Get or create connection for this routing target (room-scoped when a
|
|
6123
|
+
// room route is supplied by the live helper, else the app-level connection).
|
|
6124
|
+
let connection;
|
|
6125
|
+
try {
|
|
6126
|
+
connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
|
|
6127
|
+
}
|
|
6128
|
+
catch (error) {
|
|
6129
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
6130
|
+
if (subscriptionOptions.onError) {
|
|
6131
|
+
subscriptionOptions.onError(err);
|
|
6132
|
+
return async () => { };
|
|
6133
|
+
}
|
|
6134
|
+
throw err;
|
|
6135
|
+
}
|
|
6136
|
+
// Deliver cached data immediately if available, but only after connection
|
|
6137
|
+
// auth preflight has succeeded. An expired authenticated session must receive
|
|
6138
|
+
// an auth error, not a stale private snapshot followed by a failed connect.
|
|
6139
|
+
const cachedEntry = cacheKey ? responseCache.get(cacheKey) : undefined;
|
|
6088
6140
|
if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL && subscriptionOptions.onData) {
|
|
6089
6141
|
setTimeout(() => {
|
|
6090
6142
|
var _a;
|
|
6091
6143
|
(_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(cachedEntry.data));
|
|
6092
6144
|
}, 0);
|
|
6093
6145
|
}
|
|
6094
|
-
// Get or create connection for this routing target (room-scoped when a
|
|
6095
|
-
// room route is supplied by the live helper, else the app-level connection).
|
|
6096
|
-
const connection = await getOrCreateConnection(effectiveAppId, config.isServer, roomRoutePath, authTokenProvider, principalKey);
|
|
6097
6146
|
// Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
|
|
6098
6147
|
const shapeKey = subscriptionOptions.shape ? JSON.stringify(subscriptionOptions.shape) : '';
|
|
6099
6148
|
const filterKey = subscriptionOptions.filter ? JSON.stringify(subscriptionOptions.filter) : '';
|
|
@@ -6112,10 +6161,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6112
6161
|
}
|
|
6113
6162
|
}
|
|
6114
6163
|
if (existingSubscription) {
|
|
6164
|
+
if (responseCacheEnabled) {
|
|
6165
|
+
existingSubscription.cache = true;
|
|
6166
|
+
}
|
|
6115
6167
|
// Add callback to existing subscription
|
|
6116
6168
|
existingSubscription.callbacks.push(subscriptionOptions);
|
|
6117
|
-
// Deliver last known data immediately
|
|
6118
|
-
|
|
6169
|
+
// Deliver last known data immediately only for explicit, principal-bound
|
|
6170
|
+
// cache opt-in. Otherwise joining an existing subscription can replay a
|
|
6171
|
+
// stale private snapshot without a fresh server auth result.
|
|
6172
|
+
if (responseCacheEnabled &&
|
|
6173
|
+
existingSubscription.cache &&
|
|
6174
|
+
existingSubscription.lastData !== undefined &&
|
|
6175
|
+
subscriptionOptions.onData) {
|
|
6119
6176
|
setTimeout(() => {
|
|
6120
6177
|
var _a;
|
|
6121
6178
|
(_a = subscriptionOptions.onData) === null || _a === void 0 ? void 0 : _a.call(subscriptionOptions, addIdsToSubscriptionData(existingSubscription.lastData));
|
|
@@ -6139,6 +6196,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6139
6196
|
includeSubPaths: (_b = subscriptionOptions.includeSubPaths) !== null && _b !== void 0 ? _b : false,
|
|
6140
6197
|
callbacks: [subscriptionOptions],
|
|
6141
6198
|
lastData: undefined,
|
|
6199
|
+
cache: responseCacheEnabled,
|
|
6142
6200
|
identity,
|
|
6143
6201
|
};
|
|
6144
6202
|
connection.subscriptions.set(subscriptionId, subscription);
|
|
@@ -6270,19 +6328,21 @@ function clearCacheV2(path) {
|
|
|
6270
6328
|
*/
|
|
6271
6329
|
function getCachedDataV2(path, prompt) {
|
|
6272
6330
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
6273
|
-
// H1: response caches are identity-scoped (`<identity>|<path>:...`)
|
|
6274
|
-
// the caller's identity from an active
|
|
6275
|
-
//
|
|
6276
|
-
// bucket (which would wrongly return null). No matching sub → null (safe).
|
|
6331
|
+
// H1: response caches are identity-scoped (`<identity>|<path>:...`) and
|
|
6332
|
+
// opt-in. Resolve the caller's identity from an active cache-enabled
|
|
6333
|
+
// subscription for this path. No matching sub → null (safe).
|
|
6277
6334
|
let identity;
|
|
6278
6335
|
outer: for (const connection of connections.values()) {
|
|
6279
6336
|
for (const sub of connection.subscriptions.values()) {
|
|
6280
|
-
if (sub.path === normalizedPath && sub.prompt === prompt) {
|
|
6337
|
+
if (sub.cache && sub.path === normalizedPath && sub.prompt === prompt) {
|
|
6281
6338
|
identity = sub.identity;
|
|
6282
6339
|
break outer;
|
|
6283
6340
|
}
|
|
6284
6341
|
}
|
|
6285
6342
|
}
|
|
6343
|
+
if (!identity) {
|
|
6344
|
+
return null;
|
|
6345
|
+
}
|
|
6286
6346
|
const cacheKey = getCacheKey(path, prompt, undefined, undefined, undefined, undefined, identity);
|
|
6287
6347
|
const cachedEntry = responseCache.get(cacheKey);
|
|
6288
6348
|
if (cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL) {
|
|
@@ -6314,6 +6374,7 @@ async function reconnectWithNewAuthV2() {
|
|
|
6314
6374
|
}
|
|
6315
6375
|
}
|
|
6316
6376
|
async function doReconnectWithNewAuth() {
|
|
6377
|
+
ambientAuthFailure = null;
|
|
6317
6378
|
// SECURITY (H1): the logged-in identity is changing (login / logout / switch).
|
|
6318
6379
|
// Wipe ALL principal-scoped read caches so the new identity can never observe
|
|
6319
6380
|
// data cached for the previous one. Clear both the WS response cache and the
|
|
@@ -6335,7 +6396,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6335
6396
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6336
6397
|
}
|
|
6337
6398
|
try {
|
|
6338
|
-
const { reconnectRealtimeStoreWithNewAuth } = await
|
|
6399
|
+
const { reconnectRealtimeStoreWithNewAuth } = await import('./realtime-store-D3t7PyZl.mjs');
|
|
6339
6400
|
await reconnectRealtimeStoreWithNewAuth();
|
|
6340
6401
|
}
|
|
6341
6402
|
catch (error) {
|
|
@@ -6355,6 +6416,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6355
6416
|
connection.pendingUnsubscriptions.clear();
|
|
6356
6417
|
// Reset auth failure counter — this is a proactive reconnect (login, token refresh)
|
|
6357
6418
|
connection.consecutiveAuthFailures = 0;
|
|
6419
|
+
connection.authFailure = null;
|
|
6358
6420
|
// Close the WebSocket (this triggers reconnection in ReconnectingWebSocket)
|
|
6359
6421
|
// We use reconnect() which will close and re-open with fresh URL (including new token)
|
|
6360
6422
|
try {
|
|
@@ -6365,7 +6427,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6365
6427
|
}
|
|
6366
6428
|
}
|
|
6367
6429
|
}
|
|
6368
|
-
// ============
|
|
6430
|
+
// ============ WebSocket request helpers ============
|
|
6369
6431
|
const WS_REQUEST_TIMEOUT_MS = 30000;
|
|
6370
6432
|
function generateRequestId() {
|
|
6371
6433
|
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -6381,104 +6443,6 @@ function hasActiveConnection() {
|
|
|
6381
6443
|
}
|
|
6382
6444
|
return false;
|
|
6383
6445
|
}
|
|
6384
|
-
async function waitForConnectionAuthenticated(connection) {
|
|
6385
|
-
if (!connection.isAuthenticating || !connection.ws)
|
|
6386
|
-
return;
|
|
6387
|
-
const ws = connection.ws;
|
|
6388
|
-
await new Promise((resolve, reject) => {
|
|
6389
|
-
let timeout;
|
|
6390
|
-
let cleanup = () => { };
|
|
6391
|
-
const onMessage = (event) => {
|
|
6392
|
-
try {
|
|
6393
|
-
const message = JSON.parse(event.data);
|
|
6394
|
-
if ((message === null || message === void 0 ? void 0 : message.type) === 'authenticated') {
|
|
6395
|
-
cleanup();
|
|
6396
|
-
resolve();
|
|
6397
|
-
}
|
|
6398
|
-
}
|
|
6399
|
-
catch (_a) {
|
|
6400
|
-
// Other frames are handled by the main listener.
|
|
6401
|
-
}
|
|
6402
|
-
};
|
|
6403
|
-
const onClose = () => {
|
|
6404
|
-
cleanup();
|
|
6405
|
-
reject(new Error('WebSocket disconnected during authentication'));
|
|
6406
|
-
};
|
|
6407
|
-
const onError = () => {
|
|
6408
|
-
cleanup();
|
|
6409
|
-
reject(new Error('WebSocket authentication failed'));
|
|
6410
|
-
};
|
|
6411
|
-
cleanup = () => {
|
|
6412
|
-
clearTimeout(timeout);
|
|
6413
|
-
ws.removeEventListener('message', onMessage);
|
|
6414
|
-
ws.removeEventListener('close', onClose);
|
|
6415
|
-
ws.removeEventListener('error', onError);
|
|
6416
|
-
};
|
|
6417
|
-
timeout = setTimeout(() => {
|
|
6418
|
-
cleanup();
|
|
6419
|
-
reject(new Error('WebSocket authentication timeout'));
|
|
6420
|
-
}, 10000);
|
|
6421
|
-
if (!connection.isAuthenticating) {
|
|
6422
|
-
cleanup();
|
|
6423
|
-
resolve();
|
|
6424
|
-
return;
|
|
6425
|
-
}
|
|
6426
|
-
ws.addEventListener('message', onMessage);
|
|
6427
|
-
ws.addEventListener('close', onClose);
|
|
6428
|
-
ws.addEventListener('error', onError);
|
|
6429
|
-
});
|
|
6430
|
-
}
|
|
6431
|
-
async function sendRequest(msgBuilder) {
|
|
6432
|
-
const config = await getConfig();
|
|
6433
|
-
const appId = config.appId;
|
|
6434
|
-
const connection = await getOrCreateConnection(appId, config.isServer);
|
|
6435
|
-
// Wait for the connection to be open (getOrCreateConnection may return
|
|
6436
|
-
// while still connecting).
|
|
6437
|
-
if (!connection.isConnected && connection.ws) {
|
|
6438
|
-
await new Promise((resolve, reject) => {
|
|
6439
|
-
const timeout = setTimeout(() => {
|
|
6440
|
-
var _a;
|
|
6441
|
-
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.removeEventListener('open', onOpen);
|
|
6442
|
-
reject(new Error('WebSocket connection timeout'));
|
|
6443
|
-
}, 10000);
|
|
6444
|
-
const onOpen = () => { clearTimeout(timeout); resolve(); };
|
|
6445
|
-
if (connection.isConnected) {
|
|
6446
|
-
clearTimeout(timeout);
|
|
6447
|
-
resolve();
|
|
6448
|
-
return;
|
|
6449
|
-
}
|
|
6450
|
-
connection.ws.addEventListener('open', onOpen);
|
|
6451
|
-
});
|
|
6452
|
-
}
|
|
6453
|
-
if (!connection.ws || !connection.isConnected) {
|
|
6454
|
-
throw new Error('WebSocket connection not available');
|
|
6455
|
-
}
|
|
6456
|
-
await waitForConnectionAuthenticated(connection);
|
|
6457
|
-
const requestId = generateRequestId();
|
|
6458
|
-
const message = msgBuilder(requestId);
|
|
6459
|
-
return new Promise((resolve, reject) => {
|
|
6460
|
-
const timer = setTimeout(() => {
|
|
6461
|
-
connection.pendingRequests.delete(requestId);
|
|
6462
|
-
reject(new Error(`WebSocket request timed out after ${WS_REQUEST_TIMEOUT_MS}ms`));
|
|
6463
|
-
}, WS_REQUEST_TIMEOUT_MS);
|
|
6464
|
-
connection.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
6465
|
-
try {
|
|
6466
|
-
connection.ws.send(JSON.stringify(message));
|
|
6467
|
-
}
|
|
6468
|
-
catch (error) {
|
|
6469
|
-
connection.pendingRequests.delete(requestId);
|
|
6470
|
-
clearTimeout(timer);
|
|
6471
|
-
reject(error);
|
|
6472
|
-
}
|
|
6473
|
-
});
|
|
6474
|
-
}
|
|
6475
|
-
async function wsGet(path) {
|
|
6476
|
-
return sendRequest((requestId) => ({
|
|
6477
|
-
type: 'get',
|
|
6478
|
-
requestId,
|
|
6479
|
-
path,
|
|
6480
|
-
}));
|
|
6481
|
-
}
|
|
6482
6446
|
/**
|
|
6483
6447
|
* Send a live-room intent over the EXISTING per-room socket (fire-and-forget).
|
|
6484
6448
|
* Returns true if it was sent over an open connection, false if there is no
|
|
@@ -6537,31 +6501,6 @@ function wsIntentReliable(appId, roomRoutePath, intent) {
|
|
|
6537
6501
|
}
|
|
6538
6502
|
});
|
|
6539
6503
|
}
|
|
6540
|
-
async function wsSet(documents) {
|
|
6541
|
-
return sendRequest((requestId) => ({
|
|
6542
|
-
type: 'set',
|
|
6543
|
-
requestId,
|
|
6544
|
-
documents,
|
|
6545
|
-
}));
|
|
6546
|
-
}
|
|
6547
|
-
async function wsQuery(path, opts) {
|
|
6548
|
-
return sendRequest((requestId) => (Object.assign(Object.assign(Object.assign(Object.assign({ type: 'query', requestId,
|
|
6549
|
-
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 } : {}))));
|
|
6550
|
-
}
|
|
6551
|
-
async function wsDelete(path) {
|
|
6552
|
-
return sendRequest((requestId) => ({
|
|
6553
|
-
type: 'delete',
|
|
6554
|
-
requestId,
|
|
6555
|
-
path,
|
|
6556
|
-
}));
|
|
6557
|
-
}
|
|
6558
|
-
async function wsGetMany(paths) {
|
|
6559
|
-
return sendRequest((requestId) => ({
|
|
6560
|
-
type: 'getMany',
|
|
6561
|
-
requestId,
|
|
6562
|
-
paths,
|
|
6563
|
-
}));
|
|
6564
|
-
}
|
|
6565
6504
|
|
|
6566
6505
|
/**
|
|
6567
6506
|
* WebSocket Subscription Module
|
|
@@ -6742,1008 +6681,6 @@ function toMillis(seconds) {
|
|
|
6742
6681
|
return seconds * 1000;
|
|
6743
6682
|
}
|
|
6744
6683
|
|
|
6745
|
-
// ---------------------------------------------------------------------------
|
|
6746
|
-
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
6747
|
-
//
|
|
6748
|
-
// Manages: WS connection, in-memory state, IDB persistence, optimistic
|
|
6749
|
-
// writes, delta accumulation, loading states, ephemeral/durable tiers.
|
|
6750
|
-
// ---------------------------------------------------------------------------
|
|
6751
|
-
// ---------------------------------------------------------------------------
|
|
6752
|
-
// IDB helpers (lazy-loaded, non-blocking)
|
|
6753
|
-
// ---------------------------------------------------------------------------
|
|
6754
|
-
const IDB_NAME = 'bounded-realtime';
|
|
6755
|
-
const IDB_STORE = 'subscriptions';
|
|
6756
|
-
const IDB_VERSION = 1;
|
|
6757
|
-
let idbPromise = null;
|
|
6758
|
-
function getIDB() {
|
|
6759
|
-
if (idbPromise)
|
|
6760
|
-
return idbPromise;
|
|
6761
|
-
if (typeof indexedDB === 'undefined') {
|
|
6762
|
-
return Promise.reject(new Error('IndexedDB not available'));
|
|
6763
|
-
}
|
|
6764
|
-
idbPromise = new Promise((resolve, reject) => {
|
|
6765
|
-
const req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
|
6766
|
-
req.onupgradeneeded = () => {
|
|
6767
|
-
const db = req.result;
|
|
6768
|
-
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
|
6769
|
-
db.createObjectStore(IDB_STORE);
|
|
6770
|
-
}
|
|
6771
|
-
};
|
|
6772
|
-
req.onsuccess = () => resolve(req.result);
|
|
6773
|
-
req.onerror = () => reject(req.error);
|
|
6774
|
-
});
|
|
6775
|
-
return idbPromise;
|
|
6776
|
-
}
|
|
6777
|
-
async function idbGet(key) {
|
|
6778
|
-
try {
|
|
6779
|
-
const db = await getIDB();
|
|
6780
|
-
return new Promise((resolve) => {
|
|
6781
|
-
const tx = db.transaction(IDB_STORE, 'readonly');
|
|
6782
|
-
const store = tx.objectStore(IDB_STORE);
|
|
6783
|
-
const req = store.get(key);
|
|
6784
|
-
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
6785
|
-
req.onerror = () => resolve(null);
|
|
6786
|
-
});
|
|
6787
|
-
}
|
|
6788
|
-
catch (_a) {
|
|
6789
|
-
return null;
|
|
6790
|
-
}
|
|
6791
|
-
}
|
|
6792
|
-
async function idbSet(key, value) {
|
|
6793
|
-
try {
|
|
6794
|
-
const db = await getIDB();
|
|
6795
|
-
return new Promise((resolve) => {
|
|
6796
|
-
const tx = db.transaction(IDB_STORE, 'readwrite');
|
|
6797
|
-
const store = tx.objectStore(IDB_STORE);
|
|
6798
|
-
store.put(value, key);
|
|
6799
|
-
tx.oncomplete = () => resolve();
|
|
6800
|
-
tx.onerror = () => resolve();
|
|
6801
|
-
});
|
|
6802
|
-
}
|
|
6803
|
-
catch (_a) {
|
|
6804
|
-
// Best-effort persistence
|
|
6805
|
-
}
|
|
6806
|
-
}
|
|
6807
|
-
// ---------------------------------------------------------------------------
|
|
6808
|
-
// RealtimeStore
|
|
6809
|
-
// ---------------------------------------------------------------------------
|
|
6810
|
-
let nextRequestId = 1;
|
|
6811
|
-
function hashForKey(value) {
|
|
6812
|
-
let h = 5381;
|
|
6813
|
-
for (let i = 0; i < value.length; i++) {
|
|
6814
|
-
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
6815
|
-
}
|
|
6816
|
-
return h.toString(36);
|
|
6817
|
-
}
|
|
6818
|
-
function principalFromToken(token) {
|
|
6819
|
-
return token ? `t${hashForKey(token)}` : 'anon';
|
|
6820
|
-
}
|
|
6821
|
-
class RealtimeStore {
|
|
6822
|
-
constructor() {
|
|
6823
|
-
this.ws = null;
|
|
6824
|
-
this.wsUrl = '';
|
|
6825
|
-
this.appId = '';
|
|
6826
|
-
this.subscriptions = new Map();
|
|
6827
|
-
this.pendingRequests = new Map();
|
|
6828
|
-
this.connectPromise = null;
|
|
6829
|
-
this.reconnectTimer = null;
|
|
6830
|
-
this.reconnectDelay = 1000;
|
|
6831
|
-
this.maxReconnectDelay = 30000;
|
|
6832
|
-
this.idbFlushTimer = null;
|
|
6833
|
-
this.idbDirtyKeys = new Set();
|
|
6834
|
-
this.closed = false;
|
|
6835
|
-
this.authToken = null;
|
|
6836
|
-
this.authPrincipalKey = 'anon';
|
|
6837
|
-
this.authenticating = false;
|
|
6838
|
-
this.suppressNextReconnect = false;
|
|
6839
|
-
this.isServer = false;
|
|
6840
|
-
this.tokenRefreshTimer = null;
|
|
6841
|
-
// -----------------------------------------------------------------------
|
|
6842
|
-
// WebSocket connection
|
|
6843
|
-
// -----------------------------------------------------------------------
|
|
6844
|
-
this.initPromise = null;
|
|
6845
|
-
}
|
|
6846
|
-
// -----------------------------------------------------------------------
|
|
6847
|
-
// Initialization
|
|
6848
|
-
// -----------------------------------------------------------------------
|
|
6849
|
-
async init() {
|
|
6850
|
-
const config = await getConfig();
|
|
6851
|
-
this.appId = config.appId;
|
|
6852
|
-
this.wsUrl = config.wsApiUrl;
|
|
6853
|
-
this.isServer = config.isServer;
|
|
6854
|
-
await this.refreshToken();
|
|
6855
|
-
this.startTokenRefresh();
|
|
6856
|
-
}
|
|
6857
|
-
async refreshToken() {
|
|
6858
|
-
let token = null;
|
|
6859
|
-
try {
|
|
6860
|
-
const { getIdToken } = await Promise.resolve().then(function () { return utils; });
|
|
6861
|
-
token = await getIdToken(this.isServer);
|
|
6862
|
-
}
|
|
6863
|
-
catch ( /* no auth available */_a) { /* no auth available */ }
|
|
6864
|
-
this.authToken = token !== null && token !== void 0 ? token : null;
|
|
6865
|
-
this.authPrincipalKey = principalFromToken(this.authToken);
|
|
6866
|
-
}
|
|
6867
|
-
startTokenRefresh() {
|
|
6868
|
-
if (this.tokenRefreshTimer)
|
|
6869
|
-
return;
|
|
6870
|
-
this.tokenRefreshTimer = setInterval(async () => {
|
|
6871
|
-
const prevPrincipal = this.authPrincipalKey;
|
|
6872
|
-
await this.refreshToken();
|
|
6873
|
-
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6874
|
-
await this.applyAuthPrincipalChange();
|
|
6875
|
-
if (this.subscriptions.size > 0) {
|
|
6876
|
-
await this.ensureConnected().catch(() => {
|
|
6877
|
-
this.setAllSubscriptionStatus('error');
|
|
6878
|
-
});
|
|
6879
|
-
}
|
|
6880
|
-
}
|
|
6881
|
-
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
6882
|
-
}
|
|
6883
|
-
async ensureInitialized() {
|
|
6884
|
-
if (this.appId)
|
|
6885
|
-
return;
|
|
6886
|
-
if (!this.initPromise)
|
|
6887
|
-
this.initPromise = this.init();
|
|
6888
|
-
await this.initPromise;
|
|
6889
|
-
}
|
|
6890
|
-
async ensureCurrentAuth() {
|
|
6891
|
-
await this.ensureInitialized();
|
|
6892
|
-
const prevPrincipal = this.authPrincipalKey;
|
|
6893
|
-
await this.refreshToken();
|
|
6894
|
-
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6895
|
-
await this.applyAuthPrincipalChange();
|
|
6896
|
-
}
|
|
6897
|
-
}
|
|
6898
|
-
rekeySubscriptionsForPrincipal() {
|
|
6899
|
-
const subs = Array.from(this.subscriptions.values());
|
|
6900
|
-
this.subscriptions.clear();
|
|
6901
|
-
for (const sub of subs) {
|
|
6902
|
-
this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
|
|
6903
|
-
}
|
|
6904
|
-
}
|
|
6905
|
-
async applyAuthPrincipalChange() {
|
|
6906
|
-
if (this.idbFlushTimer) {
|
|
6907
|
-
clearTimeout(this.idbFlushTimer);
|
|
6908
|
-
this.idbFlushTimer = null;
|
|
6909
|
-
}
|
|
6910
|
-
this.idbDirtyKeys.clear();
|
|
6911
|
-
this.rekeySubscriptionsForPrincipal();
|
|
6912
|
-
for (const sub of this.subscriptions.values()) {
|
|
6913
|
-
sub.docs.clear();
|
|
6914
|
-
sub.ref.current = sub.docs;
|
|
6915
|
-
sub.error = null;
|
|
6916
|
-
sub.isStale = false;
|
|
6917
|
-
let loaded = false;
|
|
6918
|
-
if (sub.tier !== 'ephemeral') {
|
|
6919
|
-
const cached = await idbGet(this.idbKey(sub.path));
|
|
6920
|
-
if (cached && Array.isArray(cached)) {
|
|
6921
|
-
for (const doc of cached) {
|
|
6922
|
-
if (doc && doc._id)
|
|
6923
|
-
sub.docs.set(doc._id, doc);
|
|
6924
|
-
}
|
|
6925
|
-
sub.ref.current = sub.docs;
|
|
6926
|
-
loaded = sub.docs.size > 0;
|
|
6927
|
-
}
|
|
6928
|
-
}
|
|
6929
|
-
sub.status = loaded ? 'cached' : 'loading';
|
|
6930
|
-
sub.isStale = loaded;
|
|
6931
|
-
if (loaded)
|
|
6932
|
-
this.notifySubscription(sub);
|
|
6933
|
-
else
|
|
6934
|
-
this.notifyState(sub);
|
|
6935
|
-
}
|
|
6936
|
-
if (this.ws) {
|
|
6937
|
-
const ws = this.ws;
|
|
6938
|
-
this.ws = null;
|
|
6939
|
-
this.connectPromise = null;
|
|
6940
|
-
this.suppressNextReconnect = true;
|
|
6941
|
-
try {
|
|
6942
|
-
ws.close(1000, 'Auth changed');
|
|
6943
|
-
}
|
|
6944
|
-
catch ( /* ignore */_a) { /* ignore */ }
|
|
6945
|
-
}
|
|
6946
|
-
}
|
|
6947
|
-
async ensureConnected() {
|
|
6948
|
-
var _a;
|
|
6949
|
-
await this.ensureCurrentAuth();
|
|
6950
|
-
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
|
|
6951
|
-
return;
|
|
6952
|
-
if (this.connectPromise)
|
|
6953
|
-
return this.connectPromise;
|
|
6954
|
-
this.connectPromise = this.connect();
|
|
6955
|
-
return this.connectPromise;
|
|
6956
|
-
}
|
|
6957
|
-
connect() {
|
|
6958
|
-
return new Promise((resolve, reject) => {
|
|
6959
|
-
if (this.closed) {
|
|
6960
|
-
reject(new Error('Store closed'));
|
|
6961
|
-
return;
|
|
6962
|
-
}
|
|
6963
|
-
const params = new URLSearchParams();
|
|
6964
|
-
params.set('apiKey', this.appId);
|
|
6965
|
-
const url = `${this.wsUrl}?${params.toString()}`;
|
|
6966
|
-
const ws = new WebSocket(url);
|
|
6967
|
-
this.ws = ws;
|
|
6968
|
-
let authTimer = null;
|
|
6969
|
-
const finishConnected = () => {
|
|
6970
|
-
if (authTimer) {
|
|
6971
|
-
clearTimeout(authTimer);
|
|
6972
|
-
authTimer = null;
|
|
6973
|
-
}
|
|
6974
|
-
this.authenticating = false;
|
|
6975
|
-
ws.removeEventListener('error', onError);
|
|
6976
|
-
this.reconnectDelay = 1000;
|
|
6977
|
-
this.connectPromise = null;
|
|
6978
|
-
this.resubscribeAll();
|
|
6979
|
-
resolve();
|
|
6980
|
-
};
|
|
6981
|
-
const onOpen = () => {
|
|
6982
|
-
if (!this.authToken) {
|
|
6983
|
-
finishConnected();
|
|
6984
|
-
return;
|
|
6985
|
-
}
|
|
6986
|
-
this.authenticating = true;
|
|
6987
|
-
authTimer = setTimeout(() => {
|
|
6988
|
-
this.authenticating = false;
|
|
6989
|
-
this.connectPromise = null;
|
|
6990
|
-
try {
|
|
6991
|
-
ws.close(1008, 'Authentication timeout');
|
|
6992
|
-
}
|
|
6993
|
-
catch ( /* ignore */_a) { /* ignore */ }
|
|
6994
|
-
reject(new Error('WebSocket authentication timeout'));
|
|
6995
|
-
}, 10000);
|
|
6996
|
-
try {
|
|
6997
|
-
ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));
|
|
6998
|
-
}
|
|
6999
|
-
catch (e) {
|
|
7000
|
-
if (authTimer)
|
|
7001
|
-
clearTimeout(authTimer);
|
|
7002
|
-
this.authenticating = false;
|
|
7003
|
-
this.connectPromise = null;
|
|
7004
|
-
reject(e);
|
|
7005
|
-
}
|
|
7006
|
-
};
|
|
7007
|
-
const onError = (e) => {
|
|
7008
|
-
if (authTimer)
|
|
7009
|
-
clearTimeout(authTimer);
|
|
7010
|
-
this.authenticating = false;
|
|
7011
|
-
ws.removeEventListener('open', onOpen);
|
|
7012
|
-
this.connectPromise = null;
|
|
7013
|
-
reject(new Error('WebSocket connection failed'));
|
|
7014
|
-
};
|
|
7015
|
-
ws.addEventListener('open', onOpen, { once: true });
|
|
7016
|
-
ws.addEventListener('error', onError, { once: true });
|
|
7017
|
-
ws.addEventListener('message', (event) => {
|
|
7018
|
-
if (this.authenticating) {
|
|
7019
|
-
try {
|
|
7020
|
-
const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));
|
|
7021
|
-
if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {
|
|
7022
|
-
finishConnected();
|
|
7023
|
-
return;
|
|
7024
|
-
}
|
|
7025
|
-
}
|
|
7026
|
-
catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }
|
|
7027
|
-
}
|
|
7028
|
-
this.handleMessage(event.data);
|
|
7029
|
-
});
|
|
7030
|
-
ws.addEventListener('close', () => {
|
|
7031
|
-
if (authTimer)
|
|
7032
|
-
clearTimeout(authTimer);
|
|
7033
|
-
if (this.ws !== ws) {
|
|
7034
|
-
if (this.suppressNextReconnect)
|
|
7035
|
-
this.suppressNextReconnect = false;
|
|
7036
|
-
return;
|
|
7037
|
-
}
|
|
7038
|
-
this.authenticating = false;
|
|
7039
|
-
this.ws = null;
|
|
7040
|
-
this.connectPromise = null;
|
|
7041
|
-
this.rejectAllPending('WebSocket closed');
|
|
7042
|
-
this.setAllSubscriptionStatus('reconnecting');
|
|
7043
|
-
if (this.suppressNextReconnect) {
|
|
7044
|
-
this.suppressNextReconnect = false;
|
|
7045
|
-
return;
|
|
7046
|
-
}
|
|
7047
|
-
this.scheduleReconnect();
|
|
7048
|
-
});
|
|
7049
|
-
});
|
|
7050
|
-
}
|
|
7051
|
-
scheduleReconnect() {
|
|
7052
|
-
if (this.closed)
|
|
7053
|
-
return;
|
|
7054
|
-
if (this.reconnectTimer)
|
|
7055
|
-
clearTimeout(this.reconnectTimer);
|
|
7056
|
-
this.reconnectTimer = setTimeout(() => {
|
|
7057
|
-
this.ensureConnected().catch(() => {
|
|
7058
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
7059
|
-
this.scheduleReconnect();
|
|
7060
|
-
});
|
|
7061
|
-
}, this.reconnectDelay);
|
|
7062
|
-
}
|
|
7063
|
-
resubscribeAll() {
|
|
7064
|
-
for (const sub of this.subscriptions.values()) {
|
|
7065
|
-
this.sendSubscribe(sub);
|
|
7066
|
-
}
|
|
7067
|
-
}
|
|
7068
|
-
// -----------------------------------------------------------------------
|
|
7069
|
-
// Message handling
|
|
7070
|
-
// -----------------------------------------------------------------------
|
|
7071
|
-
handleMessage(raw) {
|
|
7072
|
-
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
7073
|
-
let msg;
|
|
7074
|
-
try {
|
|
7075
|
-
msg = JSON.parse(text);
|
|
7076
|
-
}
|
|
7077
|
-
catch (_a) {
|
|
7078
|
-
return;
|
|
7079
|
-
}
|
|
7080
|
-
switch (msg.type) {
|
|
7081
|
-
case 'snapshot':
|
|
7082
|
-
this.handleSnapshot(msg);
|
|
7083
|
-
break;
|
|
7084
|
-
case 'delta':
|
|
7085
|
-
this.handleDelta(msg);
|
|
7086
|
-
break;
|
|
7087
|
-
case 'result':
|
|
7088
|
-
this.handleResult(msg);
|
|
7089
|
-
break;
|
|
7090
|
-
case 'error':
|
|
7091
|
-
this.handleError(msg);
|
|
7092
|
-
break;
|
|
7093
|
-
case 'pong':
|
|
7094
|
-
break;
|
|
7095
|
-
case 'authenticated':
|
|
7096
|
-
break;
|
|
7097
|
-
// v1 compat: handle legacy message types during transition
|
|
7098
|
-
case 'subscribed':
|
|
7099
|
-
this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
|
|
7100
|
-
break;
|
|
7101
|
-
case 'data':
|
|
7102
|
-
// Legacy full-snapshot delta — treat as snapshot replacement
|
|
7103
|
-
this.handleLegacyData(msg);
|
|
7104
|
-
break;
|
|
7105
|
-
case 'response':
|
|
7106
|
-
this.handleResult(Object.assign(Object.assign({}, msg), { type: 'result', ok: msg.status === 200, doc: msg.data }));
|
|
7107
|
-
break;
|
|
7108
|
-
}
|
|
7109
|
-
}
|
|
7110
|
-
handleSnapshot(msg) {
|
|
7111
|
-
var _a, _b, _c;
|
|
7112
|
-
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
7113
|
-
if (!subId)
|
|
7114
|
-
return;
|
|
7115
|
-
const sub = this.findSubscriptionById(subId);
|
|
7116
|
-
if (!sub)
|
|
7117
|
-
return;
|
|
7118
|
-
const docs = (_c = (_b = msg.docs) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : [];
|
|
7119
|
-
const docsArray = Array.isArray(docs) ? docs : [docs];
|
|
7120
|
-
sub.docs.clear();
|
|
7121
|
-
for (const doc of docsArray) {
|
|
7122
|
-
if (doc && doc._id) {
|
|
7123
|
-
sub.docs.set(doc._id, doc);
|
|
7124
|
-
}
|
|
7125
|
-
}
|
|
7126
|
-
sub.ref.current = sub.docs;
|
|
7127
|
-
sub.status = 'live';
|
|
7128
|
-
sub.isStale = false;
|
|
7129
|
-
sub.error = null;
|
|
7130
|
-
this.notifySubscription(sub);
|
|
7131
|
-
this.markIdbDirty(sub.path);
|
|
7132
|
-
}
|
|
7133
|
-
handleDelta(msg) {
|
|
7134
|
-
var _a, _b;
|
|
7135
|
-
const subId = (_a = msg.id) !== null && _a !== void 0 ? _a : msg.subscriptionId;
|
|
7136
|
-
if (!subId)
|
|
7137
|
-
return;
|
|
7138
|
-
const sub = this.findSubscriptionById(subId);
|
|
7139
|
-
if (!sub)
|
|
7140
|
-
return;
|
|
7141
|
-
if (sub.tier === 'ephemeral') {
|
|
7142
|
-
// Ephemeral: just overwrite, no accumulation logic
|
|
7143
|
-
if (msg.change === 'removed' && msg.docId) {
|
|
7144
|
-
sub.docs.delete(msg.docId);
|
|
7145
|
-
}
|
|
7146
|
-
else if (msg.doc && msg.doc._id) {
|
|
7147
|
-
sub.docs.set(msg.doc._id, msg.doc);
|
|
7148
|
-
}
|
|
7149
|
-
sub.ref.current = sub.docs;
|
|
7150
|
-
if (sub.options.mode !== 'ref') {
|
|
7151
|
-
this.notifySubscription(sub);
|
|
7152
|
-
}
|
|
7153
|
-
return;
|
|
7154
|
-
}
|
|
7155
|
-
// Durable/checkpointed: full delta handling
|
|
7156
|
-
switch (msg.change) {
|
|
7157
|
-
case 'added':
|
|
7158
|
-
case 'modified':
|
|
7159
|
-
if (msg.doc && msg.doc._id) {
|
|
7160
|
-
sub.docs.set(msg.doc._id, msg.doc);
|
|
7161
|
-
}
|
|
7162
|
-
break;
|
|
7163
|
-
case 'removed':
|
|
7164
|
-
if (msg.docId) {
|
|
7165
|
-
sub.docs.delete(msg.docId);
|
|
7166
|
-
}
|
|
7167
|
-
else if ((_b = msg.doc) === null || _b === void 0 ? void 0 : _b._id) {
|
|
7168
|
-
sub.docs.delete(msg.doc._id);
|
|
7169
|
-
}
|
|
7170
|
-
break;
|
|
7171
|
-
}
|
|
7172
|
-
sub.ref.current = sub.docs;
|
|
7173
|
-
this.notifySubscription(sub);
|
|
7174
|
-
this.markIdbDirty(sub.path);
|
|
7175
|
-
}
|
|
7176
|
-
handleLegacyData(msg) {
|
|
7177
|
-
// Legacy v1 format: 'data' message with full snapshot or single doc
|
|
7178
|
-
const subId = msg.subscriptionId;
|
|
7179
|
-
if (!subId)
|
|
7180
|
-
return;
|
|
7181
|
-
const sub = this.findSubscriptionById(subId);
|
|
7182
|
-
if (!sub)
|
|
7183
|
-
return;
|
|
7184
|
-
if (Array.isArray(msg.data)) {
|
|
7185
|
-
// Full snapshot replacement
|
|
7186
|
-
sub.docs.clear();
|
|
7187
|
-
for (const doc of msg.data) {
|
|
7188
|
-
if (doc && doc._id)
|
|
7189
|
-
sub.docs.set(doc._id, doc);
|
|
7190
|
-
}
|
|
7191
|
-
}
|
|
7192
|
-
else if (msg.data && msg.data._id) {
|
|
7193
|
-
// Single doc update
|
|
7194
|
-
sub.docs.set(msg.data._id, msg.data);
|
|
7195
|
-
}
|
|
7196
|
-
else if (msg.data === null) ;
|
|
7197
|
-
sub.ref.current = sub.docs;
|
|
7198
|
-
sub.status = 'live';
|
|
7199
|
-
sub.isStale = false;
|
|
7200
|
-
this.notifySubscription(sub);
|
|
7201
|
-
this.markIdbDirty(sub.path);
|
|
7202
|
-
}
|
|
7203
|
-
handleResult(msg) {
|
|
7204
|
-
var _a, _b, _c, _d;
|
|
7205
|
-
const requestId = msg.requestId;
|
|
7206
|
-
if (!requestId)
|
|
7207
|
-
return;
|
|
7208
|
-
const pending = this.pendingRequests.get(requestId);
|
|
7209
|
-
if (!pending)
|
|
7210
|
-
return;
|
|
7211
|
-
this.pendingRequests.delete(requestId);
|
|
7212
|
-
clearTimeout(pending.timeout);
|
|
7213
|
-
const ok = (_a = msg.ok) !== null && _a !== void 0 ? _a : (msg.status === 200);
|
|
7214
|
-
if (ok) {
|
|
7215
|
-
pending.resolve((_c = (_b = msg.doc) !== null && _b !== void 0 ? _b : msg.data) !== null && _c !== void 0 ? _c : true);
|
|
7216
|
-
}
|
|
7217
|
-
else {
|
|
7218
|
-
pending.reject(new Error((_d = msg.error) !== null && _d !== void 0 ? _d : 'Operation failed'));
|
|
7219
|
-
}
|
|
7220
|
-
}
|
|
7221
|
-
handleError(msg) {
|
|
7222
|
-
var _a, _b, _c;
|
|
7223
|
-
const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
|
|
7224
|
-
if (msg.code)
|
|
7225
|
-
error.code = msg.code;
|
|
7226
|
-
if (msg.subscriptionId || msg.id)
|
|
7227
|
-
error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
|
|
7228
|
-
const requestId = msg.requestId;
|
|
7229
|
-
if (requestId) {
|
|
7230
|
-
const pending = this.pendingRequests.get(requestId);
|
|
7231
|
-
if (pending) {
|
|
7232
|
-
this.pendingRequests.delete(requestId);
|
|
7233
|
-
clearTimeout(pending.timeout);
|
|
7234
|
-
pending.reject(error);
|
|
7235
|
-
}
|
|
7236
|
-
}
|
|
7237
|
-
const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
|
|
7238
|
-
if (subId) {
|
|
7239
|
-
const sub = this.findSubscriptionById(subId);
|
|
7240
|
-
if (sub) {
|
|
7241
|
-
sub.status = 'error';
|
|
7242
|
-
sub.error = error;
|
|
7243
|
-
this.notifyState(sub);
|
|
7244
|
-
for (const callback of Array.from(sub.errorCallbacks)) {
|
|
7245
|
-
try {
|
|
7246
|
-
callback(error);
|
|
7247
|
-
}
|
|
7248
|
-
catch ( /* swallow */_d) { /* swallow */ }
|
|
7249
|
-
}
|
|
7250
|
-
}
|
|
7251
|
-
}
|
|
7252
|
-
}
|
|
7253
|
-
// -----------------------------------------------------------------------
|
|
7254
|
-
// Subscribe
|
|
7255
|
-
// -----------------------------------------------------------------------
|
|
7256
|
-
async subscribe(path, opts = {}) {
|
|
7257
|
-
var _a;
|
|
7258
|
-
await this.ensureCurrentAuth();
|
|
7259
|
-
const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
|
|
7260
|
-
const subKey = this.getSubKey(path, opts);
|
|
7261
|
-
let sub = this.subscriptions.get(subKey);
|
|
7262
|
-
if (sub) {
|
|
7263
|
-
// Existing subscription — add callback
|
|
7264
|
-
if (opts.onData)
|
|
7265
|
-
sub.callbacks.add(opts.onData);
|
|
7266
|
-
if (opts.onState)
|
|
7267
|
-
sub.stateCallbacks.add(opts.onState);
|
|
7268
|
-
if (opts.onError)
|
|
7269
|
-
sub.errorCallbacks.add(opts.onError);
|
|
7270
|
-
// Immediately deliver current state
|
|
7271
|
-
if (opts.onData && sub.docs.size > 0) {
|
|
7272
|
-
opts.onData(this.docsToArray(sub));
|
|
7273
|
-
}
|
|
7274
|
-
if (opts.onState) {
|
|
7275
|
-
opts.onState(this.getState(sub));
|
|
7276
|
-
}
|
|
7277
|
-
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7278
|
-
}
|
|
7279
|
-
// New subscription
|
|
7280
|
-
const subId = `sub_${nextRequestId++}`;
|
|
7281
|
-
sub = {
|
|
7282
|
-
id: subId,
|
|
7283
|
-
path,
|
|
7284
|
-
tier,
|
|
7285
|
-
options: opts,
|
|
7286
|
-
docs: new Map(),
|
|
7287
|
-
status: 'idle',
|
|
7288
|
-
isStale: false,
|
|
7289
|
-
error: null,
|
|
7290
|
-
callbacks: new Set(opts.onData ? [opts.onData] : []),
|
|
7291
|
-
stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
|
|
7292
|
-
errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
|
|
7293
|
-
ref: { current: new Map() },
|
|
7294
|
-
};
|
|
7295
|
-
this.subscriptions.set(subKey, sub);
|
|
7296
|
-
// Step 1: Load from IDB (durable/checkpointed only)
|
|
7297
|
-
if (tier !== 'ephemeral') {
|
|
7298
|
-
const cached = await idbGet(this.idbKey(path));
|
|
7299
|
-
if (cached && Array.isArray(cached)) {
|
|
7300
|
-
for (const doc of cached) {
|
|
7301
|
-
if (doc && doc._id)
|
|
7302
|
-
sub.docs.set(doc._id, doc);
|
|
7303
|
-
}
|
|
7304
|
-
sub.ref.current = sub.docs;
|
|
7305
|
-
sub.status = 'cached';
|
|
7306
|
-
sub.isStale = true;
|
|
7307
|
-
this.notifySubscription(sub);
|
|
7308
|
-
}
|
|
7309
|
-
}
|
|
7310
|
-
// Step 2: Connect and subscribe via WS
|
|
7311
|
-
sub.status = sub.docs.size > 0 ? 'cached' : 'loading';
|
|
7312
|
-
this.notifyState(sub);
|
|
7313
|
-
try {
|
|
7314
|
-
await this.ensureConnected();
|
|
7315
|
-
this.sendSubscribe(sub);
|
|
7316
|
-
}
|
|
7317
|
-
catch (_b) {
|
|
7318
|
-
sub.status = 'error';
|
|
7319
|
-
sub.error = new Error('Connection failed');
|
|
7320
|
-
this.notifyState(sub);
|
|
7321
|
-
}
|
|
7322
|
-
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7323
|
-
}
|
|
7324
|
-
getRef(path, opts = {}) {
|
|
7325
|
-
var _a;
|
|
7326
|
-
const subKey = this.getSubKey(path, opts);
|
|
7327
|
-
const sub = this.subscriptions.get(subKey);
|
|
7328
|
-
if (sub)
|
|
7329
|
-
return sub.ref;
|
|
7330
|
-
// Auto-subscribe in ref mode
|
|
7331
|
-
const ref = { current: new Map() };
|
|
7332
|
-
this.subscribe(path, Object.assign(Object.assign({}, opts), { mode: 'ref', tier: 'ephemeral' })).catch(() => { });
|
|
7333
|
-
const newSub = this.subscriptions.get(this.getSubKey(path, Object.assign(Object.assign({}, opts), { tier: 'ephemeral' })));
|
|
7334
|
-
return (_a = newSub === null || newSub === void 0 ? void 0 : newSub.ref) !== null && _a !== void 0 ? _a : ref;
|
|
7335
|
-
}
|
|
7336
|
-
// -----------------------------------------------------------------------
|
|
7337
|
-
// CRUD operations
|
|
7338
|
-
// -----------------------------------------------------------------------
|
|
7339
|
-
async set(path, doc) {
|
|
7340
|
-
var _a;
|
|
7341
|
-
await this.ensureConnected();
|
|
7342
|
-
// Resolve operations (Increment, Time.Now) client-side for optimistic update
|
|
7343
|
-
const resolvedDoc = this.resolveOperations(doc, path);
|
|
7344
|
-
// Optimistic update: apply to local state immediately
|
|
7345
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7346
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7347
|
-
const optimisticDoc = Object.assign(Object.assign({ _id: normalizedPath, pathId: normalizedPath }, resolvedDoc), {
|
|
7348
|
-
// System timestamp field name: the Bounded worker stamps the neutral
|
|
7349
|
-
// `_updatedAt`; the underscore-prefixed `_updated_at` metadata mirror.
|
|
7350
|
-
// Match it so the optimistic doc lines up with the server's confirmation.
|
|
7351
|
-
[isBoundedNetwork() ? '_updatedAt' : '_updated_at']: Date.now() });
|
|
7352
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7353
|
-
let prevDoc = null;
|
|
7354
|
-
if (sub) {
|
|
7355
|
-
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
7356
|
-
sub.docs.set(normalizedPath, optimisticDoc);
|
|
7357
|
-
sub.ref.current = sub.docs;
|
|
7358
|
-
this.notifySubscription(sub);
|
|
7359
|
-
}
|
|
7360
|
-
// Send to server
|
|
7361
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7362
|
-
try {
|
|
7363
|
-
const result = await this.sendRequest(requestId, {
|
|
7364
|
-
type: 'set',
|
|
7365
|
-
requestId,
|
|
7366
|
-
documents: [{ destinationPath: normalizedPath, document: doc }],
|
|
7367
|
-
});
|
|
7368
|
-
// Replace optimistic doc with server-confirmed version
|
|
7369
|
-
if (sub && result && typeof result === 'object') {
|
|
7370
|
-
const serverDoc = Array.isArray(result) ? result[0] : result;
|
|
7371
|
-
if (serverDoc && serverDoc._id) {
|
|
7372
|
-
sub.docs.set(serverDoc._id, serverDoc);
|
|
7373
|
-
sub.ref.current = sub.docs;
|
|
7374
|
-
this.notifySubscription(sub);
|
|
7375
|
-
this.markIdbDirty(collectionPath);
|
|
7376
|
-
}
|
|
7377
|
-
}
|
|
7378
|
-
return Array.isArray(result) ? result[0] : result;
|
|
7379
|
-
}
|
|
7380
|
-
catch (err) {
|
|
7381
|
-
// Revert optimistic update
|
|
7382
|
-
if (sub) {
|
|
7383
|
-
if (prevDoc) {
|
|
7384
|
-
sub.docs.set(normalizedPath, prevDoc);
|
|
7385
|
-
}
|
|
7386
|
-
else {
|
|
7387
|
-
sub.docs.delete(normalizedPath);
|
|
7388
|
-
}
|
|
7389
|
-
sub.ref.current = sub.docs;
|
|
7390
|
-
this.notifySubscription(sub);
|
|
7391
|
-
}
|
|
7392
|
-
throw err;
|
|
7393
|
-
}
|
|
7394
|
-
}
|
|
7395
|
-
async get(path) {
|
|
7396
|
-
await this.ensureCurrentAuth();
|
|
7397
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7398
|
-
// Check local subscriptions first
|
|
7399
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7400
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7401
|
-
if (sub && sub.status === 'live') {
|
|
7402
|
-
const doc = sub.docs.get(normalizedPath);
|
|
7403
|
-
return doc !== null && doc !== void 0 ? doc : null;
|
|
7404
|
-
}
|
|
7405
|
-
// One-shot WS fetch
|
|
7406
|
-
await this.ensureConnected();
|
|
7407
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7408
|
-
return this.sendRequest(requestId, {
|
|
7409
|
-
type: 'get',
|
|
7410
|
-
requestId,
|
|
7411
|
-
path: normalizedPath,
|
|
7412
|
-
});
|
|
7413
|
-
}
|
|
7414
|
-
async getMany(paths) {
|
|
7415
|
-
await this.ensureConnected();
|
|
7416
|
-
const normalizedPaths = paths.map(p => p.startsWith('/') ? p.slice(1) : p);
|
|
7417
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7418
|
-
return this.sendRequest(requestId, {
|
|
7419
|
-
type: 'getMany',
|
|
7420
|
-
requestId,
|
|
7421
|
-
paths: normalizedPaths,
|
|
7422
|
-
});
|
|
7423
|
-
}
|
|
7424
|
-
async delete(path) {
|
|
7425
|
-
var _a;
|
|
7426
|
-
await this.ensureConnected();
|
|
7427
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7428
|
-
// Optimistic: remove from local state
|
|
7429
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7430
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7431
|
-
let prevDoc = null;
|
|
7432
|
-
if (sub) {
|
|
7433
|
-
prevDoc = (_a = sub.docs.get(normalizedPath)) !== null && _a !== void 0 ? _a : null;
|
|
7434
|
-
sub.docs.delete(normalizedPath);
|
|
7435
|
-
sub.ref.current = sub.docs;
|
|
7436
|
-
this.notifySubscription(sub);
|
|
7437
|
-
}
|
|
7438
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7439
|
-
try {
|
|
7440
|
-
await this.sendRequest(requestId, {
|
|
7441
|
-
type: 'delete',
|
|
7442
|
-
requestId,
|
|
7443
|
-
path: normalizedPath,
|
|
7444
|
-
});
|
|
7445
|
-
if (sub)
|
|
7446
|
-
this.markIdbDirty(collectionPath);
|
|
7447
|
-
}
|
|
7448
|
-
catch (err) {
|
|
7449
|
-
// Revert
|
|
7450
|
-
if (sub && prevDoc) {
|
|
7451
|
-
sub.docs.set(normalizedPath, prevDoc);
|
|
7452
|
-
sub.ref.current = sub.docs;
|
|
7453
|
-
this.notifySubscription(sub);
|
|
7454
|
-
}
|
|
7455
|
-
throw err;
|
|
7456
|
-
}
|
|
7457
|
-
}
|
|
7458
|
-
async query(path, opts) {
|
|
7459
|
-
await this.ensureConnected();
|
|
7460
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7461
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7462
|
-
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 } : {})));
|
|
7463
|
-
}
|
|
7464
|
-
async count(path) {
|
|
7465
|
-
var _a;
|
|
7466
|
-
await this.ensureConnected();
|
|
7467
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7468
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7469
|
-
const result = await this.sendRequest(requestId, {
|
|
7470
|
-
type: 'count',
|
|
7471
|
-
requestId,
|
|
7472
|
-
path: normalizedPath,
|
|
7473
|
-
});
|
|
7474
|
-
return typeof result === 'number' ? result : ((_a = result === null || result === void 0 ? void 0 : result.value) !== null && _a !== void 0 ? _a : 0);
|
|
7475
|
-
}
|
|
7476
|
-
async aggregate(path, operation, opts) {
|
|
7477
|
-
await this.ensureConnected();
|
|
7478
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7479
|
-
const requestId = `r_${nextRequestId++}`;
|
|
7480
|
-
return this.sendRequest(requestId, Object.assign({ type: 'aggregate', requestId, path: normalizedPath, operation }, ((opts === null || opts === void 0 ? void 0 : opts.field) ? { field: opts.field } : {})));
|
|
7481
|
-
}
|
|
7482
|
-
// -----------------------------------------------------------------------
|
|
7483
|
-
// Helpers
|
|
7484
|
-
// -----------------------------------------------------------------------
|
|
7485
|
-
sendSubscribe(sub) {
|
|
7486
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
7487
|
-
return;
|
|
7488
|
-
const msg = {
|
|
7489
|
-
type: 'subscribe',
|
|
7490
|
-
subscriptionId: sub.id,
|
|
7491
|
-
path: sub.path,
|
|
7492
|
-
};
|
|
7493
|
-
if (sub.options.filter)
|
|
7494
|
-
msg.filter = sub.options.filter;
|
|
7495
|
-
if (sub.options.includeSubPaths)
|
|
7496
|
-
msg.includeSubPaths = true;
|
|
7497
|
-
if (sub.options.limit)
|
|
7498
|
-
msg.limit = sub.options.limit;
|
|
7499
|
-
if (sub.options.prompt)
|
|
7500
|
-
msg.prompt = sub.options.prompt;
|
|
7501
|
-
this.ws.send(JSON.stringify(msg));
|
|
7502
|
-
}
|
|
7503
|
-
sendRequest(requestId, msg) {
|
|
7504
|
-
return new Promise((resolve, reject) => {
|
|
7505
|
-
const timeout = setTimeout(() => {
|
|
7506
|
-
this.pendingRequests.delete(requestId);
|
|
7507
|
-
reject(new Error('Request timed out'));
|
|
7508
|
-
}, 30000);
|
|
7509
|
-
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
7510
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7511
|
-
this.ws.send(JSON.stringify(msg));
|
|
7512
|
-
}
|
|
7513
|
-
else {
|
|
7514
|
-
this.pendingRequests.delete(requestId);
|
|
7515
|
-
clearTimeout(timeout);
|
|
7516
|
-
reject(new Error('WebSocket not connected'));
|
|
7517
|
-
}
|
|
7518
|
-
});
|
|
7519
|
-
}
|
|
7520
|
-
notifySubscription(sub) {
|
|
7521
|
-
const data = this.docsToArray(sub);
|
|
7522
|
-
const callbacks = Array.from(sub.callbacks);
|
|
7523
|
-
for (const cb of callbacks) {
|
|
7524
|
-
try {
|
|
7525
|
-
cb(data);
|
|
7526
|
-
}
|
|
7527
|
-
catch ( /* swallow callback errors */_a) { /* swallow callback errors */ }
|
|
7528
|
-
}
|
|
7529
|
-
this.notifyState(sub);
|
|
7530
|
-
}
|
|
7531
|
-
notifyState(sub) {
|
|
7532
|
-
const state = this.getState(sub);
|
|
7533
|
-
const callbacks = Array.from(sub.stateCallbacks);
|
|
7534
|
-
for (const cb of callbacks) {
|
|
7535
|
-
try {
|
|
7536
|
-
cb(state);
|
|
7537
|
-
}
|
|
7538
|
-
catch ( /* swallow */_a) { /* swallow */ }
|
|
7539
|
-
}
|
|
7540
|
-
}
|
|
7541
|
-
getState(sub) {
|
|
7542
|
-
return {
|
|
7543
|
-
data: this.docsToArray(sub),
|
|
7544
|
-
status: sub.status,
|
|
7545
|
-
isStale: sub.isStale,
|
|
7546
|
-
error: sub.error,
|
|
7547
|
-
};
|
|
7548
|
-
}
|
|
7549
|
-
docsToArray(sub) {
|
|
7550
|
-
return Array.from(sub.docs.values());
|
|
7551
|
-
}
|
|
7552
|
-
findSubscriptionById(id) {
|
|
7553
|
-
for (const sub of this.subscriptions.values()) {
|
|
7554
|
-
if (sub.id === id)
|
|
7555
|
-
return sub;
|
|
7556
|
-
}
|
|
7557
|
-
return undefined;
|
|
7558
|
-
}
|
|
7559
|
-
findSubscriptionByPath(collectionPath) {
|
|
7560
|
-
for (const sub of this.subscriptions.values()) {
|
|
7561
|
-
const subPath = sub.path.startsWith('/') ? sub.path.slice(1) : sub.path;
|
|
7562
|
-
if (subPath === collectionPath)
|
|
7563
|
-
return sub;
|
|
7564
|
-
if (collectionPath.startsWith(subPath + '/'))
|
|
7565
|
-
return sub;
|
|
7566
|
-
}
|
|
7567
|
-
return undefined;
|
|
7568
|
-
}
|
|
7569
|
-
getCollectionPath(docPath) {
|
|
7570
|
-
const segments = docPath.split('/');
|
|
7571
|
-
if (segments.length % 2 === 0) {
|
|
7572
|
-
return segments.slice(0, -1).join('/');
|
|
7573
|
-
}
|
|
7574
|
-
return docPath;
|
|
7575
|
-
}
|
|
7576
|
-
getSubKey(path, opts) {
|
|
7577
|
-
const parts = [this.appId, this.authPrincipalKey, path];
|
|
7578
|
-
if (opts.filter)
|
|
7579
|
-
parts.push(JSON.stringify(opts.filter));
|
|
7580
|
-
if (opts.prompt)
|
|
7581
|
-
parts.push(opts.prompt);
|
|
7582
|
-
if (opts.tier)
|
|
7583
|
-
parts.push(opts.tier);
|
|
7584
|
-
return parts.join('::');
|
|
7585
|
-
}
|
|
7586
|
-
idbKey(path) {
|
|
7587
|
-
return `${this.appId}:${this.authPrincipalKey}:${path}`;
|
|
7588
|
-
}
|
|
7589
|
-
markIdbDirty(path) {
|
|
7590
|
-
const sub = this.findSubscriptionByPath(path);
|
|
7591
|
-
if (sub && sub.tier === 'ephemeral')
|
|
7592
|
-
return;
|
|
7593
|
-
this.idbDirtyKeys.add(path);
|
|
7594
|
-
if (!this.idbFlushTimer) {
|
|
7595
|
-
this.idbFlushTimer = setTimeout(() => {
|
|
7596
|
-
this.flushIdb();
|
|
7597
|
-
this.idbFlushTimer = null;
|
|
7598
|
-
}, 500);
|
|
7599
|
-
}
|
|
7600
|
-
}
|
|
7601
|
-
async flushIdb() {
|
|
7602
|
-
const keys = Array.from(this.idbDirtyKeys);
|
|
7603
|
-
this.idbDirtyKeys.clear();
|
|
7604
|
-
for (const path of keys) {
|
|
7605
|
-
const sub = this.findSubscriptionByPath(path);
|
|
7606
|
-
if (sub && sub.tier !== 'ephemeral') {
|
|
7607
|
-
const docs = this.docsToArray(sub);
|
|
7608
|
-
await idbSet(this.idbKey(path), docs);
|
|
7609
|
-
}
|
|
7610
|
-
}
|
|
7611
|
-
}
|
|
7612
|
-
createUnsubscribe(subKey, subId, onData, onState, onError) {
|
|
7613
|
-
return async () => {
|
|
7614
|
-
var _a;
|
|
7615
|
-
const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
|
|
7616
|
-
if (!sub)
|
|
7617
|
-
return;
|
|
7618
|
-
const currentSubKey = this.getSubKey(sub.path, sub.options);
|
|
7619
|
-
if (onData)
|
|
7620
|
-
sub.callbacks.delete(onData);
|
|
7621
|
-
if (onState)
|
|
7622
|
-
sub.stateCallbacks.delete(onState);
|
|
7623
|
-
if (onError)
|
|
7624
|
-
sub.errorCallbacks.delete(onError);
|
|
7625
|
-
// If no more callbacks, unsubscribe entirely
|
|
7626
|
-
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
|
|
7627
|
-
this.subscriptions.delete(subKey);
|
|
7628
|
-
this.subscriptions.delete(currentSubKey);
|
|
7629
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7630
|
-
this.ws.send(JSON.stringify({
|
|
7631
|
-
type: 'unsubscribe',
|
|
7632
|
-
subscriptionId: sub.id,
|
|
7633
|
-
}));
|
|
7634
|
-
}
|
|
7635
|
-
}
|
|
7636
|
-
};
|
|
7637
|
-
}
|
|
7638
|
-
resolveOperations(doc, path) {
|
|
7639
|
-
var _a;
|
|
7640
|
-
if (!doc || typeof doc !== 'object')
|
|
7641
|
-
return doc;
|
|
7642
|
-
const resolved = {};
|
|
7643
|
-
for (const [key, value] of Object.entries(doc)) {
|
|
7644
|
-
if (value && typeof value === 'object' && !Array.isArray(value) && value.operation) {
|
|
7645
|
-
const op = value;
|
|
7646
|
-
if (op.operation === 'time' && op.value === 'now') {
|
|
7647
|
-
resolved[key] = Math.floor(Date.now() / 1000);
|
|
7648
|
-
}
|
|
7649
|
-
else if (op.operation === 'increment') {
|
|
7650
|
-
// For optimistic: get current value and add
|
|
7651
|
-
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7652
|
-
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
7653
|
-
const sub = this.findSubscriptionByPath(collectionPath);
|
|
7654
|
-
const existing = sub === null || sub === void 0 ? void 0 : sub.docs.get(normalizedPath);
|
|
7655
|
-
const current = (_a = existing === null || existing === void 0 ? void 0 : existing[key]) !== null && _a !== void 0 ? _a : 0;
|
|
7656
|
-
resolved[key] = (typeof current === 'number' ? current : 0) + op.value;
|
|
7657
|
-
}
|
|
7658
|
-
else {
|
|
7659
|
-
resolved[key] = value;
|
|
7660
|
-
}
|
|
7661
|
-
}
|
|
7662
|
-
else {
|
|
7663
|
-
resolved[key] = value;
|
|
7664
|
-
}
|
|
7665
|
-
}
|
|
7666
|
-
return resolved;
|
|
7667
|
-
}
|
|
7668
|
-
rejectAllPending(reason) {
|
|
7669
|
-
for (const [requestId, pending] of this.pendingRequests) {
|
|
7670
|
-
clearTimeout(pending.timeout);
|
|
7671
|
-
pending.reject(new Error(reason));
|
|
7672
|
-
}
|
|
7673
|
-
this.pendingRequests.clear();
|
|
7674
|
-
}
|
|
7675
|
-
setAllSubscriptionStatus(status) {
|
|
7676
|
-
for (const sub of this.subscriptions.values()) {
|
|
7677
|
-
sub.status = status;
|
|
7678
|
-
this.notifyState(sub);
|
|
7679
|
-
}
|
|
7680
|
-
}
|
|
7681
|
-
// -----------------------------------------------------------------------
|
|
7682
|
-
// Lifecycle
|
|
7683
|
-
// -----------------------------------------------------------------------
|
|
7684
|
-
close() {
|
|
7685
|
-
this.closed = true;
|
|
7686
|
-
if (this.reconnectTimer)
|
|
7687
|
-
clearTimeout(this.reconnectTimer);
|
|
7688
|
-
if (this.idbFlushTimer)
|
|
7689
|
-
clearTimeout(this.idbFlushTimer);
|
|
7690
|
-
if (this.tokenRefreshTimer)
|
|
7691
|
-
clearInterval(this.tokenRefreshTimer);
|
|
7692
|
-
this.flushIdb();
|
|
7693
|
-
if (this.ws) {
|
|
7694
|
-
this.ws.close(1000, 'Store closed');
|
|
7695
|
-
this.ws = null;
|
|
7696
|
-
}
|
|
7697
|
-
this.rejectAllPending('Store closed');
|
|
7698
|
-
this.subscriptions.clear();
|
|
7699
|
-
}
|
|
7700
|
-
async reconnectWithNewAuth() {
|
|
7701
|
-
if (this.closed)
|
|
7702
|
-
return;
|
|
7703
|
-
await this.ensureInitialized();
|
|
7704
|
-
await this.refreshToken();
|
|
7705
|
-
await this.applyAuthPrincipalChange();
|
|
7706
|
-
if (this.subscriptions.size > 0) {
|
|
7707
|
-
await this.ensureConnected().catch((error) => {
|
|
7708
|
-
this.setAllSubscriptionStatus('error');
|
|
7709
|
-
for (const sub of this.subscriptions.values()) {
|
|
7710
|
-
sub.error = error instanceof Error ? error : new Error(String(error));
|
|
7711
|
-
this.notifyState(sub);
|
|
7712
|
-
}
|
|
7713
|
-
});
|
|
7714
|
-
}
|
|
7715
|
-
}
|
|
7716
|
-
}
|
|
7717
|
-
// ---------------------------------------------------------------------------
|
|
7718
|
-
// Singleton instance
|
|
7719
|
-
// ---------------------------------------------------------------------------
|
|
7720
|
-
let storeInstance = null;
|
|
7721
|
-
function getRealtimeStore() {
|
|
7722
|
-
if (!storeInstance) {
|
|
7723
|
-
storeInstance = new RealtimeStore();
|
|
7724
|
-
}
|
|
7725
|
-
return storeInstance;
|
|
7726
|
-
}
|
|
7727
|
-
function resetRealtimeStore() {
|
|
7728
|
-
if (storeInstance) {
|
|
7729
|
-
storeInstance.close();
|
|
7730
|
-
storeInstance = null;
|
|
7731
|
-
}
|
|
7732
|
-
}
|
|
7733
|
-
async function reconnectRealtimeStoreWithNewAuth() {
|
|
7734
|
-
if (storeInstance) {
|
|
7735
|
-
await storeInstance.reconnectWithNewAuth();
|
|
7736
|
-
}
|
|
7737
|
-
}
|
|
7738
|
-
|
|
7739
|
-
var realtimeStore = /*#__PURE__*/Object.freeze({
|
|
7740
|
-
__proto__: null,
|
|
7741
|
-
RealtimeStore: RealtimeStore,
|
|
7742
|
-
getRealtimeStore: getRealtimeStore,
|
|
7743
|
-
reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
|
|
7744
|
-
resetRealtimeStore: resetRealtimeStore
|
|
7745
|
-
});
|
|
7746
|
-
|
|
7747
6684
|
// ---------------------------------------------------------------------------
|
|
7748
6685
|
// functions.ts -- Bounded Functions client (the imperative escape hatch).
|
|
7749
6686
|
//
|
|
@@ -7799,11 +6736,12 @@ async function invoke(name, args = {}, opts = {}) {
|
|
|
7799
6736
|
const base = (config.functionsUrl || DEFAULT_FUNCTIONS_URL).replace(/\/$/, '');
|
|
7800
6737
|
// Attach the caller's session token automatically (same token as data calls).
|
|
7801
6738
|
// A wallet-scoped call (server WalletClient.invoke) resolves the token from the
|
|
7802
|
-
// wallet's own session;
|
|
6739
|
+
// wallet's own session; browser calls use the active web session. Top-level
|
|
6740
|
+
// server calls fail closed because there is no ambient server signer.
|
|
7803
6741
|
const authHeader = ((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders)
|
|
7804
6742
|
? await opts._overrides._getAuthHeaders()
|
|
7805
6743
|
: await createAuthHeader(config.isServer);
|
|
7806
|
-
const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId
|
|
6744
|
+
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 : {}));
|
|
7807
6745
|
const controller = new AbortController();
|
|
7808
6746
|
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
7809
6747
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -7872,8 +6810,8 @@ const functions = { invoke };
|
|
|
7872
6810
|
// Subscribing to your view: a per-player view doc lives at
|
|
7873
6811
|
// `<roomPath>/view/<myUserId>` (the policy declares
|
|
7874
6812
|
// `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
|
|
7875
|
-
//
|
|
7876
|
-
//
|
|
6813
|
+
// View paths key only by @user.id; wallet-address aliases are intentionally not
|
|
6814
|
+
// accepted by this helper.
|
|
7877
6815
|
// ---------------------------------------------------------------------------
|
|
7878
6816
|
class LiveIntentError extends Error {
|
|
7879
6817
|
constructor(message, statusCode, details) {
|
|
@@ -7967,7 +6905,7 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7967
6905
|
const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
|
|
7968
6906
|
const buildHeaders = async () => {
|
|
7969
6907
|
const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
|
|
7970
|
-
return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId
|
|
6908
|
+
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 : {}));
|
|
7971
6909
|
};
|
|
7972
6910
|
const controller = new AbortController();
|
|
7973
6911
|
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
@@ -8028,7 +6966,7 @@ async function status(roomPath, opts = {}) {
|
|
|
8028
6966
|
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
8029
6967
|
const config = await getConfig();
|
|
8030
6968
|
const base = realtimeHttpBase(config.wsApiUrl);
|
|
8031
|
-
const headers = Object.assign({ 'X-App-Id': config.appId
|
|
6969
|
+
const headers = Object.assign({ 'X-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
|
|
8032
6970
|
const controller = new AbortController();
|
|
8033
6971
|
const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
|
|
8034
6972
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -8070,29 +7008,27 @@ async function status(roomPath, opts = {}) {
|
|
|
8070
7008
|
* subscribe('<roomPath>/view/<myUserId>', { onData, onError })
|
|
8071
7009
|
*
|
|
8072
7010
|
* The view id defaults to the logged-in user's @user.id (from the session token
|
|
8073
|
-
* claims); pass `opts.userId` to override.
|
|
8074
|
-
*
|
|
8075
|
-
* function (a Promise<() => Promise<void>>, same as `subscribe`).
|
|
7011
|
+
* claims); pass `opts.userId` to override. Returns the unsubscribe function
|
|
7012
|
+
* (a Promise<() => Promise<void>>, same as `subscribe`).
|
|
8076
7013
|
*
|
|
8077
7014
|
* Note: this is a browser-first helper (the WS subscription manager is
|
|
8078
7015
|
* browser-oriented). Server consumers should use `live.intent`.
|
|
8079
7016
|
*/
|
|
8080
7017
|
async function subscribeView(roomPath, opts) {
|
|
8081
|
-
var _a, _b, _c;
|
|
8082
7018
|
if (!roomPath || typeof roomPath !== 'string') {
|
|
8083
7019
|
throw new LiveIntentError('A room path is required');
|
|
8084
7020
|
}
|
|
8085
7021
|
if (!opts || typeof opts.onData !== 'function') {
|
|
8086
7022
|
throw new LiveIntentError('subscribeView requires an onData callback');
|
|
8087
7023
|
}
|
|
8088
|
-
let viewUserId =
|
|
7024
|
+
let viewUserId = opts.userId;
|
|
8089
7025
|
if (!viewUserId) {
|
|
8090
7026
|
const config = await getConfig();
|
|
8091
7027
|
const info = await getUserInfo(config.isServer);
|
|
8092
7028
|
// getUserInfo returns the RAW idToken payload. The universal live view key
|
|
8093
|
-
// is @user.id (`custom:userId`); wallet-address keyed
|
|
8094
|
-
//
|
|
8095
|
-
viewUserId =
|
|
7029
|
+
// is @user.id (`custom:userId`); wallet-address keyed compatibility aliases
|
|
7030
|
+
// are intentionally not accepted.
|
|
7031
|
+
viewUserId = info === null || info === void 0 ? void 0 : info['custom:userId'];
|
|
8096
7032
|
}
|
|
8097
7033
|
if (!viewUserId || typeof viewUserId !== 'string') {
|
|
8098
7034
|
throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
|
|
@@ -8162,5 +7098,5 @@ function defineLiveModule(mod) {
|
|
|
8162
7098
|
return mod;
|
|
8163
7099
|
}
|
|
8164
7100
|
|
|
8165
|
-
export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager,
|
|
7101
|
+
export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, docId, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects };
|
|
8166
7102
|
//# sourceMappingURL=index.mjs.map
|