Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions control-plane/migrations/015_bootstrap_removal/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- control-plane/migrations/015_bootstrap_removal/migration.sql

-- Plan 10 PR A: explicit removal state for bootstrap issuers so a seed rerun
-- (insert-only manifest) never reinstates an issuer de-emphasized/removed by
-- staff, and is_bootstrap derivation can exclude removed rows.
ALTER TABLE bootstrap_issuers
ADD COLUMN removed_from_registry_at TIMESTAMPTZ;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
1 change: 1 addition & 0 deletions control-plane/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc",
"start": "node dist/index.js",
"migrate": "tsx src/db/migrate.ts",
"seed:bootstrap": "BOOTSTRAP_SEED=1 tsx src/scripts/seed-bootstrap.ts",
"test": "npm run test:unit",
"test:unit": "node --test --import tsx $(find src -name '*.test.ts' ! -path '*/__tests__/integration/*')",
"test:integration": "node --test --test-concurrency=1 --import tsx src/__tests__/integration/*.test.ts"
Expand Down
199 changes: 199 additions & 0 deletions control-plane/src/__tests__/integration/bootstrap-seed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// control-plane/src/__tests__/integration/bootstrap-seed.test.ts
process.env.DATABASE_URL ||=
'postgresql://verilink:verilink@127.0.0.1:15432/verilink_test';
process.env.API_KEY_HMAC_SECRET ||= 'test-hmac-secret-for-integration';

import { describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import type pg from 'pg';
import { setupTestDb, teardownTestDb, resetTestData } from '../../testutil/testDb.js';
import {
seedTenant,
seedIssuer,
seedApiKey,
authHeaders,
} from '../../testutil/seedData.js';
import { startControlPlane, type ControlPlaneHarness } from '../../testutil/appHarness.js';
import { SEED_ISSUERS } from '../../domains/bootstrap/seedManifest.js';

/**
* Plan 10 PR A: idempotent bootstrap seed, is_bootstrap derivation, PATCH
* removal survival across reruns, and root-weight write-through to the graph
* loader. All seeded rows use the app pool (dynamic imports after env setup).
*/
describe('Bootstrap Seed Integration', () => {
let pool: pg.Pool;
let harness: ControlPlaneHarness;

// Dynamic imports so db/client.ts evaluates after DATABASE_URL is set.
let seedBootstrapRegistry: () => Promise<{ issuers: number; roots: number }>;
let loadAttestationGraph: (evaluationTime: Date) => Promise<{
roots: Array<{ id: string; weight: number }>;
}>;

before(async () => {
pool = await setupTestDb();
harness = await startControlPlane();
({ seedBootstrapRegistry } = await import('../../domains/bootstrap/bootstrapSeeder.js'));
({ loadAttestationGraph } = await import('../../domains/graph/attestationGraphLoader.js'));
});

after(async () => {
await harness.stop();
await teardownTestDb(pool);
});

beforeEach(async () => {
await resetTestData(pool);
});

async function seededState() {
const { rows } = await pool.query(
`SELECT
(SELECT count(*)::int FROM issuers WHERE is_bootstrap) AS bs_issuers,
(SELECT count(*)::int FROM bootstrap_issuers) AS bs_rows,
(SELECT count(*)::int FROM principals) AS principals,
(SELECT count(*)::int FROM principal_keys) AS keys`
);
return rows[0];
}

it('seed is idempotent with exact manifest identities', async () => {
const first = await seedBootstrapRegistry();
assert.equal(first.issuers, SEED_ISSUERS.length);
assert.equal(first.roots, SEED_ISSUERS.length);

const { rows: seeded } = await pool.query(
`SELECT p.id, p.entity_kind, p.name, i.trust_weight::float AS trust_weight,
i.is_bootstrap, k.key_id, b.current_weight::float AS current_weight
FROM principals p
JOIN issuers i ON i.principal_id = p.id
JOIN principal_keys k ON k.principal_id = p.id
JOIN bootstrap_issuers b ON b.principal_id = p.id
ORDER BY p.id`
);
assert.equal(seeded.length, SEED_ISSUERS.length);
for (const entry of SEED_ISSUERS) {
const row = seeded.find((r) => r.id === entry.id);
assert.ok(row, `seeded principal ${entry.id} present`);
assert.equal(row.entity_kind, entry.entityKind);
assert.equal(row.name, entry.name);
assert.equal(row.trust_weight, 1.0, 'trust_weight untouched by seed');
assert.equal(row.is_bootstrap, true);
assert.equal(row.key_id, entry.keyId);
assert.equal(row.current_weight, 1.0);
}

const afterFirst = await seededState();
assert.equal(afterFirst.bs_issuers, SEED_ISSUERS.length);
assert.equal(afterFirst.keys, SEED_ISSUERS.length);

const second = await seedBootstrapRegistry();
assert.equal(second.issuers, SEED_ISSUERS.length);
assert.equal(second.roots, SEED_ISSUERS.length);
const afterSecond = await seededState();
assert.deepEqual(afterSecond, afterFirst, 'rerun is a no-op');
});

it('PATCH removal clears is_bootstrap and seed rerun does not reinstate', async () => {
await seedBootstrapRegistry();
const target = SEED_ISSUERS[0];

const tenantA = await seedTenant(pool, `bs-rm-${Date.now()}`);
const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']);

const res = await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) },
body: JSON.stringify({ principal_id: target.id, remove_from_registry: true }),
});
assert.equal(res.status, 200, JSON.stringify(await res.json()));

const { rows: afterRemove } = await pool.query(
`SELECT i.is_bootstrap, b.removed_from_registry_at IS NOT NULL AS removed
FROM issuers i JOIN bootstrap_issuers b ON b.principal_id = i.principal_id
WHERE i.principal_id = $1`,
[target.id]
);
assert.equal(afterRemove[0].is_bootstrap, false, 'removal clears is_bootstrap');
assert.equal(afterRemove[0].removed, true);

await seedBootstrapRegistry();

const { rows: afterRerun } = await pool.query(
`SELECT i.is_bootstrap, b.removed_from_registry_at IS NOT NULL AS removed
FROM issuers i JOIN bootstrap_issuers b ON b.principal_id = i.principal_id
WHERE i.principal_id = $1`,
[target.id]
);
assert.equal(afterRerun[0].is_bootstrap, false, 'seed rerun does not reinstate removed issuer');
assert.equal(afterRerun[0].removed, true, 'removal state preserved across rerun');
});

