@rebasepro/server-mongo 0.13.0 → 0.13.1-canary.g06dbe5b

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.
Files changed (35) hide show
  1. package/dist/MongoBootstrapper.d.ts.map +1 -1
  2. package/dist/auth/ensure-collections.d.ts.map +1 -1
  3. package/dist/db/MongoConditionBuilder.d.ts +28 -1
  4. package/dist/db/MongoConditionBuilder.d.ts.map +1 -1
  5. package/dist/db/MongoDataService.d.ts +19 -1
  6. package/dist/db/MongoDataService.d.ts.map +1 -1
  7. package/dist/db/securityRuleFilter.d.ts +65 -0
  8. package/dist/db/securityRuleFilter.d.ts.map +1 -0
  9. package/dist/ensure-collections-Bl12bSwO.js +130 -0
  10. package/dist/ensure-collections-Bl12bSwO.js.map +1 -0
  11. package/dist/factory.d.ts +11 -1
  12. package/dist/factory.d.ts.map +1 -1
  13. package/dist/index.es.js +487 -249
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/services/MongoDriver.d.ts +37 -5
  16. package/dist/services/MongoDriver.d.ts.map +1 -1
  17. package/dist/services/MongoRealtimeService.d.ts +23 -14
  18. package/dist/services/MongoRealtimeService.d.ts.map +1 -1
  19. package/dist/{websocket-74DDOdK0.js → websocket-Bs54-gR-.js} +32 -11
  20. package/dist/websocket-Bs54-gR-.js.map +1 -0
  21. package/dist/websocket.d.ts +2 -2
  22. package/dist/websocket.d.ts.map +1 -1
  23. package/package.json +5 -5
  24. package/src/MongoBootstrapper.ts +10 -2
  25. package/src/auth/ensure-collections.ts +61 -13
  26. package/src/db/MongoConditionBuilder.ts +112 -40
  27. package/src/db/MongoDataService.ts +51 -14
  28. package/src/db/securityRuleFilter.ts +398 -0
  29. package/src/factory.ts +17 -4
  30. package/src/services/MongoDriver.ts +101 -262
  31. package/src/services/MongoRealtimeService.ts +124 -66
  32. package/src/websocket.ts +66 -15
  33. package/dist/ensure-collections-Bkx_O5CQ.js +0 -94
  34. package/dist/ensure-collections-Bkx_O5CQ.js.map +0 -1
  35. package/dist/websocket-74DDOdK0.js.map +0 -1
@@ -7,25 +7,39 @@
7
7
 
8
8
  import { Db, ChangeStream, ChangeStreamDocument, Document, ObjectId } from "mongodb";
9
9
  import {
10
+ ANONYMOUS_USER_ID,
11
+ DataDriver,
10
12
  FilterValues,
11
13
  RealtimeProvider,
12
14
  CollectionSubscriptionConfig,
13
15
  SingleSubscriptionConfig,
14
16
  WebSocketMessage,
15
- User
17
+ User,
18
+ ListLimitError,
19
+ resolveClientListLimit
16
20
  } from "@rebasepro/types";
17
21
  import { WebSocket } from "ws";
18
- import { MongoDataService } from "../db/MongoDataService";
19
22
 
20
23
  import type { MongoDriver } from "./MongoDriver";
21
24
  import { logger } from "@rebasepro/server";
22
25
 
26
+ /** The acting user for a subscription, as the driver and the socket carry it. */
27
+ export interface SubscriptionAuthContext {
28
+ uid: string;
29
+ roles: string[];
30
+ }
31
+
23
32
  interface Subscription {
24
33
  type: "collection" | "single";
25
- config: CollectionSubscriptionConfig | SingleSubscriptionConfig;
34
+ /**
35
+ * Carries `authContext`. There is deliberately no second copy on this
36
+ * object: every fetch reads `config.authContext`, and the one that used to
37
+ * live here was written from three places and read from none — so a
38
+ * subscription that looked authorized was re-fetched as nobody.
39
+ */
40
+ config: (CollectionSubscriptionConfig | SingleSubscriptionConfig) & { authContext?: SubscriptionAuthContext };
26
41
  changeStream?: ChangeStream;
27
42
  callback?: (data: any) => void;
28
- authContext?: { uid: string; roles: string[] };
29
43
  }
30
44
 
