visualproof 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +99 -2
- package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
- package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
- package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
- package/dist/assets/latex/visualproof.sty +308 -0
- package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
- package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
- package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/dist/extractor/Extract.lean +1180 -0
- package/dist/extractor/Usage.lean +900 -0
- package/dist/mcp/visualproof-mcp.mjs +476 -0
- package/dist/server/access.js +95 -0
- package/dist/server/agent.js +3270 -0
- package/dist/server/agentdocs.js +35 -0
- package/dist/server/agentlibrary.js +202 -0
- package/dist/server/agentnotes.js +701 -0
- package/dist/server/agentviews.js +703 -0
- package/dist/server/askfollow.js +286 -0
- package/dist/server/boardspots.js +159 -0
- package/dist/server/dirs.js +175 -0
- package/dist/server/doctor.js +186 -0
- package/dist/server/events.js +198 -0
- package/dist/server/export.js +420 -0
- package/dist/server/extract.js +204 -0
- package/dist/server/fetchurl.js +321 -0
- package/dist/server/figures.js +158 -0
- package/dist/server/history.js +780 -0
- package/dist/server/historyapi.js +304 -0
- package/dist/server/hover.js +595 -0
- package/dist/server/identity.js +114 -0
- package/dist/server/invites.js +241 -0
- package/dist/server/layoutthread.js +51 -0
- package/dist/server/library.js +600 -0
- package/dist/server/libraryroute.js +211 -0
- package/dist/server/live.js +1101 -0
- package/dist/server/lostfound.js +199 -0
- package/dist/server/main.js +1406 -0
- package/dist/server/mcptools.js +496 -0
- package/dist/server/pastdoc.js +73 -0
- package/dist/server/pipeline.js +159 -0
- package/dist/server/poppler.js +251 -0
- package/dist/server/presence.js +252 -0
- package/dist/server/project.js +222 -0
- package/dist/server/prompt.js +53 -0
- package/dist/server/readonly.js +152 -0
- package/dist/server/readonlygate.js +123 -0
- package/dist/server/renames.js +268 -0
- package/dist/server/review.js +334 -0
- package/dist/server/reviewapi.js +188 -0
- package/dist/server/snapshot.js +50 -0
- package/dist/server/sourcestore.js +473 -0
- package/dist/server/status.js +360 -0
- package/dist/server/talk.js +476 -0
- package/dist/server/tokens.js +221 -0
- package/dist/server/usage.js +501 -0
- package/dist/server/versions.js +82 -0
- package/dist/server/view.js +164 -0
- package/dist/server/views.js +1167 -0
- package/dist/shared/askfollow.js +46 -0
- package/dist/shared/board.js +332 -0
- package/dist/shared/build.js +2 -0
- package/dist/shared/collab.js +241 -0
- package/dist/shared/comment.js +285 -0
- package/dist/shared/context.js +157 -0
- package/dist/shared/doc/authors.js +98 -0
- package/dist/shared/doc/build.js +358 -0
- package/dist/shared/doc/latex.js +455 -0
- package/dist/shared/doc/markdown.js +159 -0
- package/dist/shared/doc/model.js +37 -0
- package/dist/shared/doc/order.js +124 -0
- package/dist/shared/doc/outline.js +45 -0
- package/dist/shared/doc/proof.js +272 -0
- package/dist/shared/doc/sections.js +93 -0
- package/dist/shared/doc/settings.js +75 -0
- package/dist/shared/edgecodec.js +102 -0
- package/dist/shared/editor.js +64 -0
- package/dist/shared/excerpt.js +87 -0
- package/dist/shared/graph.js +16 -0
- package/dist/shared/items.js +92 -0
- package/dist/shared/keys.js +203 -0
- package/dist/shared/layout.js +926 -0
- package/dist/shared/lexer.js +130 -0
- package/dist/shared/library.js +711 -0
- package/dist/shared/lostfound.js +150 -0
- package/dist/shared/moves.js +244 -0
- package/dist/shared/notecard.js +295 -0
- package/dist/shared/noteplace.js +505 -0
- package/dist/shared/palette.js +46 -0
- package/dist/shared/pastgraph.js +103 -0
- package/dist/shared/places.js +80 -0
- package/dist/shared/projectnotes.js +244 -0
- package/dist/shared/query.js +818 -0
- package/dist/shared/review.js +642 -0
- package/dist/shared/reviewqueue.js +333 -0
- package/dist/shared/ring.js +366 -0
- package/dist/shared/scope.js +255 -0
- package/dist/shared/scopeedit.js +81 -0
- package/dist/shared/search.js +191 -0
- package/dist/shared/separate.js +178 -0
- package/dist/shared/sharesnap.js +281 -0
- package/dist/shared/sticky.js +501 -0
- package/dist/shared/timeline.js +73 -0
- package/dist/shared/titles.js +300 -0
- package/dist/shared/viewgraph.js +157 -0
- package/dist/shared/views.js +859 -0
- package/dist/web/bundle/app.js +52671 -0
- package/dist/web/bundle/edges.js +407 -0
- package/dist/web/bundle/layout.js +1010 -0
- package/dist/web/bundle/pdfworker.js +48322 -0
- package/dist/web/index.html +257 -0
- package/dist/web/src/motion.js +430 -0
- package/dist/web/src/past.js +224 -0
- package/dist/web/src/scene.js +804 -0
- package/dist/web/style.css +2548 -0
- package/docs/agent-api/README.md +91 -0
- package/docs/agent-api/advice.md +108 -0
- package/docs/agent-api/export.md +120 -0
- package/docs/agent-api/following.md +140 -0
- package/docs/agent-api/history.md +142 -0
- package/docs/agent-api/library.md +65 -0
- package/docs/agent-api/mcp.md +104 -0
- package/docs/agent-api/notes.md +93 -0
- package/docs/agent-api/reading.md +166 -0
- package/docs/agent-api/requests.md +94 -0
- package/docs/agent-api/talking.md +100 -0
- package/docs/agent-api/tcb.md +135 -0
- package/docs/agent-api/views.md +125 -0
- package/docs/agent-api/words.md +56 -0
- package/package.json +39 -4
- package/test/toolchains.json +5 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// The library's routes for pages (AttachmentSpec decisions 1–11): `/api/library/…`, behind the same
|
|
2
|
+
// access check as the page (main.ts lets a request get here only once it is in). What they do is
|
|
3
|
+
// server/library.ts's, which the agents' endpoints call too (server/agentlibrary.ts); here are only
|
|
4
|
+
// what HTTP needs: who asks, the body — a file's bytes as they are, sent with the file's name in the
|
|
5
|
+
// query, or JSON — and answers that are bytes (a file, a page, a clip), sent so that a browser never
|
|
6
|
+
// takes them for anything but what they are.
|
|
7
|
+
//
|
|
8
|
+
// GET /api/library the library: its files, the archived, how full it is
|
|
9
|
+
// GET /api/library/info?sha= a file's record and its links
|
|
10
|
+
// GET /api/library/file?sha= its bytes (what pdf.js draws, what an image shows)
|
|
11
|
+
// POST /api/library/upload?name=&sha=¬e= add a file: its bytes as the body (sha: the file a
|
|
12
|
+
// link names that this server lacks, and no other;
|
|
13
|
+
// note=0: the page writes the note that carries it)
|
|
14
|
+
// POST /api/library/url {url} add the file at an address, fetched once
|
|
15
|
+
// POST /api/library/edit {sha, title} its title
|
|
16
|
+
// POST /api/library/archive {sha, on} archived (on, the default) or restored
|
|
17
|
+
// GET /api/library/links?subject=&file= links of a subject, or into a file
|
|
18
|
+
// POST /api/library/link {subject, file, page?, region?, quote?, label?}
|
|
19
|
+
// POST /api/library/link/edit {id, …} its label, place or subject
|
|
20
|
+
// POST /api/library/unlink {id}
|
|
21
|
+
// GET /api/library/text?sha=&page= a page's text with the place of every word
|
|
22
|
+
// GET /api/library/search?sha=&q= where a text is in a PDF
|
|
23
|
+
// GET /api/library/page?sha=&page=&width= a page drawn as a PNG
|
|
24
|
+
// GET /api/library/clip?sha=&page=®ion=&width= a region of a page drawn as a PNG (a clip)
|
|
25
|
+
//
|
|
26
|
+
// Writing needs a name, as an opinion does (ReviewSpec decision 6); an agent that comes here with
|
|
27
|
+
// its headers is held to its level, as on the page's review (AgentSpec decision 9): files and links
|
|
28
|
+
// at advise, what others made at full.
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { readFile } from "node:fs/promises";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
import { MAX_FILE } from "../shared/library.js";
|
|
34
|
+
class TooLarge extends Error {
|
|
35
|
+
}
|
|
36
|
+
/** A request's body as bytes, at most `limit`: a larger one is refused before it is all read. */
|
|
37
|
+
function readBytes(req, limit) {
|
|
38
|
+
return new Promise((ok, no) => {
|
|
39
|
+
const length = Number(req.headers["content-length"] ?? NaN);
|
|
40
|
+
if (Number.isFinite(length) && length > limit)
|
|
41
|
+
return no(new TooLarge());
|
|
42
|
+
const parts = [];
|
|
43
|
+
let n = 0;
|
|
44
|
+
req.on("data", (b) => {
|
|
45
|
+
n += b.length;
|
|
46
|
+
if (n > limit) {
|
|
47
|
+
req.pause();
|
|
48
|
+
no(new TooLarge());
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
parts.push(b);
|
|
52
|
+
});
|
|
53
|
+
req.on("end", () => ok(Buffer.concat(parts)));
|
|
54
|
+
req.on("error", no);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** An uploaded file's bytes, the body as it is; or null, the request answered already (413) where it
|
|
58
|
+
* is larger than a file of the library may be. The agents' upload reads its body so too (main.ts). */
|
|
59
|
+
export async function readUpload(req, res) {
|
|
60
|
+
try {
|
|
61
|
+
return await readBytes(req, MAX_FILE);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (!(e instanceof TooLarge))
|
|
65
|
+
throw e;
|
|
66
|
+
// the rest is read and thrown away, and the answer goes once it has all come, as main.ts's
|
|
67
|
+
// tooLarge does: a connection closed while the client still sends is reset on some systems
|
|
68
|
+
let done = false;
|
|
69
|
+
const answer = () => {
|
|
70
|
+
if (done)
|
|
71
|
+
return;
|
|
72
|
+
done = true;
|
|
73
|
+
res.setHeader("connection", "close");
|
|
74
|
+
sendJson(res, { error: `The file is larger than ${Math.round(MAX_FILE / (1024 * 1024))} MB, the most a file of the library may be.`, hint: "Add a smaller file." }, 413);
|
|
75
|
+
};
|
|
76
|
+
req.on("data", () => { });
|
|
77
|
+
req.on("end", answer);
|
|
78
|
+
req.resume();
|
|
79
|
+
if (req.readableEnded)
|
|
80
|
+
answer();
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
answer();
|
|
83
|
+
setTimeout(() => req.destroy(), 500).unref();
|
|
84
|
+
}, 2000).unref();
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function sendJson(res, body, status = 200) {
|
|
89
|
+
const text = JSON.stringify(body);
|
|
90
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(text) });
|
|
91
|
+
res.end(text);
|
|
92
|
+
}
|
|
93
|
+
/** An answer: JSON, or bytes of a type. A file of the library is the same bytes for good (it is
|
|
94
|
+
* named by its hash), so the browser keeps it; it is sent as what it is, never sniffed, and kept
|
|
95
|
+
* from running anything should it be opened on its own. */
|
|
96
|
+
function send(req, res, a) {
|
|
97
|
+
if (!a.bytes)
|
|
98
|
+
return sendJson(res, a.body, a.status);
|
|
99
|
+
// the same request is the same bytes: a file by its hash, a page or a clip by what it asks for
|
|
100
|
+
const tag = `"${createHash("sha1").update(req.url ?? "").digest("hex").slice(0, 20)}"`;
|
|
101
|
+
const headers = {
|
|
102
|
+
"content-type": a.bytes.type,
|
|
103
|
+
"x-content-type-options": "nosniff",
|
|
104
|
+
"content-security-policy": "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox",
|
|
105
|
+
"cache-control": a.bytes.immutable ? "private, max-age=31536000, immutable" : "private, no-cache",
|
|
106
|
+
"content-disposition": "inline",
|
|
107
|
+
etag: tag,
|
|
108
|
+
};
|
|
109
|
+
if (req.headers["if-none-match"] === tag) {
|
|
110
|
+
res.writeHead(304, headers);
|
|
111
|
+
return void res.end();
|
|
112
|
+
}
|
|
113
|
+
res.writeHead(a.status, { ...headers, "content-length": String(a.bytes.data.length) });
|
|
114
|
+
res.end(a.bytes.data);
|
|
115
|
+
}
|
|
116
|
+
/** Answer a request under /api/library. */
|
|
117
|
+
export async function libraryRoute(w, req, res, url) {
|
|
118
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
119
|
+
const q = (k) => url.searchParams.get(k);
|
|
120
|
+
const api = w.api;
|
|
121
|
+
if (req.method === "GET") {
|
|
122
|
+
switch (path) {
|
|
123
|
+
case "/api/library":
|
|
124
|
+
return send(req, res, api.list());
|
|
125
|
+
case "/api/library/info":
|
|
126
|
+
return send(req, res, api.file(q("sha")));
|
|
127
|
+
case "/api/library/file":
|
|
128
|
+
return send(req, res, api.bytes(q("sha")));
|
|
129
|
+
case "/api/library/links":
|
|
130
|
+
return send(req, res, api.links({ subject: q("subject"), file: q("file") }));
|
|
131
|
+
case "/api/library/text":
|
|
132
|
+
return send(req, res, await api.text(q("sha"), q("page")));
|
|
133
|
+
case "/api/library/search":
|
|
134
|
+
return send(req, res, await api.search(q("sha"), q("q"), q("limit")));
|
|
135
|
+
case "/api/library/page":
|
|
136
|
+
return send(req, res, await api.page(q("sha"), q("page"), q("width")));
|
|
137
|
+
case "/api/library/clip":
|
|
138
|
+
return send(req, res, await api.clip(q("sha"), q("page"), q("region"), q("width")));
|
|
139
|
+
default:
|
|
140
|
+
return sendJson(res, { error: "not found" }, 404);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (req.method !== "POST")
|
|
144
|
+
return sendJson(res, { error: `${path} answers GET and POST.` }, 405);
|
|
145
|
+
// who writes: a name, as for an opinion; an agent by its headers, held to its level
|
|
146
|
+
const name = w.ids.who(req);
|
|
147
|
+
if (!name)
|
|
148
|
+
return sendJson(res, { error: "Say who you are first: what you add to the library is recorded under your name.", who: true }, 401);
|
|
149
|
+
const agent = w.ids.kind(req) === "agent";
|
|
150
|
+
if (agent && w.level === "read")
|
|
151
|
+
return sendJson(res, { error: `Agents may only read on this server (--agents ${w.level}).`, hint: "--agents advise or full allows adding files and links." }, 403);
|
|
152
|
+
const who = { name, agent, person: w.ids.person(req)?.id ?? null, full: !agent || w.level === "full" };
|
|
153
|
+
if (path === "/api/library/upload") {
|
|
154
|
+
const bytes = await readUpload(req, res);
|
|
155
|
+
if (!bytes)
|
|
156
|
+
return;
|
|
157
|
+
return send(req, res, await api.add(bytes, { name: q("name"), expect: q("sha"), note: q("note") !== "0" }, who));
|
|
158
|
+
}
|
|
159
|
+
let body;
|
|
160
|
+
try {
|
|
161
|
+
const text = (await readBytes(req, 1 << 16)).toString("utf8");
|
|
162
|
+
const raw = text ? JSON.parse(text) : {};
|
|
163
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
164
|
+
throw new Error("not an object");
|
|
165
|
+
body = raw;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
return sendJson(res, { error: e instanceof TooLarge ? "The body is larger than 64 KB." : "The body is not a JSON object." }, e instanceof TooLarge ? 413 : 400);
|
|
169
|
+
}
|
|
170
|
+
switch (path) {
|
|
171
|
+
case "/api/library/url":
|
|
172
|
+
return send(req, res, await api.addUrl(body.url, who));
|
|
173
|
+
case "/api/library/edit":
|
|
174
|
+
return send(req, res, api.editFile(body.sha, body.title, who));
|
|
175
|
+
case "/api/library/archive":
|
|
176
|
+
return send(req, res, api.archive(body.sha, body.on, who));
|
|
177
|
+
case "/api/library/link":
|
|
178
|
+
return send(req, res, api.link(body, who));
|
|
179
|
+
case "/api/library/link/edit":
|
|
180
|
+
return send(req, res, api.editLink(body, who));
|
|
181
|
+
case "/api/library/unlink":
|
|
182
|
+
return send(req, res, api.unlink(body.id, who));
|
|
183
|
+
default:
|
|
184
|
+
return sendJson(res, { error: "not found" }, 404);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** pdf.js's own package, beside the server: what some PDFs need besides themselves. */
|
|
188
|
+
let pdfjsDir = null;
|
|
189
|
+
const ASSET_TYPES = { bcmap: "application/octet-stream", pfb: "application/octet-stream", ttf: "font/ttf", otf: "font/otf", wasm: "application/wasm", js: "text/javascript; charset=utf-8", icc: "application/vnd.iccprofile" };
|
|
190
|
+
/** What pdf.js asks the server for while it reads a PDF in the reference window
|
|
191
|
+
* (web/src/paperview.ts): the standard fonts a PDF names without embedding, the character maps of
|
|
192
|
+
* CJK fonts, the decoders of JBIG2 and JPEG 2000 images, a colour profile — from pdf.js's own
|
|
193
|
+
* package, by file name alone, never anything else of it (its script engine for PDFs' JavaScript
|
|
194
|
+
* is not served). Its legacy build, which the page bundles, reads them from the package's top, as
|
|
195
|
+
* the modern one does. */
|
|
196
|
+
export async function pdfjsAsset(res, path) {
|
|
197
|
+
const m = /^\/pdfjs\/(standard_fonts|cmaps|wasm|iccs)\/([A-Za-z0-9._-]+)$/.exec(path);
|
|
198
|
+
const ext = m?.[2].split(".").pop() ?? "";
|
|
199
|
+
if (!m || m[2].startsWith("quickjs") || !Object.hasOwn(ASSET_TYPES, ext))
|
|
200
|
+
return sendJson(res, { error: "not found" }, 404);
|
|
201
|
+
pdfjsDir ??= dirname(dirname(dirname(fileURLToPath(import.meta.resolve("pdfjs-dist/legacy/build/pdf.mjs")))));
|
|
202
|
+
let body;
|
|
203
|
+
try {
|
|
204
|
+
body = await readFile(join(pdfjsDir, m[1], m[2]));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return sendJson(res, { error: "not found" }, 404);
|
|
208
|
+
}
|
|
209
|
+
res.writeHead(200, { "content-type": ASSET_TYPES[ext], "content-length": String(body.length), "cache-control": "public, max-age=86400", "x-content-type-options": "nosniff" });
|
|
210
|
+
res.end(body);
|
|
211
|
+
}
|