@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,110 @@
1
+ /**
2
+ * talosctl binary override. Fake spawner only — the named path is never executed (it only has to
3
+ * exist and be a trusted file; the fake answers `version --client` with the pinned version).
4
+ */
5
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { afterAll, expect, test } from 'bun:test';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
11
+ import { type FakeCall, fakeSpawner } from './fake-process.ts';
12
+ import { trustBoundaryForTests } from './trust-boundary.seam.ts';
13
+ import { TALOSCTL_BINARY_ENV, talosctl } from './talosctl.ts';
14
+
15
+ // ⚠️ A real, vetted file in a private directory: the system `sh` or `bun` may be a symlink or live
16
+ // in a writable directory, which the vetting (correctly) refuses.
17
+ const dir = mkdtempSync(join(tmpdir(), 'hf-talosctl-'));
18
+ chmodSync(dir, 0o755);
19
+ trustBoundaryForTests(dir);
20
+ const fixture = join(dir, 'talosctl');
21
+ writeFileSync(fixture, '#!/bin/sh\n', { mode: 0o755 });
22
+ chmodSync(fixture, 0o755);
23
+ afterAll(() => rmSync(dir, { force: true, recursive: true }));
24
+
25
+ const run = (binary: string | undefined, calls: FakeCall[]) =>
26
+ Effect.runPromise(
27
+ Effect.provideService(
28
+ talosctl(['version'], {
29
+ talosconfigPath: 'unused-talosconfig.yaml',
30
+ ...(binary === undefined ? {} : { binary }),
31
+ }),
32
+ ChildProcessSpawner.ChildProcessSpawner,
33
+ fakeSpawner(() => ({ stdout: 'Client: Tag: v1.14.2' }), calls),
34
+ ),
35
+ );
36
+
37
+ const withEnv = async (value: string | undefined, body: () => Promise<void>) => {
38
+ const saved = process.env[TALOSCTL_BINARY_ENV];
39
+ if (value === undefined) delete process.env[TALOSCTL_BINARY_ENV];
40
+ else process.env[TALOSCTL_BINARY_ENV] = value;
41
+ try {
42
+ await body();
43
+ } finally {
44
+ if (saved === undefined) delete process.env[TALOSCTL_BINARY_ENV];
45
+ else process.env[TALOSCTL_BINARY_ENV] = saved;
46
+ }
47
+ };
48
+
49
+ test('the binary option wins over HF_TALOSCTL', async () => {
50
+ await withEnv('/definitely/not/here/talosctl', async () => {
51
+ const calls: FakeCall[] = [];
52
+ await run(fixture, calls);
53
+ expect(calls[0]?.command).toBe(fixture);
54
+ });
55
+ });
56
+
57
+ test('HF_TALOSCTL selects the binary when the option is omitted', async () => {
58
+ await withEnv(fixture, async () => {
59
+ const calls: FakeCall[] = [];
60
+ await run(undefined, calls);
61
+ expect(calls[0]?.command).toBe(fixture);
62
+ });
63
+ });
64
+
65
+ const withPath = async (value: string, body: () => Promise<void>) => {
66
+ const saved = process.env['PATH'];
67
+ process.env['PATH'] = value;
68
+ try {
69
+ await body();
70
+ } finally {
71
+ if (saved === undefined) delete process.env['PATH'];
72
+ else process.env['PATH'] = saved;
73
+ }
74
+ };
75
+
76
+ test('the default talosctl is resolved on PATH, vetted and version-checked by absolute path', async () => {
77
+ await withEnv(undefined, () =>
78
+ withPath(dir, async () => {
79
+ const calls: FakeCall[] = [];
80
+ await run(undefined, calls);
81
+ expect(calls.map((call) => call.command)).toEqual([fixture, fixture]);
82
+ }),
83
+ );
84
+ });
85
+
86
+ test('the default talosctl on PATH is refused when it reports another version', async () => {
87
+ await withEnv(undefined, () =>
88
+ withPath(dir, async () => {
89
+ const outcome = await Effect.runPromise(
90
+ Effect.provideService(
91
+ talosctl(['version'], { talosconfigPath: 'unused.yaml' }),
92
+ ChildProcessSpawner.ChildProcessSpawner,
93
+ fakeSpawner(() => ({ stdout: 'Client: Tag: v1.13.8' })),
94
+ ),
95
+ ).catch((e: unknown) => String(e));
96
+ expect(outcome).toContain('is not talosctl v1.14.2');
97
+ }),
98
+ );
99
+ });
100
+
101
+ test('a default talosctl that is not on PATH is refused, never run by bare name', async () => {
102
+ await withEnv(undefined, () =>
103
+ withPath(join(dir, 'empty'), async () => {
104
+ const calls: FakeCall[] = [];
105
+ const outcome = await run(undefined, calls).catch((e: unknown) => String(e));
106
+ expect(outcome).toContain('not found on PATH');
107
+ expect(calls).toEqual([]);
108
+ }),
109
+ );
110
+ });
@@ -10,24 +10,65 @@
10
10
  * ⛔ DO NOT LOG STDOUT ON FAILURE. `talosctl kubeconfig` and error paths have been observed in the
