wiki-viewer 1.4.3 → 1.5.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/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/routes-manifest.json +34 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +8 -7
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js +2 -2
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js +3 -3
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
- package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0y9k6h~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0znncy9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_12-ni1n._.js +1 -1
- package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 14luv~jq7e6-n.js} +12 -12
- package/.next/standalone/README.md +79 -16
- package/.next/standalone/agents/bootstrap-prompt.md +12 -1
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
- package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
- package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
- package/.next/standalone/pnpm-lock.yaml +9 -9
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
- package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
- package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
- package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
- package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
- package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
- package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
- package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +49 -0
- package/.next/standalone/src/components/editor/editor.tsx +38 -0
- package/.next/standalone/src/lib/proof/audit.ts +79 -0
- package/.next/standalone/src/lib/proof/auth.ts +2 -2
- package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
- package/.next/standalone/src/lib/proof/lease.ts +104 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
- package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
- package/.next/standalone/src/lib/proof/registry.ts +1 -1
- package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
- package/.next/standalone/src/lib/proof/types.ts +13 -1
- package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
- package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
- package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
- package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/README.md +79 -16
- package/package.json +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 19j4-_brJLoulXAMekpM2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOFU registration flow for wiki-viewer.
|
|
3
|
+
*
|
|
4
|
+
* Pure function `register()` is testable with a mock fetch.
|
|
5
|
+
* The CLI wrapper lives in index.ts.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* POST /api/agent/register → {registrationId, pollUrl, status:"pending"}
|
|
9
|
+
* human approves in AI Panel
|
|
10
|
+
* GET <pollUrl> → 202 pending | 200 approved | 410 denied | 404 expired
|
|
11
|
+
* returns {token, agentId}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface RegisterScope {
|
|
15
|
+
paths: string[];
|
|
16
|
+
ops: Array<"read" | "mutate" | "delete">;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RegisterOptions {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
id: string; // must match ^ai:[a-z][a-z0-9-]{0,30}$
|
|
22
|
+
displayName: string;
|
|
23
|
+
scope: RegisterScope;
|
|
24
|
+
/** Override fetch for testing */
|
|
25
|
+
fetch?: typeof globalThis.fetch;
|
|
26
|
+
/** How long to wait between polls (ms, default 3000) */
|
|
27
|
+
pollIntervalMs?: number;
|
|
28
|
+
/** Total time to wait before giving up (ms, default 300_000) */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** Called each time we get a "pending" poll response */
|
|
31
|
+
onPending?: (registrationId: string, attempt: number) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RegisterResult {
|
|
35
|
+
token: string;
|
|
36
|
+
agentId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function register(opts: RegisterOptions): Promise<RegisterResult> {
|
|
40
|
+
const _fetch = opts.fetch ?? globalThis.fetch;
|
|
41
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 3_000;
|
|
42
|
+
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
43
|
+
const base = opts.baseUrl.replace(/\/$/, "");
|
|
44
|
+
|
|
45
|
+
// ── Step 1: POST registration ──────────────────────────────────────────────
|
|
46
|
+
const postRes = await _fetch(`${base}/api/agent/register`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
id: opts.id,
|
|
51
|
+
displayName: opts.displayName,
|
|
52
|
+
scope: opts.scope,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!postRes.ok) {
|
|
57
|
+
const body = await postRes.text().catch(() => "");
|
|
58
|
+
throw new Error(`Registration request failed (${postRes.status}): ${body}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { registrationId, pollUrl } = await postRes.json() as {
|
|
62
|
+
registrationId: string;
|
|
63
|
+
pollUrl: string;
|
|
64
|
+
status: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Resolve pollUrl — may be absolute or relative
|
|
68
|
+
const resolvedPollUrl = pollUrl.startsWith("http")
|
|
69
|
+
? pollUrl
|
|
70
|
+
: `${base}${pollUrl.startsWith("/") ? "" : "/"}${pollUrl}`;
|
|
71
|
+
|
|
72
|
+
// ── Step 2: Poll until approved / denied / timeout ─────────────────────────
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
let attempt = 0;
|
|
75
|
+
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
await sleep(pollIntervalMs);
|
|
78
|
+
attempt++;
|
|
79
|
+
|
|
80
|
+
const pollRes = await _fetch(resolvedPollUrl);
|
|
81
|
+
|
|
82
|
+
if (pollRes.status === 200) {
|
|
83
|
+
const body = await pollRes.json() as {
|
|
84
|
+
status: string;
|
|
85
|
+
agentId?: string;
|
|
86
|
+
token?: string;
|
|
87
|
+
};
|
|
88
|
+
if (body.status === "approved" && body.agentId && body.token) {
|
|
89
|
+
return { token: body.token, agentId: body.agentId };
|
|
90
|
+
}
|
|
91
|
+
if (body.status === "denied") {
|
|
92
|
+
throw new RegistrationDeniedError("Registration denied by the operator.");
|
|
93
|
+
}
|
|
94
|
+
} else if (pollRes.status === 202) {
|
|
95
|
+
// Still pending — fire callback and loop
|
|
96
|
+
opts.onPending?.(registrationId, attempt);
|
|
97
|
+
} else if (pollRes.status === 410) {
|
|
98
|
+
const body = await pollRes.json().catch(() => ({}) as Record<string, unknown>) as { status?: string };
|
|
99
|
+
throw new RegistrationDeniedError(
|
|
100
|
+
`Registration ${body.status ?? "denied/consumed"} — it can no longer be approved.`,
|
|
101
|
+
);
|
|
102
|
+
} else if (pollRes.status === 404) {
|
|
103
|
+
throw new RegistrationExpiredError(
|
|
104
|
+
"Registration expired (404). Try registering again.",
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(`Unexpected poll status ${pollRes.status}.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new RegistrationTimeoutError(
|
|
112
|
+
`Timed out after ${Math.round(timeoutMs / 1000)}s. ` +
|
|
113
|
+
`The request may still be pending in the wiki-viewer AI Panel (id: ${registrationId}).`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Typed errors ──────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export class RegistrationDeniedError extends Error {
|
|
120
|
+
constructor(message: string) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = "RegistrationDeniedError";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class RegistrationExpiredError extends Error {
|
|
127
|
+
constructor(message: string) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "RegistrationExpiredError";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class RegistrationTimeoutError extends Error {
|
|
134
|
+
constructor(message: string) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = "RegistrationTimeoutError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function sleep(ms: number): Promise<void> {
|
|
143
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-path cache for last-known file metadata from GET responses.
|
|
3
|
+
* Drives If-Match and collab-state checks on subsequent writes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CollabState = "active" | "tracked" | "untracked" | "not-markdown";
|
|
7
|
+
|
|
8
|
+
export interface PathState {
|
|
9
|
+
/** sha256 from ETag header (without quotes) */
|
|
10
|
+
sha256: string;
|
|
11
|
+
collabState: CollabState;
|
|
12
|
+
/** X-Collab-Revision value, null for non-markdown */
|
|
13
|
+
collabRevision: number | null;
|
|
14
|
+
/** X-Collab-Snapshot URL, null for non-markdown */
|
|
15
|
+
collabSnapshot: string | null;
|
|
16
|
+
/** When this cache entry was last updated (ms) */
|
|
17
|
+
fetchedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cache = new Map<string, PathState>();
|
|
21
|
+
|
|
22
|
+
export function set(path: string, state: PathState): void {
|
|
23
|
+
cache.set(normalisePath(path), state);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function get(path: string): PathState | undefined {
|
|
27
|
+
return cache.get(normalisePath(path));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function del(path: string): void {
|
|
31
|
+
cache.delete(normalisePath(path));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function rename(from: string, to: string): void {
|
|
35
|
+
const s = cache.get(normalisePath(from));
|
|
36
|
+
if (s) {
|
|
37
|
+
cache.delete(normalisePath(from));
|
|
38
|
+
cache.set(normalisePath(to), s);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Normalise leading slash away so 'foo/bar' and '/foo/bar' are the same key */
|
|
43
|
+
function normalisePath(p: string): string {
|
|
44
|
+
return p.replace(/^\/+/, "");
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"],
|
|
18
|
+
"exclude": ["src/__tests__", "dist", "node_modules"]
|
|
19
|
+
}
|
|
@@ -23,9 +23,18 @@ importers:
|
|
|
23
23
|
kysely:
|
|
24
24
|
specifier: 0.28.5
|
|
25
25
|
version: 0.28.5
|
|
26
|
+
next:
|
|
27
|
+
specifier: 16.2.4
|
|
28
|
+
version: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
|
26
29
|
proper-lockfile:
|
|
27
30
|
specifier: ^4.1.2
|
|
28
31
|
version: 4.1.2
|
|
32
|
+
react:
|
|
33
|
+
specifier: 19.2.5
|
|
34
|
+
version: 19.2.5
|
|
35
|
+
react-dom:
|
|
36
|
+
specifier: 19.2.5
|
|
37
|
+
version: 19.2.5(react@19.2.5)
|
|
29
38
|
rehype-parse:
|
|
30
39
|
specifier: ^9.0.1
|
|
31
40
|
version: 9.0.1
|
|
@@ -210,9 +219,6 @@ importers:
|
|
|
210
219
|
mermaid:
|
|
211
220
|
specifier: ^11.14.0
|
|
212
221
|
version: 11.15.0
|
|
213
|
-
next:
|
|
214
|
-
specifier: 16.2.4
|
|
215
|
-
version: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
|
216
222
|
next-themes:
|
|
217
223
|
specifier: ^0.4.6
|
|
218
224
|
version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
|
@@ -225,12 +231,6 @@ importers:
|
|
|
225
231
|
pptx-preview:
|
|
226
232
|
specifier: ^1.0.7
|
|
227
233
|
version: 1.0.7
|
|
228
|
-
react:
|
|
229
|
-
specifier: 19.2.5
|
|
230
|
-
version: 19.2.5
|
|
231
|
-
react-dom:
|
|
232
|
-
specifier: 19.2.5
|
|
233
|
-
version: 19.2.5(react@19.2.5)
|
|
234
234
|
react-markdown:
|
|
235
235
|
specifier: ^10.1.0
|
|
236
236
|
version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
|
|
@@ -21,6 +21,7 @@ import { idempotency } from "@/lib/proof/idempotency";
|
|
|
21
21
|
import { getRootDir, safeRootPath } from "@/lib/root-dir";
|
|
22
22
|
import type { Op } from "@/lib/proof/types";
|
|
23
23
|
import { checkAndConsume } from "@/lib/proof/rate-limit";
|
|
24
|
+
import { computeCollabState } from "@/lib/proof/collab-state";
|
|
24
25
|
|
|
25
26
|
export const runtime = "nodejs";
|
|
26
27
|
|
|
@@ -67,7 +68,16 @@ export async function GET(
|
|
|
67
68
|
return NextResponse.json({ error: "NOT_FOUND", message: "File not found" }, { status: 404 });
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
const collab = await computeCollabState(rootDir, rel);
|
|
72
|
+
const collabHeaders: Record<string, string> = {
|
|
73
|
+
"X-Collab-State": collab.state,
|
|
74
|
+
"X-Collab-Revision": String(collab.revision),
|
|
75
|
+
};
|
|
76
|
+
if (collab.snapshotUrl) {
|
|
77
|
+
collabHeaders["X-Collab-Snapshot"] = collab.snapshotUrl;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return NextResponse.json(snapshot, { headers: collabHeaders });
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
export async function POST(
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-1 Raw FS — file read/write/delete.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/agent/fs/file/<path> Read raw bytes (Range supported).
|
|
5
|
+
* PUT /api/agent/fs/file/<path> Atomic whole-file write.
|
|
6
|
+
* DELETE /api/agent/fs/file/<path> Delete file (+ sidecar for .md).
|
|
7
|
+
*/
|
|
8
|
+
export const runtime = "nodejs";
|
|
9
|
+
|
|
10
|
+
import { readFile, stat, unlink, rm } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { NextResponse } from "next/server";
|
|
13
|
+
import { checkAuth, enforceScope } from "@/lib/proof/auth";
|
|
14
|
+
import { getRootDir, safeRootPath } from "@/lib/root-dir";
|
|
15
|
+
import { withFileMutex } from "@/lib/proof/mutex";
|
|
16
|
+
import { readSidecar, emptySidecar, deleteSidecar } from "@/lib/proof/sidecar";
|
|
17
|
+
import { reconcileSidecar } from "@/lib/proof/ops-applier";
|
|
18
|
+
import { writeAuditRow } from "@/lib/proof/audit";
|
|
19
|
+
import {
|
|
20
|
+
safeAbsPath,
|
|
21
|
+
sha256ofBuf,
|
|
22
|
+
extractShaHex,
|
|
23
|
+
mimeByExt,
|
|
24
|
+
isMarkdown,
|
|
25
|
+
atomicWrite,
|
|
26
|
+
ensureParentDir,
|
|
27
|
+
} from "@/lib/proof/raw-fs";
|
|
28
|
+
import { computeCollabState } from "@/lib/proof/collab-state";
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function rel(segments: string[]): string {
|
|
33
|
+
return segments.join("/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function errJson(code: string, message: string, status: number): NextResponse {
|
|
37
|
+
return NextResponse.json({ error: code, message }, { status });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── GET ──────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export async function GET(
|
|
43
|
+
req: Request,
|
|
44
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
45
|
+
): Promise<NextResponse> {
|
|
46
|
+
const auth = await checkAuth(req);
|
|
47
|
+
if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
|
|
48
|
+
|
|
49
|
+
const { path: segments } = await params;
|
|
50
|
+
const relPath = rel(segments);
|
|
51
|
+
|
|
52
|
+
const basic = safeRootPath(relPath);
|
|
53
|
+
if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
|
|
54
|
+
|
|
55
|
+
const absPath = await safeAbsPath(relPath);
|
|
56
|
+
if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
|
|
57
|
+
|
|
58
|
+
const scope = enforceScope(auth.agent, { filePath: relPath, op: "read" });
|
|
59
|
+
if (!scope.ok) return errJson(scope.code, scope.message, 403);
|
|
60
|
+
|
|
61
|
+
let data: Buffer;
|
|
62
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
63
|
+
try {
|
|
64
|
+
[data, fileStat] = await Promise.all([readFile(absPath), stat(absPath)]);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
|
|
67
|
+
return errJson("NOT_FOUND", "File not found", 404);
|
|
68
|
+
}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (fileStat.isDirectory()) {
|
|
73
|
+
return errJson("IS_DIRECTORY", "Path is a directory; use /api/agent/fs/ls/", 400);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sha = sha256ofBuf(data);
|
|
77
|
+
const baseHeaders: Record<string, string> = {
|
|
78
|
+
"ETag": `"${sha}"`,
|
|
79
|
+
"X-File-Size": String(fileStat.size),
|
|
80
|
+
"X-File-Mtime": fileStat.mtime.toISOString(),
|
|
81
|
+
"Content-Type": mimeByExt(relPath),
|
|
82
|
+
"Accept-Ranges": "bytes",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// X-Collab-* headers (§3.5 mode signal)
|
|
86
|
+
const rootDir = getRootDir();
|
|
87
|
+
const collab = await computeCollabState(rootDir, relPath);
|
|
88
|
+
if (collab.state !== "not-markdown") {
|
|
89
|
+
baseHeaders["X-Collab-State"] = collab.state;
|
|
90
|
+
baseHeaders["X-Collab-Revision"] = String(collab.revision);
|
|
91
|
+
baseHeaders["X-Collab-Snapshot"] = collab.snapshotUrl!;
|
|
92
|
+
} else {
|
|
93
|
+
baseHeaders["X-Collab-State"] = collab.state;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rangeHeader = req.headers.get("range");
|
|
97
|
+
if (rangeHeader) {
|
|
98
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
|
|
99
|
+
if (!match) {
|
|
100
|
+
return new NextResponse("Invalid Range header", { status: 416 });
|
|
101
|
+
}
|
|
102
|
+
const start = parseInt(match[1]!, 10);
|
|
103
|
+
const endRaw = match[2] ? parseInt(match[2], 10) : data.length - 1;
|
|
104
|
+
if (start > endRaw || start >= data.length) {
|
|
105
|
+
return new NextResponse(null, {
|
|
106
|
+
status: 416,
|
|
107
|
+
headers: { "Content-Range": `bytes */${data.length}` },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const end = Math.min(endRaw, data.length - 1);
|
|
111
|
+
const slice = data.slice(start, end + 1);
|
|
112
|
+
return new NextResponse(slice as unknown as BodyInit, {
|
|
113
|
+
status: 206,
|
|
114
|
+
headers: {
|
|
115
|
+
...baseHeaders,
|
|
116
|
+
"Content-Range": `bytes ${start}-${end}/${data.length}`,
|
|
117
|
+
"Content-Length": String(slice.length),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new NextResponse(data as unknown as BodyInit, {
|
|
123
|
+
status: 200,
|
|
124
|
+
headers: { ...baseHeaders, "Content-Length": String(data.length) },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── PUT ──────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export async function PUT(
|
|
131
|
+
req: Request,
|
|
132
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
133
|
+
): Promise<NextResponse> {
|
|
134
|
+
const auth = await checkAuth(req);
|
|
135
|
+
if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
|
|
136
|
+
|
|
137
|
+
const { path: segments } = await params;
|
|
138
|
+
const relPath = rel(segments);
|
|
139
|
+
const url = new URL(req.url);
|
|
140
|
+
const mkdirs = url.searchParams.get("mkdirs") === "true";
|
|
141
|
+
const force = url.searchParams.get("force") === "true";
|
|
142
|
+
|
|
143
|
+
const basic = safeRootPath(relPath);
|
|
144
|
+
if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
|
|
145
|
+
|
|
146
|
+
const absPath = await safeAbsPath(relPath);
|
|
147
|
+
if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
|
|
148
|
+
|
|
149
|
+
const scope = enforceScope(auth.agent, { filePath: relPath, op: "mutate" });
|
|
150
|
+
if (!scope.ok) return errJson(scope.code, scope.message, 403);
|
|
151
|
+
|
|
152
|
+
const ifMatch = req.headers.get("if-match");
|
|
153
|
+
const bodyBuf = Buffer.from(await req.arrayBuffer());
|
|
154
|
+
const newSha = sha256ofBuf(bodyBuf);
|
|
155
|
+
|
|
156
|
+
// Check existing file
|
|
157
|
+
let existingSha: string | undefined;
|
|
158
|
+
let existed = false;
|
|
159
|
+
try {
|
|
160
|
+
const existing = await readFile(absPath);
|
|
161
|
+
existingSha = sha256ofBuf(existing);
|
|
162
|
+
existed = true;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (existed) {
|
|
168
|
+
// Overwrite: If-Match required by default (unless ?force=true)
|
|
169
|
+
if (!force && !ifMatch) {
|
|
170
|
+
return errJson(
|
|
171
|
+
"PRECONDITION_REQUIRED",
|
|
172
|
+
"If-Match header required for overwrites (use ?force=true to bypass with audit)",
|
|
173
|
+
412,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (ifMatch) {
|
|
177
|
+
const provided = extractShaHex(ifMatch);
|
|
178
|
+
const current = extractShaHex(existingSha!);
|
|
179
|
+
if (provided !== current) {
|
|
180
|
+
return errJson("PRECONDITION_FAILED", "If-Match sha256 mismatch", 412);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Create: ensure parent dir exists
|
|
185
|
+
const parentOk = await ensureParentDir(absPath, mkdirs);
|
|
186
|
+
if (!parentOk) {
|
|
187
|
+
return errJson(
|
|
188
|
+
"PARENT_NOT_FOUND",
|
|
189
|
+
"Parent directory does not exist (use ?mkdirs=true to create)",
|
|
190
|
+
400,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const rootDir = getRootDir();
|
|
196
|
+
// R6: for .md, re-check collab state atomically inside write mutex
|
|
197
|
+
const ifCollabMatch = req.headers.get("if-collab-match");
|
|
198
|
+
|
|
199
|
+
if (isMarkdown(relPath)) {
|
|
200
|
+
// R1: acquire shared mutex; R2: reconcile eagerly inside it
|
|
201
|
+
return await withFileMutex(relPath, async () => {
|
|
202
|
+
// R6: atomic collab-state check (closes TOCTOU race)
|
|
203
|
+
const { state, revision, snapshotUrl } = await computeCollabState(rootDir, relPath);
|
|
204
|
+
if (state === "active") {
|
|
205
|
+
const matchVal = ifCollabMatch?.trim();
|
|
206
|
+
if (!force && (!matchVal || matchVal !== String(revision))) {
|
|
207
|
+
return NextResponse.json(
|
|
208
|
+
{
|
|
209
|
+
error: "COLLAB_ACTIVE",
|
|
210
|
+
message:
|
|
211
|
+
"File is in active collaborative session. Use block-ops (Tier-2) or supply a matching If-Collab-Match header. Use ?force=true to override (audited).",
|
|
212
|
+
snapshotUrl,
|
|
213
|
+
revision,
|
|
214
|
+
},
|
|
215
|
+
{ status: 409 },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await atomicWrite(absPath, bodyBuf);
|
|
221
|
+
|
|
222
|
+
const content = bodyBuf.toString("utf-8");
|
|
223
|
+
const sidecar = (await readSidecar(rootDir, relPath)) ?? emptySidecar(relPath);
|
|
224
|
+
await reconcileSidecar({
|
|
225
|
+
rootDir,
|
|
226
|
+
mdPath: relPath,
|
|
227
|
+
content,
|
|
228
|
+
sidecar,
|
|
229
|
+
by: auth.agent.id,
|
|
230
|
+
eventType: "file.rawWritten",
|
|
231
|
+
fingerprint: newSha,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
writeAuditRow({
|
|
235
|
+
agentId: auth.agent.id,
|
|
236
|
+
op: "put",
|
|
237
|
+
path: relPath,
|
|
238
|
+
oldSha: existingSha,
|
|
239
|
+
newSha,
|
|
240
|
+
forced: force,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const st = await stat(absPath);
|
|
244
|
+
return NextResponse.json({
|
|
245
|
+
path: relPath,
|
|
246
|
+
sha256: newSha,
|
|
247
|
+
size: st.size,
|
|
248
|
+
mtime: st.mtime.toISOString(),
|
|
249
|
+
created: !existed,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Non-.md: atomic write + audit (no mutex, no sidecar)
|
|
255
|
+
await atomicWrite(absPath, bodyBuf);
|
|
256
|
+
writeAuditRow({
|
|
257
|
+
agentId: auth.agent.id,
|
|
258
|
+
op: "put",
|
|
259
|
+
path: relPath,
|
|
260
|
+
oldSha: existingSha,
|
|
261
|
+
newSha,
|
|
262
|
+
forced: force,
|
|
263
|
+
});
|
|
264
|
+
const st = await stat(absPath);
|
|
265
|
+
return NextResponse.json({
|
|
266
|
+
path: relPath,
|
|
267
|
+
sha256: newSha,
|
|
268
|
+
size: st.size,
|
|
269
|
+
mtime: st.mtime.toISOString(),
|
|
270
|
+
created: !existed,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── DELETE ───────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
export async function DELETE(
|
|
277
|
+
req: Request,
|
|
278
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
279
|
+
): Promise<NextResponse> {
|
|
280
|
+
const auth = await checkAuth(req);
|
|
281
|
+
if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
|
|
282
|
+
|
|
283
|
+
const { path: segments } = await params;
|
|
284
|
+
const relPath = rel(segments);
|
|
285
|
+
const url = new URL(req.url);
|
|
286
|
+
const recursive = url.searchParams.get("recursive") === "true";
|
|
287
|
+
|
|
288
|
+
const basic = safeRootPath(relPath);
|
|
289
|
+
if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
|
|
290
|
+
|
|
291
|
+
const absPath = await safeAbsPath(relPath);
|
|
292
|
+
if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
|
|
293
|
+
|
|
294
|
+
// Requires "delete" scope op
|
|
295
|
+
const scope = enforceScope(auth.agent, { filePath: relPath, op: "delete" });
|
|
296
|
+
if (!scope.ok) return errJson(scope.code, scope.message, 403);
|
|
297
|
+
|
|
298
|
+
const ifMatch = req.headers.get("if-match");
|
|
299
|
+
|
|
300
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
301
|
+
try {
|
|
302
|
+
fileStat = await stat(absPath);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") {
|
|
305
|
+
return errJson("NOT_FOUND", "File not found", 404);
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (fileStat.isDirectory()) {
|
|
311
|
+
if (!recursive) {
|
|
312
|
+
return errJson("IS_DIRECTORY", "Use ?recursive=true to delete a directory", 400);
|
|
313
|
+
}
|
|
314
|
+
// Directory delete — no single sha, skip If-Match; require recursive flag as confirmation
|
|
315
|
+
await rm(absPath, { recursive: true, force: true });
|
|
316
|
+
writeAuditRow({ agentId: auth.agent.id, op: "delete", path: relPath, forced: false });
|
|
317
|
+
return NextResponse.json({ deleted: relPath });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// File: If-Match required as confirmation
|
|
321
|
+
if (!ifMatch) {
|
|
322
|
+
return errJson("PRECONDITION_REQUIRED", "If-Match header required for delete", 412);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existing = await readFile(absPath);
|
|
326
|
+
const existingSha = sha256ofBuf(existing);
|
|
327
|
+
if (extractShaHex(ifMatch) !== extractShaHex(existingSha)) {
|
|
328
|
+
return errJson("PRECONDITION_FAILED", "If-Match sha256 mismatch", 412);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const rootDir = getRootDir();
|
|
332
|
+
|
|
333
|
+
if (isMarkdown(relPath)) {
|
|
334
|
+
await withFileMutex(relPath, async () => {
|
|
335
|
+
await unlink(absPath);
|
|
336
|
+
await deleteSidecar(rootDir, relPath);
|
|
337
|
+
});
|
|
338
|
+
} else {
|
|
339
|
+
await unlink(absPath);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
writeAuditRow({ agentId: auth.agent.id, op: "delete", path: relPath, oldSha: existingSha, forced: false });
|
|
343
|
+
return NextResponse.json({ deleted: relPath });
|
|
344
|
+
}
|