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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SPA static-file serving + client-side routing fallback.
|
|
3
|
+
*
|
|
4
|
+
* Uses a temp directory with a fake index.html + asset to avoid
|
|
5
|
+
* depending on `bun run web:build` output in CI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { Database } from "bun:sqlite";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import type { HttpConfig } from "../../config/schema.ts";
|
|
14
|
+
import { runMigrations } from "../../db/migrations.ts";
|
|
15
|
+
import { buildHttpServer } from "../server.ts";
|
|
16
|
+
|
|
17
|
+
let db: Database;
|
|
18
|
+
let webDir: string;
|
|
19
|
+
let savedPassword: string | undefined;
|
|
20
|
+
|
|
21
|
+
const AUTH_CONFIG: HttpConfig = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
port: 4098,
|
|
24
|
+
cors: { origins: ["*"] },
|
|
25
|
+
auth: { required: true },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const NO_AUTH_CONFIG: HttpConfig = {
|
|
29
|
+
enabled: true,
|
|
30
|
+
port: 4098,
|
|
31
|
+
cors: { origins: ["*"] },
|
|
32
|
+
auth: { required: false },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function basicAuthHeader(password: string): string {
|
|
36
|
+
const encoded = Buffer.from(`user:${password}`).toString("base64");
|
|
37
|
+
return `Basic ${encoded}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
savedPassword = process.env.OPENCODE_SERVER_PASSWORD;
|
|
42
|
+
process.env.OPENCODE_SERVER_PASSWORD = "test-password";
|
|
43
|
+
db = new Database(":memory:");
|
|
44
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
45
|
+
runMigrations(db);
|
|
46
|
+
|
|
47
|
+
// Create temp web dist with fake SPA files
|
|
48
|
+
webDir = mkdtempSync(join(tmpdir(), "ndomo-spa-test-"));
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(webDir, "index.html"),
|
|
51
|
+
'<!DOCTYPE html><html><body><div id="app"></div></body></html>',
|
|
52
|
+
);
|
|
53
|
+
mkdirSync(join(webDir, "assets"), { recursive: true });
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(webDir, "assets", "index-abc123.js"),
|
|
56
|
+
'console.log("spa");',
|
|
57
|
+
);
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(webDir, "assets", "index-abc123.css"),
|
|
60
|
+
"body{margin:0}",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
if (savedPassword === undefined) {
|
|
66
|
+
delete process.env.OPENCODE_SERVER_PASSWORD;
|
|
67
|
+
} else {
|
|
68
|
+
process.env.OPENCODE_SERVER_PASSWORD = savedPassword;
|
|
69
|
+
}
|
|
70
|
+
db.close();
|
|
71
|
+
rmSync(webDir, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("SPA root GET /", () => {
|
|
75
|
+
test("returns 200 text/html with <div id='app'>", async () => {
|
|
76
|
+
const { app } = await buildHttpServer({
|
|
77
|
+
db,
|
|
78
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
79
|
+
webDistDir: webDir,
|
|
80
|
+
});
|
|
81
|
+
const res = await app.handle(new Request("http://localhost/"));
|
|
82
|
+
|
|
83
|
+
expect(res.status).toBe(200);
|
|
84
|
+
expect(res.headers.get("Content-Type")).toContain("text/html");
|
|
85
|
+
const body = await res.text();
|
|
86
|
+
expect(body).toContain('<div id="app"></div>');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("SPA client-side routing fallback", () => {
|
|
91
|
+
test("unknown path returns index.html (SPA fallback)", async () => {
|
|
92
|
+
const { app } = await buildHttpServer({
|
|
93
|
+
db,
|
|
94
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
95
|
+
webDistDir: webDir,
|
|
96
|
+
});
|
|
97
|
+
const res = await app.handle(new Request("http://localhost/some/spa/route"));
|
|
98
|
+
|
|
99
|
+
expect(res.status).toBe(200);
|
|
100
|
+
expect(res.headers.get("Content-Type")).toContain("text/html");
|
|
101
|
+
const body = await res.text();
|
|
102
|
+
expect(body).toContain('<div id="app"></div>');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("SPA static assets", () => {
|
|
107
|
+
test("serves JS asset with correct content type", async () => {
|
|
108
|
+
const { app } = await buildHttpServer({
|
|
109
|
+
db,
|
|
110
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
111
|
+
webDistDir: webDir,
|
|
112
|
+
});
|
|
113
|
+
const res = await app.handle(
|
|
114
|
+
new Request("http://localhost/assets/index-abc123.js"),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(200);
|
|
118
|
+
expect(res.headers.get("Content-Type")).toContain("application/javascript");
|
|
119
|
+
const body = await res.text();
|
|
120
|
+
expect(body).toContain('console.log("spa")');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("serves CSS asset with correct content type", async () => {
|
|
124
|
+
const { app } = await buildHttpServer({
|
|
125
|
+
db,
|
|
126
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
127
|
+
webDistDir: webDir,
|
|
128
|
+
});
|
|
129
|
+
const res = await app.handle(
|
|
130
|
+
new Request("http://localhost/assets/index-abc123.css"),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(res.status).toBe(200);
|
|
134
|
+
expect(res.headers.get("Content-Type")).toContain("text/css");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("SPA does NOT swallow /api/*", () => {
|
|
139
|
+
test("GET /health returns JSON (not SPA)", async () => {
|
|
140
|
+
const { app } = await buildHttpServer({
|
|
141
|
+
db,
|
|
142
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
143
|
+
webDistDir: webDir,
|
|
144
|
+
});
|
|
145
|
+
const res = await app.handle(new Request("http://localhost/health"));
|
|
146
|
+
|
|
147
|
+
expect(res.status).toBe(200);
|
|
148
|
+
expect(res.headers.get("Content-Type")).toContain("application/json");
|
|
149
|
+
const body = (await res.json()) as { status: string };
|
|
150
|
+
expect(body.status).toBe("ok");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("GET /api/plans without auth returns 401 (not SPA)", async () => {
|
|
154
|
+
const { app } = await buildHttpServer({
|
|
155
|
+
db,
|
|
156
|
+
httpConfig: AUTH_CONFIG,
|
|
157
|
+
webDistDir: webDir,
|
|
158
|
+
});
|
|
159
|
+
const res = await app.handle(new Request("http://localhost/api/plans"));
|
|
160
|
+
|
|
161
|
+
expect(res.status).toBe(401);
|
|
162
|
+
const body = (await res.json()) as { error: string };
|
|
163
|
+
expect(body.error).toBe("invalid_credentials");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("GET /api/plans with auth returns 200 JSON", async () => {
|
|
167
|
+
const { app } = await buildHttpServer({
|
|
168
|
+
db,
|
|
169
|
+
httpConfig: AUTH_CONFIG,
|
|
170
|
+
webDistDir: webDir,
|
|
171
|
+
});
|
|
172
|
+
const res = await app.handle(
|
|
173
|
+
new Request("http://localhost/api/plans", {
|
|
174
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(res.status).toBe(200);
|
|
179
|
+
expect(res.headers.get("Content-Type")).toContain("application/json");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("GET /api/plans/nonexistent returns 404 JSON (not SPA fallback)", async () => {
|
|
183
|
+
const { app } = await buildHttpServer({
|
|
184
|
+
db,
|
|
185
|
+
httpConfig: AUTH_CONFIG,
|
|
186
|
+
webDistDir: webDir,
|
|
187
|
+
});
|
|
188
|
+
const res = await app.handle(
|
|
189
|
+
new Request("http://localhost/api/plans/nonexistent-id", {
|
|
190
|
+
headers: { Authorization: basicAuthHeader("test-password") },
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(res.status).toBe(404);
|
|
195
|
+
expect(res.headers.get("Content-Type")).toContain("application/json");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("SPA is public (no auth required for non-/api paths)", () => {
|
|
200
|
+
test("GET / returns 200 WITHOUT auth when auth.required=true", async () => {
|
|
201
|
+
const { app } = await buildHttpServer({
|
|
202
|
+
db,
|
|
203
|
+
httpConfig: AUTH_CONFIG,
|
|
204
|
+
webDistDir: webDir,
|
|
205
|
+
});
|
|
206
|
+
const res = await app.handle(new Request("http://localhost/"));
|
|
207
|
+
|
|
208
|
+
expect(res.status).toBe(200);
|
|
209
|
+
expect(res.headers.get("Content-Type")).toContain("text/html");
|
|
210
|
+
const body = await res.text();
|
|
211
|
+
expect(body).toContain('<div id="app"></div>');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("GET /plans/<any-id> returns 200 WITHOUT auth (SPA fallback)", async () => {
|
|
215
|
+
const { app } = await buildHttpServer({
|
|
216
|
+
db,
|
|
217
|
+
httpConfig: AUTH_CONFIG,
|
|
218
|
+
webDistDir: webDir,
|
|
219
|
+
});
|
|
220
|
+
const res = await app.handle(
|
|
221
|
+
new Request("http://localhost/plans/some-uuid"),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(res.status).toBe(200);
|
|
225
|
+
expect(res.headers.get("Content-Type")).toContain("text/html");
|
|
226
|
+
const body = await res.text();
|
|
227
|
+
expect(body).toContain('<div id="app"></div>');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("GET /assets/*.js returns 200 WITHOUT auth", async () => {
|
|
231
|
+
const { app } = await buildHttpServer({
|
|
232
|
+
db,
|
|
233
|
+
httpConfig: AUTH_CONFIG,
|
|
234
|
+
webDistDir: webDir,
|
|
235
|
+
});
|
|
236
|
+
const res = await app.handle(
|
|
237
|
+
new Request("http://localhost/assets/index-abc123.js"),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(res.status).toBe(200);
|
|
241
|
+
expect(res.headers.get("Content-Type")).toContain("application/javascript");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("SPA path traversal defense", () => {
|
|
246
|
+
test("path traversal attempt does not serve /etc/passwd", async () => {
|
|
247
|
+
const { app } = await buildHttpServer({
|
|
248
|
+
db,
|
|
249
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
250
|
+
webDistDir: webDir,
|
|
251
|
+
});
|
|
252
|
+
const res = await app.handle(
|
|
253
|
+
new Request("http://localhost/../../etc/passwd"),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Must not serve /etc/passwd — either 200 (SPA fallback) or 400
|
|
257
|
+
if (res.status === 200) {
|
|
258
|
+
const body = await res.text();
|
|
259
|
+
expect(body).toContain('<div id="app"></div>');
|
|
260
|
+
expect(body).not.toContain("root:");
|
|
261
|
+
} else {
|
|
262
|
+
expect([400, 404]).toContain(res.status);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("SPA disabled when web dir missing", () => {
|
|
268
|
+
test("returns 503 when webDistDir does not exist", async () => {
|
|
269
|
+
const { app } = await buildHttpServer({
|
|
270
|
+
db,
|
|
271
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
272
|
+
webDistDir: "/nonexistent/path",
|
|
273
|
+
});
|
|
274
|
+
const res = await app.handle(new Request("http://localhost/"));
|
|
275
|
+
|
|
276
|
+
expect(res.status).toBe(503);
|
|
277
|
+
const body = await res.text();
|
|
278
|
+
expect(body).toContain("SPA not built");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("SPA non-GET methods", () => {
|
|
283
|
+
test("POST to SPA path returns 404 (Elysia GET handler does not match)", async () => {
|
|
284
|
+
const { app } = await buildHttpServer({
|
|
285
|
+
db,
|
|
286
|
+
httpConfig: NO_AUTH_CONFIG,
|
|
287
|
+
webDistDir: webDir,
|
|
288
|
+
});
|
|
289
|
+
const res = await app.handle(
|
|
290
|
+
new Request("http://localhost/some/path", { method: "POST" }),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Elysia .get() does not match POST → 404
|
|
294
|
+
expect(res.status).toBe(404);
|
|
295
|
+
});
|
|
296
|
+
});
|
package/src/http/auth.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* Uses timing-safe comparison to prevent timing attacks.
|
|
7
7
|
*
|
|
8
8
|
* Uses onRequest so the hook propagates across Elysia .use() boundaries.
|
|
9
|
-
* Exempts
|
|
9
|
+
* Exempts from auth: /health (liveness probe) + non-/api paths (SPA static +
|
|
10
|
+
* SPA history fallback). Only /api/* requires auth.
|
|
10
11
|
*
|
|
11
12
|
* Behavior:
|
|
12
13
|
* - auth.required === false → skip entirely
|
|
13
14
|
* - /health path → skip (always public)
|
|
15
|
+
* - non-/api path (SPA) → skip (public — Vue SPA serves static + history fallback)
|
|
14
16
|
* - Password unset/empty → 503 auth_not_configured
|
|
15
17
|
* - Missing/malformed Authorization → 401 + WWW-Authenticate
|
|
16
18
|
* - Wrong password → 401 + WWW-Authenticate
|
|
@@ -36,6 +38,11 @@ export function httpBasicAuth(httpConfig: HttpConfig) {
|
|
|
36
38
|
const url = new URL(request.url);
|
|
37
39
|
if (url.pathname === "/health") return;
|
|
38
40
|
|
|
41
|
+
// Exempt non-/api paths — SPA static assets + history fallback are public.
|
|
42
|
+
// Auth gates only the JSON API surface (/api/*). Users browse the SPA freely,
|
|
43
|
+
// then enter the password in the in-app AuthPrompt when fetching /api/*.
|
|
44
|
+
if (!url.pathname.startsWith("/api/")) return;
|
|
45
|
+
|
|
39
46
|
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
40
47
|
|
|
41
48
|
// Password not configured → 503
|
package/src/http/server.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* 4. health route (no auth)
|
|
10
10
|
* 5. /api/plans, /api/tasks, /api/sessions (auth required)
|
|
11
11
|
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join, normalize, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
12
15
|
import type { Database } from "bun:sqlite";
|
|
13
16
|
import type { OpencodeClient } from "@opencode-ai/sdk/client";
|
|
14
17
|
import { Elysia } from "elysia";
|
|
@@ -27,6 +30,11 @@ interface BuildHttpServerArgs {
|
|
|
27
30
|
httpConfig: HttpConfig;
|
|
28
31
|
/** OpenCode SDK client for SSE events. If null/undefined, /api/events returns 503. */
|
|
29
32
|
sdkClient?: OpencodeClient;
|
|
33
|
+
/**
|
|
34
|
+
* Override web dist directory for testing. Defaults to ./web/ relative to this file.
|
|
35
|
+
* Set to a temp dir with index.html + assets for SPA tests.
|
|
36
|
+
*/
|
|
37
|
+
webDistDir?: string;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface HttpServerHandle {
|
|
@@ -64,12 +72,73 @@ export async function buildHttpServer(args: BuildHttpServerArgs) {
|
|
|
64
72
|
.use(sessionsRoute(db))
|
|
65
73
|
.use(eventsRoute(args.sdkClient ?? null));
|
|
66
74
|
|
|
67
|
-
//
|
|
75
|
+
// Resolve web dist dir (configurable for testing, defaults to ./web/ sibling)
|
|
76
|
+
const WEB_DIST = args.webDistDir
|
|
77
|
+
? resolve(args.webDistDir)
|
|
78
|
+
: fileURLToPath(new URL("./web/", import.meta.url));
|
|
79
|
+
const INDEX_HTML = join(WEB_DIST, "index.html");
|
|
80
|
+
|
|
81
|
+
// Static-file + SPA fallback sub-app
|
|
82
|
+
const spaApp = new Elysia({ name: "spa-fallback" }).get(
|
|
83
|
+
"/*",
|
|
84
|
+
({ path }) => {
|
|
85
|
+
// Try static asset first (path traversal safe)
|
|
86
|
+
// Strip leading slashes so resolve() doesn't treat path as absolute
|
|
87
|
+
const safePath = normalize(path)
|
|
88
|
+
.replace(/^(\.\.[/\\])+/g, "")
|
|
89
|
+
.replace(/^\/+/, "");
|
|
90
|
+
const assetPath = resolve(WEB_DIST, safePath);
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
assetPath.startsWith(WEB_DIST) &&
|
|
94
|
+
safePath !== "" &&
|
|
95
|
+
existsSync(assetPath)
|
|
96
|
+
) {
|
|
97
|
+
const file = Bun.file(assetPath);
|
|
98
|
+
const ext = assetPath.split(".").pop() ?? "";
|
|
99
|
+
const contentTypes: Record<string, string> = {
|
|
100
|
+
html: "text/html; charset=utf-8",
|
|
101
|
+
js: "application/javascript; charset=utf-8",
|
|
102
|
+
css: "text/css; charset=utf-8",
|
|
103
|
+
json: "application/json; charset=utf-8",
|
|
104
|
+
svg: "image/svg+xml",
|
|
105
|
+
png: "image/png",
|
|
106
|
+
jpg: "image/jpeg",
|
|
107
|
+
jpeg: "image/jpeg",
|
|
108
|
+
ico: "image/x-icon",
|
|
109
|
+
woff: "font/woff",
|
|
110
|
+
woff2: "font/woff2",
|
|
111
|
+
};
|
|
112
|
+
return new Response(file, {
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": contentTypes[ext] ?? "application/octet-stream",
|
|
115
|
+
"Cache-Control": "no-cache",
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fallback to SPA index.html for client-side routing
|
|
121
|
+
if (!existsSync(INDEX_HTML)) {
|
|
122
|
+
return new Response("SPA not built. Run: bun run web:build", {
|
|
123
|
+
status: 503,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return new Response(Bun.file(INDEX_HTML), {
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
129
|
+
"Cache-Control": "no-cache",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Compose the full app — apiProtected BEFORE spaApp so /api/* wins
|
|
68
136
|
const app = new Elysia({ name: "ndomo-http" })
|
|
69
137
|
.use(securityHeaders)
|
|
70
138
|
.use(corsMiddleware(httpConfig.cors.origins))
|
|
71
139
|
.use(healthRoute(db))
|
|
72
|
-
.use(apiProtected)
|
|
140
|
+
.use(apiProtected)
|
|
141
|
+
.use(spaApp);
|
|
73
142
|
|
|
74
143
|
return {
|
|
75
144
|
app: app as unknown as Elysia,
|
package/.bun-version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.3.14
|
package/.dockerignore
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# VCS
|
|
2
|
-
.git/
|
|
3
|
-
.gitignore
|
|
4
|
-
|
|
5
|
-
# Dependencies (regenerated in image)
|
|
6
|
-
node_modules/
|
|
7
|
-
.pnp/
|
|
8
|
-
.pnp.js
|
|
9
|
-
.yarn/
|
|
10
|
-
|
|
11
|
-
# Build output
|
|
12
|
-
dist/
|
|
13
|
-
build/
|
|
14
|
-
out/
|
|
15
|
-
*.tsbuildinfo
|
|
16
|
-
|
|
17
|
-
# OpenCode / ndomo runtime state
|
|
18
|
-
.slim/
|
|
19
|
-
.worktrees/
|
|
20
|
-
.ndomo/
|
|
21
|
-
.opencode/
|
|
22
|
-
.opencode-mem/
|
|
23
|
-
|
|
24
|
-
# DCP data
|
|
25
|
-
.dcp/
|
|
26
|
-
dcp-prompts/
|
|
27
|
-
|
|
28
|
-
# Databases — never bake into image
|
|
29
|
-
*.sqlite
|
|
30
|
-
*.sqlite-journal
|
|
31
|
-
*.sqlite-shm
|
|
32
|
-
*.sqlite-wal
|
|
33
|
-
|
|
34
|
-
# Environment / secrets — NEVER bake into image
|
|
35
|
-
.env
|
|
36
|
-
.env.*
|
|
37
|
-
.env.local
|
|
38
|
-
.env.*.local
|
|
39
|
-
|
|
40
|
-
# Logs
|
|
41
|
-
*.log
|
|
42
|
-
npm-debug.log*
|
|
43
|
-
yarn-debug.log*
|
|
44
|
-
yarn-error.log*
|
|
45
|
-
logs/
|
|
46
|
-
|
|
47
|
-
# Coverage
|
|
48
|
-
coverage/
|
|
49
|
-
.nyc_output/
|
|
50
|
-
|
|
51
|
-
# OS artifacts
|
|
52
|
-
.DS_Store
|
|
53
|
-
Thumbs.db
|
|
54
|
-
|
|
55
|
-
# Editor
|
|
56
|
-
.idea/
|
|
57
|
-
.vscode/
|
|
58
|
-
*.swp
|
|
59
|
-
*.swo
|
|
60
|
-
|
|
61
|
-
# Documentation (not needed at runtime)
|
|
62
|
-
docs/
|
|
63
|
-
CHANGELOG.md
|
|
64
|
-
README.md
|
|
65
|
-
README.es.md
|
|
66
|
-
|
|
67
|
-
# Test files (not needed at runtime)
|
|
68
|
-
*.test.ts
|
|
69
|
-
*.test.js
|
|
70
|
-
|
|
71
|
-
# Miscellaneous
|
|
72
|
-
*.tgz
|
|
73
|
-
.cache/
|
|
74
|
-
.agents/
|
|
75
|
-
skills-lock.json
|
|
76
|
-
|
|
77
|
-
# Docker metadata (not needed in image)
|
|
78
|
-
Dockerfile
|
|
79
|
-
.dockerignore
|
package/.editorconfig
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
root = true
|
|
2
|
-
|
|
3
|
-
[*]
|
|
4
|
-
charset = utf-8
|
|
5
|
-
end_of_line = lf
|
|
6
|
-
indent_style = space
|
|
7
|
-
indent_size = 2
|
|
8
|
-
insert_final_newline = true
|
|
9
|
-
trim_trailing_whitespace = true
|
|
10
|
-
|
|
11
|
-
[*.md]
|
|
12
|
-
trim_trailing_whitespace = false
|
|
13
|
-
|
|
14
|
-
[*.{yml,yaml}]
|
|
15
|
-
indent_size = 2
|
|
16
|
-
|
|
17
|
-
[Makefile]
|
|
18
|
-
indent_style = tab
|
package/.github/CODEOWNERS
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
name: Bug Report
|
|
2
|
-
description: Report a bug in ndomo
|
|
3
|
-
title: "[bug]: "
|
|
4
|
-
labels: [bug]
|
|
5
|
-
body:
|
|
6
|
-
- type: textarea
|
|
7
|
-
id: description
|
|
8
|
-
attributes:
|
|
9
|
-
label: Description
|
|
10
|
-
description: What went wrong?
|
|
11
|
-
placeholder: A clear description of the bug.
|
|
12
|
-
validations:
|
|
13
|
-
required: true
|
|
14
|
-
- type: textarea
|
|
15
|
-
id: steps
|
|
16
|
-
attributes:
|
|
17
|
-
label: Steps to reproduce
|
|
18
|
-
description: How can we reproduce the issue?
|
|
19
|
-
placeholder: |
|
|
20
|
-
1.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
validations:
|
|
24
|
-
required: true
|
|
25
|
-
- type: textarea
|
|
26
|
-
id: expected
|
|
27
|
-
attributes:
|
|
28
|
-
label: Expected behavior
|
|
29
|
-
description: What should have happened?
|
|
30
|
-
validations:
|
|
31
|
-
required: true
|
|
32
|
-
- type: textarea
|
|
33
|
-
id: actual
|
|
34
|
-
attributes:
|
|
35
|
-
label: Actual behavior
|
|
36
|
-
description: What actually happened?
|
|
37
|
-
validations:
|
|
38
|
-
required: true
|
|
39
|
-
- type: input
|
|
40
|
-
id: os
|
|
41
|
-
attributes:
|
|
42
|
-
label: OS
|
|
43
|
-
description: What operating system are you using?
|
|
44
|
-
placeholder: e.g. Ubuntu 24.04, macOS 14
|
|
45
|
-
validations:
|
|
46
|
-
required: true
|
|
47
|
-
- type: input
|
|
48
|
-
id: bun-version
|
|
49
|
-
attributes:
|
|
50
|
-
label: Bun version
|
|
51
|
-
description: What bun version are you using? (run `bun --version`)
|
|
52
|
-
placeholder: e.g. 1.3.14
|
|
53
|
-
validations:
|
|
54
|
-
required: true
|
|
55
|
-
- type: input
|
|
56
|
-
id: ndomo-version
|
|
57
|
-
attributes:
|
|
58
|
-
label: ndomo version
|
|
59
|
-
description: What ndomo version are you using?
|
|
60
|
-
placeholder: e.g. 0.1.0
|
|
61
|
-
validations:
|
|
62
|
-
required: false
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Feature Request
|
|
2
|
-
description: Suggest a new feature for ndomo
|
|
3
|
-
title: "[feature]: "
|
|
4
|
-
labels: [enhancement]
|
|
5
|
-
body:
|
|
6
|
-
- type: textarea
|
|
7
|
-
id: description
|
|
8
|
-
attributes:
|
|
9
|
-
label: Description
|
|
10
|
-
description: What feature do you want?
|
|
11
|
-
placeholder: A clear description of the feature.
|
|
12
|
-
validations:
|
|
13
|
-
required: true
|
|
14
|
-
- type: textarea
|
|
15
|
-
id: problem
|
|
16
|
-
attributes:
|
|
17
|
-
label: Problem it solves
|
|
18
|
-
description: What problem does this solve that isn't possible today?
|
|
19
|
-
validations:
|
|
20
|
-
required: true
|
|
21
|
-
- type: textarea
|
|
22
|
-
id: solution
|
|
23
|
-
attributes:
|
|
24
|
-
label: Proposed solution
|
|
25
|
-
description: How would you implement this?
|
|
26
|
-
validations:
|
|
27
|
-
required: true
|
|
28
|
-
- type: textarea
|
|
29
|
-
id: alternatives
|
|
30
|
-
attributes:
|
|
31
|
-
label: Alternatives considered
|
|
32
|
-
description: What other approaches did you think about?
|
|
33
|
-
validations:
|
|
34
|
-
required: false
|
package/.github/dependabot.yml
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# .github/dependabot.yml
|
|
2
|
-
# Dependabot config: weekly auto-PRs for npm + github-actions
|
|
3
|
-
version: 2
|
|
4
|
-
updates:
|
|
5
|
-
- package-ecosystem: "npm"
|
|
6
|
-
directory: "/"
|
|
7
|
-
schedule:
|
|
8
|
-
interval: "weekly"
|
|
9
|
-
day: "monday"
|
|
10
|
-
open-pull-requests-limit: 5
|
|
11
|
-
labels:
|
|
12
|
-
- "dependencies"
|
|
13
|
-
groups:
|
|
14
|
-
production:
|
|
15
|
-
applies-to: version-updates
|
|
16
|
-
dependency-type: "production"
|
|
17
|
-
development:
|
|
18
|
-
applies-to: version-updates
|
|
19
|
-
dependency-type: "development"
|
|
20
|
-
commit-message:
|
|
21
|
-
prefix: "chore(deps)"
|
|
22
|
-
|
|
23
|
-
- package-ecosystem: "github-actions"
|
|
24
|
-
directory: "/"
|
|
25
|
-
schedule:
|
|
26
|
-
interval: "weekly"
|
|
27
|
-
day: "monday"
|
|
28
|
-
open-pull-requests-limit: 3
|
|
29
|
-
labels:
|
|
30
|
-
- "dependencies"
|
|
31
|
-
- "ci"
|
|
32
|
-
commit-message:
|
|
33
|
-
prefix: "ci(actions)"
|
|
34
|
-
# Auto-merge patch updates for actions (they're already SHA-pinned,
|
|
35
|
-
# so updates only happen when we bump SHAs intentionally)
|
|
36
|
-
automerge: false # keep manual review
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
## Description
|
|
2
|
-
|
|
3
|
-
<!-- What does this PR do? Why is this change needed? -->
|
|
4
|
-
|
|
5
|
-
## Type of change
|
|
6
|
-
|
|
7
|
-
- [ ] Bug fix
|
|
8
|
-
- [ ] Feature
|
|
9
|
-
- [ ] Breaking change
|
|
10
|
-
- [ ] Documentation
|
|
11
|
-
- [ ] Refactor
|
|
12
|
-
- [ ] Chore (maintenance, deps, CI)
|
|
13
|
-
|
|
14
|
-
## Checklist
|
|
15
|
-
|
|
16
|
-
- [ ] Tests added/updated (if applicable)
|
|
17
|
-
- [ ] Lint passes
|
|
18
|
-
- [ ] Typecheck passes
|
|
19
|
-
- [ ] Smoke test passes
|
|
20
|
-
- [ ] Docs updated (if needed)
|
|
21
|
-
|
|
22
|
-
## Related issue
|
|
23
|
-
|
|
24
|
-
<!-- Closes #N or "No related issue" -->
|