feat(ql3): add owner-confirmed package secret binding

This commit is contained in:
whyour
2026-08-13 11:28:06 +08:00
parent d72631a2b0
commit 8ba27dc148
21 changed files with 1503 additions and 40 deletions
+5
View File
@@ -75,6 +75,11 @@
"require": "./dist/plugin-package/pluginPackageLifecycle.js",
"default": "./dist/plugin-package/pluginPackageLifecycle.js"
},
"./package-secret-binding": {
"types": "./dist/plugin-package/pluginPackageSecretBinding.d.ts",
"require": "./dist/plugin-package/pluginPackageSecretBinding.js",
"default": "./dist/plugin-package/pluginPackageSecretBinding.js"
},
"./package-recovery-catalog": {
"types": "./dist/plugin-package/pluginPackageRecoveryCatalog.d.ts",
"require": "./dist/plugin-package/pluginPackageRecoveryCatalog.js",
@@ -0,0 +1,8 @@
export {
createLocalPluginPackageSecretBindingService,
LocalPluginPackageSecretBindingConflictError,
LocalPluginPackageSecretBindingUnavailableError,
type ExecuteLocalPluginPackageSecretBindingRequest,
type LocalPluginPackageSecretBindingService,
type PlanLocalPluginPackageSecretBindingRequest,
} from '@qinglong/local-sqlite/plugin-package-secret-binding-administration';
@@ -26,6 +26,10 @@ import {
type PluginPackageLifecycleImpact,
type PluginPackageLifecycleReceipt,
} from '@qinglong/runtime-core/plugin-package-lifecycle';
import {
normalizePluginPackageSecretBindingPlan,
type PluginPackageSecretBindingPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-plan';
import {
pluginPackageInstallRecoveryAction,
type PluginPackageInstallActionInput,
@@ -35,6 +39,7 @@ import type { PluginPackageInstallProposal } from '@qinglong/runtime-core/plugin
import { createLocalPluginPackageManagementService } from '@qinglong/local-admin/package-management';
import { createLocalPluginPackageLifecycleService } from '@qinglong/local-admin/package-lifecycle';
import { createLocalPluginPackageSecretBindingService } from '@qinglong/local-admin/package-secret-binding';
const MAX_PATH_BYTES = 4096;
const MAX_DISPATCH_LIMIT = 64;
@@ -177,6 +182,30 @@ export interface ExecuteLocalPluginPackageLifecycleCommand {
};
}
export interface PlanLocalPluginPackageSecretBindingCommand {
readonly schemaVersion: 1;
readonly operation: 'plugin-package.secret-binding.plan';
readonly options: LocalPluginPackageCommandOptions;
readonly request: {
readonly projectId: string;
readonly packageName: string;
readonly assignments: readonly Readonly<{
name: string;
secretRef: string | null;
}>[];
};
}
export interface ExecuteLocalPluginPackageSecretBindingCommand {
readonly schemaVersion: 1;
readonly operation: 'plugin-package.secret-binding.execute';
readonly options: LocalPluginPackageCommandOptions;
readonly request: {
readonly plan: PluginPackageSecretBindingPlan;
readonly auditEventId: string;
};
}
export type LocalPluginPackageCommand =
| ProposeLocalPluginPackageCommand
| DecideLocalPluginPackageCommand
@@ -186,7 +215,9 @@ export type LocalPluginPackageCommand =
| ListLocalPluginPackageInstallationsCommand
| DispatchLocalPluginPackageCommand
| PlanLocalPluginPackageLifecycleCommand
| ExecuteLocalPluginPackageLifecycleCommand;
| ExecuteLocalPluginPackageLifecycleCommand
| PlanLocalPluginPackageSecretBindingCommand
| ExecuteLocalPluginPackageSecretBindingCommand;
export interface LocalPluginPackageCommandRunner {
run(
@@ -251,6 +282,19 @@ export type LocalPluginPackageCommandResult =
status: 'created' | 'existing';
approval: ReturnType<typeof approvalSummary>;
receipt: ReturnType<typeof lifecycleReceiptSummary>;
}>
| Readonly<{
schemaVersion: 1;
operation: 'plugin-package.secret-binding.plan';
plan: Readonly<PluginPackageSecretBindingPlan>;
summary: ReturnType<typeof secretBindingPlanSummary>;
}>
| Readonly<{
schemaVersion: 1;
operation: 'plugin-package.secret-binding.execute';
status: 'created' | 'existing';
bindingDigest: string;
generationDigest: string;
}>;
export class LocalPluginPackageCommandConfigurationError extends TypeError {
@@ -558,6 +602,65 @@ function normalizeCommand(value: unknown): Readonly<LocalPluginPackageCommand> {
);
}
break;
case 'plugin-package.secret-binding.plan':
exactObject(
value.request,
['assignments', 'packageName', 'projectId'],
'Secret binding plan request',
);
if (
typeof value.request.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(value.request.projectId) ||
typeof value.request.packageName !== 'string' ||
!PACKAGE_NAME_PATTERN.test(value.request.packageName) ||
!Array.isArray(value.request.assignments) ||
value.request.assignments.length < 1 ||
value.request.assignments.length > 64
) {
throw new LocalPluginPackageCommandConfigurationError(
'Secret binding plan request is invalid',
);
}
for (const assignment of value.request.assignments) {
exactObject(assignment, ['name', 'secretRef'], 'Secret assignment');
if (
typeof assignment.name !== 'string' ||
!/^[A-Z_][A-Z0-9_]{0,127}$/.test(assignment.name) ||
(assignment.secretRef !== null &&
(typeof assignment.secretRef !== 'string' ||
Buffer.byteLength(assignment.secretRef, 'utf8') > 512))
) {
throw new LocalPluginPackageCommandConfigurationError(
'Secret assignment is invalid',
);
}
}
break;
case 'plugin-package.secret-binding.execute':
exactObject(
value.request,
['auditEventId', 'plan'],
'Secret binding execution request',
);
if (
typeof value.request.auditEventId !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
value.request.auditEventId,
)
) {
throw new LocalPluginPackageCommandConfigurationError(
'Secret binding audit event ID is invalid',
);
}
try {
normalizePluginPackageSecretBindingPlan(value.request.plan);
} catch (error) {
throw new LocalPluginPackageCommandConfigurationError(
'Secret binding plan is invalid',
error,
);
}
break;
default:
throw new LocalPluginPackageCommandConfigurationError(
'operation is invalid',
@@ -714,8 +817,7 @@ function lifecycleReceiptSummary(
disposition: receipt.lifecycle.disposition,
capabilityStatus: receipt.capability.status,
taskTransitions: receipt.capability.taskTransitions.length,
previousActiveVectorDigest:
receipt.capability.previousActiveVectorDigest,
previousActiveVectorDigest: receipt.capability.previousActiveVectorDigest,
currentActiveVectorDigest: receipt.capability.currentActiveVectorDigest,
currentToolSnapshotDigest: receipt.capability.currentToolSnapshotDigest,
retainedSourceCount: receipt.capability.retainedSourceCount,
@@ -724,12 +826,68 @@ function lifecycleReceiptSummary(
});
}
function secretBindingPlanSummary(
plan: Readonly<PluginPackageSecretBindingPlan>,
) {
return Object.freeze({
projectId: plan.target.projectId,
packageName: plan.target.packageName,
installationId: plan.target.installationId,
generation: plan.target.generation,
generationDigest: plan.target.generationDigest,
manifestDigest: plan.target.manifestDigest,
assignments: Object.freeze(
plan.entries.map((entry) =>
Object.freeze({
name: entry.name,
required: entry.required,
bound: entry.secretRef !== null,
secretRef: entry.secretRef,
}),
),
),
plannedAtMs: plan.plannedAtMs,
planDigest: plan.planDigest,
});
}
async function execute(
command: Readonly<LocalPluginPackageCommand>,
database: LocalSqlitePluginPackageManagementDatabase,
authenticated: Readonly<AuthenticatedLocalCommand>,
): Promise<Readonly<LocalPluginPackageCommandResult>> {
await authenticated.confirm();
if (command.operation === 'plugin-package.secret-binding.plan') {
const service = createLocalPluginPackageSecretBindingService(
database.authority,
);
const plan = await service.plan({
...command.request,
principal: authenticated.principal,
plannedAtMs: Date.now(),
});
return Object.freeze({
schemaVersion: 1,
operation: command.operation,
plan,
summary: secretBindingPlanSummary(plan),
});
}
if (command.operation === 'plugin-package.secret-binding.execute') {
const service = createLocalPluginPackageSecretBindingService(
database.authority,
);
const result = await service.execute({
...command.request,
principal: authenticated.principal,
confirmAuthorization: authenticated.confirm,
});
return Object.freeze({
schemaVersion: 1,
operation: command.operation,
...result,
});
}
if (command.operation === 'plugin-package.lifecycle.plan') {
const service = createLocalPluginPackageLifecycleService({
authority: database.authority,
@@ -82,6 +82,7 @@ const {
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');
@@ -200,7 +201,7 @@ function packageArtifact(manifest) {
]);
}
function actionInput() {
function actionInput(secretAware = false) {
const manifest = {
apiVersion: PLUGIN_PACKAGE_API_VERSION,
kind: PLUGIN_PACKAGE_KIND,
@@ -224,8 +225,10 @@ function actionInput() {
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: ['system.command'],
secrets: secretAware ? [{ name: 'TOKEN', required: true }] : [],
tools: secretAware
? ['secret.use', 'system.command']
: ['system.command'],
},
contents: {
tasks: ['tasks/collect.json'],
@@ -507,6 +510,46 @@ async function activatePackageAutomation(databasePath, lock, manifest) {
}
}
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();
}
}
function publisherTrustRunnerWithOneSnapshotFault() {
let injectFault = true;
return createLocalPluginPackagePublisherTrustCommandRunner({
@@ -1348,6 +1391,277 @@ test('runs the private command-file package lifecycle with replay-safe IDs', asy
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('denies an authenticated non-owner before package proposal mutation', async (t) => {
const value = await fixture(t, false);
await assert.rejects(
+8 -3
View File
@@ -86,9 +86,14 @@
"default": "./dist/plugin-package/pluginPackageMaterializedRevisionRepository.js"
},
"./plugin-package-secret-binding": {
"types": "./dist/plugin-package/pluginPackageSecretBindingRepository.d.ts",
"require": "./dist/plugin-package/pluginPackageSecretBindingRepository.js",
"default": "./dist/plugin-package/pluginPackageSecretBindingRepository.js"
"types": "./dist/plugin-package/secret-binding/repository.d.ts",
"require": "./dist/plugin-package/secret-binding/repository.js",
"default": "./dist/plugin-package/secret-binding/repository.js"
},
"./plugin-package-secret-binding-administration": {
"types": "./dist/plugin-package/secret-binding/administration.d.ts",
"require": "./dist/plugin-package/secret-binding/administration.js",
"default": "./dist/plugin-package/secret-binding/administration.js"
},
"./plugin-package-task-reconciliation": {
"types": "./dist/plugin-package/pluginPackageTaskReconciliationRepository.d.ts",
@@ -0,0 +1,542 @@
import type { DatabaseSync } from 'node:sqlite';
import {
normalizePluginPackageInstallProposal,
type PluginPackageInstallProposal,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
normalizePluginPackageLock,
normalizePluginPackageInstallRecord,
type PluginPackageInstallRecord,
type PluginPackageLock,
} from '@qinglong/runtime-core/plugin-package-install';
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
createPluginPackageSecretBindingFromPlan,
createPluginPackageSecretBindingPlan,
normalizePluginPackageSecretBindingPlan,
type PluginPackageSecretBindingPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-plan';
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
import {
normalizeSecurityPrincipal,
type SecurityPolicyFence,
} from '@qinglong/runtime-core/security';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import { LocalSqliteProjectPolicyRepository } from '../../security/projectPolicyRepository';
import {
insertLocalSecurityAudit,
localSecurityAuditFromRow,
LOCAL_SECURITY_AUDIT_SELECT,
sameSecurityAuditSemantic,
} from '../../security/securityPersistence';
import { LocalSqlitePluginPackageSecretBindingRepository } from './repository';
type Row = Record<string, unknown>;
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const UUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
export interface PlanLocalPluginPackageSecretBindingRequest {
readonly projectId: string;
readonly packageName: string;
readonly assignments: readonly Readonly<{
name: string;
secretRef: string | null;
}>[];
readonly principal: SecurityPrincipal;
readonly plannedAtMs: number;
}
export interface ExecuteLocalPluginPackageSecretBindingRequest {
readonly plan: PluginPackageSecretBindingPlan;
readonly auditEventId: string;
readonly principal: SecurityPrincipal;
readonly confirmAuthorization: () => void | Promise<void>;
}
export interface LocalPluginPackageSecretBindingService {
plan(
request: PlanLocalPluginPackageSecretBindingRequest,
): Promise<Readonly<PluginPackageSecretBindingPlan>>;
execute(request: ExecuteLocalPluginPackageSecretBindingRequest): Promise<
Readonly<{
status: 'created' | 'existing';
bindingDigest: string;
generationDigest: string;
}>
>;
}
export class LocalPluginPackageSecretBindingConflictError extends Error {
readonly code = 'LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_CONFLICT';
constructor(message: string) {
super(
`Local Plugin Package Secret binding conflicts with state: ${message}`,
);
this.name = 'LocalPluginPackageSecretBindingConflictError';
}
}
export class LocalPluginPackageSecretBindingUnavailableError extends Error {
readonly code = 'LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Local Plugin Package Secret binding is unavailable', options);
this.name = 'LocalPluginPackageSecretBindingUnavailableError';
}
}
function rowText(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string')
throw new LocalPluginPackageSecretBindingUnavailableError();
return value;
}
function identity(value: unknown, label: string, pattern: RegExp): string {
if (typeof value !== 'string' || !pattern.test(value)) {
throw new TypeError(`${label} is invalid`);
}
return value;
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError(`${label} is invalid`);
}
return value as number;
}
function loadCurrent(
client: DatabaseSync,
projectId: string,
packageName: string,
): Readonly<{
record: PluginPackageInstallRecord;
lock: PluginPackageLock;
proposal: PluginPackageInstallProposal;
}> {
const row = client
.prepare(
`SELECT install.record_json AS "recordJson",
install.lock_json AS "lockJson",
proposal.proposal_json AS "proposalJson"
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = head.installation_id
JOIN "QingLong3PluginPackageAdmissionReceipts" AS admission
ON admission.installation_id = install.installation_id
JOIN "QingLong3PluginPackageInstallProposals" AS proposal
ON proposal.action_ref = admission.action_ref
LEFT JOIN "QingLong3PluginPackageQuarantineEvents" AS quarantine
ON quarantine.project_id = install.project_id
AND quarantine.package_name = install.package_name
AND quarantine.installation_id = install.installation_id
AND quarantine.lock_digest = install.lock_digest
LEFT JOIN "QingLong3PluginPackageLifecycleHeads" AS lifecycle
ON lifecycle.project_id = install.project_id
AND lifecycle.package_name = install.package_name
AND lifecycle.installation_id = install.installation_id
AND lifecycle.lock_digest = install.lock_digest
WHERE head.project_id = ?
AND head.package_name = ?
AND install.state = 'active'
AND install.active_lock_digest = install.lock_digest
AND quarantine.event_digest IS NULL
AND COALESCE(lifecycle.disposition, 'active') = 'active'
LIMIT 2`,
)
.all(projectId, packageName) as Row[];
if (row.length !== 1) {
throw new LocalPluginPackageSecretBindingConflictError(
'current active Package authority is absent or ambiguous',
);
}
try {
const record = normalizePluginPackageInstallRecord(
JSON.parse(rowText(row[0]!, 'recordJson')),
);
const lock = normalizePluginPackageLock(
JSON.parse(rowText(row[0]!, 'lockJson')),
);
const proposal = normalizePluginPackageInstallProposal(
JSON.parse(rowText(row[0]!, 'proposalJson')),
);
if (
record.projectId !== projectId ||
record.packageName !== packageName ||
record.lockDigest !== lock.lockDigest ||
proposal.actionDigest !== lock.approval.actionDigest ||
proposal.previewDigest !== lock.approval.previewDigest ||
proposal.actionInput.projectId !== projectId ||
proposal.actionInput.manifest.metadata.name !== packageName ||
proposal.actionInput.targetGeneration !== record.targetGeneration ||
proposal.actionInput.source.contentDigest !== lock.source.contentDigest ||
proposal.actionInput.manifest.metadata.version !==
record.packageVersion ||
proposal.actionInput.manifest.spec.permissions.secrets.length === 0 ||
!proposal.actionInput.manifest.spec.permissions.tools.includes(
'secret.use',
)
) {
throw new Error('current Package provenance drift');
}
return Object.freeze({ record, lock, proposal });
} catch (error) {
if (error instanceof LocalPluginPackageSecretBindingConflictError)
throw error;
throw new LocalPluginPackageSecretBindingUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
function generationFrom(current: ReturnType<typeof loadCurrent>) {
const { record, lock } = current;
return createPluginPackageResourceGenerationFromReferences({
installationId: record.installationId,
projectId: record.projectId,
packageName: record.packageName,
lockDigest: record.lockDigest,
generation: record.targetGeneration,
previousActiveLockDigest: record.previousActiveLockDigest,
contentDigest: lock.source.contentDigest,
resources: lock.resources,
});
}
function auditRecord(
plan: Readonly<PluginPackageSecretBindingPlan>,
eventId: string,
principal: Readonly<SecurityPrincipal>,
fence: Readonly<SecurityPolicyFence>,
occurredAtMs: number,
): Readonly<SecurityAuditRecord> {
return Object.freeze({
eventId,
requestId: `package_secret_binding:${plan.planDigest}`,
operationId: 'plugin_package.secret.bind',
projectId: plan.target.projectId,
subject: principal.subject,
authenticationId: principal.authenticationId,
outcome: 'allowed',
reasons: Object.freeze(['owner_confirmed_secret_binding']),
fence,
occurredAtMs,
});
}
function verifySecretVersions(
client: DatabaseSync,
plan: Readonly<PluginPackageSecretBindingPlan>,
): void {
for (const entry of plan.entries) {
if (entry.secretRef === null) continue;
const reference = parseSecretRef(entry.secretRef);
if (reference.version === undefined) {
throw new LocalPluginPackageSecretBindingConflictError(
`Secret ${entry.name} reference is not version-pinned`,
);
}
const row = client
.prepare(
`SELECT 1 AS present
FROM "QingLong3LocalSecretEnvelopes"
WHERE "project_id" = ? AND "secret_name" = ? AND "version" = ?
LIMIT 2`,
)
.all(reference.projectId, reference.name, reference.version) as Row[];
if (row.length !== 1) {
throw new LocalPluginPackageSecretBindingConflictError(
`Secret ${entry.name} version is unavailable`,
);
}
}
}
function verifyPolicyFence(
client: DatabaseSync,
principal: Readonly<SecurityPrincipal>,
fence: Readonly<SecurityPolicyFence>,
projectId: string,
): void {
const row = client
.prepare(
`SELECT project."status" AS "status",
project."version" AS "projectVersion",
binding."version" AS "bindingVersion",
binding."state" AS "bindingState",
binding."role" AS "role"
FROM "QingLong3Projects" AS project
LEFT JOIN "QingLong3ProjectRoleBindings" AS binding
ON binding."project_id" = project."id"
AND binding."subject_type" = ?
AND binding."subject_id" = ?
AND binding."version" = (
SELECT MAX(current."version")
FROM "QingLong3ProjectRoleBindings" AS current
WHERE current."project_id" = project."id"
AND current."subject_type" = ?
AND current."subject_id" = ?
)
WHERE project."id" = ?
LIMIT 2`,
)
.all(
principal.subject.type,
principal.subject.id,
principal.subject.type,
principal.subject.id,
projectId,
) as Row[];
const value = row[0];
if (
row.length !== 1 ||
!value ||
value.status !== 'active' ||
value.projectVersion !== fence.projectVersion ||
value.bindingVersion !== fence.bindingVersion ||
value.bindingState !== 'active' ||
value.role !== 'owner'
) {
throw new LocalPluginPackageSecretBindingConflictError(
'Project policy changed after planning',
);
}
}
export function createLocalPluginPackageSecretBindingService(
authorityValue: LocalSqliteOperationAuthority | DatabaseSync,
now: () => number = Date.now,
): Readonly<LocalPluginPackageSecretBindingService> {
const authority =
authorityValue instanceof LocalSqliteOperationAuthority
? authorityValue
: new LocalSqliteOperationAuthority(authorityValue);
const policy = new ProjectPolicyEngine(
new LocalSqliteProjectPolicyRepository(authority),
);
const bindings = new LocalSqlitePluginPackageSecretBindingRepository(
authority,
);
const authorize = async (
principalValue: SecurityPrincipal,
projectId: string,
observedAtMs: number,
) => {
const principal = normalizeSecurityPrincipal(principalValue, observedAtMs);
if (
principal.subject.type !== 'user' ||
principal.assurance !== 'local_console'
) {
throw new LocalPluginPackageSecretBindingConflictError(
'binding requires a local-console User',
);
}
const decision = await policy.authorize(
principal,
projectId,
'secret.manage',
);
if (decision.effect !== 'allow' || decision.fence === null) {
throw new LocalPluginPackageSecretBindingConflictError(
'current Project policy denies Secret management',
);
}
return Object.freeze({ principal, fence: decision.fence });
};
return Object.freeze({
async plan(request: PlanLocalPluginPackageSecretBindingRequest) {
const projectId = identity(request.projectId, 'Project ID', IDENTIFIER);
const packageName = identity(
request.packageName,
'Package name',
PACKAGE_NAME,
);
const plannedAtMs = timestamp(request.plannedAtMs, 'plannedAtMs');
const authorization = await authorize(
request.principal,
projectId,
plannedAtMs,
);
return authority.enqueue(
async () => {
verifyPolicyFence(
authority.client,
authorization.principal,
authorization.fence,
projectId,
);
const current = loadCurrent(authority.client, projectId, packageName);
const generation = generationFrom(current);
if (bindings.findInTransaction(generation.generationDigest)) {
throw new LocalPluginPackageSecretBindingConflictError(
'current generation is already bound; rebind requires a new generation',
);
}
const plan = createPluginPackageSecretBindingPlan({
generation,
manifest: current.proposal.actionInput.manifest,
assignments: request.assignments,
plannedAtMs,
});
verifySecretVersions(authority.client, plan);
return plan;
},
() => new LocalPluginPackageSecretBindingUnavailableError(),
);
},
async execute(request: ExecuteLocalPluginPackageSecretBindingRequest) {
const plan = normalizePluginPackageSecretBindingPlan(request.plan);
if (typeof request.confirmAuthorization !== 'function') {
throw new TypeError('confirmAuthorization is invalid');
}
const observedAtMs = timestamp(now(), 'binding execution clock');
const authorization = await authorize(
request.principal,
plan.target.projectId,
observedAtMs,
);
await request.confirmAuthorization();
return authority.enqueue(
async () => {
authority.client.exec('BEGIN IMMEDIATE');
try {
const current = loadCurrent(
authority.client,
plan.target.projectId,
plan.target.packageName,
);
const expected = createPluginPackageSecretBindingPlan({
generation: generationFrom(current),
manifest: current.proposal.actionInput.manifest,
assignments: plan.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})),
plannedAtMs: plan.plannedAtMs,
});
if (expected.planDigest !== plan.planDigest) {
throw new LocalPluginPackageSecretBindingConflictError(
'current Package generation changed after planning',
);
}
verifyPolicyFence(
authority.client,
authorization.principal,
authorization.fence,
plan.target.projectId,
);
verifySecretVersions(authority.client, plan);
const audit = auditRecord(
plan,
identity(request.auditEventId, 'audit event ID', UUID),
authorization.principal,
authorization.fence,
observedAtMs,
);
const auditsForPlan = authority.client
.prepare(
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents"
WHERE "request_id" = ? AND "operation_id" = ?
LIMIT 2`,
)
.all(audit.requestId, audit.operationId) as Row[];
if (
auditsForPlan.length > 1 ||
(auditsForPlan.length === 1 &&
rowText(auditsForPlan[0]!, 'eventId') !== audit.eventId)
) {
throw new LocalPluginPackageSecretBindingConflictError(
'binding plan already has another audit identity',
);
}
const existingAudit = authority.client
.prepare(
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = ? LIMIT 2`,
)
.get(audit.eventId) as Row | undefined;
if (existingAudit) {
if (
!sameSecurityAuditSemantic(
localSecurityAuditFromRow(existingAudit),
audit,
)
) {
throw new LocalPluginPackageSecretBindingConflictError(
'audit identity is already used by another operation',
);
}
} else {
insertLocalSecurityAudit(authority.client, audit);
}
const existingBinding = bindings.findInTransaction(
plan.target.generationDigest,
);
const result = existingBinding
? (() => {
if (
existingBinding.authority.kind !==
'local-owner-confirmation' ||
existingBinding.authority.evidenceDigest !==
plan.planDigest ||
JSON.stringify(existingBinding.target) !==
JSON.stringify(plan.target) ||
JSON.stringify(existingBinding.entries) !==
JSON.stringify(plan.entries)
) {
throw new LocalPluginPackageSecretBindingConflictError(
'current generation is already bound by another plan',
);
}
return Object.freeze({
status: 'existing' as const,
binding: existingBinding,
});
})()
: bindings.publishInTransaction(
createPluginPackageSecretBindingFromPlan(
plan,
'local-owner-confirmation',
observedAtMs,
),
);
authority.client.exec('COMMIT');
return Object.freeze({
status: result.status,
bindingDigest: result.binding.bindingDigest,
generationDigest: result.binding.target.generationDigest,
});
} catch (error) {
if (authority.client.isTransaction)
authority.client.exec('ROLLBACK');
if (
error instanceof LocalPluginPackageSecretBindingConflictError ||
error instanceof LocalPluginPackageSecretBindingUnavailableError
) {
throw error;
}
throw new LocalPluginPackageSecretBindingUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
},
() => new LocalPluginPackageSecretBindingUnavailableError(),
);
},
});
}
@@ -10,7 +10,7 @@ import {
type PluginPackageSecretBindingRepository,
} from '@qinglong/runtime-core/plugin-package-secret-binding';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
@@ -143,6 +143,16 @@ export class LocalSqlitePluginPackageSecretBindingRepository
return row ? this.parse(row) : null;
}
findInTransaction(
digest: string,
): Readonly<PluginPackageSecretBinding> | null {
try {
return this.findStored(generationDigest(digest));
} catch (error) {
throw mapStorageError(error);
}
}
private enqueue<T>(work: () => T): Promise<T> {
return this.authority.enqueue(
async () => {
@@ -169,9 +179,17 @@ export class LocalSqlitePluginPackageSecretBindingRepository
binding: Readonly<PluginPackageSecretBinding>;
}>
> {
const binding = normalizePluginPackageSecretBinding(value);
return this.enqueue(() => this.publishInTransaction(binding));
}
publishInTransaction(value: Readonly<PluginPackageSecretBinding>): Readonly<{
status: 'created' | 'existing';
binding: Readonly<PluginPackageSecretBinding>;
}> {
const binding = normalizePluginPackageSecretBinding(value);
const bindingJson = serialize(binding);
return this.enqueue(() => {
try {
const existing = this.findStored(binding.target.generationDigest);
if (existing) {
if (JSON.stringify(existing) !== bindingJson) {
@@ -245,6 +263,8 @@ export class LocalSqlitePluginPackageSecretBindingRepository
result.changes === 1 ? ('created' as const) : ('existing' as const),
binding: stored,
});
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -278,7 +278,7 @@ export async function openLocalSqliteRuntimeDatabase(
},
pluginPackageSecretBindings() {
pluginPackageSecretBindingsPromise ??= import(
'../plugin-package/pluginPackageSecretBindingRepository.js'
'../plugin-package/secret-binding/repository.js'
).then(
({ LocalSqlitePluginPackageSecretBindingRepository }) =>
new LocalSqlitePluginPackageSecretBindingRepository(authority),
@@ -13,7 +13,7 @@ const {
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const {
LocalSqlitePluginPackageSecretBindingRepository,
} = require('../dist/plugin-package/pluginPackageSecretBindingRepository');
} = require('../dist/plugin-package/secret-binding/repository');
const { migrateLocalSqliteDatabase } = require('../dist/migration/migration');
const LOCK_DIGEST = 'a'.repeat(64);
+12 -4
View File
@@ -84,7 +84,10 @@
"dist/plugin-package/pluginPackageResourceMaterialization.d.ts"
],
"plugin-package-secret-binding": [
"dist/plugin-package/pluginPackageSecretBinding.d.ts"
"dist/plugin-package/secret-binding/binding.d.ts"
],
"plugin-package-secret-binding-plan": [
"dist/plugin-package/secret-binding/plan.d.ts"
],
"plugin-package-task-reconciliation": [
"dist/plugin-package/pluginPackageTaskReconciliation.d.ts"
@@ -375,9 +378,14 @@
"default": "./dist/plugin-package/pluginPackageResourceMaterialization.js"
},
"./plugin-package-secret-binding": {
"types": "./dist/plugin-package/pluginPackageSecretBinding.d.ts",
"require": "./dist/plugin-package/pluginPackageSecretBinding.js",
"default": "./dist/plugin-package/pluginPackageSecretBinding.js"
"types": "./dist/plugin-package/secret-binding/binding.d.ts",
"require": "./dist/plugin-package/secret-binding/binding.js",
"default": "./dist/plugin-package/secret-binding/binding.js"
},
"./plugin-package-secret-binding-plan": {
"types": "./dist/plugin-package/secret-binding/plan.d.ts",
"require": "./dist/plugin-package/secret-binding/plan.js",
"default": "./dist/plugin-package/secret-binding/plan.js"
},
"./plugin-package-task-reconciliation": {
"types": "./dist/plugin-package/pluginPackageTaskReconciliation.d.ts",
@@ -27,7 +27,7 @@ import {
assertPluginPackageSecretBindingMatches,
type PluginPackageSecretBinding,
type PluginPackageSecretBindingRepository,
} from './pluginPackageSecretBinding';
} from './secret-binding/binding';
import {
normalizeTaskDefinitionLabels,
normalizeTaskDefinitionSpec,
@@ -9,7 +9,7 @@ import {
type PluginPackageResourceLockSource,
} from './pluginPackageResourceMaterialization';
import type { PluginPackageResourceGenerationSource } from './pluginPackageResourceGeneration';
import type { PluginPackageSecretBindingRepository } from './pluginPackageSecretBinding';
import type { PluginPackageSecretBindingRepository } from './secret-binding/binding';
import {
InvalidPluginPackageTaskReconciliationError,
PluginPackageTaskReconciliationConflictError,
@@ -5,13 +5,13 @@ import {
normalizePluginPackageManifest,
type PluginPackageManifest,
type PluginPackageSecretRequirement,
} from './pluginPackage';
import { pluginPackageManifestDigest } from './installation/pluginPackageInstall';
} from '../pluginPackage';
import { pluginPackageManifestDigest } from '../installation/pluginPackageInstall';
import {
normalizePluginPackageResourceGeneration,
type PluginPackageResourceGeneration,
} from './pluginPackageResourceGeneration';
import { parseSecretRef } from '../secret/secretReference';
} from '../pluginPackageResourceGeneration';
import { parseSecretRef } from '../../secret/secretReference';
export const PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA =
'qinglong/plugin-package-secret-binding@v1' as const;
@@ -67,6 +67,13 @@ export interface CreatePluginPackageSecretBindingInput {
readonly boundAtMs: number;
}
export interface CreatePluginPackageSecretBindingFromEntriesInput {
readonly target: Readonly<PluginPackageSecretBindingTarget>;
readonly entries: readonly Readonly<PluginPackageSecretBindingEntry>[];
readonly authority: Readonly<PluginPackageSecretBindingAuthority>;
readonly boundAtMs: number;
}
export interface PluginPackageSecretBindingRepository {
find(
generationDigest: string,
@@ -408,19 +415,24 @@ export function createPluginPackageSecretBinding(
return Object.freeze({ ...unsigned, bindingDigest: bindingDigest(unsigned) });
}
export function normalizePluginPackageSecretBinding(
value: unknown,
export function createPluginPackageSecretBindingFromEntries(
input: CreatePluginPackageSecretBindingFromEntriesInput,
): Readonly<PluginPackageSecretBinding> {
const binding = dataRecord(value, 'binding');
exactKeys(
binding,
['authority', 'bindingDigest', 'boundAtMs', 'entries', 'schema', 'target'],
'binding',
);
if (binding.schema !== PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA) {
return invalid('schema is unsupported');
}
const targetValue = dataRecord(binding.target, 'target');
const target = normalizeTarget(input.target);
const entries = normalizeEntries(input.entries, target.projectId);
const authority = normalizeAuthority(input.authority);
const boundAtMs = timestamp(input.boundAtMs);
const unsigned = unsignedBinding(target, entries, authority, boundAtMs);
return normalizePluginPackageSecretBinding({
...unsigned,
bindingDigest: bindingDigest(unsigned),
});
}
function normalizeTarget(
value: unknown,
): Readonly<PluginPackageSecretBindingTarget> {
const targetValue = dataRecord(value, 'target');
exactKeys(
targetValue,
[
@@ -434,7 +446,7 @@ export function normalizePluginPackageSecretBinding(
],
'target',
);
const target = Object.freeze({
return Object.freeze({
installationId: identifier(targetValue.installationId, 'installation ID'),
projectId: identifier(targetValue.projectId, 'Project ID'),
packageName: packageName(targetValue.packageName),
@@ -443,6 +455,21 @@ export function normalizePluginPackageSecretBinding(
generationDigest: digest(targetValue.generationDigest, 'generation digest'),
manifestDigest: digest(targetValue.manifestDigest, 'Manifest digest'),
});
}
export function normalizePluginPackageSecretBinding(
value: unknown,
): Readonly<PluginPackageSecretBinding> {
const binding = dataRecord(value, 'binding');
exactKeys(
binding,
['authority', 'bindingDigest', 'boundAtMs', 'entries', 'schema', 'target'],
'binding',
);
if (binding.schema !== PLUGIN_PACKAGE_SECRET_BINDING_SCHEMA) {
return invalid('schema is unsupported');
}
const target = normalizeTarget(binding.target);
const entries = normalizeEntries(binding.entries, target.projectId);
const authority = normalizeAuthority(binding.authority);
const boundAtMs = timestamp(binding.boundAtMs);
@@ -0,0 +1,168 @@
import { createHash } from 'node:crypto';
import {
createPluginPackageSecretBinding,
createPluginPackageSecretBindingFromEntries,
type PluginPackageSecretBinding,
type PluginPackageSecretBindingAssignment,
type PluginPackageSecretBindingEntry,
type PluginPackageSecretBindingTarget,
} from './binding';
import type { PluginPackageManifest } from '../pluginPackage';
import type { PluginPackageResourceGeneration } from '../pluginPackageResourceGeneration';
export const PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA =
'qinglong/plugin-package-secret-binding-plan@v1' as const;
export const MAX_PLUGIN_PACKAGE_SECRET_BINDING_PLAN_JSON_BYTES = 64 * 1024;
export interface PluginPackageSecretBindingPlan {
readonly schema: typeof PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA;
readonly target: Readonly<PluginPackageSecretBindingTarget>;
readonly entries: readonly Readonly<PluginPackageSecretBindingEntry>[];
readonly plannedAtMs: number;
readonly planDigest: string;
}
export interface CreatePluginPackageSecretBindingPlanInput {
readonly generation: Readonly<PluginPackageResourceGeneration>;
readonly manifest: Readonly<PluginPackageManifest>;
readonly assignments: readonly Readonly<PluginPackageSecretBindingAssignment>[];
readonly plannedAtMs: number;
}
const DIGEST = /^[0-9a-f]{64}$/;
const PLACEHOLDER_EVIDENCE_DIGEST = '0'.repeat(64);
const PLAN_DIGEST_DOMAIN = Buffer.from(
'qinglong/plugin-package-secret-binding-plan-digest@v1\0',
'utf8',
);
function invalid(message: string): never {
throw new TypeError(
`Plugin Package Secret binding plan is invalid: ${message}`,
);
}
function exactKeys(value: object, expected: readonly string[]): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
invalid('shape is invalid');
}
}
function unsignedPlan(
binding: Pick<PluginPackageSecretBinding, 'target' | 'entries'>,
plannedAtMs: number,
): Omit<PluginPackageSecretBindingPlan, 'planDigest'> {
if (!Number.isSafeInteger(plannedAtMs) || plannedAtMs < 0) {
return invalid('plannedAtMs is invalid');
}
return Object.freeze({
schema: PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA,
target: binding.target,
entries: binding.entries,
plannedAtMs,
});
}
function planDigest(
value: Omit<PluginPackageSecretBindingPlan, 'planDigest'>,
): string {
return createHash('sha256')
.update(PLAN_DIGEST_DOMAIN)
.update(JSON.stringify(value), 'utf8')
.digest('hex');
}
function withDigest(
value: Omit<PluginPackageSecretBindingPlan, 'planDigest'>,
): Readonly<PluginPackageSecretBindingPlan> {
const normalized = Object.freeze({
...value,
planDigest: planDigest(value),
});
if (
Buffer.byteLength(JSON.stringify(normalized), 'utf8') >
MAX_PLUGIN_PACKAGE_SECRET_BINDING_PLAN_JSON_BYTES
) {
return invalid('durable JSON byte budget exceeded');
}
return normalized;
}
export function createPluginPackageSecretBindingPlan(
input: CreatePluginPackageSecretBindingPlanInput,
): Readonly<PluginPackageSecretBindingPlan> {
const draft = createPluginPackageSecretBinding({
generation: input.generation,
manifest: input.manifest,
assignments: input.assignments,
authority: Object.freeze({
kind: 'local-owner-confirmation',
evidenceDigest: PLACEHOLDER_EVIDENCE_DIGEST,
}),
boundAtMs: 0,
});
return withDigest(unsignedPlan(draft, input.plannedAtMs));
}
export function normalizePluginPackageSecretBindingPlan(
value: unknown,
): Readonly<PluginPackageSecretBindingPlan> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return invalid('value must be an object');
}
exactKeys(value, [
'entries',
'planDigest',
'plannedAtMs',
'schema',
'target',
]);
const candidate = value as PluginPackageSecretBindingPlan;
if (candidate.schema !== PLUGIN_PACKAGE_SECRET_BINDING_PLAN_SCHEMA) {
return invalid('schema is unsupported');
}
if (
typeof candidate.planDigest !== 'string' ||
!DIGEST.test(candidate.planDigest)
) {
return invalid('plan digest is invalid');
}
const normalizedBinding = createPluginPackageSecretBindingFromEntries({
target: candidate.target,
entries: candidate.entries,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: PLACEHOLDER_EVIDENCE_DIGEST,
},
boundAtMs: 0,
});
const unsigned = unsignedPlan(normalizedBinding, candidate.plannedAtMs);
const normalized = withDigest(unsigned);
if (normalized.planDigest !== candidate.planDigest) {
return invalid('plan digest does not match content');
}
return normalized;
}
export function createPluginPackageSecretBindingFromPlan(
planValue: unknown,
authorityKind: PluginPackageSecretBinding['authority']['kind'],
boundAtMs: number,
): Readonly<PluginPackageSecretBinding> {
const plan = normalizePluginPackageSecretBindingPlan(planValue);
return createPluginPackageSecretBindingFromEntries({
target: plan.target,
entries: plan.entries,
authority: {
kind: authorityKind,
evidenceDigest: plan.planDigest,
},
boundAtMs,
});
}
@@ -25,7 +25,7 @@ const {
const { createSecretRef } = require('../dist/secret/secretReference');
const {
createPluginPackageSecretBinding,
} = require('../dist/plugin-package/pluginPackageSecretBinding');
} = require('../dist/plugin-package/secret-binding/binding');
const {
InvalidPluginPackageResourceMaterializationError,
MAX_PLUGIN_PACKAGE_MATERIALIZED_RESOURCE_BYTES,
@@ -7,7 +7,7 @@ const {
assertPluginPackageSecretBindingMatches,
createPluginPackageSecretBinding,
normalizePluginPackageSecretBinding,
} = require('../dist/plugin-package/pluginPackageSecretBinding');
} = require('../dist/plugin-package/secret-binding/binding');
const {
createPluginPackageResourceGeneration,
} = require('../dist/plugin-package/pluginPackageResourceGeneration');
@@ -0,0 +1,144 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
createPluginPackageSecretBindingFromPlan,
createPluginPackageSecretBindingPlan,
normalizePluginPackageSecretBindingPlan,
} = require('@qinglong/runtime-core/plugin-package-secret-binding-plan');
const {
createPluginPackageResourceGeneration,
} = require('@qinglong/runtime-core/plugin-package-resource-generation');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const manifest = {
apiVersion: 'qinglong.io/v1alpha1',
kind: 'Package',
metadata: {
name: 'example-monitor',
displayName: 'Example Monitor',
version: '1.0.0',
description: 'Secret binding plan fixture',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: ['arm64'],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '32Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [
{ name: 'OPTIONAL_TOKEN', required: false },
{ name: 'TOKEN', required: true },
],
tools: ['secret.use'],
},
contents: { tasks: [], workflows: [], prompts: [], tools: [] },
},
};
const generation = createPluginPackageResourceGeneration({
installationId: 'install-1',
projectId: 'project-1',
packageName: 'example-monitor',
lockDigest: 'a'.repeat(64),
generation: 1,
previousActiveLockDigest: null,
contentDigest: 'b'.repeat(64),
contents: manifest.spec.contents,
});
function plan() {
return createPluginPackageSecretBindingPlan({
generation,
manifest,
assignments: [
{ name: 'OPTIONAL_TOKEN', secretRef: null },
{
name: 'TOKEN',
secretRef: createSecretRef({
projectId: 'project-1',
name: 'runtime-token',
version: 2,
}),
},
],
plannedAtMs: 100,
});
}
test('creates one canonical content-free Secret binding plan', () => {
const value = plan();
assert.match(value.planDigest, /^[0-9a-f]{64}$/);
assert.equal(value.plannedAtMs, 100);
assert.deepEqual(normalizePluginPackageSecretBindingPlan(value), value);
assert.equal(JSON.stringify(value).includes('secret-value'), false);
assert.equal(Object.isFrozen(value), true);
});
test('rejects target, entry, time and digest drift', () => {
const value = plan();
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({ ...value, plannedAtMs: -1 }),
/plannedAtMs is invalid/,
);
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({
...value,
target: { ...value.target, projectId: 'project-2' },
}),
/crosses Project boundary|plan digest/,
);
assert.throws(
() => normalizePluginPackageSecretBindingPlan({ ...value, extra: true }),
/shape is invalid/,
);
assert.throws(
() =>
normalizePluginPackageSecretBindingPlan({
...value,
planDigest: 'c'.repeat(64),
}),
/plan digest does not match/,
);
});
test('creates Local and Cluster authority bindings from the same plan', () => {
const value = plan();
const local = createPluginPackageSecretBindingFromPlan(
value,
'local-owner-confirmation',
110,
);
const cluster = createPluginPackageSecretBindingFromPlan(
value,
'approved-action-execution',
120,
);
assert.equal(local.authority.evidenceDigest, value.planDigest);
assert.equal(cluster.authority.evidenceDigest, value.planDigest);
assert.equal(local.boundAtMs, 110);
assert.equal(cluster.boundAtMs, 120);
assert.notEqual(local.bindingDigest, cluster.bindingDigest);
});
test('exports the plan contract only through its explicit subpath', () => {
assert.equal(
require('../dist').createPluginPackageSecretBindingPlan,
undefined,
);
assert.equal(
require('@qinglong/runtime-core/plugin-package-secret-binding-plan')
.createPluginPackageSecretBindingPlan,
createPluginPackageSecretBindingPlan,
);
});