@bounded-sh/core 0.0.20 → 0.0.22

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
@@ -10,8 +10,8 @@ let clientConfig = {
10
10
  // User configured settings
11
11
  name: '',
12
12
  logoUrl: '',
13
- // Bounded production is the out-of-the-box default a Bounded app needs only
14
- // `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
13
+ // Bounded production is the endpoint default. Apps still choose one explicit
14
+ // auth method at init time. Pass `network: 'bounded-staging'` to target staging.
15
15
  network: 'bounded-production',
16
16
  wsApiUrl: 'wss://realtime.bounded.sh',
17
17
  apiUrl: 'https://realtime.bounded.sh',
@@ -19,14 +19,15 @@ let clientConfig = {
19
19
  humanAuthApiUrl: 'https://auth.bounded.sh',
20
20
  functionsUrl: 'https://functions.bounded.sh',
21
21
  appId: '',
22
- // 'email' = Bounded Auth human login (inline email OTP) — the out-of-box default
23
- // for normal apps. Hosted OAuth/social uses loginWithRedirect/loginWithPopup.
22
+ // No hidden auth fallback: browser clients must pass authMethod explicitly
23
+ // (for example 'email', 'guest', 'phantom', 'privy', or 'privy-expo').
24
+ // Hosted OAuth/social uses loginWithRedirect/loginWithPopup with authMethod:'email'.
24
25
  // Text OTP is off by default and uses hosted/headless text helpers only when
25
26
  // Bounded explicitly enables it for the issuer. For
26
27
  // crypto/onchain wallet login use authMethod:'phantom' (Solana / Phantom), or
27
28
  // signInAnonymously() for zero-friction 'guest' accounts. ('wallet' is an
28
29
  // unimplemented stub; don't use.)
29
- authMethod: 'email',
30
+ authMethod: 'none',
30
31
  chain: '',
31
32
  rpcUrl: '',
32
33
  skipBackendInit: true,
@@ -92,10 +93,14 @@ function init(newConfig) {
92
93
  }
93
94
  // Bounded is client-driven: defaults are Bounded production, `network`
94
95
  // switches the whole endpoint set (e.g. 'bounded-staging'), and anything
95
- // passed explicitly wins. No `/config` round-trip `init({ appId })` is
96
- // synchronous and works out of the box.
96
+ // passed explicitly wins. No `/config` round-trip; browser SDKs still pass
97
+ // one explicit authMethod so there is no hidden auth-provider fallback.
97
98
  // defaults (bounded-production) < network preset < explicit newConfig
98
- const preset = (newConfig.network && BOUNDED_NETWORKS[newConfig.network]) || {};
99
+ if (newConfig.network !== undefined && !(newConfig.network in BOUNDED_NETWORKS)) {
100
+ reject(new Error(`Unsupported Bounded network "${String(newConfig.network)}". Expected bounded, bounded-staging, or bounded-production.`));
101
+ return;
102
+ }
103
+ const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
99
104
  clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
100
105
  isInitialized = true;
101
106
  resolve();
@@ -5410,6 +5415,48 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5410
5415
  function principalFromIdToken(idToken) {
5411
5416
  return idToken ? `t${hashForKey(idToken)}` : null;
5412
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
+ }
5413
5460
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5414
5461
  // Per-subscription wallet override (server WalletClient.subscribe): key by
5415
5462
  // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
@@ -5485,6 +5532,9 @@ function rememberAmbientAuthFailure(error) {
5485
5532
  function failConnectionAuth(connection, error) {
5486
5533
  connection.authFailure = error;
5487
5534
  connection.pendingAuthToken = null;
5535
+ connection.pendingAuthSnapshot = null;
5536
+ connection.authTokenFingerprint = null;
5537
+ connection.authPrincipal = null;
5488
5538
  connection.isConnecting = false;
5489
5539
  connection.isConnected = false;
5490
5540
  connection.isAuthenticating = false;
@@ -5703,16 +5753,37 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5703
5753
  if (connection.authFailure) {
5704
5754
  throw connection.authFailure;
5705
5755
  }
5756
+ let currentAuthToken;
5706
5757
  try {
5707
- await getConnectionAuthToken(isServer, authTokenProvider);
5758
+ currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5708
5759
  }
5709
5760
  catch (error) {
5710
5761
  const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5711
5762
  failConnectionAuth(connection, authError);
5712
5763
  throw authError;
5713
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
+ }
5714
5786
  connection.authFailure = null;
5715
- return connection;
5716
5787
  }
5717
5788
  let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5718
5789
  // Create new connection
@@ -5730,6 +5801,9 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5730
5801
  routePath: roomKey ? routePath : undefined,
5731
5802
  authTokenProvider,
5732
5803
  pendingAuthToken: null,
5804
+ pendingAuthSnapshot: null,
5805
+ authTokenFingerprint: null,
5806
+ authPrincipal: null,
5733
5807
  tokenRefreshTimer: null,
5734
5808
  consecutiveAuthFailures: 0,
5735
5809
  authFailure: null,
@@ -5767,6 +5841,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5767
5841
  throw authError;
5768
5842
  }
5769
5843
  connection.pendingAuthToken = authToken || null;
5844
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5770
5845
  if (authToken) {
5771
5846
  // Successful token acquisition — reset failure counter
5772
5847
  connection.consecutiveAuthFailures = 0;
@@ -5806,6 +5881,8 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5806
5881
  }
5807
5882
  else {
5808
5883
  connection.isAuthenticating = false;
5884
+ connection.authTokenFingerprint = null;
5885
+ connection.authPrincipal = null;
5809
5886
  replaySubscriptions(connection);
5810
5887
  }
5811
5888
  });
