@bounded-sh/core 0.0.21 → 0.0.23

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/index.mjs CHANGED
@@ -5415,6 +5415,48 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5415
5415
  function principalFromIdToken(idToken) {
5416
5416
  return idToken ? `t${hashForKey(idToken)}` : null;
5417
5417
  }
5418
+ function livePrincipalFromIdToken(idToken) {
5419
+ if (!idToken)
5420
+ return null;
5421
+ try {
5422
+ const payload = JSON.parse(decodeBase64Url(idToken.split('.')[1]));
5423
+ const userId = payload['custom:userId'];
5424
+ if (typeof userId === 'string' && userId.length > 0)
5425
+ return userId;
5426
+ const walletAddress = payload['custom:walletAddress'];
5427
+ if (typeof walletAddress === 'string' && walletAddress.length > 0)
5428
+ return walletAddress;
5429
+ const subject = payload.sub;
5430
+ return typeof subject === 'string' && subject.length > 0 ? subject : null;
5431
+ }
5432
+ catch (_a) {
5433
+ return null;
5434
+ }
5435
+ }
5436
+ function authSnapshotFromToken(idToken) {
5437
+ return {
5438
+ tokenFingerprint: principalFromIdToken(idToken),
5439
+ principal: livePrincipalFromIdToken(idToken),
5440
+ };
5441
+ }
5442
+ function connectionMatchesAuthToken(connection, idToken) {
5443
+ const current = authSnapshotFromToken(idToken);
5444
+ if (!current.tokenFingerprint) {
5445
+ return connection.authTokenFingerprint === null && connection.authPrincipal === null;
5446
+ }
5447
+ if (connection.authTokenFingerprint && connection.authTokenFingerprint === current.tokenFingerprint) {
5448
+ return true;
5449
+ }
5450
+ return !!connection.authPrincipal && !!current.principal && connection.authPrincipal === current.principal;
5451
+ }
5452
+ async function canSendAuthenticatedLiveIntent(connection, isServer) {
5453
+ const currentToken = await getIdToken(isServer);
5454
+ if (!currentToken)
5455
+ return false;
5456
+ if (!connection.authTokenFingerprint && !connection.authPrincipal)
5457
+ return false;
5458
+ return connectionMatchesAuthToken(connection, currentToken);
5459
+ }
5418
5460
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5419
5461
  // Per-subscription wallet override (server WalletClient.subscribe): key by
5420
5462
  // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
@@ -5490,6 +5532,9 @@ function rememberAmbientAuthFailure(error) {
5490
5532
  function failConnectionAuth(connection, error) {
5491
5533
  connection.authFailure = error;
5492
5534
  connection.pendingAuthToken = null;
5535
+ connection.pendingAuthSnapshot = null;
5536
+ connection.authTokenFingerprint = null;
5537
+ connection.authPrincipal = null;
5493
5538
  connection.isConnecting = false;
5494
5539
  connection.isConnected = false;
5495
5540
  connection.isAuthenticating = false;
@@ -5708,16 +5753,37 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5708
5753
  if (connection.authFailure) {
5709
5754
  throw connection.authFailure;
5710
5755
  }
5756
+ let currentAuthToken;
5711
5757
  try {
5712
- await getConnectionAuthToken(isServer, authTokenProvider);
5758
+ currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5713
5759
  }
5714
5760
  catch (error) {
5715
5761
  const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5716
5762
  failConnectionAuth(connection, authError);
5717
5763
  throw authError;
5718
5764
  }
5765
+ if (connection.isConnected &&
5766
+ !connection.isAuthenticating &&
5767
+ !connectionMatchesAuthToken(connection, currentAuthToken)) {
5768
+ if (connection.tokenRefreshTimer) {
5769
+ clearInterval(connection.tokenRefreshTimer);
5770
+ connection.tokenRefreshTimer = null;
5771
+ }
5772
+ try {
5773
+ connection.ws.close();
5774
+ }
5775
+ catch (_a) {
5776
+ // Already closing.
5777
+ }
5778
+ connection.ws = null;
5779
+ connection.isConnected = false;
5780
+ connections.delete(connection.key);
5781
+ }
5782
+ else {
5783
+ connection.authFailure = null;
5784
+ return connection;
5785
+ }
5719
5786
  connection.authFailure = null;
5720
- return connection;
5721
5787
  }
5722
5788
  let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5723
5789
  // Create new connection
@@ -5735,6 +5801,9 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5735
5801
  routePath: roomKey ? routePath : undefined,
5736
5802
  authTokenProvider,
5737
5803
  pendingAuthToken: null,
5804
+ pendingAuthSnapshot: null,
5805
+ authTokenFingerprint: null,
5806
+ authPrincipal: null,
5738
5807
  tokenRefreshTimer: null,
5739
5808
  consecutiveAuthFailures: 0,
5740
5809
  authFailure: null,
@@ -5772,6 +5841,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5772
5841
  throw authError;
5773
5842
  }
