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.
Files changed (219) hide show
  1. package/README.md +184 -0
  2. package/apps/admin/index.html +12 -0
  3. package/apps/admin/package.json +25 -0
  4. package/apps/admin/postcss.config.js +6 -0
  5. package/apps/admin/public/anime-girl.svg +84 -0
  6. package/apps/admin/public/bhooai-nexus-logo.png +0 -0
  7. package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
  8. package/apps/admin/public/favicon.ico +0 -0
  9. package/apps/admin/src/main.tsx +10 -0
  10. package/apps/admin/src/vite-env.d.ts +18 -0
  11. package/apps/admin/tailwind.config.js +10 -0
  12. package/apps/admin/tsconfig.json +17 -0
  13. package/apps/admin/vite.config.ts +68 -0
  14. package/bin/nexus.js +52 -0
  15. package/package.json +108 -0
  16. package/packages/nexus-admin/package.json +32 -0
  17. package/packages/nexus-admin/src/App.tsx +4878 -0
  18. package/packages/nexus-admin/src/alertCenter.tsx +150 -0
  19. package/packages/nexus-admin/src/api.ts +601 -0
  20. package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
  21. package/packages/nexus-admin/src/index.css +3635 -0
  22. package/packages/nexus-admin/src/vite-env.d.ts +19 -0
  23. package/packages/nexus-ads/package.json +20 -0
  24. package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
  25. package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
  26. package/packages/nexus-ads/src/fetchTransport.ts +10 -0
  27. package/packages/nexus-ads/src/index.ts +20 -0
  28. package/packages/nexus-ads/src/types.ts +41 -0
  29. package/packages/nexus-ai-client/package.json +18 -0
  30. package/packages/nexus-ai-client/src/client.ts +159 -0
  31. package/packages/nexus-ai-client/src/errors.ts +21 -0
  32. package/packages/nexus-ai-client/src/index.ts +4 -0
  33. package/packages/nexus-ai-client/src/sse.ts +53 -0
  34. package/packages/nexus-ai-client/src/types.ts +174 -0
  35. package/packages/nexus-auth/package.json +22 -0
  36. package/packages/nexus-auth/src/auth.ts +116 -0
  37. package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
  38. package/packages/nexus-auth/src/cors.ts +91 -0
  39. package/packages/nexus-auth/src/csrf.ts +158 -0
  40. package/packages/nexus-auth/src/headers.ts +55 -0
  41. package/packages/nexus-auth/src/index.ts +11 -0
  42. package/packages/nexus-auth/src/jwt.ts +65 -0
  43. package/packages/nexus-auth/src/middleware.ts +94 -0
  44. package/packages/nexus-auth/src/oauth.ts +199 -0
  45. package/packages/nexus-auth/src/password.ts +33 -0
  46. package/packages/nexus-auth/src/rateLimit.ts +59 -0
  47. package/packages/nexus-auth/src/rbac.ts +94 -0
  48. package/packages/nexus-auth/src/session.ts +91 -0
  49. package/packages/nexus-cache/package.json +21 -0
  50. package/packages/nexus-cache/src/Cache.ts +155 -0
  51. package/packages/nexus-cache/src/PubSub.ts +98 -0
  52. package/packages/nexus-cache/src/RateLimiter.ts +43 -0
  53. package/packages/nexus-cache/src/index.ts +9 -0
  54. package/packages/nexus-cache/src/types.ts +47 -0
  55. package/packages/nexus-cli/package.json +25 -0
  56. package/packages/nexus-cli/src/commands/cluster.ts +133 -0
  57. package/packages/nexus-cli/src/commands/dev.ts +200 -0
  58. package/packages/nexus-cli/src/commands/doctor.ts +199 -0
  59. package/packages/nexus-cli/src/commands/init.ts +1009 -0
  60. package/packages/nexus-cli/src/commands/node.ts +106 -0
  61. package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
  62. package/packages/nexus-cli/src/commands/sync.ts +116 -0
  63. package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
  64. package/packages/nexus-cli/src/config-sync.ts +384 -0
  65. package/packages/nexus-cli/src/dotenv.ts +39 -0
  66. package/packages/nexus-cli/src/index.ts +94 -0
  67. package/packages/nexus-cli/src/supervisor.ts +394 -0
  68. package/packages/nexus-cli/src/util.ts +132 -0
  69. package/packages/nexus-cli/src/wizard.ts +149 -0
  70. package/packages/nexus-cli/templates/Dockerfile +60 -0
  71. package/packages/nexus-cli/templates/README.md +69 -0
  72. package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
  73. package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
  74. package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
  75. package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
  76. package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
  77. package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
  78. package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
  79. package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
  80. package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
  81. package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
  82. package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
  83. package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
  84. package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
  85. package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
  86. package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
  87. package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
  88. package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
  89. package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
  90. package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
  91. package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
  92. package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
  93. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
  94. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
  95. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
  96. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
  97. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
  98. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
  99. package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
  100. package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
  101. package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
  102. package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
  103. package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
  104. package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
  105. package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
  106. package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
  107. package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
  108. package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
  109. package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
  110. package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
  111. package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
  112. package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
  113. package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
  114. package/packages/nexus-cli/templates/bin/nexus.js +35 -0
  115. package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
  116. package/packages/nexus-cli/templates/dockerignore +15 -0
  117. package/packages/nexus-cli/templates/gitignore +12 -0
  118. package/packages/nexus-cli/templates/nexus.config.ts +69 -0
  119. package/packages/nexus-cli/templates/package.json +47 -0
  120. package/packages/nexus-cli/templates/tsconfig.json +17 -0
  121. package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
  122. package/packages/nexus-core/package.json +38 -0
  123. package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
  124. package/packages/nexus-core/src/config/defaults.ts +155 -0
  125. package/packages/nexus-core/src/config/env.ts +113 -0
  126. package/packages/nexus-core/src/config/index.ts +6 -0
  127. package/packages/nexus-core/src/config/merge.ts +28 -0
  128. package/packages/nexus-core/src/config/schema.ts +184 -0
  129. package/packages/nexus-core/src/config/types.ts +352 -0
  130. package/packages/nexus-core/src/di/Container.ts +98 -0
  131. package/packages/nexus-core/src/di/index.ts +1 -0
  132. package/packages/nexus-core/src/errors.ts +62 -0
  133. package/packages/nexus-core/src/http/Router.ts +149 -0
  134. package/packages/nexus-core/src/http/Server.ts +145 -0
  135. package/packages/nexus-core/src/http/bodyParser.ts +129 -0
  136. package/packages/nexus-core/src/http/context.ts +112 -0
  137. package/packages/nexus-core/src/http/index.ts +6 -0
  138. package/packages/nexus-core/src/http/static.ts +85 -0
  139. package/packages/nexus-core/src/http/uploads.ts +94 -0
  140. package/packages/nexus-core/src/index.ts +4 -0
  141. package/packages/nexus-crypto/package.json +18 -0
  142. package/packages/nexus-crypto/src/csr.ts +165 -0
  143. package/packages/nexus-crypto/src/der.ts +262 -0
  144. package/packages/nexus-crypto/src/https.ts +36 -0
  145. package/packages/nexus-crypto/src/index.ts +5 -0
  146. package/packages/nexus-crypto/src/keypair.ts +59 -0
  147. package/packages/nexus-crypto/src/x509.ts +156 -0
  148. package/packages/nexus-data/package.json +27 -0
  149. package/packages/nexus-data/src/connection/Connection.ts +57 -0
  150. package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
  151. package/packages/nexus-data/src/errors.ts +17 -0
  152. package/packages/nexus-data/src/index.ts +66 -0
  153. package/packages/nexus-data/src/model/Document.ts +126 -0
  154. package/packages/nexus-data/src/model/Model.ts +189 -0
  155. package/packages/nexus-data/src/model/hydrate.ts +12 -0
  156. package/packages/nexus-data/src/populate/populate.ts +114 -0
  157. package/packages/nexus-data/src/projects.ts +185 -0
  158. package/packages/nexus-data/src/query/Query.ts +158 -0
  159. package/packages/nexus-data/src/schema/Schema.ts +173 -0
  160. package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
  161. package/packages/nexus-data/src/schema/validators.ts +88 -0
  162. package/packages/nexus-email/package.json +22 -0
  163. package/packages/nexus-email/src/EmailService.ts +76 -0
  164. package/packages/nexus-email/src/TemplateEngine.ts +62 -0
  165. package/packages/nexus-email/src/index.ts +37 -0
  166. package/packages/nexus-email/src/providers.ts +63 -0
  167. package/packages/nexus-email/src/queue.ts +103 -0
  168. package/packages/nexus-email/src/types.ts +49 -0
  169. package/packages/nexus-graphql/package.json +24 -0
  170. package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
  171. package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
  172. package/packages/nexus-graphql/src/federation/executor.ts +305 -0
  173. package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
  174. package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
  175. package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
  176. package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
  177. package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
  178. package/packages/nexus-graphql/src/index.ts +14 -0
  179. package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
  180. package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
  181. package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
  182. package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
  183. package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
  184. package/packages/nexus-graphql/src/types.ts +77 -0
  185. package/packages/nexus-payments/package.json +20 -0
  186. package/packages/nexus-payments/src/errors.ts +15 -0
  187. package/packages/nexus-payments/src/http.ts +53 -0
  188. package/packages/nexus-payments/src/index.ts +106 -0
  189. package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
  190. package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
  191. package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
  192. package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
  193. package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
  194. package/packages/nexus-payments/src/signature.ts +24 -0
  195. package/packages/nexus-payments/src/types.ts +150 -0
  196. package/packages/nexus-payments/src/webhooks.ts +58 -0
  197. package/packages/nexus-plugins/package.json +20 -0
  198. package/packages/nexus-plugins/src/HookBus.ts +42 -0
  199. package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
  200. package/packages/nexus-plugins/src/host.ts +105 -0
  201. package/packages/nexus-plugins/src/index.ts +8 -0
  202. package/packages/nexus-plugins/src/manifest.ts +73 -0
  203. package/packages/nexus-plugins/src/registry.ts +123 -0
  204. package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
  205. package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
  206. package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
  207. package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
  208. package/packages/nexus-realtime/package.json +26 -0
  209. package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
  210. package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
  211. package/packages/nexus-realtime/src/index.ts +5 -0
  212. package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
  213. package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
  214. package/packages/nexus-realtime/src/types.ts +52 -0
  215. package/packages/nexus-telemetry/package.json +20 -0
  216. package/packages/nexus-telemetry/src/index.ts +3 -0
  217. package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
  218. package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
  219. package/packages/nexus-telemetry/src/trace.ts +28 -0
