@homeflare/alchemy 0.39.0 → 0.40.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 (207) hide show
  1. package/README.md +10 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +15 -6
  29. package/dist/proxmox/index.js.map +3 -3
  30. package/dist/proxmox/qemu-volume.d.ts +12 -2
  31. package/dist/proxmox/qemu-volume.d.ts.map +1 -1
  32. package/dist/talos/credentials-write.d.ts +46 -0
  33. package/dist/talos/credentials-write.d.ts.map +1 -0
  34. package/dist/talos/credentials.d.ts +51 -9
  35. package/dist/talos/credentials.d.ts.map +1 -1
  36. package/dist/talos/fake-process.d.ts +56 -0
  37. package/dist/talos/fake-process.d.ts.map +1 -0
  38. package/dist/talos/index.d.ts +19 -4
  39. package/dist/talos/index.d.ts.map +1 -1
  40. package/dist/talos/index.js +357 -173
  41. package/dist/talos/index.js.map +16 -10
  42. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  43. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  44. package/dist/talos/kubeconfig.d.ts +41 -23
  45. package/dist/talos/kubeconfig.d.ts.map +1 -1
  46. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  47. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  48. package/dist/talos/machine-config-poll.d.ts +43 -0
  49. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  50. package/dist/talos/machine-config-read.d.ts +49 -0
  51. package/dist/talos/machine-config-read.d.ts.map +1 -0
  52. package/dist/talos/resource.d.ts +18 -0
  53. package/dist/talos/resource.d.ts.map +1 -1
  54. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  55. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  56. package/dist/talos/talos-bootstrap.d.ts +87 -0
  57. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  58. package/dist/talos/talos-cluster-health.d.ts +43 -1
  59. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  60. package/dist/talos/talos-errors.d.ts +68 -0
  61. package/dist/talos/talos-errors.d.ts.map +1 -0
  62. package/dist/talos/talos-machine-config.d.ts +51 -15
  63. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  64. package/dist/talos/talosctl.d.ts +7 -0
  65. package/dist/talos/talosctl.d.ts.map +1 -1
  66. package/dist/talos/values.d.ts +23 -3
  67. package/dist/talos/values.d.ts.map +1 -1
  68. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  69. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  70. package/dist/unifi/acl-rule-form.d.ts +90 -0
  71. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  72. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  73. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  74. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  75. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  76. package/dist/unifi/acl-rule.d.ts +49 -0
  77. package/dist/unifi/acl-rule.d.ts.map +1 -0
  78. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  79. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  80. package/dist/unifi/dns-policy-form.d.ts +97 -0
  81. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  82. package/dist/unifi/dns-policy.d.ts +55 -0
  83. package/dist/unifi/dns-policy.d.ts.map +1 -0
  84. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  85. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  86. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  87. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  89. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  90. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  91. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  92. package/dist/unifi/firewall-policy.d.ts +57 -0
  93. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  94. package/dist/unifi/index.d.ts +7 -0
  95. package/dist/unifi/index.d.ts.map +1 -1
  96. package/dist/unifi/index.js +588 -3
  97. package/dist/unifi/index.js.map +24 -4
  98. package/dist/unifi/paginate.d.ts +0 -12
  99. package/dist/unifi/paginate.d.ts.map +1 -1
  100. package/dist/unifi/providers.d.ts +1 -1
  101. package/dist/unifi/providers.d.ts.map +1 -1
  102. package/dist/unifi/scan-source.d.ts +11 -0
  103. package/dist/unifi/scan-source.d.ts.map +1 -0
  104. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  105. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  106. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  107. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  108. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  109. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  110. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  111. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  112. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  113. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  114. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  115. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  116. package/docs/unifi-acl-rule.md +122 -0
  117. package/docs/unifi-dns-policy.md +75 -0
  118. package/docs/unifi-firewall-policy.md +109 -0
  119. package/docs/unifi-traffic-matching-list.md +82 -0
  120. package/docs/unifi-wifi.md +120 -0
  121. package/docs/unifi.md +10 -5
  122. package/package.json +6 -2
  123. package/src/ceph/auth-entity.ts +74 -0
  124. package/src/ceph/ceph-argv-config.ts +78 -0
  125. package/src/ceph/ceph-argv.test.ts +226 -0
  126. package/src/ceph/ceph-argv.ts +189 -0
  127. package/src/ceph/ceph-auth-form.test.ts +103 -0
  128. package/src/ceph/ceph-auth-form.ts +107 -0
  129. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  130. package/src/ceph/ceph-auth-parse.ts +121 -0
  131. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  132. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  133. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  134. package/src/ceph/ceph-config-options.test.ts +44 -0
  135. package/src/ceph/ceph-config-options.ts +36 -0
  136. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  137. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  138. package/src/ceph/ceph-transport.test.ts +125 -0
  139. package/src/ceph/ceph-transport.ts +117 -0
  140. package/src/ceph/fake-ceph-dial.ts +55 -0
  141. package/src/ceph/index.ts +12 -0
  142. package/src/index.ts +1 -0
  143. package/src/proxmox/qemu-volume.test.ts +153 -4
  144. package/src/proxmox/qemu-volume.ts +71 -14
  145. package/src/talos/credentials-write.test.ts +81 -0
  146. package/src/talos/credentials-write.ts +95 -0
  147. package/src/talos/credentials.test.ts +157 -0
  148. package/src/talos/credentials.ts +160 -90
  149. package/src/talos/fake-process.ts +94 -0
  150. package/src/talos/index.ts +39 -4
  151. package/src/talos/kubeconfig-attrs.ts +55 -0
  152. package/src/talos/kubeconfig.test.ts +256 -0
  153. package/src/talos/kubeconfig.ts +170 -90
  154. package/src/talos/machine-config-fixtures.ts +86 -0
  155. package/src/talos/machine-config-poll.test.ts +57 -0
  156. package/src/talos/machine-config-poll.ts +82 -0
  157. package/src/talos/machine-config-read.ts +118 -0
  158. package/src/talos/resource.ts +19 -0
  159. package/src/talos/secrets-gate.test.ts +63 -0
  160. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  161. package/src/talos/talos-bootstrap.test.ts +218 -0
  162. package/src/talos/talos-bootstrap.ts +151 -45
  163. package/src/talos/talos-cluster-health.test.ts +143 -0
  164. package/src/talos/talos-cluster-health.ts +91 -28
  165. package/src/talos/talos-errors.ts +85 -0
  166. package/src/talos/talos-machine-config.test.ts +189 -0
  167. package/src/talos/talos-machine-config.ts +102 -88
  168. package/src/talos/talosctl.ts +8 -0
  169. package/src/talos/values.test.ts +34 -6
  170. package/src/talos/values.ts +41 -7
  171. package/src/unifi/acl-rule-drift.ts +57 -0
  172. package/src/unifi/acl-rule-form.ts +135 -0
  173. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  174. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  175. package/src/unifi/acl-rule-ordering.ts +70 -0
  176. package/src/unifi/acl-rule.test.ts +183 -0
  177. package/src/unifi/acl-rule.ts +61 -0
  178. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  179. package/src/unifi/dns-policy-drift.ts +48 -0
  180. package/src/unifi/dns-policy-form.ts +142 -0
  181. package/src/unifi/dns-policy.test.ts +154 -0
  182. package/src/unifi/dns-policy.ts +67 -0
  183. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  184. package/src/unifi/firewall-policy-drift.ts +55 -0
  185. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  186. package/src/unifi/firewall-policy-form.ts +146 -0
  187. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  188. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  189. package/src/unifi/firewall-policy-ordering.ts +87 -0
  190. package/src/unifi/firewall-policy.test.ts +183 -0
  191. package/src/unifi/firewall-policy.ts +73 -0
  192. package/src/unifi/index.ts +63 -0
  193. package/src/unifi/paginate.test.ts +24 -0
  194. package/src/unifi/paginate.ts +15 -3
  195. package/src/unifi/providers.ts +19 -1
  196. package/src/unifi/scan-source.ts +64 -0
  197. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  198. package/src/unifi/traffic-matching-list-form.ts +77 -0
  199. package/src/unifi/traffic-matching-list.test.ts +202 -0
  200. package/src/unifi/traffic-matching-list.ts +78 -0
  201. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  202. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  203. package/src/unifi/wifi-broadcast-form.ts +211 -0
  204. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  205. package/src/unifi/wifi-broadcast.test.ts +155 -0
  206. package/src/unifi/wifi-broadcast.ts +90 -0
  207. package/src/unifi/write-op-reference.test.ts +8 -54
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A fake `ChildProcessSpawner` for offline Talos tests — no real `bao` or `talosctl` process ever
3
+ * starts. Shared by credentials.test.ts and talos-machine-config.test.ts (2026-09-26, K-A3);
4
+ * this package had NO process-level test fake before this file — every Talos resource file spawns
5
+ * a real binary in production and none of that was previously exercised offline.
6
+ *
7
+ * ★ A FULL FAKE SERVICE, NOT A REAL SPAWN WITH A FIXTURE EXECUTABLE ON `PATH`. The alternative —
8
+ * a tiny fake `bao`/`talosctl` script and a mutated `process.env.PATH` — spawns a REAL OS
9
+ * process and mutates process-global state for the test's duration, which is exactly the kind
10
+ * of cross-test interference the house rules ask tests to avoid. `ChildProcessSpawner.make`
11
+ * takes just a `spawn` function and derives everything else, so a fake `spawn` is the whole cost.
12
+ */
13
+ import * as Effect from 'effect/Effect';
14
+ import * as Sink from 'effect/Sink';
15
+ import * as Stream from 'effect/Stream';
16
+ import type { Command } from 'effect/unstable/process/ChildProcess';
17
+ import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
18
+
19
+ export type FakeResult = {
20
+ readonly stdout?: string;
21
+ readonly stderr?: string;
22
+ readonly exitCode?: number;
23
+ };
24
+ export type FakeCall = {
25
+ readonly command: string;
26
+ readonly args: readonly string[];
27
+ /**
28
+ * Decoded stdin text, present only when the real caller piped a `Stream` in (K-talos-first-boot,
29
+ * `credentials-write.ts`'s `writeKvValue`) — the whole reason this fake exists is to prove a
30
+ * value went in via stdin and never via argv, so a test needs to see it.
31
+ */
32
+ readonly stdin?: string;
33
+ };
34
+ export type FakeHandler = (call: FakeCall) => FakeResult;
35
+
36
+ const encode = (text: string) => new TextEncoder().encode(text);
37
+
38
+ const fakeHandle = (result: FakeResult): ChildProcessSpawner.ChildProcessHandle =>
39
+ ChildProcessSpawner.makeHandle({
40
+ all: Stream.fromIterable([encode((result.stdout ?? '') + (result.stderr ?? ''))]),
41
+ exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.exitCode ?? 0)),
42
+ getInputFd: () => Sink.drain,
43
+ getOutputFd: () => Stream.empty,
44
+ isRunning: Effect.succeed(false),
45
+ kill: () => Effect.void,
46
+ pid: ChildProcessSpawner.ProcessId(1),
47
+ stderr: Stream.fromIterable([encode(result.stderr ?? '')]),
48
+ stdin: Sink.drain,
49
+ stdout: Stream.fromIterable([encode(result.stdout ?? '')]),
50
+ unref: Effect.succeed(Effect.void),
51
+ });
52
+
53
+ /**
54
+ * Decode a `StandardCommand`'s `options.stdin` into text, or `undefined` when there was none to
55
+ * decode. Only `"pipe" | "inherit" | "ignore" | "overlapped"` (no bytes) and a raw `Stream` (the
56
+ * only two shapes anything in this package ever passes — `credentials-write.ts`'s `writeKvValue`
57
+ * uses `Stream.fromIterable`, matching `alchemy`'s own `Cloudflare/Workers/ViteChild.ts` usage) are
58
+ * handled; a `StdinConfig` wrapper is unwrapped to its own `.stream` field first.
59
+ */
60
+ const stdinText = (stdin: unknown) =>
61
+ Effect.gen(function* () {
62
+ if (stdin === undefined || typeof stdin === 'string') return undefined;
63
+ const stream =
64
+ typeof stdin === 'object' && stdin !== null && 'stream' in stdin
65
+ ? (stdin as { stream: unknown }).stream
66
+ : stdin;
67
+ if (stream === undefined || typeof stream === 'string') return undefined;
68
+ const chunks = yield* Stream.runCollect(stream as Stream.Stream<Uint8Array>);
69
+ return chunks.map((chunk) => new TextDecoder().decode(chunk)).join('');
70
+ }).pipe(Effect.orDie);
71
+
72
+ /**
73
+ * Build a fake `ChildProcessSpawner.Service` from `handler`, which sees every command's
74
+ * executable + argv (+ decoded stdin, when piped) and returns canned stdout/stderr/exitCode.
75
+ * `credentials.ts` and `talosctl.ts` only ever build `StandardCommand`s (never a piped command),
76
+ * so that is the only shape this fake accepts — a `PipedCommand` is a test-authoring mistake, not
77
+ * something to fake.
78
+ */
79
+ export const fakeSpawner = (handler: FakeHandler, calls: FakeCall[] = []) =>
80
+ ChildProcessSpawner.make((command: Command) =>
81
+ Effect.gen(function* () {
82
+ if (command._tag !== 'StandardCommand') {
83
+ return yield* Effect.die(new Error('fake-process: only StandardCommand is supported'));
84
+ }
85
+ const stdin = yield* stdinText(command.options.stdin);
86
+ const call: FakeCall = {
87
+ args: command.args,
88
+ command: command.command,
89
+ ...(stdin === undefined ? {} : { stdin }),
90
+ };
91
+ calls.push(call);
92
+ return fakeHandle(handler(call));
93
+ }),
94
+ );
@@ -5,10 +5,45 @@
5
5
  * These are the symbols a real stack consumes; the rest of the files are internals a
