visualproof 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -2
  3. package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
  4. package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
  5. package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
  6. package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
  7. package/dist/assets/latex/visualproof.sty +308 -0
  8. package/dist/extractor/Extract.lean +1180 -0
  9. package/dist/extractor/Usage.lean +900 -0
  10. package/dist/mcp/visualproof-mcp.mjs +476 -0
  11. package/dist/server/access.js +95 -0
  12. package/dist/server/agent.js +3235 -0
  13. package/dist/server/agentdocs.js +35 -0
  14. package/dist/server/agentlibrary.js +202 -0
  15. package/dist/server/agentnotes.js +701 -0
  16. package/dist/server/agentviews.js +703 -0
  17. package/dist/server/askfollow.js +286 -0
  18. package/dist/server/boardspots.js +159 -0
  19. package/dist/server/dirs.js +175 -0
  20. package/dist/server/doctor.js +186 -0
  21. package/dist/server/events.js +198 -0
  22. package/dist/server/export.js +420 -0
  23. package/dist/server/extract.js +204 -0
  24. package/dist/server/fetchurl.js +321 -0
  25. package/dist/server/figures.js +158 -0
  26. package/dist/server/history.js +741 -0
  27. package/dist/server/historyapi.js +304 -0
  28. package/dist/server/hover.js +595 -0
  29. package/dist/server/identity.js +114 -0
  30. package/dist/server/invites.js +241 -0
  31. package/dist/server/layoutthread.js +51 -0
  32. package/dist/server/library.js +600 -0
  33. package/dist/server/libraryroute.js +211 -0
  34. package/dist/server/live.js +1101 -0
  35. package/dist/server/lostfound.js +199 -0
  36. package/dist/server/main.js +1394 -0
  37. package/dist/server/mcptools.js +497 -0
  38. package/dist/server/pastdoc.js +73 -0
  39. package/dist/server/pipeline.js +156 -0
  40. package/dist/server/poppler.js +251 -0
  41. package/dist/server/presence.js +252 -0
  42. package/dist/server/project.js +222 -0
  43. package/dist/server/prompt.js +53 -0
  44. package/dist/server/readonly.js +152 -0
  45. package/dist/server/readonlygate.js +123 -0
  46. package/dist/server/renames.js +268 -0
  47. package/dist/server/review.js +334 -0
  48. package/dist/server/reviewapi.js +188 -0
  49. package/dist/server/snapshot.js +49 -0
  50. package/dist/server/sourcestore.js +473 -0
  51. package/dist/server/status.js +360 -0
  52. package/dist/server/talk.js +476 -0
  53. package/dist/server/tokens.js +221 -0
  54. package/dist/server/usage.js +501 -0
  55. package/dist/server/versions.js +82 -0
  56. package/dist/server/view.js +157 -0
  57. package/dist/server/views.js +1167 -0
  58. package/dist/shared/askfollow.js +46 -0
  59. package/dist/shared/board.js +332 -0
  60. package/dist/shared/build.js +2 -0
  61. package/dist/shared/collab.js +148 -0
  62. package/dist/shared/comment.js +285 -0
  63. package/dist/shared/context.js +157 -0
  64. package/dist/shared/doc/authors.js +98 -0
  65. package/dist/shared/doc/build.js +358 -0
  66. package/dist/shared/doc/latex.js +455 -0
  67. package/dist/shared/doc/markdown.js +159 -0
  68. package/dist/shared/doc/model.js +37 -0
  69. package/dist/shared/doc/order.js +107 -0
  70. package/dist/shared/doc/proof.js +272 -0
  71. package/dist/shared/doc/sections.js +93 -0
  72. package/dist/shared/doc/settings.js +75 -0
  73. package/dist/shared/edgecodec.js +102 -0
  74. package/dist/shared/editor.js +64 -0
  75. package/dist/shared/excerpt.js +87 -0
  76. package/dist/shared/graph.js +16 -0
  77. package/dist/shared/items.js +92 -0
  78. package/dist/shared/keys.js +203 -0
  79. package/dist/shared/layout.js +926 -0
  80. package/dist/shared/lexer.js +130 -0
  81. package/dist/shared/library.js +711 -0
  82. package/dist/shared/lostfound.js +150 -0
  83. package/dist/shared/moves.js +243 -0
  84. package/dist/shared/notecard.js +295 -0
  85. package/dist/shared/noteplace.js +505 -0
  86. package/dist/shared/palette.js +46 -0
  87. package/dist/shared/pastgraph.js +95 -0
  88. package/dist/shared/places.js +80 -0
  89. package/dist/shared/projectnotes.js +244 -0
  90. package/dist/shared/query.js +810 -0
  91. package/dist/shared/review.js +642 -0
  92. package/dist/shared/reviewqueue.js +333 -0
  93. package/dist/shared/ring.js +366 -0
  94. package/dist/shared/scope.js +255 -0
  95. package/dist/shared/scopeedit.js +81 -0
  96. package/dist/shared/search.js +191 -0
  97. package/dist/shared/separate.js +178 -0
  98. package/dist/shared/sharesnap.js +280 -0
  99. package/dist/shared/sticky.js +501 -0
  100. package/dist/shared/timeline.js +73 -0
  101. package/dist/shared/titles.js +298 -0
  102. package/dist/shared/viewgraph.js +139 -0
  103. package/dist/shared/views.js +859 -0
  104. package/dist/web/bundle/app.js +48733 -0
  105. package/dist/web/bundle/edges.js +409 -0
  106. package/dist/web/bundle/layout.js +1010 -0
  107. package/dist/web/bundle/pdfworker.js +48322 -0
  108. package/dist/web/index.html +239 -0
  109. package/dist/web/src/motion.js +430 -0
  110. package/dist/web/src/past.js +224 -0
  111. package/dist/web/src/scene.js +787 -0
  112. package/dist/web/style.css +2312 -0
  113. package/docs/agent-api/README.md +91 -0
  114. package/docs/agent-api/advice.md +108 -0
  115. package/docs/agent-api/export.md +120 -0
  116. package/docs/agent-api/following.md +140 -0
  117. package/docs/agent-api/history.md +142 -0
  118. package/docs/agent-api/library.md +65 -0
  119. package/docs/agent-api/mcp.md +104 -0
  120. package/docs/agent-api/notes.md +93 -0
  121. package/docs/agent-api/reading.md +166 -0
  122. package/docs/agent-api/requests.md +94 -0
  123. package/docs/agent-api/talking.md +100 -0
  124. package/docs/agent-api/tcb.md +135 -0
  125. package/docs/agent-api/views.md +124 -0
  126. package/docs/agent-api/words.md +56 -0
  127. package/package.json +39 -4
  128. package/test/toolchains.json +5 -0