31
45
  /**
@@ -37,12 +51,9 @@ interface Subscription {
37
51
  export class MongoRealtimeService implements RealtimeProvider {
38
52
  private subscriptions = new Map<string, Subscription>();
39
53
  private clients = new Map<string, WebSocket>();
40
- private dataService: MongoDataService;
41
54
  private driver?: MongoDriver;
42
55
 
43
- constructor(private db: Db) {
44
- this.dataService = new MongoDataService(db);
45
- }
56
+ constructor(private db: Db) {}
46
57
 
47
58
  setDataDriver(driver: MongoDriver) {
48
59
  this.driver = driver;
@@ -60,7 +71,7 @@ export class MongoRealtimeService implements RealtimeProvider {
60
71
  */
61
72
  subscribeToCollection(
62
73
  subscriptionId: string,
63
- config: CollectionSubscriptionConfig & { authContext?: { uid: string; roles: string[] } },
74
+ config: CollectionSubscriptionConfig & { authContext?: SubscriptionAuthContext },
64
75
  callback?: (rows: Record<string, unknown>[]) => void
65
76
  ): void {
66
77
  // Clean up existing subscription if any
@@ -89,8 +100,7 @@ export class MongoRealtimeService implements RealtimeProvider {
89
100
  type: "collection",
90
101
  config,
91
102
  changeStream,
92
- callback,
93
- authContext: config.authContext
103
+ callback
94
104
  };
95
105
 
96
106
  this.subscriptions.set(subscriptionId, subscription);
@@ -117,8 +127,7 @@ export class MongoRealtimeService implements RealtimeProvider {
117
127
  const subscription: Subscription = {
118
128
  type: "collection",
119
129
  config,
120
- callback,
121
- authContext: config.authContext
130
+ callback
122
131
  };
123
132
 
124
133
  this.subscriptions.set(subscriptionId, subscription);
@@ -133,38 +142,29 @@ export class MongoRealtimeService implements RealtimeProvider {
133
142
  */
134
143
  private async fetchAndNotifyCollection(
135
144
  subscriptionId: string,
136
- config: CollectionSubscriptionConfig & { authContext?: { uid: string; roles: string[] } },
145
+ config: CollectionSubscriptionConfig & { authContext?: SubscriptionAuthContext },
137
146
  callback?: (rows: Record<string, unknown>[]) => void
138
147
  ): Promise<void> {
139
148
  try {
140
- let rows;
141
149
  const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
142
-
143
- if (config.authContext && this.driver) {
144
- const mockUser = { uid: config.authContext.uid,
145
- roles: config.authContext.roles } as User;
146
- const authenticatedDriver = await this.driver.withAuth(mockUser);
147
- rows = await authenticatedDriver.fetchCollection({
148
- path: config.path,
149
- collection: registryCollection,
150
- filter: config.filter as FilterValues<string> | undefined,
151
- orderBy: config.orderBy,
152
- order: config.order,
153
- limit: config.limit,
154
- startAfter: config.startAfter,
155
- searchString: config.searchString
156
- });
157
- } else {
158
- rows = await this.dataService.fetchCollection(config.path, {
159
- filter: config.filter as FilterValues<string> | undefined,
160
- orderBy: config.orderBy,
161
- order: config.order,
162
- limit: config.limit,
163
- startAfter: config.startAfter,
164
- searchString: config.searchString,
165
- collection: registryCollection
166
- });
167
- }
150
+ // One path, authenticated or not. The `else` branch used to reach
151
+ // past the driver into the repository, which applies no security
152
+ // rules at all — the fallback stubbing out the contract the primary
153
+ // branch honours, and granting more while doing it. An anonymous
154
+ // subscriber is now a user like any other: rules are evaluated
155
+ // against the anonymous uid, and a rule that needs a real one
156
+ // matches nothing.
157
+ const driver = await this.scopedDriver(config.authContext);
158
+ const rows = await driver.fetchCollection({
159
+ path: config.path,
160
+ collection: registryCollection,
161
+ filter: config.filter as FilterValues<string> | undefined,
162
+ orderBy: config.orderBy,
163
+ order: config.order,
164
+ limit: config.limit,
165
+ startAfter: config.startAfter,
166
+ searchString: config.searchString
167
+ });
168
168
 
169
169
  if (callback) {
170
170
  callback(rows);
@@ -174,12 +174,27 @@ roles: config.authContext.roles } as User;
174
174
  }
175
175
  }
176
176
 
177
+ /**
178
+ * The driver scoped to a subscriber.
179
+ *
180
+ * Never the bare repository: everything a subscription delivers has to pass
181
+ * the same row authorization an HTTP read does.
182
+ */
183
+ private async scopedDriver(authContext?: SubscriptionAuthContext): Promise<DataDriver> {
184
+ if (!this.driver) {
185
+ throw new Error("MongoRealtimeService has no data driver — subscriptions cannot be authorized");
186
+ }
187
+ const user = { uid: authContext?.uid ?? ANONYMOUS_USER_ID,
188
+ roles: authContext?.roles ?? [] } as User;
189
+ return this.driver.withAuth(user);
190
+ }
191
+
177
192
  /**
178
193
  * Subscribe to single row changes
179
194
  */
180
195
  subscribeToOne(
181
196
  subscriptionId: string,
182
- config: SingleSubscriptionConfig & { authContext?: { uid: string; roles: string[] } },
197
+ config: SingleSubscriptionConfig & { authContext?: SubscriptionAuthContext },
183
198
  callback?: (row: Record<string, unknown> | null) => void
184
199
  ): void {
185
200
  // Clean up existing subscription if any
@@ -211,8 +226,7 @@ roles: config.authContext.roles } as User;
211
226
  type: "single",
212
227
  config,
213
228
  changeStream,
214
- callback,
215
- authContext: config.authContext
229
+ callback
216
230
  };
217
231
 
218
232
  this.subscriptions.set(subscriptionId, subscription);
@@ -241,8 +255,7 @@ roles: config.authContext.roles } as User;
241
255
  const subscription: Subscription = {
242
256
  type: "single",
243
257
  config,
244
- callback,
245
- authContext: config.authContext
258
+ callback
246
259
  };
247
260
 
248
261
  this.subscriptions.set(subscriptionId, subscription);
@@ -257,25 +270,17 @@ roles: config.authContext.roles } as User;
257
270
  */