5774
5843
  connection.pendingAuthToken = authToken || null;
5844
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5775
5845
  if (authToken) {
5776
5846
  // Successful token acquisition — reset failure counter
5777
5847
  connection.consecutiveAuthFailures = 0;
@@ -5811,6 +5881,8 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5811
5881
  }
5812
5882
  else {
5813
5883
  connection.isAuthenticating = false;
5884
+ connection.authTokenFingerprint = null;
5885
+ connection.authPrincipal = null;
5814
5886
  replaySubscriptions(connection);
5815
5887
  }
5816
5888
  });
@@ -5850,10 +5922,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5850
5922
  return connection;
5851
5923
  }
5852
5924
  function handleServerMessage(connection, message) {
5853
- var _a;
5925
+ var _a, _b, _c, _d, _e;
5854
5926
  switch (message.type) {
5855
5927
  case 'authenticated': {
5856
5928
  connection.isAuthenticating = false;
5929
+ connection.authTokenFingerprint = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenFingerprint) !== null && _b !== void 0 ? _b : null;
5930
+ connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
5857
5931
  replaySubscriptions(connection);
5858
5932
  break;
5859
5933
  }
@@ -5924,7 +5998,7 @@ function handleServerMessage(connection, message) {
5924
5998
  connection.pendingRequests.delete(message.requestId);
5925
5999
  clearTimeout(pendingSet.timer);
5926
6000
  if (message.statusCode >= 400) {
5927
- pendingSet.reject(new Error(((_a = message.body) === null || _a === void 0 ? void 0 : _a.message) || `Set failed with status ${message.statusCode}`));
6001
+ pendingSet.reject(new Error(((_e = message.body) === null || _e === void 0 ? void 0 : _e.message) || `Set failed with status ${message.statusCode}`));
5928
6002
  }
5929
6003
  else {
5930
6004
  pendingSet.resolve(message.body);
@@ -6452,13 +6526,16 @@ function hasActiveConnection() {
6452
6526
  * room authority — the connection is already routed to the room DO, so the
6453
6527
  * client never names a destination.
6454
6528
  */
6455
- function wsIntent(appId, roomRoutePath, intent) {
6529
+ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6456
6530
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6457
6531
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6458
6532
  const connection = connections.get(connKey);
6459
6533
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6460
6534
  return false;
6461
6535
  }
6536
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6537
+ return false;
6538
+ }
6462
6539
  try {
6463
6540
  connection.ws.send(JSON.stringify({ type: 'intent', intent }));
6464
6541
  return true;
@@ -6475,13 +6552,16 @@ function wsIntent(appId, roomRoutePath, intent) {
6475
6552
  * Promise that resolves on ack / rejects on error, or `undefined` if no live
6476
6553
  * socket exists yet (caller falls back to HTTP, which also surfaces errors).
6477
6554
  */
6478
- function wsIntentReliable(appId, roomRoutePath, intent) {
6555
+ async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
6479
6556
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6480
6557
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6481
6558
  const connection = connections.get(connKey);
6482
6559
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6483
6560
  return undefined;
6484
6561
  }
6562
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6563
+ return undefined;
6564
+ }
6485
6565
  const requestId = generateRequestId();
6486
6566
  return new Promise((resolve, reject) => {
6487
6567
  const timer = setTimeout(() => {
@@ -6893,13 +6973,13 @@ async function intent(roomPath, intent, opts = {}) {
6893
6973
  const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
6894
6974
  if (!hasAuthOverride) {
6895
6975
  if (opts.fireAndForget) {
6896
- if (wsIntent(config.appId, normalizedRoomPath, intent))
6976
+ if (await wsIntent(config.appId, normalizedRoomPath, intent, config.isServer))
6897
6977
  return { ok: true };
6898
6978
  }
6899
6979
  else {
6900
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
6980
+ const ack = await wsIntentReliable(config.appId, normalizedRoomPath, intent, config.isServer);
6901
6981
  if (ack)
6902
- return await ack;
6982
+ return ack;
6903
6983
  }
6904
6984
  }
6905
6985
  const base = realtimeHttpBase(config.wsApiUrl);