@bhooai/nexus-core 0.1.0 → 0.1.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.
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-core",
3
- "version": "0.1.0",
4
- "publishConfig": { "access": "public" },
3
+ "version": "0.1.4",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
5
7
  "type": "module",
6
8
  "main": "./src/index.ts",
7
9
  "types": "./src/index.ts",
@@ -128,7 +128,7 @@ export const defaults: NexusConfig = {
128
128
  enabled: true,
129
129
  },
130
130
  admin: {
131
- port: 3001,
131
+ port: 3300,
132
132
  host: 'localhost',
133
133
  enabled: true,
134
134
  },
@@ -155,6 +155,7 @@ export const nexusConfigSchema = z.object({
155
155
  }),
156
156
  cluster: z.object({
157
157
  enabled: z.boolean(),
158
+ role: z.enum(['backend', 'files', 'database', 'ai']).optional(),
158
159
  failOpenSingleNode: z.boolean(),
159
160
  lbHost: z.string().min(1),
160
161
  lbPort: z.number().int().min(1).max(65535),
@@ -162,6 +163,11 @@ export const nexusConfigSchema = z.object({
162
163
  nodeAgentPort: z.number().int().min(1).max(65535),
163
164
  registryFile: z.string().min(1),
164
165
  token: z.string(),
166
+ pathPins: z.array(z.object({
167
+ prefix: z.string().min(1),
168
+ nodeId: z.string().min(1),
169
+ role: z.enum(['backend', 'files', 'database', 'ai']).optional(),
170
+ })).optional(),
165
171
  autoscale: z.object({
166
172
  enabled: z.boolean(),
167
173
  mode: z.enum(['auto', 'manual']),
@@ -295,6 +295,10 @@ export interface ClusterAutoscaleConfig {
295
295
  export interface ClusterConfig {
296
296
  /** Turn the cluster (registry + LB + autoscaler) on. */
297
297
  enabled: boolean;
298
+ /** When set, this server is a cluster node (slave) of this role — `nexus dev`
299
+ * auto-starts a node agent alongside the backend so a master can link it.
300
+ * Undefined/empty on the root/central server. */
301
+ role?: NodeRole;
298
302
  /** When false, the LB fails open to a single direct node (no scaling). */
299
303
  failOpenSingleNode: boolean;
300
304
  /** Bind address of the central cluster's public load balancer. */
@@ -309,6 +313,12 @@ export interface ClusterConfig {
309
313
  registryFile: string;
310
314
  /** Shared pairing secret minted at node setup; pasted into the central. */
311
315
  token: string;
316
+ /** Path-prefix -> node-id pins: requests whose URL starts with `prefix` are
317
+ * always routed to `nodeId` (fail-closed 503 when that node is down, instead
318
+ * of round-robining). Used to allocate upload paths to specific file nodes,
319
+ * e.g. `/upload/users` -> slave-1. Optional `role` guards that the pinned
320
+ * node must be of that role (defensive). */
321
+ pathPins?: Array<{ prefix: string; nodeId: string; role?: NodeRole }>;
312
322
  autoscale: ClusterAutoscaleConfig;
313
323
  }
314
324
 
@@ -97,7 +97,7 @@ export class NexusServer {
97
97
  await next();
98
98
 
99
99
  if (!ctx.res.writableEnded) {
100
- ctx.status(404);
100
+ ctx.json({ error: { code: 'NOT_FOUND', message: `No route for ${ctx.method} ${ctx.path}` } }, 404);
101
101
  }
102
102
  }
103
103
 
@@ -41,7 +41,11 @@ export function registerUploadRoutes(router: Router, options: UploadRouteOptions
41
41
  const allowedTypes = new Set(options.allowedTypes ?? []);
42
42
  const directory = resolve(options.directory);
43
43
 
44
- router.post(path, async (ctx) => {
44
+ // The upload handler accepts both the configured path (`/uploads`) and any
45
+ // sub-path (`/uploads/images`, `/uploads/files`, ...) so the cluster's
46
+ // upload-path allocation can route different prefixes to different file nodes
47
+ // while every node still serves the same multipart endpoint.
48
+ const handler = async (ctx: import('./index.js').RequestContext) => {
45
49
  const files = (ctx.state.files as UploadedFile[] | undefined) ?? [];
46
50
  if (files.length === 0) throw new ValidationError('At least one file is required');
47
51
  if (files.length > maxFiles) throw new ValidationError(`A maximum of ${maxFiles} files may be uploaded`);
@@ -71,7 +75,12 @@ export function registerUploadRoutes(router: Router, options: UploadRouteOptions
71
75
  });
72
76
  }
73
77
  ctx.json({ files: saved }, 201);
74
- }, options.middleware ?? []);
78
+ };
79
+ const mw = options.middleware ?? [];
80
+ router.post(path, handler, mw);
81
+ // Also accept sub-paths (/uploads/images, /uploads/files, ...) so upload-path
82
+ // allocation in the cluster LB can target specific file nodes by prefix.
83
+ router.post(`${path}/*`, handler, mw);
75
84
  }
76
85
 
77
86
  function safeExtension(filename: string): string {