mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
2171 lines
66 KiB
JavaScript
2171 lines
66 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { createHash, generateKeyPairSync, sign } = 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 {
|
|
createLocalPluginPackageCommandRunner,
|
|
runLocalPluginPackageCommandFile,
|
|
} = require('@qinglong/local-owner-cli/package-command');
|
|
const {
|
|
LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA,
|
|
runLocalPluginPackageCatalogCommandFile,
|
|
} = require('@qinglong/local-owner-cli/package-catalog-command');
|
|
const {
|
|
createLocalPluginPackagePublisherTrustCommandRunner,
|
|
runLocalPluginPackagePublisherTrustCommandFile,
|
|
} = require('@qinglong/local-owner-cli/package-publisher-trust-command');
|
|
const {
|
|
analyzeLocalPluginPackageRecoveryCatalogPublisherKey,
|
|
analyzeLocalPluginPackageRecoveryCatalogPublisherKeyImpact,
|
|
LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
} = require('@qinglong/local-admin/package-recovery-catalog');
|
|
const {
|
|
confirmLocalPluginPackagePublisherKeyRevocation,
|
|
inspectLocalPluginPackagePublisherTrust,
|
|
publishLocalPluginPackagePublisherTrust,
|
|
proposeLocalPluginPackagePublisherKeyRevocation,
|
|
retireLocalPluginPackagePublisherKey,
|
|
} = require('@qinglong/local-admin/package-publisher-trust');
|
|
const {
|
|
establishAuthenticatedLocalCommand,
|
|
} = require('@qinglong/local-owner-console/authenticated-command');
|
|
const {
|
|
provisionLocalOwnerPepperKey,
|
|
} = require('@qinglong/local-owner-console');
|
|
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
LocalSqlitePluginPackageInstallRepository,
|
|
} = require('@qinglong/local-sqlite/plugin-package-install');
|
|
const {
|
|
LocalSqlitePluginPackageSecretBindingActivationPrerequisite,
|
|
} = require('@qinglong/local-sqlite/plugin-package-secret-binding-activation-prerequisite');
|
|
const {
|
|
LocalSqlitePluginPackageAutomationPublicationRepository,
|
|
} = require('@qinglong/local-sqlite/plugin-package-automation-publication');
|
|
const {
|
|
LocalSqlitePluginPackageMaterializedRevisionRepository,
|
|
} = require('@qinglong/local-sqlite/plugin-package-materialized-revision');
|
|
const {
|
|
LocalSqliteOperationAuthority,
|
|
} = require('@qinglong/local-sqlite/operation-authority');
|
|
const {
|
|
openLocalSqliteAuthenticatedManagementDatabase,
|
|
} = require('@qinglong/local-sqlite/authenticated-management');
|
|
const {
|
|
openLocalSqlitePluginPackageManagementDatabase,
|
|
} = require('@qinglong/local-sqlite/package-management');
|
|
const {
|
|
apiCredentialSecretDigest,
|
|
formatApiCredentialToken,
|
|
} = require('@qinglong/runtime-core/api-credential-token');
|
|
const {
|
|
PLUGIN_PACKAGE_API_VERSION,
|
|
PLUGIN_PACKAGE_KIND,
|
|
planPluginPackageInstall,
|
|
} = require('@qinglong/runtime-core/plugin-package');
|
|
const {
|
|
PLUGIN_PACKAGE_SIGNATURE_SCHEMA,
|
|
pluginPackageContentTreeDigest,
|
|
pluginPackagePublisherSignaturePayload,
|
|
} = require('@qinglong/runtime-core/plugin-package-bundle');
|
|
const {
|
|
serializePluginPackageManifest,
|
|
createPluginPackageActivationReceipt,
|
|
pluginPackageActivationIntentDigest,
|
|
pluginPackageInstallCommit,
|
|
transitionPluginPackageInstall,
|
|
} = require('@qinglong/runtime-core/plugin-package-install');
|
|
const {
|
|
PluginPackageRecoveryCoordinator,
|
|
} = require('@qinglong/runtime-core/plugin-package-recovery');
|
|
const {
|
|
createInitialPluginPackageAutomationPublication,
|
|
} = require('@qinglong/runtime-core/plugin-package-automation-publication');
|
|
const {
|
|
createPluginPackageResourceGeneration,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
|
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
|
const {
|
|
materializePluginPackageResources,
|
|
} = require('@qinglong/runtime-core/plugin-package-resource-materialization');
|
|
const {
|
|
createBuiltInTaskSpecSemanticRegistry,
|
|
} = require('@qinglong/runtime-core/task-spec-semantic');
|
|
|
|
const CREDENTIAL_ID = 'package-owner';
|
|
const PEPPER_KEY_ID = 'package-owner-v1';
|
|
const PEPPER = Buffer.alloc(32, 75).toString('base64url');
|
|
const SECRET = Buffer.alloc(32, 76).toString('base64url');
|
|
const TOKEN = formatApiCredentialToken(CREDENTIAL_ID, SECRET);
|
|
|
|
function digest(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(entryPath, bytes) {
|
|
const header = Buffer.alloc(512);
|
|
Buffer.from(entryPath).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);
|
|
}
|
|
|
|
const CLI_PACKAGE_RESOURCES = Object.freeze([
|
|
Object.freeze({
|
|
reference: Object.freeze({ kind: 'task', path: 'tasks/collect.json' }),
|
|
body: Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-task-resource@v1',
|
|
id: 'collect',
|
|
name: 'Collect',
|
|
labels: { 'plugin.qinglong.io/source': 'owner-cli-contract' },
|
|
enabled: true,
|
|
kind: 'command',
|
|
spec: {
|
|
schema: 'qinglong/command@v1',
|
|
config: {
|
|
command: {
|
|
kind: 'argv',
|
|
file: '/usr/bin/printf',
|
|
args: ['collect'],
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
}),
|
|
Object.freeze({
|
|
reference: Object.freeze({
|
|
kind: 'workflow',
|
|
path: 'workflows/daily.json',
|
|
}),
|
|
body: Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-workflow-resource@v1',
|
|
id: 'daily',
|
|
name: 'Daily collection',
|
|
enabled: true,
|
|
steps: [{ id: 'collect', task: 'collect', needs: [] }],
|
|
}),
|
|
),
|
|
}),
|
|
Object.freeze({
|
|
reference: Object.freeze({
|
|
kind: 'prompt',
|
|
path: 'prompts/summary.json',
|
|
}),
|
|
body: Buffer.from(
|
|
JSON.stringify({
|
|
schema: 'qinglong/plugin-package-prompt-resource@v1',
|
|
id: 'summary',
|
|
name: 'Summary',
|
|
template: 'Summarize {{result}}',
|
|
parameters: [{ name: 'result', required: true }],
|
|
}),
|
|
),
|
|
}),
|
|
]);
|
|
|
|
function packageArtifact(manifest) {
|
|
return tar([
|
|
{
|
|
path: 'package.json',
|
|
body: Buffer.from(serializePluginPackageManifest(manifest)),
|
|
},
|
|
...CLI_PACKAGE_RESOURCES.map(({ reference, body }) => ({
|
|
path: reference.path,
|
|
body,
|
|
})).sort((left, right) => left.path.localeCompare(right.path)),
|
|
]);
|
|
}
|
|
|
|
function actionInput(secretAware = false) {
|
|
const manifest = {
|
|
apiVersion: PLUGIN_PACKAGE_API_VERSION,
|
|
kind: PLUGIN_PACKAGE_KIND,
|
|
metadata: {
|
|
name: 'cli-monitor',
|
|
displayName: 'CLI Monitor',
|
|
version: '1.0.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: secretAware ? [{ name: 'TOKEN', required: true }] : [],
|
|
tools: secretAware
|
|
? ['secret.use', 'system.command']
|
|
: ['system.command'],
|
|
},
|
|
contents: {
|
|
tasks: ['tasks/collect.json'],
|
|
workflows: ['workflows/daily.json'],
|
|
prompts: ['prompts/summary.json'],
|
|
tools: [],
|
|
},
|
|
},
|
|
};
|
|
const environment = {
|
|
qinglongVersion: '3.0.0-alpha.0',
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
runtimes: [],
|
|
availableMemoryBytes: 64 * 1024 * 1024,
|
|
availableDiskBytes: 128 * 1024 * 1024,
|
|
};
|
|
const artifact = packageArtifact(manifest);
|
|
const artifactDigest = digest(artifact);
|
|
return {
|
|
lockId: 'cli-monitor-v1',
|
|
projectId: 'default',
|
|
manifest,
|
|
plan: planPluginPackageInstall(manifest, environment),
|
|
environment,
|
|
source: {
|
|
kind: 'offline',
|
|
locator: `offline:sha256:${artifactDigest}`,
|
|
artifactDigest,
|
|
artifactBytes: artifact.byteLength,
|
|
contentDigest: pluginPackageContentTreeDigest(
|
|
CLI_PACKAGE_RESOURCES.map(({ reference, body }) => ({
|
|
path: reference.path,
|
|
bytes: body.byteLength,
|
|
digest: digest(body),
|
|
})).sort((left, right) => left.path.localeCompare(right.path)),
|
|
),
|
|
},
|
|
architecture: 'arm64',
|
|
deploymentProfile: 'edge',
|
|
targetGeneration: 1,
|
|
};
|
|
}
|
|
|
|
function upgradeActionInput(previousInput, previousLockDigest) {
|
|
const manifest = {
|
|
...previousInput.manifest,
|
|
metadata: {
|
|
...previousInput.manifest.metadata,
|
|
version: '1.1.0',
|
|
description: 'One bounded upgraded package',
|
|
},
|
|
};
|
|
const artifact = packageArtifact(manifest);
|
|
const artifactDigest = digest(artifact);
|
|
return {
|
|
...previousInput,
|
|
lockId: 'cli-monitor-v2',
|
|
manifest,
|
|
plan: planPluginPackageInstall(
|
|
manifest,
|
|
previousInput.environment,
|
|
previousInput.manifest,
|
|
),
|
|
previousManifest: previousInput.manifest,
|
|
source: {
|
|
...previousInput.source,
|
|
locator: `offline:sha256:${artifactDigest}`,
|
|
artifactDigest,
|
|
artifactBytes: artifact.byteLength,
|
|
},
|
|
targetGeneration: 2,
|
|
previousLockDigest,
|
|
};
|
|
}
|
|
|
|
async function fixture(t, owner = true) {
|
|
const deploymentRoot = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-package-command-')),
|
|
);
|
|
fs.chmodSync(deploymentRoot, 0o700);
|
|
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
|
const commandsDirectory = path.join(deploymentRoot, 'commands');
|
|
const ownerPepperKeyringDirectory = path.join(deploymentRoot, 'owner-keys');
|
|
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
|
|
fs.mkdirSync(ownerPepperKeyringDirectory, { mode: 0o700 });
|
|
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
|
|
const credentialFilePath = path.join(deploymentRoot, 'credential.json');
|
|
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
|
const summary = provisionLocalOwnerPepperKey({
|
|
keyringDirectory: ownerPepperKeyringDirectory,
|
|
pepperKeyId: PEPPER_KEY_ID,
|
|
randomBytes: () => Buffer.alloc(32, 75),
|
|
});
|
|
const now = Date.now();
|
|
const database = new DatabaseSync(databasePath);
|
|
try {
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
|
"pepper_key_id", "material_digest", "backup_digest", "state",
|
|
"version", "register_mutation_id", "activate_mutation_id",
|
|
"registered_at_ms", "activated_at_ms"
|
|
) VALUES (?, ?, ?, 'active', 2, ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
PEPPER_KEY_ID,
|
|
summary.digest,
|
|
'b'.repeat(64),
|
|
'10000000-0000-4000-8000-000000000001',
|
|
'10000000-0000-4000-8000-000000000002',
|
|
now - 2_000,
|
|
now - 1_500,
|
|
);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
|
"generation", "mutation_id", "expected_generation",
|
|
"previous_pepper_key_id", "active_pepper_key_id",
|
|
"material_digest", "backup_digest", "activated_at_ms"
|
|
) VALUES (1, ?, 0, NULL, ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
'10000000-0000-4000-8000-000000000002',
|
|
PEPPER_KEY_ID,
|
|
summary.digest,
|
|
'b'.repeat(64),
|
|
now - 1_500,
|
|
);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3IdentitySubjects" (
|
|
"subject_type", "subject_id", "status", "version",
|
|
"created_at_ms", "updated_at_ms"
|
|
) VALUES ('user', 'owner-user', 'active', 1, ?, ?)`,
|
|
)
|
|
.run(now - 1_000, now - 1_000);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ApiCredentials" (
|
|
"credential_id", "version", "state", "subject_type",
|
|
"subject_id", "secret_digest", "created_at_ms",
|
|
"not_before_at_ms", "expires_at_ms"
|
|
) VALUES (?, 1, 'active', 'user', 'owner-user', ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
CREDENTIAL_ID,
|
|
apiCredentialSecretDigest(PEPPER, CREDENTIAL_ID, SECRET),
|
|
now - 1_000,
|
|
now - 1_000,
|
|
now + 10 * 60 * 1_000,
|
|
);
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
|
|
"credential_id", "credential_version", "pepper_key_id"
|
|
) VALUES (?, 1, ?)`,
|
|
)
|
|
.run(CREDENTIAL_ID, PEPPER_KEY_ID);
|
|
if (owner) {
|
|
database
|
|
.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-user', 1, 'active', 'owner',
|
|
'package-cli-owner-binding', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(now - 500);
|
|
}
|
|
} finally {
|
|
database.close();
|
|
}
|
|
fs.chmodSync(databasePath, 0o600);
|
|
fs.writeFileSync(
|
|
credentialFilePath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
kind: 'qinglong3-local-identity-credential-presentation',
|
|
token: TOKEN,
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
return {
|
|
deploymentRoot,
|
|
commandsDirectory,
|
|
databasePath,
|
|
credentialFilePath,
|
|
ownerPepperKeyringDirectory,
|
|
options: {
|
|
deploymentRoot,
|
|
databasePath,
|
|
profile: 'edge',
|
|
ownerPepperKeyringDirectory,
|
|
credentialFilePath,
|
|
},
|
|
};
|
|
}
|
|
|
|
function commandFile(value, operation, request, name) {
|
|
const commandPath = path.join(value.commandsDirectory, `${name}.json`);
|
|
fs.writeFileSync(
|
|
commandPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation,
|
|
options: value.options,
|
|
request,
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
return commandPath;
|
|
}
|
|
|
|
function assertNoSensitiveMaterial(result) {
|
|
const serialized = JSON.stringify(result);
|
|
assert.equal(serialized.includes(TOKEN), false);
|
|
assert.equal(serialized.includes(SECRET), false);
|
|
assert.doesNotMatch(serialized, /offline:sha256:/);
|
|
assert.doesNotMatch(serialized, /authenticationId/);
|
|
}
|
|
|
|
async function activatePackageAutomation(databasePath, lock, manifest) {
|
|
const client = new DatabaseSync(databasePath);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
try {
|
|
const installs = new LocalSqlitePluginPackageInstallRepository(authority);
|
|
const queued = await installs.find(lock.projectId, lock.packageName);
|
|
assert.ok(queued);
|
|
const staged = transitionPluginPackageInstall(lock, queued, {
|
|
type: 'stage_completed',
|
|
mutationId: 'owner-cli-stage-active-package',
|
|
occurredAtMs: queued.updatedAtMs + 1,
|
|
stageRef: `stage:${lock.lockDigest}`,
|
|
artifactDigest: lock.source.artifactDigest,
|
|
manifestDigest: lock.manifestDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
evidenceDigest: 'e'.repeat(64),
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(queued, staged));
|
|
const activating = transitionPluginPackageInstall(lock, staged, {
|
|
type: 'activation_started',
|
|
mutationId: 'owner-cli-start-active-package',
|
|
occurredAtMs: staged.updatedAtMs + 1,
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(staged, activating));
|
|
const active = transitionPluginPackageInstall(lock, activating, {
|
|
type: 'activation_committed',
|
|
mutationId: 'owner-cli-commit-active-package',
|
|
occurredAtMs: activating.updatedAtMs + 1,
|
|
activationRef: `activation:${lock.lockDigest}`,
|
|
intentDigest: pluginPackageActivationIntentDigest(lock, activating),
|
|
generation: lock.targetGeneration,
|
|
contentDigest: lock.source.contentDigest,
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(activating, active));
|
|
|
|
const generation = createPluginPackageResourceGeneration({
|
|
installationId: active.installationId,
|
|
projectId: active.projectId,
|
|
packageName: active.packageName,
|
|
lockDigest: active.lockDigest,
|
|
generation: active.targetGeneration,
|
|
previousActiveLockDigest: active.previousActiveLockDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
contents: manifest.spec.contents,
|
|
});
|
|
const bodies = new Map(
|
|
CLI_PACKAGE_RESOURCES.map(({ reference, body }) => [
|
|
`${reference.kind}\0${reference.path}`,
|
|
body,
|
|
]),
|
|
);
|
|
const registry = createBuiltInTaskSpecSemanticRegistry();
|
|
const revision = materializePluginPackageResources({
|
|
generation,
|
|
lock,
|
|
manifestBytes: Buffer.from(serializePluginPackageManifest(manifest)),
|
|
resources: generation.resources.map((reference) => ({
|
|
reference,
|
|
bytes: bodies.get(`${reference.kind}\0${reference.path}`),
|
|
})),
|
|
taskSpecSemanticRegistry: registry,
|
|
});
|
|
await new LocalSqlitePluginPackageMaterializedRevisionRepository(
|
|
authority,
|
|
registry,
|
|
).publish(revision);
|
|
const publication = createInitialPluginPackageAutomationPublication(
|
|
revision,
|
|
registry,
|
|
active.updatedAtMs + 1,
|
|
);
|
|
const automations =
|
|
new LocalSqlitePluginPackageAutomationPublicationRepository(authority);
|
|
await automations.publish(publication);
|
|
assert.equal(
|
|
await automations.isStartAllowed(
|
|
active.projectId,
|
|
active.packageName,
|
|
publication.publicationDigest,
|
|
),
|
|
true,
|
|
);
|
|
return { active, publication };
|
|
} finally {
|
|
await authority.close();
|
|
}
|
|
}
|
|
|
|
async function activatePackageOnly(databasePath, projectId, packageName) {
|
|
const client = new DatabaseSync(databasePath);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
try {
|
|
const installs = new LocalSqlitePluginPackageInstallRepository(authority);
|
|
const queued = await installs.find(projectId, packageName);
|
|
const lock = await installs.findLock(queued.lockDigest);
|
|
const staged = transitionPluginPackageInstall(lock, queued, {
|
|
type: 'stage_completed',
|
|
mutationId: 'secret-binding-stage-package',
|
|
occurredAtMs: queued.updatedAtMs + 1,
|
|
stageRef: `stage:${lock.lockDigest}`,
|
|
artifactDigest: lock.source.artifactDigest,
|
|
manifestDigest: lock.manifestDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
evidenceDigest: 'e'.repeat(64),
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(queued, staged));
|
|
const activating = transitionPluginPackageInstall(lock, staged, {
|
|
type: 'activation_started',
|
|
mutationId: 'secret-binding-start-package',
|
|
occurredAtMs: staged.updatedAtMs + 1,
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(staged, activating));
|
|
const active = transitionPluginPackageInstall(lock, activating, {
|
|
type: 'activation_committed',
|
|
mutationId: 'secret-binding-commit-package',
|
|
occurredAtMs: activating.updatedAtMs + 1,
|
|
activationRef: `activation:${lock.lockDigest}`,
|
|
intentDigest: pluginPackageActivationIntentDigest(lock, activating),
|
|
generation: lock.targetGeneration,
|
|
contentDigest: lock.source.contentDigest,
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(activating, active));
|
|
return { active, lock };
|
|
} finally {
|
|
await authority.close();
|
|
}
|
|
}
|
|
|
|
async function stagePackageOnly(databasePath, projectId, packageName) {
|
|
const client = new DatabaseSync(databasePath);
|
|
const authority = new LocalSqliteOperationAuthority(client);
|
|
try {
|
|
const installs = new LocalSqlitePluginPackageInstallRepository(authority);
|
|
const queued = await installs.find(projectId, packageName);
|
|
const lock = await installs.findLock(queued.lockDigest);
|
|
const staged = transitionPluginPackageInstall(lock, queued, {
|
|
type: 'stage_completed',
|
|
mutationId: `secret-binding-stage-package-${queued.targetGeneration}`,
|
|
occurredAtMs: queued.updatedAtMs + 1,
|
|
stageRef: `stage:${lock.lockDigest}`,
|
|
artifactDigest: lock.source.artifactDigest,
|
|
manifestDigest: lock.manifestDigest,
|
|
contentDigest: lock.source.contentDigest,
|
|
evidenceDigest: 'e'.repeat(64),
|
|
});
|
|
await installs.commit(pluginPackageInstallCommit(queued, staged));
|
|
return { staged, lock };
|
|
} finally {
|
|
await authority.close();
|
|
}
|
|
}
|
|
|
|
function publisherTrustRunnerWithOneSnapshotFault() {
|
|
let injectFault = true;
|
|
return createLocalPluginPackagePublisherTrustCommandRunner({
|
|
openDatabase: openLocalSqliteAuthenticatedManagementDatabase,
|
|
authenticate: establishAuthenticatedLocalCommand,
|
|
inspect: inspectLocalPluginPackagePublisherTrust,
|
|
publish: publishLocalPluginPackagePublisherTrust,
|
|
retire: retireLocalPluginPackagePublisherKey,
|
|
analyzePublisherKey: analyzeLocalPluginPackageRecoveryCatalogPublisherKey,
|
|
proposeRevocation: proposeLocalPluginPackagePublisherKeyRevocation,
|
|
async confirmRevocation(options) {
|
|
return confirmLocalPluginPackagePublisherKeyRevocation({
|
|
...options,
|
|
afterSnapshotPublished() {
|
|
if (!injectFault) return;
|
|
injectFault = false;
|
|
throw new Error(
|
|
'simulated revocation crash after quarantine and snapshot',
|
|
);
|
|
},
|
|
});
|
|
},
|
|
analyzePublisherKeyImpact:
|
|
analyzeLocalPluginPackageRecoveryCatalogPublisherKeyImpact,
|
|
now: Date.now,
|
|
});
|
|
}
|
|
|
|
test('runs the private command-file package lifecycle with replay-safe IDs', async (t) => {
|
|
const value = await fixture(t);
|
|
const proposeFile = commandFile(
|
|
value,
|
|
'plugin-package.propose',
|
|
{
|
|
actionRef: 'proposal:cli-monitor-v1',
|
|
approvalRequestId: 'approval-cli-monitor-v1',
|
|
proposalAuditEventId: '20000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '20000000-0000-4000-8000-000000000002',
|
|
actionInput: actionInput(),
|
|
},
|
|
'01-propose',
|
|
);
|
|
const proposed = await runLocalPluginPackageCommandFile(proposeFile);
|
|
assert.equal(proposed.proposalStatus, 'created');
|
|
assert.equal(proposed.approval.state, 'pending');
|
|
assert.equal(proposed.proposal.packageName, 'cli-monitor');
|
|
assertNoSensitiveMaterial(proposed);
|
|
|
|
const replayed = await runLocalPluginPackageCommandFile(proposeFile);
|
|
assert.equal(replayed.proposalStatus, 'existing');
|
|
assert.equal(replayed.approvalStatus, 'existing');
|
|
|
|
const interrupted = new DatabaseSync(value.databasePath);
|
|
try {
|
|
interrupted
|
|
.prepare('DELETE FROM "QingLong3ApprovalRequests" WHERE "request_id" = ?')
|
|
.run('approval-cli-monitor-v1');
|
|
interrupted
|
|
.prepare(
|
|
'DELETE FROM "QingLong3SecurityAuditEvents" WHERE "event_id" = ?',
|
|
)
|
|
.run('20000000-0000-4000-8000-000000000002');
|
|
} finally {
|
|
interrupted.close();
|
|
}
|
|
const resumedProposal = await runLocalPluginPackageCommandFile(proposeFile);
|
|
assert.equal(resumedProposal.proposalStatus, 'existing');
|
|
assert.equal(resumedProposal.approvalStatus, 'created');
|
|
assert.equal(
|
|
resumedProposal.approval.requestedAtMs,
|
|
resumedProposal.proposal.createdAtMs,
|
|
);
|
|
|
|
const decideFile = commandFile(
|
|
value,
|
|
'plugin-package.decide',
|
|
{
|
|
actionRef: 'proposal:cli-monitor-v1',
|
|
approvalRequestId: 'approval-cli-monitor-v1',
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-cli-monitor-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000003',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
},
|
|
'02-decide',
|
|
);
|
|
const decided = await runLocalPluginPackageCommandFile(decideFile);
|
|
assert.equal(decided.approval.state, 'approved');
|
|
const replayedDecision = await runLocalPluginPackageCommandFile(decideFile);
|
|
assert.equal(replayedDecision.status, 'existing');
|
|
|
|
const consumeFile = commandFile(
|
|
value,
|
|
'plugin-package.consume',
|
|
{
|
|
actionRef: 'proposal:cli-monitor-v1',
|
|
approvalRequestId: 'approval-cli-monitor-v1',
|
|
expectedVersion: 2,
|
|
consumptionId: 'consume-cli-monitor-v1',
|
|
dispatchId: 'dispatch-cli-monitor-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000004',
|
|
},
|
|
'03-consume',
|
|
);
|
|
const consumed = await runLocalPluginPackageCommandFile(consumeFile);
|
|
assert.equal(consumed.approval.state, 'consumed');
|
|
const replayedConsumption = await runLocalPluginPackageCommandFile(
|
|
consumeFile,
|
|
);
|
|
assert.equal(replayedConsumption.status, 'existing');
|
|
|
|
const dispatched = await runLocalPluginPackageCommandFile(
|
|
commandFile(value, 'plugin-package.dispatch', { limit: 1 }, '04-dispatch'),
|
|
);
|
|
assert.equal(dispatched.summary.scanned, 1);
|
|
assert.equal(dispatched.summary.succeeded, 1);
|
|
|
|
const unresolvedCatalogRoot = path.join(
|
|
value.deploymentRoot,
|
|
'package-catalog',
|
|
);
|
|
const unresolvedBundleRoot = path.join(
|
|
value.deploymentRoot,
|
|
'package-bundles',
|
|
);
|
|
fs.mkdirSync(unresolvedCatalogRoot, { mode: 0o700 });
|
|
fs.mkdirSync(unresolvedBundleRoot, { mode: 0o700 });
|
|
const catalogRoot = fs.realpathSync(unresolvedCatalogRoot);
|
|
const bundleRoot = fs.realpathSync(unresolvedBundleRoot);
|
|
const publicationInput = actionInput();
|
|
const sourceBundlePath = path.join(
|
|
value.deploymentRoot,
|
|
'incoming-package.bundle',
|
|
);
|
|
fs.writeFileSync(
|
|
sourceBundlePath,
|
|
packageArtifact(publicationInput.manifest),
|
|
{ mode: 0o600 },
|
|
);
|
|
const lockDatabase = new DatabaseSync(value.databasePath);
|
|
let lock;
|
|
try {
|
|
const repository = new LocalSqlitePluginPackageInstallRepository(
|
|
lockDatabase,
|
|
);
|
|
const head = await repository.find('default', 'cli-monitor');
|
|
lock = await repository.findLock(head.lockDigest);
|
|
} finally {
|
|
lockDatabase.close();
|
|
}
|
|
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
const publisher = 'packages.example.com';
|
|
const keyId = 'release-2026';
|
|
const unresolvedTrustRoot = path.join(
|
|
value.deploymentRoot,
|
|
'publisher-trust',
|
|
);
|
|
fs.mkdirSync(unresolvedTrustRoot, { mode: 0o700 });
|
|
const trustRoot = fs.realpathSync(unresolvedTrustRoot);
|
|
const trustCandidatePath = path.join(
|
|
value.deploymentRoot,
|
|
'publisher-trust-v1.json',
|
|
);
|
|
const descriptorFilePath = path.join(
|
|
value.deploymentRoot,
|
|
'publication.json',
|
|
);
|
|
const publisherKey = {
|
|
publisher,
|
|
keyId,
|
|
publicKeyPem: publicKey.export({
|
|
format: 'pem',
|
|
type: 'spki',
|
|
}),
|
|
notBeforeMs: Date.now() - 60_000,
|
|
notAfterMs: Date.now() + 10 * 60_000,
|
|
};
|
|
fs.writeFileSync(
|
|
trustCandidatePath,
|
|
`${JSON.stringify({
|
|
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
keys: [publisherKey],
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const trustOptions = {
|
|
...value.options,
|
|
trustRoot,
|
|
catalogRoot,
|
|
bundleRoot,
|
|
};
|
|
const trustProvisionPath = path.join(
|
|
value.commandsDirectory,
|
|
'07-publisher-trust-provision.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustProvisionPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.provision',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-provision-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000011',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000012',
|
|
mutationId: 'publisher-trust-provision-v1',
|
|
expectedGeneration: 0,
|
|
trustFilePath: trustCandidatePath,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustProvisionPath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.provision',
|
|
status: 'published',
|
|
generation: 1,
|
|
keyCount: 1,
|
|
},
|
|
);
|
|
const trustSnapshot = JSON.parse(
|
|
fs.readFileSync(path.join(trustRoot, '00000000000000000001.json'), 'utf8'),
|
|
);
|
|
const trustAuditDatabase = new DatabaseSync(value.databasePath, {
|
|
readOnly: true,
|
|
});
|
|
try {
|
|
assert.equal(
|
|
trustSnapshot.occurredAtMs,
|
|
trustAuditDatabase
|
|
.prepare(
|
|
`SELECT "occurred_at_ms" AS "occurredAtMs"
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "event_id" = ?`,
|
|
)
|
|
.get('20000000-0000-4000-8000-000000000011').occurredAtMs,
|
|
);
|
|
} finally {
|
|
trustAuditDatabase.close();
|
|
}
|
|
assert.equal(
|
|
(await runLocalPluginPackagePublisherTrustCommandFile(trustProvisionPath))
|
|
.status,
|
|
'existing',
|
|
);
|
|
fs.writeFileSync(
|
|
descriptorFilePath,
|
|
`${JSON.stringify({
|
|
schema: LOCAL_PLUGIN_PACKAGE_RECOVERY_PUBLICATION_SCHEMA,
|
|
bundlePath: sourceBundlePath,
|
|
manifest: publicationInput.manifest,
|
|
signature: {
|
|
schema: PLUGIN_PACKAGE_SIGNATURE_SCHEMA,
|
|
publisher,
|
|
keyId,
|
|
signature: sign(
|
|
null,
|
|
pluginPackagePublisherSignaturePayload(lock, publisher, keyId),
|
|
privateKey,
|
|
).toString('base64url'),
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const catalogOptions = {
|
|
...value.options,
|
|
catalogRoot,
|
|
bundleRoot,
|
|
trustRoot,
|
|
};
|
|
const catalogCommandPath = path.join(
|
|
value.commandsDirectory,
|
|
'07-catalog-publish.json',
|
|
);
|
|
fs.writeFileSync(
|
|
catalogCommandPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.catalog.publish',
|
|
options: catalogOptions,
|
|
request: {
|
|
requestId: 'publish-cli-monitor-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000007',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000008',
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
descriptorFilePath,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const published = await runLocalPluginPackageCatalogCommandFile(
|
|
catalogCommandPath,
|
|
);
|
|
assert.equal(published.status, 'published');
|
|
assert.equal(published.lockDigest, lock.lockDigest);
|
|
const activeAutomation = await activatePackageAutomation(
|
|
value.databasePath,
|
|
lock,
|
|
publicationInput.manifest,
|
|
);
|
|
assert.equal(
|
|
(await runLocalPluginPackageCatalogCommandFile(catalogCommandPath)).status,
|
|
'existing',
|
|
);
|
|
const catalogInspectPath = path.join(
|
|
value.commandsDirectory,
|
|
'08-catalog-inspect.json',
|
|
);
|
|
fs.writeFileSync(
|
|
catalogInspectPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.catalog.inspect',
|
|
options: catalogOptions,
|
|
request: {},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackageCatalogCommandFile(catalogInspectPath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.catalog.inspect',
|
|
entryCount: 1,
|
|
bundleCount: 1,
|
|
unresolvedTransactions: 0,
|
|
currentEntries: 1,
|
|
staleEntries: 0,
|
|
},
|
|
);
|
|
const publishedEntryPath = path.join(catalogRoot, `${lock.lockDigest}.json`);
|
|
const staleLockDigest = 'c'.repeat(64);
|
|
const staleEntry = JSON.parse(fs.readFileSync(publishedEntryPath, 'utf8'));
|
|
staleEntry.lockDigest = staleLockDigest;
|
|
fs.writeFileSync(
|
|
path.join(catalogRoot, `${staleLockDigest}.json`),
|
|
`${JSON.stringify(staleEntry)}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const catalogCollectPath = path.join(
|
|
value.commandsDirectory,
|
|
'09-catalog-collect.json',
|
|
);
|
|
fs.writeFileSync(
|
|
catalogCollectPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.catalog.collect',
|
|
options: catalogOptions,
|
|
request: {
|
|
requestId: 'collect-cli-monitor-v1',
|
|
auditEventId: '20000000-0000-4000-8000-000000000009',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000010',
|
|
limit: 1,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackageCatalogCommandFile(catalogCollectPath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.catalog.collect',
|
|
removedEntries: 1,
|
|
removedBundles: 0,
|
|
removedTransactions: 0,
|
|
remaining: false,
|
|
},
|
|
);
|
|
assert.equal(fs.existsSync(publishedEntryPath), true);
|
|
assert.equal(
|
|
fs.existsSync(path.join(catalogRoot, `${staleLockDigest}.json`)),
|
|
false,
|
|
);
|
|
const catalogHelp = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(__dirname, '../dist/plugin-package/pluginPackageCatalogCli.js'),
|
|
'--help',
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(catalogHelp.status, 0, catalogHelp.stderr);
|
|
assert.match(catalogHelp.stdout, /ql3-package-catalog run/);
|
|
assert.equal(catalogHelp.stdout.includes(TOKEN), false);
|
|
|
|
const { publicKey: nextPublicKey } = generateKeyPairSync('ed25519');
|
|
const nextTrustCandidatePath = path.join(
|
|
value.deploymentRoot,
|
|
'publisher-trust-v2.json',
|
|
);
|
|
fs.writeFileSync(
|
|
nextTrustCandidatePath,
|
|
`${JSON.stringify({
|
|
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
keys: [
|
|
publisherKey,
|
|
{
|
|
publisher,
|
|
keyId: 'release-2027',
|
|
publicKeyPem: nextPublicKey.export({
|
|
format: 'pem',
|
|
type: 'spki',
|
|
}),
|
|
notBeforeMs: Date.now() - 60_000,
|
|
notAfterMs: Date.now() + 10 * 60_000,
|
|
},
|
|
],
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const trustRotatePath = path.join(
|
|
value.commandsDirectory,
|
|
'10-publisher-trust-rotate.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustRotatePath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.rotate',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-rotate-v2',
|
|
auditEventId: '20000000-0000-4000-8000-000000000013',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000014',
|
|
mutationId: 'publisher-trust-rotate-v2',
|
|
expectedGeneration: 1,
|
|
trustFilePath: nextTrustCandidatePath,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustRotatePath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.rotate',
|
|
status: 'published',
|
|
generation: 2,
|
|
keyCount: 2,
|
|
},
|
|
);
|
|
const trustRetirePath = path.join(
|
|
value.commandsDirectory,
|
|
'11-publisher-trust-retire.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustRetirePath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.retire',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-retire-v3',
|
|
auditEventId: '20000000-0000-4000-8000-000000000015',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000016',
|
|
mutationId: 'publisher-trust-retire-v3',
|
|
expectedGeneration: 2,
|
|
publisher,
|
|
keyId: 'release-2027',
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustRetirePath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.retire',
|
|
status: 'published',
|
|
generation: 3,
|
|
keyCount: 1,
|
|
},
|
|
);
|
|
assert.equal(
|
|
(await runLocalPluginPackagePublisherTrustCommandFile(trustRetirePath))
|
|
.status,
|
|
'existing',
|
|
);
|
|
const trustInspectPath = path.join(
|
|
value.commandsDirectory,
|
|
'11-publisher-trust-inspect.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustInspectPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.inspect',
|
|
options: trustOptions,
|
|
request: {},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.deepEqual(
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustInspectPath),
|
|
{
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.inspect',
|
|
generation: 3,
|
|
keyCount: 1,
|
|
activeKeyCount: 1,
|
|
snapshotCount: 3,
|
|
retirementCount: 1,
|
|
pendingRetirementCount: 0,
|
|
revocationCount: 0,
|
|
pendingRevocationCount: 0,
|
|
quarantinedLockCount: 0,
|
|
recoveryRequired: false,
|
|
pendingGeneration: null,
|
|
unresolvedTransactions: 0,
|
|
},
|
|
);
|
|
const trustHelp = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(
|
|
__dirname,
|
|
'../dist/plugin-package/pluginPackagePublisherTrustCli.js',
|
|
),
|
|
'--help',
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(trustHelp.status, 0, trustHelp.stderr);
|
|
assert.match(trustHelp.stdout, /ql3-package-trust run/);
|
|
assert.equal(trustHelp.stdout.includes(TOKEN), false);
|
|
|
|
const revokeProposalPath = path.join(
|
|
value.commandsDirectory,
|
|
'12-publisher-trust-revoke-propose.json',
|
|
);
|
|
fs.writeFileSync(
|
|
revokeProposalPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.revoke.propose',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-revoke-v4-propose',
|
|
auditEventId: '20000000-0000-4000-8000-000000000017',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000018',
|
|
mutationId: 'publisher-trust-revoke-v4',
|
|
expectedGeneration: 3,
|
|
publisher,
|
|
keyId,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const revokeProposal = await runLocalPluginPackagePublisherTrustCommandFile(
|
|
revokeProposalPath,
|
|
);
|
|
assert.equal(revokeProposal.status, 'proposed');
|
|
assert.equal(revokeProposal.generation, 3);
|
|
assert.equal(revokeProposal.matchingEntryCount, 1);
|
|
assert.equal(revokeProposal.runtimeAction, 'stop_required');
|
|
await assert.rejects(
|
|
runLocalPluginPackageCatalogCommandFile(catalogCommandPath),
|
|
/catalog publication is unavailable/,
|
|
);
|
|
|
|
const revokeConfirmationPath = path.join(
|
|
value.commandsDirectory,
|
|
'13-publisher-trust-revoke-confirm.json',
|
|
);
|
|
const revokeDualControlPath = path.join(
|
|
value.commandsDirectory,
|
|
'13-publisher-trust-revoke-dual-control.json',
|
|
);
|
|
fs.writeFileSync(
|
|
revokeDualControlPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.revoke.confirm',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-revoke-v4-dual-control',
|
|
auditEventId: '20000000-0000-4000-8000-000000000019',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000020',
|
|
mutationId: 'publisher-trust-revoke-v4',
|
|
expectedGeneration: 3,
|
|
publisher,
|
|
keyId,
|
|
proposerSubjectId: 'owner-user',
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
expectedImpactDigest: revokeProposal.impactDigest,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackagePublisherTrustCommandFile(revokeDualControlPath),
|
|
/distinct Owner/,
|
|
);
|
|
fs.writeFileSync(
|
|
revokeConfirmationPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.revoke.confirm',
|
|
options: trustOptions,
|
|
request: {
|
|
requestId: 'publisher-trust-revoke-v4-confirm',
|
|
auditEventId: '20000000-0000-4000-8000-000000000021',
|
|
failureAuditEventId: '20000000-0000-4000-8000-000000000022',
|
|
mutationId: 'publisher-trust-revoke-v4',
|
|
expectedGeneration: 3,
|
|
publisher,
|
|
keyId,
|
|
proposerSubjectId: 'owner-user',
|
|
authorizationMode: 'break_glass',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
expectedImpactDigest: revokeProposal.impactDigest,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const faultRunner = publisherTrustRunnerWithOneSnapshotFault();
|
|
await assert.rejects(
|
|
faultRunner.run(revokeConfirmationPath),
|
|
/simulated revocation crash after quarantine and snapshot/,
|
|
);
|
|
const interruptedInspection =
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustInspectPath);
|
|
assert.equal(interruptedInspection.generation, 3);
|
|
assert.equal(interruptedInspection.pendingGeneration, 4);
|
|
assert.equal(interruptedInspection.recoveryRequired, true);
|
|
assert.equal(interruptedInspection.quarantinedLockCount, 1);
|
|
const quarantineDatabase = new DatabaseSync(value.databasePath, {
|
|
readOnly: true,
|
|
});
|
|
const quarantineAuthority = new LocalSqliteOperationAuthority(
|
|
quarantineDatabase,
|
|
);
|
|
try {
|
|
assert.deepEqual(
|
|
{
|
|
...quarantineDatabase
|
|
.prepare(
|
|
`SELECT quarantine.lock_digest AS "lockDigest",
|
|
receipt.capability_status AS "capabilityStatus",
|
|
receipt.task_count AS "taskCount"
|
|
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
|
|
JOIN "QingLong3PluginPackageWithdrawalReceipts" AS receipt
|
|
ON receipt.event_digest = quarantine.event_digest`,
|
|
)
|
|
.get(),
|
|
},
|
|
{
|
|
lockDigest: lock.lockDigest,
|
|
capabilityStatus: 'withdrawn',
|
|
taskCount: 0,
|
|
},
|
|
);
|
|
const automationHead = quarantineDatabase
|
|
.prepare(
|
|
`SELECT publication_digest AS "publicationDigest", state
|
|
FROM "QingLong3PluginPackageAutomationPublicationHeads"
|
|
WHERE project_id = ? AND package_name = ?`,
|
|
)
|
|
.get(lock.projectId, lock.packageName);
|
|
const automationRepository =
|
|
new LocalSqlitePluginPackageAutomationPublicationRepository(
|
|
quarantineAuthority,
|
|
);
|
|
const withdrawnAutomation = await automationRepository.findCurrent(
|
|
lock.projectId,
|
|
lock.packageName,
|
|
);
|
|
assert.ok(withdrawnAutomation);
|
|
assert.equal(withdrawnAutomation.state, 'withdrawn');
|
|
assert.equal(
|
|
withdrawnAutomation.previousPublicationDigest,
|
|
activeAutomation.publication.publicationDigest,
|
|
);
|
|
assert.deepEqual(
|
|
{ ...automationHead },
|
|
{
|
|
publicationDigest: withdrawnAutomation.publicationDigest,
|
|
state: 'withdrawn',
|
|
},
|
|
);
|
|
assert.equal(
|
|
await automationRepository.isStartAllowed(
|
|
lock.projectId,
|
|
lock.packageName,
|
|
activeAutomation.publication.publicationDigest,
|
|
),
|
|
false,
|
|
);
|
|
} finally {
|
|
await quarantineAuthority.close();
|
|
}
|
|
assert.deepEqual(await faultRunner.run(revokeConfirmationPath), {
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.revoke.confirm',
|
|
status: 'recovered',
|
|
generation: 4,
|
|
keyCount: 0,
|
|
authorizationMode: 'break_glass',
|
|
quarantinedLockCount: 1,
|
|
runtimeAction: 'restart_required',
|
|
});
|
|
assert.equal(
|
|
(
|
|
await runLocalPluginPackagePublisherTrustCommandFile(
|
|
revokeConfirmationPath,
|
|
)
|
|
).status,
|
|
'existing',
|
|
);
|
|
const replayDatabase = new DatabaseSync(value.databasePath, {
|
|
readOnly: true,
|
|
});
|
|
try {
|
|
assert.equal(
|
|
replayDatabase
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageQuarantineEvents"`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
} finally {
|
|
replayDatabase.close();
|
|
}
|
|
const revokedInspection =
|
|
await runLocalPluginPackagePublisherTrustCommandFile(trustInspectPath);
|
|
assert.equal(revokedInspection.generation, 4);
|
|
assert.equal(revokedInspection.keyCount, 0);
|
|
assert.equal(revokedInspection.revocationCount, 1);
|
|
assert.equal(revokedInspection.pendingRevocationCount, 0);
|
|
assert.equal(revokedInspection.quarantinedLockCount, 1);
|
|
|
|
const inspectFile = commandFile(
|
|
value,
|
|
'plugin-package.inspect',
|
|
{
|
|
actionRef: 'proposal:cli-monitor-v1',
|
|
approvalRequestId: 'approval-cli-monitor-v1',
|
|
},
|
|
'05-inspect',
|
|
);
|
|
let opened = 0;
|
|
let closed = 0;
|
|
const runner = createLocalPluginPackageCommandRunner({
|
|
async openDatabase(options) {
|
|
opened += 1;
|
|
const database = await openLocalSqlitePluginPackageManagementDatabase(
|
|
options,
|
|
);
|
|
return {
|
|
...database,
|
|
async close() {
|
|
closed += 1;
|
|
return database.close();
|
|
},
|
|
};
|
|
},
|
|
authenticate: establishAuthenticatedLocalCommand,
|
|
});
|
|
const inspected = await runner.run(inspectFile);
|
|
assert.equal(inspected.approval.state, 'consumed');
|
|
assert.equal(opened, 1);
|
|
assert.equal(closed, 1);
|
|
assertNoSensitiveMaterial(inspected);
|
|
|
|
const inspectCommand = commandFile(
|
|
value,
|
|
'plugin-package.inspect',
|
|
{
|
|
actionRef: 'proposal:cli-monitor-v1',
|
|
approvalRequestId: 'approval-cli-monitor-v1',
|
|
},
|
|
'06-product-cli',
|
|
);
|
|
const child = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(__dirname, '../dist/plugin-package/pluginPackageCli.js'),
|
|
'run',
|
|
'--command-file',
|
|
inspectCommand,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(child.status, 0, child.stderr);
|
|
assert.equal(child.stderr, '');
|
|
assert.equal(child.stdout.includes(TOKEN), false);
|
|
assert.doesNotMatch(child.stdout, /offline:sha256:/);
|
|
assert.equal(JSON.parse(child.stdout).operation, 'plugin-package.inspect');
|
|
|
|
const installationInspection = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.installation.inspect',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
},
|
|
'07-installation-inspect',
|
|
),
|
|
);
|
|
assert.equal(installationInspection.installation.packageName, 'cli-monitor');
|
|
assert.equal(installationInspection.installation.availability, 'quarantined');
|
|
assert.equal(
|
|
installationInspection.installation.quarantineReason,
|
|
'confirmed_key_compromise',
|
|
);
|
|
assert.equal(
|
|
installationInspection.installation.withdrawalStatus,
|
|
'withdrawn',
|
|
);
|
|
assertNoSensitiveMaterial(installationInspection);
|
|
|
|
const installationList = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.installation.list',
|
|
{
|
|
projectId: 'default',
|
|
limit: 1,
|
|
},
|
|
'08-installation-list',
|
|
),
|
|
);
|
|
assert.equal(installationList.installations.length, 1);
|
|
assert.deepEqual(
|
|
installationList.installations[0],
|
|
installationInspection.installation,
|
|
);
|
|
assert.equal(installationList.truncated, false);
|
|
assert.equal(installationList.next, null);
|
|
assertNoSensitiveMaterial(installationList);
|
|
});
|
|
|
|
test('plans and atomically binds a versioned Secret to the current Package generation', async (t) => {
|
|
const value = await fixture(t);
|
|
const input = actionInput(true);
|
|
const actionRef = 'proposal:secret-aware-cli-monitor-v1';
|
|
const approvalRequestId = 'approval-secret-aware-cli-monitor-v1';
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.propose',
|
|
{
|
|
actionRef,
|
|
approvalRequestId,
|
|
proposalAuditEventId: '21000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '21000000-0000-4000-8000-000000000002',
|
|
actionInput: input,
|
|
},
|
|
'secret-01-propose',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.decide',
|
|
{
|
|
actionRef,
|
|
approvalRequestId,
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-secret-aware-cli-monitor-v1',
|
|
auditEventId: '21000000-0000-4000-8000-000000000003',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
},
|
|
'secret-02-decide',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.consume',
|
|
{
|
|
actionRef,
|
|
approvalRequestId,
|
|
expectedVersion: 2,
|
|
consumptionId: 'consume-secret-aware-cli-monitor-v1',
|
|
dispatchId: 'dispatch-secret-aware-cli-monitor-v1',
|
|
auditEventId: '21000000-0000-4000-8000-000000000004',
|
|
},
|
|
'secret-03-consume',
|
|
),
|
|
);
|
|
const dispatched = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.dispatch',
|
|
{ limit: 1 },
|
|
'secret-04-dispatch',
|
|
),
|
|
);
|
|
assert.equal(dispatched.summary.succeeded, 1);
|
|
const { active } = await activatePackageOnly(
|
|
value.databasePath,
|
|
'default',
|
|
'cli-monitor',
|
|
);
|
|
const secretRef = createSecretRef({
|
|
projectId: 'default',
|
|
name: 'runtime-token',
|
|
version: 1,
|
|
});
|
|
await assert.rejects(
|
|
runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.plan',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
assignments: [
|
|
{
|
|
name: 'TOKEN',
|
|
secretRef: createSecretRef({
|
|
projectId: 'default',
|
|
name: 'runtime-token',
|
|
version: 2,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
'secret-05-missing-version',
|
|
),
|
|
),
|
|
/version is unavailable/,
|
|
);
|
|
const secretDatabase = new DatabaseSync(value.databasePath);
|
|
try {
|
|
secretDatabase
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalSecretEnvelopes" (
|
|
"project_id", "secret_name", "version", "mutation_id",
|
|
"key_id", "algorithm", "nonce", "ciphertext", "auth_tag",
|
|
"created_at_ms"
|
|
) VALUES (?, ?, 1, ?, ?, 'aes-256-gcm', ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
'default',
|
|
'runtime-token',
|
|
'secret-aware-fixture-v1',
|
|
'fixture-key-v1',
|
|
Buffer.alloc(12, 1),
|
|
Buffer.from('ciphertext'),
|
|
Buffer.alloc(16, 2),
|
|
Date.now(),
|
|
);
|
|
} finally {
|
|
secretDatabase.close();
|
|
}
|
|
|
|
const planResult = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.plan',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
assignments: [{ name: 'TOKEN', secretRef }],
|
|
},
|
|
'secret-05-plan',
|
|
),
|
|
);
|
|
assert.equal(planResult.summary.installationId, active.installationId);
|
|
assert.equal(planResult.summary.generation, 1);
|
|
assert.deepEqual(planResult.summary.assignments, [
|
|
{ name: 'TOKEN', required: true, bound: true, secretRef },
|
|
]);
|
|
assertNoSensitiveMaterial(planResult);
|
|
|
|
const revokeDatabase = new DatabaseSync(value.databasePath);
|
|
try {
|
|
revokeDatabase
|
|
.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-user', 2, 'revoked', NULL,
|
|
'secret-binding-revoke-owner', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(Date.now());
|
|
} finally {
|
|
revokeDatabase.close();
|
|
}
|
|
const executePath = commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.execute',
|
|
{
|
|
plan: planResult.plan,
|
|
auditEventId: '21000000-0000-4000-8000-000000000005',
|
|
},
|
|
'secret-06-execute',
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackageCommandFile(executePath),
|
|
/denies Secret management|Project policy changed/,
|
|
);
|
|
const restoreDatabase = new DatabaseSync(value.databasePath);
|
|
try {
|
|
assert.equal(
|
|
restoreDatabase
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageSecretBindings"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
assert.equal(
|
|
restoreDatabase
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" = 'plugin_package.secret.bind'`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
restoreDatabase
|
|
.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-user', 3, 'active', 'owner',
|
|
'secret-binding-restore-owner', 'user', 'owner-user', ?
|
|
)`,
|
|
)
|
|
.run(Date.now());
|
|
} finally {
|
|
restoreDatabase.close();
|
|
}
|
|
|
|
const created = await runLocalPluginPackageCommandFile(executePath);
|
|
assert.equal(created.status, 'created');
|
|
assert.equal(
|
|
created.generationDigest,
|
|
planResult.plan.target.generationDigest,
|
|
);
|
|
assert.match(created.bindingDigest, /^[0-9a-f]{64}$/);
|
|
assertNoSensitiveMaterial(created);
|
|
const replay = await runLocalPluginPackageCommandFile(executePath);
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(replay.bindingDigest, created.bindingDigest);
|
|
|
|
await assert.rejects(
|
|
runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.plan',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
assignments: [{ name: 'TOKEN', secretRef }],
|
|
},
|
|
'secret-07-rebind-current-generation',
|
|
),
|
|
),
|
|
/rebind requires a new generation/,
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.execute',
|
|
{
|
|
plan: planResult.plan,
|
|
auditEventId: '21000000-0000-4000-8000-000000000006',
|
|
},
|
|
'secret-08-different-audit',
|
|
),
|
|
),
|
|
/another audit identity/,
|
|
);
|
|
|
|
const inspection = new DatabaseSync(value.databasePath, { readOnly: true });
|
|
try {
|
|
assert.equal(
|
|
inspection
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageSecretBindings"`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
inspection
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3SecurityAuditEvents"
|
|
WHERE "operation_id" = 'plugin_package.secret.bind'`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
} finally {
|
|
inspection.close();
|
|
}
|
|
});
|
|
|
|
test('keeps an upgrade staged until the Owner commits its Secret transition', async (t) => {
|
|
const value = await fixture(t);
|
|
const initialInput = actionInput(true);
|
|
const initialActionRef = 'proposal:transition-cli-monitor-v1';
|
|
const initialApprovalId = 'approval-transition-cli-monitor-v1';
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.propose',
|
|
{
|
|
actionRef: initialActionRef,
|
|
approvalRequestId: initialApprovalId,
|
|
proposalAuditEventId: '22000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '22000000-0000-4000-8000-000000000002',
|
|
actionInput: initialInput,
|
|
},
|
|
'transition-01-propose',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.decide',
|
|
{
|
|
actionRef: initialActionRef,
|
|
approvalRequestId: initialApprovalId,
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-transition-cli-monitor-v1',
|
|
auditEventId: '22000000-0000-4000-8000-000000000003',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
},
|
|
'transition-02-decide',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.consume',
|
|
{
|
|
actionRef: initialActionRef,
|
|
approvalRequestId: initialApprovalId,
|
|
expectedVersion: 2,
|
|
consumptionId: 'consume-transition-cli-monitor-v1',
|
|
dispatchId: 'dispatch-transition-cli-monitor-v1',
|
|
auditEventId: '22000000-0000-4000-8000-000000000004',
|
|
},
|
|
'transition-03-consume',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.dispatch',
|
|
{ limit: 1 },
|
|
'transition-04-dispatch',
|
|
),
|
|
);
|
|
const { active, lock: activeLock } = await activatePackageOnly(
|
|
value.databasePath,
|
|
'default',
|
|
'cli-monitor',
|
|
);
|
|
const secretRef = createSecretRef({
|
|
projectId: 'default',
|
|
name: 'transition-token',
|
|
version: 1,
|
|
});
|
|
const database = new DatabaseSync(value.databasePath);
|
|
try {
|
|
database
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalSecretEnvelopes" (
|
|
"project_id", "secret_name", "version", "mutation_id",
|
|
"key_id", "algorithm", "nonce", "ciphertext", "auth_tag",
|
|
"created_at_ms"
|
|
) VALUES (?, ?, 1, ?, ?, 'aes-256-gcm', ?, ?, ?, ?)`,
|
|
)
|
|
.run(
|
|
'default',
|
|
'transition-token',
|
|
'transition-secret-v1',
|
|
'fixture-key-v1',
|
|
Buffer.alloc(12, 3),
|
|
Buffer.from('ciphertext'),
|
|
Buffer.alloc(16, 4),
|
|
Date.now(),
|
|
);
|
|
} finally {
|
|
database.close();
|
|
}
|
|
const initialPlan = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.plan',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
assignments: [{ name: 'TOKEN', secretRef }],
|
|
},
|
|
'transition-05-bind-plan',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.execute',
|
|
{
|
|
plan: initialPlan.plan,
|
|
auditEventId: '22000000-0000-4000-8000-000000000005',
|
|
},
|
|
'transition-06-bind-execute',
|
|
),
|
|
);
|
|
|
|
const upgradeInput = upgradeActionInput(initialInput, activeLock.lockDigest);
|
|
const upgradeActionRef = 'proposal:transition-cli-monitor-v2';
|
|
const upgradeApprovalId = 'approval-transition-cli-monitor-v2';
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.propose',
|
|
{
|
|
actionRef: upgradeActionRef,
|
|
approvalRequestId: upgradeApprovalId,
|
|
proposalAuditEventId: '22000000-0000-4000-8000-000000000006',
|
|
approvalAuditEventId: '22000000-0000-4000-8000-000000000007',
|
|
actionInput: upgradeInput,
|
|
},
|
|
'transition-07-upgrade-propose',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.decide',
|
|
{
|
|
actionRef: upgradeActionRef,
|
|
approvalRequestId: upgradeApprovalId,
|
|
expectedVersion: 1,
|
|
decisionId: 'decision-transition-cli-monitor-v2',
|
|
auditEventId: '22000000-0000-4000-8000-000000000008',
|
|
decision: 'approved',
|
|
reasonCode: 'reviewed',
|
|
},
|
|
'transition-08-upgrade-decide',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.consume',
|
|
{
|
|
actionRef: upgradeActionRef,
|
|
approvalRequestId: upgradeApprovalId,
|
|
expectedVersion: 2,
|
|
consumptionId: 'consume-transition-cli-monitor-v2',
|
|
dispatchId: 'dispatch-transition-cli-monitor-v2',
|
|
auditEventId: '22000000-0000-4000-8000-000000000009',
|
|
},
|
|
'transition-09-upgrade-consume',
|
|
),
|
|
);
|
|
await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.dispatch',
|
|
{ limit: 1 },
|
|
'transition-10-upgrade-dispatch',
|
|
),
|
|
);
|
|
const { staged } = await stagePackageOnly(
|
|
value.databasePath,
|
|
'default',
|
|
'cli-monitor',
|
|
);
|
|
|
|
const recoveryClient = new DatabaseSync(value.databasePath);
|
|
const recoveryAuthority = new LocalSqliteOperationAuthority(recoveryClient);
|
|
const installs = new LocalSqlitePluginPackageInstallRepository(
|
|
recoveryAuthority,
|
|
);
|
|
const published = [];
|
|
let publishedReceipt;
|
|
const activationPrerequisite =
|
|
new LocalSqlitePluginPackageSecretBindingActivationPrerequisite(
|
|
recoveryAuthority,
|
|
);
|
|
const recovery = new PluginPackageRecoveryCoordinator({
|
|
repository: installs,
|
|
stageProvider: {
|
|
async stage() {
|
|
throw new Error('staged upgrade must not be staged twice');
|
|
},
|
|
},
|
|
publisher: {
|
|
async publish(intent) {
|
|
published.push(intent.intentDigest);
|
|
const receipt = createPluginPackageActivationReceipt({
|
|
activationRef: `activation:${intent.lockDigest}`,
|
|
intentDigest: intent.intentDigest,
|
|
generation: intent.targetGeneration,
|
|
contentDigest: intent.contentDigest,
|
|
activatedAtMs: staged.updatedAtMs + 2,
|
|
});
|
|
publishedReceipt = receipt;
|
|
return receipt;
|
|
},
|
|
async inspect() {
|
|
return publishedReceipt
|
|
? { status: 'published', receipt: publishedReceipt }
|
|
: { status: 'not_published' };
|
|
},
|
|
},
|
|
activationPrerequisite,
|
|
now: () => staged.updatedAtMs + 1,
|
|
});
|
|
assert.deepEqual(
|
|
await activationPrerequisite.inspect(
|
|
staged,
|
|
await installs.findLock(staged.lockDigest),
|
|
),
|
|
{
|
|
status: 'deferred',
|
|
reason: 'secret_binding_transition_required',
|
|
},
|
|
);
|
|
const deferred = await recovery.recover({ pageSize: 1, maxPages: 1 });
|
|
assert.equal(deferred.scanned, 0);
|
|
assert.equal(deferred.safeToAdmit, true);
|
|
assert.equal((await installs.find('default', 'cli-monitor')).state, 'staged');
|
|
assert.deepEqual(published, []);
|
|
|
|
const transitionPlan = await runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.transition.plan',
|
|
{
|
|
projectId: 'default',
|
|
packageName: 'cli-monitor',
|
|
assignments: [{ name: 'TOKEN', secretRef }],
|
|
},
|
|
'transition-11-plan',
|
|
),
|
|
);
|
|
assert.equal(transitionPlan.summary.generation, 2);
|
|
assert.equal(transitionPlan.summary.kind, 'carry-forward');
|
|
const executePath = commandFile(
|
|
value,
|
|
'plugin-package.secret-binding.transition.execute',
|
|
{
|
|
plan: transitionPlan.plan,
|
|
auditEventId: '22000000-0000-4000-8000-00000000000a',
|
|
},
|
|
'transition-12-execute',
|
|
);
|
|
const created = await runLocalPluginPackageCommandFile(executePath);
|
|
assert.equal(created.status, 'created');
|
|
assert.match(created.receiptDigest, /^[0-9a-f]{64}$/);
|
|
assert.equal(
|
|
(await runLocalPluginPackageCommandFile(executePath)).status,
|
|
'existing',
|
|
);
|
|
const settled = await recovery.recoverPage({ limit: 1 });
|
|
const recovered = await installs.find('default', 'cli-monitor');
|
|
assert.equal(settled.items[0].status, 'settled');
|
|
assert.equal(
|
|
recovered.state,
|
|
'active',
|
|
JSON.stringify({ recovered, publishedReceipt }),
|
|
);
|
|
assert.equal(published.length, 1);
|
|
const inspection = new DatabaseSync(value.databasePath, { readOnly: true });
|
|
try {
|
|
assert.equal(
|
|
inspection
|
|
.prepare(
|
|
`SELECT COUNT(*) AS count
|
|
FROM "QingLong3PluginPackageSecretBindingTransitionReceipts"`,
|
|
)
|
|
.get().count,
|
|
1,
|
|
);
|
|
} finally {
|
|
inspection.close();
|
|
await recoveryAuthority.close();
|
|
}
|
|
assert.equal(active.state, 'active');
|
|
});
|
|
|
|
test('denies an authenticated non-owner before package proposal mutation', async (t) => {
|
|
const value = await fixture(t, false);
|
|
await assert.rejects(
|
|
runLocalPluginPackageCommandFile(
|
|
commandFile(
|
|
value,
|
|
'plugin-package.propose',
|
|
{
|
|
actionRef: 'proposal:forbidden-v1',
|
|
approvalRequestId: 'approval-forbidden-v1',
|
|
proposalAuditEventId: '30000000-0000-4000-8000-000000000001',
|
|
approvalAuditEventId: '30000000-0000-4000-8000-000000000002',
|
|
actionInput: actionInput(),
|
|
},
|
|
'forbidden',
|
|
),
|
|
),
|
|
{ code: 'PLUGIN_PACKAGE_MANAGEMENT_FORBIDDEN' },
|
|
);
|
|
const trustRoot = path.join(value.deploymentRoot, 'forbidden-trust');
|
|
fs.mkdirSync(trustRoot, { mode: 0o700 });
|
|
const { publicKey } = generateKeyPairSync('ed25519');
|
|
const trustCandidatePath = path.join(
|
|
value.deploymentRoot,
|
|
'forbidden-trust.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustCandidatePath,
|
|
`${JSON.stringify({
|
|
schema: LOCAL_PLUGIN_PACKAGE_PUBLISHER_TRUST_SCHEMA,
|
|
keys: [
|
|
{
|
|
publisher: 'packages.example.com',
|
|
keyId: 'forbidden-release',
|
|
publicKeyPem: publicKey.export({
|
|
format: 'pem',
|
|
type: 'spki',
|
|
}),
|
|
notBeforeMs: Date.now() - 60_000,
|
|
notAfterMs: Date.now() + 60_000,
|
|
},
|
|
],
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const trustCommandPath = path.join(
|
|
value.commandsDirectory,
|
|
'forbidden-trust-command.json',
|
|
);
|
|
fs.writeFileSync(
|
|
trustCommandPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.provision',
|
|
options: {
|
|
...value.options,
|
|
trustRoot,
|
|
catalogRoot: path.join(value.deploymentRoot, 'forbidden-catalog'),
|
|
bundleRoot: path.join(value.deploymentRoot, 'forbidden-bundles'),
|
|
},
|
|
request: {
|
|
requestId: 'forbidden-publisher-trust-v1',
|
|
auditEventId: '30000000-0000-4000-8000-000000000003',
|
|
failureAuditEventId: '30000000-0000-4000-8000-000000000004',
|
|
mutationId: 'forbidden-publisher-trust-v1',
|
|
expectedGeneration: 0,
|
|
trustFilePath: trustCandidatePath,
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackagePublisherTrustCommandFile(trustCommandPath),
|
|
{ code: 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_OWNER_REJECTED' },
|
|
);
|
|
const retireCommandPath = path.join(
|
|
value.commandsDirectory,
|
|
'forbidden-trust-retire-command.json',
|
|
);
|
|
fs.writeFileSync(
|
|
retireCommandPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.retire',
|
|
options: {
|
|
...value.options,
|
|
trustRoot,
|
|
catalogRoot: path.join(value.deploymentRoot, 'forbidden-catalog'),
|
|
bundleRoot: path.join(value.deploymentRoot, 'forbidden-bundles'),
|
|
},
|
|
request: {
|
|
requestId: 'forbidden-publisher-trust-retire-v2',
|
|
auditEventId: '30000000-0000-4000-8000-000000000005',
|
|
failureAuditEventId: '30000000-0000-4000-8000-000000000006',
|
|
mutationId: 'forbidden-publisher-trust-retire-v2',
|
|
expectedGeneration: 1,
|
|
publisher: 'packages.example.com',
|
|
keyId: 'forbidden-release',
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackagePublisherTrustCommandFile(retireCommandPath),
|
|
{ code: 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_OWNER_REJECTED' },
|
|
);
|
|
const revokeCommandPath = path.join(
|
|
value.commandsDirectory,
|
|
'forbidden-trust-revoke-command.json',
|
|
);
|
|
fs.writeFileSync(
|
|
revokeCommandPath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation: 'plugin-package.publisher-trust.revoke.propose',
|
|
options: {
|
|
...value.options,
|
|
trustRoot,
|
|
catalogRoot: path.join(value.deploymentRoot, 'forbidden-catalog'),
|
|
bundleRoot: path.join(value.deploymentRoot, 'forbidden-bundles'),
|
|
},
|
|
request: {
|
|
requestId: 'forbidden-publisher-trust-revoke-v2',
|
|
auditEventId: '30000000-0000-4000-8000-000000000007',
|
|
failureAuditEventId: '30000000-0000-4000-8000-000000000008',
|
|
mutationId: 'forbidden-publisher-trust-revoke-v2',
|
|
expectedGeneration: 1,
|
|
publisher: 'packages.example.com',
|
|
keyId: 'forbidden-release',
|
|
},
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
await assert.rejects(
|
|
runLocalPluginPackagePublisherTrustCommandFile(revokeCommandPath),
|
|
{ code: 'LOCAL_SQLITE_AUTHENTICATED_MANAGEMENT_OWNER_REJECTED' },
|
|
);
|
|
assert.equal(fs.existsSync(path.join(trustRoot, 'current.json')), false);
|
|
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
|
try {
|
|
assert.equal(
|
|
database
|
|
.prepare(
|
|
`SELECT count(*) AS count
|
|
FROM "QingLong3PluginPackageInstallProposals"`,
|
|
)
|
|
.get().count,
|
|
0,
|
|
);
|
|
} finally {
|
|
database.close();
|
|
}
|
|
});
|