@bounded-sh/core 0.0.7 → 0.0.8

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,11 +3,12 @@ export interface ClientConfig {
3
3
  name: string;
4
4
  logoUrl: string;
5
5
  apiKey: string;
6
- /** Auth method. 'email' = Bounded Better Auth human login (email OTP, inline) —
7
- * the default for most apps. 'phantom' = connect a Solana wallet (Phantom), the
8
- * recommended wallet option (yields a real @user.address). 'guest' = zero-config
9
- * anonymous (device keypair). All coexist; an app can offer email login AND Phantom
10
- * AND `signInAnonymously()` side by side. */
6
+ /** Auth method. 'email' = Bounded Auth human login (email OTP, inline) —
7
+ * the default for most apps. 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' = zero-config
11
+ * anonymous (device keypair). All coexist. */
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;
@@ -20,8 +21,10 @@ export interface ClientConfig {
20
21
  appId: string;
21
22
  /** Wallet/SIWS issuer (wallet + guest providers sign challenges against this). */
22
23
  authApiUrl: string;
23
- /** Human-login issuer (Bounded Better Auth — email OTP). The 'email' provider
24
- * calls {humanAuthApiUrl}/email + /verify. Defaults per network. */
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. */
25
28
  humanAuthApiUrl?: string;
26
29
  /**
27
30
  * Selects a Bounded backend preset. When set, the endpoint defaults
@@ -212,7 +212,7 @@ export type GetManyResult = {
212
212
  path: string;
213
213
  data: any | null;
214
214
  error?: {
215
- code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'INVALID_PATH';
215
+ code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'INVALID_PATH' | 'REQUEST_FAILED';
216
216
  message: string;
217
217
  };
218
218
  };
@@ -32,6 +32,7 @@ export declare class RealtimeStore {
32
32
  private idbDirtyKeys;
33
33
  private closed;
34
34
  private authToken;
35
+ private authenticating;
35
36
  init(): Promise<void>;
36
37
  private isServer;
37
38
  private tokenRefreshTimer;
package/dist/index.js CHANGED
@@ -40,10 +40,13 @@ let clientConfig = {
40
40
  humanAuthApiUrl: 'https://auth.bounded.sh',
41
41
  functionsUrl: 'https://functions.bounded.sh',
42
42
  appId: '',
43
- // 'email' = Bounded Better Auth human login (inline OTP) — the out-of-box default
44
- // for most apps. For wallet login use authMethod:'phantom' (Solana / Phantom, the
45
- // recommended wallet option), or signInAnonymously() for zero-friction 'guest'
46
- // accounts all coexist. ('wallet' is an unimplemented stub; don't use.)
43
+ // 'email' = Bounded Auth human login (inline email OTP) — the out-of-box default
44
+ // for normal apps. Hosted OAuth/social uses loginWithRedirect/loginWithPopup.
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.)
47
50
  authMethod: 'email',
48
51
  chain: '',
49
52
  rpcUrl: '',
@@ -4243,7 +4246,7 @@ async function makeApiRequest(method, urlPath, data, _overrides) {
4243
4246
  }
4244
4247
  }
4245
4248
 
4246
- var __rest = (undefined && undefined.__rest) || function (s, e) {
4249
+ var __rest$1 = (undefined && undefined.__rest) || function (s, e) {
4247
4250
  var t = {};
4248
4251
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4249
4252
  t[p] = s[p];
@@ -4718,7 +4721,7 @@ async function search(path, query, opts = {}) {
4718
4721
  normalizedPath = normalizedPath.slice(0, -1);
4719
4722
  }
4720
4723
  if (!normalizedPath || normalizedPath.length === 0) {
4721
- return new Error("Invalid path provided.");
4724
+ throw new Error("Invalid path provided.");
4722
4725
  }
4723
4726
  if (typeof query !== "string" || query.trim().length === 0) {
4724
4727
  throw new Error("search query must be a non-empty string");
@@ -4745,7 +4748,7 @@ async function get(path, opts = {}) {
4745
4748
  normalizedPath = normalizedPath.slice(0, -1);
4746
4749
  }
4747
4750
  if (!normalizedPath || normalizedPath.length === 0) {
4748
- return new Error("Invalid path provided.");
4751
+ throw new Error("Invalid path provided.");
4749
4752
  }
4750
4753
  // Create cache key combining path, prompt, filter, sort, includeSubPaths,
4751
4754
  // shape, limit, cursor — and (H1) the caller's appId + principal fingerprint,
@@ -4873,6 +4876,23 @@ function cleanupExpiredCache() {
4873
4876
  });
4874
4877
  lastCacheCleanup = now;
4875
4878
  }
4879
+ function classifyGetManyBatchError(error) {
4880
+ var _a, _b, _c;
4881
+ const err = error;
4882
+ const status = (_b = (_a = err === null || err === void 0 ? void 0 : err.status) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.statusCode) !== null && _b !== void 0 ? _b : (_c = err === null || err === void 0 ? void 0 : err.response) === null || _c === void 0 ? void 0 : _c.status;
4883
+ const message = error instanceof Error
4884
+ ? error.message
4885
+ : typeof (err === null || err === void 0 ? void 0 : err.message) === 'string'
4886
+ ? err.message
4887
+ : 'Unknown error';
4888
+ if (status === 401 || status === 403) {
4889
+ return { code: 'UNAUTHORIZED', message };
4890
+ }
4891
+ if (status === 400) {
4892
+ return { code: 'INVALID_PATH', message };
4893
+ }
4894
+ return { code: 'REQUEST_FAILED', message };
4895
+ }
4876
4896
  async function getMany(paths, opts = {}) {
4877
4897
  var _a, _b, _c, _d, _e;
4878
4898
  if (paths.length === 0) {
@@ -4917,6 +4937,11 @@ async function getMany(paths, opts = {}) {
4917
4937
  if (uncachedPaths.length > 0) {
4918
4938
  try {
4919
4939
  const response = await makeApiRequest('POST', 'items/batch', { paths: uncachedPaths }, opts._overrides);
4940
+ if (response.status === 404 && response.data == null) {
4941
+ const endpointError = new Error('Batch read endpoint returned 404');
4942
+ endpointError.status = 404;
4943
+ throw endpointError;
4944
+ }
4920
4945
  // makeApiRequest returns `{ data: <httpBody> }`, and the worker's items/batch
4921
4946
  // httpBody is `{ data: { results: [...] }, status }` — so the results are
4922
4947
  // double-nested at response.data.data.results. (Reading response.data.results
@@ -4959,11 +4984,12 @@ async function getMany(paths, opts = {}) {
4959
4984
  }
4960
4985
  }
4961
4986
  catch (error) {
4987
+ const batchError = classifyGetManyBatchError(error);
4962
4988
  for (const originalIndex of uncachedIndices) {
4963
4989
  results[originalIndex] = {
4964
4990
  path: normalizedPaths[originalIndex],
4965
4991
  data: null,
4966
- error: { code: 'NOT_FOUND', message: error instanceof Error ? error.message : 'Unknown error' }
4992
+ error: batchError,
4967
4993
  };
4968
4994
  }
4969
4995
  }
@@ -5125,26 +5151,23 @@ async function setMany(many, options) {
5125
5151
  return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.signature, signedTransaction: transactionResult.signedTransaction });
5126
5152
  }
5127
5153
  // Handle Solana on-chain transaction flow
5128
- let lastTxSignature = undefined;
5129
- let signedTransaction = undefined;
5130
- for (let i = 0; i < transactions.length; i++) {
5131
- const curTx = transactions[i];
5132
- let transactionResult;
5133
- if (curTx.serializedTransaction) {
5134
- transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
5135
- }
5136
- else {
5137
- transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
5138
- }
5139
- lastTxSignature = transactionResult.transactionSignature;
5140
- signedTransaction = transactionResult.signedTransaction;
5154
+ if (!Array.isArray(transactions) || transactions.length !== 1) {
5155
+ throw new Error(`Expected exactly one on-chain transaction, received ${Array.isArray(transactions) ? transactions.length : 0}`);
5156
+ }
5157
+ const curTx = transactions[0];
5158
+ let transactionResult;
5159
+ if (curTx.serializedTransaction) {
5160
+ transactionResult = await handlePreBuiltTransaction(curTx, authProvider, options);
5161
+ }
5162
+ else {
5163
+ transactionResult = await handleSolanaTransaction(curTx, authProvider, options);
5141
5164
  }
5142
5165
  // Sync items after all transactions are confirmed
5143
5166
  // Wait for 1.5 seconds to ensure all transactions are confirmed
5144
5167
  await new Promise(resolve => setTimeout(resolve, 1500));
5145
5168
  await syncItems(many.map(m => m.path), options);
5146
5169
  // TODO: Should we wait here or do the optimistic subscription updates like below?
5147
- return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: lastTxSignature, signedTransaction: signedTransaction });
5170
+ return Object.assign(Object.assign({}, documents.map(d => d.document)), { transactionId: transactionResult.transactionSignature, signedTransaction: transactionResult.signedTransaction });
5148
5171
  }
5149
5172
  else if (setResponse.status === 200) {
5150
5173
  // This means that the document was set successfully.
@@ -5157,7 +5180,7 @@ async function setMany(many, options) {
5157
5180
  else if (setResponse.data &&
5158
5181
  typeof setResponse.data === 'object' &&
5159
5182
  setResponse.data.success === true) {
5160
- const _k = setResponse.data, { success: _success } = _k, rest = __rest(_k, ["success"]);
5183
+ const _k = setResponse.data, { success: _success } = _k, rest = __rest$1(_k, ["success"]);
5161
5184
  return Object.assign(Object.assign(Object.assign({}, documents.map(d => d.document)), rest), { transactionId: null });
5162
5185
  }
5163
5186
  else {
@@ -5393,7 +5416,7 @@ async function getFiles(path, options) {
5393
5416
  try {
5394
5417
  const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
5395
5418
  if (!normalizedPath || normalizedPath.length === 0) {
5396
- return new Error("Invalid path provided.");
5419
+ throw new Error("Invalid path provided.");
5397
5420
  }
5398
5421
  const apiPath = `storage?path=${normalizedPath}`;
5399
5422
  const response = await makeApiRequest('GET', apiPath, null, options === null || options === void 0 ? void 0 : options._overrides);
@@ -5813,6 +5836,12 @@ function roomKeyFromRoutePath(routePath) {
5813
5836
  return null;
5814
5837
  return `${segs[0]}/${segs[1]}`;
5815
5838
  }
5839
+ function replaySubscriptions(connection) {
5840
+ for (const sub of connection.subscriptions.values()) {
5841
+ sub.lastData = undefined;
5842
+ sendSubscribe(connection, sub);
5843
+ }
5844
+ }
5816
5845
  async function getOrCreateConnection(appId, isServer, routePath, authTokenProvider, principalKey) {
5817
5846
  attachBrowserReconnectHooksOnce();
5818
5847
  // A per-room subscription gets its OWN connection routed to the room DO; all
@@ -5838,10 +5867,12 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5838
5867
  pendingRequests: new Map(),
5839
5868
  isConnecting: false,
5840
5869
  isConnected: false,
5870
+ isAuthenticating: false,
5841
5871
  appId,
5842
5872
  key: connKey,
5843
5873
  routePath: roomKey ? routePath : undefined,
5844
5874
  authTokenProvider,
5875
+ pendingAuthToken: null,
5845
5876
  tokenRefreshTimer: null,
5846
5877
  consecutiveAuthFailures: 0,
5847
5878
  };
@@ -5866,16 +5897,17 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5866
5897
  // Per-room connection: carry the room path so the worker routes this WS
5867
5898
  // to the room DO (appId#room#roomId) where the live view fan-out lives.
5868
5899
  if (connection.routePath) {
5869
- wsUrl.searchParams.append('path', connection.routePath);
5900
+ wsUrl.searchParams.append('routePath', connection.routePath);
5870
5901
  }
5871
- // Add auth token if available. A wallet-scoped connection resolves its
5872
- // token from the wallet's own session (self-refreshing); all others use
5873
- // the ambient env/web session.
5902
+ // Resolve auth token if available. A wallet-scoped connection resolves
5903
+ // its token from the wallet's own session (self-refreshing); all others
5904
+ // use the ambient env/web session. The token is sent as the first WS
5905
+ // frame after open, never as a URL query parameter.
5874
5906
  const authToken = connection.authTokenProvider
5875
5907
  ? await connection.authTokenProvider().catch(() => null)
5876
5908
  : await getFreshAuthToken(isServer);
5909
+ connection.pendingAuthToken = authToken || null;
5877
5910
  if (authToken) {
5878
- wsUrl.searchParams.append('authorization', authToken);
5879
5911
  // Successful token acquisition — reset failure counter
5880
5912
  connection.consecutiveAuthFailures = 0;
5881
5913
  }
@@ -5901,6 +5933,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5901
5933
  connection.ws = ws;
5902
5934
  // Handle connection open
5903
5935
  ws.addEventListener('open', () => {
5936
+ var _a, _b;
5904
5937
  connection.isConnecting = false;
5905
5938
  connection.isConnected = true;
5906
5939
  // NOTE: Do NOT reset consecutiveAuthFailures here. It is reset when a
@@ -5915,10 +5948,26 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5915
5948
  // urlProvider skips straight to unauthenticated connection.
5916
5949
  // Schedule periodic token freshness checks
5917
5950
  scheduleTokenRefresh(connection, isServer);
5918
- // Re-subscribe to all existing subscriptions after reconnect
5919
- for (const sub of connection.subscriptions.values()) {
5920
- sub.lastData = undefined;
5921
- sendSubscribe(connection, sub);
5951
+ if (connection.pendingAuthToken) {
5952
+ connection.isAuthenticating = true;
5953
+ try {
5954
+ (_a = connection.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({ type: 'auth', token: connection.pendingAuthToken }));
5955
+ }
5956
+ catch (error) {
5957
+ connection.isAuthenticating = false;
5958
+ connection.isConnected = false;
5959
+ console.error('[WS v2] Error sending auth message:', error);
5960
+ try {
5961
+ (_b = connection.ws) === null || _b === void 0 ? void 0 : _b.close(1008, 'Authentication send failed');
5962
+ }
5963
+ catch (_c) {
5964
+ // Already closed.
5965
+ }
5966
+ }
5967
+ }
5968
+ else {
5969
+ connection.isAuthenticating = false;
5970
+ replaySubscriptions(connection);
5922
5971
  }
5923
5972
  });
5924
5973
  // Handle incoming messages
@@ -5942,6 +5991,7 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5942
5991
  // Handle close
5943
5992
  ws.addEventListener('close', () => {
5944
5993
  connection.isConnected = false;
5994
+ connection.isAuthenticating = false;
5945
5995
  if (connection.tokenRefreshTimer) {
5946
5996
  clearInterval(connection.tokenRefreshTimer);
5947
5997
  connection.tokenRefreshTimer = null;
@@ -5958,6 +6008,11 @@ async function getOrCreateConnection(appId, isServer, routePath, authTokenProvid
5958
6008
  function handleServerMessage(connection, message) {
5959
6009
  var _a, _b;
5960
6010
  switch (message.type) {
6011
+ case 'authenticated': {
6012
+ connection.isAuthenticating = false;
6013
+ replaySubscriptions(connection);
6014
+ break;
6015
+ }
5961
6016
  case 'subscribed': {
5962
6017
  const subscription = connection.subscriptions.get(message.subscriptionId);
5963
6018
  if (subscription) {
@@ -6258,7 +6313,7 @@ async function subscribeV2(path, subscriptionOptions, roomRoutePath) {
6258
6313
  };
6259
6314
  connection.subscriptions.set(subscriptionId, subscription);
6260
6315
  // Send subscribe message if connected
6261
- if (connection.isConnected) {
6316
+ if (connection.isConnected && !connection.isAuthenticating) {
6262
6317
  // Create a promise to wait for subscription confirmation
6263
6318
  const subscriptionPromise = new Promise((resolve, reject) => {
6264
6319
  connection.pendingSubscriptions.set(subscriptionId, { resolve, reject });
@@ -6296,7 +6351,7 @@ async function removeCallbackFromSubscription(connection, subscriptionId, callba
6296
6351
  }
6297
6352
  // No more callbacks, unsubscribe from server
6298
6353
  connection.subscriptions.delete(subscriptionId);
6299
- if (connection.isConnected) {
6354
+ if (connection.isConnected && !connection.isAuthenticating) {
6300
6355
  // Create a promise to wait for unsubscription confirmation
6301
6356
  const unsubscribePromise = new Promise((resolve, reject) => {
6302
6357
  connection.pendingUnsubscriptions.set(subscriptionId, { resolve, reject });
@@ -6472,12 +6527,59 @@ function generateRequestId() {
6472
6527
  */
6473
6528
  function hasActiveConnection() {
6474
6529
  for (const connection of connections.values()) {
6475
- if (connection.ws && connection.isConnected) {
6530
+ if (connection.ws && connection.isConnected && !connection.isAuthenticating) {
6476
6531
  return true;
6477
6532
  }
6478
6533
  }
6479
6534
  return false;
6480
6535
  }
6536
+ async function waitForConnectionAuthenticated(connection) {
6537
+ if (!connection.isAuthenticating || !connection.ws)
6538
+ return;
6539
+ const ws = connection.ws;
6540
+ await new Promise((resolve, reject) => {
6541
+ let timeout;
6542
+ let cleanup = () => { };
6543
+ const onMessage = (event) => {
6544
+ try {
6545
+ const message = JSON.parse(event.data);
6546
+ if ((message === null || message === void 0 ? void 0 : message.type) === 'authenticated') {
6547
+ cleanup();
6548
+ resolve();
6549
+ }
6550
+ }
6551
+ catch (_a) {
6552
+ // Other frames are handled by the main listener.
6553
+ }
6554
+ };
6555
+ const onClose = () => {
6556
+ cleanup();
6557
+ reject(new Error('WebSocket disconnected during authentication'));
6558
+ };
6559
+ const onError = () => {
6560
+ cleanup();
6561
+ reject(new Error('WebSocket authentication failed'));
6562
+ };
6563
+ cleanup = () => {
6564
+ clearTimeout(timeout);
6565
+ ws.removeEventListener('message', onMessage);
6566
+ ws.removeEventListener('close', onClose);
6567
+ ws.removeEventListener('error', onError);
6568
+ };
6569
+ timeout = setTimeout(() => {
6570
+ cleanup();
6571
+ reject(new Error('WebSocket authentication timeout'));
6572
+ }, 10000);
6573
+ if (!connection.isAuthenticating) {
6574
+ cleanup();
6575
+ resolve();
6576
+ return;
6577
+ }
6578
+ ws.addEventListener('message', onMessage);
6579
+ ws.addEventListener('close', onClose);
6580
+ ws.addEventListener('error', onError);
6581
+ });
6582
+ }
6481
6583
  async function sendRequest(msgBuilder) {
6482
6584
  const config = await getConfig();
6483
6585
  const appId = config.appId;
@@ -6503,6 +6605,7 @@ async function sendRequest(msgBuilder) {
6503
6605
  if (!connection.ws || !connection.isConnected) {
6504
6606
  throw new Error('WebSocket connection not available');
6505
6607
  }
6608
+ await waitForConnectionAuthenticated(connection);
6506
6609
  const requestId = generateRequestId();
6507
6610
  const message = msgBuilder(requestId);
6508
6611
  return new Promise((resolve, reject) => {
@@ -6539,7 +6642,7 @@ function wsIntent(appId, roomRoutePath, intent) {
6539
6642
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6540
6643
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6541
6644
  const connection = connections.get(connKey);
6542
- if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
6645
+ if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6543
6646
  return false;
6544
6647
  }
6545
6648
  try {
@@ -6562,7 +6665,7 @@ function wsIntentReliable(appId, roomRoutePath, intent) {
6562
6665
  const roomKey = roomKeyFromRoutePath(roomRoutePath);
6563
6666
  const connKey = roomKey ? `${appId}#room#${roomKey}` : appId;
6564
6667
  const connection = connections.get(connKey);
6565
- if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN) {
6668
+ if (!connection || !connection.ws || connection.ws.readyState !== WS_READY_STATE_OPEN || connection.isAuthenticating) {
6566
6669
  return undefined;
6567
6670
  }
6568
6671
  const requestId = generateRequestId();
@@ -6889,6 +6992,7 @@ class RealtimeStore {
6889
6992
  this.idbDirtyKeys = new Set();
6890
6993
  this.closed = false;
6891
6994
  this.authToken = null;
6995
+ this.authenticating = false;
6892
6996
  this.isServer = false;
6893
6997
  this.tokenRefreshTimer = null;
6894
6998
  // -----------------------------------------------------------------------
@@ -6936,7 +7040,7 @@ class RealtimeStore {
6936
7040
  this.initPromise = this.init();
6937
7041
  await this.initPromise;
6938
7042
  }
6939
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN)
7043
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN && !this.authenticating)
6940
7044
  return;
6941
7045
  if (this.connectPromise)
6942
7046
  return this.connectPromise;
@@ -6951,21 +7055,52 @@ class RealtimeStore {
6951
7055
  }
6952
7056
  const params = new URLSearchParams();
6953
7057
  params.set('apiKey', this.appId);
6954
- if (this.authToken)
6955
- params.set('authorization', this.authToken);
6956
- // Note: token in URL is required until DO server supports subprotocol auth.
6957
- // WSS encrypts the full URL including query params on the wire.
6958
7058
  const url = `${this.wsUrl}?${params.toString()}`;
6959
7059
  const ws = new WebSocket(url);
6960
7060
  this.ws = ws;
6961
- const onOpen = () => {
7061
+ let authTimer = null;
7062
+ const finishConnected = () => {
7063
+ if (authTimer) {
7064
+ clearTimeout(authTimer);
7065
+ authTimer = null;
7066
+ }
7067
+ this.authenticating = false;
6962
7068
  ws.removeEventListener('error', onError);
6963
7069
  this.reconnectDelay = 1000;
6964
7070
  this.connectPromise = null;
6965
7071
  this.resubscribeAll();
6966
7072
  resolve();
6967
7073
  };
7074
+ const onOpen = () => {
7075
+ if (!this.authToken) {
7076
+ finishConnected();
7077
+ return;
7078
+ }
7079
+ this.authenticating = true;
7080
+ authTimer = setTimeout(() => {
7081
+ this.authenticating = false;
7082
+ this.connectPromise = null;
7083
+ try {
7084
+ ws.close(1008, 'Authentication timeout');
7085
+ }
7086
+ catch ( /* ignore */_a) { /* ignore */ }
7087
+ reject(new Error('WebSocket authentication timeout'));
7088
+ }, 10000);
7089
+ try {
7090
+ ws.send(JSON.stringify({ type: 'auth', token: this.authToken }));
7091
+ }
7092
+ catch (e) {
7093
+ if (authTimer)
7094
+ clearTimeout(authTimer);
7095
+ this.authenticating = false;
7096
+ this.connectPromise = null;
7097
+ reject(e);
7098
+ }
7099
+ };
6968
7100
  const onError = (e) => {
7101
+ if (authTimer)
7102
+ clearTimeout(authTimer);
7103
+ this.authenticating = false;
6969
7104
  ws.removeEventListener('open', onOpen);
6970
7105
  this.connectPromise = null;
6971
7106
  reject(new Error('WebSocket connection failed'));
@@ -6973,9 +7108,22 @@ class RealtimeStore {
6973
7108
  ws.addEventListener('open', onOpen, { once: true });
6974
7109
  ws.addEventListener('error', onError, { once: true });
6975
7110
  ws.addEventListener('message', (event) => {
7111
+ if (this.authenticating) {
7112
+ try {
7113
+ const msg = JSON.parse(typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data));
7114
+ if ((msg === null || msg === void 0 ? void 0 : msg.type) === 'authenticated') {
7115
+ finishConnected();
7116
+ return;
7117
+ }
7118
+ }
7119
+ catch ( /* fall through to normal handling */_a) { /* fall through to normal handling */ }
7120
+ }
6976
7121
  this.handleMessage(event.data);
6977
7122
  });
6978
7123
  ws.addEventListener('close', () => {
7124
+ if (authTimer)
7125
+ clearTimeout(authTimer);
7126
+ this.authenticating = false;
6979
7127
  this.ws = null;
6980
7128
  this.connectPromise = null;
6981
7129
  this.rejectAllPending('WebSocket closed');
@@ -7028,6 +7176,8 @@ class RealtimeStore {
7028
7176
  break;
7029
7177
  case 'pong':
7030
7178
  break;
7179
+ case 'authenticated':
7180
+ break;
7031
7181
  // v1 compat: handle legacy message types during transition
7032
7182
  case 'subscribed':
7033
7183
  this.handleSnapshot(Object.assign(Object.assign({}, msg), { type: 'snapshot', docs: msg.data }));
@@ -7629,6 +7779,17 @@ function resetRealtimeStore() {
7629
7779
  // rule, and dispatches to the function — returning its JSON, or throwing on
7630
7780
  // 403 / error.
7631
7781
  // ---------------------------------------------------------------------------
7782
+ var __rest = (undefined && undefined.__rest) || function (s, e) {
7783
+ var t = {};
7784
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
7785
+ t[p] = s[p];
7786
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7787
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7788
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
7789
+ t[p[i]] = s[p[i]];
7790
+ }
7791
+ return t;
7792
+ };
7632
7793
  /** Prod functions dispatcher; overridable via init({ functionsUrl }) or network preset. */
7633
7794
  const DEFAULT_FUNCTIONS_URL = 'https://functions.bounded.sh';
7634
7795
  class FunctionInvokeError extends Error {
@@ -7639,6 +7800,12 @@ class FunctionInvokeError extends Error {
7639
7800
  this.name = 'FunctionInvokeError';
7640
7801
  }
7641
7802
  }
7803
+ function stripAuthHeaders(headers) {
7804
+ if (!headers)
7805
+ return undefined;
7806
+ const { Authorization, authorization } = headers, rest = __rest(headers, ["Authorization", "authorization"]);
7807
+ return Object.keys(rest).length > 0 ? rest : undefined;
7808
+ }
7642
7809
  /**
7643
7810
  * Invoke a deployed Bounded Function by name. Returns the function's JSON.
7644
7811
  *
@@ -7661,7 +7828,7 @@ async function invoke(name, args = {}, opts = {}) {
7661
7828
  const authHeader = ((_a = opts._overrides) === null || _a === void 0 ? void 0 : _a._getAuthHeaders)
7662
7829
  ? await opts._overrides._getAuthHeaders()
7663
7830
  : await createAuthHeader(config.isServer);
7664
- const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, (authHeader !== null && authHeader !== void 0 ? authHeader : {})), ((_b = opts.headers) !== null && _b !== void 0 ? _b : {}));
7831
+ const headers = Object.assign(Object.assign({ 'Content-Type': 'application/json', 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_b = stripAuthHeaders(opts.headers)) !== null && _b !== void 0 ? _b : {})), (authHeader !== null && authHeader !== void 0 ? authHeader : {}));
7665
7832
  const controller = new AbortController();
7666
7833
  const timeoutMs = (_c = opts.timeoutMs) !== null && _c !== void 0 ? _c : 60000;
7667
7834
  const timer = setTimeout(() => controller.abort(), timeoutMs);