@cosmicdrift/kumiko-guards 0.3.0 → 0.282.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.
- package/package.json +2 -2
- package/src/_lib/guard-kit.ts +58 -0
- package/src/changes.json +17 -0
- package/src/check-runtime-isolation.ts +214 -0
- package/src/cli.ts +55 -31
- package/src/guard-error-reasons.ts +13 -5
- package/src/guard-escape-hatch-declared.ts +134 -21
- package/src/guard-upgrade-state.ts +212 -0
- package/src/index.ts +3 -0
- package/src/run-guards.ts +28 -10
- package/src/run-repo-checks.ts +22 -4
- package/src/run-ui-guards.ts +19 -4
- package/src/runtime-isolation-classify.ts +337 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.282.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"kumiko-guards": "./src/cli.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cosmicdrift/kumiko-repo-manifest": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-repo-manifest": "0.282.0",
|
|
31
31
|
"ts-morph": "^28.0.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
package/src/_lib/guard-kit.ts
CHANGED
|
@@ -479,6 +479,23 @@ export function guardKitPreflightError(guardCount: number, rootCount: number): s
|
|
|
479
479
|
return undefined;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Validates CLI args for one subcommand against the flags it actually
|
|
484
|
+
* understands — every arg must match `known` exactly (not just a `--`-prefix
|
|
485
|
+
* check, which let a single-dash typo or a stray positional through
|
|
486
|
+
* unnoticed). An unknown arg must fail loud, never pass through silently.
|
|
487
|
+
*/
|
|
488
|
+
export function cliFlagsError(
|
|
489
|
+
subcommand: string,
|
|
490
|
+
argv: readonly string[],
|
|
491
|
+
known: readonly string[],
|
|
492
|
+
): string | undefined {
|
|
493
|
+
const unknown = argv.filter((arg) => !known.includes(arg));
|
|
494
|
+
if (unknown.length === 0) return undefined;
|
|
495
|
+
const knownList = known.length > 0 ? known.join(", ") : "(none)";
|
|
496
|
+
return `Unknown argument${unknown.length > 1 ? "s" : ""} for "${subcommand}": ${unknown.join(", ")}. Known flags: ${knownList}`;
|
|
497
|
+
}
|
|
498
|
+
|
|
482
499
|
export type GuardKitBannerDeps = {
|
|
483
500
|
readonly resolution?: RootResolution;
|
|
484
501
|
};
|
|
@@ -505,6 +522,47 @@ export function printGuardKitBanner(
|
|
|
505
522
|
if (project) console.log(`Project: ${project.getSourceFiles().length} files`);
|
|
506
523
|
}
|
|
507
524
|
|
|
525
|
+
export type SuiteInventory = {
|
|
526
|
+
readonly count: number;
|
|
527
|
+
readonly names: readonly string[];
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export type GuardKitInventory = {
|
|
531
|
+
readonly version: string;
|
|
532
|
+
readonly total: number;
|
|
533
|
+
readonly suites: {
|
|
534
|
+
readonly guards: SuiteInventory;
|
|
535
|
+
readonly ui: SuiteInventory;
|
|
536
|
+
readonly checks: SuiteInventory;
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
function suiteInventory(items: readonly { readonly name: string }[]): SuiteInventory {
|
|
541
|
+
const names = items.map((item) => item.name);
|
|
542
|
+
return { count: names.length, names };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Static registration inventory for the three suites — reads each array's
|
|
547
|
+
* `.name` only, no scan/project/guard.run(). What a consumer's CI checks
|
|
548
|
+
* against instead of running the guards, e.g. so a guard dropped from a
|
|
549
|
+
* suite's array is missing here too, not just silently absent from a run.
|
|
550
|
+
*/
|
|
551
|
+
export function buildGuardKitInventory(args: {
|
|
552
|
+
readonly guards: readonly { readonly name: string }[];
|
|
553
|
+
readonly uiGuards: readonly { readonly name: string }[];
|
|
554
|
+
readonly checks: readonly { readonly name: string }[];
|
|
555
|
+
}): GuardKitInventory {
|
|
556
|
+
const guards = suiteInventory(args.guards);
|
|
557
|
+
const ui = suiteInventory(args.uiGuards);
|
|
558
|
+
const checks = suiteInventory(args.checks);
|
|
559
|
+
return {
|
|
560
|
+
version: guardKitVersion(),
|
|
561
|
+
total: guards.count + ui.count + checks.count,
|
|
562
|
+
suites: { guards, ui, checks },
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
508
566
|
/**
|
|
509
567
|
* Vacuity floor for guards with their own `main()`.
|
|
510
568
|
*
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.282.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "kumiko-guards bin now passes flags through to the guards, ui, and checks subcommands",
|
|
6
|
+
"detail": "The bin's `guards` subcommand called run-guards.ts's suite runner directly, skipping the `--explain`/`--write-security-baseline`/`--strict-security-baseline` handling that only existed in run-guards.ts's own `if (import.meta.main)` block — a consumer running `bunx @cosmicdrift/kumiko-guards guards --write-security-baseline` got a normal guard run with the flag silently dropped. Each suite (guards, ui, checks) now exports a `run*Cli(argv)` function that validates argv against that suite's known flags and applies them; both the bin and the suite's own direct-invocation entry point call the same function, so they cannot drift apart again. Validation matches each arg exactly against the known list (not just a `--`-prefix check), so a single-dash typo or a stray positional also exits 1 with the flags that subcommand accepts, rather than passing through unnoticed."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.282.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "kumiko-guards list prints the registration inventory as JSON"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.282.0",
|
|
15
|
+
"type": "improvement",
|
|
16
|
+
"title": "Port the upgrade-state and runtime-isolation guards",
|
|
17
|
+
"detail": "guard-upgrade-state and check-runtime-isolation ported from the private infra/guards package into the public @cosmicdrift/kumiko-guards package, rebuilt onto the public RepoCheck pattern with single-repo root resolution. guard-app-dockerfile, guard-doc-status, check-licenses, and check-security stay in infra/guards as CDGS-specific house rules (private registry scope, CDGS doc taxonomy, and CDGS license/security exception files with no equivalent consumer-facing mechanism in the public package) — they were deliberately not ported, not forgotten."
|
|
18
|
+
},
|
|
2
19
|
{
|
|
3
20
|
"version": "0.3.0",
|
|
4
21
|
"type": "improvement",
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Runtime-Isolation Guard.
|
|
3
|
+
//
|
|
4
|
+
// Prevents production code (runtime) from transitively loading test or
|
|
5
|
+
// tooling modules — the original trigger was a top-level Vitest import in a
|
|
6
|
+
// testing helper that crashed drizzle-kit under Node. Every file gets a
|
|
7
|
+
// runtime context assigned; every import edge is checked against a compat
|
|
8
|
+
// matrix.
|
|
9
|
+
//
|
|
10
|
+
// Per-file classification (highest priority first):
|
|
11
|
+
// 1. File directive → // @runtime <kind> in the first 5 lines
|
|
12
|
+
// 2. Path pattern → *.test.ts, *.integration.ts, *.e2e.ts,
|
|
13
|
+
// **/__tests__/**, **/testing/** → test;
|
|
14
|
+
// plus a handful of verified-isomorphic
|
|
15
|
+
// single-file/directory carve-outs (see
|
|
16
|
+
// runtime-isolation-classify.ts)
|
|
17
|
+
// 3. Workspace → package.json `"kumiko": { "runtime": "..." }`
|
|
18
|
+
// 4. Client reachability → transitively reachable, via value imports,
|
|
19
|
+
// from a browser-bundle entry
|
|
20
|
+
// (`src/client-*.tsx`) or from any file that
|
|
21
|
+
// already classifies as "client" on its own
|
|
22
|
+
// (directive/path/workspace)
|
|
23
|
+
// 5. Default → runtime
|
|
24
|
+
//
|
|
25
|
+
// Compat matrix: which runtime context may import which.
|
|
26
|
+
// runtime → runtime, client
|
|
27
|
+
// client → client
|
|
28
|
+
// dev → runtime, client, dev, tooling
|
|
29
|
+
// tooling → runtime, client, dev, tooling, test
|
|
30
|
+
// test → everything
|
|
31
|
+
//
|
|
32
|
+
// Single-repo only (unlike infra/guards' check-runtime-isolation.ts, which
|
|
33
|
+
// scans every sibling checkout in one shared ts-morph project): this public
|
|
34
|
+
// package only ever resolves the repo `cwd` sits in, so there is exactly one
|
|
35
|
+
// repo root to classify files against.
|
|
36
|
+
//
|
|
37
|
+
// Pure classification + violation detection live in
|
|
38
|
+
// `runtime-isolation-classify.ts` (unit-testable with an in-memory ts-morph
|
|
39
|
+
// project) — this file only does the repo/glob resolution, ts-morph project
|
|
40
|
+
// setup, and RepoCheck wiring.
|
|
41
|
+
//
|
|
42
|
+
// A file/import edge classified "client" importing "runtime" can be a real
|
|
43
|
+
// finding this guard needs to see, but can also be a known, reviewed
|
|
44
|
+
// exception (e.g. samples/recipes are worked examples, not the framework's
|
|
45
|
+
// own production surface — see kumiko-framework#2337 for the one currently
|
|
46
|
+
// frozen here). Exceptions are tracked via a per-repo baseline file
|
|
47
|
+
// (`.kumiko-runtime-isolation-baseline.json`, same `baselineRatchet`
|
|
48
|
+
// mechanism as check-complexity/guard-pii-annotations/etc.), not a
|
|
49
|
+
// hardcoded list in this source file: a consumer repo can freeze or clear
|
|
50
|
+
// its own findings the same way, with `--write-baseline`.
|
|
51
|
+
//
|
|
52
|
+
// Usage:
|
|
53
|
+
// bun packages/guards/src/check-runtime-isolation.ts
|
|
54
|
+
// bun packages/guards/src/check-runtime-isolation.ts --write-baseline
|
|
55
|
+
|
|
56
|
+
import * as path from "node:path";
|
|
57
|
+
import { Project } from "ts-morph";
|
|
58
|
+
import {
|
|
59
|
+
baselineRatchet,
|
|
60
|
+
type GuardViolation,
|
|
61
|
+
type RepoCheck,
|
|
62
|
+
reportResults,
|
|
63
|
+
runRepoChecks,
|
|
64
|
+
} from "./_lib/guard-kit";
|
|
65
|
+
import { frameworkTsConfigPath, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
|
|
66
|
+
import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
|
|
67
|
+
import {
|
|
68
|
+
classify,
|
|
69
|
+
computeClientReachablePaths,
|
|
70
|
+
findRuntimeIsolationViolations,
|
|
71
|
+
isClientEntryPath,
|
|
72
|
+
type Runtime,
|
|
73
|
+
} from "./runtime-isolation-classify";
|
|
74
|
+
|
|
75
|
+
const SCAN: ScanSpec = { scope: "source", extensions: ["ts", "tsx"] };
|
|
76
|
+
const BASELINE_FILE = ".kumiko-runtime-isolation-baseline.json";
|
|
77
|
+
|
|
78
|
+
type RawViolation = {
|
|
79
|
+
readonly file: string;
|
|
80
|
+
readonly line: number;
|
|
81
|
+
readonly fileRuntime: Runtime;
|
|
82
|
+
readonly importedSpec: string;
|
|
83
|
+
readonly importedRuntime: Runtime;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function printViolation(v: RawViolation, root: string): void {
|
|
87
|
+
const fileRel = path.relative(root, v.file);
|
|
88
|
+
console.log(` ${fileRel}:${v.line}`);
|
|
89
|
+
console.log(` [${v.fileRuntime}] imports [${v.importedRuntime}] "${v.importedSpec}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function violationKey(rootAbsPath: string, v: RawViolation): string {
|
|
93
|
+
return `${path.relative(rootAbsPath, v.file)}::${v.importedSpec}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function countByKey(
|
|
97
|
+
rootAbsPath: string,
|
|
98
|
+
violations: readonly RawViolation[],
|
|
99
|
+
): Record<string, number> {
|
|
100
|
+
const counts: Record<string, number> = {};
|
|
101
|
+
for (const v of violations) {
|
|
102
|
+
const key = violationKey(rootAbsPath, v);
|
|
103
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
return counts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ratchetFor(rootAbsPath: string) {
|
|
109
|
+
return baselineRatchet({
|
|
110
|
+
file: path.join(rootAbsPath, BASELINE_FILE),
|
|
111
|
+
formatVersion: 1,
|
|
112
|
+
unit: "runtime-isolation violation(s)",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function scanRoot(root: RepoRoot): {
|
|
117
|
+
allViolations: readonly RawViolation[];
|
|
118
|
+
outsideRoot: readonly string[];
|
|
119
|
+
scannedFiles: number;
|
|
120
|
+
} {
|
|
121
|
+
const tsConfigFilePath = frameworkTsConfigPath();
|
|
122
|
+
const project =
|
|
123
|
+
tsConfigFilePath !== undefined
|
|
124
|
+
? new Project({
|
|
125
|
+
tsConfigFilePath,
|
|
126
|
+
skipAddingFilesFromTsConfig: true,
|
|
127
|
+
skipFileDependencyResolution: true,
|
|
128
|
+
})
|
|
129
|
+
: new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true });
|
|
130
|
+
|
|
131
|
+
const paths = scanFiles(SCAN, [root]);
|
|
132
|
+
// Exact-path lookup, never a re-glob: `project.getSourceFiles(paths)`
|
|
133
|
+
// treats each array entry as a glob pattern — pathologically slow (and,
|
|
134
|
+
// for a bracketed filename like `[id].tsx`, a broken character class)
|
|
135
|
+
// over thousands of paths. `getSourceFile(path)` is an exact lookup.
|
|
136
|
+
const scannedFiles = paths
|
|
137
|
+
.map((p) => project.getSourceFile(p) ?? project.addSourceFileAtPath(p))
|
|
138
|
+
.filter((f) => {
|
|
139
|
+
const fp = f.getFilePath();
|
|
140
|
+
return !fp.includes("/node_modules/") && !fp.includes("/dist/");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const workspaceCache = new Map<string, Runtime | null>();
|
|
144
|
+
const clientReachable = computeClientReachablePaths(scannedFiles, (sf) => {
|
|
145
|
+
const rel = path.relative(root.absPath, sf.getFilePath());
|
|
146
|
+
if (isClientEntryPath(rel)) return true;
|
|
147
|
+
return classify(sf.getFilePath(), root.absPath, workspaceCache) === "client";
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { violations: allViolations, outsideRoot } = findRuntimeIsolationViolations(
|
|
151
|
+
scannedFiles,
|
|
152
|
+
root.absPath,
|
|
153
|
+
workspaceCache,
|
|
154
|
+
clientReachable,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return { allViolations, outsideRoot, scannedFiles: scannedFiles.length };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const check: RepoCheck = {
|
|
161
|
+
name: "Runtime-Isolation Check",
|
|
162
|
+
hint:
|
|
163
|
+
"runtime may import runtime/client only; client may import client only; " +
|
|
164
|
+
"dev/tooling/test are more permissive. See runtime-isolation-classify.ts for the compat matrix. " +
|
|
165
|
+
"New, deliberate exception? `bun packages/guards/src/check-runtime-isolation.ts --write-baseline`",
|
|
166
|
+
run(roots) {
|
|
167
|
+
const root = roots[0];
|
|
168
|
+
if (!root) return { violations: [], matchedFiles: 0, notApplicable: true };
|
|
169
|
+
const rootAbsPath = root.absPath;
|
|
170
|
+
|
|
171
|
+
const { allViolations, outsideRoot, scannedFiles } = scanRoot(root);
|
|
172
|
+
for (const v of allViolations) printViolation(v, rootAbsPath);
|
|
173
|
+
|
|
174
|
+
const counts = countByKey(rootAbsPath, allViolations);
|
|
175
|
+
const resolveLine = (key: string): number => {
|
|
176
|
+
const relFile = key.split("::")[0];
|
|
177
|
+
return allViolations.find((v) => path.relative(rootAbsPath, v.file) === relFile)?.line ?? 1;
|
|
178
|
+
};
|
|
179
|
+
const violations: GuardViolation[] = ratchetFor(rootAbsPath).check(
|
|
180
|
+
counts,
|
|
181
|
+
"runtime may import runtime/client only; client may import client only — see runtime-isolation-classify.ts for the compat matrix.",
|
|
182
|
+
{
|
|
183
|
+
formatDriftRemediation:
|
|
184
|
+
"Run `bun packages/guards/src/check-runtime-isolation.ts --write-baseline` once.",
|
|
185
|
+
resolveLine,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (outsideRoot.length > 0) {
|
|
190
|
+
console.log(
|
|
191
|
+
` Runtime-Isolation Check: ${outsideRoot.length} import target(s) outside the repo root (skipped).`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { violations, matchedFiles: scannedFiles, notApplicable: false };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (import.meta.main) {
|
|
200
|
+
const args = process.argv.slice(2);
|
|
201
|
+
if (args.includes("--write-baseline")) {
|
|
202
|
+
const root = resolveRepoRoots()[0];
|
|
203
|
+
if (!root) {
|
|
204
|
+
console.error("No repo root resolved — nothing to baseline.");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
const { allViolations } = scanRoot(root);
|
|
208
|
+
for (const v of allViolations) printViolation(v, root.absPath);
|
|
209
|
+
ratchetFor(root.absPath).write(countByKey(root.absPath, allViolations));
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
const failed = reportResults(await runRepoChecks([check]));
|
|
213
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
214
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,55 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Consumer entry point for `bunx @cosmicdrift/kumiko-guards`. Runs all three
|
|
3
3
|
// suites (or one, via subcommand) using the exact call forms the three
|
|
4
|
-
// runners already use in their own `if (import.meta.main)` blocks
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import { REPO_CHECKS } from "./run-repo-checks";
|
|
14
|
-
import { UI_GUARDS } from "./run-ui-guards";
|
|
15
|
-
|
|
16
|
-
const SUBCOMMANDS = ["guards", "ui", "checks"] as const;
|
|
4
|
+
// runners already use in their own `if (import.meta.main)` blocks — including
|
|
5
|
+
// flags, which each runner's own *Cli function validates and applies so this
|
|
6
|
+
// bin and the direct `bun run-*.ts` invocation can never drift apart.
|
|
7
|
+
import { buildGuardKitInventory, cliFlagsError } from "./_lib/guard-kit";
|
|
8
|
+
import { GUARD_FLAGS, GUARDS, runGuardsCli } from "./run-guards";
|
|
9
|
+
import { REPO_CHECK_FLAGS, REPO_CHECKS, runRepoChecksCli } from "./run-repo-checks";
|
|
10
|
+
import { runUiGuardsCli, UI_GUARD_FLAGS, UI_GUARDS } from "./run-ui-guards";
|
|
11
|
+
|
|
12
|
+
const SUBCOMMANDS = ["guards", "ui", "checks", "list"] as const;
|
|
17
13
|
type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
18
14
|
|
|
15
|
+
const SUBCOMMAND_FLAGS: Record<Subcommand, readonly string[]> = {
|
|
16
|
+
guards: GUARD_FLAGS,
|
|
17
|
+
ui: UI_GUARD_FLAGS,
|
|
18
|
+
checks: REPO_CHECK_FLAGS,
|
|
19
|
+
list: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
19
22
|
function isSubcommand(value: string): value is Subcommand {
|
|
20
23
|
return (SUBCOMMANDS as readonly string[]).includes(value);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const project = buildSharedProject(UI_GUARDS);
|
|
31
|
-
printGuardKitBanner(UI_GUARDS.length, project);
|
|
32
|
-
return reportResults(runGuards(UI_GUARDS, project));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function runRepoChecksSuite(): Promise<number> {
|
|
36
|
-
printGuardKitBanner(REPO_CHECKS.length);
|
|
37
|
-
return reportResults(await runRepoChecks(REPO_CHECKS));
|
|
26
|
+
function printHelp(): void {
|
|
27
|
+
console.log("Usage: kumiko-guards [guards|ui|checks|list] [flags]");
|
|
28
|
+
console.log();
|
|
29
|
+
for (const sub of SUBCOMMANDS) {
|
|
30
|
+
const flags = SUBCOMMAND_FLAGS[sub];
|
|
31
|
+
console.log(` ${sub}${flags.length > 0 ? ` [${flags.join("|")}]` : ""}`);
|
|
32
|
+
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
async function main(): Promise<void> {
|
|
41
36
|
const subcommand = process.argv[2];
|
|
37
|
+
if (subcommand === "--help" || subcommand === "-h") {
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
42
41
|
if (subcommand !== undefined && !isSubcommand(subcommand)) {
|
|
43
42
|
console.error(
|
|
44
43
|
`Unknown subcommand "${subcommand}". Valid subcommands: ${SUBCOMMANDS.join(", ")}`,
|
|
45
44
|
);
|
|
46
45
|
process.exit(1);
|
|
47
46
|
}
|
|
47
|
+
const flags = process.argv.slice(3);
|
|
48
|
+
|
|
49
|
+
if (subcommand === "list") {
|
|
50
|
+
const flagsError = cliFlagsError("list", flags, SUBCOMMAND_FLAGS.list);
|
|
51
|
+
if (flagsError !== undefined) {
|
|
52
|
+
console.error(flagsError);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Registration inventory only — no scan, no project, no guard.run(). What
|
|
56
|
+
// CI checks against instead of running the guards: a guard dropped from a
|
|
57
|
+
// suite's array goes missing here too, not just silently from a run.
|
|
58
|
+
const inventory = buildGuardKitInventory({
|
|
59
|
+
guards: GUARDS,
|
|
60
|
+
uiGuards: UI_GUARDS,
|
|
61
|
+
checks: REPO_CHECKS,
|
|
62
|
+
});
|
|
63
|
+
console.log(JSON.stringify(inventory));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
48
66
|
|
|
49
67
|
let failed = 0;
|
|
50
|
-
if (subcommand === undefined || subcommand === "guards")
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
if (subcommand === undefined || subcommand === "guards") {
|
|
69
|
+
failed += runGuardsCli(subcommand === "guards" ? flags : []);
|
|
70
|
+
}
|
|
71
|
+
if (subcommand === undefined || subcommand === "ui") {
|
|
72
|
+
failed += runUiGuardsCli(subcommand === "ui" ? flags : []);
|
|
73
|
+
}
|
|
74
|
+
if (subcommand === undefined || subcommand === "checks") {
|
|
75
|
+
failed += await runRepoChecksCli(subcommand === "checks" ? flags : []);
|
|
76
|
+
}
|
|
53
77
|
|
|
54
78
|
process.exit(failed > 0 ? 1 : 0);
|
|
55
79
|
}
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* 2. `failUnprocessable(X, ...)` — same rule.
|
|
22
22
|
* 3. Object literals containing `reason: "X"` — same rule for the X.
|
|
23
23
|
* Skips `openToAll: { reason: "..." }` / `escapeHatch: { reason: "..." }`
|
|
24
|
-
* — those are prose
|
|
25
|
-
*
|
|
24
|
+
* and `declareEscapeHatch({ reason: "..." })` — those are prose
|
|
25
|
+
* access-declaration justifications, enforced instead by
|
|
26
|
+
* guard-open-to-all-reason.ts / guard-escape-hatch-declared.ts.
|
|
26
27
|
*
|
|
27
28
|
* Non-literal reasons (computed, template strings with interpolation,
|
|
28
29
|
* identifier references) are assumed to be typed-from-a-const and pass.
|
|
@@ -134,14 +135,21 @@ function scanFile(sf: SourceFile): Violation[] {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// A `reason` PropertyAssignment whose object literal is the initializer of
|
|
137
|
-
// an `openToAll`
|
|
138
|
+
// an `openToAll` / `escapeHatch` PropertyAssignment, or the sole argument
|
|
139
|
+
// object of a bare `declareEscapeHatch(...)` call, is an access-declaration
|
|
138
140
|
// justification, not an error-reason code.
|
|
139
141
|
export function isAccessDeclarationReason(prop: Node): boolean {
|
|
140
142
|
const objectLiteral = prop.getParent();
|
|
141
143
|
if (!objectLiteral?.isKind(SyntaxKind.ObjectLiteralExpression)) return false;
|
|
142
144
|
const owner = objectLiteral.getParent();
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
+
if (owner?.isKind(SyntaxKind.PropertyAssignment)) {
|
|
146
|
+
return ACCESS_DECLARATION_NAMES.has(owner.getName());
|
|
147
|
+
}
|
|
148
|
+
if (owner?.isKind(SyntaxKind.CallExpression)) {
|
|
149
|
+
const callee = owner.getExpression();
|
|
150
|
+
return callee.isKind(SyntaxKind.Identifier) && callee.getText() === "declareEscapeHatch";
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
// Returns the offending string if this node is a string literal that does
|