@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.
@@ -2,12 +2,13 @@ import { AuthProvider } from '../types';
2
2
  export interface ClientConfig {
3
3
  name: string;
4
4
  logoUrl: string;
5
- /** Auth method. 'email' = Bounded Auth human login (email OTP, inline) —
6
- * the default for most apps. OAuth/social uses loginWithRedirect/
5
+ /** Auth method. Configure exactly one method at init time.
6
+ * 'email' = Bounded Auth human login (email OTP, inline).
7
+ * OAuth/social uses loginWithRedirect/
7
8
  * loginWithPopup rather than authMethod. Text OTP uses hosted/headless OTP
8
9
  * helpers only when explicitly enabled by the Bounded issuer. 'phantom' = connect a Solana wallet (Phantom), for
9
- * crypto/onchain apps that need a real @user.address. 'guest' = zero-config
10
- * anonymous (device keypair). All coexist. */
10
+ * crypto/onchain apps that need a real @user.address. 'guest' = anonymous
11
+ * device-key auth. */
11
12
  authMethod: 'none' | 'email' | 'guest' | 'wallet' | 'rainbowkit' | 'coinbase-smart-wallet' | 'onboard' | 'phantom' | 'mobile-wallet-adapter' | 'privy' | 'privy-expo';
12
13
  wsApiUrl: string;
13
14
  apiUrl: string;
@@ -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
@@ -30,8 +30,8 @@ let clientConfig = {
30
30
  // User configured settings
31
31
  name: '',
32
32
  logoUrl: '',
33
- // Bounded production is the out-of-the-box default a Bounded app needs only
34
- // `{ appId }`. Pass `network: 'bounded-staging'` to target staging.
33
+ // Bounded production is the endpoint default. Apps still choose one explicit
34
+ // auth method at init time. Pass `network: 'bounded-staging'` to target staging.
35
35
  network: 'bounded-production',
36
36
  wsApiUrl: 'wss://realtime.bounded.sh',
37
37
  apiUrl: 'https://realtime.bounded.sh',
@@ -39,14 +39,15 @@ let clientConfig = {
39
39
  humanAuthApiUrl: 'https://auth.bounded.sh',
40
40
  functionsUrl: 'https://functions.bounded.sh',
41
41
  appId: '',
42
- // 'email' = Bounded Auth human login (inline email OTP) — the out-of-box default
43
- // for normal apps. Hosted OAuth/social uses loginWithRedirect/loginWithPopup.
42
+ // No hidden auth fallback: browser clients must pass authMethod explicitly
43
+ // (for example 'email', 'guest', 'phantom', 'privy', or 'privy-expo').
44
+ // Hosted OAuth/social uses loginWithRedirect/loginWithPopup with authMethod:'email'.
44
45
  // Text OTP is off by default and uses hosted/headless text helpers only when
45
46
  // Bounded explicitly enables it for the issuer. For
46
47
  // crypto/onchain wallet login use authMethod:'phantom' (Solana / Phantom), or
47
48
  // signInAnonymously() for zero-friction 'guest' accounts. ('wallet' is an
48
49
  // unimplemented stub; don't use.)
49
- authMethod: 'email',
50
+ authMethod: 'none',
50
51
  chain: '',
51
52
  rpcUrl: '',
52
53
  skipBackendInit: true,
@@ -112,10 +113,14 @@ function init(newConfig) {
112
113
  }
113
114
  // Bounded is client-driven: defaults are Bounded production, `network`
114
115
  // switches the whole endpoint set (e.g. 'bounded-staging'), and anything
115
- // passed explicitly wins. No `/config` round-trip `init({ appId })` is
116
- // synchronous and works out of the box.
116
+ // passed explicitly wins. No `/config` round-trip; browser SDKs still pass
117
+ // one explicit authMethod so there is no hidden auth-provider fallback.
117
118
  // defaults (bounded-production) < network preset < explicit newConfig
118
- const preset = (newConfig.network && BOUNDED_NETWORKS[newConfig.network]) || {};
119
+ if (newConfig.network !== undefined && !(newConfig.network in BOUNDED_NETWORKS)) {
120
+ reject(new Error(`Unsupported Bounded network "${String(newConfig.network)}". Expected bounded, bounded-staging, or bounded-production.`));
121
+ return;
122
+ }
123
+ const preset = newConfig.network ? BOUNDED_NETWORKS[newConfig.network] : {};
119
124
  clientConfig = Object.assign(Object.assign(Object.assign({}, clientConfig), preset), newConfig);
120
125
  isInitialized = true;
121
126
  resolve();
@@ -5430,6 +5435,48 @@ function getCacheKey(path, prompt, shape, limit, cursor, filter, identity) {
5430
5435
  function principalFromIdToken(idToken) {
5431
5436
  return idToken ? `t${hashForKey(idToken)}` : null;
5432
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
+ }
5433
5480
  async function getSubscriptionIdentity(effectiveAppId, isServer, overrides) {
5434
5481
  // Per-subscription wallet override (server WalletClient.subscribe): key by
5435
5482
  // the wallet's own opaque token material, mirroring getReadPrincipalKey. Do
@@ -5505,6 +5552,9 @@ function rememberAmbientAuthFailure(error) {
5505
5552
  function failConnectionAuth(connection, error) {
5506
5553
  connection.authFailure = error;
5507
5554
  connection.pendingAuthToken = null;
5555
+ connection.pendingAuthSnapshot = null;
5556
+ connection.authTokenFingerprint = null;
5557
+ connection.authPrincipal = null;
5508
5558
  connection.isConnecting = false;
5509
5559
  connection.isConnected = false;
5510
5560
  connection.isAuthenticating = false;
@@ -5723,16 +5773,37 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5723
5773
  if (connection.authFailure) {
5724
5774
  throw connection.authFailure;
5725
5775
  }
5776
+ let currentAuthToken;
5726
5777
  try {
5727
- await getConnectionAuthToken(isServer, authTokenProvider);
5778
+ currentAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5728
5779
  }
5729
5780
  catch (error) {
5730
5781
  const authError = normalizeAuthExpiredError(error, 'Authentication expired and refresh failed');
5731
5782
  failConnectionAuth(connection, authError);
5732
5783
  throw authError;
5733
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
+ }
5734
5806
  connection.authFailure = null;
5735
- return connection;
5736
5807
  }
5737
5808
  let initialAuthToken = await getConnectionAuthToken(isServer, authTokenProvider);
5738
5809
  // Create new connection
@@ -5750,6 +5821,9 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5750
5821
  routePath: roomKey ? routePath : undefined,
5751
5822
  authTokenProvider,
5752
5823
  pendingAuthToken: null,
5824
+ pendingAuthSnapshot: null,
5825
+ authTokenFingerprint: null,
5826
+ authPrincipal: null,
5753
5827
  tokenRefreshTimer: null,
5754
5828
  consecutiveAuthFailures: 0,
5755
5829
  authFailure: null,
@@ -5787,6 +5861,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5787
5861
  throw authError;
5788
5862
  }
5789
5863
  connection.pendingAuthToken = authToken || null;
5864
+ connection.pendingAuthSnapshot = authSnapshotFromToken(authToken);
5790
5865
  if (authToken) {
5791
5866
  // Successful token acquisition — reset failure counter
5792
5867
  connection.consecutiveAuthFailures = 0;
@@ -5826,6 +5901,8 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5826
5901
  }
5827
5902
  else {
5828
5903
  connection.isAuthenticating = false;
5904
+ connection.authTokenFingerprint = null;
5905
+ connection.authPrincipal = null;
5829
5906
  replaySubscriptions(connection);
5830
5907
  }
5831
5908
  });
@@ -5865,10 +5942,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5865
5942
  return connection;
5866
5943
  }
5867
5944
  function handleServerMessage(connection, message) {
5868
- var _a;
5945
+ var _a, _b, _c, _d, _e;
5869
5946
  switch (message.type) {
5870
5947
  case 'authenticated': {
5871
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;
5872
5951
  replaySubscriptions(connection);
5873
5952
  break;
5874
5953
  }
@@ -5939,7 +6018,7 @@ function handleServerMessage(connection, message) {
5939
6018
  connection.pendingRequests.delete(message.requestId);
5940
6019
  clearTimeout(pendingSet.timer);
5941
6020
  if (message.statusCode >= 400) {
5942
- 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}`));
5943
6022
  }
5944
6023
  else {
5945
6024
  pendingSet.resolve(message.body);
@@ -6467,13 +6546,16 @@ function hasActiveConnection() {
6467
6546
  * room authority — the connection is already routed to the room DO, so the
6468
6547
  * client never names a destination.
6469
6548
  */
6470
- function wsIntent(appId, roomRoutePath, intent) {
6549
+ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6471
6550
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6472
6551
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6473
6552
  const connection = connections.get(connKey);
6474
6553
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6475
6554
  return false;
6476
6555
  }
6556
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6557
+ return false;
6558
+ }
6477
6559
  try {
6478
6560
  connection.ws.send(JSON.stringify({ type: 'intent', intent }));
6479
6561
  return true;
@@ -6490,13 +6572,16 @@ function wsIntent(appId, roomRoutePath, intent) {
6490
6572
  * Promise that resolves on ack / rejects on error, or `undefined` if no live
6491
6573
  * socket exists yet (caller falls back to HTTP, which also surfaces errors).
6492
6574
  */
6493
- function wsIntentReliable(appId, roomRoutePath, intent) {
6575
+ async function wsIntentReliable(appId, roomRoutePath, intent, isServer) {
6494
6576
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6495
6577
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6496
6578
  const connection = connections.get(connKey);
6497
6579
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6498
6580
  return undefined;
6499
6581
  }
6582
+ if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6583
+ return undefined;
6584
+ }
6500
6585
  const requestId = generateRequestId();
6501
6586
  return new Promise((resolve, reject) => {
6502
6587
  const timer = setTimeout(() => {
@@ -6908,13 +6993,13 @@ async function intent(roomPath, intent, opts = {}) {
6908
6993
  const hasAuthOverride = !!((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders);
6909
6994
  if (!hasAuthOverride) {
6910
6995
  if (opts.fireAndForget) {
6911
- if (wsIntent(config.appId, normalizedRoomPath, intent))
6996
+ if (await wsIntent(config.appId, normalizedRoomPath, intent, config.isServer))
6912
6997
  return { ok: true };
6913
6998
  }
6914
6999
  else {
6915
- const ack = wsIntentReliable(config.appId, normalizedRoomPath, intent);
7000
+ const ack = await wsIntentReliable(config.appId, normalizedRoomPath, intent, config.isServer);
6916
7001
  if (ack)
6917
- return await ack;
7002
+ return ack;
6918
7003
  }
6919
7004
  }
6920
7005
  const base = realtimeHttpBase(config.wsApiUrl);