6
6
  * provider needs but a consumer should not depend on. An `export *` here would publish
7
7
  * every helper as API and make the next refactor a breaking change.
8
+ * ★ `Talos.MachineConfig`'S RESOURCE CLASS JOINED ITS PROVIDER 2026-09-26 (K-A3) — until now this
9
+ * barrel exported only `Talos*Provider`, which builds the provider but gives a consuming stack
10
+ * nothing to `new` or call to actually DECLARE a row (the same gap proxmox/index.ts's own header
11
+ * calls out for `ProxmoxVm`). MachineConfig is safe to declare today: it fails closed on a digest
12
+ * mismatch before touching a node, and its cold-start read never silently adopts (PR 307 fix-first
13
+ * #1, machine-config-read.ts's own header).
14
+ * ★ BOOTSTRAP, CLUSTERHEALTH AND KUBECONFIG JOIN THEM (K-talos-first-boot, 2026-09-26) — PR 307's
15
+ * red team (fix-first #3) held these back because `Talos.Bootstrap` could re-run `talosctl
16
+ * bootstrap` against an already-bootstrapped cluster and `Talos.Kubeconfig` wrote a cluster-admin
17
+ * kubeconfig to un-vaulted host disk. Both are fixed now: bootstrap's "once means once" invariant
18
+ * (talos-bootstrap.ts's own header) and kubeconfig landing in OpenBao, written once at bring-up
19
+ * (kubeconfig.ts's own header) — plus `Talos.ClusterHealth`'s error handling no longer conflates a
20
+ * vault/transport failure with "cluster not healthy yet" (talos-cluster-health.ts's own header).
8
21
  * ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
9
22
  * deliberate, visible act rather than an accident of barrelling.
10
23
  */
11
- export { TalosKubeconfigProvider } from './kubeconfig.ts';
12
- export { TalosBootstrapProvider } from './talos-bootstrap.ts';
13
- export { TalosClusterHealthProvider } from './talos-cluster-health.ts';
14
- export { TalosMachineConfigProvider } from './talos-machine-config.ts';
24
+ export type { TalosCredential, TalosTarget } from './credentials.ts';
25
+ export {
26
+ type KubeconfigAttributes,
27
+ type KubeconfigProps,
28
+ TalosKubeconfig,
29
+ TalosKubeconfigProvider,
30
+ } from './kubeconfig.ts';
31
+ export type { ApplyMode, WithTarget } from './resource.ts';
32
+ export {
33
+ type BootstrapAttributes,
34
+ type BootstrapProps,
35
+ TalosBootstrap,
36
+ TalosBootstrapProvider,
37
+ } from './talos-bootstrap.ts';
38
+ export {
39
+ type ClusterHealthAttributes,
40
+ type ClusterHealthProps,
41
+ TalosClusterHealth,
42
+ TalosClusterHealthProvider,
43
+ } from './talos-cluster-health.ts';
44
+ export {
45
+ type MachineConfigAttributes,
46
+ type MachineConfigProps,
47
+ TalosMachineConfig,
48
+ TalosMachineConfigProvider,
49
+ } from './talos-machine-config.ts';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `Talos.Kubeconfig`'s attribute builder and its `Kubernetes.Connection` — split out of
3
+ * kubeconfig.ts (LAND, 2026-09-26, C1/I2 fixes) once fixing those findings pushed the file over the
4
+ * 250-line cap, the same reason machine-config-read.ts/machine-config-poll.ts/talos-errors.ts were
5
+ * split out of talos-machine-config.ts before it. Only `import type` comes back from kubeconfig.ts,
6
+ * so there is no runtime cycle between the two files.
7
+ */
8
+ import type { Connection } from 'alchemy/Kubernetes/Connection';
9
+ import type { KubeconfigAttributes, KubeconfigProps } from './kubeconfig.ts';
10
+ import { kubeconfigMetadata, sha256 } from './values.ts';
11
+
12
+ const generation = (meta: {
13
+ endpoint: string;
14
+ caFingerprint: string;
15
+ clientFingerprint: string;
16
+ context: string;
17
+ }) => sha256(`${meta.context}\n${meta.endpoint}\n${meta.caFingerprint}\n${meta.clientFingerprint}`);
18
+
19
+ /**
20
+ * ⛔ I2 FIX (LAND red team, 2026-09-26) — NEVER LEAVE `path` UNDEFINED. An undefined `path` here
21
+ * made the stock `Kubernetes.KubeConfigAdapter` fall back to `$KUBECONFIG`/`~/.kube/config`
22
+ * (alchemy `Kubernetes/internal/kubeconfig.ts`'s `resolveKubeConfigPath`, wired in
23
+ * `BuiltinAdapters.ts`) — exactly the un-vaulted, possibly-stale-context exposure this whole
24
+ * family exists to remove. A consumer wiring `Kubernetes.*` to `kube.connection` (this
25
+ * attribute's own doc invites exactly that) would silently connect through whatever
26
+ * `admin@<cluster>` context happens to be on the operator's machine. `Kubernetes.ClusterAdapter`
27
+ * is not yet wired to `mintKubeconfig` (kubeconfig.ts's own header) — no real path exists for
28
+ * this connection to use — so it must fail LOUDLY instead of silently succeeding against a
29
+ * stranger's cluster. A path that can never resolve does that: `resolveKubeContext` fails at
30
+ * `fs.readFileString` naming this exact sentinel, instead of reading whatever real file happens
31
+ * to be on `$KUBECONFIG`.
32
+ */
33
+ const UNWIRED_KUBECONFIG_PATH =
34
+ '/talos-first-boot-unwired/wire-Kubernetes.ClusterAdapter-to-mintKubeconfig';
35
+
36
+ const toConnection = (props: KubeconfigProps): Connection => ({
37
+ auth: { context: props.context, kind: 'kubeconfig', path: UNWIRED_KUBECONFIG_PATH },
38
+ });
39
+
40
+ /** Parse `raw` kubeconfig YAML into public, persistable attributes, or `undefined` if it fails to. */
41
+ export const buildAttrs = (
42
+ raw: string,
43
+ props: KubeconfigProps,
44
+ ): KubeconfigAttributes | undefined => {
45
+ const meta = kubeconfigMetadata(raw, props.context);
46
+ if (meta === undefined) return undefined;
47
+ return {
48
+ certificateAuthorityFingerprint: meta.caFingerprint,
49
+ clientCertificateFingerprint: meta.clientFingerprint,
50
+ connection: toConnection(props),
51
+ context: props.context,
52
+ credentialGeneration: generation({ ...meta, context: props.context }),
53
+ endpoint: meta.endpoint,
54
+ };
55
+ };
@@ -0,0 +1,256 @@
1
+ /**
2
+ * `Talos.Kubeconfig` landing in OpenBao, written once at bring-up (K-talos-first-boot). Fully
3
+ * offline: fake-process.ts fakes `bao`/`talosctl`; the fake `talosctl kubeconfig` handler writes a
4
+ * fixture file to the temp path it was given, standing in for what the real binary would do.
5
+ *
6
+ * ⚠️ FIXTURES ONLY — see values.test.ts's own header for why the base64 fields are built from
7
+ * plain words at run time rather than written out literally.
8
+ */
9
+ import assert from 'node:assert/strict';
10
+ import { existsSync, writeFileSync } from 'node:fs';
11
+ import { describe, it } from 'node:test';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
14
+ import { type FakeCall, fakeSpawner } from './fake-process.ts';
15
+ import {
16
+ type KubeconfigAttributes,
17
+ type KubeconfigProps,
18
+ diffKubeconfig as diff,
19
+ readKubeconfig as read,
20
+ reconcileKubeconfig as reconcile,
21
+ } from './kubeconfig.ts';
22
+
23
+ const b64 = (words: string) => Buffer.from(words).toString('base64');
24
+
25
+ const fixtureKubeconfig = (marker: string) => `
26
+ apiVersion: v1
27
+ kind: Config
28
+ clusters:
29
+ - name: hf-c1
30
+ cluster:
31
+ server: https://192.0.2.50:6443
32
+ certificate-authority-data: ${b64(`not a ca ${marker}`)}
33
+ contexts:
34
+ - name: admin@hf-c1
35
+ context: { cluster: hf-c1, user: admin@hf-c1 }
36
+ users:
37
+ - name: admin@hf-c1
38
+ user:
39
+ client-certificate-data: ${b64(`not a certificate ${marker}`)}
40
+ client-key-data: ${b64(`not a key ${marker}`)}
41
+ current-context: admin@hf-c1
42
+ `;
43
+
44
+ const TARGET = { cluster: 'c1', mount: 'talos-c1' };
45
+ const props = (): KubeconfigProps => ({
46
+ context: 'admin@hf-c1',
47
+ node: '198.51.100.10',
48
+ target: TARGET,
49
+ });
50
+
51
+ const run = <A, E>(
52
+ effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
53
+ handler: (c: FakeCall) => { stdout?: string; stderr?: string; exitCode?: number },
54
+ calls: FakeCall[] = [],
55
+ ) =>
56
+ Effect.runPromise(
57
+ Effect.provideService(
58
+ effect,
59
+ ChildProcessSpawner.ChildProcessSpawner,
60
+ fakeSpawner(handler, calls),
61
+ ),
62
+ );
63
+
64
+ describe('reconcile — CREATE writes talosctl kubeconfig output into the vault, never a host path', () => {
65
+ it('mints the temp file, writes its content to vault via stdin, cleans up the temp file', async () => {
66
+ const calls: FakeCall[] = [];
67
+ let outPath = '';
68
+ const handler = (call: FakeCall) => {
69
+ if (call.command === 'bao' && call.args[1] === 'get') {
70
+ return { stdout: JSON.stringify({ data: { data: { talosconfig: 'fake-talosconfig' } } }) };
71
+ }
72
+ if (call.command === 'bao' && call.args[1] === 'put') return {};
73
+ if (call.command === 'talosctl' && call.args[0] === 'kubeconfig') {
74
+ outPath = call.args[1] ?? '';
75
+ writeFileSync(outPath, fixtureKubeconfig('create'));
76
+ return {};
77
+ }
78
+ throw new Error(`unexpected call ${JSON.stringify(call)}`);
79
+ };
80
+ const result = (await run(
81
+ reconcile(props(), undefined),
82
+ handler,
83
+ calls,
84
+ )) as KubeconfigAttributes;
85
+
86
+ assert.equal(result.endpoint, 'https://192.0.2.50:6443');
87
+ assert.equal(result.context, 'admin@hf-c1');
88
+ assert.equal(result.connection.auth.kind, 'kubeconfig');
89
+ // ⛔ I2 (LAND red team): NEVER undefined — an undefined `path` here would let the stock
90
+ // KubeConfigAdapter fall back to $KUBECONFIG/~/.kube/config. It must be a path that can never
91
+ // resolve, so a consumer that isn't yet wired to mintKubeconfig fails loudly instead of quietly
92
+ // reaching the operator's own cluster.
93
+ const authPath = (result.connection.auth as { path?: string }).path;
94
+ assert.ok(typeof authPath === 'string' && authPath.length > 0);
95
+
96
+ const put = calls.find((c) => c.command === 'bao' && c.args[1] === 'put');
97
+ assert.ok(put);
98
+ // ⛔ C2 (LAND red team): `=-`, never `=@-` — see credentials-write.ts's own header.
99
+ assert.deepEqual(put.args, ['kv', 'put', 'talos-c1/kubeconfig', 'kubeconfig=-']);
100
+ assert.equal(put.stdin, fixtureKubeconfig('create'));
101
+ assert.ok(!put.args.some((a) => a.includes('not a key')), 'never in argv');
102
+
103
+ assert.equal(existsSync(outPath), false, 'the captured temp file must be gone after reconcile');
104
+ });
105
+ });
106
+
107
+ describe('reconcile — write-once: a second reconcile never re-runs talosctl kubeconfig', () => {
108
+ it('confirms from the vault copy instead of minting a new admin cert', async () => {
109
+ const calls: FakeCall[] = [];
110
+ const priorOutput: KubeconfigAttributes = {
111
+ certificateAuthorityFingerprint: 'x',
112
+ clientCertificateFingerprint: 'x',
113
+ connection: { auth: { context: 'admin@hf-c1', kind: 'kubeconfig' } },
114
+ context: 'admin@hf-c1',
115
+ credentialGeneration: 'x',
116
+ endpoint: 'https://192.0.2.50:6443',
117
+ };
118
+ const handler = (call: FakeCall) => {
119
+ if (call.command === 'bao' && call.args[1] === 'get') {
120
+ return {
121
+ stdout: JSON.stringify({ data: { data: { config: fixtureKubeconfig('confirm') } } }),
122
+ };
123
+ }
124
+ throw new Error(`unexpected call ${JSON.stringify(call)}`);
125
+ };
126
+ const result = (await run(
127
+ reconcile(props(), priorOutput),
128
+ handler,
129
+ calls,
130
+ )) as KubeconfigAttributes;
131
+
132
+ assert.equal(result.endpoint, 'https://192.0.2.50:6443');
133
+ assert.ok(
134
+ calls.every(
135
+ (c) => !(c.command === 'talosctl') && !(c.command === 'bao' && c.args[1] === 'put'),
136
+ ),
137
+ 'a confirm-only reconcile never spawns talosctl or writes the vault again',
138
+ );
139
+ });
140
+ });
141
+
142
+ describe('read — C1 fix: undefined means genuinely absent, never "some read failed"', () => {
143
+ it('returns undefined (never a defined empty object) when the vault key was never written', async () => {
144
+ // ⛔ MEASURED against OpenBao v2.6.2: this exit code + stderr text is exactly what `bao kv get`
145
+ // prints for a genuinely unwritten KV-v2 key — see kubeconfig.ts's `isVaultKeyAbsent` header.
146
+ const result = await run(read(props()), () => ({
147
+ exitCode: 2,
148
+ stderr: 'No value found at talos-c1/data/kubeconfig',
149
+ }));
150
+ assert.equal(result, undefined);
151
+ });
152
+
153
+ it('propagates a genuine vault/transport failure instead of reading it as absence', async () => {
154
+ await assert.rejects(
155
+ run(read(props()), () => ({ exitCode: 2, stderr: 'permission denied' })),
156
+ (error: unknown) => error instanceof Error && error.message.includes('permission denied'),
157
+ );
158
+ });
159
+ });
160
+
161
+ describe('read → reconcile chained the way the Alchemy engine drives a cold start (C1 regression)', () => {
162
+ it('a cold-start read (undefined) followed by reconcile(undefined) actually creates', async () => {
163
+ // ⛔ Alchemy beta.79 Plan.ts:1303-1352 always calls `read` first when there is no state row; a
164
+ // DEFINED result there is adopted and forces `update`, so this is the exact sequence that made
165
+ // Talos.Kubeconfig un-creatable before the C1 fix — see kubeconfig.ts's own `readKubeconfig`
166
+ // header.
167
+ const calls: FakeCall[] = [];
168
+ const coldHandler = () => ({
169
+ exitCode: 2,
170
+ stderr: 'No value found at talos-c1/data/kubeconfig',
171
+ });
172
+ const readResult = await run(read(props()), coldHandler, calls);
173
+ assert.equal(
174
+ readResult,
175
+ undefined,
176
+ "the engine's adoption probe must see a genuine cold start",
177
+ );
178
+
179
+ const createHandler = (call: FakeCall) => {
180
+ if (call.command === 'bao' && call.args[1] === 'get') {
181
+ // ★ mintTalosconfig reads the `talosconfig` key first — only the `kubeconfig` key itself
182
+ // is genuinely absent at a cold start.
183
+ return call.args[3]?.endsWith('/kubeconfig')
184
+ ? coldHandler()
185
+ : { stdout: JSON.stringify({ data: { data: { talosconfig: 'fake-talosconfig' } } }) };
186
+ }
187
+ if (call.command === 'bao' && call.args[1] === 'put') return {};
188
+ if (call.command === 'talosctl' && call.args[0] === 'kubeconfig') {
189
+ writeFileSync(call.args[1] ?? '', fixtureKubeconfig('engine-order'));
190
+ return {};
191
+ }
192
+ throw new Error(`unexpected call ${JSON.stringify(call)}`);
193
+ };
194
+ const created = (await run(
195
+ reconcile(props(), readResult),
196
+ createHandler,
197
+ )) as KubeconfigAttributes;
198
+ assert.equal(created.endpoint, 'https://192.0.2.50:6443');
199
+ assert.notEqual(created.credentialGeneration, '');
200
+ });
201
+ });
202
+
203
+ describe('reconcile — belt-and-suspenders: a defined-but-empty output is treated as absent (C1)', () => {
204
+ it('a pre-fix-shaped empty output still runs CREATE instead of failing to confirm', async () => {
205
+ const staleEmptyOutput: KubeconfigAttributes = {
206
+ certificateAuthorityFingerprint: '',
207
+ clientCertificateFingerprint: '',
208
+ connection: { auth: { context: 'admin@hf-c1', kind: 'kubeconfig' } },
209
+ context: 'admin@hf-c1',
210
+ credentialGeneration: '',
211
+ endpoint: '',
212
+ };
213
+ const handler = (call: FakeCall) => {
214
+ if (call.command === 'bao' && call.args[1] === 'get') {
215
+ return { stdout: JSON.stringify({ data: { data: { talosconfig: 'x' } } }) };
216
+ }
217
+ if (call.command === 'bao' && call.args[1] === 'put') return {};
218
+ if (call.command === 'talosctl' && call.args[0] === 'kubeconfig') {
219
+ writeFileSync(call.args[1] ?? '', fixtureKubeconfig('stale-empty'));
220
+ return {};
221
+ }
222
+ throw new Error(`unexpected call ${JSON.stringify(call)}`);
223
+ };
224
+ const result = (await run(
225
+ reconcile(props(), staleEmptyOutput),
226
+ handler,
227
+ )) as KubeconfigAttributes;
228
+ assert.notEqual(result.credentialGeneration, '');
229
+ });
230
+ });
231
+
232
+ describe('diff', () => {
233
+ it('plans noop when the vault copy matches state', async () => {
234
+ const output: KubeconfigAttributes = (await run(
235
+ reconcile(props(), undefined),
236
+ (call: FakeCall) => {
237
+ if (call.command === 'bao' && call.args[1] === 'get') {
238
+ return { stdout: JSON.stringify({ data: { data: { talosconfig: 'x' } } }) };
239
+ }
240
+ if (call.command === 'bao' && call.args[1] === 'put') return {};
241
+ if (call.command === 'talosctl') {
242
+ writeFileSync(call.args[1] ?? '', fixtureKubeconfig('diff'));
243
+ return {};
244
+ }
245
+ throw new Error('unexpected');
246
+ },
247
+ )) as KubeconfigAttributes;
248
+
249
+ const result = await run(diff(props(), output), (call: FakeCall) =>
250
+ call.args[1] === 'get'
251
+ ? { stdout: JSON.stringify({ data: { data: { config: fixtureKubeconfig('diff') } } }) }
252
+ : {},
253
+ );
254
+ assert.equal(result?.action, 'noop');
255
+ });
256
+ });