feat(ql3): complete local secret config apply lifecycle

This commit is contained in:
whyour
2026-08-24 10:38:04 +08:00
parent 7142395897
commit 8ad8ed96bd
18 changed files with 3714 additions and 24 deletions
+5
View File
@@ -50,6 +50,11 @@
"require": "./dist/legacy-adoption/secret-and-config/environmentInspection.js",
"default": "./dist/legacy-adoption/secret-and-config/environmentInspection.js"
},
"./reconciliation-secret-and-config-application": {
"types": "./dist/legacy-adoption/secret-and-config/reconciliationSecretConfigApplication.d.ts",
"require": "./dist/legacy-adoption/secret-and-config/reconciliationSecretConfigApplication.js",
"default": "./dist/legacy-adoption/secret-and-config/reconciliationSecretConfigApplication.js"
},
"./reconciliation-automation-decision": {
"types": "./dist/legacy-adoption/reconciliationAutomationDecision.d.ts",
"require": "./dist/legacy-adoption/reconciliationAutomationDecision.js",
@@ -0,0 +1,612 @@
import { createHash } from 'node:crypto';
import type { DatabaseSync } from 'node:sqlite';
import {
openLocalSqliteSecretConfigApplicationDatabase,
type LocalSecretConfigApplicationReceipt,
type LocalSecretConfigApplicationSecret,
} from '@qinglong/local-sqlite/secret-config-application';
import {
encryptLocalSecretEnvelope,
ownedLocalSecretKeyMaterial,
} from '@qinglong/local-secret';
import {
LOCAL_SECRET_ALGORITHM,
normalizeLocalSecretEnvelope,
type LocalSecretEnvelope,
type LocalSecretKeyProvider,
} from '@qinglong/runtime-core/local-secret';
import {
ProjectPolicyEngine,
ProjectPolicyUnavailableError,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
visitLegacyEnvironmentAdoption,
type LegacyEnvironmentCandidate,
} from './environmentInspection';
const DIGEST = /^[0-9a-f]{64}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const STRONG_USER_ASSURANCES = new Set(['local_console']);
export type ReconciliationSecretConfigDecisionDisposition =
| 'apply_active_binding'
| 'preserve_disabled'
| 'skip';
export interface ReconciliationSecretConfigDecision {
readonly schemaVersion: 1;
readonly kind: 'qinglong3-local-reconciliation-secret-config-decision';
readonly candidateOrdinal: number;
readonly candidateDigest: string;
readonly disposition: ReconciliationSecretConfigDecisionDisposition;
readonly reason: string;
}
export interface ReconciliationSecretConfigRequirement {
readonly candidateOrdinal: number;
readonly candidateType: 'active_binding' | 'disabled_preservation';
readonly candidateDigest: string;
readonly sourceSetDigest: string;
readonly proposedSecretName: string;
readonly requirement:
| 'review_apply_binding'
| 'review_preserve_disabled'
| 'review_skip_conflict';
}
export interface PreparedReconciliationSecretConfigMaterial {
readonly ordinal: number;
readonly disposition: 'active_binding' | 'disabled_preservation';
readonly candidateDigest: string;
readonly sourceSetDigest: string;
readonly environmentName?: string;
readonly envelope: Readonly<LocalSecretEnvelope>;
}
export interface PrepareReconciliationSecretConfigApplicationOptions {
readonly sourceClient: DatabaseSync;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly mutationId: string;
readonly appliedAtMs: number;
readonly expectedLegacyInventoryDigest: string;
readonly decisions: readonly Readonly<ReconciliationSecretConfigDecision>[];
readonly openRequirements: () => Iterable<ReconciliationSecretConfigRequirement>;
readonly keyProvider: LocalSecretKeyProvider;
readonly visitMaterial: (
material: Readonly<PreparedReconciliationSecretConfigMaterial>,
) => void;
}
export interface PreparedReconciliationSecretConfigApplicationEvidence {
readonly legacyInventoryDigest: string;
readonly secretCount: number;
readonly activeBindingCount: number;
readonly disabledPreservationCount: number;
readonly materialSetDigest: string;
}
export interface ApplyPreparedReconciliationSecretConfigApplicationOptions {
readonly databasePath: string;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly mutationId: string;
readonly requestId: string;
readonly secretConfigPlanDigest: string;
readonly decisionDigest: string;
readonly candidateSetDigest: string;
readonly automationAdoptionSetDigest: string;
readonly materials: readonly Readonly<PreparedReconciliationSecretConfigMaterial>[];
readonly principal: Readonly<SecurityPrincipal>;
readonly appliedAtMs: number;
readonly authorizationAtMs?: number;
readonly busyTimeoutMs?: number;
readonly confirmAuthenticationAuthority: () => void | Promise<void>;
readonly confirmPreparedAuthority: () => void | Promise<void>;
}
export interface ApplyPreparedReconciliationSecretConfigApplicationResult {
readonly status: 'inserted' | 'existing';
readonly receipt: Readonly<LocalSecretConfigApplicationReceipt>;
}
export class ReconciliationSecretConfigApplicationError extends Error {
readonly code = 'RECONCILIATION_SECRET_CONFIG_APPLICATION_INVALID';
constructor(message: string, readonly cause?: unknown) {
super(`Reconciliation Secret/Config application is invalid: ${message}`);
this.name = 'ReconciliationSecretConfigApplicationError';
}
}
export class ReconciliationSecretConfigApplicationAuthenticationError extends Error {
readonly code =
'RECONCILIATION_SECRET_CONFIG_APPLICATION_AUTHENTICATION_REQUIRED';
constructor() {
super('Reconciliation Secret/Config application requires its reviewer');
this.name = 'ReconciliationSecretConfigApplicationAuthenticationError';
}
}
export class ReconciliationSecretConfigApplicationAuthorizationError extends Error {
readonly code = 'RECONCILIATION_SECRET_CONFIG_APPLICATION_FORBIDDEN';
constructor() {
super('Reconciliation Secret/Config application is not authorized');
this.name = 'ReconciliationSecretConfigApplicationAuthorizationError';
}
}
export class ReconciliationSecretConfigApplicationUnavailableError extends Error {
readonly code = 'RECONCILIATION_SECRET_CONFIG_APPLICATION_UNAVAILABLE';
constructor(readonly cause?: unknown) {
super('Reconciliation Secret/Config application is unavailable');
this.name = 'ReconciliationSecretConfigApplicationUnavailableError';
}
}
function fail(message: string, cause?: unknown): never {
throw new ReconciliationSecretConfigApplicationError(message, cause);
}
function exact(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const keys = [...expected].sort();
return (
actual.length === keys.length &&
actual.every((key, index) => key === keys[index])
);
}
function deterministicMutationId(batch: string, identity: string): string {
const bytes = createHash('sha256')
.update('qinglong3.secret-config-application-material.v1\0')
.update(batch)
.update('\0')
.update(identity)
.digest()
.subarray(0, 16);
bytes[6] = ((bytes[6] as number) & 0x0f) | 0x40;
bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
12,
16,
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function iterator<T>(value: Iterable<T>, label: string): Iterator<T> {
if (
!value ||
(typeof value !== 'object' && typeof value !== 'function') ||
typeof value[Symbol.iterator] !== 'function'
) {
return fail(`${label} is invalid`);
}
const selected = value[Symbol.iterator]();
if (!selected || typeof selected.next !== 'function') {
return fail(`${label} is invalid`);
}
return selected;
}
function normalizeRequirement(
value: ReconciliationSecretConfigRequirement,
): Readonly<ReconciliationSecretConfigRequirement> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exact(value, [
'candidateDigest',
'candidateOrdinal',
'candidateType',
'proposedSecretName',
'requirement',
'sourceSetDigest',
]) ||
!Number.isSafeInteger(value.candidateOrdinal) ||
value.candidateOrdinal < 1 ||
(value.candidateType !== 'active_binding' &&
value.candidateType !== 'disabled_preservation') ||
!DIGEST.test(value.candidateDigest) ||
!DIGEST.test(value.sourceSetDigest) ||
typeof value.proposedSecretName !== 'string' ||
value.proposedSecretName.length < 1 ||
![
'review_apply_binding',
'review_preserve_disabled',
'review_skip_conflict',
].includes(value.requirement)
) {
return fail('plan requirement is invalid');
}
return Object.freeze({ ...value });
}
function normalizeDecision(
value: ReconciliationSecretConfigDecision,
ordinal: number,
): Readonly<ReconciliationSecretConfigDecision> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exact(value, [
'candidateDigest',
'candidateOrdinal',
'disposition',
'kind',
'reason',
'schemaVersion',
]) ||
value.schemaVersion !== 1 ||
value.kind !== 'qinglong3-local-reconciliation-secret-config-decision' ||
value.candidateOrdinal !== ordinal ||
!DIGEST.test(value.candidateDigest) ||
!['apply_active_binding', 'preserve_disabled', 'skip'].includes(
value.disposition,
) ||
typeof value.reason !== 'string'
) {
return fail('review decision is invalid');
}
return Object.freeze({ ...value });
}
function assertDecision(
decision: Readonly<ReconciliationSecretConfigDecision>,
requirement: Readonly<ReconciliationSecretConfigRequirement>,
candidate: Readonly<LegacyEnvironmentCandidate>,
): void {
const expectedDisposition =
requirement.requirement === 'review_apply_binding'
? 'apply_active_binding'
: requirement.requirement === 'review_preserve_disabled'
? 'preserve_disabled'
: 'skip';
const sourceSetDigest =
candidate.kind === 'active_binding'
? candidate.sourceSetDigest
: candidate.sourceDigest;
if (
decision.candidateOrdinal !== requirement.candidateOrdinal ||
decision.candidateDigest !== requirement.candidateDigest ||
decision.candidateDigest !== candidate.candidateDigest ||
decision.disposition !== expectedDisposition ||
requirement.candidateType !== candidate.kind ||
requirement.sourceSetDigest !== sourceSetDigest
) {
fail('decision, plan and sealed candidate are detached');
}
}
export function normalizePreparedReconciliationSecretConfigMaterial(
value: unknown,
): Readonly<PreparedReconciliationSecretConfigMaterial> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return fail('prepared material must be an object');
}
const selected = value as Record<string, unknown>;
const active = selected.disposition === 'active_binding';
if (
!exact(selected, [
'candidateDigest',
'disposition',
'envelope',
...(active ? ['environmentName'] : []),
'ordinal',
'sourceSetDigest',
]) ||
!Number.isSafeInteger(selected.ordinal) ||
(selected.ordinal as number) < 1 ||
(!active && selected.disposition !== 'disabled_preservation') ||
!DIGEST.test(selected.candidateDigest as string) ||
!DIGEST.test(selected.sourceSetDigest as string) ||
(active &&
(typeof selected.environmentName !== 'string' ||
selected.environmentName.length < 1))
) {
return fail('prepared material is invalid');
}
let envelope: Readonly<LocalSecretEnvelope>;
try {
envelope = normalizeLocalSecretEnvelope(
selected.envelope as LocalSecretEnvelope,
);
} catch (error) {
return fail('prepared material envelope is invalid', error);
}
return Object.freeze({
ordinal: selected.ordinal as number,
disposition: selected.disposition as
| 'active_binding'
| 'disabled_preservation',
candidateDigest: selected.candidateDigest as string,
sourceSetDigest: selected.sourceSetDigest as string,
...(active ? { environmentName: selected.environmentName as string } : {}),
envelope,
});
}
export async function prepareReconciliationSecretConfigApplication(
options: Readonly<PrepareReconciliationSecretConfigApplicationOptions>,
): Promise<Readonly<PreparedReconciliationSecretConfigApplicationEvidence>> {
if (
!options ||
typeof options !== 'object' ||
(options.profile !== 'edge' && options.profile !== 'standalone') ||
!UUID_V4.test(options.mutationId) ||
!DIGEST.test(options.expectedLegacyInventoryDigest) ||
!Number.isSafeInteger(options.appliedAtMs) ||
options.appliedAtMs < 0 ||
!Array.isArray(options.decisions) ||
options.decisions.length < 1 ||
typeof options.openRequirements !== 'function' ||
typeof options.visitMaterial !== 'function' ||
!options.keyProvider ||
typeof options.keyProvider.active !== 'function'
) {
fail('preparation options are invalid');
}
const requirements = iterator(
options.openRequirements(),
'plan requirement stream',
);
const decisions = options.decisions.map((decision, index) =>
normalizeDecision(decision, index + 1),
);
if (decisions.some((decision) => decision.disposition === 'skip')) {
fail('manual-required decision cannot enter application');
}
const material = ownedLocalSecretKeyMaterial(
await options.keyProvider.active(),
);
const materialHash = createHash('sha256').update(
'qinglong3.reconciliation-secret-config-material-set.v1\0',
);
let secretCount = 0;
let activeBindingCount = 0;
let disabledPreservationCount = 0;
try {
const inventory = visitLegacyEnvironmentAdoption(options.sourceClient, {
profile: options.profile,
visitCandidate(candidate) {
const requirementNext = requirements.next();
if (requirementNext.done) fail('sealed source exceeds plan candidates');
const requirement = normalizeRequirement(requirementNext.value);
const decision = decisions[requirement.candidateOrdinal - 1];
if (!decision) fail('decision stream is shorter than its plan');
assertDecision(decision, requirement, candidate);
if (decision.disposition === 'skip') return;
secretCount += 1;
const secretMutationId = deterministicMutationId(
options.mutationId,
`${requirement.candidateOrdinal}\0${requirement.candidateDigest}`,
);
const envelope = encryptLocalSecretEnvelope(
{
projectId: options.projectId,
name: requirement.proposedSecretName,
version: 1,
mutationId: secretMutationId,
keyId: material.keyId,
algorithm: LOCAL_SECRET_ALGORITHM,
createdAtMs: options.appliedAtMs,
},
candidate.value,
material.key,
);
const prepared = Object.freeze({
ordinal: secretCount,
disposition: candidate.kind,
candidateDigest: candidate.candidateDigest,
sourceSetDigest: requirement.sourceSetDigest,
...(candidate.kind === 'active_binding'
? { environmentName: candidate.environmentName }
: {}),
envelope,
});
materialHash.update('\0').update(JSON.stringify(prepared));
options.visitMaterial(prepared);
if (candidate.kind === 'active_binding') activeBindingCount += 1;
else disabledPreservationCount += 1;
},
});
const extraRequirement = requirements.next();
if (!extraRequirement.done || decisions.length !== secretCount) {
fail('decision, plan and sealed candidate counts differ');
}
if (inventory.inventoryDigest !== options.expectedLegacyInventoryDigest) {
fail('sealed legacy inventory drifted');
}
return Object.freeze({
legacyInventoryDigest: inventory.inventoryDigest,
secretCount,
activeBindingCount,
disabledPreservationCount,
materialSetDigest: materialHash.digest('hex'),
});
} finally {
material.key.fill(0);
}
}
function strongPrincipal(
value: Readonly<SecurityPrincipal>,
atMs: number,
): Readonly<SecurityPrincipal> {
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(value, atMs);
} catch {
throw new ReconciliationSecretConfigApplicationAuthenticationError();
}
if (
principal.subject.type !== 'user' ||
!STRONG_USER_ASSURANCES.has(principal.assurance)
) {
throw new ReconciliationSecretConfigApplicationAuthenticationError();
}
return principal;
}
function audit(
eventId: string,
operationId: 'secret-config.apply' | 'secret.create',
options: Readonly<ApplyPreparedReconciliationSecretConfigApplicationOptions>,
principal: Readonly<SecurityPrincipal>,
decision: Readonly<SecurityPolicyDecision>,
): Readonly<SecurityAuditRecord> {
return normalizeSecurityAuditRecord({
eventId,
requestId: options.requestId,
operationId,
projectId: options.projectId,
subject: principal.subject,
authenticationId: principal.authenticationId,
outcome: 'allowed',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs: options.appliedAtMs,
});
}
export async function applyPreparedReconciliationSecretConfigApplication(
options: Readonly<ApplyPreparedReconciliationSecretConfigApplicationOptions>,
): Promise<Readonly<ApplyPreparedReconciliationSecretConfigApplicationResult>> {
if (
!options ||
typeof options !== 'object' ||
!UUID_V4.test(options.mutationId) ||
!REQUEST_ID.test(options.requestId) ||
(options.profile !== 'edge' && options.profile !== 'standalone') ||
![
options.secretConfigPlanDigest,
options.decisionDigest,
options.candidateSetDigest,
options.automationAdoptionSetDigest,
].every((entry) => DIGEST.test(entry)) ||
!Array.isArray(options.materials) ||
options.materials.length < 1 ||
!Number.isSafeInteger(options.appliedAtMs) ||
options.appliedAtMs < 0 ||
(options.authorizationAtMs !== undefined &&
(!Number.isSafeInteger(options.authorizationAtMs) ||
options.authorizationAtMs < options.appliedAtMs)) ||
typeof options.confirmAuthenticationAuthority !== 'function' ||
typeof options.confirmPreparedAuthority !== 'function'
) {
fail('apply options are invalid');
}
const materials = options.materials.map(
normalizePreparedReconciliationSecretConfigMaterial,
);
if (materials.some((entry, index) => entry.ordinal !== index + 1)) {
fail('prepared material ordinal drifted');
}
const database = await openLocalSqliteSecretConfigApplicationDatabase({
databasePath: options.databasePath,
profile: options.profile,
...(options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: options.busyTimeoutMs }),
});
try {
const principal = strongPrincipal(
options.principal,
options.authorizationAtMs ?? options.appliedAtMs,
);
await options.confirmAuthenticationAuthority();
const policy = new ProjectPolicyEngine(database.projectPolicy);
let decision: Readonly<SecurityPolicyDecision>;
try {
decision = await policy.authorize(
principal,
options.projectId,
'secret.manage',
);
} catch (error) {
if (error instanceof ProjectPolicyUnavailableError) {
throw new ReconciliationSecretConfigApplicationUnavailableError(error);
}
throw error;
}
if (decision.effect !== 'allow') {
throw new ReconciliationSecretConfigApplicationAuthorizationError();
}
if (!decision.fence || decision.fence.bindingVersion === null) {
throw new ReconciliationSecretConfigApplicationUnavailableError();
}
const secrets: Readonly<LocalSecretConfigApplicationSecret>[] =
materials.map((entry) =>
Object.freeze({
...entry,
audit: audit(
entry.envelope.mutationId,
'secret.create',
options,
principal,
decision,
),
}),
);
const publication = await database.publisher.publish({
mutationId: options.mutationId,
projectId: options.projectId,
profile: options.profile,
secretConfigPlanDigest: options.secretConfigPlanDigest,
decisionDigest: options.decisionDigest,
candidateSetDigest: options.candidateSetDigest,
automationAdoptionSetDigest: options.automationAdoptionSetDigest,
subject: principal.subject,
fence: decision.fence,
audit: audit(
options.mutationId,
'secret-config.apply',
options,
principal,
decision,
),
secrets,
appliedAtMs: options.appliedAtMs,
async confirmExternalAuthority() {
await options.confirmAuthenticationAuthority();
await options.confirmPreparedAuthority();
},
});
return Object.freeze({
status: publication.status,
receipt: publication.application.receipt,
});
} catch (error) {
if (
error instanceof ReconciliationSecretConfigApplicationError ||
error instanceof
ReconciliationSecretConfigApplicationAuthenticationError ||
error instanceof
ReconciliationSecretConfigApplicationAuthorizationError ||
error instanceof ReconciliationSecretConfigApplicationUnavailableError
) {
throw error;
}
throw new ReconciliationSecretConfigApplicationUnavailableError(error);
} finally {
await database.close();
}
}
@@ -40,6 +40,9 @@ export type LocalCutoverInstanceHeadState =
| 'reconciliation_secret_config_planned'
| 'reconciliation_secret_config_decision_prepared'
| 'reconciliation_secret_config_reviewed'
| 'reconciliation_secret_config_apply_prepared'
| 'reconciliation_secret_config_applied'
| 'reconciliation_secret_config_rolled_back'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -189,6 +192,9 @@ function parseHead(value: unknown): Readonly<LocalCutoverInstanceHead> {
head.state !== 'reconciliation_secret_config_planned' &&
head.state !== 'reconciliation_secret_config_decision_prepared' &&
head.state !== 'reconciliation_secret_config_reviewed' &&
head.state !== 'reconciliation_secret_config_apply_prepared' &&
head.state !== 'reconciliation_secret_config_applied' &&
head.state !== 'reconciliation_secret_config_rolled_back' &&
head.state !== 'reconciliation_completed' &&
head.state !== 'rollback_prepared' &&
head.state !== 'legacy_restart_requested' &&
@@ -373,6 +379,9 @@ export function advanceLocalCutoverInstanceHead(
| 'reconciliation_secret_config_planned'
| 'reconciliation_secret_config_decision_prepared'
| 'reconciliation_secret_config_reviewed'
| 'reconciliation_secret_config_apply_prepared'
| 'reconciliation_secret_config_applied'
| 'reconciliation_secret_config_rolled_back'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -429,6 +438,9 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_secret_config_planned' ||
current.state === 'reconciliation_secret_config_decision_prepared' ||
current.state === 'reconciliation_secret_config_reviewed' ||
current.state === 'reconciliation_secret_config_apply_prepared' ||
current.state === 'reconciliation_secret_config_applied' ||
current.state === 'reconciliation_secret_config_rolled_back' ||
current.state === 'reconciliation_completed' ||
current.state === 'legacy_restart_requested' ||
current.state === 'legacy_running' ||
@@ -481,6 +493,12 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_secret_config_planned') ||
(state === 'reconciliation_secret_config_reviewed' &&
current.state === 'reconciliation_secret_config_decision_prepared') ||
(state === 'reconciliation_secret_config_apply_prepared' &&
current.state === 'reconciliation_secret_config_reviewed') ||
(state === 'reconciliation_secret_config_applied' &&
current.state === 'reconciliation_secret_config_apply_prepared') ||
(state === 'reconciliation_secret_config_rolled_back' &&
current.state === 'reconciliation_secret_config_applied') ||
(state === 'reconciliation_completed' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
@@ -170,6 +170,14 @@ import {
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
} from './reconciliation/application/secret-and-config/decisionCoordinator';
import {
applyLocalReconciliationSecretConfig,
applyLocalReconciliationSecretConfigCommandFile,
rollbackLocalReconciliationSecretConfigApply,
rollbackLocalReconciliationSecretConfigApplyCommandFile,
verifyLocalReconciliationSecretConfigApply,
verifyLocalReconciliationSecretConfigApplyCommandFile,
} from './reconciliation/application/secret-and-config/application/coordinator';
import {
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
@@ -234,6 +242,12 @@ export {
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
readLocalReconciliationSecretConfigDecisionTerminal,
applyLocalReconciliationSecretConfig,
applyLocalReconciliationSecretConfigCommandFile,
verifyLocalReconciliationSecretConfigApply,
verifyLocalReconciliationSecretConfigApplyCommandFile,
rollbackLocalReconciliationSecretConfigApply,
rollbackLocalReconciliationSecretConfigApplyCommandFile,
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
readLocalReconciliationRunHistoryTerminal,
@@ -381,6 +395,17 @@ export {
type LocalReconciliationSecretConfigDecisionDependencies,
type LocalReconciliationSecretConfigDecisionTerminal,
} from './reconciliation/application/secret-and-config/decisionCoordinator';
export {
normalizeLocalReconciliationSecretConfigApplyCommand,
normalizeLocalReconciliationSecretConfigApplyRollbackCommand,
normalizeLocalReconciliationSecretConfigApplyVerifyCommand,
type LocalReconciliationSecretConfigApplyCommand,
type LocalReconciliationSecretConfigApplyOptions,
type LocalReconciliationSecretConfigApplyResult,
type LocalReconciliationSecretConfigApplyRollbackCommand,
type LocalReconciliationSecretConfigApplyVerifyCommand,
} from './reconciliation/application/secret-and-config/application/contract';
export { type LocalReconciliationSecretConfigApplyDependencies } from './reconciliation/application/secret-and-config/application/coordinator';
export {
type LocalReconciliationSecretConfigDecision,
type LocalReconciliationSecretConfigDecisionDisposition,
@@ -38,6 +38,9 @@ import {
prepareLocalReconciliationSecretConfigDecisionCommandFile,
commitLocalReconciliationSecretConfigDecisionCommandFile,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
applyLocalReconciliationSecretConfigCommandFile,
verifyLocalReconciliationSecretConfigApplyCommandFile,
rollbackLocalReconciliationSecretConfigApplyCommandFile,
preserveLocalReconciliationRunHistoryCommandFile,
verifyLocalReconciliationRunHistoryCommandFile,
completeLocalReconciliationCommandFile,
@@ -57,7 +60,7 @@ import {
} from './localDeployment';
const USAGE =
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|reconciliation-review-commit|reconciliation-review-verify|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-secret-config-plan|reconciliation-secret-config-verify|reconciliation-secret-config-decision-prepare|reconciliation-secret-config-decision-commit|reconciliation-secret-config-decision-verify|reconciliation-run-history-preserve|reconciliation-run-history-verify|reconciliation-complete|reconciliation-complete-verify|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
'Usage: ql3-local-deploy <prepare|adopted-prepare|adopted-verify|status|service-intent-prepare|service-outcome-consume|service-cutover-consume|service-legacy-rollback-prepare|service-legacy-rollback-authorize|service-legacy-rollback-consume|cutover-legacy-stop|cutover-target-start|cutover-target-restart|cutover-target-stop|cutover-legacy-rollback-prepare|cutover-legacy-rollback-commit|cutover-legacy-readiness-probe|cutover-manual-diagnose|cutover-manual-resolution-prepare|cutover-manual-resolution-commit|reconciliation-capture-prepare|reconciliation-capture-commit|reconciliation-capture-verify|reconciliation-plan-prepare|reconciliation-plan-commit|reconciliation-plan-verify|reconciliation-review-prepare|reconciliation-review-diagnostics|reconciliation-review-commit|reconciliation-review-verify|reconciliation-application-prepare|reconciliation-application-commit|reconciliation-application-verify|reconciliation-automation-plan|reconciliation-automation-verify|reconciliation-automation-decision-prepare|reconciliation-automation-decision-commit|reconciliation-automation-decision-verify|reconciliation-automation-apply|reconciliation-automation-apply-verify|reconciliation-automation-apply-rollback|reconciliation-secret-config-plan|reconciliation-secret-config-verify|reconciliation-secret-config-decision-prepare|reconciliation-secret-config-decision-commit|reconciliation-secret-config-decision-verify|reconciliation-secret-config-apply|reconciliation-secret-config-apply-verify|reconciliation-secret-config-apply-rollback|reconciliation-run-history-preserve|reconciliation-run-history-verify|reconciliation-complete|reconciliation-complete-verify|compose-revision|compose-preflight|compose-apply|compose-restore-prepare|compose-restore-commit|compose-evidence-collect-prepare|compose-evidence-collect-commit> --command-file /absolute/private-command.json';
async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -112,6 +115,9 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-secret-config-decision-prepare' &&
argv[0] !== 'reconciliation-secret-config-decision-commit' &&
argv[0] !== 'reconciliation-secret-config-decision-verify' &&
argv[0] !== 'reconciliation-secret-config-apply' &&
argv[0] !== 'reconciliation-secret-config-apply-verify' &&
argv[0] !== 'reconciliation-secret-config-apply-rollback' &&
argv[0] !== 'reconciliation-run-history-preserve' &&
argv[0] !== 'reconciliation-run-history-verify' &&
argv[0] !== 'reconciliation-complete' &&
@@ -238,6 +244,12 @@ async function main(argv: readonly string[]): Promise<void> {
? commitLocalReconciliationSecretConfigDecisionCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-decision-verify'
? verifyLocalReconciliationSecretConfigDecisionCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-apply'
? applyLocalReconciliationSecretConfigCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-apply-verify'
? verifyLocalReconciliationSecretConfigApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-apply-rollback'
? rollbackLocalReconciliationSecretConfigApplyCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-preserve'
? preserveLocalReconciliationRunHistoryCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-verify'
@@ -0,0 +1,410 @@
import path from 'node:path';
import { currentIdentity } from '../../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../../foundation/error';
const DIGEST = /^[0-9a-f]{64}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const UUID_V7 =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const SAFE_PATH = /^\/[A-Za-z0-9._/@-]+$/;
const SAFE_ID = /^[A-Za-z0-9._:@-]{1,256}$/;
export interface LocalReconciliationSecretConfigApplyOptions {
readonly deploymentRoot: string;
readonly applicationRoot: string;
readonly secretConfigRoot: string;
readonly secretConfigDecisionRoot: string;
readonly secretConfigApplyRoot: string;
readonly targetDatabasePath: string;
readonly secretKeyringPath: string;
readonly ownerPepperKeyringDirectory: string;
readonly credentialFilePath: string;
readonly allowRootService: boolean;
readonly busyTimeoutMs?: number;
}
export interface LocalReconciliationSecretConfigApplyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.apply';
readonly options: Readonly<LocalReconciliationSecretConfigApplyOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedDecisionDigest: string;
expectedHeadDigest: string;
mutationId: string;
requestId: string;
appliedAtMs: number;
}>;
}
export interface LocalReconciliationSecretConfigApplyVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.apply.verify';
readonly options: Readonly<LocalReconciliationSecretConfigApplyOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedApplyDigest: string;
}>;
}
export interface LocalReconciliationSecretConfigApplyRollbackCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.apply.rollback';
readonly options: Readonly<LocalReconciliationSecretConfigApplyOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedApplyDigest: string;
expectedHeadDigest: string;
rolledBackAtMs: number;
}>;
}
export interface LocalReconciliationSecretConfigApplyResult {
readonly schemaVersion: 1;
readonly operation:
| LocalReconciliationSecretConfigApplyCommand['operation']
| LocalReconciliationSecretConfigApplyVerifyCommand['operation']
| LocalReconciliationSecretConfigApplyRollbackCommand['operation'];
readonly status: 'applied' | 'existing' | 'verified' | 'rolled_back';
readonly state:
| 'reconciliation_secret_config_applied'
| 'reconciliation_secret_config_rolled_back';
readonly decisionId: string;
readonly secretConfigId: string;
readonly applyDigest: string;
readonly publicationDigest: string;
readonly activeBindingCount: number;
readonly disabledPreservationCount: number;
readonly updatedTaskCount: number;
readonly updatedTriggerCount: number;
readonly backupSha256: string;
readonly instanceHeadDigest: string;
}
function fail(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config apply ${message}`,
);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
fail(`${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exact(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
fail(`${label} shape is invalid`);
}
}
function safePath(value: unknown, label: string): string {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.parse(value).root === value ||
path.normalize(value) !== value ||
value.includes('\0') ||
value.includes('//') ||
!SAFE_PATH.test(value) ||
Buffer.byteLength(value, 'utf8') > 4096
) {
fail(`${label} must be a safe non-root absolute path`);
}
return value;
}
function overlaps(left: string, right: string): boolean {
const relative = path.relative(left, right);
return (
relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))
);
}
function normalizeOptions(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyOptions> {
const selected = record(value, 'options');
const hasBusyTimeout = Object.hasOwn(selected, 'busyTimeoutMs');
exact(
selected,
[
'allowRootService',
'applicationRoot',
'credentialFilePath',
'deploymentRoot',
'ownerPepperKeyringDirectory',
'secretConfigApplyRoot',
'secretConfigDecisionRoot',
'secretConfigRoot',
'secretKeyringPath',
'targetDatabasePath',
...(hasBusyTimeout ? ['busyTimeoutMs'] : []),
],
'options',
);
const identity = currentIdentity();
if (
typeof selected.allowRootService !== 'boolean' ||
(identity.uid === 0) !== selected.allowRootService
) {
fail('command identity is invalid');
}
const normalized = {
deploymentRoot: safePath(selected.deploymentRoot, 'deploymentRoot'),
applicationRoot: safePath(selected.applicationRoot, 'applicationRoot'),
secretConfigRoot: safePath(selected.secretConfigRoot, 'secretConfigRoot'),
secretConfigDecisionRoot: safePath(
selected.secretConfigDecisionRoot,
'secretConfigDecisionRoot',
),
secretConfigApplyRoot: safePath(
selected.secretConfigApplyRoot,
'secretConfigApplyRoot',
),
targetDatabasePath: safePath(
selected.targetDatabasePath,
'targetDatabasePath',
),
secretKeyringPath: safePath(
selected.secretKeyringPath,
'secretKeyringPath',
),
ownerPepperKeyringDirectory: safePath(
selected.ownerPepperKeyringDirectory,
'ownerPepperKeyringDirectory',
),
credentialFilePath: safePath(
selected.credentialFilePath,
'credentialFilePath',
),
};
const roots = [
normalized.deploymentRoot,
normalized.applicationRoot,
normalized.secretConfigRoot,
normalized.secretConfigDecisionRoot,
normalized.secretConfigApplyRoot,
];
for (let left = 0; left < roots.length; left += 1) {
for (let right = left + 1; right < roots.length; right += 1) {
if (
overlaps(roots[left]!, roots[right]!) ||
overlaps(roots[right]!, roots[left]!)
) {
fail('authority roots overlap');
}
}
}
for (const candidate of [
normalized.secretKeyringPath,
normalized.ownerPepperKeyringDirectory,
normalized.credentialFilePath,
]) {
const relative = path.relative(normalized.deploymentRoot, candidate);
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
fail('authentication or Secret material must be below deploymentRoot');
}
}
if (
roots.some(
(root) =>
overlaps(root, normalized.targetDatabasePath) ||
overlaps(normalized.targetDatabasePath, root),
)
) {
fail('targetDatabasePath overlaps an authority root');
}
if (
selected.busyTimeoutMs !== undefined &&
(!Number.isSafeInteger(selected.busyTimeoutMs) ||
(selected.busyTimeoutMs as number) < 1 ||
(selected.busyTimeoutMs as number) > 60_000)
) {
fail('busyTimeoutMs is invalid');
}
return Object.freeze({
...normalized,
allowRootService: selected.allowRootService as boolean,
...(selected.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: selected.busyTimeoutMs as number }),
});
}
function command(value: unknown, operation: string) {
const selected = record(value, 'command');
exact(
selected,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
fail('command version or operation is invalid');
}
return {
options: normalizeOptions(selected.options),
request: record(selected.request, 'request'),
} as const;
}
function id(value: unknown, pattern: RegExp, label: string): string {
if (typeof value !== 'string' || !pattern.test(value)) {
fail(`${label} is invalid`);
}
return value;
}
function digest(value: unknown, label: string): string {
return id(value, DIGEST, label);
}
export function normalizeLocalReconciliationSecretConfigApplyCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.apply',
);
exact(
selected.request,
[
'appliedAtMs',
'decisionId',
'expectedDecisionDigest',
'expectedHeadDigest',
'mutationId',
'requestId',
'secretConfigId',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.appliedAtMs) ||
(selected.request.appliedAtMs as number) < 0 ||
typeof selected.request.requestId !== 'string' ||
!SAFE_ID.test(selected.request.requestId)
) {
fail('apply request timestamp or requestId is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply',
options: selected.options,
request: Object.freeze({
decisionId: id(selected.request.decisionId, UUID_V7, 'decisionId'),
secretConfigId: id(
selected.request.secretConfigId,
UUID_V4,
'secretConfigId',
),
expectedDecisionDigest: digest(
selected.request.expectedDecisionDigest,
'expectedDecisionDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
mutationId: id(selected.request.mutationId, UUID_V4, 'mutationId'),
requestId: selected.request.requestId,
appliedAtMs: selected.request.appliedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationSecretConfigApplyVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyVerifyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.apply.verify',
);
exact(
selected.request,
['decisionId', 'expectedApplyDigest', 'secretConfigId'],
'request',
);
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.verify',
options: selected.options,
request: Object.freeze({
decisionId: id(selected.request.decisionId, UUID_V7, 'decisionId'),
secretConfigId: id(
selected.request.secretConfigId,
UUID_V4,
'secretConfigId',
),
expectedApplyDigest: digest(
selected.request.expectedApplyDigest,
'expectedApplyDigest',
),
}),
});
}
export function normalizeLocalReconciliationSecretConfigApplyRollbackCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyRollbackCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.apply.rollback',
);
exact(
selected.request,
[
'decisionId',
'expectedApplyDigest',
'expectedHeadDigest',
'rolledBackAtMs',
'secretConfigId',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.rolledBackAtMs) ||
(selected.request.rolledBackAtMs as number) < 0
) {
fail('rolledBackAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.rollback',
options: selected.options,
request: Object.freeze({
decisionId: id(selected.request.decisionId, UUID_V7, 'decisionId'),
secretConfigId: id(
selected.request.secretConfigId,
UUID_V4,
'secretConfigId',
),
expectedApplyDigest: digest(
selected.request.expectedApplyDigest,
'expectedApplyDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
rolledBackAtMs: selected.request.rolledBackAtMs as number,
}),
});
}
@@ -0,0 +1,977 @@
import fs from 'node:fs';
import {
applyPreparedReconciliationSecretConfigApplication,
prepareReconciliationSecretConfigApplication,
type PreparedReconciliationSecretConfigMaterial,
} from '@qinglong/local-admin/reconciliation-secret-and-config-application';
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
import {
establishAuthenticatedLocalCommand,
type AuthenticatedLocalCommand,
} from '@qinglong/local-owner-console/authenticated-command';
import { LocalSecretKeyringFileProvider } from '@qinglong/local-secret';
import { openLocalSqliteAuthenticationReadDatabase } from '@qinglong/local-sqlite/authentication-read';
import {
createLocalSqliteRolloutBackup,
inspectLocalSqliteSnapshot,
restoreLocalSqliteSnapshot,
} from '@qinglong/local-sqlite/rollout-safety';
import type { SecurityPrincipal } from '@qinglong/runtime-core/security';
import { currentIdentity } from '../../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../../foundation/error';
import {
publishExactFile,
validatePrivateDirectory,
} from '../../../../foundation/files';
import {
advanceLocalCutoverInstanceHead,
readLocalCutoverInstanceHead,
type LocalCutoverInstanceHead,
} from '../../../../cutover/instanceLineage';
import { readLocalReconciliationCaptureIntent } from '../../../preparation';
import {
inspectLocalReconciliationSealedBundle,
withLocalReconciliationSealedDatabaseAsync,
} from '../../../sealed-bundle/reader';
import { proveLocalReconciliationStoppedState } from '../../../stoppedProof';
import {
normalizeLocalReconciliationSecretConfigApplyCommand,
normalizeLocalReconciliationSecretConfigApplyRollbackCommand,
normalizeLocalReconciliationSecretConfigApplyVerifyCommand,
type LocalReconciliationSecretConfigApplyCommand,
type LocalReconciliationSecretConfigApplyOptions,
type LocalReconciliationSecretConfigApplyResult,
} from './contract';
import {
buildLocalReconciliationSecretConfigApplyIntent,
buildLocalReconciliationSecretConfigApplyReceipt,
buildLocalReconciliationSecretConfigRollbackReceipt,
localReconciliationSecretConfigApplyEvidenceContents,
type LocalReconciliationSecretConfigApplyIntent,
type LocalReconciliationSecretConfigApplyReceipt,
} from './evidence';
import {
discardUnpreparedLocalReconciliationSecretConfigMaterials,
ensureLocalReconciliationSecretConfigApplyLayout,
localReconciliationSecretConfigApplyPaths,
prepareLocalReconciliationSecretConfigRollbackSource,
publishLocalReconciliationSecretConfigMaterials,
readLocalReconciliationSecretConfigApplyIntent,
readLocalReconciliationSecretConfigApplyReceipt,
readLocalReconciliationSecretConfigMaterials,
readLocalReconciliationSecretConfigRollbackReceipt,
sealLocalReconciliationSecretConfigAppliedStorage,
sealLocalReconciliationSecretConfigRolledBackStorage,
validateLocalReconciliationSecretConfigAppliedStorage,
validateLocalReconciliationSecretConfigApplyCatalog,
validateLocalReconciliationSecretConfigRolledBackStorage,
} from './storage';
import { readLocalReconciliationSecretConfigDecisionTerminal } from '../decisionCoordinator';
import { createLocalReconciliationSecretConfigDecisionRequirementFactory } from '../planReader';
const MAX_AUTHENTICATION_AGE_MS = 5 * 60 * 1_000;
const APPLY_HEAD_STATES = Object.freeze([
'reconciliation_secret_config_apply_prepared',
'reconciliation_secret_config_applied',
'reconciliation_secret_config_rolled_back',
] as const);
type AuthenticationDatabase = Awaited<
ReturnType<typeof openLocalSqliteAuthenticationReadDatabase>
>;
export interface LocalReconciliationSecretConfigApplyDependencies {
readonly openAuthenticationDatabase?: typeof openLocalSqliteAuthenticationReadDatabase;
readonly authenticate?: typeof establishAuthenticatedLocalCommand;
readonly prepareApplication?: typeof prepareReconciliationSecretConfigApplication;
readonly applyApplication?: typeof applyPreparedReconciliationSecretConfigApplication;
readonly createBackup?: typeof createLocalSqliteRolloutBackup;
readonly inspectSnapshot?: typeof inspectLocalSqliteSnapshot;
readonly restoreSnapshot?: typeof restoreLocalSqliteSnapshot;
readonly afterMaterialPublished?: () => void;
readonly afterBackupPublished?: () => void;
readonly afterPreparedHead?: () => void;
readonly afterDatabaseCommit?: () => void;
readonly afterReceiptPublished?: () => void;
readonly afterAppliedHead?: () => void;
readonly afterAppliedSeal?: () => void;
readonly afterRestore?: () => void;
readonly afterRollbackReceipt?: () => void;
readonly afterRollbackHead?: () => void;
readonly afterRollbackSeal?: () => void;
}
function fail(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config apply ${message}`,
{ cause },
);
}
function decisionOptions(
options: Readonly<LocalReconciliationSecretConfigApplyOptions>,
) {
return Object.freeze({
deploymentRoot: options.deploymentRoot,
applicationRoot: options.applicationRoot,
secretConfigRoot: options.secretConfigRoot,
secretConfigDecisionRoot: options.secretConfigDecisionRoot,
allowRootService: options.allowRootService,
});
}
function advance(
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
state:
| 'reconciliation_secret_config_apply_prepared'
| 'reconciliation_secret_config_applied'
| 'reconciliation_secret_config_rolled_back',
digest: string,
atMs: number,
): Readonly<LocalCutoverInstanceHead> {
return advanceLocalCutoverInstanceHead(
{
options: { deploymentRoot: intent.command.options.deploymentRoot },
request: {
cutoverId: intent.cutoverId,
profile: intent.profile,
instanceId: intent.instanceId,
expectedActivationDigest: intent.activationDigest,
requestedAtMs: atMs,
},
},
uid,
state,
intent.generation,
digest,
);
}
function result(
operation: LocalReconciliationSecretConfigApplyResult['operation'],
status: LocalReconciliationSecretConfigApplyResult['status'],
receipt: Readonly<LocalReconciliationSecretConfigApplyReceipt>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
head: Readonly<LocalCutoverInstanceHead>,
): Readonly<LocalReconciliationSecretConfigApplyResult> {
return Object.freeze({
schemaVersion: 1,
operation,
status,
state: head.state as
| 'reconciliation_secret_config_applied'
| 'reconciliation_secret_config_rolled_back',
decisionId: receipt.decisionId,
secretConfigId: receipt.secretConfigId,
applyDigest: receipt.applyDigest,
publicationDigest: receipt.publicationDigest,
activeBindingCount: receipt.activeBindingCount,
disabledPreservationCount: receipt.disabledPreservationCount,
updatedTaskCount: receipt.updatedTaskCount,
updatedTriggerCount: receipt.updatedTriggerCount,
backupSha256: intent.backup.sha256,
instanceHeadDigest: head.headDigest,
});
}
function assertReviewer(
authenticated: Readonly<AuthenticatedLocalCommand>,
reviewer: Readonly<SecurityPrincipal>,
atMs: number,
): void {
const principal = authenticated.principal;
if (
reviewer.subject.type !== 'user' ||
principal.subject.type !== 'user' ||
reviewer.subject.id !== principal.subject.id ||
principal.assurance !== 'local_console' ||
principal.authenticatedAtMs > atMs ||
atMs - principal.authenticatedAtMs > MAX_AUTHENTICATION_AGE_MS ||
principal.expiresAtMs <= atMs
) {
fail('current reviewer authentication is not strong or identical');
}
}
async function authenticate(
options: Readonly<LocalReconciliationSecretConfigApplyOptions>,
atMs: number,
profile: 'edge' | 'standalone',
reviewer: Readonly<SecurityPrincipal>,
dependencies: LocalReconciliationSecretConfigApplyDependencies,
): Promise<
Readonly<{
authenticated: Readonly<AuthenticatedLocalCommand>;
database: AuthenticationDatabase;
}>
> {
const database = await (
dependencies.openAuthenticationDatabase ??
openLocalSqliteAuthenticationReadDatabase
)({
databasePath: options.targetDatabasePath,
profile,
...(options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: options.busyTimeoutMs }),
});
try {
const authenticated = await (
dependencies.authenticate ?? establishAuthenticatedLocalCommand
)(database, {
deploymentRoot: options.deploymentRoot,
databasePath: options.targetDatabasePath,
ownerPepperKeyringDirectory: options.ownerPepperKeyringDirectory,
credentialFilePath: options.credentialFilePath,
authenticationNamespace: 'local_reconciliation_secret_config_apply',
now: () => atMs,
});
assertReviewer(authenticated, reviewer, atMs);
return Object.freeze({ authenticated, database });
} catch (error) {
await database.close();
throw error;
}
}
function exactBundle(
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
captureRoot: string,
captureId: string,
expectedBundleDigest: string,
expectedFingerprintDigest: string,
uid: number,
): void {
const current = inspectLocalReconciliationSealedBundle(
captureRoot,
captureId,
uid,
);
if (
current.receipt.bundleDigest !== expectedBundleDigest ||
current.fingerprintDigest !== expectedFingerprintDigest ||
intent.command.request.expectedDecisionDigest.length !== 64
) {
fail('sealed source authority drifted');
}
}
function verifyIntentCommand(
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
command: Readonly<LocalReconciliationSecretConfigApplyCommand>,
): void {
if (
intent.command.request.secretConfigId !== command.request.secretConfigId ||
intent.command.request.decisionId !== command.request.decisionId ||
intent.command.request.expectedDecisionDigest !==
command.request.expectedDecisionDigest ||
intent.command.request.mutationId !== command.request.mutationId ||
intent.command.request.requestId !== command.request.requestId ||
intent.command.request.appliedAtMs !== command.request.appliedAtMs ||
intent.command.request.expectedHeadDigest !==
command.request.expectedHeadDigest ||
JSON.stringify(intent.command.options) !== JSON.stringify(command.options)
) {
fail('apply command is not an exact replay');
}
}
function assertPreparedAuthority(
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
allowedState:
| 'reconciliation_secret_config_apply_prepared'
| 'reconciliation_secret_config_applied',
expectedDigest: string,
): void {
const head = readLocalCutoverInstanceHead(
intent.command.options.deploymentRoot,
intent.instanceId,
uid,
);
if (
head.state !== allowedState ||
head.sourceRecordDigest !== expectedDigest
) {
fail('apply lost current head authority');
}
}
export async function applyLocalReconciliationSecretConfig(
value: unknown,
dependencies: LocalReconciliationSecretConfigApplyDependencies = {},
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
const command = normalizeLocalReconciliationSecretConfigApplyCommand(value);
const uid = currentIdentity().uid;
for (const [directory, label] of [
[command.options.deploymentRoot, 'deploymentRoot'],
[command.options.applicationRoot, 'applicationRoot'],
[command.options.secretConfigRoot, 'secretConfigRoot'],
[command.options.secretConfigDecisionRoot, 'secretConfigDecisionRoot'],
[command.options.secretConfigApplyRoot, 'secretConfigApplyRoot'],
] as const) {
validatePrivateDirectory(directory, uid, label);
}
const selected = localReconciliationSecretConfigApplyPaths(
command.options.secretConfigApplyRoot,
command.request.secretConfigId,
);
ensureLocalReconciliationSecretConfigApplyLayout(selected, uid);
validateLocalReconciliationSecretConfigApplyCatalog(selected);
const terminal = await readLocalReconciliationSecretConfigDecisionTerminal(
decisionOptions(command.options),
command.request.secretConfigId,
uid,
APPLY_HEAD_STATES,
);
if (
terminal.receipt.decisionId !== command.request.decisionId ||
terminal.receipt.decisionDigest !==
command.request.expectedDecisionDigest ||
terminal.receipt.outcome !== 'ready' ||
terminal.receipt.skippedCount !== 0 ||
terminal.receipt.candidateCount < 1
) {
fail('apply command is detached from a ready signed decision');
}
const planTerminal = terminal.context.planTerminal;
const capture = readLocalReconciliationCaptureIntent(
planTerminal.intent.command.options.captureRoot,
planTerminal.intent.command.request.captureId,
);
if (
capture.command.request.targetDatabasePath !==
command.options.targetDatabasePath
) {
fail('target database is detached from stopped capture');
}
let intent: Readonly<LocalReconciliationSecretConfigApplyIntent>;
const recoveringPreparedIntent = fs.existsSync(selected.intent);
let head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
terminal.intent.instanceId,
uid,
);
if (recoveringPreparedIntent) {
intent = readLocalReconciliationSecretConfigApplyIntent(selected, uid);
verifyIntentCommand(intent, command);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
} else {
if (
head.state !== 'reconciliation_secret_config_reviewed' ||
head.headDigest !== command.request.expectedHeadDigest ||
head.sourceRecordDigest !== terminal.receipt.decisionDigest
) {
fail('apply lost reviewed head compare-and-swap');
}
discardUnpreparedLocalReconciliationSecretConfigMaterials(selected);
const before = proveLocalReconciliationStoppedState(capture.command, uid);
const materials: Readonly<PreparedReconciliationSecretConfigMaterial>[] =
[];
const openRequirements =
createLocalReconciliationSecretConfigDecisionRequirementFactory(
terminal.context.secretConfig.planPath,
terminal.context.secretConfig.receipt,
uid,
);
const prepared = await withLocalReconciliationSealedDatabaseAsync(
planTerminal.bundle,
'legacy',
uid,
{},
async (sourceClient) =>
(
dependencies.prepareApplication ??
prepareReconciliationSecretConfigApplication
)({
sourceClient,
profile: terminal.intent.profile,
projectId: terminal.intent.projectId,
mutationId: command.request.mutationId,
appliedAtMs: command.request.appliedAtMs,
expectedLegacyInventoryDigest:
terminal.context.secretConfig.receipt.legacyInventoryDigest,
decisions: terminal.authorization.decisions,
openRequirements,
keyProvider: new LocalSecretKeyringFileProvider(
command.options.secretKeyringPath,
),
visitMaterial(entry) {
materials.push(entry);
},
}),
);
if (!prepared) fail('sealed Legacy source requires manual handling');
const materialEvidence = publishLocalReconciliationSecretConfigMaterials(
selected,
materials,
terminal.intent.profile,
uid,
);
if (
materialEvidence.secretCount !== prepared.secretCount ||
materialEvidence.activeBindingCount !== prepared.activeBindingCount ||
materialEvidence.disabledPreservationCount !==
prepared.disabledPreservationCount ||
materialEvidence.materialSetDigest !== prepared.materialSetDigest ||
prepared.activeBindingCount !== terminal.receipt.applyBindingCount ||
prepared.disabledPreservationCount !==
terminal.receipt.preserveDisabledCount
) {
fail('prepared material evidence drifted');
}
dependencies.afterMaterialPublished?.();
const backup = await (
dependencies.createBackup ?? createLocalSqliteRolloutBackup
)({
databasePath: command.options.targetDatabasePath,
backupPath: selected.backup,
profile: terminal.intent.profile,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
});
const after = proveLocalReconciliationStoppedState(capture.command, uid);
if (after.proofDigest !== before.proofDigest) {
fail('stopped target drifted across preparation');
}
dependencies.afterBackupPublished?.();
intent = buildLocalReconciliationSecretConfigApplyIntent({
command,
instanceId: terminal.intent.instanceId,
cutoverId: terminal.intent.cutoverId,
activationDigest: terminal.intent.activationDigest,
profile: terminal.intent.profile,
projectId: terminal.intent.projectId,
generation: terminal.intent.generation,
stoppedProofDigest: before.proofDigest,
legacyInventoryDigest: prepared.legacyInventoryDigest,
candidateSetDigest: terminal.intent.candidateSetDigest,
automationAdoptionSetDigest:
terminal.context.secretConfig.receipt.automationAdoptionSetDigest,
material: materialEvidence,
backup,
});
publishExactFile(
selected.intent,
localReconciliationSecretConfigApplyEvidenceContents(intent),
0o600,
uid,
'secret config apply intent',
);
head = advance(
intent,
uid,
'reconciliation_secret_config_apply_prepared',
intent.preparationDigest,
command.request.appliedAtMs,
);
dependencies.afterPreparedHead?.();
}
if (fs.existsSync(selected.receipt)) {
const receipt = readLocalReconciliationSecretConfigApplyReceipt(
selected,
uid,
);
const current = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
if (current.sha256 !== receipt.targetAfter.sha256) {
fail('terminal apply target drifted');
}
if (
head.state === 'reconciliation_secret_config_apply_prepared' &&
head.sourceRecordDigest === intent.preparationDigest
) {
head = advance(
intent,
uid,
'reconciliation_secret_config_applied',
receipt.applyDigest,
receipt.appliedAtMs,
);
dependencies.afterAppliedHead?.();
} else if (
head.state !== 'reconciliation_secret_config_applied' ||
head.sourceRecordDigest !== receipt.applyDigest
) {
fail('terminal apply receipt drifted');
}
sealLocalReconciliationSecretConfigAppliedStorage(selected, intent, uid);
dependencies.afterAppliedSeal?.();
return result(command.operation, 'existing', receipt, intent, head);
}
if (
head.state !== 'reconciliation_secret_config_apply_prepared' ||
head.sourceRecordDigest !== intent.preparationDigest
) {
fail('apply lost prepared head compare-and-swap');
}
if (!recoveringPreparedIntent) {
const stopped = proveLocalReconciliationStoppedState(capture.command, uid);
if (stopped.proofDigest !== intent.stoppedProofDigest) {
fail('stopped proof drifted before write');
}
}
const authenticatedScope = await authenticate(
command.options,
command.request.appliedAtMs,
intent.profile,
terminal.reviewer,
dependencies,
);
let publication;
try {
publication = await (
dependencies.applyApplication ??
applyPreparedReconciliationSecretConfigApplication
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
projectId: intent.projectId,
mutationId: command.request.mutationId,
requestId: command.request.requestId,
secretConfigPlanDigest: terminal.receipt.secretConfigPlanDigest,
decisionDigest: terminal.receipt.decisionDigest,
candidateSetDigest: intent.candidateSetDigest,
automationAdoptionSetDigest: intent.automationAdoptionSetDigest,
materials: readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
),
principal: authenticatedScope.authenticated.principal,
appliedAtMs: command.request.appliedAtMs,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
confirmAuthenticationAuthority: () =>
authenticatedScope.authenticated.confirm(),
confirmPreparedAuthority() {
exactBundle(
intent,
planTerminal.bundle.captureRoot,
planTerminal.bundle.receipt.captureId,
planTerminal.bundle.receipt.bundleDigest,
planTerminal.bundle.fingerprintDigest,
uid,
);
assertPreparedAuthority(
intent,
uid,
'reconciliation_secret_config_apply_prepared',
intent.preparationDigest,
);
},
});
} finally {
await authenticatedScope.database.close();
}
dependencies.afterDatabaseCommit?.();
const targetAfter = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
const publisher = publication.receipt;
const receipt = buildLocalReconciliationSecretConfigApplyReceipt({
decisionId: command.request.decisionId,
secretConfigId: command.request.secretConfigId,
mutationId: command.request.mutationId,
preparationDigest: intent.preparationDigest,
preparedHeadDigest: head.headDigest,
publicationDigest: publisher.publicationDigest,
publisherReceiptDigest: publisher.receiptDigest,
activeBindingCount: publisher.activeBindingCount,
disabledPreservationCount: publisher.disabledPreservationCount,
updatedTaskCount: publisher.taskCount,
updatedTriggerCount: publisher.triggerCount,
targetAfter,
appliedAtMs: command.request.appliedAtMs,
});
publishExactFile(
selected.receipt,
localReconciliationSecretConfigApplyEvidenceContents(receipt),
0o600,
uid,
'secret config apply receipt',
);
dependencies.afterReceiptPublished?.();
head = advance(
intent,
uid,
'reconciliation_secret_config_applied',
receipt.applyDigest,
command.request.appliedAtMs,
);
dependencies.afterAppliedHead?.();
sealLocalReconciliationSecretConfigAppliedStorage(selected, intent, uid);
dependencies.afterAppliedSeal?.();
return result(
command.operation,
publication.status === 'existing' ? 'existing' : 'applied',
receipt,
intent,
head,
);
}
export async function verifyLocalReconciliationSecretConfigApply(
value: unknown,
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
const command =
normalizeLocalReconciliationSecretConfigApplyVerifyCommand(value);
const uid = currentIdentity().uid;
const selected = localReconciliationSecretConfigApplyPaths(
command.options.secretConfigApplyRoot,
command.request.secretConfigId,
);
validatePrivateDirectory(
command.options.secretConfigApplyRoot,
uid,
'secretConfigApplyRoot',
);
validateLocalReconciliationSecretConfigApplyCatalog(selected);
const intent = readLocalReconciliationSecretConfigApplyIntent(selected, uid);
const receipt = readLocalReconciliationSecretConfigApplyReceipt(
selected,
uid,
);
if (
intent.command.request.decisionId !== command.request.decisionId ||
receipt.decisionId !== command.request.decisionId ||
receipt.secretConfigId !== command.request.secretConfigId ||
receipt.applyDigest !== command.request.expectedApplyDigest
) {
fail('verify command is detached from apply receipt');
}
await readLocalReconciliationSecretConfigDecisionTerminal(
decisionOptions(command.options),
command.request.secretConfigId,
uid,
APPLY_HEAD_STATES,
);
const head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
uid,
);
if (head.state === 'reconciliation_secret_config_applied') {
validateLocalReconciliationSecretConfigAppliedStorage(
selected,
intent,
uid,
);
if (head.sourceRecordDigest !== receipt.applyDigest) {
fail('applied head drifted');
}
const current = await inspectLocalSqliteSnapshot({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
if (current.sha256 !== receipt.targetAfter.sha256) {
fail('applied target drifted');
}
return result(command.operation, 'verified', receipt, intent, head);
}
if (
head.state === 'reconciliation_secret_config_rolled_back' &&
fs.existsSync(selected.rollbackReceipt)
) {
validateLocalReconciliationSecretConfigRolledBackStorage(
selected,
intent,
uid,
);
const rollback = readLocalReconciliationSecretConfigRollbackReceipt(
selected,
uid,
);
if (
rollback.applyDigest !== receipt.applyDigest ||
head.sourceRecordDigest !== rollback.rollbackDigest
) {
fail('rollback head drifted');
}
const current = await inspectLocalSqliteSnapshot({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
if (
current.sha256 !== intent.backup.sha256 ||
current.sha256 !== rollback.restored.sha256
) {
fail('rolled-back target drifted');
}
return result(command.operation, 'verified', receipt, intent, head);
}
return fail('apply head is not terminal');
}
export async function rollbackLocalReconciliationSecretConfigApply(
value: unknown,
dependencies: LocalReconciliationSecretConfigApplyDependencies = {},
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
const command =
normalizeLocalReconciliationSecretConfigApplyRollbackCommand(value);
const uid = currentIdentity().uid;
const selected = localReconciliationSecretConfigApplyPaths(
command.options.secretConfigApplyRoot,
command.request.secretConfigId,
);
validatePrivateDirectory(
command.options.secretConfigApplyRoot,
uid,
'secretConfigApplyRoot',
);
validateLocalReconciliationSecretConfigApplyCatalog(selected);
const intent = readLocalReconciliationSecretConfigApplyIntent(selected, uid);
const receipt = readLocalReconciliationSecretConfigApplyReceipt(
selected,
uid,
);
if (
receipt.decisionId !== command.request.decisionId ||
receipt.secretConfigId !== command.request.secretConfigId ||
receipt.applyDigest !== command.request.expectedApplyDigest
) {
fail('rollback command is detached from apply receipt');
}
const terminal = await readLocalReconciliationSecretConfigDecisionTerminal(
decisionOptions(command.options),
command.request.secretConfigId,
uid,
APPLY_HEAD_STATES,
);
let head = readLocalCutoverInstanceHead(
command.options.deploymentRoot,
intent.instanceId,
uid,
);
if (fs.existsSync(selected.rollbackReceipt)) {
const rollback = readLocalReconciliationSecretConfigRollbackReceipt(
selected,
uid,
);
const current = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
if (current.sha256 !== rollback.restored.sha256) {
fail('rollback replay target drifted');
}
if (
head.state === 'reconciliation_secret_config_applied' &&
head.sourceRecordDigest === receipt.applyDigest
) {
head = advance(
intent,
uid,
'reconciliation_secret_config_rolled_back',
rollback.rollbackDigest,
rollback.rolledBackAtMs,
);
dependencies.afterRollbackHead?.();
} else if (
head.state !== 'reconciliation_secret_config_rolled_back' ||
head.sourceRecordDigest !== rollback.rollbackDigest
) {
fail('rollback replay drifted');
}
sealLocalReconciliationSecretConfigRolledBackStorage(selected, intent, uid);
dependencies.afterRollbackSeal?.();
return result(command.operation, 'existing', receipt, intent, head);
}
if (
head.state !== 'reconciliation_secret_config_applied' ||
head.headDigest !== command.request.expectedHeadDigest ||
head.sourceRecordDigest !== receipt.applyDigest
) {
fail('rollback lost applied head compare-and-swap');
}
const current = await (
dependencies.inspectSnapshot ?? inspectLocalSqliteSnapshot
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
});
if (current.sha256 === intent.backup.sha256) {
const rollback = buildLocalReconciliationSecretConfigRollbackReceipt({
decisionId: receipt.decisionId,
secretConfigId: receipt.secretConfigId,
applyDigest: receipt.applyDigest,
restored: current,
rolledBackAtMs: command.request.rolledBackAtMs,
});
publishExactFile(
selected.rollbackReceipt,
localReconciliationSecretConfigApplyEvidenceContents(rollback),
0o600,
uid,
'secret config rollback receipt',
);
dependencies.afterRollbackReceipt?.();
head = advance(
intent,
uid,
'reconciliation_secret_config_rolled_back',
rollback.rollbackDigest,
command.request.rolledBackAtMs,
);
dependencies.afterRollbackHead?.();
sealLocalReconciliationSecretConfigRolledBackStorage(selected, intent, uid);
dependencies.afterRollbackSeal?.();
return result(command.operation, 'existing', receipt, intent, head);
}
if (current.sha256 !== receipt.targetAfter.sha256) {
fail('rollback current target drifted');
}
sealLocalReconciliationSecretConfigAppliedStorage(selected, intent, uid);
const authenticatedScope = await authenticate(
command.options,
command.request.rolledBackAtMs,
intent.profile,
terminal.reviewer,
dependencies,
);
try {
const replay = await (
dependencies.applyApplication ??
applyPreparedReconciliationSecretConfigApplication
)({
databasePath: command.options.targetDatabasePath,
profile: intent.profile,
projectId: intent.projectId,
mutationId: intent.command.request.mutationId,
requestId: intent.command.request.requestId,
secretConfigPlanDigest: terminal.receipt.secretConfigPlanDigest,
decisionDigest: terminal.receipt.decisionDigest,
candidateSetDigest: intent.candidateSetDigest,
automationAdoptionSetDigest: intent.automationAdoptionSetDigest,
materials: readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
),
principal: authenticatedScope.authenticated.principal,
appliedAtMs: intent.command.request.appliedAtMs,
authorizationAtMs: command.request.rolledBackAtMs,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
confirmAuthenticationAuthority: () =>
authenticatedScope.authenticated.confirm(),
confirmPreparedAuthority() {
assertPreparedAuthority(
intent,
uid,
'reconciliation_secret_config_applied',
receipt.applyDigest,
);
},
});
if (
replay.status !== 'existing' ||
replay.receipt.publicationDigest !== receipt.publicationDigest ||
replay.receipt.receiptDigest !== receipt.publisherReceiptDigest
) {
fail('rollback could not verify current publication');
}
} finally {
await authenticatedScope.database.close();
}
prepareLocalReconciliationSecretConfigRollbackSource(selected, intent, uid);
const restored = await (
dependencies.restoreSnapshot ?? restoreLocalSqliteSnapshot
)({
databasePath: command.options.targetDatabasePath,
sourceSnapshotPath: selected.rollbackSource,
restoreStagePath: selected.restoreStage,
replacedDatabasePath: selected.replaced,
expectedCurrentSha256: receipt.targetAfter.sha256,
expectedSourceSha256: intent.backup.sha256,
preserveDatabaseIdentity: true,
profile: intent.profile,
...(command.options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: command.options.busyTimeoutMs }),
});
dependencies.afterRestore?.();
const rollback = buildLocalReconciliationSecretConfigRollbackReceipt({
decisionId: receipt.decisionId,
secretConfigId: receipt.secretConfigId,
applyDigest: receipt.applyDigest,
restored: Object.freeze({
contractVersion: restored.contractVersion,
sha256: restored.sha256,
bytes: restored.bytes,
pageCount: restored.pageCount,
pageSize: restored.pageSize,
}),
rolledBackAtMs: command.request.rolledBackAtMs,
});
publishExactFile(
selected.rollbackReceipt,
localReconciliationSecretConfigApplyEvidenceContents(rollback),
0o600,
uid,
'secret config rollback receipt',
);
dependencies.afterRollbackReceipt?.();
head = advance(
intent,
uid,
'reconciliation_secret_config_rolled_back',
rollback.rollbackDigest,
command.request.rolledBackAtMs,
);
dependencies.afterRollbackHead?.();
sealLocalReconciliationSecretConfigRolledBackStorage(selected, intent, uid);
dependencies.afterRollbackSeal?.();
return result(command.operation, 'rolled_back', receipt, intent, head);
}
export async function applyLocalReconciliationSecretConfigCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
return applyLocalReconciliationSecretConfig(
readPrivateLocalCommandFile(filePath),
);
}
export async function verifyLocalReconciliationSecretConfigApplyCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
return verifyLocalReconciliationSecretConfigApply(
readPrivateLocalCommandFile(filePath),
);
}
export async function rollbackLocalReconciliationSecretConfigApplyCommandFile(
filePath: string,
): Promise<Readonly<LocalReconciliationSecretConfigApplyResult>> {
return rollbackLocalReconciliationSecretConfigApply(
readPrivateLocalCommandFile(filePath),
);
}
@@ -0,0 +1,436 @@
import type {
LocalSqliteRolloutBackupEvidence,
LocalSqliteSnapshotEvidence,
} from '@qinglong/local-sqlite/rollout-safety';
import { LocalDeploymentConfigurationError } from '../../../../foundation/error';
import { cutoverDigest } from '../../../../cutover/targetEvidence';
import {
normalizeLocalReconciliationSecretConfigApplyCommand,
type LocalReconciliationSecretConfigApplyCommand,
} from './contract';
const INTENT_SCHEMA =
'qinglong3-local-reconciliation-secret-config-apply-intent';
const RECEIPT_SCHEMA =
'qinglong3-local-reconciliation-secret-config-apply-receipt';
const ROLLBACK_SCHEMA =
'qinglong3-local-reconciliation-secret-config-rollback-receipt';
const DIGEST = /^[0-9a-f]{64}$/;
export interface LocalReconciliationSecretConfigMaterialEvidence {
readonly fileBytes: number;
readonly fileDigest: string;
readonly secretCount: number;
readonly activeBindingCount: number;
readonly disabledPreservationCount: number;
readonly materialSetDigest: string;
}
export interface LocalReconciliationSecretConfigApplyIntent {
readonly schema: typeof INTENT_SCHEMA;
readonly schemaVersion: 1;
readonly command: Readonly<LocalReconciliationSecretConfigApplyCommand>;
readonly instanceId: string;
readonly cutoverId: string;
readonly activationDigest: string;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly generation: number;
readonly stoppedProofDigest: string;
readonly legacyInventoryDigest: string;
readonly candidateSetDigest: string;
readonly automationAdoptionSetDigest: string;
readonly material: Readonly<LocalReconciliationSecretConfigMaterialEvidence>;
readonly backup: Readonly<LocalSqliteRolloutBackupEvidence>;
readonly preparationDigest: string;
}
export interface LocalReconciliationSecretConfigApplyReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_secret_config_applied';
readonly decisionId: string;
readonly secretConfigId: string;
readonly mutationId: string;
readonly preparationDigest: string;
readonly preparedHeadDigest: string;
readonly publicationDigest: string;
readonly publisherReceiptDigest: string;
readonly activeBindingCount: number;
readonly disabledPreservationCount: number;
readonly updatedTaskCount: number;
readonly updatedTriggerCount: number;
readonly targetAfter: Readonly<LocalSqliteSnapshotEvidence>;
readonly appliedAtMs: number;
readonly applyDigest: string;
}
export interface LocalReconciliationSecretConfigRollbackReceipt {
readonly schema: typeof ROLLBACK_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_secret_config_rolled_back';
readonly decisionId: string;
readonly secretConfigId: string;
readonly applyDigest: string;
readonly restored: Readonly<LocalSqliteSnapshotEvidence>;
readonly rolledBackAtMs: number;
readonly rollbackDigest: string;
}
function fail(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config apply evidence ${message}`,
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
fail(`${label} must be an object`);
}
const selected = value as Record<string, unknown>;
const actual = Object.keys(selected).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
fail(`${label} shape is invalid`);
}
return selected;
}
function validDigest(value: unknown): value is string {
return typeof value === 'string' && DIGEST.test(value);
}
function nonnegative(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function positive(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function snapshot(
value: unknown,
label: string,
): Readonly<LocalSqliteSnapshotEvidence> {
const selected = exact(
value,
['bytes', 'contractVersion', 'pageCount', 'pageSize', 'sha256'],
label,
);
if (
!validDigest(selected.sha256) ||
!positive(selected.bytes) ||
!positive(selected.pageCount) ||
!positive(selected.pageSize) ||
!Number.isSafeInteger(selected.contractVersion) ||
(selected.contractVersion as number) < 1
) {
fail(`${label} is invalid`);
}
return Object.freeze({
contractVersion: selected.contractVersion,
sha256: selected.sha256,
bytes: selected.bytes,
pageCount: selected.pageCount,
pageSize: selected.pageSize,
}) as unknown as Readonly<LocalSqliteSnapshotEvidence>;
}
function backup(value: unknown): Readonly<LocalSqliteRolloutBackupEvidence> {
const selected = exact(
value,
[
'bytes',
'contractVersion',
'pageCount',
'pageSize',
'sha256',
'status',
'writeContractVersion',
],
'backup',
);
const base = snapshot(
{
bytes: selected.bytes,
contractVersion: selected.contractVersion,
pageCount: selected.pageCount,
pageSize: selected.pageSize,
sha256: selected.sha256,
},
'backup snapshot',
);
if (
(selected.status !== 'prepared' && selected.status !== 'existing') ||
!Number.isSafeInteger(selected.writeContractVersion) ||
(selected.writeContractVersion as number) < 1
) {
fail('backup is invalid');
}
return Object.freeze({
status: selected.status,
writeContractVersion: selected.writeContractVersion,
...base,
}) as Readonly<LocalSqliteRolloutBackupEvidence>;
}
function material(
value: unknown,
): Readonly<LocalReconciliationSecretConfigMaterialEvidence> {
const selected = exact(
value,
[
'activeBindingCount',
'disabledPreservationCount',
'fileBytes',
'fileDigest',
'materialSetDigest',
'secretCount',
],
'material',
);
if (
!positive(selected.fileBytes) ||
!validDigest(selected.fileDigest) ||
!validDigest(selected.materialSetDigest) ||
!positive(selected.secretCount) ||
!nonnegative(selected.activeBindingCount) ||
!nonnegative(selected.disabledPreservationCount) ||
(selected.activeBindingCount as number) +
(selected.disabledPreservationCount as number) !==
selected.secretCount
) {
fail('material is invalid');
}
return Object.freeze(
selected,
) as unknown as Readonly<LocalReconciliationSecretConfigMaterialEvidence>;
}
export function buildLocalReconciliationSecretConfigApplyIntent(
input: Omit<
LocalReconciliationSecretConfigApplyIntent,
'schema' | 'schemaVersion' | 'preparationDigest'
>,
): Readonly<LocalReconciliationSecretConfigApplyIntent> {
const payload = Object.freeze({
schema: INTENT_SCHEMA,
schemaVersion: 1 as const,
...input,
});
return Object.freeze({
...payload,
preparationDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationSecretConfigApplyIntent(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyIntent> {
const selected = exact(
value,
[
'activationDigest',
'automationAdoptionSetDigest',
'backup',
'candidateSetDigest',
'command',
'cutoverId',
'generation',
'instanceId',
'legacyInventoryDigest',
'material',
'preparationDigest',
'profile',
'projectId',
'schema',
'schemaVersion',
'stoppedProofDigest',
],
'intent',
);
const { preparationDigest, ...raw } = selected;
const normalized = Object.freeze({
...raw,
command: normalizeLocalReconciliationSecretConfigApplyCommand(
selected.command,
),
material: material(selected.material),
backup: backup(selected.backup),
});
if (
selected.schema !== INTENT_SCHEMA ||
selected.schemaVersion !== 1 ||
!validDigest(preparationDigest) ||
cutoverDigest(raw) !== preparationDigest ||
cutoverDigest(normalized) !== preparationDigest ||
![
selected.activationDigest,
selected.stoppedProofDigest,
selected.legacyInventoryDigest,
selected.candidateSetDigest,
selected.automationAdoptionSetDigest,
].every(validDigest) ||
(selected.profile !== 'edge' && selected.profile !== 'standalone') ||
!positive(selected.generation) ||
![selected.instanceId, selected.cutoverId, selected.projectId].every(
(entry) => typeof entry === 'string' && entry.length > 0,
)
) {
fail('intent binding is invalid');
}
return Object.freeze({
...normalized,
preparationDigest,
}) as unknown as Readonly<LocalReconciliationSecretConfigApplyIntent>;
}
export function buildLocalReconciliationSecretConfigApplyReceipt(
input: Omit<
LocalReconciliationSecretConfigApplyReceipt,
'schema' | 'schemaVersion' | 'state' | 'applyDigest'
>,
): Readonly<LocalReconciliationSecretConfigApplyReceipt> {
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_secret_config_applied' as const,
...input,
});
return Object.freeze({ ...payload, applyDigest: cutoverDigest(payload) });
}
export function normalizeLocalReconciliationSecretConfigApplyReceipt(
value: unknown,
): Readonly<LocalReconciliationSecretConfigApplyReceipt> {
const selected = exact(
value,
[
'activeBindingCount',
'appliedAtMs',
'applyDigest',
'decisionId',
'disabledPreservationCount',
'mutationId',
'preparationDigest',
'preparedHeadDigest',
'publicationDigest',
'publisherReceiptDigest',
'schema',
'schemaVersion',
'secretConfigId',
'state',
'targetAfter',
'updatedTaskCount',
'updatedTriggerCount',
],
'receipt',
);
const { applyDigest, ...payload } = selected;
const targetAfter = snapshot(selected.targetAfter, 'targetAfter');
if (
selected.schema !== RECEIPT_SCHEMA ||
selected.schemaVersion !== 1 ||
selected.state !== 'reconciliation_secret_config_applied' ||
!validDigest(applyDigest) ||
cutoverDigest(payload) !== applyDigest ||
![
selected.preparationDigest,
selected.preparedHeadDigest,
selected.publicationDigest,
selected.publisherReceiptDigest,
].every(validDigest) ||
![
selected.activeBindingCount,
selected.disabledPreservationCount,
selected.updatedTaskCount,
selected.updatedTriggerCount,
selected.appliedAtMs,
].every(nonnegative) ||
typeof selected.decisionId !== 'string' ||
typeof selected.secretConfigId !== 'string' ||
typeof selected.mutationId !== 'string'
) {
fail('receipt binding is invalid');
}
return Object.freeze({
...payload,
targetAfter,
applyDigest,
}) as unknown as Readonly<LocalReconciliationSecretConfigApplyReceipt>;
}
export function buildLocalReconciliationSecretConfigRollbackReceipt(
input: Omit<
LocalReconciliationSecretConfigRollbackReceipt,
'schema' | 'schemaVersion' | 'state' | 'rollbackDigest'
>,
): Readonly<LocalReconciliationSecretConfigRollbackReceipt> {
const payload = Object.freeze({
schema: ROLLBACK_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_secret_config_rolled_back' as const,
...input,
});
return Object.freeze({
...payload,
rollbackDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationSecretConfigRollbackReceipt(
value: unknown,
): Readonly<LocalReconciliationSecretConfigRollbackReceipt> {
const selected = exact(
value,
[
'applyDigest',
'decisionId',
'restored',
'rollbackDigest',
'rolledBackAtMs',
'schema',
'schemaVersion',
'secretConfigId',
'state',
],
'rollback receipt',
);
const { rollbackDigest, ...payload } = selected;
const restored = snapshot(selected.restored, 'restored');
if (
selected.schema !== ROLLBACK_SCHEMA ||
selected.schemaVersion !== 1 ||
selected.state !== 'reconciliation_secret_config_rolled_back' ||
!validDigest(selected.applyDigest) ||
!validDigest(rollbackDigest) ||
cutoverDigest(payload) !== rollbackDigest ||
!nonnegative(selected.rolledBackAtMs) ||
typeof selected.decisionId !== 'string' ||
typeof selected.secretConfigId !== 'string'
) {
fail('rollback receipt binding is invalid');
}
return Object.freeze({
...payload,
restored,
rollbackDigest,
}) as unknown as Readonly<LocalReconciliationSecretConfigRollbackReceipt>;
}
export function localReconciliationSecretConfigApplyEvidenceContents(
value:
| Readonly<LocalReconciliationSecretConfigApplyIntent>
| Readonly<LocalReconciliationSecretConfigApplyReceipt>
| Readonly<LocalReconciliationSecretConfigRollbackReceipt>,
): string {
return `${JSON.stringify(value, null, 2)}\n`;
}
@@ -0,0 +1,676 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import {
normalizePreparedReconciliationSecretConfigMaterial,
type PreparedReconciliationSecretConfigMaterial,
} from '@qinglong/local-admin/reconciliation-secret-and-config-application';
import { LocalDeploymentConfigurationError } from '../../../../foundation/error';
import {
preflightPublishedFile,
publishExactFile,
} from '../../../../foundation/files';
import type {
LocalReconciliationSecretConfigApplyIntent,
LocalReconciliationSecretConfigApplyReceipt,
LocalReconciliationSecretConfigMaterialEvidence,
LocalReconciliationSecretConfigRollbackReceipt,
} from './evidence';
import {
normalizeLocalReconciliationSecretConfigApplyIntent,
normalizeLocalReconciliationSecretConfigApplyReceipt,
normalizeLocalReconciliationSecretConfigRollbackReceipt,
} from './evidence';
const MAX_JSON_BYTES = 64 * 1024;
const MAX_LINE_BYTES = 64 * 1024;
const MAX_EDGE_MATERIAL_BYTES = 4 * 1024 * 1024;
const MAX_STANDALONE_MATERIAL_BYTES = 16 * 1024 * 1024;
export interface LocalReconciliationSecretConfigApplyPaths {
readonly root: string;
readonly backupRoot: string;
readonly rollbackRoot: string;
readonly intent: string;
readonly material: string;
readonly backup: string;
readonly receipt: string;
readonly rollbackReceipt: string;
readonly rollbackSource: string;
readonly restoreStage: string;
readonly replaced: string;
}
function fail(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config apply storage ${message}`,
{ cause },
);
}
export function localReconciliationSecretConfigApplyPaths(
root: string,
secretConfigId: string,
): Readonly<LocalReconciliationSecretConfigApplyPaths> {
const selected = path.join(root, secretConfigId);
const backupRoot = path.join(selected, 'backup');
const rollbackRoot = path.join(selected, 'rollback-work');
return Object.freeze({
root: selected,
backupRoot,
rollbackRoot,
intent: path.join(selected, 'intent.json'),
material: path.join(selected, 'materials.ndjson'),
backup: path.join(backupRoot, 'before.sqlite'),
receipt: path.join(selected, 'receipt.json'),
rollbackReceipt: path.join(rollbackRoot, 'receipt.json'),
rollbackSource: path.join(rollbackRoot, 'restore-source.sqlite'),
restoreStage: path.join(rollbackRoot, 'restore-stage.sqlite'),
replaced: path.join(rollbackRoot, 'replaced.sqlite'),
});
}
function directoryMode(
directory: string,
uid: number,
modes: readonly number[],
label: string,
): number {
let stat: fs.Stats;
try {
stat = fs.lstatSync(directory);
} catch (error) {
return fail(`${label} is unavailable`, error);
}
const mode = stat.mode & 0o777;
if (
!stat.isDirectory() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
!modes.includes(mode) ||
fs.realpathSync(directory) !== directory
) {
fail(`${label} identity is invalid`);
}
return mode;
}
function ensureDirectory(directory: string, uid: number, label: string): void {
try {
fs.mkdirSync(directory, { mode: 0o700 });
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
fail(`${label} cannot be created`, error);
}
}
directoryMode(directory, uid, [0o700], label);
}
export function ensureLocalReconciliationSecretConfigApplyLayout(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
uid: number,
): void {
if (!fs.existsSync(selected.root))
ensureDirectory(selected.root, uid, 'root');
const rootMode = directoryMode(selected.root, uid, [0o700, 0o500], 'root');
if (rootMode === 0o700) {
if (!fs.existsSync(selected.backupRoot)) {
ensureDirectory(selected.backupRoot, uid, 'backup root');
}
if (!fs.existsSync(selected.rollbackRoot)) {
ensureDirectory(selected.rollbackRoot, uid, 'rollback work root');
}
}
directoryMode(selected.backupRoot, uid, [0o700, 0o500], 'backup root');
directoryMode(
selected.rollbackRoot,
uid,
[0o700, 0o500],
'rollback work root',
);
}
export function validateLocalReconciliationSecretConfigApplyCatalog(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
): void {
const rootAllowed = new Set([
'backup',
'rollback-work',
'intent.json',
'materials.ndjson',
'receipt.json',
'.intent.json.ql3-deploy-stage',
'.materials.ndjson.ql3-deploy-stage',
'.receipt.json.ql3-deploy-stage',
]);
for (const entry of fs.readdirSync(selected.root, { withFileTypes: true })) {
if (!rootAllowed.has(entry.name) || entry.isSymbolicLink()) {
fail('root contains unknown material');
}
}
const backupAllowed = new Set([
'before.sqlite',
'.before.sqlite.ql3-backup-stage',
]);
for (const entry of fs.readdirSync(selected.backupRoot, {
withFileTypes: true,
})) {
if (!backupAllowed.has(entry.name) || entry.isSymbolicLink()) {
fail('backup root contains unknown material');
}
}
const rollbackAllowed = new Set([
'receipt.json',
'.receipt.json.ql3-deploy-stage',
'restore-source.sqlite',
'restore-stage.sqlite',
'replaced.sqlite',
]);
for (const entry of fs.readdirSync(selected.rollbackRoot, {
withFileTypes: true,
})) {
if (!rollbackAllowed.has(entry.name) || entry.isSymbolicLink()) {
fail('rollback work root contains unknown material');
}
}
}
function stableBytes(
filePath: string,
uid: number,
modes: readonly number[],
maximumBytes: number,
label: string,
): Buffer {
let descriptor: number | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
!modes.includes(Number(before.mode) & 0o777) ||
before.nlink !== 1n ||
before.size < 2n ||
before.size > BigInt(maximumBytes) ||
fs.realpathSync(filePath) !== filePath
) {
return fail(`${label} identity is invalid`);
}
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size ||
opened.mtimeNs !== before.mtimeNs ||
opened.ctimeNs !== before.ctimeNs
) {
return fail(`${label} changed while opening`);
}
const bytes = Buffer.alloc(Number(opened.size));
let offset = 0;
while (offset < bytes.length) {
const read = fs.readSync(
descriptor,
bytes,
offset,
bytes.length - offset,
offset,
);
if (read < 1) {
bytes.fill(0);
return fail(`${label} read stalled`);
}
offset += read;
}
const after = fs.fstatSync(descriptor, { bigint: true });
if (
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.mtimeNs !== opened.mtimeNs ||
after.ctimeNs !== opened.ctimeNs
) {
bytes.fill(0);
return fail(`${label} drifted while reading`);
}
return bytes;
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return fail(`${label} cannot be read`, error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
function stableJson(
filePath: string,
uid: number,
modes: readonly number[],
label: string,
): unknown {
const bytes = stableBytes(filePath, uid, modes, MAX_JSON_BYTES, label);
try {
return JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes),
) as unknown;
} catch (error) {
return fail(`${label} is not UTF-8 JSON`, error);
} finally {
bytes.fill(0);
}
}
export function publishLocalReconciliationSecretConfigMaterials(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
materials: readonly Readonly<PreparedReconciliationSecretConfigMaterial>[],
profile: 'edge' | 'standalone',
uid: number,
): Readonly<LocalReconciliationSecretConfigMaterialEvidence> {
if (materials.length < 1) fail('material set must not be empty');
const lines = materials.map((entry, index) => {
const normalized =
normalizePreparedReconciliationSecretConfigMaterial(entry);
if (normalized.ordinal !== index + 1) fail('material ordinal drifted');
const line = `${JSON.stringify(normalized)}\n`;
if (Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) {
fail('material line exceeds its byte bound');
}
return line;
});
const contents = lines.join('');
const fileBytes = Buffer.byteLength(contents, 'utf8');
const maximum =
profile === 'edge'
? MAX_EDGE_MATERIAL_BYTES
: MAX_STANDALONE_MATERIAL_BYTES;
if (fileBytes > maximum) fail('material file exceeds its profile budget');
const materialHash = createHash('sha256').update(
'qinglong3.reconciliation-secret-config-material-set.v1\0',
);
for (const entry of materials) {
materialHash.update('\0').update(JSON.stringify(entry));
}
const evidence = Object.freeze({
fileBytes,
fileDigest: createHash('sha256').update(contents).digest('hex'),
secretCount: materials.length,
activeBindingCount: materials.filter(
(entry) => entry.disposition === 'active_binding',
).length,
disabledPreservationCount: materials.filter(
(entry) => entry.disposition === 'disabled_preservation',
).length,
materialSetDigest: materialHash.digest('hex'),
});
preflightPublishedFile(
selected.material,
contents,
0o600,
uid,
'secret config prepared materials',
);
publishExactFile(
selected.material,
contents,
0o600,
uid,
'secret config prepared materials',
);
return evidence;
}
export function discardUnpreparedLocalReconciliationSecretConfigMaterials(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
): void {
if (fs.existsSync(selected.intent)) {
fail('prepared materials are already authoritative');
}
if (!fs.existsSync(selected.material)) return;
fs.unlinkSync(selected.material);
syncDirectory(selected.root);
}
export function readLocalReconciliationSecretConfigMaterials(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
profile: 'edge' | 'standalone',
uid: number,
expected?: Readonly<LocalReconciliationSecretConfigMaterialEvidence>,
): readonly Readonly<PreparedReconciliationSecretConfigMaterial>[] {
const bytes = stableBytes(
selected.material,
uid,
[0o600, 0o400],
profile === 'edge'
? MAX_EDGE_MATERIAL_BYTES
: MAX_STANDALONE_MATERIAL_BYTES,
'prepared materials',
);
try {
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
if (!text.endsWith('\n')) fail('material file is not newline framed');
const rawLines = text.slice(0, -1).split('\n');
const materials = rawLines.map((line, index) => {
if (Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) {
return fail('material line exceeds its byte bound');
}
let value: unknown;
try {
value = JSON.parse(line) as unknown;
} catch (error) {
return fail('material line is not JSON', error);
}
const normalized =
normalizePreparedReconciliationSecretConfigMaterial(value);
if (normalized.ordinal !== index + 1) fail('material ordinal drifted');
return normalized;
});
const materialHash = createHash('sha256').update(
'qinglong3.reconciliation-secret-config-material-set.v1\0',
);
for (const entry of materials) {
materialHash.update('\0').update(JSON.stringify(entry));
}
const evidence = {
fileBytes: bytes.length,
fileDigest: createHash('sha256').update(bytes).digest('hex'),
secretCount: materials.length,
activeBindingCount: materials.filter(
(entry) => entry.disposition === 'active_binding',
).length,
disabledPreservationCount: materials.filter(
(entry) => entry.disposition === 'disabled_preservation',
).length,
materialSetDigest: materialHash.digest('hex'),
};
if (
expected &&
Object.keys(evidence).some(
(key) =>
evidence[key as keyof typeof evidence] !==
expected[key as keyof typeof evidence],
)
) {
fail('material evidence drifted');
}
return Object.freeze(materials);
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return fail('material file cannot be decoded', error);
} finally {
bytes.fill(0);
}
}
export function readLocalReconciliationSecretConfigApplyIntent(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
uid: number,
): Readonly<LocalReconciliationSecretConfigApplyIntent> {
return normalizeLocalReconciliationSecretConfigApplyIntent(
stableJson(selected.intent, uid, [0o600, 0o400], 'intent'),
);
}
export function readLocalReconciliationSecretConfigApplyReceipt(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
uid: number,
): Readonly<LocalReconciliationSecretConfigApplyReceipt> {
return normalizeLocalReconciliationSecretConfigApplyReceipt(
stableJson(selected.receipt, uid, [0o600, 0o400], 'receipt'),
);
}
export function readLocalReconciliationSecretConfigRollbackReceipt(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
uid: number,
): Readonly<LocalReconciliationSecretConfigRollbackReceipt> {
return normalizeLocalReconciliationSecretConfigRollbackReceipt(
stableJson(
selected.rollbackReceipt,
uid,
[0o600, 0o400],
'rollback receipt',
),
);
}
function syncFile(filePath: string): void {
const descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function sealFile(filePath: string, uid: number, label: string): void {
const stat = fs.lstatSync(filePath);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.uid !== uid ||
stat.nlink !== 1 ||
![0o600, 0o400].includes(stat.mode & 0o777)
) {
fail(`${label} cannot be sealed`);
}
if ((stat.mode & 0o777) !== 0o400) fs.chmodSync(filePath, 0o400);
syncFile(filePath);
}
function sealDirectory(directory: string, uid: number, label: string): void {
const mode = directoryMode(directory, uid, [0o700, 0o500], label);
if (mode !== 0o500) fs.chmodSync(directory, 0o500);
syncDirectory(directory);
}
function stableFileSha256(
filePath: string,
uid: number,
modes: readonly number[],
expectedBytes: number,
expectedSha256: string,
label: string,
): void {
const bytes = stableBytes(filePath, uid, modes, expectedBytes, label);
try {
if (
bytes.length !== expectedBytes ||
createHash('sha256').update(bytes).digest('hex') !== expectedSha256
) {
fail(`${label} digest drifted`);
}
} finally {
bytes.fill(0);
}
}
function validateBackup(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
modes: readonly number[],
): void {
stableFileSha256(
selected.backup,
uid,
modes,
intent.backup.bytes,
intent.backup.sha256,
'backup',
);
}
export function sealLocalReconciliationSecretConfigAppliedStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
validateLocalReconciliationSecretConfigApplyCatalog(selected);
validateBackup(selected, intent, uid, [0o600, 0o400]);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
if (fs.readdirSync(selected.rollbackRoot).length !== 0) {
fail('rollback work root must be empty');
}
for (const [file, label] of [
[selected.intent, 'intent'],
[selected.material, 'materials'],
[selected.receipt, 'receipt'],
[selected.backup, 'backup'],
] as const) {
sealFile(file, uid, label);
}
sealDirectory(selected.backupRoot, uid, 'backup root');
sealDirectory(selected.root, uid, 'root');
}
export function validateLocalReconciliationSecretConfigAppliedStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
directoryMode(selected.root, uid, [0o500], 'root');
directoryMode(selected.backupRoot, uid, [0o500], 'backup root');
directoryMode(selected.rollbackRoot, uid, [0o700], 'rollback work root');
validateLocalReconciliationSecretConfigApplyCatalog(selected);
if (fs.readdirSync(selected.rollbackRoot).length !== 0) {
fail('rollback work root must be empty');
}
readLocalReconciliationSecretConfigApplyIntent(selected, uid);
readLocalReconciliationSecretConfigApplyReceipt(selected, uid);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
validateBackup(selected, intent, uid, [0o400]);
}
export function prepareLocalReconciliationSecretConfigRollbackSource(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
directoryMode(selected.root, uid, [0o500], 'root');
directoryMode(selected.backupRoot, uid, [0o500], 'backup root');
directoryMode(selected.rollbackRoot, uid, [0o700], 'rollback work root');
validateBackup(selected, intent, uid, [0o400]);
if (!fs.existsSync(selected.rollbackSource)) {
try {
fs.copyFileSync(
selected.backup,
selected.rollbackSource,
fs.constants.COPYFILE_EXCL,
);
fs.chmodSync(selected.rollbackSource, 0o600);
syncFile(selected.rollbackSource);
syncDirectory(selected.rollbackRoot);
} catch (error) {
if (fs.existsSync(selected.rollbackSource))
fs.unlinkSync(selected.rollbackSource);
fail('rollback source cannot be prepared', error);
}
}
stableFileSha256(
selected.rollbackSource,
uid,
[0o600],
intent.backup.bytes,
intent.backup.sha256,
'rollback source',
);
}
function unlinkIfPresent(filePath: string): void {
if (!fs.existsSync(filePath)) return;
fs.unlinkSync(filePath);
syncDirectory(path.dirname(filePath));
}
export function sealLocalReconciliationSecretConfigRolledBackStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
validateLocalReconciliationSecretConfigApplyCatalog(selected);
if (fs.existsSync(selected.backup)) {
validateBackup(selected, intent, uid, [0o400]);
}
for (const temporary of [
selected.restoreStage,
selected.replaced,
selected.rollbackSource,
]) {
unlinkIfPresent(temporary);
}
if ((fs.statSync(selected.backupRoot).mode & 0o777) !== 0o700) {
fs.chmodSync(selected.backupRoot, 0o700);
syncDirectory(selected.root);
}
unlinkIfPresent(selected.backup);
for (const [file, label] of [
[selected.intent, 'intent'],
[selected.material, 'materials'],
[selected.receipt, 'receipt'],
[selected.rollbackReceipt, 'rollback receipt'],
] as const) {
sealFile(file, uid, label);
}
sealDirectory(selected.backupRoot, uid, 'backup root');
sealDirectory(selected.rollbackRoot, uid, 'rollback work root');
sealDirectory(selected.root, uid, 'root');
}
export function validateLocalReconciliationSecretConfigRolledBackStorage(
selected: Readonly<LocalReconciliationSecretConfigApplyPaths>,
intent: Readonly<LocalReconciliationSecretConfigApplyIntent>,
uid: number,
): void {
directoryMode(selected.root, uid, [0o500], 'root');
directoryMode(selected.backupRoot, uid, [0o500], 'backup root');
directoryMode(selected.rollbackRoot, uid, [0o500], 'rollback work root');
validateLocalReconciliationSecretConfigApplyCatalog(selected);
if (fs.readdirSync(selected.backupRoot).length !== 0) {
fail('sealed backup root is not empty');
}
if (
fs
.readdirSync(selected.rollbackRoot)
.some((entry) => entry !== 'receipt.json')
) {
fail('sealed rollback work root contains temporary material');
}
readLocalReconciliationSecretConfigApplyIntent(selected, uid);
readLocalReconciliationSecretConfigApplyReceipt(selected, uid);
readLocalReconciliationSecretConfigRollbackReceipt(selected, uid);
readLocalReconciliationSecretConfigMaterials(
selected,
intent.profile,
uid,
intent.material,
);
}
@@ -1132,6 +1132,7 @@ export async function readLocalReconciliationSecretConfigDecisionTerminal(
>,
secretConfigId: string,
uid: number,
acceptedSuccessorStates: readonly LocalCutoverInstanceHead['state'][] = [],
): Promise<Readonly<LocalReconciliationSecretConfigDecisionTerminal>> {
for (const [directory, label] of [
[options.deploymentRoot, 'deploymentRoot'],
@@ -1171,11 +1172,15 @@ export async function readLocalReconciliationSecretConfigDecisionTerminal(
intent.instanceId,
uid,
);
if (
head.state !== 'reconciliation_secret_config_reviewed' ||
head.previousHeadDigest !== receipt.preparedHeadDigest ||
head.sourceRecordDigest !== receipt.decisionDigest
) {
const reviewed =
head.state === 'reconciliation_secret_config_reviewed' &&
head.previousHeadDigest === receipt.preparedHeadDigest &&
head.sourceRecordDigest === receipt.decisionDigest;
const successor =
acceptedSuccessorStates.includes(head.state) &&
head.generation === intent.generation &&
head.updatedAtMs >= receipt.issuedAtMs;
if (!reviewed && !successor) {
configurationError('terminal decision is detached from instance head');
}
return Object.freeze({
@@ -28,6 +28,7 @@ export interface LocalReconciliationSecretConfigDecisionRequirement {
readonly candidateType: 'active_binding' | 'disabled_preservation';
readonly candidateDigest: string;
readonly sourceSetDigest: string;
readonly proposedSecretName: string;
readonly requirement: LocalReconciliationSecretConfigCandidateRequirement;
}
@@ -195,6 +196,7 @@ function candidate(
| 'disabled_preservation',
candidateDigest: record.candidateDigest,
sourceSetDigest: record.sourceSetDigest,
proposedSecretName: record.proposedSecretName as string,
requirement:
record.requirement as LocalReconciliationSecretConfigCandidateRequirement,
});
@@ -12,6 +12,7 @@ const {
commitLocalReconciliationApplication,
commitLocalReconciliationAutomationDecision,
commitLocalReconciliationSecretConfigDecision,
applyLocalReconciliationSecretConfig,
completeLocalReconciliation,
applyLocalReconciliationAutomation,
commitLocalReconciliationPlan,
@@ -24,6 +25,7 @@ const {
readLocalReconciliationAutomationDecisionTerminal,
readLocalReconciliationSecretConfigDecisionTerminal,
rollbackLocalReconciliationAutomationApply,
rollbackLocalReconciliationSecretConfigApply,
planLocalReconciliationAutomation,
planLocalReconciliationSecretConfig,
prepareLocalReconciliationPlan,
@@ -32,6 +34,7 @@ const {
verifyLocalReconciliationApplication,
verifyLocalReconciliationAutomationDecision,
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationSecretConfigApply,
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationPlan,
verifyLocalReconciliationSecretConfigPlan,
@@ -41,6 +44,7 @@ const {
verifyLocalReconciliationRunHistory,
writeLocalReconciliationReviewDiagnostics,
} = require('../dist/deployment/localDeployment.js');
const { provisionLocalSecretKeyring } = require('@qinglong/local-secret');
const {
normalizeLocalReconciliationCaptureManifest,
} = require('../dist/deployment/reconciliation/bundle.js');
@@ -846,6 +850,20 @@ function secretConfigDatabaseInitializer({
);
assert.equal(migration.status, 0, migration.stderr);
fs.chmodSync(targetDatabasePath, 0o600);
const target = new DatabaseSync(targetDatabasePath);
target.exec(`
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', 'review-owner', 1, 'active', 'owner',
'secret-config-apply-owner-binding', 'user', 'review-owner', 1
);
PRAGMA wal_checkpoint(TRUNCATE);
PRAGMA journal_mode=DELETE;
`);
target.close();
};
}
@@ -3827,6 +3845,366 @@ test('Secret/Config decision reauthenticates the same reviewer, seals exact cand
);
});
test('Secret/Config apply publishes encrypted material atomically and recovers every apply and rollback boundary', async (t) => {
const state = await plannedSecretConfigDecisionFixture(t, {
suffix: 'apply-terminal',
planId: '00000000-0000-4000-8000-000000000437',
reviewId: '00000000-0000-4000-8000-000000000438',
applicationId: '00000000-0000-4000-8000-000000000439',
secretConfigId: '00000000-0000-4000-8000-00000000043a',
});
const decisionId = '019b0000-0000-7000-8000-000000000437';
const prepareCommand = secretConfigDecisionPrepareCommand(state, decisionId);
const prepared = await prepareLocalReconciliationSecretConfigDecision(
prepareCommand,
);
const review = secretConfigDecisionFile(
state,
{ result: prepared },
[
{
disposition: 'preserve_disabled',
reason: 'reviewed_disabled_preservation',
},
],
'apply-terminal',
);
const decision = secretConfigDecisionCommitFixture(
state,
{ result: prepared, commandOptions: prepareCommand.options },
review.filePath,
);
const committed = await commitLocalReconciliationSecretConfigDecision(
decision.command,
decision.dependencies,
);
assert.equal(committed.outcome, 'ready');
const secretKeyringPath = path.join(
state.deploymentRoot,
'local-secret-keyring.json',
);
await provisionLocalSecretKeyring(secretKeyringPath);
const secretConfigApplyRoot = path.join(
path.dirname(state.captureRoot),
'secret-config-apply-terminal',
);
fs.mkdirSync(secretConfigApplyRoot, { mode: 0o700 });
const appliedAtMs = decision.command.request.committedAtMs + 1;
const applyOptions = {
...prepareCommand.options,
secretConfigApplyRoot,
targetDatabasePath: state.targetDatabasePath,
secretKeyringPath,
ownerPepperKeyringDirectory:
state.command.options.ownerPepperKeyringDirectory,
credentialFilePath: state.command.options.credentialFilePath,
};
const applyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply',
options: applyOptions,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedDecisionDigest: committed.decisionDigest,
expectedHeadDigest: committed.instanceHeadDigest,
mutationId: '00000000-0000-4000-8000-00000000043b',
requestId: 'secret-config-apply-terminal',
appliedAtMs,
},
};
let authentications = 0;
let confirmations = 0;
let databaseCloses = 0;
const applyDependencies = {
async openAuthenticationDatabase() {
return {
async close() {
databaseCloses += 1;
},
};
},
async authenticate(_database, options) {
authentications += 1;
assert.equal(
options.authenticationNamespace,
'local_reconciliation_secret_config_apply',
);
const authenticatedAtMs = options.now();
return {
principal: {
subject: { type: 'user', id: 'review-owner' },
authenticationId: 'local_reconciliation_secret_config_apply:test',
authenticatedAtMs,
expiresAtMs: authenticatedAtMs + 60 * 60 * 1_000,
assurance: 'local_console',
},
databaseFence: {
credentialId: 'review-owner',
credentialVersion: 1,
pepperKeyId: 'review-owner-v1',
pepperVersion: 1,
},
async confirm() {
confirmations += 1;
},
};
},
};
const targetIdentity = fs.statSync(state.targetDatabasePath);
await assert.rejects(
applyLocalReconciliationSecretConfig(
{
...applyCommand,
options: {
...applyOptions,
secretKeyringPath: path.join(
path.dirname(state.deploymentRoot),
'outside-secret-keyring.json',
),
},
},
applyDependencies,
),
/authentication or Secret material must be below deploymentRoot/,
);
for (const boundary of ['afterMaterialPublished']) {
await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, {
...applyDependencies,
[boundary]() {
throw new Error(`secret config apply ${boundary} response loss`);
},
}),
new RegExp(`secret config apply ${boundary} response loss`),
);
}
await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, {
...applyDependencies,
async createBackup() {
const error = new Error('router storage is full');
error.code = 'ENOSPC';
throw error;
},
}),
/router storage is full/,
);
assert.equal(
fs.existsSync(
path.join(secretConfigApplyRoot, state.secretConfigId, 'intent.json'),
),
false,
);
assert.equal(
readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
).state,
'reconciliation_secret_config_reviewed',
);
for (const boundary of ['afterBackupPublished', 'afterPreparedHead']) {
await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, {
...applyDependencies,
[boundary]() {
throw new Error(`secret config apply ${boundary} response loss`);
},
}),
new RegExp(`secret config apply ${boundary} response loss`),
);
}
await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, {
...applyDependencies,
async authenticate(database, options) {
const authenticated = await applyDependencies.authenticate(
database,
options,
);
return {
...authenticated,
principal: {
...authenticated.principal,
subject: { type: 'user', id: 'another-owner' },
},
};
},
}),
/current reviewer authentication is not strong or identical/,
);
for (const boundary of [
'afterDatabaseCommit',
'afterReceiptPublished',
'afterAppliedHead',
'afterAppliedSeal',
]) {
await assert.rejects(
applyLocalReconciliationSecretConfig(applyCommand, {
...applyDependencies,
[boundary]() {
throw new Error(`secret config apply ${boundary} response loss`);
},
}),
new RegExp(`secret config apply ${boundary} response loss`),
);
}
const applied = await applyLocalReconciliationSecretConfig(
applyCommand,
applyDependencies,
);
assert.equal(applied.status, 'existing');
assert.equal(applied.state, 'reconciliation_secret_config_applied');
assert.equal(applied.activeBindingCount, 0);
assert.equal(applied.disabledPreservationCount, 1);
assert.equal(fs.statSync(state.targetDatabasePath).ino, targetIdentity.ino);
const target = new DatabaseSync(state.targetDatabasePath, {
readOnly: true,
});
assert.equal(
target
.prepare(
'SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"',
)
.get().count,
1,
);
assert.equal(
target
.prepare('SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes"')
.get().count,
1,
);
target.close();
const evidenceRoot = path.join(secretConfigApplyRoot, state.secretConfigId);
const materialsPath = path.join(evidenceRoot, 'materials.ndjson');
const materialText = fs.readFileSync(materialsPath, 'utf8');
assert.equal(materialText.includes('private-secret-value'), false);
assert.equal(materialText.includes('DISABLED_TOKEN'), false);
assert.equal(fs.statSync(evidenceRoot).mode & 0o777, 0o500);
assert.equal(fs.statSync(materialsPath).mode & 0o777, 0o400);
assert.deepEqual(fs.readdirSync(evidenceRoot).sort(), [
'backup',
'intent.json',
'materials.ndjson',
'receipt.json',
'rollback-work',
]);
const verified = await verifyLocalReconciliationSecretConfigApply({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.verify',
options: applyOptions,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedApplyDigest: applied.applyDigest,
},
});
assert.equal(verified.status, 'verified');
const verifyPath = path.join(
state.deploymentRoot,
'secret-config-apply-verify.json',
);
fs.writeFileSync(
verifyPath,
`${JSON.stringify({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.verify',
options: applyOptions,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedApplyDigest: applied.applyDigest,
},
})}\n`,
{ mode: 0o600 },
);
const verifyCli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-secret-config-apply-verify',
'--command-file',
verifyPath,
],
{ encoding: 'utf8' },
);
assert.equal(verifyCli.status, 0, verifyCli.stderr);
assert.equal(JSON.parse(verifyCli.stdout).status, 'verified');
assert.equal(verifyCli.stdout.includes('private-secret-value'), false);
const rollbackCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.rollback',
options: applyOptions,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedApplyDigest: applied.applyDigest,
expectedHeadDigest: applied.instanceHeadDigest,
rolledBackAtMs: appliedAtMs + 1,
},
};
for (const boundary of [
'afterRestore',
'afterRollbackReceipt',
'afterRollbackHead',
'afterRollbackSeal',
]) {
await assert.rejects(
rollbackLocalReconciliationSecretConfigApply(rollbackCommand, {
...applyDependencies,
[boundary]() {
throw new Error(`secret config rollback ${boundary} response loss`);
},
}),
new RegExp(`secret config rollback ${boundary} response loss`),
);
}
const rolledBack = await rollbackLocalReconciliationSecretConfigApply(
rollbackCommand,
applyDependencies,
);
assert.equal(rolledBack.status, 'existing');
assert.equal(rolledBack.state, 'reconciliation_secret_config_rolled_back');
assert.equal(fs.statSync(state.targetDatabasePath).ino, targetIdentity.ino);
assert.deepEqual(fs.readdirSync(path.join(evidenceRoot, 'backup')), []);
assert.deepEqual(fs.readdirSync(path.join(evidenceRoot, 'rollback-work')), [
'receipt.json',
]);
const restored = new DatabaseSync(state.targetDatabasePath, {
readOnly: true,
});
assert.equal(
restored
.prepare(
'SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"',
)
.get().count,
0,
);
restored.close();
const rollbackVerified = await verifyLocalReconciliationSecretConfigApply({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.apply.verify',
options: applyOptions,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedApplyDigest: applied.applyDigest,
},
});
assert.equal(
rollbackVerified.state,
'reconciliation_secret_config_rolled_back',
);
assert.ok(authentications >= 3);
assert.ok(confirmations >= 4);
assert.equal(databaseCloses, authentications);
});
test('Secret/Config decision rejects manual plans, invalid candidate choices and reviewer drift', async (t) => {
const manual = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-manual-plan',