tova 0.7.0 → 0.9.4
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/bin/tova.js +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Provisioning Script Generator for the Tova language
|
|
2
|
+
// Generates idempotent bash scripts from an infrastructure manifest.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a complete provisioning shell script from an infrastructure manifest.
|
|
6
|
+
*
|
|
7
|
+
* The script is idempotent — it checks before installing (command -v, dpkg, etc.)
|
|
8
|
+
* and is organized in layers:
|
|
9
|
+
* Layer 1: System (Bun, Caddy, UFW, tova user)
|
|
10
|
+
* Layer 2: Databases (PostgreSQL, Redis — conditional)
|
|
11
|
+
* Layer 3: App directories
|
|
12
|
+
* Layer 5: Caddy config
|
|
13
|
+
* Layer 6: systemd services
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} manifest - Infrastructure manifest from inferInfrastructure()
|
|
16
|
+
* @returns {string} Complete provisioning bash script
|
|
17
|
+
*/
|
|
18
|
+
export function generateProvisionScript(manifest) {
|
|
19
|
+
const appName = manifest.name || 'tova-app';
|
|
20
|
+
const lines = [];
|
|
21
|
+
|
|
22
|
+
lines.push('#!/bin/bash');
|
|
23
|
+
lines.push('set -euo pipefail');
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push(`# Provisioning script for ${appName}`);
|
|
26
|
+
lines.push(`# Generated by Tova deploy — idempotent, safe to re-run`);
|
|
27
|
+
lines.push('');
|
|
28
|
+
|
|
29
|
+
// ── Layer 1: System ────────────────────────────────────────
|
|
30
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
31
|
+
lines.push('# Layer 1: System dependencies');
|
|
32
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
if (manifest.requires && manifest.requires.bun) {
|
|
36
|
+
lines.push('# Install Bun runtime');
|
|
37
|
+
lines.push('if ! command -v bun &>/dev/null; then');
|
|
38
|
+
lines.push(' echo "Installing Bun..."');
|
|
39
|
+
lines.push(' curl -fsSL https://bun.sh/install | bash');
|
|
40
|
+
lines.push(' export PATH="$HOME/.bun/bin:$PATH"');
|
|
41
|
+
lines.push('fi');
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (manifest.requires && manifest.requires.caddy) {
|
|
46
|
+
lines.push('# Install Caddy web server');
|
|
47
|
+
lines.push('if ! command -v caddy &>/dev/null; then');
|
|
48
|
+
lines.push(' echo "Installing Caddy..."');
|
|
49
|
+
lines.push(' apt-get update -qq');
|
|
50
|
+
lines.push(' apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl');
|
|
51
|
+
lines.push(' curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg');
|
|
52
|
+
lines.push(' curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt" | tee /etc/apt/sources.list.d/caddy-stable.list');
|
|
53
|
+
lines.push(' apt-get update -qq');
|
|
54
|
+
lines.push(' apt-get install -y -qq caddy');
|
|
55
|
+
lines.push('fi');
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (manifest.requires && manifest.requires.ufw) {
|
|
60
|
+
lines.push('# Configure UFW firewall');
|
|
61
|
+
lines.push('if command -v ufw &>/dev/null; then');
|
|
62
|
+
lines.push(' ufw allow 22/tcp');
|
|
63
|
+
lines.push(' ufw allow 80/tcp');
|
|
64
|
+
lines.push(' ufw allow 443/tcp');
|
|
65
|
+
lines.push(' echo "y" | ufw enable || true');
|
|
66
|
+
lines.push('fi');
|
|
67
|
+
lines.push('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create tova system user
|
|
71
|
+
lines.push('# Create tova system user');
|
|
72
|
+
lines.push('if ! id "tova" &>/dev/null; then');
|
|
73
|
+
lines.push(' useradd --system --create-home --shell /bin/bash tova');
|
|
74
|
+
lines.push('fi');
|
|
75
|
+
lines.push('');
|
|
76
|
+
|
|
77
|
+
// ── Layer 2: Databases ─────────────────────────────────────
|
|
78
|
+
const databases = manifest.databases || [];
|
|
79
|
+
const hasPostgres = databases.some(d => d.engine === 'postgres');
|
|
80
|
+
const hasRedis = databases.some(d => d.engine === 'redis');
|
|
81
|
+
|
|
82
|
+
if (hasPostgres || hasRedis) {
|
|
83
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
84
|
+
lines.push('# Layer 2: Databases');
|
|
85
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (hasPostgres) {
|
|
90
|
+
const pgDb = databases.find(d => d.engine === 'postgres');
|
|
91
|
+
const dbName = (pgDb && pgDb.config && pgDb.config.name) || `${appName}_db`;
|
|
92
|
+
lines.push('# Install PostgreSQL');
|
|
93
|
+
lines.push('if ! command -v psql &>/dev/null; then');
|
|
94
|
+
lines.push(' echo "Installing PostgreSQL..."');
|
|
95
|
+
lines.push(' apt-get update -qq');
|
|
96
|
+
lines.push(' apt-get install -y -qq postgresql postgresql-contrib');
|
|
97
|
+
lines.push(' systemctl enable postgresql');
|
|
98
|
+
lines.push(' systemctl start postgresql');
|
|
99
|
+
lines.push('fi');
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(`# Create database: ${dbName}`);
|
|
102
|
+
lines.push(`sudo -u postgres psql -tc "SELECT 1 FROM pg_database WHERE datname = '${dbName}'" | grep -q 1 || sudo -u postgres createdb "${dbName}"`);
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (hasRedis) {
|
|
107
|
+
lines.push('# Install Redis');
|
|
108
|
+
lines.push('if ! command -v redis-server &>/dev/null; then');
|
|
109
|
+
lines.push(' echo "Installing Redis..."');
|
|
110
|
+
lines.push(' apt-get update -qq');
|
|
111
|
+
lines.push(' apt-get install -y -qq redis-server');
|
|
112
|
+
lines.push(' systemctl enable redis-server');
|
|
113
|
+
lines.push(' systemctl start redis-server');
|
|
114
|
+
lines.push('fi');
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Layer 3: App directories ───────────────────────────────
|
|
119
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
120
|
+
lines.push('# Layer 3: App directories');
|
|
121
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push('mkdir -p /opt/tova/apps');
|
|
124
|
+
lines.push(`APP_DIR="/opt/tova/apps/${appName}"`);
|
|
125
|
+
lines.push('mkdir -p "$APP_DIR/releases"');
|
|
126
|
+
lines.push('mkdir -p "$APP_DIR/shared/logs"');
|
|
127
|
+
lines.push('mkdir -p "$APP_DIR/shared/data"');
|
|
128
|
+
lines.push('chown -R tova:tova /opt/tova');
|
|
129
|
+
lines.push('');
|
|
130
|
+
|
|
131
|
+
// ── Layer 5: Caddy config ──────────────────────────────────
|
|
132
|
+
if (manifest.requires && manifest.requires.caddy && manifest.domain) {
|
|
133
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
134
|
+
lines.push('# Layer 5: Caddy config');
|
|
135
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
136
|
+
lines.push('');
|
|
137
|
+
const caddyConfig = generateCaddyConfig(appName, {
|
|
138
|
+
domain: manifest.domain,
|
|
139
|
+
instances: manifest.instances || 1,
|
|
140
|
+
health: manifest.health,
|
|
141
|
+
health_interval: manifest.health_interval,
|
|
142
|
+
health_timeout: manifest.health_timeout,
|
|
143
|
+
hasWebSocket: manifest.hasWebSocket,
|
|
144
|
+
});
|
|
145
|
+
lines.push(`cat > /etc/caddy/Caddyfile <<'CADDY_EOF'`);
|
|
146
|
+
lines.push(caddyConfig);
|
|
147
|
+
lines.push('CADDY_EOF');
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('systemctl reload caddy || systemctl restart caddy');
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Layer 6: systemd services ──────────────────────────────
|
|
154
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
155
|
+
lines.push('# Layer 6: systemd services');
|
|
156
|
+
lines.push('# ═══════════════════════════════════════════════════════════');
|
|
157
|
+
lines.push('');
|
|
158
|
+
const serviceUnit = generateSystemdService(appName, {
|
|
159
|
+
memory: manifest.memory || '512mb',
|
|
160
|
+
restart_on_failure: manifest.restart_on_failure !== false,
|
|
161
|
+
env: manifest.env || {},
|
|
162
|
+
});
|
|
163
|
+
lines.push(`cat > /etc/systemd/system/${appName}@.service <<'SYSTEMD_EOF'`);
|
|
164
|
+
lines.push(serviceUnit);
|
|
165
|
+
lines.push('SYSTEMD_EOF');
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push('systemctl daemon-reload');
|
|
168
|
+
|
|
169
|
+
// Enable and start instances
|
|
170
|
+
const instances = manifest.instances || 1;
|
|
171
|
+
for (let i = 0; i < instances; i++) {
|
|
172
|
+
const port = 3000 + i;
|
|
173
|
+
lines.push(`systemctl enable ${appName}@${port}`);
|
|
174
|
+
}
|
|
175
|
+
lines.push('');
|
|
176
|
+
|
|
177
|
+
lines.push(`echo "Provisioning complete for ${appName}"`);
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse a memory string like "512mb", "1gb" into bytes for systemd MemoryMax.
|
|
183
|
+
*/
|
|
184
|
+
function parseMemory(mem) {
|
|
185
|
+
if (typeof mem === 'number') return mem;
|
|
186
|
+
const str = String(mem).toLowerCase().trim();
|
|
187
|
+
const match = str.match(/^(\d+(?:\.\d+)?)\s*(mb|gb|m|g|kb|k)?$/);
|
|
188
|
+
if (!match) return str;
|
|
189
|
+
const num = parseFloat(match[1]);
|
|
190
|
+
const unit = match[2] || 'mb';
|
|
191
|
+
switch (unit) {
|
|
192
|
+
case 'kb': case 'k': return `${Math.round(num)}K`;
|
|
193
|
+
case 'mb': case 'm': return `${Math.round(num)}M`;
|
|
194
|
+
case 'gb': case 'g': return `${Math.round(num * 1024)}M`;
|
|
195
|
+
default: return str;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate a systemd unit template for the application.
|
|
201
|
+
*
|
|
202
|
+
* Uses %i for the instance port (template unit: appName@.service).
|
|
203
|
+
*
|
|
204
|
+
* @param {string} appName - Application name
|
|
205
|
+
* @param {Object} config - { memory, restart_on_failure, env }
|
|
206
|
+
* @returns {string} systemd unit file content
|
|
207
|
+
*/
|
|
208
|
+
export function generateSystemdService(appName, config = {}) {
|
|
209
|
+
const memLimit = parseMemory(config.memory || '512mb');
|
|
210
|
+
const restart = config.restart_on_failure !== false ? 'on-failure' : 'no';
|
|
211
|
+
const env = config.env || {};
|
|
212
|
+
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push('[Unit]');
|
|
215
|
+
lines.push(`Description=${appName} instance on port %i`);
|
|
216
|
+
lines.push('After=network.target');
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('[Service]');
|
|
219
|
+
lines.push('Type=simple');
|
|
220
|
+
lines.push('User=tova');
|
|
221
|
+
lines.push('Group=tova');
|
|
222
|
+
lines.push(`WorkingDirectory=/opt/tova/apps/${appName}/current`);
|
|
223
|
+
lines.push(`ExecStart=/home/tova/.bun/bin/bun run server.js --port %i`);
|
|
224
|
+
lines.push(`Restart=${restart}`);
|
|
225
|
+
lines.push('RestartSec=5');
|
|
226
|
+
lines.push(`MemoryMax=${memLimit}`);
|
|
227
|
+
lines.push('');
|
|
228
|
+
|
|
229
|
+
// Environment — load secrets from .env.production, then set inline defaults
|
|
230
|
+
lines.push(`EnvironmentFile=-/opt/tova/apps/${appName}/.env.production`);
|
|
231
|
+
lines.push('Environment=NODE_ENV=production');
|
|
232
|
+
lines.push('Environment=PORT=%i');
|
|
233
|
+
for (const [key, value] of Object.entries(env)) {
|
|
234
|
+
if (key !== 'NODE_ENV' && key !== 'PORT') {
|
|
235
|
+
lines.push(`Environment=${key}=${value}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
|
|
240
|
+
// Logging
|
|
241
|
+
lines.push('StandardOutput=journal');
|
|
242
|
+
lines.push('StandardError=journal');
|
|
243
|
+
lines.push(`SyslogIdentifier=${appName}-%i`);
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push('[Install]');
|
|
246
|
+
lines.push('WantedBy=multi-user.target');
|
|
247
|
+
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Generate a Caddy site block configuration.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} appName - Application name
|
|
255
|
+
* @param {Object} opts - { domain, instances, health, hasWebSocket }
|
|
256
|
+
* @returns {string} Caddy config block
|
|
257
|
+
*/
|
|
258
|
+
export function generateCaddyConfig(appName, opts = {}) {
|
|
259
|
+
const domain = opts.domain || 'localhost';
|
|
260
|
+
const instances = opts.instances || 1;
|
|
261
|
+
const health = opts.health || '/healthz';
|
|
262
|
+
const healthInterval = opts.health_interval || 30;
|
|
263
|
+
const healthTimeout = opts.health_timeout || 5;
|
|
264
|
+
const hasWebSocket = opts.hasWebSocket || false;
|
|
265
|
+
|
|
266
|
+
const lines = [];
|
|
267
|
+
lines.push(`${domain} {`);
|
|
268
|
+
|
|
269
|
+
// Upstream / reverse proxy
|
|
270
|
+
if (instances === 1) {
|
|
271
|
+
lines.push(' reverse_proxy localhost:3000 {');
|
|
272
|
+
} else {
|
|
273
|
+
// Multiple instances with round-robin load balancing
|
|
274
|
+
const upstreams = [];
|
|
275
|
+
for (let i = 0; i < instances; i++) {
|
|
276
|
+
upstreams.push(`localhost:${3000 + i}`);
|
|
277
|
+
}
|
|
278
|
+
lines.push(` reverse_proxy ${upstreams.join(' ')} {`);
|
|
279
|
+
lines.push(' lb_policy round_robin');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Health check
|
|
283
|
+
lines.push(` health_uri ${health}`);
|
|
284
|
+
lines.push(` health_interval ${healthInterval}s`);
|
|
285
|
+
lines.push(` health_timeout ${healthTimeout}s`);
|
|
286
|
+
|
|
287
|
+
lines.push(' }');
|
|
288
|
+
|
|
289
|
+
// WebSocket support
|
|
290
|
+
if (hasWebSocket) {
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(' @websocket {');
|
|
293
|
+
lines.push(' header Connection *Upgrade*');
|
|
294
|
+
lines.push(' header Upgrade websocket');
|
|
295
|
+
lines.push(' }');
|
|
296
|
+
if (instances === 1) {
|
|
297
|
+
lines.push(' reverse_proxy @websocket localhost:3000');
|
|
298
|
+
} else {
|
|
299
|
+
const upstreams = [];
|
|
300
|
+
for (let i = 0; i < instances; i++) {
|
|
301
|
+
upstreams.push(`localhost:${3000 + i}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push(` reverse_proxy @websocket ${upstreams.join(' ')}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Logging
|
|
308
|
+
lines.push('');
|
|
309
|
+
lines.push(' log {');
|
|
310
|
+
lines.push(` output file /var/log/caddy/${appName}.log`);
|
|
311
|
+
lines.push(' }');
|
|
312
|
+
|
|
313
|
+
lines.push('}');
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Scorecard — post-compilation security posture summary.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function generateSecurityScorecard(securityConfig, warnings, hasServer, hasEdge) {
|
|
6
|
+
if (!hasServer && !hasEdge) return null;
|
|
7
|
+
|
|
8
|
+
const items = [];
|
|
9
|
+
let score = 10;
|
|
10
|
+
|
|
11
|
+
const warningCodes = new Set((warnings || []).map(w => w.code).filter(Boolean));
|
|
12
|
+
|
|
13
|
+
if (!securityConfig) {
|
|
14
|
+
items.push({ pass: false, label: 'No security block configured', deduction: 3 });
|
|
15
|
+
score -= 3;
|
|
16
|
+
items.push({ pass: false, label: 'No auth configured', deduction: 2 });
|
|
17
|
+
score -= 2;
|
|
18
|
+
items.push({ pass: false, label: 'No CSRF protection', deduction: 1 });
|
|
19
|
+
score -= 1;
|
|
20
|
+
items.push({ pass: false, label: 'No rate limiting', deduction: 1 });
|
|
21
|
+
score -= 1;
|
|
22
|
+
items.push({ pass: false, label: 'No CSP configured', deduction: 1 });
|
|
23
|
+
score -= 1;
|
|
24
|
+
items.push({ pass: false, label: 'No audit logging', deduction: 1 });
|
|
25
|
+
score -= 1;
|
|
26
|
+
return { score: Math.max(0, score), items, format: () => formatScorecard(Math.max(0, score), items) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Auth
|
|
30
|
+
if (securityConfig.auth) {
|
|
31
|
+
const isCookie = securityConfig.auth.storage === 'cookie';
|
|
32
|
+
const authType = securityConfig.auth.authType || 'JWT';
|
|
33
|
+
if (isCookie) {
|
|
34
|
+
items.push({ pass: true, label: `${authType} auth with HttpOnly cookies` });
|
|
35
|
+
} else {
|
|
36
|
+
items.push({ pass: true, label: `${authType} auth configured` });
|
|
37
|
+
}
|
|
38
|
+
if (warningCodes.has('W_LOCALSTORAGE_TOKEN')) {
|
|
39
|
+
items.push({ pass: false, label: 'Auth tokens in localStorage (XSS vulnerable)', deduction: 1 });
|
|
40
|
+
score -= 1;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
items.push({ pass: false, label: 'No auth configured', deduction: 2 });
|
|
44
|
+
score -= 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// CSRF
|
|
48
|
+
if (securityConfig.csrf && securityConfig.csrf.enabled === false) {
|
|
49
|
+
items.push({ pass: false, label: 'CSRF protection disabled', deduction: 1 });
|
|
50
|
+
score -= 1;
|
|
51
|
+
} else if (securityConfig.auth) {
|
|
52
|
+
items.push({ pass: true, label: 'CSRF enabled with session binding' });
|
|
53
|
+
} else {
|
|
54
|
+
items.push({ pass: false, label: 'No CSRF protection', deduction: 1 });
|
|
55
|
+
score -= 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Rate limiting
|
|
59
|
+
if (securityConfig.rateLimit) {
|
|
60
|
+
items.push({ pass: true, label: 'Rate limiting configured' });
|
|
61
|
+
} else if (securityConfig.auth) {
|
|
62
|
+
items.push({ pass: false, label: 'No rate limiting (auth without brute-force protection)', deduction: 1 });
|
|
63
|
+
score -= 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// CSP
|
|
67
|
+
if (securityConfig.csp) {
|
|
68
|
+
items.push({ pass: true, label: 'Content Security Policy configured' });
|
|
69
|
+
} else {
|
|
70
|
+
items.push({ pass: false, label: 'No CSP configured', deduction: 1 });
|
|
71
|
+
score -= 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// CORS wildcard
|
|
75
|
+
if (warningCodes.has('W_CORS_WILDCARD')) {
|
|
76
|
+
items.push({ pass: false, label: 'CORS allows wildcard origins', deduction: 1 });
|
|
77
|
+
score -= 1;
|
|
78
|
+
} else if (securityConfig.cors) {
|
|
79
|
+
items.push({ pass: true, label: 'CORS restricted to specific origins' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Hardcoded secret
|
|
83
|
+
if (warningCodes.has('W_HARDCODED_SECRET')) {
|
|
84
|
+
items.push({ pass: false, label: 'Auth secret hardcoded in source', deduction: 1 });
|
|
85
|
+
score -= 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Audit logging
|
|
89
|
+
if (securityConfig.audit) {
|
|
90
|
+
items.push({ pass: true, label: 'Audit logging configured' });
|
|
91
|
+
} else {
|
|
92
|
+
items.push({ pass: false, label: 'No audit logging', deduction: 1 });
|
|
93
|
+
score -= 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
score = Math.max(0, score);
|
|
97
|
+
return { score, items, format: () => formatScorecard(score, items) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatScorecard(score, items) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
lines.push(`\x1b[1mSecurity: ${score}/10\x1b[0m`);
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
if (item.pass) {
|
|
105
|
+
lines.push(` \x1b[32m[pass]\x1b[0m ${item.label}`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(` \x1b[33m[warn]\x1b[0m ${item.label} (-${item.deduction})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
package/src/lexer/lexer.js
CHANGED
|
@@ -899,15 +899,30 @@ export class Lexer {
|
|
|
899
899
|
return;
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
-
// Special case: "style {" → read raw CSS block
|
|
902
|
+
// Special case: "style {" or "style(...) {" → read raw CSS block
|
|
903
903
|
if (value === 'style') {
|
|
904
904
|
const savedPos = this.pos;
|
|
905
905
|
const savedLine = this.line;
|
|
906
906
|
const savedCol = this.column;
|
|
907
|
-
// Skip whitespace (including newlines) to check for {
|
|
907
|
+
// Skip whitespace (including newlines) to check for ( or {
|
|
908
908
|
while (this.pos < this.length && (this.isWhitespace(this.peek()) || this.peek() === '\n')) {
|
|
909
909
|
this.advance();
|
|
910
910
|
}
|
|
911
|
+
// Check for style(...) config
|
|
912
|
+
let configPrefix = '';
|
|
913
|
+
if (this.peek() === '(') {
|
|
914
|
+
this.advance(); // skip (
|
|
915
|
+
let configStr = '';
|
|
916
|
+
while (this.pos < this.length && this.peek() !== ')') {
|
|
917
|
+
configStr += this.advance();
|
|
918
|
+
}
|
|
919
|
+
if (this.pos < this.length) this.advance(); // skip )
|
|
920
|
+
configPrefix = '__CONFIG:' + configStr.trim() + '__';
|
|
921
|
+
// Skip whitespace after )
|
|
922
|
+
while (this.pos < this.length && (this.isWhitespace(this.peek()) || this.peek() === '\n')) {
|
|
923
|
+
this.advance();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
911
926
|
if (this.peek() === '{') {
|
|
912
927
|
this.advance(); // skip {
|
|
913
928
|
let depth = 1;
|
|
@@ -924,7 +939,7 @@ export class Lexer {
|
|
|
924
939
|
if (depth > 0) {
|
|
925
940
|
this.error('Unterminated style block');
|
|
926
941
|
}
|
|
927
|
-
this.tokens.push(new Token(TokenType.STYLE_BLOCK, css.trim(), startLine, startCol));
|
|
942
|
+
this.tokens.push(new Token(TokenType.STYLE_BLOCK, configPrefix + css.trim(), startLine, startCol));
|
|
928
943
|
return;
|
|
929
944
|
}
|
|
930
945
|
// Not a style block — restore position
|