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
3 changes: 3 additions & 0 deletions .github/scripts/build-site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
set -euo pipefail
shopt -s nullglob

echo "== Validation des index de statut (fail-fast) =="
node .github/scripts/validate-members.mjs

echo "== Émission des credentials (déterministe) =="
count=0
for d in data/members/*/; do
Expand Down
32 changes: 32 additions & 0 deletions .github/scripts/lib/member-indices.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Intégrité des index de statut (Bitstring Status List). Fonction pure, testable.
// Invariants tenus par l'intake mais vérifiés à l'émission (défense en profondeur) :
// - chaque status_index est un entier >= 0 ;
// - deux membres actifs ne partagent JAMAIS un index (sinon ils partagent un bit de
// statut : révoquer l'un révoquerait l'autre) ;
// - aucun membre actif n'est sur un index présent dans le registre de révocation
// (un index révoqué est brûlé à vie -> le badge naîtrait « révoqué »).

/**
* @param {Array<{github?: string, status_index: any}>} members
* @param {Array<number>|Set<number>} revoked index révoqués (registre)
* @returns {{ ok: true } | { ok: false, error: string }}
*/
export function checkStatusIndices(members, revoked = []) {
const revokedSet = revoked instanceof Set ? revoked : new Set([...revoked].map(Number));
const seen = new Map();
for (const m of members) {
const who = m.github ? `@${m.github}` : '(membre sans handle)';
const idx = Number(m.status_index);
if (!Number.isInteger(idx) || idx < 0) {
return { ok: false, error: `status_index invalide pour ${who} : ${m.status_index}` };
}
if (seen.has(idx)) {
return { ok: false, error: `status_index ${idx} partagé par @${seen.get(idx)} et ${who} — deux membres au même bit de statut` };
}
seen.set(idx, m.github);
if (revokedSet.has(idx)) {
return { ok: false, error: `${who} est à l'index ${idx}, révoqué à vie — son badge naîtrait « révoqué »` };
}
}
return { ok: true };
}
37 changes: 37 additions & 0 deletions .github/scripts/lib/member-indices.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { checkStatusIndices } from './member-indices.mjs';

describe('checkStatusIndices', () => {
it('accepte des index distincts hors registre', () => {
const r = checkStatusIndices(
[{ github: 'a', status_index: 1 }, { github: 'b', status_index: 2 }],
[0],
);
assert.deepEqual(r, { ok: true });
});

it('rejette deux membres au même index', () => {
const r = checkStatusIndices(
[{ github: 'a', status_index: 3 }, { github: 'b', status_index: 3 }],
[],
);
assert.equal(r.ok, false);
assert.match(r.error, /partagé par @a et @b/);
});

it('rejette un membre actif sur un index révoqué (le bug blafourcade)', () => {
const r = checkStatusIndices([{ github: 'blafourcade', status_index: 0 }], [0]);
assert.equal(r.ok, false);
assert.match(r.error, /révoqué à vie/);
});

it('rejette un status_index non entier ou négatif', () => {
assert.equal(checkStatusIndices([{ github: 'a', status_index: 'x' }]).ok, false);
assert.equal(checkStatusIndices([{ github: 'a', status_index: -1 }]).ok, false);
});

it('accepte une liste vide', () => {
assert.deepEqual(checkStatusIndices([], [0, 1, 2]), { ok: true });
});
});
19 changes: 19 additions & 0 deletions .github/scripts/validate-members.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Couche I/O : garde fail-fast à l'émission. Lit tous les records + le registre de
// révocation, vérifie l'intégrité des index de statut, et échoue bruyamment (exit 1)
// avant toute signature/déploiement. Empêche de publier deux membres au même bit de
// statut, ou un membre neuf sur un index déjà révoqué. Logique pure dans lib/.
import { readFileSync, existsSync } from 'node:fs';
import { parseMemberYaml } from './lib/credential.mjs';
import { listMemberHandles, recordPathFor } from './lib/member-paths.mjs';
import { LEDGER_PATH, parseLedger } from './lib/revocation.mjs';
import { checkStatusIndices } from './lib/member-indices.mjs';

const members = listMemberHandles().map((h) => parseMemberYaml(readFileSync(recordPathFor(h), 'utf8')));
const revoked = existsSync(LEDGER_PATH) ? parseLedger(readFileSync(LEDGER_PATH, 'utf8')) : [];

const result = checkStatusIndices(members, revoked);
if (!result.ok) {
console.error(`::error::Intégrité des index de statut violée : ${result.error}`);
process.exit(1);
}
console.log(`Indices de statut OK : ${members.length} membre(s), distincts et hors registre de révocation.`);
Loading