@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.
- package/README.md +6 -32
- package/dist/talos/bao-env.d.ts +7 -0
- package/dist/talos/bao-env.d.ts.map +1 -0
- package/dist/talos/cluster-adapter-errors.d.ts +134 -0
- package/dist/talos/cluster-adapter-errors.d.ts.map +1 -0
- package/dist/talos/cluster-adapter.d.ts +115 -0
- package/dist/talos/cluster-adapter.d.ts.map +1 -0
- package/dist/talos/cluster-identity.d.ts +28 -0
- package/dist/talos/cluster-identity.d.ts.map +1 -0
- package/dist/talos/cluster-transport.d.ts +64 -0
- package/dist/talos/cluster-transport.d.ts.map +1 -0
- package/dist/talos/credentials-write.d.ts.map +1 -1
- package/dist/talos/credentials.d.ts +10 -19
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/fake-apiserver.d.ts +9 -0
- package/dist/talos/fake-apiserver.d.ts.map +1 -0
- package/dist/talos/index.d.ts +7 -0
- package/dist/talos/index.d.ts.map +1 -1
- package/dist/talos/index.js +669 -167
- package/dist/talos/index.js.map +22 -10
- package/dist/talos/kubeconfig-attrs.d.ts +14 -0
- package/dist/talos/kubeconfig-attrs.d.ts.map +1 -1
- package/dist/talos/kubeconfig-doc.d.ts +23 -0
- package/dist/talos/kubeconfig-doc.d.ts.map +1 -0
- package/dist/talos/kubeconfig.d.ts +0 -12
- package/dist/talos/kubeconfig.d.ts.map +1 -1
- package/dist/talos/literal-connection.d.ts +16 -0
- package/dist/talos/literal-connection.d.ts.map +1 -0
- package/dist/talos/machine-config-read.d.ts +1 -1
- package/dist/talos/machine-config-read.d.ts.map +1 -1
- package/dist/talos/mint-temp-file.d.ts +20 -0
- package/dist/talos/mint-temp-file.d.ts.map +1 -0
- package/dist/talos/node-temp.harness.d.ts +2 -0
- package/dist/talos/node-temp.harness.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap-etcd.d.ts +1 -1
- package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -1
- package/dist/talos/talos-cluster-health.d.ts +15 -15
- package/dist/talos/talos-cluster-health.d.ts.map +1 -1
- package/dist/talos/talos-cluster-identity.d.ts +65 -0
- package/dist/talos/talos-cluster-identity.d.ts.map +1 -0
- package/dist/talos/talosctl-binary.d.ts +43 -0
- package/dist/talos/talosctl-binary.d.ts.map +1 -0
- package/dist/talos/talosctl.d.ts +12 -15
- package/dist/talos/talosctl.d.ts.map +1 -1
- package/dist/talos/trust-boundary.d.ts +8 -0
- package/dist/talos/trust-boundary.d.ts.map +1 -0
- package/dist/talos/values.d.ts.map +1 -1
- package/dist/talos/yaml-parse.d.ts +3 -0
- package/dist/talos/yaml-parse.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +2 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/peers.md +32 -5
- package/docs/talos-openbao-adapter.md +44 -0
- package/docs/upstream-conformance-bun.md +61 -24
- package/package.json +116 -2
- package/src/talos/bao-env.ts +16 -0
- package/src/talos/bao-vetting.test.ts +97 -0
- package/src/talos/cluster-adapter-deadline.test.ts +110 -0
- package/src/talos/cluster-adapter-errors.ts +192 -0
- package/src/talos/cluster-adapter-move.test.ts +129 -0
- package/src/talos/cluster-adapter-reconcile.test.ts +165 -0
- package/src/talos/cluster-adapter-refusals.test.ts +102 -0
- package/src/talos/cluster-adapter-rename.test.ts +87 -0
- package/src/talos/cluster-adapter-secrecy.test.ts +243 -0
- package/src/talos/cluster-adapter.fixtures.ts +125 -0
- package/src/talos/cluster-adapter.test.ts +240 -0
- package/src/talos/cluster-adapter.ts +249 -0
- package/src/talos/cluster-identity.ts +63 -0
- package/src/talos/cluster-transport.ts +110 -0
- package/src/talos/credentials-write.ts +27 -5
- package/src/talos/credentials.ts +51 -45
- package/src/talos/fake-apiserver.ts +55 -0
- package/src/talos/fake-process.ts +47 -1
- package/src/talos/index.ts +32 -0
- package/src/talos/kubeconfig-adapter.test.ts +37 -0
- package/src/talos/kubeconfig-attrs.ts +12 -28
- package/src/talos/kubeconfig-doc.ts +74 -0
- package/src/talos/kubeconfig.fixtures.ts +52 -0
- package/src/talos/kubeconfig.test.ts +6 -54
- package/src/talos/kubeconfig.ts +18 -34
- package/src/talos/literal-connection.ts +59 -0
- package/src/talos/mint-temp-file.ts +56 -0
- package/src/talos/node-connect.harness.ts +78 -0
- package/src/talos/node-connect.test.ts +27 -0
- package/src/talos/node-temp.harness.ts +40 -0
- package/src/talos/node-temp.test.ts +26 -0
- package/src/talos/talos-cluster-health-declare.test.ts +54 -0
- package/src/talos/talos-cluster-health-literal.test.ts +69 -0
- package/src/talos/talos-cluster-health.test.ts +104 -18
- package/src/talos/talos-cluster-health.ts +46 -14
- package/src/talos/talos-cluster-identity.test.ts +96 -0
- package/src/talos/talos-cluster-identity.ts +130 -0
- package/src/talos/talosctl-binary.ts +150 -0
- package/src/talos/talosctl-vetting.test.ts +212 -0
- package/src/talos/talosctl.test.ts +110 -0
- package/src/talos/talosctl.ts +101 -15
- package/src/talos/trust-boundary.test.ts +17 -0
- package/src/talos/trust-boundary.ts +45 -0
- package/src/talos/values.ts +5 -12
- package/src/talos/yaml-parse.test.ts +41 -0
- package/src/talos/yaml-parse.ts +28 -0
- package/src/verify/fake-engine.ts +10 -0
- package/dist/talos/fake-process.d.ts +0 -56
- package/dist/talos/fake-process.d.ts.map +0 -1
- package/dist/talos/machine-config-fixtures.d.ts +0 -60
- package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
- package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
- package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
- package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
- package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
- package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
- package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
- package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
- package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
- package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
- package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
- package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
- package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
- package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
- package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
- package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
- package/src/release/fixtures/openbao/checksums.txt +0 -83
- package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
- package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
- package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
- package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
- package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
- package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
- package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
- 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
|
|
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
|
-
|
|
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
|
|
28
|
-
| ---------------------------------------------------------- |
|
|
29
|
-
| `openbao/cloudflare-roles-config.ts` | `Bun.YAML`
|
|
30
|
-
| `openbao/digest.ts` (14 importers) | `Bun.CryptoHasher`
|
|
31
|
-
| `openbao/cloudflare-parity-snapshot.ts` | `Bun.file`, `Bun.Glob`
|
|
32
|
-
| `openbao/approle-login-form.ts` | `Bun.file`
|
|
33
|
-
| `openbao/approle-login-result.ts` | `Bun.inspect`
|
|
34
|
-
| `openbao/forgejo-bootstrap.ts` | `Bun.spawn`
|
|
35
|
-
| `talos/credentials.ts`
|
|
36
|
-
| `talos/
|
|
37
|
-
| `talos/
|
|
38
|
-
| `talos/values.ts` | `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
41
|
-
| `launchd/
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
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:
|
|
67
|
-
talos
|
|
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.
|
|
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
|
+
});
|