@konfig.ts/core 0.0.2 → 0.0.4

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 (2) hide show
  1. package/README.md +61 -160
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,181 +1,82 @@
1
1
  # @konfig.ts/core
2
2
 
3
- The Kubernetes-agnostic primitives that the rest of konfig.ts builds on:
4
- the `Manifest<A>` carrier, the `Dep.*` kinds that drive compile-time
5
- dependency tracking, a stable YAML serializer, structural diff, an Effect
6
- `Schema` boundary helper, and the `Helm.release` integration.
3
+ The Kubernetes-agnostic primitives every other konfig.ts package builds on: the
4
+ `Manifest<A>` carrier, the `Dep.*` kinds that drive compile-time dependency
5
+ tracking, the `Module` factories, stable YAML, structural diff, a Helm
6
+ integration with digest verification, and the `render` entrypoint.
7
7
 
8
- Higher-level surfaces — workloads, services, env contracts, ArgoCD
9
- Application aggregation live in `@konfig.ts/k8s`, `@konfig.ts/env`,
10
- and `@konfig.ts/argocd`.
8
+ Most projects reach for the higher-level packages ([k8s](../k8s), [env](../env),
9
+ [argocd](../argocd)) and touch core directly only for `Helm.release`, `Dep.*`,
10
+ and `Module`.
11
11
 
12
- ## `Manifest<A>` — the carrier
12
+ ## Install
13
13
 
14
- A `Manifest<A>` is a thunk from a `RenderContext` to an Effect that
15
- produces an `A`:
16
-
17
- ```ts
18
- interface Manifest<A> {
19
- readonly render: (
20
- ctx: RenderContext
21
- ) => Effect.Effect<A, AnyRenderError, RenderServices>
22
- }
14
+ ```bash
15
+ bun add @konfig.ts/core
23
16
  ```
24
17
 
25
- `RenderServices` is `FileSystem | Path | ChildProcessSpawner | Scope`.
26
- Errors are a tagged union: `RenderError`, `EmbedYamlReadError`,
27
- `BoundaryDecodeError`, `HelmVersionTooLow`, `HelmRenderError`,
28
- `HelmDigestMismatch`, `CrdExtractError`.
29
-
30
- Constructors:
31
-
32
- | Op | Behaviour |
33
- | ------------------------------------ | ---------------------------------------------- |
34
- | `Manifest.make(run)` | Lift a `ctx → A \| Effect<A>` into a Manifest. |
35
- | `Manifest.combine({ a, b })` | Render `a` and `b` concurrently into a tuple. |
36
- | `Manifest.concat(...ms)` | Flatten an array of Manifests into one. |
37
- | `Manifest.whenever({ cond, thunk })` | Optional inclusion. |
38
- | `Manifest.embedYaml(source)` | Pass through verbatim YAML (file or literal). |
39
-
40
- The `Manifest<A>` itself is not where dependency-tracking happens — it
41
- just carries data and effect failures. The compile-time graph lives
42
- one level up; see below.
43
-
44
- ## Where the type-safety lives
45
-
46
- konfig's dep-graph claim has two boundaries:
18
+ ## Usage
47
19
 
48
- 1. **`AppOfApps.entrypoint(program)`** in `@konfig.ts/argocd`. Every
49
- `Application.define({...})` produces an Effect `Layer.Layer<Out, _, In>`
50
- that propagates the `Dep.Provide<K, N>` it produces in `Out` and the
51
- `Dep.Need<K, N>` it consumes in `In`. Compose with `Layer.provideMerge`,
52
- pass to `entrypoint`, and TypeScript checks `In === never`. A missing
53
- provider is a compile error — see
54
- [`examples/full-stack/infra/envs/broken.ts`](../../examples/full-stack/infra/envs/broken.ts).
55
-
56
- 2. **`Environment.bind({ env, secrets })`** in `@konfig.ts/env`. The
57
- bundle's secret atoms describe `{ envName, secret, key }` triples; the
58
- `bind` checks at the type level that every secret has a backend, the
59
- backend's `keys` set matches the bundle's keys, and a `requiresSource`
60
- backend has a `source` of the right key shape.
61
-
62
- Inside a single `Application.define({ build })` Effect, dep tracking is
63
- declarative: when you `yield* Dep.Secret("ghcr-pull")` to obtain a
64
- `SecretRef<"ghcr-pull">`, you add the `Need` to the requirements. Forget
65
- to yield, and nothing complains — the system catches misses at
66
- composition, not inside an Application's body.
67
-
68
- ## `Dep.*` — tracked kinds
20
+ Pull a digest-verified Helm chart inside a reusable module:
69
21
 
70
22
  ```ts
