@bounded-sh/core 0.0.56 → 0.0.58

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.
Files changed (29) hide show
  1. package/dist/client/config.d.ts +28 -0
  2. package/dist/client/live.d.ts +9 -0
  3. package/dist/{index-2e3ksoSR.js → index-B30-XpaX.js} +193 -37
  4. package/dist/index-B30-XpaX.js.map +1 -0
  5. package/dist/{index-BZhK47ZR.mjs → index-DYbzK8YT.mjs} +193 -38
  6. package/dist/index-DYbzK8YT.mjs.map +1 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{realtime-store-CpdBV8Ma.mjs → realtime-store-BSuV-Vyr.mjs} +2 -2
  12. package/dist/{realtime-store-CpdBV8Ma.mjs.map → realtime-store-BSuV-Vyr.mjs.map} +1 -1
  13. package/dist/{realtime-store-Cp7xYNS3.js → realtime-store-BpQsQYhl.js} +2 -2
  14. package/dist/{realtime-store-Cp7xYNS3.js.map → realtime-store-BpQsQYhl.js.map} +1 -1
  15. package/dist/{solana-message-Cfg_S81A.mjs → solana-message-C2_VvNX7.mjs} +2 -2
  16. package/dist/{solana-message-Cfg_S81A.mjs.map → solana-message-C2_VvNX7.mjs.map} +1 -1
  17. package/dist/{solana-message-DHJ8xSuS.js → solana-message-DKOPPJwk.js} +2 -2
  18. package/dist/{solana-message-DHJ8xSuS.js.map → solana-message-DKOPPJwk.js.map} +1 -1
  19. package/dist/{solana-operations-DcjCYmuL.js → solana-operations-BOKHbEuy.js} +16 -4
  20. package/dist/solana-operations-BOKHbEuy.js.map +1 -0
  21. package/dist/{solana-operations-BsnDXoeq.mjs → solana-operations-CuYFYlyN.mjs} +16 -4
  22. package/dist/solana-operations-CuYFYlyN.mjs.map +1 -0
  23. package/dist/utils/server-session-manager.d.ts +1 -0
  24. package/dist/utils/session-binding.d.ts +17 -0
  25. package/package.json +3 -2
  26. package/dist/index-2e3ksoSR.js.map +0 -1
  27. package/dist/index-BZhK47ZR.mjs.map +0 -1
  28. package/dist/solana-operations-BsnDXoeq.mjs.map +0 -1
  29. package/dist/solana-operations-DcjCYmuL.js.map +0 -1
@@ -92,6 +92,26 @@ export interface ClientConfig {
92
92
  network?: string | null;
93
93
  rpcUrl?: string | null;
94
94
  };
95
+ /** LOGIN MODE for the unified login widget (default 'bounded'). 'bounded' runs
96
+ * the existing Better Auth email/social/OIDC flows. 'turnkey' opts the email
97
+ * lane into Turnkey-native OTP INLINE in the widget (no second Bounded OTP);
98
+ * social + wallet lanes are unchanged. A per-call openBoundedWidget({ authMode })
99
+ * can override this. */
100
+ authMode?: 'bounded' | 'turnkey';
101
+ /** TEXT for the unified login widget. `title` replaces the default "Sign in"
102
+ * heading; `subtitle` replaces the default subline - pass "" to render no
103
+ * subline at all. A per-call openBoundedWidget({ title, subtitle }) overrides
104
+ * these. */
105
+ loginWidget?: {
106
+ title?: string;
107
+ subtitle?: string;
108
+ };
109
+ /** SITE POLICY: require an email on file for every user (default false). Native
110
+ * wallet login carries no email, so when true the unified widget suppresses the
111
+ * "Continue with wallet" lane (email/social remain). Overrides walletLogin and
112
+ * any per-call `wallet: true`. Set `walletLogin: false` to hide the wallet lane
113
+ * without requiring email. */
114
+ requireEmail?: boolean;
95
115
  /** Solana WALLET LOGIN provider configuration (advanced). Passing this object
96
116
  * ALSO opts the app into wallet login (equivalent to walletLogin). The slim,
97
117
  * dependency-free "connect wallet" path rides the injected Solana provider a
@@ -141,6 +161,14 @@ export declare const BOUNDED_NETWORKS: Record<string, BoundedEndpoints>;
141
161
  * fail-closed production default (audit SDK LOW-7).
142
162
  */