it('PATCH current_weight write-through: GraphRoot.weight changes, trust_weight stays 1.0', async () => {
await seedBootstrapRegistry();
const target = SEED_ISSUERS[0];

const tenantA = await seedTenant(pool, `bs-wt-${Date.now()}`);
const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']);

const res = await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) },
body: JSON.stringify({ principal_id: target.id, current_weight: 0.5 }),
});
assert.equal(res.status, 200, JSON.stringify(await res.json()));

const { rows: issuerRows } = await pool.query(
`SELECT trust_weight::float AS trust_weight FROM issuers WHERE principal_id = $1`,
[target.id]
);
assert.equal(issuerRows[0].trust_weight, 1.0, 'trust_weight untouched by de-emphasis');

const graph = await loadAttestationGraph(new Date());
const root = graph.roots.find((r) => r.id === target.id);
assert.ok(root, 'seeded issuer is a graph root');
assert.equal(root.weight, 0.5, 'GraphRoot.weight reflects PATCHed current_weight');

const { rows: bsRows } = await pool.query(
`SELECT is_bootstrap FROM issuers WHERE principal_id = $1`,
[target.id]
);
assert.equal(bsRows[0].is_bootstrap, true, 'positive weight keeps is_bootstrap true');
});

it('zero weight or removal drops the issuer from the graph roots', async () => {
await seedBootstrapRegistry();
const target = SEED_ISSUERS[0];

const tenantA = await seedTenant(pool, `bs-z-${Date.now()}`);
const staffKey = await seedApiKey(pool, tenantA.id, ['admin:read']);

await fetch(`${harness.url}/v1/admin/bootstrap-issuers`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...authHeaders(staffKey) },
body: JSON.stringify({ principal_id: target.id, current_weight: 0 }),
});

const graph = await loadAttestationGraph(new Date());
assert.ok(
!graph.roots.some((r) => r.id === target.id),
'zero-weight root excluded from graph'
);
});

it('seed is gated by BOOTSTRAP_SEED and refuses to run without it', async () => {
const { execFileSync } = await import('node:child_process');
assert.throws(() => {
execFileSync('npx', ['tsx', 'src/scripts/seed-bootstrap.ts'], {
cwd: process.cwd(),
env: {
...process.env,
DATABASE_URL: 'postgresql://verilink:verilink@127.0.0.1:15432/verilink_test',
BOOTSTRAP_SEED: '0',
},
encoding: 'utf-8',
});
}, /Refusing to seed without BOOTSTRAP_SEED=1/);
});
});
94 changes: 55 additions & 39 deletions control-plane/src/domains/bootstrap/bootstrapRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// control-plane/src/domains/bootstrap/bootstrapRepository.ts
import { pool } from '../../db/client.js';
import { pool, withTransaction } from '../../db/transaction.js';
import { deriveIsBootstrapForIssuer } from './bootstrapSeeder.js';

