@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.js
CHANGED
|
@@ -4335,99 +4335,54 @@ function normalizeReadResult(responseData, pathIsDocument) {
|
|
|
4335
4335
|
}
|
|
4336
4336
|
return responseData;
|
|
4337
4337
|
}
|
|
4338
|
-
function hashForKey$
|
|
4338
|
+
function hashForKey$2(value) {
|
|
4339
4339
|
let h = 5381;
|
|
4340
4340
|
for (let i = 0; i < value.length; i++) {
|
|
4341
4341
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
4342
4342
|
}
|
|
4343
4343
|
return h.toString(36);
|
|
4344
4344
|
}
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4345
|
+
function authValueFromHeaders(headers) {
|
|
4346
|
+
return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
|
|
4347
|
+
}
|
|
4348
|
+
function principalFromAuthValue(authValue) {
|
|
4349
|
+
return authValue ? `h${hashForKey$2(authValue)}` : 'anon';
|
|
4350
|
+
}
|
|
4351
4351
|
function principalFromIdToken$1(idToken) {
|
|
4352
|
-
|
|
4353
|
-
return 'anon';
|
|
4354
|
-
try {
|
|
4355
|
-
const parts = idToken.split('.');
|
|
4356
|
-
if (parts.length < 2)
|
|
4357
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4358
|
-
const payload = JSON.parse(decodeBase64Url(parts[1]));
|
|
4359
|
-
const subject =
|
|
4360
|
-
// Prefer the universal identity (@user.id) — it is the stable principal a
|
|
4361
|
-
// request presents (equals the wallet for wallet logins; the account id for
|
|
4362
|
-
// email/social logins, which carry no walletAddress). Keying the read cache
|
|
4363
|
-
// on it keeps an email user's private snapshot scoped to that user.
|
|
4364
|
-
payload['custom:userId'] ||
|
|
4365
|
-
payload['custom:walletAddress'] ||
|
|
4366
|
-
payload.walletAddress ||
|
|
4367
|
-
payload.sub ||
|
|
4368
|
-
payload.address;
|
|
4369
|
-
if (subject)
|
|
4370
|
-
return `s${hashForKey$1(String(subject))}`;
|
|
4371
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4372
|
-
}
|
|
4373
|
-
catch (_a) {
|
|
4374
|
-
return `t${hashForKey$1(idToken)}`;
|
|
4375
|
-
}
|
|
4352
|
+
return idToken ? `t${hashForKey$2(idToken)}` : 'anon';
|
|
4376
4353
|
}
|
|
4377
4354
|
/**
|
|
4378
4355
|
* SECURITY (H1): Read caches must be keyed by the caller's principal, not just
|
|
4379
4356
|
* by path/filter/shape. In a shared process / SSR worker / browser login-switch,
|
|
4380
4357
|
* keying by path alone lets User B receive User A's cached private read before
|
|
4381
4358
|
* any server read rule runs. This returns `appId:<principal>` for the identity a
|
|
4382
|
-
* given read will actually authenticate as
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
* - otherwise the ambient logged-in session's idToken subject (or `anon`)
|
|
4359
|
+
* given read will actually authenticate as. This intentionally treats JWTs as
|
|
4360
|
+
* opaque unverified bearer material — never decoded claims and never caller
|
|
4361
|
+
* identity hints such as `_walletAddress`. A forged token that merely claims
|
|
4362
|
+
* another user's `sub` must miss that user's cache and go to the server.
|
|
4387
4363
|
*/
|
|
4388
4364
|
async function getReadPrincipalKey(overrides) {
|
|
4389
|
-
var _a, _b;
|
|
4390
4365
|
const config = await getConfig();
|
|
4391
4366
|
const appId = config.appId || '';
|
|
4392
|
-
//
|
|
4393
|
-
|
|
4394
|
-
|
|
4367
|
+
// makeApiRequest applies overrides.headers AFTER its computed auth header, so
|
|
4368
|
+
// caller-supplied Authorization is the real request auth when present.
|
|
4369
|
+
const directAuth = authValueFromHeaders(overrides === null || overrides === void 0 ? void 0 : overrides.headers);
|
|
4370
|
+
if (directAuth) {
|
|
4371
|
+
return `${appId}:${principalFromAuthValue(directAuth)}`;
|
|
4395
4372
|
}
|
|
4396
|
-
// Per-request auth-header override (wallet client). Key by the
|
|
4397
|
-
// it produces
|
|
4373
|
+
// Per-request auth-header override (wallet client). Key by the exact opaque
|
|
4374
|
+
// header it produces, not decoded claims or the unverified _walletAddress hint.
|
|
4398
4375
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
4399
4376
|
try {
|
|
4400
4377
|
const headers = await overrides._getAuthHeaders();
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
// Decode the bearer token's subject when present for a stable key across
|
|
4405
|
-
// refreshes; otherwise hash whatever header material we were given.
|
|
4406
|
-
const bearer = authValue.startsWith('Bearer ')
|
|
4407
|
-
? authValue.slice('Bearer '.length)
|
|
4408
|
-
: authValue;
|
|
4409
|
-
const principal = bearer ? principalFromIdToken$1(bearer) : 'anon';
|
|
4410
|
-
return `${appId}:o${principal}`;
|
|
4411
|
-
}
|
|
4412
|
-
catch (_c) {
|
|
4378
|
+
return `${appId}:o${principalFromAuthValue(authValueFromHeaders(headers))}`;
|
|
4379
|
+
}
|
|
4380
|
+
catch (_a) {
|
|
4413
4381
|
// If we can't resolve the override identity, use a unique-ish key so we
|
|
4414
4382
|
// never collide with (and serve) another principal's cached entry.
|
|
4415
|
-
return `${appId}:o${hashForKey$
|
|
4383
|
+
return `${appId}:o${hashForKey$2(String(Date.now()) + Math.random())}`;
|
|
4416
4384
|
}
|
|
4417
4385
|
}
|
|
4418
|
-
// Direct per-request header override. makeApiRequest applies overrides.headers
|
|
4419
|
-
// AFTER its computed auth header (api.ts), so a caller-supplied Authorization
|
|
4420
|
-
// is the REAL request auth — key the cache by it so an entry is never served
|
|
4421
|
-
// to a different principal under the ambient key. (Hardening: no in-repo read
|
|
4422
|
-
// caller passes this today, but the cache must never trail the actual auth.)
|
|
4423
|
-
const directAuth = ((_a = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _a === void 0 ? void 0 : _a.Authorization) ||
|
|
4424
|
-
((_b = overrides === null || overrides === void 0 ? void 0 : overrides.headers) === null || _b === void 0 ? void 0 : _b.authorization);
|
|
4425
|
-
if (directAuth) {
|
|
4426
|
-
const bearer = directAuth.startsWith('Bearer ')
|
|
4427
|
-
? directAuth.slice('Bearer '.length)
|
|
4428
|
-
: directAuth;
|
|
4429
|
-
return `${appId}:h${bearer ? principalFromIdToken$1(bearer) : hashForKey$1(directAuth)}`;
|
|
4430
|
-
}
|
|
4431
4386
|
// Ambient session principal.
|
|
4432
4387
|
const idToken = await getIdToken(config.isServer);
|
|
4433
4388
|
return `${appId}:${principalFromIdToken$1(idToken)}`;
|
|
@@ -4727,9 +4682,9 @@ async function get(path, opts = {}) {
|
|
|
4727
4682
|
const shapeKey = opts.shape ? JSON.stringify(opts.shape) : '';
|
|
4728
4683
|
const includeSubPathsKey = opts.includeSubPaths ? ':subpaths' : '';
|
|
4729
4684
|
const limitKey = opts.limit !== undefined ? `:l${opts.limit}` : '';
|
|
4730
|
-
const cursorKey = opts.cursor ? `:c${hashForKey$
|
|
4731
|
-
const filterKey = opts.filter ? `:f${hashForKey$
|
|
4732
|
-
const sortKey = opts.sort ? `:s${hashForKey$
|
|
4685
|
+
const cursorKey = opts.cursor ? `:c${hashForKey$2(opts.cursor)}` : '';
|
|
4686
|
+
const filterKey = opts.filter ? `:f${hashForKey$2(JSON.stringify(opts.filter))}` : '';
|
|
4687
|
+
const sortKey = opts.sort ? `:s${hashForKey$2(JSON.stringify(opts.sort))}` : '';
|
|
4733
4688
|
const principalKey = await getReadPrincipalKey(opts._overrides);
|
|
4734
4689
|
const cacheKey = `${principalKey}|${normalizedPath}:${opts.prompt || ''}${filterKey}${sortKey}${includeSubPathsKey}:${shapeKey}${limitKey}${cursorKey}`;
|
|
4735
4690
|
const now = Date.now();
|
|
@@ -5479,7 +5434,7 @@ let reconnectInProgress = null;
|
|
|
5479
5434
|
function generateSubscriptionId() {
|
|
5480
5435
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5481
5436
|
}
|
|
5482
|
-
function hashForKey(value) {
|
|
5437
|
+
function hashForKey$1(value) {
|
|
5483
5438
|
let h = 5381;
|
|
5484
5439
|
for (let i = 0; i < value.length; i++) {
|
|
5485
5440
|
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
@@ -5498,49 +5453,23 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
|
5498
5453
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
5499
5454
|
const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
|
|
5500
5455
|
const limitKey = limit !== undefined ? `:l${limit}` : '';
|
|
5501
|
-
const cursorKey = cursor ? `:c${hashForKey(cursor)}` : '';
|
|
5502
|
-
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey(JSON.stringify(filter))}` : '';
|
|
5456
|
+
const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
|
|
5457
|
+
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
|
|
5503
5458
|
const identityKey = identity || 'anon';
|
|
5504
5459
|
return `${identityKey}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
5505
5460
|
}
|
|
5506
5461
|
/**
|
|
5507
|
-
* Derive
|
|
5508
|
-
*
|
|
5509
|
-
*
|
|
5462
|
+
* Derive an opaque identity string for the bearer material a subscription sends.
|
|
5463
|
+
* JWT payloads are deliberately not trusted here: cache isolation must happen
|
|
5464
|
+
* before the server has verified any claims.
|
|
5510
5465
|
*/
|
|
5511
5466
|
function principalFromIdToken(idToken) {
|
|
5512
|
-
|
|
5513
|
-
return 'anon';
|
|
5514
|
-
try {
|
|
5515
|
-
const parts = idToken.split('.');
|
|
5516
|
-
if (parts.length < 2)
|
|
5517
|
-
return `t${hashForKey(idToken)}`;
|
|
5518
|
-
const payload = JSON.parse(decodeBase64Url(parts[1]));
|
|
5519
|
-
const subject =
|
|
5520
|
-
// Universal identity (@user.id) first — the stable principal a request
|
|
5521
|
-
// presents (the account id for email/social logins, which carry no
|
|
5522
|
-
// walletAddress). Keeps the H1 response-cache scoping correct for them.
|
|
5523
|
-
payload['custom:userId'] ||
|
|
5524
|
-
payload['custom:walletAddress'] ||
|
|
5525
|
-
payload.walletAddress ||
|
|
5526
|
-
payload.sub ||
|
|
5527
|
-
payload.address;
|
|
5528
|
-
if (subject)
|
|
5529
|
-
return `s${hashForKey(String(subject))}`;
|
|
5530
|
-
return `t${hashForKey(idToken)}`;
|
|
5531
|
-
}
|
|
5532
|
-
catch (_a) {
|
|
5533
|
-
return `t${hashForKey(idToken)}`;
|
|
5534
|
-
}
|
|
5467
|
+
return idToken ? `t${hashForKey$1(idToken)}` : 'anon';
|
|
5535
5468
|
}
|
|
5536
5469
|
async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
|
|
5537
|
-
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
5538
|
-
// wallet's own token, mirroring getReadPrincipalKey
|
|
5539
|
-
//
|
|
5540
|
-
// cached snapshots are never crossed with another principal's.
|
|
5541
|
-
if (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress) {
|
|
5542
|
-
return `${effectiveAppId}:w${overrides._walletAddress}`;
|
|
5543
|
-
}
|
|
5470
|
+
// Per-subscription wallet override (server WalletClient.subscribe): key by
|
|
5471
|
+
// the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
|
|
5472
|
+
// not trust decoded claims or the unverified _walletAddress hint.
|
|
5544
5473
|
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
5545
5474
|
try {
|
|
5546
5475
|
const bearer = bearerFromAuthHeaders(await overrides._getAuthHeaders());
|
|
@@ -5911,7 +5840,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5911
5840
|
return connection;
|
|
5912
5841
|
}
|
|
5913
5842
|
function handleServerMessage(connection, message) {
|
|
5914
|
-
var _a
|
|
5843
|
+
var _a;
|
|
5915
5844
|
switch (message.type) {
|
|
5916
5845
|
case 'authenticated': {
|
|
5917
5846
|
connection.isAuthenticating = false;
|
|
@@ -6005,28 +5934,27 @@ function handleServerMessage(connection, message) {
|
|
|
6005
5934
|
err.subscriptionId = message.subscriptionId;
|
|
6006
5935
|
return err;
|
|
6007
5936
|
};
|
|
5937
|
+
const wsError = buildWsError();
|
|
6008
5938
|
// Handle CRUD request errors (requestId present)
|
|
6009
5939
|
if (message.requestId) {
|
|
6010
5940
|
const pendingReq = connection.pendingRequests.get(message.requestId);
|
|
6011
5941
|
if (pendingReq) {
|
|
6012
5942
|
connection.pendingRequests.delete(message.requestId);
|
|
6013
5943
|
clearTimeout(pendingReq.timer);
|
|
6014
|
-
pendingReq.reject(
|
|
5944
|
+
pendingReq.reject(wsError);
|
|
6015
5945
|
}
|
|
6016
5946
|
}
|
|
6017
5947
|
if (message.subscriptionId) {
|
|
6018
5948
|
// Reject pending subscription if this is a subscription error
|
|
6019
5949
|
const pending = connection.pendingSubscriptions.get(message.subscriptionId);
|
|
6020
5950
|
if (pending) {
|
|
6021
|
-
pending.reject(
|
|
5951
|
+
pending.reject(wsError);
|
|
6022
5952
|
connection.pendingSubscriptions.delete(message.subscriptionId);
|
|
6023
5953
|
}
|
|
6024
5954
|
// Notify error callbacks for this subscription
|
|
6025
5955
|
const subscription = connection.subscriptions.get(message.subscriptionId);
|
|
6026
5956
|
if (subscription) {
|
|
6027
|
-
|
|
6028
|
-
(_b = callback.onError) === null || _b === void 0 ? void 0 : _b.call(callback, buildWsError());
|
|
6029
|
-
}
|
|
5957
|
+
notifyErrorCallbacks(subscription, wsError);
|
|
6030
5958
|
}
|
|
6031
5959
|
}
|
|
6032
5960
|
break;
|
|
@@ -6079,6 +6007,25 @@ function notifyCallbacks(subscription, data) {
|
|
|
6079
6007
|
}
|
|
6080
6008
|
}
|
|
6081
6009
|
}
|
|
6010
|
+
function notifyErrorCallbacks(subscription, error) {
|
|
6011
|
+
let delivered = false;
|
|
6012
|
+
const callbacks = subscription.callbacks.slice();
|
|
6013
|
+
for (const callback of callbacks) {
|
|
6014
|
+
if (!callback.onError)
|
|
6015
|
+
continue;
|
|
6016
|
+
delivered = true;
|
|
6017
|
+
try {
|
|
6018
|
+
callback.onError(error);
|
|
6019
|
+
}
|
|
6020
|
+
catch (callbackError) {
|
|
6021
|
+
console.error('[WS v2] Error in subscription error callback:', callbackError);
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
if (delivered) {
|
|
6025
|
+
error.__boundedDeliveredToOnError = true;
|
|
6026
|
+
}
|
|
6027
|
+
return delivered;
|
|
6028
|
+
}
|
|
6082
6029
|
// WebSocket readyState constants
|
|
6083
6030
|
const WS_READY_STATE_OPEN = 1;
|
|
6084
6031
|
const WS_READY_STATE_CLOSED = 3;
|
|
@@ -6153,10 +6100,8 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6153
6100
|
const authTokenProvider = (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders)
|
|
6154
6101
|
? async () => bearerFromAuthHeaders(await overrides._getAuthHeaders()) || null
|
|
6155
6102
|
: undefined;
|
|
6156
|
-
const principalKey = (overrides === null || overrides === void 0 ? void 0 : overrides._walletAddress)
|
|
6157
|
-
? `w${overrides._walletAddress}`
|
|
6158
|
-
: (authTokenProvider ? `o${principalFromIdToken(await authTokenProvider())}` : undefined);
|
|
6159
6103
|
const identity = await getSubscriptionIdentity(effectiveAppId, config.isServer, overrides);
|
|
6104
|
+
const principalKey = authTokenProvider ? identity : undefined;
|
|
6160
6105
|
const cacheKey = getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity);
|
|
6161
6106
|
// Deliver cached data immediately if available
|
|
6162
6107
|
const cachedEntry = responseCache.get(cacheKey);
|
|
@@ -6235,7 +6180,18 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6235
6180
|
await subscriptionPromise;
|
|
6236
6181
|
}
|
|
6237
6182
|
catch (error) {
|
|
6238
|
-
|
|
6183
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
6184
|
+
if (!err.__boundedDeliveredToOnError) {
|
|
6185
|
+
notifyErrorCallbacks(subscription, err);
|
|
6186
|
+
}
|
|
6187
|
+
if (!subscriptionOptions.onError) {
|
|
6188
|
+
connection.pendingSubscriptions.delete(subscriptionId);
|
|
6189
|
+
connection.subscriptions.delete(subscriptionId);
|
|
6190
|
+
throw err;
|
|
6191
|
+
}
|
|
6192
|
+
if (!err.__boundedDeliveredToOnError) {
|
|
6193
|
+
console.warn('[WS v2] Subscription confirmation failed, keeping for reconnect recovery:', error);
|
|
6194
|
+
}
|
|
6239
6195
|
}
|
|
6240
6196
|
}
|
|
6241
6197
|
// Return unsubscribe function
|
|
@@ -6398,6 +6354,13 @@ async function doReconnectWithNewAuth() {
|
|
|
6398
6354
|
catch (error) {
|
|
6399
6355
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6400
6356
|
}
|
|
6357
|
+
try {
|
|
6358
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return realtimeStore; });
|
|
6359
|
+
await reconnectRealtimeStoreWithNewAuth();
|
|
6360
|
+
}
|
|
6361
|
+
catch (error) {
|
|
6362
|
+
console.warn('[WS v2] Failed to reset legacy realtime store on auth change:', error);
|
|
6363
|
+
}
|
|
6401
6364
|
for (const [appId, connection] of connections) {
|
|
6402
6365
|
if (!connection.ws) {
|
|
6403
6366
|
continue;
|
|
@@ -6865,6 +6828,16 @@ async function idbSet(key, value) {
|
|
|
6865
6828
|
// RealtimeStore
|
|
6866
6829
|
// ---------------------------------------------------------------------------
|
|
6867
6830
|
let nextRequestId = 1;
|
|
6831
|
+
function hashForKey(value) {
|
|
6832
|
+
let h = 5381;
|
|
6833
|
+
for (let i = 0; i < value.length; i++) {
|
|
6834
|
+
h = ((h << 5) + h + value.charCodeAt(i)) & 0x7fffffff;
|
|
6835
|
+
}
|
|
6836
|
+
return h.toString(36);
|
|
6837
|
+
}
|
|
6838
|
+
function principalFromToken(token) {
|
|
6839
|
+
return token ? `t${hashForKey(token)}` : 'anon';
|
|
6840
|
+
}
|
|
6868
6841
|
class RealtimeStore {
|
|
6869
6842
|
constructor() {
|
|
6870
6843
|
this.ws = null;
|
|
@@ -6880,7 +6853,9 @@ class RealtimeStore {
|
|
|
6880
6853
|
this.idbDirtyKeys = new Set();
|
|
6881
6854
|
this.closed = false;
|
|
6882
6855
|
this.authToken = null;
|
|
6856
|
+
this.authPrincipalKey = 'anon';
|
|
6883
6857
|
this.authenticating = false;
|
|
6858
|
+
this.suppressNextReconnect = false;
|
|
6884
6859
|
this.isServer = false;
|
|
6885
6860
|
this.tokenRefreshTimer = null;
|
|
6886
6861
|
// -----------------------------------------------------------------------
|
|
@@ -6900,34 +6875,98 @@ class RealtimeStore {
|
|
|
6900
6875
|
this.startTokenRefresh();
|
|
6901
6876
|
}
|
|
6902
6877
|
async refreshToken() {
|
|
6878
|
+
let token = null;
|
|
6903
6879
|
try {
|
|
6904
6880
|
const { getIdToken } = await Promise.resolve().then(function () { return utils; });
|
|
6905
|
-
|
|
6906
|
-
if (token)
|
|
6907
|
-
this.authToken = token;
|
|
6881
|
+
token = await getIdToken(this.isServer);
|
|
6908
6882
|
}
|
|
6909
6883
|
catch ( /* no auth available */_a) { /* no auth available */ }
|
|
6884
|
+
this.authToken = token !== null && token !== void 0 ? token : null;
|
|
6885
|
+
this.authPrincipalKey = principalFromToken(this.authToken);
|
|
6910
6886
|
}
|
|
6911
6887
|
startTokenRefresh() {
|
|
6912
6888
|
if (this.tokenRefreshTimer)
|
|
6913
6889
|
return;
|
|
6914
6890
|
this.tokenRefreshTimer = setInterval(async () => {
|
|
6915
|
-
|
|
6916
|
-
const prevToken = this.authToken;
|
|
6891
|
+
const prevPrincipal = this.authPrincipalKey;
|
|
6917
6892
|
await this.refreshToken();
|
|
6918
|
-
if (this.
|
|
6919
|
-
|
|
6920
|
-
this.
|
|
6893
|
+
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6894
|
+
await this.applyAuthPrincipalChange();
|
|
6895
|
+
if (this.subscriptions.size > 0) {
|
|
6896
|
+
await this.ensureConnected().catch(() => {
|
|
6897
|
+
this.setAllSubscriptionStatus('error');
|
|
6898
|
+
});
|
|
6899
|
+
}
|
|
6921
6900
|
}
|
|
6922
6901
|
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
6923
6902
|
}
|
|
6903
|
+
async ensureInitialized() {
|
|
6904
|
+
if (this.appId)
|
|
6905
|
+
return;
|
|
6906
|
+
if (!this.initPromise)
|
|
6907
|
+
this.initPromise = this.init();
|
|
6908
|
+
await this.initPromise;
|
|
6909
|
+
}
|
|
6910
|
+
async ensureCurrentAuth() {
|
|
6911
|
+
await this.ensureInitialized();
|
|
6912
|
+
const prevPrincipal = this.authPrincipalKey;
|
|
6913
|
+
await this.refreshToken();
|
|
6914
|
+
if (this.authPrincipalKey !== prevPrincipal) {
|
|
6915
|
+
await this.applyAuthPrincipalChange();
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
rekeySubscriptionsForPrincipal() {
|
|
6919
|
+
const subs = Array.from(this.subscriptions.values());
|
|
6920
|
+
this.subscriptions.clear();
|
|
6921
|
+
for (const sub of subs) {
|
|
6922
|
+
this.subscriptions.set(this.getSubKey(sub.path, sub.options), sub);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
async applyAuthPrincipalChange() {
|
|
6926
|
+
if (this.idbFlushTimer) {
|
|
6927
|
+
clearTimeout(this.idbFlushTimer);
|
|
6928
|
+
this.idbFlushTimer = null;
|
|
6929
|
+
}
|
|
6930
|
+
this.idbDirtyKeys.clear();
|
|
6931
|
+
this.rekeySubscriptionsForPrincipal();
|
|
6932
|
+
for (const sub of this.subscriptions.values()) {
|
|
6933
|
+
sub.docs.clear();
|
|
6934
|
+
sub.ref.current = sub.docs;
|
|
6935
|
+
sub.error = null;
|
|
6936
|
+
sub.isStale = false;
|
|
6937
|
+
let loaded = false;
|
|
6938
|
+
if (sub.tier !== 'ephemeral') {
|
|
6939
|
+
const cached = await idbGet(this.idbKey(sub.path));
|
|
6940
|
+
if (cached && Array.isArray(cached)) {
|
|
6941
|
+
for (const doc of cached) {
|
|
6942
|
+
if (doc && doc._id)
|
|
6943
|
+
sub.docs.set(doc._id, doc);
|
|
6944
|
+
}
|
|
6945
|
+
sub.ref.current = sub.docs;
|
|
6946
|
+
loaded = sub.docs.size > 0;
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
sub.status = loaded ? 'cached' : 'loading';
|
|
6950
|
+
sub.isStale = loaded;
|
|
6951
|
+
if (loaded)
|
|
6952
|
+
this.notifySubscription(sub);
|
|
6953
|
+
else
|
|
6954
|
+
this.notifyState(sub);
|
|
6955
|
+
}
|
|
6956
|
+
if (this.ws) {
|
|
6957
|
+
const ws = this.ws;
|
|
6958
|
+
this.ws = null;
|
|
6959
|
+
this.connectPromise = null;
|
|
6960
|
+
this.suppressNextReconnect = true;
|
|
6961
|
+
try {
|
|
6962
|
+
ws.close(1000, 'Auth changed');
|
|
6963
|
+
}
|
|
6964
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6924
6967
|
async ensureConnected() {
|
|
6925
6968
|
var _a;
|
|
6926
|
-
|
|
6927
|
-
if (!this.initPromise)
|
|
6928
|
-
this.initPromise = this.init();
|
|
6929
|
-
await this.initPromise;
|
|
6930
|
-
}
|
|
6969
|
+
await this.ensureCurrentAuth();
|
|
6931
6970
|
if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
|
|
6932
6971
|
return;
|
|
6933
6972
|
if (this.connectPromise)
|
|
@@ -7011,11 +7050,20 @@ class RealtimeStore {
|
|
|
7011
7050
|
ws.addEventListener('close', () => {
|
|
7012
7051
|
if (authTimer)
|
|
7013
7052
|
clearTimeout(authTimer);
|
|
7053
|
+
if (this.ws !== ws) {
|
|
7054
|
+
if (this.suppressNextReconnect)
|
|
7055
|
+
this.suppressNextReconnect = false;
|
|
7056
|
+
return;
|
|
7057
|
+
}
|
|
7014
7058
|
this.authenticating = false;
|
|
7015
7059
|
this.ws = null;
|
|
7016
7060
|
this.connectPromise = null;
|
|
7017
7061
|
this.rejectAllPending('WebSocket closed');
|
|
7018
7062
|
this.setAllSubscriptionStatus('reconnecting');
|
|
7063
|
+
if (this.suppressNextReconnect) {
|
|
7064
|
+
this.suppressNextReconnect = false;
|
|
7065
|
+
return;
|
|
7066
|
+
}
|
|
7019
7067
|
this.scheduleReconnect();
|
|
7020
7068
|
});
|
|
7021
7069
|
});
|
|
@@ -7191,14 +7239,34 @@ class RealtimeStore {
|
|
|
7191
7239
|
}
|
|
7192
7240
|
}
|
|
7193
7241
|
handleError(msg) {
|
|
7194
|
-
var _a;
|
|
7242
|
+
var _a, _b, _c;
|
|
7243
|
+
const error = new Error((_a = msg.message) !== null && _a !== void 0 ? _a : (msg.code ? `${msg.code}: Server error` : 'Server error'));
|
|
7244
|
+
if (msg.code)
|
|
7245
|
+
error.code = msg.code;
|
|
7246
|
+
if (msg.subscriptionId || msg.id)
|
|
7247
|
+
error.subscriptionId = (_b = msg.subscriptionId) !== null && _b !== void 0 ? _b : msg.id;
|
|
7195
7248
|
const requestId = msg.requestId;
|
|
7196
7249
|
if (requestId) {
|
|
7197
7250
|
const pending = this.pendingRequests.get(requestId);
|
|
7198
7251
|
if (pending) {
|
|
7199
7252
|
this.pendingRequests.delete(requestId);
|
|
7200
7253
|
clearTimeout(pending.timeout);
|
|
7201
|
-
pending.reject(
|
|
7254
|
+
pending.reject(error);
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
const subId = (_c = msg.subscriptionId) !== null && _c !== void 0 ? _c : msg.id;
|
|
7258
|
+
if (subId) {
|
|
7259
|
+
const sub = this.findSubscriptionById(subId);
|
|
7260
|
+
if (sub) {
|
|
7261
|
+
sub.status = 'error';
|
|
7262
|
+
sub.error = error;
|
|
7263
|
+
this.notifyState(sub);
|
|
7264
|
+
for (const callback of Array.from(sub.errorCallbacks)) {
|
|
7265
|
+
try {
|
|
7266
|
+
callback(error);
|
|
7267
|
+
}
|
|
7268
|
+
catch ( /* swallow */_d) { /* swallow */ }
|
|
7269
|
+
}
|
|
7202
7270
|
}
|
|
7203
7271
|
}
|
|
7204
7272
|
}
|
|
@@ -7207,6 +7275,7 @@ class RealtimeStore {
|
|
|
7207
7275
|
// -----------------------------------------------------------------------
|
|
7208
7276
|
async subscribe(path, opts = {}) {
|
|
7209
7277
|
var _a;
|
|
7278
|
+
await this.ensureCurrentAuth();
|
|
7210
7279
|
const tier = (_a = opts.tier) !== null && _a !== void 0 ? _a : 'durable';
|
|
7211
7280
|
const subKey = this.getSubKey(path, opts);
|
|
7212
7281
|
let sub = this.subscriptions.get(subKey);
|
|
@@ -7216,6 +7285,8 @@ class RealtimeStore {
|
|
|
7216
7285
|
sub.callbacks.add(opts.onData);
|
|
7217
7286
|
if (opts.onState)
|
|
7218
7287
|
sub.stateCallbacks.add(opts.onState);
|
|
7288
|
+
if (opts.onError)
|
|
7289
|
+
sub.errorCallbacks.add(opts.onError);
|
|
7219
7290
|
// Immediately deliver current state
|
|
7220
7291
|
if (opts.onData && sub.docs.size > 0) {
|
|
7221
7292
|
opts.onData(this.docsToArray(sub));
|
|
@@ -7223,7 +7294,7 @@ class RealtimeStore {
|
|
|
7223
7294
|
if (opts.onState) {
|
|
7224
7295
|
opts.onState(this.getState(sub));
|
|
7225
7296
|
}
|
|
7226
|
-
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
7297
|
+
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7227
7298
|
}
|
|
7228
7299
|
// New subscription
|
|
7229
7300
|
const subId = `sub_${nextRequestId++}`;
|
|
@@ -7238,6 +7309,7 @@ class RealtimeStore {
|
|
|
7238
7309
|
error: null,
|
|
7239
7310
|
callbacks: new Set(opts.onData ? [opts.onData] : []),
|
|
7240
7311
|
stateCallbacks: new Set(opts.onState ? [opts.onState] : []),
|
|
7312
|
+
errorCallbacks: new Set(opts.onError ? [opts.onError] : []),
|
|
7241
7313
|
ref: { current: new Map() },
|
|
7242
7314
|
};
|
|
7243
7315
|
this.subscriptions.set(subKey, sub);
|
|
@@ -7267,7 +7339,7 @@ class RealtimeStore {
|
|
|
7267
7339
|
sub.error = new Error('Connection failed');
|
|
7268
7340
|
this.notifyState(sub);
|
|
7269
7341
|
}
|
|
7270
|
-
return this.createUnsubscribe(subKey, opts.onData, opts.onState);
|
|
7342
|
+
return this.createUnsubscribe(subKey, sub.id, opts.onData, opts.onState, opts.onError);
|
|
7271
7343
|
}
|
|
7272
7344
|
getRef(path, opts = {}) {
|
|
7273
7345
|
var _a;
|
|
@@ -7341,6 +7413,7 @@ class RealtimeStore {
|
|
|
7341
7413
|
}
|
|
7342
7414
|
}
|
|
7343
7415
|
async get(path) {
|
|
7416
|
+
await this.ensureCurrentAuth();
|
|
7344
7417
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
7345
7418
|
// Check local subscriptions first
|
|
7346
7419
|
const collectionPath = this.getCollectionPath(normalizedPath);
|
|
@@ -7521,7 +7594,7 @@ class RealtimeStore {
|
|
|
7521
7594
|
return docPath;
|
|
7522
7595
|
}
|
|
7523
7596
|
getSubKey(path, opts) {
|
|
7524
|
-
const parts = [path];
|
|
7597
|
+
const parts = [this.appId, this.authPrincipalKey, path];
|
|
7525
7598
|
if (opts.filter)
|
|
7526
7599
|
parts.push(JSON.stringify(opts.filter));
|
|
7527
7600
|
if (opts.prompt)
|
|
@@ -7531,7 +7604,7 @@ class RealtimeStore {
|
|
|
7531
7604
|
return parts.join('::');
|
|
7532
7605
|
}
|
|
7533
7606
|
idbKey(path) {
|
|
7534
|
-
return `${this.appId}:${path}`;
|
|
7607
|
+
return `${this.appId}:${this.authPrincipalKey}:${path}`;
|
|
7535
7608
|
}
|
|
7536
7609
|
markIdbDirty(path) {
|
|
7537
7610
|
const sub = this.findSubscriptionByPath(path);
|
|
@@ -7556,18 +7629,23 @@ class RealtimeStore {
|
|
|
7556
7629
|
}
|
|
7557
7630
|
}
|
|
7558
7631
|
}
|
|
7559
|
-
createUnsubscribe(subKey, onData, onState) {
|
|
7632
|
+
createUnsubscribe(subKey, subId, onData, onState, onError) {
|
|
7560
7633
|
return async () => {
|
|
7561
|
-
|
|
7634
|
+
var _a;
|
|
7635
|
+
const sub = (_a = this.subscriptions.get(subKey)) !== null && _a !== void 0 ? _a : this.findSubscriptionById(subId);
|
|
7562
7636
|
if (!sub)
|
|
7563
7637
|
return;
|
|
7638
|
+
const currentSubKey = this.getSubKey(sub.path, sub.options);
|
|
7564
7639
|
if (onData)
|
|
7565
7640
|
sub.callbacks.delete(onData);
|
|
7566
7641
|
if (onState)
|
|
7567
7642
|
sub.stateCallbacks.delete(onState);
|
|
7643
|
+
if (onError)
|
|
7644
|
+
sub.errorCallbacks.delete(onError);
|
|
7568
7645
|
// If no more callbacks, unsubscribe entirely
|
|
7569
|
-
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0) {
|
|
7646
|
+
if (sub.callbacks.size === 0 && sub.stateCallbacks.size === 0 && sub.errorCallbacks.size === 0) {
|
|
7570
7647
|
this.subscriptions.delete(subKey);
|
|
7648
|
+
this.subscriptions.delete(currentSubKey);
|
|
7571
7649
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
7572
7650
|
this.ws.send(JSON.stringify({
|
|
7573
7651
|
type: 'unsubscribe',
|
|
@@ -7639,6 +7717,22 @@ class RealtimeStore {
|
|
|
7639
7717
|
this.rejectAllPending('Store closed');
|
|
7640
7718
|
this.subscriptions.clear();
|
|
7641
7719
|
}
|
|
7720
|
+
async reconnectWithNewAuth() {
|
|
7721
|
+
if (this.closed)
|
|
7722
|
+
return;
|
|
7723
|
+
await this.ensureInitialized();
|
|
7724
|
+
await this.refreshToken();
|
|
7725
|
+
await this.applyAuthPrincipalChange();
|
|
7726
|
+
if (this.subscriptions.size > 0) {
|
|
7727
|
+
await this.ensureConnected().catch((error) => {
|
|
7728
|
+
this.setAllSubscriptionStatus('error');
|
|
7729
|
+
for (const sub of this.subscriptions.values()) {
|
|
7730
|
+
sub.error = error instanceof Error ? error : new Error(String(error));
|
|
7731
|
+
this.notifyState(sub);
|
|
7732
|
+
}
|
|
7733
|
+
});
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7642
7736
|
}
|
|
7643
7737
|
// ---------------------------------------------------------------------------
|
|
7644
7738
|
// Singleton instance
|
|
@@ -7656,6 +7750,19 @@ function resetRealtimeStore() {
|
|
|
7656
7750
|
storeInstance = null;
|
|
7657
7751
|
}
|
|
7658
7752
|
}
|
|
7753
|
+
async function reconnectRealtimeStoreWithNewAuth() {
|
|
7754
|
+
if (storeInstance) {
|
|
7755
|
+
await storeInstance.reconnectWithNewAuth();
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
var realtimeStore = /*#__PURE__*/Object.freeze({
|
|
7760
|
+
__proto__: null,
|
|
7761
|
+
RealtimeStore: RealtimeStore,
|
|
7762
|
+
getRealtimeStore: getRealtimeStore,
|
|
7763
|
+
reconnectRealtimeStoreWithNewAuth: reconnectRealtimeStoreWithNewAuth,
|
|
7764
|
+
resetRealtimeStore: resetRealtimeStore
|
|
7765
|
+
});
|
|
7659
7766
|
|
|
7660
7767
|
// ---------------------------------------------------------------------------
|
|
7661
7768
|
// functions.ts -- Bounded Functions client (the imperative escape hatch).
|
|
@@ -7811,6 +7918,23 @@ function realtimeHttpBase(wsApiUrl) {
|
|
|
7811
7918
|
// Strip trailing slash from the resulting origin+path.
|
|
7812
7919
|
return url.toString().replace(/\/$/, '');
|
|
7813
7920
|
}
|
|
7921
|
+
function withoutAuthorization(headers) {
|
|
7922
|
+
if (!headers)
|
|
7923
|
+
return undefined;
|
|
7924
|
+
const clean = {};
|
|
7925
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
7926
|
+
if (key.toLowerCase() === 'authorization')
|
|
7927
|
+
continue;
|
|
7928
|
+
clean[key] = value;
|
|
7929
|
+
}
|
|
7930
|
+
return Object.keys(clean).length > 0 ? clean : undefined;
|
|
7931
|
+
}
|
|
7932
|
+
async function liveAuthHeader(configIsServer, overrides) {
|
|
7933
|
+
if (overrides === null || overrides === void 0 ? void 0 : overrides._getAuthHeaders) {
|
|
7934
|
+
return overrides._getAuthHeaders();
|
|
7935
|
+
}
|
|
7936
|
+
return createAuthHeader(configIsServer);
|
|
7937
|
+
}
|
|
7814
7938
|
/**
|
|
7815
7939
|
* Send a player intent to a running live room. Returns `{ ok: true }`.
|
|
7816
7940
|
*
|
|
@@ -7825,7 +7949,7 @@ function realtimeHttpBase(wsApiUrl) {
|
|
|
7825
7949
|
* transport error / timeout.
|
|
7826
7950
|
*/
|
|
7827
7951
|
async function intent(roomPath, intent, opts = {}) {
|
|
7828
|
-
var _a, _b, _c, _d, _e;
|
|
7952
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
7829
7953
|
if (!roomPath || typeof roomPath !== 'string') {
|
|
7830
7954
|
throw new LiveIntentError('A room path is required');
|
|
7831
7955
|
}
|
|
@@ -7846,37 +7970,48 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7846
7970
|
// (e.g. the first join before subscribeView's WS connects) — HTTP also throws
|
|
7847
7971
|
// on a non-2xx, so errors are surfaced there too.
|
|
7848
7972
|
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7973
|
+
const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
|
|
7974
|
+
if (!hasAuthOverride) {
|
|
7975
|
+
if (opts.fireAndForget) {
|
|
7976
|
+
if (wsIntent(config.appId, normalizedRoomPath, intent))
|
|
7977
|
+
return { ok: true };
|
|
7978
|
+
}
|
|
7979
|
+
else {
|
|
7980
|
+
const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
|
|
7981
|
+
if (ack)
|
|
7982
|
+
return await ack;
|
|
7983
|
+
}
|
|
7857
7984
|
}
|
|
7858
7985
|
const base = realtimeHttpBase(config.wsApiUrl);
|
|
7859
|
-
|
|
7860
|
-
const
|
|
7861
|
-
const
|
|
7986
|
+
const extraHeaders = withoutAuthorization(opts.headers);
|
|
7987
|
+
const overrideHeaders = withoutAuthorization((_b = opts._overrides) === null || _b === void 0 ? void 0 : _b.headers);
|
|
7988
|
+
const buildHeaders = async () => {
|
|
7989
|
+
const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
|
|
7990
|
+
return Object.assign(Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
|
|
7991
|
+
};
|
|
7862
7992
|
const controller = new AbortController();
|
|
7863
|
-
const timeoutMs = (
|
|
7993
|
+
const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
|
|
7864
7994
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7865
7995
|
let res;
|
|
7866
7996
|
try {
|
|
7867
|
-
|
|
7997
|
+
const send = async () => fetch(`${base}/live/intent`, {
|
|
7868
7998
|
method: 'POST',
|
|
7869
|
-
headers,
|
|
7999
|
+
headers: await buildHeaders(),
|
|
7870
8000
|
body: JSON.stringify({ path: normalizedRoomPath, intent }),
|
|
7871
8001
|
signal: controller.signal,
|
|
7872
8002
|
});
|
|
8003
|
+
res = await send();
|
|
8004
|
+
if (res.status === 401 && ((_d = opts._overrides) === null || _d === void 0 ? void 0 : _d._clearAuth)) {
|
|
8005
|
+
await opts._overrides._clearAuth();
|
|
8006
|
+
res = await send();
|
|
8007
|
+
}
|
|
7873
8008
|
}
|
|
7874
8009
|
catch (err) {
|
|
7875
8010
|
clearTimeout(timer);
|
|
7876
8011
|
if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
|
|
7877
8012
|
throw new LiveIntentError(`Live intent to "${roomPath}" timed out after ${timeoutMs}ms`);
|
|
7878
8013
|
}
|
|
7879
|
-
throw new LiveIntentError(`Failed to reach the realtime worker: ${(
|
|
8014
|
+
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)}`);
|
|
7880
8015
|
}
|
|
7881
8016
|
clearTimeout(timer);
|
|
7882
8017
|
let body = null;
|
|
@@ -7885,12 +8020,12 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7885
8020
|
try {
|
|
7886
8021
|
body = JSON.parse(text);
|
|
7887
8022
|
}
|
|
7888
|
-
catch (
|
|
8023
|
+
catch (_h) {
|
|
7889
8024
|
body = { raw: text };
|
|
7890
8025
|
}
|
|
7891
8026
|
}
|
|
7892
8027
|
if (!res.ok) {
|
|
7893
|
-
const message = (
|
|
8028
|
+
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}`;
|
|
7894
8029
|
throw new LiveIntentError(message, res.status, body);
|
|
7895
8030
|
}
|
|
7896
8031
|
return (body !== null && body !== void 0 ? body : { ok: true });
|