donguri-journal 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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +181 -0
  3. package/README.md +184 -0
  4. package/dist/db/schema.d.ts +21 -0
  5. package/dist/db/schema.js +53 -0
  6. package/dist/db/schema.js.map +1 -0
  7. package/dist/db/store.d.ts +136 -0
  8. package/dist/db/store.js +0 -0
  9. package/dist/db/store.js.map +1 -0
  10. package/dist/embedding/provider.d.ts +37 -0
  11. package/dist/embedding/provider.js +53 -0
  12. package/dist/embedding/provider.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +49 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/kernel/config.d.ts +17 -0
  17. package/dist/kernel/config.js +26 -0
  18. package/dist/kernel/config.js.map +1 -0
  19. package/dist/kernel/context.d.ts +26 -0
  20. package/dist/kernel/context.js +7 -0
  21. package/dist/kernel/context.js.map +1 -0
  22. package/dist/kernel/module.d.ts +13 -0
  23. package/dist/kernel/module.js +7 -0
  24. package/dist/kernel/module.js.map +1 -0
  25. package/dist/kernel/plugin.d.ts +68 -0
  26. package/dist/kernel/plugin.js +144 -0
  27. package/dist/kernel/plugin.js.map +1 -0
  28. package/dist/kernel/result.d.ts +14 -0
  29. package/dist/kernel/result.js +11 -0
  30. package/dist/kernel/result.js.map +1 -0
  31. package/dist/management/module.d.ts +2 -0
  32. package/dist/management/module.js +39 -0
  33. package/dist/management/module.js.map +1 -0
  34. package/dist/management/server.d.ts +18 -0
  35. package/dist/management/server.js +216 -0
  36. package/dist/management/server.js.map +1 -0
  37. package/dist/management/ui.d.ts +10 -0
  38. package/dist/management/ui.js +159 -0
  39. package/dist/management/ui.js.map +1 -0
  40. package/dist/modules/core.d.ts +2 -0
  41. package/dist/modules/core.js +386 -0
  42. package/dist/modules/core.js.map +1 -0
  43. package/dist/modules/plugins.d.ts +2 -0
  44. package/dist/modules/plugins.js +177 -0
  45. package/dist/modules/plugins.js.map +1 -0
  46. package/dist/originals/store.d.ts +50 -0
  47. package/dist/originals/store.js +185 -0
  48. package/dist/originals/store.js.map +1 -0
  49. package/dist/review/charts.d.ts +16 -0
  50. package/dist/review/charts.js +69 -0
  51. package/dist/review/charts.js.map +1 -0
  52. package/dist/review/patterns.d.ts +33 -0
  53. package/dist/review/patterns.js +73 -0
  54. package/dist/review/patterns.js.map +1 -0
  55. package/dist/review/review.d.ts +30 -0
  56. package/dist/review/review.js +82 -0
  57. package/dist/review/review.js.map +1 -0
  58. package/dist/review/window.d.ts +18 -0
  59. package/dist/review/window.js +57 -0
  60. package/dist/review/window.js.map +1 -0
  61. package/package.json +62 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Management UI host.
