@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.
- package/dist/client/config.d.ts +10 -7
- package/dist/client/operations.d.ts +1 -1
- package/dist/client/realtime-store.d.ts +1 -0
- package/dist/index.js +212 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +212 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/client/config.d.ts
CHANGED
|
@@ -3,11 +3,12 @@ export interface ClientConfig {
|
|
|
3
3
|
name: string;
|
|
4
4
|
logoUrl: string;
|
|
5
5
|
apiKey: string;
|
|
6
|
-
/** Auth method. 'email' = Bounded
|
|
7
|
-
* the default for most apps.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
24
|
-
* calls {humanAuthApiUrl}/email + /verify
|
|
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
|
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
|
|
44
|
-
// for
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
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:
|
|
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
|
-
|
|
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('
|
|
5900
|
+
wsUrl.searchParams.append('routePath', connection.routePath);
|
|
5870
5901
|
}
|
|
5871
|
-
//
|
|
5872
|
-
// token from the wallet's own session (self-refreshing); all others
|
|
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
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
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
|
-
|
|
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 }, (
|
|
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);
|