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.
- package/.next/standalone/README.md +365 -99
- package/.next/standalone/agents/bootstrap-prompt.md +1 -0
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
- package/.next/standalone/bin/wiki-viewer.js +431 -33
- package/.next/standalone/docs/agent-collab-plan.md +1615 -0
- package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
- package/.next/standalone/package.json +19 -2
- package/.next/standalone/pnpm-lock.yaml +1368 -325
- package/.next/standalone/pnpm-workspace.yaml +7 -1
- package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
- package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
- package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
- package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
- package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
- package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
- package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
- package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
- package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
- package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
- package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
- package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
- package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
- package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
- package/.next/standalone/src/app/api/system/config/route.ts +5 -1
- package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
- package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
- package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/route.ts +10 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
- package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
- package/.next/standalone/src/app/globals.css +70 -0
- package/.next/standalone/src/app/page.tsx +461 -217
- package/.next/standalone/src/app/signin/page.tsx +217 -0
- package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
- package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
- package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
- package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
- package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
- package/.next/standalone/src/components/editor/editor.tsx +513 -10
- package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
- package/.next/standalone/src/components/editor/extensions.ts +2 -0
- package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
- package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
- package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
- package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
- package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
- package/.next/standalone/src/lib/auth/client.ts +4 -0
- package/.next/standalone/src/lib/auth/csrf.ts +68 -0
- package/.next/standalone/src/lib/auth/server.ts +164 -0
- package/.next/standalone/src/lib/config.ts +4 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
- package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
- package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
- package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
- package/.next/standalone/src/lib/proof/activity.ts +74 -0
- package/.next/standalone/src/lib/proof/auth.ts +132 -0
- package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
- package/.next/standalone/src/lib/proof/blocks.ts +73 -0
- package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
- package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
- package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
- package/.next/standalone/src/lib/proof/glob.ts +51 -0
- package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
- package/.next/standalone/src/lib/proof/mutex.ts +32 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
- package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
- package/.next/standalone/src/lib/proof/pending.ts +97 -0
- package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
- package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/registry.ts +190 -0
- package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
- package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
- package/.next/standalone/src/lib/proof/types.ts +170 -0
- package/.next/standalone/src/lib/proof-config.ts +14 -0
- package/.next/standalone/src/middleware.ts +38 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
- package/.next/standalone/src/stores/editor-store.ts +115 -9
- package/.next/standalone/src/stores/proof-store.ts +123 -0
- package/.next/standalone/src/stores/view-width-store.ts +62 -0
- package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
- package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
- package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
- package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
- package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
- package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
- package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
- package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
- package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
- package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
- package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
- package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
- package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
- package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
- package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
- package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
- package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
- package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
- package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
- package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
- package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
- package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
- package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
- package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
- package/README.md +365 -99
- package/bin/wiki-viewer.js +431 -33
- package/package.json +19 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal span accept/revert. Callable by owner (browser cookie) or any agent
|
|
3
|
+
* with mutate scope. The `by` field on emitted events is the authenticated agent id.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
import { revertProofSpan } from "@/lib/proof/proof-span";
|
|
8
|
+
import { readSidecar, writeSidecar, emptySidecar } from "@/lib/proof/sidecar";
|
|
9
|
+
import { withFileMutex } from "@/lib/proof/mutex";
|
|
10
|
+
import { emitEvents } from "@/lib/proof/event-bus";
|
|
11
|
+
import { getRootDir, safeRootPath } from "@/lib/root-dir";
|
|
12
|
+
import { checkAuth, enforceScope } from "@/lib/proof/auth";
|
|
13
|
+
|
|
14
|
+
export const runtime = "nodejs";
|
|
15
|
+
|
|
16
|
+
function isMarkdown(p: string): boolean {
|
|
17
|
+
return p.endsWith(".md") || p.endsWith(".markdown");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Remove the wrapper for a specific spanId while keeping inner content.
|
|
22
|
+
*/
|
|
23
|
+
function acceptSpanById(markdown: string, spanId: string): string {
|
|
24
|
+
const escapedId = spanId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
const re = new RegExp(
|
|
26
|
+
`<proof-span\\b[^>]*\\bid="${escapedId}"[^>]*>([\\s\\S]*?)<\\/proof-span>`,
|
|
27
|
+
"g",
|
|
28
|
+
);
|
|
29
|
+
return markdown.replace(re, "$1");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function POST(req: Request): Promise<NextResponse> {
|
|
33
|
+
const authResult = await checkAuth(req);
|
|
34
|
+
if (!authResult.ok) {
|
|
35
|
+
return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let body: { path?: unknown; spanId?: unknown; action?: unknown };
|
|
39
|
+
try {
|
|
40
|
+
body = (await req.json()) as { path?: unknown; spanId?: unknown; action?: unknown };
|
|
41
|
+
} catch {
|
|
42
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: "Invalid JSON" }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof body.path !== "string" || !body.path) {
|
|
46
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: "path (string) required" }, { status: 400 });
|
|
47
|
+
}
|
|
48
|
+
if (typeof body.spanId !== "string" || !body.spanId) {
|
|
49
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: "spanId (string) required" }, { status: 400 });
|
|
50
|
+
}
|
|
51
|
+
if (body.action !== "accept" && body.action !== "revert") {
|
|
52
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: "action must be 'accept' or 'revert'" }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rel = body.path as string;
|
|
56
|
+
const spanId = body.spanId as string;
|
|
57
|
+
const action = body.action as "accept" | "revert";
|
|
58
|
+
|
|
59
|
+
if (rel.startsWith(".proof")) {
|
|
60
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path must not be under .proof" }, { status: 400 });
|
|
61
|
+
}
|
|
62
|
+
if (!isMarkdown(rel)) {
|
|
63
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const absPath = safeRootPath(rel);
|
|
67
|
+
if (!absPath) {
|
|
68
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scopeCheck = enforceScope(authResult.agent, { filePath: rel, op: "mutate" });
|
|
72
|
+
if (!scopeCheck.ok) {
|
|
73
|
+
return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rootDir = getRootDir();
|
|
77
|
+
const actorId = authResult.agent.id;
|
|
78
|
+
|
|
79
|
+
let notFound = false;
|
|
80
|
+
try {
|
|
81
|
+
await withFileMutex(rel, async () => {
|
|
82
|
+
let content: string;
|
|
83
|
+
try {
|
|
84
|
+
content = await readFile(absPath, "utf-8");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
87
|
+
notFound = true;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newContent = action === "accept"
|
|
94
|
+
? acceptSpanById(content, spanId)
|
|
95
|
+
: revertProofSpan(content, spanId);
|
|
96
|
+
|
|
97
|
+
await writeFile(absPath, newContent, "utf-8");
|
|
98
|
+
|
|
99
|
+
const sidecar = (await readSidecar(rootDir, rel)) ?? emptySidecar(rel);
|
|
100
|
+
emitEvents(sidecar, [{
|
|
101
|
+
type: action === "accept" ? "span.accepted" : "span.reverted",
|
|
102
|
+
at: new Date().toISOString(),
|
|
103
|
+
by: actorId === "owner" ? "human" : actorId,
|
|
104
|
+
spanId,
|
|
105
|
+
}]);
|
|
106
|
+
await writeSidecar(rootDir, rel, sidecar);
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return NextResponse.json({ error: "INTERNAL_ERROR", message: String(err) }, { status: 500 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (notFound) {
|
|
113
|
+
return NextResponse.json({ error: "NOT_FOUND", message: "File not found" }, { status: 404 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return NextResponse.json({ ok: true });
|
|
117
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/agent/register/:regId
|
|
3
|
+
*
|
|
4
|
+
* Poll registration status. No auth required — regId acts as secret.
|
|
5
|
+
*
|
|
6
|
+
* Responses:
|
|
7
|
+
* 202 { status: "pending" }
|
|
8
|
+
* 200 { status: "approved", agentId, token } — one-shot pickup, deletes token
|
|
9
|
+
* 410 { status: "consumed" | "denied" }
|
|
10
|
+
* 404 { status: "not_found" }
|
|
11
|
+
*/
|
|
12
|
+
import { NextResponse } from "next/server";
|
|
13
|
+
import { getRegistration, consumeRegistration } from "@/lib/proof/pending";
|
|
14
|
+
|
|
15
|
+
export const runtime = "nodejs";
|
|
16
|
+
|
|
17
|
+
export async function GET(
|
|
18
|
+
_req: Request,
|
|
19
|
+
{ params }: { params: Promise<{ regId: string }> },
|
|
20
|
+
): Promise<NextResponse> {
|
|
21
|
+
const { regId } = await params;
|
|
22
|
+
const reg = getRegistration(regId);
|
|
23
|
+
|
|
24
|
+
if (!reg) {
|
|
25
|
+
return NextResponse.json({ status: "not_found" }, { status: 404 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
switch (reg.status) {
|
|
29
|
+
case "pending":
|
|
30
|
+
return NextResponse.json({ status: "pending" }, { status: 202 });
|
|
31
|
+
|
|
32
|
+
case "approved": {
|
|
33
|
+
const token = consumeRegistration(regId);
|
|
34
|
+
if (!token) {
|
|
35
|
+
// Already consumed between the get and consume (race) — treat as consumed
|
|
36
|
+
return NextResponse.json({ status: "consumed" }, { status: 410 });
|
|
37
|
+
}
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ status: "approved", agentId: reg.agentId, token },
|
|
40
|
+
{ status: 200 },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case "consumed":
|
|
45
|
+
return NextResponse.json({ status: "consumed" }, { status: 410 });
|
|
46
|
+
|
|
47
|
+
case "denied":
|
|
48
|
+
return NextResponse.json({ status: "denied" }, { status: 410 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/agent/register
|
|
3
|
+
*
|
|
4
|
+
* Anonymous registration request. No auth required.
|
|
5
|
+
* The registrationId returned acts as the agent's secret for polling.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
import { createRegistration } from "@/lib/proof/pending";
|
|
9
|
+
import type { AgentScope } from "@/lib/proof/registry";
|
|
10
|
+
import { checkRegisterRateLimit } from "@/lib/proof/register-rate-limit";
|
|
11
|
+
|
|
12
|
+
export const runtime = "nodejs";
|
|
13
|
+
|
|
14
|
+
const AGENT_ID_RE = /^ai:[a-z][a-z0-9-]{0,30}$/i;
|
|
15
|
+
const VALID_OPS = new Set(["read", "mutate"]);
|
|
16
|
+
|
|
17
|
+
function validateScope(s: Record<string, unknown>): AgentScope | { error: string } {
|
|
18
|
+
if (!Array.isArray(s.paths) || s.paths.length === 0 || s.paths.length > 20) {
|
|
19
|
+
return { error: "scope.paths must be an array of 1\u201320 glob pattern strings" };
|
|
20
|
+
}
|
|
21
|
+
for (const p of s.paths) {
|
|
22
|
+
if (typeof p !== "string" || p.length < 1 || p.length > 200) {
|
|
23
|
+
return { error: "Each scope.paths entry must be a string of 1\u2013200 characters" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(s.ops) || s.ops.length === 0) {
|
|
27
|
+
return { error: "scope.ops must be a non-empty array" };
|
|
28
|
+
}
|
|
29
|
+
for (const op of s.ops) {
|
|
30
|
+
if (!VALID_OPS.has(op as string)) {
|
|
31
|
+
return { error: `scope.ops values must be "read" or "mutate"` };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { paths: s.paths as string[], ops: s.ops as Array<"read" | "mutate"> };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Exported for reuse in approve route. */
|
|
38
|
+
export { validateScope };
|
|
39
|
+
|
|
40
|
+
export async function POST(req: Request): Promise<NextResponse> {
|
|
41
|
+
// Rate limit by remote IP (best-effort; fall back to global key)
|
|
42
|
+
const ip =
|
|
43
|
+
(req.headers.get("x-forwarded-for") ?? "").split(",")[0]?.trim() ||
|
|
44
|
+
"__global__";
|
|
45
|
+
if (!checkRegisterRateLimit(ip)) {
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: "RATE_LIMITED", message: "Too many registration attempts" },
|
|
48
|
+
{ status: 429 },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let body: { id?: unknown; displayName?: unknown; scope?: unknown };
|
|
53
|
+
try {
|
|
54
|
+
body = (await req.json()) as { id?: unknown; displayName?: unknown; scope?: unknown };
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: "Invalid JSON" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof body.id !== "string" || !AGENT_ID_RE.test(body.id)) {
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{
|
|
62
|
+
error: "INVALID_PAYLOAD",
|
|
63
|
+
message: "id must match /^ai:[a-z][a-z0-9-]{0,30}$/i (e.g. ai:claude)",
|
|
64
|
+
},
|
|
65
|
+
{ status: 400 },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// displayName: required, 1–80 chars
|
|
70
|
+
if (
|
|
71
|
+
typeof body.displayName !== "string" ||
|
|
72
|
+
body.displayName.trim().length < 1 ||
|
|
73
|
+
body.displayName.trim().length > 80
|
|
74
|
+
) {
|
|
75
|
+
return NextResponse.json(
|
|
76
|
+
{ error: "INVALID_PAYLOAD", message: "displayName must be a string of 1\u201380 characters" },
|
|
77
|
+
{ status: 400 },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const displayName = body.displayName.trim();
|
|
81
|
+
|
|
82
|
+
// scope: required, must pass validation
|
|
83
|
+
if (
|
|
84
|
+
body.scope === undefined ||
|
|
85
|
+
body.scope === null ||
|
|
86
|
+
typeof body.scope !== "object" ||
|
|
87
|
+
Array.isArray(body.scope)
|
|
88
|
+
) {
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{ error: "INVALID_PAYLOAD", message: "scope is required" },
|
|
91
|
+
{ status: 400 },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const scopeResult = validateScope(body.scope as Record<string, unknown>);
|
|
95
|
+
if ("error" in scopeResult) {
|
|
96
|
+
return NextResponse.json({ error: "INVALID_PAYLOAD", message: scopeResult.error }, { status: 400 });
|
|
97
|
+
}
|
|
98
|
+
const requestedScope: AgentScope = scopeResult;
|
|
99
|
+
|
|
100
|
+
const reg = createRegistration({ agentId: body.id, displayName, requestedScope });
|
|
101
|
+
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{
|
|
104
|
+
registrationId: reg.registrationId,
|
|
105
|
+
pollUrl: `/api/agent/register/${reg.registrationId}`,
|
|
106
|
+
status: "pending",
|
|
107
|
+
},
|
|
108
|
+
{ status: 202 },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { checkAuth, enforceScope } from "@/lib/proof/auth";
|
|
3
|
+
import { getRootDir } from "@/lib/root-dir";
|
|
4
|
+
import { readRegistry } from "@/lib/proof/registry";
|
|
5
|
+
import { listPendingRegistrations } from "@/lib/proof/pending";
|
|
6
|
+
|
|
7
|
+
export const runtime = "nodejs";
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request): Promise<NextResponse> {
|
|
10
|
+
const auth = await checkAuth(req);
|
|
11
|
+
if (!auth.ok) {
|
|
12
|
+
return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const scopeCheck = enforceScope(auth.agent, { op: "read" });
|
|
16
|
+
if (!scopeCheck.ok) {
|
|
17
|
+
return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const registry = await readRegistry();
|
|
21
|
+
const registeredAgents = registry?.agents.length ?? 0;
|
|
22
|
+
const pendingRegistrations = listPendingRegistrations().length;
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
rateLimit: Number(process.env.AGENT_RATE_LIMIT) || 60,
|
|
26
|
+
root: getRootDir(),
|
|
27
|
+
registeredAgents,
|
|
28
|
+
pendingRegistrations,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token regeneration — deprecated and removed.
|
|
3
|
+
*
|
|
4
|
+
* Auth is now managed via TOFU registration flow. Use the AI Panel or
|
|
5
|
+
* /api/agent/admin/* endpoints to manage agents.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
|
|
9
|
+
export const runtime = "nodejs";
|
|
10
|
+
|
|
11
|
+
export async function POST(_req: Request): Promise<NextResponse> {
|
|
12
|
+
return NextResponse.json(
|
|
13
|
+
{
|
|
14
|
+
error: "GONE",
|
|
15
|
+
message:
|
|
16
|
+
"Token regeneration is deprecated. Manage agents via /api/agent/admin/* or the AI Panel.",
|
|
17
|
+
},
|
|
18
|
+
{ status: 410 },
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { checkAuth, enforceScope } from "@/lib/proof/auth";
|
|
3
|
+
import { readSidecar, emptySidecar } from "@/lib/proof/sidecar";
|
|
4
|
+
import { getRootDir, safeRootPath } from "@/lib/root-dir";
|
|
5
|
+
|
|
6
|
+
export const runtime = "nodejs";
|
|
7
|
+
|
|
8
|
+
function isMarkdown(p: string): boolean {
|
|
9
|
+
return p.endsWith(".md") || p.endsWith(".markdown");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function mdPath(segments: string[]): string {
|
|
13
|
+
return segments.join("/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET(
|
|
17
|
+
req: Request,
|
|
18
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
19
|
+
): Promise<NextResponse> {
|
|
20
|
+
const auth = await checkAuth(req);
|
|
21
|
+
if (!auth.ok) {
|
|
22
|
+
return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { path: segments } = await params;
|
|
26
|
+
const rel = mdPath(segments);
|
|
27
|
+
|
|
28
|
+
if (rel.startsWith(".proof")) {
|
|
29
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path must not be under .proof" }, { status: 400 });
|
|
30
|
+
}
|
|
31
|
+
if (!isMarkdown(rel)) {
|
|
32
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path must be .md or .markdown" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const absPath = safeRootPath(rel);
|
|
36
|
+
if (!absPath) {
|
|
37
|
+
return NextResponse.json({ error: "INVALID_PATH", message: "Path traversal rejected" }, { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const scopeCheck = enforceScope(auth.agent, { filePath: rel, op: "read" });
|
|
41
|
+
if (!scopeCheck.ok) {
|
|
42
|
+
return NextResponse.json({ error: scopeCheck.code, message: scopeCheck.message }, { status: 403 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rootDir = getRootDir();
|
|
46
|
+
const sidecar = (await readSidecar(rootDir, rel)) ?? emptySidecar(rel);
|
|
47
|
+
|
|
48
|
+
return NextResponse.json(sidecar);
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const runtime = "nodejs";
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
async function readVersion(): Promise<string> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(path.resolve(process.cwd(), "package.json"), "utf-8");
|
|
10
|
+
const p = JSON.parse(raw) as { version?: string };
|
|
11
|
+
return p.version ?? "0.0.0";
|
|
12
|
+
} catch {
|
|
13
|
+
return "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deriveEndpoint(req: NextRequest): string {
|
|
18
|
+
const proto =
|
|
19
|
+
req.headers.get("x-forwarded-proto") ??
|
|
20
|
+
(req.url.startsWith("https") ? "https" : "http");
|
|
21
|
+
const host =
|
|
22
|
+
req.headers.get("x-forwarded-host") ??
|
|
23
|
+
req.headers.get("host") ??
|
|
24
|
+
new URL(req.url).host;
|
|
25
|
+
return `${proto}://${host}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function GET(req: NextRequest): Promise<NextResponse> {
|
|
29
|
+
const [bootstrapPrompt, version] = await Promise.all([
|
|
30
|
+
readFile(path.resolve(process.cwd(), "agents/bootstrap-prompt.md"), "utf-8").catch(() => ""),
|
|
31
|
+
readVersion(),
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const endpoint = deriveEndpoint(req);
|
|
35
|
+
const hostHeader = req.headers.get("host") ?? "";
|
|
36
|
+
const hostname = hostHeader.replace(/:[0-9]+$/, "").replace(/^\[(.*)\]$/, "$1").toLowerCase();
|
|
37
|
+
const loopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
38
|
+
const extraHosts = (process.env.WIKI_OWNER_HOSTS ?? "")
|
|
39
|
+
.split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
|
|
40
|
+
const ownerHostTrusted = loopback || extraHosts.includes(hostname);
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
name: "wiki-viewer",
|
|
44
|
+
version,
|
|
45
|
+
endpoint,
|
|
46
|
+
humanInstructions:
|
|
47
|
+
"Open the wiki-viewer AI Panel (Bot icon in header). Register the agent, then approve it in the Pending Registrations list. The agent receives a one-shot token.",
|
|
48
|
+
ownerBootstrap: {
|
|
49
|
+
hostnameSeen: hostname,
|
|
50
|
+
ownerHostTrusted,
|
|
51
|
+
hint: ownerHostTrusted
|
|
52
|
+
? "This host is trusted for owner cookie issuance. Open the AI Panel in a browser and approve pending registrations."
|
|
53
|
+
: `This host (${hostname}) is not in the owner-trust allowlist, so the AI Panel cannot bootstrap an owner cookie. Restart the server with WIKI_OWNER_HOSTS=${hostname} or access via localhost.`,
|
|
54
|
+
},
|
|
55
|
+
bootstrapPrompt,
|
|
56
|
+
skillTarball: "/api/agents/skill.tar.gz",
|
|
57
|
+
skillRaw: "/api/agents/skill",
|
|
58
|
+
skillCli: "npx skills add anh-chu/wiki-viewer/agents/wiki-viewer-skill",
|
|
59
|
+
routes: [
|
|
60
|
+
{ method: "POST", path: "/api/agent/register", auth: "none", purpose: "Register an agent" },
|
|
61
|
+
{ method: "GET", path: "/api/agent/register/<regId>", auth: "none", purpose: "Poll registration status" },
|
|
62
|
+
{ method: "GET", path: "/api/agent/files/<path>.md", auth: "bearer+agent-id", purpose: "Read snapshot" },
|
|
63
|
+
{ method: "POST", path: "/api/agent/files/<path>.md", auth: "bearer+agent-id", purpose: "Apply ops" },
|
|
64
|
+
{ method: "GET", path: "/api/agent/events/<path>.md", auth: "bearer+agent-id", purpose: "Poll events" },
|
|
65
|
+
{ method: "POST", path: "/api/agent/events/<path>.md", auth: "bearer+agent-id", purpose: "Ack events" },
|
|
66
|
+
],
|
|
67
|
+
ops: [
|
|
68
|
+
"block.replace",
|
|
69
|
+
"block.insertAfter",
|
|
70
|
+
"block.insertBefore",
|
|
71
|
+
"block.delete",
|
|
72
|
+
"block.append",
|
|
73
|
+
"block.prepend",
|
|
74
|
+
"comment.add",
|
|
75
|
+
"comment.reply",
|
|
76
|
+
"comment.resolve",
|
|
77
|
+
"comment.reopen",
|
|
78
|
+
"suggestion.add",
|
|
79
|
+
"suggestion.accept",
|
|
80
|
+
"suggestion.reject",
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const runtime = "nodejs";
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: NextRequest): Promise<NextResponse> {
|
|
8
|
+
const skillPath = path.resolve(
|
|
9
|
+
process.cwd(),
|
|
10
|
+
"agents/wiki-viewer-skill/SKILL.md"
|
|
11
|
+
);
|
|
12
|
+
const content = await readFile(skillPath, "utf-8");
|
|
13
|
+
return new NextResponse(content, {
|
|
14
|
+
status: 200,
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
17
|
+
"Cache-Control": "no-store",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export const runtime = "nodejs";
|
|
2
|
+
|
|
3
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { gzipSync } from "node:zlib";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
function encodeOctal(n: number, length: number): string {
|
|
9
|
+
return n.toString(8).padStart(length - 1, "0") + "\0";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function writePaddedString(buf: Buffer, offset: number, str: string, len: number): void {
|
|
13
|
+
const bytes = Buffer.from(str, "utf-8").subarray(0, len);
|
|
14
|
+
bytes.copy(buf, offset);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tarHeader(name: string, size: number, mtime: number): Buffer {
|
|
18
|
+
const header = Buffer.alloc(512, 0);
|
|
19
|
+
|
|
20
|
+
writePaddedString(header, 0, name, 100); // name
|
|
21
|
+
writePaddedString(header, 100, "0000644\0", 8); // mode
|
|
22
|
+
writePaddedString(header, 108, "0000000\0", 8); // uid
|
|
23
|
+
writePaddedString(header, 116, "0000000\0", 8); // gid
|
|
24
|
+
writePaddedString(header, 124, encodeOctal(size, 12), 12); // size
|
|
25
|
+
writePaddedString(header, 136, encodeOctal(mtime, 12), 12); // mtime
|
|
26
|
+
header[156] = 0x30; // typeflag '0' = regular
|
|
27
|
+
// linkname: 157-256, all zero
|
|
28
|
+
writePaddedString(header, 257, "ustar\0", 6); // magic
|
|
29
|
+
writePaddedString(header, 263, "00", 2); // version
|
|
30
|
+
// uname, gname, devmajor, devminor, prefix: zeros
|
|
31
|
+
|
|
32
|
+
// checksum
|
|
33
|
+
let sum = 0;
|
|
34
|
+
for (let i = 0; i < 512; i++) {
|
|
35
|
+
// treat checksum field (148..155) as spaces while computing
|
|
36
|
+
sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
37
|
+
}
|
|
38
|
+
writePaddedString(header, 148, encodeOctal(sum, 7) + " ", 8);
|
|
39
|
+
|
|
40
|
+
return header;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildTar(entries: Array<{ name: string; content: Buffer; mtime: number }>): Buffer {
|
|
44
|
+
const parts: Buffer[] = [];
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const hdr = tarHeader(entry.name, entry.content.length, entry.mtime);
|
|
48
|
+
parts.push(hdr);
|
|
49
|
+
parts.push(entry.content);
|
|
50
|
+
// pad content to 512-byte boundary
|
|
51
|
+
const rem = entry.content.length % 512;
|
|
52
|
+
if (rem !== 0) {
|
|
53
|
+
parts.push(Buffer.alloc(512 - rem, 0));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// two zero blocks = end-of-archive
|
|
58
|
+
parts.push(Buffer.alloc(1024, 0));
|
|
59
|
+
|
|
60
|
+
return Buffer.concat(parts);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function GET(_req: NextRequest): Promise<NextResponse> {
|
|
64
|
+
const skillDir = path.resolve(process.cwd(), "agents/wiki-viewer-skill");
|
|
65
|
+
const files = await readdir(skillDir, { withFileTypes: true });
|
|
66
|
+
const mtime = Math.floor(Date.now() / 1000);
|
|
67
|
+
|
|
68
|
+
const entries: Array<{ name: string; content: Buffer; mtime: number }> = [];
|
|
69
|
+
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
if (!f.isFile()) continue;
|
|
72
|
+
const content = await readFile(path.join(skillDir, f.name));
|
|
73
|
+
entries.push({ name: `wiki-viewer-skill/${f.name}`, content, mtime });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tar = buildTar(entries);
|
|
77
|
+
const gz = gzipSync(tar);
|
|
78
|
+
|
|
79
|
+
return new NextResponse(gz, {
|
|
80
|
+
status: 200,
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/gzip",
|
|
83
|
+
"Content-Disposition": `attachment; filename="wiki-viewer-skill.tar.gz"`,
|
|
84
|
+
"Cache-Control": "no-store",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/owner/init
|
|
3
|
+
*
|
|
4
|
+
* Legacy bootstrap endpoint. Now redirects to /signin (Better Auth).
|
|
5
|
+
* Kept for back-compat — any agent or UI calling this will be redirected.
|
|
6
|
+
*/
|
|
7
|
+
import { NextResponse } from "next/server";
|
|
8
|
+
|
|
9
|
+
export const runtime = "nodejs";
|
|
10
|
+
|
|
11
|
+
export function GET(req: Request): NextResponse {
|
|
12
|
+
const url = new URL("/signin", req.url);
|
|
13
|
+
return NextResponse.redirect(url, { status: 308 });
|
|
14
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/system/auth-settings — read the effective signup allowlist.
|
|
3
|
+
* PUT /api/system/auth-settings — update the allowlist in config.json.
|
|
4
|
+
*
|
|
5
|
+
* Single-tenant local app: any authenticated user may read/edit. The effective
|
|
6
|
+
* list resolves config first, env as fallback (see getAllowlist). `source`
|
|
7
|
+
* tells the UI where the active values came from so it can warn that editing
|
|
8
|
+
* here overrides the env vars.
|
|
9
|
+
*/
|
|
10
|
+
import { NextResponse } from "next/server";
|
|
11
|
+
import { checkOrigin } from "@/lib/auth/csrf";
|
|
12
|
+
import { requireUser } from "@/lib/auth/server";
|
|
13
|
+
import { getAllowlist } from "@/lib/auth/allowlist";
|
|
14
|
+
import { readConfig, writeConfig } from "@/lib/config";
|
|
15
|
+
|
|
16
|
+
export const runtime = "nodejs";
|
|
17
|
+
|
|
18
|
+
export async function GET(request: Request) {
|
|
19
|
+
const auth = await requireUser(request);
|
|
20
|
+
if (!auth.ok)
|
|
21
|
+
return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
22
|
+
|
|
23
|
+
const config = await readConfig();
|
|
24
|
+
const effective = await getAllowlist();
|
|
25
|
+
const usingConfig =
|
|
26
|
+
(config.allowedEmails?.length ?? 0) > 0 ||
|
|
27
|
+
(config.allowedDomains?.length ?? 0) > 0;
|
|
28
|
+
|
|
29
|
+
return NextResponse.json({
|
|
30
|
+
allowedEmails: effective.emails,
|
|
31
|
+
allowedDomains: effective.domains,
|
|
32
|
+
source: usingConfig ? "config" : "env",
|
|
33
|
+
envFallbackActive: !usingConfig,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeList(value: unknown): string[] {
|
|
38
|
+
if (!Array.isArray(value)) return [];
|
|
39
|
+
return value
|
|
40
|
+
.filter((v): v is string => typeof v === "string")
|
|
41
|
+
.map((s) => s.trim().toLowerCase())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function PUT(request: Request) {
|
|
46
|
+
const csrf = checkOrigin(request);
|
|
47
|
+
if (csrf) return csrf;
|
|
48
|
+
const auth = await requireUser(request);
|
|
49
|
+
if (!auth.ok)
|
|
50
|
+
return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
|
51
|
+
|
|
52
|
+
let body: { allowedEmails?: unknown; allowedDomains?: unknown };
|
|
53
|
+
try {
|
|
54
|
+
body = await request.json();
|
|
55
|
+
} catch {
|
|
56
|
+
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const allowedEmails = normalizeList(body.allowedEmails);
|
|
60
|
+
const allowedDomains = normalizeList(body.allowedDomains);
|
|
61
|
+
|
|
62
|
+
// Reject obviously malformed email entries (must contain a single @ with text
|
|
63
|
+
// on both sides). Domains are looser but must not contain @ or spaces.
|
|
64
|
+
const badEmail = allowedEmails.find((e) => !/^[^@\s]+@[^@\s]+$/.test(e));
|
|
65
|
+
if (badEmail)
|
|
66
|
+
return NextResponse.json(
|
|
67
|
+
{ error: `Invalid email entry: ${badEmail}` },
|
|
68
|
+
{ status: 400 },
|
|
69
|
+
);
|
|
70
|
+
const badDomain = allowedDomains.find((d) => /[@\s]/.test(d));
|
|
71
|
+
if (badDomain)
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: `Invalid domain entry: ${badDomain}` },
|
|
74
|
+
{ status: 400 },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await writeConfig({ allowedEmails, allowedDomains });
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({
|
|
80
|
+
ok: true,
|
|
81
|
+
allowedEmails,
|
|
82
|
+
allowedDomains,
|
|
83
|
+
source: allowedEmails.length || allowedDomains.length ? "config" : "env",
|
|
84
|
+
});
|
|
85
|
+
}
|