ndomo 0.1.0 → 0.2.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/.env.example +4 -0
- package/README.es.md +29 -23
- package/README.md +64 -24
- package/bun.lock +447 -0
- package/docs/configuration.md +4 -4
- package/docs/installation.md +53 -34
- package/docs/installer.md +164 -0
- package/docs/integrations.md +1 -1
- package/docs/web-ui.md +124 -0
- package/package.json +43 -4
- package/scripts/install.sh +28 -0
- package/scripts/smoke-install.sh +47 -0
- package/scripts/smoke-web.sh +335 -0
- package/src/cli/__tests__/install.test.ts +733 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/install.ts +1292 -0
- package/src/config/__tests__/schema.test.ts +223 -0
- package/src/config/schema.ts +129 -16
- package/src/http/__tests__/auth.test.ts +10 -10
- package/src/http/__tests__/spa.test.ts +296 -0
- package/src/http/auth.ts +8 -1
- package/src/http/server.ts +71 -2
- package/.bun-version +0 -1
- package/.dockerignore +0 -79
- package/.editorconfig +0 -18
- package/.github/CODEOWNERS +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -2
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
- package/.github/dependabot.yml +0 -36
- package/.github/pull_request_template.md +0 -24
- package/.github/release.yml +0 -30
- package/.github/workflows/gitleaks.yml +0 -28
- package/.github/workflows/release-please.yml +0 -27
- package/.github/workflows/smoke.yml +0 -29
- package/.husky/commit-msg +0 -1
- package/CHANGELOG.md +0 -114
- package/Dockerfile +0 -32
- package/bin/ndomo-analyses.ts +0 -4
- package/bin/ndomo-status.ts +0 -4
- package/biome.json +0 -57
- package/commitlint.config.js +0 -3
- package/opencode.json +0 -5
- package/release-please-config.json +0 -11
- package/scripts/dev-bust-cache.sh +0 -164
- package/scripts/smoke-e2e.ts +0 -704
- package/scripts/smoke-hot.ts +0 -417
- package/scripts/smoke-v4.ts +0 -256
- package/scripts/smoke-v5.ts +0 -397
- package/scripts/uninstall.sh +0 -224
- package/src/index.ts +0 -37
- package/src/lib.ts +0 -65
- package/src/mem/scoped.ts +0 -65
- package/src/orchestrator/background.test.ts +0 -268
- package/src/orchestrator/background.ts +0 -293
- package/src/orchestrator/memory-hook.ts +0 -182
- package/src/orchestrator/reconciler.ts +0 -123
- package/src/orchestrator/scheduler.test.ts +0 -300
- package/src/orchestrator/scheduler.ts +0 -243
- package/src/plugin.test.ts +0 -2574
- package/src/plugin.ts +0 -1690
- package/src/worktrees/manager.ts +0 -236
- package/src/worktrees/state.ts +0 -87
- package/tests/integration/ranger-flow.test.ts +0 -257
- package/tsconfig.json +0 -31
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for src/config/schema.ts — NdomoConfig + loadHttpConfig + loadNdomoConfig.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. loadHttpConfig() defaults when no env, no file
|
|
6
|
+
* 2. loadHttpConfig() reads env vars when no file
|
|
7
|
+
* 3. loadHttpConfig() prefers file http block over env vars
|
|
8
|
+
* 4. loadNdomoConfig() reads/parses ndomo.config.json correctly
|
|
9
|
+
* 5. loadNdomoConfig() returns empty object if file missing
|
|
10
|
+
* 6. NdomoConfig.http is optional, parses valid HttpConfig shape
|
|
11
|
+
* 7. resolveConfigDir() honors XDG_CONFIG_HOME
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
15
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { loadHttpConfig, loadNdomoConfig, resolveConfigDir } from "../schema.ts";
|
|
19
|
+
|
|
20
|
+
let tmpDir: string;
|
|
21
|
+
const origEnv: Record<string, string | undefined> = {};
|
|
22
|
+
|
|
23
|
+
function saveEnv(...keys: string[]): void {
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
origEnv[key] = process.env[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function restoreEnv(...keys: string[]): void {
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
if (origEnv[key] === undefined) {
|
|
32
|
+
delete process.env[key];
|
|
33
|
+
} else {
|
|
34
|
+
process.env[key] = origEnv[key];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
tmpDir = mkdtempSync(join(tmpdir(), "ndomo-schema-"));
|
|
41
|
+
saveEnv("NDOMO_HTTP_ENABLED", "NDOMO_HTTP_PORT", "NDOMO_HTTP_CORS_ORIGINS", "NDOMO_HTTP_AUTH_REQUIRED", "XDG_CONFIG_HOME");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
restoreEnv("NDOMO_HTTP_ENABLED", "NDOMO_HTTP_PORT", "NDOMO_HTTP_CORS_ORIGINS", "NDOMO_HTTP_AUTH_REQUIRED", "XDG_CONFIG_HOME");
|
|
46
|
+
try {
|
|
47
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("loadHttpConfig", () => {
|
|
54
|
+
test("returns defaults when no env vars and no file", () => {
|
|
55
|
+
delete process.env.NDOMO_HTTP_ENABLED;
|
|
56
|
+
delete process.env.NDOMO_HTTP_PORT;
|
|
57
|
+
delete process.env.NDOMO_HTTP_CORS_ORIGINS;
|
|
58
|
+
delete process.env.NDOMO_HTTP_AUTH_REQUIRED;
|
|
59
|
+
|
|
60
|
+
const fakePath = join(tmpDir, "nonexistent.json");
|
|
61
|
+
const config = loadHttpConfig(fakePath);
|
|
62
|
+
|
|
63
|
+
expect(config.enabled).toBe(false);
|
|
64
|
+
expect(config.port).toBe(4097);
|
|
65
|
+
expect(config.cors.origins).toEqual(["*"]);
|
|
66
|
+
expect(config.auth.required).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("reads env vars when no file", () => {
|
|
70
|
+
process.env.NDOMO_HTTP_ENABLED = "true";
|
|
71
|
+
process.env.NDOMO_HTTP_PORT = "8080";
|
|
72
|
+
process.env.NDOMO_HTTP_CORS_ORIGINS = "a.com,b.com";
|
|
73
|
+
process.env.NDOMO_HTTP_AUTH_REQUIRED = "false";
|
|
74
|
+
|
|
75
|
+
const fakePath = join(tmpDir, "nonexistent.json");
|
|
76
|
+
const config = loadHttpConfig(fakePath);
|
|
77
|
+
|
|
78
|
+
expect(config.enabled).toBe(true);
|
|
79
|
+
expect(config.port).toBe(8080);
|
|
80
|
+
expect(config.cors.origins).toEqual(["a.com", "b.com"]);
|
|
81
|
+
expect(config.auth.required).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("prefers file http block over env vars", () => {
|
|
85
|
+
// Env says port 8080
|
|
86
|
+
process.env.NDOMO_HTTP_PORT = "8080";
|
|
87
|
+
process.env.NDOMO_HTTP_ENABLED = "false";
|
|
88
|
+
|
|
89
|
+
// File says port 3000, enabled true
|
|
90
|
+
const filePath = join(tmpDir, "ndomo.json");
|
|
91
|
+
writeFileSync(
|
|
92
|
+
filePath,
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
http: {
|
|
95
|
+
enabled: true,
|
|
96
|
+
port: 3000,
|
|
97
|
+
cors: { origins: ["example.com"] },
|
|
98
|
+
auth: { required: false },
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const config = loadHttpConfig(filePath);
|
|
104
|
+
|
|
105
|
+
// File wins
|
|
106
|
+
expect(config.enabled).toBe(true);
|
|
107
|
+
expect(config.port).toBe(3000);
|
|
108
|
+
expect(config.cors.origins).toEqual(["example.com"]);
|
|
109
|
+
expect(config.auth.required).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("falls back to env vars when file has no http block", () => {
|
|
113
|
+
process.env.NDOMO_HTTP_ENABLED = "true";
|
|
114
|
+
process.env.NDOMO_HTTP_PORT = "9090";
|
|
115
|
+
|
|
116
|
+
const filePath = join(tmpDir, "ndomo.json");
|
|
117
|
+
writeFileSync(filePath, JSON.stringify({ plugins: ["ndomo"] }));
|
|
118
|
+
|
|
119
|
+
const config = loadHttpConfig(filePath);
|
|
120
|
+
|
|
121
|
+
expect(config.enabled).toBe(true);
|
|
122
|
+
expect(config.port).toBe(9090);
|
|
123
|
+
expect(config.cors.origins).toEqual(["*"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("falls back to env vars when file is missing", () => {
|
|
127
|
+
process.env.NDOMO_HTTP_ENABLED = "true";
|
|
128
|
+
process.env.NDOMO_HTTP_PORT = "5555";
|
|
129
|
+
|
|
130
|
+
const config = loadHttpConfig(join(tmpDir, "missing.json"));
|
|
131
|
+
|
|
132
|
+
expect(config.enabled).toBe(true);
|
|
133
|
+
expect(config.port).toBe(5555);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("loadNdomoConfig", () => {
|
|
138
|
+
test("reads and parses ndomo.json correctly", () => {
|
|
139
|
+
const filePath = join(tmpDir, "ndomo.json");
|
|
140
|
+
const data = {
|
|
141
|
+
plugins: ["ndomo", "opencode-mem"],
|
|
142
|
+
optionalPlugins: ["@tarquinen/opencode-dcp"],
|
|
143
|
+
presets: {
|
|
144
|
+
default: {
|
|
145
|
+
foreman: { model: "minimax/MiniMax-M3", temperature: 0.3 },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
writeFileSync(filePath, JSON.stringify(data));
|
|
150
|
+
|
|
151
|
+
const config = loadNdomoConfig(filePath);
|
|
152
|
+
|
|
153
|
+
expect(config.plugins).toEqual(["ndomo", "opencode-mem"]);
|
|
154
|
+
expect(config.optionalPlugins).toEqual(["@tarquinen/opencode-dcp"]);
|
|
155
|
+
expect(config.presets?.default?.foreman?.model).toBe("minimax/MiniMax-M3");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("returns empty object if file is missing", () => {
|
|
159
|
+
const config = loadNdomoConfig(join(tmpDir, "missing.json"));
|
|
160
|
+
expect(config).toEqual({});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns empty object if file is invalid JSON", () => {
|
|
164
|
+
const filePath = join(tmpDir, "bad.json");
|
|
165
|
+
writeFileSync(filePath, "not json {{{");
|
|
166
|
+
|
|
167
|
+
const config = loadNdomoConfig(filePath);
|
|
168
|
+
expect(config).toEqual({});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("returns empty object if file is an array", () => {
|
|
172
|
+
const filePath = join(tmpDir, "array.json");
|
|
173
|
+
writeFileSync(filePath, JSON.stringify([1, 2, 3]));
|
|
174
|
+
|
|
175
|
+
const config = loadNdomoConfig(filePath);
|
|
176
|
+
expect(config).toEqual({});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("http field is optional", () => {
|
|
180
|
+
const filePath = join(tmpDir, "nohttp.json");
|
|
181
|
+
writeFileSync(filePath, JSON.stringify({ plugins: ["ndomo"] }));
|
|
182
|
+
|
|
183
|
+
const config = loadNdomoConfig(filePath);
|
|
184
|
+
expect(config.http).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("http field parses valid HttpConfig shape", () => {
|
|
188
|
+
const filePath = join(tmpDir, "withhttp.json");
|
|
189
|
+
writeFileSync(
|
|
190
|
+
filePath,
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
http: {
|
|
193
|
+
enabled: true,
|
|
194
|
+
port: 4097,
|
|
195
|
+
cors: { origins: ["*"] },
|
|
196
|
+
auth: { required: true },
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const config = loadNdomoConfig(filePath);
|
|
202
|
+
expect(config.http).toBeDefined();
|
|
203
|
+
expect(config.http?.enabled).toBe(true);
|
|
204
|
+
expect(config.http?.port).toBe(4097);
|
|
205
|
+
expect(config.http?.cors.origins).toEqual(["*"]);
|
|
206
|
+
expect(config.http?.auth.required).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("resolveConfigDir", () => {
|
|
211
|
+
test("uses XDG_CONFIG_HOME when set", () => {
|
|
212
|
+
process.env.XDG_CONFIG_HOME = "/tmp/test-xdg";
|
|
213
|
+
const dir = resolveConfigDir();
|
|
214
|
+
expect(dir).toBe("/tmp/test-xdg/opencode");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("defaults to ~/.config/opencode when XDG not set", () => {
|
|
218
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
219
|
+
const dir = resolveConfigDir();
|
|
220
|
+
expect(dir).toContain(".config");
|
|
221
|
+
expect(dir).toContain("opencode");
|
|
222
|
+
});
|
|
223
|
+
});
|
package/src/config/schema.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* HTTP server configuration for ndomo.
|
|
4
4
|
* Loaded from environment variables with sensible defaults.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Environment variables:
|
|
7
7
|
* - NDOMO_HTTP_ENABLED: "true" to enable HTTP server (default: "false")
|
|
8
8
|
* - NDOMO_HTTP_PORT: Port number (default: 4097)
|
|
@@ -10,36 +10,149 @@
|
|
|
10
10
|
* - NDOMO_HTTP_AUTH_REQUIRED: "false" to disable auth requirement (default: "true")
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
13
17
|
export type HttpConfig = {
|
|
14
|
-
enabled: boolean;
|
|
15
|
-
port: number;
|
|
18
|
+
enabled: boolean; // default false, env: NDOMO_HTTP_ENABLED
|
|
19
|
+
port: number; // default 4097, env: NDOMO_HTTP_PORT
|
|
16
20
|
cors: {
|
|
17
|
-
origins: string[];
|
|
21
|
+
origins: string[]; // default ['*'] in dev, [] in prod, env: NDOMO_HTTP_CORS_ORIGINS (comma-separated)
|
|
18
22
|
};
|
|
19
23
|
auth: {
|
|
20
|
-
required: boolean;
|
|
24
|
+
required: boolean; // default true
|
|
21
25
|
};
|
|
22
26
|
};
|
|
23
27
|
|
|
28
|
+
// ─── NdomoConfig Schema ───────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Full ndomo configuration as read from ndomo.config.json / ndomo.json.
|
|
31
|
+
* Preserves all fields from the JSON file (plugin routing, presets, etc.)
|
|
32
|
+
* and adds the optional HTTP block.
|
|
33
|
+
*/
|
|
34
|
+
export type NdomoConfig = {
|
|
35
|
+
$schema?: string;
|
|
36
|
+
plugins?: string[];
|
|
37
|
+
optionalPlugins?: string[];
|
|
38
|
+
presets?: Record<
|
|
39
|
+
string,
|
|
40
|
+
Record<string, { model?: string; temperature?: number; reasoning_effort?: string }>
|
|
41
|
+
>;
|
|
42
|
+
http?: HttpConfig;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the ndomo config directory path.
|
|
48
|
+
* Honors XDG_CONFIG_HOME, defaults to ~/.config/opencode.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveConfigDir(): string {
|
|
51
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
52
|
+
if (xdg && xdg.length > 0) {
|
|
53
|
+
return join(xdg, "opencode");
|
|
54
|
+
}
|
|
55
|
+
return join(homedir(), ".config", "opencode");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the path to ndomo.json in the config directory.
|
|
60
|
+
* @param configDir - Optional override for config directory
|
|
61
|
+
*/
|
|
62
|
+
export function resolveNdomoJsonPath(configDir?: string): string {
|
|
63
|
+
return join(configDir ?? resolveConfigDir(), "ndomo.json");
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
/**
|
|
25
|
-
* Load
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
67
|
+
* Load NdomoConfig from ndomo.json in the config directory.
|
|
68
|
+
* Returns empty object if file is missing or unparseable.
|
|
69
|
+
*
|
|
70
|
+
* @param configPath - Optional explicit path to ndomo.json
|
|
71
|
+
* @returns NdomoConfig with all fields from the file
|
|
72
|
+
*/
|
|
73
|
+
export function loadNdomoConfig(configPath?: string): NdomoConfig {
|
|
74
|
+
const filePath = configPath ?? resolveNdomoJsonPath();
|
|
75
|
+
if (!existsSync(filePath)) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
80
|
+
const parsed: unknown = JSON.parse(raw);
|
|
81
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
82
|
+
return parsed as NdomoConfig;
|
|
83
|
+
}
|
|
84
|
+
return {};
|
|
85
|
+
} catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Boolean env parsing helper ───────────────────────────────────────────────
|
|
91
|
+
function parseBoolEnv(value: string | undefined, defaultValue: boolean): boolean {
|
|
92
|
+
if (value === undefined || value === "") return defaultValue;
|
|
93
|
+
if (defaultValue === true) {
|
|
94
|
+
// Default true: any value except "false" → true
|
|
95
|
+
return value !== "false";
|
|
96
|
+
}
|
|
97
|
+
// Default false: only "true" → true
|
|
98
|
+
return value === "true";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── loadHttpConfig ───────────────────────────────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Load HTTP configuration with precedence: ndomo.json http block > env vars > defaults.
|
|
104
|
+
*
|
|
105
|
+
* If ndomo.json has a complete http block, use it.
|
|
106
|
+
* If ndomo.json is missing OR http block is incomplete, fall back to env vars.
|
|
107
|
+
* Env vars always override defaults (even when file has a partial block).
|
|
108
|
+
*
|
|
109
|
+
* @param configPath - Optional explicit path to ndomo.json
|
|
110
|
+
* @returns HttpConfig with resolved values
|
|
111
|
+
*
|
|
29
112
|
* @example
|
|
30
|
-
* //
|
|
113
|
+
* // ndomo.json: { "http": { "enabled": true, "port": 8080, "cors": { "origins": ["a.com"] }, "auth": { "required": false } } }
|
|
31
114
|
* const config = loadHttpConfig();
|
|
32
|
-
* // config = { enabled: true, port: 8080, cors: { origins: ["
|
|
115
|
+
* // config = { enabled: true, port: 8080, cors: { origins: ["a.com"] }, auth: { required: false } }
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // No ndomo.json, env: NDOMO_HTTP_ENABLED=true
|
|
119
|
+
* const config = loadHttpConfig();
|
|
120
|
+
* // config = { enabled: true, port: 4097, cors: { origins: ["*"] }, auth: { required: true } }
|
|
33
121
|
*/
|
|
34
|
-
export function loadHttpConfig(): HttpConfig {
|
|
122
|
+
export function loadHttpConfig(configPath?: string): HttpConfig {
|
|
123
|
+
const fileConfig = loadNdomoConfig(configPath);
|
|
124
|
+
const http = fileConfig.http;
|
|
125
|
+
|
|
126
|
+
// Helper: check if http block has all required fields (complete)
|
|
127
|
+
const isCompleteHttp = (h: unknown): h is HttpConfig => {
|
|
128
|
+
if (typeof h !== "object" || h === null) return false;
|
|
129
|
+
const obj = h as Record<string, unknown>;
|
|
130
|
+
return (
|
|
131
|
+
typeof obj.enabled === "boolean" &&
|
|
132
|
+
typeof obj.port === "number" &&
|
|
133
|
+
typeof obj.cors === "object" &&
|
|
134
|
+
obj.cors !== null &&
|
|
135
|
+
Array.isArray((obj.cors as Record<string, unknown>).origins) &&
|
|
136
|
+
typeof obj.auth === "object" &&
|
|
137
|
+
obj.auth !== null &&
|
|
138
|
+
typeof (obj.auth as Record<string, unknown>).required === "boolean"
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// If file has complete http block, use it (file > env)
|
|
143
|
+
if (http && isCompleteHttp(http)) {
|
|
144
|
+
return http;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// File missing or incomplete → fall back to env vars + defaults
|
|
35
148
|
return {
|
|
36
|
-
enabled: process.env.NDOMO_HTTP_ENABLED
|
|
149
|
+
enabled: parseBoolEnv(process.env.NDOMO_HTTP_ENABLED, false),
|
|
37
150
|
port: Number(process.env.NDOMO_HTTP_PORT) || 4097,
|
|
38
151
|
cors: {
|
|
39
|
-
origins: process.env.NDOMO_HTTP_CORS_ORIGINS?.split(",").map(s => s.trim()) ?? ["*"],
|
|
152
|
+
origins: process.env.NDOMO_HTTP_CORS_ORIGINS?.split(",").map((s) => s.trim()) ?? ["*"],
|
|
40
153
|
},
|
|
41
154
|
auth: {
|
|
42
|
-
required: process.env.NDOMO_HTTP_AUTH_REQUIRED
|
|
155
|
+
required: parseBoolEnv(process.env.NDOMO_HTTP_AUTH_REQUIRED, true),
|
|
43
156
|
},
|
|
44
157
|
};
|
|
45
158
|
}
|
|
@@ -47,7 +160,7 @@ export function loadHttpConfig(): HttpConfig {
|
|
|
47
160
|
/**
|
|
48
161
|
* Security baseline headers for HTTP responses.
|
|
49
162
|
* These headers should be applied to all HTTP responses to prevent common attacks.
|
|
50
|
-
*
|
|
163
|
+
*
|
|
51
164
|
* @see https://owasp.org/www-project-secure-headers/
|
|
52
165
|
*/
|
|
53
166
|
export const SECURITY_HEADERS = {
|
|
@@ -21,7 +21,7 @@ import { httpBasicAuth } from "../auth.ts";
|
|
|
21
21
|
function buildTestApp(httpConfig: HttpConfig) {
|
|
22
22
|
return new Elysia({ name: "test-auth" })
|
|
23
23
|
.use(httpBasicAuth(httpConfig))
|
|
24
|
-
.get("/protected", () => ({ ok: true }))
|
|
24
|
+
.get("/api/protected", () => ({ ok: true }))
|
|
25
25
|
.get("/health", () => ({ status: "ok" }));
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -64,7 +64,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
64
64
|
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
65
65
|
const app = buildTestApp(DEFAULT_CONFIG);
|
|
66
66
|
|
|
67
|
-
const req = new Request("http://localhost/protected", {
|
|
67
|
+
const req = new Request("http://localhost/api/protected", {
|
|
68
68
|
headers: { Authorization: basicAuthHeader("test-secret") },
|
|
69
69
|
});
|
|
70
70
|
const res = await app.handle(req);
|
|
@@ -78,7 +78,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
78
78
|
process.env.OPENCODE_SERVER_PASSWORD = "correct-password";
|
|
79
79
|
const app = buildTestApp(DEFAULT_CONFIG);
|
|
80
80
|
|
|
81
|
-
const req = new Request("http://localhost/protected", {
|
|
81
|
+
const req = new Request("http://localhost/api/protected", {
|
|
82
82
|
headers: { Authorization: basicAuthHeader("wrong-password") },
|
|
83
83
|
});
|
|
84
84
|
const res = await app.handle(req);
|
|
@@ -93,7 +93,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
93
93
|
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
94
94
|
const app = buildTestApp(DEFAULT_CONFIG);
|
|
95
95
|
|
|
96
|
-
const req = new Request("http://localhost/protected");
|
|
96
|
+
const req = new Request("http://localhost/api/protected");
|
|
97
97
|
const res = await app.handle(req);
|
|
98
98
|
|
|
99
99
|
expect(res.status).toBe(401);
|
|
@@ -106,7 +106,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
106
106
|
process.env.OPENCODE_SERVER_PASSWORD = "test-secret";
|
|
107
107
|
const app = buildTestApp(DEFAULT_CONFIG);
|
|
108
108
|
|
|
109
|
-
const req = new Request("http://localhost/protected", {
|
|
109
|
+
const req = new Request("http://localhost/api/protected", {
|
|
110
110
|
headers: { Authorization: "Bearer some-token" },
|
|
111
111
|
});
|
|
112
112
|
const res = await app.handle(req);
|
|
@@ -118,7 +118,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
118
118
|
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
119
119
|
const app = buildTestApp(DEFAULT_CONFIG);
|
|
120
120
|
|
|
121
|
-
const req = new Request("http://localhost/protected", {
|
|
121
|
+
const req = new Request("http://localhost/api/protected", {
|
|
122
122
|
headers: { Authorization: basicAuthHeader("any") },
|
|
123
123
|
});
|
|
124
124
|
const res = await app.handle(req);
|
|
@@ -147,7 +147,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
147
147
|
|
|
148
148
|
// "user:" → base64 → colonIdx=4, providedPassword=""
|
|
149
149
|
const encoded = Buffer.from("user:").toString("base64");
|
|
150
|
-
const req = new Request("http://localhost/protected", {
|
|
150
|
+
const req = new Request("http://localhost/api/protected", {
|
|
151
151
|
headers: { Authorization: `Basic ${encoded}` },
|
|
152
152
|
});
|
|
153
153
|
const res = await app.handle(req);
|
|
@@ -161,7 +161,7 @@ describe("httpBasicAuth — auth required", () => {
|
|
|
161
161
|
|
|
162
162
|
// "nocolon" (no user:pass format) → colonIdx=-1, providedPassword="nocolon"
|
|
163
163
|
const encoded = Buffer.from("nocolon").toString("base64");
|
|
164
|
-
const req = new Request("http://localhost/protected", {
|
|
164
|
+
const req = new Request("http://localhost/api/protected", {
|
|
165
165
|
headers: { Authorization: `Basic ${encoded}` },
|
|
166
166
|
});
|
|
167
167
|
const res = await app.handle(req);
|
|
@@ -174,7 +174,7 @@ describe("httpBasicAuth — auth disabled", () => {
|
|
|
174
174
|
test("no credentials → 200 (auth skipped)", async () => {
|
|
175
175
|
const app = buildTestApp(DISABLED_AUTH_CONFIG);
|
|
176
176
|
|
|
177
|
-
const req = new Request("http://localhost/protected");
|
|
177
|
+
const req = new Request("http://localhost/api/protected");
|
|
178
178
|
const res = await app.handle(req);
|
|
179
179
|
|
|
180
180
|
expect(res.status).toBe(200);
|
|
@@ -186,7 +186,7 @@ describe("httpBasicAuth — auth disabled", () => {
|
|
|
186
186
|
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
187
187
|
const app = buildTestApp(DISABLED_AUTH_CONFIG);
|
|
188
188
|
|
|
189
|
-
const req = new Request("http://localhost/protected", {
|
|
189
|
+
const req = new Request("http://localhost/api/protected", {
|
|
190
190
|
headers: { Authorization: basicAuthHeader("wrong") },
|
|
191
191
|
});
|
|
192
192
|
const res = await app.handle(req);
|