Repository navigation
Expand file tree
/
Copy pathlecteur.js
More file actions
220 lines (211 loc) · 12.5 KB
/
Copy pathlecteur.js
File metadata and controls
220 lines (211 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// lecteur.js — lire le CONTENU d un block, pas seulement constater qu il existe.
// ================================================================================================
// ⛔ CE MODULE TRAITE DU CONTENU ECRIT PAR DES INCONNUS. Le `contractURI` d un token tiers est
// arbitraire : il peut porter un SVG contenant <script> ou un `onload`. Deux regles, aucune
// negociable, et elles vivent ICI pour etre TESTABLES :
// 1. `sourceImage` n autorise que `data:image/`, `ipfs://` et `https://`. Tout autre schema
// est REFUSE, jamais devine. `javascript:` est le cas qui compte.
// 2. l appelant pose l image via <img src="…">, JAMAIS par innerHTML. Un SVG charge par <img>
// ne PEUT PAS executer de script — c est une contrainte du navigateur, pas notre politesse.
//
// ⛔ ET CHAQUE ECHEC PORTE SON ETAT. Un cadre vide se lit comme « ce block n a rien » ; ce n est
// pas la meme affirmation que « nous n avons pas su lire ». Les deux appellent des gestes
// opposes : republier, ou reessayer. On ne les confond pas.
//
// ⚠️ Extrait du HTML pour etre teste. Tant que cette logique vivait dans une balise <script>
// inline, aucune de ces regles n etait verifiable autrement qu a l oeil.
/* ⛔⛔ DEFAUT TROUVE EN PRODUCTION, ET LE SYMPTOME ETAIT TROMPEUR. Sur le site en ligne, lire
* BLOCK 0 affichait « metadata FETCH FAILED », et la console disait CORS. Mesure
* (`sonde-passerelles-ipfs.mjs`, sept passerelles) : `ipfs.io` rend 429 — un RATE LIMIT — et
* sa reponse d erreur ne porte pas l en-tete CORS. Le navigateur signale donc « CORS » la ou
* la cause est le debit. Corriger le CORS n aurait rien corrige.
* ⛔ ET LE PIEGE DE LA MESURE : depuis Node, tout marche — Node ignore CORS. Un test fait la
* aurait valide exactement le cas casse. On mesure l EN-TETE, pas le succes du telechargement.
* Resultat du jour : 429 sur ipfs.io, dweb.link, nftstorage.link, w3s.link ;
* 200 + CORS `*` sur gateway.pinata.cloud et 4everland.io.
* ⚠️ UNE SEULE PASSERELLE EST UN POINT DE DEFAILLANCE UNIQUE sur la provenance d un jeton. On en
* essaie plusieurs, dans l ordre — et si toutes echouent, on le DIT au lieu d afficher un vide. */
export const PASSERELLES = [
'https://gateway.pinata.cloud/ipfs/',
'https://4everland.io/ipfs/',
'https://ipfs.io/ipfs/',
'https://dweb.link/ipfs/',
];
export const PASSERELLE_PAR_DEFAUT = PASSERELLES[0];
/**
* Echappe une valeur destinee a un ATTRIBUT HTML entre guillemets.
*
* ⛔ NE PAS CONFONDRE AVEC UN ECHAPPEUR DE TEXTE. L app posait la source de l image avec un
* helper bati sur `textContent -> innerHTML` : il echappe `<`, `>` et `&`, mais **PAS le
* guillemet ni l apostrophe**. Mesure du 2026-09-04, sur le site EN LIGNE :
*
* data:image/svg+xml,<svg/>" data-x="oui <- accepte par la liste blanche
* -> <img src="…"> obtenue avec l attribut `data-x` INJECTE
*
* Le champ `image` d un block tiers est ecrit par son auteur. `onerror=` s executerait.
*
* ⛔ ET LA GARDE PRECEDENTE ETAIT VRAIE MAIS INSUFFISANTE. Le module affirmait qu un SVG charge
* par `<img src>` ne peut pas executer de script — c est exact, et ca protege contre
* l execution DEPUIS le SVG. Ca ne protege pas contre l injection DANS la balise. Une garde
* correcte peut couvrir la mauvaise moitie du probleme.
*
* ⚠️ `&` D ABORD, sinon on re-echappe les entites qu on vient d ecrire.
*/
export function enAttribut(v) {
return String(v)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* OU VIT l apparence d un jeton, et ce qu elle survit.
* ================================================================================================
* ⛔ C EST LA QUESTION DE LA PERSONNALISATION, PAS UN DETAIL TECHNIQUE. Un block « personnalise »
* dont le nom et l image disparaissent si un serveur ferme n est pas personnalise : il est
* loue. Et rien dans l interface ne le disait — la difference etait invisible.
*
* ⛔ MESURE DU 2026-09-07, sur la chaine, pas sur des pages :
* un jeton openlaunch -> `https://openlaunch.lol/api/launch/meta/…` (leur serveur)
* notre BLOCK 0 -> `ipfs://bafkrei…`
* ce que l app produit -> `data:application/json;base64,…` (dans la transaction)
* Leur page /rules dit « stored by this site and can change » et leur contrat dit « immutable
* after construction ». Les DEUX sont vrais : l URI est figee, mais elle POINTE vers un serveur
* dont le contenu bouge. Il fallait lire la chaine pour les reconcilier.
*
* ⛔ ET ON NE JUGE PAS. Un URI vers un serveur permet de corriger une faute de frappe ou une image
* ratee ; un URI grave ne le permet pas — mesure : personne ne peut changer l image d un block,
* pas meme nous. Ce sont deux compromis OPPOSES, pas un bon et un mauvais. On dit OU vit la
* donnee et QUI doit rester en vie ; le lecteur conclut.
*/
export function ouVitLApparence(uri) {
if (typeof uri !== 'string' || uri === '') {
return { ou: 'AUCUNE', depend: null, note: 'this token carries no metadata at all' };
}
if (uri.startsWith('data:')) {
return { ou: 'IN THE TRANSACTION', depend: null,
note: 'the name and the image are inside the chain itself — no server, no gateway, nothing to keep alive' };
}
if (uri.startsWith('ipfs://')) {
return { ou: 'IPFS', depend: 'someone pinning it',
note: 'no single party owns it, but it survives only while someone keeps a copy pinned' };
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
let hote = null;
/* ⚠️ Une URL illisible n est pas une URL vers un serveur connu : on ne devine pas un hote. */
try { hote = new URL(uri).host; } catch { return { ou: 'A SERVER', depend: 'an unreadable URL', note: 'the URL could not be parsed' }; }
return { ou: 'A SERVER — ' + hote, depend: hote,
note: 'if ' + hote + ' goes away, the token stays but its name and image do not' };
}
return { ou: 'UNKNOWN SCHEME', depend: null, note: 'starts with: ' + uri.slice(0, 24) };
}
/** Classe un contractURI SANS aller le chercher. Pur, donc testable sans reseau. */
export function classerUri(u, passerelle = PASSERELLE_PAR_DEFAUT) {
if (typeof u !== 'string' || u === '') return { type: 'VIDE' };
if (u.startsWith('data:application/json;base64,')) return { type: 'DATA64', charge: u.slice(29) };
if (u.startsWith('data:application/json,')) return { type: 'DATA', charge: u.slice(22) };
/* ⚠️ `cid` EST RENDU EN PLUS DE `url`, pas a la place : `url` reste ce qu il etait pour les
* 18 assertions existantes, et `cid` permet de reconstruire l URL sur une AUTRE passerelle.
* Sans lui, le repli multi-passerelles retomberait en silence sur une seule. */
if (u.startsWith('ipfs://')) return { type: 'IPFS', cid: u.slice(7), url: passerelle + u.slice(7) };
if (u.startsWith('http://') || u.startsWith('https://')) return { type: 'HTTP', url: u };
return { type: 'INCONNU', brut: u.slice(0, 60) };
}
/**
* Source utilisable pour une balise <img>, ou `null`.
* ⛔ LISTE BLANCHE, PAS LISTE NOIRE. Interdire `javascript:` laisserait passer `vbscript:`,
* `data:text/html`, un espace de tete, une casse melangee. N autoriser QUE ce qu on sait sur
* est la seule forme qui ne se contourne pas par une variante qu on n avait pas prevue.
*/
export function sourceImage(v, passerelle = PASSERELLE_PAR_DEFAUT) {
if (typeof v !== 'string' || v === '') return null;
if (v.startsWith('data:image/')) return v;
if (v.startsWith('ipfs://')) return passerelle + v.slice(7);
if (v.startsWith('https://')) return v;
return null;
}
/** Decode une charge base64 en texte UTF-8. Leve si la charge n est pas du base64 valide. */
function depuisBase64(charge) {
const bin = typeof atob === 'function'
? atob(charge)
: Buffer.from(charge, 'base64').toString('binary');
/* ⛔ Node accepte du base64 invalide en le TRONQUANT au lieu de lever. On re-encode et on
* compare : si l aller-retour ne rend pas la charge d origine, l entree n etait pas du base64.
* Sans ce controle, une charge corrompue produirait un JSON partiel — donc « illisible » au lieu
* de « invalide », c est-a-dire la mauvaise cause servie a l utilisateur. */
const retour = typeof btoa === 'function'
? btoa(bin)
: Buffer.from(bin, 'binary').toString('base64');
const norme = (s) => s.replace(/=+$/, '');
if (norme(retour) !== norme(charge)) throw new Error('base64 invalide');
return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
}
/**
* Recupere et decode les metadonnees. `chercher` est injecte pour rester testable hors reseau.
* Rend TOUJOURS un etat nomme — jamais `null`, jamais une exception qui remonte.
*/
export async function metadonnees(uri, { chercher = fetch, passerelle = PASSERELLE_PAR_DEFAUT } = {}) {
const c = classerUri(uri, passerelle);
if (c.type === 'VIDE') return { etat: 'NO URI', note: 'This block carries no contractURI. Nothing was engraved.' };
if (c.type === 'INCONNU') return { etat: 'UNKNOWN SCHEME', note: 'URI starts with: ' + c.brut };
let texte;
try {
if (c.type === 'DATA64') texte = depuisBase64(c.charge);
else if (c.type === 'DATA') texte = decodeURIComponent(c.charge);
else {
/* ⚠️ DEPENDANCE EXTERNE, et elle se dit : une passerelle IPFS peut etre lente, filtree ou
* absente. « injoignable » n est PAS « le block est vide ». */
/* ⛔ ON ESSAIE CHAQUE PASSERELLE, ET ON GARDE LA CAUSE DE CHACUNE. Abandonner a la premiere
* confondait « cette passerelle nous limite » avec « ce block n a pas de metadonnees » —
* deux faits opposes, et c est le second qui s affichait a l utilisateur.
* ⚠️ Seul le type IPFS a des alternatives : une URL http explicite pointe la ou elle
* pointe, et la remplacer serait aller chercher ailleurs ce que le jeton designe. */
const candidats = c.type === 'IPFS' && c.cid
? PASSERELLES.map((p) => p + c.cid) : [c.url];
const causes = [];
let lu = null;
/* ⛔⛔ ON GARDE LE PREMIER STATUT HTTP RENCONTRE, ET J AVAIS DETRUIT CETTE DISTINCTION.
* « GATEWAY 404 » dit que le contenu n est PAS LA — le geste est de republier.
* « FETCH FAILED » dit qu on n a pas pu joindre — le geste est de reessayer.
* Ma premiere version ecrasait tout sur le second : elle aurait fait reessayer
* indefiniment un contenu qui n existe pas. C est exactement la confusion que l en-tete
* de ce fichier interdit, et un test l a rattrapee. */
let premierStatut = null;
for (const url of candidats) {
try {
const r = await chercher(url);
if (!r.ok) {
if (premierStatut === null) premierStatut = r.status;
/* ⚠️ L URL COMPLETE, PAS SEULEMENT L HOTE — un test l a exige et il avait raison :
* c est le CID qui identifie le contenu, et « pinata 404 » ne dit pas QUOI a
* manque. Quatre URL longues valent mieux qu une cause qu on ne peut pas rejouer. */
causes.push(url + ' → ' + r.status);
continue;
}
lu = await r.text();
break;
} catch (e) { causes.push(url + ' → ' + (e.message || 'echec')); }
}
if (lu === null && premierStatut !== null) {
return { etat: 'GATEWAY ' + premierStatut, note: causes.join(' · '), essayees: candidats.length };
}
if (lu === null) {
/* ⛔ ON GARDE LE NOM D ETAT EXISTANT. J avais invente « GATEWAYS FAILED » : deux tests sont
* tombes, et surtout l interface BRANCHE sur `FETCH FAILED`. Renommer un etat sur
* lequel d autres branches s appuient, c est le motif « un nouvel etat a besoin de sa
* branche PARTOUT » — et ici le gain aurait ete nul. La nouveaute va dans la CAUSE. */
return { etat: 'FETCH FAILED', note: causes.join(' · '),
/* ⛔ ON NOMME COMBIEN ONT ETE ESSAYEES : « echec » sur une passerelle et sur quatre ne
* disent pas la meme chose sur la disponibilite du contenu. */
essayees: candidats.length };
}
texte = lu;
}
} catch (e) {
return { etat: c.type === 'DATA64' ? 'INVALID BASE64' : c.type === 'DATA' ? 'INVALID ENCODING' : 'FETCH FAILED',
note: e.message };
}
try { return { etat: 'LU', doc: JSON.parse(texte), source: c.type }; }
catch (e) { return { etat: 'UNREADABLE JSON', note: e.message + ' — ' + texte.slice(0, 80) }; }
}