bhooai-nexus 3.0.4 → 3.0.5

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 (36) hide show
  1. package/package.json +1 -1
  2. package/packages/nexus-admin/package.json +2 -1
  3. package/packages/nexus-admin/src/App.tsx +5 -0
  4. package/packages/nexus-ads/package.json +3 -2
  5. package/packages/nexus-ads/src/index.ts +3 -0
  6. package/packages/nexus-ai-client/package.json +1 -1
  7. package/packages/nexus-auth/package.json +1 -1
  8. package/packages/nexus-cache/package.json +1 -1
  9. package/packages/nexus-cli/package.json +1 -1
  10. package/packages/nexus-cli/src/browser.ts +20 -0
  11. package/packages/nexus-cli/src/commands/dev.ts +14 -0
  12. package/packages/nexus-cli/src/commands/license.ts +155 -97
  13. package/packages/nexus-cli/templates/base/apps/frontend/index.html.ejs +1 -1
  14. package/packages/nexus-cli/templates/base/apps/frontend/src/App.tsx.ejs +1 -1
  15. package/packages/nexus-cli/templates/base/apps/frontend/src/NotFound.tsx.ejs +8 -8
  16. package/packages/nexus-cli/templates/base/gitignore +3 -0
  17. package/packages/nexus-core/package.json +2 -1
  18. package/packages/nexus-core/src/app/createNexusApp.ts +25 -0
  19. package/packages/nexus-core/src/errors.ts +22 -1
  20. package/packages/nexus-crypto/package.json +15 -2
  21. package/packages/nexus-crypto/src/license-browser.ts +59 -0
  22. package/packages/nexus-crypto/src/license.ts +245 -89
  23. package/packages/nexus-data/package.json +1 -1
  24. package/packages/nexus-email/package.json +1 -1
  25. package/packages/nexus-examples/package.json +1 -1
  26. package/packages/nexus-fusion/package.json +1 -1
  27. package/packages/nexus-fusion-core/package.json +1 -1
  28. package/packages/nexus-future/package.json +4 -1
  29. package/packages/nexus-graphql/package.json +1 -1
  30. package/packages/nexus-payments/package.json +1 -1
  31. package/packages/nexus-plugins/package.json +1 -1
  32. package/packages/nexus-postcss/package.json +1 -1
  33. package/packages/nexus-realtime/package.json +1 -1
  34. package/packages/nexus-safe-goto/package.json +2 -1
  35. package/packages/nexus-safe-goto/src/index.tsx +4 -0
  36. package/packages/nexus-telemetry/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bhooai-nexus",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "description": "BhooAI Nexus - a Node-based full-stack framework (websockets, GraphQL federation, inbuilt security, custom ODM, OAuth, payments, WebRTC, AI, plugins). v2: multi-app, Laravel-style folders, Docker-based scaling.",
5
5
  "license": "MIT",
6
6
  "author": "BhooAI - oravianando",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-admin",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "description": "BhooAI Nexus v2 admin SPA — Apps-tab-first, live registry/health/logs/config/users/payments/AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  "src"
16
16
  ],
17
17
  "dependencies": {
18
+ "@bhooai/nexus-crypto": "^2.0.15",
18
19
  "@bhooai/nexus-safe-goto": "*",
19
20
  "react": "^18.3.1",
20
21
  "react-dom": "^18.3.1"
@@ -20,6 +20,11 @@ import {
20
20
  getCertStatus, generateCerts, type CertStatus, type CertGenerateResult,
21
21
  type UploadRecord, type UploadsResponse, type UploadStats, type UploadIndexStatus,
22
22
  } from './api.js';
23
+ import { requireLicense } from '@bhooai/nexus-crypto/browser';
24
+
25
+ // Licensed package: the admin SPA refuses to render without a key injected by
26
+ // the host (VITE_NEXUS_LICENSE_KEY or globalThis.NEXUS_LICENSE_KEY).
27
+ requireLicense('nexus-admin');
23
28
 
24
29
 
25
30
  type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'uploads' | 'schema' | 'ai' | 'graphql' | 'theme';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-ads",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -12,7 +12,8 @@
12
12
  "test": "vitest run"
13
13
  },
