@homeflare/alchemy 0.47.0 → 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 (130) hide show
  1. package/README.md +6 -32
  2. package/dist/talos/bao-env.d.ts +7 -0
  3. package/dist/talos/bao-env.d.ts.map +1 -0
  4. package/dist/talos/cluster-adapter-errors.d.ts +134 -0
  5. package/dist/talos/cluster-adapter-errors.d.ts.map +1 -0
  6. package/dist/talos/cluster-adapter.d.ts +115 -0
  7. package/dist/talos/cluster-adapter.d.ts.map +1 -0
  8. package/dist/talos/cluster-identity.d.ts +28 -0
  9. package/dist/talos/cluster-identity.d.ts.map +1 -0
  10. package/dist/talos/cluster-transport.d.ts +64 -0
  11. package/dist/talos/cluster-transport.d.ts.map +1 -0
  12. package/dist/talos/credentials-write.d.ts.map +1 -1
  13. package/dist/talos/credentials.d.ts +10 -19
  14. package/dist/talos/credentials.d.ts.map +1 -1
  15. package/dist/talos/fake-apiserver.d.ts +9 -0
  16. package/dist/talos/fake-apiserver.d.ts.map +1 -0
  17. package/dist/talos/index.d.ts +7 -0
  18. package/dist/talos/index.d.ts.map +1 -1
  19. package/dist/talos/index.js +669 -167
  20. package/dist/talos/index.js.map +22 -10
  21. package/dist/talos/kubeconfig-attrs.d.ts +14 -0
  22. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -1
  23. package/dist/talos/kubeconfig-doc.d.ts +23 -0
  24. package/dist/talos/kubeconfig-doc.d.ts.map +1 -0
  25. package/dist/talos/kubeconfig.d.ts +0 -12
  26. package/dist/talos/kubeconfig.d.ts.map +1 -1
  27. package/dist/talos/literal-connection.d.ts +16 -0
  28. package/dist/talos/literal-connection.d.ts.map +1 -0
  29. package/dist/talos/machine-config-read.d.ts +1 -1
  30. package/dist/talos/machine-config-read.d.ts.map +1 -1
  31. package/dist/talos/mint-temp-file.d.ts +20 -0
  32. package/dist/talos/mint-temp-file.d.ts.map +1 -0
  33. package/dist/talos/node-temp.harness.d.ts +2 -0
  34. package/dist/talos/node-temp.harness.d.ts.map +1 -0
  35. package/dist/talos/talos-bootstrap-etcd.d.ts +1 -1
  36. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -1
  37. package/dist/talos/talos-cluster-health.d.ts +15 -15
  38. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  39. package/dist/talos/talos-cluster-identity.d.ts +65 -0
  40. package/dist/talos/talos-cluster-identity.d.ts.map +1 -0
  41. package/dist/talos/talosctl-binary.d.ts +43 -0
  42. package/dist/talos/talosctl-binary.d.ts.map +1 -0
  43. package/dist/talos/talosctl.d.ts +12 -15
  44. package/dist/talos/talosctl.d.ts.map +1 -1
  45. package/dist/talos/trust-boundary.d.ts +8 -0
  46. package/dist/talos/trust-boundary.d.ts.map +1 -0
  47. package/dist/talos/values.d.ts.map +1 -1
  48. package/dist/talos/yaml-parse.d.ts +3 -0
  49. package/dist/talos/yaml-parse.d.ts.map +1 -0
  50. package/dist/verify/fake-engine.d.ts +2 -0
  51. package/dist/verify/fake-engine.d.ts.map +1 -1
  52. package/docs/peers.md +32 -5
  53. package/docs/talos-openbao-adapter.md +44 -0
  54. package/docs/upstream-conformance-bun.md +61 -24
  55. package/package.json +116 -2
  56. package/src/talos/bao-env.ts +16 -0
  57. package/src/talos/bao-vetting.test.ts +97 -0
  58. package/src/talos/cluster-adapter-deadline.test.ts +110 -0
  59. package/src/talos/cluster-adapter-errors.ts +192 -0
  60. package/src/talos/cluster-adapter-move.test.ts +129 -0
  61. package/src/talos/cluster-adapter-reconcile.test.ts +165 -0
  62. package/src/talos/cluster-adapter-refusals.test.ts +102 -0
  63. package/src/talos/cluster-adapter-rename.test.ts +87 -0
  64. package/src/talos/cluster-adapter-secrecy.test.ts +243 -0
  65. package/src/talos/cluster-adapter.fixtures.ts +125 -0
  66. package/src/talos/cluster-adapter.test.ts +240 -0
  67. package/src/talos/cluster-adapter.ts +249 -0
  68. package/src/talos/cluster-identity.ts +63 -0
  69. package/src/talos/cluster-transport.ts +110 -0
  70. package/src/talos/credentials-write.ts +27 -5
  71. package/src/talos/credentials.ts +51 -45
  72. package/src/talos/fake-apiserver.ts +55 -0
  73. package/src/talos/fake-process.ts +47 -1
  74. package/src/talos/index.ts +32 -0
  75. package/src/talos/kubeconfig-adapter.test.ts +37 -0
  76. package/src/talos/kubeconfig-attrs.ts +12 -28
  77. package/src/talos/kubeconfig-doc.ts +74 -0
  78. package/src/talos/kubeconfig.fixtures.ts +52 -0
  79. package/src/talos/kubeconfig.test.ts +6 -54
  80. package/src/talos/kubeconfig.ts +18 -34
  81. package/src/talos/literal-connection.ts +59 -0
  82. package/src/talos/mint-temp-file.ts +56 -0
  83. package/src/talos/node-connect.harness.ts +78 -0
  84. package/src/talos/node-connect.test.ts +27 -0
  85. package/src/talos/node-temp.harness.ts +40 -0
  86. package/src/talos/node-temp.test.ts +26 -0
  87. package/src/talos/talos-cluster-health-declare.test.ts +54 -0
  88. package/src/talos/talos-cluster-health-literal.test.ts +69 -0
  89. package/src/talos/talos-cluster-health.test.ts +104 -18
  90. package/src/talos/talos-cluster-health.ts +46 -14
  91. package/src/talos/talos-cluster-identity.test.ts +96 -0
  92. package/src/talos/talos-cluster-identity.ts +130 -0
  93. package/src/talos/talosctl-binary.ts +150 -0
  94. package/src/talos/talosctl-vetting.test.ts +212 -0
  95. package/src/talos/talosctl.test.ts +110 -0
  96. package/src/talos/talosctl.ts +101 -15
  97. package/src/talos/trust-boundary.test.ts +17 -0
  98. package/src/talos/trust-boundary.ts +45 -0
  99. package/src/talos/values.ts +5 -12
  100. package/src/talos/yaml-parse.test.ts +41 -0
  101. package/src/talos/yaml-parse.ts +28 -0
  102. package/src/verify/fake-engine.ts +10 -0
  103. package/dist/talos/fake-process.d.ts +0 -56
  104. package/dist/talos/fake-process.d.ts.map +0 -1
  105. package/dist/talos/machine-config-fixtures.d.ts +0 -60
  106. package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
  107. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
  108. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
  109. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
  110. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
  111. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
  112. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
  113. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
  114. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
  115. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
  116. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
  117. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
  118. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
  119. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
  120. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
  121. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
  122. package/src/release/fixtures/openbao/checksums.txt +0 -83
  123. package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
  124. package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
  125. package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
  126. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
  127. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
  128. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
  129. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
  130. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +0 -8
