feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,219 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
inspectLegacySqlitePath,
prepareLocalSqliteActivation,
stageLocalSqliteAdoption,
} = require('@qinglong/local-admin');
const {
bootstrapLocalAdoptedProfileStorage,
} = require('@qinglong/local-admin/adopted-profile');
function fixture(t) {
const directory = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-adopted-profile-'),
);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const value = {
directory,
sourcePath: path.join(directory, 'database.sqlite'),
targetPath: path.join(directory, 'qinglong3.sqlite'),
recoveryPath: path.join(directory, 'database.pre-ql3.sqlite'),
manifestPath: path.join(directory, 'qinglong3-adoption.json'),
activationPath: path.join(directory, 'qinglong3-activation.json'),
};
const source = new DatabaseSync(value.sourcePath);
source.exec(`
CREATE TABLE "Auths" (id INTEGER PRIMARY KEY, type TEXT, info TEXT);
CREATE TABLE "Crontabs" (
id INTEGER PRIMARY KEY, command TEXT NOT NULL, schedule TEXT
);
CREATE TABLE "Envs" (
id INTEGER PRIMARY KEY, name TEXT, value TEXT
);
INSERT INTO "Crontabs" (id, command, schedule)
VALUES (1, 'echo legacy', '0 0 * * *');
`);
source.close();
return value;
}
async function prepare(t, profile = 'edge') {
const value = fixture(t);
const plan = inspectLegacySqlitePath({
sourcePath: value.sourcePath,
profile,
});
const adoption = await stageLocalSqliteAdoption({
...value,
profile,
expectedPlanDigest: plan.planDigest,
});
const activation = await prepareLocalSqliteActivation({
...value,
expectedManifestDigest: adoption.manifestDigest,
});
return { ...value, activation };
}
test('runtime adopted composition does not load executable migration SQL', () => {
const script = `
require(${JSON.stringify(path.resolve(__dirname, '../dist/adopted-profile/localAdoptedProfile.js'))});
const loaded = Object.keys(require.cache)
.filter((entry) => /[\\/]local-sqlite[\\/]dist[\\/](?:migration|migrations[\\/])/.test(entry));
process.stdout.write(JSON.stringify(loaded));
`;
const result = spawnSync(process.execPath, ['-e', script], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), []);
});
test('disabled adopted composition never inspects activation paths', async () => {
const storageAudits = [];
const adoptionAudits = [];
const result = await bootstrapLocalAdoptedProfileStorage({
enabled: false,
profile: 'edge',
sourcePath: 'invalid',
targetPath: 'invalid',
recoveryPath: 'invalid',
manifestPath: 'invalid',
activationPath: 'invalid',
expectedActivationDigest: 'invalid',
audit: (record) => storageAudits.push(record),
adoptionAudit: (record) => adoptionAudits.push(record),
});
assert.equal(result.status, 'disabled');
assert.deepEqual(
storageAudits.map(({ state }) => state),
['disabled'],
);
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['disabled'],
);
assert.equal(await result.stop(), 'stopped');
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['disabled', 'stopped'],
);
});
test('starts target storage only while the legacy source remains fenced', async (t) => {
const value = await prepare(t, 'edge');
const storageAudits = [];
const adoptionAudits = [];
const result = await bootstrapLocalAdoptedProfileStorage({
enabled: true,
profile: 'edge',
...value,
expectedActivationDigest: value.activation.activationDigest,
busyTimeoutMs: 100,
audit: (record) => storageAudits.push(record),
adoptionAudit: (record) => adoptionAudits.push(record),
});
assert.equal(result.status, 'adopted_storage_ready');
assert.equal(result.evidence.contractName, 'local-control-core');
assert.deepEqual(await result.startupRecovery.inspectCandidates(), {
candidates: [],
truncated: false,
});
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['fence_acquired', 'storage_ready'],
);
const legacyWriter = new DatabaseSync(value.sourcePath, { timeout: 100 });
assert.throws(
() =>
legacyWriter
.prepare('INSERT INTO "Crontabs" (id, command) VALUES (?, ?)')
.run(2, 'echo blocked'),
(error) => error && error.errstr === 'database is locked',
);
assert.equal(await result.stop(), 'stopped');
assert.equal(await result.stop(), 'stopped');
legacyWriter
.prepare('INSERT INTO "Crontabs" (id, command) VALUES (?, ?)')
.run(2, 'echo released');
legacyWriter.close();
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['fence_acquired', 'storage_ready', 'stopped'],
);
assert.ok(storageAudits.some(({ state }) => state === 'storage_ready'));
assert.ok(storageAudits.some(({ state }) => state === 'stopped'));
});
test('source drift fails activation and releases the temporary fence', async (t) => {
const value = await prepare(t, 'standalone');
const source = new DatabaseSync(value.sourcePath);
source
.prepare('INSERT INTO "Crontabs" (id, command) VALUES (?, ?)')
.run(2, 'echo late');
source.close();
const adoptionAudits = [];
await assert.rejects(
bootstrapLocalAdoptedProfileStorage({
enabled: true,
profile: 'standalone',
...value,
expectedActivationDigest: value.activation.activationDigest,
busyTimeoutMs: 100,
audit() {},
adoptionAudit: (record) => adoptionAudits.push(record),
}),
/legacy source identity or catalog changed after staging/,
);
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['failed'],
);
const writer = new DatabaseSync(value.sourcePath, { timeout: 100 });
writer
.prepare('INSERT INTO "Crontabs" (id, command) VALUES (?, ?)')
.run(3, 'echo no leaked fence');
writer.close();
});
test('fails before readiness when the target path changes while storage opens', async (t) => {
const value = await prepare(t, 'edge');
const replacementPath = path.join(value.directory, 'replacement.sqlite');
const adoptionAudits = [];
await assert.rejects(
bootstrapLocalAdoptedProfileStorage({
enabled: true,
profile: 'edge',
...value,
expectedActivationDigest: value.activation.activationDigest,
busyTimeoutMs: 100,
audit(record) {
if (record.state !== 'storage_ready') return;
fs.copyFileSync(value.targetPath, replacementPath);
fs.renameSync(replacementPath, value.targetPath);
},
adoptionAudit: (record) => adoptionAudits.push(record),
}),
/target database identity does not match the activation/,
);
assert.deepEqual(
adoptionAudits.map(({ state }) => state),
['fence_acquired', 'failed'],
);
const writer = new DatabaseSync(value.sourcePath, { timeout: 100 });
writer
.prepare('INSERT INTO "Crontabs" (id, command) VALUES (?, ?)')
.run(2, 'echo no leaked fence');
writer.close();
});
@@ -0,0 +1,34 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
bootstrapEdgeAdoptedStorage,
} = require('@qinglong/local-admin/adopted-profile/edge');
const {
bootstrapStandaloneAdoptedStorage,
} = require('@qinglong/local-admin/adopted-profile/standalone');
for (const [profile, bootstrap] of [
['edge', bootstrapEdgeAdoptedStorage],
['standalone', bootstrapStandaloneAdoptedStorage],
]) {
test(`adopted ${profile} subpath fixes the Profile while remaining default-off`, async () => {
const storage = [];
const adoption = [];
const result = await bootstrap({
enabled: false,
sourcePath: 'invalid',
targetPath: 'invalid',
recoveryPath: 'invalid',
manifestPath: 'invalid',
activationPath: 'invalid',
expectedActivationDigest: 'invalid',
audit: (record) => storage.push(record),
adoptionAudit: (record) => adoption.push(record),
});
assert.equal(result.status, 'disabled');
assert.equal(result.profile, profile);
assert.equal(storage[0].profile, profile);
assert.equal(adoption[0].profile, profile);
await result.stop();
});
}
@@ -0,0 +1,25 @@
const assert = require('node:assert/strict');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
test('keeps adopted activation authority unloaded until the capability is enabled', () => {
const entrypoint = path.resolve(
__dirname,
'../dist/adopted-profile/localAdoptedProfile.js',
);
const result = spawnSync(
process.execPath,
[
'-e',
`require(${JSON.stringify(entrypoint)});
const loaded = Object.keys(require.cache).filter((file) =>
/[\\/]ql3-local-admin[\\/]dist[\\/](?:runtime|legacy-adoption[\\/])/.test(file),
);
process.stdout.write(JSON.stringify(loaded));`,
],
{ encoding: 'utf8' },
);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), []);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalSqliteAdoptionError,
inspectLegacyCrontabAdoptionDiagnostics,
inspectLegacySqlitePath,
issueReviewedLegacyCrontabAdoptionDecisionAuthorizationFile,
verifyReviewedLegacyCrontabAdoptionDecisionAuthorizationFile,
} = require('..');
const {
LegacyCrontabDecisionIssuerKeyringFileProvider,
provisionLegacyCrontabDecisionIssuerKeyring,
} = require('../dist/legacy-adoption/legacyCrontabDecisionIssuer');
const NOW_MS = 1_760_000_000_000;
const DECISION_ID = '019a2b3c-4d5e-7f60-8123-456789abcdef';
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-issuer-'));
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const sourcePath = path.join(directory, 'database.sqlite');
const source = new DatabaseSync(sourcePath);
source.exec(`
CREATE TABLE "Auths" (id INTEGER PRIMARY KEY, type TEXT, info TEXT);
CREATE TABLE "Crontabs" (
id INTEGER PRIMARY KEY,
name TEXT,
command TEXT NOT NULL,
schedule TEXT
);
CREATE TABLE "Envs" (id INTEGER PRIMARY KEY, name TEXT, value TEXT);
INSERT INTO "Crontabs" (id, name, command, schedule)
VALUES (1, 'Reviewed task', 'task /scripts/reviewed.sh', '0 0 * * *');
`);
source.close();
return {
directory,
sourcePath,
authorizationPath: path.join(directory, 'decision.ndjson'),
issuerKeyringPath: path.join(directory, 'decision-issuer.keyring'),
};
}
function review(value) {
const plan = inspectLegacySqlitePath({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
});
const page = inspectLegacyCrontabAdoptionDiagnostics({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
limit: 16,
});
assert.equal(page.diagnostics.length, 1);
assert.equal(page.diagnostics[0].classification, 'lossless');
return {
plan,
decisions: [
{
rowOrdinal: page.diagnostics[0].rowOrdinal,
sourceDigest: page.diagnostics[0].sourceDigest,
disposition: 'adopt',
reason: 'reviewed_lossless',
},
],
};
}
function reviewer(assurance = 'local_console') {
return Object.freeze({
subject: Object.freeze({ type: 'user', id: 'local-owner' }),
authenticationId: 'local-console:credential-v1',
authenticatedAtMs: NOW_MS - 1_000,
expiresAtMs: NOW_MS + 5 * 60 * 1_000,
assurance,
});
}
test('issues one bounded authorization from an authenticated capability and dedicated keyring', async (t) => {
const value = fixture(t);
const { plan, decisions } = review(value);
await provisionLegacyCrontabDecisionIssuerKeyring(value.issuerKeyringPath);
let authenticationCalls = 0;
let confirmationCalls = 0;
const result =
await issueReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
decisionId: DECISION_ID,
authorizationPath: value.authorizationPath,
issuerKeyringPath: value.issuerKeyringPath,
decisions,
async authenticateReviewer() {
authenticationCalls += 1;
return reviewer();
},
confirmIssuerAuthority() {
confirmationCalls += 1;
},
lifetimeMs: 2 * 60 * 1_000,
clock: () => NOW_MS,
});
assert.equal(authenticationCalls, 1);
assert.equal(confirmationCalls, 4);
assert.equal(result.receipt.reviewer.subject.id, 'local-owner');
assert.equal(result.receipt.issuedAtMs, NOW_MS);
assert.equal(result.receipt.expiresAtMs, NOW_MS + 2 * 60 * 1_000);
assert.match(result.file.keyId, /^qladk-/);
assert.equal(fs.statSync(value.authorizationPath).mode & 0o777, 0o600);
const verified =
await verifyReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
expectedDecisionId: DECISION_ID,
authorizationPath: value.authorizationPath,
keyProvider: new LegacyCrontabDecisionIssuerKeyringFileProvider(
value.issuerKeyringPath,
),
observedAtMs: NOW_MS + 1_000,
});
assert.equal(verified.file.fileDigest, result.file.fileDigest);
});
test('rejects a self-reported weak reviewer before reading any issuer key', async (t) => {
const value = fixture(t);
const { plan, decisions } = review(value);
await assert.rejects(
issueReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
decisionId: DECISION_ID,
authorizationPath: value.authorizationPath,
issuerKeyringPath: value.issuerKeyringPath,
decisions,
authenticateReviewer: () => reviewer('single_factor'),
confirmIssuerAuthority() {},
clock: () => NOW_MS,
}),
LocalSqliteAdoptionError,
);
assert.equal(fs.existsSync(value.authorizationPath), false);
});
test('rechecks issuer authority immediately before no-replace publication', async (t) => {
const value = fixture(t);
const { plan, decisions } = review(value);
await provisionLegacyCrontabDecisionIssuerKeyring(value.issuerKeyringPath);
let confirmationCalls = 0;
await assert.rejects(
issueReviewedLegacyCrontabAdoptionDecisionAuthorizationFile({
sourcePath: value.sourcePath,
profile: 'edge',
legacyTimezone: 'UTC',
expectedPlanDigest: plan.planDigest,
decisionId: DECISION_ID,
authorizationPath: value.authorizationPath,
issuerKeyringPath: value.issuerKeyringPath,
decisions,
authenticateReviewer: () => reviewer(),
confirmIssuerAuthority() {
confirmationCalls += 1;
if (confirmationCalls === 4) throw new Error('authority drift');
},
clock: () => NOW_MS,
}),
LocalSqliteAdoptionError,
);
assert.equal(confirmationCalls, 4);
assert.equal(fs.existsSync(value.authorizationPath), false);
assert.deepEqual(
fs.readdirSync(value.directory).filter((entry) => entry.includes('.tmp')),
[],
);
});
@@ -0,0 +1,148 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LegacyCrontabDecisionIssuerKeyringConflictError,
LegacyCrontabDecisionIssuerKeyringFileProvider,
LegacyCrontabDecisionIssuerKeyringUnavailableError,
MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS,
provisionLegacyCrontabDecisionIssuerKeyring,
rotateLegacyCrontabDecisionIssuerKeyring,
} = require('../dist/legacy-adoption/legacyCrontabDecisionIssuerKeyring');
function fixture(t) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-adoption-issuer-keyring-'),
);
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return { root, filePath: path.join(root, 'issuer.keyring') };
}
test('provisions one private dedicated key and rotates with exact replay fences', async (t) => {
const { filePath } = fixture(t);
const initial = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
assert.equal(initial.schemaVersion, 1);
assert.equal(
initial.kind,
'qinglong3-legacy-crontab-decision-issuer-keyring-summary',
);
assert.equal(initial.keyCount, 1);
assert.match(initial.activeKeyId, /^qladk-/);
assert.match(initial.keyringDigest, /^[0-9a-f]{64}$/);
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
const provider = new LegacyCrontabDecisionIssuerKeyringFileProvider(filePath);
const active = await provider.active();
assert.equal(active.keyId, initial.activeKeyId);
assert.equal(active.key.byteLength, 32);
const rotated = await rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
});
assert.equal(rotated.keyCount, 2);
assert.notEqual(rotated.activeKeyId, initial.activeKeyId);
assert.notEqual(rotated.keyringDigest, initial.keyringDigest);
assert.equal((await provider.active()).keyId, rotated.activeKeyId);
assert.equal(
(await provider.resolve(initial.activeKeyId)).keyId,
active.keyId,
);
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringConflictError,
);
});
test('never replaces an existing keyring and leaves another rotation lock intact', async (t) => {
const { filePath } = fixture(t);
const initial = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
const before = fs.readFileSync(filePath);
await assert.rejects(
provisionLegacyCrontabDecisionIssuerKeyring(filePath),
LegacyCrontabDecisionIssuerKeyringConflictError,
);
assert.deepEqual(fs.readFileSync(filePath), before);
const lockPath = `${filePath}.lock`;
fs.writeFileSync(lockPath, '', { mode: 0o600, flag: 'wx' });
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
assert.equal(fs.existsSync(lockPath), true);
});
test('fails closed for broad files, tampering, symlinks and parent replacement', async (t) => {
const { root, filePath } = fixture(t);
await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
const provider = new LegacyCrontabDecisionIssuerKeyringFileProvider(filePath);
fs.chmodSync(filePath, 0o644);
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
fs.chmodSync(filePath, 0o600);
const manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
manifest.keys[manifest.activeKeyId] = 'not-canonical-key-material';
fs.writeFileSync(filePath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
const other = path.join(root, 'other.keyring');
await provisionLegacyCrontabDecisionIssuerKeyring(other);
const link = path.join(root, 'link.keyring');
fs.symlinkSync(other, link);
await assert.rejects(
new LegacyCrontabDecisionIssuerKeyringFileProvider(link).active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
const moved = `${root}-moved`;
fs.renameSync(root, moved);
fs.mkdirSync(root, { mode: 0o700 });
t.after(() => fs.rmSync(moved, { recursive: true, force: true }));
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
});
test('retains at most eight verification keys', async (t) => {
const { filePath } = fixture(t);
let summary = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
while (summary.keyCount < MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS) {
summary = await rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: summary.activeKeyId,
expectedKeyringDigest: summary.keyringDigest,
});
}
assert.equal(summary.keyCount, MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS);
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: summary.activeKeyId,
expectedKeyringDigest: summary.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
});
@@ -0,0 +1,137 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LegacyCrontabAdoptionDecisionReviewFileError,
withPrivateLegacyCrontabAdoptionDecisionReviewFile,
} = require('../dist/legacy-adoption/legacyCrontabDecisionIssuer');
const DECISION_ID = '019a2b3c-4d5e-7f60-8123-456789abcdef';
const PLAN_DIGEST = 'a'.repeat(64);
const INVENTORY_DIGEST = 'b'.repeat(64);
const DECISION = Object.freeze({
rowOrdinal: 1,
sourceDigest: 'c'.repeat(64),
disposition: 'adopt',
reason: 'reviewed_lossless',
});
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-review-file-'));
fs.chmodSync(directory, 0o700);
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'review.ndjson');
const records = [
{
schemaVersion: 1,
kind: 'qinglong3-legacy-crontab-decision-review-file-header',
decisionId: DECISION_ID,
profile: 'edge',
planDigest: PLAN_DIGEST,
inventoryDigest: INVENTORY_DIGEST,
},
{
schemaVersion: 1,
kind: 'qinglong3-legacy-crontab-decision-review-file-row',
decision: DECISION,
},
];
fs.writeFileSync(
filePath,
`${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
{ mode: 0o600 },
);
return { directory, filePath };
}
function options(filePath) {
return {
filePath,
expectedDecisionId: DECISION_ID,
expectedProfile: 'edge',
expectedPlanDigest: PLAN_DIGEST,
expectedInventoryDigest: INVENTORY_DIGEST,
};
}
test('streams repeatable decisions from one authenticated private descriptor', async (t) => {
const value = fixture(t);
const result = await withPrivateLegacyCrontabAdoptionDecisionReviewFile(
options(value.filePath),
(scope) => {
assert.equal(scope.evidence.decisionCount, 1);
assert.match(scope.evidence.fileDigest, /^[0-9a-f]{64}$/);
assert.deepEqual([...scope.decisions], [DECISION]);
assert.deepEqual([...scope.decisions], [DECISION]);
scope.confirmIdentity();
return scope.evidence.fileDigest;
},
);
assert.match(result, /^[0-9a-f]{64}$/);
});
test('fails closed on private-path violations and in-flight replacement', async (t) => {
const value = fixture(t);
fs.chmodSync(value.filePath, 0o640);
await assert.rejects(
withPrivateLegacyCrontabAdoptionDecisionReviewFile(
options(value.filePath),
() => undefined,
),
LegacyCrontabAdoptionDecisionReviewFileError,
);
fs.chmodSync(value.filePath, 0o600);
const linkPath = path.join(value.directory, 'review-link.ndjson');
fs.symlinkSync(value.filePath, linkPath);
await assert.rejects(
withPrivateLegacyCrontabAdoptionDecisionReviewFile(
options(linkPath),
() => undefined,
),
LegacyCrontabAdoptionDecisionReviewFileError,
);
await assert.rejects(
withPrivateLegacyCrontabAdoptionDecisionReviewFile(
options(value.filePath),
() => {
const replacement = path.join(value.directory, 'replacement.ndjson');
fs.copyFileSync(value.filePath, replacement);
fs.chmodSync(replacement, 0o600);
fs.renameSync(replacement, value.filePath);
},
),
LegacyCrontabAdoptionDecisionReviewFileError,
);
});
test('rejects widened records and mismatched review identity', async (t) => {
const value = fixture(t);
const widened = fs
.readFileSync(value.filePath, 'utf8')
.replace(
'"reason":"reviewed_lossless"',
'"reason":"reviewed_lossless","override":true',
);
fs.writeFileSync(value.filePath, widened, { mode: 0o600 });
await assert.rejects(
withPrivateLegacyCrontabAdoptionDecisionReviewFile(
options(value.filePath),
() => undefined,
),
LegacyCrontabAdoptionDecisionReviewFileError,
);
const fresh = fixture(t);
await assert.rejects(
withPrivateLegacyCrontabAdoptionDecisionReviewFile(
{ ...options(fresh.filePath), expectedInventoryDigest: 'd'.repeat(64) },
() => undefined,
),
LegacyCrontabAdoptionDecisionReviewFileError,
);
});
@@ -0,0 +1,255 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalIdentityCredentialAdministrationAuthorizationError,
createLocalIdentityCredentialAdministrationService,
} = require('@qinglong/local-admin/identity-credential-administration');
const MUTATION_ID = '82000000-0000-4000-8000-000000000001';
const SUBJECT = Object.freeze({ type: 'agent', id: 'agent-planner' });
const PRINCIPAL = Object.freeze({
subject: { type: 'user', id: 'owner-user' },
authenticationId: 'local_identity_admin:test',
authenticatedAtMs: 0,
expiresAtMs: 120_000,
assurance: 'local_console',
});
function projectPolicy(role = 'owner') {
return {
async resolve() {
return {
project: {
id: 'default',
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: {
projectId: 'default',
subject: PRINCIPAL.subject,
version: 1,
state: 'active',
role,
mutationId: 'owner-binding',
changedBy: PRINCIPAL.subject,
createdAtMs: 0,
},
};
},
async append() {
throw new Error('not used');
},
};
}
test('replays a committed credential after time advances without rereading Identity state', async () => {
let nowMs = 1_000;
let identityReads = 0;
let stored;
const repository = {
async resolveAuthorityProjectId() {
return 'default';
},
async resolveIdentity() {
identityReads += 1;
return {
subject: SUBJECT,
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 0,
};
},
async resolveIdentityMutation() {
return null;
},
async appendAuthorizedIdentity() {
throw new Error('not used');
},
async inspectAuthorizedIdentity() {
throw new Error('not used');
},
async resolveCredentialMutation() {
return stored ?? null;
},
async appendAuthorizedCredential(command) {
if (!stored) {
stored = Object.freeze({
projectId: command.authorization.projectId,
credential: Object.freeze({ ...command.credential }),
mutation: Object.freeze({ ...command.mutation }),
delivery: command.delivery,
audit: command.audit,
});
return Object.freeze({ status: 'inserted', ...stored });
}
assert.deepEqual(command.credential, stored.credential);
assert.deepEqual(command.mutation, stored.mutation);
assert.deepEqual(command.delivery, stored.delivery);
return Object.freeze({ status: 'existing', ...stored });
},
async inspectAuthorizedCredential() {
throw new Error('not used');
},
async resolveDeliveryAcknowledgement() {
return null;
},
async appendAuthorizedDeliveryAcknowledgement() {
throw new Error('not used');
},
async record() {},
};
const service = createLocalIdentityCredentialAdministrationService(
projectPolicy(),
repository,
{ now: () => nowMs },
);
const request = {
projectId: 'default',
operation: 'issue',
credentialId: 'agent-planner-primary',
target: SUBJECT,
expectedCurrentVersion: 0,
pepperKeyId: 'owner-v1',
secretDigest: 'a'.repeat(64),
deliveryDigest: 'b'.repeat(64),
notBeforeAtMs: 1_000,
expiresAtMs: 61_000,
mutationId: MUTATION_ID,
requestId: 'identity-credential-replay',
principal: PRINCIPAL,
};
assert.equal((await service.changeCredential(request)).status, 'inserted');
nowMs = 30_000;
assert.equal((await service.changeCredential(request)).status, 'existing');
assert.equal(identityReads, 1);
assert.equal(stored.mutation.createdAtMs, 1_000);
assert.equal(stored.credential.notBeforeAtMs, 1_000);
});
test('inspects current versions only after Owner authorization', async () => {
const audits = [];
const identity = Object.freeze({
subject: SUBJECT,
status: 'active',
version: 7,
createdAtMs: 100,
updatedAtMs: 700,
});
const credential = Object.freeze({
credentialId: 'agent-planner-primary',
version: 5,
pepperKeyId: 'owner-v1',
state: 'active',
subject: SUBJECT,
subjectStatus: 'active',
secretDigest: 'a'.repeat(64),
createdAtMs: 500,
notBeforeAtMs: 500,
expiresAtMs: 60_500,
});
const repository = {
async resolveAuthorityProjectId() {
return 'default';
},
async resolveIdentity() {
throw new Error('not used');
},
async resolveIdentityMutation() {
throw new Error('not used');
},
async appendAuthorizedIdentity() {
throw new Error('not used');
},
async inspectAuthorizedIdentity(command) {
assert.equal(command.audit.operationId, 'identity.inspect');
assert.equal(command.authorization.actor.id, 'owner-user');
return { identity, audit: command.audit };
},
async resolveCredentialMutation() {
throw new Error('not used');
},
async appendAuthorizedCredential() {
throw new Error('not used');
},
async inspectAuthorizedCredential(command) {
assert.equal(command.audit.operationId, 'credential.inspect');
assert.equal(command.credentialId, credential.credentialId);
return { credential, audit: command.audit };
},
async resolveDeliveryAcknowledgement() {
throw new Error('not used');
},
async appendAuthorizedDeliveryAcknowledgement() {
throw new Error('not used');
},
async record(audit) {
audits.push(audit);
},
};
const owner = createLocalIdentityCredentialAdministrationService(
projectPolicy(),
repository,
{ now: () => 1_000 },
);
const identityResult = await owner.inspectIdentity({
projectId: 'default',
target: SUBJECT,
auditEventId: '82000000-0000-4000-8000-000000000002',
requestId: 'identity-inspect',
principal: PRINCIPAL,
});
const credentialResult = await owner.inspectCredential({
projectId: 'default',
credentialId: credential.credentialId,
auditEventId: '82000000-0000-4000-8000-000000000003',
requestId: 'credential-inspect',
principal: PRINCIPAL,
});
assert.equal(identityResult.identity.version, 7);
assert.equal(credentialResult.credential.version, 5);
const nonOwner = createLocalIdentityCredentialAdministrationService(
projectPolicy('admin'),
repository,
{ now: () => 1_000 },
);
await assert.rejects(
nonOwner.inspectIdentity({
projectId: 'default',
target: SUBJECT,
auditEventId: '82000000-0000-4000-8000-000000000004',
requestId: 'identity-inspect-denied',
principal: PRINCIPAL,
}),
LocalIdentityCredentialAdministrationAuthorizationError,
);
assert.equal(audits.length, 1);
assert.equal(audits[0].operationId, 'identity.inspect');
assert.equal(audits[0].outcome, 'denied');
const foreignProjectOwner =
createLocalIdentityCredentialAdministrationService(
projectPolicy(),
repository,
{ now: () => 1_000 },
);
await assert.rejects(
foreignProjectOwner.inspectCredential({
projectId: 'secondary',
credentialId: credential.credentialId,
auditEventId: '82000000-0000-4000-8000-000000000005',
requestId: 'credential-inspect-foreign-project',
principal: PRINCIPAL,
}),
LocalIdentityCredentialAdministrationAuthorizationError,
);
assert.equal(audits.length, 2);
assert.deepEqual(audits[1].reasons, ['instance_authority_project_required']);
});
@@ -0,0 +1,708 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
PLUGIN_PACKAGE_ACTIVATION_INTENT_SCHEMA,
PluginPackageActivationConflictError,
PluginPackageActivationUnavailableError,
} = require('@qinglong/runtime-core/plugin-package-activation');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package');
const {
createPluginPackageInstall,
createPluginPackageLock,
pluginPackageInstallActionDigest,
pluginPackageInstallCreate,
pluginPackageInstallPlanDigest,
} = require('@qinglong/runtime-core/plugin-package-install');
const {
createApprovalRequest,
} = require('@qinglong/runtime-core/approved-action');
const {
createPluginPackageInstallProposal,
} = require('@qinglong/runtime-core/plugin-package-proposal');
const {
PluginPackageRecoveryCoordinator,
} = require('@qinglong/runtime-core/plugin-package-recovery');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('@qinglong/local-sqlite/plugin-package-install');
const {
LocalSqliteApprovalRequestRepository,
} = require('@qinglong/local-sqlite/approved-action');
const {
LocalSqliteApprovedActionExecutionRepository,
} = require('@qinglong/local-sqlite/approved-action-execution');
const {
LocalSqlitePluginPackageInstallProposalRepository,
} = require('@qinglong/local-sqlite/plugin-package-proposal');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const {
LocalPluginPackageActivationPublisher,
isLocalPluginPackageActivePointerName,
} = require('../dist/plugin-package/pluginPackageActivation');
const {
createLocalPluginPackageInstallationCoordinator,
} = require('../dist/plugin-package/pluginPackageInstallation');
function sha(value) {
return createHash('sha256').update(value).digest('hex');
}
function harness(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-package-active-'));
const stagingRoot = path.join(root, 'staging');
const activationRoot = path.join(root, 'activation');
fs.mkdirSync(stagingRoot, { mode: 0o700 });
fs.mkdirSync(activationRoot, { mode: 0o700 });
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return { root, stagingRoot, activationRoot };
}
function createStage(stagingRoot, overrides = {}) {
const lockDigest = overrides.lockDigest ?? 'a'.repeat(64);
const contentDigest = overrides.contentDigest ?? 'b'.repeat(64);
const material = Buffer.from(overrides.material ?? 'bounded package content');
const entryPath = 'package.json';
const blob = `0000-${sha(entryPath)}.blob`;
const stageDirectory = path.join(stagingRoot, lockDigest);
const blobDirectory = path.join(stageDirectory, 'blobs');
fs.mkdirSync(stageDirectory, { mode: 0o700 });
fs.mkdirSync(blobDirectory, { mode: 0o700 });
fs.writeFileSync(path.join(blobDirectory, blob), material, { mode: 0o600 });
const receipt = {
schema: 'qinglong/plugin-package-stage-receipt@v1',
lockDigest,
inspection: { lockDigest, contentDigest },
entries: [
{
path: entryPath,
bytes: material.byteLength,
digest: sha(material),
blob,
},
],
};
const serialized = `${JSON.stringify(receipt)}\n`;
fs.writeFileSync(path.join(stageDirectory, 'receipt.json'), serialized, {
mode: 0o600,
});
return {
lockDigest,
contentDigest,
stageDirectory,
blobPath: path.join(blobDirectory, blob),
evidenceDigest: sha(serialized),
};
}
function intent(stage, overrides = {}) {
const installationId = overrides.installationId ?? 'install-001';
const targetGeneration = overrides.targetGeneration ?? 1;
const previousActiveLockDigest = overrides.previousActiveLockDigest ?? null;
const resourceGeneration = createPluginPackageResourceGeneration({
installationId,
projectId: 'default',
packageName: 'example-monitor',
lockDigest: stage.lockDigest,
generation: targetGeneration,
previousActiveLockDigest,
contentDigest: stage.contentDigest,
contents: {
tasks: ['tasks/example.yaml'],
workflows: [],
prompts: [],
tools: [],
},
});
return Object.freeze({
schema: PLUGIN_PACKAGE_ACTIVATION_INTENT_SCHEMA,
installationId,
projectId: 'default',
packageName: 'example-monitor',
lockDigest: stage.lockDigest,
targetGeneration,
previousActiveLockDigest,
stageRef: `local-stage:${stage.lockDigest}`,
stageReceiptDigest: overrides.stageReceiptDigest ?? 'c'.repeat(64),
stageEvidenceDigest: stage.evidenceDigest,
contentDigest: stage.contentDigest,
resourceGeneration: overrides.resourceGeneration ?? resourceGeneration,
intentDigest: overrides.intentDigest ?? 'd'.repeat(64),
});
}
function activationLockPath(activationRoot, value) {
const key = createHash('sha256')
.update('qinglong/plugin-package-active-pointer-key@v1\0', 'utf8')
.update(value.projectId, 'utf8')
.update('\0', 'utf8')
.update(value.packageName, 'utf8')
.digest('hex');
return path.join(activationRoot, `.${key}.lock`);
}
function installAction() {
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.2.0',
description: 'One bounded package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const environment = {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
const plan = planPluginPackageInstall(manifest, environment);
return {
lockId: 'lock-install-001',
projectId: 'default',
manifest,
plan,
environment,
source: {
kind: 'offline',
locator: `offline:sha256:${'a'.repeat(64)}`,
artifactDigest: 'a'.repeat(64),
artifactBytes: 2048,
contentDigest: 'b'.repeat(64),
},
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
};
}
function installLock() {
const action = installAction();
return createPluginPackageLock({
...action,
approval: {
requestId: 'approval-install-001',
requestVersion: 3,
dispatchId: 'dispatch-install-001',
actionDigest: pluginPackageInstallActionDigest(action),
previewDigest: pluginPackageInstallPlanDigest(action.plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 10_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200,
});
}
test('publishes one durable exact pointer and replays without advancing time', async (t) => {
const directories = harness(t);
const stage = createStage(directories.stagingRoot);
let nowCalls = 0;
const publisher = new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now() {
nowCalls += 1;
return 500;
},
});
const value = intent(stage);
assert.equal(
await publisher.findActiveResourceGeneration('default', 'example-monitor'),
null,
);
assert.deepEqual(await publisher.inspect(value), {
status: 'not_published',
});
const published = await publisher.publish(value);
assert.equal(published.intentDigest, value.intentDigest);
assert.equal(published.generation, 1);
assert.equal(published.activatedAtMs, 500);
assert.deepEqual(await publisher.inspect(value), {
status: 'published',
receipt: published,
});
assert.deepEqual(await publisher.publish(value), published);
assert.deepEqual(
await publisher.findActiveResourceGeneration('default', 'example-monitor'),
value.resourceGeneration,
);
await assert.rejects(
publisher.findActiveResourceGeneration('default', 'Example_Monitor'),
TypeError,
);
assert.equal(nowCalls, 1);
const files = fs.readdirSync(directories.activationRoot);
assert.equal(files.length, 1);
assert.equal(isLocalPluginPackageActivePointerName(files[0]), true);
assert.equal(
fs.statSync(path.join(directories.activationRoot, files[0])).mode & 0o777,
0o600,
);
});
test('replaces only the exact previous active lock generation', async (t) => {
const directories = harness(t);
const firstStage = createStage(directories.stagingRoot);
const publisher = new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now: () => 500,
});
const first = intent(firstStage);
await publisher.publish(first);
const secondStage = createStage(directories.stagingRoot, {
lockDigest: 'e'.repeat(64),
contentDigest: 'f'.repeat(64),
material: 'replacement package content',
});
const second = intent(secondStage, {
installationId: 'install-002',
targetGeneration: 2,
previousActiveLockDigest: first.lockDigest,
intentDigest: '1'.repeat(64),
});
const secondReceipt = await publisher.publish(second);
assert.equal(secondReceipt.generation, 2);
assert.equal((await publisher.inspect(second)).status, 'published');
assert.deepEqual(
await publisher.findActiveResourceGeneration('default', 'example-monitor'),
second.resourceGeneration,
);
const stale = intent(
createStage(directories.stagingRoot, {
lockDigest: '2'.repeat(64),
contentDigest: '3'.repeat(64),
material: 'stale package content',
}),
{
installationId: 'install-003',
targetGeneration: 3,
previousActiveLockDigest: first.lockDigest,
intentDigest: '4'.repeat(64),
},
);
await assert.rejects(
publisher.publish(stale),
PluginPackageActivationConflictError,
);
});
test('never removes a publication lock owned by another publisher', async (t) => {
const directories = harness(t);
const stage = createStage(directories.stagingRoot);
let nowCalls = 0;
const publisher = new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now() {
nowCalls += 1;
return 500;
},
});
const value = intent(stage);
const lockPath = activationLockPath(directories.activationRoot, value);
fs.writeFileSync(lockPath, 'another-publisher\n', { mode: 0o600 });
await assert.rejects(
publisher.publish(value),
PluginPackageActivationUnavailableError,
);
assert.equal(fs.readFileSync(lockPath, 'utf8'), 'another-publisher\n');
assert.equal(nowCalls, 0);
});
test('fails closed when staged evidence or an active pointer is tampered', async (t) => {
const directories = harness(t);
const stage = createStage(directories.stagingRoot);
const publisher = new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now: () => 500,
});
const value = intent(stage);
fs.writeFileSync(stage.blobPath, 'tampered', { mode: 0o600 });
await assert.rejects(
publisher.publish(value),
PluginPackageActivationConflictError,
);
fs.rmSync(stage.stageDirectory, { recursive: true });
const restored = createStage(directories.stagingRoot);
const restoredIntent = intent(restored);
await publisher.publish(restoredIntent);
const pointer = fs
.readdirSync(directories.activationRoot)
.find(isLocalPluginPackageActivePointerName);
const pointerPath = path.join(directories.activationRoot, pointer);
const oldPointer = JSON.parse(fs.readFileSync(pointerPath, 'utf8'));
oldPointer.schema = 'qinglong/plugin-package-active-pointer@v1';
fs.writeFileSync(pointerPath, `${JSON.stringify(oldPointer)}\n`, {
mode: 0o600,
});
await assert.rejects(
publisher.inspect(restoredIntent),
PluginPackageActivationConflictError,
);
});
test('rejects symlinked roots and keeps authority behind an explicit subpath', (t) => {
const directories = harness(t);
const linked = path.join(directories.root, 'linked');
fs.symlinkSync(directories.stagingRoot, linked);
assert.throws(
() =>
new LocalPluginPackageActivationPublisher({
stagingRoot: linked,
activationRoot: directories.activationRoot,
now: () => 500,
}),
TypeError,
);
assert.equal(require('..').LocalPluginPackageActivationPublisher, undefined);
assert.equal(
require('@qinglong/local-admin/package-activation')
.LocalPluginPackageActivationPublisher,
LocalPluginPackageActivationPublisher,
);
});
test('composes approval, SQLite lock persistence, stage and POSIX activation end to end', async (t) => {
const admittedAtMs = Date.now();
const actionInput = installAction();
const directories = harness(t);
const lock = createPluginPackageLock({
...actionInput,
approval: {
requestId: 'approval-install-001',
requestVersion: 3,
dispatchId: 'dispatch-install-001',
actionDigest: pluginPackageInstallActionDigest(actionInput),
previewDigest: pluginPackageInstallPlanDigest(actionInput.plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: admittedAtMs - 200,
expiresAtMs: admittedAtMs + 60_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: admittedAtMs,
});
const stage = createStage(directories.stagingRoot, {
lockDigest: lock.lockDigest,
contentDigest: lock.source.contentDigest,
});
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
t.after(() => client.close());
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','owner-001',1,'active','owner',
'grant-owner-1','user','owner-001',0)`,
)
.run();
const requester = { type: 'user', id: 'owner-001' };
const consumer = { type: 'system', id: 'package-dispatcher' };
const fence = { projectVersion: 1, bindingVersion: 1 };
const action = {
permission: 'package.manage',
actionType: 'plugin_package.install',
actionRef: 'proposal:example-monitor-v1',
actionDigest: lock.actionDigest,
previewDigest: lock.planDigest,
};
const proposal = createPluginPackageInstallProposal({
actionRef: action.actionRef,
actionInput,
proposedBy: requester,
proposalFence: fence,
createdAtMs: admittedAtMs - 400,
});
const audit = (
eventId,
requestId,
operationId,
subject,
authenticationId,
outcome,
reasons,
occurredAtMs,
) => ({
eventId,
requestId,
operationId,
projectId: 'default',
subject,
authenticationId,
outcome,
reasons,
fence,
occurredAtMs,
});
const approvals = new LocalSqliteApprovalRequestRepository(client);
await new LocalSqlitePluginPackageInstallProposalRepository(
client,
).createProposal({
proposal,
audit: audit(
'10000000-0000-4000-8000-000000000200',
action.actionRef,
'plugin_package.propose',
requester,
'auth-owner',
'allowed',
['package_proposal'],
proposal.createdAtMs,
),
});
await approvals.create({
request: createApprovalRequest({
id: lock.approval.requestId,
projectId: 'default',
action,
risk: 'high',
decisionMode: 'human_confirmation',
requestedBy: requester,
requestedAtMs: admittedAtMs - 300,
expiresAtMs: lock.approval.expiresAtMs,
requestFence: fence,
}),
audit: audit(
'10000000-0000-4000-8000-000000000201',
'http-package-1',
'approval.request',
requester,
'auth-owner',
'approval_required',
['package_review'],
admittedAtMs - 300,
),
});
await approvals.decide({
requestId: lock.approval.requestId,
expectedVersion: 1,
decisionId: 'decision-install-001',
decision: 'approved',
reasonCode: 'reviewed',
principal: {
subject: requester,
authenticationId: 'auth-owner-step-up',
authenticatedAtMs: admittedAtMs - 250,
expiresAtMs: lock.approval.expiresAtMs,
assurance: 'local_console',
},
decidedAtMs: lock.approval.approvedAtMs,
authorizationFence: fence,
audit: audit(
'10000000-0000-4000-8000-000000000202',
'http-package-1',
'approval.decide',
requester,
'auth-owner-step-up',
'allowed',
['role_grant'],
lock.approval.approvedAtMs,
),
});
const consumed = await approvals.consume({
requestId: lock.approval.requestId,
expectedVersion: 2,
consumptionId: 'consume-install-001',
dispatchId: lock.approval.dispatchId,
action,
requestedBy: requester,
consumedBy: consumer,
consumedAtMs: admittedAtMs - 100,
authorizationFence: fence,
audit: audit(
'10000000-0000-4000-8000-000000000203',
'dispatch-cycle-1',
'approval.consume',
consumer,
'auth-package-dispatcher',
'allowed',
['role_grant'],
admittedAtMs - 100,
),
});
const executions = new LocalSqliteApprovedActionExecutionRepository(client);
const claimed = await executions.claimExecution({
dispatchId: consumed.dispatch.id,
owner: 'package_dispatcher',
leaseToken: 'lease-install-001',
nowMs: admittedAtMs - 50,
leaseDurationMs: 60_000,
});
assert.equal(claimed.status, 'claimed');
const started = await executions.startExecution({
dispatchId: consumed.dispatch.id,
approvalRequestId: consumed.dispatch.approvalRequestId,
actionDigest: consumed.dispatch.action.actionDigest,
owner: 'package_dispatcher',
leaseToken: 'lease-install-001',
expectedVersion: claimed.snapshot.execution.version,
startedAtMs: admittedAtMs,
});
const repository = new LocalSqlitePluginPackageInstallRepository(client);
const staged = [];
const coordinator = createLocalPluginPackageInstallationCoordinator({
repository,
publisher: new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now: () => admittedAtMs + 300,
}),
});
const options = {
lock,
proposalDigest: proposal.proposalDigest,
execution: started.execution,
installationId: 'install-001',
createMutationId: 'mutation-create',
createdAtMs: admittedAtMs,
stageMutationId: 'mutation-stage',
stagedAtMs: admittedAtMs + 100,
activationStartedMutationId: 'mutation-activate',
activationCommittedMutationId: 'mutation-commit',
activationFailedMutationId: 'mutation-fail',
activationStartedAtMs: admittedAtMs + 200,
activationObservedAtMs: admittedAtMs + 301,
admissionAudit: audit(
'10000000-0000-4000-8000-000000000204',
lock.approval.dispatchId,
'plugin_package.admit',
consumer,
'auth-package-dispatcher',
'allowed',
['approved_action'],
admittedAtMs,
),
};
const stageProvider = {
async stage(value) {
staged.push(value.lockDigest);
return {
stageRef: `local-stage:${value.lockDigest}`,
artifactDigest: value.source.artifactDigest,
manifestDigest: value.manifestDigest,
contentDigest: value.source.contentDigest,
evidenceDigest: stage.evidenceDigest,
};
},
};
const active = await coordinator.install(options, stageProvider);
assert.equal(active.state, 'active');
assert.equal(active.activeLockDigest, lock.lockDigest);
assert.deepEqual(await repository.findLock(lock.lockDigest), lock);
assert.equal(
(await repository.findAdmissionReceipt(lock.approval.dispatchId))
.lockDigest,
lock.lockDigest,
);
assert.deepEqual(staged, [lock.lockDigest]);
const replay = await coordinator.install(options, {
async stage() {
throw new Error('an active exact replay must not stage again');
},
});
assert.deepEqual(replay, active);
assert.deepEqual(staged, [lock.lockDigest]);
});
test('recovers a durable queued SQLite install through the POSIX publisher without approval replay', async (t) => {
const directories = harness(t);
const lock = installLock();
const stage = createStage(directories.stagingRoot, {
lockDigest: lock.lockDigest,
contentDigest: lock.source.contentDigest,
});
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
t.after(() => client.close());
const repository = new LocalSqlitePluginPackageInstallRepository(client);
const queued = createPluginPackageInstall(lock, {
installationId: 'recovery-install-001',
mutationId: 'recovery-create',
occurredAtMs: 201,
});
await repository.create(pluginPackageInstallCreate(lock, queued, null));
let stageCalls = 0;
const coordinator = new PluginPackageRecoveryCoordinator({
repository,
stageProvider: {
async stage(value) {
stageCalls += 1;
return {
stageRef: `local-stage:${value.lockDigest}`,
artifactDigest: value.source.artifactDigest,
manifestDigest: value.manifestDigest,
contentDigest: value.source.contentDigest,
evidenceDigest: stage.evidenceDigest,
};
},
},
publisher: new LocalPluginPackageActivationPublisher({
stagingRoot: directories.stagingRoot,
activationRoot: directories.activationRoot,
now: () => 500,
}),
now: () => 250,
});
const cycle = await coordinator.recover({ pageSize: 1, maxPages: 2 });
assert.equal(cycle.settled, 1);
assert.equal(cycle.safeToAdmit, true);
assert.equal(stageCalls, 1);
const active = await repository.find(lock.projectId, lock.packageName);
assert.equal(active.state, 'active');
assert.equal(active.activeLockDigest, lock.lockDigest);
assert.deepEqual(await repository.listRecoveryPage({ limit: 1 }), {
records: [],
truncated: false,
});
});
@@ -0,0 +1,54 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createLocalPluginPackageApprovedActionDispatcher,
LOCAL_PLUGIN_PACKAGE_DISPATCH_BATCH_LIMITS,
} = require('@qinglong/local-admin/package-approved-action');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
test('composes caller-driven edge and standalone dispatchers behind one SQLite authority', async (t) => {
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
const authority = new LocalSqliteOperationAuthority(client);
let id = 0;
const limits = [];
for (const profile of ['edge', 'standalone']) {
const dispatcher = createLocalPluginPackageApprovedActionDispatcher({
authority,
profile,
owner: `package_dispatcher_${profile}`,
clock: () => 100,
createId: () => `dispatcher-id-${++id}`,
});
dispatcher.repository.listDueExecutions = async (query) => {
limits.push(query.limit);
return { executions: [], truncated: false };
};
assert.deepEqual(await dispatcher.dispatchBatch(), {
scanned: 0,
claimed: 0,
started: 0,
succeeded: 0,
failed: 0,
blocked: 0,
retrying: 0,
deferred: 0,
recoveryRequired: 0,
alreadyTerminal: 0,
unavailable: 0,
truncated: false,
});
}
assert.deepEqual(limits, [
LOCAL_PLUGIN_PACKAGE_DISPATCH_BATCH_LIMITS.edge,
LOCAL_PLUGIN_PACKAGE_DISPATCH_BATCH_LIMITS.standalone,
]);
assert.equal(
require('..').createLocalPluginPackageApprovedActionDispatcher,
undefined,
);
});
@@ -0,0 +1,213 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createProjectToolDefinitionSnapshot,
projectToolDefinitionSnapshotContribution,
} = require('@qinglong/runtime-core/project-tool-definition-snapshot');
const {
activateInstall,
pluginPackageTaskReconciliationFixture,
} = require('../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('@qinglong/local-sqlite/plugin-package-install');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('@qinglong/local-sqlite/plugin-package-materialized-revision');
const {
LocalSqlitePluginPackageTaskReconciliationRepository,
} = require('@qinglong/local-sqlite/plugin-package-task-reconciliation');
const {
LocalSqliteProjectToolDefinitionSnapshotRepository,
} = require('@qinglong/local-sqlite/project-tool-definition-snapshot');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const {
createLocalPluginPackageLifecycleService,
} = require('@qinglong/local-admin/package-lifecycle');
const OWNER = Object.freeze({ type: 'user', id: 'owner-lifecycle' });
function principal(at) {
return {
subject: OWNER,
authenticationId: 'owner-lifecycle-console',
authenticatedAtMs: at - 1,
expiresAtMs: at + 60_000,
assurance: 'local_console',
};
}
async function harness(t) {
const fixture = pluginPackageTaskReconciliationFixture('local-admin-life', {
profile: 'edge',
});
const client = new DatabaseSync(':memory:');
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3Projects"
(id, name, slug, status, version, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
)
.run(fixture.projectId, fixture.projectId, fixture.projectId);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
project_id, subject_type, subject_id, version, state, role,
mutation_id, changed_by_type, changed_by_id, created_at_ms
) VALUES (?, 'user', ?, 1, 'active', 'owner', ?, 'user', ?, 1)`,
)
.run(fixture.projectId, OWNER.id, 'grant-local-admin-life', OWNER.id);
const authority = new LocalSqliteOperationAuthority(client);
t.after(() => authority.close());
const installs = new LocalSqlitePluginPackageInstallRepository(authority);
const materialized =
new LocalSqlitePluginPackageMaterializedRevisionRepository(
authority,
fixture.registry,
);
const reconciliations =
new LocalSqlitePluginPackageTaskReconciliationRepository(
authority,
fixture.registry,
);
const snapshots =
new LocalSqliteProjectToolDefinitionSnapshotRepository(authority);
await activateInstall(installs, fixture);
await materialized.publish(fixture.revision);
await reconciliations.reconcile(fixture.revision, {
async findActiveResourceGeneration() {
return fixture.revision.generation;
},
});
await snapshots.publish(
createProjectToolDefinitionSnapshot({
projectId: fixture.projectId,
contributions: [
projectToolDefinitionSnapshotContribution(
fixture.revision,
fixture.registry,
),
],
}),
);
let time = 50_000;
const service = createLocalPluginPackageLifecycleService({
authority,
now: () => time++,
});
return { fixture, client, service, time: () => time };
}
function execution(impact, ordinal, principalValue, confirmAuthorization) {
return {
impact,
approvalRequestId: `local-life-approval-${ordinal}`,
decisionId: `local-life-decision-${ordinal}`,
consumptionId: `local-life-consumption-${ordinal}`,
dispatchId: `local-life-dispatch-${ordinal}`,
approvalAuditEventId: `91000000-0000-4000-8000-${String(
ordinal * 10 + 1,
).padStart(12, '0')}`,
decisionAuditEventId: `91000000-0000-4000-8000-${String(
ordinal * 10 + 2,
).padStart(12, '0')}`,
consumptionAuditEventId: `91000000-0000-4000-8000-${String(
ordinal * 10 + 3,
).padStart(12, '0')}`,
reasonCode: 'reviewed',
principal: principalValue,
confirmAuthorization,
};
}
test('plans and executes replay-safe local lifecycle without another package or authority', async (t) => {
const value = await harness(t);
const owner = principal(value.time());
const disable = await value.service.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
owner,
);
assert.equal(disable.expected.disposition, 'active');
assert.ok(disable.resourceCounts.tasks > 0);
let confirmations = 0;
const disableCommand = execution(disable, 1, owner, () => {
confirmations += 1;
});
const disabled = await value.service.execute(disableCommand);
assert.equal(disabled.status, 'created');
assert.equal(disabled.approval.state, 'consumed');
assert.equal(disabled.receipt.lifecycle.disposition, 'disabled');
assert.equal(disabled.receipt.capability.status, 'withdrawn');
assert.equal(
disabled.receipt.capability.taskTransitions.length,
disable.taskIds.length,
);
assert.ok(confirmations >= 3);
const replayed = await value.service.execute(disableCommand);
assert.equal(replayed.status, 'existing');
assert.equal(replayed.receipt.receiptDigest, disabled.receipt.receiptDigest);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"`,
)
.get().count,
1,
);
const enable = await value.service.plan(
'enable',
value.fixture.projectId,
value.fixture.packageName,
owner,
);
const enabled = await value.service.execute(
execution(enable, 2, owner, () => undefined),
);
assert.equal(enabled.receipt.lifecycle.disposition, 'active');
assert.equal(enabled.receipt.capability.status, 'restored');
assert.equal(
enabled.receipt.capability.taskTransitions.length,
disable.taskIds.length,
);
});
test('rejects lifecycle plan before storage mutation for an unbound local User', async (t) => {
const value = await harness(t);
const outsider = {
...principal(value.time()),
subject: { type: 'user', id: 'outsider' },
};
await assert.rejects(
value.service.plan(
'disable',
value.fixture.projectId,
value.fixture.packageName,
outsider,
),
/not authorized by current Project policy/,
);
assert.equal(
value.client
.prepare(
`SELECT COUNT(*) AS count
FROM "QingLong3PluginPackageLifecycleEvents"`,
)
.get().count,
0,
);
});
@@ -0,0 +1,280 @@
const assert = require('node:assert/strict');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
createLocalPluginPackageManagementService,
} = require('@qinglong/local-admin/package-management');
const {
LocalSqliteOperationAuthority,
} = require('@qinglong/local-sqlite/operation-authority');
const {
LocalSqlitePluginPackageInstallRepository,
} = require('@qinglong/local-sqlite/plugin-package-install');
const {
migrateLocalSqliteDatabase,
} = require('@qinglong/local-sqlite/migration');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package');
const OWNER = Object.freeze({ type: 'user', id: 'usr_owner' });
const CONSUMER = Object.freeze({
subject: { type: 'system', id: 'local_package_dispatcher' },
authenticationId: 'local-package-dispatcher-auth',
});
function actionInput() {
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.2.0',
description: 'One bounded package',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '16Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const environment = {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * 1024 * 1024,
availableDiskBytes: 256 * 1024 * 1024,
};
return {
lockId: 'proposal-monitor-v1',
projectId: 'default',
manifest,
plan: planPluginPackageInstall(manifest, environment),
environment,
source: {
kind: 'offline',
locator: `offline:sha256:${'a'.repeat(64)}`,
artifactDigest: 'a'.repeat(64),
artifactBytes: 2_048,
contentDigest: 'b'.repeat(64),
},
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
};
}
function principal(now, assurance = 'single_factor') {
return {
subject: OWNER,
authenticationId: `auth-owner-${assurance}`,
authenticatedAtMs: now - 100,
expiresAtMs: now + 100_000,
assurance,
};
}
test('runs authenticated local proposal, self-confirmation, consumption and admission end to end', async (t) => {
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner',
'grant-owner-1','user','usr_owner',0)`,
)
.run();
const authority = new LocalSqliteOperationAuthority(client);
let now = Date.now();
const requestedAtMs = now;
let generatedId = 0;
const service = createLocalPluginPackageManagementService({
authority,
profile: 'edge',
consumer: CONSUMER,
dispatcher: {
owner: 'local_package_dispatcher_1',
clock: () => now,
createId: () => `dispatcher-id-${++generatedId}`,
},
now: () => now,
});
const proposalRequest = {
actionRef: 'proposal:monitor-v1',
approvalRequestId: 'approval-monitor-v1',
proposalAuditEventId: '10000000-0000-4000-8000-000000000001',
approvalAuditEventId: '10000000-0000-4000-8000-000000000002',
requestedAtMs,
actionInput: actionInput(),
principal: principal(now),
};
const proposed = await service.propose(proposalRequest);
assert.equal(proposed.proposalStatus, 'created');
assert.equal(proposed.approvalStatus, 'created');
assert.equal(proposed.approvalRequest.decisionMode, 'human_confirmation');
const replayed = await service.propose(proposalRequest);
assert.equal(replayed.proposalStatus, 'existing');
assert.equal(replayed.approvalStatus, 'existing');
now = Date.now();
const decided = await service.decide({
approvalRequestId: 'approval-monitor-v1',
expectedVersion: 1,
decisionId: 'decision-monitor-v1',
auditEventId: '10000000-0000-4000-8000-000000000003',
decision: 'approved',
reasonCode: 'reviewed',
decidedAtMs: now,
principal: principal(now, 'local_console'),
});
assert.equal(decided.request.state, 'approved');
now = Date.now();
const consumed = await service.consume({
approvalRequestId: 'approval-monitor-v1',
expectedVersion: 2,
consumptionId: 'consume-monitor-v1',
dispatchId: 'dispatch-monitor-v1',
auditEventId: '10000000-0000-4000-8000-000000000004',
consumedAtMs: now,
});
assert.equal(consumed.request.state, 'consumed');
now = Date.now();
const dispatched = await service.dispatch();
assert.equal(dispatched.scanned, 1);
assert.equal(dispatched.started, 1);
assert.equal(dispatched.succeeded, 1);
const installation = await new LocalSqlitePluginPackageInstallRepository(
authority,
).find('default', 'example-monitor');
assert.equal(installation.state, 'queued');
assert.equal(installation.targetGeneration, 1);
const inspected = await service.inspect(
'proposal:monitor-v1',
'approval-monitor-v1',
);
assert.equal(inspected.proposal.actionRef, 'proposal:monitor-v1');
assert.equal(inspected.approvalRequest.state, 'consumed');
assert.equal(
client
.prepare(
`SELECT count(*) AS count
FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" IN (
'plugin_package.propose',
'approval.request',
'approval.decide',
'approval.consume',
'plugin_package.admit'
)`,
)
.get().count,
5,
);
assert.equal(
require('..').createLocalPluginPackageManagementService,
undefined,
);
});
test('rejects weak decisions and expired management commands before mutation', async (t) => {
const client = new DatabaseSync(':memory:');
t.after(() => client.close());
client.exec('PRAGMA foreign_keys = ON');
await migrateLocalSqliteDatabase(client);
client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings"
("project_id","subject_type","subject_id","version","state","role",
"mutation_id","changed_by_type","changed_by_id","created_at_ms")
VALUES ('default','user','usr_owner',1,'active','owner',
'grant-owner-1','user','usr_owner',0)`,
)
.run();
const authority = new LocalSqliteOperationAuthority(client);
let now = Date.now();
const requestedAtMs = now;
const service = createLocalPluginPackageManagementService({
authority,
profile: 'edge',
consumer: CONSUMER,
dispatcher: {
owner: 'local_package_dispatcher_2',
clock: () => now,
createId: () => 'dispatcher-id-fixed',
},
approvalLifetimeMs: 1_000,
now: () => now,
});
await service.propose({
actionRef: 'proposal:monitor-weak-v1',
approvalRequestId: 'approval-monitor-weak-v1',
proposalAuditEventId: '20000000-0000-4000-8000-000000000001',
approvalAuditEventId: '20000000-0000-4000-8000-000000000002',
requestedAtMs,
actionInput: actionInput(),
principal: principal(now),
});
now = requestedAtMs + 100;
await assert.rejects(
service.decide({
approvalRequestId: 'approval-monitor-weak-v1',
expectedVersion: 1,
decisionId: 'decision-monitor-weak-v1',
auditEventId: '20000000-0000-4000-8000-000000000003',
decision: 'approved',
reasonCode: 'reviewed',
decidedAtMs: now,
principal: principal(now),
}),
{ code: 'APPROVAL_HUMAN_DECISION_REQUIRED' },
);
now = requestedAtMs + 1_000;
await assert.rejects(
service.propose({
actionRef: 'proposal:expired-v1',
approvalRequestId: 'approval-expired-v1',
proposalAuditEventId: '20000000-0000-4000-8000-000000000004',
approvalAuditEventId: '20000000-0000-4000-8000-000000000005',
requestedAtMs,
actionInput: actionInput(),
principal: principal(now),
}),
{ code: 'PLUGIN_PACKAGE_MANAGEMENT_REQUEST_INVALID' },
);
assert.equal(
client
.prepare(
`SELECT count(*) AS count
FROM "QingLong3SecurityAuditEvents"
WHERE "operation_id" = 'approval.decide'`,
)
.get().count,
0,
);
});
@@ -0,0 +1,574 @@
const assert = require('node:assert/strict');
const { generateKeyPairSync } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
LocalPluginPackagePublisherTrustConfigurationError,
LocalPluginPackagePublisherTrustConflictError,
assertLocalPluginPackagePublisherKeyPublicationAllowed,
confirmLocalPluginPackagePublisherKeyRevocation,
inspectLocalPluginPackagePublisherTrust,
localPluginPackagePublisherKeyRevocationImpactDigest,
publishLocalPluginPackagePublisherTrust,
proposeLocalPluginPackagePublisherKeyRevocation,
retireLocalPluginPackagePublisherKey,
} = require('@qinglong/local-admin/package-publisher-trust');
function key(keyId, notBeforeMs = 0, notAfterMs = 1_000) {
const { publicKey } = generateKeyPairSync('ed25519');
return {
publisher: 'packages.example.com',
keyId,
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }),
notBeforeMs,
notAfterMs,
};
}
function trust(keys) {
return {
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
keys,
};
}
function fixture(t) {
const unresolved = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-publisher-trust-'),
);
const trustRoot = fs.realpathSync(unresolved);
fs.chmodSync(trustRoot, 0o700);
t.after(() => fs.rmSync(trustRoot, { recursive: true, force: true }));
return trustRoot;
}
test('provisions and overlap-rotates one immutable trust chain', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
const second = key('release-2', 50, 2_000);
let fences = 0;
const provisioned = await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
beforePublish() {
fences += 1;
},
});
assert.equal(provisioned.status, 'published');
assert.equal(provisioned.generation, 1);
assert.equal(
fs.statSync(path.join(trustRoot, 'current.json')).mode & 0o777,
0o600,
);
assert.deepEqual(
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
{
generation: 1,
keyCount: 1,
activeKeyCount: 1,
snapshotCount: 1,
retirementCount: 0,
pendingRetirementCount: 0,
revocationCount: 0,
pendingRevocationCount: 0,
quarantinedLockCount: 0,
recoveryRequired: false,
pendingGeneration: null,
pendingMutationId: null,
unresolvedTransactions: 0,
trustDigest: provisioned.trustDigest,
},
);
assert.equal(
(
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
beforePublish() {
fences += 1;
},
})
).status,
'existing',
);
const rotated = await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'rotate',
expectedGeneration: 1,
mutationId: 'trust-rotate-v2',
occurredAtMs: 100,
trust: trust([second, first]),
beforePublish() {
fences += 1;
},
});
assert.equal(rotated.status, 'published');
assert.equal(rotated.generation, 2);
assert.equal(rotated.keyCount, 2);
assert.equal(fences, 3);
assert.deepEqual(
JSON.parse(
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
).keys.map((item) => item.keyId),
['release-1', 'release-2'],
);
await assert.rejects(
publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'rotate',
expectedGeneration: 2,
mutationId: 'trust-remove-v3',
occurredAtMs: 100,
trust: trust([second]),
}),
LocalPluginPackagePublisherTrustConfigurationError,
);
});
test('exact replay promotes a snapshot left durable before current', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
const command = {
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-crash-v1',
occurredAtMs: 100,
trust: trust([first]),
};
await assert.rejects(
publishLocalPluginPackagePublisherTrust({
...command,
afterSnapshotPublished() {
throw new Error('simulated current promotion failure');
},
}),
/simulated current promotion failure/,
);
assert.equal(fs.existsSync(path.join(trustRoot, 'current.json')), false);
assert.deepEqual(
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
{
generation: 0,
keyCount: 0,
activeKeyCount: 0,
snapshotCount: 1,
retirementCount: 0,
pendingRetirementCount: 0,
revocationCount: 0,
pendingRevocationCount: 0,
quarantinedLockCount: 0,
recoveryRequired: true,
pendingGeneration: 1,
pendingMutationId: 'trust-crash-v1',
unresolvedTransactions: 0,
trustDigest: null,
},
);
const recovered = await publishLocalPluginPackagePublisherTrust(command);
assert.equal(recovered.status, 'recovered');
assert.equal(recovered.generation, 1);
assert.equal(fs.existsSync(path.join(trustRoot, 'current.json')), true);
});
test('rejects broad roots, unknown files and non-overlap rotation', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
});
fs.writeFileSync(path.join(trustRoot, 'unknown'), '', { mode: 0o600 });
assert.throws(
() =>
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
/unknown entries/,
);
fs.unlinkSync(path.join(trustRoot, 'unknown'));
const overflow = Array.from({ length: 33 }, (_, index) =>
path.join(
trustRoot,
`retirement-${index.toString(16).padStart(64, '0')}.json`,
),
);
for (const filePath of overflow) {
fs.writeFileSync(filePath, '', { mode: 0o600 });
}
assert.throws(
() =>
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
/unbounded or unknown entries/,
);
for (const filePath of overflow) fs.unlinkSync(filePath);
const revocationOverflow = Array.from({ length: 33 }, (_, index) =>
path.join(
trustRoot,
`revocation-${index.toString(16).padStart(64, '0')}.json`,
),
);
for (const filePath of revocationOverflow) {
fs.writeFileSync(filePath, '', { mode: 0o600 });
}
assert.throws(
() =>
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
/unbounded or unknown entries/,
);
for (const filePath of revocationOverflow) fs.unlinkSync(filePath);
fs.chmodSync(trustRoot, 0o755);
assert.throws(
() =>
inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
}),
/owner-only/,
);
});
test('retires only an unreferenced key and exact replay recovers durable evidence', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
const second = key('release-2');
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
});
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'rotate',
expectedGeneration: 1,
mutationId: 'trust-rotate-v2',
occurredAtMs: 100,
trust: trust([first, second]),
});
const command = {
trustRoot,
expectedGeneration: 2,
mutationId: 'trust-retire-v3',
occurredAtMs: 100,
publisher: first.publisher,
keyId: first.keyId,
proveRetirement() {
return {
catalogEntryCount: 0,
bundleCount: 0,
matchingEntryCount: 0,
unresolvedTransactions: 0,
};
},
};
await assert.rejects(
retireLocalPluginPackagePublisherKey({
...command,
afterReceiptPublished() {
throw new Error('simulated retirement snapshot failure');
},
}),
/simulated retirement snapshot failure/,
);
assert.throws(
() =>
assertLocalPluginPackagePublisherKeyPublicationAllowed({
trustRoot,
publisher: first.publisher,
keyId: first.keyId,
}),
LocalPluginPackagePublisherTrustConflictError,
);
assert.doesNotThrow(() =>
assertLocalPluginPackagePublisherKeyPublicationAllowed({
trustRoot,
publisher: second.publisher,
keyId: second.keyId,
}),
);
const pending = inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 100,
});
assert.equal(pending.recoveryRequired, true);
assert.equal(pending.pendingRetirementCount, 1);
assert.equal(pending.retirementCount, 0);
const recovered = await retireLocalPluginPackagePublisherKey(command);
assert.equal(recovered.status, 'recovered');
assert.equal(recovered.generation, 3);
assert.equal(recovered.keyCount, 1);
assert.equal(
(await retireLocalPluginPackagePublisherKey(command)).status,
'existing',
);
assert.deepEqual(
JSON.parse(
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
).keys.map((item) => item.keyId),
['release-2'],
);
});
test('retirement intent blocks publication while catalog coverage remains', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
const second = key('release-2');
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
});
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'rotate',
expectedGeneration: 1,
mutationId: 'trust-rotate-v2',
occurredAtMs: 100,
trust: trust([first, second]),
});
const command = {
trustRoot,
expectedGeneration: 2,
mutationId: 'trust-retire-blocked-v3',
occurredAtMs: 100,
publisher: first.publisher,
keyId: first.keyId,
};
await assert.rejects(
retireLocalPluginPackagePublisherKey({
...command,
proveRetirement() {
return {
catalogEntryCount: 1,
bundleCount: 1,
matchingEntryCount: 1,
unresolvedTransactions: 0,
};
},
}),
/still block retirement/,
);
assert.throws(
() =>
assertLocalPluginPackagePublisherKeyPublicationAllowed({
trustRoot,
publisher: first.publisher,
keyId: first.keyId,
}),
/blocked by a durable lifecycle mutation/,
);
await assert.rejects(
retireLocalPluginPackagePublisherKey({
...command,
proveRetirement() {
return {
catalogEntryCount: 0,
bundleCount: 0,
matchingEntryCount: 0,
unresolvedTransactions: 1,
};
},
}),
/still block retirement/,
);
const recovered = await retireLocalPluginPackagePublisherKey({
...command,
proveRetirement() {
return {
catalogEntryCount: 0,
bundleCount: 0,
matchingEntryCount: 0,
unresolvedTransactions: 0,
};
},
});
assert.equal(recovered.status, 'recovered');
});
test('blocks a compromised signer at proposal and requires dual-control confirmation', async (t) => {
const trustRoot = fixture(t);
const first = key('release-1');
const second = key('release-2');
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'provision',
expectedGeneration: 0,
mutationId: 'trust-provision-v1',
occurredAtMs: 100,
trust: trust([first]),
});
await publishLocalPluginPackagePublisherTrust({
trustRoot,
mode: 'rotate',
expectedGeneration: 1,
mutationId: 'trust-rotate-v2',
occurredAtMs: 100,
trust: trust([first, second]),
});
const impactedLockDigests = ['a'.repeat(64)];
const impact = {
catalogEntryCount: 1,
bundleCount: 1,
matchingEntryCount: 1,
unresolvedTransactions: 1,
impactedLockDigests,
impactDigest: localPluginPackagePublisherKeyRevocationImpactDigest({
publisher: first.publisher,
keyId: first.keyId,
catalogEntryCount: 1,
bundleCount: 1,
matchingEntryCount: 1,
unresolvedTransactions: 1,
impactedLockDigests,
}),
};
const proposalCommand = {
trustRoot,
expectedGeneration: 2,
mutationId: 'trust-revoke-v3',
occurredAtMs: 200,
publisher: first.publisher,
keyId: first.keyId,
proposerSubjectId: 'owner-a',
impact,
};
const proposed = await proposeLocalPluginPackagePublisherKeyRevocation(
proposalCommand,
);
assert.equal(proposed.status, 'proposed');
assert.equal(proposed.runtimeAction, 'stop_required');
assert.equal(
(await proposeLocalPluginPackagePublisherKeyRevocation(proposalCommand))
.status,
'existing',
);
assert.throws(
() =>
assertLocalPluginPackagePublisherKeyPublicationAllowed({
trustRoot,
publisher: first.publisher,
keyId: first.keyId,
}),
/blocked by a durable lifecycle mutation/,
);
const pending = inspectLocalPluginPackagePublisherTrust({
trustRoot,
observedAtMs: 200,
});
assert.equal(pending.pendingRevocationCount, 1);
assert.equal(pending.quarantinedLockCount, 1);
assert.equal(pending.recoveryRequired, true);
await assert.rejects(
confirmLocalPluginPackagePublisherKeyRevocation({
trustRoot,
expectedGeneration: 2,
mutationId: 'trust-revoke-v3',
confirmedAtMs: 300,
publisher: first.publisher,
keyId: first.keyId,
proposerSubjectId: 'owner-a',
confirmerSubjectId: 'owner-a',
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
expectedImpactDigest: impact.impactDigest,
confirmAuthorization() {},
}),
/distinct Owner/,
);
let authorizationFences = 0;
const confirmation = {
trustRoot,
expectedGeneration: 2,
mutationId: 'trust-revoke-v3',
confirmedAtMs: 300,
publisher: first.publisher,
keyId: first.keyId,
proposerSubjectId: 'owner-a',
confirmerSubjectId: 'owner-b',
authorizationMode: 'dual_control',
reasonCode: 'confirmed_key_compromise',
expectedImpactDigest: impact.impactDigest,
confirmAuthorization() {
authorizationFences += 1;
},
};
await assert.rejects(
confirmLocalPluginPackagePublisherKeyRevocation({
...confirmation,
afterSnapshotPublished() {
throw new Error('simulated revocation promotion failure');
},
}),
/simulated revocation promotion failure/,
);
const recovered = await confirmLocalPluginPackagePublisherKeyRevocation(
confirmation,
);
assert.equal(recovered.status, 'recovered');
assert.equal(recovered.generation, 3);
assert.equal(recovered.keyCount, 1);
assert.equal(recovered.quarantinedLockCount, 1);
assert.equal(recovered.runtimeAction, 'restart_required');
assert.equal(
(await confirmLocalPluginPackagePublisherKeyRevocation(confirmation))
.status,
'existing',
);
assert.equal(authorizationFences, 3);
assert.deepEqual(
JSON.parse(
fs.readFileSync(path.join(trustRoot, 'current.json'), 'utf8'),
).keys.map((item) => item.keyId),
['release-2'],
);
assert.throws(
() =>
assertLocalPluginPackagePublisherKeyPublicationAllowed({
trustRoot,
publisher: first.publisher,
keyId: first.keyId,
}),
/blocked by a durable lifecycle mutation/,
);
});
@@ -0,0 +1,205 @@
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
pluginPackageContentTreeDigest,
} = require('@qinglong/runtime-core/plugin-package-bundle');
const {
createPluginPackageResourceGenerationFromReferences,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const {
InvalidLocalPluginPackageResourceSourceError,
LocalPluginPackageResourceByteSource,
} = require('../dist/plugin-package/pluginPackageResourceMaterialization');
const LOCK_DIGEST = 'a'.repeat(64);
function digest(value) {
return createHash('sha256').update(value).digest('hex');
}
function blobName(index, entryPath) {
return `${String(index).padStart(4, '0')}-${digest(entryPath)}.blob`;
}
async function stageFixture() {
const root = await fs.realpath(
await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-resource-source-')),
);
await fs.chmod(root, 0o700);
const stage = path.join(root, LOCK_DIGEST);
const blobs = path.join(stage, 'blobs');
await fs.mkdir(stage, { mode: 0o700 });
await fs.mkdir(blobs, { mode: 0o700 });
const materials = new Map([
['package.json', Buffer.from('{"apiVersion":"qinglong.io/v1alpha1"}')],
[
'tasks/collect.json',
Buffer.from(
JSON.stringify({
schema: 'qinglong/plugin-package-task-resource@v1',
id: 'collect',
}),
),
],
]);
const entries = [...materials.entries()].map(
([entryPath, material], index) => ({
path: entryPath,
bytes: material.byteLength,
digest: digest(material),
blob: blobName(index, entryPath),
}),
);
const contentDigest = pluginPackageContentTreeDigest(
entries.slice(1).map(({ path: entryPath, bytes, digest }) => ({
path: entryPath,
bytes,
digest,
})),
);
const generation = createPluginPackageResourceGenerationFromReferences({
installationId: 'install-001',
projectId: 'project-001',
packageName: 'example-monitor',
lockDigest: LOCK_DIGEST,
generation: 1,
previousActiveLockDigest: null,
contentDigest,
resources: [{ kind: 'task', path: 'tasks/collect.json' }],
});
const inspectionEntries = entries.map(
({ path: entryPath, bytes, digest }) => ({
path: entryPath,
bytes,
digest,
}),
);
const receipt = {
schema: 'qinglong/plugin-package-stage-receipt@v1',
lockDigest: LOCK_DIGEST,
inspection: {
mediaType: 'application/vnd.qinglong.package.v1+tar',
lockDigest: LOCK_DIGEST,
packageName: generation.packageName,
packageVersion: '1.0.0',
artifactBytes: 4096,
artifactDigest: 'b'.repeat(64),
manifestDigest: entries[0].digest,
contentBytes: entries[1].bytes,
contentDigest,
entries: inspectionEntries,
signature: {
publisher: 'example.test',
keyId: 'key-001',
signatureDigest: 'c'.repeat(64),
keyNotBeforeMs: 0,
keyNotAfterMs: 10_000,
verifiedAtMs: 100,
},
},
entries,
};
for (const [index, entry] of entries.entries()) {
const file = path.join(blobs, entry.blob);
await fs.writeFile(file, materials.get(entry.path), { mode: 0o600 });
await fs.chmod(file, 0o600);
assert.equal(index < 10_000, true);
}
const receiptPath = path.join(stage, 'receipt.json');
await fs.writeFile(receiptPath, JSON.stringify(receipt), { mode: 0o600 });
await fs.chmod(receiptPath, 0o600);
return { root, stage, blobs, materials, entries, generation };
}
async function cleanup(root) {
await fs.rm(root, { recursive: true, force: true });
}
test('opens one private stage session and verifies each exact blob on demand', async () => {
const fixture = await stageFixture();
try {
const source = new LocalPluginPackageResourceByteSource({
stagingRoot: fixture.root,
});
const reader = await source.open(fixture.generation);
assert.deepEqual(
await reader.read('package.json', 64 * 1024),
fixture.materials.get('package.json'),
);
assert.deepEqual(
await reader.read('tasks/collect.json', 1024 * 1024),
fixture.materials.get('tasks/collect.json'),
);
await assert.rejects(
reader.read('tasks/collect.json', 1024 * 1024),
/duplicated/,
);
await reader.close();
await assert.rejects(reader.read('package.json', 64 * 1024), /closed/);
} finally {
await cleanup(fixture.root);
}
});
test('fails closed on blob tampering, extras and an over-tight caller bound', async () => {
const tampered = await stageFixture();
try {
const source = new LocalPluginPackageResourceByteSource({
stagingRoot: tampered.root,
});
const reader = await source.open(tampered.generation);
await fs.writeFile(
path.join(tampered.blobs, tampered.entries[1].blob),
Buffer.alloc(tampered.entries[1].bytes, 0x78),
);
await assert.rejects(
reader.read('tasks/collect.json', 1024 * 1024),
InvalidLocalPluginPackageResourceSourceError,
);
} finally {
await cleanup(tampered.root);
}
const extra = await stageFixture();
try {
await fs.writeFile(path.join(extra.blobs, 'extra'), 'x', { mode: 0o600 });
const source = new LocalPluginPackageResourceByteSource({
stagingRoot: extra.root,
});
await assert.rejects(
source.open(extra.generation),
/incomplete or contains extras/,
);
} finally {
await cleanup(extra.root);
}
const bounded = await stageFixture();
try {
const source = new LocalPluginPackageResourceByteSource({
stagingRoot: bounded.root,
});
const reader = await source.open(bounded.generation);
await assert.rejects(
reader.read('tasks/collect.json', 1),
/exceeds its requested bound/,
);
} finally {
await cleanup(bounded.root);
}
});
test('publishes the local adapter only through its explicit subpath', () => {
assert.equal(require('..').LocalPluginPackageResourceByteSource, undefined);
assert.equal(
require('@qinglong/local-admin/package-resource-materialization')
.LocalPluginPackageResourceByteSource,
LocalPluginPackageResourceByteSource,
);
});
@@ -0,0 +1,402 @@
const assert = require('node:assert/strict');
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
const {
chmod,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rm,
symlink,
unlink,
writeFile,
} = require('node:fs/promises');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { test } = require('node:test');
const {
PLUGIN_PACKAGE_SIGNATURE_SCHEMA,
PluginPackagePublisherTrustRegistry,
pluginPackageContentTreeDigest,
pluginPackagePublisherSignaturePayload,
} = require('@qinglong/runtime-core/plugin-package-bundle');
const {
createPluginPackageLock,
pluginPackageInstallActionDigest,
pluginPackageInstallPlanDigest,
serializePluginPackageManifest,
} = require('@qinglong/runtime-core/plugin-package-install');
const {
PLUGIN_PACKAGE_API_VERSION,
PLUGIN_PACKAGE_KIND,
planPluginPackageInstall,
} = require('@qinglong/runtime-core/plugin-package');
const {
InvalidPluginPackageStagingError,
PluginPackageStagingUnavailableError,
stagePluginPackageFromFile,
} = require('../dist/plugin-package/pluginPackageStaging');
const PUBLISHER = 'packages.example.com';
const KEY_ID = 'release-2026';
function manifest() {
return {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.2.0',
description: 'Collects one report',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [{ name: 'python', version: '>=3.10.0 <4.0.0' }],
resources: {
memory: { recommended: '32Mi' },
disk: { install: '8Mi', working: '32Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: [],
},
contents: {
tasks: ['tasks/collect.yaml'],
workflows: [],
prompts: [],
tools: [],
},
},
};
}
function environment() {
return {
qinglongVersion: '3.0.0-alpha.0',
architecture: 'arm64',
deploymentProfile: 'edge',
runtimes: [{ name: 'python', version: '3.12.4' }],
availableMemoryBytes: 256 * 1024 * 1024,
availableDiskBytes: 512 * 1024 * 1024,
};
}
function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
function octal(value, bytes) {
return Buffer.from(`${value.toString(8).padStart(bytes - 1, '0')}\0`);
}
function tarHeader(path, bytes) {
const header = Buffer.alloc(512);
Buffer.from(path).copy(header, 0);
Buffer.from('0000644\0').copy(header, 100);
Buffer.from('0000000\0').copy(header, 108);
Buffer.from('0000000\0').copy(header, 116);
octal(bytes, 12).copy(header, 124);
Buffer.from('00000000000\0').copy(header, 136);
header.fill(0x20, 148, 156);
Buffer.from('0').copy(header, 156);
Buffer.from('ustar\0').copy(header, 257);
Buffer.from('00').copy(header, 263);
const checksum = header.reduce((total, byte) => total + byte, 0);
Buffer.from(`${checksum.toString(8).padStart(6, '0')}\0 `).copy(header, 148);
return header;
}
function tar(entries) {
const parts = [];
for (const entry of entries) {
parts.push(tarHeader(entry.path, entry.body.byteLength), entry.body);
const padding = (512 - (entry.body.byteLength % 512)) % 512;
if (padding > 0) parts.push(Buffer.alloc(padding));
}
parts.push(Buffer.alloc(1024));
return Buffer.concat(parts);
}
function packageFixture() {
const packageManifest = manifest();
const manifestBody = Buffer.from(
serializePluginPackageManifest(packageManifest),
);
const taskBody = Buffer.from(
'apiVersion: qinglong.io/v1\nkind: Task\nmetadata:\n name: collect\n',
);
const artifact = tar([
{ path: 'package.json', body: manifestBody },
{ path: 'tasks/collect.yaml', body: taskBody },
]);
const installEnvironment = environment();
const plan = planPluginPackageInstall(packageManifest, installEnvironment);
const artifactDigest = sha256(artifact);
const source = {
kind: 'offline',
locator: `offline:sha256:${artifactDigest}`,
artifactDigest,
artifactBytes: artifact.byteLength,
contentDigest: pluginPackageContentTreeDigest([
{
path: 'tasks/collect.yaml',
bytes: taskBody.byteLength,
digest: sha256(taskBody),
},
]),
};
const actionInput = {
lockId: 'lock-stage-001',
projectId: 'project-001',
manifest: packageManifest,
plan,
environment: installEnvironment,
source,
architecture: 'arm64',
deploymentProfile: 'edge',
targetGeneration: 1,
};
const lock = createPluginPackageLock({
...actionInput,
approval: {
requestId: 'approval-001',
requestVersion: 1,
dispatchId: 'dispatch-001',
actionDigest: pluginPackageInstallActionDigest(actionInput),
previewDigest: pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'owner-001' },
approvedAtMs: 100,
expiresAtMs: 1_000,
fence: { projectVersion: 3, bindingVersion: 4 },
},
createdAtMs: 200,
});
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
const trust = new PluginPackagePublisherTrustRegistry([
{
publisher: PUBLISHER,
keyId: KEY_ID,
publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }),
notBeforeMs: 100,
notAfterMs: 1_000,
},
]);
const signature = {
schema: PLUGIN_PACKAGE_SIGNATURE_SCHEMA,
publisher: PUBLISHER,
keyId: KEY_ID,
signature: sign(
null,
pluginPackagePublisherSignaturePayload(lock, PUBLISHER, KEY_ID),
privateKey,
).toString('base64url'),
};
return { artifact, lock, packageManifest, signature, trust };
}
async function filesystemFixture(t) {
const unresolved = await mkdtemp(join(tmpdir(), 'ql3-package-stage-'));
const base = await realpath(unresolved);
const stagingRoot = join(base, 'staging');
const bundlePath = join(base, 'package.qlpkg');
await mkdir(stagingRoot, { mode: 0o700 });
await chmod(stagingRoot, 0o700);
t.after(() => rm(base, { recursive: true, force: true }));
return { base, bundlePath, stagingRoot };
}
async function writePrivateBundle(bundlePath, artifact) {
await writeFile(bundlePath, artifact, { mode: 0o600 });
await chmod(bundlePath, 0o600);
}
function stageOptions(filesystem, value, overrides = {}) {
return {
bundlePath: filesystem.bundlePath,
stagingRoot: filesystem.stagingRoot,
lock: value.lock,
manifest: value.packageManifest,
signature: value.signature,
trust: value.trust,
observedAtMs: 500,
...overrides,
};
}
test('stages a verified bundle as private opaque blobs and one receipt', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
const staged = await stagePluginPackageFromFile(
stageOptions(filesystem, value),
);
assert.equal(staged.status, 'staged');
assert.equal(staged.stageRef, `local-stage:${value.lock.lockDigest}`);
assert.equal(
staged.directory,
join(filesystem.stagingRoot, value.lock.lockDigest),
);
assert.match(staged.receiptDigest, /^[0-9a-f]{64}$/);
assert.equal((await lstat(staged.directory)).mode & 0o777, 0o700);
const rootEntries = await readdir(staged.directory);
assert.deepEqual(rootEntries.sort(), ['blobs', 'receipt.json']);
const blobNames = (await readdir(join(staged.directory, 'blobs'))).sort();
assert.equal(blobNames.length, 2);
assert.equal(
blobNames.every((name) => /^[0-9]{4}-[0-9a-f]{64}\.blob$/.test(name)),
true,
);
assert.equal(rootEntries.includes('tasks'), false);
assert.equal(
(await lstat(join(staged.directory, 'receipt.json'))).mode & 0o777,
0o600,
);
for (const blob of blobNames) {
assert.equal(
(await lstat(join(staged.directory, 'blobs', blob))).mode & 0o777,
0o600,
);
}
});
test('replays one exact stage without reopening the deleted source bundle', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
const first = await stagePluginPackageFromFile(
stageOptions(filesystem, value),
);
await unlink(filesystem.bundlePath);
const replay = await stagePluginPackageFromFile(
stageOptions(filesystem, value),
);
assert.equal(replay.status, 'existing');
assert.equal(replay.receiptDigest, first.receiptDigest);
assert.deepEqual(replay.inspection, first.inspection);
});
test('fails closed when an existing opaque blob or receipt is changed', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
const staged = await stagePluginPackageFromFile(
stageOptions(filesystem, value),
);
const blobDirectory = join(staged.directory, 'blobs');
const [blob] = await readdir(blobDirectory);
await writeFile(join(blobDirectory, blob), Buffer.from('tampered'));
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
});
test('rejects a canonical-looking receipt detached from the locked artifact', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
const staged = await stagePluginPackageFromFile(
stageOptions(filesystem, value),
);
const receiptPath = join(staged.directory, 'receipt.json');
const receipt = JSON.parse(await readFile(receiptPath, 'utf8'));
receipt.inspection.artifactDigest = 'f'.repeat(64);
await writeFile(receiptPath, `${JSON.stringify(receipt)}\n`, { mode: 0o600 });
await chmod(receiptPath, 0o600);
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
});
test('rejects broad bundle permissions and final-component symbolic links', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
await chmod(filesystem.bundlePath, 0o644);
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
assert.deepEqual(await readdir(filesystem.stagingRoot), []);
const target = join(filesystem.base, 'target.qlpkg');
await writePrivateBundle(target, value.artifact);
await unlink(filesystem.bundlePath);
await symlink(target, filesystem.bundlePath);
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
assert.deepEqual(await readdir(filesystem.stagingRoot), []);
});
test('does not create a transaction for an untrusted signature', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
await assert.rejects(
stagePluginPackageFromFile(
stageOptions(filesystem, value, {
observedAtMs: 1_000,
}),
),
/publisher signature is not trusted/,
);
assert.deepEqual(await readdir(filesystem.stagingRoot), []);
});
test('removes its bounded temporary transaction after bundle failure', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
const tampered = Buffer.from(value.artifact);
tampered[512] ^= 1;
await writePrivateBundle(filesystem.bundlePath, tampered);
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
PluginPackageStagingUnavailableError,
);
assert.deepEqual(await readdir(filesystem.stagingRoot), []);
});
test('fails closed on stale transactions, unknown root entries and broad roots', async (t) => {
const filesystem = await filesystemFixture(t);
const value = packageFixture();
await writePrivateBundle(filesystem.bundlePath, value.artifact);
const stale = join(filesystem.stagingRoot, `.qlpkg-${'a'.repeat(32)}`);
await mkdir(stale, { mode: 0o700 });
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
await rm(stale, { recursive: true });
await writeFile(join(filesystem.stagingRoot, 'unknown'), '');
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
await unlink(join(filesystem.stagingRoot, 'unknown'));
await chmod(filesystem.stagingRoot, 0o755);
await assert.rejects(
stagePluginPackageFromFile(stageOptions(filesystem, value)),
InvalidPluginPackageStagingError,
);
});
test('publishes staging authority only through the explicit local-admin subpath', () => {
const root = require('..');
const subpath = require('@qinglong/local-admin/package-staging');
assert.equal(root.stagePluginPackageFromFile, undefined);
assert.equal(subpath.stagePluginPackageFromFile, stagePluginPackageFromFile);
});
@@ -0,0 +1,325 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { test } = require('node:test');
const {
LocalSecretAuthorizationFenceConflictError,
} = require('@qinglong/runtime-core/local-secret-administration');
const {
LocalSecretMutationConflictError,
} = require('@qinglong/runtime-core/local-secret');
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
const {
openLocalSqliteRuntimeDatabase,
} = require('@qinglong/local-sqlite/runtime');
const {
LocalSecretKeyringFileProvider,
provisionLocalSecretKeyring,
} = require('@qinglong/local-secret');
const {
LocalSecretAdministrationAuthenticationError,
LocalSecretAdministrationAuthorizationError,
LocalSecretAdministrationUnavailableError,
createLocalSecretAdministrationService,
} = require('@qinglong/local-admin/secret-administration');
const NOW = 1_760_000_000_000;
function fixture(t) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-admin-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return {
directory,
profile: 'edge',
databasePath: path.join(directory, 'qinglong3.sqlite'),
keyringPath: path.join(directory, 'secret-keyring.json'),
};
}
function principal(overrides = {}) {
return {
subject: { type: 'user', id: 'user-owner' },
authenticationId: 'local-console-1',
authenticatedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
assurance: 'local_console',
...overrides,
};
}
function request(mutationId, overrides = {}) {
return {
projectId: 'default',
name: 'TOKEN',
plaintext: 'never-persist-this-plaintext',
mutationId,
requestId: `request-${mutationId.slice(-4)}`,
expectedCurrentVersion: 0,
principal: principal(),
...overrides,
};
}
async function openStore(t) {
const value = fixture(t);
await migrateLocalSqlitePath(value);
await provisionLocalSecretKeyring(value.keyringPath);
const runtime = await openLocalSqliteRuntimeDatabase(value);
t.after(() => runtime.close());
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
return { ...value, runtime, keys };
}
async function bind(runtime, role, version = 1, state = 'active') {
return runtime.projectPolicy.append({
expectedCurrentVersion: version - 1,
binding: {
projectId: 'default',
subject: { type: 'user', id: 'user-owner' },
version,
state,
...(state === 'active' ? { role } : {}),
mutationId: `binding-${version}`,
changedBy: { type: 'system', id: 'owner-bootstrap' },
createdAtMs: NOW + version,
},
});
}
function service(value, overrides = {}) {
return createLocalSecretAdministrationService(
overrides.projectPolicy ?? value.runtime.projectPolicy,
overrides.mutations ?? value.runtime.localSecretAdministration,
overrides.audit ?? value.runtime.securityAudit,
overrides.keys ?? value.keys,
{ now: () => NOW, nonceFactory: () => Buffer.alloc(12, 7) },
);
}
test('owner writes one encrypted envelope and allowed audit atomically', async (t) => {
const value = await openStore(t);
assert.equal(
(await value.runtime.projectPolicy.resolve('default', principal().subject))
.binding,
undefined,
);
await bind(value.runtime, 'owner');
const mutationId = '00000000-0000-4000-8000-000000000001';
const result = await service(value).put(request(mutationId));
assert.equal(result.status, 'inserted');
assert.equal(result.version, 1);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
const secret = database
.prepare(
`SELECT version, ciphertext FROM "QingLong3LocalSecretEnvelopes"
WHERE mutation_id = ?`,
)
.get(mutationId);
const audit = database
.prepare(
`SELECT operation_id, outcome, subject_id, fence_project_version,
fence_binding_version
FROM "QingLong3SecurityAuditEvents" WHERE event_id = ?`,
)
.get(mutationId);
assert.equal(secret.version, 1);
assert.equal(
Buffer.from(secret.ciphertext).includes(
Buffer.from('never-persist-this-plaintext'),
),
false,
);
assert.deepEqual(
{ ...audit },
{
operation_id: 'secret.create',
outcome: 'allowed',
subject_id: 'user-owner',
fence_project_version: 1,
fence_binding_version: 1,
},
);
} finally {
database.close();
}
});
test('denial is audited before key access and ownerless default stays closed', async (t) => {
const value = await openStore(t);
let keyReads = 0;
const keys = {
async active() {
keyReads += 1;
throw new Error('must not run');
},
async resolve() {
keyReads += 1;
throw new Error('must not run');
},
};
const mutationId = '00000000-0000-4000-8000-000000000002';
await assert.rejects(
service(value, { keys }).put(request(mutationId)),
LocalSecretAdministrationAuthorizationError,
);
assert.equal(keyReads, 0);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
`SELECT outcome FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get(mutationId).outcome,
'denied',
);
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
)
.get().count,
0,
);
} finally {
database.close();
}
});
test('weak authentication is audited without Policy or key access', async (t) => {
const value = await openStore(t);
let policyReads = 0;
let keyReads = 0;
const projectPolicy = {
async resolve() {
policyReads += 1;
throw new Error('must not run');
},
append: value.runtime.projectPolicy.append,
};
const keys = {
async active() {
keyReads += 1;
throw new Error('must not run');
},
async resolve() {
keyReads += 1;
throw new Error('must not run');
},
};
const mutationId = '00000000-0000-4000-8000-000000000003';
await assert.rejects(
service(value, { projectPolicy, keys }).put(
request(mutationId, {
principal: principal({ assurance: 'single_factor' }),
}),
),
LocalSecretAdministrationAuthenticationError,
);
assert.equal(policyReads, 0);
assert.equal(keyReads, 0);
});
test('revocation after Policy decision is fenced inside the write transaction', async (t) => {
const value = await openStore(t);
await bind(value.runtime, 'admin');
let revoked = false;
const projectPolicy = {
async resolve(projectId, subject) {
const snapshot = await value.runtime.projectPolicy.resolve(
projectId,
subject,
);
if (!revoked) {
revoked = true;
await bind(value.runtime, undefined, 2, 'revoked');
}
return snapshot;
},
append: (...args) => value.runtime.projectPolicy.append(...args),
};
const mutationId = '00000000-0000-4000-8000-000000000004';
await assert.rejects(
service(value, { projectPolicy }).put(request(mutationId)),
LocalSecretAuthorizationFenceConflictError,
);
const database = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
database
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
)
.get().count,
0,
);
assert.equal(
database
.prepare(
`SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents"
WHERE event_id = ?`,
)
.get(mutationId).count,
0,
);
} finally {
database.close();
}
});
test('audit insertion failure rolls the encrypted envelope back', async (t) => {
const value = await openStore(t);
await bind(value.runtime, 'owner');
const database = new DatabaseSync(value.databasePath);
database.exec(`
CREATE TRIGGER reject_secret_audit
BEFORE INSERT ON "QingLong3SecurityAuditEvents"
WHEN NEW.operation_id IN ('secret.create', 'secret.rotate')
BEGIN
SELECT RAISE(ABORT, 'audit unavailable');
END
`);
database.close();
await assert.rejects(
service(value).put(request('00000000-0000-4000-8000-000000000005')),
LocalSecretAdministrationUnavailableError,
);
const check = new DatabaseSync(value.databasePath, { readOnly: true });
try {
assert.equal(
check
.prepare(
'SELECT COUNT(*) AS count FROM "QingLong3LocalSecretEnvelopes"',
)
.get().count,
0,
);
assert.equal(
check
.prepare('SELECT COUNT(*) AS count FROM "QingLong3SecurityAuditEvents"')
.get().count,
0,
);
} finally {
check.close();
}
});
test('semantic replay returns no plaintext and conflicting replay fails closed', async (t) => {
const value = await openStore(t);
await bind(value.runtime, 'owner');
const mutationId = '00000000-0000-4000-8000-000000000006';
const admin = service(value);
assert.equal((await admin.put(request(mutationId))).status, 'inserted');
assert.equal((await admin.put(request(mutationId))).status, 'existing');
await assert.rejects(
admin.put(request(mutationId, { plaintext: 'different' })),
LocalSecretMutationConflictError,
);
});
@@ -0,0 +1,196 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalSecurityAuditQueryAuthorizationError,
LocalSecurityAuditQueryConfigurationError,
createLocalSecurityAuditQueryService,
} = require('@qinglong/local-admin/security-audit-query');
const {
LocalSecurityAuditQueryAuthorizationFenceConflictError,
} = require('@qinglong/runtime-core/local-security-audit-query');
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-user' }),
authenticationId: 'local_security_audit:test',
authenticatedAtMs: 1_000,
expiresAtMs: 61_000,
assurance: 'local_console',
});
function projectPolicy(role = 'owner') {
return {
async resolve(projectId, subject) {
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 4,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: {
projectId,
subject,
version: 7,
state: 'active',
role,
mutationId: 'owner-binding',
changedBy: subject,
createdAtMs: 0,
},
};
},
async append() {
throw new Error('not used');
},
};
}
function request(overrides = {}) {
return {
authorityProjectId: 'default',
query: {
limit: 2,
before: {
occurredAtMs: 10_000,
eventId: '91000000-0000-4000-8000-000000000001',
},
filter: {
projectId: 'project-alpha',
subject: { type: 'agent', id: 'planner' },
outcome: 'denied',
},
},
auditEventId: '92000000-0000-4000-8000-000000000001',
requestId: 'audit-query-1',
principal: PRINCIPAL,
...overrides,
};
}
test('authorizes an instance Owner and preserves bounded filter/cursor semantics', async () => {
let command;
const repository = {
async listAuthorized(value) {
command = value;
return {
records: [
{
eventId: '93000000-0000-4000-8000-000000000001',
requestId: 'denied-request',
operationId: 'tool.invoke',
projectId: 'project-alpha',
subject: { type: 'agent', id: 'planner' },
authenticationId: 'private-authentication-id',
outcome: 'denied',
reasons: ['permission_missing'],
fence: { projectVersion: 2, bindingVersion: 3 },
occurredAtMs: 9_999,
},
],
nextCursor: null,
audit: value.audit,
};
},
async record() {
throw new Error('not used');
},
};
const service = createLocalSecurityAuditQueryService(
projectPolicy(),
repository,
{ now: () => 2_000 },
);
const result = await service.list(request());
assert.equal(result.records.length, 1);
assert.deepEqual(command.query, request().query);
assert.deepEqual(command.authorization, {
authorityProjectId: 'default',
actor: PRINCIPAL.subject,
fence: { projectVersion: 4, bindingVersion: 7 },
});
assert.deepEqual(command.audit, {
eventId: '92000000-0000-4000-8000-000000000001',
requestId: 'audit-query-1',
operationId: 'security.audit.list',
projectId: 'default',
subject: PRINCIPAL.subject,
authenticationId: PRINCIPAL.authenticationId,
outcome: 'allowed',
reasons: ['instance_authority_security_audit_query'],
fence: { projectVersion: 4, bindingVersion: 7 },
occurredAtMs: 2_000,
});
});
test('rejects pages above the Edge/Standalone local cap before repository access', async () => {
let accessed = false;
const service = createLocalSecurityAuditQueryService(
projectPolicy(),
{
async listAuthorized() {
accessed = true;
throw new Error('must not run');
},
async record() {
accessed = true;
},
},
{ now: () => 2_000 },
);
await assert.rejects(
service.list(
request({
query: { limit: 65, filter: {} },
}),
),
LocalSecurityAuditQueryConfigurationError,
);
assert.equal(accessed, false);
});
test('records and rejects a non-Owner without exposing audit rows', async () => {
const audits = [];
let listed = false;
const service = createLocalSecurityAuditQueryService(
projectPolicy('viewer'),
{
async listAuthorized() {
listed = true;
throw new Error('must not run');
},
async record(audit) {
audits.push(audit);
},
},
{ now: () => 2_000 },
);
await assert.rejects(
service.list(request({ query: { limit: 1, filter: {} } })),
LocalSecurityAuditQueryAuthorizationError,
);
assert.equal(listed, false);
assert.equal(audits.length, 1);
assert.equal(audits[0].outcome, 'denied');
assert.deepEqual(audits[0].reasons, ['permission_missing']);
});
test('preserves the final credential/policy TOCTOU fence conflict', async () => {
const service = createLocalSecurityAuditQueryService(
projectPolicy(),
{
async listAuthorized() {
throw new LocalSecurityAuditQueryAuthorizationFenceConflictError();
},
async record() {},
},
{ now: () => 2_000 },
);
await assert.rejects(
service.list(request({ query: { limit: 1, filter: {} } })),
LocalSecurityAuditQueryAuthorizationFenceConflictError,
);
});
@@ -0,0 +1,205 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
LocalSecurityAuditRetentionAuthorizationError,
LocalSecurityAuditRetentionConfigurationError,
createLocalSecurityAuditRetentionService,
} = require('@qinglong/local-admin/security-audit-retention');
const {
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
} = require('@qinglong/runtime-core/local-security-audit-retention');
const NOW = 4_000_000_000;
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-user' }),
authenticationId: 'local_security_audit:test',
authenticatedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
assurance: 'local_console',
});
function projectPolicy(role = 'owner') {
return {
async resolve(projectId, subject) {
return {
project: {
id: projectId,
name: 'Default',
slug: 'default',
status: 'active',
version: 4,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: {
projectId,
subject,
version: 7,
state: 'active',
role,
mutationId: 'owner-binding',
changedBy: subject,
createdAtMs: 0,
},
};
},
async append() {
throw new Error('not used');
},
};
}
function request(overrides = {}) {
return {
authorityProjectId: 'default',
retentionMs: MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
eligibleBeforeMs: NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
limit: 64,
mutationId: 'a1000000-0000-4000-8000-000000000001',
requestId: 'audit-compact-1',
failureAuditEventId: 'a1000000-0000-4000-8000-000000000002',
principal: PRINCIPAL,
...overrides,
};
}
test('authorizes an instance Owner and binds the exact retention command', async () => {
let command;
const repository = {
async resolveCompaction() {
return null;
},
async compactAuthorized(value) {
command = value;
return {
status: 'inserted',
record: {
mutationId: value.mutationId,
requestId: value.requestId,
authorityProjectId: value.authorization.authorityProjectId,
retentionMs: value.retentionMs,
eligibleBeforeMs: value.eligibleBeforeMs,
batchLimit: value.limit,
deletedCount: 0,
deletedPayloadBytes: 0,
first: null,
last: null,
recordsDigest: 'a'.repeat(64),
createdAtMs: value.audit.occurredAtMs,
},
audit: value.audit,
};
},
async record() {
throw new Error('not used');
},
};
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
repository,
{ now: () => NOW },
);
const result = await service.compact(request());
assert.equal(result.status, 'inserted');
assert.deepEqual(command.authorization, {
authorityProjectId: 'default',
actor: PRINCIPAL.subject,
fence: { projectVersion: 4, bindingVersion: 7 },
});
assert.deepEqual(command.audit, {
eventId: request().mutationId,
requestId: request().requestId,
operationId: 'security.audit.compact',
projectId: 'default',
subject: PRINCIPAL.subject,
authenticationId: PRINCIPAL.authenticationId,
outcome: 'allowed',
reasons: ['instance_authority_security_audit_compaction'],
fence: { projectVersion: 4, bindingVersion: 7 },
occurredAtMs: NOW,
});
});
test('rejects an unsafe retention fence and oversized batch before repository access', async () => {
let accessed = false;
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
{
async resolveCompaction() {
accessed = true;
},
async compactAuthorized() {
accessed = true;
},
async record() {
accessed = true;
},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(
request({
eligibleBeforeMs: NOW - MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS + 1,
}),
),
LocalSecurityAuditRetentionConfigurationError,
);
await assert.rejects(
service.compact(request({ limit: 513 })),
LocalSecurityAuditRetentionConfigurationError,
);
assert.equal(accessed, false);
});
test('records denial with the failure identity for a non-Owner', async () => {
const audits = [];
let compacted = false;
const service = createLocalSecurityAuditRetentionService(
projectPolicy('viewer'),
{
async resolveCompaction() {
return null;
},
async compactAuthorized() {
compacted = true;
throw new Error('must not run');
},
async record(audit) {
audits.push(audit);
},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(request()),
LocalSecurityAuditRetentionAuthorizationError,
);
assert.equal(compacted, false);
assert.equal(audits.length, 1);
assert.equal(audits[0].eventId, request().failureAuditEventId);
assert.equal(audits[0].outcome, 'denied');
assert.deepEqual(audits[0].reasons, ['permission_missing']);
});
test('preserves the final credential and authority fence conflict', async () => {
const service = createLocalSecurityAuditRetentionService(
projectPolicy(),
{
async resolveCompaction() {
return null;
},
async compactAuthorized() {
throw new LocalSecurityAuditRetentionAuthorizationFenceConflictError();
},
async record() {},
},
{ now: () => NOW },
);
await assert.rejects(
service.compact(request()),
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
);
});