@intentius/chant 0.46.0 → 0.49.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/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.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/plugins.d.ts +1 -1
- package/dist/cli/plugins.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/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.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 +8 -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/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- 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/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +15 -2
- 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.d.ts +44 -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/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- 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/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/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 +12 -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/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/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +98 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -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/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.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 +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- 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 +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- 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/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +80 -0
- package/src/cli/commands/build.ts +106 -8
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +84 -11
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/components/auto-release.ts +6 -0
- 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 +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- 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/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/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +48 -3
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +2 -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/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/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- 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 +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +7 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +139 -1
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- 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 +12 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret provenance vocabulary (chant #1828, epic #1365).
|
|
3
|
+
*
|
|
4
|
+
* A secret's PROVENANCE is where its value comes from — never what the value
|
|
5
|
+
* is. chant's constitutional line on secrets: no code path may hold, log,
|
|
6
|
+
* hash, or compare a secret value. This module makes that structural: the
|
|
7
|
+
* declaration types have no field that could carry material, the factory
|
|
8
|
+
* rejects material-shaped fields at both the type level (`never` fields) and
|
|
9
|
+
* at runtime (own-property check, naming only the offending KEY), and
|
|
10
|
+
* "mismatch" anywhere downstream means presence, declared key-set, and
|
|
11
|
+
* metadata — never a value or a value-derived hash (#1365 decision 6).
|
|
12
|
+
*
|
|
13
|
+
* The closed kind set:
|
|
14
|
+
*
|
|
15
|
+
* - `referenced` — the value exists out of band (a human or an external
|
|
16
|
+
* process put it where consumers read it). chant records only that the
|
|
17
|
+
* estate depends on it, which is what lets the consumed-but-unproduced
|
|
18
|
+
* check (#1382) ship as an error instead of a guess.
|
|
19
|
+
* - `from-provider` — a declared provider binding materializes it (the
|
|
20
|
+
* operator/CRD seam; `K8s::Infisical::InfisicalSecret` from #1321 is the
|
|
21
|
+
* exemplar). The declaration POINTS at that binding, it does not re-model
|
|
22
|
+
* it.
|
|
23
|
+
* - `generated-once` — minted on first materialization, then never
|
|
24
|
+
* regenerated (present means done). The declaration carries contract flags
|
|
25
|
+
* only — e.g. the declared key-set — never mint parameters holding
|
|
26
|
+
* material. `generated-once` secrets never enter the prunable set (#1365
|
|
27
|
+
* decision 5).
|
|
28
|
+
* - `committed-encrypted` — sops-style ciphertext committed in the repo,
|
|
29
|
+
* decrypted by the delivery system (Flux) straight into the target. The
|
|
30
|
+
* declaration records a repo-relative PATH to the ciphertext, never the
|
|
31
|
+
* bytes: the factory is pure and touches no filesystem, so it still folds
|
|
32
|
+
* under `--sandbox`. The bytes are read at `buildRoots()` — the one
|
|
33
|
+
* sanctioned impure seam — and emitted as a sidecar file, never as a
|
|
34
|
+
* document in the primary output. See
|
|
35
|
+
* `docs/design/committed-encrypted-sops-provenance.md`.
|
|
36
|
+
*
|
|
37
|
+
* Declarations are serializer-neutral: discovery collects them like any
|
|
38
|
+
* entity (they are Declarables), but `partitionByLexicon` (../build.ts)
|
|
39
|
+
* excludes them from every serializer partition, so no lexicon ever emits
|
|
40
|
+
* them. They are data that lint rules and lexicons READ (via
|
|
41
|
+
* {@link collectSecretDeclarations}), not output.
|
|
42
|
+
*/
|
|
43
|
+
import { type Declarable } from "./declarable.js";
|
|
44
|
+
/** The closed union of secret origins. */
|
|
45
|
+
export type SecretProvenance = "referenced" | "from-provider" | "generated-once" | "committed-encrypted";
|
|
46
|
+
/** Every provenance kind, for exhaustiveness checks. */
|
|
47
|
+
export declare const SECRET_PROVENANCE_KINDS: readonly SecretProvenance[];
|
|
48
|
+
/**
|
|
49
|
+
* Encryption tools a {@link CommittedEncryptedSecretDeclaration} understands.
|
|
50
|
+
* A closed union with one member, not a free string: a second tool later is a
|
|
51
|
+
* deliberate widening with a detection rule attached, not an unvalidated
|
|
52
|
+
* string that silently means nothing.
|
|
53
|
+
*/
|
|
54
|
+
export type SecretEncryption = "sops";
|
|
55
|
+
/** Every encryption tool, for exhaustiveness checks. */
|
|
56
|
+
export declare const SECRET_ENCRYPTION_TOOLS: readonly SecretEncryption[];
|
|
57
|
+
/** Marker symbol identifying a secret provenance declaration. `Symbol.for` so
|
|
58
|
+
* it survives the entity-wire codec (../discovery/entity-wire-codec.ts). */
|
|
59
|
+
export declare const SECRET_DECLARATION_MARKER: unique symbol;
|
|
60
|
+
/** The `entityType` every secret declaration carries. */
|
|
61
|
+
export declare const SECRET_DECLARATION_ENTITY_TYPE = "Chant::SecretProvenance";
|
|
62
|
+
/**
|
|
63
|
+
* Points a `from-provider` declaration at the provider binding that
|
|
64
|
+
* materializes the secret — the CRD seam, not a re-model of it. The binding
|
|
65
|
+
* itself is an ordinary declarable in its own lexicon (e.g. an
|
|
66
|
+
* `InfisicalSecret` instance, #1321); this ref names it so lint can resolve
|
|
67
|
+
* the pair without core depending on any lexicon.
|
|
68
|
+
*/
|
|
69
|
+
export interface SecretProviderRef {
|
|
70
|
+
/** Entity name (export name) of the provider binding declarable in this project. */
|
|
71
|
+
readonly binding: string;
|
|
72
|
+
/**
|
|
73
|
+
* Expected `entityType` of the binding (e.g.
|
|
74
|
+
* `"K8s::Infisical::InfisicalSecret"`). Optional; when present, lint can
|
|
75
|
+
* verify the named binding is actually the kind of seam the declaration
|
|
76
|
+
* claims.
|
|
77
|
+
*/
|
|
78
|
+
readonly entityType?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fields that would carry secret material, forbidden by construction. Typed
|
|
82
|
+
* as `never` on every factory input so they fail to compile even when the
|
|
83
|
+
* input object was widened or spread (plain excess-property checks only catch
|
|
84
|
+
* object literals). The runtime check in {@link declareSecret} is the same
|
|
85
|
+
* list, for untyped callers.
|
|
86
|
+
*/
|
|
87
|
+
interface NoSecretMaterial {
|
|
88
|
+
readonly value?: never;
|
|
89
|
+
readonly data?: never;
|
|
90
|
+
readonly stringData?: never;
|
|
91
|
+
readonly material?: never;
|
|
92
|
+
readonly plaintext?: never;
|
|
93
|
+
readonly ciphertext?: never;
|
|
94
|
+
}
|
|
95
|
+
/** Fields shared by every secret declaration. */
|
|
96
|
+
interface SecretDeclarationBase extends Declarable {
|
|
97
|
+
readonly [SECRET_DECLARATION_MARKER]: true;
|
|
98
|
+
readonly lexicon: "chant";
|
|
99
|
+
readonly entityType: typeof SECRET_DECLARATION_ENTITY_TYPE;
|
|
100
|
+
/** The secret's name as consumers know it (e.g. the k8s Secret name). */
|
|
101
|
+
readonly name: string;
|
|
102
|
+
/** Which of the closed kind set this declaration is. */
|
|
103
|
+
readonly provenance: SecretProvenance;
|
|
104
|
+
}
|
|
105
|
+
/** A secret whose value exists out of band. */
|
|
106
|
+
export interface ReferencedSecretDeclaration extends SecretDeclarationBase {
|
|
107
|
+
readonly provenance: "referenced";
|
|
108
|
+
/** Where consumers find it — free-form (a namespace, a vault path, a doc link). */
|
|
109
|
+
readonly scope?: string;
|
|
110
|
+
}
|
|
111
|
+
/** A secret a declared provider binding materializes. */
|
|
112
|
+
export interface FromProviderSecretDeclaration extends SecretDeclarationBase {
|
|
113
|
+
readonly provenance: "from-provider";
|
|
114
|
+
readonly provider: SecretProviderRef;
|
|
115
|
+
}
|
|
116
|
+
/** A secret minted on first materialization and never regenerated. */
|
|
117
|
+
export interface GeneratedOnceSecretDeclaration extends SecretDeclarationBase {
|
|
118
|
+
readonly provenance: "generated-once";
|
|
119
|
+
/**
|
|
120
|
+
* The declared key-set of the materialized secret. This is the contract a
|
|
121
|
+
* materializer (#1830) checks for mismatch — presence and keys, never
|
|
122
|
+
* values.
|
|
123
|
+
*/
|
|
124
|
+
readonly keys?: readonly string[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A secret whose ciphertext is committed to the repo and decrypted by the
|
|
128
|
+
* delivery system on the way into the target.
|
|
129
|
+
*
|
|
130
|
+
* The declaration is a POINTER: `file` is a repo-relative path, and nothing
|
|
131
|
+
* here carries bytes. That is what keeps the factory pure — the ciphertext is
|
|
132
|
+
* read at `buildRoots()` and emitted as a sidecar, never inlined into the
|
|
133
|
+
* primary output an applier reads.
|
|
134
|
+
*/
|
|
135
|
+
export interface CommittedEncryptedSecretDeclaration extends SecretDeclarationBase {
|
|
136
|
+
readonly provenance: "committed-encrypted";
|
|
137
|
+
/** Repo-relative path to the committed ciphertext file. */
|
|
138
|
+
readonly file: string;
|
|
139
|
+
/** Encryption tool. Defaults to `"sops"` when omitted. */
|
|
140
|
+
readonly encryption: SecretEncryption;
|
|
141
|
+
/**
|
|
142
|
+
* Public recipient identifiers — age recipients or PGP fingerprints. Public
|
|
143
|
+
* by definition, which is the opposite of material; a private key here is
|
|
144
|
+
* refused by the factory.
|
|
145
|
+
*/
|
|
146
|
+
readonly recipients?: readonly string[];
|
|
147
|
+
/**
|
|
148
|
+
* The declared key-set of the decrypted Secret — the same contract meaning
|
|
149
|
+
* as {@link GeneratedOnceSecretDeclaration.keys}: presence and key names,
|
|
150
|
+
* never values. SOPS leaves key NAMES cleartext, so this is checkable
|
|
151
|
+
* against the file itself.
|
|
152
|
+
*/
|
|
153
|
+
readonly keys?: readonly string[];
|
|
154
|
+
}
|
|
155
|
+
/** A secret provenance declaration — the discriminant is `provenance`. */
|
|
156
|
+
export type SecretDeclaration = ReferencedSecretDeclaration | FromProviderSecretDeclaration | GeneratedOnceSecretDeclaration | CommittedEncryptedSecretDeclaration;
|
|
157
|
+
/** Narrow a declaration to the committed-encrypted kind. */
|
|
158
|
+
export declare function isCommittedEncryptedSecret(decl: SecretDeclaration): decl is CommittedEncryptedSecretDeclaration;
|
|
159
|
+
/** Factory input for a `referenced` secret. */
|
|
160
|
+
export interface ReferencedSecretInput extends NoSecretMaterial {
|
|
161
|
+
readonly name: string;
|
|
162
|
+
readonly provenance: "referenced";
|
|
163
|
+
readonly scope?: string;
|
|
164
|
+
}
|
|
165
|
+
/** Factory input for a `from-provider` secret. */
|
|
166
|
+
export interface FromProviderSecretInput extends NoSecretMaterial {
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly provenance: "from-provider";
|
|
169
|
+
readonly provider: SecretProviderRef;
|
|
170
|
+
}
|
|
171
|
+
/** Factory input for a `generated-once` secret. */
|
|
172
|
+
export interface GeneratedOnceSecretInput extends NoSecretMaterial {
|
|
173
|
+
readonly name: string;
|
|
174
|
+
readonly provenance: "generated-once";
|
|
175
|
+
readonly keys?: readonly string[];
|
|
176
|
+
}
|
|
177
|
+
/** Factory input for a `committed-encrypted` secret. */
|
|
178
|
+
export interface CommittedEncryptedSecretInput extends NoSecretMaterial {
|
|
179
|
+
readonly name: string;
|
|
180
|
+
readonly provenance: "committed-encrypted";
|
|
181
|
+
/**
|
|
182
|
+
* Repo-relative path to the committed ciphertext file — `file`, never
|
|
183
|
+
* `ciphertext`: the declaration points at bytes, it does not carry them,
|
|
184
|
+
* and `ciphertext` is a forbidden material field.
|
|
185
|
+
*/
|
|
186
|
+
readonly file: string;
|
|
187
|
+
/** Encryption tool. Closed union; `"sops"` is its only member today. */
|
|
188
|
+
readonly encryption?: SecretEncryption;
|
|
189
|
+
/** Public recipient identifiers — age recipients or PGP fingerprints. */
|
|
190
|
+
readonly recipients?: readonly string[];
|
|
191
|
+
/** The declared key-set of the decrypted Secret. Names only, never values. */
|
|
192
|
+
readonly keys?: readonly string[];
|
|
193
|
+
}
|
|
194
|
+
export type SecretDeclarationInput = ReferencedSecretInput | FromProviderSecretInput | GeneratedOnceSecretInput | CommittedEncryptedSecretInput;
|
|
195
|
+
/**
|
|
196
|
+
* Declare a secret's provenance. The returned object is a locked Declarable:
|
|
197
|
+
* discovery collects it like any entity, `chant list` shows it, lint and
|
|
198
|
+
* lexicons read it — and no serializer ever emits it.
|
|
199
|
+
*
|
|
200
|
+
* Only the fields the kind defines are copied onto the declaration; anything
|
|
201
|
+
* else on the input — in particular anything that could carry material — is
|
|
202
|
+
* either a compile error ({@link NoSecretMaterial}) or a thrown error here.
|
|
203
|
+
* The thrown message names the offending KEY only, never its value.
|
|
204
|
+
*/
|
|
205
|
+
export declare function declareSecret(input: ReferencedSecretInput): ReferencedSecretDeclaration;
|
|
206
|
+
export declare function declareSecret(input: FromProviderSecretInput): FromProviderSecretDeclaration;
|
|
207
|
+
export declare function declareSecret(input: GeneratedOnceSecretInput): GeneratedOnceSecretDeclaration;
|
|
208
|
+
export declare function declareSecret(input: CommittedEncryptedSecretInput): CommittedEncryptedSecretDeclaration;
|
|
209
|
+
/** Type guard for a secret provenance declaration. */
|
|
210
|
+
export declare function isSecretDeclaration(value: unknown): value is SecretDeclaration;
|
|
211
|
+
/**
|
|
212
|
+
* Extract the secret declarations from a discovered entity map — the read
|
|
213
|
+
* surface for lint rules (#1382) and lexicon materializers (#1830). Keyed by
|
|
214
|
+
* entity name (export name), the same key `DiscoveryResult.entities` uses.
|
|
215
|
+
*/
|
|
216
|
+
export declare function collectSecretDeclarations(entities: ReadonlyMap<string, Declarable>): Map<string, SecretDeclaration>;
|
|
217
|
+
export {};
|
|
218
|
+
//# sourceMappingURL=secret-provenance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-provenance.d.ts","sourceRoot":"","sources":["../src/secret-provenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAElE,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB,GACxB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,qBAAqB,CAAC;AAE1B,wDAAwD;AACxD,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAK9D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,wDAAwD;AACxD,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAAa,CAAC;AAqB7E;4EAC4E;AAC5E,eAAO,MAAM,yBAAyB,eAAyC,CAAC;AAEhF,yDAAyD;AACzD,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,UAAU,gBAAgB;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC;CAC7B;AAWD,iDAAiD;AACjD,UAAU,qBAAsB,SAAQ,UAAU;IAChD,QAAQ,CAAC,CAAC,yBAAyB,CAAC,EAAE,IAAI,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,8BAA8B,CAAC;IAC3D,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;CACvC;AAED,+CAA+C;AAC/C,MAAM,WAAW,2BAA4B,SAAQ,qBAAqB;IACxE,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC;IAClC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,yDAAyD;AACzD,MAAM,WAAW,6BAA8B,SAAQ,qBAAqB;IAC1E,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;CACtC;AAED,sEAAsE;AACtE,MAAM,WAAW,8BAA+B,SAAQ,qBAAqB;IAC3E,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,mCAAoC,SAAQ,qBAAqB;IAChF,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GACzB,2BAA2B,GAC3B,6BAA6B,GAC7B,8BAA8B,GAC9B,mCAAmC,CAAC;AAExC,4DAA4D;AAC5D,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,iBAAiB,GACtB,IAAI,IAAI,mCAAmC,CAE7C;AAED,+CAA+C;AAC/C,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,kDAAkD;AAClD,MAAM,WAAW,uBAAwB,SAAQ,gBAAgB;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;CACtC;AAED,mDAAmD;AACnD,MAAM,WAAW,wBAAyB,SAAQ,gBAAgB;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,wDAAwD;AACxD,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,uBAAuB,GACvB,wBAAwB,GACxB,6BAA6B,CAAC;AAElC;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,GAAG,2BAA2B,CAAC;AACzF,wBAAgB,aAAa,CAAC,KAAK,EAAE,uBAAuB,GAAG,6BAA6B,CAAC;AAC7F,wBAAgB,aAAa,CAAC,KAAK,EAAE,wBAAwB,GAAG,8BAA8B,CAAC;AAC/F,wBAAgB,aAAa,CAC3B,KAAK,EAAE,6BAA6B,GACnC,mCAAmC,CAAC;AA2KvC,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAO9E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GACxC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAMhC"}
|
package/dist/serializer.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface SerializeContext {
|
|
|
18
18
|
* ad-hoc builds (e.g. context tools) that pass no config.
|
|
19
19
|
*/
|
|
20
20
|
config?: Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Effect receipts (#1832, epic #1703) declared in this lexicon's partition.
|
|
23
|
+
* Withheld from the `entities` map by the build: receipts are observe-only
|
|
24
|
+
* to the generic apply path, and the serialized output is what feeds
|
|
25
|
+
* appliers, so a receipt must never enter the apply-bound document. A
|
|
26
|
+
* serializer that wants to render receipts for visibility (#1835) reads
|
|
27
|
+
* them here and puts them anywhere BUT the section an applier writes from;
|
|
28
|
+
* a serializer that ignores this field emits nothing for them, which is the
|
|
29
|
+
* safe default.
|
|
30
|
+
*/
|
|
31
|
+
receipts?: ReadonlyMap<string, Declarable>;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Result of serialization that may include additional files (e.g. nested stack templates).
|
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;OAaG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEtC;;;;;OAKG;IACH,SAAS,CACP,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,OAAO,CAAC,EAAE,aAAa,EAAE,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,gBAAgB,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC;CACpD"}
|
package/dist/yaml.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAwBlD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -91,5 +91,8 @@
|
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@cdktf/hcl2json": "^0.21.0",
|
|
93
93
|
"ajv": "^8.20.0"
|
|
94
|
+
},
|
|
95
|
+
"overrides": {
|
|
96
|
+
"esbuild": "^0.28.1"
|
|
94
97
|
}
|
|
95
98
|
}
|
package/src/audit/core.test.ts
CHANGED
|
@@ -187,4 +187,61 @@ describe("auditFiles", () => {
|
|
|
187
187
|
});
|
|
188
188
|
expect(findings.map((f) => f.checkId)).toEqual(["OK001"]);
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
test("parse-to-graph: an injected entities parser lets entity-reading checks fire per file (#1567)", async () => {
|
|
192
|
+
const entityCheck: PostSynthCheck = {
|
|
193
|
+
id: "ENT001",
|
|
194
|
+
description: "reads ctx.entities, not ctx.outputs",
|
|
195
|
+
check: (ctx) =>
|
|
196
|
+
[...ctx.entities.keys()].map((name) => ({ checkId: "ENT001", severity: "warning" as const, message: `saw ${name}`, entity: name })),
|
|
197
|
+
};
|
|
198
|
+
const findings = await auditFiles(
|
|
199
|
+
[
|
|
200
|
+
{ path: "a.yaml", content: "one", lexicon: "custom" },
|
|
201
|
+
{ path: "b.yaml", content: "two", lexicon: "custom" },
|
|
202
|
+
],
|
|
203
|
+
{
|
|
204
|
+
checksProvider: async () => [entityCheck],
|
|
205
|
+
entitiesProvider: async () => (content) => new Map([[content, { entityType: "T::X" } as never]]),
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
expect(findings.map((f) => `${f.file}:${f.entity}`).sort()).toEqual(["a.yaml:one", "b.yaml:two"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("parse-to-graph merge: the same entity name in two files keeps both — a duplicate check sees the collision cross-file", async () => {
|
|
212
|
+
const dupCheck: PostSynthCheck = {
|
|
213
|
+
id: "DUP001",
|
|
214
|
+
description: "fires when more than one entity is present",
|
|
215
|
+
check: (ctx) => (ctx.entities.size > 1 ? [{ checkId: "DUP001", severity: "error" as const, message: "name declared twice" }] : []),
|
|
216
|
+
};
|
|
217
|
+
const findings = await auditFiles(
|
|
218
|
+
[
|
|
219
|
+
{ path: "a.yaml", content: "x", lexicon: "custom" },
|
|
220
|
+
{ path: "b.yaml", content: "y", lexicon: "custom" },
|
|
221
|
+
],
|
|
222
|
+
{
|
|
223
|
+
checksProvider: async () => [dupCheck],
|
|
224
|
+
// both files declare the entity name "dev" — the merged pass must keep both
|
|
225
|
+
entitiesProvider: async () => () => new Map([["dev", { entityType: "T::X" } as never]]),
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
const dup = findings.find((f) => f.checkId === "DUP001");
|
|
229
|
+
expect(dup).toBeDefined();
|
|
230
|
+
expect(dup!.file).toBe(CROSS_FILE);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("an entities parser that throws contributes no entities and does not abort the audit", async () => {
|
|
234
|
+
const entityCheck: PostSynthCheck = {
|
|
235
|
+
id: "ENT002",
|
|
236
|
+
description: "counts entities",
|
|
237
|
+
check: (ctx) => (ctx.entities.size > 0 ? [{ checkId: "ENT002", severity: "warning" as const, message: "saw entities" }] : []),
|
|
238
|
+
};
|
|
239
|
+
const findings = await auditFiles([{ path: "a.yaml", content: "x", lexicon: "custom" }], {
|
|
240
|
+
checksProvider: async () => [entityCheck],
|
|
241
|
+
entitiesProvider: async () => () => {
|
|
242
|
+
throw new Error("unparseable");
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
expect(findings).toEqual([]);
|
|
246
|
+
});
|
|
190
247
|
});
|
package/src/audit/core.ts
CHANGED
|
Binary file
|
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
|
|
|
9
9
|
* service content-detect all lexicons on Workers.
|
|
10
10
|
*/
|
|
11
11
|
const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
12
|
-
const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm"];
|
|
12
|
+
const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"];
|
|
13
13
|
|
|
14
14
|
describe("detectTemplate modules are edge-bundle clean", () => {
|
|
15
15
|
test("importing every lexicon's /detect pulls in no typescript or plugin.ts", async () => {
|
|
@@ -42,6 +42,29 @@ describe("discoverByDetection (unified, detectTemplate-driven)", () => {
|
|
|
42
42
|
expect(found.some((i) => i.lexicon === "k8s")).toBe(false);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
test("fountain wins over k8s in a mixed repo — each manifest classifies to its own lexicon (#1566)", async () => {
|
|
46
|
+
const plugins = await loadAuditPlugins();
|
|
47
|
+
const found = discoverByDetection(fixture("audit-fountain"), plugins);
|
|
48
|
+
expect(targets(found)).toEqual(["fountain:agents/fleet.yaml", "k8s:k8s/deploy.yaml"]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a multi-document fountain file classifies as one fountain input (no document dropped)", async () => {
|
|
52
|
+
const plugins = await loadAuditPlugins();
|
|
53
|
+
const found = discoverByDetection(fixture("audit-fountain"), plugins);
|
|
54
|
+
const fountain = found.find((i) => i.lexicon === "fountain")!;
|
|
55
|
+
// All three documents ride along in the input's content.
|
|
56
|
+
expect(fountain.content.match(/apiVersion: fountain\.dev\/v1/g)).toHaveLength(3);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("without the fountain plugin, fountain YAML falls to k8s (not-installed convention)", () => {
|
|
60
|
+
const files = [{ path: "fleet.yaml", content: "apiVersion: fountain.dev/v1\nkind: Environment\nmetadata:\n name: dev\n" }];
|
|
61
|
+
const k8s = { name: "k8s", detectTemplate: (d: unknown) => !!d && typeof d === "object" && "apiVersion" in (d as object) && "kind" in (d as object) };
|
|
62
|
+
expect(classifyFiles(files, [k8s]).map((i) => i.lexicon)).toEqual(["k8s"]);
|
|
63
|
+
// with a fountain plugin present, fountain claims it first
|
|
64
|
+
const fountain = { name: "fountain", detectTemplate: (d: unknown) => (d as { apiVersion?: unknown })?.apiVersion === "fountain.dev/v1" };
|
|
65
|
+
expect(classifyFiles(files, [k8s, fountain]).map((i) => i.lexicon)).toEqual(["fountain"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
45
68
|
test("detects a Helm chart as a bundle, not loose manifests", async () => {
|
|
46
69
|
const plugins = await loadAuditPlugins();
|
|
47
70
|
const found = discoverByDetection(fixture("audit-helm"), plugins);
|
|
@@ -80,6 +103,7 @@ describe("lexicon hints for unclaimed files (#1623)", () => {
|
|
|
80
103
|
expect(hintLexiconForFile("charts/x/Chart.yaml", "name: x\n")).toBe("helm");
|
|
81
104
|
expect(hintLexiconForFile("infra/main.tf", "resource {}\n")).toBe("terraform");
|
|
82
105
|
expect(hintLexiconForFile("k8s/deploy.yaml", "apiVersion: apps/v1\nkind: Deployment\n")).toBe("k8s");
|
|
106
|
+
expect(hintLexiconForFile("agents/fleet.yaml", "apiVersion: fountain.dev/v1\nkind: Environment\n")).toBe("fountain");
|
|
83
107
|
expect(hintLexiconForFile("gcp/bucket.yaml", "apiVersion: storage.cnrm.cloud.google.com/v1beta1\nkind: StorageBucket\n")).toBe("gcp");
|
|
84
108
|
expect(hintLexiconForFile("cfn/stack.yaml", "AWSTemplateFormatVersion: '2010-09-09'\nResources: {}\n")).toBe("aws");
|
|
85
109
|
expect(hintLexiconForFile("cfn/stack.json", '{"Resources":{"B":{"Type":"AWS::S3::Bucket"}}}')).toBe("aws");
|
package/src/audit/discover.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* - Helm charts are a directory BUNDLE keyed by `Chart.yaml`; the helm checks
|
|
23
23
|
* read `output.files`, so the whole chart is one AuditInput.
|
|
24
24
|
* - k8s `detectTemplate` matches any `apiVersion`+`kind`, including GCP Config
|
|
25
|
-
* Connector (`cnrm.cloud.google.com`) resources
|
|
25
|
+
* Connector (`cnrm.cloud.google.com`) resources and fountain
|
|
26
|
+
* (`fountain.dev/v1`) manifests, so gcp and fountain must be tried first.
|
|
26
27
|
* - aws/azure checks `JSON.parse` their input, so YAML/templated content is
|
|
27
28
|
* normalized to a JSON string here.
|
|
28
29
|
*/
|
|
@@ -34,7 +35,7 @@ import type { LexiconPlugin } from "../lexicon";
|
|
|
34
35
|
import type { AuditInput, AuditLexicon } from "./core";
|
|
35
36
|
|
|
36
37
|
/** Lexicons the auditor knows how to detect and run checks for. */
|
|
37
|
-
export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm"] as const;
|
|
38
|
+
export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"] as const;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Load the plugins used for detection. Each is loaded in isolation (no
|
|
@@ -170,6 +171,13 @@ const CONTENT_DETECTORS: ContentDetector[] = [
|
|
|
170
171
|
accepts: isYaml,
|
|
171
172
|
detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
|
|
172
173
|
},
|
|
174
|
+
{
|
|
175
|
+
// fountain (`apiVersion: fountain.dev/v1`) — must win over k8s, whose
|
|
176
|
+
// detectTemplate matches any apiVersion+kind document (#1566).
|
|
177
|
+
lexicon: "fountain",
|
|
178
|
+
accepts: isYaml,
|
|
179
|
+
detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
|
|
180
|
+
},
|
|
173
181
|
{
|
|
174
182
|
lexicon: "k8s",
|
|
175
183
|
accepts: isYaml,
|
|
@@ -263,6 +271,7 @@ export function hintLexiconForFile(path: string, content: string): LexiconHint |
|
|
|
263
271
|
if (/\.tf$/i.test(name)) return "terraform";
|
|
264
272
|
const head = content.slice(0, 64 * 1024);
|
|
265
273
|
if (/cnrm\.cloud\.google\.com/.test(head)) return "gcp";
|
|
274
|
+
if (/fountain\.dev\/v1/.test(head)) return "fountain";
|
|
266
275
|
if (/AWSTemplateFormatVersion|["']?Type["']?\s*:\s*["']AWS::/.test(head)) return "aws";
|
|
267
276
|
if (/deploymentTemplate\.json/.test(head)) return "azure";
|
|
268
277
|
if (isYaml(name)) {
|
package/src/audit/rules-doc.ts
CHANGED
|
@@ -20,14 +20,24 @@ const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
|
|
|
20
20
|
{ heading: "Azure ARM (AZR)", prefixes: ["AZR"], blurb: "Run against ARM deployment templates (JSON)." },
|
|
21
21
|
{ heading: "GCP Config Connector (WGC)", prefixes: ["WGC"], blurb: "Run against Config Connector (cnrm.cloud.google.com) manifests." },
|
|
22
22
|
{ heading: "Helm (WHM)", prefixes: ["WHM"], blurb: "Run against Helm charts (Chart.yaml + templates)." },
|
|
23
|
+
{ heading: "fountain (FTN)", prefixes: ["FTN"], blurb: "Run against fountain manifests (`apiVersion: fountain.dev/v1`) — standalone `fountain apply` YAML is parsed back into the entity graph, so the same rules fire on `chant build` and `chant audit`." },
|
|
23
24
|
];
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* MDX treats `{expr}` as a JavaScript expression and `<x` as JSX, so rule
|
|
28
|
+
* text containing literal braces (`${VAR}` substitution references) or angle
|
|
29
|
+
* brackets must be escaped or the docs build fails at render time.
|
|
30
|
+
*/
|
|
31
|
+
function mdxEscape(text: string): string {
|
|
32
|
+
return text.replace(/\{/g, "\\{").replace(/</g, "\\<");
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
function ruleBlock(m: RuleMeta): string {
|
|
26
36
|
const tags = `${m.tier} · ${m.fixKind}`;
|
|
27
37
|
const authority = m.authority?.length
|
|
28
38
|
? `\n\nAuthority: ${m.authority.map((a) => `[${a.name}](${a.url})`).join(" · ")}`
|
|
29
39
|
: "";
|
|
30
|
-
return `### ${m.id}\n\n**${m.title}** — ${tags}\n\n${m.remediation}${authority}`;
|
|
40
|
+
return `### ${m.id}\n\n**${mdxEscape(m.title)}** — ${tags}\n\n${mdxEscape(m.remediation)}${authority}`;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/**
|
package/src/build.test.ts
CHANGED
|
@@ -338,6 +338,47 @@ export const discovered = {
|
|
|
338
338
|
expect(result.entities.get("discovered")?.entityType).toBe("Discovered");
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
+
test("a contributor sees the discovered entities, read-only", async () => {
|
|
342
|
+
await writeFile(
|
|
343
|
+
join(testDir, "app.infra.ts"),
|
|
344
|
+
`
|
|
345
|
+
export const discovered = {
|
|
346
|
+
lexicon: "test",
|
|
347
|
+
entityType: "Discovered",
|
|
348
|
+
props: {},
|
|
349
|
+
[Symbol.for("chant.declarable")]: true,
|
|
350
|
+
};
|
|
351
|
+
`,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
let seenNames: string[] = [];
|
|
355
|
+
const result = await build(testDir, [recordingSerializer([])], undefined, {
|
|
356
|
+
buildRoots: [
|
|
357
|
+
async (ctx) => {
|
|
358
|
+
seenNames = [...(ctx.entities?.keys() ?? [])];
|
|
359
|
+
return { entities: new Map() };
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(result.errors).toEqual([]);
|
|
365
|
+
expect(seenNames).toContain("discovered");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("a later contributor sees what an earlier one contributed", async () => {
|
|
369
|
+
const seen: string[][] = [];
|
|
370
|
+
await build(testDir, [recordingSerializer([])], undefined, {
|
|
371
|
+
buildRoots: [
|
|
372
|
+
async () => ({ entities: new Map([["first", entity("First")]]) }),
|
|
373
|
+
async (ctx) => {
|
|
374
|
+
seen.push([...(ctx.entities?.keys() ?? [])]);
|
|
375
|
+
return { entities: new Map() };
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
expect(seen[0]).toContain("first");
|
|
380
|
+
});
|
|
381
|
+
|
|
341
382
|
test("contributors run exactly once per build", async () => {
|
|
342
383
|
let calls = 0;
|
|
343
384
|
await build(testDir, [], undefined, {
|
package/src/build.ts
CHANGED
|
@@ -2,10 +2,12 @@ import type { Declarable } from "./declarable";
|
|
|
2
2
|
import type { Serializer, SerializerResult } from "./serializer";
|
|
3
3
|
import type { OwnershipMarker } from "./ownership";
|
|
4
4
|
import type { BuildError, DiscoveryErrorType } from "./errors";
|
|
5
|
-
import type { IntrinsicDef, BuildRootContribution } from "./lexicon";
|
|
5
|
+
import type { IntrinsicDef, BuildRootContribution, BuildRootContributor } from "./lexicon";
|
|
6
6
|
import type { BuildParamProvenance } from "./provenance";
|
|
7
7
|
import { DiscoveryError, BuildError as BuildErrorClass } from "./errors";
|
|
8
8
|
import { LexiconOutput, isLexiconOutput } from "./lexicon-output";
|
|
9
|
+
import { isSecretDeclaration } from "./secret-provenance";
|
|
10
|
+
import { splitReceiptEntities } from "./effect-receipt";
|
|
9
11
|
import { AttrRef } from "./attrref";
|
|
10
12
|
import { isAttrRefLike } from "./utils";
|
|
11
13
|
import { isChildProject, type ChildProjectInstance } from "./child-project";
|
|
@@ -112,7 +114,9 @@ export function computeStackGraph(
|
|
|
112
114
|
// Dependency map: node → producers it depends on.
|
|
113
115
|
const nodes = lexiconNames.length
|
|
114
116
|
? [...lexiconNames]
|
|
115
|
-
:
|
|
117
|
+
: // Secret provenance declarations (#1828) never form a stack — their
|
|
118
|
+
// pseudo-lexicon has no serializer and must not appear in the manifest.
|
|
119
|
+
[...new Set([...entities.values()].filter((e) => !isSecretDeclaration(e)).map((e) => e.lexicon))];
|
|
116
120
|
const deps = new Map<string, Set<string>>();
|
|
117
121
|
for (const n of nodes) deps.set(n, new Set());
|
|
118
122
|
for (const { from, to } of edges) {
|
|
@@ -229,7 +233,7 @@ export interface BuildOptions {
|
|
|
229
233
|
* binaries); a contributed name colliding with a discovered entity is a
|
|
230
234
|
* build error, never a silent overwrite.
|
|
231
235
|
*/
|
|
232
|
-
buildRoots?:
|
|
236
|
+
buildRoots?: BuildRootContributor[];
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
export interface BuildResult {
|
|
@@ -294,6 +298,11 @@ export function partitionByLexicon(
|
|
|
294
298
|
for (const [name, entity] of entities) {
|
|
295
299
|
// LexiconOutput instances are collected separately; skip them here
|
|
296
300
|
if (isLexiconOutput(entity)) continue;
|
|
301
|
+
// Secret provenance declarations (#1828) are serializer-neutral: data
|
|
302
|
+
// that lint and lexicons read from the entity map, never output. Keeping
|
|
303
|
+
// them out of every partition means no serializer sees them and no
|
|
304
|
+
// "No serializer found" warning fires for their pseudo-lexicon.
|
|
305
|
+
if (isSecretDeclaration(entity)) continue;
|
|
297
306
|
const lexicon = entity.lexicon;
|
|
298
307
|
if (!partitions.has(lexicon)) {
|
|
299
308
|
partitions.set(lexicon, new Map());
|
|
@@ -590,13 +599,18 @@ export interface BuildRootMergeResult {
|
|
|
590
599
|
*/
|
|
591
600
|
export async function mergeBuildRootEntities(
|
|
592
601
|
entities: Map<string, Declarable>,
|
|
593
|
-
contributors: ReadonlyArray<
|
|
602
|
+
contributors: ReadonlyArray<BuildRootContributor>,
|
|
594
603
|
): Promise<BuildRootMergeResult> {
|
|
595
604
|
const warnings: string[] = [];
|
|
596
605
|
const errors: string[] = [];
|
|
597
606
|
for (const contribute of contributors) {
|
|
598
607
|
try {
|
|
599
|
-
|
|
608
|
+
// The discovered set, read-only, so a contributor can react to what the
|
|
609
|
+
// project DECLARED (a committed-encrypted `declareSecret()` naming a
|
|
610
|
+
// ciphertext file to resolve). Contributors run in order, so this also
|
|
611
|
+
// carries what earlier contributors added; the merge below is still the
|
|
612
|
+
// only writer, which is what keeps the collision refusal meaningful.
|
|
613
|
+
const contribution = await contribute({ entities });
|
|
600
614
|
warnings.push(...(contribution.warnings ?? []));
|
|
601
615
|
for (const [name, entity] of contribution.entities) {
|
|
602
616
|
if (entities.has(name)) {
|
|
@@ -785,20 +799,34 @@ async function buildFromDiscoveryResult(
|
|
|
785
799
|
const outputs = new Map<string, string | SerializerResult>();
|
|
786
800
|
for (const [lexiconName, lexiconEntities] of partitions) {
|
|
787
801
|
const serializer = serializersByName.get(lexiconName);
|
|
802
|
+
// #1832: effect receipts are withheld from the apply-bound entity set at
|
|
803
|
+
// this seam — the narrowest choke point every applier's input flows
|
|
804
|
+
// through, since appliers consume the serialized outputs assembled here.
|
|
805
|
+
// Receipts ride `SerializeContext.receipts` instead, so a lexicon can
|
|
806
|
+
// render them for visibility (#1835) without them ever entering the
|
|
807
|
+
// document an applier writes (or prunes) from. The `effect()` step is the
|
|
808
|
+
// sole receipt writer (epic #1703, decision 3).
|
|
809
|
+
const { applyBound, receipts } = splitReceiptEntities(lexiconEntities);
|
|
788
810
|
if (serializer) {
|
|
789
811
|
const lexiconLexiconOutputs = [
|
|
790
812
|
...(outputsByLexicon.get(lexiconName) ?? []),
|
|
791
813
|
...unassignedOutputs,
|
|
792
814
|
];
|
|
793
|
-
const serialized = serializer.serialize(
|
|
815
|
+
const serialized = serializer.serialize(applyBound, lexiconLexiconOutputs, {
|
|
794
816
|
ownership: options?.ownership,
|
|
795
817
|
config: options?.config,
|
|
818
|
+
...(receipts.size > 0 ? { receipts } : {}),
|
|
796
819
|
});
|
|
797
820
|
// Collect any non-fatal serializer diagnostics into the build warnings.
|
|
798
821
|
if (typeof serialized !== "string" && serialized.warnings) {
|
|
799
822
|
for (const w of serialized.warnings) warnings.push(w);
|
|
800
823
|
}
|
|
801
824
|
outputs.set(lexiconName, serialized);
|
|
825
|
+
} else if (applyBound.size === 0 && receipts.size > 0) {
|
|
826
|
+
// A partition holding only receipts (the core factory's "chant"
|
|
827
|
+
// pseudo-lexicon) has nothing apply-bound to serialize; that is the
|
|
828
|
+
// designed shape until a lexicon materializes the receipt (#1835), not
|
|
829
|
+
// a missing-serializer problem.
|
|
802
830
|
} else {
|
|
803
831
|
warnings.push(`No serializer found for lexicon "${lexiconName}"`);
|
|
804
832
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
apiVersion: fountain.dev/v1
|
|
2
|
+
kind: Environment
|
|
3
|
+
metadata:
|
|
4
|
+
name: dev
|
|
5
|
+
spec:
|
|
6
|
+
networking_type: unrestricted
|
|
7
|
+
env_vars:
|
|
8
|
+
API_URL: https://api.example.com
|
|
9
|
+
AWS_SECRET_ACCESS_KEY: not-a-real-secret
|
|
10
|
+
---
|
|
11
|
+
apiVersion: fountain.dev/v1
|
|
12
|
+
kind: Vault
|
|
13
|
+
metadata:
|
|
14
|
+
name: staging
|
|
15
|
+
spec:
|
|
16
|
+
secrets:
|
|
17
|
+
- key: API_URL
|
|
18
|
+
value: https://staging.example.com
|
|
19
|
+
---
|
|
20
|
+
apiVersion: fountain.dev/v1
|
|
21
|
+
kind: Agent
|
|
22
|
+
metadata:
|
|
23
|
+
name: researcher
|
|
24
|
+
spec:
|
|
25
|
+
model: anthropic/claude-sonnet-4-6
|
|
26
|
+
runtime: claude
|
|
27
|
+
environment: dev
|