258
271
  private async fetchAndNotifyOne(
259
272
  subscriptionId: string,
260
- config: SingleSubscriptionConfig & { authContext?: { uid: string; roles: string[] } },
273
+ config: SingleSubscriptionConfig & { authContext?: SubscriptionAuthContext },
261
274
  callback?: (row: Record<string, unknown> | null) => void
262
275
  ): Promise<void> {
263
276
  try {
264
- let row;
265
277
  const registryCollection = this.driver?.registry?.getCollectionByPath(config.path);
266
-
267
- if (config.authContext && this.driver) {
268
- const mockUser = { uid: config.authContext.uid,
269
- roles: config.authContext.roles } as User;
270
- const authenticatedDriver = await this.driver.withAuth(mockUser);
271
- row = await authenticatedDriver.fetchOne({
272
- path: config.path,
273
- id: config.id,
274
- collection: registryCollection
275
- });
276
- } else {
277
- row = await this.dataService.fetchOne(config.path, config.id);
278
- }
278
+ const driver = await this.scopedDriver(config.authContext);
279
+ const row = await driver.fetchOne({
280
+ path: config.path,
281
+ id: config.id,
282
+ collection: registryCollection
283
+ });
279
284
 
280
285
  if (callback) {
281
286
  callback(row || null);
@@ -311,14 +316,21 @@ roles: config.authContext.roles } as User;
311
316
  // Find all subscriptions that might be affected by this update
312
317
  for (const [subscriptionId, subscription] of this.subscriptions) {
313
318
  if (subscription.type === "single") {
314
- const config = subscription.config as SingleSubscriptionConfig;
319
+ const config = subscription.config as SingleSubscriptionConfig & { authContext?: SubscriptionAuthContext };
315
320
  if (config.path === path && config.id.toString() === id) {
316
- if (subscription.callback) {
317
- subscription.callback(row);
321
+ if (row === null) {
322
+ // A deletion carries no row to authorize.
323
+ subscription.callback?.(null);
324
+ } else {
325
+ // Re-fetched through the subscriber's own driver rather
326
+ // than pushed verbatim: `notifyUpdate` runs after every
327
+ // save, and handing it the row as written broadcast any
328
+ // document to whoever happened to be watching its id.
329
+ await this.fetchAndNotifyOne(subscriptionId, config, subscription.callback);
318
330
  }
319
331
  }
320
332
  } else if (subscription.type === "collection") {
321
- const config = subscription.config as CollectionSubscriptionConfig;
333
+ const config = subscription.config as CollectionSubscriptionConfig & { authContext?: SubscriptionAuthContext };
322
334
  if (config.path === path) {
323
335
  // Re-fetch the collection to get updated data
324
336
  await this.fetchAndNotifyCollection(subscriptionId, config, subscription.callback);
@@ -389,6 +401,28 @@ roles: (_authContext.roles ?? []).map(String) } : undefined;
389
401
  const subscriptionId = message.payload?.subscriptionId ?? message.subscriptionId;
390
402
  if (!subscriptionId) return;
391
403
 
404
+ // The same list bound the Postgres socket and every REST route
405
+ // apply. This ingress applied none: an absent limit reached the
406
+ // driver as `undefined` and emitted no `.limit()` at all, so one
407
+ // subscribe frame streamed the whole collection — and re-streamed
408
+ // it on every matching write. An over-large limit is refused
409
+ // rather than shrunk, because a `collection_update` frame carries
410
+ // no `total` or `hasMore` for the client to notice with.
411
+ let boundedLimit: number;
412
+ try {
413
+ boundedLimit = resolveClientListLimit(message.payload?.limit);
414
+ } catch (e) {
415
+ if (!(e instanceof ListLimitError)) throw e;
416
+ logger.warn(`⚠️ [MongoRealtime] Refused subscription to '${message.payload?.path}': ${e.message}`);
417
+ ws.send(JSON.stringify({
418
+ type: "ERROR",
419
+ subscriptionId,
420
+ payload: { error: { message: e.message, code: "INVALID_LIMIT" } },
421
+ error: e.message
422
+ }));
423
+ return;
424
+ }
425
+
392
426
  this.subscribeToCollection(
393
427
  subscriptionId,
394
428
  {
@@ -397,7 +431,7 @@ roles: (_authContext.roles ?? []).map(String) } : undefined;
397
431
  filter: message.payload?.filter,
398
432
  orderBy: message.payload?.orderBy,
399
433
  order: message.payload?.order,
400
- limit: message.payload?.limit,
434
+ limit: boundedLimit,
401
435
  startAfter: message.payload?.startAfter,
402
436
  searchString: message.payload?.searchString,
403
437
  authContext
@@ -441,6 +475,30 @@ roles: (_authContext.roles ?? []).map(String) } : undefined;
441
475
  }
442
476
  break;
443
477
  }
478
+ default: {
479
+ // A silent `switch` over a wire protocol is how channel,
480
+ // presence and broadcast frames came to be accepted here and
481
+ // dropped: the client's `broadcast()` resolved, `onPresence`
482
+ // never fired, and a `channel_history` request buffered live
483
+ // messages until the catch-up timeout on every join. Say so,
484
+ // and tell the sender rather than leaving it waiting.
485
+ logger.warn(
486
+ `⚠️ [MongoRealtime] Unhandled realtime message type "${message.type}" — ` +
487
+ "channels, presence and broadcast are not implemented by the Mongo driver."
488
+ );
489
+ ws.send(JSON.stringify({
490
+ type: "ERROR",
491
+ subscriptionId: message.subscriptionId,
492
+ payload: {
493
+ error: {
494
+ message: `Realtime message type "${message.type}" is not supported by the Mongo driver`,
495
+ code: "REALTIME_UNSUPPORTED"
496
+ }
497
+ },
498
+ error: `Realtime message type "${message.type}" is not supported by the Mongo driver`
499
+ }));
500
+ break;
501
+ }
444
502
  }
445
503
  }
446
504
  }