14
14
  "dependencies": {
15
- "@bhooai/nexus-core": "^2.0.1"
15
+ "@bhooai/nexus-core": "^2.0.1",
16
+ "@bhooai/nexus-crypto": "^2.0.15"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/node": "^22.5.0",
@@ -5,6 +5,7 @@ export * from './GoogleAdsClient.js';
5
5
  import { GoogleAdsClient } from './GoogleAdsClient.js';
6
6
  import { fetchTransport } from './fetchTransport.js';
7
7
  import type { GoogleAdsConfig, HttpTransport } from './types.js';
8
+ import { requireLicense } from '@bhooai/nexus-crypto';
8
9
 
9
10
  /** Default fetch-based transport (Node >= 18). */
10
11
  export { fetchTransport };
@@ -16,5 +17,7 @@ export interface CreateAdsOptions {
16
17
 
17
18
  /** Build a Google Ads client. Throws at construction if required creds are missing. */
18
19
  export function createGoogleAds(config: GoogleAdsConfig, opts: CreateAdsOptions = {}): GoogleAdsClient {
20
+ // Licensed feature: the Google Ads client needs a master key.
21
+ requireLicense('nexus-ads');
19
22
  return new GoogleAdsClient(config, opts.transport ?? fetchTransport, opts.apiVersion);
20
23
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-ai-client",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-auth",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-cache",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-cli",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "description": "BhooAI Nexus v2 CLI — init, dev, build, test, scaffolding, plugins, queue, db.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cross-platform "open this URL in the default browser". Best-effort: the
3
+ * caller always prints the URL too, so headless/SSH sessions still work.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+
7
+ export function openBrowser(url: string): void {
8
+ try {
9
+ if (process.platform === 'win32') {
10
+ // `start` is a cmd builtin; the empty "" is the window title.
11
+ spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
12
+ } else if (process.platform === 'darwin') {
13
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
14
+ } else {
15
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
16
+ }
17
+ } catch {
18
+ // Headless / no browser — the URL was already printed.
19
+ }
20
+ }
@@ -12,6 +12,7 @@ import { join, dirname } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import type { CommandContext } from '../dispatcher.js';
14
14
  import { loadConfigAuto } from '@bhooai/nexus-core';
15
+ import { ensureLicense } from '@bhooai/nexus-crypto';
15
16
  import { resolveConfiguredPorts, findFreeShift, describePortHolder } from '../ports.js';
16
17
  import { ServiceManager, type ServiceSpec } from '../devServiceManager.js';
17
18
  import { startDevPanel } from '../devPanel.js';
@@ -24,6 +25,19 @@ const RESET = '\x1b[0m';
24
25
  export async function run(ctx: CommandContext): Promise<void> {
25
26
  const only = (ctx.args.flags.only as string)?.split(',').map((s) => s.trim()) ?? null;
26
27
  const projectRoot = process.cwd();
28
+
29
+ // License check at project start — verify with the authority
30
+ // (nexus-bhooai-com). Generation/verification live on the authority. If the
31
+ // check fails we still start the services: the backend boots in a
32
+ // license-error state and answers requests with 402 LICENSE_REQUIRED, so the
33
+ // UI can show the error instead of a proxy 500.
34
+ try {
35
+ await ensureLicense({ projectDir: projectRoot });
36
+ } catch (err) {
37
+ console.warn(`⚠ License check failed: ${(err as Error).message}`);
38
+ console.warn(' Starting anyway — API requests will return 402 LICENSE_REQUIRED.');
39
+ }
40
+
27
41
  const noPanel = !!ctx.args.flags['no-panel'];
28
42
  const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
29
43
 
@@ -1,145 +1,199 @@
1
1
  /**
2
- * nexus license — manage the master license key for this machine.
2
+ * nexus license — manage the project's signed license token.
3
3
  *
4
- * nexus license add [key] [--server <url>] — verify against the authority, then store
5
- * nexus license status source, fingerprint, live validity
6
- * nexus license verify re-check the stored key with the server
7
- * nexus license remove delete the stored key
4
+ * nexus license add browser: authorize this project, auto-store the token
5
+ * nexus license add <key> exchange a key for a signed token and store it
6
+ * nexus license status source + live validity
7
+ * nexus license verify re-check the stored token with the authority
8
+ * nexus license token — print the stored token (for CI: NEXUS_LICENSE_KEY)
9
+ * nexus license remove — delete the project's stored license
8
10
  *
9
- * The key itself unlocks nothing locally: every package derives its own key
10
- * from it (HKDF-SHA256) and gated features throw until a key is present.
11
+ * The token is signed by the authority (nexus-bhooai-com) and stored at
12
+ * `<project>/license/license.jwt`. Generation/verification live on the
13
+ * authority; this CLI only fetches + stores + checks.
11
14
  */
15
+ import { createServer } from 'node:http';
16
+ import { createHash, randomBytes } from 'node:crypto';
17
+ import type { AddressInfo } from 'node:net';
12
18
  import type { CommandContext } from '../dispatcher.js';
13
- import { promptHidden, isInteractive } from '../prompts.js';
19
+ import { isInteractive } from '../prompts.js';
20
+ import { openBrowser } from '../browser.js';
14
21
  import {
15
- loadMasterKey,
16
- saveMasterKey,
17
- removeMasterKey,
18
- keyFingerprint,
19
- LICENSE_ENV_VAR,
22
+ storeLicenseToken,
23
+ removeLicense,
24
+ resolveLicenseToken,
25
+ ensureLicense,
26
+ getLicenseServer,
27
+ licenseFilePath,
20
28
  } from '@bhooai/nexus-crypto';
21
29
 
22
- // DEV-MODE: local authority. Flip back to https://nexus.bhooai.com for release.
23
- const DEFAULT_SERVER = 'http://localhost:4000';
30
+ const AUTH_TIMEOUT_MS = 5 * 60_000;
24
31
 
25
- interface VerifyResult {
26
- valid: boolean;
27
- plan?: string;
28
- expiresAt?: string | null;
29
- message?: string;
32
+ function serverOf(ctx: CommandContext): string {
33
+ const flag = ctx.args.flags.server as string | undefined;
34
+ return (flag ?? getLicenseServer()).replace(/\/+$/, '');
30
35
  }
31
36
 
32
- async function verifyWithServer(server: string, key: string): Promise<VerifyResult> {
33
- const url = `${server.replace(/\/+$/, '')}/api/license-verify`;
37
+ async function postJson(url: string, body: unknown): Promise<{ ok: boolean; status: number; data: any }> {
34
38
  let res: Response;
35
39
  try {
36
- res = await fetch(url, {
37
- method: 'POST',
38
- headers: { 'Content-Type': 'application/json' },
39
- body: JSON.stringify({ key }),
40
- });
40
+ res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
41
41
  } catch {
42
- throw new Error(`Cannot reach the license server at ${server} — check your connection.`);
42
+ throw new Error(`Cannot reach the license authority at ${new URL(url).origin} — check your connection.`);
43
43
  }
44
- const data = (await res.json().catch(() => ({}))) as Partial<VerifyResult> & { error?: { message?: string } };
45
- if (!res.ok) throw new Error(data.error?.message ?? `License server responded with HTTP ${res.status}.`);
46
- return { valid: !!data.valid, plan: data.plan, expiresAt: data.expiresAt ?? null, message: data.message };
44
+ const data = await res.json().catch(() => ({}));
45
+ return { ok: res.ok, status: res.status, data };
47
46
  }
48
47
 
49
- function keySource(): { key: string | null; source: string } {
50
- const env = (process.env[LICENSE_ENV_VAR] ?? '').trim();
51
- if (env) return { key: env, source: `env ${LICENSE_ENV_VAR}` };
52
- const file = loadMasterKey();
53
- if (file) return { key: file, source: '~/.nexus/license.json' };
54
- return { key: null, source: '' };
48
+ /** Browser flow: loopback callback + PKCE, then exchange the code for a token. */
49
+ async function runBrowserAdd(server: string, projectRoot: string): Promise<void> {
50
+ const state = randomBytes(16).toString('base64url');
51
+ const verifier = randomBytes(32).toString('base64url');
52
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
53
+
54
+ let redirectUri = '';
55
+ const code = await new Promise<string>((resolve, reject) => {
56
+ const srv = createServer((req, res) => {
57
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
58
+ if (url.pathname !== '/callback') {
59
+ res.writeHead(404);
60
+ res.end();
61
+ return;
62
+ }
63
+ const returnedState = url.searchParams.get('state');
64
+ const err = url.searchParams.get('error');
65
+ const authCode = url.searchParams.get('code');
66
+ res.writeHead(200, { 'content-type': 'text/html' });
67
+ if (err) {
68
+ res.end(`<h1>Authorization failed</h1><p>${err}</p><p>You can close this window.</p>`);
69
+ srv.close();
70
+ reject(new Error(`Authorization failed: ${err}`));
71
+ return;
72
+ }
73
+ if (returnedState !== state || !authCode) {
74
+ res.end('<h1>Invalid callback</h1><p>State mismatch. You can close this window.</p>');
75
+ srv.close();
76
+ reject(new Error('Authorization callback failed (state mismatch).'));
77
+ return;
78
+ }
79
+ res.end('<h1>License added</h1><p>You can close this window and return to the terminal.</p>');
80
+ srv.close();
81
+ resolve(authCode);
82
+ });
83
+
84
+ const timer = setTimeout(() => {
85
+ try { srv.close(); } catch { /* ignore */ }
86
+ reject(new Error('Timed out waiting for authorization.'));
87
+ }, AUTH_TIMEOUT_MS);
88
+ timer.unref?.();
89
+
90
+ srv.listen(0, '127.0.0.1', () => {
91
+ const port = (srv.address() as AddressInfo).port;
92
+ redirectUri = `http://127.0.0.1:${port}/callback`;
93
+ const authorizeUrl =
94
+ `${server}/cli/license?redirect_uri=${encodeURIComponent(redirectUri)}` +
95
+ `&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
96
+ console.log('Opening your browser to authorize this project…');
97
+ console.log(`If it does not open, visit:\n ${authorizeUrl}\n`);
98
+ openBrowser(authorizeUrl);
99
+ });
100
+ });
101
+
102
+ const { ok, status, data } = await postJson(`${server}/api/license/cli/token`, {
103
+ code,
104
+ code_verifier: verifier,
105
+ redirect_uri: redirectUri,
106
+ });
107
+ if (!ok || !data?.token) {
108
+ throw new Error(data?.error?.message ?? `Token exchange failed (HTTP ${status}).`);
109
+ }
110
+ const file = storeLicenseToken(projectRoot, data.token as string);
111
+ console.log(`✓ License added${data.plan ? ` (${data.plan} plan)` : ''}${data.expiresAt ? ` — expires ${data.expiresAt}` : ''}.`);
112
+ console.log(` Stored in ${file}`);
113
+ }
114
+
115
+ /** Manual flow: exchange a pasted key for a signed token. */
116
+ async function runManualAdd(server: string, key: string, projectRoot: string): Promise<void> {
117
+ const { ok, status, data } = await postJson(`${server}/api/licenses/token`, { key });
118
+ if (!ok || !data?.token) {
119
+ throw new Error(data?.error?.message ?? `License rejected (HTTP ${status}).`);
120
+ }
121
+ const file = storeLicenseToken(projectRoot, data.token as string);
122
+ console.log(`✓ License added${data.plan ? ` (${data.plan} plan)` : ''}${data.expiresAt ? ` — expires ${data.expiresAt}` : ''}.`);
123
+ console.log(` Stored in ${file}`);
55
124
  }
56
125
 
57
126
  async function runAdd(ctx: CommandContext): Promise<void> {
58
- const server = (ctx.args.flags.server as string | undefined) ?? DEFAULT_SERVER;
59
- let key = (ctx.args._[1] ?? '').trim();
60
- if (!key) {
127
+ const server = serverOf(ctx);
128
+ const projectRoot = process.cwd();
129
+ const key = (ctx.args._[1] ?? '').trim();
130
+
131
+ try {
132
+ if (key) {
133
+ await runManualAdd(server, key, projectRoot);
134
+ return;
135
+ }
61
136
  if (!isInteractive()) {
62
137
  console.error('Usage: nexus license add <key> [--server <url>]');
63
138
  process.exitCode = 1;
64
139
  return;
65
140
  }
66
- key = (await promptHidden('Paste your license key (nxl_…): ')).trim();
67
- }
68
- if (!key) {
69
- console.error('No key provided.');
70
- process.exitCode = 1;
71
- return;
72
- }
73
- console.log(`Verifying with ${server} …`);
74
- let result: VerifyResult;
75
- try {
76
- result = await verifyWithServer(server, key);
141
+ await runBrowserAdd(server, projectRoot);
77
142
  } catch (err) {
78
143
  console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
79
144
  process.exitCode = 1;
80
- return;
81
- }
82
- if (!result.valid) {
83
- console.error(`✗ License rejected: ${result.message ?? 'unknown reason'}`);
84
- process.exitCode = 1;
85
- return;
86
145
  }
87
- const path = saveMasterKey(key, { server });
88
- console.log(`✓ Licensed${result.plan ? ` (${result.plan} plan)` : ''}${result.expiresAt ? ` — expires ${result.expiresAt}` : ''}.`);
89
- console.log(` Stored in ${path} (mode 0600).`);
90
146
  }
91
147
 
92
148
  async function runStatus(ctx: CommandContext): Promise<void> {
93
- const server = (ctx.args.flags.server as string | undefined) ?? DEFAULT_SERVER;
94
- const { key, source } = keySource();
95
- if (!key) {
96
- console.log('No license configured.');
97
- console.log('Run `nexus license add <key>` (get one at https://nexus.bhooai.com/account).');
149
+ const projectRoot = process.cwd();
150
+ const resolved = resolveLicenseToken({ projectDir: projectRoot });
151
+ if (!resolved) {
152
+ console.log('No license configured for this project.');
153
+ console.log('Run `nexus license add` to authorize this project in the browser.');
98
154
  return;
99
155
  }
100
- console.log(`Source: ${source}`);
101
- console.log(`Fingerprint: ${keyFingerprint(key)}…`);
156
+ console.log(`Source: ${resolved.file ?? 'env NEXUS_LICENSE_KEY'}`);
157
+ console.log(`Server: ${serverOf(ctx)}`);
102
158
  try {
103
- const result = await verifyWithServer(server, key);
104
- if (result.valid) {
105
- console.log(`Status: valid${result.plan ? ` (${result.plan})` : ''}${result.expiresAt ? ` — expires ${result.expiresAt}` : ''}`);
106
- } else {
107
- console.log(`Status: INVALID — ${result.message ?? 'rejected by server'}`);
108
- }
109
- } catch {
110
- console.log('Status: unknown (server unreachable — cached key still works offline)');
159
+ const claims = await ensureLicense({ projectDir: projectRoot, server: serverOf(ctx) });
160
+ const exp = typeof claims.exp === 'number' ? ` — expires ${new Date(claims.exp * 1000).toISOString()}` : '';
161
+ console.log(`Status: valid${claims.plan ? ` (${claims.plan})` : ''}${exp}`);
162
+ } catch (err) {
163
+ console.log(`Status: INVALID — ${err instanceof Error ? err.message : String(err)}`);
164
+ process.exitCode = 1;
111
165
  }
112
166
  }
113
167
 
114
168
  async function runVerify(ctx: CommandContext): Promise<void> {
115
- const server = (ctx.args.flags.server as string | undefined) ?? DEFAULT_SERVER;
116
- const { key } = keySource();
117
- if (!key) {
118
- console.error('No license configured — run `nexus license add <key>` first.');
119
- process.exitCode = 1;
120
- return;
121
- }
169
+ const projectRoot = process.cwd();
122
170
  try {
123
- const result = await verifyWithServer(server, key);
124
- if (result.valid) {
125
- console.log(`✓ Valid${result.plan ? ` (${result.plan})` : ''}${result.expiresAt ? ` — expires ${result.expiresAt}` : ''}.`);
126
- } else {
127
- console.error(`✗ Invalid: ${result.message ?? 'rejected by server'}.`);
128
- process.exitCode = 1;
129
- }
171
+ const claims = await ensureLicense({ projectDir: projectRoot, server: serverOf(ctx), force: true });
172
+ console.log(`✓ Valid${claims.plan ? ` (${claims.plan})` : ''}.`);
130
173
  } catch (err) {
131
174
  console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
132
175
  process.exitCode = 1;
133
176
  }
134
177
  }
135
178
 
136
- async function runRemove(): Promise<void> {
137
- if (process.env[LICENSE_ENV_VAR]) {
138
- console.log(`Unset ${LICENSE_ENV_VAR} to remove the license (env takes precedence over the file).`);
179
+ function runToken(): void {
180
+ const resolved = resolveLicenseToken({ projectDir: process.cwd() });
181
+ if (!resolved) {
182
+ console.error('No license configured — run `nexus license add` first.');
183
+ process.exitCode = 1;
139
184
  return;
140
185
  }
141
- if (removeMasterKey()) console.log('✓ License removed.');
142
- else console.log('No stored license found.');
186
+ // Bare token to stdout so CI can capture it.
187
+ process.stdout.write(resolved.token + '\n');
188
+ }
189
+
190
+ function runRemove(): void {
191
+ const projectRoot = process.cwd();
192
+ if (removeLicense(projectRoot)) {
193
+ console.log(`✓ License removed (${licenseFilePath(projectRoot)}).`);
194
+ } else {
195
+ console.log('No stored license found.');
196
+ }
143
197
  }
144
198
 
145
199
  export async function run(ctx: CommandContext): Promise<void> {
@@ -154,14 +208,18 @@ export async function run(ctx: CommandContext): Promise<void> {
154
208
  case 'verify':
155
209
  await runVerify(ctx);
156
210
  break;
211
+ case 'token':
212
+ runToken();
213
+ break;
157
214
  case 'remove':
158
- await runRemove();
215
+ runRemove();
159
216
  break;
160
217
  default:
161
218
  console.log('Usage:');
162
219
  console.log(' nexus license add [key] [--server <url>]');
163
220
  console.log(' nexus license status');
164
221
  console.log(' nexus license verify');
222
+ console.log(' nexus license token');
165
223
  console.log(' nexus license remove');
166
224
  if (sub) {
167
225
  console.error(`Unknown license subcommand: ${sub}`);
@@ -170,5 +228,5 @@ export async function run(ctx: CommandContext): Promise<void> {
170
228
  }
171
229
  }
172
230
 
173
- export const description = 'Manage the master license key (add / status / verify / remove)';
174
- export const usage = 'nexus license <add|status|verify|remove> [key] [--server <url>]';
231
+ export const description = 'Manage the project license (add / status / verify / token / remove)';
232
+ export const usage = 'nexus license <add|status|verify|token|remove> [key] [--server <url>]';
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8"/>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
6
  <title><%= name %></title>
7
- <style>html,body{margin:0;padding:0;background:#0b1220}</style>
7
+ <style>html,body{margin:0;padding:0;background:#fff}</style>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -7,7 +7,7 @@ export function App() {
7
7
  return <NotFound />;
8
8
  }
9
9
  return (
10
- <main style={{ fontFamily: 'system-ui', padding: '2rem', background: '#0b1220', color: '#e2e8f0', minHeight: '100vh', margin: 0 }}>
10
+ <main style={{ fontFamily: 'system-ui', padding: '2rem', minHeight: '100vh', margin: 0 }}>
11
11
  <h1>Welcome to <%= name %></h1>
12
12
  <p>Your Nexus app is up. Edit <code>apps/frontend/src/App.tsx</code> to get started.</p>
13
13
  </main>
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- const resetStyle = 'html,body{margin:0;padding:0;background:#0b1220}';
3
+ const resetStyle = 'html,body{margin:0;padding:0;background:#fff}';
4
4
 
5
5
  export function NotFound() {
6
6
  return (
@@ -15,25 +15,25 @@ export function NotFound() {
15
15
  justifyContent: 'center',
16
16
  margin: 0,
17
17
  padding: 0,
18
- background: '#0b1220',
19
- color: '#e2e8f0',
18
+ background: '#fff',
19
+ color: '#0f172a',
20
20
  }}
21
21
  >
22
22
  <div
23
23
  style={{
24
24
  maxWidth: '32rem',
25
25
  textAlign: 'center',
26
- background: '#0f172a',
27
- border: '1px solid #1e293b',
26
+ background: '#f8fafc',
27
+ border: '1px solid #e2e8f0',
28
28
  borderRadius: 12,
29
29
  padding: '3rem 2rem',
30
30
  }}
31
31
  >
32
- <div style={{ fontSize: '4rem', color: '#60a5fa', margin: '0 0 .5rem', fontWeight: 700 }}>
32
+ <div style={{ fontSize: '4rem', color: '#2563eb', margin: '0 0 .5rem', fontWeight: 700 }}>
33
33
  404
34
34
  </div>
35
35
  <h1 style={{ fontSize: '1.5rem', margin: '0 0 1rem', fontWeight: 600 }}>Not Found</h1>
36
- <p style={{ color: '#94a3b8', lineHeight: 1.5, margin: '0 0 1.5rem' }}>
36
+ <p style={{ color: '#64748b', lineHeight: 1.5, margin: '0 0 1.5rem' }}>
37
37
  The page you were looking for does not exist.
38
38
  </p>
39
39
  <a
@@ -42,7 +42,7 @@ export function NotFound() {
42
42
  display: 'inline-block',
43
43
  padding: '.625rem 1.125rem',
44
44
  borderRadius: 8,
45
- background: '#60a5fa',
45
+ background: '#2563eb',
46
46
  color: '#fff',
47
47
  textDecoration: 'none',
48
48
  }}
@@ -11,6 +11,9 @@ build/
11
11
  .env.local
12
12
  nexus.runtime.json
13
13
 
14
+ # License (signed token — bearer credential, per project)
15
+ license/
16
+
14
17
  # Generated
15
18
  logs/
16
19
  certs/
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-core",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,7 @@
20
20
  "dependencies": {
21
21
  "@bhooai/nexus-ai-client": "^2.0.1",
22
22
  "@bhooai/nexus-auth": "^2.0.1",
23
+ "@bhooai/nexus-crypto": "^2.0.15",
23
24
  "@bhooai/nexus-data": "^2.0.1",
24
25
  "selfsigned": "^5.5.0",
25
26
  "zod": "^3.23.8"
@@ -40,6 +40,7 @@ import { createLazyDb, registerAdminRoutes, RequestLogBuffer } from './adminModu
40
40
  import { registerAuthRoutes } from './authModule.js';
41
41
  import { issueCsrfToken, getCsrfToken, csrf, authToken, requireRole } from '@bhooai/nexus-auth';
42
42
  import { connect } from '@bhooai/nexus-data';
43
+ import { ensureLicense } from '@bhooai/nexus-crypto';
43
44
  import { createGateway, createFederatedGateway, graphqlHttpHandler, getExplorerHtml, helloSubgraph, SubscriptionServer } from '@bhooai/nexus-graphql';
44
45
  import type { Subgraph, GraphQLContext } from '@bhooai/nexus-graphql';
45
46
 
@@ -103,6 +104,19 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
103
104
  const projectRoot = resolve(opts.projectRoot ?? dirname(srcRoot));
104
105
  let config = opts.config ?? (await loadConfigAuto({ root: projectRoot }));
105
106
 
107
+ // License check at project start — verify the token (signature via fetched
108
+ // JWKS + online status) with the authority. On failure the app still boots,
109
+ // but every request is answered with 402 LICENSE_REQUIRED (middleware below)
110
+ // instead of crashing — a crashed backend surfaces as an opaque HTTP 500
111
+ // through dev proxies.
112
+ let licenseError: string | null = null;
113
+ try {
114
+ await ensureLicense({ projectDir: projectRoot });
115
+ } catch (err) {
116
+ licenseError = (err as Error).message;
117
+ console.error(`[${name}] license check failed: ${licenseError}`);
118
+ }
119
+
106
120
  // ------------------------------------------------------------------
107
121
  // Per-app ports: nexus.config.ts `apps[]` is authoritative. The app's own
108
122
  // entry (matched by `opts.name`) wins over `server.port`. NEXUS_PORT (set
@@ -516,6 +530,17 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
516
530
  },
517
531
  });
518
532
 
533
+ // License gate — when the project-start check failed, answer every request
534
+ // (except /health) with 402 so the UI can show the license error rather than
535
+ // an opaque 500 from a crashed backend.
536
+ if (licenseError) {
537
+ const message = licenseError;
538
+ server.use(async (ctx, next) => {
539
+ if (ctx.path === '/health') return next();
540
+ ctx.json({ error: { code: 'LICENSE_REQUIRED', message } }, 402);
541
+ });
542
+ }
543
+
519
544
  // ------------------------------------------------------------------
520
545
  // Realtime WS server — mount discovered ws/*.room.ts handlers (chat lobby)
521
546
  // React stack only — future uses live canvas