@cequrebackends/cequre-ts 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/adapters/bun/bun-cron.d.ts +20 -0
  2. package/dist/adapters/bun/bun-media.d.ts +9 -0
  3. package/dist/adapters/bun/bun-redis-cache.d.ts +33 -0
  4. package/dist/adapters/bun/bun-response.d.ts +13 -0
  5. package/dist/adapters/bun/bun-server.d.ts +6 -0
  6. package/dist/adapters/bun/bun-storage.d.ts +10 -0
  7. package/dist/adapters/bun/bun-websocket.d.ts +104 -0
  8. package/dist/adapters/bun/index.d.ts +9 -0
  9. package/dist/adapters/bun/index.js +547 -0
  10. package/dist/adapters/node/index.d.ts +9 -0
  11. package/dist/adapters/node/index.js +902 -0
  12. package/dist/adapters/node/node-cron.d.ts +20 -0
  13. package/dist/adapters/node/node-media.d.ts +9 -0
  14. package/dist/adapters/node/node-redis-cache.d.ts +32 -0
  15. package/dist/adapters/node/node-response.d.ts +13 -0
  16. package/dist/adapters/node/node-server.d.ts +6 -0
  17. package/dist/adapters/node/node-storage.d.ts +10 -0
  18. package/dist/adapters/node/node-websocket.d.ts +102 -0
  19. package/dist/index-17yswtmg.js +175 -0
  20. package/dist/index-kyvy0s1x.js +2662 -0
  21. package/dist/index-mfqj7cwr.js +165 -0
  22. package/dist/index-rf1kdn5b.js +732 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.js +3152 -0
  25. package/dist/interface-map.d.ts +175 -0
  26. package/dist/shared/core/adapter.d.ts +84 -0
  27. package/dist/shared/core/base-sql-adapter.d.ts +34 -0
  28. package/dist/shared/core/cache.d.ts +8 -0
  29. package/dist/shared/core/email.d.ts +53 -0
  30. package/dist/shared/core/index.d.ts +12 -0
  31. package/dist/shared/core/index.js +47 -0
  32. package/dist/shared/core/openapi.d.ts +24 -0
  33. package/dist/shared/core/plugins.d.ts +2 -0
  34. package/dist/shared/core/request.d.ts +32 -0
  35. package/dist/shared/core/sdk.d.ts +3 -0
  36. package/dist/shared/core/stream.d.ts +24 -0
  37. package/dist/shared/core/types-generator.d.ts +0 -0
  38. package/dist/shared/core/types.d.ts +304 -0
  39. package/dist/shared/core/ulid.d.ts +5 -0
  40. package/dist/shared/core/validator.d.ts +32 -0
  41. package/dist/shared/main/access-evaluator.d.ts +13 -0
  42. package/dist/shared/main/access.d.ts +34 -0
  43. package/dist/shared/main/aot.d.ts +3 -0
  44. package/dist/shared/main/hooks.d.ts +76 -0
  45. package/dist/shared/main/population.d.ts +13 -0
  46. package/dist/shared/main/router.d.ts +112 -0
  47. package/dist/shared/main/runtime.d.ts +195 -0
  48. package/dist/shared/main/sse.d.ts +87 -0
  49. package/dist/shared/utils/crypto.d.ts +23 -0
  50. package/dist/shared/utils/durable-queue.d.ts +20 -0
  51. package/dist/shared/utils/duration.d.ts +15 -0
  52. package/dist/shared/utils/error.d.ts +52 -0
  53. package/dist/shared/utils/kv/adapters/memory.d.ts +20 -0
  54. package/dist/shared/utils/kv/adapters/sqlite.d.ts +32 -0
  55. package/dist/shared/utils/kv/index.d.ts +37 -0
  56. package/dist/shared/utils/kv/types.d.ts +38 -0
  57. package/dist/shared/utils/logger.d.ts +42 -0
  58. package/dist/shared/utils/queue-manager.d.ts +64 -0
  59. package/dist/shared/utils/url.d.ts +13 -0
  60. package/package.json +63 -0