package/src/websocket.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User } from "@rebasepro/types";
1
+ import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User, AuthAdapter } from "@rebasepro/types";
2
2
  import { WebSocketServer, WebSocket } from "ws";
3
3
  import { Server } from "http";
4
4
  import { inspect } from "util";
5
- import type { AccessTokenPayload } from "@rebasepro/server";
6
5
  import { extractUserFromToken, resolveRequireAuth } from "@rebasepro/server";
7
6
  import type { RebaseAuthConfig } from "@rebasepro/server";
8
7
  import { MongoRealtimeService } from "./services/MongoRealtimeService";
@@ -17,9 +16,22 @@ function isDriverWithAuth(driver: DataDriver): driver is DriverWithAuth {
17
16
  return "withAuth" in driver && typeof (driver as Record<string, unknown>).withAuth === "function";
18
17
  }
19
18
 
19
+ /**
20
+ * Normalized user identity for WebSocket sessions — the same shape the Postgres
21
+ * socket keeps, because an `AuthAdapter` user is not an access-token payload.
22
+ */
23
+ interface WsUserIdentity {
24
+ uid: string;
25
+ email?: string;
26
+ displayName?: string;
27
+ photoURL?: string;
28
+ roles: string[];
29
+ isAdmin: boolean;
30
+ }
31
+
20
32
  interface ClientSession {
21
33
  ws: WebSocket;
22
- user?: AccessTokenPayload;
34
+ user?: WsUserIdentity;
23
35
  authenticated: boolean;
24
36
  messageCount: number;
25
37
  messageWindowStart: number;
@@ -41,8 +53,11 @@ const ADMIN_ONLY_TYPES = new Set([
41
53
  ]);
42
54
 
43
55
  function isAdminSession(session: ClientSession | undefined): boolean {
44
- if (!session?.user?.roles) return false;
45
- return session.user.roles.includes("admin");
56
+ if (!session?.user) return false;
57
+ // The adapter's own answer first; a role *named* `admin` is only the
58
+ // fallback for the built-in JWT path.
59
+ if (session.user.isAdmin) return true;
60
+ return (session.user.roles ?? []).some((r) => r === "admin");
46
61
  }
47
62
 
48
63
  export function createMongoWebSocket(
@@ -50,7 +65,8 @@ export function createMongoWebSocket(
50
65
  realtimeService: MongoRealtimeService,
51
66
  driver: MongoDriver,
52
67
  authConfig?: RebaseAuthConfig,
53
- admin?: DatabaseAdmin
68
+ admin?: DatabaseAdmin,
69
+ authAdapter?: AuthAdapter
54
70
  ) {
55
71
  // Scoped to this factory invocation rather than the module, so sessions do
56
72
  // not leak across hot reloads or a second server on the same process — the
@@ -70,12 +86,12 @@ export function createMongoWebSocket(
70
86
  // `resolveRequireAuth` for what this socket's local copy got wrong — most
71
87
  // importantly that a `false` here does not skip a check, it marks every
72
88
  // session `authenticated` at connect time.
73
- const requireAuth = resolveRequireAuth(authConfig);
89
+ const requireAuth = !!authAdapter || resolveRequireAuth(authConfig);
74
90
 
75
- if (requireAuth && !authConfig?.jwtSecret) {
91
+ if (requireAuth && !authAdapter && !authConfig?.jwtSecret) {
76
92
  logger.warn(
77
- "🔐 [WebSocket Server] Authentication is required but no jwtSecret is configured — " +
78
- "no client can complete AUTH, so every realtime message will be refused."
93
+ "🔐 [WebSocket Server] Authentication is required but no adapter or jwtSecret is " +
94
+ "configured — no client can complete AUTH, so every realtime message will be refused."
79
95
  );
80
96
  }
81
97
 
@@ -116,17 +132,52 @@ code } } }));
116
132
  return;
