@anteros/core 0.0.39 → 0.0.41

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 (64) hide show
  1. package/database/audit.ts +193 -0
  2. package/database/collection.ts +4 -4
  3. package/database/deleteLog.ts +34 -0
  4. package/database/field.crypto.ts +1 -1
  5. package/database/file.ts +3 -3
  6. package/database/mongodbadapter.ts +321 -147
  7. package/database/replication.ts +780 -0
  8. package/database/schema.ts +140 -125
  9. package/database/tenant.ts +4 -0
  10. package/database/vars.ts +405 -0
  11. package/database/workflow.ts +6 -6
  12. package/index.ts +10 -0
  13. package/lib/audit.ts +271 -0
  14. package/lib/auditResult.ts +120 -0
  15. package/lib/define.ts +20 -1
  16. package/lib/files.ts +64 -11
  17. package/lib/lifecycle.ts +141 -0
  18. package/lib/redact.ts +117 -0
  19. package/lib/workflow.ts +1 -1
  20. package/package.json +1 -1
  21. package/server/api.ts +134 -33
  22. package/server/boot.ts +185 -53
  23. package/server/flags.ts +136 -0
  24. package/server/hono.ts +1 -1
  25. package/tests/audit-file.test.ts +296 -0
  26. package/tests/audit-operations.test.ts +184 -0
  27. package/tests/audit-redact.test.ts +233 -0
  28. package/tests/audit-results.test.ts +188 -0
  29. package/tests/audit-retention-config.test.ts +76 -0
  30. package/tests/audit-run-action.test.ts +120 -0
  31. package/tests/audit.test.ts +320 -0
  32. package/tests/crypto.test.ts +17 -17
  33. package/tests/db-cache.test.ts +7 -7
  34. package/tests/field-encryption.test.ts +25 -25
  35. package/tests/file-collection-replication.test.ts +98 -0
  36. package/tests/fixtures/index-tenant/collections/variants.model.ts +2 -2
  37. package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
  38. package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
  39. package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
  40. package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
  41. package/tests/fixtures/repl-only/app.ts +26 -0
  42. package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
  43. package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
  44. package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
  45. package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
  46. package/tests/fixtures/vars-tenant/vars/private.var.ts +6 -0
  47. package/tests/index-spec.test.ts +5 -5
  48. package/tests/lifecycle.test.ts +96 -0
  49. package/tests/replication-control.test.ts +128 -0
  50. package/tests/replication-initial-sync.test.ts +86 -0
  51. package/tests/replication-only.test.ts +358 -0
  52. package/tests/replication.test.ts +188 -0
  53. package/tests/upload.test.ts +239 -0
  54. package/tests/vars-api.test.ts +123 -0
  55. package/tests/vars-replication.test.ts +95 -0
  56. package/tests/vars.test.ts +216 -0
  57. package/types/collection.d.ts +3 -0
  58. package/types/config.d.ts +64 -0
  59. package/types/file.d.ts +4 -1
  60. package/types/lifecycle.d.ts +71 -0
  61. package/types/replication.d.ts +199 -0
  62. package/types/tenant.d.ts +23 -0
  63. package/types/vars.d.ts +147 -0
  64. package/utils/func.ts +67 -52
package/server/boot.ts CHANGED
@@ -1,5 +1,7 @@
1
1
 
2
2
  import { createApp, collectHeaders } from './hono'
3
+ import { parseBootFlags, resolveCapabilities, conflictingBootFlags, offCapabilities, capabilitiesLabel, FULL_CAPABILITIES } from './flags'
4
+ import type { BootCapabilities } from './flags'
3
5
  import type { ServerConfig } from '../types/config'
4
6
  import dayjs from 'dayjs';
5
7
  import pkg from '../package.json';
@@ -9,6 +11,9 @@ import { cfg, formatConfig } from './config' // import the config
9
11
  import { syncTenants } from '../database/tenant'
10
12
  import { syncCollections } from '../database/collection'
11
13
  import { syncFileCollections } from '../database/file'
14
+ import { syncVars } from '../database/vars'
15
+ import { startReplication, stopReplication } from '../database/replication'
16
+ import { loadLifecycles, runBeforeBoot, runAfterBoot, runOnDestroy } from '../lib/lifecycle'
12
17
 
13
18
  import { loadRoutes } from '../lib/routes'
