@intentius/chant 0.46.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/provenance.test.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
describePathOrigin,
|
|
4
|
+
getPathProvenance,
|
|
5
|
+
getProvenance,
|
|
6
|
+
originOfPath,
|
|
7
|
+
setPathProvenance,
|
|
8
|
+
setProvenance,
|
|
9
|
+
type PathOrigin,
|
|
10
|
+
} from "./provenance";
|
|
11
|
+
import { Composite, expandComposite, propagate } from "./composite";
|
|
4
12
|
import { collectEntities } from "./discovery/collect";
|
|
5
13
|
import { DECLARABLE_MARKER, type Declarable } from "./declarable";
|
|
6
14
|
|
|
7
|
-
const decl = (entityType: string): Declarable =>
|
|
8
|
-
({ [DECLARABLE_MARKER]: true, lexicon: "test", entityType, kind: "resource", props
|
|
15
|
+
const decl = (entityType: string, props: Record<string, unknown> = {}): Declarable =>
|
|
16
|
+
({ [DECLARABLE_MARKER]: true, lexicon: "test", entityType, kind: "resource", props }) as unknown as Declarable;
|
|
9
17
|
|
|
10
18
|
const Pair = Composite<{ n: string }>((props) => ({
|
|
11
19
|
first: decl(`Test::First:${props.n}`),
|
|
@@ -77,3 +85,142 @@ describe("collectEntities stamps the source file", () => {
|
|
|
77
85
|
expect(prov?.composite).toBe("Pair");
|
|
78
86
|
});
|
|
79
87
|
});
|
|
88
|
+
|
|
89
|
+
describe("path provenance storage (#1443)", () => {
|
|
90
|
+
test("first writer wins per path, independently of other paths", () => {
|
|
91
|
+
const e = decl("Test::Thing");
|
|
92
|
+
setPathProvenance(e, "spec.replicas", { kind: "build-param", params: ["tier"] });
|
|
93
|
+
setPathProvenance(e, "spec.replicas", { kind: "authored" });
|
|
94
|
+
setPathProvenance(e, "", { kind: "authored" });
|
|
95
|
+
expect(getPathProvenance(e)).toEqual({
|
|
96
|
+
"": { kind: "authored" },
|
|
97
|
+
"spec.replicas": { kind: "build-param", params: ["tier"] },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("keys come back sorted, whatever order they were written in", () => {
|
|
102
|
+
const e = decl("Test::Thing");
|
|
103
|
+
setPathProvenance(e, "spec.z", { kind: "authored" });
|
|
104
|
+
setPathProvenance(e, "metadata.a", { kind: "authored" });
|
|
105
|
+
setPathProvenance(e, "", { kind: "authored" });
|
|
106
|
+
expect(Object.keys(getPathProvenance(e)!)).toEqual(["", "metadata.a", "spec.z"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("path origins stay off the serialized entity", () => {
|
|
110
|
+
const e = decl("Test::Thing");
|
|
111
|
+
setPathProvenance(e, "spec.replicas", { kind: "authored" });
|
|
112
|
+
expect(JSON.stringify(e)).not.toContain("replicas");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("nothing recorded reads as undefined, not an empty record", () => {
|
|
116
|
+
expect(getPathProvenance(decl("Test::Thing"))).toBeUndefined();
|
|
117
|
+
expect(originOfPath(undefined, "spec.replicas")).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("originOfPath resolution", () => {
|
|
122
|
+
const paths: Record<string, PathOrigin> = {
|
|
123
|
+
"": { kind: "authored" },
|
|
124
|
+
spec: { kind: "composite", composite: "WebService", instance: "web" },
|
|
125
|
+
"spec.template.spec.containers": { kind: "build-param", params: ["image", "tier"] },
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
test("the longest matching prefix wins", () => {
|
|
129
|
+
expect(originOfPath(paths, "spec.template.spec.containers")).toEqual(paths["spec.template.spec.containers"]);
|
|
130
|
+
expect(originOfPath(paths, "spec.replicas")).toEqual(paths.spec);
|
|
131
|
+
expect(originOfPath(paths, "metadata.name")).toEqual(paths[""]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a keyed list element inherits the origin recorded for the list (#1441 grammar)", () => {
|
|
135
|
+
expect(originOfPath(paths, "spec.template.spec.containers[#app].image")).toEqual(
|
|
136
|
+
paths["spec.template.spec.containers"],
|
|
137
|
+
);
|
|
138
|
+
expect(originOfPath(paths, "spec.template.spec.containers[0].image")).toEqual(
|
|
139
|
+
paths["spec.template.spec.containers"],
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("prefixes only match on a segment boundary", () => {
|
|
144
|
+
// `spec` must not claim `specialCase`, and a deeper recorded key must not
|
|
145
|
+
// claim a shallower query.
|
|
146
|
+
expect(originOfPath(paths, "specialCase")).toEqual(paths[""]);
|
|
147
|
+
expect(originOfPath({ "spec.containers.image": { kind: "authored" } }, "spec.containers")).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("describePathOrigin", () => {
|
|
152
|
+
test("renders each kind", () => {
|
|
153
|
+
expect(describePathOrigin({ kind: "authored" })).toBe("authored");
|
|
154
|
+
expect(describePathOrigin({ kind: "composite", composite: "WebService", instance: "web" })).toBe(
|
|
155
|
+
"composite WebService (web)",
|
|
156
|
+
);
|
|
157
|
+
expect(describePathOrigin({ kind: "build-param", params: ["region", "tier"] })).toBe("param region, tier");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("composite expansion records path origins (#1443)", () => {
|
|
162
|
+
test("each member's whole-entity origin names the composite and the instance", () => {
|
|
163
|
+
const expanded = expandComposite("web", Pair({ n: "x" }));
|
|
164
|
+
for (const [, entity] of expanded) {
|
|
165
|
+
expect(originOfPath(getPathProvenance(entity), "anything.at.all")).toEqual({
|
|
166
|
+
kind: "composite",
|
|
167
|
+
composite: "Pair",
|
|
168
|
+
instance: "web",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a nested member keeps the innermost composite but the outer instance", () => {
|
|
174
|
+
const Wrapper = Composite<{ n: string }>((props) => ({
|
|
175
|
+
inner: Pair({ n: props.n }) as unknown as Declarable,
|
|
176
|
+
}), "Wrapper");
|
|
177
|
+
const expanded = expandComposite("stack", Wrapper({ n: "y" }));
|
|
178
|
+
for (const [, entity] of expanded) {
|
|
179
|
+
expect(getPathProvenance(entity)?.[""]).toEqual({
|
|
180
|
+
kind: "composite",
|
|
181
|
+
composite: "Pair",
|
|
182
|
+
instance: "stack",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a propagated key the member never set is attributed to the instance", () => {
|
|
188
|
+
const Tagged = Composite<{ n: string }>(() => ({
|
|
189
|
+
only: decl("Test::Only", { name: "fixed" }),
|
|
190
|
+
}), "Tagged");
|
|
191
|
+
const expanded = expandComposite("env", propagate(Tagged({ n: "x" }), { tags: [{ key: "env", value: "prod" }] }));
|
|
192
|
+
const entity = expanded.get("envOnly")!;
|
|
193
|
+
expect(getPathProvenance(entity)?.tags).toEqual({
|
|
194
|
+
kind: "composite",
|
|
195
|
+
composite: "Tagged",
|
|
196
|
+
instance: "env",
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a key both the member and the propagation wrote is left to the whole-entity origin", () => {
|
|
201
|
+
const Tagged = Composite<{ n: string }>(() => ({
|
|
202
|
+
only: decl("Test::Only", { tags: [{ key: "own", value: "1" }] }),
|
|
203
|
+
}), "Tagged");
|
|
204
|
+
const expanded = expandComposite("env", propagate(Tagged({ n: "x" }), { tags: [{ key: "env", value: "prod" }] }));
|
|
205
|
+
const paths = getPathProvenance(expanded.get("envOnly")!);
|
|
206
|
+
expect(paths?.tags).toBeUndefined();
|
|
207
|
+
expect(paths?.[""]).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("collectEntities records the authored origin", () => {
|
|
212
|
+
test("a directly exported declarable is authored at the root", () => {
|
|
213
|
+
const a = decl("Test::A");
|
|
214
|
+
const entities = collectEntities([{ file: "/proj/src/infra.ts", exports: { a } }]);
|
|
215
|
+
expect(getPathProvenance(entities.get("a")!)?.[""]).toEqual({ kind: "authored" });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a composite member is not relabelled authored", () => {
|
|
219
|
+
const entities = collectEntities([{ file: "/proj/src/pipe.ts", exports: { p: Pair({ n: "z" }) } }]);
|
|
220
|
+
expect(getPathProvenance(entities.get("pFirst")!)?.[""]).toEqual({
|
|
221
|
+
kind: "composite",
|
|
222
|
+
composite: "Pair",
|
|
223
|
+
instance: "p",
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
package/src/provenance.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* entity object, so it never serializes into the emitted YAML/JSON — it is build
|
|
6
6
|
* metadata, not declared configuration. Read it back with {@link getProvenance}.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Entity-level provenance (which file declared it, and which composite expanded
|
|
9
|
+
* it) answers "where did this resource come from?", which is the question an
|
|
10
|
+
* agent asks before changing it. {@link PathOrigin} (chant #1443) answers the
|
|
11
|
+
* follow-up — "what set THIS field" — for the paths a build can attribute
|
|
12
|
+
* without guessing. Neither is a YAML-line source map.
|
|
11
13
|
*
|
|
12
14
|
* {@link BuildParamProvenance} (chant #1064) is the other half: not "where did
|
|
13
15
|
* this ENTITY come from" but "what INPUTS was this whole BUILD invoked with" —
|
|
@@ -30,6 +32,58 @@ export interface EntityProvenance {
|
|
|
30
32
|
* collapse a composite to a single node at coarse diagram detail levels (#494).
|
|
31
33
|
*/
|
|
32
34
|
compositeInstance?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Per-path origins within the entity's `props` (chant #1443). Read with
|
|
37
|
+
* {@link originOfPath}, which does the prefix resolution these keys are
|
|
38
|
+
* written for.
|
|
39
|
+
*
|
|
40
|
+
* Keys are dotted property paths only — `spec.template.spec.containers` —
|
|
41
|
+
* with `""` for the whole entity. They never carry an array segment, because
|
|
42
|
+
* a producing mechanism attributes a whole array at once: the coarsest path
|
|
43
|
+
* it actually determines is the honest one to record, and a positional key
|
|
44
|
+
* would go stale the moment an element moved.
|
|
45
|
+
*
|
|
46
|
+
* Query paths come from #1441's diff addressing and may carry `[#key]` for an
|
|
47
|
+
* element of an associative list or `[n]` for a positional one. Resolution is
|
|
48
|
+
* longest segment-boundary prefix, so `spec.containers[#app].image` finds a
|
|
49
|
+
* key recorded at `spec.containers`, and a key recorded at
|
|
50
|
+
* `spec.containers.image` never matches it. The two grammars agree without
|
|
51
|
+
* the recorder needing the lexicon's ordering hook, which it has no access to.
|
|
52
|
+
*
|
|
53
|
+
* Absent means this build could not record path origins, not that nothing
|
|
54
|
+
* governs the entity's fields — the run path executes modules and has no
|
|
55
|
+
* expression to inspect, and a sandboxed child's provenance does not survive
|
|
56
|
+
* the wire (`discovery/entity-wire-codec.ts` drops build metadata).
|
|
57
|
+
*/
|
|
58
|
+
paths?: Record<string, PathOrigin>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What produced one path inside an entity's props.
|
|
63
|
+
*
|
|
64
|
+
* - `authored` — written in the source file that declared the entity.
|
|
65
|
+
* - `composite` — the entity was expanded by a composite, or the path was
|
|
66
|
+
* supplied by `propagate()`'s shared props at the instance.
|
|
67
|
+
* - `build-param` — the build parameters the authored expression for this path
|
|
68
|
+
* *reads*, which is a syntactic dependency, not a value taint. For
|
|
69
|
+
* `replicas: params.tier === "prod" ? 5 : 1` a taint says the value came from
|
|
70
|
+
* the literal `5`, which is true and useless; the dependency says the field
|
|
71
|
+
* is governed by `tier`, which is what an editor needs to know. Under-reports
|
|
72
|
+
* (a shape the collector does not follow drops a dependency) and never
|
|
73
|
+
* over-reports a parameter the expression does not mention.
|
|
74
|
+
*/
|
|
75
|
+
export type PathOrigin =
|
|
76
|
+
| { kind: "authored" }
|
|
77
|
+
| { kind: "composite"; composite: string; instance: string }
|
|
78
|
+
| { kind: "build-param"; params: string[] };
|
|
79
|
+
|
|
80
|
+
/** True when `prefix` addresses `path` or an ancestor of it, on a segment boundary. */
|
|
81
|
+
function isPathPrefix(prefix: string, path: string): boolean {
|
|
82
|
+
if (prefix === "") return true;
|
|
83
|
+
if (!path.startsWith(prefix)) return false;
|
|
84
|
+
if (path.length === prefix.length) return true;
|
|
85
|
+
const next = path[prefix.length];
|
|
86
|
+
return next === "." || next === "[";
|
|
33
87
|
}
|
|
34
88
|
|
|
35
89
|
/**
|
|
@@ -44,10 +98,16 @@ export function setProvenance(entity: object, prov: EntityProvenance): void {
|
|
|
44
98
|
existing.sourceFile ??= prov.sourceFile;
|
|
45
99
|
existing.composite ??= prov.composite;
|
|
46
100
|
existing.compositeInstance ??= prov.compositeInstance;
|
|
101
|
+
if (prov.paths) {
|
|
102
|
+
existing.paths ??= {};
|
|
103
|
+
for (const [path, origin] of Object.entries(prov.paths)) {
|
|
104
|
+
existing.paths[path] ??= origin;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
47
107
|
return;
|
|
48
108
|
}
|
|
49
109
|
Object.defineProperty(entity, PROVENANCE, {
|
|
50
|
-
value: { ...prov },
|
|
110
|
+
value: { ...prov, ...(prov.paths ? { paths: { ...prov.paths } } : {}) },
|
|
51
111
|
enumerable: false,
|
|
52
112
|
writable: true,
|
|
53
113
|
configurable: true,
|
|
@@ -59,6 +119,60 @@ export function getProvenance(entity: object): EntityProvenance | undefined {
|
|
|
59
119
|
return (entity as Record<symbol, unknown>)[PROVENANCE] as EntityProvenance | undefined;
|
|
60
120
|
}
|
|
61
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Record one path's origin, first writer wins — the same rule
|
|
124
|
+
* {@link setProvenance} applies to entity-level fields, and for the same
|
|
125
|
+
* reason: expansion runs innermost-composite-first, so the earliest writer is
|
|
126
|
+
* the most specific one.
|
|
127
|
+
*/
|
|
128
|
+
export function setPathProvenance(entity: object, path: string, origin: PathOrigin): void {
|
|
129
|
+
setProvenance(entity, { paths: { [path]: origin } });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* An entity's recorded path origins in sorted key order, or `undefined` when
|
|
134
|
+
* none were recorded. Sorted because a build's outputs are compared byte for
|
|
135
|
+
* byte and object key order is not something insertion should decide.
|
|
136
|
+
*/
|
|
137
|
+
export function getPathProvenance(entity: object): Record<string, PathOrigin> | undefined {
|
|
138
|
+
const paths = getProvenance(entity)?.paths;
|
|
139
|
+
if (!paths) return undefined;
|
|
140
|
+
const keys = Object.keys(paths).sort();
|
|
141
|
+
if (keys.length === 0) return undefined;
|
|
142
|
+
const out: Record<string, PathOrigin> = {};
|
|
143
|
+
for (const key of keys) out[key] = paths[key];
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The origin governing `path` — the longest recorded key that addresses it or
|
|
149
|
+
* one of its ancestors. See {@link PROVENANCE_PATH_GRAMMAR}.
|
|
150
|
+
*/
|
|
151
|
+
export function originOfPath(
|
|
152
|
+
paths: Readonly<Record<string, PathOrigin>> | undefined,
|
|
153
|
+
path: string,
|
|
154
|
+
): PathOrigin | undefined {
|
|
155
|
+
if (!paths) return undefined;
|
|
156
|
+
let best: string | undefined;
|
|
157
|
+
for (const key of Object.keys(paths)) {
|
|
158
|
+
if (!isPathPrefix(key, path)) continue;
|
|
159
|
+
if (best === undefined || key.length > best.length) best = key;
|
|
160
|
+
}
|
|
161
|
+
return best === undefined ? undefined : paths[best];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One-line rendering of an origin, for diff output. */
|
|
165
|
+
export function describePathOrigin(origin: PathOrigin): string {
|
|
166
|
+
switch (origin.kind) {
|
|
167
|
+
case "authored":
|
|
168
|
+
return "authored";
|
|
169
|
+
case "composite":
|
|
170
|
+
return `composite ${origin.composite} (${origin.instance})`;
|
|
171
|
+
case "build-param":
|
|
172
|
+
return `param ${origin.params.join(", ")}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
62
176
|
/**
|
|
63
177
|
* One resolved build-time parameter (chant #1064, see ../build-params.ts): its
|
|
64
178
|
* final value and which source won it, so a build's parameter inputs are
|
package/src/runtime-adapter.ts
CHANGED
|
@@ -34,8 +34,13 @@ export interface RuntimeAdapter {
|
|
|
34
34
|
readonly hashAlgorithm: string;
|
|
35
35
|
/** Test whether filePath matches a glob pattern */
|
|
36
36
|
globMatch(pattern: string, filePath: string): boolean;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Spawn a child process and collect output. When `opts.stdin` is set, it is
|
|
39
|
+
* written to the child's stdin and the stream is closed — the caller never
|
|
40
|
+
* needs to round-trip content through a shell (`sh -c "echo … | cmd"`) just
|
|
41
|
+
* to feed a command that reads from stdin (e.g. `git hash-object --stdin`).
|
|
42
|
+
*/
|
|
43
|
+
spawn(cmd: string[], opts?: { cwd?: string; stdin?: string }): Promise<SpawnResult>;
|
|
39
44
|
/** Commands to use when spawning package manager / executor */
|
|
40
45
|
readonly commands: RuntimeCommands;
|
|
41
46
|
}
|
|
@@ -59,16 +64,32 @@ class NodeRuntimeAdapter implements RuntimeAdapter {
|
|
|
59
64
|
return picomatch(pattern)(filePath);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
async spawn(cmd: string[], opts?: { cwd?: string }): Promise<SpawnResult> {
|
|
67
|
+
async spawn(cmd: string[], opts?: { cwd?: string; stdin?: string }): Promise<SpawnResult> {
|
|
63
68
|
return new Promise((resolve) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const child = execFile(
|
|
70
|
+
cmd[0],
|
|
71
|
+
cmd.slice(1),
|
|
72
|
+
{ cwd: opts?.cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
73
|
+
(err, stdout, stderr) => {
|
|
74
|
+
resolve({
|
|
75
|
+
stdout: stdout ?? "",
|
|
76
|
+
stderr: stderr ?? "",
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
exitCode: err ? (err as any).code ?? 1 : 0,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
// Content is written verbatim (utf-8) — no shell involved, so nothing
|
|
83
|
+
// reinterprets backslash-escape sequences or needs quote-escaping.
|
|
84
|
+
// The error handler is load-bearing: a child that exits without reading
|
|
85
|
+
// stdin (any command that ignores it) makes this write race the pipe
|
|
86
|
+
// teardown, and an unhandled EPIPE 'error' event crashes the process.
|
|
87
|
+
if (child.stdin) {
|
|
88
|
+
child.stdin.on("error", () => {
|
|
89
|
+
/* EPIPE: child exited without consuming stdin — harmless */
|
|
70
90
|
});
|
|
71
|
-
|
|
91
|
+
child.stdin.end(opts?.stdin ?? "");
|
|
92
|
+
}
|
|
72
93
|
});
|
|
73
94
|
}
|
|
74
95
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated-once materialization engine tests (#1829, epic #1365 decisions 3
|
|
3
|
+
* and 6): read-then-write, present means done, mismatch fails loudly naming
|
|
4
|
+
* key names and metadata keys — and no code path returns, logs, or retains
|
|
5
|
+
* the generated material.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { inspect } from "node:util";
|
|
10
|
+
import {
|
|
11
|
+
SecretMaterial,
|
|
12
|
+
SecretContractMismatchError,
|
|
13
|
+
consumeSecretMaterial,
|
|
14
|
+
defaultSecretMaterialGenerator,
|
|
15
|
+
ensureSecretMaterialization,
|
|
16
|
+
type SecretMaterialGenerator,
|
|
17
|
+
type SecretStoreAdapter,
|
|
18
|
+
type SecretStoreDescription,
|
|
19
|
+
} from "./secret-materialization";
|
|
20
|
+
|
|
21
|
+
/** A recognizable plaintext no output may ever contain. */
|
|
22
|
+
const CANARY = "canary-s3kr3t-material";
|
|
23
|
+
|
|
24
|
+
/** Generator minting a per-key canary value, so leak assertions can grep for it. */
|
|
25
|
+
const canaryGenerator: SecretMaterialGenerator = (key) => SecretMaterial.mint(`${CANARY}:${key}`);
|
|
26
|
+
|
|
27
|
+
interface StoredSecret {
|
|
28
|
+
data: Record<string, string>;
|
|
29
|
+
metadata?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* In-memory fake store. `create` consumes each key's material exactly the way
|
|
34
|
+
* a real adapter writes it, so the tests can check the stored bytes without
|
|
35
|
+
* any material ever traveling back through the engine.
|
|
36
|
+
*/
|
|
37
|
+
function fakeStore(seed?: Record<string, StoredSecret>) {
|
|
38
|
+
const secrets = new Map<string, StoredSecret>(Object.entries(seed ?? {}));
|
|
39
|
+
const adapter: SecretStoreAdapter = {
|
|
40
|
+
exists: vi.fn(async (name: string) => secrets.has(name)),
|
|
41
|
+
describe: vi.fn(async (name: string): Promise<SecretStoreDescription> => {
|
|
42
|
+
const secret = secrets.get(name);
|
|
43
|
+
if (!secret) throw new Error(`fake store: no secret "${name}"`);
|
|
44
|
+
return { keys: Object.keys(secret.data), metadata: secret.metadata };
|
|
45
|
+
}),
|
|
46
|
+
create: vi.fn(async (name: string, keys: readonly string[], generate: SecretMaterialGenerator) => {
|
|
47
|
+
const data: Record<string, string> = {};
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
data[key] = consumeSecretMaterial(await generate(key));
|
|
50
|
+
}
|
|
51
|
+
secrets.set(name, { data });
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
return { adapter, secrets };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("ensureSecretMaterialization", () => {
|
|
62
|
+
it("absent: mints once through the adapter and reports created", async () => {
|
|
63
|
+
const { adapter, secrets } = fakeStore();
|
|
64
|
+
const outcome = await ensureSecretMaterialization(
|
|
65
|
+
adapter,
|
|
66
|
+
{ name: "master-key", keys: ["MASTER_SECRETS_KEY"] },
|
|
67
|
+
canaryGenerator,
|
|
68
|
+
);
|
|
69
|
+
expect(outcome).toEqual({ outcome: "created", name: "master-key", keys: ["MASTER_SECRETS_KEY"] });
|
|
70
|
+
expect(adapter.create).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(secrets.get("master-key")?.data).toEqual({
|
|
72
|
+
MASTER_SECRETS_KEY: `${CANARY}:MASTER_SECRETS_KEY`,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("present and matching: stops without any write — a rerun is byte-identical and create never fires twice", async () => {
|
|
77
|
+
const { adapter, secrets } = fakeStore();
|
|
78
|
+
const spec = { name: "master-key", keys: ["a", "b"] };
|
|
79
|
+
|
|
80
|
+
await ensureSecretMaterialization(adapter, spec, canaryGenerator);
|
|
81
|
+
const afterFirstRun = JSON.stringify(secrets.get("master-key"));
|
|
82
|
+
|
|
83
|
+
// Second run of the whole ensure — the fountain-ops e2e shape.
|
|
84
|
+
const outcome = await ensureSecretMaterialization(adapter, spec, canaryGenerator);
|
|
85
|
+
expect(outcome.outcome).toBe("present");
|
|
86
|
+
expect(adapter.create).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(JSON.stringify(secrets.get("master-key"))).toBe(afterFirstRun);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("present with a missing declared key: fails loudly naming the key, never minting over the existing value", async () => {
|
|
91
|
+
const { adapter, secrets } = fakeStore({
|
|
92
|
+
"master-key": { data: { a: "pre-existing-value" } },
|
|
93
|
+
});
|
|
94
|
+
await expect(
|
|
95
|
+
ensureSecretMaterialization(adapter, { name: "master-key", keys: ["a", "b"] }, canaryGenerator),
|
|
96
|
+
).rejects.toThrowError(SecretContractMismatchError);
|
|
97
|
+
await expect(
|
|
98
|
+
ensureSecretMaterialization(adapter, { name: "master-key", keys: ["a", "b"] }, canaryGenerator),
|
|
99
|
+
).rejects.toThrowError(/missing declared key\(s\): b/);
|
|
100
|
+
expect(adapter.create).not.toHaveBeenCalled();
|
|
101
|
+
expect(secrets.get("master-key")?.data).toEqual({ a: "pre-existing-value" });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("present with an undeclared extra key: fails naming the extra key", async () => {
|
|
105
|
+
const { adapter } = fakeStore({
|
|
106
|
+
"master-key": { data: { a: "x", rogue: "y" } },
|
|
107
|
+
});
|
|
108
|
+
await expect(
|
|
109
|
+
ensureSecretMaterialization(adapter, { name: "master-key", keys: ["a"] }, canaryGenerator),
|
|
110
|
+
).rejects.toThrowError(/undeclared key\(s\) present: rogue/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("declared metadata mismatch: fails naming the metadata KEY only, never its values", async () => {
|
|
114
|
+
const { adapter } = fakeStore({
|
|
115
|
+
"master-key": {
|
|
116
|
+
data: { a: "x" },
|
|
117
|
+
metadata: { "chant.dev/provenance": "hand-rolled" },
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
let thrown: unknown;
|
|
121
|
+
try {
|
|
122
|
+
await ensureSecretMaterialization(
|
|
123
|
+
adapter,
|
|
124
|
+
{
|
|
125
|
+
name: "master-key",
|
|
126
|
+
keys: ["a"],
|
|
127
|
+
metadata: { "chant.dev/provenance": "generated-once", "chant.dev/stack": "fountain" },
|
|
128
|
+
},
|
|
129
|
+
canaryGenerator,
|
|
130
|
+
);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
thrown = e;
|
|
133
|
+
}
|
|
134
|
+
expect(thrown).toBeInstanceOf(SecretContractMismatchError);
|
|
135
|
+
const message = (thrown as Error).message;
|
|
136
|
+
expect(message).toContain("metadata key differs: chant.dev/provenance");
|
|
137
|
+
expect(message).toContain("missing declared metadata key: chant.dev/stack");
|
|
138
|
+
// The KEY is named; neither the declared nor the stored VALUE appears.
|
|
139
|
+
expect(message).not.toContain("hand-rolled");
|
|
140
|
+
expect(message).not.toContain("generated-once");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("rejects an empty key-set — there is nothing to materialize", async () => {
|
|
144
|
+
const { adapter } = fakeStore();
|
|
145
|
+
await expect(
|
|
146
|
+
ensureSecretMaterialization(adapter, { name: "master-key", keys: [] }, canaryGenerator),
|
|
147
|
+
).rejects.toThrowError(/at least one key/);
|
|
148
|
+
expect(adapter.exists).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("never emits the generated material on any log or error surface", async () => {
|
|
152
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
153
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
154
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
155
|
+
const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
156
|
+
const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
157
|
+
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
158
|
+
|
|
159
|
+
const { adapter } = fakeStore({ mismatched: { data: { wrong: "stored-value" } } });
|
|
160
|
+
await ensureSecretMaterialization(adapter, { name: "fresh", keys: ["k1", "k2"] }, canaryGenerator);
|
|
161
|
+
const failure = await ensureSecretMaterialization(
|
|
162
|
+
adapter,
|
|
163
|
+
{ name: "mismatched", keys: ["right"] },
|
|
164
|
+
canaryGenerator,
|
|
165
|
+
).catch((e: Error) => e);
|
|
166
|
+
|
|
167
|
+
const captured = [logSpy, errorSpy, warnSpy, infoSpy, stdoutSpy, stderrSpy]
|
|
168
|
+
.flatMap((spy) => spy.mock.calls)
|
|
169
|
+
.map((call) => call.map((arg) => inspect(arg)).join(" "))
|
|
170
|
+
.join("\n");
|
|
171
|
+
expect(captured).not.toContain(CANARY);
|
|
172
|
+
expect((failure as Error).message).not.toContain(CANARY);
|
|
173
|
+
expect((failure as Error).message).not.toContain("stored-value");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("SecretMaterial", () => {
|
|
178
|
+
it("holds the plaintext out of reach: enumeration, JSON, string coercion, and inspect all redact", () => {
|
|
179
|
+
const material = SecretMaterial.mint(CANARY);
|
|
180
|
+
expect(Object.keys(material)).toEqual([]);
|
|
181
|
+
expect(JSON.stringify(material)).toBe('"[secret material]"');
|
|
182
|
+
expect(String(material)).toBe("[secret material]");
|
|
183
|
+
expect(inspect(material)).toBe("[secret material]");
|
|
184
|
+
expect(inspect({ nested: material })).not.toContain(CANARY);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is consumable exactly once — the adapter takes it, nothing re-reads it", () => {
|
|
188
|
+
const material = SecretMaterial.mint(CANARY);
|
|
189
|
+
expect(consumeSecretMaterial(material)).toBe(CANARY);
|
|
190
|
+
expect(() => consumeSecretMaterial(material)).toThrowError(/already consumed/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("default generator mints 32 CSPRNG bytes as base64url", () => {
|
|
194
|
+
const material = defaultSecretMaterialGenerator("any-key");
|
|
195
|
+
expect(material).toBeInstanceOf(SecretMaterial);
|
|
196
|
+
const plaintext = consumeSecretMaterial(material as SecretMaterial);
|
|
197
|
+
expect(plaintext).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
198
|
+
});
|
|
199
|
+
});
|