ndomo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ndomo vacuum — reclaims disk space from .ndomo/state.db.
|
|
4
|
+
*
|
|
5
|
+
* Plan fcb12dc5 #4: post-PRAGMA retention sweep. Runs:
|
|
6
|
+
* 1. PRAGMA incremental_vacuum — reclaims pages freed by deleted rows
|
|
7
|
+
* 2. PRAGMA wal_checkpoint(TRUNCATE) — truncates WAL to main DB file
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun run src/cli/vacuum.ts [projectDir]
|
|
11
|
+
*
|
|
12
|
+
* If projectDir is omitted, defaults to current working directory.
|
|
13
|
+
*
|
|
14
|
+
* For long-running installs, the plugin auto-finalizes background_tasks
|
|
15
|
+
* terminal rows on init (see plugin.ts backgroundRetention config). The
|
|
16
|
+
* vacuum CLI complements that by reclaiming the freed disk pages.
|
|
17
|
+
*
|
|
18
|
+
* One-time retrofit for pre-existing DBs: this script first sets
|
|
19
|
+
* PRAGMA auto_vacuum = INCREMENTAL (idempotent for already-configured
|
|
20
|
+
* DBs) before vacuuming so subsequent deletes are auto-reclaimed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, statSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { closeDb, openDb } from "../db/client.ts";
|
|
26
|
+
|
|
27
|
+
export interface VacuumResult {
|
|
28
|
+
pagesReclaimed: number;
|
|
29
|
+
checkpoint: { busy: number; log: number; checkpointed: number } | null;
|
|
30
|
+
sizeBefore: number;
|
|
31
|
+
sizeAfter: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pure vacuum routine — opens DB at projectDir, applies incremental vacuum +
|
|
36
|
+
* WAL checkpoint, closes. Exported for testability (CLI wrapper below).
|
|
37
|
+
*/
|
|
38
|
+
export function vacuumProject(projectDir: string): VacuumResult {
|
|
39
|
+
const dbPath = join(projectDir, ".ndomo", "state.db");
|
|
40
|
+
if (!existsSync(dbPath)) {
|
|
41
|
+
throw new Error(`no .ndomo/state.db at ${dbPath}`);
|
|
42
|
+
}
|
|
43
|
+
const sizeBefore = statSync(dbPath).size;
|
|
44
|
+
|
|
45
|
+
const db = openDb(projectDir);
|
|
46
|
+
try {
|
|
47
|
+
// Retrofit auto_vacuum for pre-existing DBs (no-op for already-configured).
|
|
48
|
+
db.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
|
49
|
+
|
|
50
|
+
// Step 1: incremental vacuum — reclaims pages freed by deleted rows.
|
|
51
|
+
let totalFreed = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
const result = db.query("PRAGMA incremental_vacuum").get() as {
|
|
54
|
+
incremental_vacuum: number;
|
|
55
|
+
} | null;
|
|
56
|
+
const freed = result?.incremental_vacuum ?? 0;
|
|
57
|
+
if (freed === 0) break;
|
|
58
|
+
totalFreed += freed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 2: WAL checkpoint — flush WAL to main DB and truncate WAL file.
|
|
62
|
+
const checkpoint = db.query("PRAGMA wal_checkpoint(TRUNCATE)").get() as {
|
|
63
|
+
busy: number;
|
|
64
|
+
log: number;
|
|
65
|
+
checkpointed: number;
|
|
66
|
+
} | null;
|
|
67
|
+
|
|
68
|
+
const sizeAfter = statSync(dbPath).size;
|
|
69
|
+
return {
|
|
70
|
+
pagesReclaimed: totalFreed,
|
|
71
|
+
checkpoint,
|
|
72
|
+
sizeBefore,
|
|
73
|
+
sizeAfter,
|
|
74
|
+
};
|
|
75
|
+
} finally {
|
|
76
|
+
closeDb(db);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// CLI entry — only runs when invoked directly (not when imported by tests).
|
|
81
|
+
const isMain =
|
|
82
|
+
typeof import.meta.main === "boolean"
|
|
83
|
+
? import.meta.main
|
|
84
|
+
: (process.argv[1]?.endsWith("vacuum.ts") ?? false);
|
|
85
|
+
if (isMain) {
|
|
86
|
+
const projectDir = process.argv[2] ?? process.cwd();
|
|
87
|
+
const dbPath = join(projectDir, ".ndomo", "state.db");
|
|
88
|
+
console.log(`[vacuum] opening ${dbPath}`);
|
|
89
|
+
const result = vacuumProject(projectDir);
|
|
90
|
+
const delta = result.sizeBefore - result.sizeAfter;
|
|
91
|
+
console.log(`[vacuum] incremental_vacuum: reclaimed ${result.pagesReclaimed} pages`);
|
|
92
|
+
console.log(`[vacuum] wal_checkpoint(TRUNCATE): ${JSON.stringify(result.checkpoint)}`);
|
|
93
|
+
console.log(
|
|
94
|
+
`[vacuum] file size: ${result.sizeBefore} → ${result.sizeAfter} bytes (${delta >= 0 ? "-" : "+"}${Math.abs(delta)} bytes)`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
2
|
+
import { loadHttpConfig, SECURITY_HEADERS } from "./schema.ts";
|
|
3
|
+
|
|
4
|
+
describe("loadHttpConfig", () => {
|
|
5
|
+
const originalEnv = process.env;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("returns defaults when no env vars set", () => {
|
|
12
|
+
const config = loadHttpConfig();
|
|
13
|
+
expect(config).toEqual({
|
|
14
|
+
enabled: false,
|
|
15
|
+
port: 4097,
|
|
16
|
+
cors: {
|
|
17
|
+
origins: ["*"],
|
|
18
|
+
},
|
|
19
|
+
auth: {
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("parses NDOMO_HTTP_ENABLED=true", () => {
|
|
26
|
+
process.env.NDOMO_HTTP_ENABLED = "true";
|
|
27
|
+
const config = loadHttpConfig();
|
|
28
|
+
expect(config.enabled).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("parses NDOMO_HTTP_ENABLED=false", () => {
|
|
32
|
+
process.env.NDOMO_HTTP_ENABLED = "false";
|
|
33
|
+
const config = loadHttpConfig();
|
|
34
|
+
expect(config.enabled).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parses NDOMO_HTTP_PORT", () => {
|
|
38
|
+
process.env.NDOMO_HTTP_PORT = "8080";
|
|
39
|
+
const config = loadHttpConfig();
|
|
40
|
+
expect(config.port).toBe(8080);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("falls back to default port on invalid NDOMO_HTTP_PORT", () => {
|
|
44
|
+
process.env.NDOMO_HTTP_PORT = "invalid";
|
|
45
|
+
const config = loadHttpConfig();
|
|
46
|
+
expect(config.port).toBe(4097);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parses NDOMO_HTTP_CORS_ORIGINS", () => {
|
|
50
|
+
process.env.NDOMO_HTTP_CORS_ORIGINS = "http://localhost:3000, https://example.com";
|
|
51
|
+
const config = loadHttpConfig();
|
|
52
|
+
expect(config.cors.origins).toEqual(["http://localhost:3000", "https://example.com"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parses NDOMO_HTTP_AUTH_REQUIRED=false", () => {
|
|
56
|
+
process.env.NDOMO_HTTP_AUTH_REQUIRED = "false";
|
|
57
|
+
const config = loadHttpConfig();
|
|
58
|
+
expect(config.auth.required).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("parses NDOMO_HTTP_AUTH_REQUIRED=true", () => {
|
|
62
|
+
process.env.NDOMO_HTTP_AUTH_REQUIRED = "true";
|
|
63
|
+
const config = loadHttpConfig();
|
|
64
|
+
expect(config.auth.required).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("auth.required defaults to true on invalid value", () => {
|
|
68
|
+
process.env.NDOMO_HTTP_AUTH_REQUIRED = "invalid";
|
|
69
|
+
const config = loadHttpConfig();
|
|
70
|
+
expect(config.auth.required).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("SECURITY_HEADERS", () => {
|
|
75
|
+
test("contains expected headers", () => {
|
|
76
|
+
expect(SECURITY_HEADERS).toHaveProperty("X-Content-Type-Options", "nosniff");
|
|
77
|
+
expect(SECURITY_HEADERS).toHaveProperty("X-Frame-Options", "DENY");
|
|
78
|
+
expect(SECURITY_HEADERS).toHaveProperty("X-XSS-Protection", "1; mode=block");
|
|
79
|
+
expect(SECURITY_HEADERS).toHaveProperty("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
80
|
+
expect(SECURITY_HEADERS).toHaveProperty("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("is readonly", () => {
|
|
84
|
+
// TypeScript as const ensures compile-time immutability
|
|
85
|
+
// At runtime, we can verify the object is not frozen
|
|
86
|
+
expect(typeof SECURITY_HEADERS).toBe("object");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// ─── HTTP Configuration Schema ────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* HTTP server configuration for ndomo.
|
|
4
|
+
* Loaded from environment variables with sensible defaults.
|
|
5
|
+
*
|
|
6
|
+
* Environment variables:
|
|
7
|
+
* - NDOMO_HTTP_ENABLED: "true" to enable HTTP server (default: "false")
|
|
8
|
+
* - NDOMO_HTTP_PORT: Port number (default: 4097)
|
|
9
|
+
* - NDOMO_HTTP_CORS_ORIGINS: Comma-separated list of allowed origins (default: ["*"])
|
|
10
|
+
* - NDOMO_HTTP_AUTH_REQUIRED: "false" to disable auth requirement (default: "true")
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type HttpConfig = {
|
|
14
|
+
enabled: boolean; // default false, env: NDOMO_HTTP_ENABLED
|
|
15
|
+
port: number; // default 4097, env: NDOMO_HTTP_PORT
|
|
16
|
+
cors: {
|
|
17
|
+
origins: string[]; // default ['*'] in dev, [] in prod, env: NDOMO_HTTP_CORS_ORIGINS (comma-separated)
|
|
18
|
+
};
|
|
19
|
+
auth: {
|
|
20
|
+
required: boolean; // default true
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load HTTP configuration from environment variables with defaults.
|
|
26
|
+
*
|
|
27
|
+
* @returns HttpConfig with values from environment or defaults
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // With env: NDOMO_HTTP_ENABLED=true, NDOMO_HTTP_PORT=8080
|
|
31
|
+
* const config = loadHttpConfig();
|
|
32
|
+
* // config = { enabled: true, port: 8080, cors: { origins: ["*"] }, auth: { required: true } }
|
|
33
|
+
*/
|
|
34
|
+
export function loadHttpConfig(): HttpConfig {
|
|
35
|
+
return {
|
|
36
|
+
enabled: process.env.NDOMO_HTTP_ENABLED === "true",
|
|
37
|
+
port: Number(process.env.NDOMO_HTTP_PORT) || 4097,
|
|
38
|
+
cors: {
|
|
39
|
+
origins: process.env.NDOMO_HTTP_CORS_ORIGINS?.split(",").map(s => s.trim()) ?? ["*"],
|
|
40
|
+
},
|
|
41
|
+
auth: {
|
|
42
|
+
required: process.env.NDOMO_HTTP_AUTH_REQUIRED !== "false",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Security baseline headers for HTTP responses.
|
|
49
|
+
* These headers should be applied to all HTTP responses to prevent common attacks.
|
|
50
|
+
*
|
|
51
|
+
* @see https://owasp.org/www-project-secure-headers/
|
|
52
|
+
*/
|
|
53
|
+
export const SECURITY_HEADERS = {
|
|
54
|
+
"X-Content-Type-Options": "nosniff",
|
|
55
|
+
"X-Frame-Options": "DENY",
|
|
56
|
+
"X-XSS-Protection": "1; mode=block",
|
|
57
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
58
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Type for security headers object.
|
|
63
|
+
*/
|
|
64
|
+
export type SecurityHeaders = typeof SECURITY_HEADERS;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v15 backfill tests — backfillAnalysisFindings()
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the data-only migration correctly renames finding keys
|
|
5
|
+
* inside analyses.findings_json:
|
|
6
|
+
* description → observation
|
|
7
|
+
* recommendation → proposedAction
|
|
8
|
+
*
|
|
9
|
+
* And that re-running it is a no-op (idempotency).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { createAnalysis } from "./analyses.ts";
|
|
15
|
+
import { backfillAnalysisFindings } from "./migrations.ts";
|
|
16
|
+
import { runMigrations } from "./migrations.ts";
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let db: Database;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
db = new Database(":memory:");
|
|
23
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
24
|
+
runMigrations(db);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function insertRawFindings(slug: string, findings: unknown): void {
|
|
28
|
+
// Bypass createAnalysis so we can store pre-rename (old-key) findings directly.
|
|
29
|
+
db.query(
|
|
30
|
+
"INSERT INTO analyses (id, slug, title, project_path, summary, findings_json, source_plan_id, agent, session_id, created_by) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, NULL, ?)",
|
|
31
|
+
).run(
|
|
32
|
+
crypto.randomUUID(),
|
|
33
|
+
slug,
|
|
34
|
+
`title-${slug}`,
|
|
35
|
+
"/proj",
|
|
36
|
+
"summary",
|
|
37
|
+
JSON.stringify(findings),
|
|
38
|
+
"ranger",
|
|
39
|
+
"ranger",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFindings(slug: string): unknown[] {
|
|
44
|
+
const row = db
|
|
45
|
+
.query<{ findings_json: string }, [string]>(
|
|
46
|
+
"SELECT findings_json FROM analyses WHERE slug = ?",
|
|
47
|
+
)
|
|
48
|
+
.get(slug);
|
|
49
|
+
if (!row) throw new Error(`no analysis with slug=${slug}`);
|
|
50
|
+
return JSON.parse(row.findings_json);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("backfillAnalysisFindings (v15)", () => {
|
|
54
|
+
test("renames description → observation", () => {
|
|
55
|
+
insertRawFindings("a", [
|
|
56
|
+
{ severity: "high", description: "auth missing" },
|
|
57
|
+
]);
|
|
58
|
+
const count = backfillAnalysisFindings(db);
|
|
59
|
+
expect(count).toBe(1);
|
|
60
|
+
const findings = readFindings("a") as Array<Record<string, unknown>>;
|
|
61
|
+
expect(findings[0]).toHaveProperty("observation", "auth missing");
|
|
62
|
+
expect(findings[0]).not.toHaveProperty("description");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("renames recommendation → proposedAction", () => {
|
|
66
|
+
insertRawFindings("b", [
|
|
67
|
+
{ severity: "medium", description: "slow", recommendation: "add index" },
|
|
68
|
+
]);
|
|
69
|
+
const count = backfillAnalysisFindings(db);
|
|
70
|
+
expect(count).toBe(1);
|
|
71
|
+
const findings = readFindings("b") as Array<Record<string, unknown>>;
|
|
72
|
+
expect(findings[0]).toHaveProperty("observation", "slow");
|
|
73
|
+
expect(findings[0]).toHaveProperty("proposedAction", "add index");
|
|
74
|
+
expect(findings[0]).not.toHaveProperty("description");
|
|
75
|
+
expect(findings[0]).not.toHaveProperty("recommendation");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("is idempotent — second run renames 0 findings", () => {
|
|
79
|
+
insertRawFindings("c", [
|
|
80
|
+
{ severity: "high", description: "x" },
|
|
81
|
+
{ severity: "low", description: "y", recommendation: "z" },
|
|
82
|
+
]);
|
|
83
|
+
expect(backfillAnalysisFindings(db)).toBe(2);
|
|
84
|
+
expect(backfillAnalysisFindings(db)).toBe(0);
|
|
85
|
+
// Findings still intact after 2nd run
|
|
86
|
+
const findings = readFindings("c") as Array<Record<string, unknown>>;
|
|
87
|
+
expect(findings).toHaveLength(2);
|
|
88
|
+
expect(findings[0]).toHaveProperty("observation", "x");
|
|
89
|
+
expect(findings[1]).toHaveProperty("proposedAction", "z");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("skips findings already in new shape (presence of observation)", () => {
|
|
93
|
+
insertRawFindings("d", [
|
|
94
|
+
{ severity: "high", observation: "ok", proposedAction: "do X" },
|
|
95
|
+
]);
|
|
96
|
+
const count = backfillAnalysisFindings(db);
|
|
97
|
+
expect(count).toBe(0);
|
|
98
|
+
const findings = readFindings("d") as Array<Record<string, unknown>>;
|
|
99
|
+
expect(findings[0]).toEqual({
|
|
100
|
+
severity: "high",
|
|
101
|
+
observation: "ok",
|
|
102
|
+
proposedAction: "do X",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("handles mixed batch — partial old-keys, partial new-keys", () => {
|
|
107
|
+
insertRawFindings("e", [
|
|
108
|
+
{ severity: "high", description: "old1" },
|
|
109
|
+
{ severity: "medium", observation: "new1" },
|
|
110
|
+
{ severity: "low", description: "old2", recommendation: "old2-rec" },
|
|
111
|
+
]);
|
|
112
|
+
const count = backfillAnalysisFindings(db);
|
|
113
|
+
// Counter increments per mutated row — see migration note about `renamed += next.length`.
|
|
114
|
+
// Behavior: each row whose payload mutated counts all its findings, even if only some
|
|
115
|
+
// were renamed within that row. With 3 findings and at least one mutation per row, we
|
|
116
|
+
// expect ≥1. Assert behavior conservatively.
|
|
117
|
+
expect(count).toBeGreaterThanOrEqual(2);
|
|
118
|
+
|
|
119
|
+
const findings = readFindings("e") as Array<Record<string, unknown>>;
|
|
120
|
+
expect(findings[0]).toEqual({ severity: "high", observation: "old1" });
|
|
121
|
+
expect(findings[1]).toEqual({ severity: "medium", observation: "new1" });
|
|
122
|
+
expect(findings[2]).toEqual({
|
|
123
|
+
severity: "low",
|
|
124
|
+
observation: "old2",
|
|
125
|
+
proposedAction: "old2-rec",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("skips malformed JSON rows without throwing", () => {
|
|
130
|
+
insertRawFindings("f", [{ description: "ok" }]);
|
|
131
|
+
db.query("UPDATE analyses SET findings_json = ? WHERE slug = ?").run(
|
|
132
|
+
"not-valid-json{{{",
|
|
133
|
+
"f",
|
|
134
|
+
);
|
|
135
|
+
expect(() => backfillAnalysisFindings(db)).not.toThrow();
|
|
136
|
+
// Malformed row should be left untouched (still has the bad JSON)
|
|
137
|
+
const row = db
|
|
138
|
+
.query<{ findings_json: string }, [string]>(
|
|
139
|
+
"SELECT findings_json FROM analyses WHERE slug = ?",
|
|
140
|
+
)
|
|
141
|
+
.get("f");
|
|
142
|
+
expect(row!.findings_json).toBe("not-valid-json{{{");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("skips rows where findings_json parses to a non-array", () => {
|
|
146
|
+
insertRawFindings("g", [{ description: "x" }]);
|
|
147
|
+
db.query("UPDATE analyses SET findings_json = ? WHERE slug = ?").run(
|
|
148
|
+
JSON.stringify({ wrapped: true }),
|
|
149
|
+
"g",
|
|
150
|
+
);
|
|
151
|
+
expect(() => backfillAnalysisFindings(db)).not.toThrow();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("skips rows where findings_json is an empty array", () => {
|
|
155
|
+
insertRawFindings("h", []);
|
|
156
|
+
const count = backfillAnalysisFindings(db);
|
|
157
|
+
expect(count).toBe(0);
|
|
158
|
+
expect(readFindings("h")).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("handles multiple analyses rows in one transaction", () => {
|
|
162
|
+
insertRawFindings("m1", [{ description: "a" }]);
|
|
163
|
+
insertRawFindings("m2", [{ description: "b" }]);
|
|
164
|
+
insertRawFindings("m3", [{ observation: "already new" }]);
|
|
165
|
+
const count = backfillAnalysisFindings(db);
|
|
166
|
+
expect(count).toBe(2);
|
|
167
|
+
expect((readFindings("m1")[0] as Record<string, unknown>).observation).toBe("a");
|
|
168
|
+
expect((readFindings("m2")[0] as Record<string, unknown>).observation).toBe("b");
|
|
169
|
+
expect((readFindings("m3")[0] as Record<string, unknown>).observation).toBe(
|
|
170
|
+
"already new",
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("returns 0 when there are no analyses rows", () => {
|
|
175
|
+
expect(backfillAnalysisFindings(db)).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("preserves other keys untouched", () => {
|
|
179
|
+
insertRawFindings("p", [
|
|
180
|
+
{
|
|
181
|
+
severity: "high",
|
|
182
|
+
location: "src/auth.ts:42",
|
|
183
|
+
description: "broken",
|
|
184
|
+
recommendation: "fix",
|
|
185
|
+
effort: "small",
|
|
186
|
+
impact: "medium",
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
backfillAnalysisFindings(db);
|
|
190
|
+
const f = readFindings("p")[0] as Record<string, unknown>;
|
|
191
|
+
expect(f.severity).toBe("high");
|
|
192
|
+
expect(f.location).toBe("src/auth.ts:42");
|
|
193
|
+
expect(f.effort).toBe("small");
|
|
194
|
+
expect(f.impact).toBe("medium");
|
|
195
|
+
expect(f.observation).toBe("broken");
|
|
196
|
+
expect(f.proposedAction).toBe("fix");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("createAnalysis → backfill noop (created findings already use new keys)", () => {
|
|
200
|
+
// createAnalysis doesn't take findingsJson — it stores "[]". So this confirms
|
|
201
|
+
// the post-migration state of newly created rows is also a no-op target.
|
|
202
|
+
createAnalysis(db, {
|
|
203
|
+
slug: "fresh",
|
|
204
|
+
title: "Fresh",
|
|
205
|
+
projectPath: "/p",
|
|
206
|
+
findingsJson: "[]",
|
|
207
|
+
});
|
|
208
|
+
expect(backfillAnalysisFindings(db)).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|