14
19
  import { runScripts } from '../lib/scripts'
@@ -28,6 +33,80 @@ import { logger } from '../utils/logger';
28
33
  type BootAppOptions = ServerConfig & {
29
34
 
30
35
  }
36
+
37
+ /**
38
+ * Console banner — name / PID / role, env, reusePort, cache, replication.
39
+ * `url` is omitted in replication-only mode (nothing is listening).
40
+ */
41
+ function renderBanner(opts: {
42
+ name: string;
43
+ role: string;
44
+ env: string;
45
+ reusePort: boolean;
46
+ url?: string;
47
+ caps?: BootCapabilities;
48
+ }): string {
49
+ const { name, role, env, reusePort, url } = opts;
50
+ const caps = opts.caps ?? FULL_CAPABILITIES;
51
+ const off = offCapabilities(caps);
52
+
53
+ let box = '';
54
+ box += `${name}`.gray.underline + ` (PID: ${process.pid}) — ${role}\n\n`
55
+ box += `Env: ${env || 'dev'}`.green.bold + '\n'
56
+ box += `reusePort: ${reusePort ? 'On'.green.bold : 'Off'.red.bold}\n`.gray.bold
57
+
58
+ // DB query cache status (server.cache) — driver: memory | filesystem | redis
59
+ const cacheCfg = cfg.server.cache;
60
+ // any: @colors/colors types chained styles (.red.bold) as Color functions
61
+ let cacheInfo: any = 'Off'.red.bold;
62
+ if (cacheCfg?.enabled) {
63
+ const driver = cacheCfg.driver ?? 'memory';
64
+ const detail = driver === 'filesystem'
65
+ ? ` ${cacheCfg.directory ?? './.cache'}`
66
+ : driver === 'redis'
67
+ ? ` ${cacheCfg.redis?.host ?? 'localhost'}${cacheCfg.redis?.port ? `:${cacheCfg.redis.port}` : ''}`
68
+ : '';
69
+ cacheInfo = `On`.green.bold + ` (${driver}${detail})`;
70
+ }
71
+ box += `Cache: ${cacheInfo}`.gray.bold + '\n'
72
+
73
+ // Replication status — tenants with active replication + their destinations
74
+ const replTenants = (cfg.tenants ?? []).filter(
75
+ (t) => t.replication?.enabled !== false && (t.replication?.destinations?.length ?? 0) > 0,
76
+ );
77
+ let replicationInfo: any = 'Off'.red.bold;
78
+ if (!caps.replication) {
79
+ // The engine is never started, whatever the tenants declare
80
+ replicationInfo = 'Off'.red.bold + ` (${capabilitiesLabel(caps)})`.gray;
81
+ } else if (replTenants.length) {
82
+ const parts = replTenants.map((t) => {
83
+ const dests = (t.replication?.destinations ?? []).map((d) => d.id).join(', ');
84
+ return `${t.id} → ${dests}`;
85
+ });
86
+ replicationInfo = `On`.green.bold + ` (${parts.join('; ')})`;
87
+ }
88
+ box += `Replication: ${replicationInfo}`.gray.bold + '\n'
89
+
90
+ // Only shown when something is disabled — the default banner stays compact
91
+ if (off.length) {
92
+ box += `Mode: ${capabilitiesLabel(caps)}`.gray.bold + ` (${off.join(', ')} off)`.yellow + '\n';
93
+ }
94
+
95
+ box += url ? `Url: ${url}`.gray.bold : `Url: none — no API`.yellow.bold;
96
+ box += '\n\n';
97
+ box += `Last boot: 🔄 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`.gray;
98
+
99
+ return boxen(box, {
100
+ title: ` @anteros/core ${pkg.version}`,
101
+ padding: 1,
102
+ float: 'left',
103
+ borderColor: 'gray',
104
+ titleAlignment: 'center',
105
+ borderStyle: 'double',
106
+ textAlignment: 'left',
107
+ dimBorder: true
108
+ })
109
+ }
31
110
  async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
32
111
 
