fa-mcp-sdk 0.4.27 → 0.4.30
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 +5 -1
- package/bin/fa-mcp.js +1 -1
- package/cli-template/.claude/skills/gen-jwt/SKILL.md +113 -0
- package/cli-template/CLAUDE.md +14 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +123 -0
- package/cli-template/package.json +1 -1
- package/config/_local.yaml +12 -0
- package/config/custom-environment-variables.yaml +1 -0
- package/config/default.yaml +12 -0
- package/config/local.yaml +7 -18
- package/dist/core/_types_/config.d.ts +3 -1
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/auth/admin-auth.d.ts +12 -1
- package/dist/core/auth/admin-auth.d.ts.map +1 -1
- package/dist/core/auth/admin-auth.js +124 -64
- package/dist/core/auth/admin-auth.js.map +1 -1
- package/dist/core/bootstrap/startup-info.d.ts.map +1 -1
- package/dist/core/bootstrap/startup-info.js +1 -0
- package/dist/core/bootstrap/startup-info.js.map +1 -1
- package/dist/core/web/admin-router.d.ts.map +1 -1
- package/dist/core/web/admin-router.js +34 -25
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +71 -0
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/agent-tester/index.html +8 -3
- package/dist/core/web/static/agent-tester/script.js +59 -15
- package/dist/core/web/static/agent-tester/styles.css +27 -1
- package/dist/core/web/static/styles.css +30 -0
- package/dist/core/web/static/token-gen/index.html +24 -2
- package/dist/core/web/static/token-gen/script.js +171 -34
- package/package.json +1 -1
- package/scripts/generate-jwt.js +191 -0
|
@@ -5,7 +5,8 @@ let keyValuePairCount = 0;
|
|
|
5
5
|
// ===========================
|
|
6
6
|
|
|
7
7
|
const AUTH_TOKEN_KEY = 'adminAuthToken';
|
|
8
|
-
let
|
|
8
|
+
let requiresFrontendAuth = false;
|
|
9
|
+
let authMethods = []; // ['token', 'basic']
|
|
9
10
|
|
|
10
11
|
// Get stored auth token from sessionStorage
|
|
11
12
|
function getStoredToken () {
|
|
@@ -22,53 +23,114 @@ function clearStoredToken () {
|
|
|
22
23
|
sessionStorage.removeItem(AUTH_TOKEN_KEY);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// Show
|
|
26
|
-
function
|
|
26
|
+
// Show authentication modal
|
|
27
|
+
function showAuthModal (errorMessage = null) {
|
|
27
28
|
const modal = document.getElementById('tokenModal');
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
// Clear errors on both forms
|
|
31
|
+
const tokenError = document.getElementById('tokenAuthError');
|
|
32
|
+
const basicError = document.getElementById('basicAuthError');
|
|
33
|
+
tokenError.style.display = 'none';
|
|
34
|
+
basicError.style.display = 'none';
|
|
29
35
|
|
|
30
36
|
if (errorMessage) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// Show error in the active form
|
|
38
|
+
const activeError = document.getElementById('basicAuthForm').style.display !== 'none'
|
|
39
|
+
? basicError : tokenError;
|
|
40
|
+
activeError.innerHTML = `<strong>Error:</strong> ${errorMessage}`;
|
|
41
|
+
activeError.style.display = 'block';
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
modal.style.display = 'flex';
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
// Hide
|
|
41
|
-
function
|
|
47
|
+
// Hide authentication modal
|
|
48
|
+
function hideAuthModal () {
|
|
42
49
|
const modal = document.getElementById('tokenModal');
|
|
43
50
|
modal.style.display = 'none';
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
//
|
|
53
|
+
// Setup auth tabs and forms based on available methods
|
|
54
|
+
function setupAuthForms (methods) {
|
|
55
|
+
const hasToken = methods.includes('token');
|
|
56
|
+
const hasBasic = methods.includes('basic');
|
|
57
|
+
|
|
58
|
+
const tabs = document.getElementById('adminAuthTabs');
|
|
59
|
+
const tokenForm = document.getElementById('tokenAuthForm');
|
|
60
|
+
const basicForm = document.getElementById('basicAuthForm');
|
|
61
|
+
|
|
62
|
+
if (hasToken && hasBasic) {
|
|
63
|
+
// Show tabs, default to token
|
|
64
|
+
tabs.style.display = 'flex';
|
|
65
|
+
tokenForm.style.display = 'block';
|
|
66
|
+
basicForm.style.display = 'none';
|
|
67
|
+
bindAuthTabs();
|
|
68
|
+
} else if (hasBasic) {
|
|
69
|
+
// Basic only
|
|
70
|
+
tabs.style.display = 'none';
|
|
71
|
+
tokenForm.style.display = 'none';
|
|
72
|
+
basicForm.style.display = 'block';
|
|
73
|
+
} else {
|
|
74
|
+
// Token only (default)
|
|
75
|
+
tabs.style.display = 'none';
|
|
76
|
+
tokenForm.style.display = 'block';
|
|
77
|
+
basicForm.style.display = 'none';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bind tab click handlers
|
|
82
|
+
function bindAuthTabs () {
|
|
83
|
+
const tabButtons = document.querySelectorAll('.admin-auth-tab');
|
|
84
|
+
const tokenForm = document.getElementById('tokenAuthForm');
|
|
85
|
+
const basicForm = document.getElementById('basicAuthForm');
|
|
86
|
+
|
|
87
|
+
tabButtons.forEach((btn) => {
|
|
88
|
+
btn.addEventListener('click', () => {
|
|
89
|
+
// Update active tab
|
|
90
|
+
tabButtons.forEach((b) => b.classList.remove('active'));
|
|
91
|
+
btn.classList.add('active');
|
|
92
|
+
|
|
93
|
+
// Hide errors
|
|
94
|
+
document.getElementById('tokenAuthError').style.display = 'none';
|
|
95
|
+
document.getElementById('basicAuthError').style.display = 'none';
|
|
96
|
+
|
|
97
|
+
// Toggle forms
|
|
98
|
+
const tab = btn.getAttribute('data-tab');
|
|
99
|
+
if (tab === 'basic') {
|
|
100
|
+
tokenForm.style.display = 'none';
|
|
101
|
+
basicForm.style.display = 'block';
|
|
102
|
+
} else {
|
|
103
|
+
tokenForm.style.display = 'block';
|
|
104
|
+
basicForm.style.display = 'none';
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Authenticated fetch wrapper - adds Authorization header
|
|
47
111
|
async function authFetch (url, options = {}) {
|
|
48
112
|
const token = getStoredToken();
|
|
49
113
|
|
|
50
|
-
if (
|
|
114
|
+
if (requiresFrontendAuth && token) {
|
|
51
115
|
options.headers = {
|
|
52
116
|
...options.headers,
|
|
53
|
-
'Authorization':
|
|
117
|
+
'Authorization': token,
|
|
54
118
|
};
|
|
55
119
|
}
|
|
56
120
|
|
|
57
121
|
const response = await fetch(url, options);
|
|
58
122
|
|
|
59
|
-
// Handle 401 Unauthorized
|
|
60
|
-
if (response.status === 401 &&
|
|
123
|
+
// Handle 401 Unauthorized
|
|
124
|
+
if (response.status === 401 && requiresFrontendAuth) {
|
|
61
125
|
clearStoredToken();
|
|
62
126
|
const errorData = await response.json().catch(() => ({}));
|
|
63
127
|
const errorMessage = errorData.error || 'Authentication failed';
|
|
64
128
|
|
|
65
|
-
// Try to show modal, but if it's not available, throw with descriptive error
|
|
66
129
|
const modal = document.getElementById('tokenModal');
|
|
67
130
|
if (modal) {
|
|
68
|
-
|
|
131
|
+
showAuthModal(errorMessage + '. Please authenticate again.');
|
|
69
132
|
}
|
|
70
133
|
|
|
71
|
-
// Throw error with status code for form error handling
|
|
72
134
|
const error = new Error(`401 Unauthorized: ${errorMessage}`);
|
|
73
135
|
error.status = 401;
|
|
74
136
|
throw error;
|
|
@@ -84,13 +146,15 @@ async function initializeAuth () {
|
|
|
84
146
|
const response = await fetch('/admin/api/auth-config');
|
|
85
147
|
const config = await response.json();
|
|
86
148
|
|
|
87
|
-
if (config.success && config.
|
|
88
|
-
|
|
149
|
+
if (config.success && config.requiresFrontendAuth) {
|
|
150
|
+
requiresFrontendAuth = true;
|
|
151
|
+
authMethods = config.methods || [];
|
|
152
|
+
setupAuthForms(authMethods);
|
|
89
153
|
|
|
90
154
|
// Check if we have a stored token
|
|
91
155
|
const storedToken = getStoredToken();
|
|
92
156
|
if (!storedToken) {
|
|
93
|
-
|
|
157
|
+
showAuthModal();
|
|
94
158
|
return false;
|
|
95
159
|
}
|
|
96
160
|
|
|
@@ -101,13 +165,36 @@ async function initializeAuth () {
|
|
|
101
165
|
|
|
102
166
|
if (!verifyData.success || !verifyData.isAuthenticated) {
|
|
103
167
|
clearStoredToken();
|
|
104
|
-
|
|
168
|
+
showAuthModal('Token is invalid or expired.');
|
|
105
169
|
return false;
|
|
106
170
|
}
|
|
107
171
|
} catch {
|
|
108
172
|
// authFetch already handles 401 and shows modal
|
|
109
173
|
return false;
|
|
110
174
|
}
|
|
175
|
+
} else if (config.success && config.requiresBearerToken) {
|
|
176
|
+
// Backward compat: old-style bearer-only
|
|
177
|
+
requiresFrontendAuth = true;
|
|
178
|
+
authMethods = ['token'];
|
|
179
|
+
setupAuthForms(authMethods);
|
|
180
|
+
|
|
181
|
+
const storedToken = getStoredToken();
|
|
182
|
+
if (!storedToken) {
|
|
183
|
+
showAuthModal();
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const verifyResponse = await authFetch('/admin/api/auth-status');
|
|
189
|
+
const verifyData = await verifyResponse.json();
|
|
190
|
+
if (!verifyData.success || !verifyData.isAuthenticated) {
|
|
191
|
+
clearStoredToken();
|
|
192
|
+
showAuthModal('Token is invalid or expired.');
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
111
198
|
}
|
|
112
199
|
|
|
113
200
|
return true;
|
|
@@ -129,32 +216,81 @@ function setupTokenAuthForm () {
|
|
|
129
216
|
const token = tokenInput.value.trim();
|
|
130
217
|
|
|
131
218
|
if (!token) {
|
|
132
|
-
|
|
219
|
+
showAuthModal('Please enter a token.');
|
|
133
220
|
return;
|
|
134
221
|
}
|
|
135
222
|
|
|
136
|
-
// Store
|
|
137
|
-
storeToken(token);
|
|
223
|
+
// Store as Bearer token
|
|
224
|
+
storeToken(`Bearer ${token}`);
|
|
138
225
|
|
|
139
226
|
try {
|
|
140
227
|
const response = await authFetch('/admin/api/auth-status');
|
|
141
228
|
const data = await response.json();
|
|
142
229
|
|
|
143
230
|
if (data.success && data.isAuthenticated) {
|
|
144
|
-
|
|
231
|
+
hideAuthModal();
|
|
145
232
|
tokenInput.value = '';
|
|
146
|
-
// Reload auth status and initialize form
|
|
147
233
|
loadAuthStatus();
|
|
148
234
|
initializeForm();
|
|
149
235
|
} else {
|
|
150
236
|
clearStoredToken();
|
|
151
|
-
|
|
237
|
+
showAuthModal(data.error || 'Invalid token.');
|
|
152
238
|
}
|
|
153
239
|
} catch (error) {
|
|
154
|
-
// Error already handled in authFetch
|
|
155
240
|
if (error.message !== 'Unauthorized') {
|
|
156
241
|
clearStoredToken();
|
|
157
|
-
|
|
242
|
+
showAuthModal('Authentication failed: ' + error.message);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle basic authentication form submission
|
|
249
|
+
function setupBasicAuthForm () {
|
|
250
|
+
const form = document.getElementById('basicAuthForm');
|
|
251
|
+
if (!form) {return;}
|
|
252
|
+
|
|
253
|
+
form.addEventListener('submit', async (e) => {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
|
|
256
|
+
const usernameInput = document.getElementById('authUsername');
|
|
257
|
+
const passwordInput = document.getElementById('authPassword');
|
|
258
|
+
const username = usernameInput.value.trim();
|
|
259
|
+
const password = passwordInput.value;
|
|
260
|
+
|
|
261
|
+
if (!username || !password) {
|
|
262
|
+
const errorDiv = document.getElementById('basicAuthError');
|
|
263
|
+
errorDiv.innerHTML = '<strong>Error:</strong> Please enter username and password.';
|
|
264
|
+
errorDiv.style.display = 'block';
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Store as Basic auth header
|
|
269
|
+
const encoded = btoa(`${username}:${password}`);
|
|
270
|
+
storeToken(`Basic ${encoded}`);
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const response = await authFetch('/admin/api/auth-status');
|
|
274
|
+
const data = await response.json();
|
|
275
|
+
|
|
276
|
+
if (data.success && data.isAuthenticated) {
|
|
277
|
+
hideAuthModal();
|
|
278
|
+
usernameInput.value = '';
|
|
279
|
+
passwordInput.value = '';
|
|
280
|
+
loadAuthStatus();
|
|
281
|
+
initializeForm();
|
|
282
|
+
} else {
|
|
283
|
+
clearStoredToken();
|
|
284
|
+
const errorDiv = document.getElementById('basicAuthError');
|
|
285
|
+
errorDiv.innerHTML = `<strong>Error:</strong> ${data.error || 'Invalid credentials.'}`;
|
|
286
|
+
errorDiv.style.display = 'block';
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error.status !== 401) {
|
|
290
|
+
clearStoredToken();
|
|
291
|
+
const errorDiv = document.getElementById('basicAuthError');
|
|
292
|
+
errorDiv.innerHTML = `<strong>Error:</strong> Authentication failed: ${error.message}`;
|
|
293
|
+
errorDiv.style.display = 'block';
|
|
158
294
|
}
|
|
159
295
|
}
|
|
160
296
|
});
|
|
@@ -514,10 +650,10 @@ async function initializeForm () {
|
|
|
514
650
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
515
651
|
async function logout () {
|
|
516
652
|
try {
|
|
517
|
-
// For
|
|
518
|
-
if (
|
|
653
|
+
// For frontend auth, clear the stored credentials
|
|
654
|
+
if (requiresFrontendAuth) {
|
|
519
655
|
clearStoredToken();
|
|
520
|
-
|
|
656
|
+
showAuthModal();
|
|
521
657
|
// Clear auth status display
|
|
522
658
|
const container = document.getElementById('authStatusContainer');
|
|
523
659
|
if (container) {
|
|
@@ -547,8 +683,9 @@ async function logout () {
|
|
|
547
683
|
|
|
548
684
|
// Initialization on page load
|
|
549
685
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
550
|
-
// Setup
|
|
686
|
+
// Setup auth form handlers
|
|
551
687
|
setupTokenAuthForm();
|
|
688
|
+
setupBasicAuthForm();
|
|
552
689
|
|
|
553
690
|
// Initialize authentication (check if token is needed and valid)
|
|
554
691
|
const authOk = await initializeAuth();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fa-mcp-sdk",
|
|
3
3
|
"productName": "FA MCP SDK",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.30",
|
|
5
5
|
"description": "Core infrastructure and templates for building Model Context Protocol (MCP) servers with TypeScript",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/core/index.js",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate JWT token for MCP server authentication.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>]
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* -u, --username Username (required). ENV: JWT_PAYLOAD_USERNAME
|
|
10
|
+
* -ttl Token lifetime: <N>s | <N>m | <N>d | <N>y (required). ENV: JWT_TTL
|
|
11
|
+
* -s, --service-name Service name (optional). ENV: JWT_PAYLOAD_SERVICE_NAME
|
|
12
|
+
* -p, --params Extra payload "key=value;key=value" (optional). ENV: JWT_PAYLOAD_PARAMS
|
|
13
|
+
*
|
|
14
|
+
* The encryptKey is read from config: webServer.auth.jwtToken.encryptKey
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from 'crypto';
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname, resolve } from 'path';
|
|
21
|
+
import configModule from 'config';
|
|
22
|
+
|
|
23
|
+
// ── CLI argument parsing ────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function getArg (shortFlag, longFlag) {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] === shortFlag || args[i] === longFlag) {
|
|
29
|
+
return args[i + 1] || '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const username = getArg('-u', '--username') ?? process.env.JWT_PAYLOAD_USERNAME;
|
|
36
|
+
const ttlRaw = getArg('-ttl', '-ttl') ?? process.env.JWT_TTL;
|
|
37
|
+
const service = getArg('-s', '--service-name') ?? process.env.JWT_PAYLOAD_SERVICE_NAME;
|
|
38
|
+
const paramsRaw = getArg('-p', '--params') ?? process.env.JWT_PAYLOAD_PARAMS;
|
|
39
|
+
|
|
40
|
+
// ── Validation ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
if (!username || !username.trim()) {
|
|
43
|
+
console.error('Error: username is required (-u / --username or ENV JWT_PAYLOAD_USERNAME)');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!ttlRaw || !ttlRaw.trim()) {
|
|
48
|
+
console.error('Error: TTL is required (-ttl or ENV JWT_TTL). Format: <N>s | <N>m | <N>d | <N>y');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ttlMatch = /^(\d+)([smdy])$/.exec(ttlRaw.trim());
|
|
53
|
+
if (!ttlMatch) {
|
|
54
|
+
console.error(`Error: invalid TTL format "${ttlRaw}". Expected: <N>s | <N>m | <N>d | <N>y`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ttlValue = parseInt(ttlMatch[1], 10);
|
|
59
|
+
const ttlUnit = ttlMatch[2];
|
|
60
|
+
|
|
61
|
+
if (ttlValue <= 0) {
|
|
62
|
+
console.error('Error: TTL value must be greater than 0');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const TTL_MULTIPLIERS = { s: 1, m: 60, d: 86400, y: 31536000 };
|
|
67
|
+
const liveTimeSec = ttlValue * TTL_MULTIPLIERS[ttlUnit];
|
|
68
|
+
|
|
69
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
let encryptKey;
|
|
72
|
+
try {
|
|
73
|
+
encryptKey = configModule.get('webServer.auth.jwtToken.encryptKey');
|
|
74
|
+
} catch {
|
|
75
|
+
// config key not found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!encryptKey || String(encryptKey).trim() === '' || encryptKey === '***') {
|
|
79
|
+
console.error('Error: webServer.auth.jwtToken.encryptKey is not configured or has a placeholder value.');
|
|
80
|
+
console.error('Set it in config/local.yaml or via ENV WS_TOKEN_ENCRYPT_KEY');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Encryption (mirrors src/core/auth/jwt.ts) ───────────────────────
|
|
85
|
+
|
|
86
|
+
const ALGORITHM = 'aes-256-ctr';
|
|
87
|
+
const KEY = crypto
|
|
88
|
+
.createHash('sha256')
|
|
89
|
+
.update(String(encryptKey))
|
|
90
|
+
.digest('base64')
|
|
91
|
+
.substring(0, 32);
|
|
92
|
+
|
|
93
|
+
function encrypt (text) {
|
|
94
|
+
const buffer = Buffer.from(text);
|
|
95
|
+
const iv = crypto.randomBytes(16);
|
|
96
|
+
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
|
|
97
|
+
const encryptedBuf = Buffer.concat([iv, cipher.update(buffer), cipher.final()]);
|
|
98
|
+
return encryptedBuf.toString('hex');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Auto-detect service name if checkMCPName is enabled ─────────────
|
|
102
|
+
|
|
103
|
+
let effectiveService = service;
|
|
104
|
+
|
|
105
|
+
if ((!effectiveService || !effectiveService.trim())) {
|
|
106
|
+
let checkMCPName = false;
|
|
107
|
+
try {
|
|
108
|
+
checkMCPName = configModule.get('webServer.auth.jwtToken.checkMCPName');
|
|
109
|
+
} catch {
|
|
110
|
+
// config key not found
|
|
111
|
+
}
|
|
112
|
+
if (checkMCPName) {
|
|
113
|
+
// 1) Try SERVICE_NAME from .env
|
|
114
|
+
if (process.env.SERVICE_NAME && process.env.SERVICE_NAME.trim()) {
|
|
115
|
+
effectiveService = process.env.SERVICE_NAME.trim();
|
|
116
|
+
} else {
|
|
117
|
+
// 2) Fallback to package.json name
|
|
118
|
+
try {
|
|
119
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
120
|
+
const __dirname = dirname(__filename);
|
|
121
|
+
const pkgPath = resolve(__dirname, '..', 'package.json');
|
|
122
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
123
|
+
if (pkg.name) {
|
|
124
|
+
effectiveService = pkg.name;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// package.json not found or unreadable
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Build payload ───────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
const payload = {};
|
|
136
|
+
payload.user = username.trim().toLowerCase();
|
|
137
|
+
|
|
138
|
+
if (effectiveService && effectiveService.trim()) {
|
|
139
|
+
payload.service = effectiveService.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Parse extra params: "key1=value1;key2=value2"
|
|
143
|
+
if (paramsRaw && paramsRaw.trim()) {
|
|
144
|
+
const pairs = paramsRaw.trim().split(';');
|
|
145
|
+
for (const pair of pairs) {
|
|
146
|
+
const eqIdx = pair.indexOf('=');
|
|
147
|
+
if (eqIdx <= 0) {
|
|
148
|
+
console.error(`Error: invalid param format "${pair}". Expected "key=value"`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const key = pair.substring(0, eqIdx).trim();
|
|
152
|
+
const value = pair.substring(eqIdx + 1).trim();
|
|
153
|
+
if (!key) {
|
|
154
|
+
console.error(`Error: empty key in param "${pair}"`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
payload[key] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const expire = Date.now() + (liveTimeSec * 1000);
|
|
162
|
+
payload.expire = expire;
|
|
163
|
+
payload.iat = new Date().toISOString();
|
|
164
|
+
|
|
165
|
+
// ── Generate token ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const token = `${expire}.${encrypt(JSON.stringify(payload))}`;
|
|
168
|
+
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log('JWT Token generated successfully');
|
|
171
|
+
console.log('─'.repeat(50));
|
|
172
|
+
console.log(` User: ${payload.user}`);
|
|
173
|
+
if (payload.service) {
|
|
174
|
+
console.log(` Service: ${payload.service}`);
|
|
175
|
+
}
|
|
176
|
+
console.log(` TTL: ${ttlRaw} (${liveTimeSec} seconds)`);
|
|
177
|
+
console.log(` Expires: ${new Date(expire).toISOString()}`);
|
|
178
|
+
if (Object.keys(payload).filter((k) => !['user', 'service', 'expire', 'iat'].includes(k)).length) {
|
|
179
|
+
const extra = Object.entries(payload)
|
|
180
|
+
.filter(([k]) => !['user', 'service', 'expire', 'iat'].includes(k))
|
|
181
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
182
|
+
.join('; ');
|
|
183
|
+
console.log(` Params: ${extra}`);
|
|
184
|
+
}
|
|
185
|
+
console.log('─'.repeat(50));
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(token);
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('__PAYLOAD_JSON__');
|
|
190
|
+
console.log(JSON.stringify({ ...payload, ttl: ttlRaw, expire_iso: new Date(expire).toISOString() }));
|
|
191
|
+
console.log('__END_PAYLOAD_JSON__');
|