3
+ *
4
+ * A small, opt-in HTTP server (started only when the owner calls the
5
+ * open_management_ui tool) that serves a local, browser-based console over the
6
+ * existing JournalStore. It is the owner's direct, non-LLM window onto the
7
+ * journal — the counterpart to the LLM-mediated MCP tools.
8
+ *
9
+ * SECURITY (this is a plain HTTP server on the owner's machine):
10
+ * - Binds to localhost only (config.uiHost, default 127.0.0.1).
11
+ * - Every /api/* request must present a per-session token (random, generated at
12
+ * startup) via the `x-donguri-token` header or `?token=`. The token is
13
+ * compared in constant time.
14
+ * - The Host header is pinned to a loopback name, so a remote page cannot use
15
+ * DNS rebinding to reach the API through the victim's browser.
16
+ * - Read-only in this first slice: only GET, no mutations, and the entry data
17
+ * it serves never includes filesystem paths.
18
+ */
19
+ import { randomBytes, timingSafeEqual } from "node:crypto";
20
+ import { statSync } from "node:fs";
21
+ import { createServer } from "node:http";
22
+ import { isIP } from "node:net";
23
+ import { z } from "zod";
24
+ import { renderApp } from "./ui.js";
25
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
26
+ const entriesQuerySchema = z.object({
27
+ since: z.string().datetime({ offset: true }).optional(),
28
+ until: z.string().datetime({ offset: true }).optional(),
29
+ source_kind: z.string().min(1).optional(),
30
+ tag: z.string().min(1).optional(),
31
+ time_field: z.enum(["created_at", "occurred_at"]).optional(),
32
+ limit: z.coerce.number().int().optional(),
33
+ include_deleted: z
34
+ .enum(["true", "false"])
35
+ .optional()
36
+ .transform((v) => v === "true"),
37
+ });
38
+ const recallQuerySchema = z.object({
39
+ q: z.string().min(1),
40
+ k: z.coerce.number().int().optional(),
41
+ });
42
+ /** Constant-time token comparison that never throws on length mismatch. */
43
+ function tokenMatches(expected, provided) {
44
+ if (!provided)
45
+ return false;
46
+ const a = Buffer.from(expected);
47
+ const b = Buffer.from(provided);
48
+ if (a.length !== b.length)
49
+ return false;
50
+ return timingSafeEqual(a, b);
51
+ }
52
+ /**
53
+ * Is this a host we're willing to bind to? Loopback only — the Host-header check
54
+ * is a browser-side DNS-rebinding guard, NOT a substitute for the bind range, so
55
+ * we refuse to expose the socket on an external interface (e.g. 0.0.0.0).
56
+ */
57
+ function isBindableLoopback(host) {
58
+ // `host` is the bare literal (brackets already stripped by the caller), since
59
+ // Node's listen() takes a raw IP, not the URL/bracketed form.
60
+ const h = host.toLowerCase();
61
+ if (h === "localhost" || h === "::1")
62
+ return true;
63
+ // Any real IPv4 literal in 127.0.0.0/8. isIP() rejects malformed octets (e.g.
64
+ // 127.999.999.999) that a loose regex would wrongly accept.
65
+ return isIP(h) === 4 && h.startsWith("127.");
66
+ }
67
+ /** Reject requests whose Host header isn't a loopback name (DNS-rebinding guard). */
68
+ function hostIsLoopback(hostHeader) {
69
+ if (!hostHeader)
70
+ return false;
71
+ // Strip the port; keep bracketed IPv6 intact.
72
+ const host = hostHeader.startsWith("[")
73
+ ? hostHeader.slice(0, hostHeader.indexOf("]") + 1)
74
+ : (hostHeader.split(":")[0] ?? "");
75
+ return LOOPBACK_HOSTS.has(host);
76
+ }
77
+ /**
78
+ * Build (but do not start) the management HTTP server. Exposed for tests, which
79
+ * drive it on an ephemeral port. Production goes through startManagementUi.
80
+ */
81
+ export function createManagementServer(ctx, token) {
82
+ const { store, originals, config } = ctx;
83
+ const sendJson = (res, status, body) => {
84
+ // no-store: responses can carry journal bodies; keep them out of any cache.
85
+ res.writeHead(status, {
86
+ "content-type": "application/json; charset=utf-8",
87
+ "cache-control": "no-store",
88
+ });
89
+ res.end(JSON.stringify(body));
90
+ };
91
+ return createServer(async (req, res) => {
92
+ try {
93
+ if (!hostIsLoopback(req.headers.host)) {
94
+ sendJson(res, 421, { error: "Misdirected request" });
95
+ return;
96
+ }
97
+ const url = new URL(req.url ?? "/", "http://localhost");
98
+ const method = req.method ?? "GET";
99
+ const provided = req.headers["x-donguri-token"] ?? url.searchParams.get("token");
100
+ const authorized = tokenMatches(token, provided ?? null);
101
+ // The shell itself is token-gated so a co-resident local process can't load
102
+ // it, read a baked-in token, and reach the API. The page then reuses the
103
+ // ?token= from its own URL for /api/* calls.
104
+ if (url.pathname === "/" && method === "GET") {
105
+ if (!authorized) {
106
+ res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
107
+ res.end("Unauthorized: open the URL printed by open_management_ui (it carries the token).");
108
+ return;
109
+ }
110
+ res.writeHead(200, {
111
+ "content-type": "text/html; charset=utf-8",
112
+ "cache-control": "no-store",
113
+ });
114
+ res.end(renderApp());
115
+ return;
116
+ }
117
+ if (url.pathname.startsWith("/api/")) {
118
+ if (method !== "GET") {
119
+ sendJson(res, 405, { error: "Method not allowed" });
120
+ return;
121
+ }
122
+ if (!authorized) {
123
+ sendJson(res, 401, { error: "Unauthorized" });
124
+ return;
125
+ }
126
+ if (url.pathname === "/api/entries") {
127
+ const parsed = entriesQuerySchema.safeParse(Object.fromEntries(url.searchParams));
128
+ if (!parsed.success) {
129
+ sendJson(res, 400, { error: "Invalid query", issues: parsed.error.issues });
130
+ return;
131
+ }
132
+ const entries = store.query(parsed.data);
133
+ sendJson(res, 200, { count: entries.length, entries });
134
+ return;
135
+ }
136
+ if (url.pathname === "/api/recall") {
137
+ const parsed = recallQuerySchema.safeParse(Object.fromEntries(url.searchParams));
138
+ if (!parsed.success) {
139
+ sendJson(res, 400, { error: "Invalid query", issues: parsed.error.issues });
140
+ return;
141
+ }
142
+ const hits = await store.recall(parsed.data.q, parsed.data.k);
143
+ sendJson(res, 200, { count: hits.length, hits });
144
+ return;
145
+ }
146
+ if (url.pathname === "/api/stats") {
147
+ let dbBytes = null;
148
+ try {
149
+ dbBytes = statSync(config.dbPath).size;
150
+ }
151
+ catch {
152
+ dbBytes = null;
153
+ }
154
+ sendJson(res, 200, {
155
+ entries: store.entryStats(),
156
+ originals: await originals.stats(),
157
+ db_bytes: dbBytes,
158
+ });
159
+ return;
160
+ }
161
+ sendJson(res, 404, { error: "Not found" });
162
+ return;
163
+ }
164
+ sendJson(res, 404, { error: "Not found" });
165
+ }
166
+ catch (err) {
167
+ ctx.log("management UI request failed:", err);
168
+ if (!res.headersSent) {
169
+ sendJson(res, 500, { error: "Internal error" });
170
+ }
171
+ else {
172
+ res.end();
173
+ }
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * Start the management UI on config.uiHost:uiPort (an ephemeral port by
179
+ * default). Returns the URL (with the session token) to hand to the owner.
180
+ */
181
+ export function startManagementUi(ctx) {
182
+ const token = randomBytes(24).toString("base64url");
183
+ const server = createManagementServer(ctx, token);
184
+ const { uiPort } = ctx.config;
185
+ // Node's listen() wants a raw IP literal (`::1`), not the bracketed URL form
186
+ // (`[::1]`); strip brackets before validating/binding.
187
+ const configured = ctx.config.uiHost;
188
+ const bareHost = configured.startsWith("[") && configured.endsWith("]") ? configured.slice(1, -1) : configured;
189
+ // Enforce localhost-only at the bind boundary; a non-loopback config value is
190
+ // refused (not silently honored) so the journal can't be exposed on the LAN.
191
+ const uiHost = isBindableLoopback(bareHost) ? bareHost : "127.0.0.1";
192
+ if (uiHost !== bareHost) {
193
+ ctx.log(`refusing to bind management UI to non-loopback host "${configured}"; using 127.0.0.1 (localhost-only by design)`);
194
+ }
195
+ return new Promise((resolve, reject) => {
196
+ server.once("error", reject);
197
+ server.listen(uiPort, uiHost, () => {
198
+ server.removeListener("error", reject);
199
+ const address = server.address();
200
+ const port = typeof address === "object" && address ? address.port : uiPort;
201
+ // Bracket IPv6 hosts for the URL authority.
202
+ const hostForUrl = uiHost.includes(":") ? `[${uiHost}]` : uiHost;
203
+ const url = `http://${hostForUrl}:${port}/?token=${token}`;
204
+ resolve({
205
+ url,
206
+ port,
207
+ token,
208
+ close: () => new Promise((res, rej) => {
209
+ server.closeAllConnections();
210
+ server.close((err) => (err ? rej(err) : res()));
211
+ }),
212
+ });
213
+ });
214
+ });
215
+ }
216
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/management/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The management console SPA, as a single self-contained HTML document
3
+ * (inline CSS + vanilla JS, no framework, no build step — consistent with the
4
+ * dependency-free approach used elsewhere, e.g. the SVG charts).
5
+ *
6
+ * It reads the session token from its own URL (`?token=`) and sends it as the
7
+ * `x-donguri-token` header on every /api/* fetch. It renders data only; it holds
8
+ * no secrets beyond that token and performs no mutations in this slice.
9
+ */
10
+ export declare function renderApp(): string;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The management console SPA, as a single self-contained HTML document
3
+ * (inline CSS + vanilla JS, no framework, no build step — consistent with the
4
+ * dependency-free approach used elsewhere, e.g. the SVG charts).
5
+ *
6
+ * It reads the session token from its own URL (`?token=`) and sends it as the
7
+ * `x-donguri-token` header on every /api/* fetch. It renders data only; it holds
8
+ * no secrets beyond that token and performs no mutations in this slice.
9
+ */
10
+ // Kept static and data-free so the served bytes never embed anything sensitive.
11
+ export function renderApp() {
12
+ return `<!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8" />
16
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
17
+ <meta name="referrer" content="no-referrer" />
18
+ <title>donguri-journal</title>
19
+ <style>
20
+ :root { color-scheme: light dark; --gap: 12px; }
21
+ * { box-sizing: border-box; }
22
+ body { margin: 0; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
23
+ header { display: flex; align-items: baseline; gap: 10px; padding: 14px 18px; border-bottom: 1px solid #8883; }
24
+ header h1 { font-size: 16px; margin: 0; }
25
+ header .sub { opacity: 0.6; font-size: 12px; }
26
+ main { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 18px; padding: 18px; align-items: start; }
27
+ @media (max-width: 820px) { main { grid-template-columns: 1fr; } }
28
+ .controls { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--gap); }
29
+ .controls input, .controls select, .controls button {
30
+ font: inherit; padding: 6px 9px; border: 1px solid #8886; border-radius: 6px; background: transparent; color: inherit;
31
+ }
32
+ .controls button { cursor: pointer; }
33
+ .controls input[type="search"] { flex: 1 1 220px; }
34
+ .entry { border: 1px solid #8883; border-radius: 8px; padding: 10px 12px; margin-bottom: 10px; }
35
+ .entry.deleted { opacity: 0.55; }
36
+ .entry .body { white-space: pre-wrap; word-break: break-word; }
37
+ .entry .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 12px; opacity: 0.7; }
38
+ .tag { font-size: 11px; padding: 1px 7px; border: 1px solid #8886; border-radius: 999px; }
39
+ .badge { font-size: 11px; padding: 1px 7px; border-radius: 999px; background: #8882; }
40
+ aside { border: 1px solid #8883; border-radius: 8px; padding: 12px 14px; position: sticky; top: 18px; }
41
+ aside h2 { font-size: 13px; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; }
42
+ aside dl { display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; margin: 0; }
43
+ aside dt { opacity: 0.7; } aside dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
44
+ .muted { opacity: 0.6; }
45
+ .status { padding: 10px 0; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <header>
50
+ <h1>🐿️ donguri-journal</h1>
51
+ <span class="sub">management console</span>
52
+ </header>
53
+ <main>
54
+ <section>
55
+ <form class="controls" id="controls">
56
+ <input type="search" id="q" placeholder="Semantic recall (leave blank to browse)…" />
57
+ <input type="text" id="tag" placeholder="tag" size="8" />
58
+ <input type="text" id="source_kind" placeholder="source kind" size="10" />
59
+ <select id="time_field" title="timestamp field">
60
+ <option value="created_at">created_at</option>
61
+ <option value="occurred_at">occurred_at</option>
62
+ </select>
63
+ <label class="badge"><input type="checkbox" id="include_deleted" /> show deleted</label>
64
+ <button type="submit">Search</button>
65
+ </form>
66
+ <div class="status muted" id="status">Loading…</div>
67
+ <div id="list"></div>
68
+ </section>
69
+ <aside>
70
+ <h2>Storage</h2>
71
+ <dl id="stats"><dd class="muted">…</dd></dl>
72
+ </aside>
73
+ </main>
74
+ <script>
75
+ (() => {
76
+ const TOKEN = new URLSearchParams(location.search).get("token") || "";
77
+ const $ = (id) => document.getElementById(id);
78
+
79
+ async function api(path, params) {
80
+ const url = new URL(path, location.origin);
81
+ if (params) for (const [k, v] of Object.entries(params)) {
82
+ if (v !== "" && v != null && v !== false) url.searchParams.set(k, v);
83
+ }
84
+ const res = await fetch(url, { headers: { "x-donguri-token": TOKEN } });
85
+ if (!res.ok) throw new Error("HTTP " + res.status);
86
+ return res.json();
87
+ }
88
+
89
+ const fmt = (iso) => { try { return new Date(iso).toLocaleString(); } catch { return iso; } };
90
+ const esc = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
91
+
92
+ function renderEntries(data) {
93
+ const list = $("list");
94
+ if (!data.entries || data.entries.length === 0) {
95
+ list.innerHTML = '<p class="muted">No entries.</p>';
96
+ return;
97
+ }
98
+ list.innerHTML = data.entries.map((e) => {
99
+ const tags = (e.tags || []).map((t) => '<span class="tag">' + esc(String(t)) + "</span>").join(" ");
100
+ const deleted = e.deleted_at ? " deleted" : "";
101
+ const delBadge = e.deleted_at ? '<span class="badge">deleted</span>' : "";
102
+ const dist = typeof e.distance === "number" ? '<span class="badge">d=' + e.distance.toFixed(3) + "</span>" : "";
103
+ return '<div class="entry' + deleted + '">' +
104
+ '<div class="body">' + esc(String(e.body || "")) + "</div>" +
105
+ '<div class="meta"><span>#' + esc(String(e.id)) + "</span>" +
106
+ "<span>" + esc(String(e.source_kind || "")) + "</span>" +
107
+ "<span>" + esc(fmt(e.occurred_at)) + "</span>" +
108
+ (e.original_ref ? '<span class="badge">original</span>' : "") +
109
+ delBadge + dist + " " + tags + "</div></div>";
110
+ }).join("");
111
+ }
112
+
113
+ function renderStats(s) {
114
+ const rows = [];
115
+ rows.push(["Active", s.entries.active]);
116
+ rows.push(["Soft-deleted", s.entries.soft_deleted]);
117
+ rows.push(["Vectors", s.entries.vectors]);
118
+ rows.push(["Originals", s.originals.count]);
119
+ if (s.db_bytes != null) rows.push(["DB size", (s.db_bytes / 1024 / 1024).toFixed(2) + " MB"]);
120
+ if (s.originals.bytes != null) rows.push(["Originals size", (s.originals.bytes / 1024 / 1024).toFixed(2) + " MB"]);
121
+ $("stats").innerHTML = rows
122
+ .map(([k, v]) => "<dt>" + esc(String(k)) + "</dt><dd>" + esc(String(v)) + "</dd>")
123
+ .join("");
124
+ }
125
+
126
+ async function search() {
127
+ const status = $("status");
128
+ status.textContent = "Loading…";
129
+ try {
130
+ const q = $("q").value.trim();
131
+ let data;
132
+ if (q) {
133
+ data = await api("/api/recall", { q, k: 50 });
134
+ data = { entries: data.hits, count: data.count };
135
+ } else {
136
+ data = await api("/api/entries", {
137
+ tag: $("tag").value.trim(),
138
+ source_kind: $("source_kind").value.trim(),
139
+ time_field: $("time_field").value,
140
+ include_deleted: $("include_deleted").checked,
141
+ limit: 200,
142
+ });
143
+ }
144
+ renderEntries(data);
145
+ status.textContent = data.count + " " + (q ? "match(es) by meaning" : "ent"+"ries");
146
+ } catch (err) {
147
+ status.textContent = "Error: " + err.message;
148
+ }
149
+ }
150
+
151
+ $("controls").addEventListener("submit", (ev) => { ev.preventDefault(); search(); });
152
+ api("/api/stats").then(renderStats).catch(() => {});
153
+ search();
154
+ })();
155
+ </script>
156
+ </body>
157
+ </html>`;
158
+ }
159
+ //# sourceMappingURL=ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/management/ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,gFAAgF;AAChF,MAAM,UAAU,SAAS;IACvB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiJD,CAAC;AACT,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { JournalModule } from "../kernel/module.js";
2
+ export declare const coreModule: JournalModule;