annotepage-client 2.0.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/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # annotepage-client
2
+
3
+ The annotepage annotation layer, browser side.
4
+
5
+ You open a page, you click what you see -- a heading, an image, a button --
6
+ and you write a remark. It is **encrypted in the browser** before it goes out,
7
+ pinned to that element, and everybody sees everybody's notes. A fixer -- human
8
+ or AI -- replies to it, marks it resolved while saying which version the fix
9
+ ships in, and the remark moves into the history without ever being deleted.
10
+
11
+ This package is **one single file**, with no dependency, loaded by a `<script>`
12
+ tag under an SRI digest. It talks about no site in particular and knows none.
13
+
14
+ The exchange format, the security model and what they do not promise are
15
+ described in `FORMAT.md`, at the root of the repository. When this file and
16
+ `FORMAT.md` contradict each other, `FORMAT.md` is right.
17
+
18
+ ## What it requires
19
+
20
+ - a recent browser, in a **secure context**: `https`, or `localhost`. Without
21
+ one, the browser does not provide WebCrypto, and the tool can neither
22
+ encrypt nor even compute the page index -- it says so on screen rather than
23
+ pretending;
24
+ - a reachable **annotepage server**: the site itself (self-hosted), or a third
25
+ party machine (relay). One single PHP codebase, deployed at either end;
26
+ - nothing else. No framework, no bundler, no stylesheet to load separately.
27
+
28
+ ## Putting the tool on a site
29
+
30
+ ### 1. Generate the salt, and put it away
31
+
32
+ Load the client once, on a page of the site, with `data-setup` and **without**
33
+ `data-project`:
34
+
35
+ ```html
36
+ <script src="https://<your-cdn>/annotepage-client@2.0.0/dist/annotepage.js"
37
+ integrity="sha384-A5Wrzv2mtFVnn8Mt0xC7BglTbxbb75unG3CNt5YHUNQ6X5QcwfFPI0OWtANjN2V/"
38
+ crossorigin="anonymous"
39
+ data-server="https://<your-server>/annotepage/api.php"
40
+ data-setup
41
+ defer></script>
42
+ ```
43
+
44
+ The setup screen generates a **256-bit salt** and gives you four things to
45
+ copy: the salt, the project id, the final tag, and the three lines to declare
46
+ on the server. No network request is made at that point.
47
+
48
+ > **SALT LOST = NOTES LOST.** The salt is the only secret of the project. It
49
+ > never leaves the browser, the server receives it in no form whatsoever, and
50
+ > nobody can give it back to you. There is no recovery, no security question,
51
+ > no escrow third party. Put it where your team keeps its passwords **before**
52
+ > continuing.
53
+
54
+ ### 2. Paste the final tag, at the end of `<body>`
55
+
56
+ ```html
57
+ <script src="https://<your-cdn>/annotepage-client@2.0.0/dist/annotepage.js"
58
+ integrity="sha384-A5Wrzv2mtFVnn8Mt0xC7BglTbxbb75unG3CNt5YHUNQ6X5QcwfFPI0OWtANjN2V/"
59
+ crossorigin="anonymous"
60
+ data-server="https://<your-server>/annotepage/api.php"
61
+ data-project="7Qb1kZ3xNvA9dLpEqKf2Zt"
62
+ data-version="1.4.12"
63
+ data-environment="staging"
64
+ defer></script>
65
+ ```
66
+
67
+ **`integrity` is not decorative.** As soon as the client goes to a CDN, the
68
+ real risk of this architecture is the supply chain: a file swapped at the CDN's
69
+ host runs in your page, with access to `localStorage` -- hence to the salt. The
70
+ SRI digest is what makes that swap useless. `crossorigin="anonymous"` goes with
71
+ it: without it the browser does not check the digest of a cross-origin
72
+ resource.
73
+
74
+ The digest **of the version you serve** is in `dist/HASHES.txt` in the package,
75
+ and the build prints it. The one above is 2.0.0's. Never copy a digest from a
76
+ documentation page for another version: the browser will refuse the file, and
77
+ that is exactly its job.
78
+
79
+ **Do not add `type="module"`.** A module script has no
80
+ `document.currentScript`: the client could no longer read its own attributes
81
+ and would stand down in silence.
82
+
83
+ ### 3. Declare the project on the server, and give the salt to the team
84
+
85
+ The server receives the project id (public) and the list of allowed origins.
86
+ The salt travels **out of band** -- the tool provides no channel for it. Each
87
+ reviewer pastes it once: the tool shows them the pasting screen, checks that
88
+ the salt really derives the id declared by the page, and remembers it in their
89
+ browser.
90
+
91
+ ## The tag attributes
92
+
93
+ | Attribute | What it declares |
94
+ |---|---|
95
+ | `data-server` | the address of `api.php`. Required as soon as the client comes from a CDN. Without it, and only if the client is served by the site, the tool deduces `../api.php` from its own address -- as in version 1.2.0 |
96
+ | `data-project` | the project id, 22 characters. Without it the tool does **nothing** (except with `data-setup`) |
97
+ | `data-setup` | opens the setup screen. To be removed once the project is created |
98
+ | `data-mode` | `encrypted` (default) or `plain`. See below |
99
+ | `data-path` | path prefix: the pages of the project. `/fr/` does not annotate `/en/` |
100
+ | `data-domains` | origins of the project, separated by commas |
101
+ | `data-version` | the version REALLY being served, as the site names it |
102
+ | `data-environment` | the name of the environment, written into the note as it stands |
103
+ | `data-labels` | a label file belonging to the site, resolved against the document |
104
+
105
+ `data-version` serves one single purpose, but it counts: when a note is marked
106
+ resolved, the tool compares the version of the fix with this one to tell
107
+ "resolved and online" -- which folds into the history -- from "resolved, not
108
+ deployed yet" -- which stays in front of the reviewer, because the defect
109
+ itself is still on screen. Missing or unreadable version: the fix is taken as
110
+ NOT deployed, the note stays visible.
111
+
112
+ A standalone tool does not guess how a site names its version: without these
113
+ attributes the fields stay empty, and that is intended.
114
+
115
+ ## Encrypted, or plain
116
+
117
+ Encryption is **on by default**. In encrypted mode, everything typed or
118
+ observed goes out in an AES-256-GCM envelope the server cannot open: the text,
119
+ but also the page, the selector, the excerpt, the reviewer's name, the version,
120
+ the environment. Encrypting the text alone would hand over the site's tree, its
121
+ wording and the list of its reviewers -- and a staging site is precisely what
122
+ is not published yet.
123
+
124
+ `data-mode="plain"` is only acceptable **when self-hosted**, where encryption
125
+ protects nothing: the notes are in the same database, on the same machine,
126
+ behind the same access restriction as the site under review. **A relay refuses
127
+ it**, with a 400, and shows its message.
128
+
129
+ The mode is written into each note. An installation that ran plain for two
130
+ weeks before turning encryption on stays entirely readable: every row says what
131
+ it is.
132
+
133
+ ## What the server never sees, and what it sees anyway
134
+
135
+ It receives neither the salt, nor the key, nor the path of your pages: it
136
+ groups by **blind index**, an HMAC of the path that it cannot invert. It does
137
+ see the number of projects and notes, the number of distinct pages, the time of
138
+ every write, the shape of the threads, the approximate length of each remark,
139
+ the IP address of each reviewer -- and, on a relay, **the domain of the site
140
+ under review**, through the `Origin` header that the domain lock requires it to
141
+ read. The promise is not "the relay does not know which site you are
142
+ reviewing"; it is "the relay cannot read your paths, your names or your
143
+ remarks".
144
+
145
+ The server's domain lock is an **anti-abuse** measure: it stops another site
146
+ from consuming a project id found in the source of a page. **It is not a
147
+ protection against XSS**: an XSS runs INSIDE the target page, so with the
148
+ legitimate origin, and it has access to `localStorage`, hence to the salt.
149
+
150
+ The path prefix (`data-path`) is checked **by the client** -- the server does
151
+ not see paths. It is **tidiness**, not a security boundary.
152
+
153
+ ## Content Security Policy (CSP)
154
+
155
+ If the site serves one, three directives concern it:
156
+
157
+ - `script-src`: the CDN's origin, without which the client does not load;
158
+ - `connect-src`: the annotepage server's origin, without which `fetch` fails
159
+ and the tool stands down in silence, as if nothing were configured;
160
+ - `style-src`: nothing to do in most cases. The stylesheet is put in as a
161
+ **constructed sheet**, which is not an inline sheet in the policy's sense. On
162
+ a browser that cannot construct one, the tool falls back on a `<style>`,
163
+ which `style-src` without `'unsafe-inline'` will block -- the tool will work,
164
+ but unstyled.
165
+
166
+ ## Two silences and a shout
167
+
168
+ **It stands down in silence when it has nothing to do.** If the API does not
169
+ answer, does not answer JSON, says it is not configured, or if the page is out
170
+ of the project's scope, the client adds nothing to the DOM and writes nothing
171
+ to the console. So the tag can be left in a template shared by the whole site.
172
+
173
+ **But once in place, it no longer keeps quiet.** Every failure is shown, with
174
+ the message the server wrote, and **the text typed stays in the form**. A
175
+ remark believed saved and not saved is worse than no tool at all.
176
+
177
+ **A refusal is named.** Defect seen in production: a hosting firewall answers
178
+ 403 with an HTML page, and the user read "the server answered something
179
+ unexpected". That was true and useless. The tool now names the refusal, gives
180
+ its code, and suggests the one move that often gets around it: rephrase the
181
+ remark, with no tags and no fragments of code. The text itself is kept -- that
182
+ has not changed.
183
+
184
+ A refusal on the very first call is shown too, unlike 1.2.0: a firewall
185
+ blocking everything made the tool entirely invisible, and one looked for the
186
+ failure in the wrong file.
187
+
188
+ ## What it does not touch
189
+
190
+ The client adds **one single element** to the site, at the end of `<body>`, and
191
+ works inside a `shadow root`: it puts no class, no attribute and no style on an
192
+ element of the page, and the pointing highlight is a rectangle drawn on its own
193
+ side, never an outline placed on the element aimed at. Its styles are prefixed
194
+ `ap-` and live inside the shadow root: they cannot reach the site, and the site
195
+ cannot reach them.
196
+
197
+ Its palette is its own and follows `prefers-color-scheme`: it reads neither the
198
+ variables nor the theme of the host site.
199
+
200
+ `textContent` everywhere, `innerHTML` nowhere: the text of a note is typed by a
201
+ human and is never interpreted as markup.
202
+
203
+ ## Translating, or changing a word
204
+
205
+ Every text shown is in `src/15-labels.js`, in a flat object, in English. Two
206
+ ways to replace them without touching the code, by priority:
207
+
208
+ ```html
209
+ <!-- 1. an object, defined BEFORE the client -->
210
+ <script>window.Annotepage = { labels: { 'button.open': 'Annoter la page' } };</script>
211
+ <script src="https://.../annotepage.js" ... defer></script>
212
+
213
+ <!-- 2. a neighbouring file, DECLARED on the tag -->
214
+ <script src="https://.../annotepage.js" data-labels="/local-labels.js" ... defer></script>
215
+ ```
216
+
217
+ A missing label falls back on English: a partial translation stays usable.
218
+
219
+ The complete French set ships in the package, in `labels/fr.json`. It is data,
220
+ not a script, so it cannot be given to `data-labels` as it stands -- that
221
+ attribute loads a **script**. Either paste the object into the page:
222
+
223
+ ```html
224
+ <script>window.Annotepage = { labels: /* the contents of labels/fr.json */ };</script>
225
+ ```
226
+
227
+ or wrap it once, in a file of your own served by your site:
228
+
229
+ ```js
230
+ // local-labels.js
231
+ window.Annotepage = window.Annotepage || {};
232
+ window.Annotepage.labels = { /* the contents of labels/fr.json */ };
233
+ ```
234
+
235
+ The keys are the same in both files; only the values change. This tool does not
236
+ force English onto the reviewers of a French site.
237
+
238
+ ## Building, checking, publishing
239
+
240
+ ```
241
+ npm run build assembles dist/annotepage.js and prints its sha384 digest
242
+ npm test checks the derivations, the blind index and the envelope
243
+ npm publish
244
+ ```
245
+
246
+ The build has **no dependency**: no bundler, no minifier. That is deliberate --
247
+ the file goes into somebody else's page, and the supply chain is the main risk
248
+ of this architecture. The file stays readable, and a digest is checked against
249
+ something one can read.
250
+
251
+ `npm test` cross-checks the format vectors against a second implementation of
252
+ HKDF-SHA-256 written by hand from RFC 5869. That is what guarantees that the
253
+ salt is the input keying material and `annotepage/1` the HKDF salt, and not the
254
+ other way round: both "work", only one is the format. The PHP server and the
255
+ MCP package can copy these vectors to check that they speak the same format.
256
+
257
+ ## What is in the package
258
+
259
+ ```
260
+ dist/annotepage.js THE served file. Generated: do not edit it by hand
261
+ dist/HASHES.txt one sha384 digest per published version
262
+ labels/fr.json the complete French label set, as an example
263
+ ```
264
+
265
+ The sources and the build tools are **not published**: what a page loads is the
266
+ one file above, and its digest is checked against it. They are in the
267
+ repository:
268
+
269
+ ```
270
+ src/00-preamble.js reading the tag: server, project, scope, limits
271
+ src/10-utils.js labels, base64url, dates, versions
272
+ src/15-labels.js EVERY text shown, English by default
273
+ src/20-crypto.js salt, HKDF, blind index, AES-256-GCM envelope
274
+ src/30-state.js state, browser memory, scope
275
+ src/40-api.js the calls, the refusals, what goes out encrypted or plain
276
+ src/50-anchors.js finding the element of a note, or calling it orphaned
277
+ src/60-ui.js all the DOM, inside the shadow root
278
+ src/70-setup.js the two screens that show or ask for the salt
279
+ src/90-boot.js the order of ignition, and the silences
280
+ src/styles.css confined styles, inlined by the build
281
+ tools/build.mjs the assembly, and the SRI digest
282
+ tools/check.mjs the format vectors
283
+ ```
284
+
285
+ The sources are not modules: they are the **sections** of one single file, put
286
+ end to end by the build inside one single scope. That is what makes it possible
287
+ to port the client from 1.2.0 without rewriting it.
288
+
289
+ ## What it does not do
290
+
291
+ This is a choice, not an oversight:
292
+
293
+ - **no authentication.** The name typed in is a convenience, not an identity.
294
+ The project id is a bearer token: whoever has it can read and write. In
295
+ encrypted mode, what they read is useless without the salt;
296
+ - **no moderation, and no deletion.** A note that is posted stays. The only
297
+ state it can change is "resolved", and that state can be taken back;
298
+ - **no salt rotation.** There is no mechanism: a leaked salt means starting
299
+ from a fresh project, abandoning the notes;
300
+ - **no channel for handing the salt** to the second reviewer;
301
+ - **no masking of the length** of the remarks: the size of the envelope gives
302
+ it away to within a few bytes.
303
+
304
+ The salt is remembered **per browser and per origin**. The day staging becomes
305
+ production, every reviewer pastes it once more on the new domain -- the notes
306
+ themselves do not move. That is exactly what the rule "the domain is not in the
307
+ key" buys.
308
+
309
+ ## Licence
310
+
311
+ MIT.
@@ -0,0 +1 @@
1
+ 2.0.0 sha384-A5Wrzv2mtFVnn8Mt0xC7BglTbxbb75unG3CNt5YHUNQ6X5QcwfFPI0OWtANjN2V/ 132399 bytes