143
163
  export declare function getWebhookKeysUrl(): string | undefined;
164
+ /**
165
+ * The app id configured via `init({ appId })`, or `undefined` when none has been set.
166
+ * Synchronous + non-blocking on purpose (mirrors {@link getWebhookKeysUrl}): a pure webhook
167
+ * receiver may never call `init()`, and `getConfig()` would block on initialization. Used by
168
+ * `verifyWebhook` to bind a webhook to THIS app by default (#072) rather than trusting any
169
+ * app's shared-key-signed delivery.
170
+ */
171
+ export declare function getConfiguredAppId(): string | undefined;
144
172
  /**
145
173
  * True when init() configured a Bounded network (the Cloudflare-native stack).
146
174
  * Synchronous + non-blocking. Used to route reads/writes that behave differently
@@ -21,6 +21,8 @@ export type LiveStatusOptions = {
21
21
  timeoutMs?: number;
22
22
  /** Extra headers (advanced/testing). */
23
23
  headers?: Record<string, string>;
24
+ /** @internal Per-call auth override used by server WalletClient. */
25
+ _overrides?: RequestOverrides;
24
26
  };
25
27
  export type LiveStopReason = 'idle' | 'lifetime' | 'error' | 'manual' | 'evicted' | string;
26
28
  export type LiveStatus = {
@@ -71,6 +73,13 @@ export declare function intent(roomPath: string, intent: unknown, opts?: LiveInt
71
73
  * This is diagnostic/ops surface. It reports whether the room facet exists,
72
74
  * whether it is currently ticking, why it last stopped, which module etag is
73
75
  * loaded, and the current generation used after terminal restarts.
76
+ *
77
+ * Auth: status attaches the caller's session token WHEN ONE EXISTS (mirroring
78
+ * `intent`), but does NOT require it. The server gates the DETAILED body on the
79
+ * room's read rule — an authenticated caller who can read the room gets full
80
+ * telemetry; an anonymous / unauthorized caller gets only the slim liveness
81
+ * shape `{available, started, module}` (#102). The caller's own `opts.headers`
82
+ * may never smuggle in Authorization: only the derived session token sets it.
74
83
  */
75
84
  export declare function status(roomPath: string, opts?: LiveStatusOptions): Promise<LiveStatus>;
76
85
  export type SubscribeViewOptions = {
@@ -82,6 +82,16 @@ function getWebhookKeysUrl() {
82
82
  return undefined;
83
83
  return host.replace(/\/$/, '') + '/.well-known/bounded-webhook-keys.json';
84
84
  }
85
+ /**
86
+ * The app id configured via `init({ appId })`, or `undefined` when none has been set.
87
+ * Synchronous + non-blocking on purpose (mirrors {@link getWebhookKeysUrl}): a pure webhook
88
+ * receiver may never call `init()`, and `getConfig()` would block on initialization. Used by
89
+ * `verifyWebhook` to bind a webhook to THIS app by default (#072) rather than trusting any
90
+ * app's shared-key-signed delivery.
91
+ */
92
+ function getConfiguredAppId() {
93
+ return clientConfig.appId || undefined;
94
+ }
85
95
  /**
86
96
  * True when init() configured a Bounded network (the Cloudflare-native stack).
87
97
  * Synchronous + non-blocking. Used to route reads/writes that behave differently
@@ -2795,19 +2805,28 @@ function resolveIssuerBase(issuer, cfg) {
2795
2805
  return want;
2796
2806
  return norm(cfg.authApiUrl);
2797
2807
  }
2798
- let axiosClient;
2808
+ // #062: key the auth client by its baseURL. The old single memoized client froze the FIRST
2809
+ // environment's authApiUrl, so after re-initializing the SDK to a different issuer (staging->
2810
+ // prod, or a custom issuer) the mint calls (/nonce, /auth/nonce, /session) kept hitting the
2811
+ // original address forever - a "works only after a full reload" login bug, and in a multi-tenant
2812
+ // process one config's address could leak into another's. refresh/revoke already resolve per
2813
+ // call via resolveIssuerBase; this makes minting do the same while still caching per URL.
2814
+ const axiosClientsByUrl = new Map();
2799
2815
  async function getAxiosAuthClient() {
2800
- if (!axiosClient) {
2801
- const config = await getConfig();
2802
- axiosClient = axios.create({
2803
- baseURL: config.authApiUrl,
2816
+ const config = await getConfig();
2817
+ const baseURL = config.authApiUrl || '';
2818
+ let client = axiosClientsByUrl.get(baseURL);
2819
+ if (!client) {
2820
+ client = axios.create({
2821
+ baseURL,
2804
2822
  headers: {
2805
2823
  'Content-Type': 'application/json',
2806
2824
  },
2807
2825
  timeout: 30000, // 30s timeout, matching makeApiRequest
2808
2826
  });
2827
+ axiosClientsByUrl.set(baseURL, client);
2809
2828
  }
2810
- return axiosClient;
2829
+ return client;
2811
2830
  }
2812
2831
  async function genAuthNonce() {
2813
2832
  const client = await getAxiosAuthClient();
@@ -2972,6 +2991,44 @@ async function revokeSession(refreshToken, issuer) {
2972
2991
  async function signSessionCreateMessage(_signMessageFunction) {
2973
2992
  }
2974
2993
 
2994
+ // Shared session-binding guard for the web + React Native session managers, so
2995
+ // the cross-principal refresh guard (#220 / #221) lives in ONE place and the two
2996
+ // managers can't drift apart. Base64url decoding differs by platform (browser
2997
+ // atob vs a configured decoder), so the caller passes its own decoder in.
2998
+ /**
2999
+ * Extract the `custom:walletAddress` claim from a JWT idToken. Returns undefined
3000
+ * when the token has no wallet claim or cannot be decoded.
3001
+ */
3002
+ function idTokenWalletAddress(idToken, decodeBase64Url) {
3003
+ try {
3004
+ const payloadB64 = idToken.split(".")[1];
3005
+ if (!payloadB64)
3006
+ return undefined;
3007
+ const wallet = JSON.parse(decodeBase64Url(payloadB64))["custom:walletAddress"];
3008
+ return typeof wallet === "string" && wallet.length > 0 ? wallet : undefined;
3009
+ }
3010
+ catch (_a) {
3011
+ return undefined;
3012
+ }
3013
+ }
3014
+ /**
3015
+ * Whether a refreshed idToken may be grafted onto the CURRENTLY-stored session.
3016
+ *
3017
+ * A silent refresh started for user A can complete after storage was replaced
3018
+ * with user B (a login, guest→wallet upgrade, or a cross-tab login). Overwriting
3019
+ * B's tokens with A's — the app-id matches, so the old guard let it through —
3020
+ * installs a genuine-but-WRONG session: the UI shows B while the app authenticates
3021
+ * as A (#220 / #221). Refuse the write when the refreshed token binds to a
3022
+ * DIFFERENT wallet than the one currently stored. A tokenless/addressless session
3023
+ * (guest / opaque) preserves the prior behavior (returns true).
3024
+ */
3025
+ function refreshedTokenMayReplaceStored(newIdToken, storedAddress, decodeBase64Url) {
3026
+ const tokenWallet = idTokenWalletAddress(newIdToken, decodeBase64Url);
3027
+ if (!tokenWallet || !storedAddress)
3028
+ return true;
3029
+ return tokenWallet === storedAddress;
3030
+ }
3031
+
2975
3032
  class WebSessionManager {
2976
3033
  /**
2977
3034
  * Decode a base64url-encoded string (used by JWT payloads).
@@ -3128,16 +3185,27 @@ class WebSessionManager {
3128
3185
  var _a;
3129
3186
  if (typeof window === "undefined")
3130
3187
  return;
3188
+ // Resolve config FIRST so the read → check → write below is one synchronous
3189
+ // turn (an atomic CAS): no `await` can let a concurrent login interleave a
3190
+ // different session between the read and the write (#221).
3191
+ const config = await getConfig();
3131
3192
  const session = localStorage.getItem(this.BOUNDED_SESSION_STORAGE_KEY);
3132
3193
  if (!session)
3133
3194
  return;
3134
3195
  const sessionObj = JSON.parse(session);
3135
3196
  /* ---------- app-id guard ---------- */
3136
- const config = await getConfig();
3137
3197
  if (sessionObj.appId && sessionObj.appId !== config.appId) {
3138
3198
  this.clearSession();
3139
3199
  return;
3140
3200
  }
3201
+ /* ---------- cross-principal guard (#221) ---------- */
3202
+ // Refuse to graft a refreshed token onto a DIFFERENT user's stored session
3203
+ // (a stale refresh completing after a login/upgrade replaced storage, incl.
3204
+ // a cross-tab login). Also re-runs the wallet-binding check storeSession does.
3205
+ if (!refreshedTokenMayReplaceStored(idToken, sessionObj.address, (s) => this.decodeBase64Url(s))) {
3206
+ console.warn("[WebSessionManager] updateIdTokenAndAccessToken: refreshed token wallet does not match the stored session address — discarding stale refresh");
3207
+ return;
3208
+ }
3141
3209
  sessionObj.idToken = idToken;
3142
3210
  sessionObj.accessToken = accessToken;
3143
3211
  // Persist the rotated refresh token when the issuer returns a new one.
@@ -3346,16 +3414,27 @@ class ReactNativeSessionManager {
3346
3414
  /* ------------------------------------------------------------------ */
3347
3415
  static async updateIdTokenAndAccessToken(idToken, accessToken, refreshToken) {
3348
3416
  var _a;
3417
+ // Resolve config FIRST so the read → check → write below is one synchronous
3418
+ // turn (an atomic CAS): no `await` can let a concurrent login interleave a
3419
+ // different session between the read and the write (#220).
3420
+ const config = await getConfig();
3349
3421
  const session = this.getStorage().getItem(this.BOUNDED_SESSION_STORAGE_KEY);
3350
3422
  if (!session)
3351
3423
  return;
3352
3424
  const sessionObj = JSON.parse(session);
3353
3425
  /* ---------- app-id guard ---------- */
3354
- const config = await getConfig();
3355
3426
  if (sessionObj.appId && sessionObj.appId !== config.appId) {
3356
3427
  this.clearSession();
3357
3428
  return;
3358
3429
  }
3430
+ /* ---------- cross-principal guard (#220) ---------- */
3431
+ // Refuse to graft a refreshed token onto a DIFFERENT user's stored session
3432
+ // (a stale refresh completing after a login/upgrade replaced storage). Also
3433
+ // re-runs the wallet-binding check storeSession does.
3434
+ if (!refreshedTokenMayReplaceStored(idToken, sessionObj.address, (s) => this.decodeBase64Url(s))) {
3435
+ console.warn("[ReactNativeSessionManager] updateIdTokenAndAccessToken: refreshed token wallet does not match the stored session address — discarding stale refresh");
3436
+ return;
3437
+ }
3359
3438
  sessionObj.idToken = idToken;
3360
3439
  sessionObj.accessToken = accessToken;
3361
3440
  if (typeof refreshToken === "string" && refreshToken.length > 0) {
@@ -3410,6 +3489,12 @@ class ServerSessionManager {
3410
3489
  this.session = null;
3411
3490
  /* Coalesce concurrent getSession() calls into a single in-flight request */
3412
3491
  this.pendingSession = null;
3492
+ /* Monotonic generation, bumped on clear/set. An in-flight createSession() only
3493
+ * commits its result to the cache if the generation still matches the value it
3494
+ * captured when it started — so a clearSession()/setSession() that happens WHILE
3495
+ * a create is in flight deterministically wins, and the late result can't
3496
+ * silently re-install a session the caller just cleared/replaced (#259). */
3497
+ this.generation = 0;
3413
3498
  this.keypair = keypair !== null && keypair !== void 0 ? keypair : null;
3414
3499
  this.sessionFactory = sessionFactory !== null && sessionFactory !== void 0 ? sessionFactory : null;
3415
3500
  }
@@ -3451,7 +3536,7 @@ class ServerSessionManager {
3451
3536
  const address = kp.publicKey.toBase58();
3452
3537
  /* fetch nonce from auth API */
3453
3538
  const nonce = await genAuthNonce();
3454
- const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-DHJ8xSuS.js'); });
3539
+ const { genSolanaMessage } = await Promise.resolve().then(function () { return require('./solana-message-DKOPPJwk.js'); });
3455
3540
  const message = await genSolanaMessage(address, nonce);
3456
3541
  /* sign the message */
3457
3542
  const sigBytes = nacl.sign.detached(new TextEncoder().encode(message), kp.secretKey);
@@ -3472,9 +3557,13 @@ class ServerSessionManager {
3472
3557
  if (this.pendingSession !== null) {
3473
3558
  return this.pendingSession;
3474
3559
  }
3560
+ const startedGeneration = this.generation;
3475
3561
  this.pendingSession = this.createSession()
3476
3562
  .then((session) => {
3477
- this.session = session;
3563
+ // Only commit if no clear/set happened while this create was in flight.
3564
+ if (this.generation === startedGeneration) {
3565
+ this.session = session;
3566
+ }
3478
3567
  return session;
3479
3568
  })
3480
3569
  .finally(() => {
@@ -3486,12 +3575,14 @@ class ServerSessionManager {
3486
3575
  * STORE (overwrites the cached value)
3487
3576
  * ---------------------------------------------- */
3488
3577
  setSession(session) {
3578
+ this.generation++;
3489
3579
  this.session = session;
3490
3580
  }
3491
3581
  /* ---------------------------------------------- *
3492
3582
  * CLEAR (e.g. after explicit logout)
3493
3583
  * ---------------------------------------------- */
3494
3584
  clearSession() {
3585
+ this.generation++;
3495
3586
  this.session = null;
3496
3587
  this.pendingSession = null;
3497
3588
  }
@@ -4025,8 +4116,27 @@ axiosRetry(apiClient, {
4025
4116
  },
4026
4117
  });
4027
4118
  const refreshInFlight = new Map();
4119
+ // Short, bounded, non-secret fingerprint of a refresh token — used only to KEY the
4120
+ // in-flight de-dup map (never logged, never the raw secret).
4121
+ function refreshTokenFingerprint(token) {
4122
+ let h = 5381;
4123
+ for (let i = 0; i < token.length; i++)
4124
+ h = ((h << 5) + h + token.charCodeAt(i)) >>> 0;
4125
+ return h.toString(16);
4126
+ }
4028
4127
  async function refreshAuthSessionOnce(appId, isServer) {
4029
- const key = `${isServer ? "server" : "web"}:${appId}`;
4128
+ // Capture the CURRENT session's refresh token up front and key the in-flight
4129
+ // refresh by it, not just by the app. Keyed by app only, a refresh started for
4130
+ // user A would be reused for a request issued after switching to user B (guest→
4131
+ // wallet upgrade, logout/login), so B's 401 recovery would piggyback on A's
4132
+ // refresh (#112). Per-session keying keeps de-dup for the SAME session while
4133
+ // making a post-switch refresh a distinct entry. (The token WRITE is separately
4134
+ // guarded per-principal in the session managers, #220/#221.)
4135
+ const refreshToken = await getRefreshToken(isServer);
4136
+ if (!refreshToken) {
4137
+ return false;
4138
+ }
4139
+ const key = `${isServer ? "server" : "web"}:${appId}:${refreshTokenFingerprint(refreshToken)}`;
4030
4140
  const existing = refreshInFlight.get(key);
4031
4141
  if (existing) {
4032
4142
  return existing;
@@ -4034,10 +4144,6 @@ async function refreshAuthSessionOnce(appId, isServer) {
4034
4144
  const refreshPromise = (async () => {
4035
4145
  var _a, _b;
4036
4146
  try {
4037
- const refreshToken = await getRefreshToken(isServer);
4038
- if (!refreshToken) {
4039
- return false;
4040
- }
4041
4147
  const refreshData = await refreshSession(refreshToken, getSessionIssuer(isServer));
4042
4148
  if (!(refreshData === null || refreshData === void 0 ? void 0 : refreshData.idToken) || !(refreshData === null || refreshData === void 0 ? void 0 : refreshData.accessToken)) {
4043
4149
  return false;
@@ -4078,9 +4184,21 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4078
4184
  ? {}
4079
4185
  : await createAuthHeader(config.isServer);
4080
4186
  const headers = Object.assign({ "Content-Type": "application/json", "X-App-Id": config.appId }, authHeader);
4081
- // Apply custom headers from _overrides
4187
+ // Apply custom headers from _overrides. #022/#023: HTTP header names are
4188
+ // case-insensitive, so normalize any override `authorization` (in any casing) onto the
4189
+ // single canonical `Authorization` key - never send two differently-cased Authorization
4190
+ // headers, and keep the credential the request sends identical to the one the read
4191
+ // cache key is derived from.
4082
4192
  if (_overrides === null || _overrides === void 0 ? void 0 : _overrides.headers) {
4083
- Object.assign(headers, _overrides.headers);
4193
+ for (const [name, value] of Object.entries(_overrides.headers)) {
4194
+ if (name.toLowerCase() === "authorization") {
4195
+ delete headers["authorization"];
4196
+ headers["Authorization"] = value;
4197
+ }
4198
+ else {
4199
+ headers[name] = value;
4200
+ }
4201
+ }
4084
4202
  }
4085
4203
  // Writes (PUT/POST/DELETE) can take significantly longer than reads —
4086
4204
  // server-side they may trigger ad-hoc LUT creation (create + extend +
@@ -5892,7 +6010,7 @@ async function doReconnectWithNewAuth() {
5892
6010
  console.warn('[WS v2] Failed to clear HTTP read cache on auth change:', error);
5893
6011
  }
5894
6012
  try {
5895
- const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-Cp7xYNS3.js'); });
6013
+ const { reconnectRealtimeStoreWithNewAuth } = await Promise.resolve().then(function () { return require('./realtime-store-BpQsQYhl.js'); });
5896
6014
  await reconnectRealtimeStoreWithNewAuth();
5897
6015
  }
5898
6016
  catch (error) {
@@ -6717,14 +6835,25 @@ function hashForKeyCached(value) {
6717
6835
  principalHashCache.set(value, h);
6718
6836
  return h;
6719
6837
  }
6720
- function hasAuthHeader(headers) {
6838
+ // #022: HTTP header names are case-insensitive on the wire, so recognize `authorization`
6839
+ // in ANY casing when deriving the per-user cache key. Otherwise a caller-supplied
6840
+ // `AUTHORIZATION: <token B>` rides into the request while the cache key is computed from
6841
+ // the ambient token A - filing B's data under A's identity. The cache key must always be
6842
+ // derived from the same credential the request authenticates with.
6843
+ function authHeaderEntry(headers) {
6721
6844
  if (!headers)
6722
- return false;
6723
- return (Object.prototype.hasOwnProperty.call(headers, 'Authorization') ||
6724
- Object.prototype.hasOwnProperty.call(headers, 'authorization'));
6845
+ return undefined;
6846
+ for (const name of Object.keys(headers)) {
6847
+ if (name.toLowerCase() === 'authorization')
6848
+ return headers[name];
6849
+ }
6850
+ return undefined;
6851
+ }
6852
+ function hasAuthHeader(headers) {
6853
+ return authHeaderEntry(headers) !== undefined;
6725
6854
  }
6726
6855
  function authValueFromHeaders(headers) {
6727
- return (headers === null || headers === void 0 ? void 0 : headers.Authorization) || (headers === null || headers === void 0 ? void 0 : headers.authorization) || '';
6856
+ return authHeaderEntry(headers) || '';
6728
6857
  }
6729
6858
  function principalFromAuthValue(authValue) {
6730
6859
  return authValue ? `h${hashForKeyCached(authValue)}` : null;
@@ -7437,8 +7566,16 @@ async function runExpressionMany(many) {
7437
7566
  let queries = [];
7438
7567
  let overrides = undefined;
7439
7568
  for (const { expression, queryArgs, returnType, _overrides } of many) {
7440
- // Collect overrides from first query that has them (all queries share same request)
7441
- if (_overrides && !overrides) {
7569
+ // All batched queries ride ONE request, so they must run under ONE credential.
7570
+ // Reject mixed per-entry _overrides (fail closed) instead of silently running
7571
+ // every query under the FIRST credential — that would evaluate a query meant
7572
+ // for user B as whoever came first (e.g. an admin), a cross-principal
7573
+ // authorization confusion the caller can't detect (#103). Split the batch by
7574
+ // credential to run under different identities.
7575
+ if (_overrides) {
7576
+ if (overrides && overrides !== _overrides) {
7577
+ throw new Error("[bounded-sdk] runExpressionMany: batched queries carry different _overrides; all queries in one batch share a single request and credential. Split the batch so each request uses one credential.");
7578
+ }
7442
7579
  overrides = _overrides;
7443
7580
  }
7444
7581
  queries.push({ queryArgs, expression, returnType });
@@ -7580,11 +7717,11 @@ async function setMany(many, options) {
7580
7717
  transactionResult = await handleEvmTransaction(curTx.evmTransaction, authProvider, options);
7581
7718
  }
7582
7719
  else if (curTx.serializedTransaction) {
7583
- const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DcjCYmuL.js'); });
7720
+ const { handlePreBuiltTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-BOKHbEuy.js'); });
7584
7721
  transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options, expectedWritePaths);
7585
7722
  }
7586
7723
  else {
7587
- const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-DcjCYmuL.js'); });
7724
+ const { handleSolanaTransaction } = await Promise.resolve().then(function () { return require('./solana-operations-BOKHbEuy.js'); });
7588
7725
  transactionResult = await handleSolanaTransaction(curTx, authProvider, options, expectedWritePaths);
7589
7726
  }
7590
7727
  // Sync items after all transactions are confirmed
@@ -8110,17 +8247,25 @@ function isQueryLevelError(error) {
8110
8247
  return (error === null || error === void 0 ? void 0 : error.isQueryError) === true || (error === null || error === void 0 ? void 0 : error.isBoundedDecline) === true || (error === null || error === void 0 ? void 0 : error.decline) != null;
8111
8248
  }
8112
8249
  /**
8113
- * A RECOGNIZED transient transport failure that a retry might clear (HTTP status like
8114
- * 429/5xx, a network/abort error). These back off SILENTLY - surfacing them per-poll
8115
- * would rebuild the 429 noise. Anything else (a bad path, a malformed response - things
8116
- * a retry won't fix) is NOT transient, so it gets surfaced once to onError instead of a
8117
- * silently-dead subscription.
8250
+ * A RECOGNIZED transient transport failure that a retry might clear (HTTP 408/429/5xx, a
8251
+ * network/abort error). These back off SILENTLY - surfacing them per-poll would rebuild the
8252
+ * 429 noise. Anything else (a bad path 404, a permission/config 401/403, a malformed request
8253
+ * 400 - things a retry won't fix) is NOT transient, so it gets surfaced once to onError
8254
+ * instead of a silently-dead subscription.
8118
8255
  */
8119
8256
  function isTransientTransportError(error) {
8120
8257
  if (error == null)
8121
8258
  return true; // unknown - be conservative and keep retrying
8122
- if (typeof error.status === 'number' || typeof error.statusCode === 'number')
8123
- return true;
8259
+ // #060: classify by the actual status, not merely by "has a status". Almost every HTTP
8260
+ // failure carries a status, including the TERMINAL ones (400/401/403/404); the old
8261
+ // `typeof status === 'number'` check lumped them all into "transient" and retried them
8262
+ // forever with no onError - the exact silently-dead subscription this is meant to avoid.
8263
+ const status = typeof error.status === 'number' ? error.status
8264
+ : typeof error.statusCode === 'number' ? error.statusCode
8265
+ : undefined;
8266
+ if (status !== undefined) {
8267
+ return status === 408 || status === 429 || status >= 500;
8268
+ }
8124
8269
  const code = error.code;
8125
8270
  if (code === 'ECONNABORTED' || code === 'ERR_NETWORK' || code === 'ETIMEDOUT' || code === 'ERR_CANCELED')
8126
8271
  return true;
@@ -8675,7 +8820,7 @@ async function buildSetDocumentsTransaction(connection, idl, programProvider, pa
8675
8820
  }
8676
8821
  /** SIWS message creation is async already, so its onchain module can be lazy. */
8677
8822
  async function genSolanaMessage(address, nonce) {
8678
- const implementation = await Promise.resolve().then(function () { return require('./solana-message-DHJ8xSuS.js'); });
8823
+ const implementation = await Promise.resolve().then(function () { return require('./solana-message-DKOPPJwk.js'); });
8679
8824
  return implementation.genSolanaMessage(address, nonce);
8680
8825
  }
8681
8826
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
@@ -9066,6 +9211,13 @@ async function intent(roomPath, intent, opts = {}) {
9066
9211
  * This is diagnostic/ops surface. It reports whether the room facet exists,
9067
9212
  * whether it is currently ticking, why it last stopped, which module etag is
9068
9213
  * loaded, and the current generation used after terminal restarts.
9214
+ *
9215
+ * Auth: status attaches the caller's session token WHEN ONE EXISTS (mirroring
9216
+ * `intent`), but does NOT require it. The server gates the DETAILED body on the
9217
+ * room's read rule — an authenticated caller who can read the room gets full
9218
+ * telemetry; an anonymous / unauthorized caller gets only the slim liveness
9219
+ * shape `{available, started, module}` (#102). The caller's own `opts.headers`
9220
+ * may never smuggle in Authorization: only the derived session token sets it.
9069
9221
  */
9070
9222
  async function status(roomPath, opts = {}) {
9071
9223
  var _a, _b, _c, _d, _e;
@@ -9075,7 +9227,10 @@ async function status(roomPath, opts = {}) {
9075
9227
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
9076
9228
  const config = await getConfig();
9077
9229
  const base = realtimeHttpBase(config.wsApiUrl);
9078
- const headers = Object.assign({ 'X-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
9230
+ const authHeader = await liveAuthHeader(config.isServer, opts._overrides);
9231
+ const extraHeaders = withoutAuthorization(opts.headers);
9232
+ const overrideHeaders = withoutAuthorization((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a.headers);
9233
+ const headers = Object.assign(Object.assign(Object.assign({ 'X-App-Id': config.appId }, (overrideHeaders !== null && overrideHeaders !== void 0 ? overrideHeaders : {})), (extraHeaders !== null && extraHeaders !== void 0 ? extraHeaders : {})), (hasAuthorization(authHeader) ? authHeader : {}));
9079
9234
  const controller = new AbortController();
9080
9235
  const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
9081
9236
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -9250,6 +9405,7 @@ exports.get = get;
9250
9405
  exports.getActiveSessionManager = getActiveSessionManager;
9251
9406
  exports.getCachedData = getCachedData;
9252
9407
  exports.getConfig = getConfig;
9408
+ exports.getConfiguredAppId = getConfiguredAppId;
9253
9409
  exports.getFiles = getFiles;
9254
9410
  exports.getIdToken = getIdToken;
9255
9411
  exports.getMany = getMany;
@@ -9288,4 +9444,4 @@ exports.toLowerEvmAddress = toLowerEvmAddress;
9288
9444
  exports.toMillis = toMillis;
9289
9445
  exports.toSeconds = toSeconds;
9290
9446
  exports.withEffects = withEffects;
9291
- //# sourceMappingURL=index-2e3ksoSR.js.map
9447
+ //# sourceMappingURL=index-B30-XpaX.js.map