@@ -0,0 +1,221 @@
1
+ // Agent tokens (AgentSpec decision 7): sharing with an AI agent hands it a credential of its own,
2
+ // never the server's key. A person makes one from Share for one agent; the token fixes the agent's
3
+ // name and what it may do, lasts a few days, reaches the agents' API only (server/main.ts), and can
4
+ // be revoked from Share by anyone with a name.
5
+ //
6
+ // A token is `vpa_<id>_<secret>`: the prefix makes one recognisable wherever it is pasted or leaks
7
+ // (a log, a config file), the id names it in lists (it is no secret), and the secret is 32 random
8
+ // bytes. The server keeps the secret, in `<site>/agent-tokens.json` (readable by its owner alone),
9
+ // so that Share can show a token's prompt again while the token is in force (decided 2026-10-08).
10
+ // That file is kept as the access key is, by the same owner on the same machine, and is worth less:
11
+ // whoever can read it can read the key, and whoever has the key can open the page and make a token
12
+ // of their own. Tokens made before the secret was kept (only test tokens) are dropped when the file
13
+ // is read.
14
+ //
15
+ // The identity is the token's, not the request's: the name is the one the person gave, "<their
16
+ // name>'s AI" unless they wrote another, and stays the same for the token's life, being the name
17
+ // its advice is logged under (shared/review.ts, logFileName). Among the tokens in force no two have
18
+ // the same name: a second "Alice's AI" is "Alice's AI 2".
19
+ //
20
+ // When a token was last used is kept in memory at each request and written with the rest at most
21
+ // once a minute (and when the server stops): no disk write per request.
22
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
23
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { MAX_NAME, uniqueName, validName } from "../shared/collab.js";
26
+ export const TOKEN_PREFIX = "vpa_";
27
+ /** how long a token may last, in days; DEFAULT_DAYS (7) is the one the page offers when none is said */
28
+ export const TOKEN_DAYS = [1, 7, 30];
29
+ export const DEFAULT_DAYS = 7;
30
+ /** a record of a token that ended (expired or revoked) is kept this long, for the Share window's
31
+ * list to say so and a conversation to be named, then forgotten */
32
+ const KEEP_ENDED_MS = 30 * 24 * 3600 * 1000;
33
+ /** lastUsed is written at most this often */
34
+ const FLUSH_MS = 60_000;
35
+ const FILE = "agent-tokens.json";
36
+ const ID = /^[0-9a-f]{10}$/;
37
+ const SECRET = /^[A-Za-z0-9_-]{43}$/;
38
+ const digest = (s) => createHash("sha256").update(s).digest();
39
+ /** Whether `s` has the shape of an agent token (to tell it from the server's key in a Bearer). */
40
+ export function looksLikeToken(s) {
41
+ return s.startsWith(TOKEN_PREFIX);
42
+ }
43
+ /** The bearer credential a request carries, or null. */
44
+ export function bearerOf(auth) {
45
+ if (!auth || !/^Bearer\s/i.test(auth))
46
+ return null;
47
+ return auth.replace(/^Bearer\s+/i, "").trim() || null;
48
+ }
49
+ /** The token itself, from what is kept of it. */
50
+ const tokenOf = (r) => `${TOKEN_PREFIX}${r.id}_${r.secret}`;
51
+ export class AgentTokens {
52
+ file;
53
+ records = [];
54
+ dirty = false;
55
+ timer = null;
56
+ /** told when a token ended (revoked, or found expired): its agent's wait ends (server/live.ts,
57
+ * server/talk.ts) */
58
+ onEnded = () => { };
59
+ /** told when the tokens in force changed (made, revoked, expired): presence lists them */
60
+ onChanged = () => { };
61
+ /** `siteDir` null keeps the tokens in memory only (tests). */
62
+ constructor(siteDir) {
63
+ this.file = siteDir ? join(siteDir, FILE) : null;
64
+ if (this.file && existsSync(this.file)) {
65
+ try {
66
+ const data = JSON.parse(readFileSync(this.file, "utf8"));
67
+ if (Array.isArray(data.tokens))
68
+ this.records = data.tokens.filter(isRecord);
69
+ // what was dropped (a token kept without its secret) leaves the file too
70
+ if (Array.isArray(data.tokens) && this.records.length < data.tokens.length)
71
+ this.dirty = true;
72
+ }
73
+ catch {
74
+ // a file that cannot be read lets no token in; it is kept aside, not overwritten blindly
75
+ renameSync(this.file, `${this.file}.broken-${Date.now()}`);
76
+ }
77
+ }
78
+ this.prune();
79
+ this.flush();
80
+ if (this.file) {
81
+ this.timer = setInterval(() => {
82
+ this.expire();
83
+ this.flush();
84
+ }, FLUSH_MS);
85
+ this.timer.unref?.();
86
+ }
87
+ }
88
+ /** Make a token for an agent: the token (for its prompt) and its record. The name and level are
89
+ * checked by the caller (server/main.ts); the name is used as given, numbered when an agent in
90
+ * force has it already; `by` is a person's name. */
91
+ create(o) {
92
+ const now = o.now ?? Date.now();
93
+ this.expire(now);
94
+ let id;
95
+ do
96
+ id = randomBytes(5).toString("hex");
97
+ while (this.records.some((r) => r.id === id));
98
+ const record = {
99
+ id,
100
+ secret: randomBytes(32).toString("base64url"),
101
+ name: uniqueName(o.name, this.inForce(now).map((r) => r.name)),
102
+ level: o.level,
103
+ by: o.by,
104
+ created: new Date(now).toISOString(),
105
+ expires: new Date(now + o.days * 24 * 3600 * 1000).toISOString(),
106
+ revoked: null,
107
+ revokedBy: null,
108
+ lastUsed: null,
109
+ };
110
+ this.records.push(record);
111
+ this.save();
112
+ this.onChanged();
113
+ return { token: tokenOf(record), record };
114
+ }
115
+ /** A token in force, whole, for its prompt to be shown again (server/main.ts); null when there is
116
+ * no such token in force — an ended one is never shown. */
117
+ token(id, now = Date.now()) {
118
+ const r = this.inForce(now).find((x) => x.id === id);
119
+ return r ? { token: tokenOf(r), record: r } : null;
120
+ }
121
+ /** What a bearer credential is: a token in force (its use noted), one that ended, or none of
122
+ * ours. The two are compared in constant time, by their digests (of one length whatever was
123
+ * sent), whether or not the id is known. */
124
+ check(token, now = Date.now()) {
125
+ if (!looksLikeToken(token))
126
+ return null;
127
+ const id = token.slice(TOKEN_PREFIX.length, TOKEN_PREFIX.length + 10);
128
+ const r = ID.test(id) && token[TOKEN_PREFIX.length + 10] === "_" ? this.records.find((x) => x.id === id) : undefined;
129
+ const same = timingSafeEqual(digest(token), digest(r ? tokenOf(r) : "no such token"));
130
+ if (!r || !same)
131
+ return null;
132
+ if (r.revoked)
133
+ return { ended: "revoked", record: r };
134
+ if (Date.parse(r.expires) <= now) {
135
+ this.expire(now);
136
+ return { ended: "expired", record: r };
137
+ }
138
+ r.lastUsed = new Date(now).toISOString();
139
+ this.dirty = true;
140
+ return { ok: r };
141
+ }
142
+ /** The tokens in force, oldest first. */
143
+ inForce(now = Date.now()) {
144
+ return this.records.filter((r) => !r.revoked && Date.parse(r.expires) > now);
145
+ }
146
+ get(id) {
147
+ return this.records.find((r) => r.id === id);
148
+ }
149
+ /** Revoke a token in force: it lets nothing in from now on, and its agent's wait ends. False when
150
+ * there is no such token in force. */
151
+ revoke(id, by, now = Date.now()) {
152
+ const r = this.records.find((x) => x.id === id);
153
+ if (!r || r.revoked || Date.parse(r.expires) <= now)
154
+ return false;
155
+ r.revoked = new Date(now).toISOString();
156
+ r.revokedBy = by;
157
+ this.save();
158
+ this.onEnded(r);
159
+ this.onChanged();
160
+ return true;
161
+ }
162
+ /** Tokens that expired since last looked at: their agents' waits end, and presence lists them no
163
+ * more. */
164
+ expire(now = Date.now()) {
165
+ let any = false;
166
+ for (const r of this.records)
167
+ if (!r.revoked && Date.parse(r.expires) <= now && !ended.has(r)) {
168
+ ended.add(r);
169
+ this.onEnded(r);
170
+ any = true;
171
+ }
172
+ if (any)
173
+ this.onChanged();
174
+ }
175
+ /** Write lastUsed, if it changed since the last write. */
176
+ flush() {
177
+ if (this.dirty)
178
+ this.save();
179
+ }
180
+ /** Stop writing on a timer (tests, the server's end), writing what is left. */
181
+ close() {
182
+ if (this.timer)
183
+ clearInterval(this.timer);
184
+ this.timer = null;
185
+ this.flush();
186
+ }
187
+ /** A token as people and agents are shown it. */
188
+ static info(r) {
189
+ const { secret: _, ...rest } = r;
190
+ return rest;
191
+ }
192
+ prune(now = Date.now()) {
193
+ const before = this.records.length;
194
+ this.records = this.records.filter((r) => now - Date.parse(r.revoked ?? r.expires) < KEEP_ENDED_MS || (!r.revoked && Date.parse(r.expires) > now));
195
+ if (this.records.length !== before)
196
+ this.save();
197
+ }
198
+ save() {
199
+ this.dirty = false;
200
+ if (!this.file)
201
+ return;
202
+ const tmp = `${this.file}.tmp`;
203
+ writeFileSync(tmp, JSON.stringify({ tokens: this.records }, null, 1) + "\n", { mode: 0o600 });
204
+ renameSync(tmp, this.file);
205
+ }
206
+ }
207
+ /** records whose expiry has been told (onEnded) once already */
208
+ const ended = new WeakSet();
209
+ /** Why a name an agent is to be given will not do; null when it will. */
210
+ export function invalidAgentName(s) {
211
+ if (typeof s !== "string" || !validName(s.trim()))
212
+ return `The agent's name is 1 to ${MAX_NAME} characters, without line breaks.`;
213
+ return null;
214
+ }
215
+ /** A record as this server keeps it. One kept before 2026-10-08 has a digest and no secret: it is
216
+ * not one, and is dropped. */
217
+ function isRecord(x) {
218
+ const r = x;
219
+ return !!r && typeof r === "object" && typeof r.id === "string" && ID.test(r.id) && typeof r.secret === "string" && SECRET.test(r.secret) &&
220
+ typeof r.name === "string" && typeof r.by === "string" && typeof r.expires === "string" && ["read", "advise", "full"].includes(r.level);
221
+ }