@rebasepro/server-mongo 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/services.d.ts +11 -23
- package/dist/db/MongoConditionBuilder.d.ts +28 -3
- package/dist/db/MongoDataService.d.ts +46 -3
- package/dist/{ensure-collections-Bl12bSwO.js → ensure-collections-DHTVMlxf.js} +1 -57
- package/dist/ensure-collections-DHTVMlxf.js.map +1 -0
- package/dist/index.es.js +268 -121
- package/dist/index.es.js.map +1 -1
- package/dist/services/MongoDriver.d.ts +16 -2
- package/dist/{websocket-C3iT6srl.js → websocket-CXSVz4oq.js} +29 -23
- package/dist/websocket-CXSVz4oq.js.map +1 -0
- package/package.json +5 -5
- package/dist/ensure-collections-Bl12bSwO.js.map +0 -1
- package/dist/websocket-C3iT6srl.js.map +0 -1
|
@@ -80,7 +80,7 @@ export declare class MongoDriver implements DataDriver {
|
|
|
80
80
|
/**
|
|
81
81
|
* Check if a field value is unique
|
|
82
82
|
*/
|
|
83
|
-
checkUniqueField(path: string, name: string, value:
|
|
83
|
+
checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
84
84
|
/**
|
|
85
85
|
* Generate a new row ID
|
|
86
86
|
*/
|
|
@@ -129,6 +129,14 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
|
|
|
129
129
|
* guest's `fetchCollection` on this same driver.
|
|
130
130
|
*/
|
|
131
131
|
private authContext;
|
|
132
|
+
/**
|
|
133
|
+
* The caller per-field `access` is judged against.
|
|
134
|
+
*
|
|
135
|
+
* Never `undefined`: that is the trusted server plane, which satisfies
|
|
136
|
+
* every non-empty role list, and every call through this driver is made
|
|
137
|
+
* for a user — an anonymous one included, as `roles: ["anon"]`.
|
|
138
|
+
*/
|
|
139
|
+
private viewer;
|
|
132
140
|
/**
|
|
133
141
|
* Evaluate the collection's rules for one row, fail-closed.
|
|
134
142
|
*
|
|
@@ -151,7 +159,13 @@ export declare class AuthenticatedMongoDriver implements DataDriver {
|
|
|
151
159
|
*/
|
|
152
160
|
save<M extends Record<string, any>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
|
|
153
161
|
delete<M extends Record<string, any>>(props: DeleteProps<M>): Promise<void>;
|
|
154
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Whether `value` is free in `name`, asked about a field this caller could
|
|
164
|
+
* have read — anything else is an oracle over a value the strip withholds,
|
|
165
|
+
* one guess at a time. A registered collection names its fields, so an
|
|
166
|
+
* undeclared name is refused rather than counted.
|
|
167
|
+
*/
|
|
168
|
+
checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
155
169
|
generateId(path: string, collection?: CollectionConfig): string;
|
|
156
170
|
count<M extends Record<string, any>>(props: FetchCollectionProps<M>): Promise<number>;
|
|
157
171
|
isReady(): boolean;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { ANONYMOUS_USER_ID, isDocumentAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
2
|
-
import { ApiError, assertWriteRequestValid, declaredErrorAnswer, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
|
|
1
|
+
import { ANONYMOUS_USER_ID, isDocumentAdmin, isSchemaAdmin, scopeGrants, scopesForRoles } from "@rebasepro/types";
|
|
2
|
+
import { ApiError, assertFieldOpsValid, assertWriteRequestValid, declaredErrorAnswer, extractUserFromToken, getAccessModel, logger, resolveRequireAuth } from "@rebasepro/server";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
4
|
//#region src/websocket.ts
|
|
5
5
|
var WS_RATE_LIMIT = 2e3;
|
|
6
6
|
var WS_RATE_WINDOW_MS = 6e4;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
7
|
+
/**
|
|
8
|
+
* WebSocket message types on the admin plane, and the scope each needs — the
|
|
9
|
+
* same verbs and scopes as the Postgres socket.
|
|
10
|
+
*/
|
|
11
|
+
var ADMIN_PLANE_TYPES = /* @__PURE__ */ new Map([
|
|
12
|
+
["EXECUTE_SQL", "database:write"],
|
|
13
|
+
["CREATE_BRANCH", "database:write"],
|
|
14
|
+
["DELETE_BRANCH", "database:write"],
|
|
15
|
+
["FETCH_DATABASES", "database:read"],
|
|
16
|
+
["FETCH_ROLES", "database:read"],
|
|
17
|
+
["FETCH_UNMAPPED_TABLES", "database:read"],
|
|
18
|
+
["FETCH_TABLE_METADATA", "database:read"],
|
|
19
|
+
["FETCH_CURRENT_DATABASE", "database:read"],
|
|
20
|
+
["LIST_BRANCHES", "database:read"]
|
|
17
21
|
]);
|
|
18
22
|
/**
|
|
19
23
|
* Who a socket reads and writes as, for its request frames and its
|
|
@@ -44,10 +48,10 @@ function sessionUser(session) {
|
|
|
44
48
|
roles: session.user.roles ?? []
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (session
|
|
50
|
-
return (session.user.roles ?? []
|
|
51
|
+
/** Everything a session may do: what its roles hold. */
|
|
52
|
+
function sessionScopes(session) {
|
|
53
|
+
if (!session?.user) return [];
|
|
54
|
+
return scopesForRoles(session.user.roles ?? [], getAccessModel());
|
|
51
55
|
}
|
|
52
56
|
function createMongoWebSocket(server, realtimeService, driver, authConfig, admin, authAdapter) {
|
|
53
57
|
const clientSessions = /* @__PURE__ */ new Map();
|
|
@@ -159,9 +163,10 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
const adminScope = ADMIN_PLANE_TYPES.get(type);
|
|
167
|
+
if (adminScope) {
|
|
168
|
+
if (!scopeGrants(sessionScopes(clientSessions.get(clientId)), adminScope)) {
|
|
169
|
+
sendError("ERROR", "SCOPE_MISSING", `This operation needs the "${adminScope}" scope`);
|
|
165
170
|
return;
|
|
166
171
|
}
|
|
167
172
|
}
|
|
@@ -197,7 +202,8 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
197
202
|
if (!path || !values || typeof values !== "object") return;
|
|
198
203
|
const collection = driver.registry?.getCollectionByPath(path);
|
|
199
204
|
if (!collection) return;
|
|
200
|
-
assertWriteRequestValid(values, collection);
|
|
205
|
+
assertWriteRequestValid(values, collection, { viewer: { roles: sessionUser(clientSessions.get(clientId)).roles ?? [] } });
|
|
206
|
+
assertFieldOpsValid(values, collection);
|
|
201
207
|
};
|
|
202
208
|
const getScopedDelegate = () => driver.withAuth(sessionUser(clientSessions.get(clientId)));
|
|
203
209
|
switch (type) {
|
|
@@ -253,9 +259,9 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
253
259
|
break;
|
|
254
260
|
}
|
|
255
261
|
case "CHECK_UNIQUE_FIELD": {
|
|
256
|
-
const { path, name, value, id
|
|
262
|
+
const { path, name, value, id } = payload;
|
|
257
263
|
assertRegisteredPath(path);
|
|
258
|
-
const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id,
|
|
264
|
+
const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id, driver.registry?.getCollectionByPath(path));
|
|
259
265
|
ws.send(JSON.stringify({
|
|
260
266
|
type: "CHECK_UNIQUE_FIELD_SUCCESS",
|
|
261
267
|
payload: { isUnique },
|
|
@@ -382,4 +388,4 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
|
|
|
382
388
|
//#endregion
|
|
383
389
|
export { createMongoWebSocket };
|
|
384
390
|
|
|
385
|
-
//# sourceMappingURL=websocket-
|
|
391
|
+
//# sourceMappingURL=websocket-CXSVz4oq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-CXSVz4oq.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { ANONYMOUS_USER_ID, RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter, scopeGrants, scopesForRoles, type AdminScope } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, resolveRequireAuth, assertWriteRequestValid, assertFieldOpsValid, ApiError, declaredErrorAnswer } from \"@rebasepro/server\";\nimport type { RebaseAuthConfig } from \"@rebasepro/server\";\nimport { MongoRealtimeService } from \"./services/MongoRealtimeService\";\nimport { MongoDriver } from \"./services/MongoDriver\";\nimport { logger, getAccessModel } 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\n/**\n * WebSocket message types on the admin plane, and the scope each needs — the\n * same verbs and scopes as the Postgres socket.\n */\nconst ADMIN_PLANE_TYPES: ReadonlyMap<string, AdminScope> = new Map<string, AdminScope>([\n [\"EXECUTE_SQL\", \"database:write\"],\n [\"CREATE_BRANCH\", \"database:write\"],\n [\"DELETE_BRANCH\", \"database:write\"],\n [\"FETCH_DATABASES\", \"database:read\"],\n [\"FETCH_ROLES\", \"database:read\"],\n [\"FETCH_UNMAPPED_TABLES\", \"database:read\"],\n [\"FETCH_TABLE_METADATA\", \"database:read\"],\n [\"FETCH_CURRENT_DATABASE\", \"database:read\"],\n [\"LIST_BRANCHES\", \"database:read\"]\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\n/** Everything a session may do: what its roles hold. */\nfunction sessionScopes(session: ClientSession | undefined): string[] {\n if (!session?.user) return [];\n return scopesForRoles(session.user.roles ?? [], getAccessModel());\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 const adminScope = ADMIN_PLANE_TYPES.get(type);\n if (adminScope) {\n const session = clientSessions.get(clientId);\n if (!scopeGrants(sessionScopes(session), adminScope)) {\n sendError(\"ERROR\", \"SCOPE_MISSING\", `This operation needs the \"${adminScope}\" scope`);\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 // The roles this socket's driver is scoped with — `[\"anon\"]`\n // for one with no session. Never `undefined`: that is the\n // trusted server plane, which satisfies every `access.write`\n // list, so omitting it let any caller write any field.\n assertWriteRequestValid(values as Record<string, unknown>, collection, {\n viewer: { roles: sessionUser(clientSessions.get(clientId)).roles ?? [] }\n });\n // `$push` on a number is the caller's mistake, answered here\n // as the 400 `PATCH` gives it — the same check the Postgres\n // socket runs at this door.\n assertFieldOpsValid(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 } = payload;\n assertRegisteredPath(path);\n const delegate = await getScopedDelegate();\n // The registry's collection, never the frame's: its\n // properties are what decides which names may be asked.\n const isUnique = await delegate.checkUniqueField(path, name, value, id, driver.registry?.getCollectionByPath(path));\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.
|
|
3
|
+
"version": "0.24.0",
|
|
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/utils": "0.
|
|
49
|
-
"@rebasepro/
|
|
47
|
+
"@rebasepro/types": "0.24.0",
|
|
48
|
+
"@rebasepro/utils": "0.24.0",
|
|
49
|
+
"@rebasepro/common": "0.24.0"
|
|
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.
|
|
62
|
+
"@rebasepro/server": "0.24.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@rebasepro/server": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-collections-Bl12bSwO.js","names":[],"sources":["../src/auth/ensure-collections.ts"],"sourcesContent":["import { Db } from \"mongodb\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Default roles to seed on first run\n */\nconst DEFAULT_ROLES = [\n {\n _id: \"admin\",\n id: \"admin\",\n name: \"Admin\",\n isAdmin: true,\n defaultPermissions: { read: true,\ncreate: true,\nedit: true,\ndelete: true },\n createdAt: new Date()\n },\n {\n _id: \"editor\",\n id: \"editor\",\n name: \"Editor\",\n isAdmin: false,\n defaultPermissions: { read: true,\ncreate: true,\nedit: true,\ndelete: true },\n createdAt: new Date()\n },\n {\n _id: \"viewer\",\n id: \"viewer\",\n name: \"Viewer\",\n isAdmin: false,\n defaultPermissions: { read: true,\ncreate: false,\nedit: false,\ndelete: false },\n createdAt: new Date()\n }\n];\n\n/**\n * Indexes created by releases up to 0.13.0, on snake_case field names the auth\n * services never wrote.\n *\n * MongoDB indexes an absent field as `null`, so a unique index whose every key\n * component is always missing admits exactly **one** document per collection:\n * the second login on the whole deployment failed with `E11000`, as did the\n * second role assignment and the second outstanding password-reset token.\n * Dropping them is therefore part of the fix, not tidying — creating the\n * correct index alongside a broken one leaves the deployment just as broken.\n *\n * `{user_id, user_agent, ip_address}` has a second reason to go: it is the\n * reverted Postgres constraint. `MongoRefreshTokenService.createToken` explains\n * why evicting by that triple signs a second browser out; the unique index\n * expressing the same rule was left behind here.\n */\nconst LEGACY_INDEX_NAMES: Record<string, string[]> = {\n rebase_user_identities: [\"provider_1_provider_id_1\", \"user_id_1\"],\n rebase_user_roles: [\"user_id_1_role_id_1\", \"user_id_1\"],\n rebase_refresh_tokens: [\"token_hash_1\", \"user_id_1_user_agent_1_ip_address_1\", \"user_id_1\"],\n rebase_password_reset_tokens: [\"token_hash_1\", \"user_id_1\"]\n};\n\nexport async function ensureAuthCollectionsExist(db: Db): Promise<void> {\n logger.info(\"🔍 Checking MongoDB auth collections and indexes...\");\n\n try {\n await dropLegacyIndexes(db);\n\n // Every key below is a field `auth/services.ts` actually writes —\n // camelCase, matching the documents. Verified by\n // `test/MongoAuthServices.test.ts`, which now runs this function.\n\n // Users\n const users = db.collection(\"rebase_users\");\n await users.createIndex({ email: 1 }, { unique: true });\n await users.createIndex({ id: 1 });\n\n // User Identities\n const identities = db.collection(\"rebase_user_identities\");\n await identities.createIndex({ provider: 1,\nproviderId: 1 }, { unique: true });\n await identities.createIndex({ uid: 1 });\n\n // User Roles (junction collection)\n const userRoles = db.collection(\"rebase_user_roles\");\n await userRoles.createIndex({ uid: 1,\nroleId: 1 }, { unique: true });\n await userRoles.createIndex({ roleId: 1 });\n\n // Refresh Tokens. No unique index on (uid, userAgent, ipAddress):\n // tokens of one sign-in accumulate under a shared sessionId.\n const refreshTokens = db.collection(\"rebase_refresh_tokens\");\n await refreshTokens.createIndex({ tokenHash: 1 }, { unique: true });\n await refreshTokens.createIndex({ uid: 1 });\n await refreshTokens.createIndex({ sessionId: 1 });\n\n // Password Reset Tokens\n const resetTokens = db.collection(\"rebase_password_reset_tokens\");\n await resetTokens.createIndex({ tokenHash: 1 }, { unique: true });\n await resetTokens.createIndex({ uid: 1 });\n\n // Seed roles\n await seedDefaultRoles(db);\n\n logger.info(\"✅ MongoDB Auth collections ready\");\n } catch (error) {\n // Not swallowed. Every index here is load-bearing for auth — a boot\n // that logs this and carries on is a deployment whose second login\n // fails, with a green boot log to say nothing is wrong.\n logger.error(\"❌ Failed to set up MongoDB auth collections\", { error: error });\n throw error;\n }\n}\n\n/** Drop the indexes of {@link LEGACY_INDEX_NAMES} that this database still has. */\nasync function dropLegacyIndexes(db: Db): Promise<void> {\n for (const [collectionName, indexNames] of Object.entries(LEGACY_INDEX_NAMES)) {\n const collection = db.collection(collectionName);\n for (const indexName of indexNames) {\n try {\n await collection.dropIndex(indexName);\n logger.info(`🧹 Dropped stale auth index ${collectionName}.${indexName}`);\n } catch {\n // NamespaceNotFound / IndexNotFound — nothing to drop here.\n }\n }\n }\n}\n\nasync function seedDefaultRoles(db: Db): Promise<void> {\n const roles = db.collection(\"rebase_roles\");\n const count = await roles.countDocuments();\n\n if (count > 0) {\n logger.info(`📋 Found ${count} existing roles`);\n return;\n }\n\n logger.info(\"🌱 Seeding default roles...\");\n\n for (const role of DEFAULT_ROLES) {\n await roles.updateOne(\n { id: role.id },\n { $setOnInsert: role },\n { upsert: true }\n );\n }\n\n logger.info(\"✅ Default roles created: admin, editor, viewer\");\n}\n"],"mappings":";;;;;AAMA,IAAM,gBAAgB;CAClB;EACI,KAAK;EACL,IAAI;EACJ,MAAM;EACN,SAAS;EACT,oBAAoB;GAAE,MAAM;GACpC,QAAQ;GACR,MAAM;GACN,QAAQ;EAAK;EACL,2BAAW,IAAI,KAAK;CACxB;CACA;EACI,KAAK;EACL,IAAI;EACJ,MAAM;EACN,SAAS;EACT,oBAAoB;GAAE,MAAM;GACpC,QAAQ;GACR,MAAM;GACN,QAAQ;EAAK;EACL,2BAAW,IAAI,KAAK;CACxB;CACA;EACI,KAAK;EACL,IAAI;EACJ,MAAM;EACN,SAAS;EACT,oBAAoB;GAAE,MAAM;GACpC,QAAQ;GACR,MAAM;GACN,QAAQ;EAAM;EACN,2BAAW,IAAI,KAAK;CACxB;AACJ;;;;;;;;;;;;;;;;;AAkBA,IAAM,qBAA+C;CACjD,wBAAwB,CAAC,4BAA4B,WAAW;CAChE,mBAAmB,CAAC,uBAAuB,WAAW;CACtD,uBAAuB;EAAC;EAAgB;EAAuC;CAAW;CAC1F,8BAA8B,CAAC,gBAAgB,WAAW;AAC9D;AAEA,eAAsB,2BAA2B,IAAuB;CACpE,OAAO,KAAK,qDAAqD;CAEjE,IAAI;EACA,MAAM,kBAAkB,EAAE;EAO1B,MAAM,QAAQ,GAAG,WAAW,cAAc;EAC1C,MAAM,MAAM,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACtD,MAAM,MAAM,YAAY,EAAE,IAAI,EAAE,CAAC;EAGjC,MAAM,aAAa,GAAG,WAAW,wBAAwB;EACzD,MAAM,WAAW,YAAY;GAAE,UAAU;GACjD,YAAY;EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACzB,MAAM,WAAW,YAAY,EAAE,KAAK,EAAE,CAAC;EAGvC,MAAM,YAAY,GAAG,WAAW,mBAAmB;EACnD,MAAM,UAAU,YAAY;GAAE,KAAK;GAC3C,QAAQ;EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACrB,MAAM,UAAU,YAAY,EAAE,QAAQ,EAAE,CAAC;EAIzC,MAAM,gBAAgB,GAAG,WAAW,uBAAuB;EAC3D,MAAM,cAAc,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EAClE,MAAM,cAAc,YAAY,EAAE,KAAK,EAAE,CAAC;EAC1C,MAAM,cAAc,YAAY,EAAE,WAAW,EAAE,CAAC;EAGhD,MAAM,cAAc,GAAG,WAAW,8BAA8B;EAChE,MAAM,YAAY,YAAY,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EAChE,MAAM,YAAY,YAAY,EAAE,KAAK,EAAE,CAAC;EAGxC,MAAM,iBAAiB,EAAE;EAEzB,OAAO,KAAK,kCAAkC;CAClD,SAAS,OAAO;EAIZ,OAAO,MAAM,+CAA+C,EAAS,MAAM,CAAC;EAC5E,MAAM;CACV;AACJ;;AAGA,eAAe,kBAAkB,IAAuB;CACpD,KAAK,MAAM,CAAC,gBAAgB,eAAe,OAAO,QAAQ,kBAAkB,GAAG;EAC3E,MAAM,aAAa,GAAG,WAAW,cAAc;EAC/C,KAAK,MAAM,aAAa,YACpB,IAAI;GACA,MAAM,WAAW,UAAU,SAAS;GACpC,OAAO,KAAK,+BAA+B,eAAe,GAAG,WAAW;EAC5E,QAAQ,CAER;CAER;AACJ;AAEA,eAAe,iBAAiB,IAAuB;CACnD,MAAM,QAAQ,GAAG,WAAW,cAAc;CAC1C,MAAM,QAAQ,MAAM,MAAM,eAAe;CAEzC,IAAI,QAAQ,GAAG;EACX,OAAO,KAAK,YAAY,MAAM,gBAAgB;EAC9C;CACJ;CAEA,OAAO,KAAK,6BAA6B;CAEzC,KAAK,MAAM,QAAQ,eACf,MAAM,MAAM,UACR,EAAE,IAAI,KAAK,GAAG,GACd,EAAE,cAAc,KAAK,GACrB,EAAE,QAAQ,KAAK,CACnB;CAGJ,OAAO,KAAK,gDAAgD;AAChE"}
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|