@homeflare/alchemy 0.47.1 → 0.48.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 (128) hide show
  1. package/dist/talos/bao-env.d.ts +7 -0
  2. package/dist/talos/bao-env.d.ts.map +1 -0
  3. package/dist/talos/cluster-adapter-errors.d.ts +134 -0
  4. package/dist/talos/cluster-adapter-errors.d.ts.map +1 -0
  5. package/dist/talos/cluster-adapter.d.ts +115 -0
  6. package/dist/talos/cluster-adapter.d.ts.map +1 -0
  7. package/dist/talos/cluster-identity.d.ts +28 -0
  8. package/dist/talos/cluster-identity.d.ts.map +1 -0
  9. package/dist/talos/cluster-transport.d.ts +64 -0
  10. package/dist/talos/cluster-transport.d.ts.map +1 -0
  11. package/dist/talos/credentials-write.d.ts.map +1 -1
  12. package/dist/talos/credentials.d.ts +10 -19
  13. package/dist/talos/credentials.d.ts.map +1 -1
  14. package/dist/talos/fake-apiserver.d.ts +9 -0
  15. package/dist/talos/fake-apiserver.d.ts.map +1 -0
  16. package/dist/talos/index.d.ts +7 -0
  17. package/dist/talos/index.d.ts.map +1 -1
  18. package/dist/talos/index.js +669 -167
  19. package/dist/talos/index.js.map +22 -10
  20. package/dist/talos/kubeconfig-attrs.d.ts +14 -0
  21. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -1
  22. package/dist/talos/kubeconfig-doc.d.ts +23 -0
  23. package/dist/talos/kubeconfig-doc.d.ts.map +1 -0
  24. package/dist/talos/kubeconfig.d.ts +0 -12
  25. package/dist/talos/kubeconfig.d.ts.map +1 -1
  26. package/dist/talos/literal-connection.d.ts +16 -0
  27. package/dist/talos/literal-connection.d.ts.map +1 -0
  28. package/dist/talos/machine-config-read.d.ts +1 -1
  29. package/dist/talos/machine-config-read.d.ts.map +1 -1
  30. package/dist/talos/mint-temp-file.d.ts +20 -0
  31. package/dist/talos/mint-temp-file.d.ts.map +1 -0
  32. package/dist/talos/node-temp.harness.d.ts +2 -0
  33. package/dist/talos/node-temp.harness.d.ts.map +1 -0
  34. package/dist/talos/talos-bootstrap-etcd.d.ts +1 -1
  35. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -1
  36. package/dist/talos/talos-cluster-health.d.ts +15 -15
  37. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  38. package/dist/talos/talos-cluster-identity.d.ts +65 -0
  39. package/dist/talos/talos-cluster-identity.d.ts.map +1 -0
  40. package/dist/talos/talosctl-binary.d.ts +43 -0
  41. package/dist/talos/talosctl-binary.d.ts.map +1 -0
  42. package/dist/talos/talosctl.d.ts +12 -15
  43. package/dist/talos/talosctl.d.ts.map +1 -1
  44. package/dist/talos/trust-boundary.d.ts +8 -0
  45. package/dist/talos/trust-boundary.d.ts.map +1 -0
  46. package/dist/talos/values.d.ts.map +1 -1
  47. package/dist/talos/yaml-parse.d.ts +3 -0
  48. package/dist/talos/yaml-parse.d.ts.map +1 -0
  49. package/dist/verify/fake-engine.d.ts +2 -0
  50. package/dist/verify/fake-engine.d.ts.map +1 -1
  51. package/docs/talos-openbao-adapter.md +44 -0
  52. package/docs/upstream-conformance-bun.md +61 -24
  53. package/package.json +6 -2
  54. package/src/talos/bao-env.ts +16 -0
  55. package/src/talos/bao-vetting.test.ts +97 -0
  56. package/src/talos/cluster-adapter-deadline.test.ts +110 -0
  57. package/src/talos/cluster-adapter-errors.ts +192 -0
  58. package/src/talos/cluster-adapter-move.test.ts +129 -0
  59. package/src/talos/cluster-adapter-reconcile.test.ts +165 -0
  60. package/src/talos/cluster-adapter-refusals.test.ts +102 -0
  61. package/src/talos/cluster-adapter-rename.test.ts +87 -0
  62. package/src/talos/cluster-adapter-secrecy.test.ts +243 -0
  63. package/src/talos/cluster-adapter.fixtures.ts +125 -0
  64. package/src/talos/cluster-adapter.test.ts +240 -0
  65. package/src/talos/cluster-adapter.ts +249 -0
  66. package/src/talos/cluster-identity.ts +63 -0
  67. package/src/talos/cluster-transport.ts +110 -0
  68. package/src/talos/credentials-write.ts +27 -5
  69. package/src/talos/credentials.ts +51 -45
  70. package/src/talos/fake-apiserver.ts +55 -0
  71. package/src/talos/fake-process.ts +47 -1
  72. package/src/talos/index.ts +32 -0
  73. package/src/talos/kubeconfig-adapter.test.ts +37 -0
  74. package/src/talos/kubeconfig-attrs.ts +12 -28
  75. package/src/talos/kubeconfig-doc.ts +74 -0
  76. package/src/talos/kubeconfig.fixtures.ts +52 -0
  77. package/src/talos/kubeconfig.test.ts +6 -54
  78. package/src/talos/kubeconfig.ts +18 -34
  79. package/src/talos/literal-connection.ts +59 -0
  80. package/src/talos/mint-temp-file.ts +56 -0
  81. package/src/talos/node-connect.harness.ts +78 -0
  82. package/src/talos/node-connect.test.ts +27 -0
  83. package/src/talos/node-temp.harness.ts +40 -0
  84. package/src/talos/node-temp.test.ts +26 -0
  85. package/src/talos/talos-cluster-health-declare.test.ts +54 -0
  86. package/src/talos/talos-cluster-health-literal.test.ts +69 -0
  87. package/src/talos/talos-cluster-health.test.ts +104 -18
  88. package/src/talos/talos-cluster-health.ts +46 -14
  89. package/src/talos/talos-cluster-identity.test.ts +96 -0
  90. package/src/talos/talos-cluster-identity.ts +130 -0
  91. package/src/talos/talosctl-binary.ts +150 -0
  92. package/src/talos/talosctl-vetting.test.ts +212 -0
  93. package/src/talos/talosctl.test.ts +110 -0
  94. package/src/talos/talosctl.ts +101 -15
  95. package/src/talos/trust-boundary.test.ts +17 -0
  96. package/src/talos/trust-boundary.ts +45 -0
  97. package/src/talos/values.ts +5 -12
  98. package/src/talos/yaml-parse.test.ts +41 -0
  99. package/src/talos/yaml-parse.ts +28 -0
  100. package/src/verify/fake-engine.ts +10 -0
  101. package/dist/talos/fake-process.d.ts +0 -56
  102. package/dist/talos/fake-process.d.ts.map +0 -1
  103. package/dist/talos/machine-config-fixtures.d.ts +0 -60
  104. package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
  105. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
  106. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
  107. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
  108. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
  109. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
  110. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
  111. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
  112. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
  113. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
  114. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
  115. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
  116. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
  117. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
  118. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
  119. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
  120. package/src/release/fixtures/openbao/checksums.txt +0 -83
  121. package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
  122. package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
  123. package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
  124. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
  125. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
  126. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
  127. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
  128. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +0 -8
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `mintKvTempFile` — the 0600 session-temp file `readKvValue`'s material lands in. Extracted from
3
+ * credentials.ts (2026-10-06, red team PR 355) once the Node-runtime fix pushed that file past the
4
+ * 250-line cap; the split mirrors credentials-write.ts, which was split out for its own header.
5
+ */
6
+ import { closeSync, openSync, writeFileSync } from 'node:fs';
7
+ import { unlink } from 'node:fs/promises';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import * as Effect from 'effect/Effect';
12
+ import type * as Scope from 'effect/Scope';
13
+
14
+ /**
15
+ * Write `raw` to a session-temp, 0600, exclusively-created file whose lifetime is the CALLER's
16
+ * `Effect.scoped`, not this call's. See the ⛔ C1 FIX note at the top of credentials.ts.
17
+ *
18
+ * ⛔ 0600, CREATED EXCLUSIVELY, AND `Bun.write` CANNOT DO EITHER. This file can hold a cluster
19
+ * admin client certificate or a machine config's bootstrap token. `Bun.write` takes no mode, so
20
+ * it lands at the process umask — world-readable on this estate — for as long as the resource
21
+ * runs. The name is unguessable, and "unguessable" is not a permission.
22
+ * ⚠️ `wx` IS THE OTHER HALF. `O_EXCL` means this cannot be made to write through a path an
23
+ * attacker pre-created (the classic /tmp symlink race), and with a UUID name a collision is a
24
+ * genuine error rather than something to paper over.
25
+ * ⚠️ A HARD KILL (SIGKILL, power loss) skips the finalizer and leaves the file behind. 0600 is
26
+ * what makes that survivable rather than a disclosure.
27
+ */
28
+ export const mintKvTempFile = (
29
+ raw: string,
30
+ label: string,
31
+ ): Effect.Effect<{ readonly path: string }, Error, Scope.Scope> =>
32
+ Effect.gen(function* () {
33
+ // ⛔ NODE, NOT BUN (red team, PR 355): the published dist runs on Node, where `Bun.env` and
34
+ // `Bun.randomUUIDv7` are missing globals — a `Talos.ClusterHealth` check would hit a
35
+ // `ReferenceError` before `talosctl` ever ran. `os.tmpdir()` and `node:crypto` `randomUUID`
36
+ // are the portable equivalents.
37
+ const path = join(tmpdir(), `hf-talos-${label}-${randomUUID()}.yaml`);
38
+ yield* Effect.acquireRelease(
39
+ Effect.try({
40
+ try: () => {
41
+ const fd = openSync(path, 'wx', 0o600);
42
+ try {
43
+ writeFileSync(fd, raw);
44
+ } finally {
45
+ closeSync(fd);
46
+ }
47
+ },
48
+ catch: (cause) => new Error(`writing ${label} temp file: ${String(cause)}`),
49
+ }),
50
+ () =>
51
+ Effect.tryPromise({ try: () => unlink(path), catch: () => undefined }).pipe(
52
+ Effect.orElseSucceed(() => undefined),
53
+ ),
54
+ );
55
+ return { path };
56
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Run UNDER NODE by `node-connect.test.ts` (never imported by bun): a successful `talos-openbao`
3
+ * connect with a fake `bao` and a fake apiserver, so any Bun-only API on the connect path (the
4
+ * `Bun.YAML` that once made every kubeconfig "unreadable" on Node) fails CI.
5
+ *
6
+ * ★ The apiserver fake patches `node:https`'s `request` and calls `syncBuiltinESMExports`, which is
7
+ * how a Node program changes what `import * as https` sees. Prints one JSON line, no secrets.
8
+ */
9
+ import { EventEmitter } from 'node:events';
10
+ import https from 'node:https';
11
+ import { syncBuiltinESMExports } from 'node:module';
12
+ import { ClusterAdapter } from 'alchemy/Kubernetes/ClusterAdapter';
13
+ import * as Effect from 'effect/Effect';
14
+ import * as Layer from 'effect/Layer';
15
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
16
+ import { TalosOpenBaoAdapter, talosOpenBaoConnection } from './cluster-adapter.ts';
17
+ import { fakeSpawner } from './fake-process.ts';
18
+
19
+ const b64 = (text: string) => Buffer.from(text).toString('base64');
20
+ const pem = (label: string) => `-----${'BEGIN'} ${label}-----\n${b64(label)}\n`;
21
+ const kubeconfig = `apiVersion: v1
22
+ kind: Config
23
+ clusters:
24
+ - name: c1
25
+ cluster: { server: 'https://c1.cluster.invalid:6443', certificate-authority-data: ${b64(pem('CERTIFICATE'))} }
26
+ contexts:
27
+ - name: admin@c1
28
+ context: { cluster: c1, user: admin@c1 }
29
+ users:
30
+ - name: admin@c1
31
+ user: { client-certificate-data: ${b64(pem('CERTIFICATE'))}, client-key-data: ${b64(pem('PRIVATE KEY'))} }
32
+ `;
33
+
34
+ const seen: string[] = [];
35
+ const fakeRequest = (
36
+ options: { hostname: string; method: string; path: string },
37
+ callback: (response: EventEmitter & { statusCode?: number }) => void,
38
+ ) => {
39
+ seen.push(`${options.method} ${options.hostname}${options.path}`);
40
+ const request = new EventEmitter() as EventEmitter & { write: () => void; end: () => void };
41
+ request.write = () => undefined;
42
+ request.end = () => {
43
+ const response = new EventEmitter() as EventEmitter & { statusCode?: number };
44
+ response.statusCode = 200;
45
+ callback(response);
46
+ response.emit('data', Buffer.from('{"metadata":{"uid":"uid-c1"}}'));
47
+ response.emit('end');
48
+ };
49
+ return request;
50
+ };
51
+ (https as unknown as { request: unknown }).request = fakeRequest;
52
+ syncBuiltinESMExports();
53
+
54
+ process.env['BAO_ADDR'] ??= 'https://bao.invalid';
55
+ process.env['BAO_TOKEN'] ??= 'fake-test-token';
56
+
57
+ // ⚠️ The trust-boundary seam registers the fixtures dir ASYNC (trust-boundary.ts's computed
58
+ // dynamic import). `readKvValue` now vets `bao`, so the walk must see the boundary before it
59
+ // runs. Awaiting the seam here flushes that registration; without it a bare `node` run can walk
60
+ // past the (755) fixtures dir into a group-writable ancestor and refuse a trusted stub.
61
+ await import('./trust-boundary.seam.ts');
62
+
63
+ const spawner = Layer.succeed(
64
+ ChildProcessSpawner.ChildProcessSpawner,
65
+ fakeSpawner(() => ({ stdout: JSON.stringify({ data: { data: { kubeconfig } } }) })),
66
+ );
67
+ const config = {
68
+ c1: { context: 'admin@c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
69
+ };
70
+
71
+ const transport = await Effect.runPromise(
72
+ Effect.gen(function* () {
73
+ const adapter = yield* ClusterAdapter('talos-openbao');
74
+ return yield* adapter.connect(talosOpenBaoConnection('uid-c1'));
75
+ }).pipe(Effect.provide(TalosOpenBaoAdapter(config).pipe(Layer.provide(spawner)))),
76
+ );
77
+ const hasCert = transport.clientCert !== undefined;
78
+ process.stdout.write(`${JSON.stringify({ endpoint: transport.endpoint, hasCert, seen })}\n`);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ⛔ THE PUBLISHED CODE RUNS ON NODE. A successful adapter connect is run under a real `node`
3
+ * process (`node-connect.harness.ts`, type stripping), so a Bun-only API on the connect path fails
4
+ * here. Reproduced before the fix: on Node 22 `Bun.YAML` was a missing global, swallowed into
5
+ * "unreadable kubeconfig", and the adapter returned no transport. Bun's own test run cannot see
6
+ * that, which is why this spawns node instead of importing the module.
7
+ */
8
+ import { expect, test } from 'bun:test';
9
+
10
+ test('a successful connect works under node, with no Bun globals', async () => {
11
+ const harness = new URL('./node-connect.harness.ts', import.meta.url).pathname;
12
+ const proc = Bun.spawn(['node', '--experimental-strip-types', '--no-warnings', harness], {
13
+ stderr: 'pipe',
14
+ stdout: 'pipe',
15
+ });
16
+ const [out, err, code] = await Promise.all([
17
+ new Response(proc.stdout).text(),
18
+ new Response(proc.stderr).text(),
19
+ proc.exited,
20
+ ]);
21
+ expect({ code, err: err.slice(0, 500) }).toEqual({ code: 0, err: '' });
22
+ expect(JSON.parse(out.trim())).toEqual({
23
+ endpoint: 'https://c1.cluster.invalid:6443',
24
+ hasCert: true,
25
+ seen: ['GET c1.cluster.invalid/api/v1/namespaces/kube-system'],
26
+ });
27
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ⛔ THE PUBLISHED CODE RUNS ON NODE. `mintKvTempFile` (credentials.ts) and `reservedTempPath`
3
+ * (credentials-write.ts) once built their path from `Bun.env`/`Bun.randomUUIDv7` — missing globals
4
+ * on Node, where a `Talos.ClusterHealth` check would hit a `ReferenceError` before `talosctl` ran.
5
+ * Run under a real `node` process (type stripping) so a Bun-only API here fails CI.
6
+ */
7
+ import { existsSync, statSync } from 'node:fs';
8
+ import * as Effect from 'effect/Effect';
9
+ import { mintKvTempFile } from './credentials.ts';
10
+ import { reservedTempPath } from './credentials-write.ts';
11
+
12
+ const report = (label: string, value: unknown) =>
13
+ process.stdout.write(`${JSON.stringify({ label, value })}\n`);
14
+
15
+ let tempCreated = false;
16
+ let tempMode = -1;
17
+ let tempGone = false;
18
+ const mintedPath = await Effect.runPromise(
19
+ Effect.scoped(
20
+ Effect.gen(function* () {
21
+ const file = yield* mintKvTempFile('node-body\n', 'c1-node-check');
22
+ tempCreated = existsSync(file.path);
23
+ tempMode = statSync(file.path).mode & 0o777;
24
+ return file.path;
25
+ }),
26
+ ),
27
+ );
28
+ tempGone = !existsSync(mintedPath);
29
+
30
+ let reservedPath = '';
31
+ const reservedGone = await Effect.runPromise(
32
+ Effect.scoped(
33
+ Effect.gen(function* () {
34
+ const reserved = yield* reservedTempPath('c1-node-reserve');
35
+ reservedPath = reserved.path;
36
+ }),
37
+ ),
38
+ ).then(() => !existsSync(reservedPath));
39
+
40
+ report('temp', { tempCreated, tempMode, tempGone, reservedGone });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ⛔ The temp-file minters (credentials.ts `mintKvTempFile`, credentials-write.ts `reservedTempPath`)
3
+ * run on Node in production. They once read `Bun.env`/`Bun.randomUUIDv7`; a Node process would
4
+ * throw `ReferenceError: Bun is not defined`. This spawns a real `node` process (type stripping) so
5
+ * a Bun-only API on that path fails here rather than in a consumer's deploy.
6
+ */
7
+ import { expect, test } from 'bun:test';
8
+
9
+ test('the temp-file minters work under node, with no Bun globals', async () => {
10
+ const harness = new URL('./node-temp.harness.ts', import.meta.url).pathname;
11
+ const proc = Bun.spawn(['node', '--experimental-strip-types', '--no-warnings', harness], {
12
+ stderr: 'pipe',
13
+ stdout: 'pipe',
14
+ });
15
+ const [out, err, code] = await Promise.all([
16
+ new Response(proc.stdout).text(),
17
+ new Response(proc.stderr).text(),
18
+ proc.exited,
19
+ ]);
20
+ expect({ code, err: err.slice(0, 500) }).toEqual({ code: 0, err: '' });
21
+ const line = out.trim().split('\n').pop() ?? '';
22
+ expect(JSON.parse(line)).toEqual({
23
+ label: 'temp',
24
+ value: { tempCreated: true, tempMode: 0o600, tempGone: true, reservedGone: true },
25
+ });
26
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A FRESH `Talos.ClusterHealth` skips `diff` while its inputs are unresolved, so the literal-uid
3
+ * refusal must already have happened when the resource is DECLARED. Driven through a real
4
+ * `Alchemy.Stack` (the engine's own declaration path, no handler called directly): an Output uid
5
+ * fails the stack before any plan exists, and a literal one still declares.
6
+ */
7
+ import { expect, test } from 'bun:test';
8
+ import * as Alchemy from 'alchemy';
9
+ import * as Effect from 'effect/Effect';
10
+ import { talosOpenBaoConnection } from './cluster-adapter.ts';
11
+ import { TalosClusterHealth, TalosClusterHealthProvider } from './talos-cluster-health.ts';
12
+ import './fake-process.ts';
13
+
14
+ const base = { controlPlaneNodes: ['198.51.100.10'], target: { cluster: 'c1', mount: 'talos-c1' } };
15
+
16
+ const compile = (body: unknown) => {
17
+ const stack = Alchemy.Stack as unknown as (
18
+ name: string,
19
+ options: { providers: unknown; state: unknown },
20
+ body: unknown,
21
+ ) => Effect.Effect<unknown, unknown, never>;
22
+ return stack(
23
+ 'talos-health-declare',
24
+ { providers: TalosClusterHealthProvider(), state: Alchemy.inMemoryState() },
25
+ body,
26
+ ).pipe(Effect.provideService(Alchemy.Stage, 'test'), Effect.scoped);
27
+ };
28
+
29
+ test('declaring a fresh health resource whose uid is an Output fails the stack', async () => {
30
+ const failure = await Effect.runPromise(
31
+ compile(
32
+ Effect.gen(function* () {
33
+ const first = yield* TalosClusterHealth('First', {
34
+ ...base,
35
+ connection: talosOpenBaoConnection('uid-c1'),
36
+ });
37
+ // A real Output: an attribute of another resource, unresolved until it deploys.
38
+ const uid = (first as unknown as { controlPlaneNodes: unknown }).controlPlaneNodes;
39
+ yield* TalosClusterHealth('Second', {
40
+ ...base,
41
+ connection: { auth: { kind: 'talos-openbao', uid } },
42
+ } as never);
43
+ }),
44
+ ).pipe(Effect.flip),
45
+ );
46
+ expect((failure as { _tag: string })._tag).toBe('TalosUidNotLiteral');
47
+ });
48
+
49
+ test('a literal uid declares fine', async () => {
50
+ const compiled = await Effect.runPromise(
51
+ compile(TalosClusterHealth('Only', { ...base, connection: talosOpenBaoConnection('uid-c1') })),
52
+ );
53
+ expect(compiled).toBeDefined();
54
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `Talos.ClusterHealth` hands its `connection` to workloads through `cluster: health`, so an
3
+ * Output there reintroduces the unresolved-uid hole `talosOpenBaoConnection` closes. `diff` still
4
+ * sees the Input, so that is where the refusal happens. Offline: fake-process.ts fakes everything.
5
+ */
6
+ import assert from 'node:assert/strict';
7
+ import { describe, it } from 'node:test';
8
+ import * as Output from 'alchemy/Output';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
11
+ import { talosOpenBaoConnection } from './cluster-adapter.ts';
12
+ import { type FakeCall, fakeSpawner } from './fake-process.ts';
13
+ import { diffClusterHealth } from './talos-cluster-health.ts';
14
+
15
+ const connection = talosOpenBaoConnection('uid-c1');
16
+ const prior = {
17
+ connection,
18
+ controlPlaneNodes: '198.51.100.10',
19
+ healthy: true,
20
+ workerNodes: '',
21
+ };
22
+ const base = {
23
+ controlPlaneNodes: ['198.51.100.10'],
24
+ target: { cluster: 'c1', mount: 'talos-c1' },
25
+ };
26
+
27
+ const diffWith = (given: unknown, calls: FakeCall[]) =>
28
+ Effect.runPromise(
29
+ Effect.provideService(
30
+ diffClusterHealth({ ...base, connection: given } as never, prior),
31
+ ChildProcessSpawner.ChildProcessSpawner,
32
+ fakeSpawner(
33
+ () => ({ stdout: JSON.stringify({ data: { data: { talosconfig: 'x' } } }) }),
34
+ calls,
35
+ ),
36
+ ),
37
+ );
38
+
39
+ describe('diffClusterHealth — literal connection', () => {
40
+ it('plans noop for a literal connection (the guard does not refuse good input)', async () => {
41
+ const calls: FakeCall[] = [];
42
+ assert.equal((await diffWith(connection, calls))?.action, 'noop');
43
+ });
44
+
45
+ it('refuses an Output uid before any vault or talosctl call', async () => {
46
+ const calls: FakeCall[] = [];
47
+ const uid = Output.literal('uid-c1');
48
+ await assert.rejects(
49
+ diffWith({ auth: { kind: 'talos-openbao', uid } }, calls),
50
+ (error: unknown) => String(error).includes('literal string'),
51
+ );
52
+ assert.deepEqual(calls, []);
53
+ });
54
+
55
+ it('refuses a connection that is itself an Output', async () => {
56
+ const calls: FakeCall[] = [];
57
+ await assert.rejects(diffWith(Output.literal(connection), calls));
58
+ assert.deepEqual(calls, []);
59
+ });
60
+
61
+ it('refuses a stock kubeconfig connection at diff time (never reaches $KUBECONFIG)', async () => {
62
+ const calls: FakeCall[] = [];
63
+ await assert.rejects(
64
+ diffWith({ auth: { kind: 'kubeconfig', context: 'admin@hf-c1' } }, calls),
65
+ (error: unknown) => String(error).includes('literal string'),
66
+ );
67
+ assert.deepEqual(calls, []);
68
+ });
69
+ });
@@ -4,9 +4,11 @@
4
4
  */
5
5
  import assert from 'node:assert/strict';
6
6
  import { describe, it } from 'node:test';
7
+ import * as Output from 'alchemy/Output';
7
8
  import * as Effect from 'effect/Effect';
8
9
  import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
9
10
  import { type FakeCall, fakeSpawner } from './fake-process.ts';
11
+ import { talosOpenBaoConnection } from './cluster-adapter.ts';
10
12
  import {
11
13
  diffClusterHealth,
12
14
  readClusterHealth,
@@ -14,7 +16,14 @@ import {
14
16
  } from './talos-cluster-health.ts';
15
17
 
16
18
  const TARGET = { cluster: 'c1', mount: 'talos-c1' };
17
- const props = () => ({ controlPlaneNodes: ['198.51.100.10'], target: TARGET });
19
+ const connection = talosOpenBaoConnection('uid-c1');
20
+ const props = () => ({ connection, controlPlaneNodes: ['198.51.100.10'], target: TARGET });
21
+ const prior = (healthy: boolean) => ({
22
+ connection,
23
+ controlPlaneNodes: '198.51.100.10',
24
+ healthy,
25
+ workerNodes: '',
26
+ });
18
27
 
19
28
  const run = <A, E>(
20
29
  effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
@@ -54,6 +63,7 @@ describe('readClusterHealth', () => {
54
63
  it('reports healthy:true when talosctl health exits zero', async () => {
55
64
  const result = await run(readClusterHealth(props()), healthy);
56
65
  assert.equal(result.healthy, true);
66
+ assert.deepEqual(result.connection, connection);
57
67
  });
58
68
 
59
69
  it('propagates a vault/transport failure instead of reporting healthy:false', async () => {
@@ -64,30 +74,105 @@ describe('readClusterHealth', () => {
64
74
  });
65
75
  });
66
76
 
77
+ /** A talosctl vetting refusal (relative HF_TALOSCTL): the binary never runs, so it is no verdict. */
78
+ const withRefusedBinary = async <A>(body: () => Promise<A>): Promise<A> => {
79
+ const saved = process.env['HF_TALOSCTL'];
80
+ process.env['HF_TALOSCTL'] = 'relative/talosctl';
81
+ try {
82
+ return await body();
83
+ } finally {
84
+ if (saved === undefined) delete process.env['HF_TALOSCTL'];
85
+ else process.env['HF_TALOSCTL'] = saved;
86
+ }
87
+ };
88
+ const refusal = (error: unknown) =>
89
+ error instanceof Error &&
90
+ error.name === 'TalosBinaryRefused' &&
91
+ error.message.includes('absolute');
92
+
93
+ describe('a talosctl vetting refusal is never a health verdict (fails on 37d835a: healthy:false)', () => {
94
+ it('read propagates it', async () => {
95
+ await withRefusedBinary(() =>
96
+ assert.rejects(run(readClusterHealth(props()), healthy), refusal),
97
+ );
98
+ });
99
+
100
+ it('diff propagates it instead of planning an update', async () => {
101
+ await withRefusedBinary(() =>
102
+ assert.rejects(run(diffClusterHealth(props(), prior(true)), healthy), refusal),
103
+ );
104
+ });
105
+
106
+ it('reconcile propagates it without the "cluster not healthy" mislabel', async () => {
107
+ await withRefusedBinary(() =>
108
+ assert.rejects(
109
+ run(reconcileClusterHealth(props()), healthy),
110
+ (error: unknown) => refusal(error) && !String(error).includes('cluster not healthy'),
111
+ ),
112
+ );
113
+ });
114
+ });
115
+
67
116
  describe('diffClusterHealth', () => {
68
117
  it('plans update when the cluster genuinely reports unhealthy', async () => {
118
+ const result = await run(diffClusterHealth(props(), prior(false)), unhealthy);
119
+ assert.equal(result?.action, 'update');
120
+ });
121
+
122
+ it('propagates a vault/transport failure rather than planning update silently', async () => {
123
+ await assert.rejects(run(diffClusterHealth(props(), prior(false)), vaultDown));
124
+ });
125
+
126
+ it('plans noop when the cluster is healthy and the connection is unchanged', async () => {
127
+ const result = await run(diffClusterHealth(props(), prior(true)), healthy);
128
+ assert.equal(result?.action, 'noop');
129
+ });
130
+
131
+ it('plans update when the cluster is healthy but the connection changed', async () => {
132
+ const drifted = talosOpenBaoConnection('uid-other');
69
133
  const result = await run(
70
- diffClusterHealth(props(), {
71
- controlPlaneNodes: '198.51.100.10',
72
- healthy: false,
73
- workerNodes: '',
74
- }),
75
- unhealthy,
134
+ diffClusterHealth(props(), { ...prior(true), connection: drifted }),
135
+ healthy,
76
136
  );
77
137
  assert.equal(result?.action, 'update');
78
138
  });
79
139
 
80
- it('propagates a vault/transport failure rather than planning update silently', async () => {
81
- await assert.rejects(
82
- run(
83
- diffClusterHealth(props(), {
84
- controlPlaneNodes: '198.51.100.10',
85
- healthy: false,
86
- workerNodes: '',
87
- }),
88
- vaultDown,
89
- ),
140
+ it('plans update when only the cluster uid changed (a real move)', async () => {
141
+ const moved = talosOpenBaoConnection('uid-c2');
142
+ const result = await run(
143
+ diffClusterHealth(props(), { ...prior(true), connection: moved }),
144
+ healthy,
145
+ );
146
+ assert.equal(result?.action, 'update');
147
+ });
148
+
149
+ it('plans update, not a throw, for a row saved before `connection` existed', async () => {
150
+ const { connection: _dropped, ...old } = prior(true);
151
+ const result = await run(
152
+ diffClusterHealth(props(), old as unknown as ReturnType<typeof prior>),
153
+ healthy,
90
154
  );
155
+ assert.equal(result?.action, 'update');
156
+ });
157
+ });
158
+
159
+ describe('diffClusterHealth — a legacy auth.cluster row (fails if the hasOwn guard is deleted)', () => {
160
+ it('plans update for a healthy cluster whose saved row still carries auth.cluster', async () => {
161
+ const legacy = { auth: { ...talosOpenBaoConnection('uid-c1').auth, cluster: 'c1' } };
162
+ const result = await run(
163
+ diffClusterHealth(props(), { ...prior(true), connection: legacy as never }),
164
+ healthy,
165
+ );
166
+ assert.equal(result?.action, 'update');
167
+ });
168
+ });
169
+
170
+ describe('diffClusterHealth — a non-literal connection is a TYPED failure', () => {
171
+ it('fails (flip would reject on a defect) with TalosUidNotLiteral for an Output uid', async () => {
172
+ const output = Output.literal('uid-c1') as unknown as string;
173
+ const bad = { ...props(), connection: { auth: { kind: 'talos-openbao', uid: output } } };
174
+ const error = await run(Effect.flip(diffClusterHealth(bad as never, prior(true))), healthy);
175
+ assert.equal((error as { _tag: string })._tag, 'TalosUidNotLiteral');
91
176
  });
92
177
  });
93
178
 
@@ -96,6 +181,7 @@ describe('check — I1 fix: --nodes names ONE contact node, not the whole cluste
96
181
  const calls: FakeCall[] = [];
97
182
  await run(
98
183
  readClusterHealth({
184
+ connection,
99
185
  controlPlaneNodes: ['198.51.100.10', '198.51.100.11', '198.51.100.12'],
100
186
  target: TARGET,
101
187
  }),
@@ -116,7 +202,7 @@ describe('check — I1 fix: --nodes names ONE contact node, not the whole cluste
116
202
 
117
203
  it('fails closed instead of spawning talosctl when controlPlaneNodes is empty', async () => {
118
204
  await assert.rejects(
119
- run(readClusterHealth({ controlPlaneNodes: [], target: TARGET }), healthy),
205
+ run(readClusterHealth({ connection, controlPlaneNodes: [], target: TARGET }), healthy),
120
206
  (error: unknown) =>
121
207
  error instanceof Error && error.message.includes('controlPlaneNodes is empty'),
122
208
  );
@@ -22,13 +22,19 @@
22
22
  * rollout would never learn the real problem was vault. Both `read` and `reconcile` now only
23
23
  * catch `TalosError` (`talosctl health` itself ran and exited non-zero — the one case this
24
24
  * family may honestly call "not healthy"); anything else propagates untouched.
25
+ * ★ `connection` IS A PASS-THROUGH so this resource is `ClusterLike`. Later rows take
26
+ * `cluster: health`. The health check itself still uses `target` and `talosctl`; the field is
27
+ * copied onto attributes and is not a credential. It is required: an optional field would not
28
+ * satisfy `{ connection: Connection }`.
25
29
  */
26
30
  import { Resource } from 'alchemy';
27
31
  import { isResolved } from 'alchemy/Diff';
28
32
  import type { Input } from 'alchemy/Input';
29
33
  import * as Provider from 'alchemy/Provider';
30
34
  import * as Effect from 'effect/Effect';
35
+ import type { TalosOpenBaoConnection } from './cluster-adapter.ts';
31
36
  import { mintTalosconfig } from './credentials.ts';
37
+ import { literalConnectionRefusal, withLiteralConnection } from './literal-connection.ts';
32
38
  import type { TalosRequirements, WithTarget } from './resource.ts';
33
39
  import { TalosError, talosctl } from './talosctl.ts';
34
40
 
@@ -49,12 +55,19 @@ export interface ClusterHealthProps extends WithTarget {
49
55
  * on kube-proxy and CoreDNS, both of which wait on a CNI that does not exist yet at bootstrap.
50
56
  */
51
57
  after?: readonly unknown[];
58
+ /**
59
+ * Kubernetes connection copied onto attributes so a later row can take `cluster: this`.
60
+ * Not read by the health check. Persist only a secret-free connection (`talos-openbao`).
61
+ */
62
+ connection: TalosOpenBaoConnection;
52
63
  }
53
64
 
54
65
  export interface ClusterHealthAttributes {
55
66
  healthy: boolean;
56
67
  controlPlaneNodes: string;
57
68
  workerNodes: string;
69
+ /** Same object as the prop. Makes these attributes `ClusterLike`. */
70
+ connection: TalosOpenBaoConnection;
58
71
  }
59
72
 
60
73
  export interface TalosClusterHealth extends Resource<
@@ -65,11 +78,34 @@ export interface TalosClusterHealth extends Resource<
65
78
  TalosRequirements
66
79
  > {}
67
80
 
68
- export const TalosClusterHealth = Resource<TalosClusterHealth>('Talos.ClusterHealth');
81
+ // ⛔ Declaration-time literal guard: a fresh resource skips `diff` (see `withLiteralConnection`).
82
+ export const TalosClusterHealth = withLiteralConnection(
83
+ Resource<TalosClusterHealth>('Talos.ClusterHealth'),
84
+ );
69
85
 
70
86
  const nodeCsv = (nodes: readonly string[] | undefined) =>
71
87
  nodes === undefined || nodes.length === 0 ? '' : nodes.join(',');
72
88
 
89
+ const attributes = (props: ClusterHealthProps, healthy: boolean): ClusterHealthAttributes => ({
90
+ connection: props.connection,
91
+ controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
92
+ healthy,
93
+ workerNodes: nodeCsv(props.workerNodes),
94
+ });
95
+
96
+ /**
97
+ * ⚠️ A ROW SAVED BEFORE `connection` EXISTED HAS NONE — `right` is `undefined` at runtime despite
98
+ * the type, and reading `right.auth` threw on a healthy cluster. Missing means "update".
99
+ */
100
+ const sameConnection = (
101
+ left: TalosOpenBaoConnection,
102
+ right: TalosOpenBaoConnection | undefined,
103
+ ): boolean =>
104
+ right?.auth?.kind === left.auth.kind &&
105
+ right.auth.uid === left.auth.uid &&
106
+ // ⚠️ A row that still carries the legacy `auth.cluster` alias must update, so it leaves state.
107
+ !Object.hasOwn(right.auth, 'cluster');
108
+
73
109
  const healthArgs = (props: ClusterHealthProps, waitTimeout: string) => {
74
110
  const args = ['health', '--wait-timeout', waitTimeout];
75
111
  const cp = nodeCsv(props.controlPlaneNodes);
@@ -113,11 +149,7 @@ const check = (props: ClusterHealthProps, waitTimeout: string) =>
113
149
  nodes: [contact],
114
150
  talosconfigPath: credential.talosconfigPath,
115
151
  });
116
- return {
117
- controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
118
- healthy: true,
119
- workerNodes: nodeCsv(props.workerNodes),
120
- };
152
+ return attributes(props, true);
121
153
  }),
122
154
  );
123
155
 
@@ -132,13 +164,7 @@ const isTalosError = (cause: unknown): cause is TalosError => cause instanceof T
132
164
  /** ★ EXPORTED for talos-cluster-health.test.ts — see talos-bootstrap.ts's own note on the pattern. */
133
165
  export const readClusterHealth = (props: ClusterHealthProps) =>
134
166
  check(props, '5s').pipe(
135
- Effect.catchIf(isTalosError, () =>
136
- Effect.succeed({
137
- controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
138
- healthy: false,
139
- workerNodes: nodeCsv(props.workerNodes),
140
- }),
141
- ),
167
+ Effect.catchIf(isTalosError, () => Effect.succeed(attributes(props, false))),
142
168
  );
143
169
 
144
170
  export const diffClusterHealth = (
@@ -146,9 +172,15 @@ export const diffClusterHealth = (
146
172
  output: ClusterHealthAttributes | undefined,
147
173
  ) =>
148
174
  Effect.gen(function* () {
175
+ // ⛔ `cluster: health` hands this connection to workloads: refuse an Output here, while it is
176
+ // still an Input (`literal-connection.ts`).
177
+ const refused = literalConnectionRefusal((news as { connection?: unknown }).connection);
178
+ if (refused !== undefined) return yield* Effect.fail(refused);
149
179
  if (output === undefined || !isResolved(news)) return undefined;
150
180
  const live = yield* readClusterHealth(news);
151
- if (live.healthy) return { action: 'noop' } as const;
181
+ if (live.healthy && sameConnection(live.connection, output.connection)) {
182
+ return { action: 'noop' } as const;
183
+ }
152
184
  return { action: 'update' } as const;
153
185
  });
154
186