creatorverse-cli 0.0.0-stage → 1.0.1
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 +97 -2
- package/bin/creatorverse +7 -0
- package/bin/creatorverse-admin +7 -0
- package/bin/creatorverse-admin.cmd +16 -0
- package/bin/creatorverse-admin.ps1 +8 -0
- package/bin/creatorverse.cmd +16 -0
- package/bin/creatorverse.ps1 +8 -0
- package/dist/creatorverse-admin.d.ts +63 -0
- package/dist/creatorverse-admin.d.ts.map +1 -0
- package/dist/creatorverse-admin.js +678 -0
- package/dist/creatorverse-admin.js.map +1 -0
- package/dist/creatorverse.d.ts +41 -0
- package/dist/creatorverse.d.ts.map +1 -0
- package/dist/creatorverse.js +458 -0
- package/dist/creatorverse.js.map +1 -0
- package/dist/credentialManager.d.ts +12 -0
- package/dist/credentialManager.d.ts.map +1 -0
- package/dist/credentialManager.js +96 -0
- package/dist/credentialManager.js.map +1 -0
- package/dist/loopbackListener.d.ts +17 -0
- package/dist/loopbackListener.d.ts.map +1 -0
- package/dist/loopbackListener.js +161 -0
- package/dist/loopbackListener.js.map +1 -0
- package/package.json +29 -4
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CreatorVerse Admin CLI — Stage E1.2 & E1.3 Controlled Admin CLI Integration
|
|
4
|
+
*
|
|
5
|
+
* Secure Administrative Command-Line Interface connecting to:
|
|
6
|
+
* - /api/admin/cli-session (POST)
|
|
7
|
+
* - /api/admin/cli-session/verify (GET)
|
|
8
|
+
* - /api/admin/cli-session/revoke (POST)
|
|
9
|
+
* - /api/admin/cli-session/audit-logs (GET)
|
|
10
|
+
* - /api/cli/auth/exchange (POST)
|
|
11
|
+
*
|
|
12
|
+
* Security Guardrails:
|
|
13
|
+
* 1. ADMIN_CLI_PRODUCTION_ENABLED = false: Disables live production execution pending verified browser auth.
|
|
14
|
+
* 2. Mandatory MFA interactive masking / Browser MFA: TOTP codes are entered privately without exposure in shell history, argv, or process listings.
|
|
15
|
+
* 3. Server-authoritative privilege checks: Local tokens/roles/permissions are never trusted; backend enforces RBAC, device binding, and revocation.
|
|
16
|
+
* 4. AES-256-GCM encrypted credential vault with strict 0o600 file / 0o700 directory permissions.
|
|
17
|
+
* 5. Safe network timeouts (5000ms AbortController) and fail-closed handling on network or authentication failure.
|
|
18
|
+
* 6. Total isolation between production and offline test mode (retaining 100% backward compatibility for existing suites).
|
|
19
|
+
*/
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import readline from 'readline';
|
|
23
|
+
import { saveSecureSession, loadSecureSession, clearSecureSession, redactSensitiveData } from './credentialManager.js';
|
|
24
|
+
import { startLoopbackListener, generatePkcePair } from './loopbackListener.js';
|
|
25
|
+
export const VERSION = '1.0.1';
|
|
26
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.creatorverse');
|
|
27
|
+
export const ADMIN_CONFIG_FILE = path.join(CONFIG_DIR, 'admin-auth-secure.enc');
|
|
28
|
+
// Production Gate: CLI disabled for live production access pending verified browser auth flow
|
|
29
|
+
export const ADMIN_CLI_PRODUCTION_ENABLED = false;
|
|
30
|
+
export function getStoredAdminSession() {
|
|
31
|
+
const session = loadSecureSession(ADMIN_CONFIG_FILE);
|
|
32
|
+
if (session && session.expiresAt && Date.now() > session.expiresAt) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
export async function fetchWithTimeout(url, options, timeoutMs = 5000) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
clearTimeout(timeoutId);
|
|
47
|
+
if (err.name === 'AbortError') {
|
|
48
|
+
const timeoutErr = new Error(`Network timeout: server failed to respond within ${timeoutMs}ms.`);
|
|
49
|
+
timeoutErr.code = 'NETWORK_TIMEOUT';
|
|
50
|
+
throw timeoutErr;
|
|
51
|
+
}
|
|
52
|
+
const netErr = new Error(`Network error connecting to administrative endpoint: ${err.message}`);
|
|
53
|
+
netErr.code = 'SERVICE_UNAVAILABLE';
|
|
54
|
+
throw netErr;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function promptMfaInteractive(providedCode, isTestMode = false) {
|
|
58
|
+
if (providedCode) {
|
|
59
|
+
if (!isTestMode) {
|
|
60
|
+
throw new Error('MFA_ARGV_DISALLOWED: Passing MFA codes via command line flags outside explicit test mode is prohibited to prevent exposure in shell history and process tables.');
|
|
61
|
+
}
|
|
62
|
+
return providedCode.trim();
|
|
63
|
+
}
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
if (!process.stdin.isTTY) {
|
|
66
|
+
let input = '';
|
|
67
|
+
process.stdin.setEncoding('utf-8');
|
|
68
|
+
process.stdin.on('data', (chunk) => {
|
|
69
|
+
input += chunk;
|
|
70
|
+
});
|
|
71
|
+
process.stdin.on('end', () => {
|
|
72
|
+
resolve(input.trim());
|
|
73
|
+
});
|
|
74
|
+
process.stdin.on('error', reject);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const rl = readline.createInterface({
|
|
78
|
+
input: process.stdin,
|
|
79
|
+
output: process.stdout,
|
|
80
|
+
terminal: true,
|
|
81
|
+
});
|
|
82
|
+
process.stdout.write('Enter Multi-Factor Authentication (TOTP) code: ');
|
|
83
|
+
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
84
|
+
if (stringToWrite.charCodeAt(0) === 13 || stringToWrite.charCodeAt(0) === 10) {
|
|
85
|
+
process.stdout.write('\n');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
process.stdout.write('*');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
rl.question('', (answer) => {
|
|
92
|
+
rl.close();
|
|
93
|
+
resolve(answer.trim());
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function validateEnvironmentSecurity(options) {
|
|
98
|
+
const effectiveApiUrl = options.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
99
|
+
const isProdEnv = process.env.CREATORVERSE_PRODUCTION === 'true' ||
|
|
100
|
+
process.env.NODE_ENV === 'production' ||
|
|
101
|
+
effectiveApiUrl.includes('api.creatorverse.network') ||
|
|
102
|
+
effectiveApiUrl.includes('creatorverse.network');
|
|
103
|
+
if (isProdEnv) {
|
|
104
|
+
if (options.isOffline) {
|
|
105
|
+
const err = new Error('Offline test mode and mock credentials cannot be used in production environments.');
|
|
106
|
+
err.code = 'OFFLINE_MODE_PROHIBITED_IN_PRODUCTION';
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
if (options.token) {
|
|
110
|
+
const err = new Error('Direct-token authentication is strictly prohibited in production mode. Interactive browser-based authentication with MFA is required.');
|
|
111
|
+
err.code = 'DIRECT_TOKEN_LOGIN_PROHIBITED';
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
if (!ADMIN_CLI_PRODUCTION_ENABLED) {
|
|
115
|
+
const err = new Error('CreatorVerse Admin CLI production access is disabled pending verified browser-based authentication flow. Use local or staging development environment.');
|
|
116
|
+
err.code = 'PRODUCTION_ACCESS_PENDING';
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
if (effectiveApiUrl.startsWith('http://')) {
|
|
120
|
+
const err = new Error('Insecure HTTP connection is strictly forbidden in production mode. Use verified HTTPS.');
|
|
121
|
+
err.code = 'INSECURE_PROTOCOL_REJECTED';
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
if (options.mfaCode === '123456' || options.mfaCode === 'valid-mfa') {
|
|
125
|
+
const err = new Error('Mock tokens and hardcoded test MFA codes are strictly forbidden in production-facing execution paths.');
|
|
126
|
+
err.code = 'TEST_CREDENTIALS_REJECTED_IN_PRODUCTION';
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function executeAdminLogin(options) {
|
|
132
|
+
validateEnvironmentSecurity(options);
|
|
133
|
+
const effectiveApiUrl = options.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
134
|
+
const isTestMode = options.isTestMode ?? (process.env.CREATORVERSE_TEST_MODE === '1' || process.env.NODE_ENV === 'test');
|
|
135
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(process.env.CREATORVERSE_API_URL) || process.env.CREATORVERSE_REMOTE === '1' || Boolean(options.isBrowser);
|
|
136
|
+
const isOffline = options.isOffline || (!isRemote && !options.isBrowser);
|
|
137
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
138
|
+
const token = options.token || 'mock-admin-token-secure';
|
|
139
|
+
const email = options.email || 'admin@creatorverse.local';
|
|
140
|
+
const uid = 'admin-uid-999';
|
|
141
|
+
const role = options.role || 'admin';
|
|
142
|
+
const deviceId = options.deviceId !== undefined ? options.deviceId : 'default-device-id';
|
|
143
|
+
const deviceName = options.deviceName || 'Admin Workstation';
|
|
144
|
+
// 1. Browser-Assisted PKCE Login Flow
|
|
145
|
+
if (options.isBrowser) {
|
|
146
|
+
const { codeVerifier, codeChallenge, state } = generatePkcePair();
|
|
147
|
+
const listener = await startLoopbackListener(state, 120000);
|
|
148
|
+
const authUrl = `${effectiveApiUrl}/cli-auth?port=${listener.port}&state=${state}&code_challenge=${codeChallenge}&client_type=admin&device_id=${encodeURIComponent(deviceId)}`;
|
|
149
|
+
let ticket = '';
|
|
150
|
+
try {
|
|
151
|
+
if (process.stdout.isTTY) {
|
|
152
|
+
console.log(`\nOpen the following URL in your browser to complete administrator authentication & MFA:\n\n ${authUrl}\n`);
|
|
153
|
+
console.log(`Waiting for browser callback on 127.0.0.1:${listener.port} (timeout: 120s)...`);
|
|
154
|
+
}
|
|
155
|
+
ticket = await listener.waitForCallback();
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
listener.shutdown();
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
const exchangeRes = await fetchWithTimeout(`${effectiveApiUrl}/api/cli/auth/exchange`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({ ticket, codeVerifier, state }),
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
const resJson = await exchangeRes.json();
|
|
170
|
+
if (!exchangeRes.ok || !resJson.success) {
|
|
171
|
+
const err = new Error(resJson.error || 'Administrative ticket exchange failed.');
|
|
172
|
+
err.code = resJson.code || 'EXCHANGE_FAILED';
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
const sessionRecord = {
|
|
176
|
+
sessionId: resJson.sessionId,
|
|
177
|
+
sessionToken: resJson.sessionToken,
|
|
178
|
+
token: resJson.sessionToken,
|
|
179
|
+
uid: resJson.uid,
|
|
180
|
+
email: resJson.email,
|
|
181
|
+
role: resJson.role,
|
|
182
|
+
permissions: resJson.permissions,
|
|
183
|
+
deviceId: resJson.deviceId,
|
|
184
|
+
apiUrl: effectiveApiUrl,
|
|
185
|
+
mfaVerified: true,
|
|
186
|
+
expiresAt: Date.now() + (resJson.expiresInSeconds * 1000)
|
|
187
|
+
};
|
|
188
|
+
saveSecureSession(ADMIN_CONFIG_FILE, sessionRecord);
|
|
189
|
+
return redactSensitiveData({
|
|
190
|
+
status: 'SUCCESS',
|
|
191
|
+
message: 'Admin session authenticated and registered via browser PKCE authorization.',
|
|
192
|
+
sessionId: resJson.sessionId,
|
|
193
|
+
uid: resJson.uid,
|
|
194
|
+
email: resJson.email,
|
|
195
|
+
role: resJson.role,
|
|
196
|
+
mfaVerified: true,
|
|
197
|
+
token: resJson.sessionToken,
|
|
198
|
+
expiresInSeconds: resJson.expiresInSeconds
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// 2. Direct Remote Session Issuance
|
|
202
|
+
if (isRemote && !isOffline) {
|
|
203
|
+
let mfaCode = '';
|
|
204
|
+
mfaCode = await promptMfaInteractive(options.mfaCode, isTestMode);
|
|
205
|
+
if (!mfaCode) {
|
|
206
|
+
const err = new Error('Administrative authentication requires valid Multi-Factor Authentication.');
|
|
207
|
+
err.code = 'MFA_REQUIRED';
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
const response = await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session`, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: {
|
|
213
|
+
'Authorization': `Bearer ${token}`,
|
|
214
|
+
'Content-Type': 'application/json',
|
|
215
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify({ mfaCode, deviceId, deviceName }),
|
|
218
|
+
}, timeoutMs);
|
|
219
|
+
const resJson = await response.json();
|
|
220
|
+
if (!response.ok || !resJson.success) {
|
|
221
|
+
const err = new Error(resJson.error || 'Administrative session issuance failed.');
|
|
222
|
+
err.code = resJson.code || 'AUTHENTICATION_FAILED';
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
const sessionRecord = {
|
|
226
|
+
sessionId: resJson.sessionId,
|
|
227
|
+
sessionToken: resJson.sessionToken,
|
|
228
|
+
token: resJson.sessionToken,
|
|
229
|
+
uid: resJson.uid,
|
|
230
|
+
email: resJson.email,
|
|
231
|
+
role: resJson.role,
|
|
232
|
+
permissions: resJson.permissions,
|
|
233
|
+
deviceId: resJson.deviceId,
|
|
234
|
+
apiUrl: effectiveApiUrl,
|
|
235
|
+
mfaVerified: true,
|
|
236
|
+
expiresAt: Date.now() + (resJson.expiresInSeconds * 1000)
|
|
237
|
+
};
|
|
238
|
+
saveSecureSession(ADMIN_CONFIG_FILE, sessionRecord);
|
|
239
|
+
return redactSensitiveData({
|
|
240
|
+
status: 'SUCCESS',
|
|
241
|
+
message: 'Admin session authenticated and registered via secure administrative session service.',
|
|
242
|
+
sessionId: resJson.sessionId,
|
|
243
|
+
uid: resJson.uid,
|
|
244
|
+
email: resJson.email,
|
|
245
|
+
role: resJson.role,
|
|
246
|
+
mfaVerified: true,
|
|
247
|
+
token: resJson.sessionToken,
|
|
248
|
+
expiresInSeconds: resJson.expiresInSeconds
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// 3. Offline / Sandbox mode
|
|
253
|
+
let mfaProvided = false;
|
|
254
|
+
if (options.mfaCode) {
|
|
255
|
+
if (isTestMode && (options.mfaCode === '123456' || options.mfaCode === 'valid-mfa')) {
|
|
256
|
+
mfaProvided = true;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
mfaProvided = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!mfaProvided) {
|
|
263
|
+
const err = new Error('Administrative authentication requires valid Multi-Factor Authentication. Hardcoded test codes are disabled outside explicit test mode.');
|
|
264
|
+
err.code = 'MFA_REQUIRED';
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
if (role !== 'admin' && role !== 'platform-owner' && role !== 'approved-developer') {
|
|
268
|
+
const err = new Error('Insufficient privileges. Admin role verification failed.');
|
|
269
|
+
err.code = 'FORBIDDEN';
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
const session = {
|
|
273
|
+
token,
|
|
274
|
+
uid,
|
|
275
|
+
email,
|
|
276
|
+
role,
|
|
277
|
+
mfaVerified: true,
|
|
278
|
+
deviceId,
|
|
279
|
+
expiresAt: Date.now() + 1800 * 1000
|
|
280
|
+
};
|
|
281
|
+
saveSecureSession(ADMIN_CONFIG_FILE, session);
|
|
282
|
+
return redactSensitiveData({
|
|
283
|
+
status: 'SUCCESS',
|
|
284
|
+
message: 'Admin session authenticated with strict MFA verification and encrypted vault storage.',
|
|
285
|
+
uid: session.uid,
|
|
286
|
+
email: session.email,
|
|
287
|
+
role: session.role,
|
|
288
|
+
mfaVerified: true,
|
|
289
|
+
token: session.token,
|
|
290
|
+
expiresInSeconds: 1800
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export async function executeAdminLogout(options = {}) {
|
|
295
|
+
const session = loadSecureSession(ADMIN_CONFIG_FILE);
|
|
296
|
+
const effectiveApiUrl = options.apiUrl || session?.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
297
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(session?.apiUrl);
|
|
298
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
299
|
+
if (session && (session.sessionToken || session.token) && isRemote && !options.isOffline) {
|
|
300
|
+
try {
|
|
301
|
+
await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session/revoke`, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'Authorization': `Bearer ${session.sessionToken || session.token}`,
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
307
|
+
},
|
|
308
|
+
body: JSON.stringify({ sessionToken: session.sessionToken || session.token })
|
|
309
|
+
}, timeoutMs);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Local vault is always cleared regardless of remote network status
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
clearSecureSession(ADMIN_CONFIG_FILE);
|
|
316
|
+
return {
|
|
317
|
+
status: 'SUCCESS',
|
|
318
|
+
message: 'Admin session revoked and encrypted vault cleared.'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export async function executeAdminWhoami(options = {}) {
|
|
322
|
+
const session = getStoredAdminSession();
|
|
323
|
+
if (!session || !session.mfaVerified) {
|
|
324
|
+
const err = new Error('Active admin session with MFA verification required. Run "creatorverse-admin login".');
|
|
325
|
+
err.code = 'UNAUTHORIZED';
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
const effectiveApiUrl = options.apiUrl || session.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
329
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(session.apiUrl);
|
|
330
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
331
|
+
if (isRemote && !options.isOffline) {
|
|
332
|
+
const response = await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session/verify`, {
|
|
333
|
+
method: 'GET',
|
|
334
|
+
headers: {
|
|
335
|
+
'Authorization': `Bearer ${session.sessionToken || session.token}`,
|
|
336
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
337
|
+
}
|
|
338
|
+
}, timeoutMs);
|
|
339
|
+
const data = await response.json();
|
|
340
|
+
if (!response.ok || !data.valid) {
|
|
341
|
+
clearSecureSession(ADMIN_CONFIG_FILE);
|
|
342
|
+
const err = new Error(data.error || 'Admin session is invalid, expired or revoked on the server.');
|
|
343
|
+
err.code = data.code || 'UNAUTHORIZED_SESSION_REVOKED';
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
return redactSensitiveData({
|
|
347
|
+
authenticated: true,
|
|
348
|
+
sessionId: data.sessionId,
|
|
349
|
+
uid: data.uid,
|
|
350
|
+
email: data.email,
|
|
351
|
+
role: data.role,
|
|
352
|
+
mfaVerified: true,
|
|
353
|
+
deviceId: data.deviceId,
|
|
354
|
+
token: session.sessionToken || session.token,
|
|
355
|
+
expiresAt: new Date(Date.now() + (data.expiresInSeconds * 1000)).toISOString()
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
// Offline mode
|
|
359
|
+
return redactSensitiveData({
|
|
360
|
+
authenticated: true,
|
|
361
|
+
uid: session.uid,
|
|
362
|
+
email: session.email,
|
|
363
|
+
role: session.role,
|
|
364
|
+
mfaVerified: session.mfaVerified,
|
|
365
|
+
deviceId: session.deviceId,
|
|
366
|
+
token: session.token,
|
|
367
|
+
expiresAt: new Date(session.expiresAt).toISOString()
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
export async function executeAdminPermissions(options = {}) {
|
|
371
|
+
const session = getStoredAdminSession();
|
|
372
|
+
if (!session || !session.mfaVerified) {
|
|
373
|
+
const err = new Error('Admin authentication required to inspect permissions.');
|
|
374
|
+
err.code = 'UNAUTHORIZED';
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
const effectiveApiUrl = options.apiUrl || session.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
378
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(session.apiUrl);
|
|
379
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
380
|
+
if (isRemote && !options.isOffline) {
|
|
381
|
+
const response = await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session/verify`, {
|
|
382
|
+
method: 'GET',
|
|
383
|
+
headers: {
|
|
384
|
+
'Authorization': `Bearer ${session.sessionToken || session.token}`,
|
|
385
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
386
|
+
}
|
|
387
|
+
}, timeoutMs);
|
|
388
|
+
const data = await response.json();
|
|
389
|
+
if (!response.ok || !data.valid) {
|
|
390
|
+
clearSecureSession(ADMIN_CONFIG_FILE);
|
|
391
|
+
const err = new Error(data.error || 'Admin session has expired or been revoked.');
|
|
392
|
+
err.code = data.code || 'UNAUTHORIZED_SESSION_REVOKED';
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
role: data.role,
|
|
397
|
+
explicitPermissions: data.permissions || []
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Offline mode
|
|
401
|
+
const permissionsMap = {
|
|
402
|
+
'admin': ['audit-logs:read', 'system-status:read', 'user-moderation:write', 'content-moderation:write'],
|
|
403
|
+
'platform-owner': ['audit-logs:read', 'system-status:read', 'user-moderation:write', 'content-moderation:write', 'billing:manage', 'platform-config:write'],
|
|
404
|
+
'approved-developer': ['system-status:read', 'diagnostics:read', 'security-tests:run']
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
role: session.role,
|
|
408
|
+
explicitPermissions: permissionsMap[session.role] || []
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export async function executeAdminSessionStatus(options = {}) {
|
|
412
|
+
const session = getStoredAdminSession();
|
|
413
|
+
if (!session) {
|
|
414
|
+
return { active: false, message: 'No active secure admin session found.' };
|
|
415
|
+
}
|
|
416
|
+
const effectiveApiUrl = options.apiUrl || session.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
417
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(session.apiUrl);
|
|
418
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
419
|
+
if (isRemote && !options.isOffline) {
|
|
420
|
+
const response = await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session/verify`, {
|
|
421
|
+
method: 'GET',
|
|
422
|
+
headers: {
|
|
423
|
+
'Authorization': `Bearer ${session.sessionToken || session.token}`,
|
|
424
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
425
|
+
}
|
|
426
|
+
}, timeoutMs);
|
|
427
|
+
const data = await response.json();
|
|
428
|
+
if (!response.ok || !data.valid) {
|
|
429
|
+
return {
|
|
430
|
+
active: false,
|
|
431
|
+
error: data.code || 'SESSION_REVOKED',
|
|
432
|
+
message: data.error || 'Admin session is invalid, expired or revoked on the server.'
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return redactSensitiveData({
|
|
436
|
+
active: true,
|
|
437
|
+
sessionId: data.sessionId,
|
|
438
|
+
uid: data.uid,
|
|
439
|
+
email: data.email,
|
|
440
|
+
role: data.role,
|
|
441
|
+
mfaVerified: true,
|
|
442
|
+
token: session.sessionToken || session.token,
|
|
443
|
+
expiresInSeconds: data.expiresInSeconds
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const isValid = Date.now() <= session.expiresAt && session.mfaVerified;
|
|
447
|
+
return redactSensitiveData({
|
|
448
|
+
active: isValid,
|
|
449
|
+
uid: session.uid,
|
|
450
|
+
email: session.email,
|
|
451
|
+
role: session.role,
|
|
452
|
+
mfaVerified: session.mfaVerified,
|
|
453
|
+
token: session.token,
|
|
454
|
+
expiresInSeconds: Math.max(0, Math.floor((session.expiresAt - Date.now()) / 1000))
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
export async function executeAdminAuditLogs(options = {}) {
|
|
458
|
+
const session = getStoredAdminSession();
|
|
459
|
+
if (!session || !session.mfaVerified) {
|
|
460
|
+
if (options.isRemote && !options.isOffline) {
|
|
461
|
+
const err = new Error('Admin authentication required.');
|
|
462
|
+
err.code = 'UNAUTHORIZED';
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
mode: 'offline-stage-b-simulated',
|
|
467
|
+
logs: [
|
|
468
|
+
{ timestamp: new Date().toISOString(), action: 'SYSTEM_BOOT', status: 'SUCCESS' },
|
|
469
|
+
{ timestamp: new Date().toISOString(), action: 'SECURITY_CHECK_STAGE_B', status: 'PASSED' }
|
|
470
|
+
]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const effectiveApiUrl = options.apiUrl || session.apiUrl || process.env.CREATORVERSE_API_URL || 'http://localhost:3000';
|
|
474
|
+
const isRemote = options.isRemote || Boolean(options.apiUrl) || Boolean(session.apiUrl);
|
|
475
|
+
const timeoutMs = options.timeoutMs || 5000;
|
|
476
|
+
if (isRemote && !options.isOffline) {
|
|
477
|
+
const response = await fetchWithTimeout(`${effectiveApiUrl}/api/admin/cli-session/audit-logs`, {
|
|
478
|
+
method: 'GET',
|
|
479
|
+
headers: {
|
|
480
|
+
'Authorization': `Bearer ${session.sessionToken || session.token}`,
|
|
481
|
+
'User-Agent': `CreatorVerse-Admin-CLI/${VERSION}`
|
|
482
|
+
}
|
|
483
|
+
}, timeoutMs);
|
|
484
|
+
const data = await response.json();
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const err = new Error(data.error || 'Failed to access audit logs.');
|
|
487
|
+
err.code = data.code || 'FORBIDDEN';
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
return redactSensitiveData(data);
|
|
491
|
+
}
|
|
492
|
+
// Offline mode
|
|
493
|
+
return redactSensitiveData({
|
|
494
|
+
mode: 'secure-stage-c',
|
|
495
|
+
adminUid: session.uid,
|
|
496
|
+
logs: [
|
|
497
|
+
{ timestamp: new Date().toISOString(), action: 'ADMIN_LOGIN_SUCCESS', admin: session.email, mfa: true, device: session.deviceId },
|
|
498
|
+
{ timestamp: new Date().toISOString(), action: 'AUDIT_LOGS_ACCESSED', status: 'AUTHORIZED' }
|
|
499
|
+
]
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
export function printHelp() {
|
|
503
|
+
console.log(`
|
|
504
|
+
CreatorVerse Admin CLI (v${VERSION})
|
|
505
|
+
|
|
506
|
+
USAGE:
|
|
507
|
+
creatorverse-admin <command> [options]
|
|
508
|
+
|
|
509
|
+
COMMANDS:
|
|
510
|
+
--version, -v Display Admin CLI version
|
|
511
|
+
--help, -h Display this help message
|
|
512
|
+
login Authenticate admin session (browser PKCE or encrypted vault)
|
|
513
|
+
logout Revoke admin session on server and clear local vault
|
|
514
|
+
whoami Display authenticated admin identity (verified with backend)
|
|
515
|
+
permissions Display server-authoritative RBAC permission matrix
|
|
516
|
+
session status Check admin session freshness and server validity
|
|
517
|
+
audit-logs Display server audit logs (requires audit-logs:read permission)
|
|
518
|
+
system-status Show system health status
|
|
519
|
+
verify-mfa-mock Simulate MFA verification check
|
|
520
|
+
|
|
521
|
+
OPTIONS:
|
|
522
|
+
--browser Launch interactive browser-assisted PKCE authorization flow
|
|
523
|
+
--api-url <url> Target backend API endpoint URL
|
|
524
|
+
--remote Execute against backend API instead of local offline sandbox
|
|
525
|
+
--offline Explicit offline test mode
|
|
526
|
+
--token <token> Firebase ID token or test token
|
|
527
|
+
--mfa-code <code> TOTP code (allowed strictly in test mode; interactive prompt required otherwise)
|
|
528
|
+
--device-id <id> Unique workstation device identifier
|
|
529
|
+
--device-name <name> Friendly workstation device name
|
|
530
|
+
--timeout <ms> Network request timeout in milliseconds (default: 5000)
|
|
531
|
+
`);
|
|
532
|
+
}
|
|
533
|
+
export async function main() {
|
|
534
|
+
const args = process.argv.slice(2);
|
|
535
|
+
const command = args[0];
|
|
536
|
+
const subCommand = args[1];
|
|
537
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
538
|
+
printHelp();
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
542
|
+
console.log(`creatorverse-admin v${VERSION}`);
|
|
543
|
+
process.exit(0);
|
|
544
|
+
}
|
|
545
|
+
const options = {};
|
|
546
|
+
for (let i = 1; i < args.length; i++) {
|
|
547
|
+
if (args[i] === '--api-url' && args[i + 1]) {
|
|
548
|
+
options.apiUrl = args[i + 1];
|
|
549
|
+
options.isRemote = true;
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
else if (args[i] === '--remote') {
|
|
553
|
+
options.isRemote = true;
|
|
554
|
+
}
|
|
555
|
+
else if (args[i] === '--browser') {
|
|
556
|
+
options.isBrowser = true;
|
|
557
|
+
options.isRemote = true;
|
|
558
|
+
}
|
|
559
|
+
else if (args[i] === '--offline') {
|
|
560
|
+
options.isOffline = true;
|
|
561
|
+
}
|
|
562
|
+
else if (args[i] === '--token' && args[i + 1]) {
|
|
563
|
+
options.token = args[i + 1];
|
|
564
|
+
i++;
|
|
565
|
+
}
|
|
566
|
+
else if (args[i] === '--email' && args[i + 1]) {
|
|
567
|
+
options.email = args[i + 1];
|
|
568
|
+
i++;
|
|
569
|
+
}
|
|
570
|
+
else if (args[i] === '--role' && args[i + 1]) {
|
|
571
|
+
options.role = args[i + 1];
|
|
572
|
+
i++;
|
|
573
|
+
}
|
|
574
|
+
else if (args[i] === '--mfa-code' && args[i + 1]) {
|
|
575
|
+
options.mfaCode = args[i + 1];
|
|
576
|
+
i++;
|
|
577
|
+
}
|
|
578
|
+
else if (args[i] === '--device-id' && args[i + 1]) {
|
|
579
|
+
options.deviceId = args[i + 1];
|
|
580
|
+
i++;
|
|
581
|
+
}
|
|
582
|
+
else if (args[i] === '--device-name' && args[i + 1]) {
|
|
583
|
+
options.deviceName = args[i + 1];
|
|
584
|
+
i++;
|
|
585
|
+
}
|
|
586
|
+
else if (args[i] === '--timeout' && args[i + 1]) {
|
|
587
|
+
options.timeoutMs = parseInt(args[i + 1], 10) || 5000;
|
|
588
|
+
i++;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
switch (command) {
|
|
593
|
+
case 'login': {
|
|
594
|
+
const res = await executeAdminLogin(options);
|
|
595
|
+
console.log(JSON.stringify(res, null, 2));
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case 'logout': {
|
|
599
|
+
const res = await executeAdminLogout(options);
|
|
600
|
+
console.log(JSON.stringify(res, null, 2));
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case 'whoami': {
|
|
604
|
+
const res = await executeAdminWhoami(options);
|
|
605
|
+
console.log(JSON.stringify(res, null, 2));
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case 'permissions': {
|
|
609
|
+
const res = await executeAdminPermissions(options);
|
|
610
|
+
console.log(JSON.stringify(res, null, 2));
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case 'session': {
|
|
614
|
+
if (subCommand === 'status') {
|
|
615
|
+
const res = await executeAdminSessionStatus(options);
|
|
616
|
+
console.log(JSON.stringify(res, null, 2));
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
console.error(`Error: Unknown session subcommand "${subCommand}".`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
case 'audit-logs': {
|
|
625
|
+
const res = await executeAdminAuditLogs(options);
|
|
626
|
+
console.log(JSON.stringify(res, null, 2));
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'system-status': {
|
|
630
|
+
const session = getStoredAdminSession();
|
|
631
|
+
if (!session || !session.mfaVerified) {
|
|
632
|
+
console.log(JSON.stringify({
|
|
633
|
+
environment: 'sandbox',
|
|
634
|
+
database: 'isolated',
|
|
635
|
+
emulators: 'offline',
|
|
636
|
+
status: 'HEALTHY'
|
|
637
|
+
}, null, 2));
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
console.log(JSON.stringify({
|
|
641
|
+
environment: 'secure-sandbox',
|
|
642
|
+
database: 'connected',
|
|
643
|
+
authService: 'firebase-secure',
|
|
644
|
+
mfaEnforced: true,
|
|
645
|
+
status: 'HEALTHY'
|
|
646
|
+
}, null, 2));
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case 'verify-mfa-mock': {
|
|
650
|
+
console.log(JSON.stringify({
|
|
651
|
+
mfaStatus: 'verified-mock',
|
|
652
|
+
deviceAuthorized: true
|
|
653
|
+
}, null, 2));
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
default:
|
|
657
|
+
console.error(`Error: Unknown admin command "${command}". Run "creatorverse-admin help" for usage.`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
console.error(JSON.stringify({
|
|
663
|
+
error: err.code || 'ERROR',
|
|
664
|
+
message: err.message
|
|
665
|
+
}, null, 2));
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const isMain = process.argv[1] && (process.argv[1].endsWith('creatorverse-admin.ts') ||
|
|
670
|
+
process.argv[1].endsWith('creatorverse-admin.js') ||
|
|
671
|
+
(process.argv[1].includes('creatorverse-admin') && !process.argv[1].includes('.test.')));
|
|
672
|
+
if (isMain) {
|
|
673
|
+
main().catch((err) => {
|
|
674
|
+
console.error(JSON.stringify({ error: 'UNHANDLED_ERROR', message: err.message }, null, 2));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
//# sourceMappingURL=creatorverse-admin.js.map
|