@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/README.md CHANGED
@@ -1,16 +1,16 @@
1
- # @tarobase/core
1
+ # @bounded-sh/core
2
2
 
3
3
  Core functionality for Tarobase SDKs. This package provides the shared functionality used by both the web and server SDKs.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @tarobase/core
8
+ npm install @bounded-sh/core
9
9
  ```
10
10
 
11
11
  ## Usage
12
12
 
13
- This package is typically not used directly, but is instead used as a dependency by `@tarobase/web` and `@tarobase/server`. If you're building a browser application, use `@tarobase/web`. If you're building a server application, use `@tarobase/server`.
13
+ This package is typically not used directly, but is instead used as a dependency by `@bounded-sh/client` and `@bounded-sh/server`. If you're building a browser application, use `@bounded-sh/client`. If you're building a server application, use `@bounded-sh/server`.
14
14
 
15
15
  ## API Reference
16
16
 
@@ -60,7 +60,7 @@ export declare function hasActiveConnection(): boolean;
60
60
  * room authority — the connection is already routed to the room DO, so the
61
61
  * client never names a destination.
62
62
  */
63
- export declare function wsIntent(appId: string, roomRoutePath: string, intent: unknown): boolean;
63
+ export declare function wsIntent(appId: string, roomRoutePath: string, intent: unknown, isServer: boolean): Promise<boolean>;
64
64
  /**
65
65
  * Send a live-room intent over the EXISTING per-room socket and AWAIT the server
66
66
  * ack — so a policy/auth denial (401/403/404/503) REJECTS instead of being
@@ -69,6 +69,6 @@ export declare function wsIntent(appId: string, roomRoutePath: string, intent: u
69
69
  * Promise that resolves on ack / rejects on error, or `undefined` if no live
70
70
  * socket exists yet (caller falls back to HTTP, which also surfaces errors).
71
71
  */
72
- export declare function wsIntentReliable(appId: string, roomRoutePath: string, intent: unknown): Promise<{
72
+ export declare function wsIntentReliable(appId: string, roomRoutePath: string, intent: unknown, isServer: boolean): Promise<{
73
73
  ok: true;
74
- }> | undefined;
74
+ } | undefined>;
package/dist/index.js CHANGED
@@ -5435,6 +5435,48 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5435
5435
  function principalFromIdToken(idToken) {
5436
5436
  return idToken ? `t${hashForKey(idToken)}` : null;
5437
5437
  }
5438
+ function livePrincipalFromIdToken(idToken) {
5439
+ if (!idToken)
5440
+ return null;
5441
+ try {
5442
+ const payload = JSON.parse(decodeBase64Url(idToken.split('.')[1]));
5443
+ const userId = payload['custom:userId'];
5444
+ if (typeof userId === 'string' && userId.length > 0)
5445
+ return userId;
5446
+ const walletAddress = payload['custom:walletAddress'];
5447
+ if (typeof walletAddress === 'string' && walletAddress.length > 0)
5448
+ return walletAddress;
5449
+ const subject = payload.sub;
5450
+ return typeof subject === 'string' && subject.length > 0 ? subject : null;
5451
+ }
5452
+ catch (_a) {
5453
+ return null;
5454
+ }
5455
+ }
5456
+ function authSnapshotFromToken(idToken) {
5457
+ return {
5458
+ tokenFingerprint: principalFromIdToken(idToken),
5459
+ principal: livePrincipalFromIdToken(idToken),
5460
+ };
5461
+ }
5462
+ function connectionMatchesAuthToken(connection, idToken) {
5463
+ const current = authSnapshotFromToken(idToken);
5464
+ if (!current.tokenFingerprint) {
5465
+ return connection.authTokenFingerprint === null && connection.authPrincipal === null;
5466
+ }
5467
+ if (connection.authTokenFingerprint && connection.authTokenFingerprint === current.tokenFingerprint) {
5468
+ return true;
5469
+ }
5470
+ return !!connection.authPrincipal && !!current.principal && connection.authPrincipal === current.principal;
5471
+ }
5472
+ async function canSendAuthenticatedLiveIntent(connection, isServer) {
5473
+ const currentToken = await getIdToken(isServer);
5474
+ if (!currentToken)
5475
+ return false;
5476
+ if (!connection.authTokenFingerprint && !connection.authPrincipal)
5477
+ return false;
5478
+ return connectionMatchesAuthToken(connection, currentToken);
5479
+ }
5438
5480
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5439
5481
  // Per-subscription wallet override (server WalletClient.subscribe): key by
5440
5482
  // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
@@ -5510,6 +5552,9 @@ function rememberAmbientAuthFailure(error) {
5510
5552
  function failConnectionAuth(connection, error) {
5511
5553
  connection.authFailure = error;
5512
5554
  connection.pendingAuthToken = null;
5555
+ connection.pendingAuthSnapshot = null;
5556
+ connection.authTokenFingerprint = null;
5557
+ connection.authPrincipal = null;
5513
5558
  connection.isConnecting = false;
5514
5559
  connection.isConnected = false;
5515
5560
  connection.isAuthenticating = false;
@@ -5728,16 +5773,37 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5728
5773
  if (connection.authFailure) {
5729
5774
  throw connection.authFailure;
5730
5775
  }
5776
+ let currentAuthToken;
5731
5777
  try {
5732
- await getConnectionAuthToken(isServer, authTokenProvider);
5778
+ currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5733
5779
  }
5734
5780
  catch (error) {
5735
5781
  const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5736
5782
  failConnectionAuth(connection, authError);
5737
5783
  throw authError;
5738
5784
  }
5785
+ if (connection.isConnected &&
5786
+ !connection.isAuthenticating &&
5787
+ !connectionMatchesAuthToken(connection, currentAuthToken)) {
5788
+ if (connection.tokenRefreshTimer) {
5789
+ clearInterval(connection.tokenRefreshTimer);
5790
+ connection.tokenRefreshTimer = null;
5791
+ }
5792
+ try {
5793
+ connection.ws.close();
5794
+ }
5795
+ catch (_a) {
5796
+ // Already closing.
5797
+ }
5798
+ connection.ws = null;
5799
+ connection.isConnected = false;
5800
+ connections.delete(connection.key);
5801
+ }
5802
+ else {
5803
+ connection.authFailure = null;
5804
+ return connection;
5805
+ }
5739
5806
  connection.authFailure = null;
5740
- return connection;
5741
5807
  }
5742
5808
  let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5743
5809
  // Create new connection
@@ -5755,6 +5821,9 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5755
5821
  routePath: roomKey ? routePath : undefined,
5756
5822
  authTokenProvider,
5757
5823
  pendingAuthToken: null,
5824
+ pendingAuthSnapshot: null,
5825
+ authTokenFingerprint: null,
5826
+ authPrincipal: null,
5758
5827
  tokenRefreshTimer: null,
5759
5828
  consecutiveAuthFailures: 0,
5760
5829
  authFailure: null,
@@ -5792,6 +5861,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5792
5861
  throw authError;
5793
5862
  }
5794
5863
  connection.pendingAuthToken = authToken || null;
5864
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5795
5865
  if (authToken) {
5796
5866
  // Successful token acquisition — reset failure counter
5797
5867
  connection.consecutiveAuthFailures = 0;
@@ -5831,6 +5901,8 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5831
5901
  }
5832
5902
  else {
5833
5903
  connection.isAuthenticating = false;
5904
+ connection.authTokenFingerprint = null;
5905
+ connection.authPrincipal = null;
5834
5906
  replaySubscriptions(connection);
5835
5907
  }
5836
5908
  });
@@ -5870,10 +5942,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5870
5942
  return connection;
5871
5943
  }
5872
5944
  function handleServerMessage(connection, message) {
5873
- var _a;
5945
+ var _a, _b, _c, _d, _e;
5874
5946
  switch (message.type) {
5875
5947
  case 'authenticated': {
5876
5948
  connection.isAuthenticating = false;
5949
+ connection.authTokenFingerprint = (_b = (_a = connection.pendingAuthSnapshot) === null || _a === void 0 ? void 0 : _a.tokenFingerprint) !== null && _b !== void 0 ? _b : null;
5950
+ connection.authPrincipal = (_d = (_c = connection.pendingAuthSnapshot) === null || _c === void 0 ? void 0 : _c.principal) !== null && _d !== void 0 ? _d : null;
5877
5951
  replaySubscriptions(connection);
5878
5952
  break;
5879
5953
  }
@@ -5944,7 +6018,7 @@ function handleServerMessage(connection, message) {
5944
6018
  connection.pendingRequests.delete(message.requestId);
5945
6019
  clearTimeout(pendingSet.timer);
5946
6020
  if (message.statusCode >= 400) {
5947
- pendingSet.reject(new Error(((_a = message.body) === null || _a === void 0 ? void 0 : _a.message) || `Set failed with status ${message.statusCode}`));
6021
+ pendingSet.reject(new Error(((_e = message.body) === null || _e === void 0 ? void 0 : _e.message) || `Set failed with status ${message.statusCode}`));
5948
6022
  }
5949
6023
  else {
5950
6024
  pendingSet.resolve(message.body);
@@ -6472,13 +6546,16 @@ function hasActiveConnection() {
6472
6546
  * room authority — the connection is already routed to the room DO, so the
6473
6547
  * client never names a destination.
6474
6548
  */
6475
- function wsIntent(appId, roomRoutePath, intent) {
6549
+ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6476
6550
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6477
6551
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6478
6552
  const connection = connections.get(connKey);
6479
6553
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6480
6554
  return false;
6481
6555
  }
6556
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6557
+ return false;
6558
+ }
6482
6559
  try {
6483
6560
  connection.ws.send(JSON.stringify({ type: 'intent', intent }));
6484
6561
  return true;
@@ -6495,13 +6572,16 @@ function wsIntent(appId, roomRoutePath, intent) {
6495
6572
  * Promise that resolves on ack / rejects on error, or `undefined` if no live
6496
6573
  * socket exists yet (caller falls back to HTTP, which also surfaces errors).
6497
6574
  */
6498
- function wsIntentReliable(appId, roomRoutePath, intent) {
6575
+ async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
6499
6576
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6500
6577
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6501
6578
  const connection = connections.get(connKey);
6502
6579
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6503
6580
  return undefined;
6504
6581
  }
6582
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6583
+ return undefined;
6584
+ }
6505
6585
  const requestId = generateRequestId();
6506
6586
  return new Promise((resolve, reject) => {
6507
6587
  const timer = setTimeout(() => {
@@ -6913,13 +6993,13 @@ async function intent(roomPath, intent, opts = {}) {
6913
6993
  const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
6914
6994
  if (!hasAuthOverride) {
6915
6995
  if (opts.fireAndForget) {
6916
- if (wsIntent(config.appId, normalizedRoomPath, intent))
6996
+ if (await wsIntent(config.appId, normalizedRoomPath, intent, config.isServer))
6917
6997
  return { ok: true };
6918
6998
  }
6919
6999
  else {
6920
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7000
+ const ack = await wsIntentReliable(config.appId, normalizedRoomPath, intent, config.isServer);
6921
7001
  if (ack)
6922
- return await ack;
7002
+ return ack;
6923
7003
  }
6924
7004
  }
6925
7005
  const base = realtimeHttpBase(config.wsApiUrl);