export interface BootstrapIssuerRow {
principal_id: string;
Expand All @@ -9,6 +10,8 @@ export interface BootstrapIssuerRow {
de_emphasized_at: Date | null;
approved_by: string | null;
seeded_at: Date;
/** Explicit removal state (migration 015). */
removed_from_registry_at: Date | null;
/** From issuers — read-only context for the queue. */
trust_weight: number;
verified_at: Date | null;
Expand All @@ -18,6 +21,7 @@ export async function listBootstrapIssuers(): Promise<BootstrapIssuerRow[]> {
const { rows } = await pool.query(
`SELECT b.principal_id, b.name, b.current_weight::float AS current_weight,
b.de_emphasis_reason, b.de_emphasized_at, b.approved_by, b.seeded_at,
b.removed_from_registry_at,
i.trust_weight::float AS trust_weight, i.verified_at
FROM bootstrap_issuers b
JOIN issuers i ON i.principal_id = b.principal_id
Expand All @@ -30,58 +34,70 @@ export interface BootstrapUpdate {
current_weight?: number;
de_emphasis_reason?: string | null;
approved_by?: string | null;
/** Staff removal from the registry (migration 015); never reinstated by seed reruns. */
remove_from_registry?: boolean;
}

/**
* Staff edit of a bootstrap issuer's Root.weight + de-emphasis reason.
* `issuers.trust_weight` is untouched (the orthogonal quality knob; de-emphasis
* is via Root.weight only — design §4.5). Sets de_emphasized_at when a reason
* is provided. Returns 404 via the caller when the row does not exist.
* is provided. Weight steps and removal re-derive is_bootstrap in the same
* transaction so the registry and the derived flag can never diverge.
* Returns 404 via the caller when the row does not exist.
*/
export async function updateBootstrapIssuer(
principalId: string,
update: BootstrapUpdate
): Promise<BootstrapIssuerRow | null> {
const sets: string[] = [];
const params: unknown[] = [];
let idx = 1;
return withTransaction(async (client) => {
const sets: string[] = [];
const params: unknown[] = [];
let idx = 1;

if (update.current_weight !== undefined) {
sets.push(`current_weight = $${idx++}`);
params.push(update.current_weight);
}
if (update.de_emphasis_reason !== undefined) {
sets.push(`de_emphasis_reason = $${idx++}`);
params.push(update.de_emphasis_reason);
sets.push(`de_emphasized_at = $${idx++}`);
params.push(update.de_emphasis_reason ? new Date() : null);
}
if (update.approved_by !== undefined) {
sets.push(`approved_by = $${idx++}`);
params.push(update.approved_by);
}
if (sets.length === 0) return null;
if (update.current_weight !== undefined) {
sets.push(`current_weight = $${idx++}`);
params.push(update.current_weight);
}
if (update.de_emphasis_reason !== undefined) {
sets.push(`de_emphasis_reason = $${idx++}`);
params.push(update.de_emphasis_reason);
sets.push(`de_emphasized_at = $${idx++}`);
params.push(update.de_emphasis_reason ? new Date() : null);
}
if (update.approved_by !== undefined) {
sets.push(`approved_by = $${idx++}`);
params.push(update.approved_by);
}
if (update.remove_from_registry !== undefined) {
sets.push(`removed_from_registry_at = $${idx++}`);
params.push(update.remove_from_registry ? new Date() : null);
}
if (sets.length === 0) return null;

params.push(principalId);
const { rows } = await pool.query(
`UPDATE bootstrap_issuers SET ${sets.join(', ')}
WHERE principal_id = $${idx}
RETURNING principal_id, name, current_weight::float AS current_weight,
de_emphasis_reason, de_emphasized_at, approved_by, seeded_at`,
params
);
if (rows.length === 0) return null;
params.push(principalId);
const { rows } = await client.query(
`UPDATE bootstrap_issuers SET ${sets.join(', ')}
WHERE principal_id = $${idx}
RETURNING principal_id, name, current_weight::float AS current_weight,
de_emphasis_reason, de_emphasized_at, approved_by, seeded_at,
removed_from_registry_at`,
params
);
if (rows.length === 0) return null;

const row = rows[0];
const issuer = await pool.query(
`SELECT trust_weight::float AS trust_weight, verified_at FROM issuers WHERE principal_id = $1`,
[principalId]
);
return {
...row,
trust_weight: issuer.rows[0]?.trust_weight ?? 1,
verified_at: issuer.rows[0]?.verified_at ?? null,
};
await deriveIsBootstrapForIssuer(client, principalId);

const issuer = await client.query(
`SELECT trust_weight::float AS trust_weight, verified_at FROM issuers WHERE principal_id = $1`,
[principalId]
);
return {
...rows[0],
trust_weight: issuer.rows[0]?.trust_weight ?? 1,
verified_at: issuer.rows[0]?.verified_at ?? null,
};
});
}

export interface UnverifiedIssuerRow {
Expand Down
Loading
Loading