wiki-viewer 1.0.0 → 1.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 (128) hide show
  1. package/.next/standalone/README.md +365 -99
  2. package/.next/standalone/agents/bootstrap-prompt.md +1 -0
  3. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
  4. package/.next/standalone/bin/wiki-viewer.js +431 -33
  5. package/.next/standalone/docs/agent-collab-plan.md +1615 -0
  6. package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
  7. package/.next/standalone/package.json +19 -2
  8. package/.next/standalone/pnpm-lock.yaml +1368 -325
  9. package/.next/standalone/pnpm-workspace.yaml +7 -1
  10. package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
  11. package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
  12. package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
  13. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
  14. package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
  15. package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
  16. package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
  17. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
  18. package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
  19. package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
  20. package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
  21. package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
  22. package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
  23. package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
  24. package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
  25. package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
  26. package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
  27. package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
  28. package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
  29. package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
  30. package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
  31. package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
  32. package/.next/standalone/src/app/api/system/config/route.ts +5 -1
  33. package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
  34. package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
  35. package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
  36. package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
  37. package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
  38. package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
  39. package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
  40. package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
  41. package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
  42. package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
  43. package/.next/standalone/src/app/api/wiki/route.ts +10 -0
  44. package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
  45. package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
  46. package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
  47. package/.next/standalone/src/app/globals.css +70 -0
  48. package/.next/standalone/src/app/page.tsx +461 -217
  49. package/.next/standalone/src/app/signin/page.tsx +217 -0
  50. package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
  51. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
  52. package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
  53. package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
  54. package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
  55. package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
  56. package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
  57. package/.next/standalone/src/components/editor/editor.tsx +513 -10
  58. package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
  59. package/.next/standalone/src/components/editor/extensions.ts +2 -0
  60. package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
  61. package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
  62. package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
  63. package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
  64. package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
  65. package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
  66. package/.next/standalone/src/lib/auth/client.ts +4 -0
  67. package/.next/standalone/src/lib/auth/csrf.ts +68 -0
  68. package/.next/standalone/src/lib/auth/server.ts +164 -0
  69. package/.next/standalone/src/lib/config.ts +4 -0
  70. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
  71. package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
  72. package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
  73. package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
  74. package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
  75. package/.next/standalone/src/lib/proof/activity.ts +74 -0
  76. package/.next/standalone/src/lib/proof/auth.ts +132 -0
  77. package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
  78. package/.next/standalone/src/lib/proof/blocks.ts +73 -0
  79. package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
  80. package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
  81. package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
  82. package/.next/standalone/src/lib/proof/glob.ts +51 -0
  83. package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
  84. package/.next/standalone/src/lib/proof/mutex.ts +32 -0
  85. package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
  86. package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
  87. package/.next/standalone/src/lib/proof/pending.ts +97 -0
  88. package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
  89. package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
  90. package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
  91. package/.next/standalone/src/lib/proof/registry.ts +190 -0
  92. package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
  93. package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
  94. package/.next/standalone/src/lib/proof/types.ts +170 -0
  95. package/.next/standalone/src/lib/proof-config.ts +14 -0
  96. package/.next/standalone/src/middleware.ts +38 -0
  97. package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
  98. package/.next/standalone/src/stores/editor-store.ts +115 -9
  99. package/.next/standalone/src/stores/proof-store.ts +123 -0
  100. package/.next/standalone/src/stores/view-width-store.ts +62 -0
  101. package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
  102. package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
  103. package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
  104. package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
  105. package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
  106. package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
  107. package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
  108. package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
  109. package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
  110. package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
  111. package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
  112. package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
  113. package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
  114. package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
  115. package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
  116. package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
  117. package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
  118. package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
  119. package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
  120. package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
  121. package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
  122. package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
  123. package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
  124. package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
  125. package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
  126. package/README.md +365 -99
  127. package/bin/wiki-viewer.js +431 -33
  128. package/package.json +19 -2
@@ -2,6 +2,7 @@ import { readdir, stat } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { NextResponse } from "next/server";
5
+ import { requireUser } from "@/lib/auth/server";
5
6
 
