feat(local): authorize secret config reconciliation decisions

This commit is contained in:
whyour
2026-08-23 18:52:10 +08:00
parent 5dd8678d7e
commit 74ae891070
17 changed files with 4292 additions and 23 deletions
+10 -4
View File
@@ -26,15 +26,21 @@
的有界 content-free 投影与当前 head`defer`、active Env 无已采纳 Legacy Task、历史 `Configs` 或 target 冲突都继续 manual。publisher 以 no-replace、
`0400/0500`、文件/目录 `fsync` 发布 `plan.ndjson``receipt.json`,覆盖 plan、receipt、terminal seal、head CAS 四个 response-loss 窗口,并只允许
`reconciliation_application_planned|reconciliation_automation_applied → reconciliation_secret_config_planned` 的合法单向推进;独立 verify 只读复算,不修复漂移。
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。Local Admin 完整测试 `96/96`Local Owner 完整测试 `287/280/7/0`
后端完整门 `1563/1561/2/0`18-package clean build/test `2934/2912/22/0`package boundary、Cluster dependency、Edge import 与十四档
第四切片增加独立逐候选 signed decision:只有 ready、非空、无 manual/conflict 的 plan 才能 prepare;每条候选精确选择
`apply_active_binding``preserve_disabled``skip`,且 `skip` 必然形成 `manual_required`,不能造成部分 apply。decision 使用与 D-391 相同的强认证
reviewer,认证年龄最多 5 分钟、授权生命期最多 30 分钟;独立 HMAC domain 精确绑定 decision/SecretConfig/profile/plan/candidate/application/preparation/
prepared head/bundle/reviewer/time。Edge/Standalone decision 与 authorization 文件上限分别为 1/4 MiB,沿用 owner-only、sealed、no-replace 与 `fsync`
lineage 仅允许 `reconciliation_secret_config_planned → reconciliation_secret_config_decision_prepared → reconciliation_secret_config_reviewed`prepare/commit
的 publication response-loss 可精确重放而不重复认证,terminal verify 只读复算 sealed decision、authorization、receipt 与 reviewed head。
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。Local Admin 完整测试 `96/96`Local Owner 完整测试 `295/288/7/0`
后端完整门 `1564/1562/2/0`18-package clean build/test `2942/2920/22/0`package boundary、Cluster dependency `62/62`、Edge import 与十四档
Local artifact audit 全部 compatible,基础 Edge/Standalone 仍为 `2,611,978 / 2,612,056 bytes`、319 files、58 loaded modulesOwner-only authority
没有进入低资源常驻制品。
Local Admin 保持 48/47Local Owner 因个职责明确的嵌套文件增至 178/177,根目录仍只有一个 50 行 binary entryworkspace 仍为 18 packages、
Local Admin 保持 48/47Local Owner 因个职责明确的 decision 嵌套文件增至 184/183,根目录仍只有一个 50 行 binary entryworkspace 仍为 18 packages、
`singleSourcePackages=[]``shallowSourcePackages=[]`,且只允许 exact Secret/Config row planner 导入 inspection subpath。
D-385D-388 的 `config.sh`/Keyv/SSH data-directory lineage 与 SQLite `Envs` 保持分离;当前无稳定生产 schema 的历史 `Configs` 表继续 sealed+manual
不猜字段。后续切片必须完成独立 signed decision、Secret envelope + audit + Task/Trigger/dispatch + receipt ledger 的单事务发布、prepared/apply/rollback
不猜字段。后续切片必须完成 Secret envelope + audit + Task/Trigger/dispatch + receipt ledger 的单事务发布、prepared/apply/rollback
lineage、completion 下一 schema 与备份回收。D-397 apply 只声明 sealed source retained 且 `physicalErasureGuaranteed=false`;明文销毁必须在 restart/
readiness、观察窗和 rollback retention 之后另行强认证。Cluster 必须使用 PostgreSQL SERIALIZABLE ledger、外部 KMS/Secret provider 与 HA evidence
不复用 Local SQLite/POSIX authority,也不得把明文写入 PostgreSQL、ConfigMap、Pod env 或 Job command。
@@ -1,6 +1,6 @@
# ADR-0491:有界 Secret/Config Reconciliation 与任务环境绑定
- 状态:ProposedD-397 已实现 Legacy Env inspection、私有有界 row plandurable plan publication独立 signed decision原子 application 尚未完成)
- 状态:ProposedD-397 已实现 Legacy Env inspection、私有有界 row plandurable plan publication独立 signed decision原子 application 尚未完成)
- 日期:2026-08-23
- 决策:D-397
- 关联:ADR-0073、ADR-0074、ADR-0092、ADR-0094、ADR-0480、ADR-0482、ADR-0483、ADR-0484、ADR-0485、ADR-0486、ADR-0487、ADR-0488、ADR-0490
@@ -27,7 +27,7 @@ application plan
→ cross-domain completion
```
Secret/Config 不消费 Automation decision 作为自身授权。D-391 把 `secret_and_config` facts 标为 `blocked`,其 review action 只能是 `manual_external|defer`D-397 专用 adapter 只接收每条 fact 都精确选择 `manual_external` 的决策流,把它重新绑定到同一 sealed bundle、D-392 application plan 与当前 target snapshot,任何 `defer` 都继续失败关闭。后续 application 还必须消费独立的逐候选 signed decision强认证 User、Project Policy、Secret custody 与 Task mutation authority 都要在写事务前及事务内重新验证。
Secret/Config 不消费 Automation decision 作为自身授权。D-391 把 `secret_and_config` facts 标为 `blocked`,其 review action 只能是 `manual_external|defer`D-397 专用 adapter 只接收每条 fact 都精确选择 `manual_external` 的决策流,把它重新绑定到同一 sealed bundle、D-392 application plan 与当前 target snapshot,任何 `defer` 都继续失败关闭。D-397 使用独立的逐候选 signed decision:只有 `plan.outcome=ready`、候选非空且没有 manual/conflict 才能 prepare;后续 application 必须消费该终态授权。强认证 User、Project Policy、Secret custody 与 Task mutation authority 都要在写事务前及事务内重新验证。
存在 active Env 时,Automation 必须已经完成,且至少一个经 `QingLong3LegacyAdoptions` 证明的 Legacy Task 可绑定;否则不得用“Secret 已保存”冒充行为迁移。只有停用 Env 的场景可以在 Automation `no_effect` 后做纯保全。
@@ -68,6 +68,8 @@ Local Owner 使用私有 NDJSON row plan 记录 header、逐行 content-free dis
durable publisher 固定写入 `<secretConfigRoot>/<secretConfigId>/{plan.ndjson,receipt.json,staging/}`,使用 no-replace publication、`0400/0500` 权限、文件与目录 `fsync`,并覆盖 plan、receipt、terminal seal、head CAS 四个 response-loss 窗口。只有 Automation 无需 adapter 时的 `reconciliation_application_planned`,或 Automation 已完成时的 `reconciliation_automation_applied`,可以单向推进到 `reconciliation_secret_config_planned`verify 只读复算 plan/receipt/seal/head 绑定,不修复漂移。active Env 若没有至少一条已采纳 Legacy Task ledger 记录仍为 manual;历史 `Configs` 计入 `unadaptedLegacyConfigCount` 并保持 manual。
独立决策使用私有 NDJSON decision file,每个候选必须按 ordinal/digest 精确选择 `apply_active_binding/reviewed_active_binding``preserve_disabled/reviewed_disabled_preservation``skip/operator_excluded|target_conflict|security_review_required`。任何 `skip` 都把终态 outcome 降为 `manual_required`,不能被 application 当成部分成功;`no_effect` 不需要决策,manual/conflict plan 也不能通过强认证升级。签名授权使用与 D-391 review 相同的强认证 User,认证年龄最多 5 分钟、授权生命期最多 30 分钟,并以独立 HMAC domain 绑定 decision、Secret/Config plan、candidate set、application、preparation、prepared head、sealed bundle、reviewer 与时间。Edge/Standalone decision/authorization 文件分别限制为 1 MiB/4 MiB,沿用 owner-only `0700/0600`、sealed `0500/0400`、no-replace 与 `fsync`。lineage 单向推进 `reconciliation_secret_config_planned → reconciliation_secret_config_decision_prepared → reconciliation_secret_config_reviewed`prepare/commit 的全部 publication response-loss 窗口都精确重放且不重复认证,terminal verify 只读复算 sealed decision、authorization、receipt 与当前 reviewed head。
### 4. 原子 application 必须同时完成 custody 与行为绑定
后续 D-397 application 必须在一个 `BEGIN IMMEDIATE` 事务内完成:
@@ -133,6 +135,6 @@ Cluster 不得把 Legacy Env 明文写入 PostgreSQL、ConfigMap、Job command
## 当前验证与后续门禁
D-397 当前切片已经实现并测试:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、四个 response-loss 窗口、只读 verify 与 plan/receipt drift。调用方 visitor 的预算异常保持原始类型,不再被误报为 SQLite 读取失败。Local Admin 完整测试为 96/96Local Owner 完整测试为 287/280/7/0;后端完整门为 1563/1561/2/018-package clean build/test 为 2934/2912/22/0。package boundary、Cluster dependency、Edge import 与十四档 Local artifact audit 全部 compatible;基础 Edge/Standalone 仍为 2,611,978 / 2,612,056 bytes、319 files、58 loaded modulesOwner-only authority 没有进入低资源常驻制品。workspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`。Local Admin 保持 48/47Local Owner 随个职责明确的嵌套文件增至 178/177;根目录仍只有一个 50 行 binary entry,没有新增平铺源文件。依赖隔离门只允许 exact Secret/Config row planner 导入 inspection subpath相邻文件继续被拒绝。
D-397 当前切片已经实现并测试:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify 与 plan/receipt/authorization/head drift。调用方 visitor 的预算异常保持原始类型,不再被误报为 SQLite 读取失败。Local Admin 完整测试为 96/96Local Owner 完整测试为 295/288/7/0;后端完整门为 1564/1562/2/018-package clean build/test 为 2942/2920/22/0。package boundary、Cluster dependency 62/62、Edge import 与十四档 Local artifact audit 全部 compatible;基础 Edge/Standalone 仍为 2,611,978 / 2,612,056 bytes、319 files、58 loaded modulesOwner-only authority 没有进入低资源常驻制品。workspace 保持 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`。Local Admin 保持 48/47Local Owner 随个职责明确的 decision 嵌套文件增至 184/183;根目录仍只有一个 50 行 binary entry,没有新增平铺源文件。依赖隔离门只允许 exact Secret/Config row planner 导入 inspection subpath并只允许 decision authorization/coordinator 导入各自最小密钥、principal 与强认证只读 authority;邻接文件继续被拒绝。
转为 Accepted 前仍必须完成:独立 signed decision、原子 Secret/Task/Trigger/dispatch publisher、prepared/apply/rollback response-loss、completion schema 演进、18-package/boundary/artifact gates、真实 Edge 空间预算、PostgreSQL HA 与 Cluster Secret provider live gate。
转为 Accepted 前仍必须完成:原子 Secret/Task/Trigger/dispatch publisher、prepared/apply/rollback response-loss、completion schema 演进、18-package/boundary/artifact gates、真实 Edge 空间预算、PostgreSQL HA 与 Cluster Secret provider live gate。
+1 -1
View File
@@ -494,7 +494,7 @@
| [ADR-0488](./ADR-0488-cross-domain-reconciliation-completion-fence.md) | 跨领域 Reconciliation 完成围栏与目标重启授权 | Accepted |
| [ADR-0489](./ADR-0489-service-manager-completion-restart-lineage.md) | Service Manager 完成围栏重启谱系 | Accepted |
| [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | Accepted |
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 inspection + durable plan |
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | ProposedD-397 inspection + durable plan + signed decision |
## 规则
@@ -38,6 +38,8 @@ export type LocalCutoverInstanceHeadState =
| 'reconciliation_automation_applied'
| 'reconciliation_automation_rolled_back'
| 'reconciliation_secret_config_planned'
| 'reconciliation_secret_config_decision_prepared'
| 'reconciliation_secret_config_reviewed'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -185,6 +187,8 @@ function parseHead(value: unknown): Readonly<LocalCutoverInstanceHead> {
head.state !== 'reconciliation_automation_applied' &&
head.state !== 'reconciliation_automation_rolled_back' &&
head.state !== 'reconciliation_secret_config_planned' &&
head.state !== 'reconciliation_secret_config_decision_prepared' &&
head.state !== 'reconciliation_secret_config_reviewed' &&
head.state !== 'reconciliation_completed' &&
head.state !== 'rollback_prepared' &&
head.state !== 'legacy_restart_requested' &&
@@ -367,6 +371,8 @@ export function advanceLocalCutoverInstanceHead(
| 'reconciliation_automation_applied'
| 'reconciliation_automation_rolled_back'
| 'reconciliation_secret_config_planned'
| 'reconciliation_secret_config_decision_prepared'
| 'reconciliation_secret_config_reviewed'
| 'reconciliation_completed'
| 'rollback_prepared'
| 'legacy_restart_requested'
@@ -421,6 +427,8 @@ export function advanceLocalCutoverInstanceHead(
current.state === 'reconciliation_automation_applied' ||
current.state === 'reconciliation_automation_rolled_back' ||
current.state === 'reconciliation_secret_config_planned' ||
current.state === 'reconciliation_secret_config_decision_prepared' ||
current.state === 'reconciliation_secret_config_reviewed' ||
current.state === 'reconciliation_completed' ||
current.state === 'legacy_restart_requested' ||
current.state === 'legacy_running' ||
@@ -469,6 +477,10 @@ export function advanceLocalCutoverInstanceHead(
(state === 'reconciliation_secret_config_planned' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
(state === 'reconciliation_secret_config_decision_prepared' &&
current.state === 'reconciliation_secret_config_planned') ||
(state === 'reconciliation_secret_config_reviewed' &&
current.state === 'reconciliation_secret_config_decision_prepared') ||
(state === 'reconciliation_completed' &&
(current.state === 'reconciliation_application_planned' ||
current.state === 'reconciliation_automation_applied')) ||
@@ -161,6 +161,15 @@ import {
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationSecretConfigPlanCommandFile,
} from './reconciliation/application/secret-and-config/coordinator';
import {
commitLocalReconciliationSecretConfigDecision,
commitLocalReconciliationSecretConfigDecisionCommandFile,
prepareLocalReconciliationSecretConfigDecision,
prepareLocalReconciliationSecretConfigDecisionCommandFile,
readLocalReconciliationSecretConfigDecisionTerminal,
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
} from './reconciliation/application/secret-and-config/decisionCoordinator';
import {
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
@@ -218,6 +227,13 @@ export {
readLocalReconciliationSecretConfigTerminal,
verifyLocalReconciliationSecretConfigPlan,
verifyLocalReconciliationSecretConfigPlanCommandFile,
prepareLocalReconciliationSecretConfigDecision,
prepareLocalReconciliationSecretConfigDecisionCommandFile,
commitLocalReconciliationSecretConfigDecision,
commitLocalReconciliationSecretConfigDecisionCommandFile,
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
readLocalReconciliationSecretConfigDecisionTerminal,
preserveLocalReconciliationRunHistory,
preserveLocalReconciliationRunHistoryCommandFile,
readLocalReconciliationRunHistoryTerminal,
@@ -349,6 +365,27 @@ export {
type LocalReconciliationSecretConfigPlanDependencies,
type LocalReconciliationSecretConfigTerminal,
} from './reconciliation/application/secret-and-config/coordinator';
export {
normalizeLocalReconciliationSecretConfigDecisionCommitCommand,
normalizeLocalReconciliationSecretConfigDecisionPrepareCommand,
normalizeLocalReconciliationSecretConfigDecisionVerifyCommand,
type LocalReconciliationSecretConfigDecisionCommitCommand,
type LocalReconciliationSecretConfigDecisionCommitOptions,
type LocalReconciliationSecretConfigDecisionOptions,
type LocalReconciliationSecretConfigDecisionPrepareCommand,
type LocalReconciliationSecretConfigDecisionPrepareResult,
type LocalReconciliationSecretConfigDecisionTerminalResult,
type LocalReconciliationSecretConfigDecisionVerifyCommand,
} from './reconciliation/application/secret-and-config/decisionContract';
export {
type LocalReconciliationSecretConfigDecisionDependencies,
type LocalReconciliationSecretConfigDecisionTerminal,
} from './reconciliation/application/secret-and-config/decisionCoordinator';
export {
type LocalReconciliationSecretConfigDecision,
type LocalReconciliationSecretConfigDecisionDisposition,
type LocalReconciliationSecretConfigDecisionReason,
} from './reconciliation/application/secret-and-config/decisionFile';
export {
MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_PLAN_BYTES,
@@ -35,6 +35,9 @@ import {
rollbackLocalReconciliationAutomationApplyCommandFile,
planLocalReconciliationSecretConfigCommandFile,
verifyLocalReconciliationSecretConfigPlanCommandFile,
prepareLocalReconciliationSecretConfigDecisionCommandFile,
commitLocalReconciliationSecretConfigDecisionCommandFile,
verifyLocalReconciliationSecretConfigDecisionCommandFile,
preserveLocalReconciliationRunHistoryCommandFile,
verifyLocalReconciliationRunHistoryCommandFile,
completeLocalReconciliationCommandFile,
@@ -54,7 +57,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-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-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')) {
@@ -106,6 +109,9 @@ async function main(argv: readonly string[]): Promise<void> {
argv[0] !== 'reconciliation-automation-apply-rollback' &&
argv[0] !== 'reconciliation-secret-config-plan' &&
argv[0] !== 'reconciliation-secret-config-verify' &&
argv[0] !== 'reconciliation-secret-config-decision-prepare' &&
argv[0] !== 'reconciliation-secret-config-decision-commit' &&
argv[0] !== 'reconciliation-secret-config-decision-verify' &&
argv[0] !== 'reconciliation-run-history-preserve' &&
argv[0] !== 'reconciliation-run-history-verify' &&
argv[0] !== 'reconciliation-complete' &&
@@ -226,6 +232,12 @@ async function main(argv: readonly string[]): Promise<void> {
? planLocalReconciliationSecretConfigCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-verify'
? verifyLocalReconciliationSecretConfigPlanCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-decision-prepare'
? prepareLocalReconciliationSecretConfigDecisionCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-decision-commit'
? commitLocalReconciliationSecretConfigDecisionCommandFile(argv[2]!)
: argv[0] === 'reconciliation-secret-config-decision-verify'
? verifyLocalReconciliationSecretConfigDecisionCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-preserve'
? preserveLocalReconciliationRunHistoryCommandFile(argv[2]!)
: argv[0] === 'reconciliation-run-history-verify'
@@ -0,0 +1,720 @@
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import type { LocalSecretKeyMaterial } from '@qinglong/runtime-core/local-secret';
import {
normalizeSecurityPrincipal,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import { LocalReconciliationReviewIssuerKeyringFileProvider } from '../../review/issuerKeyring';
import {
LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS,
normalizeLocalReconciliationSecretConfigDecision,
type LocalReconciliationSecretConfigDecision,
type LocalReconciliationSecretConfigDecisionDisposition,
} from './decisionFile';
const HEADER_KIND =
'qinglong3-local-reconciliation-secret-config-authorization-header';
const SIGNATURE_KIND =
'qinglong3-local-reconciliation-secret-config-authorization-signature';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const UUID_V4_PATTERN =
/^[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_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const MAX_LINE_BYTES = 64 * 1024;
const MAX_AUTHENTICATION_AGE_MS = 5 * 60 * 1_000;
const MAX_AUTHORIZATION_LIFETIME_MS = 30 * 60 * 1_000;
export interface LocalReconciliationSecretConfigAuthorizationHeader {
readonly schemaVersion: 1;
readonly kind: typeof HEADER_KIND;
readonly decisionId: string;
readonly secretConfigId: string;
readonly profile: 'edge' | 'standalone';
readonly secretConfigPlanDigest: string;
readonly candidateSetDigest: string;
readonly applicationPlanDigest: string;
readonly preparationDigest: string;
readonly preparedHeadDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly reviewer: Readonly<SecurityPrincipal>;
readonly issuedAtMs: number;
readonly expiresAtMs: number;
}
export interface LocalReconciliationSecretConfigDecisionCounts {
readonly apply_active_binding: number;
readonly preserve_disabled: number;
readonly skip: number;
}
interface SignatureRecord {
readonly schemaVersion: 1;
readonly kind: typeof SIGNATURE_KIND;
readonly algorithm: 'hmac-sha256';
readonly keyId: string;
readonly contentBytes: number;
readonly contentDigest: string;
readonly decisionCount: number;
readonly decisionSetDigest: string;
readonly decisionFileDigest: string;
readonly dispositionCounts: Readonly<LocalReconciliationSecretConfigDecisionCounts>;
readonly authenticationTag: string;
}
export interface LocalReconciliationSecretConfigAuthorizationEvidence {
readonly fileBytes: number;
readonly authorizationDigest: string;
readonly decisionCount: number;
readonly decisionSetDigest: string;
readonly decisionFileDigest: string;
readonly keyId: string;
readonly dispositionCounts: Readonly<LocalReconciliationSecretConfigDecisionCounts>;
readonly header: Readonly<LocalReconciliationSecretConfigAuthorizationHeader>;
}
export interface LocalReconciliationSecretConfigAuthorizationScope {
readonly evidence: Readonly<LocalReconciliationSecretConfigAuthorizationEvidence>;
readonly decisions: readonly Readonly<LocalReconciliationSecretConfigDecision>[];
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config authorization ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function canonicalLine(value: unknown): Buffer {
const bytes = Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
if (bytes.byteLength < 3 || bytes.byteLength > MAX_LINE_BYTES + 1) {
bytes.fill(0);
configurationError('record exceeds its line bound');
}
return bytes;
}
function writeAll(descriptor: number, bytes: Buffer): void {
let offset = 0;
while (offset < bytes.byteLength) {
const written = fs.writeSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
);
if (written < 1) configurationError('write stalled');
offset += written;
}
}
function syncDirectory(directory: string): void {
const descriptor = fs.openSync(directory, fs.constants.O_RDONLY);
try {
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function signingMessage(
contentDigest: string,
contentBytes: number,
decisionFileDigest: string,
): Buffer {
return Buffer.from(
`qinglong3.local-reconciliation-secret-config-authorization.v1\0${contentDigest}\0${contentBytes}\0${decisionFileDigest}`,
'utf8',
);
}
function wipe(material: LocalSecretKeyMaterial | null | undefined): void {
material?.key.fill(0);
}
function zeroCounts(): Record<
LocalReconciliationSecretConfigDecisionDisposition,
number
> {
return {
apply_active_binding: 0,
preserve_disabled: 0,
skip: 0,
};
}
function normalizeCounts(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionCounts> {
const counts = exact(
value,
[...LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS],
'disposition counts',
);
if (
LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS.some(
(key) =>
!Number.isSafeInteger(counts[key]) || (counts[key] as number) < 0,
)
) {
configurationError('disposition counts are invalid');
}
return Object.freeze({
apply_active_binding: counts.apply_active_binding as number,
preserve_disabled: counts.preserve_disabled as number,
skip: counts.skip as number,
});
}
function normalizeHeader(
value: unknown,
): Readonly<LocalReconciliationSecretConfigAuthorizationHeader> {
const record = exact(
value,
[
'applicationPlanDigest',
'bundleDigest',
'bundleFingerprintDigest',
'candidateSetDigest',
'decisionId',
'expiresAtMs',
'issuedAtMs',
'kind',
'preparationDigest',
'preparedHeadDigest',
'profile',
'reviewer',
'schemaVersion',
'secretConfigId',
'secretConfigPlanDigest',
],
'header',
);
const issuedAtMs = record.issuedAtMs;
const expiresAtMs = record.expiresAtMs;
if (
!Number.isSafeInteger(issuedAtMs) ||
(issuedAtMs as number) < 0 ||
!Number.isSafeInteger(expiresAtMs) ||
(expiresAtMs as number) <= (issuedAtMs as number) ||
(expiresAtMs as number) - (issuedAtMs as number) >
MAX_AUTHORIZATION_LIFETIME_MS
) {
configurationError('authorization lifetime is invalid');
}
let reviewer: Readonly<SecurityPrincipal>;
try {
reviewer = normalizeSecurityPrincipal(
record.reviewer as SecurityPrincipal,
issuedAtMs as number,
);
} catch (error) {
return configurationError('reviewer is invalid', error);
}
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
typeof record.decisionId !== 'string' ||
!UUID_V7_PATTERN.test(record.decisionId) ||
typeof record.secretConfigId !== 'string' ||
!UUID_V4_PATTERN.test(record.secretConfigId) ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
![
record.secretConfigPlanDigest,
record.candidateSetDigest,
record.applicationPlanDigest,
record.preparationDigest,
record.preparedHeadDigest,
record.bundleDigest,
record.bundleFingerprintDigest,
].every(
(selected) =>
typeof selected === 'string' && DIGEST_PATTERN.test(selected),
) ||
reviewer.subject.type !== 'user' ||
!['hardware', 'local_console', 'multi_factor'].includes(
reviewer.assurance,
) ||
reviewer.authenticatedAtMs > (issuedAtMs as number) ||
(issuedAtMs as number) - reviewer.authenticatedAtMs >
MAX_AUTHENTICATION_AGE_MS ||
reviewer.expiresAtMs < (expiresAtMs as number)
) {
configurationError('header binding is invalid');
}
return Object.freeze({
...(record as unknown as LocalReconciliationSecretConfigAuthorizationHeader),
reviewer,
});
}
function normalizeSignature(value: unknown): Readonly<SignatureRecord> {
const record = exact(
value,
[
'algorithm',
'authenticationTag',
'contentBytes',
'contentDigest',
'decisionCount',
'decisionFileDigest',
'decisionSetDigest',
'dispositionCounts',
'keyId',
'kind',
'schemaVersion',
],
'signature',
);
let tag: Buffer | undefined;
try {
tag =
typeof record.authenticationTag === 'string'
? Buffer.from(record.authenticationTag, 'base64url')
: Buffer.alloc(0);
if (
record.schemaVersion !== 1 ||
record.kind !== SIGNATURE_KIND ||
record.algorithm !== 'hmac-sha256' ||
typeof record.keyId !== 'string' ||
record.keyId.length < 1 ||
!Number.isSafeInteger(record.contentBytes) ||
(record.contentBytes as number) < 1 ||
typeof record.contentDigest !== 'string' ||
!DIGEST_PATTERN.test(record.contentDigest) ||
!Number.isSafeInteger(record.decisionCount) ||
(record.decisionCount as number) < 1 ||
typeof record.decisionSetDigest !== 'string' ||
!DIGEST_PATTERN.test(record.decisionSetDigest) ||
typeof record.decisionFileDigest !== 'string' ||
!DIGEST_PATTERN.test(record.decisionFileDigest) ||
typeof record.authenticationTag !== 'string' ||
tag.byteLength !== 32 ||
tag.toString('base64url') !== record.authenticationTag
) {
configurationError('signature record is invalid');
}
return Object.freeze({
...(record as unknown as SignatureRecord),
dispositionCounts: normalizeCounts(record.dispositionCounts),
});
} finally {
tag?.fill(0);
}
}
export function buildLocalReconciliationSecretConfigAuthorizationHeader(
value: Omit<
LocalReconciliationSecretConfigAuthorizationHeader,
'schemaVersion' | 'kind'
>,
): Readonly<LocalReconciliationSecretConfigAuthorizationHeader> {
return normalizeHeader({
schemaVersion: 1,
kind: HEADER_KIND,
...value,
});
}
export async function publishLocalReconciliationSecretConfigAuthorization(
options: Readonly<{
targetPath: string;
stagePath: string;
maxBytes: number;
header: Readonly<LocalReconciliationSecretConfigAuthorizationHeader>;
keyProvider: LocalReconciliationReviewIssuerKeyringFileProvider;
writeDecisions: (
append: (
decision: Readonly<LocalReconciliationSecretConfigDecision>,
) => void,
) => Readonly<{
decisionFileDigest: string;
confirmDecisionFileAuthority(): void;
}>;
confirmAuthority(): void | Promise<void>;
}>,
): Promise<Readonly<LocalReconciliationSecretConfigAuthorizationEvidence>> {
await options.confirmAuthority();
const keyringBefore = options.keyProvider.inspect();
let material: LocalSecretKeyMaterial | undefined;
let descriptor: number | undefined;
let created = false;
try {
material = await options.keyProvider.active();
if (
material.keyId !== keyringBefore.activeKeyId ||
material.key.byteLength !== 32
) {
configurationError('active issuer key drifted');
}
descriptor = fs.openSync(
options.stagePath,
fs.constants.O_CREAT |
fs.constants.O_EXCL |
fs.constants.O_WRONLY |
(fs.constants.O_NOFOLLOW ?? 0),
0o600,
);
created = true;
fs.fchmodSync(descriptor, 0o600);
const fileHash = createHash('sha256');
const contentHash = createHash('sha256');
const decisionHash = createHash('sha256').update(
'qinglong3.local-reconciliation-secret-config-decision-set.v1\0',
);
let fileBytes = 0;
let contentBytes = 0;
let decisionCount = 0;
const dispositionCounts = zeroCounts();
const writeContent = (value: unknown, selectedDecision: boolean): void => {
const line = canonicalLine(value);
try {
if (fileBytes + line.byteLength > options.maxBytes) {
configurationError('exceeds profile byte budget');
}
writeAll(descriptor!, line);
fileHash.update(line);
contentHash.update(line);
contentBytes += line.byteLength;
fileBytes += line.byteLength;
if (selectedDecision) decisionHash.update(line);
} finally {
line.fill(0);
}
};
writeContent(options.header, false);
const decisionFile = options.writeDecisions((value) => {
const selected = normalizeLocalReconciliationSecretConfigDecision(value);
writeContent(selected, true);
decisionCount += 1;
dispositionCounts[selected.disposition] += 1;
});
if (
decisionCount < 1 ||
!DIGEST_PATTERN.test(decisionFile.decisionFileDigest)
) {
configurationError('decision file evidence is invalid');
}
await options.confirmAuthority();
decisionFile.confirmDecisionFileAuthority();
const keyringAfter = options.keyProvider.inspect();
if (
keyringAfter.keyringDigest !== keyringBefore.keyringDigest ||
keyringAfter.activeKeyId !== material.keyId
) {
configurationError('issuer authority changed while signing');
}
const contentDigest = contentHash.digest('hex');
const decisionSetDigest = decisionHash.digest('hex');
const message = signingMessage(
contentDigest,
contentBytes,
decisionFile.decisionFileDigest,
);
const authenticationTag = createHmac('sha256', material.key)
.update(message)
.digest('base64url');
message.fill(0);
const signature: Readonly<SignatureRecord> = Object.freeze({
schemaVersion: 1,
kind: SIGNATURE_KIND,
algorithm: 'hmac-sha256',
keyId: material.keyId,
contentBytes,
contentDigest,
decisionCount,
decisionSetDigest,
decisionFileDigest: decisionFile.decisionFileDigest,
dispositionCounts: Object.freeze({ ...dispositionCounts }),
authenticationTag,
});
const signatureLine = canonicalLine(signature);
try {
if (fileBytes + signatureLine.byteLength > options.maxBytes) {
configurationError('exceeds profile byte budget');
}
writeAll(descriptor, signatureLine);
fileHash.update(signatureLine);
fileBytes += signatureLine.byteLength;
} finally {
signatureLine.fill(0);
}
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
await options.confirmAuthority();
decisionFile.confirmDecisionFileAuthority();
const keyringSigned = options.keyProvider.inspect();
if (
keyringSigned.keyringDigest !== keyringBefore.keyringDigest ||
keyringSigned.activeKeyId !== material.keyId
) {
configurationError('issuer authority changed after signing');
}
try {
fs.linkSync(options.stagePath, options.targetPath);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
configurationError('target already exists');
}
syncDirectory(path.dirname(options.targetPath));
fs.unlinkSync(options.stagePath);
created = false;
syncDirectory(path.dirname(options.stagePath));
return Object.freeze({
fileBytes,
authorizationDigest: fileHash.digest('hex'),
decisionCount,
decisionSetDigest,
decisionFileDigest: decisionFile.decisionFileDigest,
keyId: material.keyId,
dispositionCounts: Object.freeze({ ...dispositionCounts }),
header: options.header,
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be published', error);
} finally {
wipe(material);
if (descriptor !== undefined) fs.closeSync(descriptor);
if (created) {
try {
fs.unlinkSync(options.stagePath);
} catch {
// A complete owner-only stage is recoverable; partial stages fail closed.
}
}
}
}
export async function verifyLocalReconciliationSecretConfigAuthorization(
filePath: string,
options: Readonly<{
maxBytes: number;
allowedModes: readonly number[];
keyProvider: LocalReconciliationReviewIssuerKeyringFileProvider;
expected: Readonly<{
decisionId: string;
secretConfigId: string;
profile: 'edge' | 'standalone';
secretConfigPlanDigest: string;
candidateSetDigest: string;
applicationPlanDigest: string;
preparationDigest: string;
preparedHeadDigest: string;
bundleDigest: string;
bundleFingerprintDigest: string;
decisionFileDigest: string;
}>;
}>,
): Promise<Readonly<LocalReconciliationSecretConfigAuthorizationScope>> {
const uid = process.getuid?.();
if (!Number.isSafeInteger(uid) || uid !== process.geteuid?.()) {
configurationError('requires stable POSIX identity');
}
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
!options.allowedModes.includes(Number(before.mode) & 0o777) ||
before.nlink !== 1n ||
before.size < 2n ||
before.size > BigInt(options.maxBytes)
) {
configurationError('file identity or size is invalid');
}
let descriptor: number | undefined;
let bytes: Buffer | undefined;
let material: LocalSecretKeyMaterial | null | undefined;
try {
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
) {
configurationError('file changed while opening');
}
bytes = fs.readFileSync(descriptor);
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
) {
configurationError('file changed while reading');
}
const linked = fs.lstatSync(filePath, { bigint: true });
if (
linked.dev !== opened.dev ||
linked.ino !== opened.ino ||
linked.size !== opened.size ||
linked.mtimeNs !== opened.mtimeNs ||
linked.ctimeNs !== opened.ctimeNs
) {
configurationError('file path changed while reading');
}
let decoded: string;
try {
decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (error) {
return configurationError('file is not UTF-8', error);
}
const framed = decoded.split('\n');
if (framed.at(-1) !== '') configurationError('must end with newline');
framed.pop();
if (framed.length < 3) configurationError('file is incomplete');
const parsed = framed.map((line, index) => {
if (
Buffer.byteLength(line, 'utf8') < 2 ||
Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES
) {
configurationError(`record ${index + 1} exceeds its line bound`);
}
try {
return JSON.parse(line) as unknown;
} catch (error) {
return configurationError(`record ${index + 1} is not JSON`, error);
}
});
const selectedHeader = normalizeHeader(parsed[0]);
const signature = normalizeSignature(parsed.at(-1));
const decisions = Object.freeze(
parsed
.slice(1, -1)
.map((value) =>
normalizeLocalReconciliationSecretConfigDecision(value),
),
);
const expected = options.expected;
if (
selectedHeader.decisionId !== expected.decisionId ||
selectedHeader.secretConfigId !== expected.secretConfigId ||
selectedHeader.profile !== expected.profile ||
selectedHeader.secretConfigPlanDigest !==
expected.secretConfigPlanDigest ||
selectedHeader.candidateSetDigest !== expected.candidateSetDigest ||
selectedHeader.applicationPlanDigest !== expected.applicationPlanDigest ||
selectedHeader.preparationDigest !== expected.preparationDigest ||
selectedHeader.preparedHeadDigest !== expected.preparedHeadDigest ||
selectedHeader.bundleDigest !== expected.bundleDigest ||
selectedHeader.bundleFingerprintDigest !==
expected.bundleFingerprintDigest ||
signature.decisionFileDigest !== expected.decisionFileDigest ||
signature.decisionCount !== decisions.length
) {
configurationError('authorization binding drifted');
}
const contentLines = framed.slice(0, -1).map((line) => `${line}\n`);
const content = Buffer.from(contentLines.join(''), 'utf8');
const decisionBytes = Buffer.from(
framed
.slice(1, -1)
.map((line) => `${line}\n`)
.join(''),
'utf8',
);
try {
const counts = zeroCounts();
for (const selected of decisions) counts[selected.disposition] += 1;
if (
signature.contentBytes !== content.byteLength ||
signature.contentDigest !==
createHash('sha256').update(content).digest('hex') ||
signature.decisionSetDigest !==
createHash('sha256')
.update(
'qinglong3.local-reconciliation-secret-config-decision-set.v1\0',
)
.update(decisionBytes)
.digest('hex') ||
JSON.stringify(signature.dispositionCounts) !== JSON.stringify(counts)
) {
configurationError('authorization summary drifted');
}
material = await options.keyProvider.resolve(signature.keyId);
if (!material || material.key.byteLength !== 32) {
configurationError('issuer key is unavailable');
}
const message = signingMessage(
signature.contentDigest,
signature.contentBytes,
signature.decisionFileDigest,
);
const expectedTag = createHmac('sha256', material.key)
.update(message)
.digest();
const suppliedTag = Buffer.from(signature.authenticationTag, 'base64url');
message.fill(0);
try {
if (
suppliedTag.byteLength !== expectedTag.byteLength ||
!timingSafeEqual(suppliedTag, expectedTag)
) {
configurationError('authentication tag is invalid');
}
} finally {
expectedTag.fill(0);
suppliedTag.fill(0);
}
return Object.freeze({
evidence: Object.freeze({
fileBytes: bytes.byteLength,
authorizationDigest: createHash('sha256').update(bytes).digest('hex'),
decisionCount: decisions.length,
decisionSetDigest: signature.decisionSetDigest,
decisionFileDigest: signature.decisionFileDigest,
keyId: signature.keyId,
dispositionCounts: signature.dispositionCounts,
header: selectedHeader,
}),
decisions,
});
} finally {
content.fill(0);
decisionBytes.fill(0);
}
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be verified', error);
} finally {
wipe(material);
bytes?.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
@@ -0,0 +1,443 @@
import path from 'node:path';
import { currentIdentity } from '../../../foundation/contract';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const UUID_V4_PATTERN =
/^[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_PATTERN =
/^[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_PATTERN = /^\/[A-Za-z0-9._/@-]+$/;
const MAX_PATH_BYTES = 4_096;
export const MAX_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_LIFETIME_MS =
30 * 60 * 1_000;
export interface LocalReconciliationSecretConfigDecisionOptions {
readonly deploymentRoot: string;
readonly applicationRoot: string;
readonly secretConfigRoot: string;
readonly secretConfigDecisionRoot: string;
readonly allowRootService: boolean;
}
export interface LocalReconciliationSecretConfigDecisionPrepareCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.decision.prepare';
readonly options: Readonly<LocalReconciliationSecretConfigDecisionOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedSecretConfigPlanDigest: string;
expectedHeadDigest: string;
preparedAtMs: number;
}>;
}
export interface LocalReconciliationSecretConfigDecisionCommitOptions
extends LocalReconciliationSecretConfigDecisionOptions {
readonly targetDatabasePath: string;
readonly ownerPepperKeyringDirectory: string;
readonly credentialFilePath: string;
readonly busyTimeoutMs?: number;
}
export interface LocalReconciliationSecretConfigDecisionCommitCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.decision.commit';
readonly options: Readonly<LocalReconciliationSecretConfigDecisionCommitOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedPreparationDigest: string;
expectedHeadDigest: string;
decisionFilePath: string;
committedAtMs: number;
authorizationLifetimeMs: number;
}>;
}
export interface LocalReconciliationSecretConfigDecisionVerifyCommand {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.decision.verify';
readonly options: Readonly<LocalReconciliationSecretConfigDecisionOptions>;
readonly request: Readonly<{
decisionId: string;
secretConfigId: string;
expectedDecisionDigest: string;
}>;
}
export interface LocalReconciliationSecretConfigDecisionPrepareResult {
readonly schemaVersion: 1;
readonly operation: 'local.deployment.reconciliation.secret-config.decision.prepare';
readonly status: 'prepared' | 'existing';
readonly state: 'reconciliation_secret_config_decision_prepared';
readonly decisionId: string;
readonly secretConfigId: string;
readonly preparationDigest: string;
readonly instanceHeadDigest: string;
}
export interface LocalReconciliationSecretConfigDecisionTerminalResult {
readonly schemaVersion: 1;
readonly operation:
| 'local.deployment.reconciliation.secret-config.decision.commit'
| 'local.deployment.reconciliation.secret-config.decision.verify';
readonly status: 'prepared' | 'existing' | 'verified';
readonly state: 'reconciliation_secret_config_reviewed';
readonly decisionId: string;
readonly secretConfigId: string;
readonly decisionDigest: string;
readonly signedDecisionSetDigest: string;
readonly candidateCount: number;
readonly applyBindingCount: number;
readonly preserveDisabledCount: number;
readonly skippedCount: number;
readonly outcome: 'ready' | 'manual_required';
readonly instanceHeadDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config decision ${message}`,
);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${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])
) {
configurationError(`${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_PATTERN.test(value) ||
Buffer.byteLength(value, 'utf8') > MAX_PATH_BYTES
) {
configurationError(`${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 descendant(root: string, candidate: string, label: string): void {
const relative = path.relative(root, candidate);
if (
relative.length === 0 ||
relative === '..' ||
relative.startsWith(`..${path.sep}`) ||
path.isAbsolute(relative)
) {
configurationError(`${label} must be below deploymentRoot`);
}
}
function digest(value: unknown, label: string): string {
if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) {
configurationError(`${label} must be a SHA-256 digest`);
}
return value;
}
function secretConfigId(value: unknown): string {
if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) {
configurationError('secretConfigId must be a lowercase UUID v4');
}
return value;
}
function decisionId(value: unknown): string {
if (typeof value !== 'string' || !UUID_V7_PATTERN.test(value)) {
configurationError('decisionId must be a lowercase UUID v7');
}
return value;
}
function baseOptions(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionOptions> {
const options = object(value, 'options');
const keys = [
'allowRootService',
'applicationRoot',
'deploymentRoot',
'secretConfigDecisionRoot',
'secretConfigRoot',
];
exact(options, keys, 'options');
const identity = currentIdentity();
if (
typeof options.allowRootService !== 'boolean' ||
(identity.uid === 0) !== options.allowRootService
) {
configurationError('command identity is invalid');
}
const roots = keys
.filter((key) => key !== 'allowRootService')
.map((key) => safePath(options[key], key));
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]!)
) {
configurationError('authority roots overlap');
}
}
}
return Object.freeze({
deploymentRoot: safePath(options.deploymentRoot, 'deploymentRoot'),
applicationRoot: safePath(options.applicationRoot, 'applicationRoot'),
secretConfigRoot: safePath(options.secretConfigRoot, 'secretConfigRoot'),
secretConfigDecisionRoot: safePath(
options.secretConfigDecisionRoot,
'secretConfigDecisionRoot',
),
allowRootService: options.allowRootService as boolean,
});
}
function command(value: unknown, operation: string) {
const selected = object(value, 'command');
exact(
selected,
['operation', 'options', 'request', 'schemaVersion'],
'command',
);
if (selected.schemaVersion !== 1 || selected.operation !== operation) {
configurationError('command version or operation is invalid');
}
return Object.freeze({
options: selected.options,
request: object(selected.request, 'request'),
});
}
export function normalizeLocalReconciliationSecretConfigDecisionPrepareCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionPrepareCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.decision.prepare',
);
exact(
selected.request,
[
'decisionId',
'expectedHeadDigest',
'expectedSecretConfigPlanDigest',
'preparedAtMs',
'secretConfigId',
],
'request',
);
if (
!Number.isSafeInteger(selected.request.preparedAtMs) ||
(selected.request.preparedAtMs as number) < 0
) {
configurationError('preparedAtMs is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.prepare',
options: baseOptions(selected.options),
request: Object.freeze({
decisionId: decisionId(selected.request.decisionId),
secretConfigId: secretConfigId(selected.request.secretConfigId),
expectedSecretConfigPlanDigest: digest(
selected.request.expectedSecretConfigPlanDigest,
'expectedSecretConfigPlanDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
preparedAtMs: selected.request.preparedAtMs as number,
}),
});
}
export function normalizeLocalReconciliationSecretConfigDecisionCommitCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionCommitCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.decision.commit',
);
const options = object(selected.options, 'options');
const hasBusyTimeout = Object.hasOwn(options, 'busyTimeoutMs');
exact(
options,
[
'allowRootService',
'applicationRoot',
'credentialFilePath',
'deploymentRoot',
'ownerPepperKeyringDirectory',
'secretConfigDecisionRoot',
'secretConfigRoot',
'targetDatabasePath',
...(hasBusyTimeout ? ['busyTimeoutMs'] : []),
],
'options',
);
const base = baseOptions({
allowRootService: options.allowRootService,
applicationRoot: options.applicationRoot,
deploymentRoot: options.deploymentRoot,
secretConfigDecisionRoot: options.secretConfigDecisionRoot,
secretConfigRoot: options.secretConfigRoot,
});
const targetDatabasePath = safePath(
options.targetDatabasePath,
'targetDatabasePath',
);
const ownerPepperKeyringDirectory = safePath(
options.ownerPepperKeyringDirectory,
'ownerPepperKeyringDirectory',
);
const credentialFilePath = safePath(
options.credentialFilePath,
'credentialFilePath',
);
for (const [candidate, label] of [
[ownerPepperKeyringDirectory, 'ownerPepperKeyringDirectory'],
[credentialFilePath, 'credentialFilePath'],
] as const) {
descendant(base.deploymentRoot, candidate, label);
}
if (
options.busyTimeoutMs !== undefined &&
(!Number.isSafeInteger(options.busyTimeoutMs) ||
(options.busyTimeoutMs as number) < 1 ||
(options.busyTimeoutMs as number) > 60_000)
) {
configurationError('busyTimeoutMs is invalid');
}
exact(
selected.request,
[
'authorizationLifetimeMs',
'committedAtMs',
'decisionFilePath',
'decisionId',
'expectedHeadDigest',
'expectedPreparationDigest',
'secretConfigId',
],
'request',
);
const decisionFilePath = safePath(
selected.request.decisionFilePath,
'decisionFilePath',
);
if (
[
base.deploymentRoot,
base.applicationRoot,
base.secretConfigRoot,
base.secretConfigDecisionRoot,
].some(
(root) =>
overlaps(root, decisionFilePath) || overlaps(decisionFilePath, root),
)
) {
configurationError('decisionFilePath overlaps an authority root');
}
if (
!Number.isSafeInteger(selected.request.committedAtMs) ||
(selected.request.committedAtMs as number) < 0 ||
!Number.isSafeInteger(selected.request.authorizationLifetimeMs) ||
(selected.request.authorizationLifetimeMs as number) < 1 ||
(selected.request.authorizationLifetimeMs as number) >
MAX_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_LIFETIME_MS
) {
configurationError('decision timestamp or lifetime is invalid');
}
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.commit',
options: Object.freeze({
...base,
targetDatabasePath,
ownerPepperKeyringDirectory,
credentialFilePath,
...(options.busyTimeoutMs === undefined
? {}
: { busyTimeoutMs: options.busyTimeoutMs as number }),
}),
request: Object.freeze({
decisionId: decisionId(selected.request.decisionId),
secretConfigId: secretConfigId(selected.request.secretConfigId),
expectedPreparationDigest: digest(
selected.request.expectedPreparationDigest,
'expectedPreparationDigest',
),
expectedHeadDigest: digest(
selected.request.expectedHeadDigest,
'expectedHeadDigest',
),
decisionFilePath,
committedAtMs: selected.request.committedAtMs as number,
authorizationLifetimeMs: selected.request
.authorizationLifetimeMs as number,
}),
});
}
export function normalizeLocalReconciliationSecretConfigDecisionVerifyCommand(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionVerifyCommand> {
const selected = command(
value,
'local.deployment.reconciliation.secret-config.decision.verify',
);
exact(
selected.request,
['decisionId', 'expectedDecisionDigest', 'secretConfigId'],
'request',
);
return Object.freeze({
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.verify',
options: baseOptions(selected.options),
request: Object.freeze({
decisionId: decisionId(selected.request.decisionId),
secretConfigId: secretConfigId(selected.request.secretConfigId),
expectedDecisionDigest: digest(
selected.request.expectedDecisionDigest,
'expectedDecisionDigest',
),
}),
});
}
@@ -0,0 +1,277 @@
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import { cutoverDigest } from '../../../cutover/targetEvidence';
import {
normalizeLocalReconciliationSecretConfigDecisionPrepareCommand,
type LocalReconciliationSecretConfigDecisionPrepareCommand,
} from './decisionContract';
const INTENT_SCHEMA =
'qinglong3-local-reconciliation-secret-config-decision-intent';
const RECEIPT_SCHEMA =
'qinglong3-local-reconciliation-secret-config-decision-receipt';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export interface LocalReconciliationSecretConfigDecisionIntent {
readonly schema: typeof INTENT_SCHEMA;
readonly schemaVersion: 1;
readonly command: Readonly<LocalReconciliationSecretConfigDecisionPrepareCommand>;
readonly applicationId: string;
readonly applicationPlanDigest: string;
readonly profile: 'edge' | 'standalone';
readonly projectId: string;
readonly secretConfigPlanDigest: string;
readonly candidateSetDigest: string;
readonly bundleDigest: string;
readonly bundleFingerprintDigest: string;
readonly instanceId: string;
readonly cutoverId: string;
readonly activationDigest: string;
readonly generation: number;
readonly preparationDigest: string;
}
export interface LocalReconciliationSecretConfigDecisionReceipt {
readonly schema: typeof RECEIPT_SCHEMA;
readonly schemaVersion: 1;
readonly state: 'reconciliation_secret_config_reviewed';
readonly decisionId: string;
readonly secretConfigId: string;
readonly secretConfigPlanDigest: string;
readonly candidateSetDigest: string;
readonly applicationPlanDigest: string;
readonly preparedHeadDigest: string;
readonly authorizationDigest: string;
readonly signedDecisionSetDigest: string;
readonly decisionFileDigest: string;
readonly reviewerDigest: string;
readonly candidateCount: number;
readonly applyBindingCount: number;
readonly preserveDisabledCount: number;
readonly skippedCount: number;
readonly outcome: 'ready' | 'manual_required';
readonly issuedAtMs: number;
readonly expiresAtMs: number;
readonly decisionDigest: string;
}
function configurationError(message: string): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config decision evidence ${message}`,
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
export function buildLocalReconciliationSecretConfigDecisionIntent(
input: Omit<
LocalReconciliationSecretConfigDecisionIntent,
'schema' | 'schemaVersion' | 'preparationDigest'
>,
): Readonly<LocalReconciliationSecretConfigDecisionIntent> {
const payload = Object.freeze({
schema: INTENT_SCHEMA,
schemaVersion: 1 as const,
...input,
});
return Object.freeze({
...payload,
preparationDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationSecretConfigDecisionIntent(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionIntent> {
const intent = exact(
value,
[
'activationDigest',
'applicationId',
'applicationPlanDigest',
'bundleDigest',
'bundleFingerprintDigest',
'candidateSetDigest',
'command',
'cutoverId',
'generation',
'instanceId',
'preparationDigest',
'profile',
'projectId',
'schema',
'schemaVersion',
'secretConfigPlanDigest',
],
'intent',
);
const { preparationDigest, ...payload } = intent;
const normalizedCommand =
normalizeLocalReconciliationSecretConfigDecisionPrepareCommand(
intent.command,
);
const normalizedPayload = Object.freeze({
...payload,
command: normalizedCommand,
});
if (
intent.schema !== INTENT_SCHEMA ||
intent.schemaVersion !== 1 ||
typeof preparationDigest !== 'string' ||
!DIGEST_PATTERN.test(preparationDigest) ||
cutoverDigest(payload) !== preparationDigest ||
cutoverDigest(normalizedPayload) !== preparationDigest ||
(intent.profile !== 'edge' && intent.profile !== 'standalone') ||
![
intent.applicationPlanDigest,
intent.secretConfigPlanDigest,
intent.candidateSetDigest,
intent.bundleDigest,
intent.bundleFingerprintDigest,
intent.activationDigest,
].every(
(selected) =>
typeof selected === 'string' && DIGEST_PATTERN.test(selected),
) ||
![
intent.applicationId,
intent.projectId,
intent.instanceId,
intent.cutoverId,
].every(
(selected) => typeof selected === 'string' && selected.length > 0,
) ||
!Number.isSafeInteger(intent.generation) ||
(intent.generation as number) < 1
) {
configurationError('intent binding is invalid');
}
return Object.freeze({
...normalizedPayload,
preparationDigest,
}) as unknown as Readonly<LocalReconciliationSecretConfigDecisionIntent>;
}
export function buildLocalReconciliationSecretConfigDecisionReceipt(
input: Omit<
LocalReconciliationSecretConfigDecisionReceipt,
'schema' | 'schemaVersion' | 'state' | 'decisionDigest'
>,
): Readonly<LocalReconciliationSecretConfigDecisionReceipt> {
const payload = Object.freeze({
schema: RECEIPT_SCHEMA,
schemaVersion: 1 as const,
state: 'reconciliation_secret_config_reviewed' as const,
...input,
});
return Object.freeze({
...payload,
decisionDigest: cutoverDigest(payload),
});
}
export function normalizeLocalReconciliationSecretConfigDecisionReceipt(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecisionReceipt> {
const receipt = exact(
value,
[
'applicationPlanDigest',
'applyBindingCount',
'authorizationDigest',
'candidateCount',
'candidateSetDigest',
'decisionDigest',
'decisionFileDigest',
'decisionId',
'expiresAtMs',
'issuedAtMs',
'outcome',
'preparedHeadDigest',
'preserveDisabledCount',
'reviewerDigest',
'schema',
'schemaVersion',
'secretConfigId',
'secretConfigPlanDigest',
'signedDecisionSetDigest',
'skippedCount',
'state',
],
'receipt',
);
const { decisionDigest, ...payload } = receipt;
if (
receipt.schema !== RECEIPT_SCHEMA ||
receipt.schemaVersion !== 1 ||
receipt.state !== 'reconciliation_secret_config_reviewed' ||
typeof decisionDigest !== 'string' ||
!DIGEST_PATTERN.test(decisionDigest) ||
cutoverDigest(payload) !== decisionDigest ||
![
receipt.secretConfigPlanDigest,
receipt.candidateSetDigest,
receipt.applicationPlanDigest,
receipt.preparedHeadDigest,
receipt.authorizationDigest,
receipt.signedDecisionSetDigest,
receipt.decisionFileDigest,
receipt.reviewerDigest,
].every(
(selected) =>
typeof selected === 'string' && DIGEST_PATTERN.test(selected),
) ||
![
receipt.candidateCount,
receipt.applyBindingCount,
receipt.preserveDisabledCount,
receipt.skippedCount,
].every(
(selected) => Number.isSafeInteger(selected) && (selected as number) >= 0,
) ||
(receipt.applyBindingCount as number) +
(receipt.preserveDisabledCount as number) +
(receipt.skippedCount as number) !==
receipt.candidateCount ||
(receipt.outcome !== 'ready' && receipt.outcome !== 'manual_required') ||
(receipt.outcome === 'ready' && (receipt.skippedCount as number) !== 0) ||
(receipt.outcome === 'manual_required' &&
(receipt.skippedCount as number) === 0) ||
!Number.isSafeInteger(receipt.issuedAtMs) ||
(receipt.issuedAtMs as number) < 0 ||
!Number.isSafeInteger(receipt.expiresAtMs) ||
(receipt.expiresAtMs as number) <= (receipt.issuedAtMs as number) ||
typeof receipt.decisionId !== 'string' ||
typeof receipt.secretConfigId !== 'string'
) {
configurationError('receipt binding is invalid');
}
return Object.freeze(
receipt,
) as unknown as Readonly<LocalReconciliationSecretConfigDecisionReceipt>;
}
export function localReconciliationSecretConfigDecisionEvidenceContents(
value:
| Readonly<LocalReconciliationSecretConfigDecisionIntent>
| Readonly<LocalReconciliationSecretConfigDecisionReceipt>,
): string {
return `${JSON.stringify(value, null, 2)}\n`;
}
@@ -0,0 +1,426 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import type { LocalReconciliationSecretConfigDecisionRequirement } from './planReader';
const HEADER_KIND =
'qinglong3-local-reconciliation-secret-config-decision-header';
const DECISION_KIND = 'qinglong3-local-reconciliation-secret-config-decision';
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const UUID_V7_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const MAX_LINE_BYTES = 64 * 1024;
const READ_CHUNK_BYTES = 64 * 1024;
export const MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_BYTES =
1 * 1024 * 1024;
export const MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_BYTES =
4 * 1024 * 1024;
export const LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS =
Object.freeze(['apply_active_binding', 'preserve_disabled', 'skip'] as const);
export type LocalReconciliationSecretConfigDecisionDisposition =
(typeof LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS)[number];
export const LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_REASONS =
Object.freeze([
'reviewed_active_binding',
'reviewed_disabled_preservation',
'operator_excluded',
'target_conflict',
'security_review_required',
] as const);
export type LocalReconciliationSecretConfigDecisionReason =
(typeof LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_REASONS)[number];
export interface LocalReconciliationSecretConfigDecisionHeader {
readonly schemaVersion: 1;
readonly kind: typeof HEADER_KIND;
readonly decisionContractVersion: 1;
readonly decisionId: string;
readonly profile: 'edge' | 'standalone';
readonly secretConfigPlanDigest: string;
readonly preparationDigest: string;
}
export interface LocalReconciliationSecretConfigDecision {
readonly schemaVersion: 1;
readonly kind: typeof DECISION_KIND;
readonly candidateOrdinal: number;
readonly candidateDigest: string;
readonly disposition: LocalReconciliationSecretConfigDecisionDisposition;
readonly reason: LocalReconciliationSecretConfigDecisionReason;
}
export interface LocalReconciliationSecretConfigDecisionFileEvidence {
readonly fileBytes: number;
readonly fileDigest: string;
readonly decisionCount: number;
}
export interface LocalReconciliationSecretConfigDecisionCursor {
readonly header: Readonly<LocalReconciliationSecretConfigDecisionHeader>;
next(): Readonly<LocalReconciliationSecretConfigDecision> | null;
}
interface FileIdentity {
readonly device: bigint;
readonly inode: bigint;
readonly size: bigint;
readonly modifiedAtNs: bigint;
readonly changedAtNs: bigint;
readonly uid: number;
readonly mode: number;
readonly links: bigint;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config decision file ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function parse(line: Buffer, label: string): unknown {
if (line.byteLength < 2 || line.byteLength > MAX_LINE_BYTES) {
configurationError(`${label} line bound is invalid`);
}
try {
return JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(line),
) as unknown;
} catch (error) {
return configurationError(`${label} is not UTF-8 JSON`, error);
}
}
function header(
value: unknown,
expected: Readonly<{
decisionId: string;
profile: 'edge' | 'standalone';
secretConfigPlanDigest: string;
preparationDigest: string;
}>,
): Readonly<LocalReconciliationSecretConfigDecisionHeader> {
const record = exact(
value,
[
'decisionContractVersion',
'decisionId',
'kind',
'preparationDigest',
'profile',
'schemaVersion',
'secretConfigPlanDigest',
],
'header',
);
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
record.decisionContractVersion !== 1 ||
typeof record.decisionId !== 'string' ||
!UUID_V7_PATTERN.test(record.decisionId) ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
typeof record.secretConfigPlanDigest !== 'string' ||
!DIGEST_PATTERN.test(record.secretConfigPlanDigest) ||
typeof record.preparationDigest !== 'string' ||
!DIGEST_PATTERN.test(record.preparationDigest) ||
record.decisionId !== expected.decisionId ||
record.profile !== expected.profile ||
record.secretConfigPlanDigest !== expected.secretConfigPlanDigest ||
record.preparationDigest !== expected.preparationDigest
) {
configurationError('header binding is invalid');
}
return Object.freeze(
record,
) as unknown as Readonly<LocalReconciliationSecretConfigDecisionHeader>;
}
function decision(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecision> {
const record = exact(
value,
[
'candidateDigest',
'candidateOrdinal',
'disposition',
'kind',
'reason',
'schemaVersion',
],
'decision',
);
if (
record.schemaVersion !== 1 ||
record.kind !== DECISION_KIND ||
!Number.isSafeInteger(record.candidateOrdinal) ||
(record.candidateOrdinal as number) < 1 ||
typeof record.candidateDigest !== 'string' ||
!DIGEST_PATTERN.test(record.candidateDigest) ||
!LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_DISPOSITIONS.includes(
record.disposition as LocalReconciliationSecretConfigDecisionDisposition,
) ||
!LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_REASONS.includes(
record.reason as LocalReconciliationSecretConfigDecisionReason,
)
) {
configurationError('decision is invalid');
}
const allowed: Record<
LocalReconciliationSecretConfigDecisionDisposition,
readonly LocalReconciliationSecretConfigDecisionReason[]
> = {
apply_active_binding: ['reviewed_active_binding'],
preserve_disabled: ['reviewed_disabled_preservation'],
skip: ['operator_excluded', 'target_conflict', 'security_review_required'],
};
if (
!allowed[
record.disposition as LocalReconciliationSecretConfigDecisionDisposition
].includes(record.reason as LocalReconciliationSecretConfigDecisionReason)
) {
configurationError('decision reason does not match disposition');
}
return Object.freeze(
record,
) as unknown as Readonly<LocalReconciliationSecretConfigDecision>;
}
export function normalizeLocalReconciliationSecretConfigDecision(
value: unknown,
): Readonly<LocalReconciliationSecretConfigDecision> {
return decision(value);
}
export function assertLocalReconciliationSecretConfigDecisionMatchesRequirement(
selected: Readonly<LocalReconciliationSecretConfigDecision>,
requirement: Readonly<LocalReconciliationSecretConfigDecisionRequirement>,
): void {
if (
selected.candidateOrdinal !== requirement.candidateOrdinal ||
selected.candidateDigest !== requirement.candidateDigest
) {
configurationError('decision sequence does not match canonical candidates');
}
if (
(requirement.requirement === 'review_apply_binding' &&
selected.disposition !== 'apply_active_binding' &&
selected.disposition !== 'skip') ||
(requirement.requirement === 'review_preserve_disabled' &&
selected.disposition !== 'preserve_disabled' &&
selected.disposition !== 'skip') ||
(requirement.requirement === 'review_skip_conflict' &&
(selected.disposition !== 'skip' ||
!['target_conflict', 'security_review_required'].includes(
selected.reason,
)))
) {
configurationError('decision is not allowed for canonical candidate');
}
}
function identity(stat: fs.BigIntStats): FileIdentity {
return Object.freeze({
device: stat.dev,
inode: stat.ino,
size: stat.size,
modifiedAtNs: stat.mtimeNs,
changedAtNs: stat.ctimeNs,
uid: Number(stat.uid),
mode: Number(stat.mode) & 0o777,
links: stat.nlink,
});
}
function same(left: FileIdentity, right: FileIdentity): boolean {
return Object.keys(left).every(
(key) =>
left[key as keyof FileIdentity] === right[key as keyof FileIdentity],
);
}
export function withLocalReconciliationSecretConfigDecisionFile<T>(
filePath: string,
expected: Readonly<{
decisionId: string;
profile: 'edge' | 'standalone';
secretConfigPlanDigest: string;
preparationDigest: string;
}>,
consume: (cursor: LocalReconciliationSecretConfigDecisionCursor) => T,
): Readonly<{
result: T;
evidence: Readonly<LocalReconciliationSecretConfigDecisionFileEvidence>;
confirmIdentity(): void;
}> {
const uid = process.getuid?.();
if (!Number.isSafeInteger(uid) || uid !== process.geteuid?.()) {
configurationError('requires stable POSIX identity');
}
const parentPath = path.dirname(filePath);
const parentBefore = identity(fs.lstatSync(parentPath, { bigint: true }));
if (
parentBefore.uid !== uid ||
parentBefore.mode !== 0o700 ||
fs.realpathSync(parentPath) !== parentPath
) {
configurationError('parent must be a canonical current-UID 0700 directory');
}
const beforeStat = fs.lstatSync(filePath, { bigint: true });
const before = identity(beforeStat);
const maxBytes =
expected.profile === 'edge'
? MAX_EDGE_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_BYTES
: MAX_STANDALONE_LOCAL_RECONCILIATION_SECRET_CONFIG_DECISION_BYTES;
if (
!beforeStat.isFile() ||
beforeStat.isSymbolicLink() ||
before.uid !== uid ||
before.mode !== 0o600 ||
before.links !== 1n ||
before.size < 2n ||
before.size > BigInt(maxBytes)
) {
configurationError('identity or size is invalid');
}
let descriptor: number | undefined;
try {
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = identity(fs.fstatSync(descriptor, { bigint: true }));
if (!same(before, opened)) configurationError('changed while opening');
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
const lineBuffer = Buffer.allocUnsafe(MAX_LINE_BYTES);
const fileHash = createHash('sha256');
let chunkOffset = 0;
let chunkLength = 0;
let lineLength = 0;
let fileBytes = 0;
let decisionCount = 0;
let ended = false;
const nextLine = (): Buffer | null => {
while (true) {
if (chunkOffset >= chunkLength) {
chunkLength = fs.readSync(
descriptor!,
chunk,
0,
chunk.byteLength,
null,
);
chunkOffset = 0;
if (chunkLength === 0) {
if (lineLength !== 0) configurationError('must end with newline');
return null;
}
fileBytes += chunkLength;
fileHash.update(chunk.subarray(0, chunkLength));
}
const byte = chunk[chunkOffset++]!;
if (byte === 0x0a) {
const line = Buffer.from(lineBuffer.subarray(0, lineLength));
lineLength = 0;
return line;
}
if (lineLength >= MAX_LINE_BYTES) {
configurationError('line is too large');
}
lineBuffer[lineLength] = byte;
lineLength += 1;
}
};
const first = nextLine();
if (!first) configurationError('header is missing');
let parsedHeader: Readonly<LocalReconciliationSecretConfigDecisionHeader>;
try {
parsedHeader = header(parse(first, 'header'), expected);
} finally {
first.fill(0);
}
const cursor: LocalReconciliationSecretConfigDecisionCursor = Object.freeze(
{
header: parsedHeader,
next(): Readonly<LocalReconciliationSecretConfigDecision> | null {
if (ended) return null;
const line = nextLine();
if (!line) {
ended = true;
return null;
}
try {
const selected = decision(parse(line, 'decision'));
decisionCount += 1;
return selected;
} finally {
line.fill(0);
}
},
},
);
const result = consume(cursor);
if (cursor.next() !== null) {
configurationError('decision consumer did not consume the full file');
}
const after = identity(fs.fstatSync(descriptor, { bigint: true }));
const parentAfter = identity(fs.lstatSync(parentPath, { bigint: true }));
const current = identity(fs.lstatSync(filePath, { bigint: true }));
if (
!same(before, after) ||
!same(before, current) ||
!same(parentBefore, parentAfter) ||
fileBytes !== Number(before.size)
) {
configurationError('identity changed while reading');
}
const evidence = Object.freeze({
fileBytes,
fileDigest: fileHash.digest('hex'),
decisionCount,
});
return Object.freeze({
result,
evidence,
confirmIdentity(): void {
const parentCurrent = identity(
fs.lstatSync(parentPath, { bigint: true }),
);
const fileCurrent = identity(fs.lstatSync(filePath, { bigint: true }));
if (!same(parentBefore, parentCurrent) || !same(before, fileCurrent)) {
configurationError('identity changed after reading');
}
},
});
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('cannot be read', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
@@ -0,0 +1,478 @@
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import { cutoverDigest } from '../../../cutover/targetEvidence';
import type {
LocalReconciliationSecretConfigCandidateRequirement,
LocalReconciliationSecretConfigPlanHeader,
LocalReconciliationSecretConfigPlanReceipt,
} from './rowPlan';
const HEADER_KIND = 'qinglong3-local-reconciliation-secret-config-plan-header';
const ROW_KIND = 'qinglong3-local-reconciliation-secret-config-plan-row';
const CANDIDATE_KIND =
'qinglong3-local-reconciliation-secret-config-plan-candidate';
const FOOTER_KIND = 'qinglong3-local-reconciliation-secret-config-plan-footer';
const READ_CHUNK_BYTES = 64 * 1024;
const MAX_LINE_BYTES = 64 * 1024;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
interface FileLine {
readonly value: Buffer;
readonly framed: Buffer;
}
export interface LocalReconciliationSecretConfigDecisionRequirement {
readonly candidateOrdinal: number;
readonly candidateType: 'active_binding' | 'disabled_preservation';
readonly candidateDigest: string;
readonly sourceSetDigest: string;
readonly requirement: LocalReconciliationSecretConfigCandidateRequirement;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config plan reader ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function parse(line: Buffer, label: string): unknown {
if (line.length < 2 || line.length > MAX_LINE_BYTES) {
configurationError(`${label} exceeds its line bound`);
}
try {
return JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(line),
) as unknown;
} catch (error) {
return configurationError(`${label} is not UTF-8 JSON`, error);
}
}
function* lines(descriptor: number, size: number): Iterable<FileLine> {
let position = 0;
let pending = Buffer.alloc(0);
try {
while (position < size) {
const chunk = Buffer.allocUnsafe(
Math.min(READ_CHUNK_BYTES, size - position),
);
const bytesRead = fs.readSync(
descriptor,
chunk,
0,
chunk.length,
position,
);
if (bytesRead < 1) {
chunk.fill(0);
configurationError('file ended unexpectedly');
}
position += bytesRead;
const material = pending.length
? Buffer.concat([pending, chunk.subarray(0, bytesRead)])
: Buffer.from(chunk.subarray(0, bytesRead));
pending.fill(0);
chunk.fill(0);
let cursor = 0;
for (;;) {
const newline = material.indexOf(0x0a, cursor);
if (newline < 0) break;
const framed = Buffer.from(material.subarray(cursor, newline + 1));
const value = Buffer.from(material.subarray(cursor, newline));
cursor = newline + 1;
yield Object.freeze({ value, framed });
}
pending = Buffer.from(material.subarray(cursor));
material.fill(0);
if (pending.length > MAX_LINE_BYTES) {
configurationError('record exceeds its line bound');
}
}
if (pending.length !== 0) configurationError('file is not newline framed');
} finally {
pending.fill(0);
}
}
function candidate(
value: unknown,
expectedOrdinal: number,
): Readonly<LocalReconciliationSecretConfigDecisionRequirement> {
const record = exact(
value,
[
'candidateDigest',
'candidateOrdinal',
'candidatePlanDigest',
'candidateType',
'kind',
'proposedSecretName',
'requirement',
'schemaVersion',
'sourceRowCount',
'sourceSetDigest',
'target',
],
'candidate',
);
const { candidatePlanDigest, ...payload } = record;
const target = record.target as Record<string, unknown> | undefined;
if (
record.schemaVersion !== 1 ||
record.kind !== CANDIDATE_KIND ||
record.candidateOrdinal !== expectedOrdinal ||
(record.candidateType !== 'active_binding' &&
record.candidateType !== 'disabled_preservation') ||
typeof record.candidateDigest !== 'string' ||
!DIGEST_PATTERN.test(record.candidateDigest) ||
!Number.isSafeInteger(record.sourceRowCount) ||
(record.sourceRowCount as number) < 1 ||
typeof record.sourceSetDigest !== 'string' ||
!DIGEST_PATTERN.test(record.sourceSetDigest) ||
typeof record.proposedSecretName !== 'string' ||
record.proposedSecretName.length < 1 ||
![
'review_apply_binding',
'review_preserve_disabled',
'review_skip_conflict',
].includes(record.requirement as string) ||
!target ||
Array.isArray(target) ||
typeof candidatePlanDigest !== 'string' ||
!DIGEST_PATTERN.test(candidatePlanDigest) ||
cutoverDigest(payload) !== candidatePlanDigest
) {
configurationError('candidate binding is invalid');
}
const targetKeys = Object.keys(target).sort().join('\0');
if (
(target.state === 'absent' && targetKeys !== 'state') ||
(target.state === 'occupied' &&
(targetKeys !== ['contentDigest', 'state', 'version'].sort().join('\0') ||
!Number.isSafeInteger(target.version) ||
(target.version as number) < 1 ||
typeof target.contentDigest !== 'string' ||
!DIGEST_PATTERN.test(target.contentDigest))) ||
(target.state !== 'absent' && target.state !== 'occupied') ||
(record.requirement === 'review_skip_conflict' &&
target.state !== 'occupied') ||
(record.requirement !== 'review_skip_conflict' &&
target.state !== 'absent') ||
(record.requirement === 'review_apply_binding' &&
record.candidateType !== 'active_binding') ||
(record.requirement === 'review_preserve_disabled' &&
record.candidateType !== 'disabled_preservation')
) {
configurationError('candidate target requirement is invalid');
}
return Object.freeze({
candidateOrdinal: record.candidateOrdinal as number,
candidateType: record.candidateType as
| 'active_binding'
| 'disabled_preservation',
candidateDigest: record.candidateDigest,
sourceSetDigest: record.sourceSetDigest,
requirement:
record.requirement as LocalReconciliationSecretConfigCandidateRequirement,
});
}
function header(
value: unknown,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
): Readonly<LocalReconciliationSecretConfigPlanHeader> {
const record = exact(
value,
[
'applicationId',
'applicationPlanDigest',
'bundleDigest',
'bundleFingerprintDigest',
'headerDigest',
'kind',
'preparedAtMs',
'preparedHeadDigest',
'profile',
'projectId',
'reviewAuthorizationDigest',
'reviewDecisionFileDigest',
'reviewDecisionSetDigest',
'reviewDigest',
'schemaVersion',
'secretConfigId',
'tableDisposition',
'unadaptedLegacyConfigCount',
],
'header',
);
const { headerDigest, ...payload } = record;
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
record.secretConfigId !== receipt.secretConfigId ||
record.applicationId !== receipt.applicationId ||
record.applicationPlanDigest !== receipt.applicationPlanDigest ||
record.preparedHeadDigest !== receipt.preparedHeadDigest ||
record.preparedAtMs !== receipt.preparedAtMs ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
typeof record.projectId !== 'string' ||
record.projectId.length < 1 ||
(record.tableDisposition !== 'absent' &&
record.tableDisposition !== 'manual_external') ||
!Number.isSafeInteger(record.unadaptedLegacyConfigCount) ||
(record.unadaptedLegacyConfigCount as number) < 0 ||
![
record.applicationPlanDigest,
record.bundleDigest,
record.bundleFingerprintDigest,
record.reviewAuthorizationDigest,
record.reviewDecisionFileDigest,
record.reviewDecisionSetDigest,
record.reviewDigest,
record.preparedHeadDigest,
headerDigest,
].every(
(selected) =>
typeof selected === 'string' && DIGEST_PATTERN.test(selected),
) ||
cutoverDigest(payload) !== headerDigest
) {
configurationError('header binding is invalid');
}
return record as unknown as Readonly<LocalReconciliationSecretConfigPlanHeader>;
}
export function readLocalReconciliationSecretConfigPlanHeader(
filePath: string,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
): Readonly<LocalReconciliationSecretConfigPlanHeader> {
let descriptor: number | undefined;
const bytes = Buffer.allocUnsafe(MAX_LINE_BYTES + 1);
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o400 ||
before.nlink !== 1n ||
before.size !== BigInt(receipt.planFileBytes)
) {
configurationError('plan file 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
) {
configurationError('plan file changed while opening');
}
const bytesRead = fs.readSync(
descriptor,
bytes,
0,
Math.min(bytes.length, Number(opened.size)),
0,
);
const newline = bytes.subarray(0, bytesRead).indexOf(0x0a);
if (newline < 2 || newline > MAX_LINE_BYTES) {
configurationError('header is missing or exceeds its line bound');
}
const selected = header(
parse(bytes.subarray(0, newline), 'header'),
receipt,
);
const after = fs.fstatSync(descriptor, { bigint: true });
const linked = fs.lstatSync(filePath, { bigint: true });
if (
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.mtimeNs !== opened.mtimeNs ||
after.ctimeNs !== opened.ctimeNs ||
linked.dev !== opened.dev ||
linked.ino !== opened.ino ||
linked.size !== opened.size ||
linked.mtimeNs !== opened.mtimeNs ||
linked.ctimeNs !== opened.ctimeNs
) {
configurationError('plan file changed while reading header');
}
return selected;
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('plan header cannot be read', error);
} finally {
bytes.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
export function createLocalReconciliationSecretConfigDecisionRequirementFactory(
filePath: string,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
): () => Iterable<LocalReconciliationSecretConfigDecisionRequirement> {
return () =>
(function* (): Iterable<LocalReconciliationSecretConfigDecisionRequirement> {
let descriptor: number | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o400 ||
before.nlink !== 1n ||
before.size !== BigInt(receipt.planFileBytes)
) {
configurationError('plan file 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
) {
configurationError('plan file changed while opening');
}
const fileHash = createHash('sha256');
const candidateHash = createHash('sha256').update(
'qinglong3.local-reconciliation-secret-config-candidate-set.v1\0',
);
let headerSeen = false;
let footerSeen = false;
let candidateCount = 0;
for (const selected of lines(descriptor, Number(opened.size))) {
try {
fileHash.update(selected.framed);
const value = parse(selected.value, 'record');
const kind = (value as { readonly kind?: unknown })?.kind;
if (!headerSeen) {
header(value, receipt);
headerSeen = true;
continue;
}
if (footerSeen) configurationError('record follows footer');
if (kind === ROW_KIND) continue;
if (kind === CANDIDATE_KIND) {
const requirement = candidate(value, candidateCount + 1);
candidateHash.update(selected.framed);
candidateCount += 1;
yield requirement;
continue;
}
const footer = exact(
value,
[
'activeGroupCount',
'activeRowCount',
'adoptedLegacyTaskCount',
'automationAdoptionRecordCount',
'automationAdoptionSetDigest',
'bindingReadyCount',
'candidateSetDigest',
'disabledRowCount',
'eligibleBindingCount',
'eligiblePreservationCount',
'kind',
'legacyInventoryDigest',
'manualGroupCount',
'manualRowCount',
'outcome',
'preservationReadyCount',
'rowCount',
'rowSetDigest',
'schemaVersion',
'secretConfigId',
'secretConfigPlanDigest',
'tableState',
'targetConflictCount',
'unadaptedLegacyConfigCount',
],
'footer',
);
if (
footer.schemaVersion !== 1 ||
footer.kind !== FOOTER_KIND ||
footer.secretConfigId !== receipt.secretConfigId ||
footer.secretConfigPlanDigest !==
receipt.secretConfigPlanDigest ||
footer.candidateSetDigest !== candidateHash.digest('hex') ||
footer.candidateSetDigest !== receipt.candidateSetDigest ||
footer.eligibleBindingCount !== receipt.eligibleBindingCount ||
footer.eligiblePreservationCount !==
receipt.eligiblePreservationCount ||
footer.targetConflictCount !== receipt.targetConflictCount ||
footer.outcome !== receipt.outcome ||
candidateCount !==
receipt.eligibleBindingCount +
receipt.eligiblePreservationCount +
receipt.targetConflictCount
) {
configurationError('footer binding is invalid');
}
footerSeen = true;
} finally {
selected.value.fill(0);
selected.framed.fill(0);
}
}
const after = fs.fstatSync(descriptor, { bigint: true });
if (
!headerSeen ||
!footerSeen ||
fileHash.digest('hex') !== receipt.planFileDigest ||
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.mtimeNs !== opened.mtimeNs ||
after.ctimeNs !== opened.ctimeNs
) {
configurationError('plan file content drifted');
}
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
configurationError('plan file cannot be read', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
})();
}
@@ -775,7 +775,9 @@ function replayResult(
head.state === 'reconciliation_automation_apply_prepared' ||
head.state === 'reconciliation_automation_applied' ||
head.state === 'reconciliation_automation_rolled_back' ||
head.state === 'reconciliation_secret_config_planned');
head.state === 'reconciliation_secret_config_planned' ||
head.state === 'reconciliation_secret_config_decision_prepared' ||
head.state === 'reconciliation_secret_config_reviewed');
const completionRestartPendingHead =
(record.state === 'target_active' || record.state === 'manual_required') &&
completionFence !== undefined &&
@@ -11,6 +11,7 @@ const {
commitLocalReconciliationCapture,
commitLocalReconciliationApplication,
commitLocalReconciliationAutomationDecision,
commitLocalReconciliationSecretConfigDecision,
completeLocalReconciliation,
applyLocalReconciliationAutomation,
commitLocalReconciliationPlan,
@@ -18,8 +19,10 @@ const {
prepareLocalReconciliationCapture,
prepareLocalReconciliationApplication,
prepareLocalReconciliationAutomationDecision,
prepareLocalReconciliationSecretConfigDecision,
preserveLocalReconciliationRunHistory,
readLocalReconciliationAutomationDecisionTerminal,
readLocalReconciliationSecretConfigDecisionTerminal,
rollbackLocalReconciliationAutomationApply,
planLocalReconciliationAutomation,
planLocalReconciliationSecretConfig,
@@ -28,6 +31,7 @@ const {
verifyLocalReconciliationCapture,
verifyLocalReconciliationApplication,
verifyLocalReconciliationAutomationDecision,
verifyLocalReconciliationSecretConfigDecision,
verifyLocalReconciliationAutomationApply,
verifyLocalReconciliationAutomationPlan,
verifyLocalReconciliationSecretConfigPlan,
@@ -1224,10 +1228,8 @@ function applicationCommitCommand(state, prepared) {
async function secretConfigPlanFixture(t, options = {}) {
const suffix = options.suffix ?? 'plan';
const state = await reviewedApplicationFixture(t, {
planId:
options.planId ?? '00000000-0000-4000-8000-000000000421',
reviewId:
options.reviewId ?? '00000000-0000-4000-8000-000000000422',
planId: options.planId ?? '00000000-0000-4000-8000-000000000421',
reviewId: options.reviewId ?? '00000000-0000-4000-8000-000000000422',
applicationId:
options.applicationId ?? '00000000-0000-4000-8000-000000000423',
reviewSuffix: `secret-config-${suffix}`,
@@ -1522,6 +1524,185 @@ function automationDecisionCommitFixture(
};
}
async function plannedSecretConfigDecisionFixture(t, options = {}) {
const suffix = options.suffix ?? 'decision';
const state = await secretConfigPlanFixture(t, {
suffix,
active: options.active === true,
configs: options.configs === true,
planId: options.planId,
reviewId: options.reviewId,
applicationId: options.applicationId,
secretConfigId: options.secretConfigId,
});
const planned = await planLocalReconciliationSecretConfig(
state.secretConfigCommand,
);
const secretConfigDecisionRoot = path.join(
path.dirname(state.captureRoot),
`secret-config-decision-${suffix}`,
);
fs.mkdirSync(secretConfigDecisionRoot, { mode: 0o700 });
const planPath = path.join(
state.secretConfigRoot,
state.secretConfigId,
'plan.ndjson',
);
const candidates = fs
.readFileSync(planPath, 'utf8')
.trimEnd()
.split('\n')
.map((line) => JSON.parse(line))
.filter(
(record) =>
record.kind ===
'qinglong3-local-reconciliation-secret-config-plan-candidate',
);
return {
...state,
planned,
secretConfigDecisionRoot,
candidates,
};
}
function secretConfigDecisionPrepareCommand(state, decisionId) {
return {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.prepare',
options: {
deploymentRoot: state.deploymentRoot,
applicationRoot: state.applicationRoot,
secretConfigRoot: state.secretConfigRoot,
secretConfigDecisionRoot: state.secretConfigDecisionRoot,
allowRootService: rootAcknowledgement(),
},
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedSecretConfigPlanDigest: state.planned.secretConfigPlanDigest,
expectedHeadDigest: state.planned.instanceHeadDigest,
preparedAtMs: state.secretConfigCommand.request.preparedAtMs + 1,
},
};
}
function secretConfigDecisionFile(
state,
prepared,
dispositions,
suffix = 'decision',
) {
assert.equal(state.candidates.length, dispositions.length);
const records = [
{
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-secret-config-decision-header',
decisionContractVersion: 1,
decisionId: prepared.result.decisionId,
profile: state.captureCommand.request.profile,
secretConfigPlanDigest: state.planned.secretConfigPlanDigest,
preparationDigest: prepared.result.preparationDigest,
},
...state.candidates.map((candidate, index) => ({
schemaVersion: 1,
kind: 'qinglong3-local-reconciliation-secret-config-decision',
candidateOrdinal: candidate.candidateOrdinal,
candidateDigest: candidate.candidateDigest,
disposition: dispositions[index].disposition,
reason: dispositions[index].reason,
})),
];
const filePath = path.join(
state.diagnosticRoot,
`secret-config-decision-${suffix}.ndjson`,
);
fs.writeFileSync(
filePath,
`${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
{ mode: 0o600 },
);
return { filePath, records };
}
function secretConfigDecisionCommitFixture(
state,
prepared,
decisionFilePath,
options = {},
) {
const committedAtMs = options.committedAtMs ?? Date.now();
const authorizationLifetimeMs = 10 * 60 * 1_000;
let authentications = 0;
let confirmations = 0;
let databaseCloses = 0;
const command = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.commit',
options: {
...prepared.commandOptions,
targetDatabasePath: state.targetDatabasePath,
ownerPepperKeyringDirectory:
state.command.options.ownerPepperKeyringDirectory,
credentialFilePath: state.command.options.credentialFilePath,
},
request: {
decisionId: prepared.result.decisionId,
secretConfigId: state.secretConfigId,
expectedPreparationDigest: prepared.result.preparationDigest,
expectedHeadDigest: prepared.result.instanceHeadDigest,
decisionFilePath,
committedAtMs,
authorizationLifetimeMs,
},
};
const dependencies = {
now: () => committedAtMs,
async openAuthenticationDatabase() {
return {
async close() {
databaseCloses += 1;
},
};
},
async authenticate(_database, authenticateOptions) {
authentications += 1;
assert.equal(
authenticateOptions.authenticationNamespace,
'local_reconciliation_secret_config',
);
return {
principal: {
subject: {
type: 'user',
id: options.reviewerId ?? 'review-owner',
},
authenticationId: 'local_reconciliation_secret_config:test',
authenticatedAtMs: committedAtMs,
expiresAtMs: committedAtMs + authorizationLifetimeMs + 60_000,
assurance: options.assurance ?? 'local_console',
},
databaseFence: {
credentialId: options.reviewerId ?? 'review-owner',
credentialVersion: 1,
pepperKeyId: 'review-owner-v1',
pepperVersion: 1,
},
async confirm() {
confirmations += 1;
},
};
},
};
return {
command,
dependencies,
authenticationCount: () => authentications,
confirmationCount: () => confirmations,
databaseCloseCount: () => databaseCloses,
};
}
async function appliedAutomationFixture(t, options = {}) {
const state = await plannedAutomationFixture(t, {
suffix: options.suffix ?? 'completion-applied',
@@ -3353,7 +3534,10 @@ test('Secret/Config plan publishes, seals, verifies and stays content-free', asy
assert.equal(fs.statSync(root).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'staging')).mode & 0o777, 0o500);
assert.equal(fs.statSync(path.join(root, 'plan.ndjson')).mode & 0o777, 0o400);
assert.equal(fs.statSync(path.join(root, 'receipt.json')).mode & 0o777, 0o400);
assert.equal(
fs.statSync(path.join(root, 'receipt.json')).mode & 0o777,
0o400,
);
const serialized = fs.readFileSync(path.join(root, 'plan.ndjson'), 'utf8');
for (const privateValue of [
'DISABLED_TOKEN',
@@ -3483,17 +3667,384 @@ test('Secret/Config plan keeps active Env and unknown Configs manual', async (t)
assert.equal(planned.outcome, 'manual_required');
assert.equal(planned.unadaptedLegacyConfigCount, 1);
const serialized = fs.readFileSync(
path.join(
state.secretConfigRoot,
state.secretConfigId,
'plan.ndjson',
),
path.join(state.secretConfigRoot, state.secretConfigId, 'plan.ndjson'),
'utf8',
);
assert.equal(serialized.includes('private-config-value'), false);
});
});
test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {
const state = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-terminal',
planId: '00000000-0000-4000-8000-000000000425',
reviewId: '00000000-0000-4000-8000-000000000426',
applicationId: '00000000-0000-4000-8000-000000000427',
secretConfigId: '00000000-0000-4000-8000-000000000428',
});
assert.equal(state.planned.outcome, 'ready');
assert.equal(state.candidates.length, 1);
assert.equal(state.candidates[0].requirement, 'review_preserve_disabled');
const decisionId = '019b0000-0000-7000-8000-000000000425';
const prepareCommand = secretConfigDecisionPrepareCommand(state, decisionId);
const prepared = await prepareLocalReconciliationSecretConfigDecision(
prepareCommand,
);
assert.equal(prepared.status, 'prepared');
assert.equal(
prepared.state,
'reconciliation_secret_config_decision_prepared',
);
const decisionFile = secretConfigDecisionFile(
state,
{ result: prepared },
[
{
disposition: 'preserve_disabled',
reason: 'reviewed_disabled_preservation',
},
],
'terminal',
);
const commit = secretConfigDecisionCommitFixture(
state,
{ result: prepared, commandOptions: prepareCommand.options },
decisionFile.filePath,
);
const targetBytes = fs.readFileSync(state.targetDatabasePath);
const committed = await commitLocalReconciliationSecretConfigDecision(
commit.command,
commit.dependencies,
);
assert.equal(committed.status, 'prepared');
assert.equal(committed.state, 'reconciliation_secret_config_reviewed');
assert.equal(committed.outcome, 'ready');
assert.equal(committed.candidateCount, 1);
assert.equal(committed.applyBindingCount, 0);
assert.equal(committed.preserveDisabledCount, 1);
assert.equal(committed.skippedCount, 0);
assert.equal(commit.authenticationCount(), 1);
assert.equal(commit.confirmationCount(), 3);
assert.equal(commit.databaseCloseCount(), 1);
assert.equal(
fs.readFileSync(state.targetDatabasePath).equals(targetBytes),
true,
);
const decisionRoot = path.join(
state.secretConfigDecisionRoot,
state.secretConfigId,
);
assert.deepEqual(fs.readdirSync(decisionRoot).sort(), [
'authorization.ndjson',
'intent.json',
'receipt.json',
'staging',
]);
assert.equal(fs.statSync(decisionRoot).mode & 0o777, 0o500);
assert.equal(
fs.statSync(path.join(decisionRoot, 'staging')).mode & 0o777,
0o500,
);
for (const name of ['authorization.ndjson', 'intent.json', 'receipt.json']) {
assert.equal(
fs.statSync(path.join(decisionRoot, name)).mode & 0o777,
0o400,
);
}
const verifyCommand = {
schemaVersion: 1,
operation: 'local.deployment.reconciliation.secret-config.decision.verify',
options: prepareCommand.options,
request: {
decisionId,
secretConfigId: state.secretConfigId,
expectedDecisionDigest: committed.decisionDigest,
},
};
const verified = await verifyLocalReconciliationSecretConfigDecision(
verifyCommand,
);
assert.equal(verified.status, 'verified');
assert.equal(
verified.signedDecisionSetDigest,
committed.signedDecisionSetDigest,
);
const terminal = await readLocalReconciliationSecretConfigDecisionTerminal(
prepareCommand.options,
state.secretConfigId,
process.getuid(),
);
assert.equal(terminal.receipt.decisionDigest, committed.decisionDigest);
assert.equal(terminal.reviewer.subject.id, 'review-owner');
const serialized = JSON.stringify(verified);
for (const privateValue of [
'DISABLED_TOKEN',
'private-secret-value',
'review-owner',
decisionFile.filePath,
state.candidates[0].candidateDigest,
]) {
assert.equal(serialized.includes(privateValue), false);
}
const commandPath = path.join(
state.deploymentRoot,
'secret-config-decision-verify.json',
);
fs.writeFileSync(commandPath, `${JSON.stringify(verifyCommand)}\n`, {
mode: 0o600,
});
const cli = spawnSync(
process.execPath,
[
path.join(__dirname, '../dist/deployment/localDeploymentCli.js'),
'reconciliation-secret-config-decision-verify',
'--command-file',
commandPath,
],
{ encoding: 'utf8' },
);
assert.equal(cli.status, 0, cli.stderr);
assert.equal(JSON.parse(cli.stdout).status, 'verified');
assert.equal(cli.stdout.includes('review-owner'), false);
assert.equal(cli.stdout.includes('DISABLED_TOKEN'), false);
const head = readLocalCutoverInstanceHead(
state.deploymentRoot,
state.captureCommand.request.instanceId,
state.uid,
);
assert.equal(head.state, 'reconciliation_secret_config_reviewed');
assert.equal(head.sourceRecordDigest, committed.decisionDigest);
const authorizationPath = path.join(decisionRoot, 'authorization.ndjson');
fs.chmodSync(decisionRoot, 0o700);
fs.chmodSync(authorizationPath, 0o600);
fs.appendFileSync(authorizationPath, '{}\n');
fs.chmodSync(authorizationPath, 0o400);
fs.chmodSync(decisionRoot, 0o500);
await assert.rejects(
verifyLocalReconciliationSecretConfigDecision(verifyCommand),
/authorization|file identity or size|file is incomplete/,
);
});
test('Secret/Config decision rejects manual plans, invalid candidate choices and reviewer drift', async (t) => {
const manual = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-manual-plan',
active: true,
planId: '00000000-0000-4000-8000-000000000429',
reviewId: '00000000-0000-4000-8000-00000000042a',
applicationId: '00000000-0000-4000-8000-00000000042b',
secretConfigId: '00000000-0000-4000-8000-00000000042c',
});
assert.equal(manual.planned.outcome, 'manual_required');
await assert.rejects(
prepareLocalReconciliationSecretConfigDecision(
secretConfigDecisionPrepareCommand(
manual,
'019b0000-0000-7000-8000-000000000429',
),
),
/only a ready non-empty plan can be reviewed/,
);
const state = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-reject',
planId: '00000000-0000-4000-8000-00000000042d',
reviewId: '00000000-0000-4000-8000-00000000042e',
applicationId: '00000000-0000-4000-8000-00000000042f',
secretConfigId: '00000000-0000-4000-8000-000000000430',
});
const prepareCommand = secretConfigDecisionPrepareCommand(
state,
'019b0000-0000-7000-8000-00000000042d',
);
const prepared = await prepareLocalReconciliationSecretConfigDecision(
prepareCommand,
);
const invalid = secretConfigDecisionFile(
state,
{ result: prepared },
[
{
disposition: 'apply_active_binding',
reason: 'reviewed_active_binding',
},
],
'invalid-choice',
);
const invalidCommit = secretConfigDecisionCommitFixture(
state,
{ result: prepared, commandOptions: prepareCommand.options },
invalid.filePath,
);
await assert.rejects(
commitLocalReconciliationSecretConfigDecision(
invalidCommit.command,
invalidCommit.dependencies,
),
/decision is not allowed for canonical candidate/,
);
assert.equal(invalidCommit.authenticationCount(), 0);
const valid = secretConfigDecisionFile(
state,
{ result: prepared },
[
{
disposition: 'preserve_disabled',
reason: 'reviewed_disabled_preservation',
},
],
'reviewer-reject',
);
for (const auth of [
{ reviewerId: 'another-owner', assurance: 'local_console' },
{ reviewerId: 'review-owner', assurance: 'password' },
]) {
const rejected = secretConfigDecisionCommitFixture(
state,
{ result: prepared, commandOptions: prepareCommand.options },
valid.filePath,
auth,
);
await assert.rejects(
commitLocalReconciliationSecretConfigDecision(
rejected.command,
rejected.dependencies,
),
/requires the same recently strong authenticated User/,
);
assert.equal(rejected.authenticationCount(), 1);
assert.equal(rejected.confirmationCount(), 0);
assert.equal(rejected.databaseCloseCount(), 1);
}
});
test('Secret/Config decision replays every publication boundary without repeated authentication', async (t) => {
const prepareState = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-prepare-loss',
secretConfigId: '00000000-0000-4000-8000-000000000431',
});
const prepareCommand = secretConfigDecisionPrepareCommand(
prepareState,
'019b0000-0000-7000-8000-000000000431',
);
await assert.rejects(
prepareLocalReconciliationSecretConfigDecision(prepareCommand, {
afterHeadPrepared() {
throw new Error('secret config decision prepare response loss');
},
}),
/secret config decision prepare response loss/,
);
const prepareReplay = await prepareLocalReconciliationSecretConfigDecision(
prepareCommand,
);
assert.equal(
prepareReplay.state,
'reconciliation_secret_config_decision_prepared',
);
for (const [window, tail] of [
['authorization', '432'],
['receipt', '433'],
['seal', '434'],
['head', '435'],
]) {
await t.test(window, async (subtest) => {
const state = await plannedSecretConfigDecisionFixture(subtest, {
suffix: `decision-${window}-loss`,
secretConfigId: `00000000-0000-4000-8000-000000000${tail}`,
});
const decisionId = `019b0000-0000-7000-8000-000000000${tail}`;
const selectedPrepareCommand = secretConfigDecisionPrepareCommand(
state,
decisionId,
);
const prepared = await prepareLocalReconciliationSecretConfigDecision(
selectedPrepareCommand,
);
const review = secretConfigDecisionFile(
state,
{ result: prepared },
[
{
disposition: 'preserve_disabled',
reason: 'reviewed_disabled_preservation',
},
],
`${window}-loss`,
);
const commit = secretConfigDecisionCommitFixture(
state,
{
result: prepared,
commandOptions: selectedPrepareCommand.options,
},
review.filePath,
);
const callback =
window === 'authorization'
? 'afterAuthorizationPublished'
: window === 'receipt'
? 'afterReceiptPublished'
: window === 'seal'
? 'afterTerminalSealed'
: 'afterHeadAdvanced';
await assert.rejects(
commitLocalReconciliationSecretConfigDecision(commit.command, {
...commit.dependencies,
[callback]() {
throw new Error(`secret config decision ${window} response loss`);
},
}),
new RegExp(`secret config decision ${window} response loss`),
);
const replay = await commitLocalReconciliationSecretConfigDecision(
commit.command,
commit.dependencies,
);
assert.equal(replay.state, 'reconciliation_secret_config_reviewed');
assert.equal(commit.authenticationCount(), 1);
assert.equal(commit.confirmationCount(), 3);
assert.equal(commit.databaseCloseCount(), 1);
if (window === 'head') assert.equal(replay.status, 'existing');
});
}
});
test('Secret/Config decision can explicitly skip a ready candidate only into manual_required', async (t) => {
const state = await plannedSecretConfigDecisionFixture(t, {
suffix: 'decision-skip',
secretConfigId: '00000000-0000-4000-8000-000000000436',
});
const prepareCommand = secretConfigDecisionPrepareCommand(
state,
'019b0000-0000-7000-8000-000000000436',
);
const prepared = await prepareLocalReconciliationSecretConfigDecision(
prepareCommand,
);
const review = secretConfigDecisionFile(
state,
{ result: prepared },
[{ disposition: 'skip', reason: 'operator_excluded' }],
'skip',
);
const commit = secretConfigDecisionCommitFixture(
state,
{ result: prepared, commandOptions: prepareCommand.options },
review.filePath,
);
const result = await commitLocalReconciliationSecretConfigDecision(
commit.command,
commit.dependencies,
);
assert.equal(result.outcome, 'manual_required');
assert.equal(result.skippedCount, 1);
assert.equal(result.preserveDisabledCount, 0);
});
test('completion fence retains automation rollback backup while other domains remain manual', async (t) => {
const state = await appliedAutomationFixture(t, {
suffix: 'completion-fence',
+19
View File
@@ -2037,6 +2037,14 @@ function auditSourceImports(root, packagePath, findings) {
'@qinglong/local-sqlite/rollout-safety',
].includes(specifier)
) &&
!(
path.relative(packageDirectory, filePath) ===
'src/deployment/reconciliation/application/secret-and-config/decisionCoordinator.ts' &&
[
'@qinglong/local-owner-console/authenticated-command',
'@qinglong/local-sqlite/authentication-read',
].includes(specifier)
) &&
!(
path.relative(packageDirectory, filePath) ===
'src/deployment/reconciliation/application/automation/applyEvidence.ts' &&
@@ -2725,6 +2733,17 @@ function auditSourceImports(root, packagePath, findings) {
file: 'src/deployment/reconciliation/application/automation/applyCoordinator.ts',
specifiers: ['@qinglong/runtime-core/security'],
},
{
file: 'src/deployment/reconciliation/application/secret-and-config/decisionAuthorization.ts',
specifiers: [
'@qinglong/runtime-core/local-secret',
'@qinglong/runtime-core/security',
],
},
{
file: 'src/deployment/reconciliation/application/secret-and-config/decisionCoordinator.ts',
specifiers: ['@qinglong/runtime-core/security'],
},
].some(
({ file, specifiers }) =>
path.relative(packageDirectory, filePath) === file &&
@@ -2280,6 +2280,73 @@ test('confines reconciliation Secret and Config inspection to its exact row plan
);
});
test('confines reconciliation Secret and Config decision authority to exact owners', (t) => {
const root = fs.mkdtempSync(
path.join(
os.tmpdir(),
'ql3-reconciliation-secret-config-decision-boundary-',
),
);
const secretConfigDirectory = path.join(
root,
'packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config',
);
fs.mkdirSync(secretConfigDirectory, { recursive: true });
fs.writeFileSync(
path.join(secretConfigDirectory, 'decisionAuthorization.ts'),
[
"import type { Key } from '@qinglong/runtime-core/local-secret';",
"import type { Principal } from '@qinglong/runtime-core/security';",
].join('\n'),
);
fs.writeFileSync(
path.join(secretConfigDirectory, 'decisionCoordinator.ts'),
[
"import { authenticate } from '@qinglong/local-owner-console/authenticated-command';",
"import { database } from '@qinglong/local-sqlite/authentication-read';",
"import type { Principal } from '@qinglong/runtime-core/security';",
].join('\n'),
);
fs.writeFileSync(
path.join(secretConfigDirectory, 'neighbor.ts'),
[
"import { authenticate } from '@qinglong/local-owner-console/authenticated-command';",
"import { database } from '@qinglong/local-sqlite/authentication-read';",
"import type { Key } from '@qinglong/runtime-core/local-secret';",
"import type { Principal } from '@qinglong/runtime-core/security';",
].join('\n'),
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const findings = [];
auditSourceImports(root, 'packages/ql3-local-owner-cli', findings);
assert.deepEqual(
findings.map(({ code, file, specifier }) => ({ code, file, specifier })),
[
{
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config/neighbor.ts',
specifier: '@qinglong/local-owner-console/authenticated-command',
},
{
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config/neighbor.ts',
specifier: '@qinglong/local-sqlite/authentication-read',
},
{
code: 'FORBIDDEN_PACKAGE_SOURCE_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config/neighbor.ts',
specifier: '@qinglong/runtime-core/local-secret',
},
{
code: 'FORBIDDEN_PACKAGE_SOURCE_IMPORT',
file: 'packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config/neighbor.ts',
specifier: '@qinglong/runtime-core/security',
},
],
);
});
test('deleted Owner ceremony package names remain dependency tombstones', (t) => {
const root = fixture(
t,
+2 -2
View File
@@ -207,10 +207,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
rootSourceFileRoles: localOwnerCli.rootSourceFileRoles,
},
{
sourceFiles: 178,
sourceFiles: 184,
rootSourceFiles: 1,
rootSourceLines: 50,
nestedSourceFiles: 177,
nestedSourceFiles: 183,
rootSourceFileRoles: { 'cli.ts': 'binary_entry' },
},
);