@rpcbase/server 0.464.0 → 0.466.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkInitReplicaSet.d.ts +6 -0
- package/dist/checkInitReplicaSet.d.ts.map +1 -0
- package/dist/index-BXODFGBH.js +490 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -69
- package/dist/initServer.d.ts.map +1 -1
- package/dist/rts/index.d.ts +34 -0
- package/dist/rts/index.d.ts.map +1 -0
- package/dist/rts.d.ts +2 -0
- package/dist/rts.d.ts.map +1 -0
- package/dist/rts.js +6 -0
- package/package.json +17 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkInitReplicaSet.d.ts","sourceRoot":"","sources":["../src/checkInitReplicaSet.ts"],"names":[],"mappings":"AAGA,KAAK,SAAS,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAMtD,eAAO,MAAM,mBAAmB,GAAU,WAAW,SAAS,KAAG,OAAO,CAAC,IAAI,CAiH5E,CAAA"}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loadModel } from "@rpcbase/db";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
const TENANT_ID_QUERY_PARAM = "rb-tenant-id";
|
|
5
|
+
const USER_ID_HEADER = "rb-user-id";
|
|
6
|
+
const QUERY_KEY_MAX_LEN = 4096;
|
|
7
|
+
const QUERY_MAX_LIMIT = 4096;
|
|
8
|
+
const initializedServers = /* @__PURE__ */ new WeakSet();
|
|
9
|
+
const customHandlers = [];
|
|
10
|
+
const sockets = /* @__PURE__ */ new Map();
|
|
11
|
+
const socketMeta = /* @__PURE__ */ new Map();
|
|
12
|
+
const socketWrappers = /* @__PURE__ */ new Map();
|
|
13
|
+
const socketCleanup = /* @__PURE__ */ new Map();
|
|
14
|
+
const socketSubscriptions = /* @__PURE__ */ new Map();
|
|
15
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
16
|
+
const changeStreams = /* @__PURE__ */ new Map();
|
|
17
|
+
class RtsSocket {
|
|
18
|
+
id;
|
|
19
|
+
tenantId;
|
|
20
|
+
userId;
|
|
21
|
+
ws;
|
|
22
|
+
handlers = /* @__PURE__ */ new Map();
|
|
23
|
+
constructor({
|
|
24
|
+
id,
|
|
25
|
+
ws,
|
|
26
|
+
meta
|
|
27
|
+
}) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.ws = ws;
|
|
30
|
+
this.tenantId = meta.tenantId;
|
|
31
|
+
this.userId = meta.userId;
|
|
32
|
+
}
|
|
33
|
+
on(event, handler) {
|
|
34
|
+
const set = this.handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
35
|
+
set.add(handler);
|
|
36
|
+
this.handlers.set(event, set);
|
|
37
|
+
return () => this.off(event, handler);
|
|
38
|
+
}
|
|
39
|
+
off(event, handler) {
|
|
40
|
+
const set = this.handlers.get(event);
|
|
41
|
+
if (!set) return;
|
|
42
|
+
set.delete(handler);
|
|
43
|
+
if (!set.size) this.handlers.delete(event);
|
|
44
|
+
}
|
|
45
|
+
emit(event, payload) {
|
|
46
|
+
sendWs(this.ws, { type: "event", event, payload });
|
|
47
|
+
}
|
|
48
|
+
close() {
|
|
49
|
+
try {
|
|
50
|
+
this.ws.close();
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
dispatch(event, payload) {
|
|
55
|
+
const set = this.handlers.get(event);
|
|
56
|
+
if (!set) return;
|
|
57
|
+
for (const handler of set) {
|
|
58
|
+
handler(payload);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const rawToText = (raw) => {
|
|
63
|
+
if (typeof raw === "string") return raw;
|
|
64
|
+
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString();
|
|
65
|
+
if (Array.isArray(raw)) return Buffer.concat(raw).toString();
|
|
66
|
+
return raw.toString();
|
|
67
|
+
};
|
|
68
|
+
const safeJsonParse = (raw) => JSON.parse(rawToText(raw));
|
|
69
|
+
const sendWs = (ws, message) => {
|
|
70
|
+
if (ws.readyState !== 1) return;
|
|
71
|
+
ws.send(JSON.stringify(message));
|
|
72
|
+
};
|
|
73
|
+
const unauthorized = (socket, message = "Unauthorized") => {
|
|
74
|
+
try {
|
|
75
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
76
|
+
socket.write(`Error: ${message}\r
|
|
77
|
+
`);
|
|
78
|
+
socket.end();
|
|
79
|
+
} catch {
|
|
80
|
+
socket.destroy();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const badRequest = (socket, message = "Bad Request") => {
|
|
84
|
+
try {
|
|
85
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
86
|
+
socket.write(`Error: ${message}\r
|
|
87
|
+
`);
|
|
88
|
+
socket.end();
|
|
89
|
+
} catch {
|
|
90
|
+
socket.destroy();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const runSessionMiddleware = async (sessionMiddleware, req) => {
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
sessionMiddleware(req, {}, (err) => {
|
|
96
|
+
if (err) reject(err);
|
|
97
|
+
else resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const parseUpgradeMeta = async ({
|
|
102
|
+
req,
|
|
103
|
+
url,
|
|
104
|
+
sessionMiddleware
|
|
105
|
+
}) => {
|
|
106
|
+
const tenantId = url.searchParams.get(TENANT_ID_QUERY_PARAM);
|
|
107
|
+
if (!tenantId) {
|
|
108
|
+
throw new Error("Missing rb-tenant-id query parameter");
|
|
109
|
+
}
|
|
110
|
+
const raw = req.headers[USER_ID_HEADER];
|
|
111
|
+
const headerUserId = Array.isArray(raw) ? raw[0] : raw;
|
|
112
|
+
if (headerUserId) return { tenantId, userId: headerUserId };
|
|
113
|
+
if (!sessionMiddleware) {
|
|
114
|
+
throw new Error("Missing rb-user-id header (reverse-proxy) and no session middleware configured");
|
|
115
|
+
}
|
|
116
|
+
const upgradeReq = req;
|
|
117
|
+
try {
|
|
118
|
+
await runSessionMiddleware(sessionMiddleware, upgradeReq);
|
|
119
|
+
} catch {
|
|
120
|
+
throw new Error("Failed to load session for RTS");
|
|
121
|
+
}
|
|
122
|
+
const sessionUser = upgradeReq.session?.user;
|
|
123
|
+
const sessionUserId = sessionUser?.id;
|
|
124
|
+
if (!sessionUserId) {
|
|
125
|
+
throw new Error("Not signed in (missing session.user.id)");
|
|
126
|
+
}
|
|
127
|
+
const signedInTenants = sessionUser?.signed_in_tenants;
|
|
128
|
+
const currentTenantId = sessionUser?.current_tenant_id;
|
|
129
|
+
if (Array.isArray(signedInTenants) && signedInTenants.length > 0) {
|
|
130
|
+
if (!signedInTenants.includes(tenantId)) {
|
|
131
|
+
throw new Error("Tenant not authorized for this session");
|
|
132
|
+
}
|
|
133
|
+
} else if (currentTenantId) {
|
|
134
|
+
if (currentTenantId !== tenantId) {
|
|
135
|
+
throw new Error("Tenant not authorized for this session");
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error("Tenant not authorized for this session");
|
|
139
|
+
}
|
|
140
|
+
return { tenantId, userId: sessionUserId };
|
|
141
|
+
};
|
|
142
|
+
const getTenantModel = async (tenantId, modelName) => {
|
|
143
|
+
const ctx = {
|
|
144
|
+
req: {
|
|
145
|
+
session: {
|
|
146
|
+
user: {
|
|
147
|
+
current_tenant_id: tenantId
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return loadModel(modelName, ctx);
|
|
153
|
+
};
|
|
154
|
+
const normalizeLimit = (limit) => {
|
|
155
|
+
if (typeof limit !== "number") return QUERY_MAX_LIMIT;
|
|
156
|
+
if (!Number.isFinite(limit)) return QUERY_MAX_LIMIT;
|
|
157
|
+
return Math.min(QUERY_MAX_LIMIT, Math.abs(limit));
|
|
158
|
+
};
|
|
159
|
+
const normalizeOptions = (options) => {
|
|
160
|
+
if (!options || typeof options !== "object") return {};
|
|
161
|
+
const normalized = {};
|
|
162
|
+
if (options.projection && typeof options.projection === "object") {
|
|
163
|
+
normalized.projection = options.projection;
|
|
164
|
+
}
|
|
165
|
+
if (options.sort && typeof options.sort === "object") {
|
|
166
|
+
normalized.sort = options.sort;
|
|
167
|
+
}
|
|
168
|
+
normalized.limit = normalizeLimit(options.limit);
|
|
169
|
+
return normalized;
|
|
170
|
+
};
|
|
171
|
+
const runAndSendQuery = async ({
|
|
172
|
+
tenantId,
|
|
173
|
+
targetSocketIds,
|
|
174
|
+
modelName,
|
|
175
|
+
queryKey,
|
|
176
|
+
query,
|
|
177
|
+
options
|
|
178
|
+
}) => {
|
|
179
|
+
const model = await getTenantModel(tenantId, modelName);
|
|
180
|
+
const projection = options.projection ?? void 0;
|
|
181
|
+
const sort = options.sort;
|
|
182
|
+
const limit = normalizeLimit(options.limit);
|
|
183
|
+
const queryPromise = model.find(query, projection);
|
|
184
|
+
if (sort && Object.keys(sort).length) {
|
|
185
|
+
queryPromise.sort(sort);
|
|
186
|
+
}
|
|
187
|
+
queryPromise.limit(limit);
|
|
188
|
+
const data = await queryPromise;
|
|
189
|
+
const payload = { type: "query_payload", modelName, queryKey, data };
|
|
190
|
+
for (const socketId of targetSocketIds) {
|
|
191
|
+
const ws = sockets.get(socketId);
|
|
192
|
+
if (!ws) continue;
|
|
193
|
+
sendWs(ws, payload);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const dispatchSubscriptionsForModel = async (tenantId, modelName) => {
|
|
197
|
+
const tenantSubs = subscriptions.get(tenantId);
|
|
198
|
+
const modelSubs = tenantSubs?.get(modelName);
|
|
199
|
+
if (!modelSubs || !modelSubs.size) return;
|
|
200
|
+
for (const [queryKey, sub] of modelSubs.entries()) {
|
|
201
|
+
const targetSocketIds = Array.from(sub.socketIds);
|
|
202
|
+
if (!targetSocketIds.length) continue;
|
|
203
|
+
try {
|
|
204
|
+
await runAndSendQuery({
|
|
205
|
+
tenantId,
|
|
206
|
+
targetSocketIds,
|
|
207
|
+
modelName,
|
|
208
|
+
queryKey,
|
|
209
|
+
query: sub.query,
|
|
210
|
+
options: sub.options
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
214
|
+
const payload = { type: "query_payload", modelName, queryKey, error };
|
|
215
|
+
for (const socketId of targetSocketIds) {
|
|
216
|
+
const ws = sockets.get(socketId);
|
|
217
|
+
if (!ws) continue;
|
|
218
|
+
sendWs(ws, payload);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const ensureChangeStream = async (tenantId, modelName) => {
|
|
224
|
+
const tenantStreams = changeStreams.get(tenantId) ?? /* @__PURE__ */ new Map();
|
|
225
|
+
changeStreams.set(tenantId, tenantStreams);
|
|
226
|
+
if (tenantStreams.has(modelName)) return;
|
|
227
|
+
const model = await getTenantModel(tenantId, modelName);
|
|
228
|
+
const stream = model.watch([], {
|
|
229
|
+
fullDocument: "updateLookup"
|
|
230
|
+
});
|
|
231
|
+
stream.on("change", () => {
|
|
232
|
+
void dispatchSubscriptionsForModel(tenantId, modelName);
|
|
233
|
+
});
|
|
234
|
+
stream.on("close", () => {
|
|
235
|
+
const map = changeStreams.get(tenantId);
|
|
236
|
+
map?.delete(modelName);
|
|
237
|
+
if (map && map.size === 0) changeStreams.delete(tenantId);
|
|
238
|
+
});
|
|
239
|
+
stream.on("error", () => {
|
|
240
|
+
try {
|
|
241
|
+
stream.close();
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
tenantStreams.set(modelName, stream);
|
|
246
|
+
};
|
|
247
|
+
const addSocketSubscription = ({
|
|
248
|
+
socketId,
|
|
249
|
+
tenantId,
|
|
250
|
+
modelName,
|
|
251
|
+
queryKey,
|
|
252
|
+
query,
|
|
253
|
+
options
|
|
254
|
+
}) => {
|
|
255
|
+
const tenantSubs = subscriptions.get(tenantId) ?? /* @__PURE__ */ new Map();
|
|
256
|
+
subscriptions.set(tenantId, tenantSubs);
|
|
257
|
+
const modelSubs = tenantSubs.get(modelName) ?? /* @__PURE__ */ new Map();
|
|
258
|
+
tenantSubs.set(modelName, modelSubs);
|
|
259
|
+
const existing = modelSubs.get(queryKey);
|
|
260
|
+
if (existing) {
|
|
261
|
+
existing.socketIds.add(socketId);
|
|
262
|
+
} else {
|
|
263
|
+
modelSubs.set(queryKey, {
|
|
264
|
+
query,
|
|
265
|
+
options,
|
|
266
|
+
socketIds: /* @__PURE__ */ new Set([socketId])
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const byModel = socketSubscriptions.get(socketId) ?? /* @__PURE__ */ new Map();
|
|
270
|
+
socketSubscriptions.set(socketId, byModel);
|
|
271
|
+
const querySet = byModel.get(modelName) ?? /* @__PURE__ */ new Set();
|
|
272
|
+
byModel.set(modelName, querySet);
|
|
273
|
+
querySet.add(queryKey);
|
|
274
|
+
};
|
|
275
|
+
const removeSocketSubscription = ({
|
|
276
|
+
socketId,
|
|
277
|
+
tenantId,
|
|
278
|
+
modelName,
|
|
279
|
+
queryKey
|
|
280
|
+
}) => {
|
|
281
|
+
const tenantSubs = subscriptions.get(tenantId);
|
|
282
|
+
const modelSubs = tenantSubs?.get(modelName);
|
|
283
|
+
const sub = modelSubs?.get(queryKey);
|
|
284
|
+
if (sub) {
|
|
285
|
+
sub.socketIds.delete(socketId);
|
|
286
|
+
if (!sub.socketIds.size) {
|
|
287
|
+
modelSubs?.delete(queryKey);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const byModel = socketSubscriptions.get(socketId);
|
|
291
|
+
const set = byModel?.get(modelName);
|
|
292
|
+
if (set) {
|
|
293
|
+
set.delete(queryKey);
|
|
294
|
+
if (!set.size) {
|
|
295
|
+
byModel?.delete(modelName);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (modelSubs && modelSubs.size === 0) {
|
|
299
|
+
tenantSubs?.delete(modelName);
|
|
300
|
+
const tenantStreams = changeStreams.get(tenantId);
|
|
301
|
+
const stream = tenantStreams?.get(modelName);
|
|
302
|
+
if (stream) {
|
|
303
|
+
try {
|
|
304
|
+
stream.close();
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
tenantStreams?.delete(modelName);
|
|
308
|
+
if (tenantStreams && tenantStreams.size === 0) changeStreams.delete(tenantId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (tenantSubs && tenantSubs.size === 0) subscriptions.delete(tenantId);
|
|
312
|
+
if (byModel && byModel.size === 0) socketSubscriptions.delete(socketId);
|
|
313
|
+
};
|
|
314
|
+
const cleanupSocket = (socketId) => {
|
|
315
|
+
const meta = socketMeta.get(socketId);
|
|
316
|
+
if (meta) {
|
|
317
|
+
const byModel = socketSubscriptions.get(socketId);
|
|
318
|
+
if (byModel) {
|
|
319
|
+
for (const [modelName, keys] of byModel.entries()) {
|
|
320
|
+
for (const queryKey of keys.values()) {
|
|
321
|
+
removeSocketSubscription({
|
|
322
|
+
socketId,
|
|
323
|
+
tenantId: meta.tenantId,
|
|
324
|
+
modelName,
|
|
325
|
+
queryKey
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
socketSubscriptions.delete(socketId);
|
|
332
|
+
const cleanupFns = socketCleanup.get(socketId) ?? [];
|
|
333
|
+
socketCleanup.delete(socketId);
|
|
334
|
+
for (const fn of cleanupFns) {
|
|
335
|
+
try {
|
|
336
|
+
fn();
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
sockets.delete(socketId);
|
|
341
|
+
socketMeta.delete(socketId);
|
|
342
|
+
socketWrappers.delete(socketId);
|
|
343
|
+
};
|
|
344
|
+
const handleClientMessage = async ({
|
|
345
|
+
socketId,
|
|
346
|
+
meta,
|
|
347
|
+
message
|
|
348
|
+
}) => {
|
|
349
|
+
const ws = sockets.get(socketId);
|
|
350
|
+
if (!ws) return;
|
|
351
|
+
if (message.type === "event") {
|
|
352
|
+
const wrapper = socketWrappers.get(socketId);
|
|
353
|
+
wrapper?.dispatch(message.event, message.payload);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!message.modelName || typeof message.modelName !== "string") return;
|
|
357
|
+
if (!message.queryKey || typeof message.queryKey !== "string") return;
|
|
358
|
+
if (message.queryKey.length > QUERY_KEY_MAX_LEN) return;
|
|
359
|
+
if (message.type === "remove_query") {
|
|
360
|
+
removeSocketSubscription({
|
|
361
|
+
socketId,
|
|
362
|
+
tenantId: meta.tenantId,
|
|
363
|
+
modelName: message.modelName,
|
|
364
|
+
queryKey: message.queryKey
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!message.query || typeof message.query !== "object") return;
|
|
369
|
+
const options = normalizeOptions(message.options);
|
|
370
|
+
if (message.type === "registerQuery") {
|
|
371
|
+
addSocketSubscription({
|
|
372
|
+
socketId,
|
|
373
|
+
tenantId: meta.tenantId,
|
|
374
|
+
modelName: message.modelName,
|
|
375
|
+
queryKey: message.queryKey,
|
|
376
|
+
query: message.query,
|
|
377
|
+
options
|
|
378
|
+
});
|
|
379
|
+
try {
|
|
380
|
+
await ensureChangeStream(meta.tenantId, message.modelName);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const error = err instanceof Error ? err.message : "Unable to initialize change stream";
|
|
383
|
+
sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey, error });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await runAndSendQuery({
|
|
389
|
+
tenantId: meta.tenantId,
|
|
390
|
+
targetSocketIds: [socketId],
|
|
391
|
+
modelName: message.modelName,
|
|
392
|
+
queryKey: message.queryKey,
|
|
393
|
+
query: message.query,
|
|
394
|
+
options
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
398
|
+
sendWs(ws, { type: "query_payload", modelName: message.modelName, queryKey: message.queryKey, error });
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const initRts = ({
|
|
402
|
+
server,
|
|
403
|
+
path = "/rts",
|
|
404
|
+
sessionMiddleware
|
|
405
|
+
}) => {
|
|
406
|
+
if (initializedServers.has(server)) return;
|
|
407
|
+
initializedServers.add(server);
|
|
408
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
409
|
+
server.on("upgrade", (req, socket, head) => {
|
|
410
|
+
let url;
|
|
411
|
+
try {
|
|
412
|
+
url = new URL(req.url ?? "", `http://${req.headers.host ?? "localhost"}`);
|
|
413
|
+
} catch {
|
|
414
|
+
badRequest(socket, "Invalid URL");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (url.pathname !== path) return;
|
|
418
|
+
void (async () => {
|
|
419
|
+
try {
|
|
420
|
+
const meta = await parseUpgradeMeta({ req, url, sessionMiddleware });
|
|
421
|
+
req.__rb_rts_meta = meta;
|
|
422
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
423
|
+
wss.emit("connection", ws, req);
|
|
424
|
+
});
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const message = err instanceof Error ? err.message : "RTS upgrade failed";
|
|
427
|
+
if (message.startsWith("Missing rb-tenant-id")) {
|
|
428
|
+
badRequest(socket, message);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
unauthorized(socket, message);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
})().catch(() => {
|
|
435
|
+
badRequest(socket, "RTS upgrade failed");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
wss.on("connection", (ws, req) => {
|
|
439
|
+
const meta = req.__rb_rts_meta;
|
|
440
|
+
if (!meta) {
|
|
441
|
+
try {
|
|
442
|
+
ws.close();
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const socketId = randomUUID();
|
|
448
|
+
sockets.set(socketId, ws);
|
|
449
|
+
socketMeta.set(socketId, meta);
|
|
450
|
+
const wrapper = new RtsSocket({ id: socketId, ws, meta });
|
|
451
|
+
socketWrappers.set(socketId, wrapper);
|
|
452
|
+
const cleanupFns = [];
|
|
453
|
+
for (const handler of customHandlers) {
|
|
454
|
+
try {
|
|
455
|
+
const cleanup = handler(wrapper);
|
|
456
|
+
if (typeof cleanup === "function") cleanupFns.push(cleanup);
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (cleanupFns.length) socketCleanup.set(socketId, cleanupFns);
|
|
461
|
+
ws.on("message", (raw) => {
|
|
462
|
+
let parsed;
|
|
463
|
+
try {
|
|
464
|
+
parsed = safeJsonParse(raw);
|
|
465
|
+
} catch {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
469
|
+
const message = parsed;
|
|
470
|
+
void handleClientMessage({ socketId, meta, message });
|
|
471
|
+
});
|
|
472
|
+
ws.on("close", () => {
|
|
473
|
+
cleanupSocket(socketId);
|
|
474
|
+
});
|
|
475
|
+
ws.on("error", () => {
|
|
476
|
+
cleanupSocket(socketId);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
const registerRtsHandler = (handler) => {
|
|
481
|
+
customHandlers.push(handler);
|
|
482
|
+
};
|
|
483
|
+
const notifyRtsModelChanged = (tenantId, modelName) => {
|
|
484
|
+
void dispatchSubscriptionsForModel(tenantId, modelName);
|
|
485
|
+
};
|
|
486
|
+
export {
|
|
487
|
+
initRts as i,
|
|
488
|
+
notifyRtsModelChanged as n,
|
|
489
|
+
registerRtsHandler as r
|
|
490
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import session from "express-session";
|
|
|
2
2
|
import { RedisStore } from "connect-redis";
|
|
3
3
|
import MongoStore from "connect-mongo";
|
|
4
4
|
import { createClient } from "redis";
|
|
5
|
+
import { MongoClient } from "mongodb";
|
|
5
6
|
import env from "@rpcbase/env";
|
|
6
7
|
import { initApiClient, SsrErrorFallback, SSR_ERROR_STATE_GLOBAL_KEY, serializeSsrErrorState } from "@rpcbase/client";
|
|
7
8
|
import { dirname, posix, sep } from "path";
|
|
@@ -18,6 +19,7 @@ import { renderToPipeableStream, renderToStaticMarkup } from "react-dom/server";
|
|
|
18
19
|
import { jsx } from "react/jsx-runtime";
|
|
19
20
|
import { createPath, matchRoutes, parsePath, createStaticRouter, StaticRouterProvider } from "@rpcbase/router";
|
|
20
21
|
import { Resend } from "resend";
|
|
22
|
+
import { i, n, r } from "./index-BXODFGBH.js";
|
|
21
23
|
function getDefaultExportFromCjs(x) {
|
|
22
24
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
23
25
|
}
|
|
@@ -93,9 +95,9 @@ function requireLib() {
|
|
|
93
95
|
}
|
|
94
96
|
return ip;
|
|
95
97
|
});
|
|
96
|
-
for (var
|
|
97
|
-
if (is.ip(forwardedIps[
|
|
98
|
-
return forwardedIps[
|
|
98
|
+
for (var i2 = 0; i2 < forwardedIps.length; i2++) {
|
|
99
|
+
if (is.ip(forwardedIps[i2])) {
|
|
100
|
+
return forwardedIps[i2];
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
return null;
|
|
@@ -206,8 +208,8 @@ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname(proces
|
|
|
206
208
|
if (".js" === ext || ".mjs" === ext || ".cjs" === ext) file = file.slice(0, -1 * ext.length);
|
|
207
209
|
const decodedFile = decodeURIComponent(file);
|
|
208
210
|
if (!dir) dir = ".";
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
+
const n2 = dir.lastIndexOf("/node_modules");
|
|
212
|
+
if (n2 > -1) return `${dir.slice(n2 + 14).replace(/\//g, ".")}:${decodedFile}`;
|
|
211
213
|
if (dir.startsWith(normalizedBase)) {
|
|
212
214
|
const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, ".");
|
|
213
215
|
return moduleName ? `${moduleName}:${decodedFile}` : decodedFile;
|
|
@@ -334,12 +336,12 @@ class UUID {
|
|
|
334
336
|
}
|
|
335
337
|
if (hex) {
|
|
336
338
|
const inner = new Uint8Array(16);
|
|
337
|
-
for (let
|
|
338
|
-
const
|
|
339
|
-
inner[
|
|
340
|
-
inner[
|
|
341
|
-
inner[
|
|
342
|
-
inner[
|
|
339
|
+
for (let i2 = 0; i2 < 16; i2 += 4) {
|
|
340
|
+
const n2 = parseInt(hex.substring(2 * i2, 2 * i2 + 8), 16);
|
|
341
|
+
inner[i2 + 0] = n2 >>> 24;
|
|
342
|
+
inner[i2 + 1] = n2 >>> 16;
|
|
343
|
+
inner[i2 + 2] = n2 >>> 8;
|
|
344
|
+
inner[i2 + 3] = n2;
|
|
343
345
|
}
|
|
344
346
|
return new UUID(inner);
|
|
345
347
|
}
|
|
@@ -347,18 +349,18 @@ class UUID {
|
|
|
347
349
|
}
|
|
348
350
|
toString() {
|
|
349
351
|
let text = "";
|
|
350
|
-
for (let
|
|
351
|
-
text += DIGITS.charAt(this.bytes[
|
|
352
|
-
text += DIGITS.charAt(15 & this.bytes[
|
|
353
|
-
if (3 ===
|
|
352
|
+
for (let i2 = 0; i2 < this.bytes.length; i2++) {
|
|
353
|
+
text += DIGITS.charAt(this.bytes[i2] >>> 4);
|
|
354
|
+
text += DIGITS.charAt(15 & this.bytes[i2]);
|
|
355
|
+
if (3 === i2 || 5 === i2 || 7 === i2 || 9 === i2) text += "-";
|
|
354
356
|
}
|
|
355
357
|
return text;
|
|
356
358
|
}
|
|
357
359
|
toHex() {
|
|
358
360
|
let text = "";
|
|
359
|
-
for (let
|
|
360
|
-
text += DIGITS.charAt(this.bytes[
|
|
361
|
-
text += DIGITS.charAt(15 & this.bytes[
|
|
361
|
+
for (let i2 = 0; i2 < this.bytes.length; i2++) {
|
|
362
|
+
text += DIGITS.charAt(this.bytes[i2] >>> 4);
|
|
363
|
+
text += DIGITS.charAt(15 & this.bytes[i2]);
|
|
362
364
|
}
|
|
363
365
|
return text;
|
|
364
366
|
}
|
|
@@ -366,12 +368,12 @@ class UUID {
|
|
|
366
368
|
return this.toString();
|
|
367
369
|
}
|
|
368
370
|
getVariant() {
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
if (
|
|
372
|
-
if (
|
|
373
|
-
if (
|
|
374
|
-
if (
|
|
371
|
+
const n2 = this.bytes[8] >>> 4;
|
|
372
|
+
if (n2 < 0) throw new Error("unreachable");
|
|
373
|
+
if (n2 <= 7) return this.bytes.every((e) => 0 === e) ? "NIL" : "VAR_0";
|
|
374
|
+
if (n2 <= 11) return "VAR_10";
|
|
375
|
+
if (n2 <= 13) return "VAR_110";
|
|
376
|
+
if (n2 <= 15) return this.bytes.every((e) => 255 === e) ? "MAX" : "VAR_RESERVED";
|
|
375
377
|
else throw new Error("unreachable");
|
|
376
378
|
}
|
|
377
379
|
getVersion() {
|
|
@@ -384,8 +386,8 @@ class UUID {
|
|
|
384
386
|
return 0 === this.compareTo(other);
|
|
385
387
|
}
|
|
386
388
|
compareTo(other) {
|
|
387
|
-
for (let
|
|
388
|
-
const diff = this.bytes[
|
|
389
|
+
for (let i2 = 0; i2 < 16; i2++) {
|
|
390
|
+
const diff = this.bytes[i2] - other.bytes[i2];
|
|
389
391
|
if (0 !== diff) return Math.sign(diff);
|
|
390
392
|
}
|
|
391
393
|
return 0;
|
|
@@ -720,8 +722,8 @@ function removeTrailingSlash(url) {
|
|
|
720
722
|
}
|
|
721
723
|
async function retriable(fn, props) {
|
|
722
724
|
let lastError = null;
|
|
723
|
-
for (let
|
|
724
|
-
if (
|
|
725
|
+
for (let i2 = 0; i2 < props.retryCount + 1; i2++) {
|
|
726
|
+
if (i2 > 0) await new Promise((r2) => setTimeout(r2, props.retryDelay));
|
|
725
727
|
try {
|
|
726
728
|
const res = await fn();
|
|
727
729
|
return res;
|
|
@@ -1423,8 +1425,8 @@ function getFilenameToChunkIdMap(stackParser) {
|
|
|
1423
1425
|
if (result) acc[result[0]] = result[1];
|
|
1424
1426
|
else {
|
|
1425
1427
|
const parsedStack = stackParser(stackKey);
|
|
1426
|
-
for (let
|
|
1427
|
-
const stackFrame = parsedStack[
|
|
1428
|
+
for (let i2 = parsedStack.length - 1; i2 >= 0; i2--) {
|
|
1429
|
+
const stackFrame = parsedStack[i2];
|
|
1428
1430
|
const filename = stackFrame?.filename;
|
|
1429
1431
|
const chunkId = chunkIdMap[stackKey];
|
|
1430
1432
|
if (filename && chunkId) {
|
|
@@ -1632,8 +1634,8 @@ function createStackParser(platform, ...parsers) {
|
|
|
1632
1634
|
return (stack, skipFirstLines = 0) => {
|
|
1633
1635
|
const frames = [];
|
|
1634
1636
|
const lines = stack.split("\n");
|
|
1635
|
-
for (let
|
|
1636
|
-
const line = lines[
|
|
1637
|
+
for (let i2 = skipFirstLines; i2 < lines.length; i2++) {
|
|
1638
|
+
const line = lines[i2];
|
|
1637
1639
|
if (line.length > 1024) continue;
|
|
1638
1640
|
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, "$1") : line;
|
|
1639
1641
|
if (!cleanedLine.match(/\S*Error: /)) {
|
|
@@ -1715,10 +1717,10 @@ function extractExceptionKeysForMessage(err, maxLength = 40) {
|
|
|
1715
1717
|
const keys = Object.keys(err);
|
|
1716
1718
|
keys.sort();
|
|
1717
1719
|
if (!keys.length) return "[object has no keys]";
|
|
1718
|
-
for (let
|
|
1719
|
-
const serialized = keys.slice(0,
|
|
1720
|
+
for (let i2 = keys.length; i2 > 0; i2--) {
|
|
1721
|
+
const serialized = keys.slice(0, i2).join(", ");
|
|
1720
1722
|
if (!(serialized.length > maxLength)) {
|
|
1721
|
-
if (
|
|
1723
|
+
if (i2 === keys.length) return serialized;
|
|
1722
1724
|
return serialized.length <= maxLength ? serialized : `${serialized.slice(0, maxLength)}...`;
|
|
1723
1725
|
}
|
|
1724
1726
|
}
|
|
@@ -1827,8 +1829,8 @@ const MAX_CONTEXTLINES_COLNO = 1e3;
|
|
|
1827
1829
|
const MAX_CONTEXTLINES_LINENO = 1e4;
|
|
1828
1830
|
async function addSourceContext(frames) {
|
|
1829
1831
|
const filesToLines = {};
|
|
1830
|
-
for (let
|
|
1831
|
-
const frame = frames[
|
|
1832
|
+
for (let i2 = frames.length - 1; i2 >= 0; i2--) {
|
|
1833
|
+
const frame = frames[i2];
|
|
1832
1834
|
const filename = frame?.filename;
|
|
1833
1835
|
if (!frame || "string" != typeof filename || "number" != typeof frame.lineno || shouldSkipContextLinesForFile(filename) || shouldSkipContextLinesForFrame(frame)) continue;
|
|
1834
1836
|
const filesToLinesOutput = filesToLines[filename];
|
|
@@ -1844,7 +1846,7 @@ async function addSourceContext(frames) {
|
|
|
1844
1846
|
if (!filesToLineRanges) continue;
|
|
1845
1847
|
filesToLineRanges.sort((a, b) => a - b);
|
|
1846
1848
|
const ranges = makeLineReaderRanges(filesToLineRanges);
|
|
1847
|
-
if (ranges.every((
|
|
1849
|
+
if (ranges.every((r2) => rangeExistsInContentCache(file, r2))) continue;
|
|
1848
1850
|
const cache = emplace(LRU_FILE_CONTENTS_CACHE, file, {});
|
|
1849
1851
|
readlinePromises.push(getContextLinesFromFile(file, ranges, cache));
|
|
1850
1852
|
}
|
|
@@ -1912,8 +1914,8 @@ function addSourceContextToFrames(frames, cache) {
|
|
|
1912
1914
|
function addContextToFrame(lineno, frame, contents) {
|
|
1913
1915
|
if (void 0 === frame.lineno || void 0 === contents) return;
|
|
1914
1916
|
frame.pre_context = [];
|
|
1915
|
-
for (let
|
|
1916
|
-
const line = contents[
|
|
1917
|
+
for (let i2 = makeRangeStart(lineno); i2 < lineno; i2++) {
|
|
1918
|
+
const line = contents[i2];
|
|
1917
1919
|
if (void 0 === line) return void clearLineContext(frame);
|
|
1918
1920
|
frame.pre_context.push(line);
|
|
1919
1921
|
}
|
|
@@ -1921,8 +1923,8 @@ function addContextToFrame(lineno, frame, contents) {
|
|
|
1921
1923
|
frame.context_line = contents[lineno];
|
|
1922
1924
|
const end = makeRangeEnd(lineno);
|
|
1923
1925
|
frame.post_context = [];
|
|
1924
|
-
for (let
|
|
1925
|
-
const line = contents[
|
|
1926
|
+
for (let i2 = lineno + 1; i2 <= end; i2++) {
|
|
1927
|
+
const line = contents[i2];
|
|
1926
1928
|
if (void 0 === line) break;
|
|
1927
1929
|
frame.post_context.push(line);
|
|
1928
1930
|
}
|
|
@@ -1943,29 +1945,29 @@ function shouldSkipContextLinesForFrame(frame) {
|
|
|
1943
1945
|
function rangeExistsInContentCache(file, range) {
|
|
1944
1946
|
const contents = LRU_FILE_CONTENTS_CACHE.get(file);
|
|
1945
1947
|
if (void 0 === contents) return false;
|
|
1946
|
-
for (let
|
|
1948
|
+
for (let i2 = range[0]; i2 <= range[1]; i2++) if (void 0 === contents[i2]) return false;
|
|
1947
1949
|
return true;
|
|
1948
1950
|
}
|
|
1949
1951
|
function makeLineReaderRanges(lines) {
|
|
1950
1952
|
if (!lines.length) return [];
|
|
1951
|
-
let
|
|
1953
|
+
let i2 = 0;
|
|
1952
1954
|
const line = lines[0];
|
|
1953
1955
|
if ("number" != typeof line) return [];
|
|
1954
1956
|
let current = makeContextRange(line);
|
|
1955
1957
|
const out = [];
|
|
1956
1958
|
while (true) {
|
|
1957
|
-
if (
|
|
1959
|
+
if (i2 === lines.length - 1) {
|
|
1958
1960
|
out.push(current);
|
|
1959
1961
|
break;
|
|
1960
1962
|
}
|
|
1961
|
-
const next = lines[
|
|
1963
|
+
const next = lines[i2 + 1];
|
|
1962
1964
|
if ("number" != typeof next) break;
|
|
1963
1965
|
if (next <= current[1]) current[1] = next + DEFAULT_LINES_OF_CONTEXT;
|
|
1964
1966
|
else {
|
|
1965
1967
|
out.push(current);
|
|
1966
1968
|
current = makeContextRange(next);
|
|
1967
1969
|
}
|
|
1968
|
-
|
|
1970
|
+
i2++;
|
|
1969
1971
|
}
|
|
1970
1972
|
return out;
|
|
1971
1973
|
}
|
|
@@ -3277,6 +3279,101 @@ const metricsIngestProxyMiddleware = (app) => {
|
|
|
3277
3279
|
})
|
|
3278
3280
|
);
|
|
3279
3281
|
};
|
|
3282
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3283
|
+
let hasWarnedNoReplicationEnabled = false;
|
|
3284
|
+
const checkInitReplicaSet = async (serverEnv) => {
|
|
3285
|
+
const port = serverEnv.DB_PORT?.trim();
|
|
3286
|
+
if (!port) return;
|
|
3287
|
+
const host = (serverEnv.DB_HOST ?? "localhost").trim();
|
|
3288
|
+
const replSetName = "rs0";
|
|
3289
|
+
const memberHost = `${host}:${port}`;
|
|
3290
|
+
const maxAttempts = 10;
|
|
3291
|
+
const serverSelectionTimeoutMs = 2e3;
|
|
3292
|
+
const uri = `mongodb://${host}:${port}/?directConnection=true`;
|
|
3293
|
+
const waitForReplicaSetReady = async (admin) => {
|
|
3294
|
+
const maxWaitAttempts = 10;
|
|
3295
|
+
for (let attempt = 1; attempt <= maxWaitAttempts; attempt++) {
|
|
3296
|
+
try {
|
|
3297
|
+
const status = await admin.command({ replSetGetStatus: 1 });
|
|
3298
|
+
const state = Number(status?.myState ?? 0);
|
|
3299
|
+
if (status?.ok && (state === 1 || state === 2)) return;
|
|
3300
|
+
} catch {
|
|
3301
|
+
}
|
|
3302
|
+
await sleep(250 * attempt);
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
3306
|
+
let client2 = null;
|
|
3307
|
+
try {
|
|
3308
|
+
client2 = new MongoClient(uri, {
|
|
3309
|
+
family: 4,
|
|
3310
|
+
serverSelectionTimeoutMS: serverSelectionTimeoutMs,
|
|
3311
|
+
connectTimeoutMS: serverSelectionTimeoutMs
|
|
3312
|
+
});
|
|
3313
|
+
await client2.connect();
|
|
3314
|
+
const admin = client2.db("admin").admin();
|
|
3315
|
+
await admin.command({ ping: 1 });
|
|
3316
|
+
try {
|
|
3317
|
+
await admin.command({ replSetGetStatus: 1 });
|
|
3318
|
+
return;
|
|
3319
|
+
} catch (error) {
|
|
3320
|
+
const codeName = error?.codeName;
|
|
3321
|
+
if (codeName === "NotYetInitialized") {
|
|
3322
|
+
try {
|
|
3323
|
+
const res = await admin.command({
|
|
3324
|
+
replSetInitiate: {
|
|
3325
|
+
_id: replSetName,
|
|
3326
|
+
members: [{ _id: 0, host: memberHost }]
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
if (res?.ok) {
|
|
3330
|
+
console.warn(`[rb/server] MongoDB replica set '${replSetName}' initiated (${memberHost}).`);
|
|
3331
|
+
} else {
|
|
3332
|
+
console.warn(`[rb/server] MongoDB replica set initiation returned ok=${String(res?.ok)}.`);
|
|
3333
|
+
}
|
|
3334
|
+
} catch (initError) {
|
|
3335
|
+
const initCodeName = initError?.codeName;
|
|
3336
|
+
if (initCodeName !== "AlreadyInitialized") {
|
|
3337
|
+
const message2 = initError instanceof Error ? initError.message : String(initError);
|
|
3338
|
+
console.warn(`[rb/server] MongoDB replica set initiation failed: ${message2}`);
|
|
3339
|
+
if (initCodeName === "InvalidReplicaSetConfig") {
|
|
3340
|
+
console.warn(
|
|
3341
|
+
`[rb/server] Hint: the replica set member host must match the mongod address/port. If MongoDB runs in Docker with port mapping, ensure the container listens on ${port} (e.g. mongod --port ${port}) instead of the default 27017.`
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
await waitForReplicaSetReady(admin);
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
if (codeName === "NoReplicationEnabled") {
|
|
3350
|
+
if (!hasWarnedNoReplicationEnabled) {
|
|
3351
|
+
hasWarnedNoReplicationEnabled = true;
|
|
3352
|
+
console.warn(
|
|
3353
|
+
`[rb/server] MongoDB is not started with --replSet ${replSetName} (replication disabled). Change streams require a replica set; start mongod with --replSet.`
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3359
|
+
console.warn(`[rb/server] MongoDB replica set check failed: ${message}`);
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
} catch (error) {
|
|
3363
|
+
if (attempt === maxAttempts) {
|
|
3364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3365
|
+
console.warn(`[rb/server] MongoDB replica set auto-init skipped (unable to connect to ${host}:${port}): ${message}`);
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
await sleep(250 * attempt);
|
|
3369
|
+
} finally {
|
|
3370
|
+
try {
|
|
3371
|
+
await client2?.close();
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3280
3377
|
process.env = {
|
|
3281
3378
|
...env,
|
|
3282
3379
|
...__rb_env__,
|
|
@@ -3296,17 +3393,24 @@ const getMongoUrl = (serverEnv) => {
|
|
|
3296
3393
|
}
|
|
3297
3394
|
return void 0;
|
|
3298
3395
|
};
|
|
3299
|
-
const createMongoSessionStore = (serverEnv) => {
|
|
3396
|
+
const createMongoSessionStore = async (serverEnv) => {
|
|
3300
3397
|
const mongoUrl = getMongoUrl(serverEnv);
|
|
3301
3398
|
if (!mongoUrl) {
|
|
3302
3399
|
throw new Error("Missing REDIS_URL and Mongo connection details (MONGODB_URL/MONGO_URL/MONGODB_URI/DB_PORT)");
|
|
3303
3400
|
}
|
|
3304
3401
|
console.log("Using MongoDB session store");
|
|
3305
|
-
|
|
3306
|
-
|
|
3402
|
+
const client2 = await MongoClient.connect(mongoUrl, {
|
|
3403
|
+
family: 4,
|
|
3404
|
+
serverSelectionTimeoutMS: 2e3,
|
|
3405
|
+
connectTimeoutMS: 2e3
|
|
3406
|
+
});
|
|
3407
|
+
const store = MongoStore.create({
|
|
3408
|
+
client: client2,
|
|
3307
3409
|
collectionName: "sessions",
|
|
3308
3410
|
ttl: SESSION_MAX_AGE_S
|
|
3309
3411
|
});
|
|
3412
|
+
await store.collectionP;
|
|
3413
|
+
return store;
|
|
3310
3414
|
};
|
|
3311
3415
|
const createRedisSessionStore = async (redisUrl) => {
|
|
3312
3416
|
const reconnectStrategy = (retries) => {
|
|
@@ -3341,6 +3445,10 @@ const createRedisSessionStore = async (redisUrl) => {
|
|
|
3341
3445
|
};
|
|
3342
3446
|
const initServer = async (app, serverEnv) => {
|
|
3343
3447
|
await initApiClient({ app });
|
|
3448
|
+
const replicaSetInitPromise = checkInitReplicaSet(serverEnv).catch((error) => {
|
|
3449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3450
|
+
console.warn(`[rb/server] MongoDB replica set auto-init error: ${message}`);
|
|
3451
|
+
});
|
|
3344
3452
|
app.disable("x-powered-by");
|
|
3345
3453
|
app.set("trust proxy", true);
|
|
3346
3454
|
app.use(requestIp.mw());
|
|
@@ -3363,7 +3471,13 @@ const initServer = async (app, serverEnv) => {
|
|
|
3363
3471
|
}
|
|
3364
3472
|
const sessionSecret = getDerivedKey(serverEnv.MASTER_KEY, "express_session_key");
|
|
3365
3473
|
const redisUrl = serverEnv.REDIS_URL?.trim();
|
|
3366
|
-
|
|
3474
|
+
let store;
|
|
3475
|
+
if (redisUrl) {
|
|
3476
|
+
store = await createRedisSessionStore(redisUrl);
|
|
3477
|
+
} else {
|
|
3478
|
+
await replicaSetInitPromise;
|
|
3479
|
+
store = await createMongoSessionStore(serverEnv);
|
|
3480
|
+
}
|
|
3367
3481
|
const sessionConfig = {
|
|
3368
3482
|
name: "session",
|
|
3369
3483
|
store,
|
|
@@ -3378,7 +3492,9 @@ const initServer = async (app, serverEnv) => {
|
|
|
3378
3492
|
if (isProduction$1) {
|
|
3379
3493
|
sessionConfig.cookie.secure = true;
|
|
3380
3494
|
}
|
|
3381
|
-
|
|
3495
|
+
const sessionMiddleware = session(sessionConfig);
|
|
3496
|
+
app.locals.rbSessionMiddleware = sessionMiddleware;
|
|
3497
|
+
app.use(sessionMiddleware);
|
|
3382
3498
|
};
|
|
3383
3499
|
async function hashPassword(password, salt) {
|
|
3384
3500
|
const keyLength = 64;
|
|
@@ -3422,15 +3538,15 @@ const parseEnvInt = (value) => {
|
|
|
3422
3538
|
return parsed;
|
|
3423
3539
|
};
|
|
3424
3540
|
const isPowerOfTwo = (value) => (value & value - 1) === 0;
|
|
3425
|
-
const estimateScryptMemoryBytes = ({ N, r, p }) => {
|
|
3426
|
-
return 128 *
|
|
3541
|
+
const estimateScryptMemoryBytes = ({ N, r: r2, p }) => {
|
|
3542
|
+
return 128 * r2 * (N + p);
|
|
3427
3543
|
};
|
|
3428
3544
|
const validateScryptParams = (params) => {
|
|
3429
|
-
const { N, r, p, keylen, saltBytes, maxmemBytes } = params;
|
|
3545
|
+
const { N, r: r2, p, keylen, saltBytes, maxmemBytes } = params;
|
|
3430
3546
|
if (!Number.isSafeInteger(N) || N < 2 || N > MAX_SCRYPT_N || !isPowerOfTwo(N)) {
|
|
3431
3547
|
return { ok: false, error: "invalid_scrypt_N" };
|
|
3432
3548
|
}
|
|
3433
|
-
if (!Number.isSafeInteger(
|
|
3549
|
+
if (!Number.isSafeInteger(r2) || r2 < 1 || r2 > MAX_SCRYPT_R) {
|
|
3434
3550
|
return { ok: false, error: "invalid_scrypt_r" };
|
|
3435
3551
|
}
|
|
3436
3552
|
if (!Number.isSafeInteger(p) || p < 1 || p > MAX_SCRYPT_P) {
|
|
@@ -3445,7 +3561,7 @@ const validateScryptParams = (params) => {
|
|
|
3445
3561
|
if (!Number.isSafeInteger(maxmemBytes) || maxmemBytes < 16 * 1024 * 1024 || maxmemBytes > MAX_SCRYPT_MAXMEM_BYTES) {
|
|
3446
3562
|
return { ok: false, error: "invalid_scrypt_maxmem" };
|
|
3447
3563
|
}
|
|
3448
|
-
const estimatedMem = estimateScryptMemoryBytes({ N, r, p });
|
|
3564
|
+
const estimatedMem = estimateScryptMemoryBytes({ N, r: r2, p });
|
|
3449
3565
|
if (estimatedMem > maxmemBytes) {
|
|
3450
3566
|
return { ok: false, error: "scrypt_params_exceed_maxmem" };
|
|
3451
3567
|
}
|
|
@@ -3480,9 +3596,9 @@ const getCurrentScryptParams = (opts) => {
|
|
|
3480
3596
|
return params;
|
|
3481
3597
|
};
|
|
3482
3598
|
const scryptAsync = async (password, salt, params) => {
|
|
3483
|
-
const { N, r, p, keylen, maxmemBytes } = params;
|
|
3599
|
+
const { N, r: r2, p, keylen, maxmemBytes } = params;
|
|
3484
3600
|
return await new Promise((resolve, reject) => {
|
|
3485
|
-
crypto.scrypt(password, salt, keylen, { N, r, p, maxmem: maxmemBytes }, (err, derivedKey) => {
|
|
3601
|
+
crypto.scrypt(password, salt, keylen, { N, r: r2, p, maxmem: maxmemBytes }, (err, derivedKey) => {
|
|
3486
3602
|
if (err) {
|
|
3487
3603
|
reject(err);
|
|
3488
3604
|
return;
|
|
@@ -3529,10 +3645,10 @@ const parseStoredScryptHash = (stored) => {
|
|
|
3529
3645
|
params.set(key, value);
|
|
3530
3646
|
}
|
|
3531
3647
|
const N = params.get("N");
|
|
3532
|
-
const
|
|
3648
|
+
const r2 = params.get("r");
|
|
3533
3649
|
const p = params.get("p");
|
|
3534
3650
|
const keylen = params.get("keylen");
|
|
3535
|
-
if (N === void 0 ||
|
|
3651
|
+
if (N === void 0 || r2 === void 0 || p === void 0 || keylen === void 0) return null;
|
|
3536
3652
|
if (params.size !== 4) return null;
|
|
3537
3653
|
const salt = parseB64(saltB64);
|
|
3538
3654
|
const dk = parseB64(dkB64);
|
|
@@ -3543,31 +3659,31 @@ const parseStoredScryptHash = (stored) => {
|
|
|
3543
3659
|
const currentMaxmemBytes = getCurrentMaxmemBytes();
|
|
3544
3660
|
const validated = validateScryptParams({
|
|
3545
3661
|
N,
|
|
3546
|
-
r,
|
|
3662
|
+
r: r2,
|
|
3547
3663
|
p,
|
|
3548
3664
|
keylen,
|
|
3549
3665
|
saltBytes: salt.length,
|
|
3550
3666
|
maxmemBytes: currentMaxmemBytes
|
|
3551
3667
|
});
|
|
3552
3668
|
if (!validated.ok) return null;
|
|
3553
|
-
return { N, r, p, keylen, salt, dk };
|
|
3669
|
+
return { N, r: r2, p, keylen, salt, dk };
|
|
3554
3670
|
};
|
|
3555
3671
|
async function hashPasswordForStorage(password, opts) {
|
|
3556
|
-
const { N, r, p, keylen, saltBytes, maxmemBytes } = getCurrentScryptParams(opts);
|
|
3672
|
+
const { N, r: r2, p, keylen, saltBytes, maxmemBytes } = getCurrentScryptParams(opts);
|
|
3557
3673
|
const salt = crypto.randomBytes(saltBytes);
|
|
3558
|
-
const dk = await scryptAsync(password, salt, { N, r, p, keylen, maxmemBytes });
|
|
3674
|
+
const dk = await scryptAsync(password, salt, { N, r: r2, p, keylen, maxmemBytes });
|
|
3559
3675
|
const saltB64 = salt.toString("base64");
|
|
3560
3676
|
const dkB64 = dk.toString("base64");
|
|
3561
|
-
return `$scrypt$N=${N},r=${
|
|
3677
|
+
return `$scrypt$N=${N},r=${r2},p=${p},keylen=${keylen}$${saltB64}$${dkB64}`;
|
|
3562
3678
|
}
|
|
3563
3679
|
async function verifyPasswordFromStorage(password, stored) {
|
|
3564
3680
|
const parsed = parseStoredScryptHash(stored);
|
|
3565
3681
|
if (!parsed) return false;
|
|
3566
|
-
const { N, r, p, keylen, salt, dk } = parsed;
|
|
3682
|
+
const { N, r: r2, p, keylen, salt, dk } = parsed;
|
|
3567
3683
|
const maxmemBytes = getCurrentMaxmemBytes();
|
|
3568
3684
|
let derivedKey;
|
|
3569
3685
|
try {
|
|
3570
|
-
derivedKey = await scryptAsync(password, salt, { N, r, p, keylen, maxmemBytes });
|
|
3686
|
+
derivedKey = await scryptAsync(password, salt, { N, r: r2, p, keylen, maxmemBytes });
|
|
3571
3687
|
} catch {
|
|
3572
3688
|
return false;
|
|
3573
3689
|
}
|
|
@@ -3590,7 +3706,7 @@ function createLocation(current, to, state = null, key) {
|
|
|
3590
3706
|
return location;
|
|
3591
3707
|
}
|
|
3592
3708
|
function getShortCircuitMatches(routes) {
|
|
3593
|
-
const route = routes.length === 1 ? routes[0] : routes.find((
|
|
3709
|
+
const route = routes.length === 1 ? routes[0] : routes.find((r2) => r2.index || !r2.path || r2.path === "/") || {
|
|
3594
3710
|
id: "__shim-error-route__"
|
|
3595
3711
|
};
|
|
3596
3712
|
return {
|
|
@@ -4095,7 +4211,10 @@ export {
|
|
|
4095
4211
|
getDerivedKey,
|
|
4096
4212
|
hashPassword,
|
|
4097
4213
|
hashPasswordForStorage,
|
|
4214
|
+
i as initRts,
|
|
4098
4215
|
initServer,
|
|
4216
|
+
n as notifyRtsModelChanged,
|
|
4217
|
+
r as registerRtsHandler,
|
|
4099
4218
|
sendEmail,
|
|
4100
4219
|
ssrMiddleware,
|
|
4101
4220
|
verifyPasswordFromStorage
|
package/dist/initServer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initServer.d.ts","sourceRoot":"","sources":["../src/initServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"initServer.d.ts","sourceRoot":"","sources":["../src/initServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyBrC,KAAK,SAAS,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAuFtD,eAAO,MAAM,UAAU,GAAU,KAAK,WAAW,EAAE,WAAW,SAAS,kBAsEtE,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Server as HttpServer } from 'node:http';
|
|
2
|
+
import { RequestHandler } from 'express';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
type SocketMeta = {
|
|
5
|
+
tenantId: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
};
|
|
8
|
+
type HandlerFn = (socket: RtsSocket) => void | (() => void);
|
|
9
|
+
declare class RtsSocket {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly tenantId: string;
|
|
12
|
+
readonly userId: string;
|
|
13
|
+
private readonly ws;
|
|
14
|
+
private readonly handlers;
|
|
15
|
+
constructor({ id, ws, meta, }: {
|
|
16
|
+
id: string;
|
|
17
|
+
ws: WebSocket;
|
|
18
|
+
meta: SocketMeta;
|
|
19
|
+
});
|
|
20
|
+
on(event: string, handler: (payload: unknown) => void): () => void;
|
|
21
|
+
off(event: string, handler: (payload: unknown) => void): void;
|
|
22
|
+
emit(event: string, payload?: unknown): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
dispatch(event: string, payload: unknown): void;
|
|
25
|
+
}
|
|
26
|
+
export declare const initRts: ({ server, path, sessionMiddleware, }: {
|
|
27
|
+
server: HttpServer;
|
|
28
|
+
path?: string;
|
|
29
|
+
sessionMiddleware?: RequestHandler;
|
|
30
|
+
}) => void;
|
|
31
|
+
export declare const registerRtsHandler: (handler: HandlerFn) => void;
|
|
32
|
+
export declare const notifyRtsModelChanged: (tenantId: string, modelName: string) => void;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rts/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,IAAI,CAAA;AAqBlE,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAoBD,KAAK,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AAoB3D,cAAM,SAAS;IACb,SAAgB,EAAE,EAAE,MAAM,CAAA;IAC1B,SAAgB,QAAQ,EAAE,MAAM,CAAA;IAChC,SAAgB,MAAM,EAAE,MAAM,CAAA;IAE9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqD;gBAE3D,EACjB,EAAE,EACF,EAAE,EACF,IAAI,GACL,EAAE;QACD,EAAE,EAAE,MAAM,CAAA;QACV,EAAE,EAAE,SAAS,CAAA;QACb,IAAI,EAAE,UAAU,CAAA;KACjB;IAOM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAO7D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI;IAI5C,KAAK,IAAI,IAAI;IAQb,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAOvD;AA4aD,eAAO,MAAM,OAAO,GAAI,sCAIrB;IACD,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iBAAiB,CAAC,EAAE,cAAc,CAAA;CACnC,KAAG,IAyFH,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,KAAG,IAEvD,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,EAAE,WAAW,MAAM,KAAG,IAE3E,CAAA"}
|
package/dist/rts.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rts.d.ts","sourceRoot":"","sources":["../src/rts.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
|
package/dist/rts.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.466.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
7
7
|
],
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./rts": {
|
|
17
|
+
"types": "./dist/rts.d.ts",
|
|
18
|
+
"import": "./dist/rts.js",
|
|
19
|
+
"default": "./dist/rts.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
10
22
|
"scripts": {
|
|
11
23
|
"build": "wireit",
|
|
12
24
|
"test": "wireit",
|
|
@@ -63,10 +75,13 @@
|
|
|
63
75
|
"connect-redis": "9.0.0",
|
|
64
76
|
"express-session": "1.18.2",
|
|
65
77
|
"http-proxy-middleware": "3.0.5",
|
|
78
|
+
"mongodb": "7.0.0",
|
|
66
79
|
"redis": "5.10.0",
|
|
67
|
-
"resend": "6.5.2"
|
|
80
|
+
"resend": "6.5.2",
|
|
81
|
+
"ws": "8.18.0"
|
|
68
82
|
},
|
|
69
83
|
"devDependencies": {
|
|
84
|
+
"@types/ws": "8.18.1",
|
|
70
85
|
"request-ip": "3.3.0"
|
|
71
86
|
}
|
|
72
87
|
}
|