11
11
  * wild to include credential material; stderr is the safe diagnostic channel.
12
12
  */
13
+ import * as Duration from 'effect/Duration';
13
14
  import * as Effect from 'effect/Effect';
14
15
  import * as Stream from 'effect/Stream';
15
16
  import * as ChildProcess from 'effect/process/ChildProcess';
16
17
  import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
18
+ import {
19
+ DEFAULT_TALOSCTL_BINARY,
20
+ TALOSCTL_BINARY_ENV,
21
+ TalosBinaryRefused,
22
+ checkBinaryPath,
23
+ resolveDefault,
24
+ } from './talosctl-binary.ts';
25
+
26
+ export { DEFAULT_TALOSCTL_BINARY, TALOSCTL_BINARY_ENV, TalosBinaryRefused };
17
27
 
18
28
  export class TalosError extends Error {
19
29
  constructor(
20
30
  readonly command: string,
21
31
  readonly exitCode: number,
22
32
  detail: string,
33
+ binary = DEFAULT_TALOSCTL_BINARY,
23
34
  ) {
24
- super(`talosctl ${command} -> ${String(exitCode)}: ${detail}`);
35
+ super(`${binary} ${command} -> ${String(exitCode)}: ${detail}`);
25
36
  this.name = 'TalosError';
26
37
  }
27
38
  }
28
39
 
