@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.
- package/dist/MongoBootstrapper.d.ts.map +1 -1
- package/dist/auth/ensure-collections.d.ts.map +1 -1
- package/dist/db/MongoConditionBuilder.d.ts +28 -1
- package/dist/db/MongoConditionBuilder.d.ts.map +1 -1
- package/dist/db/MongoDataService.d.ts +19 -1
- package/dist/db/MongoDataService.d.ts.map +1 -1
- package/dist/db/securityRuleFilter.d.ts +65 -0
- package/dist/db/securityRuleFilter.d.ts.map +1 -0
- package/dist/ensure-collections-Bl12bSwO.js +130 -0
- package/dist/ensure-collections-Bl12bSwO.js.map +1 -0
- package/dist/factory.d.ts +11 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/index.es.js +487 -249
- package/dist/index.es.js.map +1 -1
- package/dist/services/MongoDriver.d.ts +37 -5
- package/dist/services/MongoDriver.d.ts.map +1 -1
- package/dist/services/MongoRealtimeService.d.ts +23 -14
- package/dist/services/MongoRealtimeService.d.ts.map +1 -1
- package/dist/{websocket-74DDOdK0.js → websocket-Bs54-gR-.js} +32 -11
- package/dist/websocket-Bs54-gR-.js.map +1 -0
- package/dist/websocket.d.ts +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/MongoBootstrapper.ts +10 -2
- package/src/auth/ensure-collections.ts +61 -13
- package/src/db/MongoConditionBuilder.ts +112 -40
- package/src/db/MongoDataService.ts +51 -14
- package/src/db/securityRuleFilter.ts +398 -0
- package/src/factory.ts +17 -4
- package/src/services/MongoDriver.ts +101 -262
- package/src/services/MongoRealtimeService.ts +124 -66
- package/src/websocket.ts +66 -15
- package/dist/ensure-collections-Bkx_O5CQ.js +0 -94
- package/dist/ensure-collections-Bkx_O5CQ.js.map +0 -1
- 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
|
-
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 (
|
|
317
|
-
|
|
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:
|
|
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?:
|
|
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
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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 =
|
|
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:
|
|
129
|
-
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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"}
|