discovery-media-player 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.
- package/CONTRAT.md +515 -0
- package/LICENSE +661 -0
- package/LICENSE-MIT +21 -0
- package/README.md +152 -0
- package/bin/__tests__/serve.test.js +84 -0
- package/bin/serve.js +115 -0
- package/context/__tests__/storage.test.js +99 -0
- package/context/standalone.js +224 -0
- package/context/storage.js +230 -0
- package/package.json +72 -0
- package/server/brands.js +44 -0
- package/server/browser.generated.js +7 -0
- package/server/handler.js +2657 -0
- package/server/presentations.js +319 -0
- package/server/shared.generated.js +93 -0
- package/server/shares.js +275 -0
- package/src/__tests__/bridge.test.ts +108 -0
- package/src/__tests__/chat.test.ts +138 -0
- package/src/__tests__/live.test.ts +211 -0
- package/src/__tests__/presentation-content.test.ts +132 -0
- package/src/__tests__/presentation-state.test.ts +81 -0
- package/src/__tests__/tracking.test.ts +217 -0
- package/src/__tests__/viewer.test.ts +133 -0
- package/src/bridge.ts +141 -0
- package/src/chat.ts +103 -0
- package/src/index.ts +14 -0
- package/src/live.ts +225 -0
- package/src/presentation-content.ts +109 -0
- package/src/presentation-state.ts +93 -0
- package/src/tracking.ts +250 -0
- package/src/viewer.ts +109 -0
- package/supabase/init.sql +242 -0
package/CONTRAT.md
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
# Contrat du player — v1
|
|
2
|
+
|
|
3
|
+
> **Le point de passage entre les projets.** Ce fichier est la seule source de vérité sur ce qu'un
|
|
4
|
+
> projet hôte peut appeler. Toute session (studio ou ADV) le lit avant de toucher à l'intégration.
|
|
5
|
+
>
|
|
6
|
+
> **Une seule SOURCE, deux formes de consommation** (décidé le 13/08/2026) :
|
|
7
|
+
> - **3D Discovery** installe le player comme **dépendance**. Son URL et sa base ne bougent pas —
|
|
8
|
+
> des liens `/doc/:slug` sont déjà chez des prospects, on ne les casse pas.
|
|
9
|
+
> - **Le second hôte** en fait tourner un **déploiement autonome**, sur son domaine et sa base.
|
|
10
|
+
> Aucune donnée ADV ne transite ni ne se stocke chez 3D Discovery. Données sensibles.
|
|
11
|
+
>
|
|
12
|
+
> Une amélioration du cœur profite aux deux au déploiement suivant, sans rien reprendre. Ce fichier
|
|
13
|
+
> ne décrit donc **que la frontière**, pas l'intérieur.
|
|
14
|
+
|
|
15
|
+
## Les cinq règles
|
|
16
|
+
|
|
17
|
+
1. **Un seul dépôt de vérité.** Le player se corrige dans le studio (`player/`, `api/doc.js`,
|
|
18
|
+
`api/_player-context.js`). **Jamais dans un hôte.** Un correctif écrit côté hôte est une copie
|
|
19
|
+
qui divergera — le précédent est documenté : un runtime copié dans 4 dépôts, 3 sur 4 servaient
|
|
20
|
+
une version périmée sans que personne ne le voie.
|
|
21
|
+
2. **Additif par défaut.** Ajouter une action, un paramètre ou un champ ne casse aucun hôte : c'est
|
|
22
|
+
libre, ça se note au journal. **Retirer ou renommer est une rupture** → nouvelle version, les
|
|
23
|
+
deux servies pendant la migration.
|
|
24
|
+
3. **⚠️ Ordre de déploiement : le player part AVANT les hôtes.** L'inverse fait disparaître la
|
|
25
|
+
fonctionnalité partout, d'un coup, sans erreur visible.
|
|
26
|
+
4. **L'hôte épingle la version qu'il vise** et un test le vérifie. Une dérive doit être bruyante,
|
|
27
|
+
pas silencieuse. Le player expose sa carte d'identité sur **`GET /api/doc?contract=1`** — sans
|
|
28
|
+
session, sans base, et sans cache : c'est un point de diagnostic, il doit répondre même quand le
|
|
29
|
+
reste ne va pas.
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{ "product": "discovery-media-player", "contract": 1, "version": "0.1.0",
|
|
33
|
+
"capabilities": ["docshare", "presentations", "embed-denied", "host-fetch", "brand-reference"],
|
|
34
|
+
"plugins": { "bot": true, "visitors": true, "brandIntro": true, "botBrowser": true, "providerQuotas": true } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**`contract` est LE champ à épingler** : il ne bouge que sur une rupture (règle 2) — ajouter une
|
|
38
|
+
action, un paramètre ou un motif de refus ne le change pas. `capabilities` se teste par
|
|
39
|
+
PRÉSENCE, jamais par ordre. `plugins` permet à un hôte de refuser de démarrer si le mur d'accès
|
|
40
|
+
manque alors qu'il compte dessus. Aucune URL, aucun secret, aucun nom d'hôte n'y figure — un
|
|
41
|
+
point de diagnostic qui divulgue sa configuration est un cadeau à qui le sonde.
|
|
42
|
+
|
|
43
|
+
**Qui prévient qui.** Une PR d'hôte qui exige une version plus récente l'écrit **dans son titre**
|
|
44
|
+
(« requiert player ≥ v2 »). Elle ne peut pas être mergée avant que l'instance correspondante soit
|
|
45
|
+
déployée. C'est la seule règle nécessaire : il n'y a qu'une personne qui déploie les deux.
|
|
46
|
+
5. **Un besoin d'hôte se demande, il ne se code pas sur place.** Section « Demandes » en bas.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Surface v1
|
|
51
|
+
|
|
52
|
+
### Pages servies
|
|
53
|
+
|
|
54
|
+
| URL | Public | Rôle |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `/doc/:slug` | prospect anonyme | lien tracé par destinataire (suivi complet) |
|
|
57
|
+
| `/present/:slug` | audience anonyme | page spectateur d'une présentation en direct |
|
|
58
|
+
| `/api/doc?preview=1&url=…` | membre de l'hôte | aperçu interne (pas de lien tracé, suivi interne séparé) |
|
|
59
|
+
|
|
60
|
+
**Paramètres de l'aperçu interne** : `url` (obligatoire, doit passer l'allowlist de Storage),
|
|
61
|
+
`name`, `title`, `docId`, `by` (nom du présentateur), `av` (avatar), `uemail` (membre — c'est lui
|
|
62
|
+
qui déclenche le suivi interne), `autopresent=1`, `resume=<slug>`, `embed=1`.
|
|
63
|
+
|
|
64
|
+
### Pont `postMessage` (iframe ↔ hôte)
|
|
65
|
+
|
|
66
|
+
Décrit une seule fois dans `player/src/bridge.ts`, importé des deux côtés. Validation **par type**,
|
|
67
|
+
pas par origine (un contrôle d'origine strict bloquait la réception en production).
|
|
68
|
+
|
|
69
|
+
- **player → hôte** : `close`, `share`, `embed-ready`, `embed-denied {reason}`, `present-left`,
|
|
70
|
+
`present-denied`, `present-invite {slug}`, `present-handover {slug}`, `present-switch {slug}`
|
|
71
|
+
- **hôte → player** : `handover-done`
|
|
72
|
+
|
|
73
|
+
Le `slug` traversant la frontière est borné (`[A-Za-z0-9_-]{1,64}`), le `reason` aussi
|
|
74
|
+
(`[a-z-]{1,40}`, ramené à `unknown` sinon).
|
|
75
|
+
|
|
76
|
+
**`bridge.ts` est importable, et c'est le but** — il est publié sous MIT précisément pour qu'un hôte
|
|
77
|
+
n'ait pas à recopier des constantes. Un `3dd-doc-embed-ready` recopié à la main est un contrat qui
|
|
78
|
+
existe en deux exemplaires : le jour où le préfixe change, un seul des deux le sait. Un hôte qui ne
|
|
79
|
+
peut pas l'importer (autre langage, autre chaîne de build) copie le fichier *tel quel* et note d'où
|
|
80
|
+
il vient, plutôt que d'en extraire trois chaînes de caractères.
|
|
81
|
+
|
|
82
|
+
### Actions `POST /api/doc`
|
|
83
|
+
|
|
84
|
+
- **Suivi** (public, sans authentification) : événements `open` / `page` / `heartbeat` / `session`.
|
|
85
|
+
Les sessions **internes** portent `internal:true` — deux populations, jamais fusionnées.
|
|
86
|
+
- **Présentation** : `present-start|page|end|touch` (démarrage public, pilotage par jeton),
|
|
87
|
+
`present-attend` (présence), `present-chat`, `present-react`, `present-msg-edit|delete`,
|
|
88
|
+
`present-chatlock`, `present-upload-url`.
|
|
89
|
+
- **Présentation, membre authentifié (JWT)** : `present-list|reclaim|handover|owner-end|stats|
|
|
90
|
+
doc-list|switch|content`.
|
|
91
|
+
- **Re-partage** : `reshare`.
|
|
92
|
+
- **Liens tracés, membre authentifié (JWT)** : `docshare.create|list|revoke|setauth|overview|sessions|test`.
|
|
93
|
+
⚠️ **QUI a le droit est une règle de l'HÔTE**, pas du player : elle passe par le contexte,
|
|
94
|
+
`identity.canManageShares(user, action)`. Le player vérifie le jeton ; il ne connaît pas les
|
|
95
|
+
rôles métier d'une application qu'il ne connaît pas. Sans réponse de l'hôte, ou en cas de panne
|
|
96
|
+
de sa règle : **refus**. Un droit qu'on ne sait pas accorder ne s'accorde pas.
|
|
97
|
+
|
|
98
|
+
⚠️ **L'ACTION est transmise** (`create`, `list`, `list.all`, `revoke`, `setauth`, `overview`,
|
|
99
|
+
`sessions`, `test`). Un droit unique pour les sept confondrait deux choses : **envoyer un
|
|
100
|
+
document à SON prospect** est un acte commercial ordinaire ; **révoquer le lien d'un autre** ou
|
|
101
|
+
**lire l'aperçu global** est un acte d'administration. Avec un seul droit, ou bien les commerciaux
|
|
102
|
+
ne peuvent rien envoyer, ou bien chacun révoque les liens de tout le monde. Un hôte sans cette
|
|
103
|
+
distinction ignore simplement le paramètre.
|
|
104
|
+
|
|
105
|
+
`list.all` est une question SUPPLÉMENTAIRE posée lors d'une liste : répondre non restreint la
|
|
106
|
+
réponse aux liens créés par le demandeur. Sans elle, un commercial verrait à qui d'autre le
|
|
107
|
+
document a été envoyé — les prospects de ses collègues.
|
|
108
|
+
|
|
109
|
+
**Limite connue — deux portées, pas trois.** Le player ne connaît que « tous » et « les miens ».
|
|
110
|
+
Un hôte dont le modèle a une portée intermédiaire (l'équipe, l'agence, le périmètre) ne peut pas
|
|
111
|
+
l'exprimer : soit il accorde tout, soit il restreint au demandeur. Signalé par ADV, dont les
|
|
112
|
+
chefs d'équipe ont une portée `all` maison — la mapper telle quelle leur donnerait les
|
|
113
|
+
destinataires de leurs collègues, c'est-à-dire la fuite que `list.all` vient de fermer. Ils s'en
|
|
114
|
+
tiennent donc à la règle du player.
|
|
115
|
+
|
|
116
|
+
**Le jour où ça deviendra nécessaire**, la forme est déjà claire : `canManageShares` répondrait
|
|
117
|
+
`true` (tout), `false` (rien), ou **une liste d'emails** dont le demandeur a le droit de voir les
|
|
118
|
+
liens. Le player filtrerait sur `created_by`, sans jamais rien savoir de l'organisation de
|
|
119
|
+
l'hôte. Non fait aujourd'hui : aucun hôte n'a la population pour l'utiliser, et un chemin de code
|
|
120
|
+
sans usage réel est un chemin non éprouvé.
|
|
121
|
+
|
|
122
|
+
⚠️ **La règle de l'hôte doit être évaluée EN DIRECT, jamais recopiée dans le jeton.** Un rôle
|
|
123
|
+
miroité dans `app_metadata` à la connexion ne peut, par construction, pas refléter une
|
|
124
|
+
**désactivation** : il reste périmé jusqu'à expiration du jeton, c'est-à-dire précisément dans le
|
|
125
|
+
seul cas qui compte. Quelqu'un ayant quitté l'entreprise continuerait à créer des liens vers des
|
|
126
|
+
prospects.
|
|
127
|
+
- **Mur d'accès visiteur** (greffon) : `visitor-request|verify|google`.
|
|
128
|
+
|
|
129
|
+
### D'où le player tire un fichier — « l'hôte sert le fichier »
|
|
130
|
+
|
|
131
|
+
Le player **ne stocke jamais** un document : il le relaie, et seulement depuis une **origine
|
|
132
|
+
déclarée** (garde anti-SSRF). Un hôte dont les documents ne vivent pas dans un Storage joignable
|
|
133
|
+
directement — cas d'ADV, dont **99 % des documents sont sur le serveur de fichiers de l'appli
|
|
134
|
+
un serveur de fichiers tiers**, derrière une clé d'API — expose **une route à lui** et n'autorise qu'elle.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
┌─▶ Serveur tiers
|
|
138
|
+
prospect ──▶ player /doc/:slug?file=1 ──▶ route de l'hôte ─┼─▶ Partage
|
|
139
|
+
authentifie ├─▶ Storage de l'hôte
|
|
140
|
+
└─▶ Dropbox, Drive…
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Le player ne voit qu'UNE porte, quel que soit le nombre de sources derrière.** C'est la
|
|
144
|
+
propriété qui compte : un hôte peut brancher un nouveau service de fichiers sans que le player
|
|
145
|
+
change d'une ligne, et sans qu'aucun lien déjà envoyé cesse de fonctionner. Les connecteurs sont
|
|
146
|
+
l'affaire de l'hôte — le player n'a ni à les connaître, ni à porter leurs identifiants.
|
|
147
|
+
|
|
148
|
+
⚠️ **Conséquence pour l'hôte : prévoir la multi-source DÈS la première.** La référence signée que
|
|
149
|
+
la route reçoit doit dire de QUELLE source vient le fichier (`{source, ref}`), même s'il n'y en a
|
|
150
|
+
qu'une au début. La coder à la forme d'un seul fournisseur oblige à reprendre la route et tous ses
|
|
151
|
+
appelants au deuxième. Et le garde-fou anti-SSRF de l'hôte devient une allowlist par source, pas
|
|
152
|
+
une origine unique.
|
|
153
|
+
|
|
154
|
+
⚠️ **Le player ne porte JAMAIS les identifiants d'un tiers.** Ils appartiennent à l'hôte, comme la
|
|
155
|
+
relation avec ce tiers. Autoriser directement l'origine du tiers donnerait au player le droit d'y
|
|
156
|
+
lire n'importe quoi, avec des identifiants : c'est précisément ce que la garde interdit.
|
|
157
|
+
|
|
158
|
+
**Trois exigences sur la route de l'hôte, faute de quoi l'expérience se dégrade sans erreur :**
|
|
159
|
+
|
|
160
|
+
1. **Elle doit relayer les requêtes `Range`** (répondre `206` + `Accept-Ranges: bytes`). C'est de
|
|
161
|
+
là que vient le chargement progressif : les premières pages s'affichent sans télécharger tout le
|
|
162
|
+
PDF. Sans Range, un document lourd reste blanc plusieurs secondes. Si la source ne sait pas
|
|
163
|
+
faire de Range, c'est à la route de l'hôte de le simuler.
|
|
164
|
+
2. **Elle doit accepter un appel SERVEUR À SERVEUR.** Un lien tracé `/doc/:slug` est ouvert par un
|
|
165
|
+
prospect **sans session chez l'hôte** : c'est l'instance du player qui va chercher le fichier,
|
|
166
|
+
pas le navigateur. La route reconnaît donc l'instance par un **secret partagé**
|
|
167
|
+
(`PLAYER_HOST_FETCH_SECRET`, en-tête serveur), et le player ne l'appelle que pour un partage
|
|
168
|
+
existant et non révoqué.
|
|
169
|
+
3. **Elle ne doit JAMAIS relayer le `Content-Length` reçu de sa source.** La plus chère des trois,
|
|
170
|
+
et la seule qui ne se voit pas. `fetch()` **décompresse le corps pour vous et garde les en-têtes
|
|
171
|
+
reçus** : si la source (CDN, S3, proxy) a répondu en `gzip`, renvoyer fidèlement son
|
|
172
|
+
`Content-Length` annonce la taille du COMPRESSÉ alors que vous servez du décompressé. Le lecteur
|
|
173
|
+
coupe à l'octet annoncé → **PDF tronqué**, sans erreur nulle part. Ce n'est plus « le chargement
|
|
174
|
+
progressif ne marche pas », c'est « le document est faux ».
|
|
175
|
+
|
|
176
|
+
La règle tient en une phrase : **annoncez la taille des octets que vous envoyez, jamais celle que
|
|
177
|
+
vous avez reçue.** Demandez `Accept-Encoding: identity` en amont, et **refusez un `206` porteur
|
|
178
|
+
d'un `Content-Encoding`** : les bornes d'un fragment portent sur les octets compressés, et un
|
|
179
|
+
morceau de gzip ne se décompresse pas seul — mieux vaut un `502` bruyant qu'un document faux.
|
|
180
|
+
|
|
181
|
+
*(Le player applique lui-même cette règle depuis le 13/08 — `relayerFichier()`, un seul chemin
|
|
182
|
+
pour ses trois routes de streaming. Le piège nous concernait aussi.)*
|
|
183
|
+
|
|
184
|
+
### Un refus se dit — `embed-denied`
|
|
185
|
+
|
|
186
|
+
Un hôte qui intègre la visionneuse (`?embed=1`) attend `embed-ready`. Il est tentant d'en faire un
|
|
187
|
+
délai d'attente : « rien reçu en 5 s ⇒ le player est absent ⇒ j'ouvre le document avec le lecteur du
|
|
188
|
+
navigateur ». **C'est un trou de sécurité**, parce que le silence recouvre deux cas opposés :
|
|
189
|
+
|
|
190
|
+
| Ce que l'hôte observe | Ce que ça peut vouloir dire | Ce que le repli produit |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| pas d'`embed-ready` | instance absente, en panne, en déploiement | ✅ repli légitime |
|
|
193
|
+
| pas d'`embed-ready` | le player **refuse** : lien révoqué, mur d'accès, greffon manquant | ❌ **ouvre le document que le player venait de fermer** |
|
|
194
|
+
|
|
195
|
+
Le player émet donc `embed-denied` avec un `reason`. **Tous** ses chemins de refus l'émettent —
|
|
196
|
+
lien tracé, aperçu interne, page d'audience — et leurs pages restent volontairement encadrables en
|
|
197
|
+
mode intégré : sinon le navigateur bloque le rendu et le message ne partirait pas.
|
|
198
|
+
|
|
199
|
+
| `reason` | Ce qui s'est passé | **Conduite de l'hôte** | Où ça se soigne |
|
|
200
|
+
|---|---|---|---|
|
|
201
|
+
| `revoked` | lien inconnu ou révoqué | **ne pas ouvrir** | chez l'hôte : le lien n'a plus lieu d'être |
|
|
202
|
+
| `auth-required` | document réservé, visiteur non connecté | **ne pas ouvrir** | nulle part : le mur reste affiché, on peut s'y connecter |
|
|
203
|
+
| `auth-unavailable` | document réservé, mur d'accès absent de l'instance | **ne pas ouvrir** | configuration du player (greffon `visitors` coupé) |
|
|
204
|
+
| `ended` | présentation terminée ou inconnue | ne pas ouvrir | nulle part : elle est finie |
|
|
205
|
+
| `url-not-allowed` | l'URL du fichier n'est pas couverte par la garde | **OUVRIR** — et signaler la configuration | configuration du player (`PLAYER_STORAGE_ORIGINS`, `PLAYER_HOST_FETCH_BASE`) |
|
|
206
|
+
|
|
207
|
+
⚠️ **`url-not-allowed` est la seule exception à « on ne replie pas », et il faut la lire avec soin.**
|
|
208
|
+
Les deux motifs de *configuration* demandent des conduites **opposées**, ce que la seule colonne
|
|
209
|
+
« où ça se soigne » ne disait pas :
|
|
210
|
+
|
|
211
|
+
- avec `url-not-allowed`, **aucune décision d'accès n'a été prise** — le player n'a pas pu atteindre
|
|
212
|
+
le fichier. Le traiter comme un refus affiche « Document indisponible » à un membre qui a
|
|
213
|
+
parfaitement le droit de lire, et l'envoie chercher un document disparu là où c'est une variable
|
|
214
|
+
d'environnement qui est en cause ;
|
|
215
|
+
- avec `auth-unavailable` au contraire, le document **était** censé être protégé et c'est le mur qui
|
|
216
|
+
manque : ouvrir contournerait la protection.
|
|
217
|
+
|
|
218
|
+
Et ce qui rend l'exception sûre plutôt que commode : **`url-not-allowed` n'est émis que par l'aperçu
|
|
219
|
+
interne**, jamais sur un lien tracé public. Le lecteur qui le reçoit est un membre de l'hôte, déjà
|
|
220
|
+
authentifié chez lui, qui a le droit de lire ce document — l'hôte n'ouvre donc rien qu'il n'aurait
|
|
221
|
+
pas ouvert de toute façon. Si un jour ce motif apparaissait sur un chemin public, cette ligne du
|
|
222
|
+
tableau devrait changer AVANT.
|
|
223
|
+
|
|
224
|
+
La règle sous-jacente, plus sûre que la liste : **on ne replie jamais sur un refus d'ACCÈS ; on peut
|
|
225
|
+
replier sur une incapacité à ATTEINDRE.** *(Distinction relevée par ADV en rangeant les motifs — la
|
|
226
|
+
version précédente de ce tableau laissait un hôte se tromper dans un sens ou dans l'autre.)*
|
|
227
|
+
|
|
228
|
+
Ces deux motifs ressemblent pourtant tous les deux à une instance injoignable, et un hôte qui
|
|
229
|
+
branche sa première visionneuse (`?preview=1&embed=1`) les rencontrera avant tout le reste. Le
|
|
230
|
+
diagnostic tient en trois mots ; sans le motif, il coûte une demi-journée.
|
|
231
|
+
|
|
232
|
+
**Un hôte ne replie jamais après un refus d'accès** (les quatre premiers motifs) — la décision est
|
|
233
|
+
prise, il ne reste qu'à afficher son propre message. **Et « ne pas replier » vaut pour ce qu'il
|
|
234
|
+
PROPOSE, pas seulement pour ce qu'il fait automatiquement** : un lien « Ouvrir ↗ » laissé dans l'en-tête ferait du refus une
|
|
235
|
+
gêne contournable d'un clic. Ne pas replier tout seul mais offrir le bouton revient au même une
|
|
236
|
+
seconde plus tard. *(Généralisation proposée par ADV, retenue.)*
|
|
237
|
+
|
|
238
|
+
*(Le `reason` est indicatif et borné — il ne porte aucune donnée du document.)*
|
|
239
|
+
|
|
240
|
+
**L'aperçu interne porte toujours la marque de l'INSTANCE**, jamais celle d'un client : il n'accepte
|
|
241
|
+
pas de `brandKey`. C'est délibéré — l'aperçu est la surface des équipes, et un membre qui ouvre un
|
|
242
|
+
document depuis sa bibliothèque doit voir son propre outil. La marque du client sert à un lecteur
|
|
243
|
+
EXTERNE, qui ne doit pas voir le nom de l'outil qui la lui sert. Conséquence assumée : un document
|
|
244
|
+
d'un client A consulté en interne s'ouvre sous la marque de l'hôte. Ce n'est pas un défaut de
|
|
245
|
+
résolution.
|
|
246
|
+
|
|
247
|
+
### Installer une instance
|
|
248
|
+
|
|
249
|
+
**Le schéma vit avec le player** : `player/supabase/init.sql` amène une base vierge à l'état
|
|
250
|
+
attendu, en un fichier rejouable, sans rien à lire ailleurs. Ce n'est pas la suite des migrations
|
|
251
|
+
de l'hôte historique — elles sont entrelacées avec les siennes, et trier 145 fichiers est une
|
|
252
|
+
question qui n'a de bonne réponse qu'une seule fois.
|
|
253
|
+
|
|
254
|
+
⚠️ **Une base neuve s'installe DÉJÀ DURCIE.** L'avertissement sur `v12420` (ne pas retirer la
|
|
255
|
+
lecture anonyme avant d'avoir vérifié le broadcast, sous peine de figer les audiences en cours) ne
|
|
256
|
+
concerne QUE l'instance historique, qui doit sortir d'un état existant. `init.sql` ne crée aucune
|
|
257
|
+
politique de lecture publique : une instance neuve ne passe jamais par cet état, même
|
|
258
|
+
transitoirement. **Le point qui interdisait d'y mettre un dossier sensible n'y est donc jamais
|
|
259
|
+
vrai.** *(Relevé par ADV — confirmé.)*
|
|
260
|
+
|
|
261
|
+
**La dépendance** : le player est publié en **dépôt public sous AGPL-3.0**. Rien à
|
|
262
|
+
configurer chez l'hôte : ni jeton de lecture, ni clé de déploiement dans ses variables Vercel.
|
|
263
|
+
|
|
264
|
+
**`PLAYER_SOURCE_URL` pointe sur ce dépôt.** C'est ce qui rend l'obligation tenable : un lecteur
|
|
265
|
+
qui suit le lien « code source » depuis une page servie trouve exactement le code qui la sert.
|
|
266
|
+
|
|
267
|
+
Sur le **câblage** de l'hôte : il *appelle* le player, il ne le modifie pas — mais il vit dans le
|
|
268
|
+
même processus, et la position prudente est de le considérer couvert. C'est sans conséquence si on
|
|
269
|
+
le conçoit pour : **aucun secret ne doit s'y trouver en clair** (ils sont dans les variables
|
|
270
|
+
d'environnement), et ce qui reste est du branchement. Un câblage qu'on ne pourrait pas publier est
|
|
271
|
+
un câblage qui contient quelque chose qui n'a rien à y faire. *(Ce n'est pas un avis juridique :
|
|
272
|
+
c'est la posture retenue, et elle est sûre dans les deux cas.)*
|
|
273
|
+
|
|
274
|
+
### Le câblage d'une instance — à qui il appartient
|
|
275
|
+
|
|
276
|
+
Une instance autonome, c'est **un petit dépôt qui appartient à l'hôte**, pas un dépôt généré par le
|
|
277
|
+
player. Il contient quatre choses et rien d'autre :
|
|
278
|
+
|
|
279
|
+
| | Ce que c'est |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `package.json` | dépend du player |
|
|
282
|
+
| la route | une ligne : `module.exports = require("discovery-media-player").handler` |
|
|
283
|
+
| **le câblage de contexte** | l'unique fichier à écrire : `storage`, `db`, `identity`, `branding`, `limits`, `mail`, `errors` |
|
|
284
|
+
| `vercel.json` + variables | domaine, `/doc/:slug`, secrets |
|
|
285
|
+
|
|
286
|
+
**Il appartient à l'hôte parce qu'il ne contient que des décisions de l'hôte** : ses secrets, sa
|
|
287
|
+
base, qui a le droit de diffuser, quelle clé désigne quel client. Un dépôt généré par le player
|
|
288
|
+
devrait les deviner, et l'hôte n'aurait plus d'endroit où les changer. Le player fournit le moteur
|
|
289
|
+
et ce contrat ; le câblage est le seul code que l'hôte écrit — quelques centaines de lignes, une
|
|
290
|
+
fois.
|
|
291
|
+
|
|
292
|
+
`api/_player-context.js` du studio en est l'exemple de référence, et il est lisible comme tel.
|
|
293
|
+
|
|
294
|
+
### Les portes se rouvrent toutes seules
|
|
295
|
+
|
|
296
|
+
Une application a plus d'un endroit qui ouvre un document, et il en réapparaît. ADV a trouvé sur sa
|
|
297
|
+
carte publique une visionneuse de 395 lignes que personne n'avait recensée : elle affichait des
|
|
298
|
+
documents **sans passer par le player**, donc sans être comptée. Ce n'est pas un oubli ponctuel,
|
|
299
|
+
c'est la pente naturelle d'un produit vivant — un `<iframe src="....pdf">` s'écrit en dix secondes.
|
|
300
|
+
|
|
301
|
+
**Chaque hôte doit tenir la liste de ses portes et la rechasser périodiquement.** Une recherche
|
|
302
|
+
suffit : `.pdf`, `window.open`, `<embed`, `<iframe` sur un fichier, `application/pdf`. Le tableau
|
|
303
|
+
des portes recensées vit chez chaque hôte, pas ici. La règle, elle, est commune : **une porte non
|
|
304
|
+
recensée est une lecture non comptée**, et l'écart ne se voit dans aucune statistique — il se voit
|
|
305
|
+
seulement quand quelqu'un le cherche.
|
|
306
|
+
|
|
307
|
+
### Configuration côté player
|
|
308
|
+
|
|
309
|
+
| Variable | Rôle |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `PLAYER_STORAGE_ORIGINS` | origines de Storage autorisées **en plus** de celle de la base du player. ⚠️ **Reste nécessaire même avec des instances séparées** : les fichiers d'un hôte vivent dans le Storage de SON appli, pas dans la base du player. |
|
|
312
|
+
| `PLAYER_PLUGINS_OFF` | greffons coupés (`bot`, `botBrowser`, `avatarClips`, `brandIntro`, `visitors`, `providerQuotas`) |
|
|
313
|
+
| `DOC_FRAME_ANCESTORS` | domaines autorisés à encadrer la visionneuse |
|
|
314
|
+
| `GOOGLE_MAPS_API_KEY` | carte et Street View de la présentation (restreinte par référent) |
|
|
315
|
+
| `PLAYER_HOST_FETCH_BASE` | **préfixe d'URL complet** de la route de fichiers de l'hôte (ex. `https://app.exemple.fr/api/documents/`). ⚠️ Un préfixe, pas une origine : autoriser un domaine entier rendrait le player capable d'appeler n'importe quelle route de l'hôte. |
|
|
316
|
+
| `PLAYER_HOST_FETCH_SECRET` | secret partagé, envoyé en en-tête **`x-player-fetch-secret`**. ⚠️ **En-tête uniquement, jamais en query** (les journaux gardent l'URL, il fuiterait en clair des deux côtés). ⚠️ **Uniquement vers la route de l'hôte** — jamais vers un Storage public, où il n'a rien à faire. Absent côté hôte ⇒ personne ne passe ; comparaison à temps constant, ≥ 32 caractères. |
|
|
317
|
+
|
|
318
|
+
### Mentions affichées aux lecteurs
|
|
319
|
+
|
|
320
|
+
| Variable | Effet |
|
|
321
|
+
|---|---|
|
|
322
|
+
| `PLAYER_SOURCE_URL` | lien « Code source » — **obligation AGPL** |
|
|
323
|
+
| `PLAYER_LEGAL_URL` | lien « Mentions légales » de l'hôte |
|
|
324
|
+
| `PLAYER_PRIVACY_URL` | lien « Confidentialité » de l'hôte |
|
|
325
|
+
| `PLAYER_TRACKING_NOTICE` | remplace le texte de la mention de mesure |
|
|
326
|
+
|
|
327
|
+
⚠️ **L'AGPL crée une obligation que la plupart des licences n'ont pas** : quiconque **utilise** le
|
|
328
|
+
logiciel à travers un réseau doit pouvoir en obtenir le source — pas seulement celui qui le
|
|
329
|
+
distribue, celui qui l'**expose**. Un lecteur de `/doc/:slug` est un utilisateur à ce titre. Chaque
|
|
330
|
+
instance doit donc offrir cet accès, et une instance **modifiée** doit offrir **sa** version.
|
|
331
|
+
|
|
332
|
+
⚠️ **La mention de mesure s'affiche PAR DÉFAUT** sur les pages qui tracent — lien tracé et page
|
|
333
|
+
audience — et seulement sur elles. Un lien tracé enregistre qui a ouvert, quelles pages, combien de
|
|
334
|
+
temps et depuis quel appareil : c'est un traitement de données personnelles, la personne doit
|
|
335
|
+
pouvoir le savoir. **L'absence des trois liens est un choix de l'hôte ; l'absence de celle-ci est
|
|
336
|
+
un risque.** L'aperçu interne ne l'affiche pas : personne d'extérieur ne le lit.
|
|
337
|
+
|
|
338
|
+
> Le texte par défaut et la base légale retenue méritent une relecture juridique. Le player fournit
|
|
339
|
+
> l'emplacement et un texte factuel, pas un avis.
|
|
340
|
+
|
|
341
|
+
### Formats affichés
|
|
342
|
+
|
|
343
|
+
Le player n'est **pas** limité au PDF. Une **image** (`png`, `jpg`, `jpeg`, `webp`, `gif`, `avif`,
|
|
344
|
+
reconnue par le NOM du fichier — le type MIME n'est pas toujours renvoyé par la source) s'affiche
|
|
345
|
+
comme une page unique, avec tout le chrome générique : loader, zoom, plein écran, partager,
|
|
346
|
+
télécharger, **et le suivi de consultation**. Une image ouverte dans le player est donc tracée
|
|
347
|
+
exactement comme un PDF.
|
|
348
|
+
|
|
349
|
+
⚠️ **Le format se déduit du NOM du fichier, pas du type MIME** (une source de stockage ne le
|
|
350
|
+
renvoie pas toujours). Concrètement :
|
|
351
|
+
|
|
352
|
+
- pour un **lien tracé**, le nom vient de `fileName` fourni à `docshare.create` — **toujours le
|
|
353
|
+
renseigner** ;
|
|
354
|
+
- pour l'**aperçu interne**, du paramètre `&name=` ;
|
|
355
|
+
- à défaut, le player retombe sur l'URL, et l'extension doit y être **en fin de chaîne ou juste
|
|
356
|
+
avant le `?`**. Une route d'hôte à URL opaque (`/raw?d=…`) ne dit rien du format : une image y
|
|
357
|
+
serait envoyée à pdf.js et n'afficherait **rien** — écran vide, pas d'erreur. Faire porter le nom
|
|
358
|
+
par le chemin (`/raw/plan.png?d=…`) est le repli sûr.
|
|
359
|
+
|
|
360
|
+
**Autres formats** (`.docx`, `.xlsx`, …) : le player **ne les affiche pas** et n'a pas de repli —
|
|
361
|
+
il affiche « Impossible d'afficher ce document ». Ce n'est donc ni le player ni un choix de
|
|
362
|
+
l'hôte : **ces formats ne doivent pas lui être envoyés**, l'hôte les sert en téléchargement.
|
|
363
|
+
|
|
364
|
+
### Marque
|
|
365
|
+
|
|
366
|
+
Par document : `brand_logo` (URL) et `brand_dark` (loader sur fond sombre) sur le lien de partage.
|
|
367
|
+
Le loader et le mur d'accès s'y conforment déjà.
|
|
368
|
+
|
|
369
|
+
**Marque de l'ÉDITEUR** (l'entreprise qui exploite l'instance — à distinguer du logo du client,
|
|
370
|
+
qui est par document) :
|
|
371
|
+
|
|
372
|
+
| Variable | Effet |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `PLAYER_BRAND_NAME` | nom affiché : suffixe de titre d'onglet, mot-marque du loader, nom par défaut de l'assistant, objet du courriel de re-partage |
|
|
375
|
+
| `PLAYER_BRAND_POWERED_BY` | mention « Propulsé par … » en pied de page et « Powered by … » sous le logo d'un client |
|
|
376
|
+
| `PLAYER_LOADER_NAME` | marque affichée **par le loader** — repli sur `PLAYER_BRAND_NAME` |
|
|
377
|
+
|
|
378
|
+
⚠️ **TROIS identités se croisent sur une page de document, et les confondre se voit.**
|
|
379
|
+
|
|
380
|
+
| | Ce que c'est | Où |
|
|
381
|
+
|---|---|---|
|
|
382
|
+
| **Le produit** | le logiciel qui sert la page | titre d'onglet, « Propulsé par » |
|
|
383
|
+
| **L'exploitant** | l'entreprise qui fait tourner l'instance | **le loader** — la première chose que voit le lecteur |
|
|
384
|
+
| **Le client** | celui dont on montre le document (`brand_logo` sur le lien) | le loader, **à la place** de l'exploitant |
|
|
385
|
+
|
|
386
|
+
L'erreur a été commise une fois : le nom du produit s'est retrouvé sur le loader, à la place de
|
|
387
|
+
la marque attendue. D'où `PLAYER_LOADER_NAME`, distinct.
|
|
388
|
+
|
|
389
|
+
**Marque PAR CLIENT — résolue par l'hôte.** Une instance sert plusieurs clients : le loader doit
|
|
390
|
+
porter la marque de celui dont on montre le document. Le lien porte une **référence** (`brandKey`,
|
|
391
|
+
posée à la création), pas une copie du logo — et le player ne sait pas ce qu'est un client. Il
|
|
392
|
+
appelle l'hôte :
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
ctx.branding.forKey(brandKey) → { logo, name, dark } | null
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
| Champ | Rôle | ⚠️ |
|
|
399
|
+
|---|---|---|
|
|
400
|
+
| `logo` | URL affichée par le loader et le mur d'accès | doit être joignable depuis le navigateur du lecteur |
|
|
401
|
+
| `name` | **le repli quand le logo ne charge pas** — et il ne charge pas toujours | **le plus oublié.** Sans lui, un logo cassé laisse un vide au lieu d'un nom |
|
|
402
|
+
| `dark` | loader sur fond sombre | `false` par défaut |
|
|
403
|
+
|
|
404
|
+
`null` (clé inconnue, client supprimé) ⇒ le lien retombe sur la marque de l'instance. Ce n'est pas
|
|
405
|
+
une erreur : c'est le comportement attendu.
|
|
406
|
+
|
|
407
|
+
⚠️ **`name` n'est pas décoratif.** C'est précisément la valeur qui sert quand le reste échoue, donc
|
|
408
|
+
celle qu'on découvre manquante le jour où on en a besoin. Le premier hôte à câbler `forKey` l'a
|
|
409
|
+
omise en lisant ce contrat — d'où ce tableau.
|
|
410
|
+
|
|
411
|
+
**Où vit `forKey` quand l'instance est séparée.** Une instance autonome ne peut pas importer le
|
|
412
|
+
code de l'application hôte — deux projets, deux déploiements. `forKey` **appelle donc une route de
|
|
413
|
+
l'hôte**, comme l'autorisation le fait déjà. Recopier la correspondance clé → logo dans le câblage
|
|
414
|
+
serait une copie de plus : bénigne au début (les URL sont absolues, c'est le fichier servi qui
|
|
415
|
+
change quand la charte bouge), mais elle réclame une modification et un déploiement du câblage
|
|
416
|
+
chaque fois qu'un client apparaît — et surtout **elle a l'air officielle**. Une source unique qui
|
|
417
|
+
coûte un aller-retour au premier affichage vaut mieux qu'une copie qui ne coûte rien jusqu'au jour
|
|
418
|
+
où elle ment.
|
|
419
|
+
|
|
420
|
+
⚠️ **Cet appel ne doit jamais empêcher de lire.** Hôte injoignable, clé inconnue, délai dépassé ⇒
|
|
421
|
+
`null` ⇒ marque de l'instance. Le loader dégrade ; le document s'ouvre. C'est déjà le comportement
|
|
422
|
+
du player (`brandForShare` ne lève jamais), mais la route de l'hôte doit tenir la même promesse :
|
|
423
|
+
répondre vite, ou répondre rien.
|
|
424
|
+
|
|
425
|
+
Il n'y a **aucun registre de marques dans le player**, et ce n'est pas un manque : l'hôte a déjà ses
|
|
426
|
+
clients quelque part (une fiche CRM, une organisation). Un second registre à tenir à jour serait une
|
|
427
|
+
copie de plus à faire diverger. *(Un registre `brand.list|upsert|delete` a existé une demi-journée
|
|
428
|
+
avant d'être retiré pour cette raison.)*
|
|
429
|
+
|
|
430
|
+
⚠️ **Pourquoi une référence et pas une copie.** Un lien tracé vit des semaines dans une boîte mail.
|
|
431
|
+
Un logo recopié dans le lien ne bouge plus : rectifier la charte d'un client ne changerait rien à
|
|
432
|
+
ce qui est déjà parti. Résolue à l'affichage, la référence se propage à tout l'existant.
|
|
433
|
+
|
|
434
|
+
**Ordre de résolution** : référence du lien → logo recopié (flux historiques, toujours accepté) →
|
|
435
|
+
marque de l'instance. Le player ne sait pas ce qu'est un client : il ne connaît que des clés, et
|
|
436
|
+
c'est l'hôte qui décide quelle clé pour quel document.
|
|
437
|
+
|
|
438
|
+
⚠️ **NEUTRE PAR DÉFAUT, et c'est voulu.** Sans configuration, le player n'affiche la marque de
|
|
439
|
+
personne : ni titre suffixé, ni mention de pied, ni mot-marque. Envoyer à un client un document
|
|
440
|
+
portant la marque d'une autre société est une faute, pas un détail — le défaut le rend impossible.
|
|
441
|
+
Les deux variables sont indépendantes : un hôte peut vouloir son nom sans mention de pied.
|
|
442
|
+
|
|
443
|
+
⚠️ **Le studio doit poser ces deux variables avant de déployer**, faute de quoi ses pages perdent
|
|
444
|
+
« Propulsé par 3D Discovery » et le suffixe de leur titre. Rien ne casse — c'est visible, pas
|
|
445
|
+
fonctionnel — mais ça se voit tout de suite.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Journal du contrat
|
|
450
|
+
|
|
451
|
+
Toute évolution de la frontière se note ici, datée, avec sa nature.
|
|
452
|
+
|
|
453
|
+
| Date | Nature | Changement |
|
|
454
|
+
|---|---|---|
|
|
455
|
+
| 2026-08-13 | — | Ouverture du contrat en v1, sur l'existant. Aucun hôte tiers branché à ce jour. |
|
|
456
|
+
| 2026-08-13 | décision | **Isolation** : ADV aura son propre déploiement, sa propre base, son propre domaine. Le studio ne bouge pas. Une seule source de code. |
|
|
457
|
+
| 2026-08-13 | précision | En-tête du secret figé : **`x-player-fetch-secret`** (nom proposé par la session ADV, retenu pour son explicité). |
|
|
458
|
+
| 2026-08-13 | précision | Le player affiche **aussi les images**, tracées comme un PDF. Ce n'était écrit nulle part. |
|
|
459
|
+
| 2026-08-13 | additif | **Mentions aux lecteurs** : lien code source (AGPL), mentions légales, confidentialité, et une mention de mesure affichée PAR DÉFAUT sur les pages qui tracent. |
|
|
460
|
+
| 2026-08-13 | additif | **Marque du client résolue par l'hôte** (`brandKey` sur le lien → `branding.forKey`) : un logo rectifié se propage aux liens déjà envoyés, sans registre à synchroniser. Migration `v12421`. |
|
|
461
|
+
| 2026-08-13 | retiré | ~~Registre des marques~~ (`brand.list|upsert|delete`, `brandKey` à la création d'un lien) : le loader porte la marque du client, par référence — un logo rectifié se propage aux liens déjà envoyés. Migration `v12421`. Rétrocompatible. |
|
|
462
|
+
| 2026-08-13 | additif | **Portée de la liste** : `list.all` décide si `docshare.list` renvoie tous les liens d'un document ou seulement ceux du demandeur. La réponse porte `scope: "all"|"mine"`. |
|
|
463
|
+
| 2026-08-13 | additif | **`canManageShares` reçoit l'ACTION** — demandé par ADV, dont le modèle sépare l'envoi (tout conseiller) de l'administration des liens. Aucune rupture : un hôte qui ne distingue pas ignore le paramètre. |
|
|
464
|
+
| 2026-08-13 | additif | **Liens tracés dans le player** : `docshare.*` quitte la route de synchro du studio pour `/api/doc`. Nouveau point de contexte `identity.canManageShares` — l'hôte décide qui a le droit de diffuser. |
|
|
465
|
+
| 2026-08-13 | additif | **Marque de l'éditeur configurable**, neutre par défaut. Aucune mention en dur ne subsiste dans les pages servies (3 tests de non-régression, un par page). ⚠️ Le studio doit poser ses variables avant de déployer. |
|
|
466
|
+
| 2026-08-13 | additif | **Suivi par DIFFUSION** : l'audience suit l'état par broadcast sur le canal `plive-<slug>` + relecture `?present=<slug>&state=1`, en parallèle de la lecture de table. Une fois vérifié en production, la migration `v12420` retire la lecture anonyme — **et l'énumération de toutes les présentations avec la clé publiable disparaît**. ⚠️ Ordre obligatoire : déployer, vérifier, PUIS migrer. |
|
|
467
|
+
| 2026-08-13 | additif | **« L'hôte sert le fichier »** : la garde de streaming accepte une route de l'hôte comme origine, appelée serveur à serveur via `PLAYER_HOST_FETCH_SECRET`. Rendu nécessaire par ADV, dont les documents vivent sur un serveur de fichiers tiers derrière une clé d'API. Le studio n'est pas concerné (Storage direct) — **additif, aucune rupture**. |
|
|
468
|
+
| 2026-08-13 | additif | **`embed-denied`** ajouté au pont : un refus d'afficher se dit, au lieu de se confondre avec une panne. Les pages de refus deviennent encadrables en mode intégré (sinon le message ne partirait pas). **Additif** — un hôte qui l'ignore garde le comportement d'avant. |
|
|
469
|
+
| 2026-08-13 | correctif | **Taille annoncée** : les trois routes de streaming passent par `relayerFichier()`, qui envoie la taille des octets servis et jamais celle reçue de l'amont, demande `Accept-Encoding: identity` et refuse un `206` compressé. Corrige un PDF tronqué silencieux. |
|
|
470
|
+
| 2026-08-13 | correctif | **Tous les chemins de refus** émettent `embed-denied`, pas seulement celui du lien tracé : l'aperçu interne (`url-not-allowed`) et la page d'audience (`ended`) se taisaient — or l'aperçu intégré est le premier mode qu'un nouvel hôte exerce. Deux motifs ajoutés, une garde de test empêche le prochain refus muet. |
|
|
471
|
+
| 2026-08-13 | précision | **`branding.forKey` renvoie `{ logo, name, dark }`** — la forme de retour manquait au contrat, et `name` (le repli quand le logo ne charge pas) a été omis par le premier hôte qui l'a lu. Le registre `brand.*`, retiré, disparaît aussi de la section Marque. |
|
|
472
|
+
| 2026-08-13 | précision | **« On ne replie pas » vaut pour ce qu'un hôte PROPOSE**, pas seulement pour ce qu'il fait tout seul : un bouton « Ouvrir ↗ » laissé après un refus revient à replier une seconde plus tard. Généralisation proposée par ADV. |
|
|
473
|
+
| 2026-08-13 | précision | **L'aperçu interne porte la marque de l'INSTANCE**, jamais celle d'un client (pas de `brandKey`) : c'est la surface des équipes. Tranché, pas subi. |
|
|
474
|
+
| 2026-08-13 | correctif | **Barre finale de `PLAYER_HOST_FETCH_BASE` normalisée** : la comparaison est un préfixe de chaîne, donc `…/api/documents` saisi sans barre ouvrait aussi `/api/documents-prives/`. Élargissement silencieux de la garde, par une variable tapée à la main. |
|
|
475
|
+
| 2026-08-13 | précision | **Colonne « conduite de l'hôte »** sur les motifs de refus : les deux motifs de *configuration* demandaient des conduites OPPOSÉES. Règle sous-jacente — on ne replie jamais sur un refus d'ACCÈS, on peut replier sur une incapacité à ATTEINDRE. |
|
|
476
|
+
| 2026-08-13 | correctif | **`name` traverse enfin** : promis par le contrat, jeté par la garde du contexte, absent des pages. Il devient le texte de remplacement de l'image du loader et du mur d'accès — le seul repli utilisable sous une CSP à nonce. Le premier hôte qui a câblé `forKey` l'avait omis, et rien ne le contredisait. |
|
|
477
|
+
| 2026-08-13 | précision | **Le câblage d'une instance appartient à l'HÔTE** (4 fichiers, un seul à écrire) : il ne contient que des décisions de l'hôte. `forKey` d'une instance séparée **appelle une route de l'hôte** plutôt que de recopier la correspondance clé → logo. |
|
|
478
|
+
| 2026-08-13 | additif | **`GET /api/doc?contract=1`** existe enfin : la règle 4 reposait sur un point qui n'avait jamais été écrit. Carte d'identité sans session, sans base, sans cache, sans URL ni secret. |
|
|
479
|
+
| 2026-08-13 | additif | **Le schéma part avec le player** (`player/supabase/init.sql`) : un fichier rejouable qui amène une base vierge à l'état attendu, **déjà durci** — une instance neuve ne connaît jamais l'état « lecture anonyme ouverte ». |
|
|
480
|
+
| 2026-08-13 | décision | **Dépôt public dès la création, AGPL-3.0** : `PLAYER_SOURCE_URL` pointe dessus, aucun jeton ni clé de déploiement chez les hôtes. Le câblage d'un hôte est considéré couvert — il ne doit donc contenir aucun secret en clair, ce qui est de toute façon la bonne façon de l'écrire. |
|
|
481
|
+
| 2026-08-13 | décision | **Aucun nom de tiers dans le dépôt publié** : les hôtes sont désignés par leur RÔLE (« l'hôte historique », « le second hôte »), jamais par leur raison sociale, et les exemples d'URL sont fictifs. Le rôle porte toute l'information technique ; le nom ne dit qu'une chose — quelles entreprises travaillent ensemble et où leurs documents vivent. |
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Demandes des hôtes
|
|
486
|
+
|
|
487
|
+
Un hôte qui a besoin d'une évolution de la frontière l'écrit ici. Il ne la code pas chez lui.
|
|
488
|
+
|
|
489
|
+
| Date | Hôte | Demande | État |
|
|
490
|
+
|---|---|---|---|
|
|
491
|
+
| 2026-08-13 | ADV | ~~Multi-locataire : colonne `tenant`~~ | **annulée** — instances séparées, chaque base n'a qu'un hôte |
|
|
492
|
+
| 2026-08-13 | ADV | ~~JWT multi-émetteurs~~ | **annulée** — chaque instance ne connaît qu'un émetteur |
|
|
493
|
+
| 2026-08-13 | ADV | **Extraire le player en dépôt déployable** (le studio le consomme en dépendance, `api/doc.js` devient un adaptateur mince) | à faire — **prérequis de tout le reste** |
|
|
494
|
+
| 2026-08-13 | ADV | **Gestion des liens tracés accessible hors studio** : `docshare.create/list/revoke` vit dans la route de synchro derrière le modèle de droits du studio (`clients_registry`) — inappelable par un autre hôte. **Point d'intégration le plus sous-estimé.** | à faire |
|
|
495
|
+
| 2026-08-13 | ADV | **Marque de l'hôte** : retirer les mentions « 3D Discovery » en dur | ✅ **livré** — `PLAYER_BRAND_NAME` / `PLAYER_BRAND_POWERED_BY`, neutre par défaut |
|
|
496
|
+
| 2026-08-13 | ADV | **Mur d'accès Microsoft** en plus d'email+code et Google One-Tap (ADV est en SSO M365) | à faire |
|
|
497
|
+
| 2026-08-13 | ADV | **Confidentialité Realtime** : la lecture anon large interdit d'y mettre des documents sensibles | 🟡 **code prêt, migration en attente de vérification en production** (`v12420`) |
|
|
498
|
+
| 2026-08-13 | ADV | **Streaming via une route de l'hôte** (Range relayé + secret serveur à serveur) — cf. section dédiée | ✅ **livré côté player** — reste à ADV d'exposer sa route |
|
|
499
|
+
| 2026-08-13 | ADV | **Le piège de la compression** : `fetch()` décompresse et garde les en-têtes → un `Content-Length` relayé sert un PDF tronqué, sans erreur | ✅ **livré** — 3e exigence de la section, et corrigé dans le player lui-même (le piège nous concernait) |
|
|
500
|
+
| 2026-08-13 | ADV | **Distinguer « player absent » de « player refuse »** : le silence pousse l'hôte à replier sur son lecteur, ce qui ouvrirait un document refusé | ✅ **livré** — `embed-denied {reason}` + pages de refus encadrables |
|
|
501
|
+
| 2026-08-13 | ADV | **Les portes se rouvrent** : visionneuse de 395 lignes trouvée sur la carte publique, hors player donc hors comptage | ✅ **inscrit au contrat** — chaque hôte tient et rechasse la liste de ses portes |
|
|
502
|
+
| 2026-08-13 | ADV | **L'aperçu interne refusait en silence** (`?preview=1&embed=1`, le premier mode qu'un hôte exerce) | ✅ **livré** — `url-not-allowed`, plus `ended` pour l'audience ; garde de test contre le prochain refus muet |
|
|
503
|
+
| 2026-08-13 | ADV | **Forme de retour de `branding.forKey` absente du contrat** — `name` omis, or c'est le repli quand le logo ne charge pas | ✅ **livré** — section Marque réécrite (et le registre `brand.*` retiré y traînait encore) |
|
|
504
|
+
| 2026-08-13 | ADV | **Généraliser « on ne replie pas » à ce qu'on PROPOSE** (retirer aussi le bouton « Ouvrir ↗ ») | ✅ **retenu au contrat** |
|
|
505
|
+
| 2026-08-13 | ADV | **La marque de l'aperçu interne** : instance ou client ? | ✅ **tranché** — toujours l'instance, l'aperçu est la surface des équipes |
|
|
506
|
+
| 2026-08-13 | ADV | **`PLAYER_HOST_FETCH_BASE` sans barre finale élargit la garde en silence** | ✅ **livré** — normalisée dans `hostFetchBase()`, 2 tests (dont la route sœur) |
|
|
507
|
+
| 2026-08-13 | ADV | **« Où ça se soigne » ne dit pas quelle conduite tenir** — les deux motifs de configuration s'opposent | ✅ **livré** — colonne « conduite », et la règle qui la sous-tend |
|
|
508
|
+
| 2026-08-13 | ADV | **Copie du contrat en retard côté ADV** | ✅ **resynchronisée** — `docs/player-contrat-v1.md`, cartouche local conservé, déposée non commitée |
|
|
509
|
+
| 2026-08-13 | ADV | **Où vit `forKey` pour une instance séparée** — copie dans le câblage, ou route de l'hôte ? | ✅ **tranché** — route de l'hôte (b). Une copie a l'air officielle, et réclame un déploiement par client |
|
|
510
|
+
| 2026-08-13 | ADV | **`name` promis mais jamais transporté** — la garde du contexte le supprimait | ✅ **corrigé** — il atteint la page comme texte de remplacement de l'image |
|
|
511
|
+
| 2026-08-13 | ADV | **Le dépôt extrait doit emporter son schéma** (145 migrations entrelacées, base ADV vierge) | ✅ **livré** — `player/supabase/init.sql` |
|
|
512
|
+
| 2026-08-13 | ADV | **Une base neuve doit s'installer déjà durcie**, sans passer par l'état « lecture anonyme » | ✅ **confirmé** — `init.sql` ne crée aucune politique publique ; l'avertissement `v12420` ne vise que l'instance historique |
|
|
513
|
+
| 2026-08-13 | ADV | **Comment on installe** : npm public, npm privé ou git ? | ✅ **tranché** — dépôt public + npm public, rien à configurer chez l'hôte |
|
|
514
|
+
| 2026-08-13 | ADV | **Cible de `PLAYER_SOURCE_URL`** avant le premier lecteur | ✅ **tranché** — le dépôt public lui-même |
|
|
515
|
+
| 2026-08-13 | ADV | **`?contract=1` n'existait pas** alors que la règle 4 repose dessus | ✅ **livré** — forme documentée, 3 tests |
|