diff --git a/.localserve/Caddyfile b/.localserve/Caddyfile new file mode 100644 index 0000000..3351c60 --- /dev/null +++ b/.localserve/Caddyfile @@ -0,0 +1,21 @@ +# Reverse-proxy HTTPS local pour reproduire la prod sans DNS. +# verify.ai-driven-dev.fr (TLS interne de confiance Caddy) : +# - /data/* -> serveur de données local (:8200) (clés, statut, preuves, annuaire, photos) +# - le reste -> le site Astro (:4321) (page /u/, /communaute) +# Prérequis : "127.0.0.1 verify.ai-driven-dev.fr" dans /etc/hosts. +# Lancement : sudo caddy run --config .localserve/Caddyfile + +{ + local_certs +} + +verify.ai-driven-dev.fr { + handle_path /data/* { + reverse_proxy 127.0.0.1:8200 { + header_up Host 127.0.0.1:8200 + } + } + reverse_proxy 127.0.0.1:4321 { + header_up Host 127.0.0.1:4321 + } +} diff --git a/.localserve/directory.json b/.localserve/directory.json new file mode 100644 index 0000000..1fa6ebb --- /dev/null +++ b/.localserve/directory.json @@ -0,0 +1,41 @@ +{ + "generatedAt": "2026-07-22T10:00:00Z", + "count": 4, + "members": [ + { + "handle": "alexmartin", + "name": "Alexandre Martin", + "role": "certifie", + "linkedin": "https://linkedin.com/in/alexmartin", + "description": "Lead dev, orchestration d'agents en prod.", + "photo": "https://verify.ai-driven-dev.fr/data/photos/alexmartin.webp", + "verify": "https://verify.ai-driven-dev.fr/u/alexmartin" + }, + { + "handle": "crousseau", + "name": "Camille Rousseau", + "role": "certifie", + "linkedin": "https://linkedin.com/in/crousseau", + "description": "Freelance, TDD assisté par IA.", + "photo": "https://verify.ai-driven-dev.fr/data/photos/crousseau.webp", + "verify": "https://verify.ai-driven-dev.fr/u/crousseau" + }, + { + "handle": "ybenali", + "name": "Yassine Benali", + "role": "certifie", + "linkedin": "https://linkedin.com/in/ybenali", + "description": "CTO, industrialisation du code généré.", + "photo": "https://verify.ai-driven-dev.fr/data/photos/ybenali.webp", + "verify": "https://verify.ai-driven-dev.fr/u/ybenali" + }, + { + "handle": "sophiedurand", + "name": "Sophie Durand", + "role": "certifie", + "description": "Ingénieure plateforme, delivery assisté par IA.", + "photo": "https://verify.ai-driven-dev.fr/data/photos/crousseau.webp", + "verify": "https://verify.ai-driven-dev.fr/u/sophiedurand" + } + ] +} \ No newline at end of file diff --git a/.localserve/issuer.json b/.localserve/issuer.json new file mode 100644 index 0000000..9751eaf --- /dev/null +++ b/.localserve/issuer.json @@ -0,0 +1,8 @@ +{ + "id": "https://verify.ai-driven-dev.fr/data/issuer.json", + "type": [ + "Profile" + ], + "name": "AI-Driven Development", + "url": "https://ai-driven-dev.fr" +} \ No newline at end of file diff --git a/.localserve/keys/IxFMJP2YJ6_xvfJm9NmDJQCjCOecO7_p9gF_wP4s6Vg.json b/.localserve/keys/IxFMJP2YJ6_xvfJm9NmDJQCjCOecO7_p9gF_wP4s6Vg.json new file mode 100644 index 0000000..eba8f32 --- /dev/null +++ b/.localserve/keys/IxFMJP2YJ6_xvfJm9NmDJQCjCOecO7_p9gF_wP4s6Vg.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "uT5rpKXaIb1Tot7MYrwbHR6D0NMVaq0E9zUQfewI3cztXhGt9UrXDddwsH7ja8rKUYSWpPQQi0hG9f5GFv5unuxwY2lR5bJjrWCnplhTzkS18fzsepXyfHPIlmWmOmtSkf9kGaYXkqdFpSqNMjgFn1xjSp3qj652g1iNNnXUkpx7j3sNsAFhOzy-Ge8dM9zVUpCuSZBMmOP_ptfLbhxq83-pf4KAW592b-nwa9PAID987qnOt8kzUOf_fhqOr-aWz6xxEV5vF3gBOk3GC6rzND1T_CM_qb964gtrvtiDgbgig15nGDP_emPNkPNmx-nK_p2TdakphsX-EqurJFtJZQ", + "e": "AQAB", + "kid": "IxFMJP2YJ6_xvfJm9NmDJQCjCOecO7_p9gF_wP4s6Vg", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/keys/LmvU8GPHiEfXcoq_HBmKcdnjJ5X2HlOf9hFpu_t_woc.json b/.localserve/keys/LmvU8GPHiEfXcoq_HBmKcdnjJ5X2HlOf9hFpu_t_woc.json new file mode 100644 index 0000000..045430f --- /dev/null +++ b/.localserve/keys/LmvU8GPHiEfXcoq_HBmKcdnjJ5X2HlOf9hFpu_t_woc.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "1scdMm0k63y6ytCDLmdfPf_4i-hSh_vFFDMyQWGjHMeFAIVpmT9_J6Uo6zvmtOdF83jOLeZhpWTyH3xQFNojOngXGU6QLWf0yMHHnF34MwzvQDJ_wdiilctoROi1IGWk04dkYLD8sF0AC6DBZNaM5Zb5-OeI4b90EJh-LImrINSDUAsV-sBtunaSZsXqfEojgykxrPvMnOQXV7W0OIhczfhTax5dXl-NVkWoixvuq_Qe4ukSqtWu6CfznSjr4PZDTsdrTH4KV4Z-7tEUSLefojZGXnIvpQwRccND44RRWiYJquXemEEs9kTswD3ab1Zi26v93BVHo0__hwQEd7NwFw", + "e": "AQAB", + "kid": "LmvU8GPHiEfXcoq_HBmKcdnjJ5X2HlOf9hFpu_t_woc", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/keys/QUIuABtMfuvJYHqDxqWkaNS-y9HvDe2n6L3RCFQyL28.json b/.localserve/keys/QUIuABtMfuvJYHqDxqWkaNS-y9HvDe2n6L3RCFQyL28.json new file mode 100644 index 0000000..71e2baf --- /dev/null +++ b/.localserve/keys/QUIuABtMfuvJYHqDxqWkaNS-y9HvDe2n6L3RCFQyL28.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "uVNrXgACWNldmi5aFImNoCDRwIeDXfFlrsxxBu6byTXvThBDOgMQ5MoaY2JjXYPoLBQNlrR05QTtFBj7d7OpNnAmpgTbYlv7eFmGj4d5etxDxD3cCI3wdOsN7qBUDzTlyYSSLdsaKb5G0Vgpwq-i2e7YmTXDIAJCtmwRK6zvPKn-V4HydFaPUg0KYDUyo9uUSY0kTLEI9MFHUB-QKvebFSY0aFN1puBRNvBG1_sKIPoDci0sE3f9iCoQEMvV2tGVdkqnZYmCElf2WjxDTH7J6GKrCfnzEEiujG15w1W3hzxCgmiSHyctlGmqjro6QKF0vwHtNb7z43PVOV_ozC3HZQ", + "e": "AQAB", + "kid": "QUIuABtMfuvJYHqDxqWkaNS-y9HvDe2n6L3RCFQyL28", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/keys/gc8SUbVmXzSBb_damoHKv7L_lcJl3t216eqdLX5Kz1g.json b/.localserve/keys/gc8SUbVmXzSBb_damoHKv7L_lcJl3t216eqdLX5Kz1g.json new file mode 100644 index 0000000..1811008 --- /dev/null +++ b/.localserve/keys/gc8SUbVmXzSBb_damoHKv7L_lcJl3t216eqdLX5Kz1g.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "rZbYWsrQqdUsFCD32BM3m5nyFXy7G4-S1RlR5SyT-BHVurCm_w0WV3SD5wsNWq6lov6GEVN083-Uo9u0TCyLa0aAnRBDdC1NmGG_52nRqx8xiEzKQ89jWtyFizY1EVM9n17UzDksSoEtbsptxRXl6n85HSiTYLUSGhCDGoC5nG5Ho-JZ-P8NTeCRkjafd9B5NS8hOFOSq3aw1_J2OKJ7up5Ft0946Ofs6PKuVOhsdhWURQDX7RgGkUjTqEl24gdcDKfd5iWZOqKAGOBRK5b2i1j_tc4eDv4Uz48nifQ_oLAo0Bdg6k-QEJiHbyXEfKoJAtCGhmykHA4YyMwygkKbIw", + "e": "AQAB", + "kid": "gc8SUbVmXzSBb_damoHKv7L_lcJl3t216eqdLX5Kz1g", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/keys/uWeTPbqfMkp8btS6JhaZf_GFAeZphOxGmBzYHDIOyWs.json b/.localserve/keys/uWeTPbqfMkp8btS6JhaZf_GFAeZphOxGmBzYHDIOyWs.json new file mode 100644 index 0000000..a31906b --- /dev/null +++ b/.localserve/keys/uWeTPbqfMkp8btS6JhaZf_GFAeZphOxGmBzYHDIOyWs.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "nLRIbctgN7_sMRtbrCWEEZRRCgcbxCc7yjhS6MqADDRJG__zo9CPQKn2kexS4KU6Y1jCwA_AtivROdccbbkQVf60PCDWf9OfOgUpIzsZQsqHaj_9jrls2pYpgwRk1PoRkZ-spIbiBph2fk04x0OYbVm33AabMxGKgIX9KiWBTG5MTn8P11vZ_RpHfV9ExSQ3AQ1OdxiRLjWOSk7sL37nP6ewnCCIa_sVID6IZuruOJlJZFO6mveJLSQF9lH-1mzWC_VRDiis9ih4fFl-pHc5T6J51wDBXqXm7hCozLBafYH5isMq59AeTuBnNmM8ngohD7ImSQgY-3upXEcV9DHPNQ", + "e": "AQAB", + "kid": "uWeTPbqfMkp8btS6JhaZf_GFAeZphOxGmBzYHDIOyWs", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/keys/zTCsma4Wlwe1N717aiC_gWioTwUg-pwoFU5R9BHgPCg.json b/.localserve/keys/zTCsma4Wlwe1N717aiC_gWioTwUg-pwoFU5R9BHgPCg.json new file mode 100644 index 0000000..00f9704 --- /dev/null +++ b/.localserve/keys/zTCsma4Wlwe1N717aiC_gWioTwUg-pwoFU5R9BHgPCg.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "n": "5Hypz3-dMsmqFlkeB2Up1ygsOoYHjTKf-d6fJejDqg_frDuTgwOOKzic-NLzExQfeZHVXWRxq9wUGzXDxbR_mr5KaILG5MmtQMBKdSlk8NV8lyPh-NZ2T4hbm0Ggt8UYE40KMg8qppm_OvBGjgiixDV_8VsOEwuaOJQW109zXuv0Czb5O7sERQ9cSPBU8D0tknKPvr7NDiFZc-gcEzByLWeLs8_l2QFQL53LtF9djMY1SQtxvly9y8SOyQtq908QfZ80kf45DcqUPKq6w99AA0XAFLA8XYZVa7hKNhmVdq7Vabrgaaf2NaMmNwXhUMbML4NetaN3J8rt__4aqULsyQ", + "e": "AQAB", + "kid": "zTCsma4Wlwe1N717aiC_gWioTwUg-pwoFU5R9BHgPCg", + "alg": "RS256", + "use": "sig" +} \ No newline at end of file diff --git a/.localserve/photos/alexmartin.webp b/.localserve/photos/alexmartin.webp new file mode 100644 index 0000000..c7e8467 Binary files /dev/null and b/.localserve/photos/alexmartin.webp differ diff --git a/.localserve/photos/crousseau.webp b/.localserve/photos/crousseau.webp new file mode 100644 index 0000000..fa9c09c Binary files /dev/null and b/.localserve/photos/crousseau.webp differ diff --git a/.localserve/photos/ybenali.webp b/.localserve/photos/ybenali.webp new file mode 100644 index 0000000..e57d5e9 Binary files /dev/null and b/.localserve/photos/ybenali.webp differ diff --git a/.localserve/serve.mjs b/.localserve/serve.mjs new file mode 100644 index 0000000..153967f --- /dev/null +++ b/.localserve/serve.mjs @@ -0,0 +1,12 @@ +import { createServer } from 'node:http'; +import { readFileSync, existsSync, statSync } from 'node:fs'; +import { join, extname } from 'node:path'; +const root = new URL('.', import.meta.url).pathname; +const TYPES={'.json':'application/json','.webp':'image/webp'}; +createServer((q,s)=>{ + const p=join(root, decodeURIComponent(q.url.split('?')[0])); + s.setHeader('Access-Control-Allow-Origin','*'); + if(!p.startsWith(root)||!existsSync(p)||statSync(p).isDirectory()){s.writeHead(404);s.end('nf');return;} + s.writeHead(200,{'content-type':TYPES[extname(p)]||'application/octet-stream'}); + s.end(readFileSync(p)); +}).listen(8200,()=>console.log('badges data on :8200')); diff --git a/.localserve/sign-samples.mjs b/.localserve/sign-samples.mjs new file mode 100644 index 0000000..498353c --- /dev/null +++ b/.localserve/sign-samples.mjs @@ -0,0 +1,102 @@ +// Signe des credentials RÉELS pour les membres de directory.json, dans .localserve, +// base = http://localhost:8200. Permet de tester /u/ en vrai (VALID) sans DNS. +// Signature RS256 via node:crypto (aucune dépendance, jose non installé en local). +// Usage : node .localserve/sign-samples.mjs (puis serve.mjs sert le tout, CORS *) +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import crypto from 'node:crypto'; +import { buildCredential, keyUrl } from '../.github/scripts/lib/credential.mjs'; +import { buildStatusListCredential } from '../.github/scripts/lib/status-list.mjs'; + +const root = dirname(fileURLToPath(import.meta.url)); +// Bases prod-like servies par Caddy (HTTPS) : données sous /data, site à la racine. +const BASE = 'https://verify.ai-driven-dev.fr/data'; // DONNÉES (clés, statut, preuves) +const SITE = 'https://verify.ai-driven-dev.fr'; // SITE (page /u/) +const write = (rel, content) => { + const p = join(root, rel); + mkdirSync(dirname(p), { recursive: true }); + writeFileSync(p, content); +}; +const b64url = (v) => Buffer.from(typeof v === 'string' ? v : JSON.stringify(v)).toString('base64url'); + +// Clé jetable de démo. En vrai : secret d'environnement CI. +const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); +const jwk = publicKey.export({ format: 'jwk' }); // { kty, n, e } +const kid = crypto.createHash('sha256') + .update(JSON.stringify({ e: jwk.e, kty: jwk.kty, n: jwk.n })).digest('base64url'); // RFC7638 +const pub = { ...jwk, kid, alg: 'RS256', use: 'sig' }; + +const header = { alg: 'RS256', typ: 'JWT', kid: keyUrl(kid, BASE) }; +const signJwt = (payload) => { + const input = `${b64url(header)}.${b64url(payload)}`; + const sig = crypto.sign('RSA-SHA256', Buffer.from(input), privateKey).toString('base64url'); + return `${input}.${sig}`; +}; + +const dir = JSON.parse(readFileSync(join(root, 'directory.json'), 'utf8')); +const members = dir.members || []; +const now = new Date(); +let issuer; + +for (let i = 0; i < members.length; i += 1) { + const m = members[i]; + const c = buildCredential({ handle: m.handle, name: m.name, statusIndex: i }, { certifiedOn: now, base: BASE }); + issuer = c.issuer; + const jwt = signJwt({ + ...c, + iss: c.issuer.id, + sub: c.credentialSubject.id, + jti: c.id, + iat: Math.floor(now / 1000), + exp: Math.floor(new Date(c.validUntil) / 1000), + }); + write(`u/${m.handle}/credential.jwt`, jwt); +} + +// --- Deux personnes de démo : une VALIDE, une INVALIDE (révoquée) — nom + photo. --- +const testPeople = [ + { handle: 'sophiedurand', name: 'Sophie Durand', role: 'certifie', photo: 'crousseau', description: 'Ingénieure plateforme, delivery assisté par IA.', statusIndex: members.length, revoke: false }, + { handle: 'marclefebvre', name: 'Marc Lefebvre', role: 'certifie', photo: 'ybenali', description: 'Tech lead, refactoring guidé par agents.', statusIndex: members.length + 1, revoke: true }, +]; +for (const t of testPeople) { + const c = buildCredential({ handle: t.handle, name: t.name, statusIndex: t.statusIndex }, { certifiedOn: now, base: BASE }); + const jwt = signJwt({ + ...c, + iss: c.issuer.id, + sub: c.credentialSubject.id, + jti: c.id, + iat: Math.floor(now / 1000), + exp: Math.floor(new Date(c.validUntil) / 1000), + }); + write(`u/${t.handle}/credential.jwt`, jwt); +} + +const revokedIndices = testPeople.filter((t) => t.revoke).map((t) => t.statusIndex); +const statusCred = await buildStatusListCredential(revokedIndices, { issuedOn: now, base: BASE }); +const statusJwt = signJwt({ ...statusCred, iss: statusCred.issuer.id, iat: Math.floor(now / 1000) }); + +write(`keys/${kid}.json`, JSON.stringify(pub, null, 2)); +write('issuer.json', JSON.stringify(issuer, null, 2)); +write('status/1', statusJwt); + +// Annuaire : membres réels + les personnes de démo NON révoquées (comme buildDirectory +// en prod, qui exclut les révoqués via isActive). Le révoqué garde sa page /u mais, +// absent de l'annuaire, son nom n'y est pas -> sa page affiche @handle, sans donnée perso. +const rewritten = [ + ...members.map((m) => ({ ...m, photo: `${BASE}/photos/${m.handle}.webp`, verify: `${SITE}/u/${m.handle}` })), + ...testPeople + .filter((t) => !t.revoke) + .map((t) => ({ + handle: t.handle, + name: t.name, + role: t.role, + description: t.description, + photo: `${BASE}/photos/${t.photo}.webp`, + verify: `${SITE}/u/${t.handle}`, + })), +]; +write('directory.json', JSON.stringify({ ...dir, count: rewritten.length, members: rewritten }, null, 2)); + +console.log(`signé ${members.length} membres + ${testPeople.length} personnes de démo — kid=${kid}`); +console.log('démo :', testPeople.map((t) => `${t.handle}(${t.revoke ? 'invalide/révoqué' : 'valide'})`).join(', ')); diff --git a/.localserve/status/1 b/.localserve/status/1 new file mode 100644 index 0000000..ed9ae3d --- /dev/null +++ b/.localserve/status/1 @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkJpdHN0cmluZ1N0YXR1c0xpc3RDcmVkZW50aWFsIl0sImlzc3VlciI6eyJpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9pc3N1ZXIuanNvbiIsInR5cGUiOlsiUHJvZmlsZSJdLCJuYW1lIjoiQUktRHJpdmVuIERldmVsb3BtZW50In0sInZhbGlkRnJvbSI6IjIwMjYtMDctMjRUMDc6NTI6MTFaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEjbGlzdCIsInR5cGUiOiJCaXRzdHJpbmdTdGF0dXNMaXN0Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJlbmNvZGVkTGlzdCI6Ikg0c0lBQUFBQUFBQUUtM0JJUUVBQUFBQ0lLUF9GenZEQWpRQUFBQUFBQUFBQUFBQUFBQUFBQURBMndENGl2dWNBRUFBQUEifSwiaXNzIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwiaWF0IjoxNzg0ODc5NTMxfQ.rRkQmvjxS0kcnIwwRNru6_9Ag5hc2UhTgyPRyvWEdg_E8T0FqztxU8ztx0jotMSOAl1XSMvqMhdBkncs3vOMqUcsn-_iqfSUYXuP6c7jfj1zOb2LfulFHxDDopVkV_c1Xw6WlMIKstmP-DS_ex-KUupz6SlwHHMsXNYgMAXxSG7LcRT3VThu_mCcf_xpovOs6DI8ONbBzsad8yTsmopVBWYoOwpNJeT5rSkH38ZdO4R-4siSjjv4Vu1pyX3Lo-ccdvlSX4Qf6xb0QzauKCaBZ_YP9ZpjNpAVh1c1zgThP7vgydHzQnW_tYQtY3jXS3QK5tVtg0AKP4NrpPoldOXfkw \ No newline at end of file diff --git a/.localserve/u/alexmartin/credential.jwt b/.localserve/u/alexmartin/credential.jwt new file mode 100644 index 0000000..54bc7a4 --- /dev/null +++ b/.localserve/u/alexmartin/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6MThkZjBmMDAtZDk1MC00ZjAwLTg2ZTgtNzZiOTAzMmUzOTAwIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzo1MjoxMVoiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzo1MjoxMVoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzAiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIwIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vYWxleG1hcnRpbiIsInR5cGUiOlsiQWNoaWV2ZW1lbnRTdWJqZWN0Il0sImFjaGlldmVtZW50Ijp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2FjaGlldmVtZW50cy9jZXJ0aWZpZWQtbWVtYmVyIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJuYW1lIjoiQ2VydGlmaWVkIE1lbWJlciIsImRlc2NyaXB0aW9uIjoiTWVtYnJlIGNlcnRpZmnDqSBkZSBsYSBjb21tdW5hdXTDqSBBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQuIiwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiRMOpY2VybsOpIGFwcsOocyByw6l1c3NpdGUgZGUgbGEgbWFzdGVyY2xhc3Mgw6l2YWx1w6llIGV0IHZhbGlkYXRpb24gcGFyIGxhIENvcmUgVGVhbS4ifX19LCJpc3MiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvaXNzdWVyLmpzb24iLCJzdWIiOiJodHRwczovL2dpdGh1Yi5jb20vYWxleG1hcnRpbiIsImp0aSI6InVybjp1dWlkOjE4ZGYwZjAwLWQ5NTAtNGYwMC04NmU4LTc2YjkwMzJlMzkwMCIsImlhdCI6MTc4NDg3OTUzMSwiZXhwIjoxODE2NDE1NTMxfQ.yiKRzZsPVdycx7IdmsWxwQUcK8Ekk2AX3qG8SkH5htJZK0IXefKlhif_TAS-C7nb5joyi5wU2gmrAXPVJe2w_1_tPp856JKTk5hWKwXFNU4l6o9OHFhXi8sfMteZ3VpjNl9kISwQGx8V3mW-lr-W6o6coEh6STC_tALgZnqNsHkibSwYruAVoTsJcJ1aNEmBe-FO7RzYvZqk8vwh4Hirw7UJ9_ny6aWFa6ayXYCM069uXTWvaZxAHUagriObH31tYBpA4wnQjerLmQUlcGJRptfY5-y_vB6aXSYRnPs8mPYj96lyHQLlZVqd6BYA4f_uKLa9jGmmT1Vzp4V4duX15g \ No newline at end of file diff --git a/.localserve/u/crousseau/credential.jwt b/.localserve/u/crousseau/credential.jwt new file mode 100644 index 0000000..fd6d94c --- /dev/null +++ b/.localserve/u/crousseau/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6OGEwYmNkMmItYTQ1Zi00YzAwLTg0M2MtYjQ5MmYxMjk5ZTNkIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzo1MjoxMVoiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzo1MjoxMVoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzEiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIxIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vY3JvdXNzZWF1IiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvYWNoaWV2ZW1lbnRzL2NlcnRpZmllZC1tZW1iZXIiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sIm5hbWUiOiJDZXJ0aWZpZWQgTWVtYmVyIiwiZGVzY3JpcHRpb24iOiJNZW1icmUgY2VydGlmacOpIGRlIGxhIGNvbW11bmF1dMOpIEFJLURyaXZlbiBEZXZlbG9wbWVudC4iLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJEw6ljZXJuw6kgYXByw6hzIHLDqXVzc2l0ZSBkZSBsYSBtYXN0ZXJjbGFzcyDDqXZhbHXDqWUgZXQgdmFsaWRhdGlvbiBwYXIgbGEgQ29yZSBUZWFtLiJ9fX0sImlzcyI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9pc3N1ZXIuanNvbiIsInN1YiI6Imh0dHBzOi8vZ2l0aHViLmNvbS9jcm91c3NlYXUiLCJqdGkiOiJ1cm46dXVpZDo4YTBiY2QyYi1hNDVmLTRjMDAtODQzYy1iNDkyZjEyOTllM2QiLCJpYXQiOjE3ODQ4Nzk1MzEsImV4cCI6MTgxNjQxNTUzMX0.NYVO-13oxFAAA2cDCHFlnjfwY9yE21OyThW7eSZg1yT_5T2KaZ4TBEQ9FwGejViDXTwgL9gIx5di84IQfizdfGTQ6NBP1ZZFwCyITGo3VGa0MsF0JINrEGIP5qk2BPSGQX7tq0bfFWpbIdGHBhVoP40MxwymWKVAwcZZnOisGQRK9sojEJ-6ydJDqW8jK6z8pHE67NdZrnKMLgp-7PjlKL7GJvv7uj_y1Pyz9DSrdAM2mNqBziBe7O43QO28LAYu6qyfp3fNa--kvyVtIV1dEib3DwBLUO9-3qwDdwYiDu5e2TPnJk7m0RZOGgyoF_c7JcKa0-rmxSOVNid-fqECHQ \ No newline at end of file diff --git a/.localserve/u/marclefebvre/credential.jwt b/.localserve/u/marclefebvre/credential.jwt new file mode 100644 index 0000000..311aacb --- /dev/null +++ b/.localserve/u/marclefebvre/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6ZDYzYWYwZWItYzcxYy00ODAwLTg4MGQtODk1MjY3NWFjYzdkIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzo1MjoxMVoiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzo1MjoxMVoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzQiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiI0Iiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbWFyY2xlZmVidnJlIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvYWNoaWV2ZW1lbnRzL2NlcnRpZmllZC1tZW1iZXIiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sIm5hbWUiOiJDZXJ0aWZpZWQgTWVtYmVyIiwiZGVzY3JpcHRpb24iOiJNZW1icmUgY2VydGlmacOpIGRlIGxhIGNvbW11bmF1dMOpIEFJLURyaXZlbiBEZXZlbG9wbWVudC4iLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJEw6ljZXJuw6kgYXByw6hzIHLDqXVzc2l0ZSBkZSBsYSBtYXN0ZXJjbGFzcyDDqXZhbHXDqWUgZXQgdmFsaWRhdGlvbiBwYXIgbGEgQ29yZSBUZWFtLiJ9fX0sImlzcyI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9pc3N1ZXIuanNvbiIsInN1YiI6Imh0dHBzOi8vZ2l0aHViLmNvbS9tYXJjbGVmZWJ2cmUiLCJqdGkiOiJ1cm46dXVpZDpkNjNhZjBlYi1jNzFjLTQ4MDAtODgwZC04OTUyNjc1YWNjN2QiLCJpYXQiOjE3ODQ4Nzk1MzEsImV4cCI6MTgxNjQxNTUzMX0.CKKaFEcLNQWJ0ara8qsodCQJfy1m6OxfDuWS1kWaU4JymSb-FCobdYOZ16zCqmk83OKUIK0iExsoHlLYxt9inFr2EmxrZidQG-GIAA4wocGKDgERJRNbwKTWZ3TldGc32Aq23MieVwxJMz__m9BqdEcs6N4-OIa3mISoCRVeuXOVIYaOQsE1HnGeHFfF6Xv8I7r6BONBo0rkbXeVwlDk68jgLqAWktY_GYiK0IicSwhKWfMuv17TaAsA3pUHlja_JAKMm5C9gRQPcgSjCO5HKjVjuE6xt8V8O_siGICKOldZm9hDfbMrjxS9pkmymSJZSafzUvSCm1AoXJA-rMHXjg \ No newline at end of file diff --git a/.localserve/u/sophiedurand/credential.jwt b/.localserve/u/sophiedurand/credential.jwt new file mode 100644 index 0000000..dbcb398 --- /dev/null +++ b/.localserve/u/sophiedurand/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6ZDljOTQzY2QtY2MzZS00ODAwLTg3ZmUtM2E3NDAyMTYxNzBiIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzo1MjoxMVoiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzo1MjoxMVoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzMiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIzIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vc29waGllZHVyYW5kIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvYWNoaWV2ZW1lbnRzL2NlcnRpZmllZC1tZW1iZXIiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sIm5hbWUiOiJDZXJ0aWZpZWQgTWVtYmVyIiwiZGVzY3JpcHRpb24iOiJNZW1icmUgY2VydGlmacOpIGRlIGxhIGNvbW11bmF1dMOpIEFJLURyaXZlbiBEZXZlbG9wbWVudC4iLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJEw6ljZXJuw6kgYXByw6hzIHLDqXVzc2l0ZSBkZSBsYSBtYXN0ZXJjbGFzcyDDqXZhbHXDqWUgZXQgdmFsaWRhdGlvbiBwYXIgbGEgQ29yZSBUZWFtLiJ9fX0sImlzcyI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9pc3N1ZXIuanNvbiIsInN1YiI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zb3BoaWVkdXJhbmQiLCJqdGkiOiJ1cm46dXVpZDpkOWM5NDNjZC1jYzNlLTQ4MDAtODdmZS0zYTc0MDIxNjE3MGIiLCJpYXQiOjE3ODQ4Nzk1MzEsImV4cCI6MTgxNjQxNTUzMX0.E8VyyJutJpf7duVFpC4OKZzUwXl3nWYstmYQth5h5Shv3Lo0GFaFr44ALbD1Sz54ZtsyyQa8AHzcuWpgnKSM6mDMgZERt1NQoS_5Yy70n4jnviEKfVUI5rdiOI3Sw2dC3eUcKq4ZMZXKShCSvYzH1mTkSe3n22joHpaKLz9AycCJUKrM48Le0hp4VRzMu1pB0xs8zy6bC6Y_XHnTZvCoJvg0PT3jBoSjs7F5gZZd-PqNH4Lwtk7YSTRAWM99UQ_A17in3wgz1FxoYudqjsicAu1dhM-9kbH_descoasUiA-LnpU7ukRnFzqFBl6DBzsn1S0LvKRzqTcTjL8bYyDCrg \ No newline at end of file diff --git a/.localserve/u/testexpire/credential.jwt b/.localserve/u/testexpire/credential.jwt new file mode 100644 index 0000000..60ee7c6 --- /dev/null +++ b/.localserve/u/testexpire/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0l4Rk1KUDJZSjZfeHZmSm05Tm1ESlFDakNPZWNPN19wOWdGX3dQNHM2VmcuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6ZGFkYWRhYTctY2U4Yi00MDAwLTg0ZWQtYTMxZTE3ZjgwYmUxIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNS0wNi0xOVQwNzozODo1M1oiLCJ2YWxpZFVudGlsIjoiMjAyNi0wNi0xOVQwNzozODo1M1oiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzMiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIzIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vdGVzdGV4cGlyZSIsInR5cGUiOlsiQWNoaWV2ZW1lbnRTdWJqZWN0Il0sImFjaGlldmVtZW50Ijp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2FjaGlldmVtZW50cy9jZXJ0aWZpZWQtbWVtYmVyIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJuYW1lIjoiQ2VydGlmaWVkIE1lbWJlciIsImRlc2NyaXB0aW9uIjoiTWVtYnJlIGNlcnRpZmnDqSBkZSBsYSBjb21tdW5hdXTDqSBBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQuIiwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiRMOpY2VybsOpIGFwcsOocyByw6l1c3NpdGUgZGUgbGEgbWFzdGVyY2xhc3Mgw6l2YWx1w6llIGV0IHZhbGlkYXRpb24gcGFyIGxhIENvcmUgVGVhbS4ifX19LCJpc3MiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvaXNzdWVyLmpzb24iLCJzdWIiOiJodHRwczovL2dpdGh1Yi5jb20vdGVzdGV4cGlyZSIsImp0aSI6InVybjp1dWlkOmRhZGFkYWE3LWNlOGItNDAwMC04NGVkLWEzMWUxN2Y4MGJlMSIsImlhdCI6MTc1MDMxODczMywiZXhwIjoxNzgxODU0NzMzfQ.GbjwG7gKIhxMK3FJPflK_wcvK9euhT8nXPZfYfwUcZIWzDd-CV3Ck3XOKO464LnEGU_fk9iyo6i8ThyNQ-dRxDlCohidCw_qGS-HkMOClGPX0vjnrXFMvEIn4Uqd8fDk6zPUB2AY4Qbcx8Msf16AG9HR4jZe-CC2joq0gW7f8IBPQbHPobbOAqA1-UzaJ9EjWa2B19j-W02Wl_HPR4reHe_MGdIfd0l2zmHTFLYbX5567HtBW-wrR5a_QFUnXqUMVilg1RmZRX5HqiYkBddJTR9UaEctuG3HjcnX0pSBt5_5-bDhZ-OskMEMcjjmwf3JvWts9RFVTd6xBNVluuu2Zg \ No newline at end of file diff --git a/.localserve/u/testrevoke/credential.jwt b/.localserve/u/testrevoke/credential.jwt new file mode 100644 index 0000000..361e1b4 --- /dev/null +++ b/.localserve/u/testrevoke/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0l4Rk1KUDJZSjZfeHZmSm05Tm1ESlFDakNPZWNPN19wOWdGX3dQNHM2VmcuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6OTM0ODAwY2MtZjBmYy00ODAwLThkN2YtNzk3NTE0ZjYxM2Q0IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzozODo1M1oiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzozODo1M1oiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzQiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiI0Iiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vdGVzdHJldm9rZSIsInR5cGUiOlsiQWNoaWV2ZW1lbnRTdWJqZWN0Il0sImFjaGlldmVtZW50Ijp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2FjaGlldmVtZW50cy9jZXJ0aWZpZWQtbWVtYmVyIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJuYW1lIjoiQ2VydGlmaWVkIE1lbWJlciIsImRlc2NyaXB0aW9uIjoiTWVtYnJlIGNlcnRpZmnDqSBkZSBsYSBjb21tdW5hdXTDqSBBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQuIiwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiRMOpY2VybsOpIGFwcsOocyByw6l1c3NpdGUgZGUgbGEgbWFzdGVyY2xhc3Mgw6l2YWx1w6llIGV0IHZhbGlkYXRpb24gcGFyIGxhIENvcmUgVGVhbS4ifX19LCJpc3MiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvaXNzdWVyLmpzb24iLCJzdWIiOiJodHRwczovL2dpdGh1Yi5jb20vdGVzdHJldm9rZSIsImp0aSI6InVybjp1dWlkOjkzNDgwMGNjLWYwZmMtNDgwMC04ZDdmLTc5NzUxNGY2MTNkNCIsImlhdCI6MTc4NDg3ODczMywiZXhwIjoxODE2NDE0NzMzfQ.iNQdSmbVwV8KpKLall9fqz0Qb9DAlbzXUcLoirrLhoTE73p3Allyb-tCgzM_-0Gq2E6p1ItaYiioyf4fIemOsRyFsKedT-CSQXabwavCctP3Q3J4EwIC3qZgWea-dN6QXJJaF9y5yFWeJyZkwQajZQ_Afi7DmfuX5f8CjhlBXZbqpyZ5AiCETZBaxvyn3uLo4YTwwRVP4dw1SW5wePgrRZNYmDnjQU-z0MKt2HKPK-QHWAZS-ryhTnUqBiVKXq3aFA_Emn0QzaNsyvcBGRVEv7Nh0Up_snKjY2SWe6KtnMIuLImZrPlABM6xZrC8nmSQYHWBk27gx_oxiRkB_fBPzw \ No newline at end of file diff --git a/.localserve/u/ybenali/credential.jwt b/.localserve/u/ybenali/credential.jwt new file mode 100644 index 0000000..c880f7e --- /dev/null +++ b/.localserve/u/ybenali/credential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Imh0dHBzOi8vdmVyaWZ5LmFpLWRyaXZlbi1kZXYuZnIvZGF0YS9rZXlzL0xtdlU4R1BIaUVmWGNvcV9IQm1LY2Ruako1WDJIbE9mOWhGcHVfdF93b2MuanNvbiJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIl0sImlkIjoidXJuOnV1aWQ6NjA3MDZlOTMtNGQ5NC00ZTAwLThlNDctMTcyYTFjNjY3Yjk1IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2lzc3Vlci5qc29uIiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQiLCJ1cmwiOiJodHRwczovL2FpLWRyaXZlbi1kZXYuZnIifSwidmFsaWRGcm9tIjoiMjAyNi0wNy0yNFQwNzo1MjoxMVoiLCJ2YWxpZFVudGlsIjoiMjAyNy0wNy0yNFQwNzo1MjoxMVoiLCJjcmVkZW50aWFsU3RhdHVzIjp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL3N0YXR1cy8xIzIiLCJ0eXBlIjoiQml0c3RyaW5nU3RhdHVzTGlzdEVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIyIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvc3RhdHVzLzEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20veWJlbmFsaSIsInR5cGUiOlsiQWNoaWV2ZW1lbnRTdWJqZWN0Il0sImFjaGlldmVtZW50Ijp7ImlkIjoiaHR0cHM6Ly92ZXJpZnkuYWktZHJpdmVuLWRldi5mci9kYXRhL2FjaGlldmVtZW50cy9jZXJ0aWZpZWQtbWVtYmVyIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJuYW1lIjoiQ2VydGlmaWVkIE1lbWJlciIsImRlc2NyaXB0aW9uIjoiTWVtYnJlIGNlcnRpZmnDqSBkZSBsYSBjb21tdW5hdXTDqSBBSS1Ecml2ZW4gRGV2ZWxvcG1lbnQuIiwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiRMOpY2VybsOpIGFwcsOocyByw6l1c3NpdGUgZGUgbGEgbWFzdGVyY2xhc3Mgw6l2YWx1w6llIGV0IHZhbGlkYXRpb24gcGFyIGxhIENvcmUgVGVhbS4ifX19LCJpc3MiOiJodHRwczovL3ZlcmlmeS5haS1kcml2ZW4tZGV2LmZyL2RhdGEvaXNzdWVyLmpzb24iLCJzdWIiOiJodHRwczovL2dpdGh1Yi5jb20veWJlbmFsaSIsImp0aSI6InVybjp1dWlkOjYwNzA2ZTkzLTRkOTQtNGUwMC04ZTQ3LTE3MmExYzY2N2I5NSIsImlhdCI6MTc4NDg3OTUzMSwiZXhwIjoxODE2NDE1NTMxfQ.ywLMra4JaUtG2i_xXc8BwYpeGxrhBTzbj39VFQDanUu_UwN1O4105_kasKnn1noqecmFe9pIsVbOgRT72YChF9qisd1SrgVbflRI9mOibO1L9CbFlcp3PuyAAJzPEXcvhscBHreqrhuytTU8AMsN1GK0dskPR6-JVS8AEgYzOskePwUZJKrRUGp8_eTIPlgH_WSQSbwJ2zFtW07uCkwK2hRPxOrUN7cJDwC4aOYcmjfc8yXi60AlVrEUGHSv6Icp_78qZAKYOAbcNiqTN-ildeqZtwWDyrvX0aOS3EC22CSCdnzXFZ8Xeb-9peEhqPjccbiWKCWik9N-Nl0XXdi3-A \ No newline at end of file diff --git a/.specstory/.project.json b/.specstory/.project.json new file mode 100644 index 0000000..b7eb7b4 --- /dev/null +++ b/.specstory/.project.json @@ -0,0 +1,7 @@ +{ + "workspace_id": "7781-e7af-7f31-cc2b", + "workspace_id_at": "2026-07-22T10:10:30Z", + "git_id": "bba2-4a7f-4f88-c843", + "git_id_at": "2026-07-22T10:10:30Z", + "project_name": "badges" +} \ No newline at end of file diff --git a/.specstory/cli/config.toml b/.specstory/cli/config.toml new file mode 100644 index 0000000..b26ea71 --- /dev/null +++ b/.specstory/cli/config.toml @@ -0,0 +1,106 @@ +# SpecStory CLI Configuration +# +# This is the project-level config file for SpecStory CLI. +# All settings here apply to this project unless overridden by CLI flags. +# +# Uncomment (remove the #) the line and edit any setting below to change the default behavior. +# For more information, see: https://docs.specstory.com/integrations/terminal-coding-agents/usage + +[local_sync] +# Write markdown files locally. (default: true) +# enabled = false # equivalent to --only-cloud-sync + +# Custom output directory for markdown files. +# Default: ./.specstory/history (relative to the project directory) +# output_dir = "~/.specstory/history" # equivalent to --output-dir "~/.specstory/history" + +# Use local timezone for file name and content timestamps (default: false, UTC) +# local_time_zone = true # equivalent to --local-time-zone + +[cloud_sync] +# Sync session data to SpecStory Cloud. (default: true, when logged in to SpecStory Cloud) +# enabled = false # equivalent to --no-cloud-sync + +[logging] +# Write logs to .specstory/debug/debug.log (default: false) +# log = true # equivalent to --log + +# Debug-level output, requires console or log (default: false) +# debug = true # equivalent to --debug + +# Custom output directory for debug data. +# Default: ./.specstory/debug (relative to the project directory) +# debug_dir = "~/.specstory/debug" # equivalent to --debug-dir "~/.specstory/debug" + +# Error/warn/info output to stdout (default: false) +# console = true # equivalent to --console + +# Suppress all non-error output (default: false) +# silent = true # equivalent to --silent + +[version_check] +# Check for new versions of the CLI on startup. +# Default: true +# enabled = false # equivalent to --no-version-check + +[analytics] +# Send anonymous product usage analytics to help improve SpecStory. +# Default: true +# enabled = false # equivalent to --no-usage-analytics + +[telemetry] +# OTLP gRPC collector endpoint (e.g., "localhost:4317" or "http://localhost:4317") +# endpoint = "localhost:4317" + +# Override the default service name (default: "specstory-cli") +# service_name = "my-service-name" + +# Include user prompt text in telemetry spans (default: true) +# prompts = false + +[resume] +# Default view mode for the 'specstory resume' picker: "dense" (more sessions) or +# "sparse" (more detail per session). The picker also remembers your last choice here. +# view_mode = "sparse" +# +# (managed automatically) the agent you last resumed into — used as the default target. +# last_agent = "claude" + +[redaction] +# Redact secrets and API keys from saved markdown history and cloud-synced +# session data. (default: true) +# Detection uses the betterleaks ruleset, covering API keys, tokens, private +# keys, and other credentials for many providers. +# enabled = false # equivalent to --no-redact-secrets + +[providers] +# Agent execution commands by provider (used by specstory run) +# Pass custom flags (e.g. claude_cmd = "claude --allow-dangerously-skip-permissions") +# Use of these is equivalent to -c "custom command" + +# Claude Code command +# claude_cmd = "claude" + +# Codex CLI command +# codex_cmd = "codex" + +# Copilot IDE commands (used by specstory run copilotide[-variant] to open the IDE) +# copilotide_cmd = "code" +# copilotide_insiders_cmd = "code-insiders" +# copilotide_vscodium_cmd = "codium" +# copilotide_vscodium_insiders_cmd = "codium-insiders" + +# Cursor CLI command +# cursor_cmd = "cursor-agent" + +# Cursor IDE command (used by specstory run cursoride to open the IDE) +# cursoride_cmd = "cursor" + +# Droid CLI command +# droid_cmd = "droid" + +# Gemini CLI command +# gemini_cmd = "gemini" + +# DeepSeek TUI command +# deepseek_cmd = "deepseek" diff --git a/.specstory/history/2026-07-22_10-11-12Z-give-a-full-picture.md b/.specstory/history/2026-07-22_10-11-12Z-give-a-full-picture.md new file mode 100644 index 0000000..d2f7338 --- /dev/null +++ b/.specstory/history/2026-07-22_10-11-12Z-give-a-full-picture.md @@ -0,0 +1,1679 @@ + + +# 2026-07-22 10:11:42Z + + + +_**User (2026-07-22 10:11:42Z)**_ + +Give a full picture of this repository to present it entierely to my community + +--- + +_**Agent (claude-sonnet-5 2026-07-22 10:11:48Z)**_ + +
+Tool use: **Bash** +List repo structure + +`ls -la && find . -maxdepth 2 -not -path './.git*' | sort` + +```text +.demo-site/ +.git/ +.github/ +.localserve/ +.specstory/ +data/ +design/ +docs/ +keys/ +public/ +site/ +.gitattributes 270B +.gitignore 764B +MAINTAINERS.md 2.0K +README.md 2.5K +directory.json 1.0K +. +./.demo-site +./.demo-site/bitstring.mjs +./.demo-site/issuer.json +./.demo-site/keys +./.demo-site/status +./.demo-site/u +./.demo-site/verify-page.mjs +./.demo-site/verify.mjs +./.localserve +./.localserve/directory.json +./.localserve/issuer.json +./.localserve/keys +./.localserve/photos +./.localserve/serve.mjs +./.localserve/sign-samples.mjs +./.localserve/status +./.localserve/u +./.specstory +./.specstory/.project.json +./.specstory/cli +./.specstory/history +./.specstory/statistics.json +./MAINTAINERS.md +./README.md +./data +./data/members +./data/revoked.json +./design +./design/.image-slots.state.json +./design/Badge AIDD - standalone.html +./design/Badges AIDD.dc.html +./design/README.md +./design/Seal.dc.html +./design/assets +./design/image-slot.js +./design/screenshots +./design/support.js +./design/uploads +./directory.json +./docs +./docs/ARCHITECTURE.md +./docs/PRD.md +./docs/integration-communaute.md +./docs/integration-verify-page.md +./docs/rgpd +./docs/spikes +./docs/verification.md +./keys +./keys/h1yqkUhhcAVAUU_Zm4lqKpFPEqytMB_swbwNrHPnuCY.json +./public +./public/issuer.json +./site +./site/badge-page.html +./site/bitstring.mjs +./site/bitstring.test.mjs +./site/package-lock.json +./site/package.json +./site/verify-page.mjs +./site/verify-page.test.mjs +./site/verify.html +./site/verify.mjs +./site/verify.test.mjs +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:11:51Z)**_ + +
+Tool use: **Read** + `./README.md` + +```text +1
+2 +3 # AIDD Badges 🇫🇷 +4 +5 ## Certification vérifiable des membres de la communauté AI-Driven Development. +6 +7

