@dailephd/my-dev-kit-lab 0.2.0 → 0.2.2

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -22
  3. package/dist/scripts/security/resolveToolRoot.js +16 -0
  4. package/dist/scripts/security/runCodeql.js +2 -1
  5. package/dist/scripts/security/runDependencyChecks.js +2 -1
  6. package/dist/scripts/security/runPackageChecks.js +2 -1
  7. package/dist/scripts/security/runSemgrep.js +2 -1
  8. package/dist/scripts/security/validate.js +70 -30
  9. package/dist/src/core/localProjectTarget.js +4 -0
  10. package/dist/src/core/runMeasuredCommand.js +10 -6
  11. package/dist/src/securityValidation/attackScenarios/attackProfile.js +20 -0
  12. package/dist/src/securityValidation/attackScenarios/attackResult.js +37 -0
  13. package/dist/src/securityValidation/attackScenarios/attackRunner.js +153 -0
  14. package/dist/src/securityValidation/attackScenarios/attackScenario.js +21 -0
  15. package/dist/src/securityValidation/attackScenarios/boundedSourceScan.js +100 -0
  16. package/dist/src/securityValidation/attackScenarios/exploitEvidence.js +69 -0
  17. package/dist/src/securityValidation/attackScenarios/payloadCorpus.js +0 -0
  18. package/dist/src/securityValidation/attackScenarios/profiles/localToolProfile.js +10 -0
  19. package/dist/src/securityValidation/attackScenarios/profiles/nodeCliPackageProfile.js +20 -0
  20. package/dist/src/securityValidation/attackScenarios/profiles/npmPackageProfile.js +10 -0
  21. package/dist/src/securityValidation/attackScenarios/reportSchemaGuard.js +41 -0
  22. package/dist/src/securityValidation/attackScenarios/scenarios/configInjectionScenario.js +144 -0
  23. package/dist/src/securityValidation/attackScenarios/scenarios/networkAssumptionScenario.js +129 -0
  24. package/dist/src/securityValidation/attackScenarios/scenarios/outputBoundaryScenario.js +69 -0
  25. package/dist/src/securityValidation/attackScenarios/scenarios/packageBoundaryScenario.js +118 -0
  26. package/dist/src/securityValidation/attackScenarios/scenarios/pathTraversalScenario.js +119 -0
  27. package/dist/src/securityValidation/attackScenarios/scenarios/reportPoisoningScenario.js +176 -0
  28. package/dist/src/securityValidation/attackScenarios/scenarios/secretLeakageScenario.js +143 -0
  29. package/dist/src/securityValidation/attackScenarios/scenarios/subprocessInjectionScenario.js +117 -0
  30. package/dist/src/securityValidation/attackScenarios/scenarios/targetSandboxScenario.js +118 -0
  31. package/dist/src/securityValidation/attackScenarios/targetSnapshot.js +70 -0
  32. package/dist/src/securityValidation/packageChecks/forbiddenPackageContents.js +4 -1
  33. package/dist/src/securityValidation/report/renderSecurityReport.js +153 -12
  34. package/dist/src/securityValidation/types.js +7 -0
  35. package/dist/src/securityValidation/validate/cliOptions.js +182 -0
  36. package/dist/src/securityValidation/validate/runCliSecuritySuiteCheck.js +124 -0
  37. package/dist/src/securityValidation/validate/runSecurityValidation.js +135 -67
  38. package/dist/src/securityValidation/validate/verdict.js +147 -0
  39. package/package.json +14 -10
@@ -6,10 +6,30 @@ import { runCodeqlCheck } from "../staticScans/codeql.js";
6
6
  import { runSemgrepCheck } from "../staticScans/semgrep.js";
7
7
  import { runAllFuzzTargets } from "../fuzz/fuzzHarness.js";
8
8
  import { ALL_FUZZ_TARGETS } from "../fuzz/fuzzTargets.js";
9
- import { runSecurityCommand, resolveNpmCommand } from "../commandRunner.js";
10
- import { calculateVerdict } from "./verdict.js";
9
+ import { calculateVerdict, summarizeVerdictReasoning } from "./verdict.js";
11
10
  import { resolveValidationTarget, targetDescription as describeTarget } from "./resolveTarget.js";
12
11
  import { DEFAULT_SECURITY_CONFIG } from "../config.js";
