bhooai-nexus 0.1.2
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/README.md +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createServer, IncomingMessage, Server as HttpServer, ServerResponse } from 'node:http';
|
|
2
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import type { Middleware, RequestContext } from './context.js';
|
|
6
|
+
import { createContext } from './context.js';
|
|
7
|
+
import type { Router } from './Router.js';
|
|
8
|
+
import { NexusError, toNexusError } from '../errors.js';
|
|
9
|
+
|
|
10
|
+
/** Generate a request id (overridable for tests/injection). */
|
|
11
|
+
export function newRequestId(): string {
|
|
12
|
+
return randomUUID();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ServerOptions {
|
|
16
|
+
router: Router;
|
|
17
|
+
/** Global middleware run before routing. */
|
|
18
|
+
middleware?: Middleware[];
|
|
19
|
+
/** Trust X-Forwarded-* headers (boolean or hop count). */
|
|
20
|
+
trustProxy?: boolean | number;
|
|
21
|
+
/** Maximum request body size in bytes. */
|
|
22
|
+
bodyLimit?: number;
|
|
23
|
+
/** HTTPS cert/key paths (enables TLS when both present). */
|
|
24
|
+
certFile?: string;
|
|
25
|
+
keyFile?: string;
|
|
26
|
+
/** Called for each error to produce a JSON error body. */
|
|
27
|
+
onError?: (err: NexusError, ctx: RequestContext) => unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Inbuilt HTTP server built on node:http (no express). Runs a middleware
|
|
32
|
+
* pipeline, dispatches to the router, propagates request/trace ids, and
|
|
33
|
+
* supports graceful shutdown.
|
|
34
|
+
*/
|
|
35
|
+
export class NexusServer {
|
|
36
|
+
private server: HttpServer;
|
|
37
|
+
private middleware: Middleware[];
|
|
38
|
+
private opts: ServerOptions;
|
|
39
|
+
|
|
40
|
+
constructor(opts: ServerOptions) {
|
|
41
|
+
this.opts = { trustProxy: false, bodyLimit: 1024 * 1024, ...opts };
|
|
42
|
+
this.middleware = opts.middleware ?? [];
|
|
43
|
+
const handler = (req: IncomingMessage, res: ServerResponse) => this.handle(req, res);
|
|
44
|
+
if (opts.certFile && opts.keyFile) {
|
|
45
|
+
this.server = createHttpsServer(
|
|
46
|
+
{ cert: readFileSync(opts.certFile), key: readFileSync(opts.keyFile) },
|
|
47
|
+
handler,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
this.server = createServer(handler);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
use(mw: Middleware): this {
|
|
55
|
+
this.middleware.push(mw);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
listen(port: number, host: string = '0.0.0.0'): Promise<void> {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
this.server.listen(port, host, () => resolve());
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get address() {
|
|
66
|
+
return this.server.address();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The underlying node:http(s) server (for attaching WebSocket upgrades, etc.). */
|
|
70
|
+
get httpServer(): HttpServer {
|
|
71
|
+
return this.server;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
close(): Promise<void> {
|
|
75
|
+
return new Promise((resolve) => this.server.close(() => resolve()));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
79
|
+
const requestId = (req.headers['x-request-id'] as string) ?? newRequestId();
|
|
80
|
+
res.setHeader('x-request-id', requestId);
|
|
81
|
+
const ctx = createContext(req, res, requestId);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await this.runPipeline(ctx);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.handleError(err, ctx);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async runPipeline(ctx: RequestContext): Promise<void> {
|
|
91
|
+
const stack = [...this.middleware, this.dispatchMiddleware()];
|
|
92
|
+
let i = 0;
|
|
93
|
+
const next = async () => {
|
|
94
|
+
const mw = stack[i++];
|
|
95
|
+
if (mw) await mw(ctx, next);
|
|
96
|
+
};
|
|
97
|
+
await next();
|
|
98
|
+
|
|
99
|
+
if (!ctx.res.writableEnded) {
|
|
100
|
+
ctx.json({ error: { code: 'NOT_FOUND', message: `No route for ${ctx.method} ${ctx.path}` } }, 404);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private dispatchMiddleware(): Middleware {
|
|
105
|
+
return async (ctx, next) => {
|
|
106
|
+
const match = this.opts.router.match(ctx.method, ctx.path);
|
|
107
|
+
if (!match) {
|
|
108
|
+
await next();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.params = match.params;
|
|
112
|
+
ctx.routePattern = match.pattern;
|
|
113
|
+
// Run route middleware then the handler.
|
|
114
|
+
let i = 0;
|
|
115
|
+
const routeStack = [...match.middleware, match.handler];
|
|
116
|
+
const routeNext = async () => {
|
|
117
|
+
const fn = routeStack[i++];
|
|
118
|
+
if (fn) await fn(ctx, routeNext);
|
|
119
|
+
};
|
|
120
|
+
await routeNext();
|
|
121
|
+
await next();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private handleError(err: unknown, ctx: RequestContext): void {
|
|
126
|
+
if (ctx.res.writableEnded) return;
|
|
127
|
+
const ne = toNexusError(err);
|
|
128
|
+
const body = this.opts.onError ? this.opts.onError(ne, ctx) : defaultErrorBody(ne);
|
|
129
|
+
if (!ctx.res.headersSent) {
|
|
130
|
+
ctx.res.statusCode = ne.statusCode;
|
|
131
|
+
ctx.res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
132
|
+
}
|
|
133
|
+
ctx.res.end(JSON.stringify(body));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defaultErrorBody(err: NexusError): unknown {
|
|
138
|
+
return {
|
|
139
|
+
error: {
|
|
140
|
+
code: err.code,
|
|
141
|
+
message: err.message,
|
|
142
|
+
...(err.details ? { details: err.details } : {}),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Middleware } from './context.js';
|
|
2
|
+
import { NexusError } from '../errors.js';
|
|
3
|
+
|
|
4
|
+
/** Read the full request body into a Buffer, capped at `limit` bytes. */
|
|
5
|
+
export function readBody(req: import('node:http').IncomingMessage, limit: number): Promise<Buffer> {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const chunks: Buffer[] = [];
|
|
8
|
+
let size = 0;
|
|
9
|
+
req.on('data', (chunk: Buffer) => {
|
|
10
|
+
size += chunk.length;
|
|
11
|
+
if (size > limit) {
|
|
12
|
+
req.destroy();
|
|
13
|
+
reject(new NexusError('Request body too large', { code: 'BODY_TOO_LARGE', statusCode: 413 }));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
chunks.push(chunk);
|
|
17
|
+
});
|
|
18
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
19
|
+
req.on('error', reject);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Body parser middleware: parses JSON, urlencoded, and multipart/form-data
|
|
25
|
+
* (urlencoded + multipart use Node's querystring / a simple parser). For
|
|
26
|
+
* multipart file uploads, populates `ctx.state.files` with { field, filename,
|
|
27
|
+
* contentType, data } entries.
|
|
28
|
+
*/
|
|
29
|
+
export function bodyParser(limit = 1024 * 1024): Middleware {
|
|
30
|
+
return async (ctx, next) => {
|
|
31
|
+
const type = (ctx.headers['content-type'] ?? '').toString().split(';')[0]?.trim() ?? '';
|
|
32
|
+
if (ctx.method === 'GET' || ctx.method === 'HEAD' || !type) {
|
|
33
|
+
await next();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const buf = await readBody(ctx.req, limit);
|
|
37
|
+
// Preserve the raw body for webhook signature verification (the parsed form
|
|
38
|
+
// loses exact byte order/encoding that the gateway's signature was computed over).
|
|
39
|
+
ctx.state.__rawBody = buf;
|
|
40
|
+
try {
|
|
41
|
+
if (type === 'application/json') {
|
|
42
|
+
ctx.body = buf.length ? JSON.parse(buf.toString('utf8')) : undefined;
|
|
43
|
+
} else if (type === 'application/x-www-form-urlencoded') {
|
|
44
|
+
ctx.body = parseUrlEncoded(buf.toString('utf8'));
|
|
45
|
+
} else if (type === 'text/plain') {
|
|
46
|
+
ctx.body = buf.toString('utf8');
|
|
47
|
+
} else if (type === 'multipart/form-data') {
|
|
48
|
+
const { fields, files } = parseMultipart(buf, getBoundary(ctx.headers['content-type']?.toString() ?? ''));
|
|
49
|
+
ctx.body = fields;
|
|
50
|
+
ctx.state.files = files;
|
|
51
|
+
} else {
|
|
52
|
+
ctx.body = buf;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
throw new NexusError('Invalid request body', { code: 'INVALID_BODY', statusCode: 400 });
|
|
56
|
+
}
|
|
57
|
+
await next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getBoundary(contentType: string): string | undefined {
|
|
62
|
+
const match = /boundary=("?)([^";]+)\1/.exec(contentType);
|
|
63
|
+
return match?.[2];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseUrlEncoded(input: string): Record<string, string | string[]> {
|
|
67
|
+
const out: Record<string, string | string[]> = {};
|
|
68
|
+
for (const pair of input.split('&')) {
|
|
69
|
+
if (!pair) continue;
|
|
70
|
+
const eq = pair.indexOf('=');
|
|
71
|
+
const key = decodeURIComponent(eq === -1 ? pair : pair.slice(0, eq)).replace(/\+/g, ' ');
|
|
72
|
+
const val = decodeURIComponent(eq === -1 ? '' : pair.slice(eq + 1)).replace(/\+/g, ' ');
|
|
73
|
+
const existing = out[key];
|
|
74
|
+
if (existing === undefined) out[key] = val;
|
|
75
|
+
else if (Array.isArray(existing)) existing.push(val);
|
|
76
|
+
else out[key] = [existing, val];
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UploadedFile {
|
|
82
|
+
field: string;
|
|
83
|
+
filename: string;
|
|
84
|
+
contentType: string;
|
|
85
|
+
data: Buffer;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Minimal multipart/form-data parser sufficient for file uploads. */
|
|
89
|
+
export function parseMultipart(buf: Buffer, boundary?: string): {
|
|
90
|
+
fields: Record<string, string>;
|
|
91
|
+
files: UploadedFile[];
|
|
92
|
+
} {
|
|
93
|
+
const fields: Record<string, string> = {};
|
|
94
|
+
const files: UploadedFile[] = [];
|
|
95
|
+
if (!boundary) return { fields, files };
|
|
96
|
+
const delim = Buffer.from(`--${boundary}`);
|
|
97
|
+
const parts = splitBuffer(buf, delim).slice(1); // drop preamble
|
|
98
|
+
for (const part of parts) {
|
|
99
|
+
if (part.length === 0 || part.toString('utf8').trim() === '--') continue;
|
|
100
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
101
|
+
if (headerEnd === -1) continue;
|
|
102
|
+
const headerStr = part.subarray(0, headerEnd).toString('utf8');
|
|
103
|
+
const bodyBuf = part.subarray(headerEnd + 4, part.length - 2); // strip trailing \r\n
|
|
104
|
+
const disposition = /Content-Disposition: form-data;[^\r\n]*/i.exec(headerStr)?.[0] ?? '';
|
|
105
|
+
const name = /name="([^"]+)"/.exec(disposition)?.[1];
|
|
106
|
+
const filename = /filename="([^"]*)"/.exec(disposition)?.[1];
|
|
107
|
+
const contentType = /Content-Type: ([^\r\n]+)/i.exec(headerStr)?.[1]?.trim() ?? 'text/plain';
|
|
108
|
+
if (!name) continue;
|
|
109
|
+
if (filename !== undefined) {
|
|
110
|
+
files.push({ field: name, filename, contentType, data: bodyBuf });
|
|
111
|
+
} else {
|
|
112
|
+
fields[name] = bodyBuf.toString('utf8');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { fields, files };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function splitBuffer(buf: Buffer, delim: Buffer): Buffer[] {
|
|
119
|
+
const out: Buffer[] = [];
|
|
120
|
+
let start = 0;
|
|
121
|
+
let idx = buf.indexOf(delim, start);
|
|
122
|
+
while (idx !== -1) {
|
|
123
|
+
out.push(buf.subarray(start, idx));
|
|
124
|
+
start = idx + delim.length;
|
|
125
|
+
idx = buf.indexOf(delim, start);
|
|
126
|
+
}
|
|
127
|
+
out.push(buf.subarray(start));
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
|
|
4
|
+
/** Route path parameters extracted from the URL. */
|
|
5
|
+
export type Params = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
/** Per-request mutable state shared across middleware. */
|
|
8
|
+
export type State = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export interface RequestContext {
|
|
11
|
+
/** The raw Node request. */
|
|
12
|
+
req: IncomingMessage;
|
|
13
|
+
/** The raw Node response. */
|
|
14
|
+
res: ServerResponse;
|
|
15
|
+
/** HTTP method, uppercased. */
|
|
16
|
+
method: string;
|
|
17
|
+
/** URL pathname (no query). */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Parsed query string. */
|
|
20
|
+
query: Record<string, string | string[]>;
|
|
21
|
+
/** Route params. */
|
|
22
|
+
params: Params;
|
|
23
|
+
/** Headers (lowercased keys). */
|
|
24
|
+
headers: Record<string, string | string[] | undefined>;
|
|
25
|
+
/** Parsed request body (set by body parser middleware). */
|
|
26
|
+
body: unknown;
|
|
27
|
+
/** Per-request state. */
|
|
28
|
+
state: State;
|
|
29
|
+
/** Request id (also in headers as x-request-id). */
|
|
30
|
+
requestId: string;
|
|
31
|
+
/** The matched route pattern, e.g. "/users/:id". */
|
|
32
|
+
routePattern?: string;
|
|
33
|
+
/** Send a JSON response. */
|
|
34
|
+
json(data: unknown, status?: number): void;
|
|
35
|
+
/** Send a text response. */
|
|
36
|
+
text(data: string, status?: number): void;
|
|
37
|
+
/** Send an HTML response. */
|
|
38
|
+
html(data: string, status?: number): void;
|
|
39
|
+
/** Send an empty/status-only response. */
|
|
40
|
+
status(status: number): void;
|
|
41
|
+
/** Redirect to a location. */
|
|
42
|
+
redirect(location: string, status?: number): void;
|
|
43
|
+
/** Set a response header. */
|
|
44
|
+
setHeader(name: string, value: string | string[]): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Next = () => Promise<void> | void;
|
|
48
|
+
|
|
49
|
+
export type Middleware = (ctx: RequestContext, next: Next) => Promise<void> | void;
|
|
50
|
+
|
|
51
|
+
export type Handler = (ctx: RequestContext) => void | Promise<void>;
|
|
52
|
+
|
|
53
|
+
/** Build a RequestContext from raw Node objects. */
|
|
54
|
+
export function createContext(
|
|
55
|
+
req: IncomingMessage,
|
|
56
|
+
res: ServerResponse,
|
|
57
|
+
requestId: string,
|
|
58
|
+
): RequestContext {
|
|
59
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
60
|
+
const query: Record<string, string | string[]> = {};
|
|
61
|
+
for (const [k, v] of url.searchParams.entries()) {
|
|
62
|
+
const existing = query[k];
|
|
63
|
+
if (existing === undefined) query[k] = v;
|
|
64
|
+
else if (Array.isArray(existing)) existing.push(v);
|
|
65
|
+
else query[k] = [existing, v];
|
|
66
|
+
}
|
|
67
|
+
const ctx: RequestContext = {
|
|
68
|
+
req,
|
|
69
|
+
res,
|
|
70
|
+
method: (req.method ?? 'GET').toUpperCase(),
|
|
71
|
+
path: url.pathname,
|
|
72
|
+
query,
|
|
73
|
+
params: {},
|
|
74
|
+
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
75
|
+
body: undefined,
|
|
76
|
+
state: {},
|
|
77
|
+
requestId,
|
|
78
|
+
json(data, status = 200) {
|
|
79
|
+
send(res, JSON.stringify(data), status, 'application/json; charset=utf-8');
|
|
80
|
+
},
|
|
81
|
+
text(data, status = 200) {
|
|
82
|
+
send(res, data, status, 'text/plain; charset=utf-8');
|
|
83
|
+
},
|
|
84
|
+
html(data, status = 200) {
|
|
85
|
+
send(res, data, status, 'text/html; charset=utf-8');
|
|
86
|
+
},
|
|
87
|
+
status(s) {
|
|
88
|
+
res.statusCode = s;
|
|
89
|
+
res.end();
|
|
90
|
+
},
|
|
91
|
+
redirect(location, status = 302) {
|
|
92
|
+
res.statusCode = status;
|
|
93
|
+
res.setHeader('location', location);
|
|
94
|
+
res.end();
|
|
95
|
+
},
|
|
96
|
+
setHeader(name, value) {
|
|
97
|
+
res.setHeader(name, value);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return ctx;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function send(res: ServerResponse, body: string, status: number, type: string): void {
|
|
104
|
+
res.statusCode = status;
|
|
105
|
+
res.setHeader('content-type', type);
|
|
106
|
+
res.end(body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** True if the socket is still writable (not closed). */
|
|
110
|
+
export function isAlive(socket: Socket | undefined): boolean {
|
|
111
|
+
return !!socket && !socket.destroyed && socket.writable;
|
|
112
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, join, normalize, sep } from 'node:path';
|
|
3
|
+
import type { Middleware } from './context.js';
|
|
4
|
+
import { NotFoundError } from '../errors.js';
|
|
5
|
+
|
|
6
|
+
const MIME: Record<string, string> = {
|
|
7
|
+
'.html': 'text/html; charset=utf-8',
|
|
8
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
9
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
10
|
+
'.css': 'text/css; charset=utf-8',
|
|
11
|
+
'.json': 'application/json; charset=utf-8',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.jpeg': 'image/jpeg',
|
|
15
|
+
'.gif': 'image/gif',
|
|
16
|
+
'.svg': 'image/svg+xml',
|
|
17
|
+
'.ico': 'image/x-icon',
|
|
18
|
+
'.woff': 'font/woff',
|
|
19
|
+
'.woff2': 'font/woff2',
|
|
20
|
+
'.ttf': 'font/ttf',
|
|
21
|
+
'.map': 'application/json',
|
|
22
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
23
|
+
'.webp': 'image/webp',
|
|
24
|
+
'.wasm': 'application/wasm',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Serve static files from `root`. Safe against path traversal. */
|
|
28
|
+
export function serveStatic(root: string, options: { index?: string; prefix?: string } = {}): Middleware {
|
|
29
|
+
const index = options.index ?? 'index.html';
|
|
30
|
+
const prefix = normalizePrefix(options.prefix);
|
|
31
|
+
return async (ctx, next) => {
|
|
32
|
+
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
|
|
33
|
+
await next();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (prefix && ctx.path !== prefix && !ctx.path.startsWith(`${prefix}/`)) {
|
|
37
|
+
await next();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const requestPath = prefix ? ctx.path.slice(prefix.length) || '/' : ctx.path;
|
|
41
|
+
const safe = normalize(requestPath).replace(/^(\.\.[/\\])+/, '');
|
|
42
|
+
let filePath = join(root, safe);
|
|
43
|
+
if (isOutside(root, filePath)) {
|
|
44
|
+
throw new NotFoundError();
|
|
45
|
+
}
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
await next();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const initialStat = statSync(filePath);
|
|
51
|
+
if (initialStat.isDirectory()) {
|
|
52
|
+
filePath = join(filePath, index);
|
|
53
|
+
if (!existsSync(filePath)) {
|
|
54
|
+
await next();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const stat = statSync(filePath);
|
|
59
|
+
const ext = extname(filePath).toLowerCase();
|
|
60
|
+
ctx.setHeader('content-type', MIME[ext] ?? 'application/octet-stream');
|
|
61
|
+
ctx.setHeader('content-length', String(stat.size));
|
|
62
|
+
ctx.setHeader('etag', `"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"`);
|
|
63
|
+
if (ctx.method === 'HEAD') {
|
|
64
|
+
ctx.status(200);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await new Promise<void>((resolveStream, rejectStream) => {
|
|
68
|
+
const stream = createReadStream(filePath);
|
|
69
|
+
stream.on('error', rejectStream);
|
|
70
|
+
stream.on('end', resolveStream);
|
|
71
|
+
stream.pipe(ctx.res);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizePrefix(prefix: string | undefined): string {
|
|
77
|
+
if (!prefix || prefix === '/') return '';
|
|
78
|
+
return `/${prefix.replace(/^\/+|\/+$/g, '')}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isOutside(root: string, target: string): boolean {
|
|
82
|
+
const rel = normalize(target).split(sep);
|
|
83
|
+
const base = normalize(root).split(sep);
|
|
84
|
+
return !normalize(target).startsWith(normalize(root) + sep) && normalize(target) !== normalize(root);
|
|
85
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, basename, join, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import type { Router } from './Router.js';
|
|
5
|
+
import type { Middleware, RequestContext } from './context.js';
|
|
6
|
+
import type { UploadedFile } from './bodyParser.js';
|
|
7
|
+
import { ValidationError } from '../errors.js';
|
|
8
|
+
|
|
9
|
+
export interface UploadRouteOptions {
|
|
10
|
+
/** Directory where uploaded files are persisted. */
|
|
11
|
+
directory: string;
|
|
12
|
+
/** POST endpoint that accepts multipart/form-data. */
|
|
13
|
+
path?: string;
|
|
14
|
+
/** Public URL prefix returned for saved files. */
|
|
15
|
+
publicPath?: string;
|
|
16
|
+
/** Maximum size of an individual file. */
|
|
17
|
+
maxFileSize?: number;
|
|
18
|
+
/** Maximum number of files accepted in one request. */
|
|
19
|
+
maxFiles?: number;
|
|
20
|
+
/** Empty means all MIME types are accepted. */
|
|
21
|
+
allowedTypes?: string[];
|
|
22
|
+
/** Optional route middleware, such as authentication. */
|
|
23
|
+
middleware?: Middleware[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SavedUpload {
|
|
27
|
+
field: string;
|
|
28
|
+
originalName: string;
|
|
29
|
+
filename: string;
|
|
30
|
+
contentType: string;
|
|
31
|
+
size: number;
|
|
32
|
+
url: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Register a safe multipart upload endpoint backed by a local directory. */
|
|
36
|
+
export function registerUploadRoutes(router: Router, options: UploadRouteOptions): void {
|
|
37
|
+
const path = options.path ?? '/uploads';
|
|
38
|
+
const publicPath = (options.publicPath ?? path).replace(/\/$/, '');
|
|
39
|
+
const maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
|
|
40
|
+
const maxFiles = options.maxFiles ?? 20;
|
|
41
|
+
const allowedTypes = new Set(options.allowedTypes ?? []);
|
|
42
|
+
const directory = resolve(options.directory);
|
|
43
|
+
|
|
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) => {
|
|
49
|
+
const files = (ctx.state.files as UploadedFile[] | undefined) ?? [];
|
|
50
|
+
if (files.length === 0) throw new ValidationError('At least one file is required');
|
|
51
|
+
if (files.length > maxFiles) throw new ValidationError(`A maximum of ${maxFiles} files may be uploaded`);
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (file.data.length > maxFileSize) {
|
|
55
|
+
throw new ValidationError(`File "${file.filename}" exceeds the ${maxFileSize}-byte limit`);
|
|
56
|
+
}
|
|
57
|
+
if (allowedTypes.size > 0 && !allowedTypes.has(file.contentType)) {
|
|
58
|
+
throw new ValidationError(`File type "${file.contentType}" is not allowed`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await mkdir(directory, { recursive: true });
|
|
63
|
+
const saved: SavedUpload[] = [];
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const extension = safeExtension(file.filename);
|
|
66
|
+
const filename = `${randomUUID()}${extension}`;
|
|
67
|
+
await writeFile(join(directory, filename), file.data, { flag: 'wx' });
|
|
68
|
+
saved.push({
|
|
69
|
+
field: file.field,
|
|
70
|
+
originalName: basename(file.filename),
|
|
71
|
+
filename,
|
|
72
|
+
contentType: file.contentType,
|
|
73
|
+
size: file.data.length,
|
|
74
|
+
url: `${publicPath}/${filename}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
ctx.json({ files: saved }, 201);
|
|
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);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeExtension(filename: string): string {
|
|
87
|
+
const extension = extname(basename(filename)).toLowerCase();
|
|
88
|
+
return /^\.[a-z0-9]{1,10}$/.test(extension) ? extension : '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Type guard useful to route handlers that consume parsed multipart state. */
|
|
92
|
+
export function uploadedFiles(ctx: RequestContext): UploadedFile[] {
|
|
93
|
+
return (ctx.state.files as UploadedFile[] | undefined) ?? [];
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-crypto",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^22.5.0",
|
|
15
|
+
"typescript": "^5.6.2",
|
|
16
|
+
"vitest": "^2.1.1"
|
|
17
|
+
}
|
|
18
|
+
}
|