@@ -4,7 +4,7 @@ Status: extracted from the [conformance ledger](./upstream-conformance.md); its
4
4
  measurements and open findings are preserved below.
5
5
 
6
6
  These are non-test files under `src/` that the standard (S42) says must be portable. "Exp"
7
- marks files exported from a subpath `index.ts`. `git grep` on `origin/main` finds 23 non-test
7
+ marks files exported from a subpath `index.ts`. `git grep` on `origin/main` finds 24 non-test
8
8
  files under `src/` that call `Bun.*` or import `node:*`/`bun:*`. Six of them are outside
9
9
  S42's scope:
10
10
 
@@ -13,9 +13,21 @@ S42's scope:
13
13
  - `proxmox/provision-cli-fake.ts`, which is used only by tests;
14
14
  - `verify/args.ts`, which belongs to the CLI and so is tooling under S43.
15
15
 
16
- That leaves 17 files. The table lists them, plus `provision-cli-fake.ts`, which should move
17
- out of `src/`. The six out-of-scope files still ship in the tarball's `src/`, but no export
16
+ That leaves 18 files. PR 355 brings the count on this branch to 29 by adding
17
+ `talos/talosctl-binary.ts` and `talos/trust-boundary.ts` (both in scope below), making
18
+ `talos/fake-process.ts` node-loading — on `origin/main` it imported only `effect/*`; here it
19
+ imports `node:fs`, `node:path` and `node:url` — and adding `talos/fake-apiserver.ts`,
20
+ `talos/node-connect.harness.ts` and the test seam `talos/trust-boundary.seam.ts`, three more
21
+ Node-loaded loopback fakes that are test-side under S44 like the other fakes. With those three
22
+ fakes and the seam, ten files on this branch are outside S42's scope, leaving 19 in scope. The
23
+ table lists those 19, plus `provision-cli-fake.ts`, which should move out of `src/`. The
24
+ out-of-scope files other than the seam still ship in the tarball's `src/`, but no export
18
25
  reaches them.
