@bhooai/nexus-core 2.0.13 → 2.0.16
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/package.json +4 -2
- package/src/app/Storage.ts +616 -9
- package/src/app/Upload.ts +114 -0
- package/src/app/adminModule.ts +256 -52
- package/src/app/authModule.ts +56 -30
- package/src/app/createNexusApp.ts +196 -28
- package/src/app/databaseAdminModule.ts +171 -2
- package/src/app/discover.ts +4 -2
- package/src/app/errorPages.ts +21 -5
- package/src/app/fusionEngine.ts +53 -0
- package/src/app/index.ts +3 -0
- package/src/app/preflightModule.ts +41 -4
- package/src/app/userStore.ts +321 -0
- package/src/config/dbAccess.ts +26 -0
- package/src/config/defaults.ts +34 -4
- package/src/config/index.ts +1 -0
- package/src/config/schema.ts +72 -6
- package/src/config/types.ts +94 -1
- package/src/errors.ts +22 -1
- package/src/http/Server.ts +27 -3
- package/src/http/static.ts +1 -1
- package/tests/config.test.ts +24 -1
- package/tests/fusion-userstore.test.ts +68 -0
- package/vitest.config.ts +1 -0
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { Router } from '../index.js';
|
|
2
|
+
import type { ActiveDatabase } from '../config/types.js';
|
|
3
|
+
import { getAppFusion } from './fusionEngine.js';
|
|
4
|
+
import type { FusionDatabase } from '@bhooai/nexus-fusion';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
* Admin routes for
|
|
7
|
+
* Admin routes for database / collection administration.
|
|
8
|
+
*
|
|
9
|
+
* MongoDB backend (default): full database/collection management with
|
|
10
|
+
* `$jsonSchema` validators (used by the AI schema generator).
|
|
11
|
+
*
|
|
12
|
+
* Fusion backend (`db.active: 'fusion'`): branches stand in for databases
|
|
13
|
+
* (routes keep the same paths); collections are implicit. Rename is
|
|
14
|
+
* copy-then-delete (the engine has no rename op); `$jsonSchema` validators
|
|
15
|
+
* are Mongo-only and rejected with a 400 explaining the alternative
|
|
16
|
+
* (`@bhooai/nexus-fusion/schemas` validation in code).
|
|
5
17
|
*
|
|
6
18
|
* The admin app uses these to create, rename and drop databases and
|
|
7
19
|
* collections, and to preview documents. Collections can be created with a
|
|
@@ -27,10 +39,25 @@ const DOC_PREVIEW_LIMIT = 10;
|
|
|
27
39
|
/** Ugly but real: dropping a db's only collection deletes the whole database. */
|
|
28
40
|
const BOOTSTRAP_COLLECTION = '_nexus_bootstrap';
|
|
29
41
|
|
|
42
|
+
/** Bootstrap doc id — materializes implicit Fusion collections; hidden from listings. */
|
|
43
|
+
const BOOTSTRAP_DOC = '_nexus_bootstrap';
|
|
44
|
+
|
|
30
45
|
/** Databases managed by MongoDB itself — hidden from the admin surface. */
|
|
31
46
|
const SYSTEM_DATABASES = new Set(['admin', 'local', 'config']);
|
|
32
47
|
|
|
33
|
-
export function registerDatabaseAdminRoutes(
|
|
48
|
+
export function registerDatabaseAdminRoutes(
|
|
49
|
+
router: Router,
|
|
50
|
+
lazy: LazyDb,
|
|
51
|
+
active: ActiveDatabase = 'mongodb',
|
|
52
|
+
): void {
|
|
53
|
+
if (active === 'fusion') {
|
|
54
|
+
registerFusionDatabaseAdminRoutes(router);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
registerMongoDatabaseAdminRoutes(router, lazy);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerMongoDatabaseAdminRoutes(router: Router, lazy: LazyDb): void {
|
|
34
61
|
router.get('/admin/databases', async (ctx) => {
|
|
35
62
|
const db = await lazy.db();
|
|
36
63
|
if (!db) { ctx.json({ error: 'MongoDB not reachable' }, 503); return; }
|
|
@@ -184,6 +211,148 @@ export function registerDatabaseAdminRoutes(router: Router, lazy: LazyDb): void
|
|
|
184
211
|
});
|
|
185
212
|
}
|
|
186
213
|
|
|
214
|
+
function registerFusionDatabaseAdminRoutes(router: Router): void {
|
|
215
|
+
// Branches stand in for databases; the engine's fixed db is 'main'.
|
|
216
|
+
const view = (branch: string): FusionDatabase => getAppFusion().branchView(branch);
|
|
217
|
+
|
|
218
|
+
router.get('/admin/databases', async (ctx) => {
|
|
219
|
+
try {
|
|
220
|
+
const fusion = getAppFusion();
|
|
221
|
+
const databases = [];
|
|
222
|
+
for (const branch of fusion.listBranches()) {
|
|
223
|
+
const bcols = view(branch).listCollections().filter((c) => c !== BOOTSTRAP_COLLECTION);
|
|
224
|
+
const collections = [];
|
|
225
|
+
for (const c of bcols) {
|
|
226
|
+
let count = 0;
|
|
227
|
+
try {
|
|
228
|
+
const snap = await view(branch).collection(c).get();
|
|
229
|
+
count = snap.docs.filter((d) => d.id !== BOOTSTRAP_DOC).length;
|
|
230
|
+
} catch { /* stats unavailable */ }
|
|
231
|
+
collections.push({ name: c, count });
|
|
232
|
+
}
|
|
233
|
+
databases.push({ name: branch, sizeOnDisk: 0, collections });
|
|
234
|
+
}
|
|
235
|
+
ctx.json({ databases });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
router.post('/admin/databases', async (ctx) => {
|
|
242
|
+
const name = assertDbName((ctx.body as { name?: unknown } | undefined)?.name, ctx);
|
|
243
|
+
if (!name) return;
|
|
244
|
+
try {
|
|
245
|
+
getAppFusion().createBranch(name);
|
|
246
|
+
ctx.json({ ok: true, name });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
router.delete('/admin/databases/:db', async (ctx) => {
|
|
253
|
+
const name = assertDbName(ctx.params.db, ctx);
|
|
254
|
+
if (!name) return;
|
|
255
|
+
try {
|
|
256
|
+
getAppFusion().dropBranch(name);
|
|
257
|
+
ctx.json({ ok: true, dropped: name });
|
|
258
|
+
} catch (err) {
|
|
259
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
router.post('/admin/databases/:db/collections', async (ctx) => {
|
|
264
|
+
const branch = assertDbName(ctx.params.db, ctx);
|
|
265
|
+
if (!branch) return;
|
|
266
|
+
const body = (ctx.body ?? {}) as { name?: unknown; jsonSchema?: unknown; validator?: unknown };
|
|
267
|
+
const name = assertCollectionName(body.name, ctx);
|
|
268
|
+
if (!name) return;
|
|
269
|
+
if (body.jsonSchema !== undefined || body.validator !== undefined) {
|
|
270
|
+
ctx.json({ error: '$jsonSchema validators are Mongo-only — validate with @bhooai/nexus-fusion/schemas in code instead' }, 400);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
// Collections are implicit in Fusion — a bootstrap doc materializes it.
|
|
275
|
+
await view(branch).collection(name).doc(BOOTSTRAP_DOC).set({ _bootstrap: true });
|
|
276
|
+
ctx.json({ ok: true, db: branch, collection: name, validator: null });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
router.put('/admin/databases/:db/collections/:name', async (ctx) => {
|
|
283
|
+
const branch = assertDbName(ctx.params.db, ctx);
|
|
284
|
+
if (!branch) return;
|
|
285
|
+
const name = assertCollectionName(ctx.params.name, ctx);
|
|
286
|
+
if (!name) return;
|
|
287
|
+
const body = (ctx.body ?? {}) as { newName?: unknown; validator?: unknown };
|
|
288
|
+
if (body.validator !== undefined) {
|
|
289
|
+
ctx.json({ error: '$jsonSchema validators are Mongo-only — validate with @bhooai/nexus-fusion/schemas in code instead' }, 400);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const changed: string[] = [];
|
|
293
|
+
try {
|
|
294
|
+
if (body.newName !== undefined) {
|
|
295
|
+
const newName = assertCollectionName(body.newName, ctx);
|
|
296
|
+
if (!newName) return;
|
|
297
|
+
if (newName !== name) {
|
|
298
|
+
// No engine rename op — copy then delete.
|
|
299
|
+
const snap = await view(branch).collection(name).get();
|
|
300
|
+
for (const d of snap.docs) {
|
|
301
|
+
if (d.id === BOOTSTRAP_DOC) continue;
|
|
302
|
+
await view(branch).collection(newName).doc(d.id).set(d.data as Record<string, unknown>);
|
|
303
|
+
}
|
|
304
|
+
for (const d of snap.docs) {
|
|
305
|
+
await view(branch).collection(name).doc(d.id).delete();
|
|
306
|
+
}
|
|
307
|
+
await view(branch).collection(newName).doc(BOOTSTRAP_DOC).set({ _bootstrap: true });
|
|
308
|
+
changed.push(`renamed to ${newName}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!changed.length) {
|
|
312
|
+
ctx.json({ error: 'nothing to modify — send newName' }, 400);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
ctx.json({ ok: true, db: branch, collection: name, changed });
|
|
316
|
+
} catch (err) {
|
|
317
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
router.delete('/admin/databases/:db/collections/:name', async (ctx) => {
|
|
322
|
+
const branch = assertDbName(ctx.params.db, ctx);
|
|
323
|
+
if (!branch) return;
|
|
324
|
+
const name = assertCollectionName(ctx.params.name, ctx);
|
|
325
|
+
if (!name) return;
|
|
326
|
+
try {
|
|
327
|
+
const snap = await view(branch).collection(name).get();
|
|
328
|
+
for (const d of snap.docs) {
|
|
329
|
+
await view(branch).collection(name).doc(d.id).delete();
|
|
330
|
+
}
|
|
331
|
+
ctx.json({ ok: true, dropped: { db: branch, collection: name } });
|
|
332
|
+
} catch (err) {
|
|
333
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
router.get('/admin/databases/:db/collections/:name/docs', async (ctx) => {
|
|
338
|
+
const branch = assertDbName(ctx.params.db, ctx);
|
|
339
|
+
if (!branch) return;
|
|
340
|
+
const name = assertCollectionName(ctx.params.name, ctx);
|
|
341
|
+
if (!name) return;
|
|
342
|
+
try {
|
|
343
|
+
const snap = await view(branch).collection<Record<string, unknown>>(name).get();
|
|
344
|
+
const docs = snap.docs
|
|
345
|
+
.filter((d) => d.id !== BOOTSTRAP_DOC)
|
|
346
|
+
.slice(0, DOC_PREVIEW_LIMIT)
|
|
347
|
+
.map((d) => ({ id: d.id, ...d.data }));
|
|
348
|
+
const count = snap.docs.filter((d) => d.id !== BOOTSTRAP_DOC).length;
|
|
349
|
+
ctx.json({ db: branch, collection: name, count, docs });
|
|
350
|
+
} catch (err) {
|
|
351
|
+
ctx.json({ error: (err as Error).message }, 500);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
187
356
|
function assertDbName(v: unknown, ctx: { json(data: unknown, status?: number): void; res: { writableEnded: boolean } }): string | null {
|
|
188
357
|
const name = typeof v === 'string' ? v.trim() : '';
|
|
189
358
|
if (!name || name.length > 63) { ctx.json({ error: 'database name is required (max 63 chars)' }, 400); return null; }
|
package/src/app/discover.ts
CHANGED
|
@@ -65,6 +65,7 @@ export interface DiscoveryResult {
|
|
|
65
65
|
migrations: DiscoveredFile[];
|
|
66
66
|
plugins: DiscoveredFile[];
|
|
67
67
|
config: DiscoveredFile[];
|
|
68
|
+
live: DiscoveredFile[];
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -74,7 +75,7 @@ export interface DiscoveryResult {
|
|
|
74
75
|
export async function discoverBackend(srcRoot: string): Promise<DiscoveryResult> {
|
|
75
76
|
const abs = resolve(srcRoot);
|
|
76
77
|
|
|
77
|
-
const [routes, graphql, rooms, mailables, errorPages, events, listeners, jobs, policies, providers, seeds, migrations, plugins, config] =
|
|
78
|
+
const [routes, graphql, rooms, mailables, errorPages, events, listeners, jobs, policies, providers, seeds, migrations, plugins, config, live] =
|
|
78
79
|
await Promise.all([
|
|
79
80
|
collectWithModules(abs, 'routes', /\.(ts|js)$/),
|
|
80
81
|
collectWithModules(abs, 'graphql', /\.graph\.(ts|js)$/),
|
|
@@ -90,6 +91,7 @@ export async function discoverBackend(srcRoot: string): Promise<DiscoveryResult>
|
|
|
90
91
|
collect(join(abs, 'database', 'migrations'), /^\d{4}_\d{2}_\d{2}_\d{6}_.*\.(ts|js)$/),
|
|
91
92
|
collectPluginDirs(join(abs, 'plugins')),
|
|
92
93
|
collect(join(abs, 'config'), /\.(ts|js)$/),
|
|
94
|
+
collect(join(abs, 'live'), /\.live\.(ts|js)$/),
|
|
93
95
|
]);
|
|
94
96
|
|
|
95
97
|
// errors/Handler.ts is a single well-known file.
|
|
@@ -98,7 +100,7 @@ export async function discoverBackend(srcRoot: string): Promise<DiscoveryResult>
|
|
|
98
100
|
? { path: errorHandlerPath, name: 'Handler', subdir: '' }
|
|
99
101
|
: null;
|
|
100
102
|
|
|
101
|
-
return { routes, graphql, rooms, mailables, errorPages, events, listeners, jobs, policies, providers, seeds, migrations, plugins, config, errorHandler };
|
|
103
|
+
return { routes, graphql, rooms, mailables, errorPages, events, listeners, jobs, policies, providers, seeds, migrations, plugins, config, live, errorHandler };
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
async function collect(dir: string, pattern: RegExp): Promise<DiscoveredFile[]> {
|
package/src/app/errorPages.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ErrorPages — renders a templated HTML page for a given HTTP status.
|
|
3
3
|
*
|
|
4
|
-
* Reads `
|
|
5
|
-
* to the framework default template. Substitutes simple
|
|
6
|
-
* markers. Production-safe (no stack traces).
|
|
4
|
+
* Reads `errors/pages/<status>.html` (backend src-level) if present, else
|
|
5
|
+
* falls back to the framework default template. Substitutes simple
|
|
6
|
+
* {{PLACEHOLDER}} markers. Production-safe (no stack traces).
|
|
7
7
|
*/
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
@@ -37,7 +37,10 @@ export interface ErrorPageRenderContext {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export class ErrorPages {
|
|
40
|
-
constructor(
|
|
40
|
+
constructor(
|
|
41
|
+
private projectRoot: string,
|
|
42
|
+
private srcRoot?: string,
|
|
43
|
+
) {}
|
|
41
44
|
|
|
42
45
|
/** Render the HTML for a given status. */
|
|
43
46
|
async render(status: number, ctx: ErrorPageRenderContext = {}): Promise<string> {
|
|
@@ -59,8 +62,21 @@ export class ErrorPages {
|
|
|
59
62
|
|
|
60
63
|
private async tryCustomPage(status: number): Promise<string | null> {
|
|
61
64
|
const candidates = [
|
|
65
|
+
// Backend src root first — the layout used by real projects
|
|
66
|
+
// (e.g. apps/backend/src/errors/pages/404.html).
|
|
67
|
+
...(this.srcRoot
|
|
68
|
+
? [
|
|
69
|
+
join(this.srcRoot, 'errors', 'pages', `${status}.html`),
|
|
70
|
+
join(this.srcRoot, 'errors', 'pages', 'error.html'),
|
|
71
|
+
]
|
|
72
|
+
: []),
|
|
73
|
+
// Project-root fallbacks (legacy + monorepo variants).
|
|
62
74
|
join(this.projectRoot, 'src', 'errors', 'pages', `${status}.html`),
|
|
63
|
-
join(this.projectRoot, 'src', 'errors', 'pages', 'error.html'),
|
|
75
|
+
join(this.projectRoot, 'src', 'errors', 'pages', 'error.html'),
|
|
76
|
+
join(this.projectRoot, 'apps', 'backend', 'src', 'errors', 'pages', `${status}.html`),
|
|
77
|
+
join(this.projectRoot, 'apps', 'backend', 'src', 'errors', 'pages', 'error.html'),
|
|
78
|
+
join(this.projectRoot, 'errors', 'pages', `${status}.html`),
|
|
79
|
+
join(this.projectRoot, 'errors', 'pages', 'error.html'), // generic catch-all
|
|
64
80
|
];
|
|
65
81
|
for (const cand of candidates) {
|
|
66
82
|
if (existsSync(cand)) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { FusionDatabase } from '@bhooai/nexus-fusion';
|
|
4
|
+
import type { NexusConfig } from '../config/types.js';
|
|
5
|
+
import { fusionDir } from '../config/dbAccess.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* App Fusion engine lifecycle — one engine per backend process, opened at
|
|
9
|
+
* boot when `db.fusion.enabled` and closed on shutdown.
|
|
10
|
+
*
|
|
11
|
+
* `@bhooai/nexus-fusion` is dynamically imported so mongo-only backends never
|
|
12
|
+
* need the package (or its Rust core) installed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let engine: FusionDatabase | null = null;
|
|
16
|
+
|
|
17
|
+
export async function openAppFusion(projectRoot: string, config: NexusConfig): Promise<FusionDatabase> {
|
|
18
|
+
if (engine) return engine;
|
|
19
|
+
let mod: typeof import('@bhooai/nexus-fusion');
|
|
20
|
+
try {
|
|
21
|
+
mod = await import('@bhooai/nexus-fusion');
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"[db] db.fusion.enabled is true but '@bhooai/nexus-fusion' cannot be loaded — " +
|
|
25
|
+
'add it as a dependency (file:../path/to/packages/nexus-fusion) and install.',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const fz = config.db.fusion;
|
|
29
|
+
const dir = fz.persist ? resolve(projectRoot, fusionDir(config)) : undefined;
|
|
30
|
+
if (dir) await mkdir(dir, { recursive: true });
|
|
31
|
+
engine = mod.fusion('main', 'prod', {
|
|
32
|
+
...(dir ? { dir } : {}),
|
|
33
|
+
cacheCapacity: fz.cacheCapacity,
|
|
34
|
+
cacheTtlMs: fz.cacheTtlMs,
|
|
35
|
+
walCompactThreshold: fz.walCompactThreshold,
|
|
36
|
+
});
|
|
37
|
+
console.log(`[db] fusion engine open (${dir ?? 'in-memory'})`);
|
|
38
|
+
return engine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getAppFusion(): FusionDatabase {
|
|
42
|
+
if (!engine) throw new Error('[db] Fusion engine is not open — enable db.fusion first.');
|
|
43
|
+
return engine;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function closeAppFusion(): Promise<void> {
|
|
47
|
+
if (!engine) return;
|
|
48
|
+
try {
|
|
49
|
+
engine.close();
|
|
50
|
+
} finally {
|
|
51
|
+
engine = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/app/index.ts
CHANGED
|
@@ -11,7 +11,10 @@ export * from './Mailable.js';
|
|
|
11
11
|
export * from './ErrorHandler.js';
|
|
12
12
|
export * from './Seeder.js';
|
|
13
13
|
export * from './Storage.js';
|
|
14
|
+
export * from './Upload.js';
|
|
14
15
|
export * from './maintenance.js';
|
|
15
16
|
export * from './adminModule.js';
|
|
16
17
|
export * from './authModule.js';
|
|
17
18
|
export * from './userModel.js';
|
|
19
|
+
export * from './userStore.js';
|
|
20
|
+
export * from './fusionEngine.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createConnection } from 'node:net';
|
|
2
2
|
import type { Router, NexusConfig } from '../index.js';
|
|
3
|
+
import { mongoEnabled, mongoUri } from '../config/dbAccess.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Preflight diagnostics — native Node connectivity/latency probes.
|
|
@@ -197,6 +198,12 @@ export interface PreflightOptions {
|
|
|
197
198
|
graphqlMounted?: boolean;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/** Port for a named app from nexus.config.ts `apps[]` (undefined = not declared). */
|
|
202
|
+
function appPort(config: NexusConfig, name: string): number | undefined {
|
|
203
|
+
const entry = config.apps?.find((a) => a.name === name);
|
|
204
|
+
return entry && entry.enabled !== false && Number.isFinite(entry.port) ? entry.port : undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
200
207
|
/** Register `POST /admin/preflight` (guarded by the caller's /admin middleware). */
|
|
201
208
|
export function registerPreflightRoutes(router: Router, config: NexusConfig, opts: PreflightOptions = {}): void {
|
|
202
209
|
router.post('/admin/preflight', async (ctx) => {
|
|
@@ -220,10 +227,12 @@ export function registerPreflightRoutes(router: Router, config: NexusConfig, opt
|
|
|
220
227
|
targets.push({ name: 'GraphQL', kind: 'http', url: graphqlUrl, timeout: 3000 });
|
|
221
228
|
}
|
|
222
229
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
// MongoDB as a TCP reachability probe (authenticated ping is out of scope).
|
|
231
|
+
// Skipped when mongodb is disabled (fusion-active backends).
|
|
232
|
+
if (mongoEnabled(config)) {
|
|
233
|
+
const mongo = endpointFromUrl(mongoUri(config), 27017);
|
|
234
|
+
targets.push({ name: 'MongoDB', kind: 'tcp', host: mongo.host, port: mongo.port, timeout: 2000 });
|
|
235
|
+
}
|
|
227
236
|
|
|
228
237
|
const redisUrl = config.redis?.url ?? '';
|
|
229
238
|
if (redisUrl) {
|
|
@@ -231,6 +240,34 @@ export function registerPreflightRoutes(router: Router, config: NexusConfig, opt
|
|
|
231
240
|
targets.push({ name: 'Redis', kind: 'tcp', host: redis.host, port: redis.port, timeout: 2000 });
|
|
232
241
|
}
|
|
233
242
|
|
|
243
|
+
// Enabled AI providers — probe reachability of each base URL's host:port.
|
|
244
|
+
for (const p of config.ai?.providers ?? []) {
|
|
245
|
+
if (!p.enabled || !p.baseUrl) continue;
|
|
246
|
+
const ep = endpointFromUrl(p.baseUrl, p.baseUrl.startsWith('https:') ? 443 : 80);
|
|
247
|
+
targets.push({ name: `AI · ${p.label}`, kind: 'tcp', host: ep.host, port: ep.port, timeout: 2500 });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// SMTP relay — only when actually configured for delivery.
|
|
251
|
+
const smtp = config.email?.smtp;
|
|
252
|
+
if (config.email?.provider === 'smtp' && smtp?.host && smtp.port) {
|
|
253
|
+
targets.push({ name: 'Email (SMTP)', kind: 'tcp', host: normalizeProbeHost(smtp.host), port: smtp.port, timeout: 2500 });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Frontend dev server — only when enabled in this stack.
|
|
257
|
+
if (config.frontend?.enabled) {
|
|
258
|
+
targets.push({ name: 'Frontend', kind: 'http', url: `http://${host}:${config.frontend.port}`, timeout: 2500 });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Fusion DB server + Python pyserver — declared in nexus.config.ts `apps[]`.
|
|
262
|
+
const fusionPort = appPort(config, 'fusion');
|
|
263
|
+
if (fusionPort) {
|
|
264
|
+
targets.push({ name: 'Fusion DB', kind: 'http', url: `http://127.0.0.1:${fusionPort}/health`, timeout: 2500 });
|
|
265
|
+
}
|
|
266
|
+
const pyPort = appPort(config, 'pyserver');
|
|
267
|
+
if (pyPort) {
|
|
268
|
+
targets.push({ name: 'PyServer', kind: 'http', url: `http://127.0.0.1:${pyPort}/health`, timeout: 2500 });
|
|
269
|
+
}
|
|
270
|
+
|
|
234
271
|
const runner = async (t: PreflightTarget): Promise<PreflightCheck> => {
|
|
235
272
|
if (t.kind === 'http') {
|
|
236
273
|
return httpProbe(t.url ?? '', t.timeout ?? 3000).then((c) => ({ ...c, name: t.name }));
|