@intentius/chant-lexicon-k8s 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.
Files changed (156) hide show
  1. package/dist/components/kubectl-apply.d.ts +5 -0
  2. package/dist/components/kubectl-apply.d.ts.map +1 -1
  3. package/dist/composites/flux-app.d.ts +12 -0
  4. package/dist/composites/flux-app.d.ts.map +1 -1
  5. package/dist/composites/index.d.ts +6 -0
  6. package/dist/composites/index.d.ts.map +1 -1
  7. package/dist/composites/inference-service.d.ts +86 -0
  8. package/dist/composites/inference-service.d.ts.map +1 -0
  9. package/dist/composites/model.d.ts +80 -0
  10. package/dist/composites/model.d.ts.map +1 -0
  11. package/dist/composites/vllm-serving-runtime.d.ts +100 -0
  12. package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
  13. package/dist/crd/crd-sources.d.ts.map +1 -1
  14. package/dist/deep-observe-hooks.d.ts.map +1 -1
  15. package/dist/generated/index.d.ts +167 -40
  16. package/dist/generated/index.d.ts.map +1 -1
  17. package/dist/group-namespace.d.ts.map +1 -1
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/integrity.json +13 -5
  21. package/dist/kube/apply.d.ts.map +1 -1
  22. package/dist/lint/audit-catalog.d.ts.map +1 -1
  23. package/dist/lint/post-synth/index.d.ts.map +1 -1
  24. package/dist/lint/post-synth/k8s-helpers.d.ts +28 -1
  25. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  26. package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
  27. package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
  28. package/dist/lint/post-synth/wk8404.d.ts +12 -0
  29. package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
  30. package/dist/lint/post-synth/wk8405.d.ts +13 -0
  31. package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
  32. package/dist/lint/post-synth/wk8406.d.ts +12 -0
  33. package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
  34. package/dist/lint/post-synth/wk8407.d.ts +21 -0
  35. package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
  36. package/dist/lint/post-synth/wk8503.d.ts +39 -0
  37. package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
  38. package/dist/lint/post-synth/wk8504.d.ts +28 -0
  39. package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
  40. package/dist/lint/post-synth/wk8505.d.ts +28 -0
  41. package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
  42. package/dist/manifest.json +1 -1
  43. package/dist/meta.json +1039 -68
  44. package/dist/okf/index.md +26 -2
  45. package/dist/okf/rules/FLUX002.md +0 -1
  46. package/dist/okf/rules/WK8404.md +11 -0
  47. package/dist/okf/rules/WK8405.md +17 -0
  48. package/dist/okf/rules/WK8406.md +11 -0
  49. package/dist/okf/rules/WK8407.md +15 -0
  50. package/dist/okf/rules/WK8503.md +17 -0
  51. package/dist/okf/rules/WK8504.md +11 -0
  52. package/dist/okf/rules/WK8505.md +15 -0
  53. package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
  54. package/dist/okf/types/AWSManagedCluster.md +14 -0
  55. package/dist/okf/types/AWSManagedControlPlane.md +14 -0
  56. package/dist/okf/types/AWSManagedMachinePool.md +14 -0
  57. package/dist/okf/types/CAPICluster.md +14 -0
  58. package/dist/okf/types/Certificate.md +1 -0
  59. package/dist/okf/types/ClusterResourceSet.md +14 -0
  60. package/dist/okf/types/ClusterServingRuntime.md +14 -0
  61. package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
  62. package/dist/okf/types/DBInstance.md +14 -0
  63. package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
  64. package/dist/okf/types/HelmChartProxy.md +14 -0
  65. package/dist/okf/types/IamRole.md +14 -0
  66. package/dist/okf/types/InferenceService.md +19 -0
  67. package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
  68. package/dist/okf/types/InfisicalSecret.md +4 -0
  69. package/dist/okf/types/Kustomization.md +1 -0
  70. package/dist/okf/types/MachinePool.md +14 -0
  71. package/dist/okf/types/PDB.md +1 -0
  72. package/dist/okf/types/PodIdentityAssociation.md +14 -0
  73. package/dist/okf/types/Policy.md +14 -0
  74. package/dist/okf/types/S3Bucket.md +14 -0
  75. package/dist/okf/types/ServingRuntime.md +14 -0
  76. package/dist/okf/types/StatefulSet.md +1 -0
  77. package/dist/okf/types/User.md +14 -0
  78. package/dist/op/activities/ensure-secret.d.ts +41 -0
  79. package/dist/op/activities/ensure-secret.d.ts.map +1 -0
  80. package/dist/op/activities/index.d.ts +2 -0
  81. package/dist/op/activities/index.d.ts.map +1 -1
  82. package/dist/op/activities/kubectl.d.ts +9 -0
  83. package/dist/op/activities/kubectl.d.ts.map +1 -1
  84. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  85. package/dist/plugin.d.ts.map +1 -1
  86. package/dist/rules/k8s-helpers.ts +55 -1
  87. package/dist/rules/sops-helpers.ts +76 -0
  88. package/dist/rules/wk8404.ts +55 -0
  89. package/dist/rules/wk8405.ts +88 -0
  90. package/dist/rules/wk8406.ts +60 -0
  91. package/dist/rules/wk8407.ts +71 -0
  92. package/dist/rules/wk8503.ts +220 -0
  93. package/dist/rules/wk8504.ts +51 -0
  94. package/dist/rules/wk8505.ts +63 -0
  95. package/dist/secret-labels.d.ts +31 -0
  96. package/dist/secret-labels.d.ts.map +1 -0
  97. package/dist/secret-store.d.ts +73 -0
  98. package/dist/secret-store.d.ts.map +1 -0
  99. package/dist/serializer.d.ts.map +1 -1
  100. package/dist/sops/encrypted-secret-file.d.ts +52 -0
  101. package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
  102. package/dist/sops/entity.d.ts +31 -0
  103. package/dist/sops/entity.d.ts.map +1 -0
  104. package/dist/teardown.d.ts.map +1 -1
  105. package/dist/types/index.d.ts +261 -9
  106. package/package.json +4 -4
  107. package/src/components/kubectl-apply.ts +5 -0
  108. package/src/composites/composites.test.ts +273 -0
  109. package/src/composites/flux-app.ts +23 -0
  110. package/src/composites/index.ts +11 -0
  111. package/src/composites/inference-service.ts +140 -0
  112. package/src/composites/model.ts +150 -0
  113. package/src/composites/vllm-serving-runtime.test.ts +167 -0
  114. package/src/composites/vllm-serving-runtime.ts +196 -0
  115. package/src/crd/cnpg.test.ts +8 -3
  116. package/src/crd/crd-sources.ts +183 -0
  117. package/src/crd/parser.test.ts +14 -0
  118. package/src/deep-observe-hooks.ts +24 -0
  119. package/src/deep-observe.test.ts +89 -0
  120. package/src/generated/index.d.ts +261 -9
  121. package/src/generated/index.ts +167 -40
  122. package/src/generated/lexicon-k8s.json +1039 -68
  123. package/src/generated/operations.json +272 -0
  124. package/src/group-namespace.test.ts +31 -0
  125. package/src/group-namespace.ts +13 -0
  126. package/src/index.ts +6 -0
  127. package/src/kube/apply.ts +5 -0
  128. package/src/lint/audit-catalog.ts +7 -0
  129. package/src/lint/post-synth/index.ts +14 -0
  130. package/src/lint/post-synth/k8s-helpers.ts +55 -1
  131. package/src/lint/post-synth/post-synth.test.ts +826 -0
  132. package/src/lint/post-synth/sops-helpers.ts +76 -0
  133. package/src/lint/post-synth/wk8404.ts +55 -0
  134. package/src/lint/post-synth/wk8405.ts +88 -0
  135. package/src/lint/post-synth/wk8406.ts +60 -0
  136. package/src/lint/post-synth/wk8407.ts +71 -0
  137. package/src/lint/post-synth/wk8503.ts +220 -0
  138. package/src/lint/post-synth/wk8504.ts +51 -0
  139. package/src/lint/post-synth/wk8505.ts +63 -0
  140. package/src/op/activities/ensure-secret.ts +71 -0
  141. package/src/op/activities/index.ts +7 -0
  142. package/src/op/activities/kubectl.test.ts +32 -0
  143. package/src/op/activities/kubectl.ts +28 -5
  144. package/src/op/activities/wait-for-ready.test.ts +34 -0
  145. package/src/op/activities/wait-for-ready.ts +26 -0
  146. package/src/plugin.ts +21 -2
  147. package/src/secret-labels.ts +36 -0
  148. package/src/secret-store.test.ts +184 -0
  149. package/src/secret-store.ts +165 -0
  150. package/src/serializer.ts +33 -2
  151. package/src/sops/encrypted-secret-file.test.ts +333 -0
  152. package/src/sops/encrypted-secret-file.ts +264 -0
  153. package/src/sops/entity.ts +53 -0
  154. package/src/teardown.test.ts +36 -0
  155. package/src/teardown.ts +14 -0
  156. package/src/testdata/sops/db-credentials.sops.yaml +23 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * WK8503: Workload consumes a Secret nothing in the output produces
