feat(ql3): gate local secret transitions before activation

This commit is contained in:
whyour
2026-08-13 20:29:47 +08:00
parent 326783ff14
commit c877147d0f
37 changed files with 2495 additions and 39 deletions
@@ -106,6 +106,8 @@ import { local0093PluginPackageSecretMaterializationGuardMigration } from '../mi
import { local0094CapabilityV47Migration } from '../migrations/0094-capability-v47';
import { local0095PluginPackageSecretBindingTargetGuardMigration } from '../migrations/0095-plugin-package-secret-binding-target-guard';
import { local0096CapabilityV48Migration } from '../migrations/0096-capability-v48';
import { local0097PluginPackageSecretBindingTransitionReceiptsMigration } from '../migrations/0097-plugin-package-secret-binding-transition-receipts';
import { local0098CapabilityV49Migration } from '../migrations/0098-capability-v49';
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
import {
LOCAL_SQLITE_MIGRATION_STREAM_ID,
@@ -224,6 +226,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
local0094CapabilityV47Migration,
local0095PluginPackageSecretBindingTargetGuardMigration,
local0096CapabilityV48Migration,
local0097PluginPackageSecretBindingTransitionReceiptsMigration,
local0098CapabilityV49Migration,
]),
});
@@ -492,5 +492,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
checksum:
'07118f8e2f1e4f4aa7b9bb95ba9b70276f62de0e63d63cf5dcceacd3532853d9',
}),
Object.freeze({
id: '0097-plugin-package-secret-binding-transition-receipts',
checksum:
'8d4e1bd0cf40e6c3c8d8b628cc04d95209a99b38344b7997966050f90db567c0',
}),
Object.freeze({
id: '0098-capability-v49',
checksum:
'133bdb78900256971bb6e13de8a129024d09f9d4c9d9290137eca3ff6e8b30eb',
}),
]),
});
@@ -0,0 +1,78 @@
import { defineLocalSqliteMigration } from './sqlMigration';
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_SQL } from '../plugin-package/secret-binding/transitionReceiptSchemaContract';
export const local0097PluginPackageSecretBindingTransitionReceiptsMigration =
defineLocalSqliteMigration({
id: '0097-plugin-package-secret-binding-transition-receipts',
statements: [
`
CREATE TABLE "QingLong3PluginPackageSecretBindingTransitionReceipts" (
generation_digest TEXT PRIMARY KEY NOT NULL,
transition_digest TEXT NOT NULL,
project_id TEXT NOT NULL,
package_name TEXT NOT NULL,
installation_id TEXT NOT NULL,
lock_digest TEXT NOT NULL,
generation INTEGER NOT NULL,
manifest_digest TEXT NOT NULL,
previous_active_lock_digest TEXT NOT NULL,
authority_kind TEXT NOT NULL,
evidence_digest TEXT NOT NULL,
binding_digest TEXT,
committed_at_ms INTEGER NOT NULL,
receipt_digest TEXT NOT NULL,
receipt_json TEXT NOT NULL,
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_install_fk
FOREIGN KEY (installation_id)
REFERENCES "QingLong3PluginPackageInstalls" (installation_id)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_binding_fk
FOREIGN KEY (binding_digest)
REFERENCES "QingLong3PluginPackageSecretBindings" (binding_digest)
ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_identity_check CHECK (
length(project_id) BETWEEN 1 AND 128 AND
length(package_name) BETWEEN 1 AND 63 AND
length(installation_id) BETWEEN 1 AND 128 AND
generation BETWEEN 2 AND 2147483647 AND
authority_kind IN ('approved-action-execution','local-owner-confirmation') AND
committed_at_ms >= 0
),
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_digest_check CHECK (
length(generation_digest) = 64 AND generation_digest NOT GLOB '*[^0-9a-f]*' AND
length(transition_digest) = 64 AND transition_digest NOT GLOB '*[^0-9a-f]*' AND
length(lock_digest) = 64 AND lock_digest NOT GLOB '*[^0-9a-f]*' AND
length(manifest_digest) = 64 AND manifest_digest NOT GLOB '*[^0-9a-f]*' AND
length(previous_active_lock_digest) = 64 AND previous_active_lock_digest NOT GLOB '*[^0-9a-f]*' AND
length(evidence_digest) = 64 AND evidence_digest NOT GLOB '*[^0-9a-f]*' AND
(binding_digest IS NULL OR
(length(binding_digest) = 64 AND binding_digest NOT GLOB '*[^0-9a-f]*')) AND
length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^0-9a-f]*'
),
CONSTRAINT ql3_plugin_package_secret_binding_transition_receipt_json_check CHECK (
length(CAST(receipt_json AS BLOB)) BETWEEN 2 AND 196608 AND
json_valid(receipt_json) AND json_type(receipt_json) = 'object' AND
json_extract(receipt_json, '$.schema') = 'qinglong/plugin-package-secret-binding-transition-receipt@v1' AND
json_extract(receipt_json, '$.transitionPlan.transitionDigest') = transition_digest AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.generationDigest') = generation_digest AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.projectId') = project_id AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.packageName') = package_name AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.installationId') = installation_id AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.lockDigest') = lock_digest AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.generation') = generation AND
json_extract(receipt_json, '$.transitionPlan.nextTarget.manifestDigest') = manifest_digest AND
json_extract(receipt_json, '$.transitionPlan.previousActiveLockDigest') = previous_active_lock_digest AND
json_extract(receipt_json, '$.authority.kind') = authority_kind AND
json_extract(receipt_json, '$.authority.evidenceDigest') = evidence_digest AND
json_extract(receipt_json, '$.bindingDigest') IS binding_digest AND
json_extract(receipt_json, '$.committedAtMs') = committed_at_ms AND
json_extract(receipt_json, '$.receiptDigest') = receipt_digest
)
)
`,
`CREATE UNIQUE INDEX ql3_plugin_package_secret_binding_transition_receipt_transition_uidx ON "QingLong3PluginPackageSecretBindingTransitionReceipts" (transition_digest)`,
`CREATE UNIQUE INDEX ql3_plugin_package_secret_binding_transition_receipt_digest_uidx ON "QingLong3PluginPackageSecretBindingTransitionReceipts" (receipt_digest)`,
`CREATE INDEX ql3_plugin_package_secret_binding_transition_receipt_install_idx ON "QingLong3PluginPackageSecretBindingTransitionReceipts" (installation_id, generation_digest)`,
LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_SQL,
],
});
@@ -0,0 +1,14 @@
import { CAPABILITIES_V48 } from './0096-capability-v48';
import { defineLocalSqliteMigration } from './sqlMigration';
export const CAPABILITIES_V49 = CAPABILITIES_V48.replace(
'"plugin_package_secret_binding_transition":1,',
'"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,',
);
export const local0098CapabilityV49Migration = defineLocalSqliteMigration({
id: '0098-capability-v49',
statements: [
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 49, migration_id = '0097-plugin-package-secret-binding-transition-receipts', capabilities = '${CAPABILITIES_V49}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 48 AND migration_id = '0095-plugin-package-secret-binding-target-guard' AND capabilities = '${CAPABILITIES_V48}'`,
],
});
@@ -1142,6 +1142,41 @@ export class LocalSqlitePluginPackageInstallRepository
JOIN "QingLong3PluginPackageInstalls" AS install
ON install."installation_id" = head."installation_id"
WHERE install."state" IN ('queued','staged','activating')
AND NOT (
install."state" = 'staged' AND
install."previous_active_lock_digest" IS NOT NULL AND
(
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageSecretBindings" AS previous_binding
JOIN "QingLong3PluginPackageInstalls" AS previous_install
ON previous_install.project_id = install.project_id
AND previous_install.package_name = install.package_name
AND previous_install.lock_digest =
install.previous_active_lock_digest
WHERE previous_binding.installation_id =
previous_install.installation_id
) OR
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageAdmissionReceipts" AS admission
JOIN "QingLong3PluginPackageInstallProposals" AS proposal
ON proposal.action_ref = admission.action_ref
WHERE admission.installation_id = install.installation_id
AND json_array_length(
json_extract(
proposal.proposal_json,
'$.actionInput.manifest.spec.permissions.secrets'
)
) > 0
)
) AND
NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageSecretBindingTransitionReceipts" AS transition_receipt
WHERE transition_receipt.installation_id = install.installation_id
)
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageQuarantineEvents" AS quarantine
@@ -0,0 +1,159 @@
import type { DatabaseSync } from 'node:sqlite';
import {
normalizePluginPackageInstallProposal,
type PluginPackageInstallProposal,
} from '@qinglong/runtime-core/plugin-package-proposal';
import {
assertPluginPackageInstallMatchesLock,
normalizePluginPackageInstallRecord,
normalizePluginPackageLock,
type PluginPackageInstallRecord,
type PluginPackageLock,
} from '@qinglong/runtime-core/plugin-package-install';
import type {
PluginPackageActivationPrerequisite,
PluginPackageActivationPrerequisiteObservation,
} from '@qinglong/runtime-core/plugin-package-installation';
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
import { createPluginPackageSecretBindingTarget } from '@qinglong/runtime-core/plugin-package-secret-binding';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import { LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository } from './transitionReceiptRepository';
type Row = Record<string, unknown>;
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new Error('Secret binding activation prerequisite is unavailable');
}
return value;
}
export class LocalSqlitePluginPackageSecretBindingActivationPrerequisite
implements PluginPackageActivationPrerequisite
{
readonly #authority: LocalSqliteOperationAuthority;
readonly #receipts: LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
this.#receipts =
new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository(
this.#authority,
);
}
async inspect(
recordValue: Readonly<PluginPackageInstallRecord>,
lockValue: Readonly<PluginPackageLock>,
): Promise<Readonly<PluginPackageActivationPrerequisiteObservation>> {
const record = normalizePluginPackageInstallRecord(recordValue);
const lock = normalizePluginPackageLock(lockValue);
assertPluginPackageInstallMatchesLock(lock, record);
if (record.state !== 'staged') {
throw new Error(
'Secret binding activation prerequisite requires staged install',
);
}
if (record.previousActiveLockDigest === null) {
return Object.freeze({ status: 'ready' as const });
}
return this.#authority.enqueue(
async () => {
const row = this.#authority.client
.prepare(
`SELECT proposal.proposal_json AS "proposalJson",
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstalls" AS previous
JOIN "QingLong3PluginPackageSecretBindings" AS binding
ON binding.installation_id = previous.installation_id
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = install.previous_active_lock_digest
) AS "previousBindingPresent"
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
WHERE head.project_id = ? AND head.package_name = ?
AND install.installation_id = ? AND install.lock_digest = ?
AND install.state = 'staged'
AND install.previous_active_lock_digest = ?
LIMIT 2`,
)
.all(
record.projectId,
record.packageName,
record.installationId,
record.lockDigest,
record.previousActiveLockDigest,
) as Row[];
if (row.length !== 1) {
throw new Error(
'Secret binding activation prerequisite is unavailable',
);
}
const proposal = normalizePluginPackageInstallProposal(
JSON.parse(
text(row[0]!, 'proposalJson'),
) as PluginPackageInstallProposal,
);
if (
proposal.actionDigest !== lock.approval.actionDigest ||
proposal.previewDigest !== lock.approval.previewDigest ||
proposal.actionInput.targetGeneration !== record.targetGeneration ||
proposal.actionInput.source.contentDigest !==
lock.source.contentDigest
) {
throw new Error(
'Secret binding activation prerequisite provenance drift',
);
}
const required =
proposal.actionInput.manifest.spec.permissions.secrets.length > 0 ||
row[0]!.previousBindingPresent === 1;
if (!required) return Object.freeze({ status: 'ready' as const });
const generation = 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,
});
const target = createPluginPackageSecretBindingTarget(
generation,
proposal.actionInput.manifest,
);
const receipt = this.#receipts.findInTransaction(
target.generationDigest,
);
if (
!receipt ||
JSON.stringify(receipt.transitionPlan.nextTarget) !==
JSON.stringify(target) ||
receipt.transitionPlan.previousActiveLockDigest !==
record.previousActiveLockDigest
) {
return Object.freeze({
status: 'deferred' as const,
reason: 'secret_binding_transition_required' as const,
});
}
return Object.freeze({ status: 'ready' as const });
},
() => new Error('Secret binding activation prerequisite is unavailable'),
);
}
}
@@ -11,12 +11,22 @@ import {
type PluginPackageLock,
} from '@qinglong/runtime-core/plugin-package-install';
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
import { createPluginPackageSecretBindingTarget } from '@qinglong/runtime-core/plugin-package-secret-binding';
import {
createPluginPackageSecretBindingFromPlan,
createPluginPackageSecretBindingPlan,
normalizePluginPackageSecretBindingPlan,
type PluginPackageSecretBindingPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-plan';
import {
createPluginPackageSecretBindingTransitionPlan,
normalizePluginPackageSecretBindingTransitionPlan,
type PluginPackageSecretBindingTransitionPlan,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-plan';
import {
createPluginPackageSecretBindingFromTransitionPlan,
createPluginPackageSecretBindingTransitionReceipt,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
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';
@@ -35,6 +45,7 @@ import {
sameSecurityAuditSemantic,
} from '../../security/securityPersistence';
import { LocalSqlitePluginPackageSecretBindingRepository } from './repository';
import { LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository } from './transitionReceiptRepository';
type Row = Record<string, unknown>;
@@ -61,6 +72,24 @@ export interface ExecuteLocalPluginPackageSecretBindingRequest {
readonly confirmAuthorization: () => void | Promise<void>;
}
export interface PlanLocalPluginPackageSecretBindingTransitionRequest {
readonly projectId: string;
readonly packageName: string;
readonly assignments: readonly Readonly<{
name: string;
secretRef: string | null;
}>[];
readonly principal: SecurityPrincipal;
readonly plannedAtMs: number;
}
export interface ExecuteLocalPluginPackageSecretBindingTransitionRequest {
readonly plan: PluginPackageSecretBindingTransitionPlan;
readonly auditEventId: string;
readonly principal: SecurityPrincipal;
readonly confirmAuthorization: () => void | Promise<void>;
}
export interface LocalPluginPackageSecretBindingService {
plan(
request: PlanLocalPluginPackageSecretBindingRequest,
@@ -72,6 +101,20 @@ export interface LocalPluginPackageSecretBindingService {
generationDigest: string;
}>
>;
planTransition(
request: PlanLocalPluginPackageSecretBindingTransitionRequest,
): Promise<Readonly<PluginPackageSecretBindingTransitionPlan>>;
executeTransition(
request: ExecuteLocalPluginPackageSecretBindingTransitionRequest,
): Promise<
Readonly<{
status: 'created' | 'existing';
transitionDigest: string;
receiptDigest: string;
bindingDigest: string | null;
generationDigest: string;
}>
>;
}
export class LocalPluginPackageSecretBindingConflictError extends Error {
@@ -213,6 +256,145 @@ function generationFrom(current: ReturnType<typeof loadCurrent>) {
});
}
function loadTransition(
client: DatabaseSync,
projectId: string,
packageName: string,
): Readonly<{
previous: ReturnType<typeof loadCurrent>;
next: ReturnType<typeof loadCurrent>;
previousAttemptGeneration: number;
}> {
const rows = client
.prepare(
`SELECT current.record_json AS "nextRecordJson",
current.lock_json AS "nextLockJson",
current_proposal.proposal_json AS "nextProposalJson",
previous.record_json AS "previousRecordJson",
previous.lock_json AS "previousLockJson",
previous_proposal.proposal_json AS "previousProposalJson",
(
SELECT MAX(history.target_generation)
FROM "QingLong3PluginPackageInstalls" AS history
WHERE history.project_id = current.project_id
AND history.package_name = current.package_name
AND history.target_generation < current.target_generation
) AS "previousAttemptGeneration"
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS current
ON current.installation_id = head.installation_id
JOIN "QingLong3PluginPackageAdmissionReceipts" AS current_admission
ON current_admission.installation_id = current.installation_id
JOIN "QingLong3PluginPackageInstallProposals" AS current_proposal
ON current_proposal.action_ref = current_admission.action_ref
JOIN "QingLong3PluginPackageInstalls" AS previous
ON previous.project_id = current.project_id
AND previous.package_name = current.package_name
AND previous.lock_digest = current.previous_active_lock_digest
JOIN "QingLong3PluginPackageAdmissionReceipts" AS previous_admission
ON previous_admission.installation_id = previous.installation_id
JOIN "QingLong3PluginPackageInstallProposals" AS previous_proposal
ON previous_proposal.action_ref = previous_admission.action_ref
WHERE head.project_id = ?
AND head.package_name = ?
AND current.state = 'staged'
AND current.previous_active_lock_digest IS NOT NULL
AND current.active_lock_digest = current.previous_active_lock_digest
AND current.target_generation = (
SELECT MAX(latest.target_generation)
FROM "QingLong3PluginPackageInstalls" AS latest
WHERE latest.project_id = current.project_id
AND latest.package_name = current.package_name
)
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
LIMIT 2`,
)
.all(projectId, packageName) as Row[];
if (rows.length !== 1) {
throw new LocalPluginPackageSecretBindingConflictError(
'reviewed staged Package generation is absent or ambiguous',
);
}
try {
const row = rows[0]!;
const next = Object.freeze({
record: normalizePluginPackageInstallRecord(
JSON.parse(rowText(row, 'nextRecordJson')),
),
lock: normalizePluginPackageLock(
JSON.parse(rowText(row, 'nextLockJson')),
),
proposal: normalizePluginPackageInstallProposal(
JSON.parse(rowText(row, 'nextProposalJson')),
),
});
const previous = Object.freeze({
record: normalizePluginPackageInstallRecord(
JSON.parse(rowText(row, 'previousRecordJson')),
),
lock: normalizePluginPackageLock(
JSON.parse(rowText(row, 'previousLockJson')),
),
proposal: normalizePluginPackageInstallProposal(
JSON.parse(rowText(row, 'previousProposalJson')),
),
});
const previousAttemptGeneration = row.previousAttemptGeneration;
if (
!Number.isSafeInteger(previousAttemptGeneration) ||
previousAttemptGeneration !== next.record.targetGeneration - 1 ||
next.record.projectId !== projectId ||
next.record.packageName !== packageName ||
next.record.lockDigest !== next.lock.lockDigest ||
next.record.previousActiveLockDigest !== previous.lock.lockDigest ||
next.proposal.actionDigest !== next.lock.approval.actionDigest ||
next.proposal.previewDigest !== next.lock.approval.previewDigest ||
next.proposal.actionInput.targetGeneration !==
next.record.targetGeneration ||
next.proposal.actionInput.manifest.metadata.name !== packageName ||
next.proposal.actionInput.source.contentDigest !==
next.lock.source.contentDigest ||
previous.record.lockDigest !== previous.lock.lockDigest ||
previous.proposal.actionDigest !== previous.lock.approval.actionDigest ||
previous.proposal.previewDigest !==
previous.lock.approval.previewDigest ||
previous.proposal.actionInput.targetGeneration !==
previous.record.targetGeneration ||
previous.proposal.actionInput.source.contentDigest !==
previous.lock.source.contentDigest
) {
throw new Error('Package transition provenance drift');
}
return Object.freeze({
previous,
next,
previousAttemptGeneration: previousAttemptGeneration as number,
});
} catch (error) {
if (error instanceof LocalPluginPackageSecretBindingConflictError)
throw error;
throw new LocalPluginPackageSecretBindingUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
function transitionGeneration(
value: ReturnType<typeof loadTransition>['next'],
) {
return createPluginPackageResourceGenerationFromReferences({
installationId: value.record.installationId,
projectId: value.record.projectId,
packageName: value.record.packageName,
lockDigest: value.record.lockDigest,
generation: value.record.targetGeneration,
previousActiveLockDigest: value.record.previousActiveLockDigest,
contentDigest: value.lock.source.contentDigest,
resources: value.lock.resources,
});
}
function auditRecord(
plan: Readonly<PluginPackageSecretBindingPlan>,
eventId: string,
@@ -234,6 +416,27 @@ function auditRecord(
});
}
function transitionAuditRecord(
plan: Readonly<PluginPackageSecretBindingTransitionPlan>,
eventId: string,
principal: Readonly<SecurityPrincipal>,
fence: Readonly<SecurityPolicyFence>,
occurredAtMs: number,
): Readonly<SecurityAuditRecord> {
return Object.freeze({
eventId,
requestId: `package_secret_binding_transition:${plan.transitionDigest}`,
operationId: 'plugin_package.secret.transition',
projectId: plan.nextTarget.projectId,
subject: principal.subject,
authenticationId: principal.authenticationId,
outcome: 'allowed',
reasons: Object.freeze([`owner_confirmed_secret_${plan.kind}`]),
fence,
occurredAtMs,
});
}
function verifySecretVersions(
client: DatabaseSync,
plan: Readonly<PluginPackageSecretBindingPlan>,
@@ -327,6 +530,10 @@ export function createLocalPluginPackageSecretBindingService(
const bindings = new LocalSqlitePluginPackageSecretBindingRepository(
authority,
);
const transitionReceipts =
new LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository(
authority,
);
const authorize = async (
principalValue: SecurityPrincipal,
@@ -538,5 +745,250 @@ export function createLocalPluginPackageSecretBindingService(
() => new LocalPluginPackageSecretBindingUnavailableError(),
);
},
async planTransition(
request: PlanLocalPluginPackageSecretBindingTransitionRequest,
) {
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 transition = loadTransition(
authority.client,
projectId,
packageName,
);
const previousGeneration = generationFrom(transition.previous);
const previousTarget = createPluginPackageSecretBindingTarget(
previousGeneration,
transition.previous.proposal.actionInput.manifest,
);
const previousBinding = bindings.findInTransaction(
previousTarget.generationDigest,
);
if (
transitionReceipts.findInTransaction(
transitionGeneration(transition.next).generationDigest,
)
) {
throw new LocalPluginPackageSecretBindingConflictError(
'staged generation transition is already committed',
);
}
const plan = createPluginPackageSecretBindingTransitionPlan({
previousTarget,
previousBinding,
previousAttemptGeneration: transition.previousAttemptGeneration,
nextGeneration: transitionGeneration(transition.next),
nextManifest: transition.next.proposal.actionInput.manifest,
assignments: request.assignments,
plannedAtMs,
});
if (plan.nextBindingPlan) {
verifySecretVersions(authority.client, plan.nextBindingPlan);
}
return plan;
},
() => new LocalPluginPackageSecretBindingUnavailableError(),
);
},
async executeTransition(
request: ExecuteLocalPluginPackageSecretBindingTransitionRequest,
) {
const plan = normalizePluginPackageSecretBindingTransitionPlan(
request.plan,
);
if (typeof request.confirmAuthorization !== 'function') {
throw new TypeError('confirmAuthorization is invalid');
}
const observedAtMs = timestamp(now(), 'transition execution clock');
const authorization = await authorize(
request.principal,
plan.nextTarget.projectId,
observedAtMs,
);
await request.confirmAuthorization();
return authority.enqueue(
async () => {
authority.client.exec('BEGIN IMMEDIATE');
try {
verifyPolicyFence(
authority.client,
authorization.principal,
authorization.fence,
plan.nextTarget.projectId,
);
const auditEventId = identity(
request.auditEventId,
'audit event ID',
UUID,
);
const auditsForPlan = authority.client
.prepare(
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents"
WHERE "request_id" = ? AND "operation_id" = ?
LIMIT 2`,
)
.all(
`package_secret_binding_transition:${plan.transitionDigest}`,
'plugin_package.secret.transition',
) as Row[];
if (
auditsForPlan.length > 1 ||
(auditsForPlan.length === 1 &&
rowText(auditsForPlan[0]!, 'eventId') !== auditEventId)
) {
throw new LocalPluginPackageSecretBindingConflictError(
'transition plan already has another audit identity',
);
}
const existingReceipt = transitionReceipts.findInTransaction(
plan.nextTarget.generationDigest,
);
if (existingReceipt) {
if (
existingReceipt.transitionPlan.transitionDigest !==
plan.transitionDigest ||
existingReceipt.authority.kind !== 'local-owner-confirmation' ||
existingReceipt.authority.evidenceDigest !==
plan.transitionDigest ||
auditsForPlan.length !== 1
) {
throw new LocalPluginPackageSecretBindingConflictError(
'generation is committed by another transition authority',
);
}
authority.client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
transitionDigest: plan.transitionDigest,
receiptDigest: existingReceipt.receiptDigest,
bindingDigest: existingReceipt.bindingDigest,
generationDigest: plan.nextTarget.generationDigest,
});
}
const transition = loadTransition(
authority.client,
plan.nextTarget.projectId,
plan.nextTarget.packageName,
);
const previousTarget = createPluginPackageSecretBindingTarget(
generationFrom(transition.previous),
transition.previous.proposal.actionInput.manifest,
);
const expected = createPluginPackageSecretBindingTransitionPlan({
previousTarget,
previousBinding: bindings.findInTransaction(
previousTarget.generationDigest,
),
previousAttemptGeneration: transition.previousAttemptGeneration,
nextGeneration: transitionGeneration(transition.next),
nextManifest: transition.next.proposal.actionInput.manifest,
assignments:
plan.nextBindingPlan?.entries.map(({ name, secretRef }) => ({
name,
secretRef,
})) ?? [],
plannedAtMs: plan.nextBindingPlan?.plannedAtMs ?? observedAtMs,
});
if (expected.transitionDigest !== plan.transitionDigest) {
throw new LocalPluginPackageSecretBindingConflictError(
'staged Package generation changed after transition planning',
);
}
if (plan.nextBindingPlan) {
verifySecretVersions(authority.client, plan.nextBindingPlan);
}
const audit = transitionAuditRecord(
plan,
auditEventId,
authorization.principal,
authorization.fence,
observedAtMs,
);
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 binding = createPluginPackageSecretBindingFromTransitionPlan(
plan,
'local-owner-confirmation',
plan.transitionDigest,
observedAtMs,
);
const bindingResult = binding
? bindings.publishInTransaction(binding)
: null;
const receipt = createPluginPackageSecretBindingTransitionReceipt({
transitionPlan: plan,
authority: {
kind: 'local-owner-confirmation',
evidenceDigest: plan.transitionDigest,
},
binding: bindingResult?.binding ?? null,
committedAtMs: observedAtMs,
});
const receiptResult =
transitionReceipts.publishInTransaction(receipt);
authority.client.exec('COMMIT');
return Object.freeze({
status: receiptResult.status,
transitionDigest: plan.transitionDigest,
receiptDigest: receiptResult.receipt.receiptDigest,
bindingDigest: receiptResult.receipt.bindingDigest,
generationDigest: plan.nextTarget.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(),
);
},
});
}
@@ -0,0 +1,247 @@
import type { DatabaseSync } from 'node:sqlite';
import {
MAX_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_JSON_BYTES,
normalizePluginPackageSecretBindingTransitionReceipt,
type PluginPackageSecretBindingTransitionReceipt,
type PluginPackageSecretBindingTransitionReceiptRepository,
} from '@qinglong/runtime-core/plugin-package-secret-binding-transition-receipt';
import {
PluginPackageSecretBindingConflictError,
PluginPackageSecretBindingUnavailableError,
} from '@qinglong/runtime-core/plugin-package-secret-binding';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
type Row = Record<string, unknown>;
const DIGEST = /^[0-9a-f]{64}$/;
function digest(value: unknown): string {
if (typeof value !== 'string' || !DIGEST.test(value)) {
throw new TypeError(
'Secret binding transition generation digest is invalid',
);
}
return value;
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new PluginPackageSecretBindingUnavailableError();
}
return value;
}
function nullableText(row: Row, key: string): string | null {
const value = row[key];
if (value !== null && typeof value !== 'string') {
throw new PluginPackageSecretBindingUnavailableError();
}
return value;
}
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PluginPackageSecretBindingUnavailableError();
}
return value as number;
}
function mapStorageError(error: unknown): Error {
if (
error instanceof PluginPackageSecretBindingConflictError ||
error instanceof PluginPackageSecretBindingUnavailableError ||
error instanceof TypeError
) {
return error;
}
if (
error &&
typeof error === 'object' &&
'code' in error &&
typeof error.code === 'string' &&
error.code.startsWith('SQLITE_CONSTRAINT')
) {
return new PluginPackageSecretBindingConflictError(
'durable transition receipt identity is already bound',
);
}
return new PluginPackageSecretBindingUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
export class LocalSqlitePluginPackageSecretBindingTransitionReceiptRepository
implements PluginPackageSecretBindingTransitionReceiptRepository
{
constructor(
readonly authority: LocalSqliteOperationAuthority | DatabaseSync,
) {
this.authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
private parse(
row: Row,
): Readonly<PluginPackageSecretBindingTransitionReceipt> {
try {
const receipt = normalizePluginPackageSecretBindingTransitionReceipt(
JSON.parse(text(row, 'receiptJson')),
);
if (
receipt.transitionPlan.nextTarget.generationDigest !==
text(row, 'generationDigest') ||
receipt.transitionPlan.transitionDigest !==
text(row, 'transitionDigest') ||
receipt.transitionPlan.nextTarget.projectId !==
text(row, 'projectId') ||
receipt.transitionPlan.nextTarget.packageName !==
text(row, 'packageName') ||
receipt.transitionPlan.nextTarget.installationId !==
text(row, 'installationId') ||
receipt.transitionPlan.nextTarget.lockDigest !==
text(row, 'lockDigest') ||
receipt.transitionPlan.nextTarget.generation !==
integer(row, 'generation') ||
receipt.transitionPlan.nextTarget.manifestDigest !==
text(row, 'manifestDigest') ||
receipt.transitionPlan.previousActiveLockDigest !==
text(row, 'previousActiveLockDigest') ||
receipt.authority.kind !== text(row, 'authorityKind') ||
receipt.authority.evidenceDigest !== text(row, 'evidenceDigest') ||
receipt.bindingDigest !== nullableText(row, 'bindingDigest') ||
receipt.committedAtMs !== integer(row, 'committedAtMs') ||
receipt.receiptDigest !== text(row, 'receiptDigest')
) {
throw new PluginPackageSecretBindingUnavailableError();
}
return receipt;
} catch (error) {
if (error instanceof PluginPackageSecretBindingUnavailableError)
throw error;
throw new PluginPackageSecretBindingUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
private findStored(
generationDigest: string,
): Readonly<PluginPackageSecretBindingTransitionReceipt> | null {
const row = (this.authority as LocalSqliteOperationAuthority).client
.prepare(
`SELECT generation_digest AS "generationDigest",
transition_digest AS "transitionDigest",
project_id AS "projectId", package_name AS "packageName",
installation_id AS "installationId", lock_digest AS "lockDigest",
generation, manifest_digest AS "manifestDigest",
previous_active_lock_digest AS "previousActiveLockDigest",
authority_kind AS "authorityKind", evidence_digest AS "evidenceDigest",
binding_digest AS "bindingDigest", committed_at_ms AS "committedAtMs",
receipt_digest AS "receiptDigest", receipt_json AS "receiptJson"
FROM "QingLong3PluginPackageSecretBindingTransitionReceipts"
WHERE generation_digest = ?`,
)
.get(generationDigest) as Row | undefined;
return row ? this.parse(row) : null;
}
findInTransaction(
generationDigestValue: string,
): Readonly<PluginPackageSecretBindingTransitionReceipt> | null {
try {
return this.findStored(digest(generationDigestValue));
} catch (error) {
throw mapStorageError(error);
}
}
async find(
generationDigestValue: string,
): Promise<Readonly<PluginPackageSecretBindingTransitionReceipt> | null> {
const normalized = digest(generationDigestValue);
return (this.authority as LocalSqliteOperationAuthority).enqueue(
async () => this.findStored(normalized),
() => new PluginPackageSecretBindingUnavailableError(),
);
}
publishInTransaction(
value: Readonly<PluginPackageSecretBindingTransitionReceipt>,
): Readonly<{
status: 'created' | 'existing';
receipt: Readonly<PluginPackageSecretBindingTransitionReceipt>;
}> {
const receipt = normalizePluginPackageSecretBindingTransitionReceipt(value);
const receiptJson = JSON.stringify(receipt);
if (
Buffer.byteLength(receiptJson, 'utf8') >
MAX_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_JSON_BYTES
) {
throw new TypeError(
'Secret binding transition receipt exceeds durable budget',
);
}
try {
const existing = this.findStored(
receipt.transitionPlan.nextTarget.generationDigest,
);
if (existing) {
if (JSON.stringify(existing) !== receiptJson) {
throw new PluginPackageSecretBindingConflictError(
'generation is bound to another transition receipt',
);
}
return Object.freeze({
status: 'existing' as const,
receipt: existing,
});
}
const target = receipt.transitionPlan.nextTarget;
const result = (this.authority as LocalSqliteOperationAuthority).client
.prepare(
`INSERT INTO "QingLong3PluginPackageSecretBindingTransitionReceipts" (
generation_digest, transition_digest, project_id, package_name,
installation_id, lock_digest, generation, manifest_digest,
previous_active_lock_digest, authority_kind, evidence_digest,
binding_digest, committed_at_ms, receipt_digest, receipt_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (generation_digest) DO NOTHING`,
)
.run(
target.generationDigest,
receipt.transitionPlan.transitionDigest,
target.projectId,
target.packageName,
target.installationId,
target.lockDigest,
target.generation,
target.manifestDigest,
receipt.transitionPlan.previousActiveLockDigest,
receipt.authority.kind,
receipt.authority.evidenceDigest,
receipt.bindingDigest,
receipt.committedAtMs,
receipt.receiptDigest,
receiptJson,
);
const stored = this.findStored(target.generationDigest);
if (!stored || JSON.stringify(stored) !== receiptJson) {
throw new PluginPackageSecretBindingConflictError(
'transition receipt target is not the reviewed staged generation',
);
}
return Object.freeze({
status:
result.changes === 1 ? ('created' as const) : ('existing' as const),
receipt: stored,
});
} catch (error) {
throw mapStorageError(error);
}
}
}
@@ -0,0 +1,62 @@
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_NAME =
'ql3_plugin_package_secret_binding_transition_receipt_guard' as const;
export const LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_SQL =
`
CREATE TRIGGER ${LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_NAME}
BEFORE INSERT ON "QingLong3PluginPackageSecretBindingTransitionReceipts"
FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstallHeads" AS head
JOIN "QingLong3PluginPackageInstalls" AS install
ON install.installation_id = head.installation_id
AND install.project_id = head.project_id
AND install.package_name = head.package_name
WHERE head.project_id = NEW.project_id
AND head.package_name = NEW.package_name
AND install.installation_id = NEW.installation_id
AND install.lock_digest = NEW.lock_digest
AND install.target_generation = NEW.generation
AND install.state = 'staged'
AND install.previous_active_lock_digest = NEW.previous_active_lock_digest
AND install.active_lock_digest = install.previous_active_lock_digest
AND json_extract(install.lock_json, '$.manifestDigest') = NEW.manifest_digest
AND install.target_generation = (
SELECT MAX(history.target_generation)
FROM "QingLong3PluginPackageInstalls" AS history
WHERE history.project_id = install.project_id
AND history.package_name = install.package_name
)
AND EXISTS (
SELECT 1
FROM "QingLong3PluginPackageInstalls" AS previous
WHERE previous.project_id = install.project_id
AND previous.package_name = install.package_name
AND previous.lock_digest = NEW.previous_active_lock_digest
AND previous.state = 'active'
AND previous.active_lock_digest = previous.lock_digest
AND previous.target_generation < install.target_generation
)
AND (
(NEW.binding_digest IS NULL AND
json_type(NEW.receipt_json, '$.transitionPlan.nextBindingPlan') = 'null')
OR
(NEW.binding_digest IS NOT NULL AND
json_type(NEW.receipt_json, '$.transitionPlan.nextBindingPlan') = 'object' AND
EXISTS (
SELECT 1
FROM "QingLong3PluginPackageSecretBindings" AS binding
WHERE binding.generation_digest = NEW.generation_digest
AND binding.binding_digest = NEW.binding_digest
AND binding.authority_kind = NEW.authority_kind
AND binding.evidence_digest = NEW.evidence_digest
AND binding.bound_at_ms = NEW.committed_at_ms
))
)
) THEN RAISE(ABORT,
'Plugin Package Secret binding transition receipt target is not reviewed staged generation')
END;
END
`.trim();
@@ -57,6 +57,7 @@ export type LocalProfileStorageBootstrapResult =
readonly pluginPackageInstalls: LocalSqliteRuntimeDatabase['pluginPackageInstalls'];
readonly pluginPackageMaterializedRevisions: LocalSqliteRuntimeDatabase['pluginPackageMaterializedRevisions'];
readonly pluginPackageSecretBindings: LocalSqliteRuntimeDatabase['pluginPackageSecretBindings'];
readonly pluginPackageActivationPrerequisite: LocalSqliteRuntimeDatabase['pluginPackageActivationPrerequisite'];
readonly pluginPackageTaskReconciliations: LocalSqliteRuntimeDatabase['pluginPackageTaskReconciliations'];
readonly pluginPackageAutomationPublications: LocalSqliteRuntimeDatabase['pluginPackageAutomationPublications'];
readonly projectToolDefinitionSnapshots: LocalSqliteRuntimeDatabase['projectToolDefinitionSnapshots'];
@@ -153,6 +154,8 @@ export async function bootstrapLocalProfileStorage(
pluginPackageMaterializedRevisions:
database.pluginPackageMaterializedRevisions,
pluginPackageSecretBindings: database.pluginPackageSecretBindings,
pluginPackageActivationPrerequisite:
database.pluginPackageActivationPrerequisite,
pluginPackageTaskReconciliations:
database.pluginPackageTaskReconciliations,
pluginPackageAutomationPublications:
@@ -4,13 +4,14 @@ import { localSqliteMigrationManifest } from '../migration/migrationManifest';
import { LocalSqliteMigrationStreamStore } from '../migration/migrationStreamStore';
import { LOCAL_PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGER_SQL } from '../plugin-package/pluginPackageSecretMaterializationSchemaContract';
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL } from '../plugin-package/secret-binding/pluginPackageSecretBindingTargetSchemaContract';
import { LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_SQL } from '../plugin-package/secret-binding/transitionReceiptSchemaContract';
import {
LOCAL_STEP_RUN_REFERENCE_TRIGGERS,
normalizeLocalSqliteSchemaSql,
} from '../run/stepRunSchemaContract';
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
export const LOCAL_SQLITE_CONTRACT_VERSION = 48;
export const LOCAL_SQLITE_CONTRACT_VERSION = 49;
const PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS = Object.freeze([
Object.freeze({
@@ -18,6 +19,11 @@ const PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGERS = Object.freeze([
tableName: 'QingLong3PluginPackageSecretBindings',
sql: LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TARGET_TRIGGER_SQL,
}),
Object.freeze({
name: 'ql3_plugin_package_secret_binding_transition_receipt_guard',
tableName: 'QingLong3PluginPackageSecretBindingTransitionReceipts',
sql: LOCAL_PLUGIN_PACKAGE_SECRET_BINDING_TRANSITION_RECEIPT_TRIGGER_SQL,
}),
]);
const PLUGIN_PACKAGE_SECRET_MATERIALIZATION_TRIGGERS = Object.freeze([
@@ -1181,6 +1187,30 @@ const REQUIRED_SCHEMA = Object.freeze({
'ql3_plugin_package_secret_binding_install_idx',
]),
}),
QingLong3PluginPackageSecretBindingTransitionReceipts: Object.freeze({
columns: Object.freeze([
'generation_digest',
'transition_digest',
'project_id',
'package_name',
'installation_id',
'lock_digest',
'generation',
'manifest_digest',
'previous_active_lock_digest',
'authority_kind',
'evidence_digest',
'binding_digest',
'committed_at_ms',
'receipt_digest',
'receipt_json',
]),
indexes: Object.freeze([
'ql3_plugin_package_secret_binding_transition_receipt_transition_uidx',
'ql3_plugin_package_secret_binding_transition_receipt_digest_uidx',
'ql3_plugin_package_secret_binding_transition_receipt_install_idx',
]),
}),
QingLong3ProjectToolDefinitionSnapshots: Object.freeze({
columns: Object.freeze([
'project_id',
@@ -2574,10 +2604,10 @@ export async function auditLocalSqliteReadiness(
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
capability.migration_id !==
'0095-plugin-package-secret-binding-target-guard' ||
'0097-plugin-package-secret-binding-transition-receipts' ||
typeof capability.capabilities !== 'string' ||
capability.capabilities !==
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
typeof capability.updated_at_ms !== 'number' ||
!Number.isSafeInteger(capability.updated_at_ms) ||
capability.updated_at_ms < 0
@@ -30,6 +30,7 @@ import type {
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import type { PluginPackageMaterializedRevisionRepository } from '@qinglong/runtime-core/plugin-package-resource-materialization';
import type { PluginPackageSecretBindingRepository } from '@qinglong/runtime-core/plugin-package-secret-binding';
import type { PluginPackageActivationPrerequisite } from '@qinglong/runtime-core/plugin-package-installation';
import type { PluginPackageTaskReconciliationRepository } from '@qinglong/runtime-core/plugin-package-task-reconciliation';
import type { PluginPackageTaskPublicationRecoverySource } from '@qinglong/runtime-core/plugin-package-task-publication';
import type { StepRunRepository } from '@qinglong/runtime-core/step-run';
@@ -111,6 +112,7 @@ export interface LocalSqliteRuntimeDatabase {
pluginPackageInstalls(): Promise<PluginPackageInstallRepository>;
pluginPackageMaterializedRevisions(): Promise<PluginPackageMaterializedRevisionRepository>;
pluginPackageSecretBindings(): Promise<PluginPackageSecretBindingRepository>;
pluginPackageActivationPrerequisite(): Promise<PluginPackageActivationPrerequisite>;
pluginPackageTaskReconciliations(): Promise<
PluginPackageTaskReconciliationRepository &
PluginPackageTaskPublicationRecoverySource
@@ -200,6 +202,9 @@ export async function openLocalSqliteRuntimeDatabase(
let pluginPackageSecretBindingsPromise:
| Promise<PluginPackageSecretBindingRepository>
| undefined;
let pluginPackageActivationPrerequisitePromise:
| Promise<PluginPackageActivationPrerequisite>
| undefined;
let pluginPackageTaskReconciliationsPromise:
| Promise<
PluginPackageTaskReconciliationRepository &
@@ -285,6 +290,17 @@ export async function openLocalSqliteRuntimeDatabase(
);
return pluginPackageSecretBindingsPromise;
},
pluginPackageActivationPrerequisite() {
pluginPackageActivationPrerequisitePromise ??= import(
'../plugin-package/secret-binding/activationPrerequisite.js'
).then(
({ LocalSqlitePluginPackageSecretBindingActivationPrerequisite }) =>
new LocalSqlitePluginPackageSecretBindingActivationPrerequisite(
authority,
),
);
return pluginPackageActivationPrerequisitePromise;
},
pluginPackageTaskReconciliations() {
pluginPackageTaskReconciliationsPromise ??= import(
'../plugin-package/pluginPackageTaskReconciliationRepository.js'
@@ -1273,6 +1273,60 @@ export const pluginPackageSecretBindings = sqliteTable(
],
);
export const pluginPackageSecretBindingTransitionReceipts = sqliteTable(
'QingLong3PluginPackageSecretBindingTransitionReceipts',
{
generationDigest: text('generation_digest').primaryKey(),
transitionDigest: text('transition_digest').notNull(),
projectId: text('project_id').notNull(),
packageName: text('package_name').notNull(),
installationId: text('installation_id')
.notNull()
.references(() => pluginPackageInstalls.installationId, {
onDelete: 'restrict',
onUpdate: 'restrict',
}),
lockDigest: text('lock_digest').notNull(),
generation: integer('generation').notNull(),
manifestDigest: text('manifest_digest').notNull(),
previousActiveLockDigest: text('previous_active_lock_digest').notNull(),
authorityKind: text('authority_kind').notNull(),
evidenceDigest: text('evidence_digest').notNull(),
bindingDigest: text('binding_digest').references(
() => pluginPackageSecretBindings.bindingDigest,
{ onDelete: 'restrict', onUpdate: 'restrict' },
),
committedAtMs: integer('committed_at_ms').notNull(),
receiptDigest: text('receipt_digest').notNull(),
receiptJson: text('receipt_json', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull(),
},
(table) => [
check(
'ql3_plugin_package_secret_binding_transition_receipt_identity_check',
sql`length(${table.projectId}) between 1 and 128 and length(${table.packageName}) between 1 and 63 and length(${table.installationId}) between 1 and 128 and ${table.generation} between 2 and 2147483647 and ${table.authorityKind} in ('approved-action-execution','local-owner-confirmation') and ${table.committedAtMs} >= 0`,
),
check(
'ql3_plugin_package_secret_binding_transition_receipt_digest_check',
sql`length(${table.generationDigest}) = 64 and ${table.generationDigest} not glob '*[^0-9a-f]*' and length(${table.transitionDigest}) = 64 and ${table.transitionDigest} not glob '*[^0-9a-f]*' and length(${table.lockDigest}) = 64 and ${table.lockDigest} not glob '*[^0-9a-f]*' and length(${table.manifestDigest}) = 64 and ${table.manifestDigest} not glob '*[^0-9a-f]*' and length(${table.previousActiveLockDigest}) = 64 and ${table.previousActiveLockDigest} not glob '*[^0-9a-f]*' and length(${table.evidenceDigest}) = 64 and ${table.evidenceDigest} not glob '*[^0-9a-f]*' and (${table.bindingDigest} is null or (length(${table.bindingDigest}) = 64 and ${table.bindingDigest} not glob '*[^0-9a-f]*')) and length(${table.receiptDigest}) = 64 and ${table.receiptDigest} not glob '*[^0-9a-f]*'`,
),
check(
'ql3_plugin_package_secret_binding_transition_receipt_json_check',
sql`length(cast(${table.receiptJson} as blob)) between 2 and 196608 and json_valid(${table.receiptJson}) and json_type(${table.receiptJson}) = 'object' and json_extract(${table.receiptJson}, '$.schema') = 'qinglong/plugin-package-secret-binding-transition-receipt@v1' and json_extract(${table.receiptJson}, '$.transitionPlan.transitionDigest') = ${table.transitionDigest} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.generationDigest') = ${table.generationDigest} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.projectId') = ${table.projectId} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.packageName') = ${table.packageName} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.installationId') = ${table.installationId} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.lockDigest') = ${table.lockDigest} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.generation') = ${table.generation} and json_extract(${table.receiptJson}, '$.transitionPlan.nextTarget.manifestDigest') = ${table.manifestDigest} and json_extract(${table.receiptJson}, '$.transitionPlan.previousActiveLockDigest') = ${table.previousActiveLockDigest} and json_extract(${table.receiptJson}, '$.authority.kind') = ${table.authorityKind} and json_extract(${table.receiptJson}, '$.authority.evidenceDigest') = ${table.evidenceDigest} and json_extract(${table.receiptJson}, '$.bindingDigest') is ${table.bindingDigest} and json_extract(${table.receiptJson}, '$.committedAtMs') = ${table.committedAtMs} and json_extract(${table.receiptJson}, '$.receiptDigest') = ${table.receiptDigest}`,
),
uniqueIndex(
'ql3_plugin_package_secret_binding_transition_receipt_transition_uidx',
).on(table.transitionDigest),
uniqueIndex(
'ql3_plugin_package_secret_binding_transition_receipt_digest_uidx',
).on(table.receiptDigest),
index(
'ql3_plugin_package_secret_binding_transition_receipt_install_idx',
).on(table.installationId, table.generationDigest),
],
);
export const projectToolDefinitionSnapshots = sqliteTable(
'QingLong3ProjectToolDefinitionSnapshots',
{
@@ -4945,6 +4999,7 @@ export const localSqliteSchema = Object.freeze({
pluginPackageInstallMutations,
pluginPackageMaterializedRevisions,
pluginPackageSecretBindings,
pluginPackageSecretBindingTransitionReceipts,
pluginPackageQuarantineEvents,
pluginPackageWithdrawalReceipts,
pluginPackageWithdrawalTasks,