@rebasepro/server-postgres 0.12.0 → 0.12.1-canary.gdfba2a1

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.
@@ -0,0 +1,528 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import process from "process";
3
+ __createRequire(import.meta.url);
4
+ import { s as __exportAll } from "./connection-B5Wndbr1.js";
5
+ import "./src-DoU9yPqq.js";
6
+ import { extractUserFromToken, logger, safeCompare } from "@rebasepro/server";
7
+ import { WebSocketServer } from "ws";
8
+ import { inspect } from "util";
9
+ //#region ../types/src/types/backend.ts
10
+ /**
11
+ * Type guard: does this admin support SQL operations?
12
+ * @group Admin
13
+ */
14
+ function isSQLAdmin(admin) {
15
+ return !!admin && typeof admin.executeSql === "function";
16
+ }
17
+ /**
18
+ * Type guard: does this admin support schema management?
19
+ * @group Admin
20
+ */
21
+ function isSchemaAdmin(admin) {
22
+ return !!admin && (typeof admin.fetchUnmappedTables === "function" || typeof admin.fetchTableMetadata === "function");
23
+ }
24
+ //#endregion
25
+ //#region src/websocket.ts
26
+ var websocket_exports = /* @__PURE__ */ __exportAll({ createPostgresWebSocket: () => createPostgresWebSocket });
27
+ /** Maximum messages per client per window */
28
+ var WS_RATE_LIMIT = 2e3;
29
+ /** Rate limit window in milliseconds (60 seconds) */
30
+ var WS_RATE_WINDOW_MS = 6e4;
31
+ /** Admin-only WebSocket message types */
32
+ var ADMIN_ONLY_TYPES = /* @__PURE__ */ new Set([
33
+ "EXECUTE_SQL",
34
+ "FETCH_DATABASES",
35
+ "FETCH_ROLES",
36
+ "FETCH_UNMAPPED_TABLES",
37
+ "FETCH_TABLE_METADATA",
38
+ "FETCH_CURRENT_DATABASE",
39
+ "CREATE_BRANCH",
40
+ "DELETE_BRANCH",
41
+ "LIST_BRANCHES"
42
+ ]);
43
+ /**
44
+ * Recursively extract the deepest error message from an error's cause chain (e.g., Drizzle wrapping a PG error).
45
+ */
46
+ function extractErrorMessage(error) {
47
+ if (!error) return "Unknown error";
48
+ if (error instanceof Error) {
49
+ if ("cause" in error && error.cause) return extractErrorMessage(error.cause);
50
+ return error.message;
51
+ }
52
+ if (typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
53
+ return String(error);
54
+ }
55
+ /**
56
+ * Check if the current session belongs to an admin user.
57
+ */
58
+ function isAdminSession(session) {
59
+ if (!session?.user) return false;
60
+ if (session.user.isAdmin) return true;
61
+ if (!session.user.roles) return false;
62
+ return session.user.roles.some((r) => r === "admin");
63
+ }
64
+ function createPostgresWebSocket(server, realtimeService, driver, authConfig, authAdapter) {
65
+ const clientSessions = /* @__PURE__ */ new Map();
66
+ const isProduction = process.env.NODE_ENV === "production";
67
+ /** Debug logger that is suppressed in production to prevent PII/data leaks */
68
+ const wsDebug = (...args) => {
69
+ if (!isProduction) console.debug(...args);
70
+ };
71
+ const wss = new WebSocketServer({ server });
72
+ wss.on("error", (err) => {
73
+ if (err.code === "EADDRINUSE") return;
74
+ logger.error("❌ [WebSocket Server] Error", { error: err });
75
+ });
76
+ const requireAuth = authAdapter ? true : authConfig?.requireAuth !== false && !!authConfig?.jwtSecret;
77
+ wss.on("connection", (ws) => {
78
+ const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
79
+ wsDebug(`WebSocket client connected: ${clientId}`);
80
+ clientSessions.set(clientId, {
81
+ ws,
82
+ authenticated: !requireAuth,
83
+ messageCount: 0,
84
+ messageWindowStart: Date.now()
85
+ });
86
+ realtimeService.addClient(clientId, ws);
87
+ ws.on("close", () => {
88
+ wsDebug(`WebSocket client disconnected: ${clientId}`);
89
+ clientSessions.delete(clientId);
90
+ });
91
+ ws.on("message", async (message) => {
92
+ let requestId;
93
+ try {
94
+ const { type, payload, requestId: reqId } = JSON.parse(message.toString());
95
+ requestId = reqId;
96
+ wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : "");
97
+ const sendError = (errType, code, msg) => {
98
+ ws.send(JSON.stringify({
99
+ type: errType,
100
+ requestId,
101
+ payload: { error: {
102
+ message: msg,
103
+ code
104
+ } }
105
+ }));
106
+ };
107
+ if (type === "AUTHENTICATE") {
108
+ const { token } = payload || {};
109
+ if (!token) {
110
+ sendError("AUTH_ERROR", "INVALID_INPUT", "Token is required");
111
+ return;
112
+ }
113
+ let verifiedUser = null;
114
+ if (authAdapter) try {
115
+ const adapterUser = authAdapter.verifyToken ? await authAdapter.verifyToken(token) : await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", { headers: { Authorization: `Bearer ${token}` } }));
116
+ if (adapterUser) verifiedUser = {
117
+ uid: adapterUser.uid,
118
+ roles: adapterUser.roles,
119
+ isAdmin: adapterUser.isAdmin
120
+ };
121
+ } catch {}
122
+ else if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) verifiedUser = {
123
+ uid: "service",
124
+ roles: ["admin"],
125
+ isAdmin: true
126
+ };
127
+ else {
128
+ const jwtPayload = extractUserFromToken(token);
129
+ if (jwtPayload) verifiedUser = {
130
+ uid: jwtPayload.uid,
131
+ roles: jwtPayload.roles ?? [],
132
+ isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin")
133
+ };
134
+ }
135
+ if (verifiedUser) {
136
+ const session = clientSessions.get(clientId);
137
+ if (session) {
138
+ session.user = verifiedUser;
139
+ session.authenticated = true;
140
+ }
141
+ wsDebug(`[WS] replying AUTH_SUCCESS for requestId ${requestId}`);
142
+ ws.send(JSON.stringify({
143
+ type: "AUTH_SUCCESS",
144
+ requestId,
145
+ payload: {
146
+ uid: verifiedUser.uid,
147
+ roles: verifiedUser.roles
148
+ }
149
+ }));
150
+ wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.uid}`);
151
+ } else {
152
+ wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);
153
+ sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
154
+ }
155
+ return;
156
+ }
157
+ if (requireAuth) {
158
+ if (!clientSessions.get(clientId)?.authenticated) {
159
+ sendError("ERROR", "UNAUTHORIZED", "Authentication required");
160
+ return;
161
+ }
162
+ }
163
+ {
164
+ const session = clientSessions.get(clientId);
165
+ if (session) {
166
+ const now = Date.now();
167
+ if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {
168
+ session.messageCount = 0;
169
+ session.messageWindowStart = now;
170
+ }
171
+ session.messageCount++;
172
+ if (session.messageCount > WS_RATE_LIMIT) {
173
+ sendError("ERROR", "RATE_LIMITED", "Too many requests. Please slow down.");
174
+ return;
175
+ }
176
+ }
177
+ }
178
+ if (ADMIN_ONLY_TYPES.has(type)) {
179
+ if (!isAdminSession(clientSessions.get(clientId))) {
180
+ sendError("ERROR", "FORBIDDEN", "Admin access required for this operation");
181
+ return;
182
+ }
183
+ }
184
+ const getScopedDelegate = async () => {
185
+ const session = clientSessions.get(clientId);
186
+ if (typeof driver.withAuth === "function") try {
187
+ const userForAuth = session?.user ? {
188
+ uid: session.user.uid,
189
+ displayName: null,
190
+ email: null,
191
+ photoURL: null,
192
+ providerId: "websocket",
193
+ isAnonymous: false,
194
+ roles: session.user.roles ?? []
195
+ } : {
196
+ uid: "anon",
197
+ displayName: null,
198
+ email: null,
199
+ photoURL: null,
200
+ providerId: "websocket",
201
+ isAnonymous: true,
202
+ roles: ["anon"]
203
+ };
204
+ return await driver.withAuth(userForAuth);
205
+ } catch (e) {
206
+ logger.error("Failed to create RLS scoped delegate for WS request", { error: e });
207
+ throw new Error("Internal authentication error");
208
+ }
209
+ return driver;
210
+ };
211
+ switch (type) {
212
+ case "FETCH_COLLECTION":
213
+ {
214
+ wsDebug("📋 [WebSocket Server] Processing FETCH_COLLECTION request");
215
+ const request = payload;
216
+ const rows = await (await getScopedDelegate()).fetchCollection(request);
217
+ wsDebug("📋 [WebSocket Server] FETCH_COLLECTION result - rows count:", rows.length);
218
+ const response = {
219
+ type: "FETCH_COLLECTION_SUCCESS",
220
+ payload: { rows },
221
+ requestId
222
+ };
223
+ wsDebug("📋 [WebSocket Server] Sending FETCH_COLLECTION_SUCCESS response");
224
+ ws.send(JSON.stringify(response));
225
+ }
226
+ break;
227
+ case "FETCH_ONE":
228
+ {
229
+ wsDebug("📄 [WebSocket Server] Processing FETCH_ENTITY request");
230
+ const request = payload;
231
+ const row = await (await getScopedDelegate()).fetchOne(request);
232
+ wsDebug("📄 [WebSocket Server] FETCH_ENTITY result:", row);
233
+ const response = {
234
+ type: "FETCH_ONE_SUCCESS",
235
+ payload: { row: row ?? null },
236
+ requestId
237
+ };
238
+ wsDebug("📄 [WebSocket Server] Sending FETCH_ENTITY_SUCCESS response");
239
+ ws.send(JSON.stringify(response));
240
+ }
241
+ break;
242
+ case "SAVE":
243
+ {
244
+ wsDebug("💾 [WebSocket Server] Processing SAVE_ENTITY request");
245
+ const request = payload;
246
+ wsDebug("💾 [WebSocket Server] Saving row with request:", inspect(request, {
247
+ depth: null,
248
+ colors: true
249
+ }));
250
+ const row = await (await getScopedDelegate()).save(request);
251
+ wsDebug("💾 [WebSocket Server] SAVE_ENTITY result:", inspect(row, {
252
+ depth: null,
253
+ colors: true
254
+ }));
255
+ const response = {
256
+ type: "SAVE_SUCCESS",
257
+ payload: { row },
258
+ requestId
259
+ };
260
+ wsDebug("💾 [WebSocket Server] Sending SAVE_ENTITY_SUCCESS response");
261
+ ws.send(JSON.stringify(response));
262
+ }
263
+ break;
264
+ case "DELETE":
265
+ {
266
+ wsDebug("🗑️ [WebSocket Server] Processing DELETE_ENTITY request");
267
+ const request = payload;
268
+ wsDebug("🗑️ [WebSocket Server] Deleting row:", request.row);
269
+ await (await getScopedDelegate()).delete(request);
270
+ wsDebug("🗑️ [WebSocket Server] DELETE_ENTITY completed successfully");
271
+ const response = {
272
+ type: "DELETE_SUCCESS",
273
+ payload: { success: true },
274
+ requestId
275
+ };
276
+ wsDebug("🗑️ [WebSocket Server] Sending DELETE_ENTITY_SUCCESS response");
277
+ ws.send(JSON.stringify(response));
278
+ }
279
+ break;
280
+ case "CHECK_UNIQUE_FIELD":
281
+ {
282
+ wsDebug("🔍 [WebSocket Server] Processing CHECK_UNIQUE_FIELD request");
283
+ const { path, name, value, id, collection } = payload;
284
+ const isUnique = await (await getScopedDelegate()).checkUniqueField(path, name, value, id, collection);
285
+ wsDebug("🔍 [WebSocket Server] CHECK_UNIQUE_FIELD result:", isUnique);
286
+ const response = {
287
+ type: "CHECK_UNIQUE_FIELD_SUCCESS",
288
+ payload: { isUnique },
289
+ requestId
290
+ };
291
+ wsDebug("🔍 [WebSocket Server] Sending CHECK_UNIQUE_FIELD_SUCCESS response");
292
+ ws.send(JSON.stringify(response));
293
+ }
294
+ break;
295
+ case "COUNT":
296
+ {
297
+ const request = payload;
298
+ const response = {
299
+ type: "COUNT_SUCCESS",
300
+ payload: { count: await (await getScopedDelegate()).count(request) },
301
+ requestId
302
+ };
303
+ ws.send(JSON.stringify(response));
304
+ }
305
+ break;
306
+ case "EXECUTE_SQL":
307
+ {
308
+ const { sql, options } = payload;
309
+ try {
310
+ const admin = (await getScopedDelegate()).admin;
311
+ if (!isSQLAdmin(admin)) {
312
+ sendError("ERROR", "NOT_SUPPORTED", "SQL execution is not available for this driver.");
313
+ break;
314
+ }
315
+ const result = await admin.executeSql(sql, options);
316
+ if (process.env.NODE_ENV !== "production") wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : "non-array"} rows.`);
317
+ const auditSession = clientSessions.get(clientId);
318
+ console.log("[SQL Audit] WebSocket SQL execution", JSON.stringify({
319
+ sql: typeof sql === "string" ? sql.substring(0, 500) : sql,
320
+ options,
321
+ resultRows: Array.isArray(result) ? result.length : "unknown",
322
+ uid: auditSession?.user?.uid ?? "unknown",
323
+ roles: auditSession?.user?.roles ?? [],
324
+ isAdmin: auditSession?.user?.isAdmin ?? false
325
+ }));
326
+ const response = {
327
+ type: "EXECUTE_SQL_SUCCESS",
328
+ payload: { result },
329
+ requestId
330
+ };
331
+ ws.send(JSON.stringify(response));
332
+ } catch (sqlError) {
333
+ sendError("ERROR", "SQL_ERROR", extractErrorMessage(sqlError));
334
+ }
335
+ }
336
+ break;
337
+ case "FETCH_DATABASES":
338
+ {
339
+ wsDebug("📚 [WebSocket Server] Processing FETCH_DATABASES request");
340
+ const admin = (await getScopedDelegate()).admin;
341
+ let databases = [];
342
+ if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) databases = await admin.fetchAvailableDatabases();
343
+ wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);
344
+ const response = {
345
+ type: "FETCH_DATABASES_SUCCESS",
346
+ payload: { databases },
347
+ requestId
348
+ };
349
+ ws.send(JSON.stringify(response));
350
+ }
351
+ break;
352
+ case "FETCH_ROLES":
353
+ {
354
+ wsDebug("👤 [WebSocket Server] Processing FETCH_ROLES request");
355
+ const admin = (await getScopedDelegate()).admin;
356
+ let roles = [];
357
+ if (isSQLAdmin(admin) && admin.fetchAvailableRoles) roles = await admin.fetchAvailableRoles();
358
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);
359
+ const response = {
360
+ type: "FETCH_ROLES_SUCCESS",
361
+ payload: { roles },
362
+ requestId
363
+ };
364
+ ws.send(JSON.stringify(response));
365
+ }
366
+ break;
367
+ case "FETCH_APPLICATION_ROLES":
368
+ {
369
+ wsDebug("👤 [WebSocket Server] Processing FETCH_APPLICATION_ROLES request");
370
+ const admin = (await getScopedDelegate()).admin;
371
+ let roles = [];
372
+ if (isSQLAdmin(admin) && admin.fetchApplicationRoles) roles = await admin.fetchApplicationRoles();
373
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} application roles.`);
374
+ const response = {
375
+ type: "FETCH_APPLICATION_ROLES_SUCCESS",
376
+ payload: { roles },
377
+ requestId
378
+ };
379
+ ws.send(JSON.stringify(response));
380
+ }
381
+ break;
382
+ case "FETCH_CURRENT_DATABASE":
383
+ {
384
+ wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
385
+ const admin = (await getScopedDelegate()).admin;
386
+ let database = void 0;
387
+ if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) database = await admin.fetchCurrentDatabase();
388
+ const response = {
389
+ type: "FETCH_CURRENT_DATABASE_SUCCESS",
390
+ payload: { database },
391
+ requestId
392
+ };
393
+ ws.send(JSON.stringify(response));
394
+ }
395
+ break;
396
+ case "FETCH_UNMAPPED_TABLES":
397
+ {
398
+ wsDebug("📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request");
399
+ const admin = (await getScopedDelegate()).admin;
400
+ let tables = [];
401
+ if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) tables = await admin.fetchUnmappedTables(payload?.mappedPaths);
402
+ wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);
403
+ const response = {
404
+ type: "FETCH_UNMAPPED_TABLES_SUCCESS",
405
+ payload: { tables },
406
+ requestId
407
+ };
408
+ ws.send(JSON.stringify(response));
409
+ }
410
+ break;
411
+ case "FETCH_TABLE_METADATA":
412
+ {
413
+ wsDebug("📋 [WebSocket Server] Processing FETCH_TABLE_METADATA request");
414
+ const { tableName } = payload;
415
+ const admin = (await getScopedDelegate()).admin;
416
+ let metadata;
417
+ if (isSchemaAdmin(admin) && admin.fetchTableMetadata) metadata = await admin.fetchTableMetadata(tableName);
418
+ wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);
419
+ const response = {
420
+ type: "FETCH_TABLE_METADATA_SUCCESS",
421
+ payload: { metadata },
422
+ requestId
423
+ };
424
+ ws.send(JSON.stringify(response));
425
+ }
426
+ break;
427
+ case "CREATE_BRANCH":
428
+ {
429
+ wsDebug("🌿 [WebSocket Server] Processing CREATE_BRANCH request");
430
+ const { name, options } = payload;
431
+ const delegate = await getScopedDelegate();
432
+ if (!delegate.admin?.createBranch) {
433
+ sendError("ERROR", "NOT_SUPPORTED", "Database branching is not available. Configure adminConnectionString.");
434
+ break;
435
+ }
436
+ const branch = await delegate.admin.createBranch(name, options);
437
+ wsDebug(`🌿 [WebSocket Server] Branch created: ${branch.name}`);
438
+ const response = {
439
+ type: "CREATE_BRANCH_SUCCESS",
440
+ payload: { branch },
441
+ requestId
442
+ };
443
+ ws.send(JSON.stringify(response));
444
+ }
445
+ break;
446
+ case "DELETE_BRANCH":
447
+ {
448
+ wsDebug("🗑️ [WebSocket Server] Processing DELETE_BRANCH request");
449
+ const { name: branchName } = payload;
450
+ const delegate = await getScopedDelegate();
451
+ if (!delegate.admin?.deleteBranch) {
452
+ sendError("ERROR", "NOT_SUPPORTED", "Database branching is not available.");
453
+ break;
454
+ }
455
+ await delegate.admin.deleteBranch(branchName);
456
+ wsDebug(`🗑️ [WebSocket Server] Branch deleted: ${branchName}`);
457
+ const response = {
458
+ type: "DELETE_BRANCH_SUCCESS",
459
+ payload: { success: true },
460
+ requestId
461
+ };
462
+ ws.send(JSON.stringify(response));
463
+ }
464
+ break;
465
+ case "LIST_BRANCHES":
466
+ {
467
+ wsDebug("🌿 [WebSocket Server] Processing LIST_BRANCHES request");
468
+ const delegate = await getScopedDelegate();
469
+ let branches = [];
470
+ if (delegate.admin?.listBranches) branches = await delegate.admin.listBranches();
471
+ wsDebug(`🌿 [WebSocket Server] Listed ${branches.length} branches.`);
472
+ const response = {
473
+ type: "LIST_BRANCHES_SUCCESS",
474
+ payload: { branches },
475
+ requestId
476
+ };
477
+ ws.send(JSON.stringify(response));
478
+ }
479
+ break;
480
+ case "subscribe_collection":
481
+ case "subscribe_one":
482
+ case "unsubscribe":
483
+ case "join_channel":
484
+ case "leave_channel":
485
+ case "broadcast":
486
+ case "presence_track":
487
+ case "presence_untrack":
488
+ case "presence_state":
489
+ case "channel_history": {
490
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
491
+ const session = clientSessions.get(clientId);
492
+ const authContext = session?.user ? {
493
+ uid: session.user.uid,
494
+ roles: session.user.roles ?? []
495
+ } : {
496
+ uid: "anon",
497
+ roles: ["anon"]
498
+ };
499
+ await realtimeService.handleClientMessage(clientId, {
500
+ type,
501
+ payload,
502
+ subscriptionId: payload?.subscriptionId
503
+ }, authContext);
504
+ break;
505
+ }
506
+ default: logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
507
+ }
508
+ } catch (error) {
509
+ logger.error("💥 [WebSocket Server] Error handling message", { error });
510
+ if (error instanceof Error) logger.error("Stack trace", { detail: error.stack });
511
+ const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" : extractErrorMessage(error);
512
+ const errorResponse = {
513
+ type: "ERROR",
514
+ requestId,
515
+ payload: { error: {
516
+ message: errorMessage,
517
+ code: "INTERNAL_ERROR"
518
+ } }
519
+ };
520
+ ws.send(JSON.stringify(errorResponse));
521
+ }
522
+ });
523
+ });
524
+ }
525
+ //#endregion
526
+ export { websocket_exports as n, createPostgresWebSocket as t };
527
+
528
+ //# sourceMappingURL=websocket-BKcGvILX.js.map