26
+ ⚠️ PR 355 red team re-measured 2026-10-06: `talos/credentials.ts` and
27
+ `talos/talos-machine-config.ts` no longer call `Bun.*` or import `node:*`/`bun:*` (the temp-file
28
+ mint moved to `talos/mint-temp-file.ts`, and `credentials.ts` reads through `resolveBao`), so
29
+ they left the table and `talos/mint-temp-file.ts` took their row's place; `talos/credentials-write.ts`
30
+ (always node-loaded, previously unlisted) is the +1 that had made the prose count one short.
19
31
  ⚠️ The first version of this table said 16 files and left out `launchd/job-form.ts`.
20
32
  ⚠️ Corrected 2026-09-22: 14 of the 17 break upstream's rule. `launchd/job-form.ts`,
21
33
  `proxmox/write-only.ts` and `proxmox/pbs-notification-target-wire.ts` use only synchronous
@@ -24,28 +36,46 @@ reaches them.
24
36
  among them `Fly/Secret.ts` and `Railway/Variable.ts`. They were listed under a blanket
25
37
  `node:*` ban that upstream does not have.
26
38
 
27
- | file | API | exp | portable replacement |
28
- | ---------------------------------------------------------- | -------------------------------------------- | --- | -------------------------------------------- |
29
- | `openbao/cloudflare-roles-config.ts` | `Bun.YAML` | yes | parse on the tooling side; pass data in |
30
- | `openbao/digest.ts` (14 importers) | `Bun.CryptoHasher` | — | `Crypto.digest` + hex; keep `canonical()` ⚠️ |
31
- | `openbao/cloudflare-parity-snapshot.ts` | `Bun.file`, `Bun.Glob` | — | `FileSystem` + `Path` |
32
- | `openbao/approle-login-form.ts` | `Bun.file` | — | `FileSystem.readFileString` |
33
- | `openbao/approle-login-result.ts` | `Bun.inspect` | — | a plain formatter |
34
- | `openbao/forgejo-bootstrap.ts` | `Bun.spawn` | — | unreferenced; `ChildProcessSpawner` or drop |
35
- | `talos/credentials.ts` | `Bun.write/file/env/randomUUIDv7`, `node:fs` | — | `FileSystem.makeTempFileScoped` |
36
- | `talos/kubeconfig.ts` | `Bun.file` | yes | `FileSystem` |
37
- | `talos/talos-machine-config.ts` | `Bun.file` | yes | `FileSystem` |
38
- | `talos/values.ts` | `Bun.YAML`, `node:crypto` | — | tooling-side parse; hash may stay |
39
- | `launchd/local-runner.ts` | `node:child_process`, `node:fs/promises` | yes | H3 above |
40
- | `launchd/sudo-stage.ts` | `node:fs/promises`, `node:os`, `node:path` | — | `FileSystem`, `Path` |
41
- | `launchd/job-form.ts` | `node:crypto` (`createHash`) | — | allowed in `Effect.sync` |
42
- | `linux/ssh-runner.ts` | `node:child_process`, `node:crypto` | yes | H3 above |
43
- | `caddy/caddy-http-client.ts` | `node:http`, `node:stream` (unix socket) | yes | inherent — an Effect `HttpClient`, not H3 ⚠️ |
44
- | `proxmox/write-only.ts`, `pbs-notification-target-wire.ts` | `node:crypto`, `node:buffer` | — | allowed in `Effect.sync` ⚠️ |
45
- | `proxmox/provision-cli-fake.ts` | `Bun.spawn`, `node:fs` | — | test-only; move out of `src/` |
39
+ | file | API | exp | portable replacement |
40
+ | ---------------------------------------------------------- | --------------------------------------------------------- | --- | -------------------------------------------- |
41
+ | `openbao/cloudflare-roles-config.ts` | `Bun.YAML` | yes | parse on the tooling side; pass data in |
42
+ | `openbao/digest.ts` (14 importers) | `Bun.CryptoHasher` | — | `Crypto.digest` + hex; keep `canonical()` ⚠️ |
43
+ | `openbao/cloudflare-parity-snapshot.ts` | `Bun.file`, `Bun.Glob` | — | `FileSystem` + `Path` |
44
+ | `openbao/approle-login-form.ts` | `Bun.file` | — | `FileSystem.readFileString` |
45
+ | `openbao/approle-login-result.ts` | `Bun.inspect` | — | a plain formatter |
46
+ | `openbao/forgejo-bootstrap.ts` | `Bun.spawn` | — | unreferenced; `ChildProcessSpawner` or drop |
47
+ | `talos/credentials-write.ts` | `node:fs/promises`, `node:crypto`, `node:os`, `node:path` | — | `FileSystem.makeTempFileScoped` |
48
+ | `talos/mint-temp-file.ts` | `node:fs`, `node:crypto`, `node:os`, `node:path` | — | `FileSystem.makeTempFileScoped` |
49
+ | `talos/kubeconfig.ts` | `Bun.file`, `node:fs` | yes | `FileSystem` |
50
+ | `talos/values.ts` | `node:crypto` | — | allowed in `Effect.sync` |
51
+ | `talos/talosctl-binary.ts` | `node:fs`, `node:path` (binary vetting) ⚠️ | — | recorded difference, below |
52
+ | `talos/trust-boundary.ts` | `node:fs` (`realpathSync`, test seam) | — | recorded difference, below |
53
+ | `launchd/local-runner.ts` | `node:child_process`, `node:fs/promises` | yes | H3 above |
54
+ | `launchd/sudo-stage.ts` | `node:fs/promises`, `node:os`, `node:path` | — | `FileSystem`, `Path` |
55
+ | `launchd/job-form.ts` | `node:crypto` (`createHash`) | — | allowed in `Effect.sync` |
56
+ | `linux/ssh-runner.ts` | `node:child_process`, `node:crypto` | yes | H3 above |
57
+ | `caddy/caddy-http-client.ts` | `node:http`, `node:stream` (unix socket) | yes | inherent — an Effect `HttpClient`, not H3 ⚠️ |
58
+ | `proxmox/write-only.ts`, `pbs-notification-target-wire.ts` | `node:crypto`, `node:buffer` | — | allowed in `Effect.sync` ⚠️ |
59
+ | `proxmox/provision-cli-fake.ts` | `Bun.spawn`, `node:fs` | — | test-only; move out of `src/` |
46
60
 
