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.
Files changed (65) hide show
  1. package/.env.example +4 -0
  2. package/README.es.md +29 -23
  3. package/README.md +64 -24
  4. package/bun.lock +447 -0
  5. package/docs/configuration.md +4 -4
  6. package/docs/installation.md +53 -34
  7. package/docs/installer.md +164 -0
  8. package/docs/integrations.md +1 -1
  9. package/docs/web-ui.md +124 -0
  10. package/package.json +43 -4
  11. package/scripts/install.sh +28 -0
  12. package/scripts/smoke-install.sh +47 -0
  13. package/scripts/smoke-web.sh +335 -0
  14. package/src/cli/__tests__/install.test.ts +733 -0
  15. package/src/cli/index.ts +8 -0
  16. package/src/cli/install.ts +1292 -0
  17. package/src/config/__tests__/schema.test.ts +223 -0
  18. package/src/config/schema.ts +129 -16
  19. package/src/http/__tests__/auth.test.ts +10 -10
  20. package/src/http/__tests__/spa.test.ts +296 -0
  21. package/src/http/auth.ts +8 -1
  22. package/src/http/server.ts +71 -2
  23. package/.bun-version +0 -1
  24. package/.dockerignore +0 -79
  25. package/.editorconfig +0 -18
  26. package/.github/CODEOWNERS +0 -8
  27. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  28. package/.github/ISSUE_TEMPLATE/config.yml +0 -2
  29. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
  30. package/.github/dependabot.yml +0 -36
  31. package/.github/pull_request_template.md +0 -24
  32. package/.github/release.yml +0 -30
  33. package/.github/workflows/gitleaks.yml +0 -28
  34. package/.github/workflows/release-please.yml +0 -27
  35. package/.github/workflows/smoke.yml +0 -29
  36. package/.husky/commit-msg +0 -1
  37. package/CHANGELOG.md +0 -114
  38. package/Dockerfile +0 -32
  39. package/bin/ndomo-analyses.ts +0 -4
  40. package/bin/ndomo-status.ts +0 -4
  41. package/biome.json +0 -57
  42. package/commitlint.config.js +0 -3
  43. package/opencode.json +0 -5
  44. package/release-please-config.json +0 -11
  45. package/scripts/dev-bust-cache.sh +0 -164
  46. package/scripts/smoke-e2e.ts +0 -704
  47. package/scripts/smoke-hot.ts +0 -417
  48. package/scripts/smoke-v4.ts +0 -256
  49. package/scripts/smoke-v5.ts +0 -397
  50. package/scripts/uninstall.sh +0 -224
  51. package/src/index.ts +0 -37
  52. package/src/lib.ts +0 -65
  53. package/src/mem/scoped.ts +0 -65
  54. package/src/orchestrator/background.test.ts +0 -268
  55. package/src/orchestrator/background.ts +0 -293
  56. package/src/orchestrator/memory-hook.ts +0 -182
  57. package/src/orchestrator/reconciler.ts +0 -123
  58. package/src/orchestrator/scheduler.test.ts +0 -300
  59. package/src/orchestrator/scheduler.ts +0 -243
  60. package/src/plugin.test.ts +0 -2574
  61. package/src/plugin.ts +0 -1690
  62. package/src/worktrees/manager.ts +0 -236
  63. package/src/worktrees/state.ts +0 -87
  64. package/tests/integration/ranger-flow.test.ts +0 -257
  65. 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
+ });
@@ -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; // default false, env: NDOMO_HTTP_ENABLED
15
- port: number; // default 4097, env: NDOMO_HTTP_PORT
18
+ enabled: boolean; // default false, env: NDOMO_HTTP_ENABLED
19
+ port: number; // default 4097, env: NDOMO_HTTP_PORT
16
20
  cors: {
17
- origins: string[]; // default ['*'] in dev, [] in prod, env: NDOMO_HTTP_CORS_ORIGINS (comma-separated)
21
+ origins: string[]; // default ['*'] in dev, [] in prod, env: NDOMO_HTTP_CORS_ORIGINS (comma-separated)
18
22
  };
19
23
  auth: {
20
- required: boolean; // default true
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 HTTP configuration from environment variables with defaults.
26
- *
27
- * @returns HttpConfig with values from environment or defaults
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
- * // With env: NDOMO_HTTP_ENABLED=true, NDOMO_HTTP_PORT=8080
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: ["*"] }, auth: { required: true } }
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 === "true",
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 !== "false",
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);