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.
- package/package.json +1 -1
- package/packages/nexus-admin/package.json +2 -1
- package/packages/nexus-admin/src/App.tsx +5 -0
- package/packages/nexus-ads/package.json +3 -2
- package/packages/nexus-ads/src/index.ts +3 -0
- package/packages/nexus-ai-client/package.json +1 -1
- package/packages/nexus-auth/package.json +1 -1
- package/packages/nexus-cache/package.json +1 -1
- package/packages/nexus-cli/package.json +1 -1
- package/packages/nexus-cli/src/browser.ts +20 -0
- package/packages/nexus-cli/src/commands/dev.ts +14 -0
- package/packages/nexus-cli/src/commands/license.ts +155 -97
- package/packages/nexus-cli/templates/base/apps/frontend/index.html.ejs +1 -1
- package/packages/nexus-cli/templates/base/apps/frontend/src/App.tsx.ejs +1 -1
- package/packages/nexus-cli/templates/base/apps/frontend/src/NotFound.tsx.ejs +8 -8
- package/packages/nexus-cli/templates/base/gitignore +3 -0
- package/packages/nexus-core/package.json +2 -1
- package/packages/nexus-core/src/app/createNexusApp.ts +25 -0
- package/packages/nexus-core/src/errors.ts +22 -1
- package/packages/nexus-crypto/package.json +15 -2
- package/packages/nexus-crypto/src/license-browser.ts +59 -0
- package/packages/nexus-crypto/src/license.ts +245 -89
- package/packages/nexus-data/package.json +1 -1
- package/packages/nexus-email/package.json +1 -1
- package/packages/nexus-examples/package.json +1 -1
- package/packages/nexus-fusion/package.json +1 -1
- package/packages/nexus-fusion-core/package.json +1 -1
- package/packages/nexus-future/package.json +4 -1
- package/packages/nexus-graphql/package.json +1 -1
- package/packages/nexus-payments/package.json +1 -1
- package/packages/nexus-plugins/package.json +1 -1
- package/packages/nexus-postcss/package.json +1 -1
- package/packages/nexus-realtime/package.json +1 -1
- package/packages/nexus-safe-goto/package.json +2 -1
- package/packages/nexus-safe-goto/src/index.tsx +4 -0
- 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.
|
|
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.
|
|
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.
|
|
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
|
}
|
|
@@ -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
|
|
2
|
+
* nexus license — manage the project's signed license token.
|
|
3
3
|
*
|
|
4
|
-
* nexus license add
|
|
5
|
-
* nexus license
|
|
6
|
-
* nexus license
|
|
7
|
-
* nexus license
|
|
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
|
|
10
|
-
*
|
|
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 {
|
|
19
|
+
import { isInteractive } from '../prompts.js';
|
|
20
|
+
import { openBrowser } from '../browser.js';
|
|
14
21
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
storeLicenseToken,
|
|
23
|
+
removeLicense,
|
|
24
|
+
resolveLicenseToken,
|
|
25
|
+
ensureLicense,
|
|
26
|
+
getLicenseServer,
|
|
27
|
+
licenseFilePath,
|
|
20
28
|
} from '@bhooai/nexus-crypto';
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
const DEFAULT_SERVER = 'http://localhost:4000';
|
|
30
|
+
const AUTH_TIMEOUT_MS = 5 * 60_000;
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
42
|
+
throw new Error(`Cannot reach the license authority at ${new URL(url).origin} — check your connection.`);
|
|
43
43
|
}
|
|
44
|
-
const data =
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
const {
|
|
95
|
-
if (!
|
|
96
|
-
console.log('No license configured.');
|
|
97
|
-
console.log('Run `nexus license add
|
|
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:
|
|
101
|
-
console.log(`
|
|
156
|
+
console.log(`Source: ${resolved.file ?? 'env NEXUS_LICENSE_KEY'}`);
|
|
157
|
+
console.log(`Server: ${serverOf(ctx)}`);
|
|
102
158
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
|
124
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
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:#
|
|
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',
|
|
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:#
|
|
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: '#
|
|
19
|
-
color: '#
|
|
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: '#
|
|
27
|
-
border: '1px solid #
|
|
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: '#
|
|
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: '#
|
|
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: '#
|
|
45
|
+
background: '#2563eb',
|
|
46
46
|
color: '#fff',
|
|
47
47
|
textDecoration: 'none',
|
|
48
48
|
}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bhooai/nexus-core",
|
|
3
|
-
"version": "2.0.
|
|
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
|