47
61
  The ⚠️ rows:
48
62
 
63
+ - `talos/talosctl-binary.ts` (added 2026-10-06, hunt round 5; extracted from `talosctl.ts` in
64
+ round 6, where it also walks every ancestor directory): the binary vetting needs `lstat` and a
65
+ synchronous PATH walk. `effect/FileSystem` has `stat` (follows symlinks) and `readLink`
66
+ but no `lstat`, so "not a symlink" cannot be asked of it without a second probe, and the
67
+ Service would become a requirement of every `talosctl` caller and test (the offline fakes
68
+ provide only a `ChildProcessSpawner`). The published `dist/` also runs on Node, where
69
+ `Bun.which` is absent. The calls are synchronous, read-only, inside `Effect.try`; no
70
+ write, no async. Revisit when `FileSystem` gains `lstat`.
71
+
72
+ - `talos/trust-boundary.ts` (added 2026-10-06 by PR 355): the same `node:fs` rationale as
73
+ `talos/talosctl-binary.ts`, which imports it. Its query resolves the directory with
74
+ `realpathSync` so the boundary compare is canonical; the call is synchronous, read-only and
75
+ made inside `Effect.try`. The register function lives in `talos/trust-boundary.seam.ts`
76
+ (test-side, excluded from the tarball), so the shipped module offers no way to stop the walk
77
+ early. Revisit together with `talosctl-binary.ts` when `FileSystem` gains `lstat`.
78
+
49
79
  - `openbao/digest.ts`: every `Bao.*` family persists this digest in state, so a swap must
50
80
  hash the same bytes (`canonical()`, lowercase hex), or every row plans an update.
51
81
  `Crypto.digest` from `effect/Crypto` is in every stack's services and fails with a typed
