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 +311 -0
- package/dist/HASHES.txt +1 -0
- package/dist/annotepage.js +2573 -0
- package/labels/fr.json +103 -0
- package/package.json +46 -0
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.
|
package/dist/HASHES.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.0.0 sha384-A5Wrzv2mtFVnn8Mt0xC7BglTbxbb75unG3CNt5YHUNQ6X5QcwfFPI0OWtANjN2V/ 132399 bytes
|