@@ -0,0 +1,155 @@
1
+ import { createClient, type RedisClientType } from 'redis';
2
+ import type { CacheOptions } from './types.js';
3
+
4
+ /**
5
+ * Redis-backed cache wrapper. Lazily creates and connects a `node-redis`
6
+ * client on first use. All keys pass through {@link Cache.key} to apply an
7
+ * optional namespace/prefix.
8
+ */
9
+ export class Cache {
10
+ private client: RedisClientType;
11
+ private connectPromise: Promise<void> | null = null;
12
+ private readonly keyPrefix: string;
13
+ protected readonly url?: string;
14
+ protected readonly connectTimeout?: number;
15
+
16
+ constructor(opts: CacheOptions = {}) {
17
+ this.url = opts.url;
18
+ this.connectTimeout = opts.connectTimeout;
19
+ this.keyPrefix = opts.keyPrefix ?? (opts.namespace ? `${opts.namespace}:` : '');
20
+ // `redis` v4 createClient returns a client in a closed state; we connect
21
+ // lazily on first use via ensureConnected().
22
+ this.client = createClient({
23
+ ...(this.url ? { url: this.url } : {}),
24
+ ...(this.connectTimeout != null ? { socket: { connectTimeout: this.connectTimeout } } : {}),
25
+ });
26
+ }
27
+
28
+ /** Connect the underlying client if not already connected. Idempotent. */
29
+ async ensureConnected(): Promise<void> {
30
+ if (this.client.isOpen) return;
31
+ if (!this.connectPromise) {
32
+ this.connectPromise = this.client.connect().then(() => {
33
+ // connected
34
+ });
35
+ }
36
+ try {
37
+ await this.connectPromise;
38
+ } catch (err) {
39
+ // Reset so a later retry can attempt reconnection.
40
+ this.connectPromise = null;
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ /** Build the fully-prefixed redis key for a logical name. */
46
+ key(name: string): string {
47
+ return `${this.keyPrefix}${name}`;
48
+ }
49
+
50
+ /** The underlying redis client (connected after `ensureConnected`). */
51
+ protected getClient(): RedisClientType {
52
+ return this.client;
53
+ }
54
+
55
+ /** Get a JSON-decoded value, or undefined on miss/null. */
56
+ async get<T>(name: string): Promise<T | undefined> {
57
+ await this.ensureConnected();
58
+ const raw = await this.client.get(this.key(name));
59
+ if (raw == null) return undefined;
60
+ return JSON.parse(raw) as T;
61
+ }
62
+
63
+ /** Set a JSON-encoded value, optionally with a TTL in seconds. */
64
+ async set<T>(name: string, value: T, ttlSeconds?: number): Promise<void> {
65
+ await this.ensureConnected();
66
+ const encoded = JSON.stringify(value);
67
+ if (ttlSeconds != null) {
68
+ await this.client.set(this.key(name), encoded, { EX: ttlSeconds });
69
+ } else {
70
+ await this.client.set(this.key(name), encoded);
71
+ }
72
+ }
73
+
74
+ /** Delete a key. */
75
+ async del(name: string): Promise<void> {
76
+ await this.ensureConnected();
77
+ await this.client.del(this.key(name));
78
+ }
79
+
80
+ /**
81
+ * INCR a key. When `ttlSeconds` is supplied and the result becomes 1 (first
82
+ * hit in a fresh window) the key is expired after `ttlSeconds` seconds.
83
+ * Returns the post-increment integer value.
84
+ */
85
+ async incr(name: string, ttlSeconds?: number): Promise<number> {
86
+ await this.ensureConnected();
87
+ const key = this.key(name);
88
+ const count = await this.client.incr(key);
89
+ if (ttlSeconds != null && count === 1) {
90
+ await this.client.expire(key, ttlSeconds);
91
+ }
92
+ return count;
93
+ }
94
+
95
+ /** Whether a key exists. */
96
+ async exists(name: string): Promise<boolean> {
97
+ await this.ensureConnected();
98
+ const n = await this.client.exists(this.key(name));
99
+ return n > 0;
100
+ }
101
+
102
+ /** Set an expiry (seconds) on an existing key. */
103
+ async expire(name: string, ttlSeconds: number): Promise<void> {
104
+ await this.ensureConnected();
105
+ await this.client.expire(this.key(name), ttlSeconds);
106
+ }
107
+
108
+ /** Remaining TTL in seconds (-1 no expiry, -2 missing key). */
109
+ async ttl(name: string): Promise<number> {
110
+ await this.ensureConnected();
111
+ return this.client.ttl(this.key(name));
112
+ }
113
+
114
+ /**
115
+ * Delete all keys matching a glob pattern using non-blocking SCAN + DEL.
116
+ * Returns the number of keys removed.
117
+ */
118
+ async flushPattern(pattern: string): Promise<number> {
119
+ await this.ensureConnected();
120
+ const fullPattern = `${this.keyPrefix}${pattern}`;
121
+ let cursor = 0;
122
+ let removed = 0;
123
+ do {
124
+ const reply = await this.client.scan(cursor, { MATCH: fullPattern, COUNT: 100 });
125
+ cursor = Number(reply.cursor);
126
+ const keys = reply.keys;
127
+ if (keys.length > 0) {
128
+ await this.client.del(keys);
129
+ removed += keys.length;
130
+ }
131
+ } while (cursor !== 0);
132
+ return removed;
133
+ }
134
+
135
+ /**
136
+ * Cache-aside helper: return the cached value if present, otherwise invoke
137
+ * `loader`, store its result with `ttlSeconds`, and return it. No negative
138
+ * caching in v1 — loader errors propagate to the caller.
139
+ */
140
+ async cacheAside<T>(name: string, ttlSeconds: number, loader: () => Promise<T>): Promise<T> {
141
+ const cached = await this.get<T>(name);
142
+ if (cached !== undefined) return cached;
143
+ const fresh = await loader();
144
+ await this.set(name, fresh, ttlSeconds);
145
+ return fresh;
146
+ }
147
+
148
+ /** Disconnect the underlying client. */
149
+ async close(): Promise<void> {
150
+ if (this.client.isOpen) {
151
+ await this.client.quit();
152
+ }
153
+ this.connectPromise = null;
154
+ }
155
+ }
@@ -0,0 +1,98 @@
1
+ import { createClient, type RedisClientType } from 'redis';
2
+ import type { Cache } from './Cache.js';
3
+
4
+ type Handler = (msg: unknown) => void;
5
+
6
+ /**
7
+ * Redis pub/sub helper. Uses the {@link Cache} client for publishing and a
8
+ * separate, dedicated subscriber client (required by `redis` v4) for
9
+ * subscriptions. Both clients are connected lazily.
10
+ */
11
+ export class PubSub {
12
+ private subscriber: RedisClientType;
13
+ private subscriberConnectPromise: Promise<void> | null = null;
14
+ private handlers = new Map<string, Set<Handler>>();
15
+
16
+ constructor(private cache: Cache) {
17
+ // Reuse the same URL/connection options as the cache's publisher client.
18
+ // The cache exposes its url/connectTimeout via protected fields; here we
19
+ // reconstruct from the cache's key prefix-agnostic config by reading the
20
+ // same options the cache used. We mirror the cache's createClient call.
21
+ const url = (cache as unknown as { url?: string }).url;
22
+ const connectTimeout = (cache as unknown as { connectTimeout?: number }).connectTimeout;
23
+ this.subscriber = createClient({
24
+ ...(url ? { url } : {}),
25
+ ...(connectTimeout != null ? { socket: { connectTimeout } } : {}),
26
+ });
27
+ }
28
+
29
+ private async ensureSubscriberConnected(): Promise<void> {
30
+ if (this.subscriber.isOpen) return;
31
+ if (!this.subscriberConnectPromise) {
32
+ this.subscriberConnectPromise = this.subscriber.connect().then(() => undefined);
33
+ }
34
+ try {
35
+ await this.subscriberConnectPromise;
36
+ } catch (err) {
37
+ this.subscriberConnectPromise = null;
38
+ throw err;
39
+ }
40
+ }
41
+
42
+ /** Publish a JSON-encoded message to `channel`. */
43
+ async publish(channel: string, message: unknown): Promise<void> {
44
+ // Ensure the cache (publisher) client is connected.
45
+ await this.cache.ensureConnected();
46
+ const publisher = (this.cache as unknown as { getClient: () => RedisClientType }).getClient();
47
+ await publisher.publish(channel, JSON.stringify(message));
48
+ }
49
+
50
+ /**
51
+ * Subscribe to `channel`; `handler` is invoked with the JSON-parsed payload.
52
+ * Returns an unsubscribe function that removes the handler and, when no
53
+ * handlers remain for the channel, unsubscribes from Redis.
54
+ */
55
+ async subscribe(channel: string, handler: Handler): Promise<() => Promise<void>> {
56
+ await this.ensureSubscriberConnected();
57
+ let set = this.handlers.get(channel);
58
+ if (!set) {
59
+ set = new Set();
60
+ this.handlers.set(channel, set);
61
+ // `redis` v4 subscribe(channel, callback) registers a per-channel
62
+ // listener that receives the message string.
63
+ await this.subscriber.subscribe(channel, (message) => {
64
+ const handlers = this.handlers.get(channel);
65
+ if (!handlers) return;
66
+ let parsed: unknown;
67
+ try {
68
+ parsed = JSON.parse(message);
69
+ } catch {
70
+ parsed = message;
71
+ }
72
+ for (const h of handlers) h(parsed);
73
+ });
74
+ }
75
+ set.add(handler);
76
+ return async () => {
77
+ const current = this.handlers.get(channel);
78
+ if (!current) return;
79
+ current.delete(handler);
80
+ if (current.size === 0) {
81
+ this.handlers.delete(channel);
82
+ try {
83
+ await this.subscriber.unsubscribe(channel);
84
+ } catch {
85
+ /* ignore — client may be closing */
86
+ }
87
+ }
88
+ };
89
+ }
90
+
91
+ /** Close the dedicated subscriber client. */
92
+ async close(): Promise<void> {
93
+ if (this.subscriber.isOpen) {
94
+ await this.subscriber.quit();
95
+ }
96
+ this.subscriberConnectPromise = null;
97
+ }
98
+ }
@@ -0,0 +1,43 @@
1
+ import type { Cache } from './Cache.js';
2
+ import type { RateLimitResult, RateLimitRule } from './types.js';
3
+
4
+ /**
5
+ * Fixed-window counter rate limiter backed by Redis INCR via a {@link Cache}.
6
+ * Each call to {@link RateLimiter.check} atomically increments a per-key
7
+ * counter and, on the first hit of a window, sets the window expiry.
8
+ */
9
+ export class RateLimiter {
10
+ constructor(private cache: Cache) {}
11
+
12
+ /**
13
+ * Check (and consume) one unit against the rule for `key`. Redis INCR both
14
+ * increments and returns the count, so the counter is consumed regardless of
15
+ * whether the request is allowed — matching the in-memory store semantics.
16
+ */
17
+ async check(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
18
+ const redisKey = `${key}:rate`;
19
+ const count = await this.cache.incr(redisKey, rule.windowSeconds);
20
+ const limit = rule.max;
21
+ const allowed = count <= limit;
22
+ const remaining = Math.max(0, limit - count);
23
+ // Use the live TTL to compute resetAt; falls back to now + window.
24
+ let ttl = await this.cache.ttl(redisKey);
25
+ if (ttl < 0) ttl = rule.windowSeconds;
26
+ const resetAt = Date.now() + ttl * 1000;
27
+ const result: RateLimitResult = {
28
+ allowed,
29
+ limit,
30
+ remaining,
31
+ resetAt,
32
+ };
33
+ if (!allowed) {
34
+ result.retryAfter = rule.windowSeconds;
35
+ }
36
+ return result;
37
+ }
38
+
39
+ /** Alias of {@link check}; the counter is already consumed by INCR. */
40
+ async consume(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
41
+ return this.check(key, rule);
42
+ }
43
+ }
@@ -0,0 +1,9 @@
1
+ export { Cache } from './Cache.js';
2
+ export { RateLimiter } from './RateLimiter.js';
3
+ export { PubSub } from './PubSub.js';
4
+ export type {
5
+ CacheOptions,
6
+ CacheEntry,
7
+ RateLimitResult,
8
+ RateLimitRule,
9
+ } from './types.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Options for constructing a {@link Cache} instance.
3
+ */
4
+ export interface CacheOptions {
5
+ /** Redis connection URL (e.g. `redis://localhost:6379`). */
6
+ url?: string;
7
+ /** Logical namespace applied to all keys as `${namespace}:` prefix. */
8
+ namespace?: string;
9
+ /** Explicit key prefix (overrides namespace prefix when provided). */
10
+ keyPrefix?: string;
11
+ /** Connection timeout in milliseconds passed to the redis client. */
12
+ connectTimeout?: number;
13
+ }
14
+
15
+ /**
16
+ * A cached value entry (kept for symmetry; values are JSON-stringified as-is).
17
+ */
18
+ export interface CacheEntry<T> {
19
+ value: T;
20
+ expiresAt?: number;
21
+ }
22
+
23
+ /**
24
+ * Result of a rate-limit check.
25
+ */
26
+ export interface RateLimitResult {
27
+ /** Whether the request is allowed under the current window. */
28
+ allowed: boolean;
29
+ /** Maximum requests permitted in the window. */
30
+ limit: number;
31
+ /** Remaining requests in the current window (>= 0). */
32
+ remaining: number;
33
+ /** Epoch ms when the current window resets. */
34
+ resetAt: number;
35
+ /** Seconds until the next request would be allowed (present when denied). */
36
+ retryAfter?: number;
37
+ }
38
+
39
+ /**
40
+ * Fixed-window rate-limit rule.
41
+ */
42
+ export interface RateLimitRule {
43
+ /** Window length in seconds. */
44
+ windowSeconds: number;
45
+ /** Maximum requests permitted per window. */
46
+ max: number;
47
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@bhooai/nexus-cli",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "bin": {
9
+ "nexus": "./bin/nexus.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "test": "vitest run"
14
+ },
15
+ "dependencies": {
16
+ "@bhooai/nexus-cluster": "^0.1.0",
17
+ "@bhooai/nexus-core": "^0.1.0",
18
+ "@bhooai/nexus-data": "^0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.5.0",
22
+ "typescript": "^5.6.2",
23
+ "vitest": "^2.1.1"
24
+ }
25
+ }
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { loadConfigAuto } from '../../../nexus-core/src/index.js';
4
+ import { ClusterManager } from '@bhooai/nexus-cluster';
5
+ import type { DeepPartial, NexusConfig } from '@bhooai/nexus-core';
6
+
7
+ const GREEN = '\x1b[32m';
8
+ const RED = '\x1b[31m';
9
+ const YELLOW = '\x1b[33m';
10
+ const CYAN = '\x1b[36m';
11
+ const DIM = '\x1b[2m';
12
+ const RESET = '\x1b[0m';
13
+
14
+ /**
15
+ * `nexus cluster <subcommand>` - central-side mesh orchestration.
16
+ *
17
+ * cluster status list linked nodes + LB RPS
18
+ * cluster link <url> handshake + register a node by its agent URL
19
+ * cluster unlink <id> forget a node
20
+ * cluster start|stop|restart|kill <id>
21
+ * cluster scale <n> target exactly n backend nodes
22
+ * cluster serve hold process: LB + autoscaler (foreground)
23
+ * cluster check poll every node once (health + metrics)
24
+ */
25
+ export async function cluster(args: string[]): Promise<number> {
26
+ const cfg = await loadConfigAuto({ root: process.cwd() });
27
+ const [sub, a, b] = args;
28
+
29
+ // `serve` is the explicit "start the cluster" command - if a prior admin
30
+ // stop wrote {cluster:{enabled:false}} into nexus.runtime.json, that
31
+ // override wins over nexus.config.ts and would silently keep the cluster
32
+ // off. Re-enable it in runtime.json so the LB actually starts.
33
+ if (sub === 'serve' && !cfg.cluster.enabled) {
34
+ enableClusterInRuntime(process.cwd());
35
+ console.log(`${YELLOW}cluster was disabled in nexus.runtime.json - re-enabling${RESET}`);
36
+ }
37
+
38
+ // For non-serve commands, warn (don't block) when the cluster is off.
39
+ if (sub !== 'serve' && !cfg.cluster.enabled) {
40
+ console.warn(`${RED}cluster.enabled is false${RESET} - run ${CYAN}nexus cluster serve${RESET} to start it, or set cluster: { enabled: true } in nexus.config.ts`);
41
+ }
42
+
43
+ const manager = new ClusterManager({ config: cfg.cluster, root: process.cwd(), aiServerUrl: cfg.ai.serverUrl });
44
+
45
+ switch (sub ?? 'help') {
46
+ case 'status': {
47
+ const nodes = manager.list();
48
+ const rps = manager.rps();
49
+ console.log(`${CYAN}cluster${RESET} - ${nodes.length} linked node(s)`);
50
+ for (const n of nodes) {
51
+ const color = n.status === 'ready' ? GREEN : n.status === 'unreachable' ? RED : CYAN;
52
+ console.log(` ${color}${n.status}${RESET} ${DIM}${n.identity.id}${RESET} role=${n.identity.role} tier=${n.identity.tier} enabled=${n.enabled} rps=${rps[n.identity.id] ?? 0}${n.lastMetrics ? ` cpu=${n.lastMetrics.cpu}% mem=${n.lastMetrics.memoryMb}MiB` : ''}`);
53
+ }
54
+ return 0;
55
+ }
56
+ case 'link': {
57
+ if (!a) { console.error('cluster link <agent-url>'); return 1; }
58
+ try {
59
+ const node = await manager.link(a);
60
+ const color = node.status === 'ready' ? GREEN : CYAN;
61
+ console.log(`${color}linked${RESET} ${node.identity.id} role=${node.identity.role} base=${node.identity.baseUrl}`);
62
+ return 0;
63
+ } catch (err) {
64
+ console.error(`${RED}link failed${RESET}: ${(err as Error).message}`);
65
+ return 1;
66
+ }
67
+ }
68
+ case 'unlink': {
69
+ if (!a) { console.error('cluster unlink <id>'); return 1; }
70
+ return manager.unlink(a) ? (console.log(`removed ${a}`), 0) : (console.error(`no such node: ${a}`), 1);
71
+ }
72
+ case 'start':
73
+ case 'stop':
74
+ case 'restart':
75
+ case 'kill': {
76
+ if (!a) { console.error(`cluster ${sub} <id>`); return 1; }
77
+ await manager.exec(a, sub === 'kill' ? 'stop' : sub);
78
+ console.log(`${sub} ${a}`);
79
+ return 0;
80
+ }
81
+ case 'scale': {
82
+ if (!a || !Number.isFinite(Number(a))) { console.error('cluster scale <n>'); return 1; }
83
+ try {
84
+ await manager.scaleTo(Number(a));
85
+ console.log(`scaled to ${a} backend node(s)`);
86
+ return 0;
87
+ } catch (err) {
88
+ console.error(`${RED}${(err as Error).message}${RESET}`);
89
+ return 1;
90
+ }
91
+ }
92
+ case 'check': {
93
+ await manager.pollAll();
94
+ console.log(`checked ${manager.list().length} node(s)`);
95
+ return 0;
96
+ }
97
+ case 'serve': {
98
+ console.log(`${CYAN}cluster${RESET} LB on ${cfg.cluster.lbHost}:${cfg.cluster.lbPort}${RESET}`);
99
+ await manager.listenLb(cfg.cluster.lbHost);
100
+ manager.autoscaler.start(10_000);
101
+ setInterval(() => void manager.pollAll().catch(() => {}), 15_000);
102
+ return 0;
103
+ }
104
+ case 'help':
105
+ default:
106
+ console.log(`Usage: nexus cluster <status|link|unlink|start|stop|restart|kill|scale|check|serve>`);
107
+ return 0;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Patch `nexus.runtime.json` to set `cluster.enabled = true`. The admin
113
+ * "stop cluster" endpoint writes `{cluster:{enabled:false}}` there, and that
114
+ * override beats `nexus.config.ts` in the loader precedence - so `nexus
115
+ * cluster serve` would read `enabled:false` and the LB would never bind.
116
+ * This removes the stale override so `serve` actually starts the cluster.
117
+ */
118
+ function enableClusterInRuntime(root: string): void {
119
+ const runtimePath = resolve(root, 'nexus.runtime.json');
120
+ let runtime: Record<string, unknown> = {};
121
+ if (existsSync(runtimePath)) {
122
+ try {
123
+ runtime = JSON.parse(readFileSync(runtimePath, 'utf8')) as Record<string, unknown>;
124
+ } catch {
125
+ // corrupt runtime file - start fresh
126
+ runtime = {};
127
+ }
128
+ }
129
+ const cluster = (runtime['cluster'] ?? {}) as Record<string, unknown>;
130
+ cluster['enabled'] = true;
131
+ runtime['cluster'] = cluster;
132
+ writeFileSync(runtimePath, `${JSON.stringify(runtime, null, 2)}\n`);
133
+ }