@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.
@@ -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
- /** The acting user, in the shape the realtime subscriptions carry. */
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 = async () => {
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
- await (await getScopedDelegate()).delete(request);
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
- const session = clientSessions.get(clientId);
281
- const authContext = session?.user ? {
282
- uid: session.user.uid,
283
- roles: session.user.roles ?? []
284
- } : void 0;
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
- }, authContext);
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
- if (error instanceof ApiError || error?.name === "ApiError") {
296
- const apiError = error;
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: apiError.message,
302
- code: apiError.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-B3LiQfFN.js.map
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.0",
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/types": "0.21.0",
48
- "@rebasepro/utils": "0.21.0",
49
- "@rebasepro/common": "0.21.0"
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.0"
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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"}