@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.
- package/database/audit.ts +193 -0
- package/database/collection.ts +4 -4
- package/database/deleteLog.ts +34 -0
- package/database/field.crypto.ts +1 -1
- package/database/file.ts +3 -3
- package/database/mongodbadapter.ts +321 -147
- package/database/replication.ts +780 -0
- package/database/schema.ts +140 -125
- package/database/tenant.ts +4 -0
- package/database/vars.ts +405 -0
- package/database/workflow.ts +6 -6
- package/index.ts +10 -0
- package/lib/audit.ts +271 -0
- package/lib/auditResult.ts +120 -0
- package/lib/define.ts +20 -1
- package/lib/files.ts +64 -11
- package/lib/lifecycle.ts +141 -0
- package/lib/redact.ts +117 -0
- package/lib/workflow.ts +1 -1
- package/package.json +1 -1
- package/server/api.ts +134 -33
- package/server/boot.ts +185 -53
- package/server/flags.ts +136 -0
- package/server/hono.ts +1 -1
- package/tests/audit-file.test.ts +296 -0
- package/tests/audit-operations.test.ts +184 -0
- package/tests/audit-redact.test.ts +233 -0
- package/tests/audit-results.test.ts +188 -0
- package/tests/audit-retention-config.test.ts +76 -0
- package/tests/audit-run-action.test.ts +120 -0
- package/tests/audit.test.ts +320 -0
- package/tests/crypto.test.ts +17 -17
- package/tests/db-cache.test.ts +7 -7
- package/tests/field-encryption.test.ts +25 -25
- package/tests/file-collection-replication.test.ts +98 -0
- package/tests/fixtures/index-tenant/collections/variants.model.ts +2 -2
- package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
- package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
- package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
- package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
- package/tests/fixtures/repl-only/app.ts +26 -0
- package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
- package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
- package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
- package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +6 -0
- package/tests/index-spec.test.ts +5 -5
- package/tests/lifecycle.test.ts +96 -0
- package/tests/replication-control.test.ts +128 -0
- package/tests/replication-initial-sync.test.ts +86 -0
- package/tests/replication-only.test.ts +358 -0
- package/tests/replication.test.ts +188 -0
- package/tests/upload.test.ts +239 -0
- package/tests/vars-api.test.ts +123 -0
- package/tests/vars-replication.test.ts +95 -0
- package/tests/vars.test.ts +216 -0
- package/types/collection.d.ts +3 -0
- package/types/config.d.ts +64 -0
- package/types/file.d.ts +4 -1
- package/types/lifecycle.d.ts +71 -0
- package/types/replication.d.ts +199 -0
- package/types/tenant.d.ts +23 -0
- package/types/vars.d.ts +147 -0
- 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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
282
|
-
|
|
420
|
+
if (caps.scripts) {
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
runScripts().catch(); // run scripts
|
|
283
423
|
|
|
284
|
-
|
|
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 });
|
package/server/flags.ts
ADDED
|
@@ -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
|
-
//
|
|
348
|
+
// Flaw 1: never log sensitive headers (JWT, cookies)
|
|
349
349
|
delete requestCtx.authorization;
|
|
350
350
|
delete requestCtx.cookie;
|
|
351
351
|
requestCtxStorage.set('meta', {
|