12
+ import { runCliSecuritySuiteCheck } from "./runCliSecuritySuiteCheck.js";
13
+ import { DEFAULT_SECURITY_CHECKS, DEFAULT_SECURITY_PROFILE, IMPLEMENTED_SECURITY_CHECK_IDS, PLANNED_SECURITY_CHECK_IDS, } from "./cliOptions.js";
14
+ import { runAttackScenarios } from "../attackScenarios/attackRunner.js";
15
+ import { toSecurityCheckResult } from "../attackScenarios/attackResult.js";
16
+ import { captureTargetSnapshot } from "../attackScenarios/targetSnapshot.js";
17
+ const NOT_SELECTED_REASON = "Not selected via --checks.";
18
+ function skippedCheck(id, name, category, skippedReason) {
19
+ const now = new Date().toISOString();
20
+ return {
21
+ id,
22
+ name,
23
+ category,
24
+ status: "skipped",
25
+ severity: "skipped",
26
+ startedAt: now,
27
+ finishedAt: now,
28
+ durationMs: 0,
29
+ findings: [],
30
+ skippedReason,
31
+ };
32
+ }
13
33
  export async function runSecurityValidation(options) {
14
34
  const toolRoot = options.cwd;
15
35
  const config = {
@@ -20,9 +40,15 @@ export async function runSecurityValidation(options) {
20
40
  };
21
41
  // Resolve and validate the target project
22
42
  const target = resolveValidationTarget(options.targetPath, toolRoot);
43
+ // v0.2.2 Batch 3: read-only git-status snapshot taken before any checks run,
44
+ // so the target-sandbox attack scenario (which runs last, among the
45
+ // attack-scenario checks) can distinguish pre-existing target dirtiness
46
+ // from changes caused by this validation run.
47
+ const targetSnapshotBefore = captureTargetSnapshot(target.targetRoot, target.hasGit);
23
48
  const startedAt = new Date().toISOString();
24
49
  const allChecks = [];
25
50
  const allFindings = [];
51
+ const selectedChecks = new Set(options.selectedChecks ?? DEFAULT_SECURITY_CHECKS);
26
52
  function collect(check) {
27
53
  allChecks.push(check);
28
54
  allFindings.push(...check.findings);
@@ -34,88 +60,122 @@ export async function runSecurityValidation(options) {
34
60
  // --- Dependency checks (runs in target root) ---
35
61
  // If target has no package.json/lockfile, runDependencyChecks will surface that
36
62
  // as a structured finding rather than crashing.
37
- try {
38
- const depOutput = await runDependencyChecks({ cwd: target.targetRoot, config });
39
- collectMany(depOutput.checks);
63
+ if (selectedChecks.has("deps")) {
64
+ try {
65
+ const depOutput = await runDependencyChecks({ cwd: target.targetRoot, config });
66
+ collectMany(depOutput.checks);
67
+ }
68
+ catch (err) {
69
+ collect(errorCheck("dependency-checks", "Dependency checks", "dependency-audit", err));
70
+ }
40
71
  }
41
- catch (err) {
42
- collect(errorCheck("dependency-checks", "Dependency checks", "dependency-audit", err));
72
+ else {
73
+ // Preserve the mandatory-check IDs verdict.ts relies on so deselecting this
74
+ // group still shows up as a skipped mandatory check, not a silent gap.
75
+ collect(skippedCheck("npm-audit-full", "npm audit (full)", "dependency-audit", NOT_SELECTED_REASON));
76
+ collect(skippedCheck("npm-audit-runtime", "npm audit (runtime)", "dependency-audit", NOT_SELECTED_REASON));
43
77
  }
44
78
  // --- Package content checks (runs in target root) ---
45
79
  // Not all projects are publishable npm packages; runPackageChecks handles
46
80
  // missing package.json gracefully.
47
- try {
48
- const pkgOutput = await runPackageChecks({ cwd: target.targetRoot, config });
49
- collectMany(pkgOutput.checks);
81
+ if (selectedChecks.has("package")) {
82
+ try {
83
+ const pkgOutput = await runPackageChecks({ cwd: target.targetRoot, config });
84
+ collectMany(pkgOutput.checks);
85
+ }
86
+ catch (err) {
87
+ collect(errorCheck("package-content", "Package content checks", "package-content", err));
88
+ }
50
89
  }
51
- catch (err) {
52
- collect(errorCheck("package-content", "Package content checks", "package-content", err));
90
+ else {
91
+ collect(skippedCheck("npm-pack-dry-run", "npm pack --dry-run", "package-content", NOT_SELECTED_REASON));
53
92
  }
54
93
  // --- Static scans (scan target root; config from tool root) ---
55
- try {
56
- collect(await runCodeqlCheck({ cwd: toolRoot, targetRoot: target.targetRoot, timeoutMs: config.commandTimeoutMs }));
94
+ if (selectedChecks.has("static")) {
95
+ try {
96
+ collect(await runCodeqlCheck({ cwd: toolRoot, targetRoot: target.targetRoot, timeoutMs: config.commandTimeoutMs }));
97
+ }
98
+ catch (err) {
99
+ collect(errorCheck("codeql-scan", "CodeQL static analysis", "static-scan", err));
100
+ }
101
+ try {
102
+ collect(await runSemgrepCheck({
103
+ targetRoot: target.targetRoot,
104
+ toolRoot,
105
+ timeoutMs: config.commandTimeoutMs,
106
+ }));
107
+ }
108
+ catch (err) {
109
+ collect(errorCheck("semgrep-scan", "Semgrep static analysis", "static-scan", err));
110
+ }
57
111
  }
58
- catch (err) {
59
- collect(errorCheck("codeql-scan", "CodeQL static analysis", "static-scan", err));
112
+ else {
113
+ collect(skippedCheck("codeql-scan", "CodeQL static analysis", "static-scan", NOT_SELECTED_REASON));
114
+ collect(skippedCheck("semgrep-scan", "Semgrep static analysis", "static-scan", NOT_SELECTED_REASON));
60
115
  }
61
- try {
62
- collect(await runSemgrepCheck({
63
- targetRoot: target.targetRoot,
116
+ // --- Security suite check ---
117
+ // Self-validation runs the lab's own test:security suite.
118
+ // External validation runs the target project's test:security script when present.
119
+ if (selectedChecks.has("cli-adversarial")) {
120
+ collect(await runCliSecuritySuiteCheck({
64
121
  toolRoot,
122
+ target,
65
123
  timeoutMs: config.commandTimeoutMs,
66
124
  }));
67
125
  }
68
- catch (err) {
69
- collect(errorCheck("semgrep-scan", "Semgrep static analysis", "static-scan", err));
70
- }
71
- // --- CLI adversarial tests (always tool-internal; tests my-dev-kit-lab harness) ---
72
- // These run in the tool root because they use fake-adversarial-cli.js fixtures.
73
- // Labeled clearly so the report shows they are not target-specific checks.
74
- {
75
- const startedAt2 = new Date().toISOString();
76
- const npm = resolveNpmCommand();
77
- const cmd = await runSecurityCommand({
78
- command: npm,
79
- args: ["run", "test:security", "--", "--reporter=json"],
80
- cwd: toolRoot,
81
- timeoutMs: Math.min(config.commandTimeoutMs * 3, 180_000),
82
- });
83
- const finishedAt2 = new Date().toISOString();
84
- const passed = cmd.exitCode === 0;
85
- collect({
86
- id: "cli-adversarial-suite",
87
- name: "CLI adversarial test suite (tool self-tests, not target-specific)",
88
- category: "cli-adversarial",
89
- status: passed ? "passed" : "failed",
90
- severity: "major",
91
- startedAt: startedAt2,
92
- finishedAt: finishedAt2,
93
- durationMs: cmd.durationMs,
94
- findings: passed
95
- ? []
96
- : [
97
- {
98
- id: "cli-adversarial-suite-failed",
99
- title: "CLI adversarial test suite had failures",
100
- severity: "major",
101
- category: "cli-adversarial",
102
- description: `test:security exited with code ${String(cmd.exitCode)}. Run 'npm run test:security' for details.`,
103
- recommendation: "Fix failing adversarial tests before release.",
104
- releaseImpact: "Should fix before release",
105
- },
106
- ],
107
- command: `${npm} run test:security`,
108
- });
126
+ else {
127
+ collect(skippedCheck("cli-adversarial-suite", "Target security test suite", "cli-adversarial", NOT_SELECTED_REASON));
109
128
  }
110
129
  // --- Fuzz smoke (always tool-internal; tests library parser/helper code) ---
111
- try {
112
- collect(await runAllFuzzTargets(ALL_FUZZ_TARGETS, {
113
- seed: options.fuzzSeed ?? 0xdeadbeef,
114
- iterations: options.fuzzIterations ?? 50,
115
- }));
130
+ if (selectedChecks.has("fuzz")) {
131
+ try {
132
+ collect(await runAllFuzzTargets(ALL_FUZZ_TARGETS, {
133
+ seed: options.fuzzSeed ?? 0xdeadbeef,
134
+ iterations: options.fuzzIterations ?? 50,
135
+ }));
136
+ }
137
+ catch (err) {
138
+ collect(errorCheck("fuzz-smoke", "Fuzz smoke", "fuzz-smoke", err));
139
+ }
140
+ }
141
+ else {
142
+ collect(skippedCheck("fuzz-smoke", "Fuzz smoke", "fuzz-smoke", NOT_SELECTED_REASON));
116
143
  }
117
- catch (err) {
118
- collect(errorCheck("fuzz-smoke", "Fuzz smoke", "fuzz-smoke", err));
144
+ // --- Attack-scenario checks (boundary, subprocess, secrets, network) ---
145
+ // v0.2.2 Batch 2: routed through the attack-scenario runner. The registry
146
+ // is empty in Batch 2, so these currently resolve to explicit "no
147
+ // scenarios registered yet" skipped results — never "passed".
148
+ const profile = options.profile ?? DEFAULT_SECURITY_PROFILE;
149
+ const attackScenarioChecks = PLANNED_SECURITY_CHECK_IDS.filter((id) => selectedChecks.has(id));
150
+ let attackResults = [];
151
+ if (attackScenarioChecks.length > 0) {
152
+ try {
153
+ attackResults = await runAttackScenarios({
154
+ selectedChecks: attackScenarioChecks,
155
+ profile,
156
+ toolRoot,
157
+ target,
158
+ config,
159
+ targetSnapshotBefore,
160
+ });
161
+ }
162
+ catch (err) {
163
+ attackResults = attackScenarioChecks.map((checkId) => ({
164
+ scenarioId: `${checkId}-attack-runner-error`,
165
+ scenarioTitle: `Attack scenario runner error for '${checkId}'`,
166
+ checkId,
167
+ profileId: profile,
168
+ status: "blocked",
169
+ severity: "major",
170
+ confidence: "low",
171
+ evidence: [],
172
+ category: "cli-adversarial",
173
+ errorSummary: err instanceof Error ? err.message : String(err),
174
+ }));
175
+ }
176
+ for (const result of attackResults) {
177
+ collect(toSecurityCheckResult(result));
178
+ }
119
179
  }
120
180
  const finishedAt = new Date().toISOString();
121
181
  // --- Tool root metadata ---
@@ -133,6 +193,11 @@ export async function runSecurityValidation(options) {
133
193
  // Use defaults.
134
194
  }
135
195
  const { verdict, recommendedNextStep } = calculateVerdict(allChecks, allFindings);
196
+ const verdictReasonSummary = summarizeVerdictReasoning(allChecks, allFindings);
197
+ // "Full release gate" = every classic pre-v0.2.2 check group was selected.
198
+ // Attack-scenario checks (boundary/subprocess/secrets/network) are
199
+ // additional coverage, not part of that traditional baseline gate.
200
+ const isFullReleaseGate = IMPLEMENTED_SECURITY_CHECK_IDS.every((id) => selectedChecks.has(id));
136
201
  return {
137
202
  toolRoot,
138
203
  toolPackageName,
@@ -150,6 +215,9 @@ export async function runSecurityValidation(options) {
150
215
  findings: allFindings,
151
216
  verdict,
152
217
  recommendedNextStep,
218
+ attackResults,
219
+ verdictReasonSummary,
220
+ isFullReleaseGate,
153
221
  };
154
222
  }
155
223
  function errorCheck(id, name, category, err) {
@@ -1,3 +1,4 @@
1
+ import { VERDICT_IMPACTS } from "../types.js";
1
2
  // ---------------------------------------------------------------------------
2
3
  // Verdict calculation
3
4
  //
@@ -71,3 +72,149 @@ export function verdictToHumanLabel(verdict) {
71
72
  return "inconclusive: audit environment incomplete";
72
73
  }
73
74
  }
75
+ // ---------------------------------------------------------------------------
76
+ // v0.2.2 Batch 5 — fail-on severity threshold
77
+ //
78
+ // --fail-on uses a separate vocabulary (blocker/high/medium/low) from
79
+ // SecuritySeverity (blocker/major/minor/informational/skipped) — Batch 1
80
+ // deliberately kept these distinct. This maps one onto the other so a
81
+ // --fail-on breach can be detected without renaming any existing severity.
82
+ // Mapping (conservative — never understates a blocker-level finding):
83
+ // blocker fail-on -> only "blocker" severity findings breach it
84
+ // high fail-on -> "blocker" or "major" ("major" is treated as "high")
85
+ // medium fail-on -> "blocker", "major", or "minor" ("minor" -> "medium")
86
+ // low fail-on -> "blocker", "major", "minor", or "informational"
87
+ // "skipped" severity never breaches any threshold — a skipped optional tool
88
+ // is not a finding.
89
+ // ---------------------------------------------------------------------------
90
+ const SEVERITY_THRESHOLD_RANK = {
91
+ blocker: 4,
92
+ major: 3,
93
+ minor: 2,
94
+ informational: 1,
95
+ skipped: 0,
96
+ };
97
+ const FAIL_ON_THRESHOLD_RANK = {
98
+ blocker: 4,
99
+ high: 3,
100
+ medium: 2,
101
+ low: 1,
102
+ };
103
+ export function severityMeetsFailOnThreshold(severity, threshold) {
104
+ return SEVERITY_THRESHOLD_RANK[severity] >= FAIL_ON_THRESHOLD_RANK[threshold];
105
+ }
106
+ // True when at least one finding's severity breaches the given --fail-on
107
+ // threshold. Callers should OR this with the existing verdict-based exit
108
+ // decision, never use it to replace/weaken that decision — this only adds
109
+ // stricter exit-1 triggers for --fail-on medium/low (which the default
110
+ // verdict policy doesn't already cover via its blocker/major rules); for
111
+ // --fail-on blocker/high it is always a no-op given the existing policy.
112
+ export function findingsBreachFailOnThreshold(findings, threshold) {
113
+ return findings.some((f) => severityMeetsFailOnThreshold(f.severity, threshold));
114
+ }
115
+ export const VERDICT_REASON_CATEGORIES = [
116
+ "release-blocker",
117
+ "target-project-blocker",
118
+ "tool-framework-blocker",
119
+ "environment-inconclusive",
120
+ "optional-skipped-tool",
121
+ "adversarial-scenario-skipped",
122
+ "scanner-finding",
123
+ "adversarial-scenario-failure",
124
+ "required-evidence-missing",
125
+ "informational-evidence",
126
+ ];
127
+ // v0.2.2 Batch 6: scenario blocker-impact classification now comes directly
128
+ // from SecurityCheckResult.verdictImpact (carried through from the
129
+ // originating AttackScenario's static declaration — see attackScenario.ts /
130
+ // attackResult.ts). verdict.ts no longer owns a separate scenario-id map, so
131
+ // there is nothing to keep in sync when a new scenario is registered — as
132
+ // long as the scenario declares verdictImpact, categorization is correct
133
+ // automatically. Checks with no verdictImpact but a scenario-shaped id
134
+ // (placeholder/error results, or a scenario that doesn't declare one) fall
135
+ // back to the generic "adversarial-scenario-failure"/"-skipped" buckets.
136
+ //
137
+ // A check is "scenario-shaped" if it carries a verdictImpact OR its id
138
+ // matches the synthetic placeholder/error patterns produced by
139
+ // attackRunner.ts for unregistered checks or runner crashes (those never
140
+ // carry verdictImpact since there's no real AttackScenario object).
141
+ function isAttackScenarioCheckId(id) {
142
+ return id.endsWith("-no-scenarios-registered") || id.endsWith("-attack-runner-error");
143
+ }
144
+ // Categorizes a single check for verdict-reasoning purposes. Returns null
145
+ // for passed/warning checks — those don't need a blocker-reasoning bucket.
146
+ export function categorizeCheck(check) {
147
+ const scenario = isAttackScenarioCheckId(check.id) || check.verdictImpact !== undefined;
148
+ if (check.status === "skipped") {
149
+ if (scenario)
150
+ return "adversarial-scenario-skipped";
151
+ if (MANDATORY_CHECK_IDS.has(check.id))
152
+ return "required-evidence-missing";
153
+ return "optional-skipped-tool";
154
+ }
155
+ if (check.status === "failed") {
156
+ if (check.verdictImpact && VERDICT_IMPACTS.includes(check.verdictImpact)) {
157
+ return check.verdictImpact;
158
+ }
159
+ if (scenario)
160
+ return "adversarial-scenario-failure";
161
+ if (check.severity === "informational")
162
+ return "informational-evidence";
163
+ return "scanner-finding";
164
+ }
165
+ return null;
166
+ }
167
+ // Aggregates categorizeCheck() across all checks, plus a direct informational
168
+ // finding count (informational findings don't get their own check-level
169
+ // bucket since a check can carry multiple findings of mixed severity).
170
+ export function summarizeVerdictReasoning(checks, findings) {
171
+ const summary = {
172
+ releaseBlockerCount: 0,
173
+ targetProjectBlockerCount: 0,
174
+ toolFrameworkBlockerCount: 0,
175
+ environmentInconclusiveCount: 0,
176
+ optionalSkippedToolCount: 0,
177
+ adversarialScenarioSkippedCount: 0,
178
+ scannerFindingCount: 0,
179
+ adversarialScenarioFailureCount: 0,
180
+ requiredEvidenceMissingCount: 0,
181
+ informationalEvidenceCount: 0,
182
+ };
183
+ for (const check of checks) {
184
+ const category = categorizeCheck(check);
185
+ switch (category) {
186
+ case "release-blocker":
187
+ summary.releaseBlockerCount++;
188
+ break;
189
+ case "target-project-blocker":
190
+ summary.targetProjectBlockerCount++;
191
+ break;
192
+ case "tool-framework-blocker":
193
+ summary.toolFrameworkBlockerCount++;
194
+ break;
195
+ case "environment-inconclusive":
196
+ summary.environmentInconclusiveCount++;
197
+ break;
198
+ case "optional-skipped-tool":
199
+ summary.optionalSkippedToolCount++;
200
+ break;
201
+ case "adversarial-scenario-skipped":
202
+ summary.adversarialScenarioSkippedCount++;
203
+ break;
204
+ case "scanner-finding":
205
+ summary.scannerFindingCount++;
206
+ break;
207
+ case "adversarial-scenario-failure":
208
+ summary.adversarialScenarioFailureCount++;
209
+ break;
210
+ case "required-evidence-missing":
211
+ summary.requiredEvidenceMissingCount++;
212
+ break;
213
+ case "informational-evidence":
214
+ case null:
215
+ break;
216
+ }
217
+ }
218
+ summary.informationalEvidenceCount = findings.filter((f) => f.severity === "informational").length;
219
+ return summary;
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.2.0",
2
+ "name": "@dailephd/my-dev-kit-lab",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {
@@ -19,13 +19,13 @@
19
19
  "url": "git+https://github.com/dailephd/my-dev-kit-lab.git"
20
20
  },
21
21
  "homepage": "https://github.com/dailephd/my-dev-kit-lab#readme",
22
- "bugs": {
23
- "url": "https://github.com/dailephd/my-dev-kit-lab/issues"
24
- },
25
- "publishConfig": {
26
- "access": "public"
27
- },
28
- "funding": [
22
+ "bugs": {
23
+ "url": "https://github.com/dailephd/my-dev-kit-lab/issues"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "funding": [
29
29
  "https://github.com/sponsors/dailephd",
30
30
  "https://paypal.me/daile88"
31
31
  ],
@@ -38,9 +38,10 @@
38
38
  "claude"
39
39
  ],
40
40
  "author": "Dai Le",
41
- "license": "UNLICENSED",
41
+ "license": "MIT",
42
42
  "scripts": {
43
43
  "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json",
44
+ "typecheck": "tsc --noEmit",
44
45
  "test": "vitest run",
45
46
  "test:benchmarks": "vitest run tests/benchmarks tests/scripts",
46
47
  "test:report": "vitest run tests/report tests/commands",
@@ -85,6 +86,9 @@
85
86
  "typescript": "^5.8.3",
86
87
  "vitest": "^3.2.4"
87
88
  },
89
+ "overrides": {
90
+ "vite": "7.3.6"
91
+ },
88
92
  "engines": {
89
93
  "node": ">=20"
90
94
  }