117
133
  }
118
134
 
119
- const user = extractUserFromToken(token);
120
- if (user) {
135
+ // The adapter verifies when one is configured, exactly as the
136
+ // HTTP routes do; the built-in JWT path is the fallback.
137
+ let verifiedUser: WsUserIdentity | null = null;
138
+
139
+ if (authAdapter) {
140
+ try {
141
+ const adapterUser = authAdapter.verifyToken
142
+ ? await authAdapter.verifyToken(token)
143
+ : await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", {
144
+ headers: { Authorization: `Bearer ${token}` }
145
+ }));
146
+ if (adapterUser) {
147
+ verifiedUser = {
148
+ uid: adapterUser.uid,
149
+ email: adapterUser.email,
150
+ roles: adapterUser.roles ?? [],
151
+ isAdmin: !!adapterUser.isAdmin
152
+ };
153
+ }
154
+ } catch {
155
+ // Adapter threw — treat as invalid token
156
+ }
157
+ } else {
158
+ const jwtPayload = extractUserFromToken(token);
159
+ if (jwtPayload) {
160
+ verifiedUser = {
161
+ uid: jwtPayload.uid,
162
+ email: jwtPayload.email,
163
+ displayName: jwtPayload.displayName,
164
+ photoURL: jwtPayload.photoURL,
165
+ roles: jwtPayload.roles ?? [],
166
+ isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === "admin")
167
+ };
168
+ }
169
+ }
170
+
171
+ if (verifiedUser) {
121
172
  const session = clientSessions.get(clientId);
122
173
  if (session) {
123
- session.user = user;
174
+ session.user = verifiedUser;
124
175
  session.authenticated = true;
125
176
  }
126
177
  ws.send(JSON.stringify({ type: "AUTH_SUCCESS",
127
178
  requestId,
128
- payload: { uid: user.uid,
129
- roles: user.roles } }));
179
+ payload: { uid: verifiedUser.uid,
180
+ roles: verifiedUser.roles } }));
130
181
  } else {
131
182
  sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
132
183
  }
