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,105 @@
|
|
|
1
|
+
import type { Capability, PluginManifest } from './manifest.js';
|
|
2
|
+
|
|
3
|
+
/** Admin extension specs a plugin can register (consumed by the admin app). */
|
|
4
|
+
export interface AdminPageSpec {
|
|
5
|
+
/** Route path in the admin app, e.g. "/plugins/analytics". */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Nav label. */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Slot group this page belongs to (for nav grouping). */
|
|
10
|
+
group?: string;
|
|
11
|
+
/** Optional icon name. */
|
|
12
|
+
icon?: string;
|
|
13
|
+
/** Optional order within its group. */
|
|
14
|
+
order?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SlotSpec {
|
|
18
|
+
/** Named insertion point in the admin UI, e.g. "dashboard:top". */
|
|
19
|
+
slot: string;
|
|
20
|
+
/** Component/module the admin should render there. */
|
|
21
|
+
component: string;
|
|
22
|
+
/** Optional ordering. */
|
|
23
|
+
order?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A scheduler job descriptor. */
|
|
27
|
+
export interface ScheduledJob {
|
|
28
|
+
name: string;
|
|
29
|
+
cron: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Host bindings — the actual implementation the app provides for each registrar.
|
|
34
|
+
* The plugin context (trusted or sandboxed) delegates to these. Sandbox mode
|
|
35
|
+
* intercepts calls to enforce capability policy before reaching the bindings.
|
|
36
|
+
*
|
|
37
|
+
* `ctx` params are deliberately `any` (the host's RequestContext type lives in
|
|
38
|
+
* nexus-core; plugins should not import it directly — they receive a
|
|
39
|
+
* PluginRequest via the sandbox proxy).
|
|
40
|
+
*/
|
|
41
|
+
export interface HostBindings {
|
|
42
|
+
http: {
|
|
43
|
+
addRoute(method: string, path: string, handler: (ctx: any) => void | Promise<void>): void;
|
|
44
|
+
addMiddleware(mw: (ctx: any, next: () => Promise<void>) => Promise<void>, phase?: 'before-route' | 'after-route'): void;
|
|
45
|
+
};
|
|
46
|
+
graphql: {
|
|
47
|
+
addSubgraph(subgraph: { name: string; typeDefs: string; resolvers: unknown }): Promise<void>;
|
|
48
|
+
removeSubgraph(name: string): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
data: { registerModel(name: string, schema: unknown): unknown; getModel(name: string): unknown };
|
|
51
|
+
realtime: { join(room: string, connId: string): void; broadcast(room: string, msg: unknown): void };
|
|
52
|
+
scheduler: { schedule(name: string, cron: string, fn: () => void | Promise<void>): void; cancel(name: string): void };
|
|
53
|
+
events: { publish(topic: string, payload: unknown): void; subscribe(topic: string, handler: (payload: unknown) => void): () => void };
|
|
54
|
+
admin: { registerAdminPage(page: AdminPageSpec): void; registerSlot(slot: SlotSpec): void };
|
|
55
|
+
services: { register(name: string, svc: unknown): void; get(name: string): unknown };
|
|
56
|
+
config: { get(pluginName: string): unknown; set(pluginName: string, cfg: unknown): void };
|
|
57
|
+
logger: { child(meta: Record<string, unknown>): { info(m: string, meta?: unknown): void; warn(m: string, meta?: unknown): void; error(m: string, meta?: unknown): void } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The facade a plugin's lifecycle functions receive. */
|
|
61
|
+
export interface PluginContext {
|
|
62
|
+
readonly pluginName: string;
|
|
63
|
+
readonly capabilities: Capability[];
|
|
64
|
+
readonly config: unknown;
|
|
65
|
+
readonly logger: { info(m: string, meta?: unknown): void; warn(m: string, meta?: unknown): void; error(m: string, meta?: unknown): void };
|
|
66
|
+
http: {
|
|
67
|
+
addRoute(method: string, path: string, handler: (ctx: any) => void | Promise<void>): void;
|
|
68
|
+
addMiddleware(mw: (ctx: any, next: () => Promise<void>) => Promise<void>, phase?: 'before-route' | 'after-route'): void;
|
|
69
|
+
};
|
|
70
|
+
graphql: { addSubgraph(subgraph: { name: string; typeDefs: string; resolvers: unknown }): Promise<void> };
|
|
71
|
+
data: { registerModel(name: string, schema: unknown): unknown; getModel(name: string): unknown };
|
|
72
|
+
realtime: { join(room: string, connId: string): void; broadcast(room: string, msg: unknown): void };
|
|
73
|
+
scheduler: { schedule(name: string, cron: string, fn: () => void | Promise<void>): void; cancel(name: string): void };
|
|
74
|
+
events: { publish(topic: string, payload: unknown): void; subscribe(topic: string, handler: (payload: unknown) => void): () => void };
|
|
75
|
+
admin: { registerAdminPage(page: AdminPageSpec): void; registerSlot(slot: SlotSpec): void };
|
|
76
|
+
services: { register(name: string, svc: unknown): void; get(name: string): unknown };
|
|
77
|
+
/** Filesystem access (only if 'fs' capability granted; sandboxed: allowlisted paths). */
|
|
78
|
+
fs?: { readFile(path: string): Promise<Buffer | string>; writeFile(path: string, data: string | Buffer): Promise<void> };
|
|
79
|
+
/** Network access (only if 'net' capability granted; sandboxed: allowlisted hosts). */
|
|
80
|
+
net?: { fetch(url: string, init?: { method?: string; headers?: Record<string, string>; body?: string }): Promise<{ status: number; body: string }> };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Plugin module shape: a default export factory returning lifecycle handlers. */
|
|
84
|
+
export interface PluginModule {
|
|
85
|
+
install?(ctx: PluginContext): Promise<void> | void;
|
|
86
|
+
init?(ctx: PluginContext): Promise<void> | void;
|
|
87
|
+
start?(ctx: PluginContext): Promise<void> | void;
|
|
88
|
+
/** Called when the plugin's runtime config changes. */
|
|
89
|
+
onConfigChange?(ctx: PluginContext, newConfig: unknown): Promise<void> | void;
|
|
90
|
+
stop?(ctx: PluginContext): Promise<void> | void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A loaded plugin handle. */
|
|
94
|
+
export interface PluginHandle {
|
|
95
|
+
manifest: PluginManifest;
|
|
96
|
+
state: 'loaded' | 'installed' | 'initialized' | 'started' | 'stopped' | 'errored';
|
|
97
|
+
install(): Promise<void>;
|
|
98
|
+
init(): Promise<void>;
|
|
99
|
+
start(): Promise<void>;
|
|
100
|
+
stop(): Promise<void>;
|
|
101
|
+
/** Push a config change (re-validates + calls onConfigChange). */
|
|
102
|
+
updateConfig(cfg: unknown): Promise<void>;
|
|
103
|
+
/** Terminate the plugin (stop + for sandboxed, terminate the worker). */
|
|
104
|
+
unload(): Promise<void>;
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './manifest.js';
|
|
2
|
+
export * from './HookBus.js';
|
|
3
|
+
export * from './host.js';
|
|
4
|
+
export * from './runtime/TrustedPlugin.js';
|
|
5
|
+
export * from './sandbox/SandboxedPlugin.js';
|
|
6
|
+
export * from './sandbox/capabilityPolicy.js';
|
|
7
|
+
export * from './registry.js';
|
|
8
|
+
export * from './admin/adminExtensions.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Plugin manifest + capabilities. */
|
|
2
|
+
|
|
3
|
+
/** A capability grants access to a host registrar or resource. */
|
|
4
|
+
export type Capability =
|
|
5
|
+
| 'http' // register routes/middleware
|
|
6
|
+
| 'graphql' // add a subgraph
|
|
7
|
+
| 'data' // ODM models
|
|
8
|
+
| 'auth' // auth helpers
|
|
9
|
+
| 'realtime' // rooms/pubsub
|
|
10
|
+
| 'scheduler' // cron-style jobs
|
|
11
|
+
| 'events' // cross-plugin event bus
|
|
12
|
+
| 'admin' // admin pages/slots
|
|
13
|
+
| 'fs' // filesystem (allowlisted paths)
|
|
14
|
+
| 'net'; // network (allowlisted hosts)
|
|
15
|
+
|
|
16
|
+
export const ALL_CAPABILITIES: Capability[] = [
|
|
17
|
+
'http', 'graphql', 'data', 'auth', 'realtime', 'scheduler', 'events', 'admin', 'fs', 'net',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Lifecycle hooks a plugin can implement. */
|
|
21
|
+
export type LifecycleHook = 'install' | 'init' | 'start' | 'config:change' | 'stop';
|
|
22
|
+
export const LIFECYCLE_HOOKS: LifecycleHook[] = ['install', 'init', 'start', 'config:change', 'stop'];
|
|
23
|
+
|
|
24
|
+
export interface PluginManifest {
|
|
25
|
+
/** Unique plugin name (kebab-case). */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Semver version. */
|
|
28
|
+
version: string;
|
|
29
|
+
/** Entry module path relative to the plugin dir, e.g. "index.js". */
|
|
30
|
+
entry: string;
|
|
31
|
+
/** 'trusted' runs in-process with full Node API; 'sandboxed' runs in a worker_thread. */
|
|
32
|
+
mode: 'trusted' | 'sandboxed';
|
|
33
|
+
/** Capabilities this plugin requests. Sandbox mode gates every registrar by these. */
|
|
34
|
+
capabilities: Capability[];
|
|
35
|
+
/** Optional JSON schema for the plugin's config (validated before init). */
|
|
36
|
+
configSchema?: Record<string, unknown>;
|
|
37
|
+
/** Lifecycle hooks the plugin implements (for documentation/manifest validation). */
|
|
38
|
+
hooks?: LifecycleHook[];
|
|
39
|
+
/** Other plugin names that must start first (topological ordering). */
|
|
40
|
+
dependencies?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Per-plugin sandbox limits (sandboxed mode only). */
|
|
44
|
+
export interface SandboxLimits {
|
|
45
|
+
/** node:worker_threads resourceLimits. */
|
|
46
|
+
resourceLimits?: {
|
|
47
|
+
maxOldGenerationSizeMb?: number;
|
|
48
|
+
maxYoungGenerationSizeMb?: number;
|
|
49
|
+
codeRangeSizeMb?: number;
|
|
50
|
+
stackSizeMb?: number;
|
|
51
|
+
};
|
|
52
|
+
/** Heartbeat interval (ms); if a beat is missed within `heartbeatTimeoutMs` the worker is killed. */
|
|
53
|
+
heartbeatMs?: number;
|
|
54
|
+
heartbeatTimeoutMs?: number;
|
|
55
|
+
/** Filesystem paths the plugin may access (when 'fs' capability is granted). */
|
|
56
|
+
fsAllowPaths?: string[];
|
|
57
|
+
/** Hostnames the plugin may reach (when 'net' capability is granted). */
|
|
58
|
+
netAllowHosts?: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Validate a manifest's shape; throws on missing/invalid fields. */
|
|
62
|
+
export function validateManifest(m: PluginManifest): void {
|
|
63
|
+
if (!m.name || !/^[a-z0-9-]+$/.test(m.name)) throw new Error(`[nexus-plugins] invalid manifest name: ${m.name}`);
|
|
64
|
+
if (!m.version) throw new Error(`[nexus-plugins] manifest version required`);
|
|
65
|
+
if (m.mode !== 'trusted' && m.mode !== 'sandboxed') throw new Error(`[nexus-plugins] manifest mode must be trusted|sandboxed`);
|
|
66
|
+
if (!m.entry) throw new Error(`[nexus-plugins] manifest entry required`);
|
|
67
|
+
for (const c of m.capabilities ?? []) {
|
|
68
|
+
if (!ALL_CAPABILITIES.includes(c)) throw new Error(`[nexus-plugins] unknown capability: ${c}`);
|
|
69
|
+
}
|
|
70
|
+
for (const dep of m.dependencies ?? []) {
|
|
71
|
+
if (typeof dep !== 'string' || !dep.trim()) throw new Error(`[nexus-plugins] invalid dependency`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { PluginManifest } from './manifest.js';
|
|
4
|
+
import { validateManifest } from './manifest.js';
|
|
5
|
+
import type { PluginHandle, HostBindings } from './host.js';
|
|
6
|
+
import { TrustedPlugin } from './runtime/TrustedPlugin.js';
|
|
7
|
+
import { SandboxedPlugin } from './sandbox/SandboxedPlugin.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PluginHost: loads plugin manifests from a directory, constructs the right handle
|
|
11
|
+
* (trusted in-process vs sandboxed worker), orders them topologically by their
|
|
12
|
+
* declared `dependencies`, and drives the install → init → start → stop lifecycle
|
|
13
|
+
* in that order. Reload supports updating config and (for sandboxed) restarting.
|
|
14
|
+
*/
|
|
15
|
+
export class PluginHost {
|
|
16
|
+
private handles = new Map<string, PluginHandle>();
|
|
17
|
+
private readonly bindings: HostBindings;
|
|
18
|
+
private readonly pluginsDir: string;
|
|
19
|
+
private loadOrder: string[] = [];
|
|
20
|
+
|
|
21
|
+
constructor(bindings: HostBindings, pluginsDir: string) {
|
|
22
|
+
this.bindings = bindings;
|
|
23
|
+
this.pluginsDir = pluginsDir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Discover and load (but not start) all plugins. Returns the load order. */
|
|
27
|
+
async load(configByPlugin: Record<string, unknown> = {}, limitsByPlugin: Record<string, unknown> = {}): Promise<string[]> {
|
|
28
|
+
let entries: string[] = [];
|
|
29
|
+
try { entries = await readdir(this.pluginsDir); } catch { /* no plugins dir */ }
|
|
30
|
+
|
|
31
|
+
const manifests = new Map<string, { manifest: PluginManifest; dir: string }>();
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const dir = join(this.pluginsDir, entry);
|
|
34
|
+
let isDir = false;
|
|
35
|
+
try { isDir = (await stat(dir)).isDirectory(); } catch { continue; }
|
|
36
|
+
if (!isDir) continue;
|
|
37
|
+
let raw: string;
|
|
38
|
+
try { raw = await readFile(join(dir, 'plugin.json'), 'utf8'); } catch { continue; }
|
|
39
|
+
const manifest = JSON.parse(raw) as PluginManifest;
|
|
40
|
+
validateManifest(manifest);
|
|
41
|
+
manifests.set(manifest.name, { manifest, dir });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.loadOrder = this.topoSort(manifests);
|
|
45
|
+
|
|
46
|
+
for (const name of this.loadOrder) {
|
|
47
|
+
const { manifest, dir } = manifests.get(name)!;
|
|
48
|
+
const cfg = configByPlugin[name];
|
|
49
|
+
if (manifest.mode === 'trusted') {
|
|
50
|
+
this.handles.set(name, new TrustedPlugin(manifest, dir, this.bindings, cfg));
|
|
51
|
+
} else {
|
|
52
|
+
this.handles.set(name, new SandboxedPlugin(manifest, dir, this.bindings, cfg, (limitsByPlugin[name] ?? {}) as any));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return this.loadOrder;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Topologically sort manifest names by `dependencies`. Throws on cycles/missing deps. */
|
|
59
|
+
private topoSort(manifests: Map<string, { manifest: PluginManifest }>): string[] {
|
|
60
|
+
const visited = new Set<string>();
|
|
61
|
+
const visiting = new Set<string>();
|
|
62
|
+
const out: string[] = [];
|
|
63
|
+
const visit = (name: string) => {
|
|
64
|
+
if (visited.has(name)) return;
|
|
65
|
+
if (visiting.has(name)) throw new Error(`[nexus-plugins] dependency cycle at ${name}`);
|
|
66
|
+
const entry = manifests.get(name);
|
|
67
|
+
if (!entry) throw new Error(`[nexus-plugins] missing dependency target: ${name}`);
|
|
68
|
+
visiting.add(name);
|
|
69
|
+
for (const dep of entry.manifest.dependencies ?? []) visit(dep);
|
|
70
|
+
visiting.delete(name);
|
|
71
|
+
visited.add(name);
|
|
72
|
+
out.push(name);
|
|
73
|
+
};
|
|
74
|
+
for (const name of manifests.keys()) visit(name);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Run a lifecycle hook across all plugins in dependency order. */
|
|
79
|
+
async runLifecycle(hook: 'install' | 'init' | 'start' | 'stop'): Promise<void> {
|
|
80
|
+
const order = hook === 'stop' ? [...this.loadOrder].reverse() : this.loadOrder;
|
|
81
|
+
for (const name of order) {
|
|
82
|
+
const handle = this.handles.get(name)!;
|
|
83
|
+
try {
|
|
84
|
+
await handle[hook]();
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
handle.state = 'errored';
|
|
87
|
+
this.bindings.logger.child({ plugin: name }).error(`lifecycle ${hook} failed`, { error: String(err?.message ?? err) });
|
|
88
|
+
throw new Error(`[nexus-plugins] ${name} failed during ${hook}: ${err?.message ?? err}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async install(): Promise<void> { return this.runLifecycle('install'); }
|
|
94
|
+
async init(): Promise<void> { return this.runLifecycle('init'); }
|
|
95
|
+
async start(): Promise<void> { return this.runLifecycle('start'); }
|
|
96
|
+
|
|
97
|
+
async stop(): Promise<void> {
|
|
98
|
+
// Stop in reverse dependency order; never throw — best-effort shutdown.
|
|
99
|
+
const order = [...this.loadOrder].reverse();
|
|
100
|
+
for (const name of order) {
|
|
101
|
+
try { await this.handles.get(name)?.stop(); } catch { /* swallow */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Update a single plugin's config and notify it. */
|
|
106
|
+
async updateConfig(name: string, config: unknown): Promise<void> {
|
|
107
|
+
const handle = this.handles.get(name);
|
|
108
|
+
if (!handle) throw new Error(`[nexus-plugins] unknown plugin: ${name}`);
|
|
109
|
+
await handle.updateConfig(config);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async unload(name: string): Promise<void> {
|
|
113
|
+
const handle = this.handles.get(name);
|
|
114
|
+
if (!handle) return;
|
|
115
|
+
await handle.unload();
|
|
116
|
+
this.handles.delete(name);
|
|
117
|
+
this.loadOrder = this.loadOrder.filter((n) => n !== name);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get(name: string): PluginHandle | undefined { return this.handles.get(name); }
|
|
121
|
+
list(): PluginHandle[] { return this.loadOrder.map((n) => this.handles.get(n)!).filter(Boolean); }
|
|
122
|
+
order(): string[] { return [...this.loadOrder]; }
|
|
123
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import type { PluginManifest } from '../manifest.js';
|
|
3
|
+
import type { PluginContext, PluginModule, PluginHandle, HostBindings } from '../host.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trusted in-process plugin: the entry module is imported directly into the host
|
|
7
|
+
* and its lifecycle functions run with full Node API access. Every registrar on
|
|
8
|
+
* the context is checked against the manifest's capabilities (defense in depth —
|
|
9
|
+
* a trusted plugin could reach the host directly, but going through the context
|
|
10
|
+
* keeps the contract uniform with sandboxed plugins and lets the app audit usage).
|
|
11
|
+
*/
|
|
12
|
+
export class TrustedPlugin implements PluginHandle {
|
|
13
|
+
readonly manifest: PluginManifest;
|
|
14
|
+
state: PluginHandle['state'] = 'loaded';
|
|
15
|
+
private readonly bindings: HostBindings;
|
|
16
|
+
private readonly pluginDir: string;
|
|
17
|
+
private module: PluginModule | null = null;
|
|
18
|
+
private ctx: PluginContext | null = null;
|
|
19
|
+
private config: unknown;
|
|
20
|
+
|
|
21
|
+
constructor(manifest: PluginManifest, pluginDir: string, bindings: HostBindings, config: unknown) {
|
|
22
|
+
this.manifest = manifest;
|
|
23
|
+
this.bindings = bindings;
|
|
24
|
+
this.pluginDir = pluginDir;
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private requireCap(cap: string): void {
|
|
29
|
+
if (!this.manifest.capabilities.includes(cap as any)) {
|
|
30
|
+
throw new Error(`[nexus-plugins] ${this.manifest.name} lacks capability: ${cap}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private buildContext(): PluginContext {
|
|
35
|
+
const self = this;
|
|
36
|
+
const logger = this.bindings.logger.child({ plugin: this.manifest.name });
|
|
37
|
+
const has = (c: string) => this.manifest.capabilities.includes(c as any);
|
|
38
|
+
const ctx: PluginContext = {
|
|
39
|
+
pluginName: this.manifest.name,
|
|
40
|
+
capabilities: this.manifest.capabilities,
|
|
41
|
+
config: this.config,
|
|
42
|
+
logger,
|
|
43
|
+
http: {
|
|
44
|
+
addRoute: (method, path, handler) => { self.requireCap('http'); self.bindings.http.addRoute(method, path, handler); },
|
|
45
|
+
addMiddleware: (mw, phase) => { self.requireCap('http'); self.bindings.http.addMiddleware(mw, phase); },
|
|
46
|
+
},
|
|
47
|
+
graphql: { addSubgraph: async (sg) => { self.requireCap('graphql'); await self.bindings.graphql.addSubgraph(sg); } },
|
|
48
|
+
data: {
|
|
49
|
+
registerModel: (n, s) => { self.requireCap('data'); return self.bindings.data.registerModel(n, s); },
|
|
50
|
+
getModel: (n) => { self.requireCap('data'); return self.bindings.data.getModel(n); },
|
|
51
|
+
},
|
|
52
|
+
realtime: {
|
|
53
|
+
join: (r, c) => { self.requireCap('realtime'); self.bindings.realtime.join(r, c); },
|
|
54
|
+
broadcast: (r, m) => { self.requireCap('realtime'); self.bindings.realtime.broadcast(r, m); },
|
|
55
|
+
},
|
|
56
|
+
scheduler: {
|
|
57
|
+
schedule: (n, cron, fn) => { self.requireCap('scheduler'); self.bindings.scheduler.schedule(n, cron, fn); },
|
|
58
|
+
cancel: (n) => { self.requireCap('scheduler'); self.bindings.scheduler.cancel(n); },
|
|
59
|
+
},
|
|
60
|
+
events: {
|
|
61
|
+
publish: (t, p) => { self.requireCap('events'); self.bindings.events.publish(t, p); },
|
|
62
|
+
subscribe: (t, h) => { self.requireCap('events'); return self.bindings.events.subscribe(t, h); },
|
|
63
|
+
},
|
|
64
|
+
admin: {
|
|
65
|
+
registerAdminPage: (p) => { self.requireCap('admin'); self.bindings.admin.registerAdminPage(p); },
|
|
66
|
+
registerSlot: (s) => { self.requireCap('admin'); self.bindings.admin.registerSlot(s); },
|
|
67
|
+
},
|
|
68
|
+
services: {
|
|
69
|
+
register: (n, s) => { self.requireCap('events'); self.bindings.services.register(n, s); },
|
|
70
|
+
get: (n) => { self.requireCap('events'); return self.bindings.services.get(n); },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
if (has('fs')) {
|
|
74
|
+
ctx.fs = {
|
|
75
|
+
readFile: async (p) => { self.requireCap('fs'); return import('node:fs/promises').then((f) => f.readFile(p)); },
|
|
76
|
+
writeFile: async (p, d) => { self.requireCap('fs'); return import('node:fs/promises').then((f) => f.writeFile(p, d)); },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (has('net')) {
|
|
80
|
+
ctx.net = {
|
|
81
|
+
fetch: async (url, init) => { self.requireCap('net'); const r = await fetch(url, { method: init?.method, headers: init?.headers, body: init?.body }); return { status: r.status, body: await r.text() }; },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
this.ctx = ctx;
|
|
85
|
+
return ctx;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async loadModule(): Promise<PluginModule> {
|
|
89
|
+
if (this.module) return this.module;
|
|
90
|
+
const entryUrl = pathToFileURL(`${this.pluginDir}/${this.manifest.entry}`).href;
|
|
91
|
+
const mod = await import(entryUrl);
|
|
92
|
+
this.module = (mod.default ?? mod) as PluginModule;
|
|
93
|
+
return this.module;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async install(): Promise<void> {
|
|
97
|
+
const mod = await this.loadModule();
|
|
98
|
+
const ctx = this.buildContext();
|
|
99
|
+
if (mod.install) await mod.install(ctx);
|
|
100
|
+
this.state = 'installed';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async init(): Promise<void> {
|
|
104
|
+
const mod = this.module ?? (await this.loadModule());
|
|
105
|
+
const ctx = this.ctx ?? this.buildContext();
|
|
106
|
+
if (mod.init) await mod.init(ctx);
|
|
107
|
+
this.state = 'initialized';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async start(): Promise<void> {
|
|
111
|
+
const mod = this.module ?? (await this.loadModule());
|
|
112
|
+
const ctx = this.ctx ?? this.buildContext();
|
|
113
|
+
if (mod.start) await mod.start(ctx);
|
|
114
|
+
this.state = 'started';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async stop(): Promise<void> {
|
|
118
|
+
if (this.module?.stop && this.ctx) await this.module.stop(this.ctx);
|
|
119
|
+
this.state = 'stopped';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async updateConfig(cfg: unknown): Promise<void> {
|
|
123
|
+
this.config = cfg;
|
|
124
|
+
if (this.ctx) (this.ctx as { config: unknown }).config = cfg;
|
|
125
|
+
if (this.module?.onConfigChange && this.ctx) await this.module.onConfigChange(this.ctx, cfg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async unload(): Promise<void> {
|
|
129
|
+
await this.stop();
|
|
130
|
+
this.module = null;
|
|
131
|
+
this.ctx = null;
|
|
132
|
+
this.state = 'stopped';
|
|
133
|
+
}
|
|
134
|
+
}
|