@bounded-sh/core 0.0.26 → 0.0.35

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.
@@ -3,12 +3,11 @@ export interface ClientConfig {
3
3
  name: string;
4
4
  logoUrl: string;
5
5
  /** Auth method. Configure exactly one method at init time.
6
- * 'email' = Bounded Auth human login (email OTP, inline).
7
- * OAuth/social uses loginWithRedirect/
8
- * loginWithPopup rather than authMethod. Text OTP uses hosted/headless OTP
9
- * helpers only when explicitly enabled by the Bounded issuer. 'phantom' = connect a Solana wallet (Phantom), for
10
- * crypto/onchain apps that need a real @user.address. 'guest' = anonymous
11
- * device-key auth. */
6
+ * Bounded Auth is the default restore/logout surface; start email, social,
7
+ * and optional text login with loginWithRedirect/loginWithPopup.
8
+ * 'phantom' = connect a Solana wallet (Phantom), for crypto/onchain apps
9
+ * that need a real @user.address. Guests use signInAnonymously(), not
10
+ * authMethod. */
12
11
  authMethod: 'none' | 'email' | 'guest' | 'wallet' | 'rainbowkit' | 'coinbase-smart-wallet' | 'onboard' | 'phantom' | 'mobile-wallet-adapter' | 'privy' | 'privy-expo';
13
12
  wsApiUrl: string;
14
13
  apiUrl: string;
@@ -21,10 +20,9 @@ export interface ClientConfig {
21
20
  appId: string;
22
21
  /** Wallet/SIWS issuer (wallet + guest providers sign challenges against this). */
23
22
  authApiUrl: string;
24
- /** Human-login issuer (Bounded Better Auth — email OTP + OAuth, plus optional text OTP). The
25
- * inline 'email' provider calls {humanAuthApiUrl}/email + /verify; hosted
26
- * login and headless helpers can also use text OTP when the issuer enables it.
27
- * Defaults per network. */
23
+ /** Human-login issuer for hosted Bounded Auth: email OTP, OAuth/social, and
24
+ * optional text OTP. App-origin OTP helpers are retired; use hosted
25
+ * loginWithRedirect/loginWithPopup against this issuer. Defaults per network. */
28
26
  humanAuthApiUrl?: string;
29
27
  /**
30
28
  * Selects a Bounded backend preset. When set, the endpoint defaults
@@ -1,4 +1,5 @@
1
1
  import type { RequestOverrides } from './operations';
2
+ import type { SubscriptionOptions } from '../types';
2
3
  export type LiveIntentOptions = {
3
4
  /** Per-call timeout (ms) for the HTTP request to the realtime worker. */
4
5
  timeoutMs?: number;
@@ -79,6 +80,14 @@ export type SubscribeViewOptions = {
79
80
  onData: (view: any) => void;
80
81
  /** Called on subscription error. */
81
82
  onError?: (error: any) => void;
83
+ /**
84
+ * @internal Per-subscription auth overrides (server `WalletClient` path).
85
+ * Lets a keypair-backed agent player subscribe to its live view under its
86
+ * own wallet session instead of any global session — the same override seam
87
+ * `subscribe()` already honors. Without this, a Node agent could send
88
+ * intents but never SEE its view (the documented agent-as-player pattern).
89
+ */
90
+ _overrides?: SubscriptionOptions['_overrides'];
82
91
  };
83
92
  /**
84
93
  * Subscribe to YOUR per-player view of a room. Thin sugar over:
@@ -89,8 +98,8 @@ export type SubscribeViewOptions = {
89
98
  * claims); pass `opts.userId` to override. Returns the unsubscribe function
90
99
  * (a Promise<() => Promise<void>>, same as `subscribe`).
91
100
  *
92
- * Note: this is a browser-first helper (the WS subscription manager is
93
- * browser-oriented). Server consumers should use `live.intent`.
101
+ * Server/agent consumers: use `WalletClient.live.subscribeView` (it supplies
102
+ * the wallet session + view id); browser apps just call this directly.
94
103
  */
95
104
  export declare function subscribeView(roomPath: string, opts: SubscribeViewOptions): Promise<() => Promise<void>>;
96
105
  /** The `bounded.live` namespace surface. */
@@ -61,6 +61,8 @@ export declare function hasActiveConnection(): boolean;
61
61
  * client never names a destination.
62
62
  */
63
63
  export declare function wsIntent(appId: string, roomRoutePath: string, intent: unknown, isServer: boolean): Promise<boolean>;
64
+ /** True when an OPEN room socket exists for this room path. */
65
+ export declare function hasOpenRoomConnection(appId: string, roomRoutePath: string): boolean;
64
66
  /**
65
67
  * Send a live-room intent over the EXISTING per-room socket and AWAIT the server
66
68
  * ack — so a policy/auth denial (401/403/404/503) REJECTS instead of being
package/dist/index.js CHANGED
@@ -39,14 +39,10 @@ let clientConfig = {
39
39
  humanAuthApiUrl: 'https://auth.bounded.sh',
40
40
  functionsUrl: 'https://functions.bounded.sh',
41
41
  appId: '',
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'.
45
- // Text OTP is off by default and uses hosted/headless text helpers only when
46
- // Bounded explicitly enables it for the issuer. For
47
- // crypto/onchain wallet login use authMethod:'phantom' (Solana / Phantom), or
48
- // signInAnonymously() for zero-friction 'guest' accounts. ('wallet' is an
49
- // unimplemented stub; don't use.)
42
+ // No hidden modal fallback: browser clients start hosted Bounded Auth with
43
+ // loginWithRedirect/loginWithPopup. For crypto/onchain wallet login use
44
+ // authMethod:'phantom' (Solana / Phantom), and use signInAnonymously() for
45
+ // zero-friction guest accounts. ('wallet' is an unimplemented stub; don't use.)
50
46
  authMethod: 'none',
51
47
  chain: '',
52
48
  rpcUrl: '',
@@ -2826,6 +2822,7 @@ async function genAuthNonce() {
2826
2822
  // from 'phantom' and couldn't be gated or flagged.
2827
2823
  const SIGNATURE_AUTH_METHODS = new Set(['phantom', 'mobile-wallet-adapter', 'guest']);
2828
2824
  async function createSessionWithSignature(address, message, signature) {
2825
+ var _a, _b;
2829
2826
  const client = await getAxiosAuthClient();
2830
2827
  const config = await getConfig();
2831
2828
  const appId = config.appId;
@@ -2835,14 +2832,30 @@ async function createSessionWithSignature(address, message, signature) {
2835
2832
  // for a signature exchange. This is what makes the server SDK + guest device
2836
2833
  // keypair work under the email-default config.
2837
2834
  const authMethod = SIGNATURE_AUTH_METHODS.has(config.authMethod) ? config.authMethod : 'phantom';
2838
- const response = await client.post('/session', {
2839
- appId,
2840
- address,
2841
- message,
2842
- signature,
2843
- authMethod
2844
- });
2845
- return response.data;
2835
+ try {
2836
+ const response = await client.post('/session', {
2837
+ appId,
2838
+ address,
2839
+ message,
2840
+ signature,
2841
+ authMethod
2842
+ });
2843
+ return response.data;
2844
+ }
2845
+ catch (err) {
2846
+ // Surface the issuer's actionable message (e.g. "Anonymous auth is not enabled for
2847
+ // this app. Add \"auth\": { \"anonymous\": true } to policy.json and redeploy.")
2848
+ // instead of axios's generic "Request failed with status code 403".
2849
+ const d = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data;
2850
+ if (d && (d.message || d.error)) {
2851
+ const e = new Error(String(d.message || d.error));
2852
+ e.code = d.error;
2853
+ e.status = (_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status;
2854
+ e.hint = d.hint;
2855
+ throw e;
2856
+ }
2857
+ throw err;
2858
+ }
2846
2859
  }
2847
2860
  // Deduplicate concurrent refreshSession calls to prevent multiple code paths
2848
2861
  // (WebSocket reconnection, periodic refresh, API 401 retry) from all hitting
@@ -6562,7 +6575,30 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6562
6575
  if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6563
6576
  return false;
6564
6577
  }
6565
- if (!(await canSendAuthenticatedLiveIntent(connection, isServer))) {
6578
+ // Identity-aware send gate. The server attributes intents to the
6579
+ // CONNECTION's authenticated principal, so the only correct reasons to
6580
+ // refuse the socket are identity ones:
6581
+ // - the socket is anonymous while the user is now authenticated, or
6582
+ // - the current principal DIFFERS from the socket principal.
6583
+ // A mere token rotation for the SAME principal, or a transiently-null
6584
+ // token mid-refresh, must NOT reroute a 30Hz command stream to
6585
+ // per-command HTTP — that collapses delivery rate and unbounds the game's
6586
+ // ack lag (field-debugged: server 500+ commands behind).
6587
+ const currentToken = await getIdToken(isServer).catch(() => null);
6588
+ if (!currentToken) {
6589
+ // Mid-refresh: the socket principal still governs server-side.
6590
+ if (!connection.authTokenFingerprint && !connection.authPrincipal)
6591
+ return false;
6592
+ }
6593
+ else if (!connection.authTokenFingerprint && !connection.authPrincipal) {
6594
+ // Anonymous socket + authenticated user: do not attribute this user's
6595
+ // input to the anon principal. Reauth the socket; caller may fall back.
6596
+ void maybeReauthRoomConnection(connection);
6597
+ return false;
6598
+ }
6599
+ else if (!connectionMatchesAuthToken(connection, currentToken)) {
6600
+ // Different principal (identity switch): never send as the old user.
6601
+ void maybeReauthRoomConnection(connection);
6566
6602
  return false;
6567
6603
  }
6568
6604
  try {
@@ -6573,6 +6609,26 @@ async function wsIntent(appId, roomRoutePath, intent, isServer) {
6573
6609
  return false;
6574
6610
  }
6575
6611
  }
6612
+ // Debounced background reauth: when the client's current token no longer
6613
+ // matches the connection's auth fingerprint (token rotation), refresh the
6614
+ // socket auth ONCE rather than degrading every subsequent send.
6615
+ const reauthInFlight = new WeakSet();
6616
+ async function maybeReauthRoomConnection(connection, isServer) {
6617
+ try {
6618
+ if (reauthInFlight.has(connection))
6619
+ return;
6620
+ reauthInFlight.add(connection);
6621
+ try {
6622
+ await reconnectWithNewAuthV2();
6623
+ }
6624
+ finally {
6625
+ reauthInFlight.delete(connection);
6626
+ }
6627
+ }
6628
+ catch (_a) {
6629
+ // Best-effort: the next send retries; the server still enforces auth.
6630
+ }
6631
+ }
6576
6632
  /**
6577
6633
  * Send a live-room intent over the EXISTING per-room socket and AWAIT the server
6578
6634
  * ack — so a policy/auth denial (401/403/404/503) REJECTS instead of being
@@ -7140,8 +7196,8 @@ async function status(roomPath, opts = {}) {
7140
7196
  * claims); pass `opts.userId` to override. Returns the unsubscribe function
7141
7197
  * (a Promise<() => Promise<void>>, same as `subscribe`).
7142
7198
  *
7143
- * Note: this is a browser-first helper (the WS subscription manager is
7144
- * browser-oriented). Server consumers should use `live.intent`.
7199
+ * Server/agent consumers: use `WalletClient.live.subscribeView` (it supplies
7200
+ * the wallet session + view id); browser apps just call this directly.
7145
7201
  */
7146
7202
  async function subscribeView(roomPath, opts) {
7147
7203
  if (!roomPath || typeof roomPath !== 'string') {
@@ -7171,7 +7227,7 @@ async function subscribeView(roomPath, opts) {
7171
7227
  // derives the room from the `?path=` the connection carries). Without this the
7172
7228
  // subscription lands on the app-level project DO and never sees the room's
7173
7229
  // ephemeral view writes.
7174
- return subscribeV2(viewPath, { onData: opts.onData, onError: opts.onError }, normalizedRoomPath);
7230
+ return subscribeV2(viewPath, { onData: opts.onData, onError: opts.onError, _overrides: opts._overrides }, normalizedRoomPath);
7175
7231
  }
7176
7232
  /** The `bounded.live` namespace surface. */
7177
7233
  const live = { intent, status, subscribeView };