package/dist/index.js ADDED
@@ -0,0 +1,3152 @@
1
+ // @bun
2
+ import {
3
+ BaseSqlAdapter,
4
+ CequreMailer,
5
+ MemoryCacheStore,
6
+ MemoryStreamStore,
7
+ buildCollectionCreateSchema,
8
+ buildCollectionUpdateSchema,
9
+ createMailer,
10
+ createRouteContext,
11
+ exports_sdk,
12
+ generateBackendSchemas,
13
+ generateOpenAPISpec,
14
+ generateScalarHTML,
15
+ generateSharedModels,
16
+ generateTypeScriptClientSDK,
17
+ init_openapi,
18
+ init_sdk,
19
+ parseQuery,
20
+ sortPlugins,
21
+ sqlIdentifier,
22
+ sqlLiteral,
23
+ validateCreate,
24
+ validateUpdate
25
+ } from "./index-kyvy0s1x.js";
26
+ import {
27
+ CequreStreamMedia,
28
+ methodNotAllowed,
29
+ notFound,
30
+ toResponse
31
+ } from "./index-mfqj7cwr.js";
32
+ import {
33
+ CequreErrorLogs,
34
+ CequreKV,
35
+ CequreQueueManager,
36
+ createLogger,
37
+ init_kv,
38
+ init_logger,
39
+ logger
40
+ } from "./index-rf1kdn5b.js";
41
+ import {
42
+ CequreError,
43
+ __require,
44
+ __toCommonJS,
45
+ buildErrorResponse,
46
+ extractPathname,
47
+ getConstraintErrorMessage,
48
+ getConstraintErrorStatus,
49
+ parseConstraintError,
50
+ toErrorResponse,
51
+ ulid
52
+ } from "./index-17yswtmg.js";
53
+
54
+ // shared/main/sse.ts
55
+ var encoder = new TextEncoder;
56
+ function toSSE(payload) {
57
+ if (typeof payload === "string")
58
+ return encoder.encode(`data: ${payload}
59
+
60
+ `);
61
+ if (payload && typeof payload === "object" && "toSSE" in payload && typeof payload.toSSE === "function") {
62
+ return encoder.encode(payload.toSSE());
63
+ }
64
+ const item = payload;
65
+ if (item && typeof item === "object" && (("data" in item) || ("event" in item) || ("id" in item) || ("retry" in item))) {
66
+ const lines = [];
67
+ if (item.id !== undefined)
68
+ lines.push(`id: ${item.id}`);
69
+ if (item.event)
70
+ lines.push(`event: ${item.event}`);
71
+ if (item.retry !== undefined)
72
+ lines.push(`retry: ${item.retry}`);
73
+ const data = item.data === undefined ? "" : typeof item.data === "string" ? item.data : JSON.stringify(item.data);
74
+ for (const line of data.split(`
75
+ `))
76
+ lines.push(`data: ${line}`);
77
+ return encoder.encode(`${lines.join(`
78
+ `)}
79
+
80
+ `);
81
+ }
82
+ return encoder.encode(`data: ${JSON.stringify(payload)}
83
+
84
+ `);
85
+ }
86
+ function parseChannels(request) {
87
+ const url = new URL(request.url);
88
+ const channels = new Set;
89
+ for (const channel of url.searchParams.getAll("channel")) {
90
+ const trimmed = channel.trim();
91
+ if (trimmed)
92
+ channels.add(trimmed);
93
+ }
94
+ const rawChannels = url.searchParams.get("channels");
95
+ if (rawChannels) {
96
+ for (const channel of rawChannels.split(",")) {
97
+ const trimmed = channel.trim();
98
+ if (trimmed)
99
+ channels.add(trimmed);
100
+ }
101
+ }
102
+ return [...channels];
103
+ }
104
+
105
+ class SSEManager {
106
+ connections = new Map;
107
+ channels = new Map;
108
+ metadata = new Map;
109
+ register(connection) {
110
+ this.connections.set(connection.id, connection);
111
+ this.metadata.set(connection.id, {});
112
+ }
113
+ remove(id) {
114
+ const connection = this.connections.get(id);
115
+ if (!connection)
116
+ return;
117
+ for (const channel of connection.channels)
118
+ this.unsubscribe(id, channel);
119
+ this.connections.delete(id);
120
+ this.metadata.delete(id);
121
+ }
122
+ subscribe(id, channel) {
123
+ const connection = this.connections.get(id);
124
+ if (!connection)
125
+ return;
126
+ if (!this.channels.has(channel))
127
+ this.channels.set(channel, new Set);
128
+ this.channels.get(channel).add(id);
129
+ connection.channels.add(channel);
130
+ }
131
+ unsubscribe(id, channel) {
132
+ this.channels.get(channel)?.delete(id);
133
+ if (this.channels.get(channel)?.size === 0)
134
+ this.channels.delete(channel);
135
+ this.connections.get(id)?.channels.delete(channel);
136
+ }
137
+ sendTo(id, payload) {
138
+ const connection = this.connections.get(id);
139
+ if (!connection)
140
+ return false;
141
+ try {
142
+ connection.controller.enqueue(toSSE(payload));
143
+ return true;
144
+ } catch {
145
+ this.remove(id);
146
+ return false;
147
+ }
148
+ }
149
+ broadcastToChannel(channel, payload, { exclude = [] } = {}) {
150
+ const members = this.channels.get(channel);
151
+ if (!members)
152
+ return 0;
153
+ let sent = 0;
154
+ for (const id of members) {
155
+ if (exclude.includes(id))
156
+ continue;
157
+ if (this.sendTo(id, payload))
158
+ sent++;
159
+ }
160
+ return sent;
161
+ }
162
+ broadcastAll(payload, { exclude = [] } = {}) {
163
+ let sent = 0;
164
+ for (const id of this.connections.keys()) {
165
+ if (exclude.includes(id))
166
+ continue;
167
+ if (this.sendTo(id, payload))
168
+ sent++;
169
+ }
170
+ return sent;
171
+ }
172
+ setMeta(id, key, value) {
173
+ const meta = this.metadata.get(id);
174
+ if (meta)
175
+ meta[key] = value;
176
+ }
177
+ getMeta(id, key) {
178
+ return this.metadata.get(id)?.[key];
179
+ }
180
+ getChannelMembers(channel) {
181
+ return [...this.channels.get(channel) ?? []];
182
+ }
183
+ getConnectionChannels(id) {
184
+ return [...this.connections.get(id)?.channels ?? []];
185
+ }
186
+ listChannels() {
187
+ return [...this.channels.keys()];
188
+ }
189
+ get connectionCount() {
190
+ return this.connections.size;
191
+ }
192
+ get channelCount() {
193
+ return this.channels.size;
194
+ }
195
+ getStats() {
196
+ return {
197
+ connections: this.connectionCount,
198
+ channels: Object.fromEntries([...this.channels.entries()].map(([channel, ids]) => [channel, ids.size]))
199
+ };
200
+ }
201
+ }
202
+ var sseManager = new SSEManager;
203
+ function createBunSSERoute({
204
+ path = "/sse",
205
+ heartbeat,
206
+ resolveUser,
207
+ onConnect,
208
+ onSubscribe
209
+ } = {}) {
210
+ return async (request) => {
211
+ const user = resolveUser ? await resolveUser(request) : null;
212
+ const channels = parseChannels(request);
213
+ const id = ulid();
214
+ const info = { id, request, user };
215
+ if (onConnect) {
216
+ const allowed = await onConnect(info);
217
+ if (allowed !== true) {
218
+ const message = typeof allowed === "string" ? allowed : user ? "Forbidden" : "Unauthorized";
219
+ return new Response(JSON.stringify({ message }), {
220
+ status: user ? 403 : 401,
221
+ headers: { "Content-Type": "application/json" }
222
+ });
223
+ }
224
+ }
225
+ const allowedChannels = [];
226
+ for (const channel of channels) {
227
+ if (!onSubscribe || await onSubscribe({ ...info, channel }) === true) {
228
+ allowedChannels.push(channel);
229
+ }
230
+ }
231
+ let heartbeatTimer = null;
232
+ const stream = new ReadableStream({
233
+ start(controller) {
234
+ const connection = {
235
+ id,
236
+ controller,
237
+ channels: new Set,
238
+ request,
239
+ user,
240
+ connectedAt: Date.now()
241
+ };
242
+ sseManager.register(connection);
243
+ for (const channel of allowedChannels)
244
+ sseManager.subscribe(id, channel);
245
+ controller.enqueue(toSSE({ event: "connected", data: { id, channels: allowedChannels, authenticated: !!user } }));
246
+ if (heartbeat !== false) {
247
+ heartbeatTimer = setInterval(() => {
248
+ sseManager.sendTo(id, {
249
+ event: heartbeat?.event ?? "ping",
250
+ data: heartbeat?.data ?? { ts: new Date().toISOString() }
251
+ });
252
+ }, heartbeat?.interval ?? 30000);
253
+ if (heartbeatTimer.unref)
254
+ heartbeatTimer.unref();
255
+ }
256
+ request.signal.addEventListener("abort", () => {
257
+ if (heartbeatTimer)
258
+ clearInterval(heartbeatTimer);
259
+ sseManager.remove(id);
260
+ try {
261
+ controller.close();
262
+ } catch {}
263
+ }, { once: true });
264
+ },
265
+ cancel() {
266
+ if (heartbeatTimer)
267
+ clearInterval(heartbeatTimer);
268
+ sseManager.remove(id);
269
+ }
270
+ });
271
+ return new Response(stream, {
272
+ headers: {
273
+ "Content-Type": "text/event-stream",
274
+ "Cache-Control": "no-cache",
275
+ Connection: "keep-alive"
276
+ }
277
+ });
278
+ };
279
+ }
280
+
281
+ class CequreSSE {
282
+ static get manager() {
283
+ return sseManager;
284
+ }
285
+ static route(options = {}) {
286
+ return createBunSSERoute(options);
287
+ }
288
+ static register(connection) {
289
+ return sseManager.register(connection);
290
+ }
291
+ static remove(id) {
292
+ return sseManager.remove(id);
293
+ }
294
+ static subscribe(id, channel) {
295
+ return sseManager.subscribe(id, channel);
296
+ }
297
+ static unsubscribe(id, channel) {
298
+ return sseManager.unsubscribe(id, channel);
299
+ }
300
+ static sendTo(id, payload) {
301
+ return sseManager.sendTo(id, payload);
302
+ }
303
+ static broadcastToChannel(channel, payload, opts) {
304
+ return sseManager.broadcastToChannel(channel, payload, opts);
305
+ }
306
+ static broadcastAll(payload, opts) {
307
+ return sseManager.broadcastAll(payload, opts);
308
+ }
309
+ static setMeta(id, key, value) {
310
+ return sseManager.setMeta(id, key, value);
311
+ }
312
+ static getMeta(id, key) {
313
+ return sseManager.getMeta(id, key);
314
+ }
315
+ static getChannelMembers(channel) {
316
+ return sseManager.getChannelMembers(channel);
317
+ }
318
+ static getConnectionChannels(id) {
319
+ return sseManager.getConnectionChannels(id);
320
+ }
321
+ static listChannels() {
322
+ return sseManager.listChannels();
323
+ }
324
+ static get connectionCount() {
325
+ return sseManager.connectionCount;
326
+ }
327
+ static get channelCount() {
328
+ return sseManager.channelCount;
329
+ }
330
+ static getStats() {
331
+ return sseManager.getStats();
332
+ }
333
+ }
334
+
335
+ // shared/main/access-evaluator.ts
336
+ function evaluateAccessExpression(expr, ctx, action) {
337
+ if (expr === null) {
338
+ return null;
339
+ }
340
+ if (expr === undefined) {
341
+ return false;
342
+ }
343
+ if (typeof expr !== "object") {
344
+ return expr;
345
+ }
346
+ switch (expr.type) {
347
+ case "identifier":
348
+ return resolveIdentifier(expr.name, ctx);
349
+ case "memberExpression":
350
+ return resolveMember(expr.path, ctx);
351
+ case "unaryExpression":
352
+ return evalUnary(expr, ctx, action);
353
+ case "binaryExpression":
354
+ return evalBinary(expr, ctx, action);
355
+ default:
356
+ return false;
357
+ }
358
+ }
359
+ function resolveIdentifier(name, ctx) {
360
+ switch (name) {
361
+ case "user":
362
+ return ctx.user;
363
+ case "data":
364
+ return ctx.data;
365
+ case "id":
366
+ return ctx.id;
367
+ case "true":
368
+ return true;
369
+ case "false":
370
+ return false;
371
+ case "null":
372
+ return false;
373
+ default:
374
+ return false;
375
+ }
376
+ }
377
+ function resolveMember(path, ctx) {
378
+ const root = path[0];
379
+ let val;
380
+ switch (root) {
381
+ case "user":
382
+ val = ctx.user;
383
+ break;
384
+ case "data":
385
+ val = ctx.data;
386
+ break;
387
+ case "id":
388
+ val = ctx.id;
389
+ break;
390
+ default:
391
+ return;
392
+ }
393
+ for (let i = 1;i < path.length; i++) {
394
+ if (val === null || val === undefined)
395
+ return;
396
+ val = val[path[i]];
397
+ }
398
+ return val;
399
+ }
400
+ function evalUnary(expr, ctx, action) {
401
+ const inner = evaluateAccessExpression(expr.argument, ctx, action);
402
+ const boolVal = toBool(inner);
403
+ return !boolVal;
404
+ }
405
+ function evalBinary(expr, ctx, action) {
406
+ const { operator, left, right } = expr;
407
+ if (operator === "&&" || operator === "||") {
408
+ return evalLogical(operator, left, right, ctx, action);
409
+ }
410
+ return evalComparison(operator, left, right, ctx, action);
411
+ }
412
+ function evalLogical(op, leftExpr, rightExpr, ctx, action) {
413
+ const leftVal = evaluateAccessExpression(leftExpr, ctx, action);
414
+ const rightVal = evaluateAccessExpression(rightExpr, ctx, action);
415
+ if (op === "&&") {
416
+ if (!leftVal || !rightVal)
417
+ return false;
418
+ if (leftVal === true && rightVal === true)
419
+ return true;
420
+ if (leftVal === true && typeof rightVal === "object")
421
+ return rightVal;
422
+ if (typeof leftVal === "object" && rightVal === true)
423
+ return leftVal;
424
+ if (typeof leftVal === "object" && typeof rightVal === "object") {
425
+ return { AND: [leftVal, rightVal] };
426
+ }
427
+ }
428
+ if (op === "||") {
429
+ if (leftVal === true || rightVal === true)
430
+ return true;
431
+ if (typeof leftVal === "object" && typeof rightVal === "object") {
432
+ return { OR: [leftVal, rightVal] };
433
+ }
434
+ if (typeof leftVal === "object" && !rightVal)
435
+ return leftVal;
436
+ if (!leftVal && typeof rightVal === "object")
437
+ return rightVal;
438
+ }
439
+ return false;
440
+ }
441
+ function evalComparison(op, leftExpr, rightExpr, ctx, action) {
442
+ const leftVal = evaluateAccessExpression(leftExpr, ctx, action);
443
+ const rightVal = evaluateAccessExpression(rightExpr, ctx, action);
444
+ if (action !== "create" && (op === "==" || op === "!=")) {
445
+ if (leftExpr && leftExpr.type === "memberExpression" && leftExpr.path && leftExpr.path[0] === "data") {
446
+ const fieldName = leftExpr.path[1];
447
+ if (fieldName) {
448
+ if (rightVal === undefined)
449
+ return false;
450
+ const filterOp = op === "==" ? "eq" : "neq";
451
+ return { [fieldName]: { [filterOp]: rightVal } };
452
+ }
453
+ }
454
+ if (rightExpr && rightExpr.type === "memberExpression" && rightExpr.path && rightExpr.path[0] === "data") {
455
+ const fieldName = rightExpr.path[1];
456
+ if (fieldName) {
457
+ if (leftVal === undefined)
458
+ return false;
459
+ const filterOp = op === "==" ? "eq" : "neq";
460
+ return { [fieldName]: { [filterOp]: leftVal } };
461
+ }
462
+ }
463
+ }
464
+ const lv = leftVal;
465
+ const rv = rightVal;
466
+ switch (op) {
467
+ case "==":
468
+ return lv === rv;
469
+ case "!=":
470
+ return lv !== rv;
471
+ case ">":
472
+ return lv > rv;
473
+ case ">=":
474
+ return lv >= rv;
475
+ case "<":
476
+ return lv < rv;
477
+ case "<=":
478
+ return lv <= rv;
479
+ default:
480
+ return false;
481
+ }
482
+ }
483
+ function toBool(val) {
484
+ if (typeof val === "boolean")
485
+ return val;
486
+ if (val && typeof val === "object")
487
+ return Object.keys(val).length > 0;
488
+ return !!val;
489
+ }
490
+
491
+ // shared/main/router.ts
492
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
493
+ import { Value } from "@sinclair/typebox/value";
494
+
495
+ // shared/main/aot.ts
496
+ function compileRouteHandler(route, options, ctx) {
497
+ const needsRequestId = options.requestId?.enabled === true;
498
+ const needsUser = !!options.resolveUser;
499
+ const plugins = options.plugins || [];
500
+ const compileState = {};
501
+ const compileVars = {
502
+ request: "request",
503
+ server: "server",
504
+ ctx: "ctx",
505
+ routeCtx: "routeCtx",
506
+ response: "response",
507
+ errResponse: "errResponse",
508
+ error: "error",
509
+ pluginState: "pluginState"
510
+ };
511
+ const runPhase = (phase, extraVars) => {
512
+ let out = "";
513
+ const vars = extraVars ? { ...compileVars, ...extraVars } : compileVars;
514
+ for (const plugin of plugins) {
515
+ if (plugin.onCompile) {
516
+ const snippet = plugin.onCompile({ phase, vars, state: compileState });
517
+ if (snippet)
518
+ out += snippet + `
519
+ `;
520
+ }
521
+ }
522
+ return out;
523
+ };
524
+ const guards = route.options?.beforeHandle ? Array.isArray(route.options.beforeHandle) ? route.options.beforeHandle : [route.options.beforeHandle] : [];
525
+ const hasResponseHeaders = ctx.hasResponseHeaders === true;
526
+ const hasState = ctx.hasState === true;
527
+ const supportsStreaming = route.options?.stream === true;
528
+ let code = `
529
+ return function handler(request, server) {
530
+ const pluginState = {};
531
+ ${runPhase("request.start")}
532
+ `;
533
+ code += `
534
+ return dispatch(request, server, pluginState);
535
+ };
536
+ `;
537
+ code += `
538
+ function dispatch(request, server, pluginState) {
539
+ try {
540
+ `;
541
+ if (needsRequestId) {
542
+ code += `
543
+ const requestId = ctx.options.monitoring?.requestId?.(request) ?? ctx.ulid();
544
+ const res = ctx.requestContextStorage.run(requestId, () => runRequest(requestId, request, server, pluginState));
545
+ if (res instanceof Promise) {
546
+ return res.catch(err => {
547
+ const errResponse = ctx.options.onError ? ctx.options.onError(err, request) : ctx.toErrorResponse(err, request, ctx.options.cequre?.adapter);
548
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(request, undefined))" : "errResponse"};
549
+ });
550
+ }
551
+ return res;
552
+ `;
553
+ } else {
554
+ code += `
555
+ const res = runRequest(undefined, request, server, pluginState);
556
+ if (res instanceof Promise) {
557
+ return res.catch(err => {
558
+ const errResponse = ctx.options.onError ? ctx.options.onError(err, request) : ctx.toErrorResponse(err, request, ctx.options.cequre?.adapter);
559
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(request, undefined))" : "errResponse"};
560
+ });
561
+ }
562
+ return res;
563
+ `;
564
+ }
565
+ code += `
566
+ } catch (error) {
567
+ const errResponse = ctx.options.onError ? ctx.options.onError(error, request) : ctx.toErrorResponse(error, request, ctx.options.cequre?.adapter);
568
+ ${runPhase("request.error")}
569
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(request, undefined))" : "errResponse"};
570
+ }
571
+ }
572
+ `;
573
+ code += `
574
+ function runRequest(requestId, request, server, pluginState) {
575
+ const requestHeaders = requestId ? { "X-Request-ID": requestId } : undefined;
576
+ const handlerRequest = requestId
577
+ ? new Request(request, { headers: new Headers([...request.headers, ["X-Request-ID", requestId]]) })
578
+ : request;
579
+ `;
580
+ if (hasState) {
581
+ code += `
582
+ const state = ctx.resolveState(server);
583
+ `;
584
+ } else {
585
+ code += `
586
+ const state = undefined;
587
+ `;
588
+ }
589
+ code += `
590
+ function executePluginsAndProcess(user, state) {
591
+ `;
592
+ if (plugins.length > 0) {
593
+ code += `
594
+ const pluginCtx = { request: handlerRequest, user, cequre: ctx.options.cequre, state };
595
+ `;
596
+ code += `
597
+ function runPlugin(index) {
598
+ if (index >= ${plugins.length}) return processRoute(pluginCtx.user !== undefined ? pluginCtx.user : user, pluginCtx.state !== undefined ? pluginCtx.state : state);
599
+ const plugin = ctx.options.plugins[index];
600
+ if (plugin.beforeRequest) {
601
+ try {
602
+ const res = plugin.beforeRequest(pluginCtx);
603
+ if (res instanceof Promise) {
604
+ return res.then(() => runPlugin(index + 1)).catch((err) => {
605
+ const errResponse = ctx.options.onError ? ctx.options.onError(err, handlerRequest) : ctx.toErrorResponse(err, handlerRequest, ctx.options.cequre?.adapter);
606
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(handlerRequest, requestHeaders))" : "errResponse"};
607
+ });
608
+ }
609
+ } catch (err) {
610
+ const errResponse = ctx.options.onError ? ctx.options.onError(err, handlerRequest) : ctx.toErrorResponse(err, handlerRequest, ctx.options.cequre?.adapter);
611
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(handlerRequest, requestHeaders))" : "errResponse"};
612
+ }
613
+ }
614
+ return runPlugin(index + 1);
615
+ }
616
+ return runPlugin(0);
617
+ `;
618
+ } else {
619
+ code += ` return processRoute(user, state);
620
+ `;
621
+ }
622
+ code += `
623
+ }
624
+ `;
625
+ if (needsUser) {
626
+ code += `
627
+ const userResult = ctx.options.resolveUser(handlerRequest);
628
+ if (userResult instanceof Promise) {
629
+ return userResult.then((user) => executePluginsAndProcess(user, state));
630
+ }
631
+ return executePluginsAndProcess(userResult, state);
632
+ `;
633
+ } else {
634
+ code += ` return executePluginsAndProcess(null, state);
635
+ `;
636
+ }
637
+ code += `
638
+ function processRoute(user, state) {
639
+ `;
640
+ if (route.shouldReadBody) {
641
+ code += `
642
+ const rawBodyResult = ctx.readBody(handlerRequest, ctx.options.maxBodyBytes);
643
+ if (rawBodyResult instanceof Promise) {
644
+ return rawBodyResult.then((rawBody) => {
645
+ const body = ctx.route.bodyCoercer ? ctx.route.bodyCoercer(rawBody) : rawBody;
646
+ return runHandler(user, state, body);
647
+ });
648
+ }
649
+ const body = ctx.route.bodyCoercer ? ctx.route.bodyCoercer(rawBodyResult) : rawBodyResult;
650
+ return runHandler(user, state, body);
651
+ `;
652
+ } else {
653
+ code += ` return runHandler(user, state, undefined);
654
+ `;
655
+ }
656
+ code += ` }
657
+ `;
658
+ code += `
659
+ function runHandler(user, state, body) {
660
+ `;
661
+ if (supportsStreaming) {
662
+ code += `
663
+ let resolveStream;
664
+ const streamPromise = new Promise((resolve) => { resolveStream = resolve; });
665
+ `;
666
+ }
667
+ code += `
668
+ const routeCtx = ctx.createRouteContext(ctx.route, handlerRequest, request.params ?? ctx.EMPTY_PARAMS, state, user, body, ctx.options.cequre);
669
+ `;
670
+ if (supportsStreaming) {
671
+ code += `
672
+ routeCtx._streamResolver = resolveStream;
673
+ `;
674
+ }
675
+ code += `
676
+ let handlerResult;
677
+ try {
678
+ ${runPhase("beforeHandler")}
679
+ `;
680
+ if (guards.length > 0) {
681
+ code += `
682
+ const rawGuards = ctx.route.options.beforeHandle;
683
+ const guards = Array.isArray(rawGuards) ? rawGuards : [rawGuards];
684
+ function executeGuards(index) {
685
+ if (index >= guards.length) return ctx.route.handler(routeCtx);
686
+ const guardResult = guards[index](routeCtx);
687
+ if (guardResult instanceof Promise) {
688
+ return guardResult.then((res) => {
689
+ if (res !== undefined) return res;
690
+ return executeGuards(index + 1);
691
+ });
692
+ }
693
+ if (guardResult !== undefined) return guardResult;
694
+ return executeGuards(index + 1);
695
+ }
696
+ handlerResult = executeGuards(0);
697
+ `;
698
+ } else {
699
+ code += ` handlerResult = ctx.route.handler(routeCtx);
700
+ `;
701
+ }
702
+ code += `
703
+ } catch (syncErr) {
704
+ const errResponse = ctx.options.onError ? ctx.options.onError(syncErr, handlerRequest) : ctx.toErrorResponse(syncErr, handlerRequest, ctx.options.cequre?.adapter);
705
+ ${runPhase("request.error", { error: "syncErr" })}
706
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(handlerRequest, requestHeaders))" : "errResponse"};
707
+ }
708
+ `;
709
+ code += `
710
+ if (handlerResult instanceof Promise) {
711
+ const finalPromise = handlerResult.then((res) => {
712
+ `;
713
+ if (supportsStreaming) {
714
+ code += `
715
+ if (routeCtx._streamInitiated) {
716
+ if (routeCtx._streamWriter) routeCtx._streamWriter.close().catch(()=>{});
717
+ return streamPromise;
718
+ }
719
+ `;
720
+ }
721
+ code += `
722
+ return finish(res, user);
723
+ }).catch((asyncErr) => {
724
+ `;
725
+ if (supportsStreaming) {
726
+ code += `
727
+ if (routeCtx._streamInitiated) {
728
+ if (routeCtx._streamWriter) routeCtx._streamWriter.close().catch(()=>{});
729
+ return undefined;
730
+ }
731
+ `;
732
+ }
733
+ code += `
734
+ const errResponse = ctx.options.onError ? ctx.options.onError(asyncErr, handlerRequest) : ctx.toErrorResponse(asyncErr, handlerRequest, ctx.options.cequre?.adapter);
735
+ ${runPhase("request.error", { error: "asyncErr" })}
736
+ return ${hasResponseHeaders ? "ctx.withHeaders(errResponse, ctx.requestResponseHeaders(handlerRequest, requestHeaders))" : "errResponse"};
737
+ });
738
+ `;
739
+ code += `
740
+ const timeoutMs = ctx.options.cequre?.schema?.core?.api?.requestTimeoutMs;
741
+ let finalExecutionPromise = ${supportsStreaming ? "Promise.race([streamPromise, finalPromise])" : "finalPromise"};
742
+ if (timeoutMs) {
743
+ let timeoutHandle;
744
+ const timeoutPromise = new Promise((_, reject) => {
745
+ timeoutHandle = setTimeout(() => reject(new ctx.CequreError("Request Timeout", "TIMEOUT", 408)), timeoutMs);
746
+ });
747
+ finalExecutionPromise = Promise.race([
748
+ finalExecutionPromise.finally(() => clearTimeout(timeoutHandle)),
749
+ timeoutPromise
750
+ ]);
751
+ }
752
+ return finalExecutionPromise;
753
+ `;
754
+ code += ` }
755
+ `;
756
+ if (supportsStreaming) {
757
+ code += `
758
+ if (routeCtx._streamInitiated) {
759
+ if (routeCtx._streamWriter) routeCtx._streamWriter.close().catch(()=>{});
760
+ return streamPromise;
761
+ }
762
+ `;
763
+ }
764
+ code += `
765
+ return finish(handlerResult, user);
766
+ }
767
+ `;
768
+ code += `
769
+ function finish(resolvedResult, user) {
770
+ const response = ctx.toResponse(resolvedResult);
771
+ ${runPhase("request.completed")}
772
+ `;
773
+ if (hasResponseHeaders) {
774
+ code += ` const finalResponse = ctx.withHeaders(response, ctx.requestResponseHeaders(handlerRequest, requestHeaders));
775
+ `;
776
+ } else {
777
+ code += ` const finalResponse = response;
778
+ `;
779
+ }
780
+ code += ` ${runPhase("afterHandler", { response: "finalResponse" })}
781
+ `;
782
+ if (plugins.length > 0) {
783
+ code += `
784
+ const pluginCtx = { request: handlerRequest, user, cequre: ctx.options.cequre, state: null };
785
+ function runAfterPlugin(index) {
786
+ if (index >= ${plugins.length}) return finalResponse;
787
+ const plugin = ctx.options.plugins[index];
788
+ if (plugin.afterRequest) {
789
+ try {
790
+ const afterRes = plugin.afterRequest(pluginCtx, finalResponse);
791
+ if (afterRes instanceof Promise) {
792
+ return afterRes.then(() => runAfterPlugin(index + 1)).catch(() => runAfterPlugin(index + 1));
793
+ }
794
+ } catch (err) { console.error("AOT AFTERREQUEST ERR:", err); }
795
+ }
796
+ return runAfterPlugin(index + 1);
797
+ }
798
+ return runAfterPlugin(0);
799
+ `;
800
+ } else {
801
+ code += ` return finalResponse;
802
+ `;
803
+ }
804
+ code += ` }
805
+ `;
806
+ code += `
807
+ }
808
+ `;
809
+ return new Function("ctx", code)(ctx);
810
+ }
811
+
812
+ // shared/main/router.ts
813
+ var BODY_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
814
+ var EMPTY_PARAMS = Object.freeze({});
815
+ var EMPTY_QUERY = Object.freeze({});
816
+ function normalizePath(path) {
817
+ if (!path)
818
+ return "/";
819
+ const prefixed = path.startsWith("/") ? path : `/${path}`;
820
+ if (prefixed.length === 1)
821
+ return prefixed;
822
+ return prefixed.replace(/\/+$/, "");
823
+ }
824
+ function splitPath(path) {
825
+ const normalized = normalizePath(path);
826
+ if (normalized === "/")
827
+ return [];
828
+ return normalized.slice(1).split("/");
829
+ }
830
+ function matchPath(route, pathname) {
831
+ const requestSegments = splitPath(pathname);
832
+ if (route.segments.length !== requestSegments.length)
833
+ return null;
834
+ const params = {};
835
+ for (let i = 0;i < route.segments.length; i++) {
836
+ const expected = route.segments[i];
837
+ const actual = requestSegments[i];
838
+ if (expected === undefined || actual === undefined)
839
+ return null;
840
+ if (expected.startsWith(":")) {
841
+ params[expected.slice(1)] = decodeURIComponent(actual);
842
+ continue;
843
+ }
844
+ if (expected !== actual)
845
+ return null;
846
+ }
847
+ return params;
848
+ }
849
+ function parseQueryString(url) {
850
+ const queryStart = url.indexOf("?");
851
+ if (queryStart === -1)
852
+ return EMPTY_QUERY;
853
+ const hashStart = url.indexOf("#", queryStart + 1);
854
+ const queryEnd = hashStart === -1 ? url.length : hashStart;
855
+ if (queryEnd <= queryStart + 1)
856
+ return {};
857
+ const query = {};
858
+ let pairStart = queryStart + 1;
859
+ while (pairStart < queryEnd) {
860
+ const ampersand = url.indexOf("&", pairStart);
861
+ const pairEnd = ampersand === -1 || ampersand > queryEnd ? queryEnd : ampersand;
862
+ if (pairEnd === pairStart) {
863
+ pairStart = pairEnd + 1;
864
+ continue;
865
+ }
866
+ const separator = url.indexOf("=", pairStart);
867
+ const valueStart = separator === -1 || separator > pairEnd ? pairEnd : separator + 1;
868
+ const rawKey = separator === -1 || separator > pairEnd ? url.slice(pairStart, pairEnd) : url.slice(pairStart, separator);
869
+ const rawValue = valueStart === pairEnd ? "" : url.slice(valueStart, pairEnd);
870
+ const key = decodeQueryPart(rawKey);
871
+ const value = decodeQueryPart(rawValue);
872
+ const existing = query[key];
873
+ if (existing === undefined) {
874
+ query[key] = value;
875
+ } else if (Array.isArray(existing)) {
876
+ existing.push(value);
877
+ } else {
878
+ query[key] = [existing, value];
879
+ }
880
+ pairStart = pairEnd + 1;
881
+ }
882
+ return query;
883
+ }
884
+ function decodeQueryPart(value) {
885
+ if (!value.includes("+") && !value.includes("%"))
886
+ return value;
887
+ try {
888
+ return decodeURIComponent(value.replace(/\+/g, " "));
889
+ } catch {
890
+ return value;
891
+ }
892
+ }
893
+ function createFastObjectConverter(schema) {
894
+ if (schema.type !== "object")
895
+ return;
896
+ const properties = schema.properties;
897
+ if (!properties)
898
+ return;
899
+ const converters = [];
900
+ for (const [key, property] of Object.entries(properties)) {
901
+ if (property.type === "string") {
902
+ converters.push([key, (value) => typeof value === "string" || Array.isArray(value) ? value : String(value)]);
903
+ continue;
904
+ }
905
+ if (property.type === "number") {
906
+ converters.push([
907
+ key,
908
+ (value) => {
909
+ if (typeof value !== "string")
910
+ return value;
911
+ if (!/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(value))
912
+ return value;
913
+ return Number(value);
914
+ }
915
+ ]);
916
+ continue;
917
+ }
918
+ if (property.type === "boolean") {
919
+ converters.push([
920
+ key,
921
+ (value) => {
922
+ if (value === "true" || value === "1")
923
+ return true;
924
+ if (value === "false" || value === "0")
925
+ return false;
926
+ return value;
927
+ }
928
+ ]);
929
+ continue;
930
+ }
931
+ return;
932
+ }
933
+ return (value) => {
934
+ if (!value || typeof value !== "object" || Array.isArray(value))
935
+ return value;
936
+ const converted = { ...value };
937
+ for (const [key, convert] of converters) {
938
+ if (key in converted)
939
+ converted[key] = convert(converted[key]);
940
+ }
941
+ return converted;
942
+ };
943
+ }
944
+ function createSchemaCoercer(schema, label) {
945
+ if (!schema)
946
+ return;
947
+ const check = TypeCompiler.Compile(schema);
948
+ const fastConvert = createFastObjectConverter(schema);
949
+ return (value) => {
950
+ if (check.Check(value))
951
+ return value;
952
+ const converted = fastConvert?.(value) ?? Value.Convert(schema, value);
953
+ if (check.Check(converted))
954
+ return converted;
955
+ const firstError = check.Errors(converted).First();
956
+ const path = firstError?.path ? ` at ${firstError.path}` : "";
957
+ throw CequreError.BadRequest(`${label} does not match route schema${path}`);
958
+ };
959
+ }
960
+ function resolveCorsHeaders(origin, cors) {
961
+ if (!cors || !origin)
962
+ return;
963
+ const allowed = cors.origin;
964
+ const allowAny = allowed === "*" || Array.isArray(allowed) && allowed.includes("*");
965
+ const allowedOrigins = Array.isArray(allowed) ? allowed : [allowed];
966
+ const allowOrigin = allowAny ? origin : allowedOrigins.includes(origin) ? origin : undefined;
967
+ if (!allowOrigin)
968
+ return;
969
+ return {
970
+ "Access-Control-Allow-Origin": allowOrigin,
971
+ ...cors.credentials ? { "Access-Control-Allow-Credentials": "true" } : {},
972
+ Vary: "Origin"
973
+ };
974
+ }
975
+ function createCorsPreflightResponse(request, cors, methods, baseHeaders) {
976
+ const corsHeaders = resolveCorsHeaders(request.headers.get("origin"), cors);
977
+ if (!corsHeaders)
978
+ return new Response(null, { status: 403, headers: baseHeaders });
979
+ const requestedHeaders = request.headers.get("access-control-request-headers");
980
+ return new Response(null, {
981
+ status: 204,
982
+ headers: {
983
+ ...baseHeaders,
984
+ ...corsHeaders,
985
+ "Access-Control-Allow-Methods": [...new Set([...methods, "OPTIONS"])].sort().join(", "),
986
+ "Access-Control-Allow-Headers": requestedHeaders || "Content-Type, Authorization, X-API-Key, X-CEQURE-CSRF, X-CSRF-Token, X-Requested-With",
987
+ "Access-Control-Max-Age": "86400"
988
+ }
989
+ });
990
+ }
991
+ function readBody(request, maxBodyBytes) {
992
+ if (!request.body)
993
+ return;
994
+ if (maxBodyBytes && maxBodyBytes > 0) {
995
+ const contentLength = parseInt(request.headers.get("content-length") ?? "0", 10);
996
+ if (contentLength > maxBodyBytes) {
997
+ throw CequreError.BadRequest("Request body too large");
998
+ }
999
+ }
1000
+ const contentType = request.headers.get("content-type") ?? "";
1001
+ if (contentType.includes("application/json")) {
1002
+ try {
1003
+ return request.json();
1004
+ } catch {
1005
+ throw CequreError.BadRequest("Invalid JSON body");
1006
+ }
1007
+ }
1008
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
1009
+ return request.formData().then((formData) => {
1010
+ const entries = {};
1011
+ for (const [key, value] of formData) {
1012
+ const existing = entries[key];
1013
+ if (existing === undefined)
1014
+ entries[key] = value;
1015
+ else if (Array.isArray(existing))
1016
+ existing.push(value);
1017
+ else
1018
+ entries[key] = [existing, value];
1019
+ }
1020
+ return entries;
1021
+ });
1022
+ }
1023
+ return request.text();
1024
+ }
1025
+ function createRouteContext2(route, request, params, state, user, body, cequre) {
1026
+ let url;
1027
+ let parsedQuery;
1028
+ let query;
1029
+ if (route.queryCoercer) {
1030
+ parsedQuery = parseQueryString(request.url);
1031
+ query = route.queryCoercer(parsedQuery);
1032
+ }
1033
+ return {
1034
+ request,
1035
+ get url() {
1036
+ return url ??= new URL(request.url);
1037
+ },
1038
+ params: route.paramsCoercer?.(params) ?? params,
1039
+ get query() {
1040
+ if (query !== undefined)
1041
+ return query;
1042
+ if (parsedQuery === undefined)
1043
+ parsedQuery = parseQueryString(request.url);
1044
+ query = parsedQuery;
1045
+ return query;
1046
+ },
1047
+ body,
1048
+ cequre,
1049
+ user,
1050
+ state,
1051
+ async stream(chunk, options) {
1052
+ if (!this._streamInitiated) {
1053
+ this._streamInitiated = true;
1054
+ const { readable, writable } = new TransformStream;
1055
+ this._streamWriter = writable.getWriter();
1056
+ if (this._streamResolver) {
1057
+ const headers = new Headers;
1058
+ if (options?.event) {
1059
+ headers.set("Content-Type", "text/event-stream");
1060
+ headers.set("Cache-Control", "no-cache");
1061
+ headers.set("Connection", "keep-alive");
1062
+ } else {
1063
+ headers.set("Content-Type", "text/plain");
1064
+ }
1065
+ this._streamResolver(new Response(readable, { headers }));
1066
+ }
1067
+ }
1068
+ if (!this._streamWriter)
1069
+ return;
1070
+ const encoder2 = new TextEncoder;
1071
+ let data = chunk;
1072
+ if (options?.event) {
1073
+ if (typeof data !== "string" && !(data instanceof Uint8Array)) {
1074
+ data = JSON.stringify(data);
1075
+ }
1076
+ data = `event: ${options.event}
1077
+ data: ${data}
1078
+
1079
+ `;
1080
+ } else if (typeof data !== "string" && !(data instanceof Uint8Array)) {
1081
+ data = JSON.stringify(data) + `
1082
+ `;
1083
+ }
1084
+ if (typeof data === "string") {
1085
+ data = encoder2.encode(data);
1086
+ }
1087
+ if (this.request.signal.aborted) {
1088
+ throw new Error("ClientDisconnected: stream aborted by client");
1089
+ }
1090
+ try {
1091
+ await this._streamWriter.write(data);
1092
+ } catch (err) {
1093
+ throw new Error("ClientDisconnected: stream write failed");
1094
+ }
1095
+ if (options?.delay) {
1096
+ await new Promise((r) => setTimeout(r, options.delay));
1097
+ }
1098
+ }
1099
+ };
1100
+ }
1101
+
1102
+ class CequreRouter {
1103
+ routes = [];
1104
+ prefix;
1105
+ constructor(prefix = "") {
1106
+ this.prefix = prefix || "";
1107
+ }
1108
+ applyPrefix(path, options) {
1109
+ if (options?.ignorePrefix)
1110
+ return path;
1111
+ if (!this.prefix)
1112
+ return path;
1113
+ const normalized = normalizePath(path);
1114
+ if (normalized === "/")
1115
+ return normalized;
1116
+ if (normalized === this.prefix)
1117
+ return normalized;
1118
+ if (normalized.startsWith(this.prefix + "/"))
1119
+ return normalized;
1120
+ return `${this.prefix}${normalized}`;
1121
+ }
1122
+ matchRoute(method, pathname) {
1123
+ for (const route of this.routes) {
1124
+ if (route.method !== "ALL" && route.method !== method.toUpperCase())
1125
+ continue;
1126
+ const params = matchPath(route, pathname);
1127
+ if (params !== null) {
1128
+ return route;
1129
+ }
1130
+ }
1131
+ return;
1132
+ }
1133
+ add(method, path, handler, options) {
1134
+ const normalizedPath = this.applyPrefix(path, options);
1135
+ this.routes.push({
1136
+ method: method.toUpperCase(),
1137
+ path: normalizedPath,
1138
+ segments: splitPath(normalizedPath),
1139
+ handler,
1140
+ options,
1141
+ bodyCoercer: createSchemaCoercer(options?.body, "Body"),
1142
+ queryCoercer: createSchemaCoercer(options?.query, "Query"),
1143
+ paramsCoercer: createSchemaCoercer(options?.params, "Params"),
1144
+ shouldReadBody: Boolean(options?.body && BODY_METHODS.has(method.toUpperCase()))
1145
+ });
1146
+ return this;
1147
+ }
1148
+ get(path, handler, options) {
1149
+ return this.add("GET", path, handler, options);
1150
+ }
1151
+ post(path, handler, options) {
1152
+ return this.add("POST", path, handler, options);
1153
+ }
1154
+ put(path, handler, options) {
1155
+ return this.add("PUT", path, handler, options);
1156
+ }
1157
+ patch(path, handler, options) {
1158
+ return this.add("PATCH", path, handler, options);
1159
+ }
1160
+ delete(path, handler, options) {
1161
+ return this.add("DELETE", path, handler, options);
1162
+ }
1163
+ all(path, handler, options) {
1164
+ return this.add("ALL", path, handler, options);
1165
+ }
1166
+ list() {
1167
+ return this.routes.map(({ method, path, handler, options }) => ({ method, path, handler, options }));
1168
+ }
1169
+ use(routes) {
1170
+ if (!routes)
1171
+ return this;
1172
+ const routeList = routes instanceof CequreRouter ? routes.list() : routes;
1173
+ for (const route of routeList) {
1174
+ this.add(route.method, route.path, route.handler, route.options);
1175
+ }
1176
+ return this;
1177
+ }
1178
+ toNativeRoutes(options = {}) {
1179
+ const nativeRoutes = {};
1180
+ const grouped = new Map;
1181
+ const headers = Object.entries(options.headers ?? {});
1182
+ const hasMonitoring = options.monitoring?.hasHandlers?.() === true;
1183
+ const shouldResolveUser = options.resolveUser !== undefined;
1184
+ const maxBodyBytes = options.maxBodyBytes;
1185
+ const withHeaders = (response, extraHeaders) => {
1186
+ if (!response)
1187
+ return response;
1188
+ if (headers.length === 0 && !extraHeaders)
1189
+ return response;
1190
+ if (headers.length > 0) {
1191
+ for (let i = 0;i < headers.length; i++) {
1192
+ const header = headers[i];
1193
+ if (!header)
1194
+ continue;
1195
+ const [key, value] = header;
1196
+ if (!response.headers.has(key))
1197
+ response.headers.set(key, value);
1198
+ }
1199
+ }
1200
+ if (extraHeaders) {
1201
+ for (const key in extraHeaders) {
1202
+ const val = extraHeaders[key];
1203
+ if (val !== undefined && !response.headers.has(key))
1204
+ response.headers.set(key, val);
1205
+ }
1206
+ }
1207
+ return response;
1208
+ };
1209
+ const requestResponseHeaders = options.cors ? (request, routeHeaders) => {
1210
+ const origin = request.headers.get("origin");
1211
+ const corsHeaders = origin ? resolveCorsHeaders(origin, options.cors) : undefined;
1212
+ return corsHeaders ? { ...corsHeaders, ...routeHeaders } : routeHeaders;
1213
+ } : (_request, routeHeaders) => routeHeaders;
1214
+ const stateCache = new WeakMap;
1215
+ let undefinedServerState;
1216
+ let hasUndefinedServerState = false;
1217
+ const resolveState = typeof options.state === "function" ? (server) => {
1218
+ if (!server) {
1219
+ if (!hasUndefinedServerState) {
1220
+ undefinedServerState = options.state(server);
1221
+ hasUndefinedServerState = true;
1222
+ }
1223
+ return undefinedServerState;
1224
+ }
1225
+ const cached = stateCache.get(server);
1226
+ if (cached !== undefined)
1227
+ return cached;
1228
+ const state = options.state(server);
1229
+ stateCache.set(server, state);
1230
+ return state;
1231
+ } : () => options.state;
1232
+ for (const route of this.routes) {
1233
+ const routes = grouped.get(route.path) ?? [];
1234
+ routes.push(route);
1235
+ grouped.set(route.path, routes);
1236
+ }
1237
+ const createHandler = (route) => {
1238
+ const ctx = {
1239
+ route,
1240
+ options,
1241
+ performance: globalThis.performance,
1242
+ extractPathname,
1243
+ ulid,
1244
+ withHeaders,
1245
+ requestResponseHeaders,
1246
+ requestContextStorage,
1247
+ CequreError,
1248
+ toErrorResponse,
1249
+ toResponse,
1250
+ readBody,
1251
+ createRouteContext: createRouteContext2,
1252
+ EMPTY_PARAMS,
1253
+ resolveState,
1254
+ hasResponseHeaders: headers.length > 0 || !!options.cors,
1255
+ hasState: typeof options.state === "function"
1256
+ };
1257
+ return compileRouteHandler(route, options, ctx);
1258
+ };
1259
+ for (const [path, routes] of grouped) {
1260
+ const allRoute = routes.find((route) => route.method === "ALL");
1261
+ if (allRoute) {
1262
+ nativeRoutes[path] = createHandler(allRoute);
1263
+ continue;
1264
+ }
1265
+ const methods = routes.map((route) => route.method);
1266
+ nativeRoutes[path] = Object.fromEntries([...routes].reverse().map((route) => [route.method, createHandler(route)]));
1267
+ if (options.cors && typeof nativeRoutes[path] === "object" && nativeRoutes[path] !== null) {
1268
+ nativeRoutes[path].OPTIONS = (request) => createCorsPreflightResponse(request, options.cors, methods, Object.fromEntries(headers));
1269
+ }
1270
+ }
1271
+ return nativeRoutes;
1272
+ }
1273
+ async handle(request, state, user = null, cequre) {
1274
+ const url = new URL(request.url);
1275
+ const method = request.method.toUpperCase();
1276
+ const allowedMethods = new Set;
1277
+ for (const route of this.routes) {
1278
+ const params = matchPath(route, url.pathname);
1279
+ if (!params)
1280
+ continue;
1281
+ if (route.method !== method && route.method !== "ALL") {
1282
+ allowedMethods.add(route.method);
1283
+ continue;
1284
+ }
1285
+ const body = route.shouldReadBody ? route.bodyCoercer?.(await readBody(request, cequre?.schema?.core?.api?.maxBodyBytes)) : undefined;
1286
+ const context = createRouteContext2(route, request, params, state, user, body, cequre);
1287
+ let resolveStream;
1288
+ const streamPromise = new Promise((resolve) => {
1289
+ resolveStream = resolve;
1290
+ });
1291
+ context._streamResolver = resolveStream;
1292
+ let handlerResult;
1293
+ try {
1294
+ handlerResult = route.handler(context);
1295
+ } catch (syncErr) {
1296
+ throw syncErr;
1297
+ }
1298
+ if (handlerResult instanceof Promise) {
1299
+ return Promise.race([
1300
+ streamPromise,
1301
+ handlerResult.then((res) => {
1302
+ if (context._streamInitiated) {
1303
+ context._streamWriter?.close();
1304
+ return streamPromise;
1305
+ }
1306
+ return toResponse(res);
1307
+ })
1308
+ ]);
1309
+ }
1310
+ if (context._streamInitiated) {
1311
+ context._streamWriter?.close();
1312
+ return streamPromise;
1313
+ }
1314
+ return toResponse(handlerResult);
1315
+ }
1316
+ if (allowedMethods.size > 0) {
1317
+ return methodNotAllowed([...allowedMethods].sort());
1318
+ }
1319
+ return notFound();
1320
+ }
1321
+ }
1322
+ function createRouter() {
1323
+ return new CequreRouter;
1324
+ }
1325
+
1326
+ // shared/main/population.ts
1327
+ async function populateDocuments(schema, collectionSlug, docs, adapter, depth) {
1328
+ let currentDepth = depth;
1329
+ if (currentDepth > 2)
1330
+ currentDepth = 2;
1331
+ if (currentDepth <= 0 || !docs || docs.length === 0) {
1332
+ return docs;
1333
+ }
1334
+ const collection = (schema.collections || []).concat(schema.globals || []).find((c) => c.slug === collectionSlug);
1335
+ if (!collection)
1336
+ return docs;
1337
+ const relationFields = collection.fields.filter((f) => (f.type === "relation" || f.type === "relationship" || f.type === "link") && f.target);
1338
+ if (relationFields.length === 0)
1339
+ return docs;
1340
+ const hydratedDocs = JSON.parse(JSON.stringify(docs));
1341
+ for (const field of relationFields) {
1342
+ const targetCollection = field.target;
1343
+ const fieldName = field.name;
1344
+ const isArray = !!field.isArray;
1345
+ const isVirtualLink = field.type === "link";
1346
+ const onField = field.on;
1347
+ if (isVirtualLink && onField) {
1348
+ const sourceIds = hydratedDocs.map((doc) => doc.id).filter(Boolean);
1349
+ if (sourceIds.length === 0)
1350
+ continue;
1351
+ let relatedRecords = [];
1352
+ try {
1353
+ const result = await adapter.find(targetCollection, {
1354
+ where: { [onField]: { in: sourceIds } },
1355
+ limit: isArray ? 1000 : sourceIds.length
1356
+ });
1357
+ relatedRecords = result.docs;
1358
+ } catch (e) {
1359
+ console.error("VIRTUAL LINK FETCH ERROR:", e);
1360
+ continue;
1361
+ }
1362
+ let fullyPopulatedRelatedRecords = relatedRecords;
1363
+ if (currentDepth > 1) {
1364
+ fullyPopulatedRelatedRecords = await populateDocuments(schema, targetCollection, relatedRecords, adapter, currentDepth - 1);
1365
+ }
1366
+ const relatedMap = new Map;
1367
+ for (const record of fullyPopulatedRelatedRecords) {
1368
+ const sourceId = typeof record[onField] === "string" ? record[onField] : record[onField]?.id;
1369
+ if (sourceId) {
1370
+ if (!relatedMap.has(sourceId))
1371
+ relatedMap.set(sourceId, []);
1372
+ relatedMap.get(sourceId).push(record);
1373
+ }
1374
+ }
1375
+ for (const doc of hydratedDocs) {
1376
+ if (!doc.id)
1377
+ continue;
1378
+ const matches = relatedMap.get(doc.id) || [];
1379
+ if (isArray) {
1380
+ doc[fieldName] = matches;
1381
+ } else {
1382
+ doc[fieldName] = matches.length > 0 ? matches[0] : null;
1383
+ }
1384
+ }
1385
+ } else {
1386
+ const uniqueIds = new Set;
1387
+ for (const doc of hydratedDocs) {
1388
+ const val = doc[fieldName];
1389
+ if (val) {
1390
+ if (isArray && Array.isArray(val)) {
1391
+ for (const id of val) {
1392
+ if (typeof id === "string")
1393
+ uniqueIds.add(id);
1394
+ else if (id && typeof id === "object" && typeof id.id === "string")
1395
+ uniqueIds.add(id.id);
1396
+ }
1397
+ } else {
1398
+ if (typeof val === "string")
1399
+ uniqueIds.add(val);
1400
+ else if (typeof val === "object" && typeof val.id === "string")
1401
+ uniqueIds.add(val.id);
1402
+ }
1403
+ }
1404
+ }
1405
+ if (uniqueIds.size === 0)
1406
+ continue;
1407
+ const targetIds = Array.from(uniqueIds);
1408
+ let relatedRecords = [];
1409
+ try {
1410
+ const result = await adapter.find(targetCollection, {
1411
+ where: { id: { in: targetIds } },
1412
+ limit: targetIds.length
1413
+ });
1414
+ relatedRecords = result.docs;
1415
+ } catch (e) {
1416
+ continue;
1417
+ }
1418
+ let fullyPopulatedRelatedRecords = relatedRecords;
1419
+ if (currentDepth > 1) {
1420
+ fullyPopulatedRelatedRecords = await populateDocuments(schema, targetCollection, relatedRecords, adapter, currentDepth - 1);
1421
+ }
1422
+ const relatedMap = new Map;
1423
+ for (const record of fullyPopulatedRelatedRecords) {
1424
+ relatedMap.set(record.id, record);
1425
+ }
1426
+ for (const doc of hydratedDocs) {
1427
+ const val = doc[fieldName];
1428
+ if (val) {
1429
+ if (isArray && Array.isArray(val)) {
1430
+ doc[fieldName] = val.map((id) => {
1431
+ const rawId = typeof id === "string" ? id : id.id;
1432
+ const hydrated = relatedMap.get(rawId);
1433
+ return hydrated ? hydrated : id;
1434
+ });
1435
+ } else {
1436
+ const rawId = typeof val === "string" ? val : val.id;
1437
+ const hydrated = relatedMap.get(rawId);
1438
+ if (hydrated) {
1439
+ doc[fieldName] = hydrated;
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ return hydratedDocs;
1447
+ }
1448
+
1449
+ // shared/main/runtime.ts
1450
+ init_openapi();
1451
+ init_kv();
1452
+ import { RateLimiter } from "@cequrebackends/cequre-plugin-security";
1453
+ init_logger();
1454
+ import path from "path";
1455
+ import { AsyncLocalStorage } from "async_hooks";
1456
+ import { computeAuditHmac, verifyAuditChain } from "@cequrebackends/cequre-plugin-security";
1457
+ import { encryptField, decryptField, isEncrypted } from "@cequrebackends/cequre-plugin-security";
1458
+
1459
+ // shared/utils/crypto.ts
1460
+ async function sha256Hex(input) {
1461
+ const data = new TextEncoder().encode(input);
1462
+ const buf = await crypto.subtle.digest("SHA-256", data);
1463
+ return Array.from(new Uint8Array(buf)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1464
+ }
1465
+ function timingSafeEqual(a, b) {
1466
+ if (a.length !== b.length)
1467
+ return false;
1468
+ let diff = 0;
1469
+ for (let i = 0;i < a.length; i++)
1470
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1471
+ return diff === 0;
1472
+ }
1473
+
1474
+ // shared/utils/durable-queue.ts
1475
+ init_logger();
1476
+
1477
+ class CequreDurableQueue {
1478
+ adapter;
1479
+ storage;
1480
+ intervalId;
1481
+ constructor(adapter, storage) {
1482
+ this.adapter = adapter;
1483
+ this.storage = storage;
1484
+ }
1485
+ async enqueue(task, payload) {
1486
+ try {
1487
+ await this.adapter.create("cequre_jobs", {
1488
+ task,
1489
+ payload,
1490
+ status: "pending",
1491
+ retries: 0
1492
+ });
1493
+ } catch (e) {
1494
+ logger.error(e, "[CequreQueue] Failed to enqueue task:");
1495
+ }
1496
+ }
1497
+ startWorker(intervalMs = 60000) {
1498
+ if (this.intervalId)
1499
+ return;
1500
+ this.intervalId = setInterval(async () => {
1501
+ try {
1502
+ await this.processNextBatch();
1503
+ } catch (e) {
1504
+ logger.error(e, "[CequreQueue] Worker error:");
1505
+ }
1506
+ }, intervalMs);
1507
+ }
1508
+ stopWorker() {
1509
+ if (this.intervalId) {
1510
+ clearInterval(this.intervalId);
1511
+ this.intervalId = undefined;
1512
+ }
1513
+ }
1514
+ async processNextBatch() {
1515
+ let result;
1516
+ try {
1517
+ result = await this.adapter.find("cequre_jobs", {
1518
+ where: { status: "pending" },
1519
+ limit: 10
1520
+ });
1521
+ } catch (e) {
1522
+ return;
1523
+ }
1524
+ const jobs = result?.docs;
1525
+ if (!jobs || jobs.length === 0)
1526
+ return;
1527
+ for (const job of jobs) {
1528
+ try {
1529
+ if (job.task === "DELETE_FILE" && this.storage) {
1530
+ const payload = typeof job.payload === "string" ? JSON.parse(job.payload) : job.payload;
1531
+ if (payload && payload.filename) {
1532
+ await this.storage.deleteFile(payload.filename);
1533
+ }
1534
+ }
1535
+ await this.adapter.delete("cequre_jobs", job.id);
1536
+ } catch (e) {
1537
+ try {
1538
+ const retries = (job.retries || 0) + 1;
1539
+ const status = retries > 3 ? "failed" : "pending";
1540
+ await this.adapter.update("cequre_jobs", job.id, {
1541
+ retries,
1542
+ status,
1543
+ error: String(e instanceof Error ? e.message : e)
1544
+ });
1545
+ } catch (updateError) {
1546
+ logger.error(updateError, "[CequreQueue] Failed to update job status:");
1547
+ }
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ // shared/main/runtime.ts
1554
+ var log = createLogger("runtime");
1555
+ var requestContextStorage = new AsyncLocalStorage;
1556
+ class MemoryCronProvider {
1557
+ jobs = new Map;
1558
+ async registerJob(name, cronExpression, handler) {
1559
+ if (this.jobs.has(name))
1560
+ throw new Error(`Cron job ${name} already registered`);
1561
+ this.jobs.set(name, { interval: setInterval(handler, 60000) });
1562
+ }
1563
+ stop(name) {
1564
+ const job = this.jobs.get(name);
1565
+ if (!job)
1566
+ return false;
1567
+ clearInterval(job.interval);
1568
+ this.jobs.delete(name);
1569
+ return true;
1570
+ }
1571
+ async list() {
1572
+ return Array.from(this.jobs.keys());
1573
+ }
1574
+ stopAll() {
1575
+ for (const name of this.jobs.keys())
1576
+ this.stop(name);
1577
+ }
1578
+ startAll() {}
1579
+ }
1580
+
1581
+ class CequreRuntime {
1582
+ schema;
1583
+ adapter;
1584
+ router;
1585
+ rateLimiter;
1586
+ storage;
1587
+ cache;
1588
+ queue;
1589
+ plugins;
1590
+ mailer;
1591
+ streamStore;
1592
+ serverProvider;
1593
+ cronManager;
1594
+ realtimeProvider;
1595
+ monitoring;
1596
+ auditEnabled = false;
1597
+ auditKeyPromise;
1598
+ auditRetentionTimer;
1599
+ encryptionKeyPromise;
1600
+ authEngine;
1601
+ isTransaction = false;
1602
+ inFlightRequests = 0;
1603
+ shuttingDown = false;
1604
+ idempotencyKV;
1605
+ lockoutKV;
1606
+ hooksMap = new Map;
1607
+ _access = new Map;
1608
+ _cacheKeyRegistry = new Map;
1609
+ setAuthEngine(engine) {
1610
+ this.authEngine = engine;
1611
+ }
1612
+ enableAudit(keyPromise, retentionDays) {
1613
+ this.auditEnabled = true;
1614
+ this.auditKeyPromise = keyPromise;
1615
+ if (retentionDays && retentionDays > 0) {
1616
+ this.auditRetentionTimer = setInterval(() => this.purgeOldAuditEntries(retentionDays).catch((err) => {
1617
+ log.warn({ err }, "Audit retention cleanup failed");
1618
+ }), 24 * 60 * 60 * 1000);
1619
+ this.auditRetentionTimer.unref();
1620
+ }
1621
+ }
1622
+ enableEncryption(keyPromise) {
1623
+ this.encryptionKeyPromise = keyPromise;
1624
+ }
1625
+ setMonitoring(api) {
1626
+ this.monitoring = api;
1627
+ }
1628
+ constructor(schema, config) {
1629
+ this.schema = this.resolveEnvVariables(schema);
1630
+ this.adapter = config.adapter;
1631
+ const prefix = this.schema.core?.api?.prefix || "/api";
1632
+ this.storage = config.storageProvider || {
1633
+ async saveFile(file) {
1634
+ return { filename: file.name || "test.txt", originalName: file.name || "test.txt", mimetype: file.type || "text/plain", size: file.size || 0, url: "http://localhost/uploads/test.txt" };
1635
+ },
1636
+ async deleteFile() {}
1637
+ };
1638
+ this.queue = new CequreDurableQueue(this.adapter, this.storage);
1639
+ this.cache = config.cache || new MemoryCacheStore;
1640
+ this.streamStore = config.streamStore || new MemoryStreamStore;
1641
+ this.serverProvider = config.serverProvider;
1642
+ this.cronManager = config.cronProvider || new MemoryCronProvider;
1643
+ this.realtimeProvider = config.realtimeProvider;
1644
+ this.plugins = config.plugins || [];
1645
+ if (this.adapter.registerCollections) {
1646
+ this.adapter.registerCollections(this.schema.collections);
1647
+ }
1648
+ this.router = new CequreRouter(prefix);
1649
+ this.rateLimiter = new RateLimiter(this.schema, new CequreKV(config.kv ?? { driver: "memory" }), this.schema.core?.api?.trustedProxies);
1650
+ this.idempotencyKV = new CequreKV(config.kv ?? { driver: "memory" });
1651
+ this.lockoutKV = new CequreKV(config.kv ?? { driver: "memory" });
1652
+ if (this.schema.email || config.email) {
1653
+ this.mailer = createMailer({
1654
+ ...this.schema.email,
1655
+ ...config.email
1656
+ });
1657
+ }
1658
+ }
1659
+ get prefix() {
1660
+ const base = this.schema.core?.api?.prefix || "/api";
1661
+ const version = this.schema.core?.api?.version;
1662
+ return version ? `${base}/v${version}` : base;
1663
+ }
1664
+ get rawPrefix() {
1665
+ return this.schema.core?.api?.prefix || "/api";
1666
+ }
1667
+ get apiVersion() {
1668
+ return this.schema.core?.api?.version ?? null;
1669
+ }
1670
+ get isDeprecatedVersion() {
1671
+ const version = this.apiVersion;
1672
+ if (!version)
1673
+ return false;
1674
+ const deprecated = this.schema.core?.api?.deprecatedVersions;
1675
+ return Array.isArray(deprecated) && deprecated.includes(version);
1676
+ }
1677
+ access(collection, rules) {
1678
+ this._access.set(collection, rules);
1679
+ }
1680
+ hooks(collection, hooks) {
1681
+ this.hooksMap.set(collection, hooks);
1682
+ }
1683
+ async checkLockout(collection, email, lockoutConfig) {
1684
+ if (!lockoutConfig?.maxAttempts)
1685
+ return { locked: false };
1686
+ const key = `lockout:${collection}:${email.toLowerCase()}`;
1687
+ const entryStr = await this.lockoutKV.get(key);
1688
+ if (!entryStr)
1689
+ return { locked: false };
1690
+ const entry = JSON.parse(entryStr);
1691
+ if (entry.lockedUntil > 0 && Date.now() < entry.lockedUntil) {
1692
+ return { locked: true, remainingMs: entry.lockedUntil - Date.now() };
1693
+ }
1694
+ return { locked: false };
1695
+ }
1696
+ async recordFailedLogin(collection, email, lockoutConfig) {
1697
+ if (!lockoutConfig?.maxAttempts)
1698
+ return;
1699
+ const key = `lockout:${collection}:${email.toLowerCase()}`;
1700
+ const entryStr = await this.lockoutKV.get(key);
1701
+ const entry = entryStr ? JSON.parse(entryStr) : null;
1702
+ const count = entry ? entry.count + 1 : 1;
1703
+ const maxAttempts = lockoutConfig.maxAttempts;
1704
+ const durationMs = (lockoutConfig.durationMinutes ?? 15) * 60 * 1000;
1705
+ const ttlSeconds = Math.ceil(durationMs / 1000) * 2;
1706
+ if (count >= maxAttempts) {
1707
+ await this.lockoutKV.set(key, JSON.stringify({ count, lockedUntil: Date.now() + durationMs }), ttlSeconds);
1708
+ } else {
1709
+ await this.lockoutKV.set(key, JSON.stringify({ count, lockedUntil: 0 }), ttlSeconds);
1710
+ }
1711
+ }
1712
+ async clearFailedLogins(collection, email) {
1713
+ await this.lockoutKV.delete(`lockout:${collection}:${email.toLowerCase()}`);
1714
+ }
1715
+ resolveEnvVariables(obj) {
1716
+ if (!obj)
1717
+ return obj;
1718
+ if (typeof obj === "object") {
1719
+ if (Array.isArray(obj)) {
1720
+ return obj.map((item) => this.resolveEnvVariables(item));
1721
+ }
1722
+ if (obj.$env !== undefined) {
1723
+ const envValue = process.env[obj.$env];
1724
+ let finalValue = envValue !== undefined ? envValue : obj.$default;
1725
+ if (typeof obj.$default === "number" && typeof finalValue === "string") {
1726
+ const parsed = Number(finalValue);
1727
+ if (!isNaN(parsed))
1728
+ finalValue = parsed;
1729
+ } else if (typeof obj.$default === "boolean" && typeof finalValue === "string") {
1730
+ if (finalValue.toLowerCase() === "true")
1731
+ finalValue = true;
1732
+ else if (finalValue.toLowerCase() === "false")
1733
+ finalValue = false;
1734
+ } else if (typeof finalValue === "string") {
1735
+ if (finalValue.toLowerCase() === "true")
1736
+ finalValue = true;
1737
+ else if (finalValue.toLowerCase() === "false")
1738
+ finalValue = false;
1739
+ }
1740
+ return finalValue;
1741
+ }
1742
+ const newObj = {};
1743
+ for (const key in obj) {
1744
+ newObj[key] = this.resolveEnvVariables(obj[key]);
1745
+ }
1746
+ return newObj;
1747
+ }
1748
+ return obj;
1749
+ }
1750
+ async getContext(req) {
1751
+ let user = null;
1752
+ let token = null;
1753
+ const authHeader = req.headers.get("Authorization");
1754
+ if (authHeader?.startsWith("Bearer ")) {
1755
+ token = authHeader.split(" ")[1] ?? null;
1756
+ } else {
1757
+ const cookiesConfig = this.schema.security?.auth?.jwt?.cookies;
1758
+ if (cookiesConfig) {
1759
+ const cookieName = cookiesConfig.name || "cequre_auth";
1760
+ const cookieHeader = req.headers.get("cookie");
1761
+ if (cookieHeader) {
1762
+ const match = cookieHeader.match(new RegExp(`(^|;\\s*)${cookieName}=([^;]+)`));
1763
+ if (match)
1764
+ token = match[2] ?? null;
1765
+ }
1766
+ }
1767
+ }
1768
+ if (token && this.authEngine) {
1769
+ user = await this.authEngine.verifyAccessToken(token);
1770
+ }
1771
+ if (!user && this.schema.security?.auth?.apiKey?.enabled) {
1772
+ const apiKeyHeader = this.schema.security.auth.apiKey.header || "x-api-key";
1773
+ const apiKeyValue = req.headers.get(apiKeyHeader);
1774
+ if (apiKeyValue) {
1775
+ try {
1776
+ const hashedKey = await sha256Hex(apiKeyValue);
1777
+ const result = await this.secureFind("cequre_api_keys", {
1778
+ where: { keyHash: { eq: hashedKey } },
1779
+ limit: 1
1780
+ });
1781
+ if (result.docs.length > 0) {
1782
+ const keyDoc = result.docs[0];
1783
+ user = { id: keyDoc.userId, role: keyDoc.role ?? "api", collection: keyDoc.collection };
1784
+ }
1785
+ } catch {}
1786
+ }
1787
+ }
1788
+ return { request: req, user, cequre: this };
1789
+ }
1790
+ async checkAccess(collection, action, ctx) {
1791
+ if (process.env.TEST_DISABLE_ACCESS === "1")
1792
+ return;
1793
+ const tsRules = this._access.get(collection);
1794
+ const isAuthAction = ["register", "login", "refresh", "logout", "forgotPassword", "resetPassword", "mfaLogin", "mfaEnroll", "mfaVerify", "mfaDisable"].includes(action);
1795
+ if (tsRules && tsRules[action]) {
1796
+ const allowed = await tsRules[action](ctx);
1797
+ if (!allowed) {
1798
+ throw CequreError.Forbidden(`Unauthorized to ${action} on ${collection}`);
1799
+ }
1800
+ return typeof allowed === "object" ? allowed : undefined;
1801
+ }
1802
+ const schemaCollection = this.schema.collections.find((c) => c.slug === collection);
1803
+ if (schemaCollection && schemaCollection.access && schemaCollection.access[action]) {
1804
+ const expressions = schemaCollection.access[action];
1805
+ if (Array.isArray(expressions) && expressions.length > 0) {
1806
+ let anyAllowed = false;
1807
+ let resultingWhereClause;
1808
+ for (const exp of expressions) {
1809
+ const result = evaluateAccessExpression(exp, ctx, action);
1810
+ if (result) {
1811
+ anyAllowed = true;
1812
+ if (typeof result === "object") {
1813
+ resultingWhereClause = result;
1814
+ }
1815
+ break;
1816
+ }
1817
+ }
1818
+ if (!anyAllowed) {
1819
+ throw CequreError.Forbidden(`Unauthorized to ${action} on ${collection} (DSL Denied)`);
1820
+ }
1821
+ return resultingWhereClause;
1822
+ }
1823
+ }
1824
+ if (isAuthAction)
1825
+ return;
1826
+ throw CequreError.Forbidden(`Unauthorized to ${action} on ${collection} (Access denied by default)`);
1827
+ }
1828
+ async runBeforeHooks(collection, action, ctx, data) {
1829
+ const h = this.hooksMap.get(collection);
1830
+ const hookName = `before${action}`;
1831
+ if (h && h[hookName]) {
1832
+ const result = await h[hookName]({ ...ctx, collection, data });
1833
+ return result !== undefined ? result : data;
1834
+ }
1835
+ return data;
1836
+ }
1837
+ async runAfterHooks(collection, action, ctx, data) {
1838
+ const h = this.hooksMap.get(collection);
1839
+ const hookName = `after${action}`;
1840
+ if (h && h[hookName]) {
1841
+ const result = await h[hookName]({ ...ctx, collection, data });
1842
+ return result !== undefined ? result : data;
1843
+ }
1844
+ return data;
1845
+ }
1846
+ _registerCacheKey(collection, key) {
1847
+ let keys = this._cacheKeyRegistry.get(collection);
1848
+ if (!keys) {
1849
+ keys = new Set;
1850
+ this._cacheKeyRegistry.set(collection, keys);
1851
+ }
1852
+ keys.add(key);
1853
+ }
1854
+ async initPlugins() {
1855
+ for (const plugin of this.plugins) {
1856
+ if (plugin.onInit) {
1857
+ await plugin.onInit(this);
1858
+ }
1859
+ }
1860
+ }
1861
+ async buildRoutes() {
1862
+ await this.initPlugins();
1863
+ const prefix = this.schema.core?.api?.prefix || "/api";
1864
+ this.router.get(`${prefix}/sdk.ts`, async (ctx) => {
1865
+ if (false) {}
1866
+ const { generateTypeScriptClientSDK: generateTypeScriptClientSDK2 } = (init_sdk(), __toCommonJS(exports_sdk));
1867
+ const code = generateTypeScriptClientSDK2(this.schema, this.router.list());
1868
+ return new Response(code, {
1869
+ headers: { "Content-Type": "application/typescript" }
1870
+ });
1871
+ }, { detail: { hide: true } });
1872
+ const docsConfig = this.schema.core?.api?.docs;
1873
+ this.router.get(`${prefix}/docs/openapi.json`, async (ctx) => {
1874
+ const req = ctx.request;
1875
+ if (docsConfig?.enabled === false)
1876
+ return new Response("Not Found", { status: 404 });
1877
+ if (docsConfig?.authRequired) {
1878
+ const reqCtx = await this.getContext(req);
1879
+ if (!reqCtx.user)
1880
+ return new Response("Unauthorized", { status: 401 });
1881
+ }
1882
+ const url = new URL(req.url);
1883
+ const proto = req.headers.get("x-forwarded-proto") ?? url.protocol.replace(/:$/, "");
1884
+ const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? url.host;
1885
+ const serverUrl = `${proto}://${host}`;
1886
+ const mappedApp = {
1887
+ routes: this.router.list().map((route) => ({
1888
+ method: route.method,
1889
+ path: route.path,
1890
+ hooks: {
1891
+ body: route.options?.body,
1892
+ query: route.options?.query,
1893
+ response: route.options?.response,
1894
+ detail: route.options?.detail,
1895
+ stream: route.options?.stream
1896
+ }
1897
+ }))
1898
+ };
1899
+ const spec = generateOpenAPISpec(this.schema, serverUrl, prefix, mappedApp);
1900
+ return new Response(JSON.stringify(spec, null, 2), {
1901
+ headers: { "Content-Type": "application/json" }
1902
+ });
1903
+ });
1904
+ this.router.get(`${prefix}/docs`, async (ctx) => {
1905
+ const req = ctx.request;
1906
+ if (docsConfig?.enabled === false)
1907
+ return new Response("Not Found", { status: 404 });
1908
+ if (docsConfig?.authRequired) {
1909
+ const reqCtx = await this.getContext(req);
1910
+ if (!reqCtx.user)
1911
+ return new Response("Unauthorized", { status: 401 });
1912
+ }
1913
+ const specUrl = `${prefix}/docs/openapi.json`;
1914
+ const html = generateScalarHTML(specUrl);
1915
+ return new Response(html, {
1916
+ headers: { "Content-Type": "text/html" }
1917
+ });
1918
+ });
1919
+ const checkPlatformAdmin = async (ctx) => {
1920
+ const reqCtx = await this.getContext(ctx.request);
1921
+ if (!reqCtx.user?.isPlatformAdmin) {
1922
+ return new Response(JSON.stringify({ error: "Unauthorized. Platform Admin required." }), { status: 403, headers: { "Content-Type": "application/json" } });
1923
+ }
1924
+ return null;
1925
+ };
1926
+ this.router.get(`${prefix}/_collections`, async (ctx) => {
1927
+ const authErr = await checkPlatformAdmin(ctx);
1928
+ if (authErr)
1929
+ return authErr;
1930
+ const adminConfig = this.schema.adminUI || {};
1931
+ const collectionsList = (this.schema.collections || []).filter((col) => !col.hidden).map((col, index) => {
1932
+ return {
1933
+ id: index + 1,
1934
+ name: col.slug,
1935
+ type: "collection",
1936
+ items: 0
1937
+ };
1938
+ });
1939
+ return new Response(JSON.stringify({ collections: collectionsList, config: adminConfig }), {
1940
+ headers: { "Content-Type": "application/json" }
1941
+ });
1942
+ });
1943
+ this.router.get(`${prefix}/_collections/:collection/schema`, async (ctx) => {
1944
+ const authErr = await checkPlatformAdmin(ctx);
1945
+ if (authErr)
1946
+ return authErr;
1947
+ const params = ctx.params;
1948
+ const col = this.schema.collections?.find((c) => c.slug === params.collection);
1949
+ if (!col)
1950
+ return new Response(JSON.stringify({ error: "Collection not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
1951
+ return new Response(JSON.stringify({
1952
+ name: col.slug,
1953
+ fields: col.fields || [],
1954
+ auth: col.auth || false
1955
+ }), {
1956
+ headers: { "Content-Type": "application/json" }
1957
+ });
1958
+ });
1959
+ this.router.get(`${prefix}/audit/export`, async (ctx) => {
1960
+ const req = ctx.request;
1961
+ if (!this.auditEnabled)
1962
+ return new Response("Audit logging is not enabled", { status: 404 });
1963
+ const reqCtx = await this.getContext(req);
1964
+ if (!reqCtx.user)
1965
+ return new Response("Unauthorized", { status: 401 });
1966
+ try {
1967
+ const result = await this.secureFind("cequre_audit_log", { limit: 1e4, sort: "createdAt" });
1968
+ const entries = result.docs;
1969
+ let chainStatus = null;
1970
+ try {
1971
+ const key = await this.auditKeyPromise;
1972
+ chainStatus = await verifyAuditChain(key, entries);
1973
+ } catch {}
1974
+ const exportPayload = {
1975
+ exportedAt: new Date().toISOString(),
1976
+ totalEntries: entries.length,
1977
+ chainStatus,
1978
+ entries
1979
+ };
1980
+ return new Response(JSON.stringify(exportPayload, null, 2), {
1981
+ headers: { "Content-Type": "application/json" }
1982
+ });
1983
+ } catch (err) {
1984
+ log.warn({ err }, "Failed to export audit log");
1985
+ return new Response(JSON.stringify({ error: "Failed to export audit log" }), {
1986
+ status: 500,
1987
+ headers: { "Content-Type": "application/json" }
1988
+ });
1989
+ }
1990
+ });
1991
+ const isRealtimeGlobal = this.schema.collections.some((c) => c.realtime?.ws || c.realtime?.sse || c.realtime?.durableStream);
1992
+ if (isRealtimeGlobal) {
1993
+ const checkSecureChannel = async (channel, request, user) => {
1994
+ const collection = this.schema.collections.find((c) => c.slug === channel);
1995
+ if (!collection || !collection.realtime?.secure)
1996
+ return true;
1997
+ if (!user)
1998
+ return false;
1999
+ try {
2000
+ const ctx = {
2001
+ request,
2002
+ user,
2003
+ cequre: this
2004
+ };
2005
+ await this.checkAccess(collection.slug, "read", ctx);
2006
+ return true;
2007
+ } catch {
2008
+ return false;
2009
+ }
2010
+ };
2011
+ const onSubscribe = async (info) => {
2012
+ return checkSecureChannel(info.channel, info.request, info.user);
2013
+ };
2014
+ const onJoinRoom = async (room, ws) => {
2015
+ const req = ws.data.request;
2016
+ const user = ws.data.user;
2017
+ if (!req) {
2018
+ const collection = this.schema.collections.find((c) => c.slug === room);
2019
+ if (collection?.realtime?.secure)
2020
+ return false;
2021
+ return true;
2022
+ }
2023
+ return checkSecureChannel(room, req, user);
2024
+ };
2025
+ this.router.get(`${prefix}/ws`, async (ctx) => this.realtimeProvider?.websocket?.route?.({ path: `${prefix}/ws`, onJoinRoom })(ctx.request, undefined));
2026
+ this.router.get(`${prefix}/sse`, async (ctx) => {
2027
+ const resolveUser = async (request) => {
2028
+ const reqCtx = await this.getContext(request);
2029
+ return reqCtx.user;
2030
+ };
2031
+ return CequreSSE.route({ path: `${prefix}/sse`, onSubscribe, resolveUser })(ctx.request);
2032
+ }, { detail: { hide: true } });
2033
+ }
2034
+ this.router.post(`${prefix}/uploads`, async (ctx) => {
2035
+ const req = ctx.request;
2036
+ const formData = await req.formData().catch(() => null);
2037
+ if (!formData)
2038
+ return new Response(JSON.stringify({ error: "Failed to parse form data" }), { status: 400 });
2039
+ const file = formData.get("file");
2040
+ if (!(file instanceof File))
2041
+ return new Response(JSON.stringify({ error: "A 'file' field is required" }), { status: 400 });
2042
+ const filename = formData.get("filename")?.toString();
2043
+ const uploadedFile = await this.storage.saveFile(file, filename ? { filename } : undefined);
2044
+ return new Response(JSON.stringify(uploadedFile), { status: 201, headers: { "Content-Type": "application/json" } });
2045
+ }, {
2046
+ detail: { tags: ["Uploads"], summary: "Upload a file" }
2047
+ });
2048
+ this.router.post(`${prefix}/uploads/multipart/init`, async (ctx) => {
2049
+ const req = ctx.request;
2050
+ const data = await req.json().catch(() => null);
2051
+ if (!data || !data.filename || !data.mimetype || !data.size) {
2052
+ return new Response(JSON.stringify({ error: "filename, mimetype, and size are required" }), { status: 400 });
2053
+ }
2054
+ if (this.storage.initMultipartUpload) {
2055
+ try {
2056
+ const result = await this.storage.initMultipartUpload(data);
2057
+ return new Response(JSON.stringify(result), { status: 200, headers: { "Content-Type": "application/json" } });
2058
+ } catch (err) {
2059
+ return new Response(JSON.stringify({ error: err.message }), { status: 500 });
2060
+ }
2061
+ } else {
2062
+ const uploadId = ulid();
2063
+ const tmpDir = path.join(__require("os").tmpdir(), "cequre-uploads", uploadId);
2064
+ await __require("fs/promises").mkdir(tmpDir, { recursive: true }).catch(() => {});
2065
+ return new Response(JSON.stringify({ uploadId, parts: null }), { status: 200, headers: { "Content-Type": "application/json" } });
2066
+ }
2067
+ }, { detail: { tags: ["Uploads"], summary: "Initialize multipart upload" } });
2068
+ this.router.post(`${prefix}/uploads/multipart/chunk`, async (ctx) => {
2069
+ const req = ctx.request;
2070
+ const url = new URL(req.url);
2071
+ const uploadId = url.searchParams.get("uploadId");
2072
+ const partNumber = url.searchParams.get("partNumber");
2073
+ if (!uploadId || !partNumber) {
2074
+ return new Response(JSON.stringify({ error: "uploadId and partNumber query params are required" }), { status: 400 });
2075
+ }
2076
+ const tmpFile = path.join(__require("os").tmpdir(), "cequre-uploads", uploadId, `part-${partNumber}`);
2077
+ await __require("fs/promises").writeFile(tmpFile, Buffer.from(await req.arrayBuffer()));
2078
+ return new Response(JSON.stringify({ etag: `local-${partNumber}` }), { status: 200, headers: { "Content-Type": "application/json" } });
2079
+ }, { detail: { tags: ["Uploads"], summary: "Upload a file chunk (fallback)" } });
2080
+ this.router.post(`${prefix}/uploads/multipart/complete`, async (ctx) => {
2081
+ const req = ctx.request;
2082
+ const data = await req.json().catch(() => null);
2083
+ if (!data || !data.uploadId || !data.filename || !data.parts) {
2084
+ return new Response(JSON.stringify({ error: "uploadId, filename, and parts are required" }), { status: 400 });
2085
+ }
2086
+ if (this.storage.completeMultipartUpload) {
2087
+ try {
2088
+ const result = await this.storage.completeMultipartUpload(data.uploadId, data.filename, data.parts);
2089
+ return new Response(JSON.stringify(result), { status: 200, headers: { "Content-Type": "application/json" } });
2090
+ } catch (err) {
2091
+ return new Response(JSON.stringify({ error: err.message }), { status: 500 });
2092
+ }
2093
+ } else {
2094
+ const tmpDir = path.join(__require("os").tmpdir(), "cequre-uploads", data.uploadId);
2095
+ const stitchedPath = path.join(__require("os").tmpdir(), "cequre-uploads", `${data.uploadId}-${data.filename}`);
2096
+ try {
2097
+ const sortedParts = data.parts.sort((a, b) => a.partNumber - b.partNumber);
2098
+ const writer = __require("fs").createWriteStream(stitchedPath);
2099
+ for (const part of sortedParts) {
2100
+ const partPath = path.join(tmpDir, `part-${part.partNumber}`);
2101
+ const partBuffer = await __require("fs/promises").readFile(partPath);
2102
+ writer.write(partBuffer);
2103
+ }
2104
+ await new Promise((resolve) => writer.end(resolve));
2105
+ const stitchedBuffer = await __require("fs/promises").readFile(stitchedPath);
2106
+ const finalFile = new File([stitchedBuffer], data.filename, { type: "application/octet-stream" });
2107
+ const uploadedFile = await this.storage.saveFile(finalFile, { filename: data.filename });
2108
+ const fs = __require("fs/promises");
2109
+ fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
2110
+ fs.rm(stitchedPath, { force: true }).catch(() => {});
2111
+ return new Response(JSON.stringify(uploadedFile), { status: 200, headers: { "Content-Type": "application/json" } });
2112
+ } catch (err) {
2113
+ return new Response(JSON.stringify({ error: `Stitching failed: ${err.message}` }), { status: 500 });
2114
+ }
2115
+ }
2116
+ }, { detail: { tags: ["Uploads"], summary: "Complete multipart upload" } });
2117
+ this.router.get(`${prefix}/uploads/:filename`, async (ctx) => {
2118
+ const req = ctx.request;
2119
+ const params = ctx.params;
2120
+ const safeName = path.basename(params.filename);
2121
+ if (safeName !== params.filename)
2122
+ return new Response("Forbidden", { status: 403 });
2123
+ if (this.storage.getFileUrl) {
2124
+ try {
2125
+ const url = await this.storage.getFileUrl(safeName);
2126
+ if (url) {
2127
+ return new Response(null, { status: 302, headers: { Location: url } });
2128
+ }
2129
+ } catch (err) {
2130
+ return new Response("Not found in external storage", { status: 404 });
2131
+ }
2132
+ }
2133
+ const uploadDir = path.resolve(process.cwd(), "uploads");
2134
+ const filePath = path.join(uploadDir, safeName);
2135
+ if (!filePath.startsWith(uploadDir + path.sep))
2136
+ return new Response("Forbidden", { status: 403 });
2137
+ try {
2138
+ const fileBuffer = await __require("fs/promises").readFile(filePath);
2139
+ return new Response(fileBuffer, {
2140
+ headers: {
2141
+ "Content-Type": "application/octet-stream"
2142
+ }
2143
+ });
2144
+ } catch (err) {
2145
+ if (err.code === "ENOENT") {
2146
+ return new Response("Not found", { status: 404 });
2147
+ }
2148
+ throw err;
2149
+ }
2150
+ }, {
2151
+ detail: { tags: ["Uploads"], summary: "Serve an uploaded file" }
2152
+ });
2153
+ for (const collection of this.schema.collections) {
2154
+ const basePath = `${prefix}/${collection.slug}`;
2155
+ const hasCache = !!collection.cache;
2156
+ const cacheTTL = collection.cache?.ttl || 3600;
2157
+ const isRealtime = !!collection.realtime;
2158
+ this.router.get(basePath, async (ctx) => {
2159
+ const req = ctx.request;
2160
+ const accessWhere = await this.checkAccess(collection.slug, "read", ctx);
2161
+ if (hasCache) {
2162
+ const cacheKey = `list:${collection.slug}:${new URL(req.url).search}`;
2163
+ const cached = await this.cache.get(cacheKey);
2164
+ if (cached)
2165
+ return new Response(JSON.stringify(cached), { headers: { "Content-Type": "application/json", "X-Cache": "HIT" } });
2166
+ }
2167
+ let queryArgs = {};
2168
+ if (ctx.query.q) {
2169
+ try {
2170
+ queryArgs = typeof ctx.query.q === "string" ? JSON.parse(ctx.query.q) : ctx.query.q;
2171
+ } catch (e) {}
2172
+ }
2173
+ queryArgs = await this.runBeforeHooks(collection.slug, "Read", ctx, queryArgs);
2174
+ let finalWhere = queryArgs.where || {};
2175
+ if (accessWhere) {
2176
+ if (Object.keys(finalWhere).length > 0) {
2177
+ finalWhere = { AND: [accessWhere, finalWhere] };
2178
+ } else {
2179
+ finalWhere = accessWhere;
2180
+ }
2181
+ }
2182
+ queryArgs.where = Object.keys(finalWhere).length > 0 ? finalWhere : undefined;
2183
+ const result = await this.secureFind(collection.slug, queryArgs);
2184
+ const userRole = ctx.user?.role ?? null;
2185
+ if (this.getRestrictedFields(collection.slug, "readRoles").size > 0) {
2186
+ result.docs = result.docs.map((doc) => this.filterReadableFields(collection.slug, doc, userRole));
2187
+ }
2188
+ if (hasCache) {
2189
+ const cacheKey = `list:${collection.slug}:${new URL(req.url).search}`;
2190
+ await this.cache.set(cacheKey, result, Number(cacheTTL));
2191
+ this._registerCacheKey(collection.slug, cacheKey);
2192
+ }
2193
+ const hookedResult = await this.runAfterHooks(collection.slug, "Read", ctx, result);
2194
+ return new Response(JSON.stringify(hookedResult), { headers: { "Content-Type": "application/json", "X-Cache": "MISS" } });
2195
+ });
2196
+ if (collection.realtime?.durableStream) {
2197
+ this.router.get(`${basePath}/stream`, async (ctx) => {
2198
+ await this.checkAccess(collection.slug, "read", ctx);
2199
+ const url = new URL(ctx.request.url);
2200
+ const lastEventId = url.searchParams.get("lastEventId") || undefined;
2201
+ const events = await this.streamStore.read(collection.slug, lastEventId);
2202
+ return new Response(JSON.stringify(events), { headers: { "Content-Type": "application/json" } });
2203
+ });
2204
+ }
2205
+ this.router.get(`${basePath}/:id`, async (ctx) => {
2206
+ const req = ctx.request;
2207
+ const params = ctx.params;
2208
+ const accessWhere = await this.checkAccess(collection.slug, "read", { ...ctx, id: params.id });
2209
+ if (hasCache) {
2210
+ const cacheKey = `doc:${collection.slug}:${params.id}${new URL(req.url).search}`;
2211
+ const cached = await this.cache.get(cacheKey);
2212
+ if (cached)
2213
+ return new Response(JSON.stringify(cached), { headers: { "Content-Type": "application/json", "X-Cache": "HIT" } });
2214
+ }
2215
+ let queryArgs = {};
2216
+ if (ctx.query.q) {
2217
+ try {
2218
+ queryArgs = typeof ctx.query.q === "string" ? JSON.parse(ctx.query.q) : ctx.query.q;
2219
+ } catch (e) {}
2220
+ }
2221
+ queryArgs = await this.runBeforeHooks(collection.slug, "Read", ctx, queryArgs);
2222
+ const result = await this.secureFindById(collection.slug, params.id, queryArgs);
2223
+ if (!result)
2224
+ return new Response("Not found", { status: 404 });
2225
+ if (accessWhere) {
2226
+ const verification = await this.secureFind(collection.slug, { where: { ...accessWhere, id: { eq: params.id } }, limit: 1 });
2227
+ if (!verification.docs.length)
2228
+ return new Response("Not found", { status: 404 });
2229
+ }
2230
+ const userRole = ctx.user?.role ?? null;
2231
+ const filteredResult = this.filterReadableFields(collection.slug, result, userRole);
2232
+ if (hasCache) {
2233
+ const cacheKey = `doc:${collection.slug}:${params.id}${new URL(req.url).search}`;
2234
+ await this.cache.set(cacheKey, filteredResult, Number(cacheTTL));
2235
+ this._registerCacheKey(collection.slug, cacheKey);
2236
+ }
2237
+ const hookedResult = await this.runAfterHooks(collection.slug, "Read", ctx, filteredResult);
2238
+ return new Response(JSON.stringify(hookedResult), { headers: { "Content-Type": "application/json", "X-Cache": "MISS" } });
2239
+ });
2240
+ const invalidateCache = async (specificDocId) => {
2241
+ if (specificDocId) {
2242
+ await this.cache.del(`doc:${collection.slug}:${specificDocId}`);
2243
+ }
2244
+ const keys = this._cacheKeyRegistry.get(collection.slug);
2245
+ if (keys && keys.size > 0) {
2246
+ await this.cache.del(...keys);
2247
+ keys.clear();
2248
+ }
2249
+ };
2250
+ const broadcastChange = (action, data) => {
2251
+ if (!isRealtime)
2252
+ return;
2253
+ const r = collection.realtime;
2254
+ const payload = { event: `${collection.slug}:${action}`, data };
2255
+ const isAllowed = (setting) => setting === true || Array.isArray(setting) && setting.includes(action);
2256
+ if (isAllowed(r?.ws))
2257
+ this.realtimeProvider?.websocket?.broadcastToRoom?.(collection.slug, payload);
2258
+ if (isAllowed(r?.sse))
2259
+ CequreSSE.broadcastToChannel(collection.slug, payload);
2260
+ if (isAllowed(r?.durableStream) && this.streamStore) {
2261
+ this.streamStore.publish(collection.slug, action, data).catch((e) => log.error({ err: e }, "Stream publish failed"));
2262
+ }
2263
+ };
2264
+ this.router.post(basePath, async (ctx) => {
2265
+ const req = ctx.request;
2266
+ let data = await req.json();
2267
+ await this.checkAccess(collection.slug, "create", { ...ctx, data });
2268
+ data = this.stripForbiddenFields(collection.slug, data);
2269
+ const userRole = ctx.user?.role ?? null;
2270
+ data = this.filterWritableFields(collection.slug, data, userRole);
2271
+ data = validateCreate(collection, data);
2272
+ data = await this.runBeforeHooks(collection.slug, "Create", ctx, data);
2273
+ const result = await this.secureCreate(collection.slug, data);
2274
+ await this.runAfterHooks(collection.slug, "Create", ctx, result);
2275
+ if (hasCache)
2276
+ await invalidateCache();
2277
+ if (isRealtime)
2278
+ broadcastChange("created", result);
2279
+ const responseResult = this.filterReadableFields(collection.slug, result, userRole);
2280
+ return new Response(JSON.stringify(responseResult), { status: 201, headers: { "Content-Type": "application/json" } });
2281
+ });
2282
+ this.router.patch(`${basePath}/:id`, async (ctx) => {
2283
+ const req = ctx.request;
2284
+ const params = ctx.params;
2285
+ let data = await req.json();
2286
+ const accessWhere = await this.checkAccess(collection.slug, "update", { ...ctx, id: params.id, data });
2287
+ if (accessWhere) {
2288
+ const verification = await this.secureFind(collection.slug, { where: { ...accessWhere, id: { eq: params.id } }, limit: 1 });
2289
+ if (!verification.docs.length)
2290
+ return new Response("Not found", { status: 404 });
2291
+ }
2292
+ data = this.stripForbiddenFields(collection.slug, data);
2293
+ const userRole = ctx.user?.role ?? null;
2294
+ data = this.filterWritableFields(collection.slug, data, userRole);
2295
+ data = validateUpdate(collection, data);
2296
+ data = await this.runBeforeHooks(collection.slug, "Update", ctx, data);
2297
+ const result = await this.secureUpdate(collection.slug, params.id, data);
2298
+ await this.runAfterHooks(collection.slug, "Update", ctx, result);
2299
+ if (hasCache)
2300
+ await invalidateCache(params.id);
2301
+ if (isRealtime)
2302
+ broadcastChange("updated", result);
2303
+ const responseResult = this.filterReadableFields(collection.slug, result, userRole);
2304
+ return new Response(JSON.stringify(responseResult), { headers: { "Content-Type": "application/json" } });
2305
+ });
2306
+ this.router.delete(`${basePath}/:id`, async (ctx) => {
2307
+ const req = ctx.request;
2308
+ const params = ctx.params;
2309
+ const accessWhere = await this.checkAccess(collection.slug, "delete", { ...ctx, id: params.id });
2310
+ let record = null;
2311
+ if (accessWhere) {
2312
+ const verification = await this.secureFind(collection.slug, { where: { ...accessWhere, id: { eq: params.id } }, limit: 1 });
2313
+ if (!verification.docs.length)
2314
+ return new Response("Not found", { status: 404 });
2315
+ record = verification.docs[0];
2316
+ } else {
2317
+ const verification = await this.secureFind(collection.slug, { where: { id: { eq: params.id } }, limit: 1 });
2318
+ if (!verification.docs.length)
2319
+ return new Response("Not found", { status: 404 });
2320
+ record = verification.docs[0];
2321
+ }
2322
+ await this.runBeforeHooks(collection.slug, "Delete", ctx, { id: params.id });
2323
+ await this.adapter.delete(collection.slug, params.id);
2324
+ if (collection.fields && record) {
2325
+ for (const field of collection.fields) {
2326
+ if (field.type === "upload" || field.type === "media" || field.type === "file") {
2327
+ const fileData = record[field.name];
2328
+ if (fileData) {
2329
+ const files = Array.isArray(fileData) ? fileData : [fileData];
2330
+ for (const f of files) {
2331
+ let filename = null;
2332
+ if (typeof f === "string") {
2333
+ try {
2334
+ const parsed = JSON.parse(f);
2335
+ filename = parsed.filename || f;
2336
+ } catch {
2337
+ filename = f;
2338
+ }
2339
+ } else if (f && typeof f === "object" && f.filename) {
2340
+ filename = f.filename;
2341
+ }
2342
+ if (filename) {
2343
+ this.queue.enqueue("DELETE_FILE", { filename }).catch((e) => log.error("Queue enqueue error", e));
2344
+ }
2345
+ }
2346
+ }
2347
+ }
2348
+ }
2349
+ }
2350
+ await this.runAfterHooks(collection.slug, "Delete", ctx, { id: params.id });
2351
+ if (hasCache)
2352
+ await invalidateCache(params.id);
2353
+ if (isRealtime)
2354
+ broadcastChange("deleted", { id: params.id });
2355
+ return new Response(JSON.stringify({ deleted: true }), { headers: { "Content-Type": "application/json" } });
2356
+ });
2357
+ }
2358
+ if (this.schema.globals) {
2359
+ for (const glob of this.schema.globals) {
2360
+ const basePath = `${prefix}/globals/${glob.slug}`;
2361
+ this.router.get(basePath, async (ctx) => {
2362
+ try {
2363
+ await this.checkAccess(glob.slug, "read", ctx);
2364
+ const data = await this.secureFindById("cequre_globals", glob.slug);
2365
+ return new Response(JSON.stringify(data || {}), { status: 200, headers: { "Content-Type": "application/json" } });
2366
+ } catch (e) {
2367
+ return toErrorResponse(e, ctx.request, this.adapter);
2368
+ }
2369
+ });
2370
+ const updateHandler = async (ctx) => {
2371
+ try {
2372
+ await this.checkAccess(glob.slug, "update", ctx);
2373
+ let body = await ctx.request.json();
2374
+ body = await this.runBeforeHooks(glob.slug, "Update", ctx, body);
2375
+ const existing = await this.secureFindById("cequre_globals", glob.slug);
2376
+ let result;
2377
+ if (existing) {
2378
+ result = await this.secureUpdate("cequre_globals", glob.slug, body);
2379
+ } else {
2380
+ result = await this.secureCreate("cequre_globals", { id: glob.slug, ...body });
2381
+ }
2382
+ await this.runAfterHooks(glob.slug, "Update", ctx, result);
2383
+ return new Response(JSON.stringify(result), { status: 200, headers: { "Content-Type": "application/json" } });
2384
+ } catch (e) {
2385
+ return toErrorResponse(e, ctx.request, this.adapter);
2386
+ }
2387
+ };
2388
+ this.router.post(basePath, updateHandler);
2389
+ this.router.patch(basePath, updateHandler);
2390
+ }
2391
+ }
2392
+ }
2393
+ auditLock = Promise.resolve();
2394
+ async purgeOldAuditEntries(retentionDays) {
2395
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
2396
+ try {
2397
+ const oldEntries = await this.secureFind("cequre_audit_log", {
2398
+ where: { createdAt: { lt: cutoff } },
2399
+ limit: 1000
2400
+ });
2401
+ let deleted = 0;
2402
+ for (const doc of oldEntries.docs) {
2403
+ try {
2404
+ await this.adapter.delete("cequre_audit_log", doc.id);
2405
+ deleted++;
2406
+ } catch {}
2407
+ }
2408
+ if (deleted > 0) {
2409
+ log.info({ deleted, retentionDays }, "Purged old audit entries");
2410
+ }
2411
+ return deleted;
2412
+ } catch (err) {
2413
+ log.warn({ err }, "Failed to purge old audit entries \u2014 table may not exist yet");
2414
+ return 0;
2415
+ }
2416
+ }
2417
+ async audit(event) {
2418
+ if (!this.auditEnabled)
2419
+ return;
2420
+ this.auditLock = this.auditLock.then(() => this._doAudit(event)).catch(() => this._doAudit(event));
2421
+ return this.auditLock;
2422
+ }
2423
+ async _doAudit(event) {
2424
+ try {
2425
+ const key = await this.auditKeyPromise;
2426
+ const recent = await this.secureFind("cequre_audit_log", { limit: 1, sort: "-createdAt" });
2427
+ const prevHmac = recent.docs.length > 0 ? recent.docs[0].hmac ?? null : null;
2428
+ const hmac = await computeAuditHmac(key, event, prevHmac);
2429
+ const now = new Date().toISOString();
2430
+ await this.secureCreate("cequre_audit_log", {
2431
+ id: ulid(),
2432
+ action: event.action,
2433
+ collection: event.collection,
2434
+ recordId: event.recordId,
2435
+ userId: event.userId,
2436
+ requestId: event.requestId,
2437
+ metadata: event.metadata ?? {},
2438
+ hmac,
2439
+ prevHmac,
2440
+ createdAt: now,
2441
+ updatedAt: now
2442
+ });
2443
+ } catch (err) {
2444
+ log.error({ err }, "Audit persistence failed");
2445
+ }
2446
+ }
2447
+ getRestrictedFields(collection, roleType) {
2448
+ const col = this.schema.collections.find((c) => c.slug === collection);
2449
+ if (!col?.fields)
2450
+ return new Map;
2451
+ const result = new Map;
2452
+ for (const f of col.fields) {
2453
+ const roles = f[roleType];
2454
+ if (roles && roles.length > 0) {
2455
+ result.set(f.name, roles);
2456
+ }
2457
+ }
2458
+ return result;
2459
+ }
2460
+ filterFields(collection, data, roleType, userRole) {
2461
+ if (!data || typeof data !== "object")
2462
+ return data;
2463
+ const restricted = this.getRestrictedFields(collection, roleType);
2464
+ if (restricted.size === 0)
2465
+ return data;
2466
+ const result = { ...data };
2467
+ for (const [field, allowedRoles] of restricted) {
2468
+ if (!allowedRoles.includes(userRole ?? "")) {
2469
+ delete result[field];
2470
+ }
2471
+ }
2472
+ return result;
2473
+ }
2474
+ filterReadableFields(collection, doc, userRole) {
2475
+ return this.filterFields(collection, doc, "readRoles", userRole);
2476
+ }
2477
+ filterWritableFields(collection, data, userRole) {
2478
+ return this.filterFields(collection, data, "writeRoles", userRole);
2479
+ }
2480
+ stripForbiddenFields(collection, data) {
2481
+ if (!data || typeof data !== "object")
2482
+ return data;
2483
+ const col = this.schema.collections.find((c) => c.slug === collection);
2484
+ if (!col?.fields)
2485
+ return data;
2486
+ const result = { ...data };
2487
+ for (const field of col.fields) {
2488
+ if (field.type === "password" || field.omit || field.type === "link") {
2489
+ delete result[field.name];
2490
+ }
2491
+ }
2492
+ return result;
2493
+ }
2494
+ getEncryptedFields(collection, globalSlug) {
2495
+ if (!this.encryptionKeyPromise)
2496
+ return new Set;
2497
+ let fieldsArray = undefined;
2498
+ if (collection === "cequre_globals" && globalSlug) {
2499
+ const glob = this.schema.globals?.find((g) => g.slug === globalSlug);
2500
+ fieldsArray = glob?.fields;
2501
+ } else {
2502
+ const col = this.schema.collections.find((c) => c.slug === collection);
2503
+ fieldsArray = col?.fields;
2504
+ }
2505
+ if (!fieldsArray)
2506
+ return new Set;
2507
+ const fields = new Set;
2508
+ for (const f of fieldsArray) {
2509
+ if (f.encrypt)
2510
+ fields.add(f.name);
2511
+ }
2512
+ return fields;
2513
+ }
2514
+ async encryptDataFields(collection, data, isCreate, globalSlug) {
2515
+ let fieldsArray = undefined;
2516
+ if (collection === "cequre_globals" && (globalSlug || data.id)) {
2517
+ const glob = this.schema.globals?.find((g) => g.slug === (globalSlug || data.id));
2518
+ fieldsArray = glob?.fields;
2519
+ } else {
2520
+ const col = this.schema.collections.find((c) => c.slug === collection);
2521
+ fieldsArray = col?.fields;
2522
+ }
2523
+ if (!fieldsArray)
2524
+ return data;
2525
+ const key = await this.encryptionKeyPromise;
2526
+ if (!key)
2527
+ return data;
2528
+ const result = { ...data };
2529
+ for (const f of fieldsArray) {
2530
+ if (f.encrypt) {
2531
+ if (isCreate && result[f.name] === undefined && f.default !== undefined) {
2532
+ result[f.name] = f.default;
2533
+ }
2534
+ const v = result[f.name];
2535
+ if (v !== undefined && typeof v === "string" && !isEncrypted(v)) {
2536
+ result[f.name] = await encryptField(key, v);
2537
+ }
2538
+ }
2539
+ }
2540
+ return result;
2541
+ }
2542
+ async decryptDataFields(collection, docs) {
2543
+ const key = await this.encryptionKeyPromise;
2544
+ if (!key)
2545
+ return docs;
2546
+ const decryptDoc = async (doc) => {
2547
+ if (!doc || typeof doc !== "object")
2548
+ return doc;
2549
+ const encFields = this.getEncryptedFields(collection, doc.id);
2550
+ if (encFields.size === 0)
2551
+ return doc;
2552
+ const result = { ...doc };
2553
+ for (const [k, v] of Object.entries(result)) {
2554
+ if (encFields.has(k) && typeof v === "string" && isEncrypted(v)) {
2555
+ const decrypted = await decryptField(key, v);
2556
+ if (decrypted !== null)
2557
+ result[k] = decrypted;
2558
+ }
2559
+ }
2560
+ return result;
2561
+ };
2562
+ if (Array.isArray(docs)) {
2563
+ return Promise.all(docs.map(decryptDoc));
2564
+ }
2565
+ return decryptDoc(docs);
2566
+ }
2567
+ async secureCreate(collection, data) {
2568
+ const encData = await this.encryptDataFields(collection, data, true);
2569
+ const result = await this.adapter.create(collection, encData);
2570
+ if (this.encryptionKeyPromise) {
2571
+ return await this.decryptDataFields(collection, result);
2572
+ }
2573
+ return result;
2574
+ }
2575
+ async secureUpdate(collection, id, data) {
2576
+ const encData = await this.encryptDataFields(collection, data, false, id);
2577
+ const result = await this.adapter.update(collection, id, encData);
2578
+ if (this.encryptionKeyPromise) {
2579
+ return await this.decryptDataFields(collection, result);
2580
+ }
2581
+ return result;
2582
+ }
2583
+ async secureFind(collection, query) {
2584
+ const depth = query?.depth ? parseInt(query.depth, 10) : 0;
2585
+ const safeQuery = { ...query };
2586
+ delete safeQuery.depth;
2587
+ const result = await this.adapter.find(collection, safeQuery);
2588
+ if (this.encryptionKeyPromise && result?.docs) {
2589
+ result.docs = await this.decryptDataFields(collection, result.docs);
2590
+ }
2591
+ if (depth > 0 && result?.docs?.length > 0) {
2592
+ result.docs = await populateDocuments(this.schema, collection, result.docs, this.adapter, depth);
2593
+ }
2594
+ return result;
2595
+ }
2596
+ async secureFindById(collection, id, query) {
2597
+ const depth = query?.depth ? parseInt(query.depth, 10) : 0;
2598
+ let doc = await this.adapter.findById(collection, id);
2599
+ if (this.encryptionKeyPromise && doc) {
2600
+ doc = await this.decryptDataFields(collection, doc);
2601
+ }
2602
+ if (depth > 0 && doc) {
2603
+ const populated = await populateDocuments(this.schema, collection, [doc], this.adapter, depth);
2604
+ if (populated.length > 0)
2605
+ doc = populated[0];
2606
+ }
2607
+ return doc;
2608
+ }
2609
+ async transaction(callback) {
2610
+ if (this.isTransaction) {
2611
+ throw new CequreError("Please refactor your code. Nested transactions not supported.", "NESTED_TRANSACTION", 500);
2612
+ }
2613
+ if (!this.adapter.transaction) {
2614
+ throw new CequreError(`The configured database adapter (${this.adapter.adapterType || "unknown"}) does not support transactions.`, "NOT_SUPPORTED", 500);
2615
+ }
2616
+ return this.adapter.transaction(async (trxAdapter) => {
2617
+ const trxRuntime = Object.create(this);
2618
+ trxRuntime.adapter = trxAdapter;
2619
+ trxRuntime.isTransaction = true;
2620
+ return callback(trxRuntime);
2621
+ });
2622
+ }
2623
+ async find(collection, query) {
2624
+ return this.secureFind(collection, query);
2625
+ }
2626
+ async findById(collection, id, query) {
2627
+ return this.secureFindById(collection, id, query);
2628
+ }
2629
+ async create(collection, data) {
2630
+ const result = await this.secureCreate(collection, data);
2631
+ if (collection !== "cequre_audit_log" && collection !== "cequre_refresh_tokens") {
2632
+ const requestId = requestContextStorage.getStore();
2633
+ this.audit({ action: "create", collection, recordId: result.id, requestId, metadata: { data } }).catch((e) => log.error({ err: e }, "Audit write failed"));
2634
+ }
2635
+ return result;
2636
+ }
2637
+ async update(collection, id, data) {
2638
+ const result = await this.secureUpdate(collection, id, data);
2639
+ if (collection !== "cequre_audit_log" && collection !== "cequre_refresh_tokens") {
2640
+ const requestId = requestContextStorage.getStore();
2641
+ this.audit({ action: "update", collection, recordId: id, requestId, metadata: { data } }).catch((e) => log.error({ err: e }, "Audit write failed"));
2642
+ }
2643
+ return result;
2644
+ }
2645
+ async delete(collection, id) {
2646
+ await this.adapter.delete(collection, id);
2647
+ if (collection !== "cequre_audit_log" && collection !== "cequre_refresh_tokens") {
2648
+ const requestId = requestContextStorage.getStore();
2649
+ this.audit({ action: "delete", collection, recordId: id, requestId }).catch((e) => log.error({ err: e }, "Audit write failed"));
2650
+ }
2651
+ }
2652
+ async eraseUser(collection, userId) {
2653
+ const colName = String(collection);
2654
+ const conf = this.schema.collections.find((c) => c.slug === colName);
2655
+ if (!conf || !conf.auth) {
2656
+ throw new CequreError(`Collection ${colName} is not an auth-enabled collection.`, "BAD_REQUEST", 400);
2657
+ }
2658
+ this.audit({
2659
+ action: "eraseUser",
2660
+ collection: colName,
2661
+ recordId: userId,
2662
+ requestId: "internal_erasure",
2663
+ userId: "system"
2664
+ }).catch((err) => log.error({ err }, "Failed to log eraseUser audit event"));
2665
+ await this.adapter.delete(colName, userId);
2666
+ try {
2667
+ const result = await this.adapter.find("cequre_refresh_tokens", {
2668
+ where: { userId: { eq: userId } },
2669
+ limit: 1000
2670
+ });
2671
+ for (const doc of result.docs) {
2672
+ await this.adapter.delete("cequre_refresh_tokens", doc.id);
2673
+ }
2674
+ } catch (e) {
2675
+ if (e?.message !== "Database not connected") {
2676
+ log.warn({ err: e }, "Failed to purge refresh tokens during user erasure");
2677
+ }
2678
+ }
2679
+ }
2680
+ async count(collection, query) {
2681
+ return this.adapter.count(collection, query);
2682
+ }
2683
+ async syncDatabaseSchema() {
2684
+ if (!this.adapter)
2685
+ return;
2686
+ const syncLog = createLogger("schema-sync");
2687
+ if (this.adapter.getSystemTableStatements && this.adapter.raw) {
2688
+ const stmts = this.adapter.getSystemTableStatements();
2689
+ for (const stmt of stmts) {
2690
+ try {
2691
+ await this.adapter.raw(stmt);
2692
+ } catch (e) {
2693
+ const msg = e?.message || String(e);
2694
+ if (!msg.toLowerCase().includes("already exists")) {
2695
+ throw e;
2696
+ }
2697
+ }
2698
+ }
2699
+ }
2700
+ if (!this.adapter.getCurrentSchema || !this.adapter.createTableDDL || !this.adapter.addColumnDDL || !this.adapter.raw) {
2701
+ return;
2702
+ }
2703
+ const currentSchema = await this.adapter.getCurrentSchema();
2704
+ const existingTables = new Set(currentSchema.tables);
2705
+ const configuredTables = new Set(this.schema.collections.map((c) => c.slug));
2706
+ const isProduction = false;
2707
+ const dropOrphan = !isProduction && this.schema.migrate?.dropOrphan === true;
2708
+ for (const existingTable of existingTables) {
2709
+ if (existingTable !== "cequre_migrations" && existingTable !== "cequre_refresh_tokens" && !configuredTables.has(existingTable)) {
2710
+ if (dropOrphan) {
2711
+ if (this.adapter.dropTableDDL) {
2712
+ syncLog.info(`Dropping orphaned table: ${existingTable}`);
2713
+ await this.adapter.raw(this.adapter.dropTableDDL(existingTable));
2714
+ }
2715
+ } else {
2716
+ syncLog.warn(`Orphaned table detected but not dropped: ${existingTable}`);
2717
+ }
2718
+ }
2719
+ }
2720
+ const sortedCollections = [];
2721
+ const visited = new Set;
2722
+ const visiting = new Set;
2723
+ const visit = (col) => {
2724
+ if (visited.has(col.slug) || visiting.has(col.slug))
2725
+ return;
2726
+ visiting.add(col.slug);
2727
+ for (const field of col.fields) {
2728
+ if (field.type === "relationship" && field.target) {
2729
+ const targetCol = this.schema.collections.find((c) => c.slug === field.target);
2730
+ if (targetCol)
2731
+ visit(targetCol);
2732
+ }
2733
+ }
2734
+ visiting.delete(col.slug);
2735
+ visited.add(col.slug);
2736
+ sortedCollections.push(col);
2737
+ };
2738
+ for (const col of this.schema.collections)
2739
+ visit(col);
2740
+ for (const collection of sortedCollections) {
2741
+ const tableName = collection.slug;
2742
+ if (!existingTables.has(tableName)) {
2743
+ const ddl = this.adapter.createTableDDL(collection);
2744
+ syncLog.info(`Creating table: ${tableName}`);
2745
+ await this.adapter.raw(ddl);
2746
+ } else {
2747
+ const existingColumnsArray = currentSchema.columns[tableName] || [];
2748
+ const existingColumnNames = new Set(existingColumnsArray.map((c) => c.split(" ")[0]?.replace(/"/g, "") || ""));
2749
+ const configuredFields = new Set(collection.fields.map((f) => f.name));
2750
+ for (const field of collection.fields) {
2751
+ if (!existingColumnNames.has(field.name)) {
2752
+ const ddl = this.adapter.addColumnDDL(tableName, field);
2753
+ if (ddl) {
2754
+ syncLog.info(`Adding column: ${tableName}.${field.name}`);
2755
+ await this.adapter.raw(ddl);
2756
+ }
2757
+ }
2758
+ }
2759
+ for (const colName of existingColumnNames) {
2760
+ if (colName === "id" || colName === "createdAt" || colName === "updatedAt")
2761
+ continue;
2762
+ if (!configuredFields.has(colName)) {
2763
+ if (dropOrphan) {
2764
+ if (this.adapter.dropColumnDDL) {
2765
+ syncLog.info(`Dropping orphaned column: ${tableName}.${colName}`);
2766
+ await this.adapter.raw(this.adapter.dropColumnDDL(tableName, colName));
2767
+ }
2768
+ } else {
2769
+ syncLog.warn(`Orphaned column detected but not dropped: ${tableName}.${colName}`);
2770
+ }
2771
+ }
2772
+ }
2773
+ }
2774
+ }
2775
+ }
2776
+ cron(name, schedule, handler) {
2777
+ return this.cronManager?.registerJob?.(name, schedule, () => {
2778
+ handler(this);
2779
+ });
2780
+ }
2781
+ async fetch(req) {
2782
+ const rateLimitResponse = await this.rateLimiter.check(req);
2783
+ if (rateLimitResponse)
2784
+ return rateLimitResponse;
2785
+ let ctx;
2786
+ try {
2787
+ ctx = await this.getContext(req);
2788
+ for (const plugin of this.plugins) {
2789
+ if (plugin.beforeRequest) {
2790
+ await plugin.beforeRequest(ctx);
2791
+ }
2792
+ }
2793
+ let executionPromise = this.router.handle(req, undefined, ctx.user, this).then((r) => r || new Response("Not found", { status: 404 }));
2794
+ const timeoutMs = this.schema.core?.api?.requestTimeoutMs;
2795
+ console.log(`[TEST DEBUG] fetch url=${req.url}, timeoutMs=${timeoutMs}`);
2796
+ if (timeoutMs) {
2797
+ let timeoutHandle;
2798
+ const timeoutPromise = new Promise((_, reject) => {
2799
+ timeoutHandle = setTimeout(() => reject(new CequreError("Request Timeout", "TIMEOUT", 408)), timeoutMs);
2800
+ });
2801
+ executionPromise = Promise.race([
2802
+ executionPromise.finally(() => clearTimeout(timeoutHandle)),
2803
+ timeoutPromise
2804
+ ]);
2805
+ }
2806
+ let res = await executionPromise;
2807
+ for (const plugin of this.plugins) {
2808
+ if (plugin.afterRequest) {
2809
+ await plugin.afterRequest(ctx, res);
2810
+ }
2811
+ }
2812
+ return res;
2813
+ } catch (err) {
2814
+ if (!ctx)
2815
+ ctx = { request: req, user: null, cequre: this };
2816
+ for (const plugin of this.plugins) {
2817
+ if (plugin.onError) {
2818
+ const customRes = await plugin.onError(ctx, err);
2819
+ if (customRes instanceof Response)
2820
+ return customRes;
2821
+ }
2822
+ }
2823
+ if (!(err instanceof CequreError)) {
2824
+ log.error({ err }, "Unhandled error in request pipeline");
2825
+ }
2826
+ return toErrorResponse(err, req, this.adapter);
2827
+ }
2828
+ }
2829
+ async start(options = {}) {
2830
+ if (false) {}
2831
+ for (const plugin of this.plugins) {
2832
+ if (plugin.onInit) {
2833
+ await plugin.onInit(this);
2834
+ }
2835
+ }
2836
+ if (process.env.CEQURE_DEV) {
2837
+ log.info("Connecting to database...");
2838
+ const g = globalThis;
2839
+ if (g.__cequre_active_adapter && g.__cequre_active_adapter !== this.adapter) {
2840
+ try {
2841
+ await g.__cequre_active_adapter.disconnect();
2842
+ } catch (e) {}
2843
+ }
2844
+ g.__cequre_active_adapter = this.adapter;
2845
+ }
2846
+ await this.adapter.connect();
2847
+ if (this.adapter.configureCollections) {
2848
+ await this.adapter.configureCollections(this.schema.collections);
2849
+ }
2850
+ if (process.env.CEQURE_DEV) {
2851
+ log.info("Connected to database.");
2852
+ }
2853
+ await this.syncDatabaseSchema();
2854
+ await this.buildRoutes();
2855
+ this.queue.startWorker();
2856
+ if (process.env.CEQURE_CLI_GENERATE) {
2857
+ const { generateTypeScriptClientSDK: generateTypeScriptClientSDK2 } = (init_sdk(), __toCommonJS(exports_sdk));
2858
+ const sdkCode = generateTypeScriptClientSDK2(this.schema, this.router.list());
2859
+ process.stdout.write(`---CEQURE_SDK_START---
2860
+ ` + sdkCode + `
2861
+ ---CEQURE_SDK_END---
2862
+ `);
2863
+ if (this.adapter?.disconnect) {
2864
+ try {
2865
+ await this.adapter.disconnect();
2866
+ } catch {}
2867
+ }
2868
+ process.exit(0);
2869
+ }
2870
+ const nativeRoutes = this.router.toNativeRoutes({
2871
+ cequre: this,
2872
+ plugins: this.plugins,
2873
+ monitoring: this.monitoring,
2874
+ requestId: this.schema.monitoring?.requestId,
2875
+ cors: this.schema.security?.cors,
2876
+ csrfTrustedOrigins: this.schema.security?.csrfTrustedOrigins,
2877
+ rateLimiter: this.rateLimiter,
2878
+ resolveUser: async (request) => {
2879
+ const ctx = await this.getContext(request);
2880
+ return ctx.user;
2881
+ },
2882
+ maxBodyBytes: this.schema.core?.api?.maxBodyBytes,
2883
+ idempotencyKV: this.idempotencyKV,
2884
+ headers: {
2885
+ ...this.schema.security?.headers?.enabled ? {
2886
+ "X-Content-Type-Options": "nosniff",
2887
+ "X-Frame-Options": "DENY",
2888
+ "X-XSS-Protection": "1; mode=block",
2889
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
2890
+ "Referrer-Policy": "strict-origin-when-cross-origin",
2891
+ "Permissions-Policy": "geolocation=(), microphone=(), camera=()",
2892
+ ...this.schema.security.headers.contentSecurityPolicy === false ? {} : { "Content-Security-Policy": this.schema.security.headers.contentSecurityPolicy ?? "default-src 'self'" }
2893
+ } : {},
2894
+ ...this.isDeprecatedVersion ? {
2895
+ Deprecation: "true",
2896
+ ...this.schema.core?.api?.sunsetDate ? { Sunset: this.schema.core.api.sunsetDate } : {}
2897
+ } : {}
2898
+ }
2899
+ });
2900
+ const hasPlugins = this.plugins.length > 0;
2901
+ let maxRequestBodySize;
2902
+ const bodyLimitStr = this.schema.core?.bodyLimit;
2903
+ if (typeof bodyLimitStr === "string") {
2904
+ const match = bodyLimitStr.match(/^(\d+)(kb|mb|gb|b)?$/i);
2905
+ if (match) {
2906
+ const val = parseInt(match[1]);
2907
+ const unit = match[2]?.toLowerCase() || "b";
2908
+ if (unit === "b")
2909
+ maxRequestBodySize = val;
2910
+ if (unit === "kb")
2911
+ maxRequestBodySize = val * 1024;
2912
+ if (unit === "mb")
2913
+ maxRequestBodySize = val * 1024 * 1024;
2914
+ if (unit === "gb")
2915
+ maxRequestBodySize = val * 1024 * 1024 * 1024;
2916
+ }
2917
+ } else if (typeof bodyLimitStr === "number") {
2918
+ maxRequestBodySize = bodyLimitStr;
2919
+ }
2920
+ let finalTls = options.tls;
2921
+ if (!finalTls && this.schema.security?.tls) {
2922
+ const dslTls = this.schema.security.tls;
2923
+ if (dslTls.cert && dslTls.key) {
2924
+ finalTls = {
2925
+ cert: dslTls.cert,
2926
+ key: dslTls.key,
2927
+ passphrase: dslTls.passphrase,
2928
+ serverName: dslTls.serverName
2929
+ };
2930
+ }
2931
+ }
2932
+ let server;
2933
+ if (!this.serverProvider) {
2934
+ console.warn("\u26A0\uFE0F No ServerProvider configured in CequreRuntime. The HTTP server will not start automatically.");
2935
+ server = { port: options.port || 3000, stop: () => {} };
2936
+ } else {
2937
+ server = this.serverProvider.start({
2938
+ port: options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : 3000),
2939
+ tls: finalTls,
2940
+ ...maxRequestBodySize ? { maxRequestBodySize } : {},
2941
+ routes: nativeRoutes,
2942
+ fetch: async (req) => {
2943
+ if (this.shuttingDown) {
2944
+ return new Response("Service Unavailable", { status: 503, headers: { Connection: "close" } });
2945
+ }
2946
+ const method = req.method;
2947
+ const idempotencyKey = req.headers.get("idempotency-key");
2948
+ if (idempotencyKey && (method === "POST" || method === "PUT" || method === "PATCH") && this.idempotencyKV) {
2949
+ const cacheKey = `idempotency:${idempotencyKey}`;
2950
+ const cached = await this.idempotencyKV.get(cacheKey);
2951
+ if (cached) {
2952
+ const { status, body, contentType } = JSON.parse(cached);
2953
+ return new Response(body, { status, headers: { "Content-Type": contentType || "application/json", "X-Idempotent-Replay": "true" } });
2954
+ }
2955
+ }
2956
+ this.inFlightRequests++;
2957
+ try {
2958
+ const rateLimitResponse = await this.rateLimiter.check(req);
2959
+ if (rateLimitResponse)
2960
+ return rateLimitResponse;
2961
+ if (!hasPlugins)
2962
+ return new Response("Not found", { status: 404 });
2963
+ let ctx;
2964
+ try {
2965
+ ctx = await this.getContext(req);
2966
+ for (const plugin of this.plugins) {
2967
+ if (plugin.beforeRequest)
2968
+ await plugin.beforeRequest(ctx);
2969
+ }
2970
+ let executionPromise = (async () => {
2971
+ let r2 = await this.router.handle(req, undefined, ctx.user, this);
2972
+ return r2 || new Response("Not found", { status: 404 });
2973
+ })();
2974
+ const timeoutMs = this.schema.core?.api?.requestTimeoutMs;
2975
+ let timeoutHandle;
2976
+ if (timeoutMs) {
2977
+ const timeoutPromise = new Promise((_, reject) => {
2978
+ timeoutHandle = setTimeout(() => reject(new CequreError("Request Timeout", "TIMEOUT", 408)), timeoutMs);
2979
+ });
2980
+ executionPromise = Promise.race([
2981
+ executionPromise.finally(() => clearTimeout(timeoutHandle)),
2982
+ timeoutPromise
2983
+ ]);
2984
+ }
2985
+ let res = await executionPromise;
2986
+ for (const plugin of this.plugins) {
2987
+ if (plugin.afterRequest)
2988
+ await plugin.afterRequest(ctx, res);
2989
+ }
2990
+ return res;
2991
+ } catch (err) {
2992
+ if (!ctx)
2993
+ ctx = { request: req, user: null, cequre: this };
2994
+ for (const plugin of this.plugins) {
2995
+ if (plugin.onError) {
2996
+ const customRes = await plugin.onError(ctx, err);
2997
+ if (customRes instanceof Response)
2998
+ return customRes;
2999
+ }
3000
+ }
3001
+ if (!(err instanceof CequreError)) {
3002
+ log.error({ err }, "Unhandled error in request pipeline");
3003
+ }
3004
+ return toErrorResponse(err, req, this.adapter);
3005
+ }
3006
+ } finally {
3007
+ this.inFlightRequests--;
3008
+ }
3009
+ },
3010
+ websocket: this.realtimeProvider?.websocket?.handlers?.()
3011
+ });
3012
+ }
3013
+ const originalStop = server.stop.bind(server);
3014
+ const drainTimeoutMs = options.drainTimeoutMs ?? (process.env.CEQURE_DEV ? 0 : 30000);
3015
+ server.stop = (closeActiveConnections) => {
3016
+ this.shuttingDown = true;
3017
+ this.cronManager?.stopAll?.();
3018
+ this.queue.stopWorker();
3019
+ if (this.auditRetentionTimer)
3020
+ clearInterval(this.auditRetentionTimer);
3021
+ if (this.monitoring)
3022
+ this.monitoring.stop().catch(() => {});
3023
+ const drainPromise = new Promise((resolve) => {
3024
+ const start = Date.now();
3025
+ const checkInterval = setInterval(() => {
3026
+ if (this.inFlightRequests === 0 || Date.now() - start > drainTimeoutMs) {
3027
+ clearInterval(checkInterval);
3028
+ if (this.inFlightRequests > 0) {
3029
+ log.warn({ inFlight: this.inFlightRequests, drainTimeoutMs }, "Drain timeout reached \u2014 closing with in-flight requests");
3030
+ }
3031
+ resolve();
3032
+ }
3033
+ }, 50);
3034
+ });
3035
+ drainPromise.then(async () => {
3036
+ if (this.adapter?.disconnect) {
3037
+ try {
3038
+ await this.adapter.disconnect();
3039
+ } catch {}
3040
+ }
3041
+ log.info("Graceful shutdown complete");
3042
+ originalStop(closeActiveConnections);
3043
+ });
3044
+ return drainPromise;
3045
+ };
3046
+ process.on("SIGTERM", () => {
3047
+ log.info("SIGTERM received \u2014 initiating graceful shutdown");
3048
+ server.stop(true);
3049
+ });
3050
+ const r = "\x1B[38;2;34;211;238m";
3051
+ const w = "\x1B[38;2;255;255;255m";
3052
+ const d = "\x1B[38;2;161;161;170m";
3053
+ const b = "\x1B[1m";
3054
+ const reset = "\x1B[0m";
3055
+ const banner = `
3056
+ ${r}${b} ____ ${reset}
3057
+ ${r}${b} / ___|___ __ _ _ _ _ __ ___ ${reset}
3058
+ ${r}${b} | | / _ \\/ _\` | | | | '__/ _ \\${reset}
3059
+ ${r}${b} | |__| __/ (_| | |_| | | | __/${reset}
3060
+ ${r}${b} \\____\\___|\\__, |\\__,_|_| \\___|${reset}
3061
+ ${r}${b} |_| ${reset}
3062
+
3063
+ ${w}\u2728 Cequre Engine Online${reset}
3064
+ ${d}\uD83C\uDF0D http://localhost:${server?.port}${reset}
3065
+ `;
3066
+ if (!process.env.CEQURE_DEV) {
3067
+ console.log(banner);
3068
+ } else {
3069
+ const p = server ? server.port : options.port;
3070
+ console.log(`\uD83D\uDE80 Cequre API Framework started gracefully on port ${p}`);
3071
+ }
3072
+ return server;
3073
+ }
3074
+ }
3075
+
3076
+ // index.ts
3077
+ init_kv();
3078
+ init_logger();
3079
+
3080
+ // shared/utils/duration.ts
3081
+ function parseDuration(duration) {
3082
+ const match = duration.match(/^(\d+)([smhd])$/);
3083
+ if (!match) {
3084
+ return 86400;
3085
+ }
3086
+ const value = parseInt(match[1] || "0", 10);
3087
+ switch (match[2]) {
3088
+ case "s":
3089
+ return value;
3090
+ case "m":
3091
+ return value * 60;
3092
+ case "h":
3093
+ return value * 3600;
3094
+ case "d":
3095
+ return value * 86400;
3096
+ default:
3097
+ return 86400;
3098
+ }
3099
+ }
3100
+
3101
+ // index.ts
3102
+ import { Type } from "@sinclair/typebox";
3103
+ export {
3104
+ validateUpdate,
3105
+ validateCreate,
3106
+ ulid,
3107
+ toErrorResponse,
3108
+ timingSafeEqual,
3109
+ Type as t,
3110
+ sseManager,
3111
+ sqlLiteral,
3112
+ sqlIdentifier,
3113
+ sortPlugins,
3114
+ sha256Hex,
3115
+ requestContextStorage,
3116
+ populateDocuments,
3117
+ parseQuery,
3118
+ parseDuration,
3119
+ parseConstraintError,
3120
+ logger,
3121
+ getConstraintErrorStatus,
3122
+ getConstraintErrorMessage,
3123
+ generateTypeScriptClientSDK,
3124
+ generateSharedModels,
3125
+ generateScalarHTML,
3126
+ generateOpenAPISpec,
3127
+ generateBackendSchemas,
3128
+ extractPathname,
3129
+ createRouter,
3130
+ createRouteContext,
3131
+ createMailer,
3132
+ createLogger,
3133
+ createBunSSERoute,
3134
+ compileRouteHandler,
3135
+ buildErrorResponse,
3136
+ buildCollectionUpdateSchema,
3137
+ buildCollectionCreateSchema,
3138
+ SSEManager,
3139
+ MemoryStreamStore,
3140
+ MemoryCacheStore,
3141
+ CequreStreamMedia,
3142
+ CequreSSE,
3143
+ CequreRuntime,
3144
+ CequreRouter,
3145
+ CequreQueueManager,
3146
+ CequreMailer,
3147
+ CequreKV,
3148
+ CequreErrorLogs,
3149
+ CequreError,
3150
+ CequreDurableQueue,
3151
+ BaseSqlAdapter
3152
+ };