3
+ *
4
+ * A pod that references a Secret which no manifest in the same build creates
5
+ * passes `chant build`, passes `chant lint`, applies cleanly — and then fails
6
+ * at pod start with `CreateContainerConfigError` (chant #1382). This check
7
+ * closes that gap with the same cross-resource bundle-join ARGO003 uses: every
8
+ * secret reference in a pod spec (envFrom.secretRef, env valueFrom
9
+ * secretKeyRef, secret volumes, projected secret sources, imagePullSecrets)
10
+ * must resolve to a Secret the output produces.
11
+ *
12
+ * Producers are the Secret manifests themselves plus the secret-materializing
13
+ * CRs the lexicon knows: `ExternalSecret` (spec.target.name, defaulting to the
14
+ * CR's own name), `InfisicalSecret` / `InfisicalDynamicSecret`
15
+ * (spec.managedSecretReference.secretName — its secretNamespace is honored
16
+ * separately from the CR's metadata.namespace), and cert-manager
17
+ * `Certificate` (spec.secretName). Namespaces must be compatible; a side with
18
+ * no namespace matches any.
19
+ *
20
+ * The typed waiver is a `SecretProvenance` declaration (chant #1828, epic
21
+ * #1365): a `declareSecret({ name, provenance: "referenced" | "from-provider"
22
+ * | "generated-once" })` covering the secret's name says the estate knows
23
+ * where the value comes from, and silences the check. That is what lets this
24
+ * ship at error severity with no suppression comment — a secret minted out of
25
+ * band is declared, not ignored. References marked `optional: true` are
26
+ * skipped (the author already said it may not exist).
27
+ *
28
+ * `committed-encrypted` is deliberately NOT in that waiver set. The other
29
+ * three kinds are promises about something outside the build; a
30
+ * committed-encrypted declaration is a claim about an artifact inside it, and
31
+ * the build emits that artifact. So it joins the PRODUCER set instead, with
32
+ * its namespace read from the emitted ciphertext (SOPS leaves metadata
33
+ * cleartext) — which means namespace matching applies to it exactly as it
34
+ * does to a literal Secret, and a declaration whose file is broken fails
35
+ * WK8504 rather than silently waiving this check.
36
+ */
37
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
38
+ import { collectSecretDeclarations } from "@intentius/chant/secret-provenance";
39
+ import { extractPodSpec, type K8sManifest } from "./k8s-helpers";
40
+ import { allManifests } from "./argo-helpers";
41
+ import { resolveEncryptedSecretClaims } from "./sops-helpers";
42
+
43
+ /** A Secret some manifest in the output materializes. */
44
+ interface ProducedSecret {
45
+ name: string;
46
+ namespace?: string;
47
+ }
48
+
49
+ /** One secret reference found in a pod spec. */
50
+ interface SecretRef {
51
+ name: string;
52
+ /** Which field referenced it, for the message (e.g. "envFrom.secretRef"). */
53
+ via: string;
54
+ }
55
+
56
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
57
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined;
58
+ }
59
+
60
+ function asArray(value: unknown): unknown[] {
61
+ return Array.isArray(value) ? value : [];
62
+ }
63
+
64
+ /** Collect every Secret the output produces, from literal Secrets and the
65
+ * secret-materializing CR kinds the lexicon ships. */
66
+ function collectProducedSecrets(manifests: K8sManifest[]): ProducedSecret[] {
67
+ const produced: ProducedSecret[] = [];
68
+ for (const m of manifests) {
69
+ const metaName = m.metadata?.name;
70
+ const metaNs = m.metadata?.namespace;
71
+ switch (m.kind) {
72
+ case "Secret": {
73
+ if (typeof metaName === "string") produced.push({ name: metaName, namespace: metaNs });
74
+ break;
75
+ }
76
+ case "ExternalSecret": {
77
+ // external-secrets.io — writes spec.target.name, defaulting to the CR's own name.
78
+ const target = asRecord(m.spec?.target);
79
+ const targetName = typeof target?.name === "string" ? target.name : metaName;
80
+ if (typeof targetName === "string") produced.push({ name: targetName, namespace: metaNs });
81
+ break;
82
+ }
83
+ case "InfisicalSecret":
84
+ case "InfisicalDynamicSecret": {
85
+ // secrets.infisical.com — writes spec.managedSecretReference.secretName;
86
+ // secretNamespace is carried separately from the CR's own namespace.
87
+ const ref = asRecord(m.spec?.managedSecretReference);
88
+ if (typeof ref?.secretName === "string") {
89
+ const ns = typeof ref.secretNamespace === "string" ? ref.secretNamespace : metaNs;
90
+ produced.push({ name: ref.secretName, namespace: ns });
91
+ }
92
+ break;
93
+ }
94
+ case "Certificate": {
95
+ // cert-manager — materializes the keypair into spec.secretName.
96
+ const secretName = m.spec?.secretName;
97
+ if (typeof secretName === "string") produced.push({ name: secretName, namespace: metaNs });
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ return produced;
103
+ }
104
+
105
+ /** Collect every non-optional secret reference in a pod spec. */
106
+ function collectSecretRefs(podSpec: Record<string, unknown>): SecretRef[] {
107
+ const refs: SecretRef[] = [];
108
+
109
+ const containers = [
110
+ ...asArray(podSpec.containers),
111
+ ...asArray(podSpec.initContainers),
112
+ ...asArray(podSpec.ephemeralContainers),
113
+ ];
114
+ for (const c of containers) {
115
+ const container = asRecord(c);
116
+ if (!container) continue;
117
+ for (const e of asArray(container.envFrom)) {
118
+ const secretRef = asRecord(asRecord(e)?.secretRef);
119
+ if (typeof secretRef?.name === "string" && secretRef.optional !== true) {
120
+ refs.push({ name: secretRef.name, via: "envFrom.secretRef" });
121
+ }
122
+ }
123
+ for (const e of asArray(container.env)) {
124
+ const keyRef = asRecord(asRecord(asRecord(e)?.valueFrom)?.secretKeyRef);
125
+ if (typeof keyRef?.name === "string" && keyRef.optional !== true) {
126
+ refs.push({ name: keyRef.name, via: "env[].valueFrom.secretKeyRef" });
127
+ }
128
+ }
129
+ }
130
+
131
+ for (const v of asArray(podSpec.volumes)) {
132
+ const volume = asRecord(v);
133
+ if (!volume) continue;
134
+ const secret = asRecord(volume.secret);
135
+ if (typeof secret?.secretName === "string" && secret.optional !== true) {
136
+ refs.push({ name: secret.secretName, via: "volumes[].secret.secretName" });
137
+ }
138
+ const projected = asRecord(volume.projected);
139
+ for (const s of asArray(projected?.sources)) {
140
+ const source = asRecord(asRecord(s)?.secret);
141
+ if (typeof source?.name === "string" && source.optional !== true) {
142
+ refs.push({ name: source.name, via: "volumes[].projected.sources[].secret" });
143
+ }
144
+ }
145
+ }
146
+
147
+ for (const p of asArray(podSpec.imagePullSecrets)) {
148
+ const pull = asRecord(p);
149
+ if (typeof pull?.name === "string") {
150
+ refs.push({ name: pull.name, via: "imagePullSecrets" });
151
+ }
152
+ }
153
+
154
+ return refs;
155
+ }
156
+
157
+ /** True when a produced Secret satisfies a reference. A side with no
158
+ * namespace matches any — only two explicit, different namespaces miss. */
159
+ function producerCovers(producer: ProducedSecret, name: string, namespace: string | undefined): boolean {
160
+ if (producer.name !== name) return false;
161
+ return producer.namespace === undefined || namespace === undefined || producer.namespace === namespace;
162
+ }
163
+
164
+ export const wk8503: PostSynthCheck = {
165
+ id: "WK8503",
166
+ description:
167
+ "Workload consumes a Secret nothing in the output produces — produce it (Secret, ExternalSecret, InfisicalSecret, Certificate) or declare its provenance with declareSecret()",
168
+
169
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
170
+ const diagnostics: PostSynthDiagnostic[] = [];
171
+ const manifests = allManifests(ctx);
172
+ const produced = collectProducedSecrets(manifests);
173
+
174
+ // Committed ciphertext produces a Secret as surely as a Secret manifest
175
+ // does — Flux decrypts the emitted sidecar straight into the cluster — so
176
+ // it joins the producer set, namespace and all. A claim whose file did
177
+ // not resolve produces nothing: WK8504 reports why, and this check still
178
+ // fires, which is the point of not treating it as a waiver.
179
+ for (const claim of resolveEncryptedSecretClaims(ctx)) {
180
+ if (claim.problems.length > 0) continue;
181
+ produced.push({ name: claim.declaration.name, namespace: claim.document?.namespace });
182
+ }
183
+
184
+ // The typed waiver: a SecretProvenance declaration covering the name, for
185
+ // the three kinds whose value genuinely comes from outside this build.
186
+ const declaredNames = new Set<string>();
187
+ for (const decl of collectSecretDeclarations(ctx.entities).values()) {
188
+ if (decl.provenance === "committed-encrypted") continue;
189
+ declaredNames.add(decl.name);
190
+ }
191
+
192
+ for (const manifest of manifests) {
193
+ const podSpec = extractPodSpec(manifest);
194
+ if (!podSpec) continue;
195
+ const workload = manifest.metadata?.name ?? manifest.kind ?? "workload";
196
+ const namespace = manifest.metadata?.namespace;
197
+
198
+ const reported = new Set<string>();
199
+ for (const ref of collectSecretRefs(podSpec)) {
200
+ if (reported.has(ref.name)) continue;
201
+ if (declaredNames.has(ref.name)) continue;
202
+ if (produced.some((p) => producerCovers(p, ref.name, namespace))) continue;
203
+ reported.add(ref.name);
204
+ diagnostics.push({
205
+ checkId: "WK8503",
206
+ severity: "error",
207
+ message:
208
+ `${manifest.kind} "${workload}" consumes Secret "${ref.name}" (via ${ref.via}), ` +
209
+ `but nothing in the build output produces it — the pod will fail at start. ` +
210
+ `Produce it in this build, or declare its provenance: ` +
211
+ `declareSecret({ name: "${ref.name}", provenance: "referenced" }).`,
212
+ entity: workload,
213
+ lexicon: "k8s",
214
+ });
215
+ }
216
+ }
217
+
218
+ return diagnostics;
219
+ },
220
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WK8504: committed-encrypted secret declaration does not resolve
3
+ *
4
+ * `declareSecret({ provenance: "committed-encrypted", file })` is a claim
5
+ * about an artifact INSIDE the build, which is what separates it from the
6
+ * three kinds that promise something outside one. A claim inside the build
7
+ * should be verified rather than taken on trust, and this is the check that
8
+ * makes it falsifiable: it fires when the declared file produced no sidecar,
9
+ * when the emitted document is not a `v1` Secret of the declared name, when
10
+ * it carries no `sops` block, or when a `data`/`stringData` value is not
11
+ * `ENC[...]`-shaped.
12
+ *
13
+ * That last case is the failure this rule exists for: someone edits
14
+ * `db-credentials.sops.yaml` by hand, forgets `sops -e`, and commits
15
+ * plaintext. Before this, nothing in chant would notice.
16
+ *
17
+ * It reads `output.files`, not `getPrimaryOutput`. The ciphertext is
18
+ * deliberately kept out of the primary output — that is what makes applying
19
+ * an undecrypted Secret structurally impossible — so every other post-synth
20
+ * check in the lexicon is blind to it by construction, and this one has to
21
+ * ask for the sidecars explicitly.
22
+ *
23
+ * A value is only ever tested for the `ENC[` prefix; the message names the
24
+ * offending KEY, never what was found there.
25
+ */
26
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
27
+ import { resolveEncryptedSecretClaims } from "./sops-helpers";
28
+
29
+ export const wk8504: PostSynthCheck = {
30
+ id: "WK8504",
31
+ description:
32
+ "committed-encrypted secret declaration does not resolve — the declared file is missing, is not the named Secret, or is not encrypted",
33
+
34
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
35
+ const diagnostics: PostSynthDiagnostic[] = [];
36
+
37
+ for (const claim of resolveEncryptedSecretClaims(ctx)) {
38
+ for (const problem of claim.problems) {
39
+ diagnostics.push({
40
+ checkId: "WK8504",
41
+ severity: "error",
42
+ message: `declareSecret("${claim.declaration.name}"): ${problem}`,
43
+ entity: claim.declaration.name,
44
+ lexicon: "k8s",
45
+ });
46
+ }
47
+ }
48
+
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * WK8505: committed-encrypted secret with no Flux decryption wiring
3
+ *
4
+ * Flux is the thing that actually decrypts a committed-encrypted secret's
5
+ * sidecar, via `spec.decryption` on the reconciling `Kustomization`
6
+ * (`FluxAppFor`'s `decryption` option). Nothing forces the two calls to be
7
+ * wired together: `declareSecret({ provenance: "committed-encrypted" })` and
8
+ * `FluxAppFor(...)` are independent, so it is easy to add the ciphertext
9
+ * without adding the wiring — the build succeeds, WK8504 passes because the
10
+ * file resolves cleanly, and Flux still applies the sidecar as raw
11
+ * `ENC[...]` bytes because nothing told it to decrypt first.
12
+ *
13
+ * Warning, not error. The reference project shape (`examples/flux-apps`)
14
+ * runs the workload build and the Flux build as separate `chant build`
15
+ * invocations, so "no Kustomization in THIS build sets spec.decryption" can
16
+ * mean "wired up in the other build" as easily as "forgotten" — the design
17
+ * doc's §4 explains why no path-to-build-target join exists to tell those
18
+ * apart. Promote to error if that join ever becomes available.
19
+ *
20
+ * Fires only on claims that actually resolved (`problems.length === 0`, via
21
+ * `resolveEncryptedSecretClaims`), not on every raw declaration. An
22
+ * unresolved declaration is already WK8504's job at error severity; firing
23
+ * this warning on top of it would report a Flux wiring gap for a Secret the
24
+ * build has not confirmed exists.
25
+ */
26
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
27
+ import { allManifests, manifestsOfKind } from "./argo-helpers";
28
+ import type { K8sManifest } from "./k8s-helpers";
29
+ import { resolveEncryptedSecretClaims } from "./sops-helpers";
30
+
31
+ /** Flux Kustomizations only — the kustomize.config.k8s.io kind shares the name. */
32
+ function fluxKustomizations(manifests: K8sManifest[]): K8sManifest[] {
33
+ return manifestsOfKind(manifests, "Kustomization").filter((m) => {
34
+ const apiVersion = m.apiVersion;
35
+ return typeof apiVersion !== "string" || apiVersion.startsWith("kustomize.toolkit.fluxcd.io/");
36
+ });
37
+ }
38
+
39
+ export const wk8505: PostSynthCheck = {
40
+ id: "WK8505",
41
+ description:
42
+ "committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it",
43
+
44
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
+ const kustomizations = fluxKustomizations(allManifests(ctx));
46
+ if (kustomizations.length === 0) return [];
47
+ if (kustomizations.some((k) => k.spec?.decryption !== undefined)) return [];
48
+
49
+ const resolved = resolveEncryptedSecretClaims(ctx).filter((claim) => claim.problems.length === 0);
50
+
51
+ return resolved.map((claim) => ({
52
+ checkId: "WK8505",
53
+ severity: "warning",
54
+ message:
55
+ `declareSecret("${claim.declaration.name}") is committed-encrypted, but no Flux Kustomization ` +
56
+ `in this build sets spec.decryption — Flux will apply "${claim.filename}" without decrypting it. ` +
57
+ `Add decryption: "sops" to the FluxAppFor reconciling the path that carries it, or ignore if that ` +
58
+ `Kustomization is declared in a different build.`,
59
+ entity: claim.declaration.name,
60
+ lexicon: "k8s",
61
+ }));
62
+ },
63
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `generated-once` marker label (chant #1830, epic #1365 decision 5).
3
+ *
4
+ * `k8sSecretStore` (./secret-store.ts) stamps this label onto every Secret it
5
+ * mints, next to the ownership marker. Both k8s prune paths key on it:
6
+ *
7
+ * - the apply-path prune (`applyManifest`'s owned-only sweep,
8
+ * op/activities/kubectl.ts) excludes a labeled Secret from the prunable set
9
+ * and reports it `retained` instead of deleting it;
10
+ * - env teardown (./teardown.ts `executeTeardown`) reports the same `retained`
11
+ * outcome for it, loudly, and never deletes it.
12
+ *
13
+ * The rationale is the materialization contract: the stored bytes are the ONLY
14
+ * copy of material chant never held, so no sweep may destroy them. Deletion is
15
+ * an explicit human act — `kubectl delete secret <name>`, or a future gated
16
+ * op — never a side effect of "no longer declared".
17
+ *
18
+ * Dependency-light on purpose: no client import, so the constants are usable
19
+ * from the build path, the prune paths, and the docs/tests alike.
20
+ */
21
+ /** The label key a generated-once Secret carries. */
22
+ export declare const GENERATED_ONCE_LABEL_KEY = "chant.intentius.io/generated-once";
23
+ /** The label value `k8sSecretStore` stamps. */
24
+ export declare const GENERATED_ONCE_LABEL_VALUE = "true";
25
+ /**
26
+ * True when a live object's labels mark it generated-once. Any non-empty
27
+ * value counts: the label's presence is the claim, and a sweep must err on
28
+ * the side of keeping.
29
+ */
30
+ export declare function isGeneratedOnce(labels: Record<string, unknown> | undefined): boolean;
31
+ //# sourceMappingURL=secret-labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-labels.d.ts","sourceRoot":"","sources":["../src/secret-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,qDAAqD;AACrD,eAAO,MAAM,wBAAwB,sCAAsC,CAAC;AAE5E,+CAA+C;AAC/C,eAAO,MAAM,0BAA0B,SAAS,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CAGpF"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The Kubernetes row of the secret-store seam (chant #1830, epic #1365) — a
3
+ * `SecretStoreAdapter` over the typed client, for the `ensure-secret`
4
+ * capability verb and the `ensureSecret(...)` op step (both #1829, both
5
+ * surfaces over core's one materialization engine).
6
+ *
7
+ * The constitutional line, held structurally:
8
+ *
9
+ * - `exists` and `describe` read the live Secret and report key NAMES and
10
+ * labels/annotations only. The `data` values are projected to their key
11
+ * names inside one function frame and the object is dropped — nothing
12
+ * value-shaped is returned, logged, or retained. The mismatch check in core
13
+ * therefore has nothing to compare but names, by construction.
14
+ * - `create` is the only writer. It redeems each key's material through
15
+ * `consumeSecretMaterial` AT the write — the engine hands the generator
16
+ * through this seam and never calls it — and returns nothing.
17
+ *
18
+ * Every minted Secret carries chant's ownership marker plus the
19
+ * `chant.intentius.io/generated-once` label (./secret-labels.ts), which is
20
+ * what both prune paths key their `retained` outcome on: a generated-once
21
+ * Secret is never in any prunable set, because the stored bytes are the only
22
+ * copy of material chant never held.
23
+ *
24
+ * Not exported from the package entry point — like ./teardown.ts, this module
25
+ * names the API client, which must stay off the build path (chant #1074,
26
+ * examples/k8s-client-boundary.test.ts). Consumers reach it by subpath:
27
+ * `@intentius/chant-lexicon-k8s/secret-store`.
28
+ */
29
+ import type { SecretStoreAdapter } from "@intentius/chant/secret-materialization";
30
+ import { type EnsureSecretInput, type EnsureSecretOutput } from "@intentius/chant/components/verbs/ensure-secret";
31
+ import type { Capability } from "@intentius/chant/components/capability";
32
+ import { type OwnershipMarker } from "@intentius/chant/ownership";
33
+ import type { K8sClient } from "@intentius/chant-k8s-client";
34
+ import { type K8sConnector } from "./api/connect.js";
35
+ export interface K8sSecretStoreOptions {
36
+ /**
37
+ * Ownership identity to stamp on a minted Secret, next to the
38
+ * generated-once label. Omitted stamps `managed-by=chant` alone — the
39
+ * Secret is still chant-owned and still never prunable.
40
+ */
41
+ marker?: OwnershipMarker;
42
+ /** chant environment, resolved through `k8s.profiles.<env>.context` when no client is handed in. */
43
+ environment?: string;
44
+ /** Explicit kubectl context, for the op write path. */
45
+ context?: string;
46
+ /** Project directory whose `chant.config.ts` carries the k8s profiles. Defaults to cwd. */
47
+ cwd?: string;
48
+ /** The connector to build a client with when none is handed in. Test seam. */
49
+ connect?: K8sConnector;
50
+ }
51
+ /**
52
+ * Build the k8s `SecretStoreAdapter` for one namespace.
53
+ *
54
+ * Pass a `client` where the caller already holds one (an activity that
55
+ * connected for the rest of its work); omit it and the adapter connects
56
+ * lazily through the same environment→cluster binding every other k8s path
57
+ * uses (./api/connect.ts).
58
+ */
59
+ export declare function k8sSecretStore(namespace: string, client?: K8sClient, options?: K8sSecretStoreOptions): SecretStoreAdapter;
60
+ /**
61
+ * The real `ensure-secret` capability for a Kubernetes namespace — core's
62
+ * factory (#1829) over this module's adapter. Replaces the starter set's
63
+ * typed stub for a project whose secret store is the cluster:
64
+ *
65
+ * ```ts
66
+ * registry.register(k8sEnsureSecretCapability("prod", undefined, {
67
+ * marker: { stack: "shop", env: "prod" },
68
+ * environment: "prod",
69
+ * }));
70
+ * ```
71
+ */
72
+ export declare function k8sEnsureSecretCapability(namespace: string, client?: K8sClient, options?: K8sSecretStoreOptions): Capability<EnsureSecretInput, EnsureSecretOutput>;
73
+ //# sourceMappingURL=secret-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-store.d.ts","sourceRoot":"","sources":["../src/secret-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAEV,kBAAkB,EAEnB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,iDAAiD,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAKvE,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,SAAS,EAClB,OAAO,GAAE,qBAA0B,GAClC,kBAAkB,CA8DpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,SAAS,EAClB,OAAO,GAAE,qBAA0B,GAClC,UAAU,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,CAEnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA8MlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UA2K3B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA+MlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAyM3B,CAAC"}
@@ -0,0 +1,52 @@
1
+ import { type Declarable } from "@intentius/chant/declarable";
2
+ import type { BuildRootContext, BuildRootContribution } from "@intentius/chant/lexicon";
3
+ import { type CommittedEncryptedSecretDeclaration } from "@intentius/chant/secret-provenance";
4
+ export { ENCRYPTED_SECRET_FILE_MARKER, ENCRYPTED_SECRET_FILE_TYPE, ENCRYPTED_SECRET_ENTITY_PREFIX, isEncryptedSecretFileEntity, sidecarFilename, type EncryptedSecretFileEntity, } from "./entity.js";
5
+ /** A parsed ciphertext document, or the reasons it is not one. */
6
+ export interface EncryptedSecretDocument {
7
+ /** `metadata.name`, when the document had one. */
8
+ name?: string;
9
+ /** `metadata.namespace`, when the document had one. */
10
+ namespace?: string;
11
+ /** Key names found under `data`/`stringData` — names only, never values. */
12
+ keys: string[];
13
+ }
14
+ /**
15
+ * Validate one committed ciphertext document against its declaration.
16
+ *
17
+ * Returns every problem found, so an author sees all of them at once rather
18
+ * than one per build. Both consumers share this: the `buildRoots()` stage
19
+ * turns the list into a refusal, and WK8504 turns it into diagnostics.
20
+ *
21
+ * A value is only ever tested for the `ENC[` prefix, inside this one frame,
22
+ * and only the offending KEY name is reported — the discipline
23
+ * `declareSecret`'s own runtime guard uses.
24
+ */
25
+ export declare function validateEncryptedSecretDocument(text: string, declared: {
26
+ name: string;
27
+ file: string;
28
+ }): {
29
+ problems: string[];
30
+ document?: EncryptedSecretDocument;
31
+ };
32
+ /** Options for {@link resolveEncryptedSecrets} — `readFile` is injectable for tests. */
33
+ export interface ResolveEncryptedSecretsOptions {
34
+ projectRoot: string;
35
+ declarations: ReadonlyMap<string, CommittedEncryptedSecretDeclaration>;
36
+ readFile?: (path: string) => string;
37
+ }
38
+ /**
39
+ * Read and validate every committed-encrypted declaration, returning one
40
+ * entity per resolved file. Throws on the first declaration that does not
41
+ * resolve, with every problem for that declaration in the message — a build
42
+ * that would emit a broken or unencrypted Secret must not proceed.
43
+ */
44
+ export declare function resolveEncryptedSecrets(options: ResolveEncryptedSecretsOptions): Map<string, Declarable>;
45
+ /** Every committed-encrypted declaration in an entity map, keyed by entity name. */
46
+ export declare function committedEncryptedDeclarations(entities: ReadonlyMap<string, Declarable>): Map<string, CommittedEncryptedSecretDeclaration>;
47
+ /**
48
+ * The `buildRoots` contribution: one entity per committed-encrypted
49
+ * declaration, holding the file's bytes verbatim.
50
+ */
51
+ export declare function encryptedSecretBuildRoot(ctx: BuildRootContext): Promise<BuildRootContribution>;
52
+ //# sourceMappingURL=encrypted-secret-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-secret-file.d.ts","sourceRoot":"","sources":["../../src/sops/encrypted-secret-file.ts"],"names":[],"mappings":"AAuCA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAQjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACxF,OAAO,EAGL,KAAK,mCAAmC,EACzC,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EACL,4BAA4B,EAC5B,0BAA0B,EAC1B,8BAA8B,EAC9B,2BAA2B,EAC3B,eAAe,EACf,KAAK,yBAAyB,GAC/B,MAAM,UAAU,CAAC;AAElB,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACtC,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,uBAAuB,CAAA;CAAE,CAiF5D;AAQD,wFAAwF;AACxF,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAAC;IACvE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAmDzB;AAED,oFAAoF;AACpF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,GACxC,GAAG,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAMlD;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAIpG"}
@@ -0,0 +1,31 @@
1
+ import type { Declarable } from "@intentius/chant/declarable";
2
+ /** Marks an entity whose bytes are committed ciphertext, emitted verbatim. */
3
+ export declare const ENCRYPTED_SECRET_FILE_MARKER: unique symbol;
4
+ /**
5
+ * The entity's type name. Deliberately outside the `K8s::{Group}::{Kind}`
6
+ * surface a real Secret speaks: an observer must NOT read this against a live
7
+ * cluster — the ciphertext is not the thing in the cluster, the decrypted
8
+ * Secret is — so a type with no generated operation surface, reporting the
9
+ * honest NOT-OBSERVED, is the right answer.
10
+ */
11
+ export declare const ENCRYPTED_SECRET_FILE_TYPE = "K8s::Sops::EncryptedSecretFile";
12
+ /** Entity-map key prefix, colon-separated so it cannot collide with a TS export name. */
13
+ export declare const ENCRYPTED_SECRET_ENTITY_PREFIX = "sops:";
14
+ export interface EncryptedSecretFileEntity extends Declarable {
15
+ readonly entityType: typeof ENCRYPTED_SECRET_FILE_TYPE;
16
+ readonly [ENCRYPTED_SECRET_FILE_MARKER]: true;
17
+ /** Sidecar filename written beside the primary output. */
18
+ readonly filename: string;
19
+ /** The committed bytes, exactly as read. */
20
+ readonly text: string;
21
+ /** `metadata.name` of the encrypted Secret — equal to the declaration's name. */
22
+ readonly secretName: string;
23
+ /** `metadata.namespace`, cleartext in the file (SOPS encrypts values, not structure). */
24
+ readonly namespace?: string;
25
+ /** The declared repo-relative path, for diagnostics. */
26
+ readonly sourcePath: string;
27
+ }
28
+ export declare function isEncryptedSecretFileEntity(entity: Declarable): entity is EncryptedSecretFileEntity;
29
+ /** The sidecar filename a declaration's file is emitted under. */
30
+ export declare function sidecarFilename(file: string): string;
31
+ //# sourceMappingURL=entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../src/sops/entity.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,8EAA8E;AAC9E,eAAO,MAAM,4BAA4B,eAA8C,CAAC;AAExF;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,mCAAmC,CAAC;AAE3E,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,UAAU,CAAC;AAEtD,MAAM,WAAW,yBAA0B,SAAQ,UAAU;IAC3D,QAAQ,CAAC,UAAU,EAAE,OAAO,0BAA0B,CAAC;IACvD,QAAQ,CAAC,CAAC,4BAA4B,CAAC,EAAE,IAAI,CAAC;IAC9C,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI,yBAAyB,CAEnG;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../src/teardown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EAGlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAIpC,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAIvE,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;CACzB;AAoBD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CA4D9B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,GAAG;IAAE,UAAU,EAAE,iBAAiB,EAAE,CAAA;CAAE,EACjE,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAe5B"}
1
+ {"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../src/teardown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EAGlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAIpC,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAKvE,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;CACzB;AAoBD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CA4D9B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,GAAG;IAAE,UAAU,EAAE,iBAAiB,EAAE,CAAA;CAAE,EACjE,OAAO,GAAE,YAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CAe5B"}