@@ -63,5 +93,12 @@ The ⚠️ rows:
63
93
  more-portable replacement to point to; the file already conforms to S19 (no async/await,
64
94
  no raw `Promise` — `Effect.callback`/`Effect.tryPromise` throughout).
65
95
 
66
- Test runners: 51 files import `node:test`/`node:assert` (openbao 42, proxmox 7, forgejo 1,
67
- talos 1), and 126 import `bun:test`. S43 says `bun:test`.
96
+ Test runners: 63 files import `node:test`/`node:assert` (openbao 41, proxmox 10, forgejo 1,
97
+ talos 11), and 395 import `bun:test`. S43 says `bun:test`.
98
+
99
+ Measured 2026-10-06 from this worktree with:
100
+ `grep -rl --include='*.ts' -e 'node:test' -e 'node:assert' src/ | wc -l` (per directory, the same
101
+ grep scoped to `src/<area>`), and `grep -rl --include='*.ts' -e 'bun:test' src/ | wc -l`.
102
+
103
+ Re-measured 2026-10-06 (PR 355 red team) — the in-scope table rows above:
104
+ `grep -rlE --include='*.ts' "from ['\"](node|bun):|Bun\\.[A-Za-z]" src/talos/ | grep -v '\\.test\\.ts' | grep -vE 'fake-|harness|\\\\.seam\\\\.'`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -18,6 +18,9 @@
18
18
  "dist",
19
19
  "docs",
20
20
  "src",
21
+ "!src/**/fixtures",
22
+ "!src/**/trust-boundary.seam.ts",
23
+ "!dist/**/trust-boundary.seam.*",
21
24
  "README.md",
22
25
  "LICENSE"
23
26
  ],
@@ -139,7 +142,8 @@
139
142
  "@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.1",
140
143
  "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.5",
141
144
  "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.2",
142
- "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.4.1"
145
+ "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.4.1",
146
+ "yaml": "2.9.1"
143
147
  },
144
148
  "peerDependencies": {
145
149
  "@distilled.cloud/argocd": "1.0.0-rc.13",
@@ -152,5 +156,115 @@
152
156
  "alchemy": "2.0.0-beta.81",
153
157
  "effect": "4.0.1",
154
158
  "mime": "4.1.0"
159
+ },
160
+ "homeflare": {
161
+ "consumer": {
162
+ "pins": {
163
+ "effect": "4.0.1",
164
+ "@effect/ai-openai-compat": "4.0.1",
165
+ "@effect/platform-bun": "4.0.1",
166
+ "@effect/platform-node": "4.0.1",
167
+ "@effect/platform-node-shared": "4.0.1",
168
+ "@effect/sql-pg": "4.0.1",
169
+ "alchemy": "2.0.0-beta.81",
170
+ "@alchemy.run/frontend-frameworks": "2.0.0-beta.81",
171
+ "@distilled.cloud/core": "1.0.0-rc.13",
172
+ "@distilled.cloud/argocd": "1.0.0-rc.13",
173
+ "@distilled.cloud/cloudflare": "1.0.0-rc.13",
174
+ "@distilled.cloud/discord": "1.0.0-rc.13",
175
+ "@distilled.cloud/forgejo": "1.0.0-rc.13",
176
+ "@distilled.cloud/google-workspace": "1.0.0-rc.13",
177
+ "mime": "4.1.0"
178
+ },
179
+ "overrides": {
180
+ "effect": "4.0.1",
181
+ "@effect/platform-bun": "4.0.1",
182
+ "@effect/platform-node": "4.0.1",
183
+ "@effect/platform-node-shared": "4.0.1",
184
+ "rolldown": "1.2.8",
185
+ "redis": "6.3.0"
186
+ },
187
+ "importMoves": [
188
+ {
189
+ "from": "effect/unstable/httpapi/",
190
+ "to": "effect/http-api/"
191
+ },
192
+ {
193
+ "from": "effect/unstable/ai/",
194
+ "to": "effect/ai/"
195
+ },
196
+ {
197
+ "from": "effect/unstable/cli/",
198
+ "to": "effect/cli/"
199
+ },
200
+ {
201
+ "from": "effect/unstable/cluster/",
202
+ "to": "effect/cluster/"
203
+ },
204
+ {
205
+ "from": "effect/unstable/devtools/",
206
+ "to": "effect/devtools/"
207
+ },
208
+ {
209
+ "from": "effect/unstable/encoding/",
210
+ "to": "effect/encoding/"
211
+ },
212
+ {
213
+ "from": "effect/unstable/eventlog/",
214
+ "to": "effect/eventlog/"
215
+ },
216
+ {
217
+ "from": "effect/unstable/http/",
218
+ "to": "effect/http/"
219
+ },
220
+ {
221
+ "from": "effect/unstable/net/",
222
+ "to": "effect/net/"
223
+ },
224
+ {
225
+ "from": "effect/unstable/observability/",
226
+ "to": "effect/observability/"
227
+ },
228
+ {
229
+ "from": "effect/unstable/persistence/",
230
+ "to": "effect/persistence/"
231
+ },
232
+ {
233
+ "from": "effect/unstable/process/",
234
+ "to": "effect/process/"
235
+ },
236
+ {
237
+ "from": "effect/unstable/reactivity/",
238
+ "to": "effect/reactivity/"
239
+ },
240
+ {
241
+ "from": "effect/unstable/rpc/",
242
+ "to": "effect/rpc/"
243
+ },
244
+ {
245
+ "from": "effect/unstable/schema/",
246
+ "to": "effect/schema/"
247
+ },
248
+ {
249
+ "from": "effect/unstable/socket/",
250
+ "to": "effect/socket/"
251
+ },
252
+ {
253
+ "from": "effect/unstable/sql/",
254
+ "to": "effect/sql/"
255
+ },
256
+ {
257
+ "from": "effect/unstable/workers/",
258
+ "to": "effect/workers/"
259
+ },
260
+ {
261
+ "from": "effect/unstable/workflow/",
262
+ "to": "effect/workflow/"
263
+ }
264
+ ],
265
+ "unresolved": [
266
+ "effect/unstable/arbitrary/"
267
+ ]
268
+ }
155
269
  }