40
+ /** The Talos client version this family is written against (`--client` of the pinned binary). */
41
+ export const TALOSCTL_PINNED_VERSION = 'v1.14.2';
42
+
43
+ const talosctlBinary = (override: string | undefined): string => {
44
+ const fromOption = override?.trim();
45
+ if (fromOption !== undefined && fromOption !== '') return fromOption;
46
+ const fromEnv = process.env[TALOSCTL_BINARY_ENV]?.trim();
47
+ if (fromEnv !== undefined && fromEnv !== '') return fromEnv;
48
+ return DEFAULT_TALOSCTL_BINARY;
49
+ };
50
+
51
+ /**
52
+ * ⛔ THE CHILD GETS A MINIMAL ENV, NOT `process.env`: BAO_TOKEN and every other credential in the
53
+ * caller's environment stay out of an override that is only vetted by path and version. The
54
+ * talosconfig travels as a file argument, so the binary needs nothing else but `PATH`/`HOME`.
55
+ */
56
+ const talosctlEnv = (): Record<string, string> => {
57
+ const env: Record<string, string> = {};
58
+ for (const name of ['PATH', 'HOME', 'TMPDIR']) {
59
+ const value = process.env[name];
60
+ if (value !== undefined && value !== '') env[name] = value;
61
+ }
62
+ return env;
63
+ };
64
+
29
65
  export type TalosRunOptions = {
30
66
  readonly talosconfigPath: string;
67
+ /**
68
+ * `talosctl` executable. Wins over {@link TALOSCTL_BINARY_ENV}.
69
+ * @default `talosctl` on `PATH`, or `HF_TALOSCTL` when that variable is set.
70
+ */
71
+ readonly binary?: string;
31
72
  readonly nodes?: readonly string[];
32
73
  readonly endpoints?: readonly string[];
33
74
  /**
@@ -39,21 +80,16 @@ export type TalosRunOptions = {
39
80
  readonly insecure?: boolean;
40
81
  };
41
82
 
42
- /** Run `talosctl <args…>`. Returns trimmed stdout, or '' when empty. */
43
- export const talosctl = (args: readonly string[], options: TalosRunOptions) =>
83
+ const capture = (binary: string, argv: readonly string[]) =>
44
84
  Effect.gen(function* () {
45
85
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
46
- const argv = [
47
- ...args,
48
- '--talosconfig',
49
- options.talosconfigPath,
50
- ...(options.nodes === undefined ? [] : ['--nodes', options.nodes.join(',')]),
51
- ...(options.endpoints === undefined ? [] : ['--endpoints', options.endpoints.join(',')]),
52
- ...(options.insecure === true ? ['--insecure'] : []),
53
- ];
54
- const result = yield* ChildProcess.make('talosctl', argv, {
55
- detached: false,
56
- extendEnv: true,
86
+ return yield* ChildProcess.make(binary, argv, {
87
+ // ⛔ No `detached: false`, plus `forceKillAfter`, as credentials.ts's bao spawn: the child
88
+ // leads its own process group (killed whole on interrupt) and a SIGTERM-ignoring talosctl
89
+ // is SIGKILLed 1 s later instead of blocking interruption.
90
+ env: talosctlEnv(),
91
+ extendEnv: false,
92
+ forceKillAfter: Duration.seconds(1),
57
93
  stderr: 'pipe',
58
94
  stdin: 'ignore',
59
95
  stdout: 'pipe',
@@ -79,11 +115,61 @@ export const talosctl = (args: readonly string[], options: TalosRunOptions) =>
79
115
  ),
80
116
  Effect.scoped,
81
117
  );
118
+ });
119
+
120
+ /** ⚠️ EXACT TOKEN MATCH: `includes` would accept v1.14.20 for v1.14.2. */
121
+ const reportsPinnedVersion = (stdout: string) =>
122
+ stdout.split(/[^\w.+-]+/).includes(TALOSCTL_PINNED_VERSION);
123
+
124
+ /**
125
+ * ⛔ AN OVERRIDE MUST REPORT THE PINNED CLIENT VERSION (`version --client`, no talosconfig, so
126
+ * nothing secret reaches an untrusted binary before it is vetted).
127
+ */
128
+ const checkOverrideVersion = (binary: string, source: string) =>
129
+ capture(binary, ['version', '--client']).pipe(
130
+ Effect.flatMap((out) =>
131
+ out.exitCode === 0 && reportsPinnedVersion(out.stdout)
132
+ ? Effect.void
133
+ : Effect.fail(
134
+ new TalosBinaryRefused(
135
+ 'version --client',
136
+ `${source} is not talosctl ${TALOSCTL_PINNED_VERSION} (exit ${String(out.exitCode)})`,
137
+ binary,
138
+ ),
139
+ ),
140
+ ),
141
+ );
142
+
143
+ /** Run `talosctl <args…>`. Returns trimmed stdout, or '' when empty. */
144
+ export const talosctl = (args: readonly string[], options: TalosRunOptions) =>
145
+ Effect.gen(function* () {
146
+ const requested = talosctlBinary(options.binary);
147
+ const resolved =
148
+ requested === DEFAULT_TALOSCTL_BINARY ? yield* resolveDefault(requested) : requested;
149
+ const fromOption = options.binary?.trim();
150
+ const source =
151
+ requested === DEFAULT_TALOSCTL_BINARY
152
+ ? 'talosctl on PATH'
153
+ : fromOption !== undefined && fromOption !== ''
154
+ ? 'the `binary` option'
155
+ : TALOSCTL_BINARY_ENV;
156
+ // ⛔ Launch exactly the vetted canonical path, for the probe and the credentialed call alike.
157
+ const binary = yield* checkBinaryPath(resolved, source);
158
+ yield* checkOverrideVersion(binary, source);
159
+ const argv = [
160
+ ...args,
161
+ '--talosconfig',
162
+ options.talosconfigPath,
163
+ ...(options.nodes === undefined ? [] : ['--nodes', options.nodes.join(',')]),
164
+ ...(options.endpoints === undefined ? [] : ['--endpoints', options.endpoints.join(',')]),
165
+ ...(options.insecure === true ? ['--insecure'] : []),
166
+ ];
167
+ const result = yield* capture(binary, argv);
82
168
 
83
169
  const command = args.join(' ');
84
170
  if (result.exitCode !== 0) {
85
171
  return yield* Effect.fail(
86
- new TalosError(command, result.exitCode, result.stderr.slice(0, 300)),
172
+ new TalosError(command, result.exitCode, result.stderr.slice(0, 300), binary),
87
173
  );
88
174
  }
89
175
  return result.stdout;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The shipped trust-boundary surface. ⛔ The talosctl ancestor walk stops early above a
3
+ * registered directory, so anything that can register a directory can switch that vetting off —
4
+ * the module a consumer imports must therefore offer no way to register one. The register
5
+ * function lives in trust-boundary.seam.ts, which the published tarball excludes (package.json
6
+ * `files`, asserted by scripts/smoke.ts), so a non-test import of this module cannot reach it.
7
+ */
8
+ import { expect, test } from 'bun:test';
9
+ import * as shipped from './trust-boundary.ts';
10
+
11
+ test('the shipped module exposes only the read-only boundary query', () => {
12
+ expect(Object.keys(shipped).sort()).toEqual(['isTrustedBoundary']);
13
+ });
14
+
15
+ test('isTrustedBoundary answers false for an unregistered directory', () => {
16
+ expect(shipped.isTrustedBoundary('/definitely/not/registered')).toBe(false);
17
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Which directories the talosctl ancestor walk (talosctl-binary.ts) may stop above.
3
+ *
4
+ * ⛔ READ-ONLY SHIPPED SURFACE, TEST-ONLY BY CONSTRUCTION. This is the only module the walk
5
+ * consults, and its only export is the query below — the register function lives in
6
+ * `trust-boundary.seam.ts`, which the published tarball does not contain (package.json `files`
7
+ * excludes it; scripts/smoke.ts asserts that). A non-test import of this module therefore
8
+ * cannot stop the walk early: `trustBoundaryForTests` is unreachable, the set it feeds is not
9
+ * exported, and in a consumer install the dynamic pull below finds no seam, keeps the empty
10
+ * set and every ancestor of a binary is vetted (fail closed). The seam is pulled with a
11
+ * COMPUTED specifier so no bundler inlines it into the shipped module.
12
+ *
13
+ * Plain syntax on purpose: fake-process.ts's graph is loaded under Node's strip-only
14
+ * TypeScript (node-connect.harness.ts), which cannot load talosctl-binary.ts (parameter
15
+ * properties). Not exported from the subpath index.
16
+ */
17
+ import { realpathSync } from 'node:fs';
18
+
19
+ let boundaries: ReadonlySet<string> = new Set<string>();
20
+
21
+ /**
22
+ * Canonical compare: the walk hands over a real path, but the boundary is a security decision —
23
+ * resolving again makes it independent of what a caller (now or future) cached. ⛔ A directory
24
+ * that cannot be resolved is answered FALSE, never thrown: it is certainly not a registered
25
+ * boundary, and the walk must treat "no boundary" as "vet everything" (fail closed).
26
+ */
27
+ export const isTrustedBoundary = (dir: string): boolean => {
28
+ try {
29
+ return boundaries.has(realpathSync(dir));
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+
35
+ const seamSpecifier = './trust-boundary' + '.seam.ts';
36
+ void import(seamSpecifier).then(
37
+ (seam: { boundaries: ReadonlySet<string> }) => {
38
+ boundaries = seam.boundaries;
39
+ },
40
+ () => {
41
+ // Published package: no seam in the tarball — the set stays empty and the walk is never
42
+ // shortened. In-repo this only fires if the seam file was deleted; boundary-dependent tests
43
+ // then fail loudly instead of passing short-handed.
44
+ },
45
+ );
@@ -6,6 +6,7 @@
6
6
  * reconcile time; tests use fixtures only.
7
7
  */
8
8
  import { createHash } from 'node:crypto';
9
+ import { parseYaml } from './yaml-parse.ts';
9
10
 
10
11
  /** Trim trailing whitespace only — leading space in YAML is meaningful. */
11
12
  export const canonicalText = (text: string) => text.trimEnd();
@@ -39,12 +40,7 @@ export const configDigest = (text: string) => sha256(text);
39
40
  * silently picking one would repeat the exact shipped bug.
40
41
  */
41
42
  export const extractMachineConfigSpec = (wrapperYaml: string): string | undefined => {
42
- let doc: unknown;
43
- try {
44
- doc = Bun.YAML.parse(wrapperYaml);
45
- } catch {
46
- return undefined;
47
- }
43
+ let doc = parseYaml(wrapperYaml);
48
44
  if (Array.isArray(doc)) {
49
45
  if (doc.length !== 1) return undefined;
50
46
  doc = doc[0];
@@ -77,12 +73,9 @@ export const kubeconfigMetadata = (
77
73
  yamlText: string,
78
74
  contextName: string,
79
75
  ): { endpoint: string; caFingerprint: string; clientFingerprint: string } | undefined => {
80
- let doc: KubeconfigDoc;
81
- try {
82
- doc = Bun.YAML.parse(yamlText) as KubeconfigDoc;
83
- } catch {
84
- return undefined;
85
- }
76
+ const parsed = parseYaml(yamlText);
77
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
78
+ const doc = parsed as KubeconfigDoc;
86
79
 
87
80
  const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;
88
81
  const clusterName = ctx?.cluster;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `parseYaml` — a syntax error is "not a document"; nothing else is swallowed. And the adapter's
3
+ * kubeconfig reader no longer reports a parser/runtime fault as an unreadable kubeconfig.
4
+ */
5
+ import { expect, spyOn, test } from 'bun:test';
6
+ import { kubeconfigTransport } from './kubeconfig-doc.ts';
7
+ import { parseYaml } from './yaml-parse.ts';
8
+
9
+ test('valid YAML parses and a syntax error is undefined', () => {
10
+ expect(parseYaml('a: 1\nb: [x, y]\n')).toEqual({ a: 1, b: ['x', 'y'] });
11
+ expect(parseYaml('a: [unclosed\n')).toBeUndefined();
12
+ });
13
+
14
+ test('an unresolved tag beside key material emits no warning carrying the source', () => {
15
+ // ⛔ yaml's default logLevel is 'warn': process.emitWarning with the SOURCE TEXT. Reproduced in
16
+ // hunt round 5: the whole client-key-data landed on stderr while parsing still succeeded.
17
+ // A fresh random needle per run: a fixed high-entropy literal here is exactly what the secret scan refuses.
18
+ const secret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64');
19
+ const text = `users:\n - user:\n client-key-data: ${secret}\n other: !nosuchtag value\n`;
20
+ const emitted: string[] = [];
21
+ const warn = spyOn(process, 'emitWarning').mockImplementation(((w: unknown) => {
22
+ emitted.push(String(w instanceof Error ? w.message : w));
23
+ }) as never);
24
+ const err = spyOn(process.stderr, 'write').mockImplementation(((c: unknown) => {
25
+ emitted.push(String(c));
26
+ return true;
27
+ }) as never);
28
+ try {
29
+ expect(parseYaml(text)).toBeDefined();
30
+ } finally {
31
+ warn.mockRestore();
32
+ err.mockRestore();
33
+ }
34
+ expect(emitted.join('\n')).not.toContain(secret);
35
+ expect(emitted).toEqual([]);
36
+ });
37
+
38
+ test('kubeconfigTransport returns undefined for a non-document, not for a fault', () => {
39
+ expect(kubeconfigTransport('a: [unclosed\n', 'admin@c1')).toBeUndefined();
40
+ expect(kubeconfigTransport('- just\n- a list\n', 'admin@c1')).toBeUndefined();
41
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The one YAML parser of the Talos family — runtime-neutral, so it runs under Bun AND Node.
3
+ *
4
+ * ⛔ NOT `Bun.YAML`. The published `dist/` runs on Node for consumers (AGENTS.md: Bun is the
5
+ * toolchain, not the runtime). `Bun.YAML` is a missing global there: the old code swallowed that
6
+ * ReferenceError and reported every kubeconfig as unreadable (reproduced on Node 22: the adapter
7
+ * returned no transport). ★ `yaml` is the parser alchemy itself depends on (`yaml ^2.9.0`), so no
8
+ * new package enters a consumer's tree.
9
+ * ⚠️ ONLY a YAML syntax error is `undefined` ("this is not a document"). Anything else — a broken
10
+ * import, a runtime fault — propagates, so a parser/runtime defect is never dressed up as bad
11
+ * vault content.
12
+ */
13
+ import { YAMLParseError, parse } from 'yaml';
14
+
15
+ /** Parse one YAML document, or `undefined` when the text is not valid YAML. */
16
+ export const parseYaml = (text: string): unknown => {
17
+ try {
18
+ // ⛔ yaml reports unresolved tags etc. via process.emitWarning with the SOURCE TEXT in the
19
+ // message (reproduced, hunt round 5: a kubeconfig with an unknown tag beside client-key-data
20
+ // put the whole key on stderr). `logLevel: 'error'` silences warnings (yaml options.d.ts:
21
+ // LogLevelId; doc.warnings are never emitted) and `prettyErrors: false` keeps source excerpts
22
+ // out of any thrown error's message; syntax errors still throw YAMLParseError.
23
+ return parse(text, { logLevel: 'error', prettyErrors: false });
24
+ } catch (error) {
25
+ if (error instanceof YAMLParseError) return undefined;
26
+ throw error;
27
+ }
28
+ };
@@ -143,6 +143,8 @@ export interface FakeEngine {
143
143
  * `verify.ts`'s `readWithState`, which already wraps that same call in `Effect.catchCause`.
144
144
  */
145
145
  readonly drift: (body: Body) => Promise<Record<string, DriftRow>>;
146
+ /** `alchemy drift --repair` (`Alchemy.Drift.repair`): read, then reconcile what drifted. */
147
+ readonly repair: (body: Body) => Promise<Record<string, DriftRow>>;
146
148
  /**
147
149
  * Every state row the store holds, as JSON — what a Postgres store would persist. ★ For tests
148
150
  * that must prove a value NEVER reaches state (proxmox/pbs-notification-target-state.test.ts).
@@ -173,6 +175,8 @@ export const engineOver = <ROut, E, RIn>(
173
175
  stage: string;
174
176
  }) => Effect.Effect<{ resources: Record<string, DriftRow> }, unknown>;
175
177
 
178
+ const repairDrift = Alchemy.Drift.repair as unknown as typeof detectDrift;
179
+
176
180
  const run = <A>(body: Body, next: (compiled: Compiled) => Effect.Effect<A, unknown, unknown>) =>
177
181
  Effect.runPromise(
178
182
  Effect.gen(function* () {
@@ -208,6 +212,12 @@ export const engineOver = <ROut, E, RIn>(
208
212
  Effect.map((detected) => detected.resources),
209
213
  ),
210
214
  ),
215
+ repair: (body) =>
216
+ run(body, (compiled) =>
217
+ repairDrift({ name: compiled.name, stage: compiled.stage }).pipe(
218
+ Effect.map((detected) => detected.resources),
219
+ ),
220
+ ),
211
221
  };
212
222
  };
213
223
 
@@ -1,56 +0,0 @@
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 Stream from 'effect/Stream';
15
- import type { Command } from 'effect/process/ChildProcess';
16
- import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
17
- export type FakeResult = {
18
- readonly stdout?: string;
19
- readonly stderr?: string;
20
- readonly exitCode?: number;
21
- };
22
- export type FakeCall = {
23
- readonly command: string;
24
- readonly args: readonly string[];
25
- /**
26
- * Decoded stdin text, present only when the real caller piped a `Stream` in (K-talos-first-boot,
27
- * `credentials-write.ts`'s `writeKvValue`) — the whole reason this fake exists is to prove a
28
- * value went in via stdin and never via argv, so a test needs to see it.
29
- */
30
- readonly stdin?: string;
31
- };
32
- export type FakeHandler = (call: FakeCall) => FakeResult;
33
- /**
34
- * Build a fake `ChildProcessSpawner.Service` from `handler`, which sees every command's
35
- * executable + argv (+ decoded stdin, when piped) and returns canned stdout/stderr/exitCode.
36
- * `credentials.ts` and `talosctl.ts` only ever build `StandardCommand`s (never a piped command),
37
- * so that is the only shape this fake accepts — a `PipedCommand` is a test-authoring mistake, not
38
- * something to fake.
39
- */
40
- export declare const fakeSpawner: (handler: FakeHandler, calls?: FakeCall[]) => {
41
- spawn(command: Command): Effect.Effect<ChildProcessSpawner.ChildProcessHandle, import("effect/PlatformError").PlatformError, import("effect/Scope").Scope>;
42
- exitCode(command: Command): Effect.Effect<ChildProcessSpawner.ExitCode, import("effect/PlatformError").PlatformError>;
43
- streamString(command: Command, options?: {
44
- readonly includeStderr?: boolean | undefined;
45
- }): Stream.Stream<string, import("effect/PlatformError").PlatformError>;
46
- streamLines(command: Command, options?: {
47
- readonly includeStderr?: boolean | undefined;
48
- }): Stream.Stream<string, import("effect/PlatformError").PlatformError>;
49
- lines(command: Command, options?: {
50
- readonly includeStderr?: boolean | undefined;
51
- }): Effect.Effect<Array<string>, import("effect/PlatformError").PlatformError>;
52
- string(command: Command, options?: {
53
- readonly includeStderr?: boolean | undefined;
54
- }): Effect.Effect<string, import("effect/PlatformError").PlatformError>;
55
- };
56
- //# sourceMappingURL=fake-process.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fake-process.d.ts","sourceRoot":"","sources":["../../src/talos/fake-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAC;AAE1E,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,QAAQ,KAAK,UAAU,CAAC;AAsCzD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,YAAa,WAAW,UAAS,QAAQ,EAAE;;;;;;;;;;;;;;;CAehE,CAAC"}
@@ -1,60 +0,0 @@
1
- /**
2
- * Shared fixtures for talos-machine-config.test.ts and machine-config-poll.test.ts — split out
3
- * so neither test file needs to duplicate the fake `bao`/`talosctl` dispatcher (2026-09-26, K-A3).
4
- *
5
- * ⚠️ FIXTURES ONLY. `CONFIG_TEXT` is a plain marker string, never anything resembling a real
6
- * Talos machine config, CA key or bootstrap token. No provider imports this file.
7
- */
8
- import * as Effect from 'effect/Effect';
9
- import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
10
- import { type FakeCall } from './fake-process.ts';
11
- import type { MachineConfigProps } from './talos-machine-config.ts';
12
- export declare const CONFIG_TEXT = "machine:\n type: worker\n";
13
- export declare const PIN: string;
14
- export declare const TARGET: {
15
- cluster: string;
16
- mount: string;
17
- };
18
- export declare const FAST_POLL: {
19
- immediate: {
20
- intervalMs: number;
21
- maxAttempts: number;
22
- };
23
- rebootish: {
24
- intervalMs: number;
25
- maxAttempts: number;
26
- };
27
- };
28
- export declare const props: (overrides?: Partial<MachineConfigProps>) => MachineConfigProps;
29
- /** Shape REASONED from talosctl's `get machineconfig -o yaml` — see values.ts's own header. */
30
- export declare const wrapperFor: (specText: string, version?: string) => string;
31
- /** Routes fake `bao kv get` (config + talosconfig keys) and `talosctl` calls in one place. */
32
- export declare const dispatcher: (opts: {
33
- configText?: string;
34
- liveWrapper?: () => {
35
- stdout?: string;
36
- exitCode?: number;
37
- };
38
- applyExitCode?: number;
39
- /**
40
- * Controls the fix-first #1 maintenance-mode probe (`talosctl version --insecure`), reached only
41
- * when the authenticated `get machineconfig` above fails. Defaults to `false` so a bare failing
42
- * `liveWrapper` (most tests) still exercises the "both reads fail → propagate" path rather than
43
- * silently succeeding at the probe.
44
- */
45
- maintenanceReachable?: boolean;
46
- }) => (call: FakeCall) => {
47
- stdout?: string;
48
- exitCode?: number;
49
- } | {
50
- exitCode: number;
51
- stderr: string;
52
- };
53
- export declare const run: <A, E>(effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>, handler: (c: FakeCall) => {
54
- stdout?: string;
55
- stderr?: string;
56
- exitCode?: number;
57
- }, calls?: FakeCall[]) => Promise<A>;
58
- /** `Data.TaggedError` stamps `_tag`, not `.message` — see talos-errors.ts. */
59
- export declare const hasTag: (tag: string) => (error: unknown) => boolean;
60
- //# sourceMappingURL=machine-config-fixtures.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"machine-config-fixtures.d.ts","sourceRoot":"","sources":["../../src/talos/machine-config-fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,KAAK,QAAQ,EAAe,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAGpE,eAAO,MAAM,WAAW,+BAA+B,CAAC;AACxD,eAAO,MAAM,GAAG,QAA4B,CAAC;AAC7C,eAAO,MAAM,MAAM;IAAK,OAAO;IAAQ,KAAK;CAAc,CAAC;AAC3D,eAAO,MAAM,SAAS;IACpB,SAAS;QAAI,UAAU;QAAK,WAAW;;IACvC,SAAS;QAAI,UAAU;QAAK,WAAW;;CACxC,CAAC;AAEF,eAAO,MAAM,KAAK,eAAe,OAAO,CAAC,kBAAkB,CAAC,KAAQ,kBAMlE,CAAC;AAEH,+FAA+F;AAC/F,eAAO,MAAM,UAAU,aAAc,MAAM,6BAC6D,CAAC;AAEzG,8FAA8F;AAC9F,eAAO,MAAM,UAAU,SAAU;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,YAEe,QAAQ;aAXS,MAAM;eAAa,MAAM;;;;CAgCzD,CAAC;AAEF,eAAO,MAAM,GAAG,GAAI,CAAC,EAAE,CAAC,UACd,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,mBAAmB,CAAC,mBAAmB,CAAC,WAC3D,CAAC,CAAC,EAAE,QAAQ,KAAK;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,UAC1E,QAAQ,EAAE,eAQhB,CAAC;AAEJ,8EAA8E;AAC9E,eAAO,MAAM,MAAM,QAAS,MAAM,aAAa,OAAO,YACH,CAAC"}