mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(local): publish secret config application atomically
This commit is contained in:
@@ -391,7 +391,7 @@ jobs:
|
|||||||
- name: Verify non-root identity and architecture
|
- name: Verify non-root identity and architecture
|
||||||
env:
|
env:
|
||||||
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-local-application:ci-${{ matrix.image_arch }}
|
||||||
EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 51 51 1
|
EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
actual="$(docker image inspect --format '{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}' "${IMAGE}")"
|
actual="$(docker image inspect --format '{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}' "${IMAGE}")"
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ LABEL org.opencontainers.image.title="QingLong 3.0 Local Application" \
|
|||||||
io.qinglong.ai="excluded" \
|
io.qinglong.ai="excluded" \
|
||||||
io.qinglong.local.application-config="2,3,4" \
|
io.qinglong.local.application-config="2,3,4" \
|
||||||
io.qinglong.local.sqlite-contract-min="51" \
|
io.qinglong.local.sqlite-contract-min="51" \
|
||||||
io.qinglong.local.sqlite-contract-max="51" \
|
io.qinglong.local.sqlite-contract-max="52" \
|
||||||
io.qinglong.local.sqlite-write-contract="51" \
|
io.qinglong.local.sqlite-write-contract="52" \
|
||||||
io.qinglong.local.compose-selection="1"
|
io.qinglong.local.compose-selection="1"
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
- 目标版本:QingLong 3.x
|
- 目标版本:QingLong 3.x
|
||||||
- 作者:QingLong Maintainers
|
- 作者:QingLong Maintainers
|
||||||
- 创建日期:2026-07-17
|
- 创建日期:2026-07-17
|
||||||
- 最后更新:2026-08-23
|
- 最后更新:2026-08-24
|
||||||
- 讨论范围:架构与演进路线,不包含最终 UI 视觉方案
|
- 讨论范围:架构与演进路线,不包含最终 UI 视觉方案
|
||||||
|
|
||||||
最新增量证据(2026-08-23):
|
最新增量证据(2026-08-24):
|
||||||
|
|
||||||
- D-397/ADR-0491(进行中):Secret/Config reconciliation 已先冻结行为保持边界,不能把“密文已保存”冒充“任务已迁移”。2.x `Envs`
|
- D-397/ADR-0491(进行中):Secret/Config reconciliation 已先冻结行为保持边界,不能把“密文已保存”冒充“任务已迁移”。2.x `Envs`
|
||||||
的 active 行必须按 `isPinned DESC、position DESC、createdAt ASC、id ASC` 重放旧顺序,同名值用 `&` 形成唯一 effective Secret;该
|
的 active 行必须按 `isPinned DESC、position DESC、createdAt ASC、id ASC` 重放旧顺序,同名值用 `&` 形成唯一 effective Secret;该
|
||||||
@@ -37,18 +37,24 @@
|
|||||||
digest。Secret/Config planner 不再扫描 `legacy-cron:*` 或相信聚合计数,而是流式复算 provenance,并验证当前 Task/Trigger head、Trigger schedule 与非
|
digest。Secret/Config planner 不再扫描 `legacy-cron:*` 或相信聚合计数,而是流式复算 provenance,并验证当前 Task/Trigger head、Trigger schedule 与非
|
||||||
Plugin ownership。旧记录缺少逐项 provenance 为 `missing`,current revision/schedule/ownership 漂移为 `drifted`,两者都强制
|
Plugin ownership。旧记录缺少逐项 provenance 为 `missing`,current revision/schedule/ownership 漂移为 `drifted`,两者都强制
|
||||||
`manual_required`;不得猜测或自动回填。聚合 adoption 记录仍只保留 Edge/Standalone 128/512 条有界 Map,Task/Trigger 逐行读取,无目标规模 Set。
|
`manual_required`;不得猜测或自动回填。聚合 adoption 记录仍只保留 Edge/Standalone 128/512 条有界 Map,Task/Trigger 逐行读取,无目标规模 Set。
|
||||||
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。Local SQLite、Local Admin 与 Local Owner 完整测试分别为 `241/241`、
|
第六切片把原子 DML 下沉到既有 `@qinglong/local-sqlite/secret-config-application` 私有短生命周期 authority,不新增 package 或 Cluster 依赖。contract v52
|
||||||
`96/96` 与 `296 total / 289 pass / 7 conditional skip / 0 fail`;18-package clean build/test 在非沙箱环境全部通过。后端完整门在受限沙箱中为
|
增加 application/secret/task/trigger 四类 ledger;publisher 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、两次外部 authority、逐项 provenance、当前
|
||||||
`1544 total / 1531 pass / 11 loopback EPERM / 2 conditional skip`,四个受影响文件随后在非沙箱环境 `38/38` 通过,未发现代码失败。package
|
Task/Trigger head、Plugin ownership 与每 Task Trigger 数量,然后同时写入加密 Secret、content-free audit、Task rev2、local dispatch、Trigger rev2、schedule
|
||||||
boundary、72 项 Cluster dependency/legacy boundary、Edge import、本地镜像与十四档 Local artifact audit 全部 compatible;基础 Edge/Standalone 为
|
和 receipt。item 使用 deferred parent FK 流式落库,最终 receipt 才关闭父引用,因此最多 100,000 Task/500,000 Trigger 不进入 JS 全集或 O(N×M) 扫描。
|
||||||
`2,620,531 / 2,620,609 bytes`、321 files、58 loaded modules,Owner-only authority 没有进入低资源常驻制品。fresh Edge readiness 为 contract v51、
|
commit response-loss exact replay 会重验 Secret envelope、Task/Trigger durable head 与 schedule;Secret 占用、provenance 缺项和提交前 authority 漂移均回滚全部 DML。
|
||||||
102 migrations、85 tables、SQLite 3.53.3、`DELETE` journal;PostgreSQL 18.6 arm64 physical HA 以 timeline `1 → 2`、146 gates 通过。
|
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`,publisher 定向回归 `6/6`;fresh Edge
|
||||||
Local Admin 保持 48/47,Local Owner 因六个职责明确的 decision 嵌套文件增至 184/183,根目录仍只有一个 50 行 binary entry;workspace 仍为 18 packages、
|
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Owner 为
|
||||||
`singleSourcePackages=[]`、`shallowSourcePackages=[]`,且只允许 exact Secret/Config row planner 导入 inspection subpath。
|
`296 total / 289 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为
|
||||||
|
`1566 total / 1564 pass / 2 conditional skip / 0 fail`。package boundary、Cluster dependency/legacy boundary、122-module Edge import、本地镜像与
|
||||||
|
`14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB
|
||||||
|
上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。
|
||||||
|
Local SQLite 为 209 source / 208 nested / 1 root public export;Local Owner 保持 184/183,根目录仍只有一个 50 行 binary entry;workspace 仍为
|
||||||
|
18 packages、`singleSourcePackages=[]`、`shallowSourcePackages=[]`。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,
|
||||||
|
因而不重跑且不重新占有 PostgreSQL HA 证明;相邻已通过的 remote CI/HA 只作为基线。
|
||||||
|
|
||||||
D-385~D-388 的 `config.sh`/Keyv/SSH data-directory lineage 与 SQLite `Envs` 保持分离;当前无稳定生产 schema 的历史 `Configs` 表继续 sealed+manual,
|
D-385~D-388 的 `config.sh`/Keyv/SSH data-directory lineage 与 SQLite `Envs` 保持分离;当前无稳定生产 schema 的历史 `Configs` 表继续 sealed+manual,
|
||||||
不猜字段。后续切片必须完成 Secret envelope + audit + Task/Trigger/dispatch + receipt ledger 的单事务发布、prepared/apply/rollback
|
不猜字段。后续切片必须完成 Owner prepared/apply/rollback、写前 backup、receipt/head/seal response-loss、completion 下一 schema 与备份回收。D-397 apply
|
||||||
lineage、completion 下一 schema 与备份回收。D-397 apply 只声明 sealed source retained 且 `physicalErasureGuaranteed=false`;明文销毁必须在 restart/
|
只声明 sealed source retained 且 `physicalErasureGuaranteed=false`;明文销毁必须在 restart/
|
||||||
readiness、观察窗和 rollback retention 之后另行强认证。Cluster 必须使用 PostgreSQL SERIALIZABLE ledger、外部 KMS/Secret provider 与 HA evidence,
|
readiness、观察窗和 rollback retention 之后另行强认证。Cluster 必须使用 PostgreSQL SERIALIZABLE ledger、外部 KMS/Secret provider 与 HA evidence,
|
||||||
不复用 Local SQLite/POSIX authority,也不得把明文写入 PostgreSQL、ConfigMap、Pod env 或 Job command。
|
不复用 Local SQLite/POSIX authority,也不得把明文写入 PostgreSQL、ConfigMap、Pod env 或 Job command。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ADR-0491:有界 Secret/Config Reconciliation 与任务环境绑定
|
# ADR-0491:有界 Secret/Config Reconciliation 与任务环境绑定
|
||||||
|
|
||||||
- 状态:Proposed(D-397 已实现 Legacy Env inspection、私有有界 row plan、durable plan publication、独立 signed decision 与逐项 Automation adoption provenance;原子 application 尚未完成)
|
- 状态:Proposed(D-397 已实现 Legacy Env inspection、私有有界 row plan、durable plan publication、独立 signed decision、逐项 Automation adoption provenance 与 Local SQLite 原子 application publisher;Owner prepared/apply/rollback 编排尚未完成)
|
||||||
- 日期:2026-08-23
|
- 日期:2026-08-23
|
||||||
- 决策:D-397
|
- 决策: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
|
- 关联: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
|
||||||
@@ -137,8 +137,8 @@ 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、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify,以及 v51 逐 Task/Trigger adoption provenance。新 provenance 与 Automation Task/Trigger/audit/ledger 在同一事务发布并在 replay 时完整流式复算;Secret/Config plan 对 complete、missing、current-revision drift 和 Plugin ownership 失败关闭。
|
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、v51 逐 Task/Trigger adoption provenance,以及 v52 Local SQLite 原子 application publisher。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledger;deferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DML。
|
||||||
|
|
||||||
本切片验证已闭合:Local SQLite `241/241`、Local Admin `96/96`、Local Owner `296 total / 289 pass / 7 conditional skip / 0 fail`,Secret/Config 定向回归 `51/51`;18-package clean build/test 在非沙箱环境全部通过。后端完整门在受限沙箱中仅有 11 个 loopback `listen EPERM`,四个受影响文件在非沙箱环境 `38/38` 通过。package boundary 保持 18 packages、`singleSourcePackages=[]`、`shallowSourcePackages=[]`,72 项 Cluster dependency/legacy boundary、Edge import、本地镜像与十四档 artifact audit 全 compatible;基础 Edge/Standalone 为 `2,620,531 / 2,620,609 bytes`、321 files、58 modules。fresh Edge readiness 为 contract v51、102 migrations、85 tables、SQLite 3.53.3、`DELETE` journal。PostgreSQL 18.6 arm64 physical HA 以 146 gates、timeline `1 → 2` 通过;两次前置 `initdb` 超时均由本机 Docker 虚拟磁盘 100% 占满导致,释放可重建缓存后同一门成功。
|
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`;fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Owner 为 `296 total / 289 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为 `1566 total / 1564 pass / 2 conditional skip / 0 fail`。package boundary、Cluster dependency/legacy boundary、122-module Edge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
|
||||||
|
|
||||||
转为 Accepted 前仍必须完成:原子 Secret/Task/Trigger/dispatch publisher、prepared/apply/rollback response-loss、completion schema 演进、在后续原子 application 切片上重新执行 18-package/boundary/artifact gates、真实 Edge 空间预算与 Cluster Secret provider live gate;本切片的 PostgreSQL HA 证据不得冒充后续 Cluster 密钥托管证明。
|
转为 Accepted 前仍必须完成:Owner prepared/apply/rollback 与写前 backup、application receipt/head/seal response-loss、completion schema 演进、真实 Edge 空间预算与 Cluster Secret provider live gate;本切片的 Local SQLite publisher 或 PostgreSQL HA 证据不得冒充 Owner 编排、外部密钥托管或跨领域 completion 已完成。
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ordered_ledger",
|
"kind": "ordered_ledger",
|
||||||
"maxDirectSourceFiles": 103,
|
"maxDirectSourceFiles": 105,
|
||||||
"path": "packages/ql3-local-sqlite/src/migrations",
|
"path": "packages/ql3-local-sqlite/src/migrations",
|
||||||
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
|
"rationale": "SQLite migrations are an append-only version ledger whose ordering and discoverability are safer in one reviewed directory."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,8 +367,8 @@ function adoptedDockerHarness(state, options = {}) {
|
|||||||
],
|
],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.qinglong.local.sqlite-contract-min': '51',
|
'io.qinglong.local.sqlite-contract-min': '51',
|
||||||
'io.qinglong.local.sqlite-contract-max': '51',
|
'io.qinglong.local.sqlite-contract-max': '52',
|
||||||
'io.qinglong.local.sqlite-write-contract': '51',
|
'io.qinglong.local.sqlite-write-contract': '52',
|
||||||
'io.qinglong.local.application-config': '2,3,4',
|
'io.qinglong.local.application-config': '2,3,4',
|
||||||
'io.qinglong.local.compose-selection': '1',
|
'io.qinglong.local.compose-selection': '1',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
@@ -839,7 +839,7 @@ test('preflights adopted Compose identity mounts and rejects mount drift', async
|
|||||||
);
|
);
|
||||||
assert.equal(ready.status, 'ready');
|
assert.equal(ready.status, 'ready');
|
||||||
assert.equal(ready.profile, 'edge');
|
assert.equal(ready.profile, 'edge');
|
||||||
assert.equal(ready.sqlite.contractVersion, 51);
|
assert.equal(ready.sqlite.contractVersion, 52);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
preflightLocalDeploymentCompose(composePreflightCommand(state, 1), {
|
preflightLocalDeploymentCompose(composePreflightCommand(state, 1), {
|
||||||
runDocker: adoptedDockerHarness(state, { driftMount: true }).runDocker,
|
runDocker: adoptedDockerHarness(state, { driftMount: true }).runDocker,
|
||||||
|
|||||||
@@ -491,8 +491,8 @@ function composeDockerHarness(
|
|||||||
],
|
],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.qinglong.local.sqlite-contract-min': '51',
|
'io.qinglong.local.sqlite-contract-min': '51',
|
||||||
'io.qinglong.local.sqlite-contract-max': '51',
|
'io.qinglong.local.sqlite-contract-max': '52',
|
||||||
'io.qinglong.local.sqlite-write-contract': '51',
|
'io.qinglong.local.sqlite-write-contract': '52',
|
||||||
'io.qinglong.local.application-config': '2',
|
'io.qinglong.local.application-config': '2',
|
||||||
'io.qinglong.local.compose-selection': '1',
|
'io.qinglong.local.compose-selection': '1',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
@@ -1200,8 +1200,8 @@ test('preflights exact local image, Compose merge and SQLite capability', async
|
|||||||
],
|
],
|
||||||
Labels: {
|
Labels: {
|
||||||
'io.qinglong.local.sqlite-contract-min': '51',
|
'io.qinglong.local.sqlite-contract-min': '51',
|
||||||
'io.qinglong.local.sqlite-contract-max': '51',
|
'io.qinglong.local.sqlite-contract-max': '52',
|
||||||
'io.qinglong.local.sqlite-write-contract': '51',
|
'io.qinglong.local.sqlite-write-contract': '52',
|
||||||
'io.qinglong.local.application-config': '2',
|
'io.qinglong.local.application-config': '2',
|
||||||
'io.qinglong.local.compose-selection': '1',
|
'io.qinglong.local.compose-selection': '1',
|
||||||
'io.qinglong.ai': 'excluded',
|
'io.qinglong.ai': 'excluded',
|
||||||
@@ -1259,7 +1259,7 @@ test('preflights exact local image, Compose merge and SQLite capability', async
|
|||||||
assert.equal(result.status, 'ready');
|
assert.equal(result.status, 'ready');
|
||||||
assert.equal(result.generation, 1);
|
assert.equal(result.generation, 1);
|
||||||
assert.equal(result.profile, 'edge');
|
assert.equal(result.profile, 'edge');
|
||||||
assert.equal(result.sqlite.contractVersion, 51);
|
assert.equal(result.sqlite.contractVersion, 52);
|
||||||
assert.equal(result.image.architecture, 'arm64');
|
assert.equal(result.image.architecture, 'arm64');
|
||||||
assert.equal(calls.length, 2);
|
assert.equal(calls.length, 2);
|
||||||
assert.deepEqual(calls[0].slice(0, 2), ['image', 'inspect']);
|
assert.deepEqual(calls[0].slice(0, 2), ['image', 'inspect']);
|
||||||
@@ -1359,8 +1359,8 @@ test('applies one Compose generation and exactly replays its health receipt', as
|
|||||||
assert.equal(mode(receiptPath), 0o600);
|
assert.equal(mode(receiptPath), 0o600);
|
||||||
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
||||||
assert.deepEqual(receipt.sqlite, {
|
assert.deepEqual(receipt.sqlite, {
|
||||||
contractVersion: 51,
|
contractVersion: 52,
|
||||||
writeContractVersion: 51,
|
writeContractVersion: 52,
|
||||||
writeObservation: 'unchanged',
|
writeObservation: 'unchanged',
|
||||||
backup: null,
|
backup: null,
|
||||||
});
|
});
|
||||||
@@ -1661,8 +1661,8 @@ test('rolls a failed Compose candidate forward to a healthy prior digest', async
|
|||||||
`${command.request.rolloutId}.sqlite`,
|
`${command.request.rolloutId}.sqlite`,
|
||||||
);
|
);
|
||||||
assert.equal(mode(backupPath), 0o600);
|
assert.equal(mode(backupPath), 0o600);
|
||||||
assert.equal(receipt.sqlite.contractVersion, 51);
|
assert.equal(receipt.sqlite.contractVersion, 52);
|
||||||
assert.equal(receipt.sqlite.writeContractVersion, 51);
|
assert.equal(receipt.sqlite.writeContractVersion, 52);
|
||||||
assert.equal(receipt.sqlite.writeObservation, 'changed');
|
assert.equal(receipt.sqlite.writeObservation, 'changed');
|
||||||
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
|
assert.match(receipt.sqlite.backup.sha256, /^[0-9a-f]{64}$/);
|
||||||
assert.equal(receipt.sqlite.backup.bytes > 0, true);
|
assert.equal(receipt.sqlite.backup.bytes > 0, true);
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ test('inspects the exact fresh Profile schema without exposing its path', async
|
|||||||
assert.equal(result.status, 'ready');
|
assert.equal(result.status, 'ready');
|
||||||
assert.equal(result.profile, 'edge');
|
assert.equal(result.profile, 'edge');
|
||||||
assert.equal(result.storage.contractName, 'local-control-core');
|
assert.equal(result.storage.contractName, 'local-control-core');
|
||||||
assert.equal(result.storage.contractVersion, 51);
|
assert.equal(result.storage.contractVersion, 52);
|
||||||
assert.equal(result.storage.migrationCount, 102);
|
assert.equal(result.storage.migrationCount, 104);
|
||||||
assert.equal(result.storage.journalMode, 'delete');
|
assert.equal(result.storage.journalMode, 'delete');
|
||||||
assert.equal(JSON.stringify(result).includes(state.directory), false);
|
assert.equal(JSON.stringify(result).includes(state.directory), false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,6 +90,11 @@
|
|||||||
"require": "./dist/adoption/data-directory/applicationCommit.js",
|
"require": "./dist/adoption/data-directory/applicationCommit.js",
|
||||||
"default": "./dist/adoption/data-directory/applicationCommit.js"
|
"default": "./dist/adoption/data-directory/applicationCommit.js"
|
||||||
},
|
},
|
||||||
|
"./secret-config-application": {
|
||||||
|
"types": "./dist/adoption/secret-config/secretConfigApplicationDatabase.d.ts",
|
||||||
|
"require": "./dist/adoption/secret-config/secretConfigApplicationDatabase.js",
|
||||||
|
"default": "./dist/adoption/secret-config/secretConfigApplicationDatabase.js"
|
||||||
|
},
|
||||||
"./plugin-package-install": {
|
"./plugin-package-install": {
|
||||||
"types": "./dist/plugin-package/pluginPackageInstallRepository.d.ts",
|
"types": "./dist/plugin-package/pluginPackageInstallRepository.d.ts",
|
||||||
"require": "./dist/plugin-package/pluginPackageInstallRepository.js",
|
"require": "./dist/plugin-package/pluginPackageInstallRepository.js",
|
||||||
|
|||||||
+1048
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,8 @@ import { local0099LegacyDataDirectoryAdoptionsMigration } from '../migrations/00
|
|||||||
import { local0100CapabilityV50Migration } from '../migrations/0100-capability-v50';
|
import { local0100CapabilityV50Migration } from '../migrations/0100-capability-v50';
|
||||||
import { local0101LegacyAdoptionProvenanceMigration } from '../migrations/0101-legacy-adoption-provenance';
|
import { local0101LegacyAdoptionProvenanceMigration } from '../migrations/0101-legacy-adoption-provenance';
|
||||||
import { local0102CapabilityV51Migration } from '../migrations/0102-capability-v51';
|
import { local0102CapabilityV51Migration } from '../migrations/0102-capability-v51';
|
||||||
|
import { local0103SecretConfigApplicationsMigration } from '../migrations/0103-secret-config-applications';
|
||||||
|
import { local0104CapabilityV52Migration } from '../migrations/0104-capability-v52';
|
||||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||||
import {
|
import {
|
||||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||||
@@ -236,6 +238,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
|
|||||||
local0100CapabilityV50Migration,
|
local0100CapabilityV50Migration,
|
||||||
local0101LegacyAdoptionProvenanceMigration,
|
local0101LegacyAdoptionProvenanceMigration,
|
||||||
local0102CapabilityV51Migration,
|
local0102CapabilityV51Migration,
|
||||||
|
local0103SecretConfigApplicationsMigration,
|
||||||
|
local0104CapabilityV52Migration,
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -522,5 +522,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
|
|||||||
checksum:
|
checksum:
|
||||||
'539f9d60b41b7cfebb203888804329241c178b1ee0105d14949750856260ac1f',
|
'539f9d60b41b7cfebb203888804329241c178b1ee0105d14949750856260ac1f',
|
||||||
}),
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: '0103-secret-config-applications',
|
||||||
|
checksum:
|
||||||
|
'd94aea3a7fc7bdd72cb4f0a6ae4807e12b17d90cd16e242f7d813dfb67a1630e',
|
||||||
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
id: '0104-capability-v52',
|
||||||
|
checksum:
|
||||||
|
'6eb1dab3a49d0075a1e5a18aa90df4e8013af2c006dd2e97a78e8a644438e94d',
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||||
|
|
||||||
|
export const local0103SecretConfigApplicationsMigration =
|
||||||
|
defineLocalSqliteMigration({
|
||||||
|
id: '0103-secret-config-applications',
|
||||||
|
statements: [
|
||||||
|
`
|
||||||
|
CREATE TABLE "QingLong3SecretConfigApplications" (
|
||||||
|
"mutation_id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"project_id" TEXT NOT NULL,
|
||||||
|
"profile" TEXT NOT NULL,
|
||||||
|
"secret_config_plan_digest" TEXT NOT NULL,
|
||||||
|
"decision_digest" TEXT NOT NULL,
|
||||||
|
"candidate_set_digest" TEXT NOT NULL,
|
||||||
|
"automation_adoption_set_digest" TEXT NOT NULL,
|
||||||
|
"active_binding_count" INTEGER NOT NULL,
|
||||||
|
"disabled_preservation_count" INTEGER NOT NULL,
|
||||||
|
"task_count" INTEGER NOT NULL,
|
||||||
|
"trigger_count" INTEGER NOT NULL,
|
||||||
|
"publication_digest" TEXT NOT NULL,
|
||||||
|
"audit_event_id" TEXT NOT NULL,
|
||||||
|
"applied_at_ms" INTEGER NOT NULL,
|
||||||
|
"receipt_digest" TEXT NOT NULL,
|
||||||
|
"receipt_json" TEXT NOT NULL,
|
||||||
|
CONSTRAINT ql3_secret_config_application_project_fk
|
||||||
|
FOREIGN KEY ("project_id") REFERENCES "QingLong3Projects" ("id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_application_audit_fk
|
||||||
|
FOREIGN KEY ("audit_event_id")
|
||||||
|
REFERENCES "QingLong3SecurityAuditEvents" ("event_id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_application_identity_check CHECK (
|
||||||
|
length("mutation_id") = 36 AND
|
||||||
|
replace("mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("project_id") BETWEEN 1 AND 128 AND
|
||||||
|
"profile" IN ('edge', 'standalone') AND
|
||||||
|
"active_binding_count" BETWEEN 0 AND 256 AND
|
||||||
|
"disabled_preservation_count" BETWEEN 0 AND 512 AND
|
||||||
|
"task_count" BETWEEN 0 AND 100000 AND
|
||||||
|
"trigger_count" BETWEEN 0 AND 500000 AND
|
||||||
|
"applied_at_ms" >= 0
|
||||||
|
),
|
||||||
|
CONSTRAINT ql3_secret_config_application_digest_check CHECK (
|
||||||
|
length("secret_config_plan_digest") = 64 AND
|
||||||
|
"secret_config_plan_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("decision_digest") = 64 AND
|
||||||
|
"decision_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("candidate_set_digest") = 64 AND
|
||||||
|
"candidate_set_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("automation_adoption_set_digest") = 64 AND
|
||||||
|
"automation_adoption_set_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("publication_digest") = 64 AND
|
||||||
|
"publication_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("receipt_digest") = 64 AND
|
||||||
|
"receipt_digest" NOT GLOB '*[^0-9a-f]*'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_applications_plan_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplications" ("secret_config_plan_digest")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_applications_decision_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplications" ("decision_digest")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_applications_receipt_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplications" ("receipt_digest")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE INDEX "ql3_secret_config_applications_project_time_idx"
|
||||||
|
ON "QingLong3SecretConfigApplications" ("project_id", "applied_at_ms")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE TABLE "QingLong3SecretConfigApplicationSecrets" (
|
||||||
|
"application_mutation_id" TEXT NOT NULL,
|
||||||
|
"ordinal" INTEGER NOT NULL,
|
||||||
|
"project_id" TEXT NOT NULL,
|
||||||
|
"disposition" TEXT NOT NULL,
|
||||||
|
"candidate_digest" TEXT NOT NULL,
|
||||||
|
"source_set_digest" TEXT NOT NULL,
|
||||||
|
"environment_name" TEXT,
|
||||||
|
"secret_name" TEXT NOT NULL,
|
||||||
|
"secret_version" INTEGER NOT NULL,
|
||||||
|
"secret_mutation_id" TEXT NOT NULL,
|
||||||
|
"secret_ref" TEXT NOT NULL,
|
||||||
|
"item_digest" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("application_mutation_id", "ordinal"),
|
||||||
|
CONSTRAINT ql3_secret_config_secret_parent_fk
|
||||||
|
FOREIGN KEY ("application_mutation_id")
|
||||||
|
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
CONSTRAINT ql3_secret_config_secret_envelope_fk
|
||||||
|
FOREIGN KEY ("project_id", "secret_name", "secret_version")
|
||||||
|
REFERENCES "QingLong3LocalSecretEnvelopes" (
|
||||||
|
"project_id", "secret_name", "version"
|
||||||
|
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_secret_identity_check CHECK (
|
||||||
|
"ordinal" BETWEEN 1 AND 768 AND
|
||||||
|
"disposition" IN ('active_binding', 'disabled_preservation') AND
|
||||||
|
(("disposition" = 'active_binding' AND "environment_name" IS NOT NULL) OR
|
||||||
|
("disposition" = 'disabled_preservation' AND "environment_name" IS NULL)) AND
|
||||||
|
"secret_version" = 1 AND
|
||||||
|
length("secret_mutation_id") = 36 AND
|
||||||
|
replace("secret_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||||
|
),
|
||||||
|
CONSTRAINT ql3_secret_config_secret_digest_check CHECK (
|
||||||
|
length("candidate_digest") = 64 AND
|
||||||
|
"candidate_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("source_set_digest") = 64 AND
|
||||||
|
"source_set_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("item_digest") = 64 AND
|
||||||
|
"item_digest" NOT GLOB '*[^0-9a-f]*'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_secrets_mutation_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationSecrets" ("secret_mutation_id")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_secrets_candidate_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationSecrets" ("candidate_digest")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_secrets_target_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationSecrets" ("project_id", "secret_name")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE TABLE "QingLong3SecretConfigApplicationTasks" (
|
||||||
|
"application_mutation_id" TEXT NOT NULL,
|
||||||
|
"ordinal" INTEGER NOT NULL,
|
||||||
|
"project_id" TEXT NOT NULL,
|
||||||
|
"adoption_mutation_id" TEXT NOT NULL,
|
||||||
|
"adoption_row_ordinal" INTEGER NOT NULL,
|
||||||
|
"task_id" TEXT NOT NULL,
|
||||||
|
"previous_revision" INTEGER NOT NULL,
|
||||||
|
"previous_content_digest" TEXT NOT NULL,
|
||||||
|
"task_revision" INTEGER NOT NULL,
|
||||||
|
"task_mutation_id" TEXT NOT NULL,
|
||||||
|
"task_content_digest" TEXT NOT NULL,
|
||||||
|
"item_digest" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("application_mutation_id", "ordinal"),
|
||||||
|
CONSTRAINT ql3_secret_config_task_parent_fk
|
||||||
|
FOREIGN KEY ("application_mutation_id")
|
||||||
|
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
CONSTRAINT ql3_secret_config_task_adoption_fk
|
||||||
|
FOREIGN KEY (
|
||||||
|
"adoption_mutation_id", "adoption_row_ordinal", "project_id",
|
||||||
|
"task_id", "previous_revision"
|
||||||
|
) REFERENCES "QingLong3LegacyAdoptionTasks" (
|
||||||
|
"adoption_mutation_id", "row_ordinal", "project_id", "task_id",
|
||||||
|
"task_revision"
|
||||||
|
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_task_revision_fk
|
||||||
|
FOREIGN KEY ("project_id", "task_id", "task_revision")
|
||||||
|
REFERENCES "QingLong3TaskDefinitionRevisions" (
|
||||||
|
"project_id", "task_id", "revision"
|
||||||
|
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_task_identity_check CHECK (
|
||||||
|
"ordinal" BETWEEN 1 AND 100000 AND
|
||||||
|
"adoption_row_ordinal" BETWEEN 1 AND 100000 AND
|
||||||
|
"previous_revision" = 1 AND
|
||||||
|
"task_revision" = 2 AND
|
||||||
|
length("task_mutation_id") = 36 AND
|
||||||
|
replace("task_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||||
|
),
|
||||||
|
CONSTRAINT ql3_secret_config_task_digest_check CHECK (
|
||||||
|
length("previous_content_digest") = 64 AND
|
||||||
|
"previous_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("task_content_digest") = 64 AND
|
||||||
|
"task_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("item_digest") = 64 AND
|
||||||
|
"item_digest" NOT GLOB '*[^0-9a-f]*'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_tasks_identity_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationTasks" ("project_id", "task_id")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_tasks_mutation_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationTasks" ("task_mutation_id")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE TABLE "QingLong3SecretConfigApplicationTriggers" (
|
||||||
|
"application_mutation_id" TEXT NOT NULL,
|
||||||
|
"ordinal" INTEGER NOT NULL,
|
||||||
|
"project_id" TEXT NOT NULL,
|
||||||
|
"adoption_mutation_id" TEXT NOT NULL,
|
||||||
|
"adoption_row_ordinal" INTEGER NOT NULL,
|
||||||
|
"adoption_trigger_ordinal" INTEGER NOT NULL,
|
||||||
|
"task_id" TEXT NOT NULL,
|
||||||
|
"task_revision" INTEGER NOT NULL,
|
||||||
|
"trigger_id" TEXT NOT NULL,
|
||||||
|
"previous_revision" INTEGER NOT NULL,
|
||||||
|
"previous_content_digest" TEXT NOT NULL,
|
||||||
|
"trigger_revision" INTEGER NOT NULL,
|
||||||
|
"trigger_mutation_id" TEXT NOT NULL,
|
||||||
|
"trigger_content_digest" TEXT NOT NULL,
|
||||||
|
"item_digest" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("application_mutation_id", "ordinal"),
|
||||||
|
CONSTRAINT ql3_secret_config_trigger_parent_fk
|
||||||
|
FOREIGN KEY ("application_mutation_id")
|
||||||
|
REFERENCES "QingLong3SecretConfigApplications" ("mutation_id")
|
||||||
|
ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
DEFERRABLE INITIALLY DEFERRED,
|
||||||
|
CONSTRAINT ql3_secret_config_trigger_adoption_fk
|
||||||
|
FOREIGN KEY (
|
||||||
|
"adoption_mutation_id", "adoption_row_ordinal",
|
||||||
|
"adoption_trigger_ordinal"
|
||||||
|
) REFERENCES "QingLong3LegacyAdoptionTriggers" (
|
||||||
|
"adoption_mutation_id", "row_ordinal", "trigger_ordinal"
|
||||||
|
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_trigger_revision_fk
|
||||||
|
FOREIGN KEY ("project_id", "trigger_id", "trigger_revision")
|
||||||
|
REFERENCES "QingLong3TriggerRevisions" (
|
||||||
|
"project_id", "trigger_id", "revision"
|
||||||
|
) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT ql3_secret_config_trigger_identity_check CHECK (
|
||||||
|
"ordinal" BETWEEN 1 AND 500000 AND
|
||||||
|
"adoption_row_ordinal" BETWEEN 1 AND 100000 AND
|
||||||
|
"adoption_trigger_ordinal" BETWEEN 1 AND 500000 AND
|
||||||
|
"task_revision" = 2 AND
|
||||||
|
"previous_revision" = 1 AND
|
||||||
|
"trigger_revision" = 2 AND
|
||||||
|
length("trigger_mutation_id") = 36 AND
|
||||||
|
replace("trigger_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||||
|
),
|
||||||
|
CONSTRAINT ql3_secret_config_trigger_digest_check CHECK (
|
||||||
|
length("previous_content_digest") = 64 AND
|
||||||
|
"previous_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("trigger_content_digest") = 64 AND
|
||||||
|
"trigger_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||||
|
length("item_digest") = 64 AND
|
||||||
|
"item_digest" NOT GLOB '*[^0-9a-f]*'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_triggers_identity_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationTriggers" ("project_id", "trigger_id")
|
||||||
|
`,
|
||||||
|
`
|
||||||
|
CREATE UNIQUE INDEX "ql3_secret_config_triggers_mutation_uidx"
|
||||||
|
ON "QingLong3SecretConfigApplicationTriggers" ("trigger_mutation_id")
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { CAPABILITIES_V51 } from './0102-capability-v51';
|
||||||
|
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||||
|
|
||||||
|
export const CAPABILITIES_V52 = CAPABILITIES_V51.replace(
|
||||||
|
'"legacy_adoption_provenance":1,',
|
||||||
|
'"legacy_adoption_provenance":1,"secret_config_application":1,',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const local0104CapabilityV52Migration = defineLocalSqliteMigration({
|
||||||
|
id: '0104-capability-v52',
|
||||||
|
statements: [
|
||||||
|
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 52, migration_id = '0103-secret-config-applications', capabilities = '${CAPABILITIES_V52}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 51 AND migration_id = '0101-legacy-adoption-provenance' AND capabilities = '${CAPABILITIES_V51}'`,
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '../run/stepRunSchemaContract';
|
} from '../run/stepRunSchemaContract';
|
||||||
|
|
||||||
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
||||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 51;
|
export const LOCAL_SQLITE_CONTRACT_VERSION = 52;
|
||||||
|
|
||||||
const LEGACY_DATA_DIRECTORY_ADOPTION_TRIGGERS = Object.freeze([
|
const LEGACY_DATA_DIRECTORY_ADOPTION_TRIGGERS = Object.freeze([
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
@@ -1439,6 +1439,96 @@ const REQUIRED_SCHEMA = Object.freeze({
|
|||||||
'ql3_legacy_adoption_triggers_item_uidx',
|
'ql3_legacy_adoption_triggers_item_uidx',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
QingLong3SecretConfigApplications: Object.freeze({
|
||||||
|
columns: Object.freeze([
|
||||||
|
'mutation_id',
|
||||||
|
'project_id',
|
||||||
|
'profile',
|
||||||
|
'secret_config_plan_digest',
|
||||||
|
'decision_digest',
|
||||||
|
'candidate_set_digest',
|
||||||
|
'automation_adoption_set_digest',
|
||||||
|
'active_binding_count',
|
||||||
|
'disabled_preservation_count',
|
||||||
|
'task_count',
|
||||||
|
'trigger_count',
|
||||||
|
'publication_digest',
|
||||||
|
'audit_event_id',
|
||||||
|
'applied_at_ms',
|
||||||
|
'receipt_digest',
|
||||||
|
'receipt_json',
|
||||||
|
]),
|
||||||
|
indexes: Object.freeze([
|
||||||
|
'ql3_secret_config_applications_plan_uidx',
|
||||||
|
'ql3_secret_config_applications_decision_uidx',
|
||||||
|
'ql3_secret_config_applications_receipt_uidx',
|
||||||
|
'ql3_secret_config_applications_project_time_idx',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
QingLong3SecretConfigApplicationSecrets: Object.freeze({
|
||||||
|
columns: Object.freeze([
|
||||||
|
'application_mutation_id',
|
||||||
|
'ordinal',
|
||||||
|
'project_id',
|
||||||
|
'disposition',
|
||||||
|
'candidate_digest',
|
||||||
|
'source_set_digest',
|
||||||
|
'environment_name',
|
||||||
|
'secret_name',
|
||||||
|
'secret_version',
|
||||||
|
'secret_mutation_id',
|
||||||
|
'secret_ref',
|
||||||
|
'item_digest',
|
||||||
|
]),
|
||||||
|
indexes: Object.freeze([
|
||||||
|
'ql3_secret_config_secrets_mutation_uidx',
|
||||||
|
'ql3_secret_config_secrets_candidate_uidx',
|
||||||
|
'ql3_secret_config_secrets_target_uidx',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
QingLong3SecretConfigApplicationTasks: Object.freeze({
|
||||||
|
columns: Object.freeze([
|
||||||
|
'application_mutation_id',
|
||||||
|
'ordinal',
|
||||||
|
'project_id',
|
||||||
|
'adoption_mutation_id',
|
||||||
|
'adoption_row_ordinal',
|
||||||
|
'task_id',
|
||||||
|
'previous_revision',
|
||||||
|
'previous_content_digest',
|
||||||
|
'task_revision',
|
||||||
|
'task_mutation_id',
|
||||||
|
'task_content_digest',
|
||||||
|
'item_digest',
|
||||||
|
]),
|
||||||
|
indexes: Object.freeze([
|
||||||
|
'ql3_secret_config_tasks_identity_uidx',
|
||||||
|
'ql3_secret_config_tasks_mutation_uidx',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
QingLong3SecretConfigApplicationTriggers: Object.freeze({
|
||||||
|
columns: Object.freeze([
|
||||||
|
'application_mutation_id',
|
||||||
|
'ordinal',
|
||||||
|
'project_id',
|
||||||
|
'adoption_mutation_id',
|
||||||
|
'adoption_row_ordinal',
|
||||||
|
'adoption_trigger_ordinal',
|
||||||
|
'task_id',
|
||||||
|
'task_revision',
|
||||||
|
'trigger_id',
|
||||||
|
'previous_revision',
|
||||||
|
'previous_content_digest',
|
||||||
|
'trigger_revision',
|
||||||
|
'trigger_mutation_id',
|
||||||
|
'trigger_content_digest',
|
||||||
|
'item_digest',
|
||||||
|
]),
|
||||||
|
indexes: Object.freeze([
|
||||||
|
'ql3_secret_config_triggers_identity_uidx',
|
||||||
|
'ql3_secret_config_triggers_mutation_uidx',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
QingLong3LegacyDataDirectoryAdoptions: Object.freeze({
|
QingLong3LegacyDataDirectoryAdoptions: Object.freeze({
|
||||||
columns: Object.freeze([
|
columns: Object.freeze([
|
||||||
'mutation_id',
|
'mutation_id',
|
||||||
@@ -2699,10 +2789,10 @@ export async function auditLocalSqliteReadiness(
|
|||||||
!capability ||
|
!capability ||
|
||||||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
||||||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
||||||
capability.migration_id !== '0101-legacy-adoption-provenance' ||
|
capability.migration_id !== '0103-secret-config-applications' ||
|
||||||
typeof capability.capabilities !== 'string' ||
|
typeof capability.capabilities !== 'string' ||
|
||||||
capability.capabilities !==
|
capability.capabilities !==
|
||||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,"secret_config_application":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||||
typeof capability.updated_at_ms !== 'number' ||
|
typeof capability.updated_at_ms !== 'number' ||
|
||||||
!Number.isSafeInteger(capability.updated_at_ms) ||
|
!Number.isSafeInteger(capability.updated_at_ms) ||
|
||||||
capability.updated_at_ms < 0
|
capability.updated_at_ms < 0
|
||||||
|
|||||||
@@ -3472,6 +3472,259 @@ export const legacyAdoptionTriggers = sqliteTable(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const secretConfigApplications = sqliteTable(
|
||||||
|
'QingLong3SecretConfigApplications',
|
||||||
|
{
|
||||||
|
mutationId: text('mutation_id').primaryKey(),
|
||||||
|
projectId: text('project_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => localProjects.id, {
|
||||||
|
onDelete: 'restrict',
|
||||||
|
onUpdate: 'restrict',
|
||||||
|
}),
|
||||||
|
profile: text('profile').notNull(),
|
||||||
|
secretConfigPlanDigest: text('secret_config_plan_digest').notNull(),
|
||||||
|
decisionDigest: text('decision_digest').notNull(),
|
||||||
|
candidateSetDigest: text('candidate_set_digest').notNull(),
|
||||||
|
automationAdoptionSetDigest: text(
|
||||||
|
'automation_adoption_set_digest',
|
||||||
|
).notNull(),
|
||||||
|
activeBindingCount: integer('active_binding_count').notNull(),
|
||||||
|
disabledPreservationCount: integer('disabled_preservation_count').notNull(),
|
||||||
|
taskCount: integer('task_count').notNull(),
|
||||||
|
triggerCount: integer('trigger_count').notNull(),
|
||||||
|
publicationDigest: text('publication_digest').notNull(),
|
||||||
|
auditEventId: text('audit_event_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => localSecurityAuditEvents.eventId, {
|
||||||
|
onDelete: 'restrict',
|
||||||
|
onUpdate: 'restrict',
|
||||||
|
}),
|
||||||
|
appliedAtMs: integer('applied_at_ms').notNull(),
|
||||||
|
receiptDigest: text('receipt_digest').notNull(),
|
||||||
|
receiptJson: text('receipt_json', { mode: 'json' })
|
||||||
|
.$type<Record<string, unknown>>()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_application_identity_check',
|
||||||
|
sql`length(${table.mutationId}) = 36 and replace(${table.mutationId}, '-', '') not glob '*[^0-9a-f]*' and length(${table.projectId}) between 1 and 128 and ${table.profile} in ('edge', 'standalone') and ${table.activeBindingCount} between 0 and 256 and ${table.disabledPreservationCount} between 0 and 512 and ${table.taskCount} between 0 and 100000 and ${table.triggerCount} between 0 and 500000 and ${table.appliedAtMs} >= 0`,
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_application_digest_check',
|
||||||
|
sql`length(${table.secretConfigPlanDigest}) = 64 and ${table.secretConfigPlanDigest} not glob '*[^0-9a-f]*' and length(${table.decisionDigest}) = 64 and ${table.decisionDigest} not glob '*[^0-9a-f]*' and length(${table.candidateSetDigest}) = 64 and ${table.candidateSetDigest} not glob '*[^0-9a-f]*' and length(${table.automationAdoptionSetDigest}) = 64 and ${table.automationAdoptionSetDigest} not glob '*[^0-9a-f]*' and length(${table.publicationDigest}) = 64 and ${table.publicationDigest} not glob '*[^0-9a-f]*' and length(${table.receiptDigest}) = 64 and ${table.receiptDigest} not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_applications_plan_uidx').on(
|
||||||
|
table.secretConfigPlanDigest,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_applications_decision_uidx').on(
|
||||||
|
table.decisionDigest,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_applications_receipt_uidx').on(
|
||||||
|
table.receiptDigest,
|
||||||
|
),
|
||||||
|
index('ql3_secret_config_applications_project_time_idx').on(
|
||||||
|
table.projectId,
|
||||||
|
table.appliedAtMs,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const secretConfigApplicationSecrets = sqliteTable(
|
||||||
|
'QingLong3SecretConfigApplicationSecrets',
|
||||||
|
{
|
||||||
|
applicationMutationId: text('application_mutation_id').notNull(),
|
||||||
|
ordinal: integer('ordinal').notNull(),
|
||||||
|
projectId: text('project_id').notNull(),
|
||||||
|
disposition: text('disposition').notNull(),
|
||||||
|
candidateDigest: text('candidate_digest').notNull(),
|
||||||
|
sourceSetDigest: text('source_set_digest').notNull(),
|
||||||
|
environmentName: text('environment_name'),
|
||||||
|
secretName: text('secret_name').notNull(),
|
||||||
|
secretVersion: integer('secret_version').notNull(),
|
||||||
|
secretMutationId: text('secret_mutation_id').notNull(),
|
||||||
|
secretRef: text('secret_ref').notNull(),
|
||||||
|
itemDigest: text('item_digest').notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.applicationMutationId],
|
||||||
|
foreignColumns: [secretConfigApplications.mutationId],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.projectId, table.secretName, table.secretVersion],
|
||||||
|
foreignColumns: [
|
||||||
|
localSecretEnvelopes.projectId,
|
||||||
|
localSecretEnvelopes.name,
|
||||||
|
localSecretEnvelopes.version,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_secret_identity_check',
|
||||||
|
sql`${table.ordinal} between 1 and 768 and ${table.disposition} in ('active_binding', 'disabled_preservation') and ((${table.disposition} = 'active_binding' and ${table.environmentName} is not null) or (${table.disposition} = 'disabled_preservation' and ${table.environmentName} is null)) and ${table.secretVersion} = 1 and length(${table.secretMutationId}) = 36 and replace(${table.secretMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_secret_digest_check',
|
||||||
|
sql`length(${table.candidateDigest}) = 64 and ${table.candidateDigest} not glob '*[^0-9a-f]*' and length(${table.sourceSetDigest}) = 64 and ${table.sourceSetDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_secrets_mutation_uidx').on(
|
||||||
|
table.secretMutationId,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_secrets_candidate_uidx').on(
|
||||||
|
table.candidateDigest,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_secrets_target_uidx').on(
|
||||||
|
table.projectId,
|
||||||
|
table.secretName,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const secretConfigApplicationTasks = sqliteTable(
|
||||||
|
'QingLong3SecretConfigApplicationTasks',
|
||||||
|
{
|
||||||
|
applicationMutationId: text('application_mutation_id').notNull(),
|
||||||
|
ordinal: integer('ordinal').notNull(),
|
||||||
|
projectId: text('project_id').notNull(),
|
||||||
|
adoptionMutationId: text('adoption_mutation_id').notNull(),
|
||||||
|
adoptionRowOrdinal: integer('adoption_row_ordinal').notNull(),
|
||||||
|
taskId: text('task_id').notNull(),
|
||||||
|
previousRevision: integer('previous_revision').notNull(),
|
||||||
|
previousContentDigest: text('previous_content_digest').notNull(),
|
||||||
|
taskRevision: integer('task_revision').notNull(),
|
||||||
|
taskMutationId: text('task_mutation_id').notNull(),
|
||||||
|
taskContentDigest: text('task_content_digest').notNull(),
|
||||||
|
itemDigest: text('item_digest').notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.applicationMutationId],
|
||||||
|
foreignColumns: [secretConfigApplications.mutationId],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [
|
||||||
|
table.adoptionMutationId,
|
||||||
|
table.adoptionRowOrdinal,
|
||||||
|
table.projectId,
|
||||||
|
table.taskId,
|
||||||
|
table.previousRevision,
|
||||||
|
],
|
||||||
|
foreignColumns: [
|
||||||
|
legacyAdoptionTasks.adoptionMutationId,
|
||||||
|
legacyAdoptionTasks.rowOrdinal,
|
||||||
|
legacyAdoptionTasks.projectId,
|
||||||
|
legacyAdoptionTasks.taskId,
|
||||||
|
legacyAdoptionTasks.taskRevision,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.projectId, table.taskId, table.taskRevision],
|
||||||
|
foreignColumns: [
|
||||||
|
taskDefinitionRevisions.projectId,
|
||||||
|
taskDefinitionRevisions.taskId,
|
||||||
|
taskDefinitionRevisions.revision,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_task_identity_check',
|
||||||
|
sql`${table.ordinal} between 1 and 100000 and ${table.adoptionRowOrdinal} between 1 and 100000 and ${table.previousRevision} = 1 and ${table.taskRevision} = 2 and length(${table.taskMutationId}) = 36 and replace(${table.taskMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_task_digest_check',
|
||||||
|
sql`length(${table.previousContentDigest}) = 64 and ${table.previousContentDigest} not glob '*[^0-9a-f]*' and length(${table.taskContentDigest}) = 64 and ${table.taskContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_tasks_identity_uidx').on(
|
||||||
|
table.projectId,
|
||||||
|
table.taskId,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_tasks_mutation_uidx').on(
|
||||||
|
table.taskMutationId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const secretConfigApplicationTriggers = sqliteTable(
|
||||||
|
'QingLong3SecretConfigApplicationTriggers',
|
||||||
|
{
|
||||||
|
applicationMutationId: text('application_mutation_id').notNull(),
|
||||||
|
ordinal: integer('ordinal').notNull(),
|
||||||
|
projectId: text('project_id').notNull(),
|
||||||
|
adoptionMutationId: text('adoption_mutation_id').notNull(),
|
||||||
|
adoptionRowOrdinal: integer('adoption_row_ordinal').notNull(),
|
||||||
|
adoptionTriggerOrdinal: integer('adoption_trigger_ordinal').notNull(),
|
||||||
|
taskId: text('task_id').notNull(),
|
||||||
|
taskRevision: integer('task_revision').notNull(),
|
||||||
|
triggerId: text('trigger_id').notNull(),
|
||||||
|
previousRevision: integer('previous_revision').notNull(),
|
||||||
|
previousContentDigest: text('previous_content_digest').notNull(),
|
||||||
|
triggerRevision: integer('trigger_revision').notNull(),
|
||||||
|
triggerMutationId: text('trigger_mutation_id').notNull(),
|
||||||
|
triggerContentDigest: text('trigger_content_digest').notNull(),
|
||||||
|
itemDigest: text('item_digest').notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
primaryKey({ columns: [table.applicationMutationId, table.ordinal] }),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.applicationMutationId],
|
||||||
|
foreignColumns: [secretConfigApplications.mutationId],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [
|
||||||
|
table.adoptionMutationId,
|
||||||
|
table.adoptionRowOrdinal,
|
||||||
|
table.adoptionTriggerOrdinal,
|
||||||
|
],
|
||||||
|
foreignColumns: [
|
||||||
|
legacyAdoptionTriggers.adoptionMutationId,
|
||||||
|
legacyAdoptionTriggers.rowOrdinal,
|
||||||
|
legacyAdoptionTriggers.triggerOrdinal,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.projectId, table.triggerId, table.triggerRevision],
|
||||||
|
foreignColumns: [
|
||||||
|
triggerRevisions.projectId,
|
||||||
|
triggerRevisions.triggerId,
|
||||||
|
triggerRevisions.revision,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.onDelete('restrict')
|
||||||
|
.onUpdate('restrict'),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_trigger_identity_check',
|
||||||
|
sql`${table.ordinal} between 1 and 500000 and ${table.adoptionRowOrdinal} between 1 and 100000 and ${table.adoptionTriggerOrdinal} between 1 and 500000 and ${table.taskRevision} = 2 and ${table.previousRevision} = 1 and ${table.triggerRevision} = 2 and length(${table.triggerMutationId}) = 36 and replace(${table.triggerMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
check(
|
||||||
|
'ql3_secret_config_trigger_digest_check',
|
||||||
|
sql`length(${table.previousContentDigest}) = 64 and ${table.previousContentDigest} not glob '*[^0-9a-f]*' and length(${table.triggerContentDigest}) = 64 and ${table.triggerContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_triggers_identity_uidx').on(
|
||||||
|
table.projectId,
|
||||||
|
table.triggerId,
|
||||||
|
),
|
||||||
|
uniqueIndex('ql3_secret_config_triggers_mutation_uidx').on(
|
||||||
|
table.triggerMutationId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
export const legacyDataDirectoryAdoptions = sqliteTable(
|
export const legacyDataDirectoryAdoptions = sqliteTable(
|
||||||
'QingLong3LegacyDataDirectoryAdoptions',
|
'QingLong3LegacyDataDirectoryAdoptions',
|
||||||
{
|
{
|
||||||
@@ -5326,6 +5579,10 @@ export const localSqliteSchema = Object.freeze({
|
|||||||
legacyAdoptions,
|
legacyAdoptions,
|
||||||
legacyAdoptionTasks,
|
legacyAdoptionTasks,
|
||||||
legacyAdoptionTriggers,
|
legacyAdoptionTriggers,
|
||||||
|
secretConfigApplications,
|
||||||
|
secretConfigApplicationSecrets,
|
||||||
|
secretConfigApplicationTasks,
|
||||||
|
secretConfigApplicationTriggers,
|
||||||
legacyDataDirectoryAdoptions,
|
legacyDataDirectoryAdoptions,
|
||||||
legacyDataDirectoryAdoptionSecrets,
|
legacyDataDirectoryAdoptionSecrets,
|
||||||
localIdentitySubjects,
|
localIdentitySubjects,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ test('authentication projection opens the target read-only without journal or fi
|
|||||||
try {
|
try {
|
||||||
assert.equal(database.profile, 'edge');
|
assert.equal(database.profile, 'edge');
|
||||||
assert.equal(database.readiness.contractName, 'local-control-core');
|
assert.equal(database.readiness.contractName, 'local-control-core');
|
||||||
assert.equal(database.readiness.contractVersion, 51);
|
assert.equal(database.readiness.contractVersion, 52);
|
||||||
assert.equal(await database.apiCredentials.resolve('absent'), null);
|
assert.equal(await database.apiCredentials.resolve('absent'), null);
|
||||||
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
|
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -152,9 +152,11 @@ test('creates a reviewed edge database and opens runtime only after readiness',
|
|||||||
'0100-capability-v50',
|
'0100-capability-v50',
|
||||||
'0101-legacy-adoption-provenance',
|
'0101-legacy-adoption-provenance',
|
||||||
'0102-capability-v51',
|
'0102-capability-v51',
|
||||||
|
'0103-secret-config-applications',
|
||||||
|
'0104-capability-v52',
|
||||||
]);
|
]);
|
||||||
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
assert.equal(migrated.readiness.contractName, 'local-control-core');
|
||||||
assert.equal(migrated.readiness.contractVersion, 51);
|
assert.equal(migrated.readiness.contractVersion, 52);
|
||||||
assert.equal(migrated.readiness.journalMode, 'delete');
|
assert.equal(migrated.readiness.journalMode, 'delete');
|
||||||
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
assert.equal(fs.statSync(databasePath).mode & 0o777, 0o600);
|
||||||
|
|
||||||
@@ -600,8 +602,8 @@ test('backfills v14 execution revisions with a verified independent digest', asy
|
|||||||
.get(),
|
.get(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contract_version: 51,
|
contract_version: 52,
|
||||||
migration_id: '0101-legacy-adoption-provenance',
|
migration_id: '0103-secret-config-applications',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -788,19 +790,19 @@ test('excludes reviewed optional feature tables while preserving unknown table d
|
|||||||
const options = { databasePath, profile: 'edge' };
|
const options = { databasePath, profile: 'edge' };
|
||||||
await migrateLocalSqlitePath(options);
|
await migrateLocalSqlitePath(options);
|
||||||
const client = new DatabaseSync(databasePath);
|
const client = new DatabaseSync(databasePath);
|
||||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
|
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
|
||||||
client.exec(
|
client.exec(
|
||||||
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
|
'CREATE TABLE "ModelInvocationFeatureHead" (feature_id TEXT PRIMARY KEY)',
|
||||||
);
|
);
|
||||||
client.close();
|
client.close();
|
||||||
|
|
||||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 85);
|
assert.equal((await auditLocalSqlitePath(options)).tableCount, 89);
|
||||||
|
|
||||||
const unknownClient = new DatabaseSync(databasePath);
|
const unknownClient = new DatabaseSync(databasePath);
|
||||||
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
|
unknownClient.exec('CREATE TABLE "UserExtensionData" (id TEXT PRIMARY KEY)');
|
||||||
unknownClient.close();
|
unknownClient.close();
|
||||||
|
|
||||||
assert.equal((await auditLocalSqlitePath(options)).tableCount, 86);
|
assert.equal((await auditLocalSqlitePath(options)).tableCount, 90);
|
||||||
|
|
||||||
const triggerClient = new DatabaseSync(databasePath);
|
const triggerClient = new DatabaseSync(databasePath);
|
||||||
triggerClient.exec(`
|
triggerClient.exec(`
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ test('atomically admits one generation-bound Workflow Run and exactly replays it
|
|||||||
},
|
},
|
||||||
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
|
{ runs: 1, steps: 2, events: 3, mutations: 2, admissions: 1 },
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
|
test('runs an optional authorization guard inside new and replay transactions', async (t) => {
|
||||||
@@ -288,7 +288,7 @@ test('exactly replays immutable admission after the Workflow StepRun advances',
|
|||||||
},
|
},
|
||||||
{ status: 'running', version: 5, eventSequence: 5 },
|
{ status: 'running', version: 5, eventSequence: 5 },
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fails closed before writing when the exact installation is not active', async (t) => {
|
test('fails closed before writing when the exact installation is not active', async (t) => {
|
||||||
|
|||||||
+1
-1
@@ -231,7 +231,7 @@ test('atomically admits the exact reconciled local Task revision and replays it'
|
|||||||
stepAttemptCount: 0,
|
stepAttemptCount: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 51);
|
assert.equal((await auditLocalSqliteReadiness(client)).contractVersion, 52);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
|
test('bounds candidate paging before SQL and fences cancellation', async (t) => {
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ test('creates and exactly replays a reviewed rollout backup', async (t) => {
|
|||||||
await migrateLocalSqlitePath(state);
|
await migrateLocalSqlitePath(state);
|
||||||
const prepared = await createLocalSqliteRolloutBackup(state);
|
const prepared = await createLocalSqliteRolloutBackup(state);
|
||||||
assert.equal(prepared.status, 'prepared');
|
assert.equal(prepared.status, 'prepared');
|
||||||
assert.equal(prepared.contractVersion, 51);
|
assert.equal(prepared.contractVersion, 52);
|
||||||
assert.equal(prepared.writeContractVersion, 51);
|
assert.equal(prepared.writeContractVersion, 52);
|
||||||
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 51);
|
assert.equal(LOCAL_SQLITE_WRITE_CONTRACT_VERSION, 52);
|
||||||
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
assert.match(prepared.sha256, /^[0-9a-f]{64}$/);
|
||||||
assert.equal(prepared.bytes > 0, true);
|
assert.equal(prepared.bytes > 0, true);
|
||||||
assert.equal(prepared.pageCount > 0, true);
|
assert.equal(prepared.pageCount > 0, true);
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const { migrateLocalSqlitePath } = require('../dist/migration/migration');
|
||||||
|
const {
|
||||||
|
openLocalSqliteAdoptionDatabase,
|
||||||
|
} = require('@qinglong/local-sqlite/adoption');
|
||||||
|
const {
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
openLocalSqliteSecretConfigApplicationDatabase,
|
||||||
|
} = require('@qinglong/local-sqlite/secret-config-application');
|
||||||
|
|
||||||
|
const SUBJECT = Object.freeze({ type: 'user', id: 'local-owner' });
|
||||||
|
const FENCE = Object.freeze({ projectVersion: 1, bindingVersion: 1 });
|
||||||
|
const ADOPTION_MUTATION = '12345678-1234-4123-8123-123456789abc';
|
||||||
|
const APPLICATION_MUTATION = '87654321-4321-4123-8123-cba987654321';
|
||||||
|
const SECRET_MUTATION = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
|
||||||
|
|
||||||
|
function digest(character) {
|
||||||
|
return character.repeat(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixture(t) {
|
||||||
|
const directory = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'ql3-secret-config-application-'),
|
||||||
|
);
|
||||||
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||||
|
return path.join(directory, 'qinglong3.sqlite');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preparedDatabase(t) {
|
||||||
|
const databasePath = fixture(t);
|
||||||
|
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
||||||
|
const client = new DatabaseSync(databasePath);
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO "QingLong3ProjectRoleBindings" (
|
||||||
|
"project_id", "subject_type", "subject_id", "version", "state", "role",
|
||||||
|
"mutation_id", "changed_by_type", "changed_by_id", "created_at_ms"
|
||||||
|
) VALUES ('default', 'user', 'local-owner', 1, 'active', 'owner',
|
||||||
|
'secret-config-owner-binding', 'user', 'local-owner', 1)`,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
const adoption = await openLocalSqliteAdoptionDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
await adoption.publisher.publish({
|
||||||
|
mutationId: ADOPTION_MUTATION,
|
||||||
|
decisionId: '019f7200-0000-7000-8000-000000000001',
|
||||||
|
projectId: 'default',
|
||||||
|
profile: 'edge',
|
||||||
|
planDigest: digest('1'),
|
||||||
|
inventoryDigest: digest('2'),
|
||||||
|
decisionDigest: digest('3'),
|
||||||
|
receiptDigest: digest('4'),
|
||||||
|
authorizationFileDigest: digest('5'),
|
||||||
|
rowCount: 1,
|
||||||
|
skippedCount: 0,
|
||||||
|
subject: SUBJECT,
|
||||||
|
fence: FENCE,
|
||||||
|
audit: {
|
||||||
|
eventId: ADOPTION_MUTATION,
|
||||||
|
requestId: 'automation-adoption',
|
||||||
|
operationId: 'task.adopt',
|
||||||
|
projectId: 'default',
|
||||||
|
subject: SUBJECT,
|
||||||
|
authenticationId: 'local-console:review',
|
||||||
|
outcome: 'allowed',
|
||||||
|
reasons: ['project_role_allowed'],
|
||||||
|
fence: FENCE,
|
||||||
|
occurredAtMs: 100,
|
||||||
|
},
|
||||||
|
candidates: [
|
||||||
|
{
|
||||||
|
rowOrdinal: 1,
|
||||||
|
sourceDigest: digest('6'),
|
||||||
|
task: {
|
||||||
|
taskId: 'legacy-cron:1',
|
||||||
|
name: 'Legacy Task',
|
||||||
|
kind: 'command',
|
||||||
|
spec: {
|
||||||
|
schema: 'qinglong/command@v1',
|
||||||
|
config: {
|
||||||
|
command: { kind: 'argv', file: '/bin/echo', args: ['legacy'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labels: { source: 'legacy-adoption' },
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
triggers: [
|
||||||
|
{
|
||||||
|
triggerId: 'legacy-cron:1:cron:1',
|
||||||
|
spec: {
|
||||||
|
schema: 'qinglong/cron@v1',
|
||||||
|
config: {
|
||||||
|
expression: '0 0 * * *',
|
||||||
|
timezone: 'UTC',
|
||||||
|
misfirePolicy: 'skip',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
confirmExternalAuthority() {},
|
||||||
|
createdAtMs: 100,
|
||||||
|
});
|
||||||
|
await adoption.close();
|
||||||
|
return databasePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applicationCommand(confirmExternalAuthority = () => {}) {
|
||||||
|
return {
|
||||||
|
mutationId: APPLICATION_MUTATION,
|
||||||
|
projectId: 'default',
|
||||||
|
profile: 'edge',
|
||||||
|
secretConfigPlanDigest: digest('7'),
|
||||||
|
decisionDigest: digest('8'),
|
||||||
|
candidateSetDigest: digest('9'),
|
||||||
|
automationAdoptionSetDigest: digest('a'),
|
||||||
|
subject: SUBJECT,
|
||||||
|
fence: FENCE,
|
||||||
|
audit: {
|
||||||
|
eventId: APPLICATION_MUTATION,
|
||||||
|
requestId: 'secret-config-application',
|
||||||
|
operationId: 'secret-config.apply',
|
||||||
|
projectId: 'default',
|
||||||
|
subject: SUBJECT,
|
||||||
|
authenticationId: 'local-console:review',
|
||||||
|
outcome: 'allowed',
|
||||||
|
reasons: ['project_role_allowed'],
|
||||||
|
fence: FENCE,
|
||||||
|
occurredAtMs: 200,
|
||||||
|
},
|
||||||
|
secrets: [
|
||||||
|
{
|
||||||
|
ordinal: 1,
|
||||||
|
disposition: 'active_binding',
|
||||||
|
candidateDigest: digest('b'),
|
||||||
|
sourceSetDigest: digest('c'),
|
||||||
|
environmentName: 'LEGACY_TOKEN',
|
||||||
|
envelope: {
|
||||||
|
projectId: 'default',
|
||||||
|
name: 'legacy-db-env-bbbbbbbbbbbbbbbb',
|
||||||
|
version: 1,
|
||||||
|
mutationId: SECRET_MUTATION,
|
||||||
|
keyId: 'active-key',
|
||||||
|
algorithm: 'aes-256-gcm',
|
||||||
|
nonce: Buffer.alloc(12, 1).toString('base64url'),
|
||||||
|
ciphertext: Buffer.from('ciphertext').toString('base64url'),
|
||||||
|
authTag: Buffer.alloc(16, 2).toString('base64url'),
|
||||||
|
createdAtMs: 200,
|
||||||
|
},
|
||||||
|
audit: {
|
||||||
|
eventId: SECRET_MUTATION,
|
||||||
|
requestId: 'secret-config-application',
|
||||||
|
operationId: 'secret.create',
|
||||||
|
projectId: 'default',
|
||||||
|
subject: SUBJECT,
|
||||||
|
authenticationId: 'local-console:review',
|
||||||
|
outcome: 'allowed',
|
||||||
|
reasons: ['project_role_allowed'],
|
||||||
|
fence: FENCE,
|
||||||
|
occurredAtMs: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
appliedAtMs: 200,
|
||||||
|
confirmExternalAuthority,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('atomically binds Secret, Task, dispatch, Trigger, schedule and replay ledger', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
const command = applicationCommand();
|
||||||
|
const inserted = await database.publisher.publish(command);
|
||||||
|
assert.equal(inserted.status, 'inserted');
|
||||||
|
assert.equal(inserted.application.receipt.activeBindingCount, 1);
|
||||||
|
assert.equal(inserted.application.receipt.taskCount, 1);
|
||||||
|
assert.equal(inserted.application.receipt.triggerCount, 1);
|
||||||
|
assert.equal((await database.publisher.publish(command)).status, 'existing');
|
||||||
|
await database.close();
|
||||||
|
|
||||||
|
const client = new DatabaseSync(databasePath, { readOnly: true });
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT "current_revision" AS revision FROM "QingLong3Triggers" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT "trigger_revision" AS revision FROM "QingLong3LocalTriggerSchedules" WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3LocalTaskExecutionRevisions" WHERE "task_id" = 'legacy-cron:1' AND "task_revision" LIKE 'qltd:v1:2:%'`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rolls every database mutation back when the commit authority changes', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
let checks = 0;
|
||||||
|
await assert.rejects(
|
||||||
|
database.publisher.publish(
|
||||||
|
applicationCommand(() => {
|
||||||
|
checks += 1;
|
||||||
|
if (checks === 2) throw new Error('instance head drifted');
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
/unavailable/,
|
||||||
|
);
|
||||||
|
await database.close();
|
||||||
|
const client = new DatabaseSync(databasePath, { readOnly: true });
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3LocalSecretEnvelopes" WHERE "secret_name" = 'legacy-db-env-bbbbbbbbbbbbbbbb'`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails closed on occupied Secret without advancing Task or Trigger heads', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const client = new DatabaseSync(databasePath);
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO "QingLong3LocalSecretEnvelopes" ("project_id", "secret_name", "version", "mutation_id", "key_id", "algorithm", "nonce", "ciphertext", "auth_tag", "created_at_ms") VALUES ('default', 'legacy-db-env-bbbbbbbbbbbbbbbb', 1, '11111111-2222-4333-8444-555555555555', 'other-key', 'aes-256-gcm', ?, ?, ?, 150)`,
|
||||||
|
)
|
||||||
|
.run(Buffer.alloc(12), Buffer.from('occupied'), Buffer.alloc(16));
|
||||||
|
client.close();
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
database.publisher.publish(applicationCommand()),
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
);
|
||||||
|
await database.close();
|
||||||
|
const inspected = new DatabaseSync(databasePath, { readOnly: true });
|
||||||
|
assert.equal(
|
||||||
|
inspected
|
||||||
|
.prepare(
|
||||||
|
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
inspected
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplications"`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
inspected.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects incomplete Trigger provenance and rolls the streamed transaction back', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const client = new DatabaseSync(databasePath);
|
||||||
|
client.prepare(`DELETE FROM "QingLong3LegacyAdoptionTriggers"`).run();
|
||||||
|
client.close();
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
database.publisher.publish(applicationCommand()),
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
);
|
||||||
|
await database.close();
|
||||||
|
const inspected = new DatabaseSync(databasePath, { readOnly: true });
|
||||||
|
assert.equal(
|
||||||
|
inspected
|
||||||
|
.prepare(
|
||||||
|
`SELECT "current_revision" AS revision FROM "QingLong3TaskDefinitions" WHERE "task_id" = 'legacy-cron:1'`,
|
||||||
|
)
|
||||||
|
.get().revision,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
inspected
|
||||||
|
.prepare(
|
||||||
|
`SELECT count(*) AS count FROM "QingLong3SecretConfigApplicationTasks"`,
|
||||||
|
)
|
||||||
|
.get().count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
inspected.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects replay after the durable Trigger schedule drifts', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
const command = applicationCommand();
|
||||||
|
await database.publisher.publish(command);
|
||||||
|
await database.close();
|
||||||
|
const client = new DatabaseSync(databasePath);
|
||||||
|
client
|
||||||
|
.prepare(
|
||||||
|
`UPDATE "QingLong3LocalTriggerSchedules" SET "trigger_revision" = 1 WHERE "trigger_id" = 'legacy-cron:1:cron:1'`,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
client.close();
|
||||||
|
const reopened = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
reopened.publisher.publish(command),
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
);
|
||||||
|
await reopened.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty applications and Secret timestamps outside the application instant', async (t) => {
|
||||||
|
const databasePath = await preparedDatabase(t);
|
||||||
|
const database = await openLocalSqliteSecretConfigApplicationDatabase({
|
||||||
|
databasePath,
|
||||||
|
profile: 'edge',
|
||||||
|
});
|
||||||
|
assert.throws(
|
||||||
|
() => database.publisher.publish({ ...applicationCommand(), secrets: [] }),
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
);
|
||||||
|
const command = applicationCommand();
|
||||||
|
command.secrets[0].envelope.createdAtMs = command.appliedAtMs - 1;
|
||||||
|
assert.throws(
|
||||||
|
() => database.publisher.publish(command),
|
||||||
|
LocalSecretConfigApplicationConflictError,
|
||||||
|
);
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
@@ -262,8 +262,8 @@ function auditDockerfile(contents, findings) {
|
|||||||
!contents.includes('io.qinglong.profile="edge,standalone"') ||
|
!contents.includes('io.qinglong.profile="edge,standalone"') ||
|
||||||
!contents.includes('io.qinglong.local.application-config="2,3,4"') ||
|
!contents.includes('io.qinglong.local.application-config="2,3,4"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
!contents.includes('io.qinglong.local.sqlite-contract-min="51"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-contract-max="51"') ||
|
!contents.includes('io.qinglong.local.sqlite-contract-max="52"') ||
|
||||||
!contents.includes('io.qinglong.local.sqlite-write-contract="51"') ||
|
!contents.includes('io.qinglong.local.sqlite-write-contract="52"') ||
|
||||||
!contents.includes('io.qinglong.local.compose-selection="1"')
|
!contents.includes('io.qinglong.local.compose-selection="1"')
|
||||||
) {
|
) {
|
||||||
addFinding(findings, 'RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
addFinding(findings, 'RUNTIME_IDENTITY_OR_LABEL_DRIFT');
|
||||||
@@ -285,7 +285,7 @@ function auditWorkflow(contents, findings) {
|
|||||||
'pnpm audit:local-image:ql3',
|
'pnpm audit:local-image:ql3',
|
||||||
'docker build',
|
'docker build',
|
||||||
'--file deploy/containers/ql3-local-application/Dockerfile',
|
'--file deploy/containers/ql3-local-application/Dockerfile',
|
||||||
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 51 51 1',
|
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1',
|
||||||
'actual="$(docker image inspect --format \'{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}\' "${IMAGE}")"',
|
'actual="$(docker image inspect --format \'{{.Architecture}} {{.Config.User}} {{index .Config.Labels "io.qinglong.local.application-config"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-min"}} {{index .Config.Labels "io.qinglong.local.sqlite-contract-max"}} {{index .Config.Labels "io.qinglong.local.sqlite-write-contract"}} {{index .Config.Labels "io.qinglong.local.compose-selection"}}\' "${IMAGE}")"',
|
||||||
'io.qinglong.local.application-config',
|
'io.qinglong.local.application-config',
|
||||||
'io.qinglong.local.sqlite-contract-min',
|
'io.qinglong.local.sqlite-contract-min',
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ test('rejects removal of the SQLite rollout compatibility labels', () => {
|
|||||||
const dockerfilePath = path.join(current.target, 'Dockerfile');
|
const dockerfilePath = path.join(current.target, 'Dockerfile');
|
||||||
const dockerfile = fs
|
const dockerfile = fs
|
||||||
.readFileSync(dockerfilePath, 'utf8')
|
.readFileSync(dockerfilePath, 'utf8')
|
||||||
.replace(' io.qinglong.local.sqlite-write-contract="51" \\\n', '');
|
.replace(' io.qinglong.local.sqlite-write-contract="52" \\\n', '');
|
||||||
fs.writeFileSync(dockerfilePath, dockerfile);
|
fs.writeFileSync(dockerfilePath, dockerfile);
|
||||||
const report = auditLocalImageContract(current.root);
|
const report = auditLocalImageContract(current.root);
|
||||||
assert.equal(report.compatible, false);
|
assert.equal(report.compatible, false);
|
||||||
@@ -266,7 +266,7 @@ test('rejects a stale SQLite contract expectation in the native image CI gate',
|
|||||||
const workflow = fs
|
const workflow = fs
|
||||||
.readFileSync(workflowPath, 'utf8')
|
.readFileSync(workflowPath, 'utf8')
|
||||||
.replace(
|
.replace(
|
||||||
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 51 51 1',
|
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 51 52 52 1',
|
||||||
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 50 50 50 1',
|
'EXPECTED: ${{ matrix.image_arch }} 65532:65532 2,3,4 50 50 50 1',
|
||||||
);
|
);
|
||||||
fs.writeFileSync(workflowPath, workflow);
|
fs.writeFileSync(workflowPath, workflow);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
directory: 'packages/ql3-local-sqlite/src/migrations',
|
directory: 'packages/ql3-local-sqlite/src/migrations',
|
||||||
directSourceFiles: 103,
|
directSourceFiles: 105,
|
||||||
reviewKind: 'ordered_ledger',
|
reviewKind: 'ordered_ledger',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -540,10 +540,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
|
rootSourceFileRoles: localSqlite.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 206,
|
sourceFiles: 209,
|
||||||
rootSourceFiles: 1,
|
rootSourceFiles: 1,
|
||||||
rootSourceLines: 31,
|
rootSourceLines: 31,
|
||||||
nestedSourceFiles: 205,
|
nestedSourceFiles: 208,
|
||||||
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
rootSourceFileRoles: { 'index.ts': 'public_export' },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user