71
- import { Dep } from "@konfig.ts/core"
23
+ import { Application } from "@konfig.ts/argocd"
24
+ import { Dep, Helm, Module } from "@konfig.ts/core"
25
+ import { Namespace } from "@konfig.ts/k8s"
26
+
27
+ export const definePostgres = Module.fixedNs({
28
+ target: Application.target,
29
+ namespace: "data",
30
+ build: ({ namespace }, opts: { storageGi: number }) => [
31
+ Namespace.make({ name: namespace }),
32
+ Helm.release({
33
+ repo: "https://charts.bitnami.com/bitnami",
34
+ chart: "postgresql",
35
+ version: "16.0.0",
36
+ digest: "sha256:…", // verified after pull AND on every cache hit
37
+ namespace,
38
+ values: { primary: { persistence: { size: `${opts.storageGi}Gi` } } }
39
+ })
40
+ ]
41
+ })
72
42
  ```
73
43
 
74
- | Kind | Service | Provided value |
75
- | --------------------------------- | ------------------------------ | ----------------------------------------- |
76
- | `Dep.App<Name>(name)` | `Need<"App", Name>` | `Application` record |
77
- | `Dep.Application<Name>(name)` | `Need<"Application", Name>` | `Name` literal |
78
- | `Dep.Namespace<Name>(name)` | `Need<"Namespace", Name>` | `Name` literal |
79
- | `Dep.Secret<Name>(name)` | `Need<"Secret", Name>` | `SecretRef<Name>` brand |
80
- | `Dep.SecretValues<Name, K>(name)` | `Need<"SecretValues", Name>` | `{ readonly [k in K]: Redacted<string> }` |
81
- | `Dep.ConfigMap<Name>(name)` | `Need<"ConfigMap", Name>` | `ConfigMapRef<Name>` brand |
82
- | `Dep.ServiceAccount<Name>(name)` | `Need<"ServiceAccount", Name>` | `ServiceAccountRef<Name>` brand |
83
- | `Dep.Pvc<Name>(name)` | `Need<"Pvc", Name>` | `PvcRef<Name>` brand |
84
-
85
- For each kind there's a matching `provideX` helper (`provideSecret`,
86
- `provideNamespace`, …) that emits a `Layer.Layer<Provide<K, N>>`.
87
-
88
- `brand(value)` and `unsafeCoerce(value, reason)` from `@konfig.ts/core`
89
- are escape hatches. `brand` is fine — it's a nominal-typing primitive
90
- for the `*Ref` types. `unsafeCoerce` is the unchecked cast and takes a
91
- mandatory reason string so every call site documents WHY the cast is
92
- sound; prefer the schema boundary below for values crossing a trust
93
- boundary. (`coerce` is deprecated and will be removed before 1.0.)
94
-
95
- ## Stable YAML — `Yaml.serialize({ value })`
96
-
97
- Output rules:
98
-
99
- - Top-level keys ordered: `apiVersion`, `kind`, `metadata`, `spec`,
100
- `status`, then alphabetical.
101
- - Inside `metadata`: `name`, `namespace`, `labels`, `annotations`, then
102
- alphabetical.
103
- - Every other map: alphabetical.
104
- - Lists preserve insertion order.
105
- - LF endings, 2-space indent, single trailing newline.
106
- - YAML 1.1 explicit (kubectl/ArgoCD compatibility).
107
-
108
- `Yaml.filenameFor(resource)` returns `<Kind>-<metadata.name>.yaml` —
109
- deterministic per-resource filenames for ArgoCD-friendly diffs.
110
-
111
- ## Structural diff — `diffFiles({ left, right })`
112
-
113
- Each file's YAML is parsed and redacted (Helm-volatile metadata stripped)
114
- before deep-equality. Output is `Map<filename, FileDiff>`, formatted via
115
- `formatDiff(result, "summary" | "detail" | "json")`. Today's diff is
116
- two-way only and treats each file as a single document; richer per-doc
117
- and anchor-aware diffing is on the roadmap.
118
-
119
- ## `Helm.release({...})`
120
-
121
- Calls `helm pull` and `helm template`, lifts each emitted YAML document
122
- as a `RawYaml` Manifest under the parent Application. SHA-256 of the
123
- `.tgz` is verified against `opts.digest` after pull **and** on every
124
- cache hit — flipping a byte in a cached tarball fails the next render
125
- with `HelmDigestMismatch`.
126
-
127
- `opts.digest` must include the `sha256:` prefix; the cache file name
128
- includes the first 12 hex digits, but the verifier compares the full
129
- hash.
130
-
131
- ## Boundary decode — `boundary({ schema, label })`
132
-
133
- Wraps `Schema.decodeUnknownEffect` so that any decode failure becomes
134
- a `BoundaryDecodeError` with `schema: label`. Use at the seams where
135
- untyped input enters a module:
44
+ Inside a build, `yield* Dep.Secret("ghcr-pull")` records a typed dependency that
45
+ another module must provide the graph is checked when you compose everything
46
+ at `AppOfApps.entrypoint` (see [`@konfig.ts/argocd`](../argocd)).
136
47
 
137
- ```ts
138
- const decodeApiOptions = boundary({ schema: ApiOptions, label: "api" })
139
- const cfg = yield * decodeApiOptions(input)
140
- ```
48
+ ## Surface
141
49
 
142
- Every place that previously did `coerce<T>(YAML.parse(stdout))` over
143
- untrusted external output is now expected to use this helper.
50
+ | Area | Exports |
51
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
52
+ | Manifest | `Manifest` (`.make` / `.combine` / `.concat` / `.whenever` / `.embedYaml`), `render`, `renderManifest`, `RenderContext` |
53
+ | Deps | `Dep.*` kinds + `Dep.provide*` helpers; branded `SecretRef` / `ConfigMapRef` / `PvcRef` / `ServiceAccountRef` / `BuiltImageRef` |
54
+ | Modules | `Module.fixedNs`, `Module.dynamicNs` |
55
+ | Helm | `Helm.release` — chart pull + SHA-256 digest verification |
56
+ | YAML & diff | `Yaml.serialize` / `Yaml.filenameFor`; `diffFiles`, `formatDiff`, `parseYaml`, `redact` |
57
+ | Config | `KonfigConfig`, `ImagesConfig`, and their decoders |
58
+ | Boundaries | `boundary` (Schema decode → `BoundaryDecodeError`); `brand` / `unsafeCoerce` escape hatches |
59
+ | Errors | the tagged union `AnyRenderError` (`HelmDigestMismatch`, `BoundaryDecodeError`, …) |
144
60
 
145
- ## Layout
61
+ ## Internals
146
62
 
147
- ```
148
- src/
149
- ├── index.ts barrel
150
- ├── _cast.ts brand + coerce (escape hatch)
151
- ├── boundary.ts Schema.decode wrapper → BoundaryDecodeError
152
- ├── deps.ts Dep.* kinds + provide* helpers
153
- ├── diff.ts structural diff + redaction
154
- ├── Helm.ts Helm.release with digest verification
155
- ├── images.ts ImagesConfig + decoders
156
- ├── konfigConfig.ts KonfigConfig schema (top-level config)
157
- ├── Manifest.ts Manifest interface + combinators
158
- ├── render.ts render entrypoint
159
- ├── RenderContext.ts threaded through every render
160
- ├── RenderError.ts tagged error union
161
- ├── types.ts Kind enum (legacy; superseded by Dep.*)
162
- └── yaml/
163
- ├── index.ts
164
- └── serialize.ts stable YAML serializer + filenameFor
165
- ```
63
+ `Manifest<A>` is only a recipe from a `RenderContext` to an Effect that produces
64
+ an `A` — it does not track deps in its own type. Per-kind dependency tracking
65
+ lives one level up, in the Effect `Layer`s that `Module` and
66
+ `Application.define` compose. See
67
+ [`.docs/architecture.md`](../../.docs/architecture.md).
166
68
 
167
69
  ## Requirements
168
70
 
169
- konfig.ts builds on [Effect](https://effect.website/), which is still in
170
- beta. Until Effect ships a stable 4.x, you must install the exact beta
171
- konfig is developed against:
71
+ konfig.ts is built on [Effect](https://effect.website/), currently in beta.
72
+ Until Effect ships a stable 4.x, install the exact beta konfig.ts is built
73
+ against:
172
74
 
173
- - **`effect@4.0.0-beta.70`** — required.
174
- - **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
175
- (the Node filesystem/subprocess entrypoint); manifest-only consumers can
176
- omit it.
75
+ - **`effect@4.0.0-beta.70`** — required by every package.
76
+ - **`@effect/platform-node@4.0.0-beta.70`** — required only when you call
77
+ `render()` (the Node filesystem/subprocess entrypoint); manifest-only
78
+ consumers can omit it (it is declared as an optional peer).
177
79
 
178
- The peer dependency is pinned to the exact version on purpose: Effect's beta
179
- line makes breaking changes between builds, so a looser range would surface
180
- as `ERESOLVE` install conflicts rather than a working install. This pin will
181
- relax to a caret range once Effect reaches a stable 4.x.
80
+ The pin is exact on purpose: Effect's beta line makes breaking changes between
81
+ builds, so a looser range surfaces as `ERESOLVE` install conflicts. It relaxes
82
+ to a caret range once Effect reaches a stable 4.x.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konfig.ts/core",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Core abstractions (Manifest, RenderContext, Helm, deps) for konfig.ts — typesafe Kubernetes + ArgoCD config in TypeScript.",
5
5
  "license": "MIT",
6
6
  "author": "David Stahl-Gruber",