+8 Open Badges 3.0 · VC-JWT RS256 · Git-natif · Sans email +9

+10 +11 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +12 [![Tests](https://github.com/ai-driven-dev/badges/actions/workflows/scripts-tests.yml/badge.svg?branch=main)](https://github.com/ai-driven-dev/badges/actions/workflows/scripts-tests.yml) +13 [![Made in France](https://img.shields.io/badge/made%20in-France-0055A4?labelColor=EF4135)](https://www.ai-driven-dev.fr/) +14 +15
+16 +17 --- +18 +19 Chaque membre certifié reçoit un **badge Open Badges 3.0** (signé RS256) dont +20 l'authenticité se **vérifie par un tiers, indépendamment de nos serveurs**, +21 affichable sur LinkedIn et ailleurs. +22 +23 ## Le process +24 +25 1. **S'inscrire** — un membre remplit un formulaire (issue GitHub). Son identité = son compte GitHub. +26 2. **Générer** — un bot en fait une pull request (fiche + photo normalisée + aperçu de modération). +27 3. **Émettre** — un mainteneur **merge** : c'est le point d'autorité. Le badge est signé et publié. +28 4. **Vérifier** — la page du badge contrôle la signature **dans le navigateur** (QR, preuve téléchargeable). +29 5. **Retirer** — une demande de retrait supprime la fiche + la photo et révoque le badge (RGPD). +30 +31 Détail des flux : **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**. +32 Guide des mainteneurs : **[MAINTAINERS.md](MAINTAINERS.md)**. +33 +34 ## Tester en local (sans DNS) +35 +36 ```bash +37 cd .github/scripts && npm ci +38 npm test # ~140 tests +39 npm run demo # signe un badge de démo et le sert -> http://localhost:8000/u/demo +40 ``` +41 +42 ## Structure +43 +44 | Chemin | Contenu | +45 |---|---| +46 | `docs/ARCHITECTURE.md` | L'architecture et les flux (diagrammes) | +47 | `MAINTAINERS.md` | Guide des mainteneurs, étape par étape | +48 | `docs/PRD.md` | Spécification + contraintes techniques (CT-1…CT-14) | +49 | `docs/rgpd/` | Information (art. 13) + justification non-AIPD | +50 | `.github/scripts/` | Intake, émission, révocation, annuaire (+ tests) | +51 | `site/` | Code de vérification navigateur (+ tests) | +52 | `data/members//` | Fiche + photo d'un membre (photo en Git LFS) | +53 +54 ## Garde-fous (non négociables) +55 +56 1. **La clé privée de signature ne va jamais dans le dépôt** — secret CI. Sa fuite = badges forgés. +57 2. **Aucune donnée personnelle effaçable dans l'historique Git** — photos en LFS, retrait = suppression d'objet. +58 3. **Le dépôt reste public** — les verrous de confiance l'exigent au plan gratuit (CT-14). +59 +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:11:52Z)**_ + +
+Tool use: **Read** + `./docs/ARCHITECTURE.md` + +```text +1 # Architecture +2 +3 ## Deux couches +4 +5 | Couche | Où | Rôle | +6 |---|---|---| +7 | **Données** | GitHub **Pages** (`ai-driven-dev.github.io/badges`) | Clés, statut, issuer, preuves, photos. L'**ancre permanente** (uptime GitHub) et le **fallback**. | +8 | **Affichage** | Le **site** (VPS, Astro : `verify.ai-driven-dev.fr`) | La **belle** page de vérif + le badge visuel + l'annuaire `/communaute`. Lit les données depuis Pages. | +9 +10 Le badge (et son QR) pointent vers la belle page du **site**. Si le site tombe, la +11 preuve reste vérifiable via Pages — c'est le fallback. La **génération/signature** est +12 de la CI GitHub (la clé privée n'est jamais sur un serveur). +13 +14 ```mermaid +15 flowchart LR +16 subgraph gh["GitHub"] +17 CI["CI : signe (clé privée)"] +18 Pages["Pages : clés, statut, preuves
(ancre + fallback)"] +19 end +20 subgraph vps["Site — VPS, Astro"] +21 UI["/u/handle (belle vérif) + /communaute"] +22 end +23 Visiteur["Visiteur"] +24 CI -->|"publie"| Pages +25 Visiteur -->|"clic badge"| UI +26 UI -->|"lit les données"| Pages +27 ``` +28 +29 ## Inscription → émission +30 +31 Le merge par un mainteneur **est** le point d'autorité : il déclenche la signature. +32 +33 ```mermaid +34 sequenceDiagram +35 participant M as Membre +36 participant B as Bot (GitHub App) +37 participant R as Mainteneur +38 participant CI as CI (env signing) +39 M->>B: Issue Form (inscription + photo) +40 B->>B: parse, photo WebP + strip EXIF (LFS) +41 B->>R: PR + aperçu de modération +42 R->>CI: merge sur main (autorité) +43 CI->>CI: signe le credential (RS256, déterministe) +44 CI->>Pages: publie clés, preuve, QR, annuaire +45 ``` +46 +47 ## Vérification (dans le navigateur, indépendante) +48 +49 ```mermaid +50 flowchart TD +51 A["Ouvre la page du badge"] --> B["charge credential.jwt (Pages)"] +52 B --> C["récupère la clé via le kid (Pages)"] +53 C --> D{"Signature valide ?"} +54 D -->|non| INV["Invalide"] +55 D -->|oui| E{"Révoqué ? (status list)"} +56 E -->|oui| REV["Révoqué"] +57 E -->|non| F{"Expiré ?"} +58 F -->|oui| EXP["Expiré"] +59 F -->|non| OK["Valide"] +60 ``` +61 +62 ## Retrait RGPD +63 +64 ```mermaid +65 sequenceDiagram +66 participant M as Membre +67 participant B as Bot +68 participant R as Mainteneur +69 participant CI as CI +70 M->>B: Issue Form (retrait) +71 B->>B: rm -rf data/members/handle + index vers revoked.json +72 B->>R: PR de retrait +73 R->>CI: merge +74 CI->>Pages: status list révoque, annuaire retire, photo supprimée +75 ``` +76 +77 ## Données d'un membre +78 +79 `data/members//record.yml` (fiche) + `photo.webp` (objet Git LFS). L'effacement +80 RGPD = `rm -rf data/members//`. La révocation survit dans `data/revoked.json` +81 (juste des entiers, non personnels). Schéma des champs : `PRD.md`. +82 +83 ## Clé de signature +84 +85 Une paire RS256. La **privée** vit en secret d'environnement CI (`signing`), utilisée +86 seulement au merge ; jamais dans le dépôt. La **publique** est publiée à vie +87 (`keys/.json`) : retirer une ancienne clé rendrait ses badges invérifiables. +88 Rotation : un workflow manuel gaté par une review Habilité (`key-ceremony`). +89 +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:11:52Z)**_ + +
+Tool use: **Read** + `./MAINTAINERS.md` + +```text +1 # Guide des mainteneurs +2 +3 Ce que fait un mainteneur, étape par étape. Rien d'autre n'est requis au quotidien. +4 +5 ## Valider une inscription +6 +7 1. Une PR **« Inscription : … »** s'ouvre toute seule (le membre a rempli le formulaire). +8 2. Ouvrir la PR → lire le **commentaire de modération** : nom, LinkedIn, site, description, **photo**. +9 3. Vérifier : photo correcte, infos cohérentes, le compte est bien un membre certifié. +10 4. **Merger** la PR. → le badge est émis et publié automatiquement. Fin. +11 +12 > Rien n'est public tant que la PR n'est pas mergée. Le merge **est** l'autorisation. +13 +14 ## Traiter un retrait (RGPD) +15 +16 1. Une PR **« Retrait RGPD : @… »** s'ouvre (le membre a demandé le retrait). +17 2. **Merger** la PR. → la fiche et la photo sont supprimées, le badge est révoqué. Fin. +18 +19 > Une preuve déjà téléchargée par le membre apparaîtra **révoquée** à la vérification. +20 +21 ## Faire tourner la clé de signature +22 +23 À faire rarement (compromission suspectée, rotation périodique). +24 +25 1. Onglet **Actions** → **« Cérémonie de clé »** → **Run workflow**. +26 2. Le job se met en pause → **approuver** le déploiement (review Habilité). +27 3. Une **PR de clé publique** s'ouvre → la **merger**. +28 +29 > Les anciennes clés publiques restent publiées : les badges déjà émis restent vérifiables. +30 +31 ## Refuser / corriger +32 +33 - Une PR pas conforme : la **fermer** (ne pas merger). Le membre peut rouvrir une demande. +34 - Un souci sur la photo/les infos : commenter, fermer, demander une nouvelle inscription. +35 +36 --- +37 +38 ## Setup initial (une seule fois) +39 +40 À faire au démarrage du dépôt. Détails et checklists : +41 +42 - **Deux GitHub Apps** (moindre privilège) : `key-writer` (écrit le secret de clé) et +43 `intake-bot` (ouvre les PR). Voir les secrets `KEY_WRITER_*` / `INTAKE_BOT_*`. +44 - **Première clé** : lancer la « Cérémonie de clé » une fois. +45 - **DNS** : `verify.ai-driven-dev.fr` → le **site (VPS)**. Les **données** restent sur +46 `ai-driven-dev.github.io/badges` (Pages, rien à faire). +47 - **Dépôt public** obligatoire (verrous de confiance, CT-14). +48 +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:11:52Z)**_ + +
+Tool use: **Read** + `./docs/PRD.md` + +```text +1 # Badges AIDD — certification vérifiable des membres +2 +3 Un système qui délivre aux membres certifiés d'AIDD une preuve vérifiable par un tiers, affichable sur LinkedIn et dans un annuaire public, pour que le titre cesse d'être une simple auto-déclaration. L'intake et l'émission sont git-natifs, sur le modèle du dépôt `manifest`. +4 +5 ## Overview +6 +7 AIDD décerne un titre à ses membres : **Certifié** (niveau 1) après une masterclass évaluée, et — plus tard — **Habilité** (niveau 2). Ce titre atteste qu'ils savent appliquer le flow AIDD et ses concepts. +8 +9 Hors de la communauté, ce titre ne vaut rien : un recruteur qui le lit sur un CV n'a aucun moyen de savoir s'il est réel. Sans preuve, la certification n'est qu'une décoration. +10 +11 Ce projet donne à chaque certifié une preuve que **n'importe qui peut vérifier de façon indépendante**, sans avoir à croire AIDD sur parole, et un badge affichable sur LinkedIn, en signature email, sur un site, et dans un **annuaire public des certifiés**. Petit périmètre : moins de 100 certifications par an. +12 +13 Le mécanisme reprend le modèle éprouvé du dépôt `ai-driven-dev/manifest` : une demande ouvre une **issue**, un bot ouvre une **pull request**, un mainteneur **merge**, et le merge déclenche la génération du badge signé. Le merge par un mainteneur *est* le point d'autorité d'émission. +14 +15 ## Problem Statement +16 +17 N'importe qui peut écrire « Certified Member AIDD » sur son profil LinkedIn. Rien ne distingue un vrai certifié d'un imposteur. +18 +19 - **Pour le membre certifié** : son titre n'a aucun poids face à un employeur. L'effort de la masterclass ne se convertit pas en signal reconnaissable. +20 - **Pour l'employeur** : il ne peut ni vérifier le titre, ni s'y fier. Il l'ignore donc. +21 - **Pour AIDD** : le titre décerné ne construit aucune valeur externe, et n'importe qui peut l'usurper sans risque. +22 +23 Le besoin : qu'un tiers puisse **confirmer lui-même** qu'un badge est authentique, qu'il a bien été émis par AIDD, et qu'il appartient bien à la personne qui le présente. +24 +25 ## Goals +26 +27 - Un membre certifié dispose d'une **preuve exportable** de sa certification, affichable sur LinkedIn et ailleurs. +28 - Un tiers peut **vérifier l'authenticité d'un badge avec un outil indépendant**, sans dépendre du verdict d'AIDD ni de la disponibilité de ses serveurs. +29 - Un tiers peut **confirmer que le badge appartient à la personne** qui le lui présente, et pas seulement qu'il a été émis à quelqu'un du même nom. +30 - Un badge falsifié, forgé ou altéré est **rejeté**, y compris par un vérificateur tiers. +31 - Le titre **conserve sa valeur dans le temps** : il expire, et ne prétend donc jamais attester une compétence périmée. +32 - Les certifiés sont présentés dans un **annuaire public**. +33 - AIDD reste **maître de l'outil** : auto-hébergé, standards ouverts, aucune dépendance à un fournisseur payant. +34 +35 ## Non-Goals +36 +37 - **Émettre sans revue humaine.** La demande est initiée par le membre, mais aucun badge n'est émis sans le merge d'un mainteneur. C'est une revue, pas du self-service auto-appliqué. +38 - **Révoquer un badge pour non-paiement.** Qui cesse de payer conserve son badge jusqu'à son échéance. +39 - **Détecter la triche** ou gérer une contestation de certification. +40 - **Archiver les notes ou rendus** de la masterclass — hors de ce système. +41 - **Émettre le badge « Habilité »** en v1 — le niveau 2 est prévu ultérieurement ; le modèle de données l'anticipe (le type de rôle est une donnée). +42 - **Collecter l'email** des membres — l'identité repose sur le compte GitHub (voir CT-3). +43 - **La création du visuel du badge** — produite à part (voir `design/`), simplement intégrée ici. +44 +45 ## User Stories +46 +47 - En tant que **membre certifié**, je veux demander mon badge via un formulaire simple, afin de ne pas avoir à manipuler du YAML ou Git à la main. +48 - En tant qu'**équipe AIDD**, je veux qu'aucun badge ne soit émis sans que je merge la pull request correspondante, afin de rester la seule autorité d'émission. +49 - En tant qu'**équipe AIDD**, je veux voir la photo et les informations dans la pull request avant publication, afin de modérer ce qui apparaît sur l'annuaire public. +50 - En tant que **membre certifié**, je veux recevoir mon badge et une preuve téléchargeable, afin de pouvoir la présenter à qui je veux. +51 - En tant que **membre certifié**, je veux ajouter mon badge à mon profil LinkedIn sans me tromper de champ, afin que les recruteurs qui me consultent puissent le vérifier. +52 - En tant que **membre de longue date**, je veux pouvoir repasser la certification librement, afin de renouveler mon badge quand il expire. +53 - En tant que **visiteur**, je veux parcourir l'annuaire public des certifiés, afin de voir qui est membre. +54 - En tant qu'**employeur**, je veux vérifier un badge en un clic, afin de savoir immédiatement s'il est valide. +55 - En tant qu'**employeur méfiant**, je veux vérifier un badge avec un outil que je choisis moi-même, afin de ne pas avoir à faire confiance à AIDD. +56 - En tant qu'**employeur**, je veux confirmer que le badge appartient bien au candidat en face de moi, afin qu'il ne puisse pas présenter celui d'un autre. +57 - En tant que **membre**, je veux pouvoir demander le retrait de mes données, afin de ne pas rester exposé après mon départ. +58 +59 ## Acceptance Criteria +60 +61 **Intake & émission** +62 - Remplir le formulaire ouvre une **issue**, puis un bot ouvre automatiquement une **pull request** contenant l'enregistrement du membre. +63 - **Aucun badge n'est émis tant que la pull request n'est pas mergée** par un mainteneur. +64 - Le **merge** déclenche la génération de la preuve signée, la mise à jour de la liste de statuts, et le déploiement de l'annuaire. +65 - Une demande en double (personne déjà certifiée) est **détectée et arrêtée** avant d'ouvrir une seconde pull request. +66 +67 **Vérification** +68 - Ouvrir l'URL d'un badge réel affiche **« valide »**, avec le nom du titulaire, les dates et l'émetteur. +69 - Un badge **altéré d'un octet** est rejeté comme **invalide**. +70 - Un badge **forgé sans la clé privée d'AIDD** est rejeté comme **invalide**. +71 - Un badge **échu** affiche une page propre « expiré ». +72 - La page de vérification **contrôle réellement la preuve cryptographique**. Une page qui afficherait « valide » sans le faire est un échec, pas une approximation. +73 +74 **Vérification indépendante** +75 - Un tiers **télécharge la preuve brute**, récupère la clé publique d'AIDD, et obtient le même verdict **via un outil de vérification tiers reconnu**, sans utiliser la page d'AIDD. +76 - Un badge de production est **accepté par le validateur public 1EdTech**. +77 - L'authenticité reste vérifiable **même si les serveurs d'AIDD sont injoignables**. +78 +79 **Liaison d'identité** +80 - Le badge est lié au **compte GitHub** du titulaire, dont le contrôle a été prouvé à l'émission (la demande émane de son compte). +81 - Un tiers peut confirmer que le badge correspond à la personne en vérifiant qu'elle contrôle ce compte GitHub (ou le profil LinkedIn associé). +82 +83 **Annuaire & modération** +84 - Les certifiés apparaissent dans l'**annuaire public**. +85 - La **photo** d'un membre est visible dans la pull request et n'est publiée qu'après merge. +86 - Une demande de retrait fait **disparaître la personne de l'annuaire** et **supprime sa photo** (objet Git LFS supprimé), sans réécriture de l'historique principal. +87 +88 **Cycle de vie** +89 - Tout badge **expire au bout d'un an**. +90 - Après renouvellement, l'ancien badge **reste valide jusqu'à sa propre échéance** — il n'est jamais signalé comme révoqué. +91 +92 ## Dependencies +93 +94 - **Modèle `manifest`** (`ai-driven-dev/manifest`) : le pipeline issue → bot → pull request → merge est calqué sur ce dépôt. Réutiliser son Issue Form, son workflow et son script de lecture de demande comme point de départ. +95 - **Une GitHub App « bot »** (App ID + clé privée en secret CI) pour ouvrir les pull requests au nom du bot — le `GITHUB_TOKEN` par défaut ne déclenche pas les checks requis. Identique au `SIGNATURE_BOT` du manifest. +96 - **La clé privée de signature des badges**, stockée en **secret GitHub Actions**, jamais dans le dépôt (CT-7). C'est l'ancre de confiance. +97 - **Git LFS** pour les photos (CT-12). +98 - **Conformité Open Badges 3.0**, mesurée par l'acceptation du badge par le validateur public 1EdTech. +99 - **LinkedIn ne pré-remplit plus les certifications** : le membre saisit chaque champ, dont l'URL de vérification. Le produit doit compenser ce risque (CT-10). +100 - **Une Page LinkedIn AIDD** (`linkedin.com/company/ai-driven-dev`) pour le bouton d'ajout au profil. +101 - **Le sous-domaine `verify.ai-driven-dev.fr`** (décidé, SP1), renouvelé à vie, inscrit dans l'identifiant de chaque badge : le changer casse tous les badges déjà émis. Servi en **pages statiques GitHub Pages** (aucune logique serveur au runtime ; voir SP1). +102 - **Le visuel du badge** (`design/`), produit séparément. +103 +104 ## Open Questions +105 +106 - **Rendu de l'annuaire et de la photo** : quel générateur pour le site (le manifest utilise Astro) ? *(SP3)* +107 - **Effacement d'une photo dans LFS** : procédure exacte de suppression de l'objet LFS côté GitHub, à documenter et tester. +108 - **Aucun juriste n'a validé le dispositif.** Le risque RGPD est faible et documenté (voir annexe), pas levé. +109 +110 --- +111 +112 # Annexe — Spécification technique +113 +114 *Cette annexe sort du périmètre « solution-agnostic » du PRD. Elle fixe les contraintes non négociables. Le framework du site, l'ORM éventuel et l'hébergement applicatif restent libres.* +115 +116 **Conventions.** « DOIT », « NE DOIT PAS », « PEUT » au sens RFC 2119. Chaque contrainte est normative ; justification et source sont fournies pour permettre de la contester sur pièces. +117 +118 ## CT-1 — Format de preuve : VC-JWT signé en RS256 +119 +120 Les credentials DOIVENT être émis au format **VC-JWT**, signés en **RS256**, clés publiées en **JSON Web Key**. **JWT + EdDSA NE DOIT PAS être utilisé.** Open Badges 3.0 ne reconnaît que deux formats conformes : *Linked Data Proofs en EdDSA*, ou *JWT en RS256*. La combinaison JWT + EdDSA n'est aucun des deux et est rejetée par les vérificateurs tiers — silencieusement (tout marche en local). Le chemin JWT est retenu pour éviter la canonicalisation JSON-LD ; son prix est RSA. +121 > [OB 3.0 Implementation Guide](https://www.imsglobal.org/spec/ob/v3p0/impl) +122 +123 ## CT-2 — La vérification cryptographique DOIT être effective +124 +125 La vérification DOIT résoudre la clé publique et **contrôler la signature**. Un contrôle limité à la présence des champs NE constitue PAS une vérification. Mode de défaillance documenté : l'émetteur open source Certo (`main`, `bd8a840`) signe correctement mais sa vérification est un stub — `return { valid: true }` après contrôle des seuls champs, sans jamais vérifier la signature. Une page de vérification qui ment est pire que pas de page : elle légitime les contrefaçons. +126 +127 ## CT-3 — Liaison d'identité : compte GitHub, prouvé à l'émission +128 +129 Le badge DOIT être lié au **compte GitHub** du titulaire. Le contrôle de ce compte est **prouvé à l'émission** : la demande (l'issue, puis la pull request) émane du compte GitHub de la personne, et le merge par un mainteneur atteste cette provenance. Aucun email n'est collecté. +130 +131 **Justification.** La preuve est exportable (CT-6) ; un badge qui ne porterait qu'un nom serait un **titre au porteur**. Le manifest résout cela par l'authentification GitHub à l'intake plutôt que par un secret vérifié après coup. Le handle GitHub est public, mais le **contrôle** est établi au moment de l'émission, et reste vérifiable a posteriori (le titulaire prouve qu'il contrôle le compte GitHub, ou le profil LinkedIn associé). Cela supprime le besoin d'un magasin d'emails privé. +132 +133 *Nuance assumée.* Cette liaison est plus faible qu'une preuve cryptographique de possession côté titulaire (un DID lié). Elle est cohérente avec le modèle communautaire et suffisante ici. Un `credentialSubject.id` de type DID reste une évolution possible. +134 +135 ## CT-4 — Révocation par Bitstring Status List v1.0 +136 +137 Le statut de révocation DOIT être publié via une **Bitstring Status List v1.0**, référencée depuis chaque credential. Un format propriétaire NE DOIT PAS être utilisé : il n'est lisible par aucun vérificateur conforme, et un badge révoqué continuerait d'apparaître valide partout ailleurs. +138 +139 ## CT-5 — Le canal de révocation est réservé au retrait +140 +141 Le `statusPurpose: revocation` NE DOIT servir **qu'**à honorer une demande de retrait de données. Il NE DOIT PAS signaler une supersession : la liste de statuts n'a pas d'état « remplacé », un vérificateur y lirait « révoqué » = invalide. Un badge remplacé **reste valide** jusqu'à son échéance ; la supersession est affichée sur la page, pas publiée dans la liste. +142 +143 ## CT-6 — Exportabilité de la preuve +144 +145 La preuve brute (JWT signé) DOIT être téléchargeable depuis la page du credential. La procédure de vérification indépendante DOIT être documentée publiquement, en désignant un outil tiers conforme (validateur public 1EdTech). La vérification n'est indépendante que si elle peut s'opérer hors de l'infrastructure d'AIDD, et survivre à la disparition du service. +146 +147 ## CT-7 — Gestion et rotation des clés +148 +149 Chaque JWT DOIT porter un `kid` **égal à une URL HTTPS déréférençable**. Cette URL DOIT résoudre vers **un document JWK nu** (`{"kty","n","e",…}`) — **pas** une enveloppe JWKS `{"keys":[…]}` : le validateur 1EdTech lit `kty` à la racine de la réponse (vérifié empiriquement, SP4). Concrètement, publier **un fichier JWK par clé** (ex. `/keys/.json`) ; un index JWKS agrégé PEUT être publié en plus pour d'autres consommateurs, mais n'est pas ce que le `kid` déréférence. Après rotation, **les clés publiques antérieures DOIVENT rester publiées indéfiniment** (les retirer rend invérifiables tous les badges signés avec elles). **La clé privée de signature DOIT être un secret GitHub Actions, jamais commitée**, ni en Git ni en LFS. Elle n'est utilisée qu'au **build** (au merge, en CI) ; le runtime de vérification ne lit que la clé publique. Sa fuite permet de forger des badges AIDD indétectables. +150 +151 ## CT-8 — Cycle de vie +152 +153 Tout credential DOIT expirer **un an** après l'émission. Toute page de credential — valide, expiré, remplacé, révoqué, inconnu — DOIT retourner une page explicite (jamais un 404). La page d'un credential révoqué NE DOIT divulguer **aucune donnée personnelle**. +154 +155 ## CT-9 — Primitives cryptographiques +156 +157 Signature et vérification DOIVENT reposer sur une bibliothèque auditée couvrant RS256, JWKS et `kid`. Aucune primitive NE DOIT être réimplémentée. En JavaScript, [`jose`](https://github.com/panva/jose) convient (`SignJWT`, `jwtVerify`, `createRemoteJWKSet`) — vérifié. Tout équivalent dans un autre langage DOIT être vérifié sur ces trois points avant adoption. +158 +159 ## CT-10 — Intégration LinkedIn +160 +161 La page du credential et le message de livraison DOIVENT présenter les valeurs à reporter dans LinkedIn, champ par champ, dont la **Credential URL**. LinkedIn ne pré-remplit plus les certifications (le microsite `addtoprofile.linkedin.com` redirige vers l'aide : « it will no longer autofill and members must enter the relevant information directly »). La Credential URL étant l'unique lien entre le profil et la preuve, une entrée sans elle est indiscernable d'une auto-déclaration. +162 > [LinkedIn Help](https://www.linkedin.com/help/linkedin/answer/a528030). Prérequis : une Page LinkedIn. +163 +164 ## CT-11 — Pipeline d'intake git-natif +165 +166 L'intake DOIT suivre le modèle `manifest` : un **Issue Form** pose un label infalsifiable côté serveur ; un workflow, agissant sous une **GitHub App** (pas le `GITHUB_TOKEN` par défaut), lit la demande, écrit l'enregistrement, ouvre une **pull request** et ferme l'issue. **L'émission (signature) n'a lieu qu'au merge sur `main`.** Le merge par un mainteneur EST le point d'autorité (CT-3, E0). Aucune voie NE DOIT permettre d'émettre un badge sans ce merge. +167 +168 ## CT-12 — Séparation du magasin de données +169 +170 Les données DOIVENT être scindées selon leur effaçabilité : +171 - **Publiques et durables** (handle GitHub, nom, LinkedIn, rôle, dates, identifiant de badge) → fichiers versionnés dans Git. Publiées par conception, destinées à persister ; base légale = exécution du contrat. +172 - **Photos** → **Git LFS**. Le binaire vit comme objet LFS hors de l'historique principal ; Git ne garde qu'un pointeur. L'effacement DOIT être la **suppression de l'objet LFS**, sans réécriture d'historique. Les photos DOIVENT être **redimensionnées, converties en WebP/AVIF et débarrassées de leurs métadonnées EXIF** (géolocalisation) avant publication. L'original n'est pas conservé. +173 - **Aucune donnée personnelle effaçable NE DOIT être écrite dans l'historique Git ordinaire** (il est append-only ; `git rm` n'efface pas le passé). +174 +175 ## CT-13 — Aucun secret dans le dépôt +176 +177 La clé privée de signature (CT-7) et la clé privée de la GitHub App (CT-11) NE DOIVENT jamais être commitées, ni en Git ni en LFS. Elles vivent en secrets GitHub Actions. Le `.gitignore` DOIT bloquer les extensions de clés et les fichiers d'environnement. +178 +179 ## CT-14 — Dépôt public +180 +181 Le dépôt DOIT être **public**. Les verrous de confiance dont dépend tout le dispositif — protection de branche (le merge par un mainteneur = point d'autorité, CT-11) et protection d'environnement (le gate de review sur la signature, CT-7) — **ne sont pas disponibles sur un dépôt privé au plan gratuit** (vérifié : `403`/`422` de l'API GitHub). Les rendre effectifs impose soit le public (gratuit), soit un plan payant (écarté, cf. « aucune dépendance payante »). Le public est sans risque ici : la clé privée est un secret hors dépôt (CT-7/CT-13), aucune donnée effaçable ne vit en Git (CT-12), et les données des membres sont de toute façon destinées à l'annuaire public. Le public renforce aussi l'intake : tout titulaire d'un compte GitHub peut déposer sa demande (CT-11). Repasser le dépôt en privé désactiverait les verrous et casserait le modèle de confiance. +182 +183 ## Modèle de données +184 +185 **Un dossier par membre**, nommé d'après son handle GitHub : `data/members//record.yml` (la fiche) + `data/members//photo.webp` (objet Git LFS). L'effacement RGPD est alors `rm -rf data/members//`. Ces champs sont **publics et durables** (CT-12) et vivent dans Git. +186 +187 ```yaml +188 github: # requis — identité du badge (CT-3), = auteur de l'issue de demande +189 role: certifie # requis — certifie | habilite +190 name: Prénom Nom # requis — nom public +191 linkedin: https://... # requis — URL de profil +192 photo: # requis — data/members//photo.webp (objet Git LFS), normalisé WebP sans EXIF +193 status_index: # requis — index permanent dans la Bitstring Status List (CT-4), assigné à l'intake +194 website: https://... # optionnel — site personnel +195 description: # optionnel — présentation affichée dans l'annuaire (≤ 280) +196 renewed_on: # optionnel — renouvellement (#27) : prime sur la date de 1re certif +197 ``` +198 +199 **Renouvellement (#27).** Par défaut, la date d'émission = date du commit qui a +200 ajouté le record (1re certification). Un renouvellement fixe `renewed_on` dans le +201 record : l'émission repart de cette date (nouvelle fenêtre de 1 an). Une preuve +202 déjà exportée garde son ancienne échéance et reste valide jusque-là. +203 +204 **Retrait RGPD (#36).** Un retrait **supprime tout le dossier** `data/members//` (fiche + photo LFS) — l'effacement, pas une réécriture d'historique. Pour que le badge reste révoqué à vie malgré la disparition de la fiche, son `status_index` est inscrit dans un **registre de révocation** `data/revoked.json` (une liste d'entiers — pas de donnée personnelle). La liste de statuts (CT-4) se construit à partir de ce registre. Une preuve déjà exportée reste présentable mais apparaît **révoquée** à la vérification (CT-5, seule voie). +205 +206 Le site public `ai-driven-dev.fr` (`/communaute`) affiche l'annuaire ; il consomme le flux **`directory.json`** publié par la CI sur `verify.ai-driven-dev.fr` (nom, LinkedIn, photo, site, description, URL de vérif). Les photos sont servies sur `verify.ai-driven-dev.fr/photos/.webp`. Les membres révoqués sont exclus du flux. +207 +208 **Généré au merge, pas saisi** (le job d'émission le calcule à partir de la date de merge, pour qu'un demandeur ne fixe pas lui-même sa validité) : +209 +210 - `badge_id` — identifiant unique du credential ; +211 - `certified_on` — date d'émission (= date de merge) ; +212 - `expires_on` — `certified_on` + 1 an (CT-8). +213 +214 Ces valeurs ne figurent pas dans le YAML d'intake : elles sont scellées dans le credential signé et exposées par la page de vérification. +215 +216 **Photo** : jamais le binaire dans le YAML — seulement un chemin vers un objet **Git LFS** (CT-12). L'effacement est la suppression de l'objet LFS, sans réécriture d'historique. +217 +218 **Aucune donnée effaçable (email) n'est stockée** — l'identité repose sur le compte GitHub (CT-3). +219 +220 ## Validation de conformité +221 +222 Le validateur public [`1EdTech/digital-credentials-public-validator`](https://github.com/1EdTech/digital-credentials-public-validator) fait référence : un credential qu'il rejette n'est pas conforme, quel que soit le verdict de notre page. +223 +224 ## Solutions évaluées et écartées +225 +226 | Solution | Motif | +227 |---|---| +228 | **Credly** | Tarification sur devis, ~3 000 $/an rapportés pour 500 credentials (estimation indirecte). Lock-in. | +229 | **Accredible** | Pas d'offre gratuite pérenne (~45 $/mois, engagement 12 mois). | +230 | **Badgr / Canvas** | Émission gratuite arrêtée le 31/12/2025. | +231 | **Certifier.io** | SaaS ; conformité OB 3.0 réelle non confirmée. | +232 | **Certo** | Vérification non implémentée (CT-2). | +233 | **Modèle manifest pur (sans crypto)** | Abandonnerait la vérifiabilité tierce, jugée critique. Retenu pour l'intake, pas pour la preuve. | +234 | **Linked Data Proofs / EdDSA** | Impose la canonicalisation JSON-LD (CT-1). | +235 +236 --- +237 +238 # Annexe — Feuille de route de construction +239 +240 *Séquence indicative. L'ordre suit les dépendances : chaque phase produit un résultat vérifiable.* +241 +242 ## Surfaces +243 +244 | Surface | Rôle | Accès | +245 |---|---|---| +246 | Formulaire / Issue Form | Demande de certification | Public (membre) | +247 | Bot + workflow PR | Écrit l'enregistrement, ouvre la PR | GitHub App | +248 | Merge (mainteneur) | Point d'autorité → déclenche l'émission | Équipe AIDD | +249 | Job de signature (CI) | Génère le VC-JWT signé au merge | Secret CI | +250 | Annuaire + page de vérification | Affichage public, vérification réelle | Public | +251 | JWKS + Bitstring Status List | Clés publiques, statuts | Public | +252 +253 ## Phase 0 — Socle de confiance +254 - Réserver le sous-domaine permanent + TLS. +255 - Générer la paire RSA ; clé privée en **secret GitHub Actions** (jamais commitée). +256 - Publier le JWKS avec `kid`, conservation des anciennes clés prévue. +257 - Créer la GitHub App « bot » (App ID + clé privée en secret). +258 - Activer **Git LFS** pour `*.webp`/`*.avif`. +259 +260 ## Phase 1 — Intake (calqué sur manifest) +261 - Issue Form (rôle, handle déduit du compte, nom, LinkedIn, photo). +262 - Workflow sous GitHub App : lecture de la demande, dédup, écriture de l'enregistrement + photo (LFS, WebP + strip EXIF), ouverture de la PR, fermeture de l'issue. +263 - **Vérifiable :** une demande ouvre une PR contenant l'enregistrement et la photo, visible en revue. +264 +265 ## Phase 2 — Émission au merge +266 - Au merge sur `main` : générer le credential OB 3.0 signé (RS256, `kid`, expiration 1 an, `credentialStatus`, liaison au handle GitHub), publier la preuve téléchargeable. +267 - Ré-émission : l'ancien badge reste valide jusqu'à échéance (pas de révocation). +268 - **Vérifiable :** un badge émis est **accepté par le validateur public 1EdTech**. Jalon de conformité. +269 +270 ## Phase 3 — Vérification & annuaire +271 - Page de vérification : signature + `exp` + statut, réellement contrôlés ; états valide/expiré/remplacé/révoqué/invalide, toujours en page propre. +272 - Téléchargement de la preuve brute + doc de vérification indépendante. +273 - Bitstring Status List publiée. +274 - Annuaire public des certifiés (photo, nom, rôle, lien de vérification). +275 - **Vérifiable :** test adverse (badge forgé + badge altéré rejetés par la page **et** par le validateur tiers) ; authenticité vérifiable serveur de statut injoignable. +276 +277 ## Phase 4 — LinkedIn & partage +278 - Bouton Add-to-Profile + bloc de valeurs à copier (page + email). +279 - Kits de partage (anneau LinkedIn, signature email, badge embed) issus de `design/`. +280 +281 ## Phase 5 — Retrait & RGPD +282 - Procédure de retrait : révocation dans la liste de statuts + retrait de l'annuaire + **suppression de l'objet LFS de la photo**. +283 - Vérifier que les CGU décrivent ce traitement (information art. 13, pas de consentement). +284 - Rédiger et archiver la justification de non-réalisation d'une AIPD. +285 +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:11:57Z)**_ + +
+Tool use: **Bash** + +```bash +echo "--- .github ---"; find .github -maxdepth 3 -not -path '*/node_modules*' | sort +echo "--- data/members ---"; ls -la data/members 2>/dev/null | head -30 +echo "--- data/revoked.json ---"; cat data/revoked.json 2>/dev/null +echo "--- directory.json ---"; cat directory.json 2>/dev/null +echo "--- design/README ---"; cat design/README.md 2>/dev/null | head -50 +``` + +```text +--- .github --- +.github +.github/CODEOWNERS +.github/ISSUE_TEMPLATE +.github/ISSUE_TEMPLATE/certification-request.yml +.github/ISSUE_TEMPLATE/removal-request.yml +.github/scripts +.github/scripts/build-directory.mjs +.github/scripts/build-site.sh +.github/scripts/demo.mjs +.github/scripts/key-ceremony.mjs +.github/scripts/lib +.github/scripts/lib/certification-request.mjs +.github/scripts/lib/certification-request.test.mjs +.github/scripts/lib/credential.mjs +.github/scripts/lib/credential.test.mjs +.github/scripts/lib/directory.mjs +.github/scripts/lib/directory.test.mjs +.github/scripts/lib/issue-form.mjs +.github/scripts/lib/issue-form.test.mjs +.github/scripts/lib/member-paths.mjs +.github/scripts/lib/member-paths.test.mjs +.github/scripts/lib/photo-url.mjs +.github/scripts/lib/photo-url.test.mjs +.github/scripts/lib/photo.mjs +.github/scripts/lib/photo.test.mjs +.github/scripts/lib/qr.mjs +.github/scripts/lib/qr.test.mjs +.github/scripts/lib/revocation.mjs +.github/scripts/lib/revocation.test.mjs +.github/scripts/lib/signing.test.mjs +.github/scripts/lib/status-list.mjs +.github/scripts/lib/status-list.test.mjs +.github/scripts/package-lock.json +.github/scripts/package.json +.github/scripts/process-photo.mjs +.github/scripts/read-certification-request.mjs +.github/scripts/remove-member.mjs +.github/scripts/sign-credential.mjs +.github/scripts/sign-status-list.mjs +.github/workflows +.github/workflows/certification-pr.yml +.github/workflows/emit-and-deploy.yml +.github/workflows/key-ceremony.yml +.github/workflows/removal-pr.yml +.github/workflows/scripts-tests.yml +--- data/members --- +alexmartin/ +crousseau/ +ybenali/ +--- data/revoked.json --- +{ + "revoked": [] +} +--- directory.json --- +{ + "generatedAt": "2026-07-22T10:00:00Z", + "count": 3, + "members": [ + { + "handle": "alexmartin", + "name": "Alexandre Martin", + "role": "certifie", + "linkedin": "https://linkedin.com/in/alexmartin", + "photo": "http://localhost:8200/photos/alexmartin.webp", + "verify": "http://localhost:4321/u/alexmartin", + "description": "Lead dev, orchestration d'agents en prod." + }, + { + "handle": "crousseau", + "name": "Camille Rousseau", + "role": "certifie", + "linkedin": "https://linkedin.com/in/crousseau", + "photo": "http://localhost:8200/photos/crousseau.webp", + "verify": "http://localhost:4321/u/crousseau", + "description": "Freelance, TDD assisté par IA." + }, + { + "handle": "ybenali", + "name": "Yassine Benali", + "role": "certifie", + "linkedin": "https://linkedin.com/in/ybenali", + "photo": "http://localhost:8200/photos/ybenali.webp", + "verify": "http://localhost:4321/u/ybenali", + "description": "CTO, industrialisation du code généré." + } + ] +} +--- design/README --- +# Design — Badges AIDD + +Source : projet Claude Design **« Badge de certification communauté »** +`https://claude.ai/design/p/1a46ce33-ef04-435c-8a71-76f115cdfc87` + +Kit de badges pour deux rôles — **Certifié** (niveau 1) et **Habilité** (niveau 2) — couvrant : sceau, anneau de photo LinkedIn + coin de bannière, badge intégrable web, signature email, et la maquette de la page de vérification. + +## Fichiers + +| Fichier | Rôle | +|---|---| +| `Badges AIDD.dc.html` | Maquette principale (source Design Canvas) | +| `Seal.dc.html` | Le sceau — cœur visuel, réutilisé partout via `` | +| `Badge AIDD - standalone.html` | Bundle autonome (aperçu hors runtime) | +| `support.js` | Runtime Design Canvas requis par les `.dc.html` | +| `image-slot.js` | Composant de dépôt d'image | +| `assets/logo.png`, `uploads/` | Sources graphiques | +| `screenshots/` | Aperçus rendus | + +Import complet, récupéré par export ZIP depuis Claude Design (le MCP plafonne la lecture à 256 Kio, dépassé par le sceau et le bundle qui inline des images). + +## À réaligner avec le PRD + +Le design est une maquette ; ces trois points sont à corriger à l'implémentation, pas dans le PRD : + +1. **Domaine.** Le design montre `verify.aidd.community/u/`. Domaine réel retenu (SP1) : **`verify.ai-driven-dev.fr/u/`** — à substituer dans le design. +2. **Promesse de vérification.** Le design affiche « Signature cryptographique validée par le **serveur AIDD** ». Le PRD garantit l'inverse — une vérification **indépendante du serveur** (CT-2, CT-6). Copie à réaligner sur la promesse réelle, sinon le design vend plus faible que le produit. +3. **Habilité.** Traité à parité avec Certifié dans le design ; le PRD le scope en v2. Cohérent tant que la v1 n'émet que Certifié — le design est simplement en avance. + +Note : les données affichées (nom, identifiant `AIDD-2K7F-9QX3`, dates) sont des **exemples de maquette**, pas de vraies données. +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:12:06Z)**_ + +
+Tool use: **Bash** + +```bash +cd .github/scripts && cat package.json | head -20 +echo "---" +cd ../../site && cat package.json +echo "--- workflows ---" +cat ../.github/workflows/emit-and-deploy.yml | head -40 +``` + +```text +{ + "name": "aidd-badges-scripts", + "private": true, + "type": "module", + "description": "Scripts d'intake et d'émission des badges AIDD", + "scripts": { + "test": "node --test", + "demo": "node demo.mjs" + }, + "dependencies": { + "jose": "^6.2.3", + "qrcode": "^1.5.4", + "sharp": "^0.33.5" + } +} +--- +{ + "name": "aidd-badges-site", + "private": true, + "type": "module", + "description": "Code navigateur de vérification des badges (sans dépendance runtime)", + "scripts": { + "test": "node --test" + }, + "devDependencies": { + "jose": "^6.2.3" + } +} +--- workflows --- +# Émission signée + publication (#23). Au merge sur main (ou manuellement), +# régénère TOUS les credentials de façon déterministe et déploie le site statique +# sur GitHub Pages : clés publiques, profil issuer, une preuve VC-JWT par membre. +# +# Déterminisme : certified_on = date du commit qui a ajouté data/members//record.yml. +# RS256 étant déterministe, régénérer produit le même credential — aucune preuve +# n'est committée, aucun contournement de la protection de main n'est requis. +name: Émettre et publier + +on: + push: + branches: [main] + paths: + - "data/members/**" + - "data/revoked.json" + - "keys/**" + - "public/**" + - "site/**" + - ".github/scripts/sign-credential.mjs" + - ".github/scripts/sign-status-list.mjs" + - ".github/scripts/build-directory.mjs" + - ".github/scripts/build-site.sh" + - ".github/scripts/lib/**" + - ".github/workflows/emit-and-deploy.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + environment: signing # main uniquement, expose SIGNING_PRIVATE_KEY (pas d'approbation) + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:12:10Z)**_ + +
+Tool use: **Bash** + +```bash +git status --short | head -20 +echo "---tracked members---" +git ls-files data/members/ | head -5 +git ls-files directory.json +``` + +```text +?? ../.localserve/ +?? ../.specstory/ +?? ../data/members/ +?? ../directory.json---tracked members--- +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:12:12Z)**_ + +Confirmed: no members tracked in git yet. Those 3 (alexmartin, crousseau, ybenali) sit untracked locally — demo/test data, not real production members. Repo is pre-launch: pipeline built, zero real certifieds yet. + +Now building community presentation artifact. + +_**Agent (claude-sonnet-5 2026-07-22 10:12:12Z)**_ + +
+Tool use: **Skill** + +**Input:** + +- skill: `artifact-design` + +
+ +--- + +_**User (2026-07-22 10:12:13Z)**_ + +Approach this as the design lead at a small studio known for their versatility, giving every client a visual identity pitched at the treatment the task actually calls for. Make deliberate choices about palette, typography, and layout that are specific to this subject, and avoid templated designs. + +## Read the request first + +Calibrate treatment, not whether to design. A doc deserves the same craft as a landing page — what changes is the treatment that craft is delivered in. + +Many requests call for a more utilitarian treatment: a plan, a memo, a demo. Make it polished: include real typographic hierarchy, considered spacing, and a proper palette, but avoid over-designing. Most pages do not need a flashy, gigantic hero. Keep flourishes tasteful and limited. + +Some requests call for an editorial treatment: a landing page, a game, an app or tool they'll keep or share. + +When unsure: a well-composed page is never the wrong answer; an over-designed visual identity sometimes is. + +Fundamentals below apply to everything. The editorial process after that runs only when the read above says so. + +## Fundamentals for every artifact + +**Honor what's already there** Look for an existing design system first — CLAUDE.md, a tokens or theme file, existing component styles. When one exists, apply it; everything below fills gaps and never overrides. Precedence is always: the user's own words, then the project's existing system, then your choices. + +**Ground it in the subject.** If the subject isn't already clear, pin it: one concrete subject, its audience, and the page's single job. The subject's own world — its materials, instruments, vernacular — is where distinctive choices come from. Build with real content throughout, never lorem. + +**Pair typefaces** Typography carries the page even when the page isn't about typography. The Artifact CSP blocks font CDNs, so don't link a webfont URL and risk a silent fallback. Instead inline the face as a @font-face data URI. Keep running text near 65 characters wide; set a type scale and stay on it; give headings `text-wrap: balance`, body text room to breathe, and uppercase labels a touch of letter-spacing. + +**Choose neutrals, don't default to them.** A pure mid-grey reads as unconsidered; a grey with a slight hue bias toward the page's accent reads as chosen. Pure white and near-black are fine grounds when they suit the subject — the point is that the neutral was picked, not inherited. + +**Design both themes.** The page renders in the viewer's theme: `prefers-color-scheme` carries the OS preference, and the viewer's toggle stamps `data-theme="dark"` / `data-theme="light"` on the root element, which must override the media query in both directions. The robust pattern is token-level: define the palette as custom properties on `:root`, redefine only the tokens under `@media (prefers-color-scheme: dark)` — style components through the tokens, never directly inside the media query — then redefine them again under `:root[data-theme="dark"]` and `:root[data-theme="light"]`. Give the second theme the same care as the first — don't naively invert; keep contrast legible and the accent working on both grounds. A design that deliberately commits to one visual world (a neon arcade screen, a letterpress invitation) may stay single-theme — make it a choice, not an omission. + +**Let layout do the spacing.** Lay out sibling groups with flex or grid and `gap`, not per-element margins that silently collapse or double. Wide content — tables, code, diagrams — gets `overflow-x: auto` on its own container so the page body never scrolls sideways. Reach for `font-variant-numeric: tabular-nums` wherever digits line up in columns. + +**Avoid AI-generated design** AI-generated design currently clusters around a few looks: warm cream (#F4F1EA) with a serif display and terracotta accent; near-black with a lone acid-green or vermilion pop; broadsheet hairline rules with dense columns; a purple-to-blue gradient hero on white; Inter or Space Grotesk as the "safe" face; emoji as section markers; everything centered; `rounded-lg` everywhere; accent bar/rail on rounded cards. Where the user pins down a visual direction, follow it exactly — their words always win, including when they ask for one of these looks. Where nothing is specified, don't spend that freedom on one of these defaults. + +**Build cleanly** Be cognizant of overlapping elements, cascade collisions, silent font fallbacks; visual bugs hide in the gap between source and output. Close every non-void element, double-quote attributes, give keyboard focus a visible state, respect `prefers-reduced-motion`. For generative or decorative graphics, reach for Canvas or WebGL rather than hand-authoring long SVG path data. + +**CSS rules** When writing the CSS, watch your selector specificities. It is easy to generate classes that cancel each other out — a type-based selector like `.section` fighting an element-based one like `.cta` over padding and margins between sections. Structure the cascade so it doesn't silently undo your spacing. + +**Writing the copy** Words are design material, not decoration. Write from the user's side of the screen — name things by what people recognize, not how the system is built (a person manages *notifications*, not *webhook config*). Active voice; a control says exactly what happens ("Publish", then a toast that says "Published"). Errors explain what went wrong and how to fix it — no apologies, no vagueness. Specific beats clever. + +**Structure is information** Structural devices, numbering, eyebrows, dividers, labels, should encode something true about the content, not decorate it. Many generic designs use numbered markers (01 / 02 / 03), but that's only appropriate if the content actually is a sequence - like a real process or a typed timeline where order carries information the reader needs. Question if choices like numbered markers actually make sense before incorporating them. + +**When it's a UI, not a document** A dashboard or tool is scanned and operated, not read top-to-bottom, so the craft shifts from typography to information design. Surface the summary before the detail; encode state in form as well as number — a pill, a chip, a severity stripe — so what needs attention reads at a glance. Semantic color (good / warning / critical) is separate from the accent hue and doesn't count as your accent. Give sparklines and charts the same care as type: an area fill, a faint grid, an emphasized endpoint. What's interactive should look interactive. + + + +## Process + +Before writing code, sketch a short design plan — a compact token system with color, type, and layout: +- **Color**: describe the palette as 4–6 named hex values. +- **Type**: typefaces for 2+ roles — a characterful display face used with restraint, a complementary body face, and a utility face for captions or data if needed. +- **Layout**: a layout concept in one or two sentences. + +Then build, following the plan and deriving every color and type decision from it. + +## When the request is editorial + +The stance shifts: the client has already rejected proposals that felt templated, and is paying for a distinctive point of view. Make opinionated calls, and take one real aesthetic risk where it serves the work. + +Review the design plan against the subject before building: if any part of it reads like the generic default you would produce for any similar page, revise that part, and note what you changed and why. Only after you've confirmed the plan's uniqueness do you write the code, following the revised plan exactly. + +**Principles** + +- The hero is a thesis: open with the most characteristic thing in the subject's world — headline, image, live demo, interactive moment. +- Typography carries the personality of the page. Pair the display and body faces deliberately, not the same families you would reach for on any other project, and set a clear type scale with intentional weights, widths, and spacing. Make the type treatment itself a memorable part of the design, not a neutral delivery vehicle for the content. +- Leverage motion deliberately. Think about where and if animation can serve the subject: a page-load sequence, a scroll-triggered reveal, hover micro-interactions, ambient atmosphere. An orchestrated moment usually lands harder than scattered effects; choose what the direction calls for. However, sometimes less is more, and extra animation contributes to the feeling that the design is AI-generated. +- Match complexity to the vision. Maximalist directions need elaborate execution; minimal directions need precision in spacing, type, and detail. Elegance is executing the chosen vision well. +- Spend your boldness in one place; keep everything around it quiet. If the accent fights the ground, shift it toward analogous or drop saturation rather than replacing it. + + +--- + +_**Agent (claude-sonnet-5 2026-07-22 10:12:45Z)**_ + +
+Tool use: **Bash** + +```bash +mkdir -p /private/tmp/claude-501/-Users-baptistelafourcade-Projects-freelance-aidd-aidd-badges/196f9048-108a-4c15-98d7-f6d91a2a7704/scratchpad +ls .github/ISSUE_TEMPLATE/ +cat .github/ISSUE_TEMPLATE/certification-request.yml | head -40 +``` + +```text +ls: .github/ISSUE_TEMPLATE/: No such file or directory +cat: .github/ISSUE_TEMPLATE/certification-request.yml: No such file or directory +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:12:48Z)**_ + +
+Tool use: **Bash** + +`pwd` + +```text +/Users/baptistelafourcade/Projects/freelance/aidd/aidd/badges/site +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:12:50Z)**_ + +Right, cwd drifted. Enough info gathered — building the artifact now. + +_**Agent (claude-sonnet-5 2026-07-22 10:14:09Z)**_ + +
+Tool use: **Write** + +```html +AIDD Badges — présentation du dépôt + + +
+ +
+
+

ai-driven-dev / badges

+

Le badge qui prouve, pas qui déclare

+

Comment AIDD délivre à ses membres certifiés une preuve vérifiable par n'importe qui — sans jamais dépendre de nos serveurs.

+
+ +
+ +
+ + État actuel : pipeline complet et testé (~140 tests) — 0 certifié réel publié pour l'instant. Le dépôt est en pré-lancement. +
+ +
+

« Certifié AIDD » écrit sur un profil LinkedIn ne prouve rien : n'importe qui peut l'écrire. Ce dépôt corrige ça. Chaque certifié reçoit un badge Open Badges 3.0, signé cryptographiquement (RS256), que n'importe quel tiers peut vérifier sans nous croire sur parole — même si nos serveurs sont hors ligne.

+
+ +
+

01 — Le problème

+

Un titre sans preuve ne vaut rien

+

Un employeur qui lit « Certifié AIDD » sur un CV n'a aucun moyen de vérifier si c'est vrai. Le membre a pourtant passé une masterclass évaluée — mais son effort ne se convertit en aucun signal reconnaissable. Le badge existe pour combler exactement ça : donner à un tiers un moyen de confirmer lui-même l'authenticité, avec un outil qu'il choisit, indépendant d'AIDD.

+
+ +
+

02 — Le parcours

+

Cinq étapes, une seule autorité

+
    +
  1. + 1 +
    +
    S'inscrire
    +
    Le membre remplit un formulaire GitHub (Issue Form). Son identité, c'est son compte GitHub — aucun email collecté.
    +
    +
  2. +
  3. + 2 +
    +
    Générer
    +
    Un bot ouvre automatiquement une pull request : fiche du membre, photo normalisée (WebP, sans EXIF), aperçu de modération.
    +
    +
  4. +
  5. + 3 +
    +
    Émettre
    +
    Un mainteneur merge — c'est le seul point d'autorité du système. Le merge déclenche la signature et la publication.
    +
    +
  6. +
  7. + 4 +
    +
    Vérifier
    +
    La page du badge contrôle la signature dans le navigateur du visiteur, pas sur un serveur AIDD. Preuve brute téléchargeable.
    +
    +
  8. +
  9. + 5 +
    +
    Retirer
    +
    Une demande de retrait supprime la fiche et la photo, et révoque le badge — traitement RGPD.
    +
    +
  10. +
+
+ +
+

03 — Architecture

+

Deux couches : les données, et la belle page

+

Les données qui font foi (clés, statuts, preuves, photos) vivent sur GitHub Pages — l'ancre permanente, gratuite, quasi jamais indisponible. La belle page de vérification et l'annuaire tournent sur un site à part (VPS, Astro), qui lit ces données. Si le site tombe, la preuve reste vérifiable via Pages : c'est le filet de sécurité.

+
+\```mermaid +flowchart LR + subgraph gh["GitHub"] + CI["CI — signe
(clé privée, secret)"] + Pages["Pages — clés, statuts,
preuves (ancre + fallback)"] + end + subgraph vps["Site — VPS, Astro"] + UI["/u/handle — vérification
+ /communaute — annuaire"] + end + Visiteur["Visiteur / recruteur"] + CI -->|publie| Pages + Visiteur -->|clic sur le badge| UI + UI -->|lit les données| Pages +\``` +
La signature n'a jamais lieu sur un serveur : uniquement en CI, au moment du merge.
+
+ +

Ce que la page de vérification contrôle réellement

+
+\```mermaid +flowchart TD + A["Ouvre la page du badge"] --> B["charge credential.jwt"] + B --> C["résout la clé publique via le kid"] + C --> D{"Signature valide ?"} + D -->|non| INV["Invalide"] + D -->|oui| E{"Révoqué ?"} + E -->|oui| REV["Révoqué"] + E -->|non| F{"Expiré ?"} + F -->|oui| EXP["Expiré"] + F -->|non| OK["Valide"] +\``` +
Un badge altéré d'un seul octet, ou forgé sans la clé privée d'AIDD, est rejeté — y compris par un vérificateur tiers.
+
+
+ +
+

04 — Garanties

+

Ce qui rend la preuve sérieuse

+
+ Open Badges 3.0 + VC-JWT · RS256 + Bitstring Status List v1.0 + Validateur public 1EdTech + Git-natif + Sans email +
+
+
+

Clé privée hors du dépôt

+

La clé de signature vit en secret GitHub Actions, jamais commitée. Sa fuite forgerait des badges indétectables — c'est le seul vrai risque du système.

+
+
+

Vérification réellement cryptographique

+

Pas un simple contrôle de champs présents : la signature RS256 est recalculée et comparée, dans le navigateur du vérificateur.

+
+
+

Révocation honnête

+

Le canal de révocation ne sert qu'au retrait de données — jamais à signaler un remplacement. Un badge renouvelé reste valide jusqu'à son échéance.

+
+
+

RGPD sans donnée irréversible

+

Les photos vivent en Git LFS, hors de l'historique. Un retrait supprime l'objet — pas de réécriture d'historique, pas de donnée effaçable coincée dans Git.

+
+
+

Dépôt public, volontairement

+

Les verrous de confiance (merge = autorité, review de clé) exigent un dépôt public au plan gratuit. Sans risque : la clé privée n'y est jamais, et les fiches sont de toute façon destinées à l'annuaire.

+
+
+

Ancienne clé jamais retirée

+

Après une rotation, les anciennes clés publiques restent publiées à vie — sinon leurs badges deviendraient invérifiables.

+
+
+
+ +
+

05 — Structure du dépôt

+

Où vit quoi

+
+ + + + + + + + + + + + + +
CheminContenu
docs/PRD.mdSpécification produit + 14 contraintes techniques non négociables (CT-1…CT-14)
docs/ARCHITECTURE.mdLes flux détaillés, avec diagrammes
MAINTAINERS.mdGuide des mainteneurs — valider, retirer, faire tourner la clé
.github/scripts/Intake, signature, révocation, annuaire — et leurs tests
.github/workflows/PR d'inscription, PR de retrait, émission + déploiement, cérémonie de clé
site/Code de vérification exécuté dans le navigateur (+ tests)
data/members/<handle>/Fiche + photo (LFS) d'un membre — un dossier par certifié
keys/Clés publiques JWK publiées à vie (jamais la clé privée)
design/Maquettes du visuel de badge (sceau, anneau LinkedIn, page de vérif)
+
+
+ +
+

06 — Feuille de route

+

Où on en est

+
+
+
FAIT
+
Phase 0–3 — Socle, intake, émission, vérificationClés RS256, pipeline issue → bot → PR → merge, signature déterministe, annuaire, ~140 tests, CI qui tourne à chaque push.
+
+
+
EN COURS
+
Phase 4 — LinkedIn & partageBouton d'ajout au profil, kit de partage (anneau LinkedIn, signature email), intégration de l'annuaire sur ai-driven-dev.fr/communaute.
+
+ +
+

Périmètre volontairement restreint : moins de 100 certifications par an attendues.

+
+ +
+

07 — Qui fait quoi

+

Pas de self-service

+

Aucun badge n'est émis sans revue humaine. Un mainteneur lit la fiche et la photo dans la pull request avant de merger — c'est de la modération, pas de l'automatisation pure. Un titre décerné pour non-conformité se ferme, sans merge ; le membre peut redéposer une demande.

+

Ce que ce système ne fait pas, volontairement : pas de détection de triche, pas de gestion de contestation, pas de révocation pour non-paiement — qui cesse de payer garde son badge jusqu'à échéance.

+
+ +
+ Licence MIT + · + Tests : cd .github/scripts && npm test + · + Démo locale : npm run demo +
+ +
+ +``` + +```text +File created successfully at: /private/tmp/claude-501/-Users-baptistelafourcade-Projects-freelance-aidd-aidd-badges/196f9048-108a-4c15-98d7-f6d91a2a7704/scratchpad/aidd-badges-overview.html (file state is current in your context — no need to Read it back) +``` +
+ +_**Agent (claude-sonnet-5 2026-07-22 10:14:13Z)**_ + +
+Tool use: **Artifact** +Comment AIDD délivre à ses membres un badge vérifiable cryptographiquement, indépendant de ses serveurs. + +```text +Published /private/tmp/claude-501/-Users-baptistelafourcade-Projects-freelance-aidd-aidd-badges/196f9048-108a-4c15-98d7-f6d91a2a7704/scratchpad/aidd-badges-overview.html at https://claude.ai/code/artifact/c942ecfc-fa01-4c76-a28d-0d6f7ebfe750 + +Warning: The document's own ("AIDD Badges — présentation du dépôt") names this artifact; the `title` parameter ("AIDD Badges — présentation") was not applied — the tag always wins. To rename, edit the <title> in the HTML. + +Live subscription: skipped — the live-subscribe feature flag is off in this session's cached config, so other sessions' republishes will not be reported here. + +To update: republish the same file path in this conversation (keeps this URL), or pass the URL as `url` from any other conversation — a conversation that didn't publish this artifact otherwise mints a new URL. Artifacts are private unless shared from the page's share menu; with Claude Code on the web, the user can browse theirs at claude.ai/code/artifacts. +``` +</details></tool-use> + +_**Agent (claude-sonnet-5 2026-07-22 10:14:18Z)**_ + +Page done: https://claude.ai/code/artifact/c942ecfc-fa01-4c76-a28d-0d6f7ebfe750 + +Covers: problem, 5-step flow (inscrire→générer→émettre→vérifier→retirer), two-layer architecture (Pages=data, VPS site=display) with mermaid diagrams, trust guarantees (key never in repo, real crypto check, honest revocation, GDPR via LFS, public repo rationale), repo structure table, roadmap (phases 0-3 done, phase 4 in progress, phase 5 next), and governance (merge = sole authority, no self-service). + +Flagged clearly: **0 real certified members published yet** — repo pre-launch, only untracked local demo data (alexmartin/crousseau/ybenali) exist, not committed. Don't present those 3 as real members to community. + +--- + +_**User (2026-07-22 10:26:48Z)**_ + +Décrit ici simplement etc les fichiers où ils sont etc + +--- + +_**Agent (claude-sonnet-5 2026-07-22 10:27:05Z)**_ + +Repo root: `/Users/baptistelafourcade/Projects/freelance/aidd/aidd/badges` + +``` +badges/ +├── README.md — intro projet, process en 5 étapes, quickstart tests/demo +├── MAINTAINERS.md — guide mainteneur: merger inscription, retrait RGPD, rotation clé +├── directory.json — annuaire généré (⚠ untracked, données démo locales) +│ +├── docs/ +│ ├── PRD.md — spec produit + 14 contraintes techniques (CT-1…CT-14) +│ ├── ARCHITECTURE.md — schémas des flux (2 couches, inscription, vérif, retrait) +│ ├── verification.md — doc vérif indépendante +│ ├── integration-communaute.md — intégration annuaire sur ai-driven-dev.fr +│ ├── integration-verify-page.md — intégration page vérif +│ ├── rgpd/ — info art.13 + justification non-AIPD +│ └── spikes/ — recherches techniques ponctuelles (SP1, SP3, SP4…) +│ +├── .github/ +│ ├── ISSUE_TEMPLATE/ +│ │ ├── certification-request.yml — formulaire inscription +│ │ └── removal-request.yml — formulaire retrait +│ ├── workflows/ +│ │ ├── certification-pr.yml — bot ouvre PR inscription +│ │ ├── removal-pr.yml — bot ouvre PR retrait +│ │ ├── emit-and-deploy.yml — au merge main: signe + publie sur Pages +│ │ ├── key-ceremony.yml — rotation clé (manuel, gated) +│ │ └── scripts-tests.yml — CI tests +│ └── scripts/ +│ ├── lib/ — logique: credential.mjs, photo.mjs, revocation.mjs, status-list.mjs, qr.mjs, directory.mjs, issue-form.mjs… (+ 1 test.mjs chacun) +│ ├── read-certification-request.mjs +│ ├── process-photo.mjs — WebP + strip EXIF +│ ├── sign-credential.mjs — signe VC-JWT RS256 +│ ├── sign-status-list.mjs — Bitstring Status List +│ ├── build-directory.mjs — génère directory.json +│ ├── build-site.sh +│ ├── remove-member.mjs +│ ├── key-ceremony.mjs +│ └── demo.mjs — `npm run demo` +│ +├── site/ — code navigateur, tourne sur VPS (verify.ai-driven-dev.fr) +│ ├── verify.html / verify.mjs / verify-page.mjs — page vérif + logique +│ ├── badge-page.html +│ ├── bitstring.mjs +│ └── *.test.mjs +│ +├── data/ +│ ├── members/<handle>/ — 1 dossier par membre: record.yml (fiche) + photo.webp (Git LFS) +│ │ └── ⚠ actuellement: alexmartin, crousseau, ybenali — untracked, démo locale, PAS de vrais membres +│ └── revoked.json — liste d'entiers (status_index révoqués), vide en prod +│ +├── keys/ +│ └── <kid>.json — clés publiques JWK publiées à vie (clé PRIVÉE jamais ici) +│ +├── public/ +│ └── issuer.json — profil émetteur (GitHub Pages) +│ +├── design/ — maquettes visuelles (Claude Design export) +│ ├── Badges AIDD.dc.html — maquette principale +│ ├── Seal.dc.html — le sceau, réutilisé partout +│ ├── Badge AIDD - standalone.html +│ ├── support.js / image-slot.js +│ ├── assets/ uploads/ screenshots/ +│ └── README.md — 3 écarts à corriger vs PRD (domaine, promesse vérif, "Habilité") +│ +├── .demo-site/ et .localserve/ — env de test local (serveur, clés démo, photos démo) +└── .specstory/ — historique sessions IA (outil externe, pas du projet) +``` + +Point clé: `data/members/`, `directory.json`, `.localserve/`, `.specstory/` — tout **untracked** (git status). Rien de ça publié en prod. diff --git a/.specstory/statistics.json b/.specstory/statistics.json new file mode 100644 index 0000000..d76da0e --- /dev/null +++ b/.specstory/statistics.json @@ -0,0 +1,13 @@ +{ + "sessions": { + "196f9048-108a-4c15-98d7-f6d91a2a7704": { + "user_message_count": 3, + "agent_message_count": 25, + "start_timestamp": "2026-07-22T10:11:42.997Z", + "end_timestamp": "2026-07-22T10:27:05.716Z", + "markdown_size_bytes": 83479, + "provider": "claude-code", + "last_updated": "2026-07-22T10:27:05Z" + } + } +} diff --git a/data/members/alexmartin/photo.webp b/data/members/alexmartin/photo.webp new file mode 100644 index 0000000..c7e8467 Binary files /dev/null and b/data/members/alexmartin/photo.webp differ diff --git a/data/members/alexmartin/record.yml b/data/members/alexmartin/record.yml new file mode 100644 index 0000000..fab8528 --- /dev/null +++ b/data/members/alexmartin/record.yml @@ -0,0 +1,7 @@ +github: "alexmartin" +role: "certifie" +name: "Alexandre Martin" +linkedin: "https://linkedin.com/in/alexmartin" +photo: "data/members/alexmartin/photo.webp" +status_index: 0 +description: "Lead dev, orchestration d'agents en prod." diff --git a/data/members/crousseau/photo.webp b/data/members/crousseau/photo.webp new file mode 100644 index 0000000..fa9c09c Binary files /dev/null and b/data/members/crousseau/photo.webp differ diff --git a/data/members/crousseau/record.yml b/data/members/crousseau/record.yml new file mode 100644 index 0000000..36e880b --- /dev/null +++ b/data/members/crousseau/record.yml @@ -0,0 +1,7 @@ +github: "crousseau" +role: "certifie" +name: "Camille Rousseau" +linkedin: "https://linkedin.com/in/crousseau" +photo: "data/members/crousseau/photo.webp" +status_index: 1 +description: "Freelance, TDD assisté par IA." diff --git a/data/members/ybenali/photo.webp b/data/members/ybenali/photo.webp new file mode 100644 index 0000000..e57d5e9 Binary files /dev/null and b/data/members/ybenali/photo.webp differ diff --git a/data/members/ybenali/record.yml b/data/members/ybenali/record.yml new file mode 100644 index 0000000..db90b8b --- /dev/null +++ b/data/members/ybenali/record.yml @@ -0,0 +1,7 @@ +github: "ybenali" +role: "certifie" +name: "Yassine Benali" +linkedin: "https://linkedin.com/in/ybenali" +photo: "data/members/ybenali/photo.webp" +status_index: 2 +description: "CTO, industrialisation du code généré." diff --git a/directory.json b/directory.json new file mode 100644 index 0000000..ee16264 --- /dev/null +++ b/directory.json @@ -0,0 +1,33 @@ +{ + "generatedAt": "2026-07-22T10:00:00Z", + "count": 3, + "members": [ + { + "handle": "alexmartin", + "name": "Alexandre Martin", + "role": "certifie", + "linkedin": "https://linkedin.com/in/alexmartin", + "photo": "http://localhost:8200/photos/alexmartin.webp", + "verify": "http://localhost:4321/u/alexmartin", + "description": "Lead dev, orchestration d'agents en prod." + }, + { + "handle": "crousseau", + "name": "Camille Rousseau", + "role": "certifie", + "linkedin": "https://linkedin.com/in/crousseau", + "photo": "http://localhost:8200/photos/crousseau.webp", + "verify": "http://localhost:4321/u/crousseau", + "description": "Freelance, TDD assisté par IA." + }, + { + "handle": "ybenali", + "name": "Yassine Benali", + "role": "certifie", + "linkedin": "https://linkedin.com/in/ybenali", + "photo": "http://localhost:8200/photos/ybenali.webp", + "verify": "http://localhost:4321/u/ybenali", + "description": "CTO, industrialisation du code généré." + } + ] +} diff --git a/site/verify.mjs b/site/verify.mjs index c01673c..b426fdd 100644 --- a/site/verify.mjs +++ b/site/verify.mjs @@ -100,7 +100,7 @@ export async function verifyBadge(jwt, { fetchKey, fetchStatus, now = new Date() catch { return { state: STATE.INVALID, reason: 'preuve illisible' }; } const kidUrl = decoded.header.kid; - const getKey = fetchKey || (async (url) => (await fetch(url)).json()); + const getKey = fetchKey || (async (url) => (await fetch(url, { cache: 'no-store' })).json()); let jwk; try { jwk = await getKey(kidUrl); } @@ -110,7 +110,7 @@ export async function verifyBadge(jwt, { fetchKey, fetchStatus, now = new Date() if (!signatureOk) return { state: STATE.INVALID, reason: 'signature invalide' }; const details = { ...readDetails(decoded.payload), kidUrl }; - const getJwt = fetchStatus || (async (url) => (await fetch(url)).text()); + const getJwt = fetchStatus || (async (url) => (await fetch(url, { cache: 'no-store' })).text()); const revocation = await checkRevocation(decoded.payload.credentialStatus, { getJwt, getKey }); if (revocation.revoked) return { state: STATE.REVOKED, details, header: decoded.header, payload: decoded.payload };