33
112
  try {
@@ -36,6 +115,15 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
36
115
  //******************************* */
37
116
  options = formatConfig(options);
38
117
 
118
+ // Boot capabilities — `--replication-only`, `--no-replication`, `--api-only`,
119
+ // `--no-scripts`, `--no-sockets` (CLI, wins) on top of `server.mode`.
120
+ const bootFlags = parseBootFlags();
121
+ const caps = resolveCapabilities(bootFlags, cfg.server?.mode);
122
+ cfg.server.mode = capabilitiesLabel(caps);
123
+ if (conflictingBootFlags(bootFlags)) {
124
+ console.log('⚠ --replication-only conflicts with --no-replication/--api-only — running replication-only'.yellow);
125
+ }
126
+
39
127
  // Logging: configure from cfg.server.logging (console + file, e.g. logs/anteros.log)
40
128
  logger.configure(cfg.server.logging);
41
129
  // File-only: the boxen banner already shows name / PID / URL / env on the console
@@ -48,14 +136,77 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
48
136
  await syncTenants(); // sync tenants and connect to database
49
137
  await syncCollections(); // sync collections and create collections on database
50
138
  await syncFileCollections(); // sync file collections
51
- await loadServices(); // load services
52
- await loadSockets(); // load websocket handlers
53
- await syncWorkflows();
54
- await syncMcpTools(); // load MCP tools per tenant (mcp/**/*.tool.ts)
55
- await loadTenantsMiddlewares();
56
- loadRoutes(); // load routes
139
+ await syncVars(); // load define.Vars + seed default variables (`_vars_` replication filters on them)
140
+
141
+ // Loaders only needed when an API is served — skipped when the API is off
142
+ if (caps.api) {
143
+ await loadServices(); // load services
144
+ if (caps.sockets) await loadSockets(); // load websocket handlers
145
+ await syncWorkflows();
146
+ await syncMcpTools(); // load MCP tools per tenant (mcp/**/*.tool.ts)
147
+ await loadTenantsMiddlewares();
148
+ await loadRoutes(); // load routes
149
+ }
150
+
151
+ await loadLifecycles(); // load {tenant.dir}/lifecycle.ts
152
+ await runBeforeBoot(); // blocking lifecycle hooks (migrations, seeding…) — fail-fast
153
+ if (caps.replication) {
154
+ await startReplication(); // connect replication destinations and schedule
155
+ } else {
156
+ logger.file('Replication engine disabled', { serverMode: cfg.server.mode });
157
+ }
57
158
  //******************************* */
58
159
 
160
+ // Uncaught errors → log file (MongoDB-style), then exit
161
+ process.on('uncaughtException', (err) => {
162
+ logger.error('Uncaught exception', { message: err?.message, stack: err?.stack });
163
+ process.exit(1);
164
+ });
165
+ process.on('unhandledRejection', (reason: any) => {
166
+ logger.error('Unhandled rejection', { message: reason?.message ?? String(reason), stack: reason?.stack });
167
+ });
168
+
169
+ // ── NO API: replication-only process ──
170
+ if (!caps.api) {
171
+ const NAME = cfg.server.name || process.env.APP_NAME || 'SERVER';
172
+ const env: string = process.env.NODE_ENV || Bun.env.NODE_ENV || 'dev';
173
+
174
+ // Nothing else keeps the event loop alive — replication timers are unref'd
175
+ const keepAlive = setInterval(() => { }, 60 * 60 * 1000);
176
+
177
+ const shutdownReplicationOnly = async (signal: string) => {
178
+ console.log(`\n${signal} received — stopping replication…`.gray);
179
+ clearInterval(keepAlive);
180
+ try { await runOnDestroy(signal as 'SIGINT' | 'SIGTERM'); } catch {}
181
+ try { await stopReplication(); } catch {}
182
+ for (const tenant of cfg.tenants ?? []) {
183
+ try { tenant.database?.client?.close(); } catch {}
184
+ }
185
+ process.exit(0);
186
+ };
187
+ process.once('SIGINT', () => shutdownReplicationOnly('SIGINT'));
188
+ process.once('SIGTERM', () => shutdownReplicationOnly('SIGTERM'));
189
+
190
+ console.log(renderBanner({
191
+ name: NAME,
192
+ role: 'replication-only',
193
+ env,
194
+ reusePort: false,
195
+ caps,
196
+ }));
197
+
198
+ logger.file('Replication-only process started', {
199
+ name: NAME,
200
+ pid: process.pid,
201
+ role: 'replication-only',
202
+ env,
203
+ tenants: cfg.tenants?.map((t) => t.id),
204
+ logFile: logger.filePath || undefined,
205
+ });
206
+
207
+ return null;
208
+ }
209
+
59
210
 
60
211
  // JWT_SECRET check: auto-generate if missing and auth is enabled
61
212
  const hasAuth = cfg.collections?.some((c: any) => c.api?.auth?.enabled);
@@ -117,12 +268,17 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
117
268
 
118
269
  console.log(`[cluster] Aggregated /health → http://localhost:${metricsPort}/health`.gray);
119
270
 
271
+ // Master owns the boot: run the tenant lifecycles once here (workers skip).
272
+ await runAfterBoot(metricsServer, io);
273
+
120
274
  // Graceful shutdown: stop workers + metrics server so a restart
121
275
  // (e.g. bun --watch) never leaves the port occupied
122
- const shutdownCluster = (signal: string) => {
276
+ const shutdownCluster = async (signal: string) => {
123
277
  console.log(`\n${signal} received — shutting down workers…`.gray);
124
278
  try { master.shutdown(); } catch {}
125
279
  try { metricsServer.stop(true); } catch {}
280
+ try { await runOnDestroy(signal as 'SIGINT' | 'SIGTERM'); } catch {}
281
+ try { await stopReplication(); } catch {}
126
282
  for (const tenant of cfg.tenants ?? []) {
127
283
  try { tenant.database?.client?.close(); } catch {}
128
284
  }
@@ -170,7 +326,7 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
170
326
  return res;
171
327
  });
172
328
 
173
- if (url.pathname === "/socket.io/") {
329
+ if (caps.sockets && url.pathname === "/socket.io/") {
174
330
  const start = performance.now();
175
331
  return respond(engineIo.handleRequest(req, server)).then((res) => {
176
332
  // Access log for the Socket.IO upgrade path (handled outside Hono) —
@@ -208,6 +364,8 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
208
364
  }
209
365
  return respond(app.fetch(req, server));
210
366
  },
367
+ // Sockets are disabled by not serving `/socket.io/` (below): without an
368
+ // explicit `server.upgrade()` call no client can ever open a websocket.
211
369
  websocket: websocket,
212
370
  maxRequestBodySize: 1024 * 1024 * 100, // 100MB
213
371
  })
@@ -215,11 +373,16 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
215
373
  // Register server for built-in metrics (GET /health)
216
374
  registerMetrics(server);
217
375
 
376
+ // The server is listening — run the tenant lifecycles (standalone only).
377
+ if (!isWorker) await runAfterBoot(server, io);
378
+
218
379
  // Graceful shutdown: release the port & DB connections so restarts
219
380
  // (e.g. bun --watch) never hit a zombie / EADDRINUSE
220
- const shutdownServer = (signal: string) => {
381
+ const shutdownServer = async (signal: string) => {
221
382
  console.log(`\n${signal} received — shutting down…`.gray);
222
383
  try { server.stop(true); } catch {}
384
+ try { await runOnDestroy(signal as 'SIGINT' | 'SIGTERM'); } catch {}
385
+ try { await stopReplication(); } catch {}
223
386
  for (const tenant of cfg.tenants ?? []) {
224
387
  try { tenant.database?.client?.close(); } catch {}
225
388
  }
@@ -241,47 +404,25 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
241
404
  }, 5000);
242
405
  }