156
270
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ⛔ THE CHILD GETS A MINIMAL ENV, NOT `process.env`: `PATH` to find `bao`, the vault address and
3
+ * token, and the optional namespace / CA. `undefined` when BAO_ADDR or BAO_TOKEN is missing —
4
+ * `bao` would then read a cached `~/.vault-token` login and act as whoever last logged in.
5
+ */
6
+ export const baoEnv = (): Record<string, string> | undefined => {
7
+ const addr = process.env['BAO_ADDR'];
8
+ const token = process.env['BAO_TOKEN'];
9
+ if (addr === undefined || addr === '' || token === undefined || token === '') return undefined;
10
+ const env: Record<string, string> = { BAO_ADDR: addr, BAO_TOKEN: token };
11
+ for (const name of ['PATH', 'BAO_NAMESPACE', 'BAO_CACERT']) {
12
+ const value = process.env[name];
13
+ if (value !== undefined && value !== '') env[name] = value;
14
+ }
15
+ return env;
16
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `bao` vetting (PR 355 red team, round 1): `bao` fetches the cluster credentials, so it must be
3
+ * resolved and vetted exactly like `talosctl` before any environment reaches it. A fake `bao` in a
4
+ * group/world-writable PATH folder (umask 002, the round-10 incident) must be refused, never
5
+ * launched. Fails on a084e18, where `readKvValue`/`writeKvValue` spawned a bare `bao` from PATH
6
+ * with no vetting.
7
+ */
8
+ import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { expect, test } from 'bun:test';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
14
+ import { readKvValue } from './credentials.ts';
15
+ import { writeKvValue } from './credentials-write.ts';
16
+ import { type FakeCall, fakeSpawner } from './fake-process.ts';
17
+
18
+ /**
19
+ * Run `effect` with a fake `bao` planted in a 775 PATH folder prepended to the real PATH, capture
20
+ * every spawn the fake spawner saw, and restore PATH when done. Returns the failure text.
21
+ */
22
+ const withFakeBaoIn775 = async (
23
+ effect: Effect.Effect<unknown, Error, ChildProcessSpawner.ChildProcessSpawner>,
24
+ ): Promise<{ failure: string; calls: FakeCall[]; options: unknown[] }> => {
25
+ const dir = mkdtempSync(join(tmpdir(), 'hf-bao-vet-'));
26
+ const open = join(dir, 'open');
27
+ mkdirSync(open);
28
+ writeFileSync(join(open, 'bao'), '#!/bin/sh\n', { mode: 0o755 });
29
+ chmodSync(join(open, 'bao'), 0o755);
30
+ chmodSync(open, 0o775); // group-writable, like a umask-002 checkout
31
+ const savedPath = process.env['PATH'];
32
+ process.env['PATH'] = `${realpathSync(open)}:${savedPath ?? ''}`;
33
+ const calls: FakeCall[] = [];
34
+ const options: unknown[] = [];
35
+ try {
36
+ const failure = await Effect.runPromise(
37
+ Effect.flip(
38
+ Effect.provideService(
39
+ effect,
40
+ ChildProcessSpawner.ChildProcessSpawner,
41
+ ChildProcessSpawner.make((cmd) => {
42
+ if (cmd._tag === 'StandardCommand') options.push(cmd.options);
43
+ return fakeSpawner(() => ({}), calls).spawn(cmd);
44
+ }),
45
+ ),
46
+ ),
47
+ ).then((e) => String(e));
48
+ return { failure, calls, options };
49
+ } finally {
50
+ if (savedPath === undefined) delete process.env['PATH'];
51
+ else process.env['PATH'] = savedPath;
52
+ rmSync(dir, { force: true, recursive: true });
53
+ }
54
+ };
55
+
56
+ test('a fake bao in a group/world-writable PATH folder is refused before any env reaches it', async () => {
57
+ const { failure, calls } = await withFakeBaoIn775(
58
+ readKvValue('talos-c1', 'kubeconfig', ['kubeconfig', 'config']),
59
+ );
60
+ expect(failure).toContain('writable directory');
61
+ // ⛔ The refusal happens before the spawn, so the fake `bao` never ran and never saw an env.
62
+ expect(calls).toEqual([]);
63
+ });
64
+
65
+ test('writeKvValue vets bao the same way and passes a minimal env with extendEnv off', async () => {
66
+ const { failure, calls } = await withFakeBaoIn775(
67
+ writeKvValue('talos-c1', 'kubeconfig', 'kubeconfig', 'secret-body'),
68
+ );
69
+ expect(failure).toContain('writable directory');
70
+ expect(calls).toEqual([]);
71
+ });
72
+
73
+ test('a vetted bao gets baoEnv with extendEnv off and forceKillAfter set', async () => {
74
+ // Uses the committed stub `bao` (fake-process.ts), which passes vetting, and observes the spawn.
75
+ const options: Array<{
76
+ env?: Record<string, string | undefined> | undefined;
77
+ extendEnv?: boolean | undefined;
78
+ forceKillAfter?: unknown;
79
+ }> = [];
80
+ const outcome = await Effect.runPromise(
81
+ Effect.provideService(
82
+ writeKvValue('talos-c1', 'kubeconfig', 'kubeconfig', 'secret-body'),
83
+ ChildProcessSpawner.ChildProcessSpawner,
84
+ ChildProcessSpawner.make((cmd) => {
85
+ if (cmd._tag === 'StandardCommand') options.push(cmd.options);
86
+ return fakeSpawner(() => ({}), []).spawn(cmd);
87
+ }),
88
+ ),
89
+ );
90
+ expect(outcome).toBeUndefined();
91
+ expect(options.length).toBe(1);
92
+ const opts = options[0];
93
+ expect(opts?.extendEnv).toBe(false);
94
+ expect(opts?.forceKillAfter).toBeDefined();
95
+ expect(Object.keys(opts?.env ?? {})).toContain('BAO_ADDR');
96
+ expect(Object.keys(opts?.env ?? {})).toContain('BAO_TOKEN');
97
+ });
@@ -0,0 +1,110 @@
1
+ /**
2
+ * One deadline over the whole `talos-openbao` connect path, with a REAL child process: a `bao`
3
+ * that never exits, forks a sleeper, or ignores SIGTERM must fail connect with
4
+ * `TalosOpenBaoConnectTimeout` and must not outlive it. The fake `bao` is a throwaway executable
5
+ * in `os.tmpdir()`, removed on every exit path; PATH is the only process-global touched and is
6
+ * restored.
7
+ */
8
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { expect, test } from 'bun:test';
12
+ import * as NodeServices from '@effect/platform-node/NodeServices';
13
+ import * as Duration from 'effect/Duration';
14
+ import * as Effect from 'effect/Effect';
15
+ import { connectTalosOpenBao, talosOpenBaoConnection } from './cluster-adapter.ts';
16
+ import './fake-process.ts';
17
+
18
+ const alive = (pid: number) => {
19
+ try {
20
+ process.kill(pid, 0);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ };
26
+
27
+ const settle = async (pid: number) => {
28
+ for (let i = 0; i < 40 && alive(pid); i++) await Bun.sleep(100);
29
+ return alive(pid);
30
+ };
31
+
32
+ const config = { c1: { context: 'admin@c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' } };
33
+ const BOUND = Duration.millis(1500);
34
+ // `__DIR__` is declared at the top of each script (the child gets a minimal env, no process.env).
35
+ const recordPid = `require('node:fs').writeFileSync(__DIR__ + '/pid', String(process.pid));\n`;
36
+ const hang = 'setInterval(() => {}, 1000);\n';
37
+ // The sleeper is a node child of `bao`: only a group kill reaches it.
38
+ const forkSleeper = [
39
+ 'const sleeper = "require(\'node:fs\').writeFileSync(" + JSON.stringify(__DIR__ + "/grandchild")',
40
+ ' + ", String(process.pid)); setInterval(() => {}, 1000)";',
41
+ "require('node:child_process').spawn(process.execPath, ['-e', sleeper], { stdio: 'ignore' });",
42
+ '',
43
+ ].join('\n');
44
+
45
+ /**
46
+ * Run the production connect path against a throwaway `bao` script on PATH, with only the bound
47
+ * shortened. `pids` reads a pid file the script wrote, while the directory still exists.
48
+ */
49
+ const connectAgainst = async (
50
+ script: string,
51
+ assert: (run: {
52
+ failure: { _tag: string };
53
+ elapsed: number;
54
+ pid: (name: string) => number;
55
+ }) => Promise<void>,
56
+ ) => {
57
+ const dir = mkdtempSync(join(tmpdir(), 'hf-deadline-'));
58
+ const saved = { path: process.env['PATH'] };
59
+ try {
60
+ const header = `#!${process.execPath}\nconst __DIR__ = ${JSON.stringify(dir)};\n`;
61
+ writeFileSync(join(dir, 'bao'), `${header}${script}`);
62
+ chmodSync(join(dir, 'bao'), 0o700);
63
+ process.env['PATH'] = `${dir}:${saved.path ?? ''}`;
64
+ const started = Date.now();
65
+ const failure = await Effect.runPromise(
66
+ connectTalosOpenBao(config, talosOpenBaoConnection('uid-c1'), BOUND).pipe(
67
+ Effect.flip,
68
+ Effect.provide(NodeServices.layer),
69
+ ),
70
+ );
71
+ await assert({
72
+ elapsed: Date.now() - started,
73
+ failure: failure as { _tag: string },
74
+ pid: (name) => Number(readFileSync(join(dir, name), 'utf8')),
75
+ });
76
+ } finally {
77
+ if (saved.path === undefined) delete process.env['PATH'];
78
+ else process.env['PATH'] = saved.path;
79
+ rmSync(dir, { force: true, recursive: true });
80
+ }
81
+ };
82
+
83
+ test('a bao that never exits fails connect with the typed timeout and the child is killed', async () => {
84
+ await connectAgainst(`${recordPid}${hang}`, async (run) => {
85
+ expect(run.failure._tag).toBe('TalosOpenBaoConnectTimeout');
86
+ expect(run.pid('pid')).toBeGreaterThan(0);
87
+ expect(await settle(run.pid('pid'))).toBe(false);
88
+ });
89
+ });
90
+
91
+ test('a bao that forks a sleeper leaves no grandchild alive after the timeout', async () => {
92
+ // ⛔ Killing only the leader (the old `detached: false`) leaves the sleeper running.
93
+ await connectAgainst(`${recordPid}${forkSleeper}${hang}`, async (run) => {
94
+ expect(run.failure._tag).toBe('TalosOpenBaoConnectTimeout');
95
+ await Bun.sleep(300);
96
+ const grandchild = run.pid('grandchild');
97
+ expect(grandchild).toBeGreaterThan(0);
98
+ expect(await settle(grandchild)).toBe(false);
99
+ expect(await settle(run.pid('pid'))).toBe(false);
100
+ });
101
+ });
102
+
103
+ test('a bao that ignores SIGTERM still fails connect within the deadline plus ~2 s', async () => {
104
+ // ⛔ Without `forceKillAfter` the finalizer waits on a leader that never exits.
105
+ await connectAgainst(`${recordPid}process.on('SIGTERM', () => {});\n${hang}`, async (run) => {
106
+ expect(run.failure._tag).toBe('TalosOpenBaoConnectTimeout');
107
+ expect(run.elapsed).toBeLessThan(1500 + 2500);
108
+ expect(await settle(run.pid('pid'))).toBe(false);
109
+ });
110
+ });