cryptoserve 0.1.0

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Professional CLI styling for CryptoServe.
3
+ *
4
+ * Provides consistent, enterprise-grade terminal output styling.
5
+ * Port of sdk/python/cryptoserve/_cli_style.py — zero dependencies.
6
+ */
7
+
8
+ import { env, stdout } from 'node:process';
9
+
10
+ function supportsColor() {
11
+ if (env.NO_COLOR) return false;
12
+ if (env.FORCE_COLOR) return true;
13
+ if (!stdout.isTTY) return false;
14
+ if (process.platform === 'win32') {
15
+ return env.TERM === 'xterm' || !!env.ANSICON;
16
+ }
17
+ return true;
18
+ }
19
+
20
+ const ENABLED = supportsColor();
21
+ const e = (code) => ENABLED ? code : '';
22
+
23
+ export const Colors = {
24
+ RESET: e('\x1b[0m'),
25
+ BLACK: e('\x1b[30m'),
26
+ RED: e('\x1b[31m'),
27
+ GREEN: e('\x1b[32m'),
28
+ YELLOW: e('\x1b[33m'),
29
+ BLUE: e('\x1b[34m'),
30
+ MAGENTA: e('\x1b[35m'),
31
+ CYAN: e('\x1b[36m'),
32
+ WHITE: e('\x1b[37m'),
33
+ BRIGHT_BLACK: e('\x1b[90m'),
34
+ BRIGHT_RED: e('\x1b[91m'),
35
+ BRIGHT_GREEN: e('\x1b[92m'),
36
+ BRIGHT_YELLOW: e('\x1b[93m'),
37
+ BRIGHT_BLUE: e('\x1b[94m'),
38
+ BRIGHT_MAGENTA: e('\x1b[95m'),
39
+ BRIGHT_CYAN: e('\x1b[96m'),
40
+ BRIGHT_WHITE: e('\x1b[97m'),
41
+ BOLD: e('\x1b[1m'),
42
+ DIM: e('\x1b[2m'),
43
+ ITALIC: e('\x1b[3m'),
44
+ UNDERLINE: e('\x1b[4m'),
45
+ };
46
+
47
+ export const Style = {
48
+ SUCCESS: Colors.BRIGHT_GREEN,
49
+ ERROR: Colors.BRIGHT_RED,
50
+ WARNING: Colors.BRIGHT_YELLOW,
51
+ INFO: Colors.BRIGHT_BLUE,
52
+ HEADER: Colors.BRIGHT_CYAN + Colors.BOLD,
53
+ SUBHEADER: Colors.CYAN,
54
+ LABEL: Colors.BRIGHT_WHITE + Colors.BOLD,
55
+ VALUE: Colors.WHITE,
56
+ DIM: Colors.BRIGHT_BLACK,
57
+ ACCENT: Colors.BRIGHT_MAGENTA,
58
+ RESET: Colors.RESET,
59
+ };
60
+
61
+ export const Box = {
62
+ TOP_LEFT: '╭',
63
+ TOP_RIGHT: '╮',
64
+ BOTTOM_LEFT: '╰',
65
+ BOTTOM_RIGHT: '╯',
66
+ HORIZONTAL: '─',
67
+ VERTICAL: '│',
68
+ T_DOWN: '┬',
69
+ T_UP: '┴',
70
+ T_RIGHT: '├',
71
+ T_LEFT: '┤',
72
+ CROSS: '┼',
73
+ DOUBLE_HORIZONTAL: '═',
74
+ DOUBLE_VERTICAL: '║',
75
+ };
76
+
77
+ export const Icons = {
78
+ SUCCESS: '+',
79
+ ERROR: 'x',
80
+ WARNING: '!',
81
+ INFO: '*',
82
+ PENDING: 'o',
83
+ IN_PROGRESS: '-',
84
+ ARROW_RIGHT: '>',
85
+ ARROW_LEFT: '<',
86
+ BULLET: '-',
87
+ STAR: '*',
88
+ LOCK: '[locked]',
89
+ UNLOCK: '[unlocked]',
90
+ KEY: '[key]',
91
+ SHIELD: '[secure]',
92
+ CHECK: '[x]',
93
+ ROCKET: '[deploy]',
94
+ CLOCK: '[time]',
95
+ LINK: '[link]',
96
+ };
97
+
98
+ export function header(text, width = 60) {
99
+ const pad = width - 4;
100
+ const centered = text.length >= pad
101
+ ? text.slice(0, pad)
102
+ : ' '.repeat(Math.floor((pad - text.length) / 2)) + text +
103
+ ' '.repeat(Math.ceil((pad - text.length) / 2));
104
+ return [
105
+ `${Style.HEADER}${Box.TOP_LEFT}${Box.HORIZONTAL.repeat(width - 2)}${Box.TOP_RIGHT}${Style.RESET}`,
106
+ `${Style.HEADER}${Box.VERTICAL}${Style.RESET} ${centered} ${Style.HEADER}${Box.VERTICAL}${Style.RESET}`,
107
+ `${Style.HEADER}${Box.BOTTOM_LEFT}${Box.HORIZONTAL.repeat(width - 2)}${Box.BOTTOM_RIGHT}${Style.RESET}`,
108
+ ].join('\n');
109
+ }
110
+
111
+ export function compactHeader(command = '') {
112
+ if (command) {
113
+ return `\n${Style.HEADER}CRYPTOSERVE${Style.RESET} ${Style.DIM}›${Style.RESET} ${Style.LABEL}${command}${Style.RESET}\n`;
114
+ }
115
+ return `\n${Style.HEADER}CRYPTOSERVE${Style.RESET}\n`;
116
+ }
117
+
118
+ export function subheader(text, width = 60) {
119
+ const line = Box.HORIZONTAL.repeat(width);
120
+ return `\n${Style.SUBHEADER}${line}\n ${text}\n${line}${Style.RESET}`;
121
+ }
122
+
123
+ export function section(title) {
124
+ return `\n${Style.LABEL}${title}${Style.RESET}`;
125
+ }
126
+
127
+ export function divider(width = 60, char = '─') {
128
+ return `${Style.DIM}${char.repeat(width)}${Style.RESET}`;
129
+ }
130
+
131
+ export function success(text) {
132
+ return `${Style.SUCCESS}${Icons.SUCCESS}${Style.RESET} ${text}`;
133
+ }
134
+
135
+ export function error(text) {
136
+ return `${Style.ERROR}${Icons.ERROR}${Style.RESET} ${text}`;
137
+ }
138
+
139
+ export function warning(text) {
140
+ return `${Style.WARNING}${Icons.WARNING}${Style.RESET} ${text}`;
141
+ }
142
+
143
+ export function info(text) {
144
+ return `${Style.INFO}${Icons.INFO}${Style.RESET} ${text}`;
145
+ }
146
+
147
+ export function dim(text) {
148
+ return `${Style.DIM}${text}${Style.RESET}`;
149
+ }
150
+
151
+ export function bold(text) {
152
+ return `${Colors.BOLD}${text}${Style.RESET}`;
153
+ }
154
+
155
+ export function labelValue(label, value, labelWidth = 20) {
156
+ return ` ${Style.LABEL}${label.padEnd(labelWidth)}${Style.RESET} ${Style.VALUE}${value}${Style.RESET}`;
157
+ }
158
+
159
+ export function tableRow(columns, widths) {
160
+ const parts = columns.map((col, i) =>
161
+ String(col).padEnd(widths[i]).slice(0, widths[i])
162
+ );
163
+ return ` ${parts.join(' ')}`;
164
+ }
165
+
166
+ export function tableHeader(columns, widths) {
167
+ const headerLine = tableRow(columns, widths);
168
+ const underline = ' ' + widths.map(w => Box.HORIZONTAL.repeat(w)).join(' ');
169
+ return `${Style.LABEL}${headerLine}${Style.RESET}\n${Style.DIM}${underline}${Style.RESET}`;
170
+ }
171
+
172
+ export function progressBar(current, total, width = 30, showPercent = true) {
173
+ const percent = total === 0 ? 100 : Math.floor((current / total) * 100);
174
+ const filled = Math.floor((current / Math.max(total, 1)) * width);
175
+ const empty = width - filled;
176
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
177
+ const color = percent >= 80 ? Style.SUCCESS : percent >= 50 ? Style.WARNING : Style.ERROR;
178
+ return showPercent
179
+ ? `${color}${bar}${Style.RESET} ${percent}%`
180
+ : `${color}${bar}${Style.RESET}`;
181
+ }
182
+
183
+ export function statusBadge(status) {
184
+ const lower = status.toLowerCase();
185
+ if (['ready', 'active', 'success', 'healthy', 'ok'].includes(lower)) {
186
+ return `${Style.SUCCESS}● ${status}${Style.RESET}`;
187
+ }
188
+ if (['pending', 'waiting', 'in_progress'].includes(lower)) {
189
+ return `${Style.WARNING}○ ${status}${Style.RESET}`;
190
+ }
191
+ if (['error', 'failed', 'blocked'].includes(lower)) {
192
+ return `${Style.ERROR}● ${status}${Style.RESET}`;
193
+ }
194
+ return `${Style.DIM}○ ${status}${Style.RESET}`;
195
+ }
196
+
197
+ export function codeBlock(code) {
198
+ const lines = code.trim().split('\n');
199
+ const formatted = [];
200
+ formatted.push(`${Style.DIM}┌${'─'.repeat(58)}┐${Style.RESET}`);
201
+ for (const line of lines) {
202
+ formatted.push(`${Style.DIM}│${Style.RESET} ${Style.ACCENT}${line.padEnd(56)}${Style.RESET} ${Style.DIM}│${Style.RESET}`);
203
+ }
204
+ formatted.push(`${Style.DIM}└${'─'.repeat(58)}┘${Style.RESET}`);
205
+ return formatted.join('\n');
206
+ }
207
+
208
+ export function brandHeader() {
209
+ return `
210
+ ${Style.HEADER}╭────────────────────────────────────────────────────────╮
211
+ │ │
212
+ │ ${Colors.BRIGHT_WHITE}CRYPTOSERVE${Style.HEADER} │
213
+ │ ${Style.DIM}Enterprise Cryptography Platform${Style.HEADER} │
214
+ │ │
215
+ ╰────────────────────────────────────────────────────────╯${Style.RESET}
216
+ `;
217
+ }
package/lib/client.mjs ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * HTTP client for CryptoServe server API.
3
+ *
4
+ * Uses global fetch (Node 18+) — zero dependencies.
5
+ * Used only when connected to a server; all scanning/PQC/crypto work offline.
6
+ */
7
+
8
+ import { loadToken, saveToken } from './credentials.mjs';
9
+ import { createServer } from 'node:http';
10
+
11
+ const DEFAULT_SERVER = 'https://localhost:8003';
12
+ const CALLBACK_PORT = 9876;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // URL validation (SSRF protection)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const BLOCKED_HOSTS = [
19
+ '169.254.169.254', // AWS metadata
20
+ 'metadata.google.internal', // GCP metadata
21
+ '100.100.100.200', // Alibaba metadata
22
+ 'fd00::', // IPv6 link-local
23
+ ];
24
+
25
+ function validateServerUrl(url) {
26
+ try {
27
+ const parsed = new URL(url);
28
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
29
+ throw new Error('Server URL must use http or https');
30
+ }
31
+ if (BLOCKED_HOSTS.some(h => parsed.hostname === h || parsed.hostname.includes(h))) {
32
+ throw new Error('Server URL points to a blocked address');
33
+ }
34
+ return parsed.toString().replace(/\/$/, '');
35
+ } catch (e) {
36
+ if (e.message.includes('blocked') || e.message.includes('must use')) throw e;
37
+ throw new Error(`Invalid server URL: ${url}`);
38
+ }
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // API client
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export async function apiCall(method, path, body = null, options = {}) {
46
+ const creds = loadToken();
47
+ if (!creds) throw new Error('Not logged in. Run "cryptoserve login" first.');
48
+
49
+ const server = validateServerUrl(options.server || creds.server || DEFAULT_SERVER);
50
+ const url = `${server}${path}`;
51
+
52
+ const headers = {
53
+ 'Content-Type': 'application/json',
54
+ 'Cookie': `session=${creds.token}`,
55
+ };
56
+
57
+ const fetchOpts = {
58
+ method,
59
+ headers,
60
+ signal: AbortSignal.timeout(options.timeout || 30000),
61
+ };
62
+
63
+ if (body) fetchOpts.body = JSON.stringify(body);
64
+
65
+ const response = await fetch(url, fetchOpts);
66
+
67
+ if (!response.ok) {
68
+ if (response.status === 401) {
69
+ throw new Error('Session expired. Run "cryptoserve login" again.');
70
+ }
71
+ throw new Error(`API error ${response.status}: ${await response.text()}`);
72
+ }
73
+
74
+ return response.json();
75
+ }
76
+
77
+ export async function getStatus(server = null) {
78
+ const creds = loadToken();
79
+ const url = validateServerUrl(server || creds?.server || DEFAULT_SERVER);
80
+
81
+ try {
82
+ const start = Date.now();
83
+ const response = await fetch(`${url}/health`, {
84
+ signal: AbortSignal.timeout(10000),
85
+ });
86
+ const latency = Date.now() - start;
87
+ const data = await response.json();
88
+ return { connected: true, latency, ...data };
89
+ } catch (e) {
90
+ return { connected: false, error: e.message };
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Login flow (browser OAuth with localhost callback)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ export async function login(serverUrl = DEFAULT_SERVER) {
99
+ const server = validateServerUrl(serverUrl);
100
+
101
+ // Start local callback server
102
+ return new Promise((resolve, reject) => {
103
+ const httpServer = createServer((req, res) => {
104
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
105
+ const token = url.searchParams.get('token') || url.searchParams.get('session');
106
+
107
+ if (token) {
108
+ saveToken(token, server);
109
+ res.writeHead(200, { 'Content-Type': 'text/html' });
110
+ res.end('<html><body><h2>Login successful</h2><p>You can close this tab.</p></body></html>');
111
+ httpServer.close();
112
+ resolve({ success: true, server });
113
+ } else {
114
+ res.writeHead(400);
115
+ res.end('Missing token');
116
+ }
117
+ });
118
+
119
+ httpServer.listen(CALLBACK_PORT, () => {
120
+ const authUrl = `${server}/auth/cli?redirect=http://localhost:${CALLBACK_PORT}/callback`;
121
+ console.log(`\nOpen this URL to log in:\n ${authUrl}\n`);
122
+
123
+ // Try to open browser
124
+ const { exec } = import('node:child_process').then(m => {
125
+ const cmd = process.platform === 'darwin' ? 'open'
126
+ : process.platform === 'win32' ? 'start'
127
+ : 'xdg-open';
128
+ m.exec(`${cmd} "${authUrl}"`);
129
+ });
130
+ });
131
+
132
+ // Timeout after 120 seconds
133
+ setTimeout(() => {
134
+ httpServer.close();
135
+ reject(new Error('Login timed out after 120 seconds'));
136
+ }, 120000);
137
+ });
138
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Context-aware algorithm resolver for the CryptoServe CLI.
3
+ *
4
+ * Port of the 5-layer context model from the CryptoServe backend
5
+ * (backend/app/core/algorithm_resolver.py + backend/app/schemas/context.py).
6
+ *
7
+ * Resolves context labels like "user-pii" to optimal algorithms from the
8
+ * set the CLI can actually execute: AES-256-GCM, AES-128-GCM, ChaCha20-Poly1305.
9
+ *
10
+ * Zero dependencies — uses only node:fs for loading .cryptoserve.json.
11
+ */
12
+
13
+ import { readFileSync } from 'node:fs';
14
+ import { resolve } from 'node:path';
15
+
16
+ // =============================================================================
17
+ // Enums / Constants
18
+ // =============================================================================
19
+
20
+ const SENSITIVITY = { critical: 'critical', high: 'high', medium: 'medium', low: 'low' };
21
+
22
+ const ADVERSARIES = {
23
+ opportunistic: 'opportunistic',
24
+ organized_crime: 'organized_crime',
25
+ nation_state: 'nation_state',
26
+ insider: 'insider',
27
+ quantum: 'quantum',
28
+ };
29
+
30
+ const USAGE = {
31
+ at_rest: 'at_rest',
32
+ in_transit: 'in_transit',
33
+ in_use: 'in_use',
34
+ streaming: 'streaming',
35
+ disk: 'disk',
36
+ };
37
+
38
+ const FREQUENCY = { high: 'high', medium: 'medium', low: 'low', rare: 'rare' };
39
+
40
+ // Executable algorithms — what the CLI can actually do
41
+ const EXECUTABLE_ALGORITHMS = {
42
+ 'AES-256-GCM': { keyBits: 256, securityBits: 256, hwAccelerated: true, mode: 'gcm' },
43
+ 'AES-128-GCM': { keyBits: 128, securityBits: 128, hwAccelerated: true, mode: 'gcm' },
44
+ 'ChaCha20-Poly1305': { keyBits: 256, securityBits: 256, hwAccelerated: false, mode: 'stream' },
45
+ };
46
+
47
+ // Sensitivity → minimum key bits
48
+ const SENSITIVITY_MIN_BITS = {
49
+ critical: 256,
50
+ high: 256,
51
+ medium: 128,
52
+ low: 128,
53
+ };
54
+
55
+ // Sensitivity → key rotation recommendation (days)
56
+ const SENSITIVITY_ROTATION = {
57
+ critical: 30,
58
+ high: 90,
59
+ medium: 180,
60
+ low: 365,
61
+ };
62
+
63
+ // Compliance framework constraints
64
+ const COMPLIANCE_CONSTRAINTS = {
65
+ 'PCI-DSS': { minBits: 256, maxRotationDays: 90 },
66
+ 'HIPAA': { minBits: 256, maxRotationDays: 90 },
67
+ 'GDPR': { minBits: 128, maxRotationDays: 365 },
68
+ 'SOX': { minBits: 256, maxRotationDays: 180 },
69
+ 'OWASP': { minBits: 128, maxRotationDays: 365 },
70
+ 'CCPA': { minBits: 128, maxRotationDays: 365 },
71
+ 'NIST': { minBits: 256, maxRotationDays: 365 },
72
+ };
73
+
74
+ // =============================================================================
75
+ // Built-in presets (ported from backend/app/main.py seed contexts)
76
+ // =============================================================================
77
+
78
+ const BUILT_IN_CONTEXTS = {
79
+ 'user-pii': {
80
+ displayName: 'User Personal Data',
81
+ description: 'Personally identifiable information that can identify an individual',
82
+ sensitivity: 'high',
83
+ compliance: ['GDPR'],
84
+ adversaries: ['organized_crime', 'nation_state'],
85
+ protectionYears: 20,
86
+ usage: 'at_rest',
87
+ frequency: 'high',
88
+ pii: true,
89
+ examples: ['email', 'SSN', 'phone number', 'home address', 'date of birth'],
90
+ },
91
+ 'payment-data': {
92
+ displayName: 'Payment & Financial',
93
+ description: 'Payment card data and financial account information',
94
+ sensitivity: 'critical',
95
+ compliance: ['PCI-DSS'],
96
+ adversaries: ['organized_crime', 'insider'],
97
+ protectionYears: 7,
98
+ usage: 'at_rest',
99
+ frequency: 'high',
100
+ pci: true,
101
+ examples: ['credit card number', 'bank account', 'CVV', 'billing address'],
102
+ },
103
+ 'session-tokens': {
104
+ displayName: 'Session & Auth Tokens',
105
+ description: 'Temporary authentication and session data',
106
+ sensitivity: 'medium',
107
+ compliance: ['OWASP'],
108
+ adversaries: ['opportunistic'],
109
+ protectionYears: 0.01,
110
+ usage: 'in_transit',
111
+ frequency: 'high',
112
+ examples: ['JWT tokens', 'session IDs', 'refresh tokens', 'API keys'],
113
+ },
114
+ 'health-data': {
115
+ displayName: 'Health Information',
116
+ description: 'Protected health information and medical records',
117
+ sensitivity: 'critical',
118
+ compliance: ['HIPAA'],
119
+ adversaries: ['organized_crime', 'nation_state'],
120
+ protectionYears: 25,
121
+ usage: 'at_rest',
122
+ frequency: 'medium',
123
+ phi: true,
124
+ examples: ['diagnosis', 'prescriptions', 'medical history', 'insurance ID'],
125
+ },
126
+ 'general': {
127
+ displayName: 'General Purpose',
128
+ description: 'General data without specific regulatory requirements',
129
+ sensitivity: 'medium',
130
+ compliance: [],
131
+ adversaries: ['opportunistic'],
132
+ protectionYears: 5,
133
+ usage: 'at_rest',
134
+ frequency: 'medium',
135
+ examples: ['internal IDs', 'configuration secrets', 'API responses'],
136
+ },
137
+ };
138
+
139
+ // Context config defaults for custom contexts
140
+ const CONTEXT_DEFAULTS = {
141
+ sensitivity: 'medium',
142
+ compliance: [],
143
+ adversaries: ['opportunistic'],
144
+ protectionYears: 5,
145
+ usage: 'at_rest',
146
+ frequency: 'medium',
147
+ };
148
+
149
+ // =============================================================================
150
+ // Custom context loader
151
+ // =============================================================================
152
+
153
+ /**
154
+ * Load custom contexts from .cryptoserve.json in the given directory.
155
+ * Returns merged built-in + custom contexts.
156
+ */
157
+ export function loadContexts(dir = process.cwd()) {
158
+ const contexts = { ...BUILT_IN_CONTEXTS };
159
+
160
+ try {
161
+ const configPath = resolve(dir, '.cryptoserve.json');
162
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
163
+ if (config.contexts && typeof config.contexts === 'object') {
164
+ for (const [name, userCtx] of Object.entries(config.contexts)) {
165
+ // Validate name format
166
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) continue;
167
+ // Merge with defaults
168
+ contexts[name] = { ...CONTEXT_DEFAULTS, ...userCtx, _custom: true };
169
+ }
170
+ }
171
+ } catch { /* no config file or parse error — use built-ins only */ }
172
+
173
+ return contexts;
174
+ }
175
+
176
+ // =============================================================================
177
+ // 5-Layer Algorithm Resolver
178
+ // =============================================================================
179
+
180
+ /**
181
+ * Resolve a context name to an optimal algorithm and rationale.
182
+ *
183
+ * @param {string} contextName - Context label (e.g. "user-pii")
184
+ * @param {string} [dir] - Directory to look for .cryptoserve.json
185
+ * @returns {{ algorithm, keyBits, context, factors[], alternatives[] }}
186
+ */
187
+ export function resolveContext(contextName, dir = process.cwd()) {
188
+ const contexts = loadContexts(dir);
189
+ const ctx = contexts[contextName];
190
+ if (!ctx) {
191
+ return { error: `Unknown context: "${contextName}"`, validContexts: Object.keys(contexts) };
192
+ }
193
+
194
+ const factors = [];
195
+ const alternatives = [];
196
+
197
+ // ---- Layer 1: Data Identity (Sensitivity) ----
198
+ const sensitivity = ctx.sensitivity || 'medium';
199
+ let minBits = SENSITIVITY_MIN_BITS[sensitivity] || 128;
200
+ factors.push(`Sensitivity: ${sensitivity.toUpperCase()} → ${minBits}-bit minimum`);
201
+
202
+ // ---- Layer 2: Regulatory Mapping (Compliance) ----
203
+ let rotationDays = SENSITIVITY_ROTATION[sensitivity] || 180;
204
+ const compliance = ctx.compliance || [];
205
+ for (const framework of compliance) {
206
+ const constraint = COMPLIANCE_CONSTRAINTS[framework];
207
+ if (constraint) {
208
+ minBits = Math.max(minBits, constraint.minBits);
209
+ rotationDays = Math.min(rotationDays, constraint.maxRotationDays);
210
+ factors.push(`Compliance: ${framework} → ${constraint.minBits}-bit min, ${constraint.maxRotationDays}-day rotation`);
211
+ }
212
+ }
213
+
214
+ // ---- Layer 3: Threat Model ----
215
+ const adversaries = ctx.adversaries || ['opportunistic'];
216
+ const protectionYears = ctx.protectionYears ?? 5;
217
+
218
+ if (adversaries.includes('nation_state')) {
219
+ minBits = Math.max(minBits, 256);
220
+ factors.push('Threat: Nation-state adversary → 256-bit enforced');
221
+ }
222
+
223
+ let quantumRisk = false;
224
+ if (adversaries.includes('quantum') || protectionYears > 10) {
225
+ quantumRisk = true;
226
+ minBits = Math.max(minBits, 256);
227
+ const reason = adversaries.includes('quantum')
228
+ ? 'Quantum adversary specified'
229
+ : `Protection period ${protectionYears}yr exceeds quantum horizon`;
230
+ factors.push(`Quantum: ${reason} → 256-bit, PQC recommended`);
231
+ }
232
+
233
+ // ---- Layer 4: Access Patterns ----
234
+ const usage = ctx.usage || 'at_rest';
235
+ const frequency = ctx.frequency || 'medium';
236
+ let preferHwAccel = false;
237
+
238
+ if (frequency === 'high') {
239
+ preferHwAccel = true;
240
+ factors.push('Access: High frequency → hardware acceleration preferred (AES-NI)');
241
+ }
242
+
243
+ if (usage === 'streaming') {
244
+ factors.push('Usage: Streaming → stream cipher preferred');
245
+ }
246
+
247
+ // ---- Layer 5: Algorithm Selection ----
248
+ let algorithm;
249
+
250
+ if (usage === 'streaming' && minBits <= 256) {
251
+ // Streaming contexts prefer ChaCha20 (native stream cipher)
252
+ algorithm = 'ChaCha20-Poly1305';
253
+ factors.push('Selected: ChaCha20-Poly1305 (native stream cipher for streaming context)');
254
+ alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If AES-NI hardware acceleration is available' });
255
+ } else if (minBits > 128) {
256
+ // 256-bit requirement
257
+ if (preferHwAccel) {
258
+ algorithm = 'AES-256-GCM';
259
+ factors.push('Selected: AES-256-GCM (256-bit, hardware accelerated, FIPS 197 + SP 800-38D)');
260
+ alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better on systems without AES-NI' });
261
+ } else {
262
+ // No strong hw-accel preference — still default to AES-256-GCM (most compatible)
263
+ algorithm = 'AES-256-GCM';
264
+ factors.push('Selected: AES-256-GCM (256-bit, widely supported, FIPS compliant)');
265
+ alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Equal security, better without AES-NI' });
266
+ }
267
+ } else {
268
+ // 128-bit sufficient
269
+ if (preferHwAccel) {
270
+ algorithm = 'AES-128-GCM';
271
+ factors.push('Selected: AES-128-GCM (128-bit sufficient, fast with AES-NI)');
272
+ alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If future-proofing or compliance requires 256-bit' });
273
+ alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better on systems without AES-NI' });
274
+ } else {
275
+ algorithm = 'AES-128-GCM';
276
+ factors.push('Selected: AES-128-GCM (128-bit sufficient for threat model)');
277
+ alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better performance without AES-NI' });
278
+ alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If upgrading to 256-bit for future-proofing' });
279
+ }
280
+ }
281
+
282
+ if (quantumRisk) {
283
+ factors.push('Note: Post-quantum migration recommended — use CryptoServe server for hybrid PQC');
284
+ }
285
+
286
+ return {
287
+ algorithm,
288
+ keyBits: EXECUTABLE_ALGORITHMS[algorithm].keyBits,
289
+ context: {
290
+ name: contextName,
291
+ displayName: ctx.displayName || contextName,
292
+ description: ctx.description || '',
293
+ sensitivity,
294
+ compliance,
295
+ adversaries,
296
+ protectionYears,
297
+ usage,
298
+ frequency,
299
+ pii: ctx.pii || false,
300
+ phi: ctx.phi || false,
301
+ pci: ctx.pci || false,
302
+ examples: ctx.examples || [],
303
+ custom: !!ctx._custom,
304
+ },
305
+ rotationDays,
306
+ quantumRisk,
307
+ factors,
308
+ alternatives,
309
+ };
310
+ }
311
+
312
+ /**
313
+ * List all available contexts (built-in + custom).
314
+ *
315
+ * @param {string} [dir] - Directory to look for .cryptoserve.json
316
+ * @returns {Array<{ name, displayName, sensitivity, algorithm, compliance, custom }>}
317
+ */
318
+ export function listContexts(dir = process.cwd()) {
319
+ const contexts = loadContexts(dir);
320
+ const result = [];
321
+
322
+ for (const [name, ctx] of Object.entries(contexts)) {
323
+ const resolved = resolveContext(name, dir);
324
+ result.push({
325
+ name,
326
+ displayName: ctx.displayName || name,
327
+ description: ctx.description || '',
328
+ sensitivity: ctx.sensitivity || 'medium',
329
+ algorithm: resolved.algorithm,
330
+ compliance: ctx.compliance || [],
331
+ custom: !!ctx._custom,
332
+ });
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ // Re-export for CLI validation
339
+ export { BUILT_IN_CONTEXTS, EXECUTABLE_ALGORITHMS };