From a87867edf256c6ac104d24df6584c36c3d090073 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Wed, 22 Jul 2026 07:51:38 +0200 Subject: [PATCH] =?UTF-8?q?feat(hosting):=20s=C3=A9parer=20donn=C3=A9es=20?= =?UTF-8?q?(Pages)=20et=20belle=20page=20(site=20VPS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux bases : DATA_BASE = ai-driven-dev.github.io/badges (Pages, ancre permanente : clés, statut, issuer, preuves, photos) ; SITE_BASE = verify.ai-driven-dev.fr (le site VPS, la belle page de vérif). Le credential ancre ses URLs sur les données (Pages) ; le QR et le lien verify de l'annuaire pointent vers le site. Pages ne revendique plus verify.* (CNAME retiré). issuer.json sur Pages. Doc annuaire à jour. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/lib/credential.mjs | 16 +++++++++++++++- .github/scripts/lib/credential.test.mjs | 4 ++-- .github/scripts/lib/directory.mjs | 21 +++++++++++---------- .github/scripts/lib/directory.test.mjs | 4 ++-- .github/scripts/lib/qr.mjs | 15 +++++++-------- .github/scripts/lib/signing.test.mjs | 2 +- .github/scripts/lib/status-list.test.mjs | 2 +- .github/scripts/sign-credential.mjs | 8 ++++---- .github/workflows/emit-and-deploy.yml | 4 ++-- docs/integration-communaute.md | 8 ++++---- public/issuer.json | 2 +- 11 files changed, 50 insertions(+), 36 deletions(-) diff --git a/.github/scripts/lib/credential.mjs b/.github/scripts/lib/credential.mjs index 1979adf..230f77d 100644 --- a/.github/scripts/lib/credential.mjs +++ b/.github/scripts/lib/credential.mjs @@ -1,7 +1,21 @@ // Construction du credential Open Badges 3.0 (forme prouvée conforme au validateur // 1EdTech, spike SP4). Fonctions pures : la signature est faite ailleurs (jose). -export const DEFAULT_BASE = 'https://verify.ai-driven-dev.fr'; +// Deux hébergements distincts : +// - DATA_BASE : GitHub Pages, l'ancre permanente. Clés, statut, issuer, preuves. +// Doit vivre à vie (sinon badges invérifiables) -> uptime GitHub. C'est aussi le fallback. +// - SITE_BASE : le site (VPS, Astro), la belle page de vérif que le badge fait ouvrir. +// Lit les données depuis DATA_BASE et les rend au design. +export const DATA_BASE = process.env.DATA_BASE || 'https://ai-driven-dev.github.io/badges'; +export const SITE_BASE = process.env.SITE_BASE || 'https://verify.ai-driven-dev.fr'; + +// Rétrocompat : DEFAULT_BASE = la base des données signées. +export const DEFAULT_BASE = DATA_BASE; + +/** URL de la belle page de vérif d'un membre (sur le site). */ +export function siteVerifyUrl(handle, base = SITE_BASE) { + return `${base}/u/${handle}`; +} /** Parse un enregistrement YAML plat `clé: "valeur"` (valeurs échappées en JSON). */ export function parseMemberYaml(text) { diff --git a/.github/scripts/lib/credential.test.mjs b/.github/scripts/lib/credential.test.mjs index 09a95f8..0dd2a05 100644 --- a/.github/scripts/lib/credential.test.mjs +++ b/.github/scripts/lib/credential.test.mjs @@ -87,7 +87,7 @@ describe('resolveEmissionDate', () => { describe('keyUrl', () => { it('construit une URL déréférençable par kid sous le domaine permanent', () => { - assert.equal(keyUrl('ABC'), 'https://verify.ai-driven-dev.fr/keys/ABC.json'); + assert.equal(keyUrl('ABC'), 'https://ai-driven-dev.github.io/badges/keys/ABC.json'); }); }); @@ -133,7 +133,7 @@ describe('buildCredential', () => { assert.equal(credential.credentialStatus.type, 'BitstringStatusListEntry'); assert.equal(credential.credentialStatus.statusPurpose, 'revocation'); assert.equal(credential.credentialStatus.statusListIndex, '42'); - assert.equal(credential.credentialStatus.statusListCredential, 'https://verify.ai-driven-dev.fr/status/1'); + assert.equal(credential.credentialStatus.statusListCredential, 'https://ai-driven-dev.github.io/badges/status/1'); }); it('rejette une date de certification invalide', () => { diff --git a/.github/scripts/lib/directory.mjs b/.github/scripts/lib/directory.mjs index ed422de..1be423c 100644 --- a/.github/scripts/lib/directory.mjs +++ b/.github/scripts/lib/directory.mjs @@ -1,21 +1,22 @@ // Construit le flux public de l'annuaire (#33/#49) : directory.json consommé par // le site ai-driven-dev.fr. Pur : reçoit les enregistrements déjà parsés. -import { DEFAULT_BASE } from './credential.mjs'; +// photo = données (Pages) ; verify = belle page (site). +import { DATA_BASE, SITE_BASE, siteVerifyUrl } from './credential.mjs'; -/** URL publique de la photo servie (copiée depuis LFS au build). */ -export function photoUrlFor(handle, base = DEFAULT_BASE) { - return `${base}/photos/${handle}.webp`; +/** URL publique de la photo servie par Pages (copiée depuis LFS au build). */ +export function photoUrlFor(handle, dataBase = DATA_BASE) { + return `${dataBase}/photos/${handle}.webp`; } /** Une entrée d'annuaire pour un membre. */ -export function toDirectoryEntry(member, base = DEFAULT_BASE) { +export function toDirectoryEntry(member, { dataBase = DATA_BASE, siteBase = SITE_BASE } = {}) { const entry = { handle: member.github, name: member.name, role: member.role || 'certifie', linkedin: member.linkedin, - photo: photoUrlFor(member.github, base), - verify: `${base}/u/${member.github}`, + photo: photoUrlFor(member.github, dataBase), + verify: siteVerifyUrl(member.github, siteBase), }; if (member.website) entry.website = member.website; if (member.description) entry.description = member.description; @@ -30,12 +31,12 @@ export function isActive(member) { /** * Assemble le flux annuaire trié par nom, révoqués exclus. * @param {object[]} members enregistrements parsés - * @param {{ base?: string, generatedAt?: string }} options + * @param {{ dataBase?, siteBase?, generatedAt? }} options */ -export function buildDirectory(members, { base = DEFAULT_BASE, generatedAt } = {}) { +export function buildDirectory(members, { dataBase = DATA_BASE, siteBase = SITE_BASE, generatedAt } = {}) { const entries = members .filter(isActive) - .map((m) => toDirectoryEntry(m, base)) + .map((m) => toDirectoryEntry(m, { dataBase, siteBase })) .sort((a, b) => a.name.localeCompare(b.name, 'fr')); return { generatedAt, count: entries.length, members: entries }; } diff --git a/.github/scripts/lib/directory.test.mjs b/.github/scripts/lib/directory.test.mjs index 294a78e..dcf6b98 100644 --- a/.github/scripts/lib/directory.test.mjs +++ b/.github/scripts/lib/directory.test.mjs @@ -9,7 +9,7 @@ const member = (over = {}) => ({ describe('photoUrlFor', () => { it('pointe vers la photo servie sur le domaine de vérif', () => { - assert.equal(photoUrlFor('jd'), 'https://verify.ai-driven-dev.fr/photos/jd.webp'); + assert.equal(photoUrlFor("jd"), "https://ai-driven-dev.github.io/badges/photos/jd.webp"); }); }); @@ -19,7 +19,7 @@ describe('toDirectoryEntry', () => { assert.equal(e.handle, 'jd'); assert.equal(e.name, 'Jean Dupont'); assert.equal(e.linkedin, 'https://linkedin.com/in/jd'); - assert.equal(e.photo, 'https://verify.ai-driven-dev.fr/photos/jd.webp'); + assert.equal(e.photo, "https://ai-driven-dev.github.io/badges/photos/jd.webp"); assert.equal(e.verify, 'https://verify.ai-driven-dev.fr/u/jd'); }); diff --git a/.github/scripts/lib/qr.mjs b/.github/scripts/lib/qr.mjs index aa5db48..6ab568d 100644 --- a/.github/scripts/lib/qr.mjs +++ b/.github/scripts/lib/qr.mjs @@ -1,15 +1,14 @@ -// QR code de la page de vérification (#60). Généré au build (SVG, sans réseau), -// il encode l'URL publique de vérification du membre. Affiché et téléchargeable -// depuis la page ; le scan mène à la vérification. +// QR code de la page de vérification (#60). Généré au build (SVG, sans réseau). +// Il encode la belle page de vérif (le site, SITE_BASE) : scanner -> ouvre la fiche. import QRCode from 'qrcode'; -import { DEFAULT_BASE } from './credential.mjs'; +import { SITE_BASE, siteVerifyUrl } from './credential.mjs'; -/** URL de vérification publique d'un membre. */ -export function verifyUrlFor(handle, base = DEFAULT_BASE) { - return `${base}/u/${handle}`; +/** URL de vérification publique d'un membre (la belle page, sur le site). */ +export function verifyUrlFor(handle, base = SITE_BASE) { + return siteVerifyUrl(handle, base); } /** SVG du QR encodant l'URL de vérification du membre. */ -export function generateQrSvg(handle, base = DEFAULT_BASE) { +export function generateQrSvg(handle, base = SITE_BASE) { return QRCode.toString(verifyUrlFor(handle, base), { type: 'svg', margin: 1, errorCorrectionLevel: 'M' }); } diff --git a/.github/scripts/lib/signing.test.mjs b/.github/scripts/lib/signing.test.mjs index 6c165a5..57cff22 100644 --- a/.github/scripts/lib/signing.test.mjs +++ b/.github/scripts/lib/signing.test.mjs @@ -41,7 +41,7 @@ describe('signature du credential', () => { const header = decodeProtectedHeader(jwt); assert.equal(header.alg, 'RS256'); - assert.equal(header.kid, `https://verify.ai-driven-dev.fr/keys/${kid}.json`); + assert.equal(header.kid, `https://ai-driven-dev.github.io/badges/keys/${kid}.json`); }); it('rejette un JWT altéré', async () => { diff --git a/.github/scripts/lib/status-list.test.mjs b/.github/scripts/lib/status-list.test.mjs index 799180f..845be84 100644 --- a/.github/scripts/lib/status-list.test.mjs +++ b/.github/scripts/lib/status-list.test.mjs @@ -30,7 +30,7 @@ describe('buildStatusListCredential', () => { it('porte l\'identifiant stable de la liste de statuts', async () => { const credential = await buildStatusListCredential([], { issuedOn }); - assert.equal(credential.id, 'https://verify.ai-driven-dev.fr/status/1'); + assert.equal(credential.id, 'https://ai-driven-dev.github.io/badges/status/1'); }); it('rejette une date d\'émission invalide', async () => { diff --git a/.github/scripts/sign-credential.mjs b/.github/scripts/sign-credential.mjs index 20109d9..fe91fde 100644 --- a/.github/scripts/sign-credential.mjs +++ b/.github/scripts/sign-credential.mjs @@ -36,12 +36,12 @@ async function main() { // Le thumbprint ignore `d` → identique au kid de la clé publique publiée. const kid = await calculateJwkThumbprint(await exportJWK(privateKey)); - // BASE permet de tester en local (ex. http://localhost:8000) ; défaut = domaine réel. - const base = process.env.BASE || DEFAULT_BASE; - const credential = buildCredential({ handle: member.github, name: member.name, statusIndex }, { certifiedOn: issuedAt, base }); + // Le credential (clés, statut, issuer) est ancré sur les DONNÉES (Pages, DEFAULT_BASE). + // Le QR, lui, pointe vers la belle page du site (SITE_BASE, défaut dans qr.mjs). + const credential = buildCredential({ handle: member.github, name: member.name, statusIndex }, { certifiedOn: issuedAt }); const jwt = await new SignJWT(credential) - .setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: keyUrl(kid, base) }) + .setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: keyUrl(kid) }) .setIssuer(credential.issuer.id) .setSubject(credential.credentialSubject.id) .setJti(credential.id) diff --git a/.github/workflows/emit-and-deploy.yml b/.github/workflows/emit-and-deploy.yml index 81cf859..25bc7c6 100644 --- a/.github/workflows/emit-and-deploy.yml +++ b/.github/workflows/emit-and-deploy.yml @@ -53,8 +53,8 @@ jobs: SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} run: bash .github/scripts/build-site.sh - - name: Fichier CNAME (spécifique à Pages) - run: echo "verify.ai-driven-dev.fr" > _site/CNAME + # Pas de CNAME : Pages sert les DONNÉES sur ai-driven-dev.github.io/badges + # (l'ancre permanente). verify.ai-driven-dev.fr est le site (VPS), pas Pages. - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: diff --git a/docs/integration-communaute.md b/docs/integration-communaute.md index bfaa2c9..c3a52ed 100644 --- a/docs/integration-communaute.md +++ b/docs/integration-communaute.md @@ -1,7 +1,7 @@ # Afficher l'annuaire sur `ai-driven-dev.fr` (`/communaute`) Le repo `badges` publie la liste des certifiés en **`directory.json`** sur -`https://verify.ai-driven-dev.fr/directory.json` (CORS ouvert). Le site principal +`https://ai-driven-dev.github.io/badges/directory.json` (CORS ouvert). Le site principal n'a qu'à le **lire côté navigateur** et rendre la liste. Pas de reconstruction : la liste est **toujours à jour** (le fetch a lieu à l'ouverture de la page). @@ -17,7 +17,7 @@ la liste est **toujours à jour** (le fetch a lieu à l'ouverture de la page). "name": "Ada Lovelace", "role": "certifie", "linkedin": "https://linkedin.com/in/ada", - "photo": "https://verify.ai-driven-dev.fr/photos/ada.webp", + "photo": "https://ai-driven-dev.github.io/badges/photos/ada.webp", "verify": "https://verify.ai-driven-dev.fr/u/ada", "website": "https://ada.dev", // optionnel "description": "Développeuse IA" // optionnel @@ -27,7 +27,7 @@ la liste est **toujours à jour** (le fetch a lieu à l'ouverture de la page). ``` Les membres retirés (RGPD) sont **absents** du flux. Photos servies sur -`verify.ai-driven-dev.fr/photos/.webp`. +`ai-driven-dev.github.io/badges/photos/.webp` (servies par Pages). ## Snippet à déposer (Astro ou n'importe quelle page) @@ -36,7 +36,7 @@ Rendu **côté navigateur**, toujours frais, sans dépendance : ```astro --- // src/pages/communaute.astro (extrait) — le rendu se fait au runtime, pas au build. -const FEED = 'https://verify.ai-driven-dev.fr/directory.json'; +const FEED = 'https://ai-driven-dev.github.io/badges/directory.json'; ---

Membres certifiés

diff --git a/public/issuer.json b/public/issuer.json index 8088898..5db6744 100644 --- a/public/issuer.json +++ b/public/issuer.json @@ -3,7 +3,7 @@ "https://www.w3.org/ns/credentials/v2", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" ], - "id": "https://verify.ai-driven-dev.fr/issuer.json", + "id": "https://ai-driven-dev.github.io/badges/issuer.json", "type": ["Profile"], "name": "AI-Driven Development", "url": "https://ai-driven-dev.fr"