@bounded-sh/core 0.0.100-performance.1 → 0.0.101
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/app-token.d.ts +77 -0
- package/dist/client/create-client.d.ts +69 -17
- package/dist/client/subscription-v2.d.ts +23 -0
- package/dist/{index-B8PpRj6x.js → index-BEGeDFQI.js} +739 -106
- package/dist/index-BEGeDFQI.js.map +1 -0
- package/dist/{index-DW5yMQwV.mjs → index-CMT0saxy.mjs} +736 -107
- package/dist/index-CMT0saxy.mjs.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-CeDT7vJF.mjs → realtime-store-DqAAqfo9.mjs} +2 -2
- package/dist/{realtime-store-CeDT7vJF.mjs.map → realtime-store-DqAAqfo9.mjs.map} +1 -1
- package/dist/{realtime-store-D7mJ7rTj.js → realtime-store-DrtEj-bE.js} +2 -2
- package/dist/{realtime-store-D7mJ7rTj.js.map → realtime-store-DrtEj-bE.js.map} +1 -1
- package/dist/{solana-message-w2FxOkFA.js → solana-message-CJTnYiJu.js} +2 -2
- package/dist/{solana-message-w2FxOkFA.js.map → solana-message-CJTnYiJu.js.map} +1 -1
- package/dist/{solana-message-BGkYFU3C.mjs → solana-message-C_717vAx.mjs} +2 -2
- package/dist/{solana-message-BGkYFU3C.mjs.map → solana-message-C_717vAx.mjs.map} +1 -1
- package/dist/{solana-operations-DJQDYdrk.js → solana-operations-CU0YbiMu.js} +3 -3
- package/dist/{solana-operations-DJQDYdrk.js.map → solana-operations-CU0YbiMu.js.map} +1 -1
- package/dist/{solana-operations-CHgW7Pue.mjs → solana-operations-DGNA2nGZ.mjs} +4 -4
- package/dist/{solana-operations-CHgW7Pue.mjs.map → solana-operations-DGNA2nGZ.mjs.map} +1 -1
- package/dist/{solana-v1-operations-BPn-poee.mjs → solana-v1-operations-Bpd4TASG.mjs} +3 -3
- package/dist/{solana-v1-operations-BPn-poee.mjs.map → solana-v1-operations-Bpd4TASG.mjs.map} +1 -1
- package/dist/{solana-v1-operations-BF2rjG23.js → solana-v1-operations-yQII8r1s.js} +3 -3
- package/dist/{solana-v1-operations-BF2rjG23.js.map → solana-v1-operations-yQII8r1s.js.map} +1 -1
- package/package.json +2 -2
- package/dist/index-B8PpRj6x.js.map +0 -1
- package/dist/index-DW5yMQwV.mjs.map +0 -1
|
@@ -4190,7 +4190,7 @@ class ServerSessionManager {
|
|
|
4190
4190
|
const sessionConfig = (_a = this.sessionConfig) !== null && _a !== void 0 ? _a : undefined;
|
|
4191
4191
|
/* fetch nonce from auth API (bound to the snapshotted app+issuer) */
|
|
4192
4192
|
const nonce = await genAuthNonce(sessionConfig);
|
|
4193
|
-
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-
|
|
4193
|
+
const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-CJTnYiJu.js'); });
|
|
4194
4194
|
const message = await genSolanaMessage(address, nonce, sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.appId);
|
|
4195
4195
|
/* sign the message */
|
|
4196
4196
|
const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
|
|
@@ -5282,6 +5282,37 @@ const MIN_BROWSER_RECONNECT_INTERVAL_MS = 5000;
|
|
|
5282
5282
|
// cancel teardown and reuse it; genuinely idle sockets still close promptly.
|
|
5283
5283
|
const IDLE_CONNECTION_CLOSE_GRACE_MS = 5000;
|
|
5284
5284
|
const SUBSCRIPTION_CONFIRM_TIMEOUT_MS = 10000;
|
|
5285
|
+
/**
|
|
5286
|
+
* Live subscriptions the SDK puts on ONE socket before opening another.
|
|
5287
|
+
*
|
|
5288
|
+
* The realtime worker refuses the 51st distinct subscribe on a connection with
|
|
5289
|
+
* `{ code: 'subscription_limit', status: 429 }` - see
|
|
5290
|
+
* `packages/cdk/cloudflare/realtime-worker/src/subscriptions.ts`
|
|
5291
|
+
* (`attachment.subscriptions.length >= 50`). The cap is structural there: every
|
|
5292
|
+
* subscription spec has to fit the 16KB Durable Object hibernation attachment,
|
|
5293
|
+
* and each write fans out across the whole set of one socket.
|
|
5294
|
+
*
|
|
5295
|
+
* 45 leaves deliberate headroom under that 50: two concurrent subscribeV2 calls
|
|
5296
|
+
* can both see room and overshoot by a frame or two, and another tab may already
|
|
5297
|
+
* hold subscriptions on the same DO. A refusal that happens anyway is treated as
|
|
5298
|
+
* backpressure, not a failure (see rehomeRefusedSubscription).
|
|
5299
|
+
*/
|
|
5300
|
+
const SHARD_CAP = 45;
|
|
5301
|
+
/**
|
|
5302
|
+
* Sockets one routing target (app, room, or wallet identity) may hold. Past this
|
|
5303
|
+
* a subscribe fails loudly with `subscription_limit` instead of opening
|
|
5304
|
+
* connections without end - 8 * 45 = 360 concurrent live subscriptions per
|
|
5305
|
+
* target, far beyond any legitimate UI.
|
|
5306
|
+
*/
|
|
5307
|
+
const MAX_SHARDS_PER_KEY = 8;
|
|
5308
|
+
/** Re-homes one logical subscription may attempt before its refusal surfaces. */
|
|
5309
|
+
const MAX_REHOME_ATTEMPTS = MAX_SHARDS_PER_KEY;
|
|
5310
|
+
const SUBSCRIPTION_LIMIT_CODE = 'subscription_limit';
|
|
5311
|
+
// Mirror of the server's `clampSubscriptionLimit` (realtime-worker
|
|
5312
|
+
// src/subscriptions.ts): an omitted or non-numeric limit becomes the default, a
|
|
5313
|
+
// value above the ceiling is clamped down, and a zero/negative limit is rejected.
|
|
5314
|
+
const SUBSCRIPTION_DEFAULT_LIMIT = 100;
|
|
5315
|
+
const SUBSCRIPTION_MAX_LIMIT = 1000;
|
|
5285
5316
|
const WS_CONFIG = {
|
|
5286
5317
|
// Keep retrying indefinitely so long outages recover without page refresh.
|
|
5287
5318
|
maxRetries: Infinity,
|
|
@@ -5302,6 +5333,26 @@ let reconnectInProgress = null;
|
|
|
5302
5333
|
function generateSubscriptionId() {
|
|
5303
5334
|
return `sub_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
5304
5335
|
}
|
|
5336
|
+
/**
|
|
5337
|
+
* Apply the server's own subscription-limit clamp so the client identifies a
|
|
5338
|
+
* subscription by the value the server will actually key it on.
|
|
5339
|
+
*
|
|
5340
|
+
* The worker de-dups a connection's subscriptions on the CLAMPED limit and
|
|
5341
|
+
* REPLACES a colliding entry (sending `unsubscribed` for the displaced id), so
|
|
5342
|
+
* `limit: undefined` and `limit: 100` are one server subscription. Matching on
|
|
5343
|
+
* the raw option made the SDK register two, and the second silently retired the
|
|
5344
|
+
* first stream. `null` mirrors the server's rejection of a zero/negative limit.
|
|
5345
|
+
* Authority: `clampSubscriptionLimit` in realtime-worker src/subscriptions.ts.
|
|
5346
|
+
*/
|
|
5347
|
+
function normalizeSubscriptionLimit(raw) {
|
|
5348
|
+
if (raw === undefined || raw === null)
|
|
5349
|
+
return SUBSCRIPTION_DEFAULT_LIMIT;
|
|
5350
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw))
|
|
5351
|
+
return SUBSCRIPTION_DEFAULT_LIMIT;
|
|
5352
|
+
if (raw <= 0)
|
|
5353
|
+
return null;
|
|
5354
|
+
return raw > SUBSCRIPTION_MAX_LIMIT ? SUBSCRIPTION_MAX_LIMIT : Math.floor(raw);
|
|
5355
|
+
}
|
|
5305
5356
|
function hashForKey$1(value) {
|
|
5306
5357
|
let h = 5381;
|
|
5307
5358
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -5321,7 +5372,11 @@ function hashForKey$1(value) {
|
|
|
5321
5372
|
function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
|
|
5322
5373
|
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
5323
5374
|
const shapeKey = shape && Object.keys(shape).length > 0 ? JSON.stringify(shape) : '';
|
|
5324
|
-
|
|
5375
|
+
// Normalized, for the same reason the de-dup scan is: two options that clamp
|
|
5376
|
+
// to one server subscription must share one cache entry, or a joined callback
|
|
5377
|
+
// cannot read the snapshot its own subscription just wrote.
|
|
5378
|
+
const normalizedLimit = normalizeSubscriptionLimit(limit);
|
|
5379
|
+
const limitKey = `:l${normalizedLimit === null ? 'rejected' : normalizedLimit}`;
|
|
5325
5380
|
const cursorKey = cursor ? `:c${hashForKey$1(cursor)}` : '';
|
|
5326
5381
|
const filterKey = filter && Object.keys(filter).length > 0 ? `:f${hashForKey$1(JSON.stringify(filter))}` : '';
|
|
5327
5382
|
return `${identity}|${normalizedPath}:${prompt || 'default'}:${shapeKey}${limitKey}${cursorKey}${filterKey}`;
|
|
@@ -5650,18 +5705,76 @@ function replaySubscriptions(connection) {
|
|
|
5650
5705
|
* connections key by their routing target; wallet-scoped connections pin the
|
|
5651
5706
|
* exact bearer so two distinct tokens never share a flight or a lease, even
|
|
5652
5707
|
* when their compact `#id#<principalKey>` hashes collide.
|
|
5708
|
+
*
|
|
5709
|
+
* `shardIndex` selects which socket of that target's pool this is. Shard 0 adds
|
|
5710
|
+
* nothing, so every key an unsharded client produces is unchanged.
|
|
5653
5711
|
*/
|
|
5654
|
-
function connectionLeaseKey(appId, routePath, principalKey, principalToken, anonymous) {
|
|
5712
|
+
function connectionLeaseKey(appId, routePath, principalKey, principalToken, anonymous, shardIndex = 0) {
|
|
5655
5713
|
const roomKey = roomKeyFromRoutePath(routePath);
|
|
5656
5714
|
// A read-only scoped instance is anonymous and must never share a socket with
|
|
5657
5715
|
// the authenticated global path even when it targets the same appId.
|
|
5658
5716
|
const appKey = anonymous ? `${appId}#ro` : appId;
|
|
5659
|
-
const base = roomKey ? `${appKey}#room#${roomKey}` : appKey;
|
|
5717
|
+
const base = routingBaseKey(roomKey ? `${appKey}#room#${roomKey}` : appKey, shardIndex);
|
|
5660
5718
|
if (!principalKey)
|
|
5661
5719
|
return base;
|
|
5662
5720
|
const bucket = `${base}#id#${principalKey}`;
|
|
5663
5721
|
return principalToken ? `${bucket}#tok#${principalToken}` : bucket;
|
|
5664
5722
|
}
|
|
5723
|
+
/**
|
|
5724
|
+
* Apply a shard suffix to the ROUTING part of a connection key, ahead of any
|
|
5725
|
+
* `#id#`/`#tok#` identity bucket. Keeping the suffix here means the room/app
|
|
5726
|
+
* prefix a caller already knows (`<appId>#room#<collection>/<roomId>`) is still
|
|
5727
|
+
* the shard-0 key, so existing single-socket lookups keep working verbatim.
|
|
5728
|
+
*/
|
|
5729
|
+
function routingBaseKey(base, shardIndex) {
|
|
5730
|
+
return shardIndex > 0 ? `${base}#s${shardIndex}` : base;
|
|
5731
|
+
}
|
|
5732
|
+
/** The live connection holding a given lease key, across identity collisions. */
|
|
5733
|
+
function connectionForLeaseKey(leaseKey) {
|
|
5734
|
+
for (const connection of connections.values()) {
|
|
5735
|
+
if (connection.acquisitionKey === leaseKey)
|
|
5736
|
+
return connection;
|
|
5737
|
+
}
|
|
5738
|
+
return undefined;
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* The connection that currently owns a subscription id.
|
|
5742
|
+
*
|
|
5743
|
+
* A subscriber's unsubscribe closure captures the connection it registered on,
|
|
5744
|
+
* but a `subscription_limit` re-home can move the subscription to another shard
|
|
5745
|
+
* afterwards. Re-resolving by id means an unsubscribe still reaches the socket
|
|
5746
|
+
* that actually holds it instead of silently no-oping and leaking the
|
|
5747
|
+
* subscription. Ids are minted locally and globally unique, so an id identifies
|
|
5748
|
+
* exactly one subscription across every connection.
|
|
5749
|
+
*/
|
|
5750
|
+
function connectionHoldingSubscription(subscriptionId) {
|
|
5751
|
+
for (const connection of connections.values()) {
|
|
5752
|
+
if (connection.subscriptions.has(subscriptionId))
|
|
5753
|
+
return connection;
|
|
5754
|
+
}
|
|
5755
|
+
return undefined;
|
|
5756
|
+
}
|
|
5757
|
+
/**
|
|
5758
|
+
* Walk a routing target's shard pool, shard 0 first, and return the first
|
|
5759
|
+
* connection the caller accepts. Callers that used to read a single
|
|
5760
|
+
* `connections.get(<base>)` need this because shard 0 closes when it drains
|
|
5761
|
+
* while a later shard is still carrying subscriptions - a bare lookup would
|
|
5762
|
+
* then report "no socket" for an app that has several.
|
|
5763
|
+
*/
|
|
5764
|
+
function findShardConnection(baseKey, accept) {
|
|
5765
|
+
for (let shardIndex = 0; shardIndex < MAX_SHARDS_PER_KEY; shardIndex++) {
|
|
5766
|
+
const connection = connections.get(routingBaseKey(baseKey, shardIndex));
|
|
5767
|
+
if (connection && accept(connection))
|
|
5768
|
+
return connection;
|
|
5769
|
+
}
|
|
5770
|
+
return undefined;
|
|
5771
|
+
}
|
|
5772
|
+
/** An OPEN socket that is not mid-auth: safe to send an authorized frame on. */
|
|
5773
|
+
function isWarmConnection(connection) {
|
|
5774
|
+
return !!connection.ws
|
|
5775
|
+
&& connection.ws.readyState === WS_READY_STATE_OPEN
|
|
5776
|
+
&& !connection.isAuthenticating;
|
|
5777
|
+
}
|
|
5665
5778
|
function cancelIdleConnectionClose(connection) {
|
|
5666
5779
|
if (!connection.idleCloseTimer)
|
|
5667
5780
|
return;
|
|
@@ -5725,9 +5838,9 @@ function scheduleConnectionCloseIfIdle(connection) {
|
|
|
5725
5838
|
* reuse. The lease is released once the caller has registered its
|
|
5726
5839
|
* subscription/callback.
|
|
5727
5840
|
*/
|
|
5728
|
-
async function acquireConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, source) {
|
|
5841
|
+
async function acquireConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, source, shardIndex = 0) {
|
|
5729
5842
|
var _a;
|
|
5730
|
-
const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken, source === null || source === void 0 ? void 0 : source.anonymous);
|
|
5843
|
+
const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken, source === null || source === void 0 ? void 0 : source.anonymous, shardIndex);
|
|
5731
5844
|
connectionAcquisitions.set(leaseKey, ((_a = connectionAcquisitions.get(leaseKey)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
5732
5845
|
let released = false;
|
|
5733
5846
|
let releaseTimer = null;
|
|
@@ -5749,7 +5862,7 @@ async function acquireConnection(appId, isServer, routePath, authTokenProvider,
|
|
|
5749
5862
|
scheduleConnectionCloseIfIdle(connection);
|
|
5750
5863
|
};
|
|
5751
5864
|
try {
|
|
5752
|
-
const connection = await getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, source);
|
|
5865
|
+
const connection = await getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, source, shardIndex);
|
|
5753
5866
|
const releaseConnection = () => release(connection);
|
|
5754
5867
|
// Registration is synchronous after the caller's await. This fail-safe
|
|
5755
5868
|
// releases on the next task even if an option getter/callback throws
|
|
@@ -5762,15 +5875,94 @@ async function acquireConnection(appId, isServer, routePath, authTokenProvider,
|
|
|
5762
5875
|
throw error;
|
|
5763
5876
|
}
|
|
5764
5877
|
}
|
|
5765
|
-
|
|
5878
|
+
function shardTargetFromConnection(connection) {
|
|
5879
|
+
var _a;
|
|
5880
|
+
return {
|
|
5881
|
+
appId: connection.appId,
|
|
5882
|
+
isServer: connection.isServer,
|
|
5883
|
+
routePath: connection.routePath,
|
|
5884
|
+
authTokenProvider: connection.authTokenProvider,
|
|
5885
|
+
principalKey: connection.principalKey,
|
|
5886
|
+
principalToken: (_a = connection.connectionIdentityToken) !== null && _a !== void 0 ? _a : undefined,
|
|
5887
|
+
source: connection.source,
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
function shardLeaseKeyFor(target, shardIndex) {
|
|
5891
|
+
var _a;
|
|
5892
|
+
return connectionLeaseKey(target.appId, target.routePath, target.principalKey, target.principalToken, (_a = target.source) === null || _a === void 0 ? void 0 : _a.anonymous, shardIndex);
|
|
5893
|
+
}
|
|
5894
|
+
function subscriptionLimitExhaustedError(target) {
|
|
5895
|
+
const error = new Error(`${SUBSCRIPTION_LIMIT_CODE}: this client already holds the maximum ${MAX_SHARDS_PER_KEY * SHARD_CAP} live subscriptions for ${target.appId}`);
|
|
5896
|
+
error.code = SUBSCRIPTION_LIMIT_CODE;
|
|
5897
|
+
error.status = 429;
|
|
5898
|
+
return error;
|
|
5899
|
+
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Choose the shard that should carry a subscription.
|
|
5902
|
+
*
|
|
5903
|
+
* The de-dup scan spans EVERY shard of the target before any placement decision:
|
|
5904
|
+
* a second subscriber with identical specs must ref-count onto the existing
|
|
5905
|
+
* subscription wherever it lives, never open a duplicate on another socket.
|
|
5906
|
+
* Failing that, the lowest-index shard with room wins, so a client that drops
|
|
5907
|
+
* back under one socket's worth of subscriptions collapses onto shard 0 again.
|
|
5908
|
+
*/
|
|
5909
|
+
function planSubscriptionShard(target, matches) {
|
|
5910
|
+
let firstShardWithRoom = -1;
|
|
5911
|
+
for (let shardIndex = 0; shardIndex < MAX_SHARDS_PER_KEY; shardIndex++) {
|
|
5912
|
+
const connection = connectionForLeaseKey(shardLeaseKeyFor(target, shardIndex));
|
|
5913
|
+
if (!connection) {
|
|
5914
|
+
if (firstShardWithRoom < 0)
|
|
5915
|
+
firstShardWithRoom = shardIndex;
|
|
5916
|
+
continue;
|
|
5917
|
+
}
|
|
5918
|
+
for (const subscription of connection.subscriptions.values()) {
|
|
5919
|
+
if (matches(subscription))
|
|
5920
|
+
return { shardIndex, existing: subscription };
|
|
5921
|
+
}
|
|
5922
|
+
if (firstShardWithRoom < 0 && connection.subscriptions.size < connection.capacityCeiling) {
|
|
5923
|
+
firstShardWithRoom = shardIndex;
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
return { shardIndex: firstShardWithRoom < 0 ? MAX_SHARDS_PER_KEY : firstShardWithRoom };
|
|
5927
|
+
}
|
|
5928
|
+
/**
|
|
5929
|
+
* Acquire the connection that will carry a subscription, opening another shard
|
|
5930
|
+
* when the current ones are at SHARD_CAP.
|
|
5931
|
+
*
|
|
5932
|
+
* Placement is re-decided after every acquisition because the auth preflight is
|
|
5933
|
+
* an await: concurrently mounting subscribers can fill the shard this call
|
|
5934
|
+
* planned for, and the subscription it planned to join can be gone. Both cases
|
|
5935
|
+
* re-plan rather than overshoot, bounded by the shard ceiling.
|
|
5936
|
+
*/
|
|
5937
|
+
async function acquireSubscriptionShard(target, matches) {
|
|
5938
|
+
for (let attempt = 0; attempt <= MAX_SHARDS_PER_KEY; attempt++) {
|
|
5939
|
+
const placement = planSubscriptionShard(target, matches);
|
|
5940
|
+
if (placement.shardIndex >= MAX_SHARDS_PER_KEY)
|
|
5941
|
+
throw subscriptionLimitExhaustedError(target);
|
|
5942
|
+
const acquisition = await acquireConnection(target.appId, target.isServer, target.routePath, target.authTokenProvider, target.principalKey, target.principalToken, target.source, placement.shardIndex);
|
|
5943
|
+
const { connection } = acquisition;
|
|
5944
|
+
const existing = placement.existing;
|
|
5945
|
+
if (existing) {
|
|
5946
|
+
if (connection.subscriptions.get(existing.subscriptionId) === existing) {
|
|
5947
|
+
return { acquisition, existing };
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
else if (connection.subscriptions.size < connection.capacityCeiling) {
|
|
5951
|
+
return { acquisition };
|
|
5952
|
+
}
|
|
5953
|
+
acquisition.release();
|
|
5954
|
+
}
|
|
5955
|
+
throw subscriptionLimitExhaustedError(target);
|
|
5956
|
+
}
|
|
5957
|
+
async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, source, shardIndex = 0) {
|
|
5766
5958
|
// Singleflight: concurrent first-use of the same routing target shares one
|
|
5767
5959
|
// in-flight connection rather than each creating an orphan socket. Keyed by
|
|
5768
5960
|
// the identity-bucket lease key so distinct wallet tokens never coalesce.
|
|
5769
|
-
const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken, source === null || source === void 0 ? void 0 : source.anonymous);
|
|
5961
|
+
const leaseKey = connectionLeaseKey(appId, routePath, principalKey, principalToken, source === null || source === void 0 ? void 0 : source.anonymous, shardIndex);
|
|
5770
5962
|
const activeFlight = connectionFlights.get(leaseKey);
|
|
5771
5963
|
if (activeFlight)
|
|
5772
5964
|
return activeFlight;
|
|
5773
|
-
const flight = getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source);
|
|
5965
|
+
const flight = getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source, shardIndex);
|
|
5774
5966
|
connectionFlights.set(leaseKey, flight);
|
|
5775
5967
|
try {
|
|
5776
5968
|
return await flight;
|
|
@@ -5781,7 +5973,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
|
|
|
5781
5973
|
}
|
|
5782
5974
|
}
|
|
5783
5975
|
}
|
|
5784
|
-
async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source) {
|
|
5976
|
+
async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenProvider, principalKey, principalToken, leaseKey, source, shardIndex = 0) {
|
|
5785
5977
|
var _a, _b;
|
|
5786
5978
|
attachBrowserReconnectHooksOnce();
|
|
5787
5979
|
const anonymous = (source === null || source === void 0 ? void 0 : source.anonymous) === true;
|
|
@@ -5795,8 +5987,11 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
|
|
|
5795
5987
|
// never shares a connection (or token) with any top-level server caller. A
|
|
5796
5988
|
// read-only scoped (anonymous) instance is likewise isolated by an `#ro`
|
|
5797
5989
|
// marker so it never shares the authenticated global socket on the same appId.
|
|
5990
|
+
// Shard N>0 of the same routing target is a SEPARATE socket to the SAME
|
|
5991
|
+
// Durable Object: the server's cap is per websocket attachment, so an extra
|
|
5992
|
+
// connection buys an extra budget without changing where anything routes.
|
|
5798
5993
|
const appKey = anonymous ? `${appId}#ro` : appId;
|
|
5799
|
-
const base = roomKey ? `${appKey}#room#${roomKey}` : appKey;
|
|
5994
|
+
const base = routingBaseKey(roomKey ? `${appKey}#room#${roomKey}` : appKey, shardIndex);
|
|
5800
5995
|
const identityBucketKey = principalKey
|
|
5801
5996
|
? `${base}#id#${principalKey}`
|
|
5802
5997
|
: base;
|
|
@@ -5871,6 +6066,9 @@ async function getOrCreateConnectionOnce(appId, isServer, routePath, authTokenPr
|
|
|
5871
6066
|
appId,
|
|
5872
6067
|
key: connKey,
|
|
5873
6068
|
acquisitionKey: leaseKey,
|
|
6069
|
+
shardIndex,
|
|
6070
|
+
principalKey,
|
|
6071
|
+
capacityCeiling: SHARD_CAP,
|
|
5874
6072
|
routePath: roomKey ? routePath : undefined,
|
|
5875
6073
|
authTokenProvider,
|
|
5876
6074
|
connectionIdentityToken: principalToken !== null && principalToken !== void 0 ? principalToken : null,
|
|
@@ -6171,6 +6369,16 @@ function handleServerMessage(connection, message) {
|
|
|
6171
6369
|
if (connection.isAuthenticating && connection.pendingAuthToken && !connection.authTokenProvider && !connection.anonymous) {
|
|
6172
6370
|
void recoverFromServerAuthRejection(connection);
|
|
6173
6371
|
}
|
|
6372
|
+
// `subscription_limit` is backpressure on THIS socket, not a verdict
|
|
6373
|
+
// on the read: the DO refuses a subscribe once its hibernation
|
|
6374
|
+
// attachment is full. Move the subscription to another shard instead
|
|
6375
|
+
// of failing the caller. The client shards below the server's cap, so
|
|
6376
|
+
// this is the residual case - another tab on the same DO, a race, or
|
|
6377
|
+
// a server enforcing a lower cap.
|
|
6378
|
+
if (message.code === SUBSCRIPTION_LIMIT_CODE && message.subscriptionId
|
|
6379
|
+
&& rehomeRefusedSubscription(connection, message.subscriptionId, message)) {
|
|
6380
|
+
break;
|
|
6381
|
+
}
|
|
6174
6382
|
// Build an Error that ALSO carries the stable `code` as a structured
|
|
6175
6383
|
// field (not just string-interpolated into the message) so callers can
|
|
6176
6384
|
// branch on it — e.g. `catch (e) { if (e.code === 'invariant_violation') }`
|
|
@@ -6179,6 +6387,11 @@ function handleServerMessage(connection, message) {
|
|
|
6179
6387
|
const buildWsError = () => {
|
|
6180
6388
|
const err = new Error(`${message.code}: ${message.message}`);
|
|
6181
6389
|
err.code = message.code;
|
|
6390
|
+
// The server's HTTP-equivalent status, so a caller can tell 429
|
|
6391
|
+
// backpressure from a 413 oversize frame from a 403 policy
|
|
6392
|
+
// denial without pattern-matching the code string.
|
|
6393
|
+
if (typeof message.status === 'number')
|
|
6394
|
+
err.status = message.status;
|
|
6182
6395
|
if (message.requestId)
|
|
6183
6396
|
err.requestId = message.requestId;
|
|
6184
6397
|
if (message.subscriptionId)
|
|
@@ -6354,6 +6567,23 @@ function rejectPendingSubscriptionConfirmation(connection, subscriptionId, error
|
|
|
6354
6567
|
pending.reject(error);
|
|
6355
6568
|
return true;
|
|
6356
6569
|
}
|
|
6570
|
+
/**
|
|
6571
|
+
* Take a confirmation record off a connection WITHOUT settling it, so it can be
|
|
6572
|
+
* re-armed on another socket. A `subscription_limit` refusal has not failed the
|
|
6573
|
+
* caller's subscribe - the subscription is still being placed - so rejecting the
|
|
6574
|
+
* promise here would turn backpressure into an error the app has to handle.
|
|
6575
|
+
*/
|
|
6576
|
+
function detachPendingSubscriptionConfirmation(connection, subscriptionId) {
|
|
6577
|
+
const pending = connection.pendingSubscriptions.get(subscriptionId);
|
|
6578
|
+
if (!pending)
|
|
6579
|
+
return null;
|
|
6580
|
+
connection.pendingSubscriptions.delete(subscriptionId);
|
|
6581
|
+
if (pending.timer) {
|
|
6582
|
+
clearTimeout(pending.timer);
|
|
6583
|
+
pending.timer = null;
|
|
6584
|
+
}
|
|
6585
|
+
return pending;
|
|
6586
|
+
}
|
|
6357
6587
|
function armSubscriptionConfirmationTimeout(connection, subscription, pending) {
|
|
6358
6588
|
pending.timer = setTimeout(() => {
|
|
6359
6589
|
if (connection.pendingSubscriptions.get(subscription.subscriptionId) !== pending)
|
|
@@ -6447,6 +6677,96 @@ function registerDetachedReplayConfirmation(connection, subscription) {
|
|
|
6447
6677
|
}
|
|
6448
6678
|
});
|
|
6449
6679
|
}
|
|
6680
|
+
function subscriptionLimitError(message) {
|
|
6681
|
+
const error = new Error(`${message.code}: ${message.message}`);
|
|
6682
|
+
error.code = message.code;
|
|
6683
|
+
error.status = typeof message.status === 'number' ? message.status : 429;
|
|
6684
|
+
if (message.subscriptionId)
|
|
6685
|
+
error.subscriptionId = message.subscriptionId;
|
|
6686
|
+
return error;
|
|
6687
|
+
}
|
|
6688
|
+
/**
|
|
6689
|
+
* Handle a server `subscription_limit` refusal as backpressure: unregister the
|
|
6690
|
+
* subscription from the socket that refused it and place it on another shard.
|
|
6691
|
+
*
|
|
6692
|
+
* Unregistering is what makes the refusal non-permanent. A refused subscription
|
|
6693
|
+
* that stayed in `connection.subscriptions` was re-sent by replaySubscriptions()
|
|
6694
|
+
* on every reconnect, to be refused again forever.
|
|
6695
|
+
*
|
|
6696
|
+
* Returns true when this refusal has been taken over and must not also be
|
|
6697
|
+
* reported to the subscription's callbacks.
|
|
6698
|
+
*/
|
|
6699
|
+
function rehomeRefusedSubscription(connection, subscriptionId, message) {
|
|
6700
|
+
const subscription = connection.subscriptions.get(subscriptionId);
|
|
6701
|
+
if (!subscription)
|
|
6702
|
+
return false;
|
|
6703
|
+
connection.subscriptions.delete(subscriptionId);
|
|
6704
|
+
// Believe the server over SHARD_CAP: whatever this socket still holds is the
|
|
6705
|
+
// most it demonstrably accepts. The planner stops offering it until it drains.
|
|
6706
|
+
connection.capacityCeiling = Math.min(connection.capacityCeiling, connection.subscriptions.size);
|
|
6707
|
+
const pending = detachPendingSubscriptionConfirmation(connection, subscriptionId);
|
|
6708
|
+
subscription.rehomeAttempts += 1;
|
|
6709
|
+
if (subscription.rehomeAttempts > MAX_REHOME_ATTEMPTS) {
|
|
6710
|
+
// Every shard refused it. This is the one case where the cap reaches the
|
|
6711
|
+
// app, and it arrives as a real error with `code` and `status`.
|
|
6712
|
+
const error = subscriptionLimitError(message);
|
|
6713
|
+
notifyErrorCallbacks(subscription, error);
|
|
6714
|
+
rejectSubscriptionRefresh(subscription, error);
|
|
6715
|
+
pending === null || pending === void 0 ? void 0 : pending.reject(error);
|
|
6716
|
+
scheduleConnectionCloseIfIdle(connection);
|
|
6717
|
+
return true;
|
|
6718
|
+
}
|
|
6719
|
+
void moveSubscriptionToAnotherShard(connection, subscription, pending, message);
|
|
6720
|
+
scheduleConnectionCloseIfIdle(connection);
|
|
6721
|
+
return true;
|
|
6722
|
+
}
|
|
6723
|
+
async function moveSubscriptionToAnotherShard(refusedConnection, subscription, pending, message) {
|
|
6724
|
+
const target = shardTargetFromConnection(refusedConnection);
|
|
6725
|
+
let acquisition;
|
|
6726
|
+
try {
|
|
6727
|
+
// Never join an existing subscription here: this id is the thing being
|
|
6728
|
+
// placed, and its de-dup identity was already settled when it was created.
|
|
6729
|
+
({ acquisition } = await acquireSubscriptionShard(target, () => false));
|
|
6730
|
+
}
|
|
6731
|
+
catch (error) {
|
|
6732
|
+
const failure = error instanceof Error ? error : subscriptionLimitError(message);
|
|
6733
|
+
notifyErrorCallbacks(subscription, failure);
|
|
6734
|
+
rejectSubscriptionRefresh(subscription, failure);
|
|
6735
|
+
pending === null || pending === void 0 ? void 0 : pending.reject(failure);
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
const connection = acquisition.connection;
|
|
6739
|
+
try {
|
|
6740
|
+
if (subscription.callbacks.length === 0) {
|
|
6741
|
+
// Every consumer unsubscribed while the re-home was in flight.
|
|
6742
|
+
pending === null || pending === void 0 ? void 0 : pending.reject(new Error('Subscription unsubscribed'));
|
|
6743
|
+
return;
|
|
6744
|
+
}
|
|
6745
|
+
// The new socket owes this subscription a fresh snapshot, exactly as a
|
|
6746
|
+
// reconnect replay does, so callbacks are not left holding stale data.
|
|
6747
|
+
subscription.lastData = undefined;
|
|
6748
|
+
connection.subscriptions.set(subscription.subscriptionId, subscription);
|
|
6749
|
+
if (pending) {
|
|
6750
|
+
pending.retrySent = false;
|
|
6751
|
+
connection.pendingSubscriptions.set(subscription.subscriptionId, pending);
|
|
6752
|
+
}
|
|
6753
|
+
else {
|
|
6754
|
+
registerDetachedReplayConfirmation(connection, subscription);
|
|
6755
|
+
}
|
|
6756
|
+
if (isWarmConnection(connection)) {
|
|
6757
|
+
sendSubscribeWithConfirmation(connection, subscription);
|
|
6758
|
+
}
|
|
6759
|
+
else if (pending) {
|
|
6760
|
+
// Cold or authenticating: replaySubscriptions() sends it and re-arms
|
|
6761
|
+
// the per-socket window once the socket authenticates. Bound the wait
|
|
6762
|
+
// until then, the same way an original cold registration does.
|
|
6763
|
+
armSubscriptionConfirmationTimeout(connection, subscription, pending);
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
finally {
|
|
6767
|
+
acquisition.release();
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6450
6770
|
function resolveSubscriptionRefresh(subscription) {
|
|
6451
6771
|
const refresh = subscription.refresh;
|
|
6452
6772
|
if (!refresh)
|
|
@@ -6542,7 +6862,7 @@ function sendUnsubscribe(connection, subscriptionId) {
|
|
|
6542
6862
|
* the subscription onto, since a WS connection routes to a single DO.
|
|
6543
6863
|
*/
|
|
6544
6864
|
async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
6545
|
-
var _a, _b, _c, _d, _e
|
|
6865
|
+
var _a, _b, _c, _d, _e;
|
|
6546
6866
|
const overrides = subscriptionOptions._overrides;
|
|
6547
6867
|
// A scoped createClient() instance passes its own resolved config; the
|
|
6548
6868
|
// default path reads the module-global init() config.
|
|
@@ -6581,16 +6901,52 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6581
6901
|
const cacheKey = responseCacheEnabled
|
|
6582
6902
|
? getCacheKey(normalizedPath, subscriptionOptions.prompt, subscriptionOptions.shape, subscriptionOptions.limit, subscriptionOptions.cursor, subscriptionOptions.filter, identity)
|
|
6583
6903
|
: null;
|
|
6904
|
+
// Stringify THIS call's specs once; existing subscriptions carry their
|
|
6905
|
+
// precomputed specKey, so the shared-subscription scan is a string compare per
|
|
6906
|
+
// entry (previously each new subscribe re-stringified every existing
|
|
6907
|
+
// subscription's shape/filter/sort - O(N²) with many live hooks). Resolved
|
|
6908
|
+
// before acquisition because shard placement needs the de-dup predicate.
|
|
6909
|
+
const specKey = JSON.stringify([
|
|
6910
|
+
(_b = subscriptionOptions.shape) !== null && _b !== void 0 ? _b : null,
|
|
6911
|
+
(_c = subscriptionOptions.filter) !== null && _c !== void 0 ? _c : null,
|
|
6912
|
+
(_d = subscriptionOptions.sort) !== null && _d !== void 0 ? _d : null,
|
|
6913
|
+
]);
|
|
6914
|
+
const normalizedLimit = normalizeSubscriptionLimit(subscriptionOptions.limit);
|
|
6915
|
+
const includeSubPaths = (_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false;
|
|
6916
|
+
// H1: the scan also requires the same principal identity (compact hash AND
|
|
6917
|
+
// exact token) - a different logged-in user must not piggy-back on (and
|
|
6918
|
+
// inherit lastData from) another user's existing subscription, even with
|
|
6919
|
+
// identical path/prompt/shape/filter/sort.
|
|
6920
|
+
const matchesThisSubscription = (sub) => (sub.identity === identity
|
|
6921
|
+
&& sub.identityToken === identityToken
|
|
6922
|
+
&& sub.path === normalizedPath
|
|
6923
|
+
&& sub.prompt === subscriptionOptions.prompt
|
|
6924
|
+
&& sub.specKey === specKey
|
|
6925
|
+
&& sub.includeSubPaths === includeSubPaths
|
|
6926
|
+
&& sub.normalizedLimit === normalizedLimit
|
|
6927
|
+
&& sub.cursor === subscriptionOptions.cursor);
|
|
6584
6928
|
// Acquire a connection for this routing target (room-scoped when a room route
|
|
6585
|
-
// is supplied by the live helper, else the app-level connection)
|
|
6586
|
-
//
|
|
6587
|
-
//
|
|
6929
|
+
// is supplied by the live helper, else the app-level connection), on the shard
|
|
6930
|
+
// that either already holds this exact subscription or still has room under
|
|
6931
|
+
// the server's per-socket cap. The lease is held across auth preflight and
|
|
6932
|
+
// released once this call registers below, so a concurrent old-route teardown
|
|
6933
|
+
// cannot close a socket we are about to reuse.
|
|
6588
6934
|
let connection;
|
|
6589
6935
|
let releaseConnection;
|
|
6936
|
+
let existingSubscription;
|
|
6590
6937
|
try {
|
|
6591
|
-
const acquisition = await
|
|
6938
|
+
const { acquisition, existing } = await acquireSubscriptionShard({
|
|
6939
|
+
appId: effectiveAppId,
|
|
6940
|
+
isServer: config.isServer,
|
|
6941
|
+
routePath: roomRoutePath,
|
|
6942
|
+
authTokenProvider,
|
|
6943
|
+
principalKey,
|
|
6944
|
+
principalToken,
|
|
6945
|
+
source,
|
|
6946
|
+
}, matchesThisSubscription);
|
|
6592
6947
|
connection = acquisition.connection;
|
|
6593
6948
|
releaseConnection = acquisition.release;
|
|
6949
|
+
existingSubscription = existing;
|
|
6594
6950
|
}
|
|
6595
6951
|
catch (error) {
|
|
6596
6952
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -6601,34 +6957,6 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6601
6957
|
throw err;
|
|
6602
6958
|
}
|
|
6603
6959
|
const cachedEntry = cacheKey ? getResponseCacheEntry(cacheKey, identityToken) : undefined;
|
|
6604
|
-
// Check if we already have a subscription for this path+prompt+shape+limit+cursor+filter+sort
|
|
6605
|
-
let existingSubscription;
|
|
6606
|
-
let specKey = '';
|
|
6607
|
-
try {
|
|
6608
|
-
// Stringify THIS call's specs once; existing subscriptions carry their
|
|
6609
|
-
// precomputed specKey, so the scan is a string compare per entry
|
|
6610
|
-
// (previously each new subscribe re-stringified every existing
|
|
6611
|
-
// subscription's shape/filter/sort — O(N²) with many live hooks).
|
|
6612
|
-
specKey = JSON.stringify([
|
|
6613
|
-
(_b = subscriptionOptions.shape) !== null && _b !== void 0 ? _b : null,
|
|
6614
|
-
(_c = subscriptionOptions.filter) !== null && _c !== void 0 ? _c : null,
|
|
6615
|
-
(_d = subscriptionOptions.sort) !== null && _d !== void 0 ? _d : null,
|
|
6616
|
-
]);
|
|
6617
|
-
for (const sub of connection.subscriptions.values()) {
|
|
6618
|
-
// H1: also require the same principal identity (compact hash AND exact
|
|
6619
|
-
// token) — a different logged-in user must not piggy-back on (and
|
|
6620
|
-
// inherit lastData from) another user's existing subscription, even
|
|
6621
|
-
// with identical path/prompt/shape/filter/sort.
|
|
6622
|
-
if (sub.identity === identity && sub.identityToken === identityToken && sub.path === normalizedPath && sub.prompt === subscriptionOptions.prompt && sub.specKey === specKey && sub.includeSubPaths === ((_e = subscriptionOptions.includeSubPaths) !== null && _e !== void 0 ? _e : false) && sub.limit === subscriptionOptions.limit && sub.cursor === subscriptionOptions.cursor) {
|
|
6623
|
-
existingSubscription = sub;
|
|
6624
|
-
break;
|
|
6625
|
-
}
|
|
6626
|
-
}
|
|
6627
|
-
}
|
|
6628
|
-
catch (error) {
|
|
6629
|
-
releaseConnection();
|
|
6630
|
-
throw error;
|
|
6631
|
-
}
|
|
6632
6960
|
if (existingSubscription) {
|
|
6633
6961
|
let replayData = undefined;
|
|
6634
6962
|
let replayOnData;
|
|
@@ -6711,8 +7039,9 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6711
7039
|
prompt: subscriptionOptions.prompt,
|
|
6712
7040
|
shape: subscriptionOptions.shape,
|
|
6713
7041
|
limit: subscriptionOptions.limit,
|
|
7042
|
+
normalizedLimit,
|
|
6714
7043
|
cursor: subscriptionOptions.cursor,
|
|
6715
|
-
includeSubPaths
|
|
7044
|
+
includeSubPaths,
|
|
6716
7045
|
callbacks: [subscriptionOptions],
|
|
6717
7046
|
lastData: undefined,
|
|
6718
7047
|
dataRevision: 0,
|
|
@@ -6721,6 +7050,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6721
7050
|
identity,
|
|
6722
7051
|
identityToken,
|
|
6723
7052
|
specKey,
|
|
7053
|
+
rehomeAttempts: 0,
|
|
6724
7054
|
};
|
|
6725
7055
|
connection.subscriptions.set(subscriptionId, subscription);
|
|
6726
7056
|
}
|
|
@@ -6794,15 +7124,21 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
|
|
|
6794
7124
|
await removeCallbackFromSubscription(connection, subscriptionId, subscriptionOptions);
|
|
6795
7125
|
};
|
|
6796
7126
|
}
|
|
6797
|
-
async function removeCallbackFromSubscription(
|
|
6798
|
-
var _a;
|
|
7127
|
+
async function removeCallbackFromSubscription(registeredOn, subscriptionId, callback) {
|
|
7128
|
+
var _a, _b;
|
|
7129
|
+
// A `subscription_limit` re-home can have moved this subscription to another
|
|
7130
|
+
// shard since the caller captured its connection, so resolve the current
|
|
7131
|
+
// owner rather than no-oping (which would leak the subscription forever).
|
|
7132
|
+
const connection = registeredOn.subscriptions.has(subscriptionId)
|
|
7133
|
+
? registeredOn
|
|
7134
|
+
: (_a = connectionHoldingSubscription(subscriptionId)) !== null && _a !== void 0 ? _a : registeredOn;
|
|
6799
7135
|
const subscription = connection.subscriptions.get(subscriptionId);
|
|
6800
7136
|
if (!subscription) {
|
|
6801
7137
|
return;
|
|
6802
7138
|
}
|
|
6803
7139
|
// Remove this callback
|
|
6804
7140
|
subscription.callbacks = subscription.callbacks.filter(cb => cb !== callback);
|
|
6805
|
-
(
|
|
7141
|
+
(_b = subscription.refresh) === null || _b === void 0 ? void 0 : _b.callbacks.delete(callback);
|
|
6806
7142
|
// If there are still callbacks, don't unsubscribe
|
|
6807
7143
|
if (subscription.callbacks.length > 0) {
|
|
6808
7144
|
return;
|
|
@@ -6968,7 +7304,7 @@ async function doReconnectWithNewAuth() {
|
|
|
6968
7304
|
console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
|
|
6969
7305
|
}
|
|
6970
7306
|
try {
|
|
6971
|
-
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-
|
|
7307
|
+
const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-DrtEj-bE.js'); });
|
|
6972
7308
|
await reconnectRealtimeStoreWithNewAuth();
|
|
6973
7309
|
}
|
|
6974
7310
|
catch (error) {
|
|
@@ -7043,8 +7379,10 @@ const WS_RUNQUERY_MAX_FRAME_BYTES = 60000;
|
|
|
7043
7379
|
*/
|
|
7044
7380
|
async function runNamedQueriesOverSocket(appId, queries, isServer) {
|
|
7045
7381
|
var _a;
|
|
7046
|
-
|
|
7047
|
-
|
|
7382
|
+
// Any warm shard of this app's pool will do: a query is not bound to the
|
|
7383
|
+
// socket a subscription happens to live on.
|
|
7384
|
+
const connection = findShardConnection(appId, isWarmConnection);
|
|
7385
|
+
if (!connection) {
|
|
7048
7386
|
return null; // no warm socket -> HTTP (never open one just for queries)
|
|
7049
7387
|
}
|
|
7050
7388
|
if (!((_a = connection.capabilities) === null || _a === void 0 ? void 0 : _a.runQuery)) {
|
|
@@ -7094,9 +7432,8 @@ async function runNamedQueriesOverSocket(appId, queries, isServer) {
|
|
|
7094
7432
|
*/
|
|
7095
7433
|
async function wsIntent(appId, roomRoutePath, intent, isServer) {
|
|
7096
7434
|
const roomKey = roomKeyFromRoutePath(roomRoutePath);
|
|
7097
|
-
const
|
|
7098
|
-
|
|
7099
|
-
if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
|
|
7435
|
+
const connection = findShardConnection(roomKey ? `${appId}#room#${roomKey}` : appId, isWarmConnection);
|
|
7436
|
+
if (!connection) {
|
|
7100
7437
|
return false;
|
|
7101
7438
|
}
|
|
7102
7439
|
// Identity-aware send gate. The server attributes intents to the
|
|
@@ -7126,6 +7463,9 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
|
|
|
7126
7463
|
return false;
|
|
7127
7464
|
}
|
|
7128
7465
|
try {
|
|
7466
|
+
// The identity gate above awaits, so the socket is re-read here; a
|
|
7467
|
+
// teardown in that window throws and is reported as not-sent, exactly as
|
|
7468
|
+
// wsIntentReliable does.
|
|
7129
7469
|
connection.ws.send(JSON.stringify({ type: 'intent', intent }));
|
|
7130
7470
|
return true;
|
|
7131
7471
|
}
|
|
@@ -7163,9 +7503,8 @@ async function maybeReauthRoomConnection(connection, isServer) {
|
|
|
7163
7503
|
*/
|
|
7164
7504
|
async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
|
|
7165
7505
|
const roomKey = roomKeyFromRoutePath(roomRoutePath);
|
|
7166
|
-
const
|
|
7167
|
-
|
|
7168
|
-
if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
|
|
7506
|
+
const connection = findShardConnection(roomKey ? `${appId}#room#${roomKey}` : appId, isWarmConnection);
|
|
7507
|
+
if (!connection) {
|
|
7169
7508
|
return undefined;
|
|
7170
7509
|
}
|
|
7171
7510
|
if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
|
|
@@ -8419,7 +8758,7 @@ async function setMany(many, options) {
|
|
|
8419
8758
|
'Nothing was signed or submitted.');
|
|
8420
8759
|
}
|
|
8421
8760
|
const solanaChain = workerChain;
|
|
8422
|
-
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-
|
|
8761
|
+
const { handlePreBuiltTransaction, buildExpectedWriteIntent } = await Promise.resolve().then(function () { return require('./solana-operations-CU0YbiMu.js'); });
|
|
8423
8762
|
// OA-0001: derive the expected intent from OUR OWN request (the one
|
|
8424
8763
|
// ground truth a tampered builder response cannot change) so the
|
|
8425
8764
|
// validator can reject any transaction whose operation semantics
|
|
@@ -8783,7 +9122,7 @@ async function getTransactionJson(rpc, signature, commitment = 'confirmed') {
|
|
|
8783
9122
|
}
|
|
8784
9123
|
|
|
8785
9124
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
8786
|
-
const DEFAULT_MAX_ATTEMPTS = 20;
|
|
9125
|
+
const DEFAULT_MAX_ATTEMPTS$1 = 20;
|
|
8787
9126
|
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
8788
9127
|
/** Mirror read after a late confirmation. No settle delay: the tx already landed. */
|
|
8789
9128
|
const RECONCILE_MIRROR_SETTLE_MS = 0;
|
|
@@ -8990,7 +9329,7 @@ async function waitForSetResult(receipt, options) {
|
|
|
8990
9329
|
return reconcileSetResult(receipt, options);
|
|
8991
9330
|
}
|
|
8992
9331
|
const timeoutMs = (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_TIMEOUT_MS;
|
|
8993
|
-
const maxAttempts = (_b = options === null || options === void 0 ? void 0 : options.maxAttempts) !== null && _b !== void 0 ? _b : DEFAULT_MAX_ATTEMPTS;
|
|
9332
|
+
const maxAttempts = (_b = options === null || options === void 0 ? void 0 : options.maxAttempts) !== null && _b !== void 0 ? _b : DEFAULT_MAX_ATTEMPTS$1;
|
|
8994
9333
|
const pollIntervalMs = (_c = options === null || options === void 0 ? void 0 : options.pollIntervalMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_INTERVAL_MS;
|
|
8995
9334
|
const startedAt = Date.now();
|
|
8996
9335
|
let current = receipt;
|
|
@@ -9108,72 +9447,95 @@ async function reconnectWithNewAuth() {
|
|
|
9108
9447
|
return reconnectWithNewAuthV2();
|
|
9109
9448
|
}
|
|
9110
9449
|
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
*
|
|
9124
|
-
* per-subscription override channel, plus the websocket layer already keying
|
|
9125
|
-
* connections by appId.
|
|
9126
|
-
*
|
|
9127
|
-
* ## v1 scope: read-only, unauthenticated
|
|
9128
|
-
*
|
|
9129
|
-
* A scoped instance is UNAUTHENTICATED. It sends no auth, never reads or
|
|
9130
|
-
* refreshes the global session, and connects to its app's realtime endpoint
|
|
9131
|
-
* anonymously. It is therefore only useful against collections a Bounded app
|
|
9132
|
-
* exposes with a public read rule (`read: true`) - private/per-user reads will
|
|
9133
|
-
* be denied by the target app's policy, exactly as an anonymous caller would be.
|
|
9134
|
-
*
|
|
9135
|
-
* Writes and any auth-requiring surface are NOT supported on a scoped instance
|
|
9136
|
-
* and throw a clear error. Per-instance authentication is a planned follow-up.
|
|
9137
|
-
*/
|
|
9138
|
-
/** Error thrown when an unsupported (write / auth-requiring) call is made on a
|
|
9139
|
-
* read-only scoped client. */
|
|
9450
|
+
var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
|
|
9451
|
+
var t = {};
|
|
9452
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
9453
|
+
t[p] = s[p];
|
|
9454
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
9455
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
9456
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9457
|
+
t[p[i]] = s[p[i]];
|
|
9458
|
+
}
|
|
9459
|
+
return t;
|
|
9460
|
+
};
|
|
9461
|
+
/** Error thrown when an unsupported (write) call is made on a scoped client.
|
|
9462
|
+
* Reads may be authenticated per instance; writes still are not supported. */
|
|
9140
9463
|
class ScopedClientReadOnlyError extends Error {
|
|
9141
9464
|
constructor(operation) {
|
|
9142
|
-
super(`createClient() instances are read-only
|
|
9143
|
-
`Use the global init() client for authenticated writes, or read
|
|
9144
|
-
`(read: true) collections
|
|
9465
|
+
super(`createClient() instances are read-only: "${operation}" is not supported. ` +
|
|
9466
|
+
`Use the global init() client for authenticated writes, or read public ` +
|
|
9467
|
+
`(read: true) collections - and, with getAuthHeaders, this credential's own ` +
|
|
9468
|
+
`readable collections - on this scoped instance.`);
|
|
9145
9469
|
this.name = 'ScopedClientReadOnlyError';
|
|
9146
9470
|
}
|
|
9147
9471
|
}
|
|
9148
9472
|
/**
|
|
9149
|
-
* Create a read-only client scoped to a single Bounded app
|
|
9473
|
+
* Create a read-only client scoped to a single Bounded app, anonymous by default
|
|
9474
|
+
* and authenticated when given `getAuthHeaders`.
|
|
9150
9475
|
*
|
|
9151
9476
|
* @example
|
|
9152
9477
|
* ```ts
|
|
9153
9478
|
* // Global client stays the venue's own authenticated app:
|
|
9154
9479
|
* await init({ appId: VENUE_APP, network: 'bounded-production', authMethod: 'email' });
|
|
9155
9480
|
*
|
|
9156
|
-
* // A second,
|
|
9481
|
+
* // A second, anonymous client for the steward runtime app's public collections:
|
|
9157
9482
|
* const steward = createClient({ appId: STEWARD_APP, network: 'bounded-production' });
|
|
9158
9483
|
* const state = await steward.get('runtime/current');
|
|
9159
9484
|
* const stop = await steward.subscribe('runtime/current', (d) => render(d));
|
|
9485
|
+
*
|
|
9486
|
+
* // The same second app, read AS THE SIGNED-IN HUMAN: the venue session is
|
|
9487
|
+
* // exchanged for a token pinned to the child app (see createAppTokenAuthHeaders).
|
|
9488
|
+
* const mine = createClient({
|
|
9489
|
+
* appId: ROOT_APP,
|
|
9490
|
+
* network: 'bounded-production',
|
|
9491
|
+
* getAuthHeaders: createAppTokenAuthHeaders({
|
|
9492
|
+
* authApiUrl: HUMAN_AUTH_URL,
|
|
9493
|
+
* appId: ROOT_APP,
|
|
9494
|
+
* parentToken: () => getIdToken(),
|
|
9495
|
+
* }),
|
|
9496
|
+
* });
|
|
9497
|
+
* const stopChat = await mine.subscribe(`conversations/${id}/m`, (d) => render(d));
|
|
9160
9498
|
* ```
|
|
9161
9499
|
*/
|
|
9162
9500
|
function createClient(config) {
|
|
9163
9501
|
if (!config || !config.appId) {
|
|
9164
9502
|
throw new Error('createClient requires an appId.');
|
|
9165
9503
|
}
|
|
9504
|
+
// The credential source is an instance concern, not a ClientConfig field: keep
|
|
9505
|
+
// it out of the resolved config so it can never leak through getConfig() or the
|
|
9506
|
+
// `_config` override channel.
|
|
9507
|
+
const { getAuthHeaders } = config, clientFields = __rest$1(config, ["getAuthHeaders"]);
|
|
9166
9508
|
// Resolve from the PRISTINE defaults (never the mutated global) so a scoped
|
|
9167
9509
|
// instance is fully independent of any prior init(). Same precedence as init:
|
|
9168
9510
|
// defaults < network preset < explicit fields.
|
|
9169
|
-
const resolved = resolveClientConfig(DEFAULT_CLIENT_CONFIG,
|
|
9511
|
+
const resolved = resolveClientConfig(DEFAULT_CLIENT_CONFIG, clientFields);
|
|
9512
|
+
// Normalize the provider once. Downstream (api.ts, operations.ts,
|
|
9513
|
+
// subscription-v2.ts) consumes the same `_getAuthHeaders` shape a server wallet
|
|
9514
|
+
// client uses, so a null/undefined return becomes "no headers" here rather than
|
|
9515
|
+
// becoming a special case in three transports.
|
|
9516
|
+
const authHeaders = getAuthHeaders
|
|
9517
|
+
? async () => { var _a; return (_a = (await getAuthHeaders())) !== null && _a !== void 0 ? _a : {}; }
|
|
9518
|
+
: undefined;
|
|
9519
|
+
/** True when the provider currently yields an Authorization header. */
|
|
9520
|
+
const hasCredential = async () => {
|
|
9521
|
+
if (!authHeaders)
|
|
9522
|
+
return false;
|
|
9523
|
+
const headers = await authHeaders();
|
|
9524
|
+
return Object.keys(headers).some((name) => name.toLowerCase() === 'authorization');
|
|
9525
|
+
};
|
|
9170
9526
|
// The override channel that carries this instance's identity through the
|
|
9171
9527
|
// shared transport and subscription code. `_config` retargets endpoints +
|
|
9172
|
-
// appId
|
|
9173
|
-
|
|
9528
|
+
// appId. Then either `_getAuthHeaders` (this instance's credential is the SOLE
|
|
9529
|
+
// auth source - never the ambient session, and never a managed-credential 401
|
|
9530
|
+
// recovery that could sign another principal out) or `_readOnly` (omit auth and
|
|
9531
|
+
// block ambient-session/global-cache reads).
|
|
9532
|
+
const instanceOverrides = () => authHeaders
|
|
9533
|
+
? { _config: resolved, _getAuthHeaders: authHeaders }
|
|
9534
|
+
: { _config: resolved, _readOnly: true };
|
|
9174
9535
|
// Merge caller-supplied opts with the instance overrides. The instance
|
|
9175
|
-
// `_config`/`_readOnly` are applied LAST so app code
|
|
9176
|
-
// routing/identity of a scoped call (but may still pass
|
|
9536
|
+
// `_config` plus its `_getAuthHeaders`/`_readOnly` are applied LAST so app code
|
|
9537
|
+
// can never override the routing/identity of a scoped call (but may still pass
|
|
9538
|
+
// e.g. an AbortSignal).
|
|
9177
9539
|
const withOverrides = (opts) => {
|
|
9178
9540
|
var _a;
|
|
9179
9541
|
const base = (opts !== null && opts !== void 0 ? opts : {});
|
|
@@ -9181,6 +9543,7 @@ function createClient(config) {
|
|
|
9181
9543
|
};
|
|
9182
9544
|
return {
|
|
9183
9545
|
appId: resolved.appId,
|
|
9546
|
+
authenticated: !!authHeaders,
|
|
9184
9547
|
getConfig: () => (Object.assign({}, resolved)),
|
|
9185
9548
|
get: (path, opts) => get(path, withOverrides(opts)),
|
|
9186
9549
|
getMany: (paths, opts) => getMany(paths, withOverrides(opts)),
|
|
@@ -9190,11 +9553,19 @@ function createClient(config) {
|
|
|
9190
9553
|
aggregate: (path, operation, opts) => aggregate(path, operation, withOverrides(opts)),
|
|
9191
9554
|
runQuery: (absolutePath, queryName, queryArgs, opts) => runQuery(absolutePath, queryName, queryArgs, withOverrides(opts)),
|
|
9192
9555
|
runQueryMany: (many, opts) => runQueryMany(many, withOverrides(opts)),
|
|
9193
|
-
subscribe: (path, options) => {
|
|
9194
|
-
var _a;
|
|
9556
|
+
subscribe: async (path, options) => {
|
|
9557
|
+
var _a, _b;
|
|
9195
9558
|
const opts = typeof options === 'function' ? { onData: options } : Object.assign({}, options);
|
|
9196
|
-
// Force this instance's scoped
|
|
9197
|
-
|
|
9559
|
+
// Force this instance's scoped routing onto the subscription. An
|
|
9560
|
+
// authenticated instance threads its credential provider, which gives the
|
|
9561
|
+
// subscription its own connection keyed by that exact credential; with no
|
|
9562
|
+
// credential right now it subscribes anonymously (the `#ro` socket) instead
|
|
9563
|
+
// of failing, so a signed-out page still gets public data. This is resolved
|
|
9564
|
+
// per subscribe, so the socket a subscription lands on always matches the
|
|
9565
|
+
// credential it was authorized with - including after a shard re-home,
|
|
9566
|
+
// which rebuilds the same routing + auth arguments from the connection.
|
|
9567
|
+
opts._overrides = (await hasCredential())
|
|
9568
|
+
? Object.assign(Object.assign({}, ((_a = opts._overrides) !== null && _a !== void 0 ? _a : {})), { _config: resolved, _getAuthHeaders: authHeaders }) : Object.assign(Object.assign({}, ((_b = opts._overrides) !== null && _b !== void 0 ? _b : {})), { _config: resolved, _readOnly: true });
|
|
9198
9569
|
return subscribe(path, opts);
|
|
9199
9570
|
},
|
|
9200
9571
|
set: () => { throw new ScopedClientReadOnlyError('set'); },
|
|
@@ -9202,6 +9573,264 @@ function createClient(config) {
|
|
|
9202
9573
|
};
|
|
9203
9574
|
}
|
|
9204
9575
|
|
|
9576
|
+
/**
|
|
9577
|
+
* Second-app tokens: exchange a signed-in app's id token for one pinned to a
|
|
9578
|
+
* DIFFERENT Bounded app the platform records as its child.
|
|
9579
|
+
*
|
|
9580
|
+
* WHY. A browser signed in to app A holds an id token pinned to A
|
|
9581
|
+
* (`custom:appId`), and the realtime data plane refuses it for app B
|
|
9582
|
+
* (`app_mismatch`). When B is a child app of A - an OpenApps agent root under its
|
|
9583
|
+
* venue - the Bounded Auth issuer will mint a short-lived, B-pinned token for the
|
|
9584
|
+
* SAME human at `POST {authApiUrl}/app-token/exchange`. The issuer, not this
|
|
9585
|
+
* helper, decides whether the relationship holds; it reads it from the platform's
|
|
9586
|
+
* own records and refuses anything else. `custom:userId` of the minted token is
|
|
9587
|
+
* the presenting token's own, so B sees the same principal A does.
|
|
9588
|
+
*
|
|
9589
|
+
* WHAT THIS ADDS over a bare POST, all of it in memory and per process:
|
|
9590
|
+
* - a cache keyed by (exact parent credential, child appId), so one exchange
|
|
9591
|
+
* serves every read and socket for that pair;
|
|
9592
|
+
* - refresh shortly BEFORE expiry, driven by use rather than a timer, so no
|
|
9593
|
+
* interval keeps a process alive and no request ever carries a token that
|
|
9594
|
+
* expires in flight;
|
|
9595
|
+
* - single-flight, so N components mounting at once make ONE exchange;
|
|
9596
|
+
* - BOUNDED retry with backoff and a recorded give-up. A 429 stops the schedule
|
|
9597
|
+
* immediately rather than feeding it, and so does any other definitive
|
|
9598
|
+
* refusal. There is no unbounded retry loop anywhere in this module;
|
|
9599
|
+
* - invalidation on sign-out and identity change: the cache key IS the exact
|
|
9600
|
+
* parent credential (hashed), so a new session cannot read the old session's
|
|
9601
|
+
* entry, and `clearAppTokenCache()` drops entries outright.
|
|
9602
|
+
*
|
|
9603
|
+
* The parent token is treated as OPAQUE credential material for keying: it is
|
|
9604
|
+
* hashed, never decoded for identity, and never logged.
|
|
9605
|
+
*/
|
|
9606
|
+
/** Thrown when an exchange stops trying. `giveUp` carries the recorded reason. */
|
|
9607
|
+
class AppTokenExchangeError extends Error {
|
|
9608
|
+
constructor(giveUp) {
|
|
9609
|
+
super(`app token exchange for ${giveUp.appId} gave up after ${giveUp.attempts} attempt(s): ${giveUp.reason}`);
|
|
9610
|
+
this.name = 'AppTokenExchangeError';
|
|
9611
|
+
this.giveUp = giveUp;
|
|
9612
|
+
}
|
|
9613
|
+
}
|
|
9614
|
+
/** Re-exchange once the remaining life drops under this. */
|
|
9615
|
+
const REFRESH_SKEW_MS = 60000;
|
|
9616
|
+
/** Floor for a response that reports no usable expiry of its own. */
|
|
9617
|
+
const FALLBACK_LIFETIME_MS = 5 * 60000;
|
|
9618
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
9619
|
+
const MAX_ATTEMPTS_CEILING = 5;
|
|
9620
|
+
const FIRST_RETRY_DELAY_MS = 300;
|
|
9621
|
+
const MAX_RETRY_DELAY_MS = 2000;
|
|
9622
|
+
const cache = new Map();
|
|
9623
|
+
const inFlight = new Map();
|
|
9624
|
+
/** Opaque, stable key material for a credential we must never decode or log. */
|
|
9625
|
+
function fingerprint(value) {
|
|
9626
|
+
let hash = 5381;
|
|
9627
|
+
for (let index = 0; index < value.length; index++) {
|
|
9628
|
+
hash = ((hash << 5) + hash + value.charCodeAt(index)) >>> 0;
|
|
9629
|
+
}
|
|
9630
|
+
return hash.toString(16);
|
|
9631
|
+
}
|
|
9632
|
+
function cacheKey(appId, parentToken) {
|
|
9633
|
+
return `${appId}:${fingerprint(parentToken)}`;
|
|
9634
|
+
}
|
|
9635
|
+
function usable(entry, nowMs) {
|
|
9636
|
+
return !!entry && entry.expiresAtMs - REFRESH_SKEW_MS > nowMs;
|
|
9637
|
+
}
|
|
9638
|
+
/** Cache entries above which a lookup also drops the expired ones. */
|
|
9639
|
+
const PRUNE_THRESHOLD = 32;
|
|
9640
|
+
/**
|
|
9641
|
+
* Drop expired entries so a long-lived process (SSR, React Native) that sees a
|
|
9642
|
+
* new parent credential every rotation does not grow this map forever. Keyed by
|
|
9643
|
+
* the exact credential, entries are otherwise only replaced by a same-key refresh.
|
|
9644
|
+
*/
|
|
9645
|
+
function pruneExpired(nowMs) {
|
|
9646
|
+
if (cache.size <= PRUNE_THRESHOLD)
|
|
9647
|
+
return;
|
|
9648
|
+
for (const [key, entry] of Array.from(cache.entries())) {
|
|
9649
|
+
if (entry.expiresAtMs <= nowMs)
|
|
9650
|
+
cache.delete(key);
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
function exchangeUrl(authApiUrl) {
|
|
9654
|
+
return `${authApiUrl.replace(/\/+$/, '')}/app-token/exchange`;
|
|
9655
|
+
}
|
|
9656
|
+
/**
|
|
9657
|
+
* When the minted token stops being usable: the issuer's own `expiresAt`, else
|
|
9658
|
+
* the token's `exp`, else a short floor for a token that carries neither.
|
|
9659
|
+
*
|
|
9660
|
+
* Returns null when both stated expiries are already in the PAST. Caching a dead
|
|
9661
|
+
* credential under an invented lifetime would make every request fail for that
|
|
9662
|
+
* long, so an expired mint is a give-up instead.
|
|
9663
|
+
*/
|
|
9664
|
+
function expiryFromResponse(body, token, nowMs) {
|
|
9665
|
+
const reported = Number(body.expiresAt);
|
|
9666
|
+
if (Number.isFinite(reported))
|
|
9667
|
+
return reported > nowMs ? reported : null;
|
|
9668
|
+
try {
|
|
9669
|
+
const payload = JSON.parse(decodeBase64Url(token.split('.')[1]));
|
|
9670
|
+
const exp = Number(payload.exp);
|
|
9671
|
+
if (Number.isFinite(exp))
|
|
9672
|
+
return exp * 1000 > nowMs ? exp * 1000 : null;
|
|
9673
|
+
}
|
|
9674
|
+
catch (_a) {
|
|
9675
|
+
/* an opaque token is fine; fall through to the floor */
|
|
9676
|
+
}
|
|
9677
|
+
return nowMs + FALLBACK_LIFETIME_MS;
|
|
9678
|
+
}
|
|
9679
|
+
/** 5xx and transport faults are worth one more try; nothing else is. */
|
|
9680
|
+
function retriable(status) {
|
|
9681
|
+
return status === null || (status >= 500 && status !== 501);
|
|
9682
|
+
}
|
|
9683
|
+
function delay(ms) {
|
|
9684
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9685
|
+
}
|
|
9686
|
+
function recordGiveUp(options, giveUp) {
|
|
9687
|
+
var _a;
|
|
9688
|
+
const report = (_a = options.onGiveUp) !== null && _a !== void 0 ? _a : ((info) => console.warn('[bounded] app token exchange gave up', info));
|
|
9689
|
+
try {
|
|
9690
|
+
report(giveUp);
|
|
9691
|
+
}
|
|
9692
|
+
catch (_b) {
|
|
9693
|
+
/* a reporting fault must not replace the exchange failure */
|
|
9694
|
+
}
|
|
9695
|
+
return new AppTokenExchangeError(giveUp);
|
|
9696
|
+
}
|
|
9697
|
+
async function performExchange(options) {
|
|
9698
|
+
var _a, _b;
|
|
9699
|
+
const attemptCeiling = Math.max(1, Math.min(MAX_ATTEMPTS_CEILING, (_a = options.maxAttempts) !== null && _a !== void 0 ? _a : DEFAULT_MAX_ATTEMPTS));
|
|
9700
|
+
const doFetch = (_b = options.fetchImpl) !== null && _b !== void 0 ? _b : fetch;
|
|
9701
|
+
let retryDelay = FIRST_RETRY_DELAY_MS;
|
|
9702
|
+
let lastStatus = null;
|
|
9703
|
+
let lastReason = 'unknown';
|
|
9704
|
+
for (let attempt = 1; attempt <= attemptCeiling; attempt++) {
|
|
9705
|
+
let status = null;
|
|
9706
|
+
let reason = 'transport_failed';
|
|
9707
|
+
try {
|
|
9708
|
+
const response = await doFetch(exchangeUrl(options.authApiUrl), {
|
|
9709
|
+
method: 'POST',
|
|
9710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9711
|
+
body: JSON.stringify({ idToken: options.parentToken, appId: options.appId }),
|
|
9712
|
+
});
|
|
9713
|
+
status = response.status;
|
|
9714
|
+
const body = await response.json().catch(() => ({}));
|
|
9715
|
+
if (response.ok) {
|
|
9716
|
+
const token = typeof body.idToken === 'string' ? body.idToken : '';
|
|
9717
|
+
if (!token) {
|
|
9718
|
+
throw recordGiveUp(options, { appId: options.appId, status, reason: 'no_token_in_response', attempts: attempt });
|
|
9719
|
+
}
|
|
9720
|
+
const nowMs = Date.now();
|
|
9721
|
+
const expiresAtMs = expiryFromResponse(body, token, nowMs);
|
|
9722
|
+
if (expiresAtMs === null) {
|
|
9723
|
+
throw recordGiveUp(options, { appId: options.appId, status, reason: 'expired_token_in_response', attempts: attempt });
|
|
9724
|
+
}
|
|
9725
|
+
return { token, expiresAtMs, appId: options.appId };
|
|
9726
|
+
}
|
|
9727
|
+
reason = typeof body.error === 'string' && body.error ? body.error : `http_${status}`;
|
|
9728
|
+
}
|
|
9729
|
+
catch (error) {
|
|
9730
|
+
// A give-up raised above is already final.
|
|
9731
|
+
if (error instanceof AppTokenExchangeError)
|
|
9732
|
+
throw error;
|
|
9733
|
+
status = null;
|
|
9734
|
+
reason = 'transport_failed';
|
|
9735
|
+
}
|
|
9736
|
+
lastStatus = status;
|
|
9737
|
+
lastReason = reason;
|
|
9738
|
+
// A 429 is the issuer telling us to stop. It stops the schedule; it never
|
|
9739
|
+
// feeds it. Every other definitive refusal stops too.
|
|
9740
|
+
if (!retriable(status) || attempt === attemptCeiling) {
|
|
9741
|
+
throw recordGiveUp(options, { appId: options.appId, status, reason, attempts: attempt });
|
|
9742
|
+
}
|
|
9743
|
+
await delay(retryDelay);
|
|
9744
|
+
retryDelay = Math.min(MAX_RETRY_DELAY_MS, retryDelay * 2);
|
|
9745
|
+
}
|
|
9746
|
+
// Unreachable: the loop always returns or throws.
|
|
9747
|
+
throw recordGiveUp(options, { appId: options.appId, status: lastStatus, reason: lastReason, attempts: attemptCeiling });
|
|
9748
|
+
}
|
|
9749
|
+
/**
|
|
9750
|
+
* Get a child-app id token for the human the parent token belongs to, from cache
|
|
9751
|
+
* when one is still comfortably fresh and otherwise by exchanging once.
|
|
9752
|
+
*
|
|
9753
|
+
* Concurrent callers for the same (parent credential, appId) share one exchange.
|
|
9754
|
+
* On give-up this rejects with {@link AppTokenExchangeError} and caches nothing.
|
|
9755
|
+
*/
|
|
9756
|
+
async function exchangeAppToken(options) {
|
|
9757
|
+
if (!(options === null || options === void 0 ? void 0 : options.authApiUrl))
|
|
9758
|
+
throw new Error('exchangeAppToken requires an authApiUrl.');
|
|
9759
|
+
if (!(options === null || options === void 0 ? void 0 : options.appId))
|
|
9760
|
+
throw new Error('exchangeAppToken requires an appId.');
|
|
9761
|
+
if (!(options === null || options === void 0 ? void 0 : options.parentToken))
|
|
9762
|
+
throw new Error('exchangeAppToken requires a parentToken.');
|
|
9763
|
+
const key = cacheKey(options.appId, options.parentToken);
|
|
9764
|
+
pruneExpired(Date.now());
|
|
9765
|
+
const cached = cache.get(key);
|
|
9766
|
+
if (usable(cached, Date.now()))
|
|
9767
|
+
return cached;
|
|
9768
|
+
// Past the refresh skew the entry is no longer served; drop it so a failed
|
|
9769
|
+
// refresh can never fall back to a token that is about to expire mid-request.
|
|
9770
|
+
if (cached)
|
|
9771
|
+
cache.delete(key);
|
|
9772
|
+
const active = inFlight.get(key);
|
|
9773
|
+
if (active)
|
|
9774
|
+
return active;
|
|
9775
|
+
const flight = performExchange(options).then((token) => {
|
|
9776
|
+
cache.set(key, token);
|
|
9777
|
+
return token;
|
|
9778
|
+
}).finally(() => {
|
|
9779
|
+
if (inFlight.get(key) === flight)
|
|
9780
|
+
inFlight.delete(key);
|
|
9781
|
+
});
|
|
9782
|
+
inFlight.set(key, flight);
|
|
9783
|
+
return flight;
|
|
9784
|
+
}
|
|
9785
|
+
/**
|
|
9786
|
+
* Drop cached child-app tokens. Call this on sign-out; an identity change needs
|
|
9787
|
+
* no call, because the cache key is the exact parent credential.
|
|
9788
|
+
*
|
|
9789
|
+
* With no argument every entry goes. `appId` narrows to one child app, and
|
|
9790
|
+
* `parentToken` narrows to one session's entries.
|
|
9791
|
+
*/
|
|
9792
|
+
function clearAppTokenCache(scope) {
|
|
9793
|
+
if (!scope || (!scope.appId && !scope.parentToken)) {
|
|
9794
|
+
cache.clear();
|
|
9795
|
+
inFlight.clear();
|
|
9796
|
+
return;
|
|
9797
|
+
}
|
|
9798
|
+
if (scope.appId && scope.parentToken) {
|
|
9799
|
+
const key = cacheKey(scope.appId, scope.parentToken);
|
|
9800
|
+
cache.delete(key);
|
|
9801
|
+
inFlight.delete(key);
|
|
9802
|
+
return;
|
|
9803
|
+
}
|
|
9804
|
+
const prefix = scope.appId ? `${scope.appId}:` : null;
|
|
9805
|
+
const suffix = scope.parentToken ? `:${fingerprint(scope.parentToken)}` : null;
|
|
9806
|
+
for (const key of Array.from(cache.keys())) {
|
|
9807
|
+
if ((prefix && key.startsWith(prefix)) || (suffix && key.endsWith(suffix)))
|
|
9808
|
+
cache.delete(key);
|
|
9809
|
+
}
|
|
9810
|
+
for (const key of Array.from(inFlight.keys())) {
|
|
9811
|
+
if ((prefix && key.startsWith(prefix)) || (suffix && key.endsWith(suffix)))
|
|
9812
|
+
inFlight.delete(key);
|
|
9813
|
+
}
|
|
9814
|
+
}
|
|
9815
|
+
/**
|
|
9816
|
+
* Build the `getAuthHeaders` a scoped `createClient({ appId, getAuthHeaders })`
|
|
9817
|
+
* instance authenticates with, backed by {@link exchangeAppToken}.
|
|
9818
|
+
*
|
|
9819
|
+
* Signed out (`parentToken()` yields nothing) returns NO headers, which leaves
|
|
9820
|
+
* the scoped instance anonymous - the public read lane - instead of failing. A
|
|
9821
|
+
* give-up on the exchange itself rejects, so a broken exchange surfaces as an
|
|
9822
|
+
* error rather than as silently unauthenticated reads.
|
|
9823
|
+
*/
|
|
9824
|
+
function createAppTokenAuthHeaders(options) {
|
|
9825
|
+
return async () => {
|
|
9826
|
+
const parentToken = await options.parentToken();
|
|
9827
|
+
if (!parentToken)
|
|
9828
|
+
return {};
|
|
9829
|
+
const minted = await exchangeAppToken(Object.assign(Object.assign({}, options), { parentToken }));
|
|
9830
|
+
return { Authorization: `Bearer ${minted.token}` };
|
|
9831
|
+
};
|
|
9832
|
+
}
|
|
9833
|
+
|
|
9205
9834
|
/**
|
|
9206
9835
|
* Live named-query subscriptions (subscribeQuery) - HTTP client poller (v1).
|
|
9207
9836
|
*
|
|
@@ -9904,7 +10533,7 @@ function toMillis(seconds) {
|
|
|
9904
10533
|
|
|
9905
10534
|
/** SIWS message creation is async already, so its onchain module can be lazy. */
|
|
9906
10535
|
async function genSolanaMessage(address, nonce) {
|
|
9907
|
-
const implementation = await Promise.resolve().then(function () { return require('./solana-message-
|
|
10536
|
+
const implementation = await Promise.resolve().then(function () { return require('./solana-message-CJTnYiJu.js'); });
|
|
9908
10537
|
return implementation.genSolanaMessage(address, nonce);
|
|
9909
10538
|
}
|
|
9910
10539
|
|
|
@@ -10455,6 +11084,7 @@ function defineLiveModule(mod) {
|
|
|
10455
11084
|
|
|
10456
11085
|
exports.AUTH_CHANGED_CODE = AUTH_CHANGED_CODE;
|
|
10457
11086
|
exports.AUTH_EXPIRED_CODE = AUTH_EXPIRED_CODE;
|
|
11087
|
+
exports.AppTokenExchangeError = AppTokenExchangeError;
|
|
10458
11088
|
exports.BoundedDeclineError = BoundedDeclineError;
|
|
10459
11089
|
exports.DECLINE_EVENT_NAME = DECLINE_EVENT_NAME;
|
|
10460
11090
|
exports.DEFAULT_QUERY_POLL_INTERVAL_MS = DEFAULT_QUERY_POLL_INTERVAL_MS;
|
|
@@ -10477,10 +11107,12 @@ exports.browserSignInDomain = browserSignInDomain;
|
|
|
10477
11107
|
exports.bufferExports = bufferExports;
|
|
10478
11108
|
exports.canonicalizeArgs = canonicalizeArgs;
|
|
10479
11109
|
exports.canonicalizeResult = canonicalizeResult;
|
|
11110
|
+
exports.clearAppTokenCache = clearAppTokenCache;
|
|
10480
11111
|
exports.clearCache = clearCache;
|
|
10481
11112
|
exports.closeAllQuerySubscriptions = closeAllQuerySubscriptions;
|
|
10482
11113
|
exports.closeAllSubscriptions = closeAllSubscriptions;
|
|
10483
11114
|
exports.count = count;
|
|
11115
|
+
exports.createAppTokenAuthHeaders = createAppTokenAuthHeaders;
|
|
10484
11116
|
exports.createClient = createClient;
|
|
10485
11117
|
exports.createSessionWithSignature = createSessionWithSignature;
|
|
10486
11118
|
exports.createSolanaSignInMessageText = createSolanaSignInMessageText;
|
|
@@ -10491,6 +11123,7 @@ exports.deriveUserIdentityFromIdToken = deriveUserIdentityFromIdToken;
|
|
|
10491
11123
|
exports.dispatchDeclineEvent = dispatchDeclineEvent;
|
|
10492
11124
|
exports.dispatchRateLimitEvent = dispatchRateLimitEvent;
|
|
10493
11125
|
exports.docId = docId;
|
|
11126
|
+
exports.exchangeAppToken = exchangeAppToken;
|
|
10494
11127
|
exports.functions = functions;
|
|
10495
11128
|
exports.genAuthNonce = genAuthNonce;
|
|
10496
11129
|
exports.genSiweMessage = genSiweMessage;
|
|
@@ -10547,4 +11180,4 @@ exports.toMillis = toMillis;
|
|
|
10547
11180
|
exports.toSeconds = toSeconds;
|
|
10548
11181
|
exports.waitForSetResult = waitForSetResult;
|
|
10549
11182
|
exports.withEffects = withEffects;
|
|
10550
|
-
//# sourceMappingURL=index-
|
|
11183
|
+
//# sourceMappingURL=index-BEGeDFQI.js.map
|