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