243
406
 
244
- let box = '';
245
407
  const role = isWorker ? 'worker' : (reusePort ? 'master' : 'standalone');
246
- box += `${NAME}`.gray.underline + ` (PID: ${process.pid}) — ${role}\n\n`
247
- box += `Env: ${env || 'dev'}`.green.bold + '\n'
248
- box += `reusePort: ${(reusePort || isWorker) ? 'On'.green.bold : 'Off'.red.bold}\n`.gray.bold
249
-
250
- // DB query cache status (server.cache) — driver: memory | filesystem | redis
251
- const cacheCfg = cfg.server.cache;
252
- // any: @colors/colors type les styles chaînés (.red.bold) comme des fonctions Color
253
- let cacheInfo: any = 'Off'.red.bold;
254
- if (cacheCfg?.enabled) {
255
- const driver = cacheCfg.driver ?? 'memory';
256
- const detail = driver === 'filesystem'
257
- ? ` ${cacheCfg.directory ?? './.cache'}`
258
- : driver === 'redis'
259
- ? ` ${cacheCfg.redis?.host ?? 'localhost'}${cacheCfg.redis?.port ? `:${cacheCfg.redis.port}` : ''}`
260
- : '';
261
- cacheInfo = `On`.green.bold + ` (${driver}${detail})`;
262
- }
263
- box += `Cache: ${cacheInfo}`.gray.bold + '\n'
264
- box += `Url: http://localhost:${PORT}`.gray.bold;
265
- box += '\n\n';
266
- box += `Last boot: 🔄 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`.gray;
267
-
268
- console.log(boxen(box, {
269
- title: ` @anteros/core ${pkg.version}`,
270
- padding: 1,
271
- float: 'left',
272
- borderColor: 'gray',
273
- titleAlignment: 'center',
274
- borderStyle: 'double',
275
- textAlignment: 'left',
276
- dimBorder: true
408
+
409
+ console.log(renderBanner({
410
+ name: NAME,
411
+ role,
412
+ env,
413
+ reusePort: reusePort || isWorker,
414
+ url: `http://localhost:${PORT}`,
415
+ caps,
277
416
  }))
278
417
 
279
418
 
280
419
  // After boot and App ready.
281
- setTimeout(() => {
282
- runScripts().catch(); // run scripts
420
+ if (caps.scripts) {
421
+ setTimeout(() => {
422
+ runScripts().catch(); // run scripts
283
423
 
284
- }, 150);
424
+ }, 150);
425
+ }
285
426
 
286
427
  // Boot event — recorded in the log file only (the console banner already
287
428
  // shows name / PID / role / URL, so this line would be redundant on screen)
@@ -294,15 +435,6 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
294
435
  logFile: logger.filePath || undefined,
295
436
  });
296
437
 
297
- // Uncaught errors → log file (MongoDB-style), then exit
298
- process.on('uncaughtException', (err) => {
299
- logger.error('Uncaught exception', { message: err?.message, stack: err?.stack });
300
- process.exit(1);
301
- });
302
- process.on('unhandledRejection', (reason: any) => {
303
- logger.error('Unhandled rejection', { message: reason?.message ?? String(reason), stack: reason?.stack });
304
- });
305
-
306
438
  return server;
307
439
  } catch (err: any) {
308
440
  logger.error('Failed to boot server', cfg.debug ? { error: err, message: err?.message } : { message: err?.message });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * What a process actually runs.
3
+ *
4
+ * `api: false` disables everything the HTTP server carries — routes, services,
5
+ * MCP tools, middlewares, websockets — and the scripts that run alongside it.
6
+ */
7
+ export type BootCapabilities = {
8
+ /** HTTP server: routes, services, MCP tools, middlewares, `/health` */
9
+ api: boolean
10
+ /** Replication engine: schedule, tombstones, `_replication_` state */
11
+ replication: boolean
12
+ /** `runScripts()` shortly after boot */
13
+ scripts: boolean
14
+ /** Socket.IO handlers and the `/socket.io/` endpoint */
15
+ sockets: boolean
16
+ }
17
+
18
+ /**
19
+ * Named boot modes (configurable through `server.mode`, overridable by flags).
20
+ *
21
+ * - `full` (default): everything.
22
+ * - `replication-only`: replication engine only — no API at all.
23
+ * - `no-replication`: full server that never starts the replication engine.
24
+ * - `api-only`: the HTTP API and nothing around it (no replication, no scripts, no sockets).
25
+ */
26
+ export type BootMode = 'full' | 'replication-only' | 'no-replication' | 'api-only'
27
+
28
+ export type BootFlags = {
29
+ /** `--replication-only` — replication engine only, no API */
30
+ replicationOnly: boolean
31
+ /** `--no-replication` — full server, replication engine not started */
32
+ noReplication: boolean
33
+ /** `--api-only` — the HTTP API and nothing around it */
34
+ apiOnly: boolean
35
+ /** `--no-scripts` — never run the tenant scripts (`*.run.ts` under `scripts/`) */
36
+ noScripts: boolean
37
+ /** `--no-sockets` — no Socket.IO handlers, no `/socket.io/` endpoint */
38
+ noSockets: boolean
39
+ }
40
+
41
+ export const FULL_CAPABILITIES: BootCapabilities = {
42
+ api: true,
43
+ replication: true,
44
+ scripts: true,
45
+ sockets: true,
46
+ }
47
+
48
+ const REPLICATION_ONLY: BootCapabilities = {
49
+ api: false,
50
+ replication: true,
51
+ scripts: false,
52
+ sockets: false,
53
+ }
54
+
55
+ const API_ONLY: BootCapabilities = {
56
+ api: true,
57
+ replication: false,
58
+ scripts: false,
59
+ sockets: false,
60
+ }
61
+
62
+ function bootModeCapabilities(mode?: BootMode): BootCapabilities {
63
+ switch (mode) {
64
+ case 'replication-only': return { ...REPLICATION_ONLY }
65
+ case 'api-only': return { ...API_ONLY }
66
+ case 'no-replication': return { ...FULL_CAPABILITIES, replication: false }
67
+ default: return { ...FULL_CAPABILITIES }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Read the boot flags from the command line (camelCase spellings accepted).
73
+ *
74
+ * @example bun index.ts --replication-only
75
+ * @example bun index.ts --api-only
76
+ * @example bun index.ts --no-scripts --no-sockets
77
+ */
78
+ function parseBootFlags(argv: string[] = process.argv): BootFlags {
79
+ const has = (...names: string[]) => names.some((name) => argv.includes(name))
80
+ return {
81
+ replicationOnly: has('--replication-only', '--replicationOnly'),
82
+ noReplication: has('--no-replication', '--noReplication'),
83
+ apiOnly: has('--api-only', '--apiOnly'),
84
+ noScripts: has('--no-scripts', '--noScripts'),
85
+ noSockets: has('--no-sockets', '--noSockets'),
86
+ }
87
+ }
88
+
89
+ /** True when the flags ask for contradictory things — worth warning the operator */
90
+ function conflictingBootFlags(flags: BootFlags): boolean {
91
+ return flags.replicationOnly && (flags.noReplication || flags.apiOnly)
92
+ }
93
+
94
+ /**
95
+ * What the process will run: `server.mode` first, then the flags, which win.
96
+ * `--api-only` is the preset "API and nothing around it".
97
+ */
98
+ function resolveCapabilities(flags: BootFlags, configured?: BootMode): BootCapabilities {
99
+ // A flag always wins over the configured mode
100
+ if (flags.replicationOnly) return { ...REPLICATION_ONLY }
101
+ if (flags.apiOnly) return { ...API_ONLY }
102
+
103
+ const caps = bootModeCapabilities(configured)
104
+ if (flags.noReplication) caps.replication = false
105
+ if (flags.noScripts) caps.scripts = false
106
+ if (flags.noSockets) caps.sockets = false
107
+
108
+ // Scripts and sockets belong to the API process
109
+ if (!caps.api) {
110
+ caps.scripts = false
111
+ caps.sockets = false
112
+ }
113
+
114
+ return caps
115
+ }
116
+
117
+ /** Disabled capabilities, for the banner and logs — e.g. `['scripts', 'sockets']` */
118
+ function offCapabilities(caps: BootCapabilities): (keyof BootCapabilities)[] {
119
+ return (Object.keys(caps) as (keyof BootCapabilities)[]).filter((key) => !caps[key])
120
+ }
121
+
122
+ /** Closest named mode, used by the banner — all flags off is simply `full` */
123
+ function capabilitiesLabel(caps: BootCapabilities): BootMode {
124
+ if (!caps.api && caps.replication) return 'replication-only'
125
+ if (caps.api && !caps.replication && !caps.scripts && !caps.sockets) return 'api-only'
126
+ // `--no-scripts` / `--no-sockets` alone stay in `full`: the banner lists what is off
127
+ return 'full'
128
+ }
129
+
130
+ export {
131
+ parseBootFlags,
132
+ conflictingBootFlags,
133
+ resolveCapabilities,
134
+ offCapabilities,
135
+ capabilitiesLabel,
136
+ }
package/server/hono.ts CHANGED
@@ -345,7 +345,7 @@ function createApp(): Hono<{ Variables: HonoVariables }> {
345
345
 
346
346
 
347
347
  const requestCtx: Record<string, string> = c.req.header();
348
- // Faille 1: never log sensitive headers (JWT, cookies)
348
+ // Flaw 1: never log sensitive headers (JWT, cookies)
349
349
  delete requestCtx.authorization;
350
350
  delete requestCtx.cookie;
351
351
  requestCtxStorage.set('meta', {