tova 0.10.3 → 0.11.12
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 +93 -6604
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +3 -812
- package/src/analyzer/auth-analyzer.js +123 -0
- package/src/analyzer/cli-analyzer.js +91 -0
- package/src/analyzer/concurrency-analyzer.js +126 -0
- package/src/analyzer/edge-analyzer.js +215 -0
- package/src/analyzer/security-analyzer.js +297 -0
- package/src/cli/build.js +1044 -0
- package/src/cli/check.js +154 -0
- package/src/cli/compile.js +815 -0
- package/src/cli/completions.js +192 -0
- package/src/cli/deploy.js +15 -0
- package/src/cli/dev.js +612 -0
- package/src/cli/doctor.js +130 -0
- package/src/cli/format.js +52 -0
- package/src/cli/info.js +72 -0
- package/src/cli/migrate.js +386 -0
- package/src/cli/new.js +1435 -0
- package/src/cli/package.js +452 -0
- package/src/cli/repl.js +437 -0
- package/src/cli/run.js +137 -0
- package/src/cli/test.js +255 -0
- package/src/cli/upgrade.js +261 -0
- package/src/cli/utils.js +190 -0
- package/src/codegen/codegen.js +51 -17
- package/src/registry/plugins/auth-plugin.js +13 -2
- package/src/registry/plugins/cli-plugin.js +13 -2
- package/src/registry/plugins/concurrency-plugin.js +4 -0
- package/src/registry/plugins/edge-plugin.js +13 -2
- package/src/registry/plugins/security-plugin.js +13 -2
- package/src/version.js +1 -1
package/src/analyzer/analyzer.js
CHANGED
|
@@ -1319,818 +1319,9 @@ export class Analyzer {
|
|
|
1319
1319
|
return best;
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
1322
|
-
visitSecurityBlock
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
for (const stmt of node.body) {
|
|
1326
|
-
if (stmt.type === 'SecurityRoleDeclaration') {
|
|
1327
|
-
if (localRoles.has(stmt.name)) {
|
|
1328
|
-
this.warnings.push({
|
|
1329
|
-
message: `Duplicate role definition: '${stmt.name}'`,
|
|
1330
|
-
loc: stmt.loc,
|
|
1331
|
-
code: 'W_DUPLICATE_ROLE',
|
|
1332
|
-
category: 'security',
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
localRoles.add(stmt.name);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
visitCliBlock(node) {
|
|
1341
|
-
const validKeys = new Set(['name', 'version', 'description']);
|
|
1342
|
-
|
|
1343
|
-
// Validate config keys
|
|
1344
|
-
for (const field of node.config) {
|
|
1345
|
-
if (!validKeys.has(field.key)) {
|
|
1346
|
-
this.warnings.push({
|
|
1347
|
-
message: `Unknown cli config key '${field.key}' — valid keys are: ${[...validKeys].join(', ')}`,
|
|
1348
|
-
loc: field.loc,
|
|
1349
|
-
code: 'W_UNKNOWN_CLI_CONFIG',
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// Validate commands
|
|
1355
|
-
const commandNames = new Set();
|
|
1356
|
-
for (const cmd of node.commands) {
|
|
1357
|
-
// Duplicate command names
|
|
1358
|
-
if (commandNames.has(cmd.name)) {
|
|
1359
|
-
this.warnings.push({
|
|
1360
|
-
message: `Duplicate cli command '${cmd.name}'`,
|
|
1361
|
-
loc: cmd.loc,
|
|
1362
|
-
code: 'W_DUPLICATE_CLI_COMMAND',
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
commandNames.add(cmd.name);
|
|
1366
|
-
|
|
1367
|
-
// Check for positional args after flags
|
|
1368
|
-
let seenFlag = false;
|
|
1369
|
-
for (const param of cmd.params) {
|
|
1370
|
-
if (param.isFlag) {
|
|
1371
|
-
seenFlag = true;
|
|
1372
|
-
} else if (seenFlag) {
|
|
1373
|
-
this.warnings.push({
|
|
1374
|
-
message: `Positional argument '${param.name}' after flag in command '${cmd.name}' — positionals should come before flags`,
|
|
1375
|
-
loc: param.loc,
|
|
1376
|
-
code: 'W_POSITIONAL_AFTER_FLAG',
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Visit command body with params in scope
|
|
1382
|
-
this.pushScope('function');
|
|
1383
|
-
for (const param of cmd.params) {
|
|
1384
|
-
this.currentScope.define(param.name,
|
|
1385
|
-
new Symbol(param.name, 'parameter', null, false, param.loc));
|
|
1386
|
-
}
|
|
1387
|
-
this.visitNode(cmd.body);
|
|
1388
|
-
this.popScope();
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
visitConcurrentBlock(node) {
|
|
1393
|
-
// Validate mode
|
|
1394
|
-
const validModes = new Set(['all', 'cancel_on_error', 'first', 'timeout']);
|
|
1395
|
-
if (!validModes.has(node.mode)) {
|
|
1396
|
-
this.warn(`Unknown concurrent block mode '${node.mode}'`, node.loc, null, {
|
|
1397
|
-
code: 'W_UNKNOWN_CONCURRENT_MODE',
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Validate timeout
|
|
1402
|
-
if (node.mode === 'timeout' && !node.timeout) {
|
|
1403
|
-
this.warn("concurrent timeout mode requires a timeout value", node.loc, null, {
|
|
1404
|
-
code: 'W_MISSING_TIMEOUT',
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// Warn on empty block
|
|
1409
|
-
if (node.body.length === 0) {
|
|
1410
|
-
this.warn("Empty concurrent block", node.loc, null, {
|
|
1411
|
-
code: 'W_EMPTY_CONCURRENT',
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
// Track concurrent depth for spawn validation
|
|
1416
|
-
this._concurrentDepth = (this._concurrentDepth || 0) + 1;
|
|
1417
|
-
|
|
1418
|
-
// Visit body statements (concurrent block does NOT create a new scope —
|
|
1419
|
-
// variables assigned inside should be visible after the block)
|
|
1420
|
-
for (const stmt of node.body) {
|
|
1421
|
-
this.visitNode(stmt);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
// Check spawned functions for WASM compatibility — warn if mixed WASM/non-WASM
|
|
1425
|
-
let hasWasm = false;
|
|
1426
|
-
let hasNonWasm = false;
|
|
1427
|
-
for (const stmt of node.body) {
|
|
1428
|
-
const spawn = (stmt.type === 'Assignment' && stmt.values && stmt.values[0] && stmt.values[0].type === 'SpawnExpression')
|
|
1429
|
-
? stmt.values[0]
|
|
1430
|
-
: (stmt.type === 'ExpressionStatement' && stmt.expression && stmt.expression.type === 'SpawnExpression')
|
|
1431
|
-
? stmt.expression
|
|
1432
|
-
: null;
|
|
1433
|
-
if (!spawn) continue;
|
|
1434
|
-
const calleeName = spawn.callee && spawn.callee.type === 'Identifier' ? spawn.callee.name : null;
|
|
1435
|
-
if (calleeName) {
|
|
1436
|
-
const sym = this.currentScope.lookup(calleeName);
|
|
1437
|
-
if (sym && sym.isWasm) {
|
|
1438
|
-
hasWasm = true;
|
|
1439
|
-
} else {
|
|
1440
|
-
hasNonWasm = true;
|
|
1441
|
-
}
|
|
1442
|
-
} else {
|
|
1443
|
-
// Lambda or complex expression — always non-WASM
|
|
1444
|
-
hasNonWasm = true;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
if (hasWasm && hasNonWasm) {
|
|
1448
|
-
this.warn(
|
|
1449
|
-
"concurrent block mixes @wasm and non-WASM tasks — non-WASM tasks will fall back to async JS execution",
|
|
1450
|
-
node.loc, null, { code: 'W_SPAWN_WASM_FALLBACK' }
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
this._concurrentDepth--;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
visitSelectStatement(node) {
|
|
1458
|
-
if (node.cases.length === 0) {
|
|
1459
|
-
this.warn("Empty select block", node.loc, null, {
|
|
1460
|
-
code: 'W_EMPTY_SELECT',
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
let defaultCount = 0;
|
|
1465
|
-
let timeoutCount = 0;
|
|
1466
|
-
for (const c of node.cases) {
|
|
1467
|
-
if (c.kind === 'default') defaultCount++;
|
|
1468
|
-
if (c.kind === 'timeout') timeoutCount++;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
if (defaultCount > 1) {
|
|
1472
|
-
this.warn("select block has multiple default cases", node.loc, null, {
|
|
1473
|
-
code: 'W_DUPLICATE_SELECT_DEFAULT',
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
if (timeoutCount > 1) {
|
|
1477
|
-
this.warn("select block has multiple timeout cases", node.loc, null, {
|
|
1478
|
-
code: 'W_DUPLICATE_SELECT_TIMEOUT',
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
if (defaultCount > 0 && timeoutCount > 0) {
|
|
1482
|
-
this.warn("select block has both default and timeout — default makes timeout unreachable", node.loc, null, {
|
|
1483
|
-
code: 'W_SELECT_DEFAULT_TIMEOUT',
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// Visit each case's expressions and body
|
|
1488
|
-
for (const c of node.cases) {
|
|
1489
|
-
if (c.channel) this.visitNode(c.channel);
|
|
1490
|
-
if (c.value) this.visitNode(c.value);
|
|
1491
|
-
|
|
1492
|
-
if (c.kind === 'receive' && c.binding) {
|
|
1493
|
-
// Create scope for the binding variable
|
|
1494
|
-
this.pushScope('select-case');
|
|
1495
|
-
this.currentScope.define(c.binding,
|
|
1496
|
-
new Symbol(c.binding, 'variable', null, false, c.loc));
|
|
1497
|
-
for (const stmt of c.body) {
|
|
1498
|
-
this.visitNode(stmt);
|
|
1499
|
-
}
|
|
1500
|
-
this.popScope();
|
|
1501
|
-
} else {
|
|
1502
|
-
for (const stmt of c.body) {
|
|
1503
|
-
this.visitNode(stmt);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
_validateCliCrossBlock() {
|
|
1510
|
-
const cliBlocks = this.ast.body.filter(n => n.type === 'CliBlock');
|
|
1511
|
-
if (cliBlocks.length === 0) return;
|
|
1512
|
-
|
|
1513
|
-
// Warn if cli + server coexist
|
|
1514
|
-
const hasServer = this.ast.body.some(n => n.type === 'ServerBlock');
|
|
1515
|
-
if (hasServer) {
|
|
1516
|
-
this.warnings.push({
|
|
1517
|
-
message: 'cli {} and server {} blocks in the same file — cli produces a standalone executable, not a web server',
|
|
1518
|
-
loc: cliBlocks[0].loc,
|
|
1519
|
-
code: 'W_CLI_WITH_SERVER',
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
// Check for missing name across all cli blocks
|
|
1524
|
-
let hasName = false;
|
|
1525
|
-
for (const block of cliBlocks) {
|
|
1526
|
-
for (const field of block.config) {
|
|
1527
|
-
if (field.key === 'name') hasName = true;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
if (!hasName) {
|
|
1531
|
-
this.warnings.push({
|
|
1532
|
-
message: 'cli block has no name: field — consider adding name: "your-tool"',
|
|
1533
|
-
loc: cliBlocks[0].loc,
|
|
1534
|
-
code: 'W_CLI_MISSING_NAME',
|
|
1535
|
-
});
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
visitEdgeBlock(node) {
|
|
1540
|
-
const validTargets = new Set(['cloudflare', 'deno', 'vercel', 'lambda', 'bun']);
|
|
1541
|
-
const validConfigKeys = new Set(['target']);
|
|
1542
|
-
const bindingNames = new Set();
|
|
1543
|
-
|
|
1544
|
-
// Binding support matrix per target
|
|
1545
|
-
const BINDING_SUPPORT = {
|
|
1546
|
-
cloudflare: { kv: true, sql: true, storage: true, queue: true },
|
|
1547
|
-
deno: { kv: true, sql: false, storage: false, queue: false },
|
|
1548
|
-
vercel: { kv: false, sql: false, storage: false, queue: false },
|
|
1549
|
-
lambda: { kv: false, sql: false, storage: false, queue: false },
|
|
1550
|
-
bun: { kv: false, sql: true, storage: false, queue: false },
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1553
|
-
// Targets that support schedule/consume/middleware
|
|
1554
|
-
const SCHEDULE_TARGETS = new Set(['cloudflare', 'deno']);
|
|
1555
|
-
const CONSUME_TARGETS = new Set(['cloudflare']);
|
|
1556
|
-
|
|
1557
|
-
// Determine target from config fields
|
|
1558
|
-
let target = 'cloudflare';
|
|
1559
|
-
for (const stmt of node.body) {
|
|
1560
|
-
if (stmt.type === 'EdgeConfigField' && stmt.key === 'target' && stmt.value.type === 'StringLiteral') {
|
|
1561
|
-
target = stmt.value.value;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
this.pushScope('edge');
|
|
1566
|
-
|
|
1567
|
-
let kvCount = 0;
|
|
1568
|
-
const queueNames = new Set();
|
|
1569
|
-
const consumers = [];
|
|
1570
|
-
|
|
1571
|
-
for (const stmt of node.body) {
|
|
1572
|
-
// Validate config fields
|
|
1573
|
-
if (stmt.type === 'EdgeConfigField') {
|
|
1574
|
-
if (!validConfigKeys.has(stmt.key)) {
|
|
1575
|
-
this.warnings.push({
|
|
1576
|
-
message: `Unknown edge config key '${stmt.key}' — valid keys are: ${[...validConfigKeys].join(', ')}`,
|
|
1577
|
-
loc: stmt.loc,
|
|
1578
|
-
code: 'W_UNKNOWN_EDGE_CONFIG',
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
if (stmt.key === 'target' && stmt.value.type === 'StringLiteral') {
|
|
1582
|
-
if (!validTargets.has(stmt.value.value)) {
|
|
1583
|
-
this.warnings.push({
|
|
1584
|
-
message: `Unknown edge target '${stmt.value.value}' — valid targets are: ${[...validTargets].join(', ')}`,
|
|
1585
|
-
loc: stmt.loc,
|
|
1586
|
-
code: 'W_UNKNOWN_EDGE_TARGET',
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
continue;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
// Check for duplicate binding names
|
|
1594
|
-
if (stmt.type === 'EdgeKVDeclaration' || stmt.type === 'EdgeSQLDeclaration' ||
|
|
1595
|
-
stmt.type === 'EdgeStorageDeclaration' || stmt.type === 'EdgeQueueDeclaration') {
|
|
1596
|
-
if (bindingNames.has(stmt.name)) {
|
|
1597
|
-
this.warnings.push({
|
|
1598
|
-
message: `Duplicate edge binding '${stmt.name}'`,
|
|
1599
|
-
loc: stmt.loc,
|
|
1600
|
-
code: 'W_DUPLICATE_EDGE_BINDING',
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
bindingNames.add(stmt.name);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// Track queue names for consume validation
|
|
1607
|
-
if (stmt.type === 'EdgeQueueDeclaration') {
|
|
1608
|
-
queueNames.add(stmt.name);
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
// Check for duplicate env/secret names
|
|
1612
|
-
if (stmt.type === 'EdgeEnvDeclaration' || stmt.type === 'EdgeSecretDeclaration') {
|
|
1613
|
-
if (bindingNames.has(stmt.name)) {
|
|
1614
|
-
this.warnings.push({
|
|
1615
|
-
message: `Duplicate edge binding '${stmt.name}'`,
|
|
1616
|
-
loc: stmt.loc,
|
|
1617
|
-
code: 'W_DUPLICATE_EDGE_BINDING',
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
bindingNames.add(stmt.name);
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// Unsupported binding warnings (per target)
|
|
1624
|
-
const support = BINDING_SUPPORT[target] || BINDING_SUPPORT.cloudflare;
|
|
1625
|
-
if (stmt.type === 'EdgeKVDeclaration' && !support.kv) {
|
|
1626
|
-
this.warnings.push({
|
|
1627
|
-
message: `KV binding '${stmt.name}' is not supported on target '${target}' — it will be stubbed as null`,
|
|
1628
|
-
loc: stmt.loc,
|
|
1629
|
-
code: 'W_UNSUPPORTED_KV',
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
if (stmt.type === 'EdgeSQLDeclaration' && !support.sql) {
|
|
1633
|
-
this.warnings.push({
|
|
1634
|
-
message: `SQL binding '${stmt.name}' is not supported on target '${target}' — it will be stubbed as null`,
|
|
1635
|
-
loc: stmt.loc,
|
|
1636
|
-
code: 'W_UNSUPPORTED_SQL',
|
|
1637
|
-
});
|
|
1638
|
-
}
|
|
1639
|
-
if (stmt.type === 'EdgeStorageDeclaration' && !support.storage) {
|
|
1640
|
-
this.warnings.push({
|
|
1641
|
-
message: `Storage binding '${stmt.name}' is not supported on target '${target}' — it will be stubbed as null`,
|
|
1642
|
-
loc: stmt.loc,
|
|
1643
|
-
code: 'W_UNSUPPORTED_STORAGE',
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
if (stmt.type === 'EdgeQueueDeclaration' && !support.queue) {
|
|
1647
|
-
this.warnings.push({
|
|
1648
|
-
message: `Queue binding '${stmt.name}' is not supported on target '${target}' — it will be stubbed as null`,
|
|
1649
|
-
loc: stmt.loc,
|
|
1650
|
-
code: 'W_UNSUPPORTED_QUEUE',
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// Deno multi-KV warning
|
|
1655
|
-
if (stmt.type === 'EdgeKVDeclaration') {
|
|
1656
|
-
kvCount++;
|
|
1657
|
-
if (kvCount > 1 && target === 'deno') {
|
|
1658
|
-
this.warnings.push({
|
|
1659
|
-
message: `Deno Deploy supports only one KV store — '${stmt.name}' will share the same store as the first KV binding`,
|
|
1660
|
-
loc: stmt.loc,
|
|
1661
|
-
code: 'W_DENO_MULTI_KV',
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
// Validate schedule cron expressions + target support
|
|
1667
|
-
if (stmt.type === 'EdgeScheduleDeclaration') {
|
|
1668
|
-
const parts = stmt.cron.split(/\s+/);
|
|
1669
|
-
if (parts.length < 5 || parts.length > 6) {
|
|
1670
|
-
this.warnings.push({
|
|
1671
|
-
message: `Invalid cron expression '${stmt.cron}' — expected 5 or 6 space-separated fields`,
|
|
1672
|
-
loc: stmt.loc,
|
|
1673
|
-
code: 'W_INVALID_CRON',
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
if (!SCHEDULE_TARGETS.has(target)) {
|
|
1677
|
-
this.warnings.push({
|
|
1678
|
-
message: `Scheduled tasks are not supported on target '${target}' — schedule '${stmt.name}' will be ignored. Supported targets: ${[...SCHEDULE_TARGETS].join(', ')}`,
|
|
1679
|
-
loc: stmt.loc,
|
|
1680
|
-
code: 'W_UNSUPPORTED_SCHEDULE',
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
// Collect consume declarations for post-loop validation
|
|
1686
|
-
if (stmt.type === 'EdgeConsumeDeclaration') {
|
|
1687
|
-
consumers.push(stmt);
|
|
1688
|
-
if (!CONSUME_TARGETS.has(target)) {
|
|
1689
|
-
this.warnings.push({
|
|
1690
|
-
message: `Queue consumers are not supported on target '${target}' — consume '${stmt.queue}' will be ignored. Supported targets: ${[...CONSUME_TARGETS].join(', ')}`,
|
|
1691
|
-
loc: stmt.loc,
|
|
1692
|
-
code: 'W_UNSUPPORTED_CONSUME',
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// Visit child nodes — edge-specific types are noop in the registry,
|
|
1698
|
-
// so explicitly visit bodies that contain statements
|
|
1699
|
-
if (stmt.type === 'EdgeScheduleDeclaration' && stmt.body) {
|
|
1700
|
-
for (const s of stmt.body.body || []) this.visitNode(s);
|
|
1701
|
-
} else if (stmt.type === 'FunctionDeclaration' || stmt.type === 'RouteDeclaration') {
|
|
1702
|
-
this.visitNode(stmt);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// Post-loop: validate consume references a declared queue
|
|
1707
|
-
for (const consumer of consumers) {
|
|
1708
|
-
if (!queueNames.has(consumer.queue)) {
|
|
1709
|
-
this.warnings.push({
|
|
1710
|
-
message: `consume '${consumer.queue}' references undeclared queue binding — add 'queue ${consumer.queue}' to the edge block`,
|
|
1711
|
-
loc: consumer.loc,
|
|
1712
|
-
code: 'W_CONSUME_UNKNOWN_QUEUE',
|
|
1713
|
-
});
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// Warn if edge block has no route or schedule handlers
|
|
1718
|
-
const hasRoutes = node.body.some(s => s.type === 'RouteDeclaration');
|
|
1719
|
-
const hasSchedules = node.body.some(s => s.type === 'EdgeScheduleDeclaration');
|
|
1720
|
-
const hasConsumers = consumers.length > 0;
|
|
1721
|
-
if (!hasRoutes && !hasSchedules && !hasConsumers) {
|
|
1722
|
-
this.warnings.push({
|
|
1723
|
-
message: 'edge block has no routes, schedules, or consumers — it will produce no handlers',
|
|
1724
|
-
loc: node.loc,
|
|
1725
|
-
code: 'W_EDGE_NO_HANDLERS',
|
|
1726
|
-
});
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
this.popScope();
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
_validateEdgeCrossBlock() {
|
|
1733
|
-
const edgeBlocks = this.ast.body.filter(n => n.type === 'EdgeBlock');
|
|
1734
|
-
if (edgeBlocks.length === 0) return;
|
|
1735
|
-
|
|
1736
|
-
// Warn if edge + cli coexist (cli takes over with earlyReturn)
|
|
1737
|
-
const hasCli = this.ast.body.some(n => n.type === 'CliBlock');
|
|
1738
|
-
if (hasCli) {
|
|
1739
|
-
this.warnings.push({
|
|
1740
|
-
message: 'edge {} and cli {} blocks in the same file — cli produces a standalone executable, edge block will be ignored',
|
|
1741
|
-
loc: edgeBlocks[0].loc,
|
|
1742
|
-
code: 'W_EDGE_WITH_CLI',
|
|
1743
|
-
});
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
_validateSecurityCrossBlock() {
|
|
1748
|
-
// W_NO_SECURITY_BLOCK: server/edge block without security block
|
|
1749
|
-
const hasServerOrEdge = this.ast.body.some(n => n.type === 'ServerBlock' || n.type === 'EdgeBlock');
|
|
1750
|
-
const hasSecurityBlock = this.ast.body.some(n => n.type === 'SecurityBlock');
|
|
1751
|
-
if (hasServerOrEdge && !hasSecurityBlock) {
|
|
1752
|
-
const block = this.ast.body.find(n => n.type === 'ServerBlock' || n.type === 'EdgeBlock');
|
|
1753
|
-
this.warnings.push({
|
|
1754
|
-
message: 'Server/edge block defined without a security block — consider adding security { ... } for auth, CORS, and CSRF protection',
|
|
1755
|
-
loc: block.loc,
|
|
1756
|
-
code: 'W_NO_SECURITY_BLOCK',
|
|
1757
|
-
category: 'security',
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
// Collect ALL security declarations across ALL security blocks in the AST
|
|
1762
|
-
const allRoles = new Set();
|
|
1763
|
-
const allProtects = [];
|
|
1764
|
-
const allSensitives = [];
|
|
1765
|
-
let hasAuth = false;
|
|
1766
|
-
let hasProtect = false;
|
|
1767
|
-
let authDecl = null;
|
|
1768
|
-
let corsDecl = null;
|
|
1769
|
-
let rateLimitDecl = null;
|
|
1770
|
-
let csrfDecl = null;
|
|
1771
|
-
|
|
1772
|
-
// Check for top-level AuthBlock (independent auth block, not security sub-block)
|
|
1773
|
-
for (const node of this.ast.body) {
|
|
1774
|
-
if (node.type === 'AuthBlock') { hasAuth = true; authDecl = node; break; }
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
const roleDecls = []; // track all role declarations for cross-block duplicate detection
|
|
1778
|
-
for (const node of this.ast.body) {
|
|
1779
|
-
if (node.type !== 'SecurityBlock') continue;
|
|
1780
|
-
for (const stmt of node.body) {
|
|
1781
|
-
if (stmt.type === 'SecurityRoleDeclaration') {
|
|
1782
|
-
roleDecls.push(stmt);
|
|
1783
|
-
allRoles.add(stmt.name);
|
|
1784
|
-
} else if (stmt.type === 'SecurityProtectDeclaration') {
|
|
1785
|
-
allProtects.push(stmt);
|
|
1786
|
-
hasProtect = true;
|
|
1787
|
-
} else if (stmt.type === 'SecuritySensitiveDeclaration') {
|
|
1788
|
-
allSensitives.push(stmt);
|
|
1789
|
-
} else if (stmt.type === 'SecurityAuthDeclaration') {
|
|
1790
|
-
hasAuth = true;
|
|
1791
|
-
authDecl = stmt;
|
|
1792
|
-
} else if (stmt.type === 'SecurityCorsDeclaration') {
|
|
1793
|
-
corsDecl = stmt;
|
|
1794
|
-
} else if (stmt.type === 'SecurityRateLimitDeclaration') {
|
|
1795
|
-
rateLimitDecl = stmt;
|
|
1796
|
-
} else if (stmt.type === 'SecurityCsrfDeclaration') {
|
|
1797
|
-
csrfDecl = stmt;
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// W_DUPLICATE_ROLE across blocks: detect roles with same name in different security blocks
|
|
1803
|
-
const seenRoleNames = new Map(); // name -> first declaration
|
|
1804
|
-
for (const decl of roleDecls) {
|
|
1805
|
-
const prev = seenRoleNames.get(decl.name);
|
|
1806
|
-
if (prev && prev.loc !== decl.loc) {
|
|
1807
|
-
// Only warn if this is from a different block (same-block dupes handled by visitSecurityBlock)
|
|
1808
|
-
const prevInSameBlock = this.ast.body.some(b =>
|
|
1809
|
-
b.type === 'SecurityBlock' && b.body.includes(prev) && b.body.includes(decl)
|
|
1810
|
-
);
|
|
1811
|
-
if (!prevInSameBlock) {
|
|
1812
|
-
this.warnings.push({
|
|
1813
|
-
message: `Role '${decl.name}' is defined in multiple security blocks — later definition overwrites earlier one`,
|
|
1814
|
-
loc: decl.loc,
|
|
1815
|
-
code: 'W_DUPLICATE_ROLE',
|
|
1816
|
-
category: 'security',
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
seenRoleNames.set(decl.name, decl);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
// W_UNKNOWN_AUTH_TYPE — validate auth type is a known value
|
|
1824
|
-
if (authDecl && authDecl.authType) {
|
|
1825
|
-
const validAuthTypes = ['jwt', 'api_key'];
|
|
1826
|
-
if (!validAuthTypes.includes(authDecl.authType)) {
|
|
1827
|
-
this.warnings.push({
|
|
1828
|
-
message: `Unknown auth type '${authDecl.authType}' — supported types are: ${validAuthTypes.join(', ')}`,
|
|
1829
|
-
loc: authDecl.loc,
|
|
1830
|
-
code: 'W_UNKNOWN_AUTH_TYPE',
|
|
1831
|
-
category: 'security',
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
// Fix 2: W_HARDCODED_SECRET — warn if auth secret is a string literal
|
|
1837
|
-
// Only check SecurityAuthDeclaration (which has .config), not top-level AuthBlock
|
|
1838
|
-
if (authDecl && authDecl.config && authDecl.config.secret) {
|
|
1839
|
-
const secretNode = authDecl.config.secret;
|
|
1840
|
-
if (secretNode.type === 'StringLiteral') {
|
|
1841
|
-
this.warnings.push({
|
|
1842
|
-
message: 'Auth secret is hardcoded as a string literal — use env("SECRET_NAME") instead',
|
|
1843
|
-
loc: authDecl.loc,
|
|
1844
|
-
code: 'W_HARDCODED_SECRET',
|
|
1845
|
-
category: 'security',
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// Fix 7: W_CORS_WILDCARD — warn if cors origins contains "*"
|
|
1851
|
-
if (corsDecl && corsDecl.config.origins) {
|
|
1852
|
-
const originsNode = corsDecl.config.origins;
|
|
1853
|
-
if (originsNode.elements) {
|
|
1854
|
-
for (const elem of originsNode.elements) {
|
|
1855
|
-
if (elem.type === 'StringLiteral' && elem.value === '*') {
|
|
1856
|
-
this.warnings.push({
|
|
1857
|
-
message: 'CORS origins contains wildcard "*" — consider restricting to specific origins',
|
|
1858
|
-
loc: corsDecl.loc,
|
|
1859
|
-
code: 'W_CORS_WILDCARD',
|
|
1860
|
-
category: 'security',
|
|
1861
|
-
});
|
|
1862
|
-
break;
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// W_INVALID_RATE_LIMIT — validate rate limit max/window are positive numbers
|
|
1869
|
-
const _rlNumericValue = (node) => {
|
|
1870
|
-
if (!node) return null;
|
|
1871
|
-
if (node.type === 'NumberLiteral') return node.value;
|
|
1872
|
-
if (node.type === 'UnaryExpression' && node.operator === '-' && node.operand && node.operand.type === 'NumberLiteral') return -node.operand.value;
|
|
1873
|
-
return null;
|
|
1874
|
-
};
|
|
1875
|
-
if (rateLimitDecl && rateLimitDecl.config) {
|
|
1876
|
-
const rlMaxVal = _rlNumericValue(rateLimitDecl.config.max);
|
|
1877
|
-
const rlWindowVal = _rlNumericValue(rateLimitDecl.config.window);
|
|
1878
|
-
if (rlMaxVal !== null && rlMaxVal <= 0) {
|
|
1879
|
-
this.warnings.push({
|
|
1880
|
-
message: `Rate limit max must be a positive number, got ${rlMaxVal}`,
|
|
1881
|
-
loc: rateLimitDecl.loc,
|
|
1882
|
-
code: 'W_INVALID_RATE_LIMIT',
|
|
1883
|
-
category: 'security',
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
if (rlWindowVal !== null && rlWindowVal <= 0) {
|
|
1887
|
-
this.warnings.push({
|
|
1888
|
-
message: `Rate limit window must be a positive number, got ${rlWindowVal}`,
|
|
1889
|
-
loc: rateLimitDecl.loc,
|
|
1890
|
-
code: 'W_INVALID_RATE_LIMIT',
|
|
1891
|
-
category: 'security',
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
// W_CSRF_DISABLED — warn when CSRF is explicitly disabled
|
|
1897
|
-
if (csrfDecl && csrfDecl.config && csrfDecl.config.enabled) {
|
|
1898
|
-
const enabledNode = csrfDecl.config.enabled;
|
|
1899
|
-
if ((enabledNode.type === 'BooleanLiteral' && enabledNode.value === false) ||
|
|
1900
|
-
(enabledNode.type === 'Identifier' && enabledNode.name === 'false')) {
|
|
1901
|
-
this.warnings.push({
|
|
1902
|
-
message: 'CSRF protection is explicitly disabled — this increases vulnerability to cross-site request forgery attacks',
|
|
1903
|
-
loc: csrfDecl.loc,
|
|
1904
|
-
code: 'W_CSRF_DISABLED',
|
|
1905
|
-
category: 'security',
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
// W_LOCALSTORAGE_TOKEN — warn when auth uses default localStorage storage (XSS-vulnerable)
|
|
1911
|
-
if (authDecl && authDecl.authType === 'jwt' && authDecl.config) {
|
|
1912
|
-
const storageNode = authDecl.config.storage;
|
|
1913
|
-
const isCookieAuth = storageNode && storageNode.type === 'StringLiteral' && storageNode.value === 'cookie';
|
|
1914
|
-
if (!isCookieAuth) {
|
|
1915
|
-
this.warnings.push({
|
|
1916
|
-
message: 'Auth tokens stored in localStorage are vulnerable to XSS attacks — consider using storage: "cookie" for HttpOnly cookie storage',
|
|
1917
|
-
loc: authDecl.loc,
|
|
1918
|
-
code: 'W_LOCALSTORAGE_TOKEN',
|
|
1919
|
-
category: 'security',
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
// Fix 5: W_INMEMORY_RATELIMIT — warn that rate limiting is in-memory only
|
|
1925
|
-
if (rateLimitDecl) {
|
|
1926
|
-
this.warnings.push({
|
|
1927
|
-
message: 'Rate limiting uses in-memory storage — not shared across server instances. Consider an external store for production multi-instance deployments',
|
|
1928
|
-
loc: rateLimitDecl.loc,
|
|
1929
|
-
code: 'W_INMEMORY_RATELIMIT',
|
|
1930
|
-
category: 'security',
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
// Fix 6: W_NO_AUTH_RATELIMIT — warn when auth exists but no rate limiting protects against brute-force
|
|
1935
|
-
if (hasAuth && !rateLimitDecl) {
|
|
1936
|
-
const hasAuthRateLimit = allProtects.some(p => p.config && p.config.rate_limit);
|
|
1937
|
-
if (!hasAuthRateLimit) {
|
|
1938
|
-
this.warnings.push({
|
|
1939
|
-
message: 'Auth is configured without rate limiting — consider adding rate_limit to protect against brute-force attacks',
|
|
1940
|
-
loc: authDecl.loc,
|
|
1941
|
-
code: 'W_NO_AUTH_RATELIMIT',
|
|
1942
|
-
category: 'security',
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
// Fix 7: W_HASH_NOT_ENFORCED — warn when sensitive declares hash but it's not auto-enforced
|
|
1948
|
-
for (const s of allSensitives) {
|
|
1949
|
-
if (s.config && s.config.hash) {
|
|
1950
|
-
const hashVal = s.config.hash.value || s.config.hash;
|
|
1951
|
-
this.warnings.push({
|
|
1952
|
-
message: `sensitive ${s.typeName}.${s.fieldName} declares hash: "${hashVal}" but hashing is not automatically enforced — use hash_password() in your write handlers`,
|
|
1953
|
-
loc: s.loc,
|
|
1954
|
-
code: 'W_HASH_NOT_ENFORCED',
|
|
1955
|
-
category: 'security',
|
|
1956
|
-
});
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// No security blocks → nothing to validate (but allow auth/cors checks above)
|
|
1961
|
-
if (!hasProtect && allSensitives.length === 0) return;
|
|
1962
|
-
|
|
1963
|
-
// W_PROTECT_WITHOUT_AUTH: protect rules exist but no auth configured
|
|
1964
|
-
if (hasProtect && !hasAuth) {
|
|
1965
|
-
// Find first protect for location
|
|
1966
|
-
this.warnings.push({
|
|
1967
|
-
message: 'Route protection rules exist but no auth is configured — all protected routes will be inaccessible',
|
|
1968
|
-
loc: allProtects[0].loc,
|
|
1969
|
-
code: 'W_PROTECT_WITHOUT_AUTH',
|
|
1970
|
-
category: 'security',
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
// W_UNDEFINED_ROLE: protect rules reference roles not defined anywhere
|
|
1975
|
-
for (const protect of allProtects) {
|
|
1976
|
-
const requireExpr = protect.config.require;
|
|
1977
|
-
if (!requireExpr) {
|
|
1978
|
-
// W_PROTECT_NO_REQUIRE: protect rule has no require key
|
|
1979
|
-
this.warnings.push({
|
|
1980
|
-
message: `Protect rule for "${protect.pattern}" has no 'require' — route is unprotected`,
|
|
1981
|
-
loc: protect.loc,
|
|
1982
|
-
code: 'W_PROTECT_NO_REQUIRE',
|
|
1983
|
-
category: 'security',
|
|
1984
|
-
});
|
|
1985
|
-
continue;
|
|
1986
|
-
}
|
|
1987
|
-
if (requireExpr.type === 'Identifier' && requireExpr.name !== 'authenticated') {
|
|
1988
|
-
if (!allRoles.has(requireExpr.name)) {
|
|
1989
|
-
this.warnings.push({
|
|
1990
|
-
message: `Protect rule references undefined role '${requireExpr.name}'`,
|
|
1991
|
-
loc: protect.loc,
|
|
1992
|
-
code: 'W_UNDEFINED_ROLE',
|
|
1993
|
-
category: 'security',
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// W_UNDEFINED_ROLE: sensitive visible_to references roles not defined anywhere
|
|
2000
|
-
for (const sensitive of allSensitives) {
|
|
2001
|
-
const visibleTo = sensitive.config.visible_to;
|
|
2002
|
-
if (visibleTo && (visibleTo.type === 'ArrayExpression' || visibleTo.type === 'ArrayLiteral')) {
|
|
2003
|
-
for (const elem of visibleTo.elements) {
|
|
2004
|
-
if (elem.type === 'Identifier' && elem.name !== 'self') {
|
|
2005
|
-
if (!allRoles.has(elem.name)) {
|
|
2006
|
-
this.warnings.push({
|
|
2007
|
-
message: `Sensitive field '${sensitive.typeName}.${sensitive.fieldName}' visible_to references undefined role '${elem.name}'`,
|
|
2008
|
-
loc: sensitive.loc,
|
|
2009
|
-
code: 'W_UNDEFINED_ROLE',
|
|
2010
|
-
category: 'security',
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
visitAuthBlock(node) {
|
|
2020
|
-
const validHookEvents = new Set(['signup', 'login', 'logout', 'oauth_link']);
|
|
2021
|
-
const providerTypes = new Set();
|
|
2022
|
-
let hasProvider = false;
|
|
2023
|
-
|
|
2024
|
-
for (const stmt of node.body) {
|
|
2025
|
-
if (stmt.type === 'AuthConfigField') {
|
|
2026
|
-
if (stmt.key === 'secret' && stmt.value.type === 'StringLiteral') {
|
|
2027
|
-
this.warnings.push({
|
|
2028
|
-
message: 'Auth secret should use env() — hardcoded secrets are insecure',
|
|
2029
|
-
loc: stmt.loc, code: 'W_AUTH_HARDCODED_SECRET', category: 'auth',
|
|
2030
|
-
});
|
|
2031
|
-
}
|
|
2032
|
-
if (stmt.key === 'token_expires' && stmt.value.type === 'NumberLiteral' && stmt.value.value < 300) {
|
|
2033
|
-
this.warnings.push({
|
|
2034
|
-
message: 'Access token expires too quickly (< 5 minutes) — may cause frequent logouts',
|
|
2035
|
-
loc: stmt.loc, code: 'W_AUTH_SHORT_TOKEN', category: 'auth',
|
|
2036
|
-
});
|
|
2037
|
-
}
|
|
2038
|
-
if (stmt.key === 'refresh_expires' && stmt.value.type === 'NumberLiteral' && stmt.value.value > 2592000) {
|
|
2039
|
-
this.warnings.push({
|
|
2040
|
-
message: 'Refresh token lives longer than 30 days — consider shorter lifetime',
|
|
2041
|
-
loc: stmt.loc, code: 'W_AUTH_LONG_REFRESH', category: 'auth',
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
if (stmt.key === 'storage' && stmt.value.type === 'StringLiteral' && stmt.value.value === 'local') {
|
|
2045
|
-
this.warnings.push({
|
|
2046
|
-
message: 'localStorage tokens are vulnerable to XSS — prefer storage: "cookie"',
|
|
2047
|
-
loc: stmt.loc, code: 'W_AUTH_LOCAL_STORAGE', category: 'auth',
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
if (stmt.type === 'AuthProviderDeclaration') {
|
|
2053
|
-
hasProvider = true;
|
|
2054
|
-
const key = stmt.providerType + (stmt.name ? ':' + stmt.name : '');
|
|
2055
|
-
if (providerTypes.has(key)) {
|
|
2056
|
-
this.warnings.push({
|
|
2057
|
-
message: `Duplicate auth provider '${key}'`,
|
|
2058
|
-
loc: stmt.loc, code: 'W_AUTH_DUPLICATE_PROVIDER', category: 'auth',
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
providerTypes.add(key);
|
|
2062
|
-
|
|
2063
|
-
if (stmt.providerType === 'email') {
|
|
2064
|
-
if (!stmt.config.confirm_email) {
|
|
2065
|
-
this.warnings.push({
|
|
2066
|
-
message: 'Email provider without confirm_email — consider requiring email verification',
|
|
2067
|
-
loc: stmt.loc, code: 'W_AUTH_NO_CONFIRM', category: 'auth',
|
|
2068
|
-
});
|
|
2069
|
-
}
|
|
2070
|
-
if (stmt.config.password_min && stmt.config.password_min.type === 'NumberLiteral' && stmt.config.password_min.value < 8) {
|
|
2071
|
-
this.warnings.push({
|
|
2072
|
-
message: 'Minimum password length less than 8 — weak passwords allowed',
|
|
2073
|
-
loc: stmt.loc, code: 'W_AUTH_WEAK_PASSWORD', category: 'auth',
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
if (stmt.type === 'AuthHookDeclaration') {
|
|
2080
|
-
if (!validHookEvents.has(stmt.event)) {
|
|
2081
|
-
this.warnings.push({
|
|
2082
|
-
message: `Unknown auth hook event '${stmt.event}' — valid: ${[...validHookEvents].join(', ')}`,
|
|
2083
|
-
loc: stmt.loc, code: 'W_AUTH_UNKNOWN_HOOK', category: 'auth',
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
if (stmt.type === 'AuthProtectedRoute') {
|
|
2089
|
-
if (!stmt.config.redirect) {
|
|
2090
|
-
this.warnings.push({
|
|
2091
|
-
message: `Protected route '${stmt.pattern}' has no redirect`,
|
|
2092
|
-
loc: stmt.loc, code: 'W_AUTH_PROTECTED_NO_REDIRECT', category: 'auth',
|
|
2093
|
-
});
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
if (!hasProvider) {
|
|
2099
|
-
this.warnings.push({
|
|
2100
|
-
message: 'Auth block has no providers — add at least one',
|
|
2101
|
-
loc: node.loc, code: 'W_AUTH_MISSING_PROVIDER', category: 'auth',
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
_validateAuthCrossBlock() {
|
|
2107
|
-
const securityRoles = new Set();
|
|
2108
|
-
for (const node of this.ast.body) {
|
|
2109
|
-
if (node.type === 'SecurityBlock') {
|
|
2110
|
-
for (const stmt of node.body) {
|
|
2111
|
-
if (stmt.type === 'SecurityRoleDeclaration') {
|
|
2112
|
-
securityRoles.add(stmt.name);
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
for (const node of this.ast.body) {
|
|
2119
|
-
if (node.type === 'AuthBlock') {
|
|
2120
|
-
for (const stmt of node.body) {
|
|
2121
|
-
if (stmt.type === 'AuthProtectedRoute' && stmt.config.require) {
|
|
2122
|
-
const roleName = stmt.config.require.type === 'Identifier' ? stmt.config.require.name : null;
|
|
2123
|
-
if (roleName && securityRoles.size > 0 && !securityRoles.has(roleName)) {
|
|
2124
|
-
this.warnings.push({
|
|
2125
|
-
message: `Protected route requires role '${roleName}' not defined in security block`,
|
|
2126
|
-
loc: stmt.loc, code: 'W_AUTH_UNKNOWN_ROLE', category: 'auth',
|
|
2127
|
-
});
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
1322
|
+
// visitSecurityBlock, visitCliBlock, visitConcurrentBlock, visitSelectStatement,
|
|
1323
|
+
// visitEdgeBlock, visitAuthBlock and their cross-block validators are in their
|
|
1324
|
+
// respective *-analyzer.js files (lazy-loaded via plugins)
|
|
2134
1325
|
|
|
2135
1326
|
// visitBrowserBlock and other browser visitors are in browser-analyzer.js (lazy-loaded)
|
|
2136
1327
|
|