6
7
  interface BrowseEntry {
7
8
  name: string;
@@ -10,6 +11,9 @@ interface BrowseEntry {
10
11
  }
11
12
 
12
13
  export async function GET(request: Request) {
14
+ const auth = await requireUser(request);
15
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
16
+
13
17
  const { searchParams } = new URL(request.url);
14
18
  const rawPath = searchParams.get("path");
15
19
 
@@ -1,8 +1,15 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { checkOrigin } from "@/lib/auth/csrf";
3
+ import { requireUser } from "@/lib/auth/server";
2
4
  import { writeConfig } from "@/lib/config";
3
5
  import { clearRootDir } from "@/lib/root-dir";
4
6
 
5
- export async function POST() {
7
+ export async function POST(request: Request) {
8
+ const csrf = checkOrigin(request);
9
+ if (csrf) return csrf;
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
6
13
  clearRootDir();
7
14
  await writeConfig({ lastOpenedPath: undefined });
8
15
  return NextResponse.json({ ok: true });
@@ -1,7 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { requireUser } from "@/lib/auth/server";
2
3
  import { readConfig } from "@/lib/config";
3
4
 
4
- export async function GET() {
5
+ export async function GET(request: Request) {
6
+ const auth = await requireUser(request);
7
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
8
+
5
9
  const config = await readConfig();
6
10
  return NextResponse.json({
7
11
  pinnedPaths: config.pinnedPaths ?? [],
@@ -1,8 +1,15 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { NextResponse } from "next/server";
3
+ import { checkOrigin } from "@/lib/auth/csrf";
4
+ import { requireUser } from "@/lib/auth/server";
3
5
  import { readConfig, writeConfig } from "@/lib/config";
4
6
 
5
7
  export async function POST(request: Request) {
8
+ const csrf = checkOrigin(request);
9
+ if (csrf) return csrf;
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
6
13
  const body: { path?: string; action?: "pin" | "unpin" } = await request.json();
7
14
  const p = body.path?.trim();
8
15
  const action = body.action ?? "pin";
@@ -1,9 +1,16 @@
1
1
  import { exec } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
4
6
  import { getRootDir } from "@/lib/root-dir";
5
7
 
6
8
  export async function POST(request: Request) {
9
+ const csrf = checkOrigin(request);
10
+ if (csrf) return csrf;
11
+ const auth = await requireUser(request);
12
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
13
+
7
14
  const body: { path?: string } = await request.json();
8
15
  const rel = body.path;
9
16
  if (!rel || typeof rel !== "string")
@@ -1,7 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { requireUser } from "@/lib/auth/server";
2
3
  import { getRootDir, isRootDirSet } from "@/lib/root-dir";
3
4
 
4
- export async function GET() {
5
+ export async function GET(request: Request) {
6
+ const auth = await requireUser(request);
7
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
8
+
5
9
  return NextResponse.json({
6
10
  configured: isRootDirSet(),
7
11
  path: isRootDirSet() ? getRootDir() : null,
@@ -1,9 +1,16 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { NextResponse } from "next/server";
3
+ import { checkOrigin } from "@/lib/auth/csrf";
4
+ import { requireUser } from "@/lib/auth/server";
3
5
  import { writeConfig } from "@/lib/config";
4
6
  import { setRootDir } from "@/lib/root-dir";
5
7
 
6
8
  export async function POST(request: Request) {
9
+ const csrf = checkOrigin(request);
10
+ if (csrf) return csrf;
11
+ const auth = await requireUser(request);
12
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
13
+
7
14
  const body: { path?: string } = await request.json();
8
15
  const dir = body.path?.trim();
9
16
  if (!dir) return NextResponse.json({ error: "Missing path" }, { status: 400 });
@@ -1,10 +1,15 @@
1
1
  import path from "node:path";
2
2
  import { NextResponse } from "next/server";
3
+ import { checkOrigin } from "@/lib/auth/csrf";
4
+ import { requireUser } from "@/lib/auth/server";
3
5
  import { getStatus, startApp, stopApp } from "@/lib/app-runner";
4
6
  import { getRootDir, safeRootPath } from "@/lib/root-dir";
5
7
 
6
8
  // GET /api/wiki/app?path=relative/path
7
9
  export async function GET(request: Request) {
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
8
13
  const { searchParams } = new URL(request.url);
9
14
  const rel = searchParams.get("path") ?? "";
10
15
  return NextResponse.json(getStatus(rel));
@@ -12,6 +17,11 @@ export async function GET(request: Request) {
12
17
 
13
18
  // POST /api/wiki/app { path: "relative/path" }
14
19
  export async function POST(request: Request) {
20
+ const csrf = checkOrigin(request);
21
+ if (csrf) return csrf;
22
+ const auth = await requireUser(request);
23
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
24
+
15
25
  const body: { path?: string } = await request.json();
16
26
  const rel = body.path;
17
27
  if (!rel || typeof rel !== "string")
@@ -31,6 +41,11 @@ export async function POST(request: Request) {
31
41
 
32
42
  // DELETE /api/wiki/app { path: "relative/path" }
33
43
  export async function DELETE(request: Request) {
44
+ const csrf = checkOrigin(request);
45
+ if (csrf) return csrf;
46
+ const auth = await requireUser(request);
47
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
48
+
34
49
  const body: { path?: string } = await request.json();
35
50
  const rel = body.path;
36
51
  if (!rel || typeof rel !== "string")
@@ -1,14 +1,23 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { NextResponse } from "next/server";
4
- import { safeRootPath } from "@/lib/root-dir";
5
+ import { checkOrigin } from "@/lib/auth/csrf";
6
+ import { requireUser } from "@/lib/auth/server";
7
+ import { emitEvents, trimEvents } from "@/lib/proof/event-bus";
8
+ import { withFileMutex } from "@/lib/proof/mutex";
9
+ import { emptySidecar, readSidecar, writeSidecar } from "@/lib/proof/sidecar";
10
+ import { SIDECAR_EVENT_TRIM_SIZE } from "@/lib/proof-config";
11
+ import { getRootDir, safeRootPath } from "@/lib/root-dir";
5
12
 
6
13
  const TEXT_EXTS = new Set([
7
- "txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv",
14
+ "txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv", "tsv",
8
15
  "xml", "html", "css", "js", "ts", "tsx", "jsx", "sh", "bash",
9
16
  "zsh", "rb", "py", "go", "rs", "java", "c", "cpp", "h", "php",
10
- "swift", "kt", "lua", "sql", "scss",
17
+ "swift", "kt", "lua", "sql", "scss", "mmd", "mermaid", "ini",
18
+ "env", "log", "conf",
11
19
  ]);
20
+ const MARKDOWN_EXTS = new Set(["md", "markdown"]);
12
21
  const MAX_EDIT_SIZE = 1 * 1024 * 1024; // 1MB
13
22
 
14
23
  function isTextFile(filename: string): boolean {
@@ -16,7 +25,19 @@ function isTextFile(filename: string): boolean {
16
25
  return TEXT_EXTS.has(ext);
17
26
  }
18
27
 
28
+ function isMarkdownFile(filename: string): boolean {
29
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
30
+ return MARKDOWN_EXTS.has(ext);
31
+ }
32
+
33
+ function sha256content(content: string): string {
34
+ return "sha256:" + createHash("sha256").update(content, "utf8").digest("hex");
35
+ }
36
+
19
37
  export async function GET(request: Request) {
38
+ const sessionAuth = await requireUser(request);
39
+ if (!sessionAuth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
40
+
20
41
  const { searchParams } = new URL(request.url);
21
42
  const rel = searchParams.get("path") ?? "";
22
43
  const filePath = safeRootPath(rel);
@@ -31,14 +52,32 @@ export async function GET(request: Request) {
31
52
  { error: "File too large (max 1MB)" },
32
53
  { status: 413 },
33
54
  );
34
- return NextResponse.json({ content: buffer.toString("utf-8") });
55
+ const content = buffer.toString("utf-8");
56
+ const headers: Record<string, string> = {};
57
+ if (isMarkdownFile(path.basename(filePath))) {
58
+ const rootDir = getRootDir();
59
+ const sc = await readSidecar(rootDir, rel);
60
+ if (sc) {
61
+ headers["X-Wiki-Revision"] = String(sc.revision);
62
+ headers["X-Wiki-Fingerprint"] = sc.fingerprint;
63
+ }
64
+ }
65
+ return NextResponse.json({ content }, { headers });
35
66
  } catch {
36
67
  return NextResponse.json({ error: "File not found" }, { status: 404 });
37
68
  }
38
69
  }
39
70
 
40
71
  export async function PUT(request: Request) {
41
- const body: { path?: string; content?: string } = await request.json();
72
+ const csrf = checkOrigin(request);
73
+ if (csrf) return csrf;
74
+ const user = await requireUser(request);
75
+ if (!user.ok) {
76
+ return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
77
+ }
78
+
79
+ const body: { path?: string; content?: string; baseRevision?: number } =
80
+ await request.json();
42
81
  const rel = body.path;
43
82
  const content = body.content;
44
83
  if (!rel || typeof rel !== "string")
@@ -50,11 +89,71 @@ export async function PUT(request: Request) {
50
89
  return NextResponse.json({ error: "Invalid path" }, { status: 400 });
51
90
  if (!isTextFile(path.basename(filePath)))
52
91
  return NextResponse.json({ error: "Not a text file" }, { status: 400 });
53
- try {
54
- await mkdir(path.dirname(filePath), { recursive: true });
55
- await writeFile(filePath, content, "utf-8");
56
- return NextResponse.json({ ok: true });
57
- } catch {
58
- return NextResponse.json({ error: "Failed to save" }, { status: 500 });
92
+
93
+ // Non-markdown files: plain write, no revision tracking.
94
+ if (!isMarkdownFile(path.basename(filePath))) {
95
+ try {
96
+ await mkdir(path.dirname(filePath), { recursive: true });
97
+ await writeFile(filePath, content, "utf-8");
98
+ return NextResponse.json({ ok: true });
99
+ } catch {
100
+ return NextResponse.json({ error: "Failed to save" }, { status: 500 });
101
+ }
59
102
  }
103
+
104
+ // Markdown files: mutex + sidecar revision check + fingerprint update + event.
105
+ return withFileMutex(filePath, async () => {
106
+ const rootDir = getRootDir();
107
+ const sc = (await readSidecar(rootDir, rel)) ?? emptySidecar(rel);
108
+
109
+ // baseRevision is required for markdown to prevent lost writes.
110
+ if (body.baseRevision === undefined || typeof body.baseRevision !== "number") {
111
+ return NextResponse.json(
112
+ { error: "BASE_REVISION_REQUIRED", message: "baseRevision is required when saving markdown files." },
113
+ { status: 400 },
114
+ );
115
+ }
116
+
117
+ // Enforce staleness check.
118
+ if (
119
+ body.baseRevision !== sc.revision
120
+ ) {
121
+ return NextResponse.json(
122
+ {
123
+ error: "STALE_REVISION",
124
+ currentRevision: sc.revision,
125
+ message: "File was modified since your last read. Reload and retry.",
126
+ },
127
+ { status: 409 },
128
+ );
129
+ }
130
+
131
+ try {
132
+ await mkdir(path.dirname(filePath), { recursive: true });
133
+ await writeFile(filePath, content, "utf-8");
134
+ } catch {
135
+ return NextResponse.json({ error: "Failed to save" }, { status: 500 });
136
+ }
137
+
138
+ // Update sidecar metadata.
139
+ const newRevision = sc.revision + 1;
140
+ sc.revision = newRevision;
141
+ sc.fingerprint = sha256content(content);
142
+ sc.updatedAt = new Date().toISOString();
143
+
144
+ // Emit a human-edit event so agents can see the change.
145
+ emitEvents(sc, [
146
+ {
147
+ type: "file.edited",
148
+ at: new Date().toISOString(),
149
+ by: `user:${user.user.id}`,
150
+ revision: newRevision,
151
+ },
152
+ ]);
153
+ trimEvents(sc, SIDECAR_EVENT_TRIM_SIZE);
154
+
155
+ await writeSidecar(rootDir, rel, sc);
156
+
157
+ return NextResponse.json({ ok: true, revision: newRevision });
158
+ });
60
159
  }
@@ -1,8 +1,15 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { NextResponse } from "next/server";
3
+ import { checkOrigin } from "@/lib/auth/csrf";
4
+ import { requireUser } from "@/lib/auth/server";
3
5
  import { safeRootPath } from "@/lib/root-dir";
4
6
 
5
7
  export async function POST(request: Request) {
8
+ const csrf = checkOrigin(request);
9
+ if (csrf) return csrf;
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
6
13
  const body: { path?: string } = await request.json();
7
14
  const rel = body.path;
8
15
 
@@ -1,9 +1,16 @@
1
1
  import { rename, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
4
6
  import { safeRootPath } from "@/lib/root-dir";
5
7
 
6
8
  export async function POST(request: Request) {
9
+ const csrf = checkOrigin(request);
10
+ if (csrf) return csrf;
11
+ const auth = await requireUser(request);
12
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
13
+
7
14
  const body: { from?: string; to?: string } = await request.json();
8
15
  if (
9
16
  !body.from ||
@@ -0,0 +1,55 @@
1
+ import { mkdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
6
+ import { safeRootPath } from "@/lib/root-dir";
7
+
8
+ export async function POST(request: Request) {
9
+ const csrf = checkOrigin(request);
10
+ if (csrf) return csrf;
11
+ const auth = await requireUser(request);
12
+ if (!auth.ok)
13
+ return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
14
+
15
+ const body: { path?: string } = await request.json();
16
+ const rel = body.path;
17
+
18
+ if (!rel || typeof rel !== "string" || /[<>:"|?*]/.test(rel)) {
19
+ return NextResponse.json({ error: "Invalid file path" }, { status: 400 });
20
+ }
21
+ if (rel.endsWith("/")) {
22
+ return NextResponse.json({ error: "Invalid file path" }, { status: 400 });
23
+ }
24
+
25
+ const filePath = safeRootPath(rel);
26
+ if (!filePath)
27
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
28
+
29
+ try {
30
+ await stat(filePath);
31
+ return NextResponse.json(
32
+ { error: "File already exists", path: rel },
33
+ { status: 409 },
34
+ );
35
+ } catch (e: unknown) {
36
+ const code = (e as NodeJS.ErrnoException).code;
37
+ if (code !== "ENOENT") {
38
+ return NextResponse.json(
39
+ { error: "Failed to create file" },
40
+ { status: 500 },
41
+ );
42
+ }
43
+ }
44
+
45
+ try {
46
+ await mkdir(path.dirname(filePath), { recursive: true });
47
+ await writeFile(filePath, "", "utf-8");
48
+ return NextResponse.json({ ok: true, path: rel });
49
+ } catch {
50
+ return NextResponse.json(
51
+ { error: "Failed to create file" },
52
+ { status: 500 },
53
+ );
54
+ }
55
+ }
@@ -1,6 +1,8 @@
1
1
  import { mkdir, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
4
6
  import { safeRootPath } from "@/lib/root-dir";
5
7
 
6
8
  const VALID_DIRS = new Set(["entities", "concepts", "comparisons"]);
@@ -33,6 +35,11 @@ function singularType(
33
35
  }
34
36
 
35
37
  export async function POST(request: Request) {
38
+ const csrf = checkOrigin(request);
39
+ if (csrf) return csrf;
40
+ const auth = await requireUser(request);
41
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
42
+
36
43
  const body: PageBody = await request.json();
37
44
  const { dir, slug } = body;
38
45
 
@@ -1,10 +1,15 @@
1
1
  import { readdir, rmdir, stat, unlink } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
4
6
  import { getRootDir, safeRootPath } from "@/lib/root-dir";
5
7
  import { isAppFolder, isNodeApp } from "@/lib/wiki-helpers";
6
8
 
7
9
  export async function GET(request: Request) {
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
8
13
  const { searchParams } = new URL(request.url);
9
14
  const dir = searchParams.get("dir") ?? "";
10
15
 
@@ -67,6 +72,11 @@ export async function GET(request: Request) {
67
72
  }
68
73
 
69
74
  export async function DELETE(request: Request) {
75
+ const csrf = checkOrigin(request);
76
+ if (csrf) return csrf;
77
+ const auth = await requireUser(request);
78
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
79
+
70
80
  const body: { path?: string } = await request.json();
71
81
  const rel = body.path;
72
82
  if (!rel || typeof rel !== "string")
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { requireUser } from "@/lib/auth/server";
4
5
  import { getRootDir } from "@/lib/root-dir";
5
6
 
6
7
  type SlugBuckets = {
@@ -26,7 +27,10 @@ async function readMarkdownSlugsFromDir(dirPath: string): Promise<string[]> {
26
27
  }
27
28
  }
28
29
 
29
- export async function GET() {
30
+ export async function GET(request: Request) {
31
+ const auth = await requireUser(request);
32
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
33
+
30
34
  try {
31
35
  // Scan root + known dirs (entities, concepts, comparisons for wiki compat)
32
36
  // plus any other immediate subdirectories
@@ -1,6 +1,8 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { NextResponse } from "next/server";
4
+ import { checkOrigin } from "@/lib/auth/csrf";
5
+ import { requireUser } from "@/lib/auth/server";
4
6
  import { safeRootPath } from "@/lib/root-dir";
5
7
 
6
8
  const ALLOWED_MIME_TYPES = new Set([
@@ -35,6 +37,11 @@ function sanitizeFilename(name: string): string {
35
37
  }
36
38
 
37
39
  export async function POST(request: Request) {
40
+ const csrf = checkOrigin(request);
41
+ if (csrf) return csrf;
42
+ const auth = await requireUser(request);
43
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
44
+
38
45
  let formData: FormData;
39
46
  try {
40
47
  formData = await request.formData();
@@ -2,9 +2,14 @@ export const runtime = "nodejs";
2
2
 
3
3
  import { watch } from "chokidar";
4
4
  import path from "node:path";
5
+ import { NextResponse } from "next/server";
6
+ import { requireUser } from "@/lib/auth/server";
5
7
  import { getRootDir } from "@/lib/root-dir";
6
8
 
7
- export async function GET() {
9
+ export async function GET(request: Request) {
10
+ const auth = await requireUser(request);
11
+ if (!auth.ok) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
12
+
8
13
  const rootDir = getRootDir();
9
14
  if (!rootDir) {
10
15
  return new Response("Root not configured", { status: 503 });
@@ -714,3 +714,73 @@ nextjs-portal {
714
714
  .wiki-link[data-broken="true"]:hover {
715
715
  background: color-mix(in srgb, var(--destructive) 10%, transparent);
716
716
  }
717
+
718
+ /* Proof-span provenance marks */
719
+ proof-span.proof-span,
720
+ .proof-span {
721
+ background: linear-gradient(
722
+ to right,
723
+ rgba(165, 180, 252, 0.08),
724
+ rgba(165, 180, 252, 0)
725
+ );
726
+ border-left: 2px solid rgb(165, 180, 252);
727
+ padding-left: 4px;
728
+ border-radius: 2px;
729
+ cursor: pointer;
730
+ transition: background 120ms ease;
731
+ }
732
+ proof-span.proof-span[origin="human"],
733
+ .proof-span[origin="human"] {
734
+ border-left-color: rgb(110, 231, 183);
735
+ background: linear-gradient(
736
+ to right,
737
+ rgba(110, 231, 183, 0.08),
738
+ rgba(110, 231, 183, 0)
739
+ );
740
+ }
741
+ proof-span.proof-span:hover,
742
+ .proof-span:hover {
743
+ background: linear-gradient(
744
+ to right,
745
+ rgba(165, 180, 252, 0.18),
746
+ rgba(165, 180, 252, 0.04)
747
+ );
748
+ }
749
+ .dark proof-span.proof-span,
750
+ .dark .proof-span {
751
+ background: linear-gradient(
752
+ to right,
753
+ rgba(165, 180, 252, 0.14),
754
+ rgba(165, 180, 252, 0)
755
+ );
756
+ }
757
+
758
+ /* Read-only markdown viewer code styling */
759
+ .md-pre {
760
+ background: var(--muted);
761
+ color: var(--foreground);
762
+ padding: 0.75rem 0.9rem;
763
+ border-radius: calc(var(--radius) * 2);
764
+ overflow-x: auto;
765
+ font-size: 0.8125rem;
766
+ line-height: 1.55;
767
+ margin: 0.75rem 0;
768
+ }
769
+ .md-pre .md-code,
770
+ .md-pre code {
771
+ background: transparent;
772
+ color: inherit;
773
+ padding: 0;
774
+ border-radius: 0;
775
+ font-size: inherit;
776
+ white-space: pre;
777
+ display: block;
778
+ }
779
+ .md-code {
780
+ background: var(--muted);
781
+ color: var(--foreground);
782
+ padding: 0.1rem 0.3rem;
783
+ border-radius: var(--radius);
784
+ font-size: 0.85em;
785
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
786
+ }