Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/scripts/lib/credential.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions .github/scripts/lib/credential.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down
21 changes: 11 additions & 10 deletions .github/scripts/lib/directory.mjs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 };
}
4 changes: 2 additions & 2 deletions .github/scripts/lib/directory.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand All @@ -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');
});

Expand Down
15 changes: 7 additions & 8 deletions .github/scripts/lib/qr.mjs
Original file line number Diff line number Diff line change
@@ -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' });
}
2 changes: 1 addition & 1 deletion .github/scripts/lib/signing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/lib/status-list.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 4 additions & 4 deletions .github/scripts/sign-credential.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/emit-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions docs/integration-communaute.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -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
Expand All @@ -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/<handle>.webp`.
`ai-driven-dev.github.io/badges/photos/<handle>.webp` (servies par Pages).

## Snippet à déposer (Astro ou n'importe quelle page)

Expand All @@ -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';
---
<section>
<h1>Membres certifiés</h1>
Expand Down
2 changes: 1 addition & 1 deletion public/issuer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading