@rebasepro/server-mongo 0.21.0 → 0.21.1
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/README.md +9 -9
- package/dist/index.es.js +126 -15
- package/dist/index.es.js.map +1 -1
- package/dist/services/MongoDriver.d.ts +11 -10
- package/dist/services/MongoRealtimeService.d.ts +34 -2
- package/dist/{websocket-B3LiQfFN.js → websocket-C3iT6srl.js} +100 -38
- package/dist/websocket-C3iT6srl.js.map +1 -0
- package/package.json +5 -5
- package/dist/websocket-B3LiQfFN.js.map +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Db } from "mongodb";
|
|
8
8
|
import { DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, SaveProps, CollectionRegistryInterface, User, RebaseServerClient, RebaseSdkData } from "@rebasepro/types";
|
|
9
9
|
import { MongoDataService } from "../db/MongoDataService.js";
|
|
10
|
-
import { MongoRealtimeService } from "./MongoRealtimeService.js";
|
|
10
|
+
import { MongoRealtimeService, type SubscriptionAuthContext } from "./MongoRealtimeService.js";
|
|
11
11
|
import { MongoHistoryService } from "./MongoHistoryService.js";
|
|
12
12
|
/**
|
|
13
13
|
* MongoDB DataDriver Delegate
|
|
@@ -60,10 +60,7 @@ export declare class MongoDriver implements DataDriver {
|
|
|
60
60
|
* what every re-fetch reads — the wrapper used to stamp the field on the
|
|
61
61
|
* `Subscription` object instead, and nothing has ever read that one.
|
|
62
62
|
*/
|
|
63
|
-
listenCollection<M extends Record<string, any>>({ onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>, authContext?:
|
|
64
|
-
uid: string;
|
|
65
|
-
roles: string[];
|
|
66
|
-
}): () => void;
|
|
63
|
+
listenCollection<M extends Record<string, any>>({ onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>, authContext?: SubscriptionAuthContext): () => void;
|
|
67
64
|
/**
|
|
68
65
|
* Fetch a single row
|
|
69
66
|
*/
|
|
@@ -71,10 +68,7 @@ export declare class MongoDriver implements DataDriver {
|
|
|
71
68
|
/**
|
|
72
69
|
* Listen to row changes
|
|
73
70
|
*/
|
|
74
|
-
listenOne<M extends Record<string, any>>({ path, id, collection, onUpdate, onError }: ListenOneProps<M>, authContext?:
|
|
75
|
-
uid: string;
|
|
76
|
-
roles: string[];
|
|
77
|
-
}): () => void;
|
|
71
|
+
listenOne<M extends Record<string, any>>({ path, id, collection, onUpdate, onError }: ListenOneProps<M>, authContext?: SubscriptionAuthContext): () => void;
|
|
78
72
|
/**
|
|
79
73
|
* Save an row (create or update)
|
|
80
74
|
*/
|
|
@@ -126,7 +120,14 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
|
|
|
126
120
|
currentTime(): Date;
|
|
127
121
|
fetchCollection<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
|
|
128
122
|
listenCollection<M extends Record<string, any>>(props: ListenCollectionProps<M>): () => void;
|
|
129
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The acting user, in the shape the realtime subscriptions carry.
|
|
125
|
+
*
|
|
126
|
+
* `isAnonymous` included: a guest has a real uid, and without the flag
|
|
127
|
+
* every fetch a listener's subscription makes reads as an account, so
|
|
128
|
+
* `policy.registered()` handed a guest's listener what it withholds from a
|
|
129
|
+
* guest's `fetchCollection` on this same driver.
|
|
130
|
+
*/
|
|
130
131
|
private authContext;
|
|
131
132
|
/**
|
|
132
133
|
* Evaluate the collection's rules for one row, fail-closed.
|
|
@@ -12,6 +12,13 @@ import type { MongoDriver } from "./MongoDriver.js";
|
|
|
12
12
|
export interface SubscriptionAuthContext {
|
|
13
13
|
uid: string;
|
|
14
14
|
roles: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether the subscriber is a guest — anonymous sign-in rather than an
|
|
17
|
+
* account. A guest has a real uid, so without this every fetch the
|
|
18
|
+
* subscription makes reads as an account, and `policy.registered()` lets
|
|
19
|
+
* it through. Absent reads as "not a guest".
|
|
20
|
+
*/
|
|
21
|
+
isAnonymous?: boolean;
|
|
15
22
|
}
|
|
16
23
|
interface Subscription {
|
|
17
24
|
type: "collection" | "single";
|
|
@@ -26,6 +33,13 @@ interface Subscription {
|
|
|
26
33
|
};
|
|
27
34
|
changeStream?: ChangeStream;
|
|
28
35
|
callback?: (data: any) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Told when a fetch for this subscription fails — the initial one or any
|
|
38
|
+
* re-fetch. Without it the failure was logged and nothing else happened:
|
|
39
|
+
* the subscriber had been sent neither rows nor an error, so its view
|
|
40
|
+
* stayed loading and its `onError` never fired.
|
|
41
|
+
*/
|
|
42
|
+
onError?: (error: unknown) => void;
|
|
29
43
|
/**
|
|
30
44
|
* How many deliveries have been started for this subscription, and the
|
|
31
45
|
* highest that has already reached the callback.
|
|
@@ -78,14 +92,31 @@ export declare class MongoRealtimeService implements RealtimeProvider {
|
|
|
78
92
|
* fetch behind it is the newest thing known about the row, so it must also
|
|
79
93
|
* be the thing that closes the door on an older fetch still in flight —
|
|
80
94
|
* otherwise the deleted row reappears a moment after it vanished.
|
|
95
|
+
*
|
|
96
|
+
* `mayReportFailure()` is the same check for reporting this delivery's
|
|
97
|
+
* failure, except that it also answers yes to the delivery that already
|
|
98
|
+
* claimed the slot. That is the send itself failing (the socket closure's
|
|
99
|
+
* `JSON.stringify` on a row that will not serialise) after the check passed
|
|
100
|
+
* and before anything reached the subscriber. Same rule as the Postgres
|
|
101
|
+
* service's `beginDelivery`.
|
|
81
102
|
*/
|
|
82
103
|
private beginDelivery;
|
|
104
|
+
/**
|
|
105
|
+
* Tell the subscriber a fetch failed, through the slot the rows would have
|
|
106
|
+
* used.
|
|
107
|
+
*
|
|
108
|
+
* The slot matters as much for an error as for rows. Without it, a fetch
|
|
109
|
+
* that a newer delivery has overtaken would mark a view showing current
|
|
110
|
+
* data as failed, and one whose subscription was cancelled or replaced
|
|
111
|
+
* under the same id would fail a different subscription's view.
|
|
112
|
+
*/
|
|
113
|
+
private reportFetchFailure;
|
|
83
114
|
/**
|
|
84
115
|
* Subscribe to collection changes
|
|
85
116
|
*/
|
|
86
117
|
subscribeToCollection(subscriptionId: string, config: CollectionSubscriptionConfig & {
|
|
87
118
|
authContext?: SubscriptionAuthContext;
|
|
88
|
-
}, callback?: (rows: Record<string, unknown>[]) => void): void;
|
|
119
|
+
}, callback?: (rows: Record<string, unknown>[]) => void, onError?: (error: unknown) => void): void;
|
|
89
120
|
/**
|
|
90
121
|
* Fetch collection and notify callback
|
|
91
122
|
*/
|
|
@@ -102,7 +133,7 @@ export declare class MongoRealtimeService implements RealtimeProvider {
|
|
|
102
133
|
*/
|
|
103
134
|
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig & {
|
|
104
135
|
authContext?: SubscriptionAuthContext;
|
|
105
|
-
}, callback?: (row: Record<string, unknown> | null) => void): void;
|
|
136
|
+
}, callback?: (row: Record<string, unknown> | null) => void, onError?: (error: unknown) => void): void;
|
|
106
137
|
/**
|
|
107
138
|
* Fetch row and notify callback
|
|
108
139
|
*/
|
|
@@ -142,6 +173,7 @@ export declare class MongoRealtimeService implements RealtimeProvider {
|
|
|
142
173
|
}, _authContext?: {
|
|
143
174
|
uid: string;
|
|
144
175
|
roles: unknown[];
|
|
176
|
+
isAnonymous?: boolean;
|
|
145
177
|
}): Promise<void>;
|
|
146
178
|
}
|
|
147
179
|
export {};
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { isDocumentAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
2
|
-
import { ApiError, assertWriteRequestValid, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
|
|
1
|
+
import { ANONYMOUS_USER_ID, isDocumentAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
2
|
+
import { ApiError, assertWriteRequestValid, declaredErrorAnswer, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
4
|
//#region src/websocket.ts
|
|
5
|
-
function isDriverWithAuth(driver) {
|
|
6
|
-
return "withAuth" in driver && typeof driver.withAuth === "function";
|
|
7
|
-
}
|
|
8
5
|
var WS_RATE_LIMIT = 2e3;
|
|
9
6
|
var WS_RATE_WINDOW_MS = 6e4;
|
|
10
7
|
var ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -18,6 +15,35 @@ var ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
|
18
15
|
"DELETE_BRANCH",
|
|
19
16
|
"LIST_BRANCHES"
|
|
20
17
|
]);
|
|
18
|
+
/**
|
|
19
|
+
* Who a socket reads and writes as, for its request frames and its
|
|
20
|
+
* subscriptions alike — one answer, so the two cannot disagree.
|
|
21
|
+
*
|
|
22
|
+
* A socket with no session (only possible with `requireAuth: false`) is the
|
|
23
|
+
* anonymous user, exactly as REST scopes the same caller. It used to get the
|
|
24
|
+
* base driver instead, which on this engine applies no security rules at all:
|
|
25
|
+
* such a socket read every row and wrote wherever it liked.
|
|
26
|
+
*/
|
|
27
|
+
function sessionUser(session) {
|
|
28
|
+
if (!session?.user) return {
|
|
29
|
+
uid: ANONYMOUS_USER_ID,
|
|
30
|
+
displayName: null,
|
|
31
|
+
email: null,
|
|
32
|
+
photoURL: null,
|
|
33
|
+
providerId: "websocket",
|
|
34
|
+
isAnonymous: false,
|
|
35
|
+
roles: ["anon"]
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
uid: session.user.uid,
|
|
39
|
+
email: session.user.email ?? "",
|
|
40
|
+
displayName: session.user.displayName ?? "",
|
|
41
|
+
photoURL: session.user.photoURL ?? "",
|
|
42
|
+
providerId: "jwt",
|
|
43
|
+
isAnonymous: session.user.isAnonymous,
|
|
44
|
+
roles: session.user.roles ?? []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
21
47
|
function isAdminSession(session) {
|
|
22
48
|
if (!session?.user) return false;
|
|
23
49
|
if (session.user.isAdmin) return true;
|
|
@@ -79,7 +105,8 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
79
105
|
uid: adapterUser.uid,
|
|
80
106
|
email: adapterUser.email,
|
|
81
107
|
roles: adapterUser.roles ?? [],
|
|
82
|
-
isAdmin: !!adapterUser.isAdmin
|
|
108
|
+
isAdmin: !!adapterUser.isAdmin,
|
|
109
|
+
isAnonymous: adapterUser.isAnonymous === true
|
|
83
110
|
};
|
|
84
111
|
} catch {}
|
|
85
112
|
else {
|
|
@@ -90,7 +117,8 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
90
117
|
displayName: jwtPayload.displayName,
|
|
91
118
|
photoURL: jwtPayload.photoURL,
|
|
92
119
|
roles: jwtPayload.roles ?? [],
|
|
93
|
-
isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin")
|
|
120
|
+
isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin"),
|
|
121
|
+
isAnonymous: jwtPayload.isAnonymous === true
|
|
94
122
|
};
|
|
95
123
|
}
|
|
96
124
|
if (verifiedUser) {
|
|
@@ -137,6 +165,33 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Refuse a frame whose path names no registered data collection.
|
|
170
|
+
*
|
|
171
|
+
* MongoDB has no row-level security, so on this engine the
|
|
172
|
+
* registry *is* the access model: `securityRules` are enforced
|
|
173
|
+
* only for a collection the registry resolves, and
|
|
174
|
+
* `MongoDataService.getCollection` maps any path to a physical
|
|
175
|
+
* collection by replacing `/` with `_`. A path the registry does
|
|
176
|
+
* not know therefore reaches the database with no rule to apply —
|
|
177
|
+
* `AuthenticatedMongoDriver.authorize(undefined)` answers
|
|
178
|
+
* "allowed" and the RLS filter for an undefined collection is
|
|
179
|
+
* "match all" — which put the auth store (`rebase_users` and its
|
|
180
|
+
* password hashes, `rebase_user_roles`, `rebase_refresh_tokens`)
|
|
181
|
+
* one frame away from any caller: authenticated, or anonymous
|
|
182
|
+
* when `requireAuth` is false.
|
|
183
|
+
*
|
|
184
|
+
* The socket is the one client-facing door onto this driver. REST
|
|
185
|
+
* mounts routes per registered slug and 404s everything else, and
|
|
186
|
+
* in-process writes are trusted server code — so the registry
|
|
187
|
+
* check lives here, at the boundary this door owns, exactly as the
|
|
188
|
+
* write validation beside it does. A `notFound` matches what REST
|
|
189
|
+
* answers for an unknown collection, and does not distinguish a
|
|
190
|
+
* collection that exists in Mongo from one that does not.
|
|
191
|
+
*/
|
|
192
|
+
const assertRegisteredPath = (path) => {
|
|
193
|
+
if (!path || !driver.registry?.getCollectionByPath(path)) throw ApiError.notFound(`Unknown collection at path "${path ?? ""}": it is not a registered data collection.`);
|
|
194
|
+
};
|
|
140
195
|
/** @see the Postgres socket — same rule, same reason. */
|
|
141
196
|
const assertWriteRequest = (path, values) => {
|
|
142
197
|
if (!path || !values || typeof values !== "object") return;
|
|
@@ -144,28 +199,11 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
144
199
|
if (!collection) return;
|
|
145
200
|
assertWriteRequestValid(values, collection);
|
|
146
201
|
};
|
|
147
|
-
const getScopedDelegate =
|
|
148
|
-
const session = clientSessions.get(clientId);
|
|
149
|
-
if (session?.user && isDriverWithAuth(driver)) try {
|
|
150
|
-
const userForAuth = {
|
|
151
|
-
uid: session.user.uid,
|
|
152
|
-
email: session.user.email ?? "",
|
|
153
|
-
displayName: session.user.displayName ?? "",
|
|
154
|
-
photoURL: session.user.photoURL ?? "",
|
|
155
|
-
providerId: "jwt",
|
|
156
|
-
isAnonymous: false,
|
|
157
|
-
roles: session.user.roles ?? []
|
|
158
|
-
};
|
|
159
|
-
return await driver.withAuth(userForAuth);
|
|
160
|
-
} catch (e) {
|
|
161
|
-
logger.error("Failed to create authenticated delegate for WS request", { error: e });
|
|
162
|
-
return driver;
|
|
163
|
-
}
|
|
164
|
-
return driver;
|
|
165
|
-
};
|
|
202
|
+
const getScopedDelegate = () => driver.withAuth(sessionUser(clientSessions.get(clientId)));
|
|
166
203
|
switch (type) {
|
|
167
204
|
case "FETCH_COLLECTION": {
|
|
168
205
|
const request = payload;
|
|
206
|
+
assertRegisteredPath(request.path);
|
|
169
207
|
const rows = await (await getScopedDelegate()).fetchCollection(request);
|
|
170
208
|
ws.send(JSON.stringify({
|
|
171
209
|
type: "FETCH_COLLECTION_SUCCESS",
|
|
@@ -176,6 +214,7 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
176
214
|
}
|
|
177
215
|
case "FETCH_ONE": {
|
|
178
216
|
const request = payload;
|
|
217
|
+
assertRegisteredPath(request.path);
|
|
179
218
|
const row = await (await getScopedDelegate()).fetchOne(request);
|
|
180
219
|
ws.send(JSON.stringify({
|
|
181
220
|
type: "FETCH_ONE_SUCCESS",
|
|
@@ -186,6 +225,7 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
186
225
|
}
|
|
187
226
|
case "SAVE": {
|
|
188
227
|
const request = payload;
|
|
228
|
+
assertRegisteredPath(request.path);
|
|
189
229
|
assertWriteRequest(request.path, request.values);
|
|
190
230
|
const row = await (await getScopedDelegate()).save(request);
|
|
191
231
|
ws.send(JSON.stringify({
|
|
@@ -197,7 +237,14 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
197
237
|
}
|
|
198
238
|
case "DELETE": {
|
|
199
239
|
const request = payload;
|
|
200
|
-
|
|
240
|
+
assertRegisteredPath(request.row?.path);
|
|
241
|
+
await (await getScopedDelegate()).delete({
|
|
242
|
+
row: {
|
|
243
|
+
id: request.row.id,
|
|
244
|
+
path: request.row.path
|
|
245
|
+
},
|
|
246
|
+
hard: request.hard
|
|
247
|
+
});
|
|
201
248
|
ws.send(JSON.stringify({
|
|
202
249
|
type: "DELETE_SUCCESS",
|
|
203
250
|
payload: { success: true },
|
|
@@ -207,6 +254,7 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
207
254
|
}
|
|
208
255
|
case "CHECK_UNIQUE_FIELD": {
|
|
209
256
|
const { path, name, value, id, collection } = payload;
|
|
257
|
+
assertRegisteredPath(path);
|
|
210
258
|
const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id, collection);
|
|
211
259
|
ws.send(JSON.stringify({
|
|
212
260
|
type: "CHECK_UNIQUE_FIELD_SUCCESS",
|
|
@@ -217,6 +265,7 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
217
265
|
}
|
|
218
266
|
case "COUNT": {
|
|
219
267
|
const request = payload;
|
|
268
|
+
assertRegisteredPath(request.path);
|
|
220
269
|
const count = await (await getScopedDelegate()).count(request);
|
|
221
270
|
ws.send(JSON.stringify({
|
|
222
271
|
type: "COUNT_SUCCESS",
|
|
@@ -277,29 +326,42 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
277
326
|
case "subscribe_collection":
|
|
278
327
|
case "subscribe_one":
|
|
279
328
|
case "unsubscribe": {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
if (type !== "unsubscribe" && !driver.registry?.getCollectionByPath(payload?.path)) {
|
|
330
|
+
ws.send(JSON.stringify({
|
|
331
|
+
type: "ERROR",
|
|
332
|
+
requestId,
|
|
333
|
+
subscriptionId: payload?.subscriptionId,
|
|
334
|
+
payload: { error: {
|
|
335
|
+
message: `Unknown collection at path "${payload?.path ?? ""}": it is not a registered data collection.`,
|
|
336
|
+
code: "NOT_FOUND"
|
|
337
|
+
} }
|
|
338
|
+
}));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const subscriber = sessionUser(clientSessions.get(clientId));
|
|
285
342
|
await realtimeService.handleClientMessage(clientId, {
|
|
286
343
|
type,
|
|
287
344
|
payload,
|
|
288
345
|
subscriptionId: payload?.subscriptionId
|
|
289
|
-
},
|
|
346
|
+
}, {
|
|
347
|
+
uid: subscriber.uid,
|
|
348
|
+
roles: subscriber.roles ?? [],
|
|
349
|
+
isAnonymous: subscriber.isAnonymous
|
|
350
|
+
});
|
|
290
351
|
break;
|
|
291
352
|
}
|
|
292
353
|
default: logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
|
|
293
354
|
}
|
|
294
355
|
} catch (error) {
|
|
295
|
-
|
|
296
|
-
|
|
356
|
+
const answer = declaredErrorAnswer(error);
|
|
357
|
+
if (answer) {
|
|
297
358
|
ws.send(JSON.stringify({
|
|
298
359
|
type: "ERROR",
|
|
299
360
|
requestId,
|
|
300
361
|
payload: { error: {
|
|
301
|
-
message:
|
|
302
|
-
code:
|
|
362
|
+
message: answer.message,
|
|
363
|
+
code: answer.code,
|
|
364
|
+
...answer.details !== void 0 && { details: answer.details }
|
|
303
365
|
} }
|
|
304
366
|
}));
|
|
305
367
|
return;
|
|
@@ -320,4 +382,4 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
320
382
|
//#endregion
|
|
321
383
|
export { createMongoWebSocket };
|
|
322
384
|
|
|
323
|
-
//# sourceMappingURL=websocket-
|
|
385
|
+
//# sourceMappingURL=websocket-C3iT6srl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-C3iT6srl.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { ANONYMOUS_USER_ID, RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, ApiError, declaredErrorAnswer } from \"@rebasepro/server\";\nimport type { RebaseAuthConfig } from \"@rebasepro/server\";\nimport { MongoRealtimeService } from \"./services/MongoRealtimeService\";\nimport { MongoDriver } from \"./services/MongoDriver\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Normalized user identity for WebSocket sessions — the same shape the Postgres\n * socket keeps, because an `AuthAdapter` user is not an access-token payload.\n */\ninterface WsUserIdentity {\n uid: string;\n email?: string;\n displayName?: string;\n photoURL?: string;\n roles: string[];\n isAdmin: boolean;\n /**\n * Whether this session is a guest — anonymous sign-in rather than an\n * account. Required, so every way of signing in has to say: a guest has a\n * real uid, and `policy.registered()` has nothing else to tell it from an\n * account by. Neither sign-in path read it, and every frame was then\n * scoped as an account.\n */\n isAnonymous: boolean;\n}\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n messageCount: number;\n messageWindowStart: number;\n}\n\nconst WS_RATE_LIMIT = 2000;\nconst WS_RATE_WINDOW_MS = 60_000;\n\nconst ADMIN_ONLY_TYPES = new Set([\n \"EXECUTE_SQL\",\n \"FETCH_DATABASES\",\n \"FETCH_ROLES\",\n \"FETCH_UNMAPPED_TABLES\",\n \"FETCH_TABLE_METADATA\",\n \"FETCH_CURRENT_DATABASE\",\n \"CREATE_BRANCH\",\n \"DELETE_BRANCH\",\n \"LIST_BRANCHES\"\n]);\n\n/**\n * Who a socket reads and writes as, for its request frames and its\n * subscriptions alike — one answer, so the two cannot disagree.\n *\n * A socket with no session (only possible with `requireAuth: false`) is the\n * anonymous user, exactly as REST scopes the same caller. It used to get the\n * base driver instead, which on this engine applies no security rules at all:\n * such a socket read every row and wrote wherever it liked.\n */\nfunction sessionUser(session: ClientSession | undefined): User {\n if (!session?.user) {\n return {\n uid: ANONYMOUS_USER_ID,\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: false,\n roles: [\"anon\"]\n };\n }\n return {\n uid: session.user.uid,\n email: session.user.email ?? \"\",\n displayName: session.user.displayName ?? \"\",\n photoURL: session.user.photoURL ?? \"\",\n providerId: \"jwt\",\n isAnonymous: session.user.isAnonymous,\n roles: session.user.roles ?? []\n };\n}\n\nfunction isAdminSession(session: ClientSession | undefined): boolean {\n if (!session?.user) return false;\n // The adapter's own answer first; a role *named* `admin` is only the\n // fallback for the built-in JWT path.\n if (session.user.isAdmin) return true;\n return (session.user.roles ?? []).some((r) => r === \"admin\");\n}\n\nexport function createMongoWebSocket(\n server: Server,\n realtimeService: MongoRealtimeService,\n driver: MongoDriver,\n authConfig?: RebaseAuthConfig,\n admin?: DatabaseAdmin,\n authAdapter?: AuthAdapter\n) {\n // Scoped to this factory invocation rather than the module, so sessions do\n // not leak across hot reloads or a second server on the same process — the\n // Postgres socket keeps it here for the same reason.\n const clientSessions = new Map<string, ClientSession>();\n\n const isProduction = process.env.NODE_ENV === \"production\";\n const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };\n const wss = new WebSocketServer({ server });\n\n wss.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") return;\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // The same predicate the HTTP data routes use, from the same function. See\n // `resolveRequireAuth` for what this socket's local copy got wrong — most\n // importantly that a `false` here does not skip a check, it marks every\n // session `authenticated` at connect time.\n const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);\n\n if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is \" +\n \"configured — no client can complete AUTH, so every realtime message will be refused.\"\n );\n }\n\n wss.on(\"connection\", (ws) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\nmessageCount: 0,\nmessageWindowStart: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n clientSessions.delete(clientId);\n });\n\n ws.on(\"message\", async (message) => {\n let requestId: string | undefined;\n try {\n const { type, payload, requestId: reqId } = JSON.parse(message.toString());\n requestId = reqId;\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n ws.send(JSON.stringify({ type: errType,\nrequestId,\npayload: { error: { message: msg,\ncode } } }));\n };\n\n if (type === \"AUTHENTICATE\") {\n const { token } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n\n // The adapter verifies when one is configured, exactly as the\n // HTTP routes do; the built-in JWT path is the fallback.\n let verifiedUser: WsUserIdentity | null = null;\n\n if (authAdapter) {\n try {\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n if (adapterUser) {\n verifiedUser = {\n uid: adapterUser.uid,\n email: adapterUser.email,\n roles: adapterUser.roles ?? [],\n isAdmin: !!adapterUser.isAdmin,\n // Absent from an adapter with no such\n // concept, and absent reads as \"not a guest\".\n isAnonymous: adapterUser.isAnonymous === true\n };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else {\n const jwtPayload = await extractUserFromToken(token);\n if (jwtPayload) {\n verifiedUser = {\n uid: jwtPayload.uid,\n email: jwtPayload.email,\n displayName: jwtPayload.displayName,\n photoURL: jwtPayload.photoURL,\n roles: jwtPayload.roles ?? [],\n isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === \"admin\"),\n isAnonymous: jwtPayload.isAnonymous === true\n };\n }\n }\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n }\n ws.send(JSON.stringify({ type: \"AUTH_SUCCESS\",\nrequestId,\npayload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles } }));\n } else {\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n\n if (ADMIN_ONLY_TYPES.has(type)) {\n const session = clientSessions.get(clientId);\n if (!isAdminSession(session)) {\n sendError(\"ERROR\", \"FORBIDDEN\", \"Admin access required for this operation\");\n return;\n }\n }\n\n /**\n * Refuse a frame whose path names no registered data collection.\n *\n * MongoDB has no row-level security, so on this engine the\n * registry *is* the access model: `securityRules` are enforced\n * only for a collection the registry resolves, and\n * `MongoDataService.getCollection` maps any path to a physical\n * collection by replacing `/` with `_`. A path the registry does\n * not know therefore reaches the database with no rule to apply —\n * `AuthenticatedMongoDriver.authorize(undefined)` answers\n * \"allowed\" and the RLS filter for an undefined collection is\n * \"match all\" — which put the auth store (`rebase_users` and its\n * password hashes, `rebase_user_roles`, `rebase_refresh_tokens`)\n * one frame away from any caller: authenticated, or anonymous\n * when `requireAuth` is false.\n *\n * The socket is the one client-facing door onto this driver. REST\n * mounts routes per registered slug and 404s everything else, and\n * in-process writes are trusted server code — so the registry\n * check lives here, at the boundary this door owns, exactly as the\n * write validation beside it does. A `notFound` matches what REST\n * answers for an unknown collection, and does not distinguish a\n * collection that exists in Mongo from one that does not.\n */\n const assertRegisteredPath = (path: string | undefined): void => {\n if (!path || !driver.registry?.getCollectionByPath(path)) {\n throw ApiError.notFound(\n `Unknown collection at path \"${path ?? \"\"}\": it is not a registered data collection.`\n );\n }\n };\n\n /** @see the Postgres socket — same rule, same reason. */\n const assertWriteRequest = (path: string | undefined, values: unknown): void => {\n if (!path || !values || typeof values !== \"object\") return;\n const collection = driver.registry?.getCollectionByPath(path);\n if (!collection) return;\n assertWriteRequestValid(values as Record<string, unknown>, collection);\n };\n\n // Always scoped. A failure to scope propagates to the frame's\n // `catch` rather than falling back to the base driver, which\n // is what it did: the fallback is the unscoped read.\n const getScopedDelegate = (): Promise<DataDriver> =>\n driver.withAuth(sessionUser(clientSessions.get(clientId)));\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const rows = await delegate.fetchCollection(request);\n ws.send(JSON.stringify({ type: \"FETCH_COLLECTION_SUCCESS\",\npayload: { rows },\nrequestId }));\n break;\n }\n case \"FETCH_ONE\": {\n const request: FetchOneProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n ws.send(JSON.stringify({ type: \"FETCH_ONE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"SAVE\": {\n const request: SaveProps = payload;\n // The REST layer's write checks, at this boundary too —\n // the socket is a second way in, and it used to be the\n // unchecked one. Collection from the registry by path,\n // never from the client's `request.collection`.\n assertRegisteredPath(request.path);\n assertWriteRequest(request.path, request.values as Record<string, unknown>);\n const delegate = await getScopedDelegate();\n const row = await delegate.save(request);\n ws.send(JSON.stringify({ type: \"SAVE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"DELETE\": {\n const request: DeleteProps = payload;\n assertRegisteredPath(request.row?.path);\n const delegate = await getScopedDelegate();\n // The address, and nothing else the frame says: the\n // driver reads the row and resolves the collection by\n // path — see the Postgres socket's DELETE.\n await delegate.delete({\n row: { id: request.row.id, path: request.row.path },\n hard: request.hard\n });\n ws.send(JSON.stringify({ type: \"DELETE_SUCCESS\",\npayload: { success: true },\nrequestId }));\n break;\n }\n case \"CHECK_UNIQUE_FIELD\": {\n const { path, name, value, id, collection } = payload;\n assertRegisteredPath(path);\n const delegate = await getScopedDelegate();\n const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);\n ws.send(JSON.stringify({ type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\npayload: { isUnique },\nrequestId }));\n break;\n }\n case \"COUNT\": {\n const request: FetchCollectionProps = payload;\n assertRegisteredPath(request.path);\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n ws.send(JSON.stringify({ type: \"COUNT_SUCCESS\",\npayload: { count },\nrequestId }));\n break;\n }\n case \"EXECUTE_SQL\": {\n const { sql, options } = payload;\n if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {\n const result = await admin.executeAggregate(sql as Record<string, unknown>[]);\n ws.send(JSON.stringify({ type: \"EXECUTE_SQL_SUCCESS\",\npayload: { result },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: \"SQL execution not supported for this driver\",\ncode: \"NOT_SUPPORTED\" } } }));\n }\n break;\n }\n case \"FETCH_UNMAPPED_TABLES\": {\n if (admin && isSchemaAdmin(admin)) {\n const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables: [] },\nrequestId }));\n }\n break;\n }\n case \"FETCH_TABLE_METADATA\": {\n const { tableName } = payload;\n if (admin && isSchemaAdmin(admin)) {\n const metadata = await admin.fetchTableMetadata?.(tableName);\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata: null },\nrequestId }));\n }\n break;\n }\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\": {\n // A subscription is a read that re-runs on every matching\n // write, so an unregistered path leaks exactly as\n // FETCH_COLLECTION does — and re-leaks. Refused here, with\n // the subscription id the client keys its errors on, since\n // `handleClientMessage`'s frames carry no `requestId`.\n if (type !== \"unsubscribe\" && !driver.registry?.getCollectionByPath(payload?.path)) {\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n subscriptionId: payload?.subscriptionId,\n payload: { error: {\n message: `Unknown collection at path \"${payload?.path ?? \"\"}\": it is not a registered data collection.`,\n code: \"NOT_FOUND\"\n } }\n }));\n return;\n }\n // The same principal the request frames use, guest flag\n // included: every re-fetch reads as it.\n const subscriber = sessionUser(clientSessions.get(clientId));\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, {\n uid: subscriber.uid,\n roles: subscriber.roles ?? [],\n isAnonymous: subscriber.isAnonymous\n });\n break;\n }\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\n // A refused write keeps its message: it is the only thing that\n // tells the caller what to send instead, and the generic branch\n // below drops it in production. \"Refused\" is whatever REST\n // answers with the error's own status — `ApiError`, and the\n // `RebaseApiError` every collection-callback veto is — by the\n // same predicate, so the two cannot list different classes.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details })\n} } }));\n return;\n }\n const errorMessage = process.env.NODE_ENV === \"production\" ? \"An unexpected error occurred\" : (error instanceof Error ? error.message : \"An unexpected error occurred\");\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: errorMessage,\ncode: \"INTERNAL_ERROR\" } } }));\n }\n });\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-mongo",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "MongoDB backend for Rebase",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"mongodb": "^7.5.0",
|
|
46
46
|
"ws": "^8.21.1",
|
|
47
|
-
"@rebasepro/
|
|
48
|
-
"@rebasepro/
|
|
49
|
-
"@rebasepro/
|
|
47
|
+
"@rebasepro/common": "0.21.1",
|
|
48
|
+
"@rebasepro/types": "0.21.1",
|
|
49
|
+
"@rebasepro/utils": "0.21.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/jest": "^30.0.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"vite": "^8.1.5"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@rebasepro/server": "0.21.
|
|
62
|
+
"@rebasepro/server": "0.21.1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@rebasepro/server": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"websocket-B3LiQfFN.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, ApiError } from \"@rebasepro/server\";\nimport type { RebaseAuthConfig } from \"@rebasepro/server\";\nimport { MongoRealtimeService } from \"./services/MongoRealtimeService\";\nimport { MongoDriver } from \"./services/MongoDriver\";\nimport { logger } from \"@rebasepro/server\";\n\ninterface DriverWithAuth extends DataDriver {\n withAuth(user: Record<string, unknown>): Promise<DataDriver>;\n}\n\nfunction isDriverWithAuth(driver: DataDriver): driver is DriverWithAuth {\n return \"withAuth\" in driver && typeof (driver as Record<string, unknown>).withAuth === \"function\";\n}\n\n/**\n * Normalized user identity for WebSocket sessions — the same shape the Postgres\n * socket keeps, because an `AuthAdapter` user is not an access-token payload.\n */\ninterface WsUserIdentity {\n uid: string;\n email?: string;\n displayName?: string;\n photoURL?: string;\n roles: string[];\n isAdmin: boolean;\n}\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n messageCount: number;\n messageWindowStart: number;\n}\n\nconst WS_RATE_LIMIT = 2000;\nconst WS_RATE_WINDOW_MS = 60_000;\n\nconst ADMIN_ONLY_TYPES = new Set([\n \"EXECUTE_SQL\",\n \"FETCH_DATABASES\",\n \"FETCH_ROLES\",\n \"FETCH_UNMAPPED_TABLES\",\n \"FETCH_TABLE_METADATA\",\n \"FETCH_CURRENT_DATABASE\",\n \"CREATE_BRANCH\",\n \"DELETE_BRANCH\",\n \"LIST_BRANCHES\"\n]);\n\nfunction isAdminSession(session: ClientSession | undefined): boolean {\n if (!session?.user) return false;\n // The adapter's own answer first; a role *named* `admin` is only the\n // fallback for the built-in JWT path.\n if (session.user.isAdmin) return true;\n return (session.user.roles ?? []).some((r) => r === \"admin\");\n}\n\nexport function createMongoWebSocket(\n server: Server,\n realtimeService: MongoRealtimeService,\n driver: MongoDriver,\n authConfig?: RebaseAuthConfig,\n admin?: DatabaseAdmin,\n authAdapter?: AuthAdapter\n) {\n // Scoped to this factory invocation rather than the module, so sessions do\n // not leak across hot reloads or a second server on the same process — the\n // Postgres socket keeps it here for the same reason.\n const clientSessions = new Map<string, ClientSession>();\n\n const isProduction = process.env.NODE_ENV === \"production\";\n const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };\n const wss = new WebSocketServer({ server });\n\n wss.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") return;\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // The same predicate the HTTP data routes use, from the same function. See\n // `resolveRequireAuth` for what this socket's local copy got wrong — most\n // importantly that a `false` here does not skip a check, it marks every\n // session `authenticated` at connect time.\n const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);\n\n if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is \" +\n \"configured — no client can complete AUTH, so every realtime message will be refused.\"\n );\n }\n\n wss.on(\"connection\", (ws) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\nmessageCount: 0,\nmessageWindowStart: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n clientSessions.delete(clientId);\n });\n\n ws.on(\"message\", async (message) => {\n let requestId: string | undefined;\n try {\n const { type, payload, requestId: reqId } = JSON.parse(message.toString());\n requestId = reqId;\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n ws.send(JSON.stringify({ type: errType,\nrequestId,\npayload: { error: { message: msg,\ncode } } }));\n };\n\n if (type === \"AUTHENTICATE\") {\n const { token } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n\n // The adapter verifies when one is configured, exactly as the\n // HTTP routes do; the built-in JWT path is the fallback.\n let verifiedUser: WsUserIdentity | null = null;\n\n if (authAdapter) {\n try {\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n if (adapterUser) {\n verifiedUser = {\n uid: adapterUser.uid,\n email: adapterUser.email,\n roles: adapterUser.roles ?? [],\n isAdmin: !!adapterUser.isAdmin\n };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else {\n const jwtPayload = await extractUserFromToken(token);\n if (jwtPayload) {\n verifiedUser = {\n uid: jwtPayload.uid,\n email: jwtPayload.email,\n displayName: jwtPayload.displayName,\n photoURL: jwtPayload.photoURL,\n roles: jwtPayload.roles ?? [],\n isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === \"admin\")\n };\n }\n }\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n }\n ws.send(JSON.stringify({ type: \"AUTH_SUCCESS\",\nrequestId,\npayload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles } }));\n } else {\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n\n if (ADMIN_ONLY_TYPES.has(type)) {\n const session = clientSessions.get(clientId);\n if (!isAdminSession(session)) {\n sendError(\"ERROR\", \"FORBIDDEN\", \"Admin access required for this operation\");\n return;\n }\n }\n\n /** @see the Postgres socket — same rule, same reason. */\n const assertWriteRequest = (path: string | undefined, values: unknown): void => {\n if (!path || !values || typeof values !== \"object\") return;\n const collection = driver.registry?.getCollectionByPath(path);\n if (!collection) return;\n assertWriteRequestValid(values as Record<string, unknown>, collection);\n };\n\n const getScopedDelegate = async (): Promise<DataDriver> => {\n const session = clientSessions.get(clientId);\n if (session?.user && isDriverWithAuth(driver)) {\n try {\n const userForAuth: User = {\n uid: session.user.uid,\n email: session.user.email ?? \"\",\n displayName: session.user.displayName ?? \"\",\n photoURL: session.user.photoURL ?? \"\",\n providerId: \"jwt\",\n isAnonymous: false,\n roles: session.user.roles ?? []\n };\n return await driver.withAuth(userForAuth);\n } catch (e) {\n logger.error(\"Failed to create authenticated delegate for WS request\", { error: e });\n return driver;\n }\n }\n return driver;\n };\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n const request: FetchCollectionProps = payload;\n const delegate = await getScopedDelegate();\n const rows = await delegate.fetchCollection(request);\n ws.send(JSON.stringify({ type: \"FETCH_COLLECTION_SUCCESS\",\npayload: { rows },\nrequestId }));\n break;\n }\n case \"FETCH_ONE\": {\n const request: FetchOneProps = payload;\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n ws.send(JSON.stringify({ type: \"FETCH_ONE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"SAVE\": {\n const request: SaveProps = payload;\n // The REST layer's write checks, at this boundary too —\n // the socket is a second way in, and it used to be the\n // unchecked one. Collection from the registry by path,\n // never from the client's `request.collection`.\n assertWriteRequest(request.path, request.values as Record<string, unknown>);\n const delegate = await getScopedDelegate();\n const row = await delegate.save(request);\n ws.send(JSON.stringify({ type: \"SAVE_SUCCESS\",\npayload: { row },\nrequestId }));\n break;\n }\n case \"DELETE\": {\n const request: DeleteProps = payload;\n const delegate = await getScopedDelegate();\n await delegate.delete(request);\n ws.send(JSON.stringify({ type: \"DELETE_SUCCESS\",\npayload: { success: true },\nrequestId }));\n break;\n }\n case \"CHECK_UNIQUE_FIELD\": {\n const { path, name, value, id, collection } = payload;\n const delegate = await getScopedDelegate();\n const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);\n ws.send(JSON.stringify({ type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\npayload: { isUnique },\nrequestId }));\n break;\n }\n case \"COUNT\": {\n const request: FetchCollectionProps = payload;\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n ws.send(JSON.stringify({ type: \"COUNT_SUCCESS\",\npayload: { count },\nrequestId }));\n break;\n }\n case \"EXECUTE_SQL\": {\n const { sql, options } = payload;\n if (admin && isDocumentAdmin(admin) && admin.executeAggregate) {\n const result = await admin.executeAggregate(sql as Record<string, unknown>[]);\n ws.send(JSON.stringify({ type: \"EXECUTE_SQL_SUCCESS\",\npayload: { result },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: \"SQL execution not supported for this driver\",\ncode: \"NOT_SUPPORTED\" } } }));\n }\n break;\n }\n case \"FETCH_UNMAPPED_TABLES\": {\n if (admin && isSchemaAdmin(admin)) {\n const tables = await admin.fetchUnmappedTables?.(payload?.mappedPaths) || [];\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\npayload: { tables: [] },\nrequestId }));\n }\n break;\n }\n case \"FETCH_TABLE_METADATA\": {\n const { tableName } = payload;\n if (admin && isSchemaAdmin(admin)) {\n const metadata = await admin.fetchTableMetadata?.(tableName);\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata },\nrequestId }));\n } else {\n ws.send(JSON.stringify({ type: \"FETCH_TABLE_METADATA_SUCCESS\",\npayload: { metadata: null },\nrequestId }));\n }\n break;\n }\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\": {\n const session = clientSessions.get(clientId);\n const authContext = session?.user ? { uid: session.user.uid,\nroles: session.user.roles ?? [] } : undefined;\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, authContext);\n break;\n }\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\n // A refused write keeps its message: it is the only thing that\n // tells the caller what to send instead, and the generic branch\n // below drops it in production.\n if (error instanceof ApiError || (error as Error)?.name === \"ApiError\") {\n const apiError = error as ApiError;\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: apiError.message,\ncode: apiError.code } } }));\n return;\n }\n const errorMessage = process.env.NODE_ENV === \"production\" ? \"An unexpected error occurred\" : (error instanceof Error ? error.message : \"An unexpected error occurred\");\n ws.send(JSON.stringify({ type: \"ERROR\",\nrequestId,\npayload: { error: { message: errorMessage,\ncode: \"INTERNAL_ERROR\" } } }));\n }\n });\n });\n}\n"],"mappings":"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"}
|