@rebasepro/server-mongo 0.14.0 → 0.14.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.
@@ -59,7 +59,7 @@ export declare class MongoDriver implements DataDriver {
59
59
  * what every re-fetch reads — the wrapper used to stamp the field on the
60
60
  * `Subscription` object instead, and nothing has ever read that one.
61
61
  */
62
- listenCollection<M extends Record<string, any>>({ path, collection, filter, limit, startAfter, orderBy, searchString, order, onUpdate, onError }: ListenCollectionProps<M>, authContext?: {
62
+ listenCollection<M extends Record<string, any>>({ onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>, authContext?: {
63
63
  uid: string;
64
64
  roles: string[];
65
65
  }): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"MongoDriver.d.ts","sourceRoot":"","sources":["../../src/services/MongoDriver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MongoDriver.d.ts","sourceRoot":"","sources":["../../src/services/MongoDriver.ts"],"names":[],"mappings":"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"}
@@ -26,6 +26,22 @@ interface Subscription {
26
26
  };
27
27
  changeStream?: ChangeStream;
28
28
  callback?: (data: any) => void;
29
+ /**
30
+ * How many deliveries have been started for this subscription, and the
31
+ * highest that has already reached the callback.
32
+ *
33
+ * Every delivery here is a re-fetch, and three independent things start one
34
+ * for the same subscription: the initial fetch, the change stream, and
35
+ * `notifyUpdate` after a save. They overlap, and a fetch that started
36
+ * earlier can finish later — at which point the callback replaces the
37
+ * client's whole list with the state before the change. Nothing corrects it
38
+ * until something else happens to that collection.
39
+ *
40
+ * A counter taken before the await and checked after it is what makes the
41
+ * last *started* delivery the last *delivered* one.
42
+ */
43
+ started: number;
44
+ delivered: number;
29
45
  }
30
46
  /**
31
47
  * MongoDB Realtime Service
@@ -44,6 +60,26 @@ export declare class MongoRealtimeService implements RealtimeProvider {
44
60
  * Get the collection name from a path
45
61
  */
46
62
  private getCollectionName;
63
+ /**
64
+ * Claim a delivery slot for a subscription, before doing the work.
65
+ *
66
+ * Returns the check to run immediately before calling the callback. It
67
+ * refuses in three cases, all of which used to deliver:
68
+ *
69
+ * - **Out of order.** A newer fetch has already delivered, so this one is
70
+ * stale — the client would go back to the state before the change.
71
+ * - **Unsubscribed.** The subscription was cancelled while the fetch was in
72
+ * flight, and its callback belongs to a client that stopped listening.
73
+ * - **Re-subscribed.** `subscribeToCollection` unsubscribes first, so the
74
+ * same id can name a *different* subscription by the time a fetch lands —
75
+ * with a different filter, and a different caller's rows.
76
+ *
77
+ * Synchronous deliveries claim a slot too. A `delete` notification with no
78
+ * fetch behind it is the newest thing known about the row, so it must also
79
+ * be the thing that closes the door on an older fetch still in flight —
80
+ * otherwise the deleted row reappears a moment after it vanished.
81
+ */
82
+ private beginDelivery;
47
83
  /**
48
84
  * Subscribe to collection changes
49
85
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MongoRealtimeService.d.ts","sourceRoot":"","sources":["../../src/services/MongoRealtimeService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,EAAE,EAAE,YAAY,EAA4C,MAAM,SAAS,CAAC;AACrF,OAAO,EAIH,gBAAgB,EAChB,4BAA4B,EAC5B,wBAAwB,EAK3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,iFAAiF;AACjF,MAAM,WAAW,uBAAuB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,UAAU,YAAY;IAClB,IAAI,EAAE,YAAY,GAAG,QAAQ,CAAC;IAC9B;;;;;OAKG;IACH,MAAM,EAAE,CAAC,4BAA4B,GAAG,wBAAwB,CAAC,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IAK7C,OAAO,CAAC,EAAE;IAJtB,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,MAAM,CAAC,CAAc;gBAET,EAAE,EAAE,EAAE;IAE1B,aAAa,CAAC,MAAM,EAAE,WAAW;IAIjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,qBAAqB,CACjB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,4BAA4B,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,EAChF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,IAAI,GACrD,IAAI;IAgEP;;OAEG;YACW,wBAAwB;IAkCtC;;;;;OAKG;YACW,YAAY;IAS1B;;OAEG;IACH,cAAc,CACV,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,wBAAwB,GAAG;QAAE,WAAW,CAAC,EAAE,uBAAuB,CAAA;KAAE,EAC5E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,GACzD,IAAI;IAqEP;;OAEG;YACW,iBAAiB;IAsB/B;;OAEG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAUzC;;;OAGG;IACG,YAAY,CACd,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACnC,WAAW,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC;IA2BhB;;OAEG;IACH,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAI7C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS;IAazC;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACG,mBAAmB,CACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,EAAE,CAAA;KAAE,GACjD,OAAO,CAAC,IAAI,CAAC;CAgHnB"}
1
+ {"version":3,"file":"MongoRealtimeService.d.ts","sourceRoot":"","sources":["../../src/services/MongoRealtimeService.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { isDocumentAdmin, isSchemaAdmin } from "@rebasepro/types";
2
- import { extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
2
+ import { ApiError, assertWriteRequestValid, extractUserFromToken, logger, resolveRequireAuth } from "@rebasepro/server";
3
3
  import { WebSocketServer } from "ws";
4
4
  //#region src/websocket.ts
5
5
  function isDriverWithAuth(driver) {
@@ -137,6 +137,13 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
137
137
  return;
138
138
  }
139
139
  }
140
+ /** @see the Postgres socket — same rule, same reason. */
141
+ const assertWriteRequest = (path, values) => {
142
+ if (!path || !values || typeof values !== "object") return;
143
+ const collection = driver.registry?.getCollectionByPath(path);
144
+ if (!collection) return;
145
+ assertWriteRequestValid(values, collection);
146
+ };
140
147
  const getScopedDelegate = async () => {
141
148
  const session = clientSessions.get(clientId);
142
149
  if (session?.user && isDriverWithAuth(driver)) try {
@@ -179,6 +186,7 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
179
186
  }
180
187
  case "SAVE": {
181
188
  const request = payload;
189
+ assertWriteRequest(request.path, request.values);
182
190
  const row = await (await getScopedDelegate()).save(request);
183
191
  ws.send(JSON.stringify({
184
192
  type: "SAVE_SUCCESS",
@@ -284,6 +292,18 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
284
292
  default: logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
285
293
  }
286
294
  } catch (error) {
295
+ if (error instanceof ApiError || error?.name === "ApiError") {
296
+ const apiError = error;
297
+ ws.send(JSON.stringify({
298
+ type: "ERROR",
299
+ requestId,
300
+ payload: { error: {
301
+ message: apiError.message,
302
+ code: apiError.code
303
+ } }
304
+ }));
305
+ return;
306
+ }
287
307
  const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" : error instanceof Error ? error.message : "An unexpected error occurred";
288
308
  ws.send(JSON.stringify({
289
309
  type: "ERROR",
@@ -300,4 +320,4 @@ function createMongoWebSocket(server, realtimeService, driver, authConfig, admin
300
320
  //#endregion
301
321
  export { createMongoWebSocket };
302
322
 
303
- //# sourceMappingURL=websocket-Bs54-gR-.js.map
323
+ //# sourceMappingURL=websocket-Do3I42Uc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-Do3I42Uc.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 = 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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4G,aAAa,EAAwC,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE9M,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAG9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAuDrD,wBAAgB,oBAAoB,CAChC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,oBAAoB,EACrC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,gBAAgB,EAC7B,KAAK,CAAC,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,WAAW,QAsS5B"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4G,aAAa,EAAwC,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE9M,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAG9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAuDrD,wBAAgB,oBAAoB,CAChC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,oBAAoB,EACrC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,gBAAgB,EAC7B,KAAK,CAAC,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,WAAW,QA8T5B"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-mongo",
3
3
  "type": "module",
4
- "version": "0.14.0",
4
+ "version": "0.14.1",
5
5
  "description": "MongoDB backend for Rebase",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -40,9 +40,9 @@
40
40
  "dependencies": {
41
41
  "mongodb": "^7.5.0",
42
42
  "ws": "^8.21.1",
43
- "@rebasepro/common": "0.14.0",
44
- "@rebasepro/utils": "0.14.0",
45
- "@rebasepro/types": "0.14.0"
43
+ "@rebasepro/common": "0.14.1",
44
+ "@rebasepro/types": "0.14.1",
45
+ "@rebasepro/utils": "0.14.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",
@@ -55,7 +55,7 @@
55
55
  "vite": "^8.1.5"
56
56
  },
57
57
  "peerDependencies": {
58
- "@rebasepro/server": "0.14.0"
58
+ "@rebasepro/server": "0.14.1"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@rebasepro/server": {
@@ -20,7 +20,8 @@ import {
20
20
  ListUsersOptions,
21
21
  PaginatedUsersResult,
22
22
  MfaFactor,
23
- MfaChallengeInfo
23
+ MfaChallengeInfo,
24
+ ApiError
24
25
  } from "@rebasepro/server";
25
26
 
26
27
  export type Role = RoleData;
@@ -77,7 +78,19 @@ export class MongoUserService implements UserRepository {
77
78
  createdAt: now,
78
79
  updatedAt: now
79
80
  };
80
- await this.collection.insertOne(doc);
81
+ try {
82
+ await this.collection.insertOne(doc);
83
+ } catch (error) {
84
+ // 11000 is Mongo's duplicate key, and the unique index on `email`
85
+ // in `ensure-collections.ts` is what raises it. Same answer as
86
+ // Postgres gives for its 23505, and the same answer the route
87
+ // gives when its pre-check sees the row — see
88
+ // `UserRepository.createUser`.
89
+ if ((error as { code?: number })?.code === 11000) {
90
+ throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
91
+ }
92
+ throw error;
93
+ }
81
94
  return toUser(doc);
82
95
  }
83
96
 
@@ -772,9 +785,27 @@ export class MongoAuthRepository implements AuthRepository {
772
785
  await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);
773
786
  }
774
787
 
775
- // MFA Repository Stub
788
+ // ── MFA: not implemented on this engine ──────────────────────────────
789
+ //
790
+ // The routes are mounted for every backend, so `POST /auth/mfa/enroll`
791
+ // is live here and reaches these. Throwing a bare `Error` made every one
792
+ // of them a 500 — and a 500's message is sanitized on the way out, so the
793
+ // caller was told "Internal Server Error" while the reason stayed in the
794
+ // server log. Someone turning on two-factor auth got a server fault
795
+ // instead of an answer, every time, on this engine.
796
+ //
797
+ // 501 with the reason, which is what `init.ts` already does for an admin
798
+ // surface it mounts and cannot serve: "they answer 501 instead, and stay
799
+ // mounted to say why". The reads below stay as they are — no factor can
800
+ // exist here, so `[]`, `null` and `false` are true rather than merely
801
+ // convenient, and `assertMfaSatisfied` reading `false` correctly leaves
802
+ // the login gate inert.
776
803
  async createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {
777
- throw new Error("MFA is not implemented for MongoDB");
804
+ throw new ApiError(
805
+ 501,
806
+ "MFA_NOT_SUPPORTED",
807
+ "Multi-factor authentication is not implemented for the MongoDB backend, so a factor cannot be stored. Use a Postgres data source for accounts that need a second factor."
808
+ );
778
809
  }
779
810
  async getMfaFactors(uid: string): Promise<MfaFactor[]> {
780
811
  return [];
@@ -783,22 +814,42 @@ export class MongoAuthRepository implements AuthRepository {
783
814
  return null;
784
815
  }
785
816
  async verifyMfaFactor(factorId: string): Promise<void> {
786
- throw new Error("MFA is not implemented for MongoDB");
817
+ throw new ApiError(
818
+ 501,
819
+ "MFA_NOT_SUPPORTED",
820
+ "Multi-factor authentication is not implemented for the MongoDB backend, so a factor's verification cannot be stored. Use a Postgres data source for accounts that need a second factor."
821
+ );
787
822
  }
788
823
  async deleteMfaFactor(factorId: string, uid: string): Promise<void> {
789
- throw new Error("MFA is not implemented for MongoDB");
824
+ throw new ApiError(
825
+ 501,
826
+ "MFA_NOT_SUPPORTED",
827
+ "Multi-factor authentication is not implemented for the MongoDB backend, so a factor cannot be stored. Use a Postgres data source for accounts that need a second factor."
828
+ );
790
829
  }
791
830
  async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {
792
- throw new Error("MFA is not implemented for MongoDB");
831
+ throw new ApiError(
832
+ 501,
833
+ "MFA_NOT_SUPPORTED",
834
+ "Multi-factor authentication is not implemented for the MongoDB backend, so a challenge cannot be stored. Use a Postgres data source for accounts that need a second factor."
835
+ );
793
836
  }
794
837
  async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {
795
838
  return null;
796
839
  }
797
840
  async verifyMfaChallenge(challengeId: string): Promise<void> {
798
- throw new Error("MFA is not implemented for MongoDB");
841
+ throw new ApiError(
842
+ 501,
843
+ "MFA_NOT_SUPPORTED",
844
+ "Multi-factor authentication is not implemented for the MongoDB backend, so a challenge's verification cannot be stored. Use a Postgres data source for accounts that need a second factor."
845
+ );
799
846
  }
800
847
  async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {
801
- throw new Error("MFA is not implemented for MongoDB");
848
+ throw new ApiError(
849
+ 501,
850
+ "MFA_NOT_SUPPORTED",
851
+ "Multi-factor authentication is not implemented for the MongoDB backend, so recovery codes cannot be stored. Use a Postgres data source for accounts that need a second factor."
852
+ );
802
853
  }
803
854
  async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {
804
855
  return false;
@@ -4,8 +4,8 @@
4
4
  * Translates Rebase filter conditions to MongoDB query operators.
5
5
  */
6
6
 
7
- import { CollectionConfig, FilterCondition, FilterValues, LogicalCondition, WhereFilterOp } from "@rebasepro/types";
8
- import { toFilterTuples } from "@rebasepro/common";
7
+ import { CollectionConfig, FilterCondition, FilterValues, LogicalCondition, OrderByTuple, WhereFilterOp } from "@rebasepro/types";
8
+ import { normalizeDriverOrderBy, toFilterTuples } from "@rebasepro/common";
9
9
  import { Filter, Document } from "mongodb";
10
10
  import { ApiError, logger } from "@rebasepro/server";
11
11
 
@@ -292,18 +292,57 @@ export class MongoConditionBuilder {
292
292
  return this.combineConditionsWithAnd(conditions) ?? {};
293
293
  }
294
294
 
295
+ /**
296
+ * The primary key's name in a stored document.
297
+ *
298
+ * Rows leave this driver with `_id` renamed to `id` — see
299
+ * `MongoDataService.documentToRow` — so `id` is the only name a caller ever
300
+ * sees, and the one the SDK's own examples use for a tie-breaker. A sort
301
+ * document naming `id` names a field no document has: Mongo does not
302
+ * complain, it just returns them in natural order, so `.orderBy("id")` read
303
+ * as "no sort at all" with a 200 to go with it.
304
+ */
305
+ private static readonly ID_FIELD = "_id";
306
+
295
307
  /**
296
308
  * Build MongoDB sort options from Rebase options
297
309
  *
298
- * @param orderBy - Field to order by
299
- * @param order - Sort direction
310
+ * A Mongo sort document is already an ordered map of field to direction, so
311
+ * a multi-key sort is expressed directly: the keys are applied in insertion
312
+ * order, each breaking ties on the one before it.
313
+ *
314
+ * The id closes every sort, descending, for the same reason the Postgres
315
+ * driver appends `id DESC`: it is what makes the ordering *total*. Without
316
+ * it, two rows sharing a sort value are returned in whatever order the
317
+ * engine pleases, and that order is free to differ between two executions of
318
+ * the same query — so paging by `offset` over a non-unique sort column
319
+ * repeats some rows and skips others. A single-column sort has always had
320
+ * this exposure here; a multi-key sort merely made it easier to reach, since
321
+ * the whole point of the later keys is that the earlier ones tie.
322
+ *
323
+ * @param orderBy - Field to order by, or the `[field, direction]` list
324
+ * @param order - Sort direction, for the single-field spelling
300
325
  * @returns MongoDB sort object
301
326
  */
302
327
  static buildSort(
303
- orderBy?: string,
328
+ orderBy?: string | OrderByTuple[],
304
329
  order?: "asc" | "desc"
305
330
  ): Record<string, 1 | -1> | undefined {
306
- if (!orderBy) return undefined;
307
- return { [orderBy]: order === "desc" ? -1 : 1 };
331
+ const keys = normalizeDriverOrderBy(orderBy, order);
332
+ if (!keys) return undefined;
333
+ const sort: Record<string, 1 | -1> = {};
334
+ // A repeated field is the first occurrence's: that is the key Mongo
335
+ // would sort by, and letting a later duplicate overwrite it would order
336
+ // by a direction the caller listed as less significant.
337
+ for (const [field, direction] of keys) {
338
+ const key = field === "id" ? MongoConditionBuilder.ID_FIELD : field;
339
+ if (!(key in sort)) sort[key] = direction === "desc" ? -1 : 1;
340
+ }
341
+ // Already named by the caller — at whatever direction and rank they
342
+ // chose — the sort is total and there is nothing left to break.
343
+ if (!(MongoConditionBuilder.ID_FIELD in sort)) {
344
+ sort[MongoConditionBuilder.ID_FIELD] = -1;
345
+ }
346
+ return sort;
308
347
  }
309
348
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { Db, ObjectId, Collection, Document, FindOptions, Filter } from "mongodb";
9
- import { FilterValues, DataRepository, CollectionConfig, EntityReference, LogicalCondition } from "@rebasepro/types";
9
+ import { FilterValues, DataRepository, CollectionConfig, EntityReference, LogicalCondition, OrderByTuple } from "@rebasepro/types";
10
10
  import { MongoConditionBuilder } from "./MongoConditionBuilder";
11
11
  import { ApiError } from "@rebasepro/server";
12
12
 
@@ -195,7 +195,7 @@ export class MongoDataService implements DataRepository {
195
195
  filter?: FilterValues<Extract<keyof M, string>>;
196
196
  /** An `or(...)`/`and(...)` group, AND-ed with `filter`. */
197
197
  logical?: LogicalCondition;
198
- orderBy?: string;
198
+ orderBy?: string | OrderByTuple[];
199
199
  order?: "desc" | "asc";
200
200
  limit?: number;
201
201
  offset?: number;
@@ -253,7 +253,7 @@ export class MongoDataService implements DataRepository {
253
253
  searchString: string,
254
254
  options: {
255
255
  filter?: FilterValues<Extract<keyof M, string>>;
256
- orderBy?: string;
256
+ orderBy?: string | OrderByTuple[];
257
257
  order?: "desc" | "asc";
258
258
  limit?: number;
259
259
  databaseId?: string;
@@ -176,18 +176,13 @@ propertyCallbacks: undefined };
176
176
  * what every re-fetch reads — the wrapper used to stamp the field on the
177
177
  * `Subscription` object instead, and nothing has ever read that one.
178
178
  */
179
- listenCollection<M extends Record<string, any>>({
180
- path,
181
- collection,
182
- filter,
183
- limit,
184
- startAfter,
185
- orderBy,
186
- searchString,
187
- order,
188
- onUpdate,
189
- onError
190
- }: ListenCollectionProps<M>, authContext?: { uid: string; roles: string[] }): () => void {
179
+ listenCollection<M extends Record<string, any>>(
180
+ // `collection` is re-resolved from the registry on every re-fetch, and
181
+ // `vectorSearch` is not a thing a subscription can do — the Postgres
182
+ // service refuses it outright rather than run it once and never again.
183
+ { onUpdate, onError, collection, vectorSearch, ...query }: ListenCollectionProps<M>,
184
+ authContext?: { uid: string; roles: string[] }
185
+ ): () => void {
191
186
  const subscriptionId = this.generateSubscriptionId();
192
187
 
193
188
  const callback = (rows: Record<string, unknown>[]) => {
@@ -201,19 +196,15 @@ propertyCallbacks: undefined };
201
196
  }
202
197
  };
203
198
 
199
+ // Forwarded whole rather than re-listed, for the reason `fetchCollection`
200
+ // gives above: the hand-written list named seven of the eleven fields
201
+ // `ListenCollectionProps` declares, so `logical` and `offset` were
202
+ // accepted at every type-checked boundary and then dropped — an
203
+ // `or(...)` subscription was pushed every row, and a subscription to
204
+ // page two was pushed page one.
204
205
  this.realtimeService.subscribeToCollection(
205
206
  subscriptionId,
206
- {
207
- clientId: "driver",
208
- path,
209
- filter,
210
- orderBy,
211
- order,
212
- limit,
213
- startAfter,
214
- searchString,
215
- authContext
216
- },
207
+ { clientId: "driver", ...query, authContext },
217
208
  callback
218
209
  );
219
210