@bounded-sh/core 0.0.15 → 0.0.16
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/live.d.ts +3 -0
- package/dist/client/realtime-store.d.ts +8 -0
- package/dist/index.js +292 -157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -157
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -4315,99 +4315,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
|
|
|
4315
4315
|
}
|
|
4316
4316
|
return responseData;
|
|
4317
4317
|
}
|
|
4318
|
-
function hashForKey$
|
|
4318
|
+
function hashForKey$2(value) {
|
|
4319
4319
|
let h = 5381;
|
|
4320
4320
|
for (let i = 0; i < value.length; i++) {
|
|
4321
4321
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
4322
4322
|
}
|
|
4323
4323
|
return h.toString(36);
|
|
4324
4324
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4325
|
+
function authValueFromHeaders(headers) {
|
|
4326
|
+
return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
|
|
4327
|
+
}
|
|
4328
|
+
function principalFromAuthValue(authValue) {
|
|
4329
|
+
return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
|
|
4330
|
+
}
|
|
4331
4331
|
function principalFromIdToken$1(idToken) {
|
|
4332
|
-
|
|
4333
|
-
return 'anon';
|
|
4334
|
-
try {
|
|
4335
|
-
const parts = idToken.split('.');
|
|
4336
|
-
if (parts.length < 2)
|
|
4337
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4338
|
-
const payload = JSON.parse(decodeBase64Url(parts[1]));
|
|
4339
|
-
const subject =
|
|
4340
|
-
// Prefer the universal identity (@user.id) — it is the stable principal a
|
|
4341
|
-
// request presents (equals the wallet for wallet logins; the account id for
|
|
4342
|
-
// email/social logins, which carry no walletAddress). Keying the read cache
|
|
4343
|
-
// on it keeps an email user's private snapshot scoped to that user.
|
|
4344
|
-
payload['custom:userId'] ||
|
|
4345
|
-
payload['custom:walletAddress'] ||
|
|
4346
|
-
payload.walletAddress ||
|
|
4347
|
-
payload.sub ||
|
|
4348
|
-
payload.address;
|
|
4349
|
-
if (subject)
|
|
4350
|
-
return `s${hashForKey$1(String(subject))}`;
|
|
4351
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4352
|
-
}
|
|
4353
|
-
catch (_a) {
|
|
4354
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4355
|
-
}
|
|
4332
|
+
return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
|
|
4356
4333
|
}
|
|
4357
4334
|
/**
|
|
4358
4335
|
* SECURITY (H1): Read caches must be keyed by the caller's principal, not just
|
|
4359
4336
|
* by path/filter/shape. In a shared process / SSR worker / browser login-switch,
|
|
4360
4337
|
* keying by path alone lets User B receive User A's cached private read before
|
|
4361
4338
|
* any server read rule runs. This returns `appId:<principal>` for the identity a
|
|
4362
|
-
* given read will actually authenticate as
|
|
4363
|
-
*
|
|
4364
|
-
*
|
|
4365
|
-
*
|
|
4366
|
-
* - otherwise the ambient logged-in session's idToken subject (or `anon`)
|
|
4339
|
+
* given read will actually authenticate as. This intentionally treats JWTs as
|
|
4340
|
+
* opaque unverified bearer material — never decoded claims and never caller
|
|
4341
|
+
* identity hints such as `_walletAddress`. A forged token that merely claims
|
|
4342
|
+
* another user's `sub` must miss that user's cache and go to the server.
|
|
4367
4343
|
*/
|
|
4368
4344
|
async function getReadPrincipalKey(overrides) {
|
|
4369
|
-
var _a, _b;
|
|
4370
4345
|
const config = await getConfig();
|
|
4371
4346
|
const appId = config.appId || '';
|
|
4372
|
-
//
|
|
4373
|
-
|
|
4374
|
-
|
|
4347
|
+
// makeApiRequest applies overrides.headers AFTER its computed auth header, so
|
|
4348
|
+
// caller-supplied Authorization is the real request auth when present.
|
|
4349
|
+
const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
|
|
4350
|
+
if (directAuth) {
|
|
4351
|
+
return `${appId}:${principalFromAuthValue(directAuth)}`;
|
|
4375
4352
|
}
|
|
4376
|
-
// Per-request auth-header override (wallet client). Key by the
|
|
4377
|
-
// it produces
|
|
4353
|
+
// Per-request auth-header override (wallet client). Key by the exact opaque
|
|
4354
|
+
// header it produces, not decoded claims or the unverified _walletAddress hint.
|
|
4378
4355
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
4379
4356
|
try {
|
|
4380
4357
|
const headers = await overrides._getAuthHeaders();
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
// Decode the bearer token's subject when present for a stable key across
|
|
4385
|
-
// refreshes; otherwise hash whatever header material we were given.
|
|
4386
|
-
const bearer = authValue.startsWith('Bearer ')
|
|
4387
|
-
? authValue.slice('Bearer '.length)
|
|
4388
|
-
: authValue;
|
|
4389
|
-
const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
|
|
4390
|
-
return `${appId}:o${principal}`;
|
|
4391
|
-
}
|
|
4392
|
-
catch (_c) {
|
|
4358
|
+
return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
|
|
4359
|
+
}
|
|
4360
|
+
catch (_a) {
|
|
4393
4361
|
// If we can't resolve the override identity, use a unique-ish key so we
|
|
4394
4362
|
// never collide with (and serve) another principal's cached entry.
|
|
4395
|
-
return `${appId}:o${hashForKey$
|
|
4363
|
+
return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
|
|
4396
4364
|
}
|
|
4397
4365
|
}
|
|
4398
|
-
// Direct per-request header override. makeApiRequest applies overrides.headers
|
|
4399
|
-
// AFTER its computed auth header (api.ts), so a caller-supplied Authorization
|
|
4400
|
-
// is the REAL request auth — key the cache by it so an entry is never served
|
|
4401
|
-
// to a different principal under the ambient key. (Hardening: no in-repo read
|
|
4402
|
-
// caller passes this today, but the cache must never trail the actual auth.)
|
|
4403
|
-
const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
|
|
4404
|
-
((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
|
|
4405
|
-
if (directAuth) {
|
|
4406
|
-
const bearer = directAuth.startsWith('Bearer ')
|
|
4407
|
-
? directAuth.slice('Bearer '.length)
|
|
4408
|
-
: directAuth;
|
|
4409
|
-
return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
|
|
4410
|
-
}
|
|
4411
4366
|
// Ambient session principal.
|
|
4412
4367
|
const idToken = await getIdToken(config.isServer);
|
|
4413
4368
|
return `${appId}:${principalFromIdToken$1(idToken)}`;
|
|
@@ -4707,9 +4662,9 @@ async function get(path, opts = {}) {
|
|
|
4707
4662
|
const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
|
|
4708
4663
|
const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
|
|
4709
4664
|
const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
|
|
4710
|
-
const cursorKey = opts.cursor ? `:c${hashForKey$
|
|
4711
|
-
const filterKey = opts.filter ? `:f${hashForKey$
|
|
4712
|
-
const sortKey = opts.sort ? `:s${hashForKey$
|
|
4665
|
+
const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
|
|
4666
|
+
const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
|
|
4667
|
+
const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
|
|
4713
4668
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4714
4669
|
const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4715
4670
|
const now = Date.now();
|
|
@@ -5459,7 +5414,7 @@ let reconnectInProgress = null;
|
|
|
5459
5414
|
function generateSubscriptionId() {
|
|
5460
5415
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5461
5416
|
}
|
|
5462
|
-
function hashForKey(value) {
|
|
5417
|
+
function hashForKey$1(value) {
|
|
5463
5418
|
let h = 5381;
|
|
5464
5419
|
for (let i = 0; i < value.length; i++) {
|
|
5465
5420
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
@@ -5478,49 +5433,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
|
5478
5433
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
5479
5434
|
const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
|
|
5480
5435
|
const limitKey = limit !== undefined ? `:l${limit}` : '';
|
|
5481
|
-
const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
|
|
5482
|
-
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
|
|
5436
|
+
const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
|
|
5437
|
+
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
|
|
5483
5438
|
const identityKey = identity || 'anon';
|
|
5484
5439
|
return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5485
5440
|
}
|
|
5486
5441
|
/**
|
|
5487
|
-
* Derive
|
|
5488
|
-
*
|
|
5489
|
-
*
|
|
5442
|
+
* Derive an opaque identity string for the bearer material a subscription sends.
|
|
5443
|
+
* JWT payloads are deliberately not trusted here: cache isolation must happen
|
|
5444
|
+
* before the server has verified any claims.
|
|
5490
5445
|
*/
|
|
5491
5446
|
function principalFromIdToken(idToken) {
|
|
5492
|
-
|
|
5493
|
-
return 'anon';
|
|
5494
|
-
try {
|
|
5495
|
-
const parts = idToken.split('.');
|
|
5496
|
-
if (parts.length < 2)
|
|
5497
|
-
return `t${hashForKey(idToken)}`;
|
|
5498
|
-
const payload = JSON.parse(decodeBase64Url(parts[1]));
|
|
5499
|
-
const subject =
|
|
5500
|
-
// Universal identity (@user.id) first — the stable principal a request
|
|
5501
|
-
// presents (the account id for email/social logins, which carry no
|
|
5502
|
-
// walletAddress). Keeps the H1 response-cache scoping correct for them.
|
|
5503
|
-
payload['custom:userId'] ||
|
|
5504
|
-
payload['custom:walletAddress'] ||
|
|
5505
|
-
payload.walletAddress ||
|
|
5506
|
-
payload.sub ||
|
|
5507
|
-
payload.address;
|
|
5508
|
-
if (subject)
|
|
5509
|
-
return `s${hashForKey(String(subject))}`;
|
|
5510
|
-
return `t${hashForKey(idToken)}`;
|
|
5511
|
-
}
|
|
5512
|
-
catch (_a) {
|
|
5513
|
-
return `t${hashForKey(idToken)}`;
|
|
5514
|
-
}
|
|
5447
|
+
return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
|
|
5515
5448
|
}
|
|
5516
5449
|
async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
5517
|
-
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
5518
|
-
// wallet's own token, mirroring getReadPrincipalKey
|
|
5519
|
-
//
|
|
5520
|
-
// cached snapshots are never crossed with another principal's.
|
|
5521
|
-
if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
|
|
5522
|
-
return `${effectiveAppId}:w${overrides._walletAddress}`;
|
|
5523
|
-
}
|
|
5450
|
+
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
5451
|
+
// the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
|
|
5452
|
+
// not trust decoded claims or the unverified _walletAddress hint.
|
|
5524
5453
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
5525
5454
|
try {
|
|
5526
5455
|
const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
|
|
@@ -5891,7 +5820,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5891
5820
|
return connection;
|
|
5892
5821
|
}
|
|
5893
5822
|
function handleServerMessage(connection, message) {
|
|
5894
|
-
var _a
|
|
5823
|
+
var _a;
|
|
5895
5824
|
switch (message.type) {
|
|
5896
5825
|
case 'authenticated': {
|
|
5897
5826
|
connection.isAuthenticating = false;
|
|
@@ -5985,28 +5914,27 @@ function handleServerMessage(connection, message) {
|
|
|
5985
5914
|
err.subscriptionId = message.subscriptionId;
|
|
5986
5915
|
return err;
|
|
5987
5916
|
};
|
|
5917
|
+
const wsError = buildWsError();
|
|
5988
5918
|
// Handle CRUD request errors (requestId present)
|
|
5989
5919
|
if (message.requestId) {
|
|
5990
5920
|
const pendingReq = connection.pendingRequests.get(message.requestId);
|
|
5991
5921
|
if (pendingReq) {
|
|
5992
5922
|
connection.pendingRequests.delete(message.requestId);
|
|
5993
5923
|
clearTimeout(pendingReq.timer);
|
|
5994
|
-
pendingReq.reject(
|
|
5924
|
+
pendingReq.reject(wsError);
|
|
5995
5925
|
}
|
|
5996
5926
|
}
|
|
5997
5927
|
if (message.subscriptionId) {
|
|
5998
5928
|
// Reject pending subscription if this is a subscription error
|
|
5999
5929
|
const pending = connection.pendingSubscriptions.get(message.subscriptionId);
|
|
6000
5930
|
if (pending) {
|
|
6001
|
-
pending.reject(
|
|
5931
|
+
pending.reject(wsError);
|
|
6002
5932
|
connection.pendingSubscriptions.delete(message.subscriptionId);
|
|
6003
5933
|
}
|
|
6004
5934
|
// Notify error callbacks for this subscription
|
|
6005
5935
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
6006
5936
|
if (subscription) {
|
|
6007
|
-
|
|
6008
|
-
(_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
|
|
6009
|
-
}
|
|
5937
|
+
notifyErrorCallbacks(subscription, wsError);
|
|
6010
5938
|
}
|
|
6011
5939
|
}
|
|
6012
5940
|
break;
|
|
@@ -6059,6 +5987,25 @@ function notifyCallbacks(subscription, data) {
|
|
|
6059
5987
|
}
|
|
6060
5988
|
}
|
|
6061
5989
|
}
|
|
5990
|
+
function notifyErrorCallbacks(subscription, error) {
|
|
5991
|
+
let delivered = false;
|
|
5992
|
+
const callbacks = subscription.callbacks.slice();
|
|
5993
|
+
for (const callback of callbacks) {
|
|
5994
|
+
if (!callback.onError)
|
|
5995
|
+
continue;
|
|
5996
|
+
delivered = true;
|
|
5997
|
+
try {
|
|
5998
|
+
callback.onError(error);
|
|
5999
|
+
}
|
|
6000
|
+
catch (callbackError) {
|
|
6001
|
+
console.error('[WS v2] Error in subscription error callback:', callbackError);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
if (delivered) {
|
|
6005
|
+
error.__boundedDeliveredToOnError = true;
|
|
6006
|
+
}
|
|
6007
|
+
return delivered;
|
|
6008
|
+
}
|
|
6062
6009
|
// WebSocket readyState constants
|
|
6063
6010
|
const WS_READY_STATE_OPEN = 1;
|
|
6064
6011
|
const WS_READY_STATE_CLOSED = 3;
|
|
@@ -6133,10 +6080,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6133
6080
|
const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
|
|
6134
6081
|
? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
|
|
6135
6082
|
: undefined;
|
|
6136
|
-
const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
|
|
6137
|
-
? `w${overrides._walletAddress}`
|
|
6138
|
-
: (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
|
|
6139
6083
|
const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
|
|
6084
|
+
const principalKey = authTokenProvider ? identity : undefined;
|
|
6140
6085
|
const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
|
|
6141
6086
|
// Deliver cached data immediately if available
|
|
6142
6087
|
const cachedEntry = responseCache.get(cacheKey);
|
|
@@ -6215,7 +6160,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6215
6160
|
await subscriptionPromise;
|
|
6216
6161
|
}
|
|
6217
6162
|
catch (error) {
|
|
6218
|
-
|
|
6163
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
6164
|
+
if (!err.__boundedDeliveredToOnError) {
|
|
6165
|
+
notifyErrorCallbacks(subscription, err);
|
|
6166
|
+
}
|
|
6167
|
+
if (!subscriptionOptions.onError) {
|
|
6168
|
+
connection.pendingSubscriptions.delete(subscriptionId);
|
|
6169
|
+
connection.subscriptions.delete(subscriptionId);
|
|
6170
|
+
throw err;
|
|
6171
|
+
}
|
|
6172
|
+
if (!err.__boundedDeliveredToOnError) {
|
|
6173
|
+
console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
|
|
6174
|
+
}
|
|
6219
6175
|
}
|
|
6220
6176
|
}
|
|
6221
6177
|
// Return unsubscribe function
|
|
@@ -6378,6 +6334,13 @@ async function doReconnectWithNewAuth() {
|
|
|
6378
6334
|
catch (error) {
|
|
6379
6335
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6380
6336
|
}
|
|
6337
|
+
try {
|
|
6338
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
|
|
6339
|
+
await reconnectRealtimeStoreWithNewAuth();
|
|
6340
|
+
}
|
|
6341
|
+
catch (error) {
|
|
6342
|
+
console.warn('[WS v2] Failed to reset legacy realtime store on auth change:', error);
|
|
6343
|
+
}
|
|
6381
6344
|
for (const [appId, connection] of connections) {
|
|
6382
6345
|
if (!connection.ws) {
|
|
6383
6346
|
continue;
|
|
@@ -6845,6 +6808,16 @@ async function idbSet(key, value) {
|
|
|
6845
6808
|
// RealtimeStore
|
|
6846
6809
|
// ---------------------------------------------------------------------------
|
|
6847
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
|
+
}
|
|
6848
6821
|
class RealtimeStore {
|
|
6849
6822
|
constructor() {
|
|
6850
6823
|
this.ws = null;
|
|
@@ -6860,7 +6833,9 @@ class RealtimeStore {
|
|
|
6860
6833
|
this.idbDirtyKeys = new Set();
|
|
6861
6834
|
this.closed = false;
|
|
6862
6835
|
this.authToken = null;
|
|
6836
|
+
this.authPrincipalKey = 'anon';
|
|
6863
6837
|
this.authenticating = false;
|
|
6838
|
+
this.suppressNextReconnect = false;
|
|
6864
6839
|
this.isServer = false;
|
|
6865
6840
|
this.tokenRefreshTimer = null;
|
|
6866
6841
|
// -----------------------------------------------------------------------
|
|
@@ -6880,34 +6855,98 @@ class RealtimeStore {
|
|
|
6880
6855
|
this.startTokenRefresh();
|
|
6881
6856
|
}
|
|
6882
6857
|
async refreshToken() {
|
|
6858
|
+
let token = null;
|
|
6883
6859
|
try {
|
|
6884
6860
|
const { getIdToken } = await Promise.resolve().then(function () { return utils; });
|
|
6885
|
-
|
|
6886
|
-
if (token)
|
|
6887
|
-
this.authToken = token;
|
|
6861
|
+
token = await getIdToken(this.isServer);
|
|
6888
6862
|
}
|
|
6889
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);
|
|
6890
6866
|
}
|
|
6891
6867
|
startTokenRefresh() {
|
|
6892
6868
|
if (this.tokenRefreshTimer)
|
|
6893
6869
|
return;
|
|
6894
6870
|
this.tokenRefreshTimer = setInterval(async () => {
|
|
6895
|
-
|
|
6896
|
-
const prevToken = this.authToken;
|
|
6871
|
+
const prevPrincipal = this.authPrincipalKey;
|
|
6897
6872
|
await this.refreshToken();
|
|
6898
|
-
if (this.
|
|
6899
|
-
|
|
6900
|
-
this.
|
|
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
|
+
}
|
|
6901
6880
|
}
|
|
6902
6881
|
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
6903
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
|
+
}
|
|
6904
6947
|
async ensureConnected() {
|
|
6905
6948
|
var _a;
|
|
6906
|
-
|
|
6907
|
-
if (!this.initPromise)
|
|
6908
|
-
this.initPromise = this.init();
|
|
6909
|
-
await this.initPromise;
|
|
6910
|
-
}
|
|
6949
|
+
await this.ensureCurrentAuth();
|
|
6911
6950
|
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
|
|
6912
6951
|
return;
|
|
6913
6952
|
if (this.connectPromise)
|
|
@@ -6991,11 +7030,20 @@ class RealtimeStore {
|
|
|
6991
7030
|
ws.addEventListener('close', () => {
|
|
6992
7031
|
if (authTimer)
|
|
6993
7032
|
clearTimeout(authTimer);
|
|
7033
|
+
if (this.ws !== ws) {
|
|
7034
|
+
if (this.suppressNextReconnect)
|
|
7035
|
+
this.suppressNextReconnect = false;
|
|
7036
|
+
return;
|
|
7037
|
+
}
|
|
6994
7038
|
this.authenticating = false;
|
|
6995
7039
|
this.ws = null;
|
|
6996
7040
|
this.connectPromise = null;
|
|
6997
7041
|
this.rejectAllPending('WebSocket closed');
|
|
6998
7042
|
this.setAllSubscriptionStatus('reconnecting');
|
|
7043
|
+
if (this.suppressNextReconnect) {
|
|
7044
|
+
this.suppressNextReconnect = false;
|
|
7045
|
+
return;
|
|
7046
|
+
}
|
|
6999
7047
|
this.scheduleReconnect();
|
|
7000
7048
|
});
|
|
7001
7049
|
});
|
|
@@ -7171,14 +7219,34 @@ class RealtimeStore {
|
|
|
7171
7219
|
}
|
|
7172
7220
|
}
|
|
7173
7221
|
handleError(msg) {
|
|
7174
|
-
var _a;
|
|
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;
|
|
7175
7228
|
const requestId = msg.requestId;
|
|
7176
7229
|
if (requestId) {
|
|
7177
7230
|
const pending = this.pendingRequests.get(requestId);
|
|
7178
7231
|
if (pending) {
|
|
7179
7232
|
this.pendingRequests.delete(requestId);
|
|
7180
7233
|
clearTimeout(pending.timeout);
|
|
7181
|
-
pending.reject(
|
|
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
|
+
}
|
|
7182
7250
|
}
|
|
7183
7251
|
}
|
|
7184
7252
|
}
|
|
@@ -7187,6 +7255,7 @@ class RealtimeStore {
|
|
|
7187
7255
|
// -----------------------------------------------------------------------
|
|
7188
7256
|
async subscribe(path, opts = {}) {
|
|
7189
7257
|
var _a;
|
|
7258
|
+
await this.ensureCurrentAuth();
|
|
7190
7259
|
const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
|
|
7191
7260
|
const subKey = this.getSubKey(path, opts);
|
|
7192
7261
|
let sub = this.subscriptions.get(subKey);
|
|
@@ -7196,6 +7265,8 @@ class RealtimeStore {
|
|
|
7196
7265
|
sub.callbacks.add(opts.onData);
|
|
7197
7266
|
if (opts.onState)
|
|
7198
7267
|
sub.stateCallbacks.add(opts.onState);
|
|
7268
|
+
if (opts.onError)
|
|
7269
|
+
sub.errorCallbacks.add(opts.onError);
|
|
7199
7270
|
// Immediately deliver current state
|
|
7200
7271
|
if (opts.onData && sub.docs.size > 0) {
|
|
7201
7272
|
opts.onData(this.docsToArray(sub));
|
|
@@ -7203,7 +7274,7 @@ class RealtimeStore {
|
|
|
7203
7274
|
if (opts.onState) {
|
|
7204
7275
|
opts.onState(this.getState(sub));
|
|
7205
7276
|
}
|
|
7206
|
-
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
7277
|
+
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7207
7278
|
}
|
|
7208
7279
|
// New subscription
|
|
7209
7280
|
const subId = `sub_${nextRequestId++}`;
|
|
@@ -7218,6 +7289,7 @@ class RealtimeStore {
|
|
|
7218
7289
|
error: null,
|
|
7219
7290
|
callbacks: new Set(opts.onData ? [opts.onData] : []),
|
|
7220
7291
|
stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
|
|
7292
|
+
errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
|
|
7221
7293
|
ref: { current: new Map() },
|
|
7222
7294
|
};
|
|
7223
7295
|
this.subscriptions.set(subKey, sub);
|
|
@@ -7247,7 +7319,7 @@ class RealtimeStore {
|
|
|
7247
7319
|
sub.error = new Error('Connection failed');
|
|
7248
7320
|
this.notifyState(sub);
|
|
7249
7321
|
}
|
|
7250
|
-
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
7322
|
+
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7251
7323
|
}
|
|
7252
7324
|
getRef(path, opts = {}) {
|
|
7253
7325
|
var _a;
|
|
@@ -7321,6 +7393,7 @@ class RealtimeStore {
|
|
|
7321
7393
|
}
|
|
7322
7394
|
}
|
|
7323
7395
|
async get(path) {
|
|
7396
|
+
await this.ensureCurrentAuth();
|
|
7324
7397
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7325
7398
|
// Check local subscriptions first
|
|
7326
7399
|
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
@@ -7501,7 +7574,7 @@ class RealtimeStore {
|
|
|
7501
7574
|
return docPath;
|
|
7502
7575
|
}
|
|
7503
7576
|
getSubKey(path, opts) {
|
|
7504
|
-
const parts = [path];
|
|
7577
|
+
const parts = [this.appId, this.authPrincipalKey, path];
|
|
7505
7578
|
if (opts.filter)
|
|
7506
7579
|
parts.push(JSON.stringify(opts.filter));
|
|
7507
7580
|
if (opts.prompt)
|
|
@@ -7511,7 +7584,7 @@ class RealtimeStore {
|
|
|
7511
7584
|
return parts.join('::');
|
|
7512
7585
|
}
|
|
7513
7586
|
idbKey(path) {
|
|
7514
|
-
return `${this.appId}:${path}`;
|
|
7587
|
+
return `${this.appId}:${this.authPrincipalKey}:${path}`;
|
|
7515
7588
|
}
|
|
7516
7589
|
markIdbDirty(path) {
|
|
7517
7590
|
const sub = this.findSubscriptionByPath(path);
|
|
@@ -7536,18 +7609,23 @@ class RealtimeStore {
|
|
|
7536
7609
|
}
|
|
7537
7610
|
}
|
|
7538
7611
|
}
|
|
7539
|
-
createUnsubscribe(subKey, onData, onState) {
|
|
7612
|
+
createUnsubscribe(subKey, subId, onData, onState, onError) {
|
|
7540
7613
|
return async () => {
|
|
7541
|
-
|
|
7614
|
+
var _a;
|
|
7615
|
+
const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
|
|
7542
7616
|
if (!sub)
|
|
7543
7617
|
return;
|
|
7618
|
+
const currentSubKey = this.getSubKey(sub.path, sub.options);
|
|
7544
7619
|
if (onData)
|
|
7545
7620
|
sub.callbacks.delete(onData);
|
|
7546
7621
|
if (onState)
|
|
7547
7622
|
sub.stateCallbacks.delete(onState);
|
|
7623
|
+
if (onError)
|
|
7624
|
+
sub.errorCallbacks.delete(onError);
|
|
7548
7625
|
// If no more callbacks, unsubscribe entirely
|
|
7549
|
-
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
|
|
7626
|
+
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
|
|
7550
7627
|
this.subscriptions.delete(subKey);
|
|
7628
|
+
this.subscriptions.delete(currentSubKey);
|
|
7551
7629
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7552
7630
|
this.ws.send(JSON.stringify({
|
|
7553
7631
|
type: 'unsubscribe',
|
|
@@ -7619,6 +7697,22 @@ class RealtimeStore {
|
|
|
7619
7697
|
this.rejectAllPending('Store closed');
|
|
7620
7698
|
this.subscriptions.clear();
|
|
7621
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
|
+
}
|
|
7622
7716
|
}
|
|
7623
7717
|
// ---------------------------------------------------------------------------
|
|
7624
7718
|
// Singleton instance
|
|
@@ -7636,6 +7730,19 @@ function resetRealtimeStore() {
|
|
|
7636
7730
|
storeInstance = null;
|
|
7637
7731
|
}
|
|
7638
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
|
+
});
|
|
7639
7746
|
|
|
7640
7747
|
// ---------------------------------------------------------------------------
|
|
7641
7748
|
// functions.ts -- Bounded Functions client (the imperative escape hatch).
|
|
@@ -7791,6 +7898,23 @@ function realtimeHttpBase(wsApiUrl) {
|
|
|
7791
7898
|
// Strip trailing slash from the resulting origin+path.
|
|
7792
7899
|
return url.toString().replace(/\/$/, '');
|
|
7793
7900
|
}
|
|
7901
|
+
function withoutAuthorization(headers) {
|
|
7902
|
+
if (!headers)
|
|
7903
|
+
return undefined;
|
|
7904
|
+
const clean = {};
|
|
7905
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
7906
|
+
if (key.toLowerCase() === 'authorization')
|
|
7907
|
+
continue;
|
|
7908
|
+
clean[key] = value;
|
|
7909
|
+
}
|
|
7910
|
+
return Object.keys(clean).length > 0 ? clean : undefined;
|
|
7911
|
+
}
|
|
7912
|
+
async function liveAuthHeader(configIsServer, overrides) {
|
|
7913
|
+
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
7914
|
+
return overrides._getAuthHeaders();
|
|
7915
|
+
}
|
|
7916
|
+
return createAuthHeader(configIsServer);
|
|
7917
|
+
}
|
|
7794
7918
|
/**
|
|
7795
7919
|
* Send a player intent to a running live room. Returns `{ ok: true }`.
|
|
7796
7920
|
*
|
|
@@ -7805,7 +7929,7 @@ function realtimeHttpBase(wsApiUrl) {
|
|
|
7805
7929
|
* transport error / timeout.
|
|
7806
7930
|
*/
|
|
7807
7931
|
async function intent(roomPath, intent, opts = {}) {
|
|
7808
|
-
var _a, _b, _c, _d, _e;
|
|
7932
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
7809
7933
|
if (!roomPath || typeof roomPath !== 'string') {
|
|
7810
7934
|
throw new LiveIntentError('A room path is required');
|
|
7811
7935
|
}
|
|
@@ -7826,37 +7950,48 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7826
7950
|
// (e.g. the first join before subscribeView's WS connects) — HTTP also throws
|
|
7827
7951
|
// on a non-2xx, so errors are surfaced there too.
|
|
7828
7952
|
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7953
|
+
const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
|
|
7954
|
+
if (!hasAuthOverride) {
|
|
7955
|
+
if (opts.fireAndForget) {
|
|
7956
|
+
if (wsIntent(config.appId, normalizedRoomPath, intent))
|
|
7957
|
+
return { ok: true };
|
|
7958
|
+
}
|
|
7959
|
+
else {
|
|
7960
|
+
const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
|
|
7961
|
+
if (ack)
|
|
7962
|
+
return await ack;
|
|
7963
|
+
}
|
|
7837
7964
|
}
|
|
7838
7965
|
const base = realtimeHttpBase(config.wsApiUrl);
|
|
7839
|
-
|
|
7840
|
-
const
|
|
7841
|
-
const
|
|
7966
|
+
const extraHeaders = withoutAuthorization(opts.headers);
|
|
7967
|
+
const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
|
|
7968
|
+
const buildHeaders = async () => {
|
|
7969
|
+
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, 'X-Public-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
|
|
7971
|
+
};
|
|
7842
7972
|
const controller = new AbortController();
|
|
7843
|
-
const timeoutMs = (
|
|
7973
|
+
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
7844
7974
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7845
7975
|
let res;
|
|
7846
7976
|
try {
|
|
7847
|
-
|
|
7977
|
+
const send = async () => fetch(`${base}/live/intent`, {
|
|
7848
7978
|
method: 'POST',
|
|
7849
|
-
headers,
|
|
7979
|
+
headers: await buildHeaders(),
|
|
7850
7980
|
body: JSON.stringify({ path: normalizedRoomPath, intent }),
|
|
7851
7981
|
signal: controller.signal,
|
|
7852
7982
|
});
|
|
7983
|
+
res = await send();
|
|
7984
|
+
if (res.status === 401 && ((_d = opts._overrides) === null || _d === void 0 ? void 0 : _d._clearAuth)) {
|
|
7985
|
+
await opts._overrides._clearAuth();
|
|
7986
|
+
res = await send();
|
|
7987
|
+
}
|
|
7853
7988
|
}
|
|
7854
7989
|
catch (err) {
|
|
7855
7990
|
clearTimeout(timer);
|
|
7856
7991
|
if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
|
|
7857
7992
|
throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
|
|
7858
7993
|
}
|
|
7859
|
-
throw new LiveIntentError(`Failed to reach the realtime worker: ${(
|
|
7994
|
+
throw new LiveIntentError(`Failed to reach the realtime worker: ${(_e = err === null || err === void 0 ? void 0 : err.message) !== null && _e !== void 0 ? _e : String(err)}`);
|
|
7860
7995
|
}
|
|
7861
7996
|
clearTimeout(timer);
|
|
7862
7997
|
let body = null;
|
|
@@ -7865,12 +8000,12 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7865
8000
|
try {
|
|
7866
8001
|
body = JSON.parse(text);
|
|
7867
8002
|
}
|
|
7868
|
-
catch (
|
|
8003
|
+
catch (_h) {
|
|
7869
8004
|
body = { raw: text };
|
|
7870
8005
|
}
|
|
7871
8006
|
}
|
|
7872
8007
|
if (!res.ok) {
|
|
7873
|
-
const message = (
|
|
8008
|
+
const message = (_g = (_f = body === null || body === void 0 ? void 0 : body.error) !== null && _f !== void 0 ? _f : body === null || body === void 0 ? void 0 : body.message) !== null && _g !== void 0 ? _g : `Live intent failed with HTTP ${res.status}`;
|
|
7874
8009
|
throw new LiveIntentError(message, res.status, body);
|
|
7875
8010
|
}
|
|
7876
8011
|
return (body !== null && body !== void 0 ? body : { ok: true });
|