@@ -5845,10 +5922,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5845
5922
  return connection;
5846
5923
  }
5847
5924
  function handleServerMessage(connection, message) {
5848
- var _a;
5925
+ var _a, _b, _c, _d, _e;
5849
5926
  switch (message.type) {
5850
5927
  case 'authenticated': {
5851
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;
5852
5931
  replaySubscriptions(connection);
5853
5932
  break;
5854
5933
  }
@@ -5919,7 +5998,7 @@ function handleServerMessage(connection, message) {
5919
5998
  connection.pendingRequests.delete(message.requestId);
5920
5999
  clearTimeout(pendingSet.timer);
5921
6000
  if (message.statusCode >= 400) {
5922
- 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}`));
5923
6002
  }
5924
6003
  else {
5925
6004
  pendingSet.resolve(message.body);
@@ -6447,13 +6526,16 @@ function hasActiveConnection() {
6447
6526
  * room authority — the connection is already routed to the room DO, so the
6448
6527
  * client never names a destination.
6449
6528
  */
6450
- function wsIntent(appId, roomRoutePath, intent) {
6529
+ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6451
6530
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6452
6531
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6453
6532
  const connection = connections.get(connKey);
6454
6533
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6455
6534
  return false;
6456
6535
  }
6536
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6537
+ return false;
6538
+ }
6457
6539
  try {
6458
6540
  connection.ws.send(JSON.stringify({ type: 'intent', intent }));
6459
6541
  return true;
@@ -6470,13 +6552,16 @@ function wsIntent(appId, roomRoutePath, intent) {
6470
6552
  * Promise that resolves on ack / rejects on error, or `undefined` if no live
6471
6553
  * socket exists yet (caller falls back to HTTP, which also surfaces errors).
6472
6554
  */
6473
- function wsIntentReliable(appId, roomRoutePath, intent) {
6555
+ async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
6474
6556
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6475
6557
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6476
6558
  const connection = connections.get(connKey);
6477
6559
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6478
6560
  return undefined;
6479
6561
  }
6562
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6563
+ return undefined;
6564
+ }
6480
6565
  const requestId = generateRequestId();
6481
6566
  return new Promise((resolve, reject) => {
6482
6567
  const timer = setTimeout(() => {
@@ -6888,13 +6973,13 @@ async function intent(roomPath, intent, opts = {}) {
6888
6973
  const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
6889
6974
  if (!hasAuthOverride) {
6890
6975
  if (opts.fireAndForget) {
6891
- if (wsIntent(config.appId, normalizedRoomPath, intent))
6976
+ if (await wsIntent(config.appId, normalizedRoomPath, intent, config.isServer))
6892
6977
  return { ok: true };
6893
6978
  }
6894
6979
  else {
6895
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
6980
+ const ack = await wsIntentReliable(config.appId, normalizedRoomPath, intent, config.isServer);
6896
6981
  if (ack)
6897
- return await ack;
6982
+ return ack;
6898
6983
  }
6899
6984
  }
6900
6985
  const base = realtimeHttpBase(config.wsApiUrl);