@@ -1,94 +0,0 @@
1
- import { logger } from "@rebasepro/server";
2
- //#region src/auth/ensure-collections.ts
3
- /**
4
- * Default roles to seed on first run
5
- */
6
- var DEFAULT_ROLES = [
7
- {
8
- _id: "admin",
9
- id: "admin",
10
- name: "Admin",
11
- isAdmin: true,
12
- defaultPermissions: {
13
- read: true,
14
- create: true,
15
- edit: true,
16
- delete: true
17
- },
18
- createdAt: /* @__PURE__ */ new Date()
19
- },
20
- {
21
- _id: "editor",
22
- id: "editor",
23
- name: "Editor",
24
- isAdmin: false,
25
- defaultPermissions: {
26
- read: true,
27
- create: true,
28
- edit: true,
29
- delete: true
30
- },
31
- createdAt: /* @__PURE__ */ new Date()
32
- },
33
- {
34
- _id: "viewer",
35
- id: "viewer",
36
- name: "Viewer",
37
- isAdmin: false,
38
- defaultPermissions: {
39
- read: true,
40
- create: false,
41
- edit: false,
42
- delete: false
43
- },
44
- createdAt: /* @__PURE__ */ new Date()
45
- }
46
- ];
47
- async function ensureAuthCollectionsExist(db) {
48
- logger.info("🔍 Checking MongoDB auth collections and indexes...");
49
- try {
50
- await db.collection("rebase_users").createIndex({ email: 1 }, { unique: true });
51
- const identities = db.collection("rebase_user_identities");
52
- await identities.createIndex({
53
- provider: 1,
54
- provider_id: 1
55
- }, { unique: true });
56
- await identities.createIndex({ user_id: 1 });
57
- const userRoles = db.collection("rebase_user_roles");
58
- await userRoles.createIndex({
59
- user_id: 1,
60
- role_id: 1
61
- }, { unique: true });
62
- await userRoles.createIndex({ user_id: 1 });
63
- const refreshTokens = db.collection("rebase_refresh_tokens");
64
- await refreshTokens.createIndex({ token_hash: 1 }, { unique: true });
65
- await refreshTokens.createIndex({
66
- user_id: 1,
67
- user_agent: 1,
68
- ip_address: 1
69
- }, { unique: true });
70
- await refreshTokens.createIndex({ user_id: 1 });
71
- const resetTokens = db.collection("rebase_password_reset_tokens");
72
- await resetTokens.createIndex({ token_hash: 1 }, { unique: true });
73
- await resetTokens.createIndex({ user_id: 1 });
74
- await seedDefaultRoles(db);
75
- logger.info("✅ MongoDB Auth collections ready");
76
- } catch (error) {
77
- logger.error("❌ Failed to set up MongoDB auth collections", { error });
78
- }
79
- }
80
- async function seedDefaultRoles(db) {
81
- const roles = db.collection("rebase_roles");
82
- const count = await roles.countDocuments();
83
- if (count > 0) {
84
- logger.info(`📋 Found ${count} existing roles`);
85
- return;
86
- }
87
- logger.info("🌱 Seeding default roles...");
88
- for (const role of DEFAULT_ROLES) await roles.updateOne({ id: role.id }, { $setOnInsert: role }, { upsert: true });
89
- logger.info("✅ Default roles created: admin, editor, viewer");
90
- }
91
- //#endregion
92
- export { ensureAuthCollectionsExist };
93
-
94
- //# sourceMappingURL=ensure-collections-Bkx_O5CQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ensure-collections-Bkx_O5CQ.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\nexport async function ensureAuthCollectionsExist(db: Db): Promise<void> {\n logger.info(\"🔍 Checking MongoDB auth collections and indexes...\");\n\n try {\n // Users\n const users = db.collection(\"rebase_users\");\n await users.createIndex({ email: 1 }, { unique: true });\n\n // User Identities\n const identities = db.collection(\"rebase_user_identities\");\n await identities.createIndex({ provider: 1,\nprovider_id: 1 }, { unique: true });\n await identities.createIndex({ user_id: 1 });\n\n // User Roles (junction collection)\n const userRoles = db.collection(\"rebase_user_roles\");\n await userRoles.createIndex({ user_id: 1,\nrole_id: 1 }, { unique: true });\n await userRoles.createIndex({ user_id: 1 });\n\n // Refresh Tokens\n const refreshTokens = db.collection(\"rebase_refresh_tokens\");\n await refreshTokens.createIndex({ token_hash: 1 }, { unique: true });\n await refreshTokens.createIndex({ user_id: 1,\nuser_agent: 1,\nip_address: 1 }, { unique: true });\n await refreshTokens.createIndex({ user_id: 1 });\n\n // Password Reset Tokens\n const resetTokens = db.collection(\"rebase_password_reset_tokens\");\n await resetTokens.createIndex({ token_hash: 1 }, { unique: true });\n await resetTokens.createIndex({ user_id: 1 });\n\n // Seed roles\n await seedDefaultRoles(db);\n\n logger.info(\"✅ MongoDB Auth collections ready\");\n } catch (error) {\n logger.error(\"❌ Failed to set up MongoDB auth collections\", { error: error });\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;AAEA,eAAsB,2BAA2B,IAAuB;CACpE,OAAO,KAAK,qDAAqD;CAEjE,IAAI;EAGA,MADc,GAAG,WAAW,cACtB,CAAA,CAAM,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EAGtD,MAAM,aAAa,GAAG,WAAW,wBAAwB;EACzD,MAAM,WAAW,YAAY;GAAE,UAAU;GACjD,aAAa;EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EAC1B,MAAM,WAAW,YAAY,EAAE,SAAS,EAAE,CAAC;EAG3C,MAAM,YAAY,GAAG,WAAW,mBAAmB;EACnD,MAAM,UAAU,YAAY;GAAE,SAAS;GAC/C,SAAS;EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACtB,MAAM,UAAU,YAAY,EAAE,SAAS,EAAE,CAAC;EAG1C,MAAM,gBAAgB,GAAG,WAAW,uBAAuB;EAC3D,MAAM,cAAc,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACnE,MAAM,cAAc,YAAY;GAAE,SAAS;GACnD,YAAY;GACZ,YAAY;EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACzB,MAAM,cAAc,YAAY,EAAE,SAAS,EAAE,CAAC;EAG9C,MAAM,cAAc,GAAG,WAAW,8BAA8B;EAChE,MAAM,YAAY,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;EACjE,MAAM,YAAY,YAAY,EAAE,SAAS,EAAE,CAAC;EAG5C,MAAM,iBAAiB,EAAE;EAEzB,OAAO,KAAK,kCAAkC;CAClD,SAAS,OAAO;EACZ,OAAO,MAAM,+CAA+C,EAAS,MAAM,CAAC;CAChF;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-74DDOdK0.js","names":[],"sources":["../src/websocket.ts"],"sourcesContent":["import { RealtimeProvider, DataDriver, FetchCollectionProps, FetchOneProps, SaveProps, DeleteProps, TableMetadata, DatabaseAdmin, isSchemaAdmin, isDocumentAdmin, User } from \"@rebasepro/types\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport type { AccessTokenPayload } from \"@rebasepro/server\";\nimport { extractUserFromToken, resolveRequireAuth } 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\ninterface ClientSession {\n ws: WebSocket;\n user?: AccessTokenPayload;\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?.roles) return false;\n return session.user.roles.includes(\"admin\");\n}\n\nexport function createMongoWebSocket(\n server: Server,\n realtimeService: MongoRealtimeService,\n driver: MongoDriver,\n authConfig?: RebaseAuthConfig,\n admin?: DatabaseAdmin\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 = resolveRequireAuth(authConfig);\n\n if (requireAuth && !authConfig?.jwtSecret) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no jwtSecret is configured — \" +\n \"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 const user = extractUserFromToken(token);\n if (user) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = user;\n session.authenticated = true;\n }\n ws.send(JSON.stringify({ type: \"AUTH_SUCCESS\",\nrequestId,\npayload: { uid: user.uid,\nroles: user.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 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 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 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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"}