discovery-media-player 0.1.133 → 0.1.135

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 CHANGED
@@ -16,6 +16,7 @@ to a third-party SaaS.
16
16
  [![CI](https://github.com/Juli1artha/discovery-media-player/actions/workflows/ci.yml/badge.svg)](https://github.com/Juli1artha/discovery-media-player/actions/workflows/ci.yml)
17
17
  [![CodeQL](https://github.com/Juli1artha/discovery-media-player/actions/workflows/codeql.yml/badge.svg)](https://github.com/Juli1artha/discovery-media-player/actions/workflows/codeql.yml)
18
18
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Juli1artha/discovery-media-player/badge)](https://scorecard.dev/viewer/?uri=github.com/Juli1artha/discovery-media-player)
19
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14197/badge)](https://www.bestpractices.dev/projects/14197)
19
20
  [![npm](https://img.shields.io/npm/v/discovery-media-player?logo=npm&color=cb3837)](https://www.npmjs.com/package/discovery-media-player)
20
21
  [![Container](https://img.shields.io/badge/ghcr.io-discovery--media--player-2496ed?logo=docker&logoColor=white)](https://github.com/Juli1artha/discovery-media-player/pkgs/container/discovery-media-player)
21
22
  [![Node](https://img.shields.io/node/v/discovery-media-player?logo=node.js&color=5fa04e)](package.json)
@@ -126,8 +127,8 @@ forking it. A fix lands once and reaches every instance on its next deploy.
126
127
  It reads `req.query` when the platform provides it (serverless, Express) and falls back to parsing
127
128
  `req.url` when it does not — so a bare `http.createServer` works too, without a shim.
128
129
 
129
- See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the boundary, and
130
- [`docs/API.md`](docs/API.md) for the surface an integrator implements.
130
+ See [`docs/ARCHITECTURE.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/ARCHITECTURE.md) for the boundary, and
131
+ [`docs/API.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/API.md) for the surface an integrator implements.
131
132
 
132
133
  ---
133
134
 
@@ -151,8 +152,8 @@ Minimum configuration:
151
152
  | `PLAYER_BRAND_NAME`, `PLAYER_LOADER_NAME` | your name in the tab title and the loader |
152
153
  | `PLAYER_SOURCE_URL` | where readers can obtain the source (AGPL, see below) |
153
154
 
154
- Full list: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md).
155
- Examples you can copy: [`examples/`](examples/).
155
+ Full list: [`docs/CONFIGURATION.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/CONFIGURATION.md).
156
+ Examples you can copy: [`examples/`](https://github.com/Juli1artha/discovery-media-player/tree/main/examples).
156
157
 
157
158
  ---
158
159
 
@@ -165,10 +166,10 @@ your private network.
165
166
 
166
167
  The project has been through repeated external audits. The reports are published in the
167
168
  repository, unedited, with their follow-up ledgers — findings, fixes, and what was rejected
168
- with its reason: see [`docs/`](docs/README.md). Fixes are traced version by version in the
169
- [CHANGELOG](CHANGELOG.md).
169
+ with its reason: see [`docs/`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/README.md). Fixes are traced version by version in the
170
+ [CHANGELOG](https://github.com/Juli1artha/discovery-media-player/blob/main/CHANGELOG.md).
170
171
 
171
- Found a hole? [`SECURITY.md`](SECURITY.md) — please do not open a public issue.
172
+ Found a hole? [`SECURITY.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/SECURITY.md) — please do not open a public issue.
172
173
 
173
174
  ---
174
175
 
@@ -184,7 +185,7 @@ Player* are trademarks of 3D Discovery: fork the code freely, but call your fork
184
185
  This is the usual arrangement in open source, and it protects you as much as us — nobody should
185
186
  be able to publish something under this name that we did not write.
186
187
 
187
- One exception, on purpose: **[`src/bridge.ts`](src/bridge.ts) is MIT**
188
+ One exception, on purpose: **[`src/bridge.ts`](https://github.com/Juli1artha/discovery-media-player/blob/main/src/bridge.ts) is MIT**
188
189
  ([`LICENSE-MIT`](LICENSE-MIT)). It is the message contract a host application imports to talk to
189
190
  the player. Putting it under the core licence would make integration itself a toll. We protect
190
191
  the player, not the people plugging into it.
@@ -193,10 +194,10 @@ the player, not the people plugging into it.
193
194
 
194
195
  ## Contributing
195
196
 
196
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to run the tests, what the review looks for, and the
197
+ [`CONTRIBUTING.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/CONTRIBUTING.md) — how to run the tests, what the review looks for, and the
197
198
  one rule that matters: a behaviour worth keeping is worth a test that fails without it.
198
199
 
199
- Your first pull request asks you to sign the [CLA](CLA.md) — one reply, once, for good. You keep
200
+ Your first pull request asks you to sign the [CLA](https://github.com/Juli1artha/discovery-media-player/blob/main/CLA.md) — one reply, once, for good. You keep
200
201
  the copyright in your work; you grant a licence that may be sublicensed, so that the core can stay
201
202
  AGPL while a commercial licence remains possible for organisations that cannot live with the
202
203
  network clause. Better said before you write the patch than after.
@@ -6,7 +6,7 @@
6
6
  > `node_modules` paths by hand is a guess about our tree, and it broke twice in one day.
7
7
 
8
8
  What a host application may call, what it must implement, and what will not change without a
9
- version bump. If you are integrating the player, this page and [`API.md`](API.md) are the two you
9
+ version bump. If you are integrating the player, this page and [`API.md`](https://github.com/Juli1artha/discovery-media-player/blob/main/docs/API.md) are the two you
10
10
  need.
11
11
 
12
12
  ## Five rules
@@ -362,7 +362,7 @@ Four requirements, in order of what they cost when missed:
362
362
 
363
363
  ## The postMessage bridge
364
364
 
365
- Described once in [`src/bridge.ts`](../src/bridge.ts) and published as `discovery-media-player/bridge`
365
+ Described once in [`src/bridge.ts`](https://github.com/Juli1artha/discovery-media-player/blob/main/src/bridge.ts) and published as `discovery-media-player/bridge`
366
366
  — **compiled JavaScript with type declarations, under MIT** rather than the core's AGPL, so that
367
367
  importing it is not a toll. Import it rather than copying constants: a message name retyped by hand
368
368
  is a contract in two copies, and the day it changes only one of them knows.
@@ -391,13 +391,63 @@ The rule underneath, safer than the list: **never fall back on a refusal of *acc
391
391
  back on an inability to *reach*.** And "do not fall back" applies to what you **offer** — an
392
392
  "Open ↗" button left in place is falling back one second later.
393
393
 
394
- ## Two things that will bite
394
+ ## Four things that will bite
395
395
 
396
396
  **Your document-opening doors reappear.** A host has more than one place that opens a file, and new
397
397
  ones get written. Keep the list and hunt it periodically — and note that **your search criteria
398
398
  decide what you find**: search by what the user *obtains* (a document opens), not by the technique
399
399
  you expect to see.
400
400
 
401
+ **A row written on behalf of a session also carries the document.** The player hands every bot
402
+ plugin call both the `sessionId` and the `share`. If your plugin stores only the session, then the
403
+ day a session-binding defect is found — one was in 0.1.131, and four more actions in 0.1.133 — you
404
+ cannot say whether anything crossed. The honest answer is not *"nothing found"*, it is **"not
405
+ measurable"**, and those are different sentences to give a client.
406
+
407
+ Measured on a production host the day the second defect was fixed:
408
+
409
+ | table | what it records beside the session | verdict |
410
+ | --- | --- | --- |
411
+ | leads (`bot-contact`, `bot-book`) | the share **and** the document | 46 leads, 21 carrying contact details — **none crossed** |
412
+ | messages (`bot-say`) | the session alone | 1 693 messages — **not measurable** |
413
+
414
+ Same incident, same instance, two verdicts — decided months earlier by one column. It costs nothing
415
+ at write time, and it decides what you are able to say afterwards.
416
+
417
+ ⚠️ **The expectation is answerability, not a column name** — and this paragraph earned that sentence
418
+ the hard way, one day after it was written. The same host then swept all eight of their tables
419
+ carrying a session: seven already recorded the document, under four different names (`doc_id`,
420
+ `share_slug`, `link_id`, `xp_id`). **One** did not. The discipline was everywhere and written
421
+ nowhere, which is exactly why it gave way at the single place nobody thought about.
422
+
423
+ Their first sweep looked for the two names this page happens to use, and would have accused three
424
+ correct tables. So when you check your own schema, write the check from **what your code stores**,
425
+ not from a list of column names borrowed from someone else's — a coarse check has no false
426
+ positives to excuse, it has a pattern to derive. The question each row must be able to answer is
427
+ *which document was this written for*; the key that answers it is yours to name.
428
+
429
+ **A database error carries its status as a number, not inside its message.** Set `statusCode` (or
430
+ `status`) on whatever `db.request` throws. Both contexts shipped here already do; a host that
431
+ implements the seam itself may not, and the player then has to guess from the text.
432
+
433
+ Guessing was the state until 24/08, at six call sites: `message.includes("409")` — the digits
434
+ anywhere in the string. But the message carries the **path**, so it carries the slug, the id, the
435
+ page number. Measured on the real shapes:
436
+
437
+ ```
438
+ Supabase POST /doc_presentation_attendees → 409 conflict ✅
439
+ Supabase POST /doc_presentation_attendees?slug=eq.demo409 → 500 conflict ❌
440
+ Supabase PATCH /doc_bot_sessions?id=eq.sess-409abc → 500 conflict ❌
441
+ Supabase GET /doc_pages?page=eq.409 → 503 conflict ❌
442
+ ```
443
+
444
+ Three in four. And every site reads `if (!conflict) throw`, so a genuine 500 was **swallowed** and
445
+ the code carried on as though the row already existed. One document whose slug contains `409` —
446
+ a reference number, a date — was enough.
447
+
448
+ The fallback that remains accepts `409` only **after the arrow**, where a status lives and a slug
449
+ cannot. Setting the number spares you that reasoning entirely.
450
+
401
451
  **Configured is not served.** When a diagnosis is disputed, the useful question is not who is right
402
452
  but *did you measure exactly what fails*. Two true statements about the same instance can describe
403
453
  different responses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
@@ -33,7 +33,6 @@
33
33
  "server",
34
34
  "supabase",
35
35
  "types",
36
- "README.md",
37
36
  "LICENSE",
38
37
  "LICENSE-MIT",
39
38
  "docs/HOST-CONTRACT.md",
@@ -0,0 +1,58 @@
1
+ // LE FAIT, PAS LE LIBELLÉ — COMMENT ON RECONNAÎT UN CONFLIT D'UNICITÉ.
2
+ //
3
+ // ⚠️ CE QUE FAISAIENT LES SIX SITES, ET CE QUE ÇA COÛTAIT.
4
+ //
5
+ // if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
6
+ //
7
+ // Le message que `context/standalone.js` compose est `Supabase POST /chemin?… → 409`, et le CHEMIN
8
+ // contient le slug, l'identifiant, le numéro de page. Chercher « 409 » n'importe où dans cette
9
+ // chaîne confond donc le statut avec les DONNÉES. Mesuré le 24/08 sur les formes réelles :
10
+ //
11
+ // Supabase POST /doc_presentation_attendees → 409 conflit ✅
12
+ // Supabase POST /doc_presentation_attendees?slug=eq.demo409 → 500 conflit ❌
13
+ // Supabase PATCH /doc_bot_sessions?id=eq.sess-409abc → 500 conflit ❌
14
+ // Supabase GET /doc_pages?page=eq.409 → 503 conflit ❌
15
+ //
16
+ // Trois sur quatre. Et l'usage est toujours `if (!conflit) throw` : une vraie panne 500 était donc
17
+ // AVALÉE, et le code continuait comme si la ligne existait déjà. Il suffit d'un document dont le
18
+ // slug porte « 409 » — un numéro de référence, une date — pour qu'une erreur de base disparaisse.
19
+ //
20
+ // ⚠️ LE FAIT EXISTE DÉJÀ. `context/standalone.js` pose `erreur.statusCode = r.status` depuis la
21
+ // correction de PGRST202, et son commentaire dit que le studio expose la même chose : « les deux
22
+ // formes convergent enfin ». Six sites lisaient encore le texte à côté.
23
+ //
24
+ // La leçon vient d'un exploitant, qui l'a payée sur son propre outillage : un tri sur un LIBELLÉ
25
+ // s'est renversé le jour où il a trouvé un meilleur mot. Un test sur un libellé a la même
26
+ // fragilité, en pire — il ne se renverse pas, il se trompe en silence.
27
+ //
28
+ // ⚠️ POURQUOI IL RESTE UN REPLI TEXTUEL. Un hôte tiers implémente `db.request` lui-même et peut
29
+ // n'avoir jamais posé `statusCode`. Le repli existe donc, mais il ne cherche plus « 409 » n'importe
30
+ // où : il ne l'accepte QU'À LA PLACE DU STATUT, en fin de message, là où le contrat le met. Un slug
31
+ // ne peut pas s'y trouver.
32
+
33
+ /** Le statut HTTP d'une erreur de base, ou `null` si personne ne l'a posé. */
34
+ const statutDe = (erreur) => {
35
+ const s = erreur && (erreur.statusCode ?? erreur.status);
36
+ return Number.isInteger(s) ? s : null;
37
+ };
38
+
39
+ /**
40
+ * Vrai si cette erreur EST un conflit d'unicité (409).
41
+ *
42
+ * ⚠️ L'ordre compte : le fait d'abord, le texte seulement à défaut. Inverser reviendrait à
43
+ * consulter le libellé même quand le statut est là — c'est-à-dire à garder le défaut avec une
44
+ * façade.
45
+ */
46
+ function estConflit(erreur) {
47
+ const statut = statutDe(erreur);
48
+ if (statut !== null) return statut === 409;
49
+ // ⚠️ REPLI : « 409 » EN POSITION DE STATUT, c'est-à-dire APRÈS la flèche — jamais ailleurs.
50
+ // Un premier essai exigeait la fin de message ou un tiret de détail : c'était se caler sur le
51
+ // format d'UN contexte, et quatre bancs l'ont refusé en lançant « → 409 duplicate key … ». Ce
52
+ // qu'on peut affirmer sans connaître l'hôte, c'est la POSITION : ce qui suit la flèche est le
53
+ // statut. Un slug, lui, vit dans le chemin — avant elle. C'est ce qui distingue ce repli du
54
+ // défaut qu'il remplace : « 409 » n'importe où, contre « 409 » là où le statut se trouve.
55
+ return /→\s*409\b/.test(String((erreur && erreur.message) || ""));
56
+ }
57
+
58
+ module.exports = { estConflit };
@@ -589,7 +589,7 @@ async function addMessage(slug, { name, email, avatar, isPresenter, isMember, bo
589
589
  // attendu n'est pas un incident — mais rien ne distinguait « je sais ce que je rattrape » de
590
590
  // « j'avale tout ». On le dit donc : ce qui n'est pas le conflit attendu remonte, et le conflit
591
591
  // attendu est journalisé une fois, en clair, parce qu'un renvoi fréquent est une information.
592
- const conflit = cle && String((erreur && erreur.message) || "").includes("409");
592
+ const conflit = cle && estConflit(erreur);
593
593
  if (!conflit) throw erreur;
594
594
  try { PLAYER.errors.capture(new Error("message déjà enregistré (renvoi) : " + String(slug)), { route: "present-chat", benin: true }); } catch { /* jamais bloquant */ }
595
595
  const deja = await PLAYER.db.request(
@@ -1163,7 +1163,7 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
1163
1163
  // simultanés (deux onglets ouverts ensemble), et le second recevait un 409 que personne ne
1164
1164
  // rattrapait — un 500 pour un battement, bénin mais faux. Le conflit dit « la ligne existe
1165
1165
  // maintenant » : on la relit et on continue en mise à jour. Tout autre échec remonte.
1166
- if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
1166
+ if (!estConflit(erreur)) throw erreur;
1167
1167
  // Journalisé comme bénin : deux onglets qui arrivent ensemble sont une information, pas
1168
1168
  // une panne — et la garde des écritures muettes exige que tout rattrapage parle.
1169
1169
  try { PLAYER.errors.capture(new Error("présence déjà ouverte (second onglet) : " + String(slug)), { route: "present-attend", benin: true }); } catch { /* jamais bloquant */ }
@@ -1284,6 +1284,7 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
1284
1284
  // Il traverse trois frontières (présentateur → serveur → audience) : deux implémentations
1285
1285
  // finissaient par diverger, et une audience qui ne voit pas la bonne carte n'émet aucune erreur.
1286
1286
  const { sanitizeContent } = require("./shared.generated.js");
1287
+ const { estConflit } = require("./erreurs-base.js");
1287
1288
 
1288
1289
  /**
1289
1290
  * Une présentation close ne se pilote plus par le chemin PROPRIÉTAIRE.
@@ -2,6 +2,7 @@
2
2
  // Reste à PLAT dans server/ (les gardes de forge ciblent server/*.js).
3
3
 
4
4
  const { adresseAppelant } = require("./appelant");
5
+ const { estConflit } = require("./erreurs-base.js");
5
6
  const { createShare, createReshare, sendReshareEmail, revokeShare, setShareAuth, listSharesForDoc, listSessionsForDoc, internalStatsForDoc, cleIdempotence, getShareBySlug, logView, upsertSession, upsertInternalSession, overview: docOverview } = require("./shares");
6
7
  const { SESSION_QUOTA_PER_HOUR, VIEW_QUOTA_PER_HOUR } = require("./shared.generated.js");
7
8
 
@@ -142,7 +143,7 @@ async function traiter(req, res, body, slug) {
142
143
  body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, revoked: false, ...(cleDispo ? { idem_key: cleHote } : {}) },
143
144
  });
144
145
  } catch (erreur) {
145
- if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
146
+ if (!estConflit(erreur)) throw erreur;
146
147
  try { PLAYER.errors.capture(new Error("backfill hôte : la clé était déjà posée ailleurs — " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
147
148
  const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
148
149
  if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
@@ -169,7 +170,7 @@ async function traiter(req, res, body, slug) {
169
170
  });
170
171
  return jd(200, { ok: true, slug: neuf.slug, reused: false });
171
172
  } catch (erreur) {
172
- if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
173
+ if (!estConflit(erreur)) throw erreur;
173
174
  try { PLAYER.errors.capture(new Error("lien hôte déjà créé par une demande simultanée : " + docId), { route: "hostshare", benin: true }); } catch { /* jamais bloquant */ }
174
175
  const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleHote)}&select=slug&limit=1`);
175
176
  if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur; // 409 d'autre chose : on ne l'invente pas
@@ -219,7 +220,7 @@ async function traiter(req, res, body, slug) {
219
220
  try {
220
221
  await PLAYER.db.request(`commercial_doc_shares?slug=eq.${encodeURIComponent(ex[0].slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { doc_title: body.docTitle || null, file_url: String(body.fileUrl), file_name: body.fileName || null, bot_enabled: true, bot_guided: true, bot_profile_id: (body.profileId || "").trim() || null, revoked: false, ...(cleDispo ? { idem_key: cleTest } : {}) } });
221
222
  } catch (erreur) {
222
- if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
223
+ if (!estConflit(erreur)) throw erreur;
223
224
  try { PLAYER.errors.capture(new Error("backfill répétition : la clé était déjà posée ailleurs — " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
224
225
  const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
225
226
  if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
@@ -232,7 +233,7 @@ async function traiter(req, res, body, slug) {
232
233
  const t = await createShare({ docId, docTitle: body.docTitle, fileUrl: body.fileUrl, fileName: body.fileName, recipientName: "Répétition (test)", createdBy: u.email, bot: true, guided: true, profileId: body.profileId, isTest: true, idemKey: cleTest });
233
234
  return jd(200, { ok: true, slug: t.slug });
234
235
  } catch (erreur) {
235
- if (!String((erreur && erreur.message) || "").includes("409")) throw erreur;
236
+ if (!estConflit(erreur)) throw erreur;
236
237
  try { PLAYER.errors.capture(new Error("lien de répétition déjà créé par une demande simultanée : " + docId), { route: "docshare-test", benin: true }); } catch { /* jamais bloquant */ }
237
238
  const gagnant = await PLAYER.db.request(`commercial_doc_shares?idem_key=eq.${encodeURIComponent(cleTest)}&select=slug&limit=1`);
238
239
  if (!Array.isArray(gagnant) || !gagnant[0]) throw erreur;
package/docs/README.md DELETED
@@ -1,56 +0,0 @@
1
- # Documentation
2
-
3
- Three readers, one section each. Start with the one that matches what you are trying to do —
4
- no document assumes you have read the others.
5
-
6
- ## You are integrating the player into an application
7
-
8
- | Document | What it gives you |
9
- |---|---|
10
- | [`ARCHITECTURE.md`](ARCHITECTURE.md) | The one idea that holds the design: the core knows nothing about its host. Read this first. |
11
- | [`API.md`](API.md) | What a host can call, and what it must implement. |
12
- | [`HOST-CONTRACT.md`](HOST-CONTRACT.md) | The binding contract, with the dated journal of every boundary change. It ships **inside the package**: `require.resolve("discovery-media-player/contrat")`. |
13
-
14
- ## You are running an instance
15
-
16
- | Document | What it gives you |
17
- |---|---|
18
- | [`CONFIGURATION.md`](CONFIGURATION.md) | Every environment variable. An instance is described entirely by its environment — there is no configuration file, on purpose. |
19
- | [`MIGRATIONS.md`](MIGRATIONS.md) | What happens to a database **already in service** when the player expects a newer schema. (French.) |
20
- | [`VERIFYING-RELEASES.md`](VERIFYING-RELEASES.md) | how to check, yourself, that the package or image you pulled is the one this repository built — the commands, the expected output, and which signing identity to expect. |
21
- | [`RETENTION.md`](RETENTION.md) | The declared perimeter of data retention: every personal-data column has a written policy, and CI enforces that the list is complete. Also an export of the package: `require.resolve("discovery-media-player/retention")`. |
22
-
23
- ## You are contributing, or publishing a version
24
-
25
- | Document | What it gives you |
26
- |---|---|
27
- | [`../CONTRIBUTING.md`](../CONTRIBUTING.md) | how to run the benches, what review looks for, and the one rule: a behaviour worth keeping is worth a test that fails without it. |
28
- | [`../AGENTS.md`](../AGENTS.md) | the conventions that are not obvious from the file tree — which ones a guard enforces, and which ones only review does. |
29
- | [`RELEASING.md`](RELEASING.md) | the release train, freezing the candidate SHA, the read-only preflight to run **before** the tag, and what to do when a tag lands on the wrong commit. |
30
- | [`DEPENDENCIES.md`](DEPENDENCIES.md) | what the project depends on and why so little of it: the bar a new dependency has to clear, how updates arrive, the two upgrades deliberately held back, and the vulnerability thresholds a change has to clear to merge. |
31
- | [`SECURITY-PRACTICES.md`](SECURITY-PRACTICES.md) | the two standing policies: how secrets are handled (there are no long-lived ones, on purpose) and what happens when a security tool reports something. |
32
- | [`../MAINTAINERS.md`](../MAINTAINERS.md) | who can merge, who can publish, who reads a vulnerability report — and what one maintainer means for the answer. |
33
-
34
- ## You are evaluating the project
35
-
36
- The external audit trail is public, unedited, and kept in the state it was received —
37
- an audit rewritten after the fact is no longer a trace. Findings and their fixes are
38
- tracked version by version in the [CHANGELOG](../CHANGELOG.md).
39
-
40
- | Document | What it is |
41
- |---|---|
42
- | [`THREAT-MODEL.md`](THREAT-MODEL.md) | what an attacker would go after, what stands in the way, and — the part most threat models omit — what is deliberately left standing. |
43
- | [`AUDIT-2026-08-14-RAPPORT.md`](AUDIT-2026-08-14-RAPPORT.md) | First external audit, on `0.1.17`. Historical. (French.) |
44
- | [`AUDIT-2026-08-14-SUIVI.md`](AUDIT-2026-08-14-SUIVI.md) | The follow-up ledger: done, decided-but-not-done, and rejected-with-reason. Historical. (French.) |
45
- | [`AUDIT-2026-08-15-SECONDE-PASSE.md`](AUDIT-2026-08-15-SECONDE-PASSE.md) | Second pass, on `0.1.26` — including what the first follow-up had marked too optimistically. Historical. (French.) |
46
-
47
- ## Work in progress
48
-
49
- | Document | What it is |
50
- |---|---|
51
- | [`SPEC-MEMBRE-INJECTE.md`](SPEC-MEMBRE-INJECTE.md) | A specification sent to a host for agreement **before** the contract moves. Nothing in it is implemented. (French.) |
52
-
53
- Public entry points are in English. Documents that remain in French — audit traces, and the
54
- operational documents marked *(French)* above — are labelled explicitly, so nobody discovers
55
- the language after clicking. The reasoning behind the split is at the end of the
56
- [README](../README.md#contributing).