@homeflare/alchemy 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cloudflare/client.d.ts +45 -0
- package/dist/cloudflare/client.d.ts.map +1 -0
- package/dist/cloudflare/index.d.ts +11 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +129 -0
- package/dist/cloudflare/index.js.map +13 -0
- package/dist/cloudflare/lock-rules.d.ts +50 -0
- package/dist/cloudflare/lock-rules.d.ts.map +1 -0
- package/dist/cloudflare/providers.d.ts +19 -0
- package/dist/cloudflare/providers.d.ts.map +1 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
- package/dist/forgejo/branch-protection-form.d.ts +19 -0
- package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
- package/dist/forgejo/branch-protection.d.ts +47 -0
- package/dist/forgejo/branch-protection.d.ts.map +1 -0
- package/dist/forgejo/client.d.ts +37 -0
- package/dist/forgejo/client.d.ts.map +1 -0
- package/dist/forgejo/index.d.ts +18 -0
- package/dist/forgejo/index.d.ts.map +1 -0
- package/dist/forgejo/index.js +479 -0
- package/dist/forgejo/index.js.map +21 -0
- package/dist/forgejo/org-actions-secrets.d.ts +45 -0
- package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
- package/dist/forgejo/org-label.d.ts +46 -0
- package/dist/forgejo/org-label.d.ts.map +1 -0
- package/dist/forgejo/org-team.d.ts +42 -0
- package/dist/forgejo/org-team.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook-form.d.ts +27 -0
- package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook.d.ts +46 -0
- package/dist/forgejo/repo-webhook.d.ts.map +1 -0
- package/dist/forgejo/repository.d.ts +45 -0
- package/dist/forgejo/repository.d.ts.map +1 -0
- package/dist/forgejo/resource.d.ts +83 -0
- package/dist/forgejo/resource.d.ts.map +1 -0
- package/dist/forgejo/team-member.d.ts +39 -0
- package/dist/forgejo/team-member.d.ts.map +1 -0
- package/dist/forgejo/values.d.ts +24 -0
- package/dist/forgejo/values.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +9 -0
- package/dist/openbao/auth-role-form.d.ts +38 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -0
- package/dist/openbao/auth-role.d.ts +20 -0
- package/dist/openbao/auth-role.d.ts.map +1 -0
- package/dist/openbao/bao-address.d.ts +72 -0
- package/dist/openbao/bao-address.d.ts.map +1 -0
- package/dist/openbao/bao-http.d.ts +71 -0
- package/dist/openbao/bao-http.d.ts.map +1 -0
- package/dist/openbao/bao-status.d.ts +74 -0
- package/dist/openbao/bao-status.d.ts.map +1 -0
- package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
- package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity.d.ts +14 -0
- package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
- package/dist/openbao/cloudflare-policy.d.ts +65 -0
- package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-form.d.ts +110 -0
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
- package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role.d.ts +35 -0
- package/dist/openbao/cloudflare-role.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
- package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
- package/dist/openbao/digest.d.ts +24 -0
- package/dist/openbao/digest.d.ts.map +1 -0
- package/dist/openbao/fake-bao.d.ts +40 -0
- package/dist/openbao/fake-bao.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
- package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +18 -0
- package/dist/openbao/index.d.ts.map +1 -0
- package/dist/openbao/index.js +1158 -0
- package/dist/openbao/index.js.map +33 -0
- package/dist/openbao/mount-form.d.ts +60 -0
- package/dist/openbao/mount-form.d.ts.map +1 -0
- package/dist/openbao/mount-wire.d.ts +24 -0
- package/dist/openbao/mount-wire.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +20 -0
- package/dist/openbao/mount.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +118 -0
- package/dist/openbao/pki-role-form.d.ts.map +1 -0
- package/dist/openbao/pki-role.d.ts +50 -0
- package/dist/openbao/pki-role.d.ts.map +1 -0
- package/dist/openbao/policy-assembly.d.ts +13 -0
- package/dist/openbao/policy-assembly.d.ts.map +1 -0
- package/dist/openbao/policy-wire.d.ts +41 -0
- package/dist/openbao/policy-wire.d.ts.map +1 -0
- package/dist/openbao/policy.d.ts +83 -0
- package/dist/openbao/policy.d.ts.map +1 -0
- package/dist/openbao/proxmox-role-form.d.ts +92 -0
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
- package/dist/openbao/proxmox-role.d.ts +47 -0
- package/dist/openbao/proxmox-role.d.ts.map +1 -0
- package/dist/openbao/ssh-role-form.d.ts +99 -0
- package/dist/openbao/ssh-role-form.d.ts.map +1 -0
- package/dist/openbao/ssh-role.d.ts +43 -0
- package/dist/openbao/ssh-role.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts +72 -0
- package/dist/proxmox/acl.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts +59 -0
- package/dist/proxmox/api-token-form.d.ts.map +1 -0
- package/dist/proxmox/api-token.d.ts +159 -0
- package/dist/proxmox/api-token.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +112 -0
- package/dist/proxmox/backup-job.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon.d.ts +162 -0
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +131 -0
- package/dist/proxmox/ceph-flag.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs.d.ts +124 -0
- package/dist/proxmox/ceph-fs.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
- package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-write.d.ts +36 -0
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd.d.ts +102 -0
- package/dist/proxmox/ceph-osd.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-form.d.ts +217 -0
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool.d.ts +48 -0
- package/dist/proxmox/ceph-pool.d.ts.map +1 -0
- package/dist/proxmox/client.d.ts +64 -0
- package/dist/proxmox/client.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +153 -0
- package/dist/proxmox/credentials.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias-form.d.ts +98 -0
- package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias.d.ts +141 -0
- package/dist/proxmox/firewall-alias.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs.d.ts +388 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve.d.ts +4713 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -0
- package/dist/proxmox/group.d.ts +104 -0
- package/dist/proxmox/group.d.ts.map +1 -0
- package/dist/proxmox/ha-resource.d.ts +104 -0
- package/dist/proxmox/ha-resource.d.ts.map +1 -0
- package/dist/proxmox/ha-rule-form.d.ts +103 -0
- package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
- package/dist/proxmox/ha-rule.d.ts +156 -0
- package/dist/proxmox/ha-rule.d.ts.map +1 -0
- package/dist/proxmox/index.d.ts +43 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +2322 -0
- package/dist/proxmox/index.js.map +70 -0
- package/dist/proxmox/lease-cache.d.ts +98 -0
- package/dist/proxmox/lease-cache.d.ts.map +1 -0
- package/dist/proxmox/lxc.d.ts +59 -0
- package/dist/proxmox/lxc.d.ts.map +1 -0
- package/dist/proxmox/members.d.ts +56 -0
- package/dist/proxmox/members.d.ts.map +1 -0
- package/dist/proxmox/metric-server-form.d.ts +53 -0
- package/dist/proxmox/metric-server-form.d.ts.map +1 -0
- package/dist/proxmox/metric-server-otel.d.ts +49 -0
- package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +123 -0
- package/dist/proxmox/metric-server.d.ts.map +1 -0
- package/dist/proxmox/network-apply-read.d.ts +93 -0
- package/dist/proxmox/network-apply-read.d.ts.map +1 -0
- package/dist/proxmox/network-apply.d.ts +102 -0
- package/dist/proxmox/network-apply.d.ts.map +1 -0
- package/dist/proxmox/node-network-form.d.ts +128 -0
- package/dist/proxmox/node-network-form.d.ts.map +1 -0
- package/dist/proxmox/node-network.d.ts +173 -0
- package/dist/proxmox/node-network.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +44 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +146 -0
- package/dist/proxmox/notification-target.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +140 -0
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +168 -0
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +246 -0
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +145 -0
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
- package/dist/proxmox/pool.d.ts +42 -0
- package/dist/proxmox/pool.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +46 -0
- package/dist/proxmox/qemu.d.ts.map +1 -0
- package/dist/proxmox/replication-job-form.d.ts +71 -0
- package/dist/proxmox/replication-job-form.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +162 -0
- package/dist/proxmox/replication-job.d.ts.map +1 -0
- package/dist/proxmox/resource.d.ts +157 -0
- package/dist/proxmox/resource.d.ts.map +1 -0
- package/dist/proxmox/role.d.ts +97 -0
- package/dist/proxmox/role.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply-read.d.ts +80 -0
- package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply.d.ts +63 -0
- package/dist/proxmox/sdn-apply.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
- package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet.d.ts +161 -0
- package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-vnet.d.ts +113 -0
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-zone.d.ts +126 -0
- package/dist/proxmox/sdn-zone.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +120 -0
- package/dist/proxmox/storage.d.ts.map +1 -0
- package/dist/proxmox/user.d.ts +107 -0
- package/dist/proxmox/user.d.ts.map +1 -0
- package/dist/proxmox/values.d.ts +134 -0
- package/dist/proxmox/values.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +76 -0
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +146 -0
- package/dist/proxmox/zfs-pool.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +30 -0
- package/dist/talos/credentials.d.ts.map +1 -0
- package/dist/talos/index.d.ts +15 -0
- package/dist/talos/index.d.ts.map +1 -0
- package/dist/talos/index.js +392 -0
- package/dist/talos/index.js.map +16 -0
- package/dist/talos/kubeconfig.d.ts +53 -0
- package/dist/talos/kubeconfig.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +32 -0
- package/dist/talos/resource.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +27 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -0
- package/dist/talos/talos-cluster-health.d.ts +36 -0
- package/dist/talos/talos-cluster-health.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +41 -0
- package/dist/talos/talos-machine-config.d.ts.map +1 -0
- package/dist/talos/talosctl.d.ts +29 -0
- package/dist/talos/talosctl.d.ts.map +1 -0
- package/dist/talos/values.d.ts +20 -0
- package/dist/talos/values.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/cloudflare/client.ts +76 -0
- package/src/cloudflare/index.ts +10 -0
- package/src/cloudflare/lock-rules.ts +60 -0
- package/src/cloudflare/providers.ts +23 -0
- package/src/cloudflare/r2-bucket-lock.ts +220 -0
- package/src/forgejo/branch-protection-form.ts +29 -0
- package/src/forgejo/branch-protection.ts +101 -0
- package/src/forgejo/client.ts +136 -0
- package/src/forgejo/index.ts +17 -0
- package/src/forgejo/org-actions-secrets.ts +99 -0
- package/src/forgejo/org-label.ts +100 -0
- package/src/forgejo/org-team.ts +105 -0
- package/src/forgejo/repo-webhook-form.ts +47 -0
- package/src/forgejo/repo-webhook.ts +118 -0
- package/src/forgejo/repository.ts +126 -0
- package/src/forgejo/resource.ts +171 -0
- package/src/forgejo/team-member.ts +97 -0
- package/src/forgejo/values.test.ts +48 -0
- package/src/forgejo/values.ts +61 -0
- package/src/index.ts +18 -0
- package/src/openbao/auth-role-form.ts +112 -0
- package/src/openbao/auth-role.ts +101 -0
- package/src/openbao/bao-address.test.ts +89 -0
- package/src/openbao/bao-address.ts +97 -0
- package/src/openbao/bao-http-gate.test.ts +88 -0
- package/src/openbao/bao-http-unix.test.ts +70 -0
- package/src/openbao/bao-http.test.ts +161 -0
- package/src/openbao/bao-http.ts +185 -0
- package/src/openbao/bao-status.test.ts +114 -0
- package/src/openbao/bao-status.ts +137 -0
- package/src/openbao/cloudflare-group-scope.test.ts +103 -0
- package/src/openbao/cloudflare-group-scope.ts +83 -0
- package/src/openbao/cloudflare-parity-catalog.ts +104 -0
- package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
- package/src/openbao/cloudflare-parity.ts +152 -0
- package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
- package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
- package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
- package/src/openbao/cloudflare-permission-groups.ts +299 -0
- package/src/openbao/cloudflare-policy.ts +180 -0
- package/src/openbao/cloudflare-role-form.test.ts +177 -0
- package/src/openbao/cloudflare-role-form.ts +183 -0
- package/src/openbao/cloudflare-role-wire.test.ts +132 -0
- package/src/openbao/cloudflare-role-wire.ts +41 -0
- package/src/openbao/cloudflare-role.ts +189 -0
- package/src/openbao/cloudflare-roles-config.ts +230 -0
- package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
- package/src/openbao/cloudflare-roles-expand.ts +231 -0
- package/src/openbao/digest.ts +26 -0
- package/src/openbao/fake-bao.ts +84 -0
- package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
- package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
- package/src/openbao/forgejo-bootstrap.ts +181 -0
- package/src/openbao/index.ts +17 -0
- package/src/openbao/mount-form.ts +147 -0
- package/src/openbao/mount-wire.test.ts +108 -0
- package/src/openbao/mount-wire.ts +79 -0
- package/src/openbao/mount.ts +132 -0
- package/src/openbao/pki-role-form.ts +248 -0
- package/src/openbao/pki-role.ts +202 -0
- package/src/openbao/policy-assembly.test.ts +28 -0
- package/src/openbao/policy-assembly.ts +13 -0
- package/src/openbao/policy-wire.test.ts +90 -0
- package/src/openbao/policy-wire.ts +53 -0
- package/src/openbao/policy.ts +212 -0
- package/src/openbao/proxmox-role-form.ts +174 -0
- package/src/openbao/proxmox-role.ts +214 -0
- package/src/openbao/ssh-role-form.ts +248 -0
- package/src/openbao/ssh-role.ts +240 -0
- package/src/proxmox/acl.ts +267 -0
- package/src/proxmox/api-token-form.ts +122 -0
- package/src/proxmox/api-token.ts +222 -0
- package/src/proxmox/backup-job.ts +213 -0
- package/src/proxmox/ceph-daemon-form.ts +84 -0
- package/src/proxmox/ceph-daemon.ts +243 -0
- package/src/proxmox/ceph-flag.ts +249 -0
- package/src/proxmox/ceph-fs-wire.ts +246 -0
- package/src/proxmox/ceph-fs.ts +249 -0
- package/src/proxmox/ceph-osd-tree.ts +193 -0
- package/src/proxmox/ceph-osd-write.ts +76 -0
- package/src/proxmox/ceph-osd.ts +244 -0
- package/src/proxmox/ceph-pool-form.ts +249 -0
- package/src/proxmox/ceph-pool-settle.ts +70 -0
- package/src/proxmox/ceph-pool.ts +212 -0
- package/src/proxmox/client.test.ts +185 -0
- package/src/proxmox/client.ts +240 -0
- package/src/proxmox/credentials.test.ts +144 -0
- package/src/proxmox/credentials.ts +254 -0
- package/src/proxmox/firewall-alias-form.ts +112 -0
- package/src/proxmox/firewall-alias.ts +208 -0
- package/src/proxmox/generated/pbs.ts +461 -0
- package/src/proxmox/generated/pve.ts +7344 -0
- package/src/proxmox/group.ts +199 -0
- package/src/proxmox/ha-resource.ts +169 -0
- package/src/proxmox/ha-rule-form.ts +185 -0
- package/src/proxmox/ha-rule.ts +240 -0
- package/src/proxmox/index.ts +42 -0
- package/src/proxmox/lease-cache.test.ts +163 -0
- package/src/proxmox/lease-cache.ts +131 -0
- package/src/proxmox/lxc.ts +117 -0
- package/src/proxmox/members.test.ts +54 -0
- package/src/proxmox/members.ts +174 -0
- package/src/proxmox/metric-server-form.ts +128 -0
- package/src/proxmox/metric-server-otel.ts +99 -0
- package/src/proxmox/metric-server.ts +214 -0
- package/src/proxmox/network-apply-read.ts +228 -0
- package/src/proxmox/network-apply.ts +244 -0
- package/src/proxmox/node-network-form.ts +213 -0
- package/src/proxmox/node-network.ts +243 -0
- package/src/proxmox/notification-target-form.ts +104 -0
- package/src/proxmox/notification-target.ts +222 -0
- package/src/proxmox/pbs-datastore-form.ts +248 -0
- package/src/proxmox/pbs-datastore-guard.ts +105 -0
- package/src/proxmox/pbs-datastore.ts +271 -0
- package/src/proxmox/pbs-prune-job-form.ts +208 -0
- package/src/proxmox/pbs-prune-job.ts +227 -0
- package/src/proxmox/pbs-sync-job-form.ts +234 -0
- package/src/proxmox/pbs-sync-job.ts +258 -0
- package/src/proxmox/pbs-verify-job-form.ts +131 -0
- package/src/proxmox/pbs-verify-job.ts +227 -0
- package/src/proxmox/pool.test.ts +42 -0
- package/src/proxmox/pool.ts +80 -0
- package/src/proxmox/qemu.ts +89 -0
- package/src/proxmox/replication-job-form.ts +89 -0
- package/src/proxmox/replication-job.ts +248 -0
- package/src/proxmox/resource.ts +257 -0
- package/src/proxmox/role.ts +161 -0
- package/src/proxmox/sdn-apply-read.test.ts +152 -0
- package/src/proxmox/sdn-apply-read.ts +189 -0
- package/src/proxmox/sdn-apply.ts +154 -0
- package/src/proxmox/sdn-subnet-form.ts +156 -0
- package/src/proxmox/sdn-subnet.ts +238 -0
- package/src/proxmox/sdn-vnet.ts +228 -0
- package/src/proxmox/sdn-zone.ts +246 -0
- package/src/proxmox/storage.ts +245 -0
- package/src/proxmox/user.ts +214 -0
- package/src/proxmox/values.test.ts +193 -0
- package/src/proxmox/values.ts +203 -0
- package/src/proxmox/zfs-pool-write.ts +130 -0
- package/src/proxmox/zfs-pool.ts +253 -0
- package/src/talos/credentials.ts +136 -0
- package/src/talos/index.ts +14 -0
- package/src/talos/kubeconfig.ts +170 -0
- package/src/talos/resource.ts +31 -0
- package/src/talos/talos-bootstrap.ts +109 -0
- package/src/talos/talos-cluster-health.ts +119 -0
- package/src/talos/talos-machine-config.ts +146 -0
- package/src/talos/talosctl.ts +94 -0
- package/src/talos/values.test.ts +80 -0
- package/src/talos/values.ts +70 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mint` against a fake OpenBao on Bun.serve — lease parsing, the headers, agent mode, the
|
|
3
|
+
* failures that must never become a credential, and the unix-socket path.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ NO REAL VAULT AND NO REAL ENVIRONMENT. Every call passes an explicit environment, so a
|
|
6
|
+
* BAO_ADDR or BAO_TOKEN in the shell running the tests is never read.
|
|
7
|
+
*/
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { describe, it } from 'node:test';
|
|
13
|
+
import * as Effect from 'effect/Effect';
|
|
14
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
15
|
+
import { type BaoEnvironment, type PveTarget, mint } from './credentials.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ⚠️ THIS PACKAGE'S tsconfig CARRIES NO BUN TYPES, and its package.json is out of bounds for this
|
|
19
|
+
* change, so the one Bun API the fake needs is typed here, minimally. The tests run under
|
|
20
|
+
* `bun test`, where `Bun.serve` exists.
|
|
21
|
+
*/
|
|
22
|
+
type ServeOptions = { fetch(request: Request): Promise<Response> } & (
|
|
23
|
+
| { hostname: string; port: number }
|
|
24
|
+
| { unix: string }
|
|
25
|
+
);
|
|
26
|
+
const { Bun } = globalThis as unknown as {
|
|
27
|
+
Bun: { serve(options: ServeOptions): { url: URL; stop(closeActive: boolean): void } };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TOKEN = 'hvs.FAKE-sentinel-never-a-real-token';
|
|
31
|
+
const SECRET = 'fake-pve-secret-0000';
|
|
32
|
+
const TB4: PveTarget = {
|
|
33
|
+
members: ['pve.example'],
|
|
34
|
+
mount: 'proxmox-tb4',
|
|
35
|
+
scheme: 'pve',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type Seen = { path: string; headers: Headers };
|
|
39
|
+
type Reply = { status: number; json: unknown };
|
|
40
|
+
|
|
41
|
+
const withFake = async (
|
|
42
|
+
reply: Reply,
|
|
43
|
+
body: (address: string, seen: Seen[]) => Promise<void>,
|
|
44
|
+
unix?: string,
|
|
45
|
+
) => {
|
|
46
|
+
const seen: Seen[] = [];
|
|
47
|
+
const fetch = async (request: Request) => {
|
|
48
|
+
seen.push({ headers: request.headers, path: new URL(request.url).pathname });
|
|
49
|
+
return Response.json(reply.json, { status: reply.status });
|
|
50
|
+
};
|
|
51
|
+
const server = Bun.serve(
|
|
52
|
+
unix === undefined ? { fetch, hostname: '127.0.0.1', port: 0 } : { fetch, unix },
|
|
53
|
+
);
|
|
54
|
+
try {
|
|
55
|
+
await body(unix === undefined ? server.url.origin : `unix://${unix}`, seen);
|
|
56
|
+
} finally {
|
|
57
|
+
server.stop(true);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const minted = (env: BaoEnvironment) =>
|
|
62
|
+
Effect.runPromise(mint(TB4, 'read', env).pipe(Effect.provide(FetchHttpClient.layer)));
|
|
63
|
+
const refused = (env: BaoEnvironment) =>
|
|
64
|
+
Effect.runPromise(
|
|
65
|
+
mint(TB4, 'read', env).pipe(Effect.flip, Effect.provide(FetchHttpClient.layer)),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const CREDENTIAL = {
|
|
69
|
+
json: { data: { secret: SECRET, token_id: 'hf-read@pve!hf-read-1' }, lease_duration: 3600 },
|
|
70
|
+
status: 200,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe('mint', () => {
|
|
74
|
+
it('parses the lease and sends the token, the namespace and X-Vault-Request', async () => {
|
|
75
|
+
await withFake(CREDENTIAL, async (address, seen) => {
|
|
76
|
+
const env = { BAO_ADDR: address, BAO_NAMESPACE: 'homeflare', BAO_TOKEN: TOKEN };
|
|
77
|
+
assert.deepEqual(await minted(env), {
|
|
78
|
+
leaseSeconds: 3600,
|
|
79
|
+
secret: SECRET,
|
|
80
|
+
tokenId: 'hf-read@pve!hf-read-1',
|
|
81
|
+
});
|
|
82
|
+
assert.equal(seen[0]?.path, '/v1/proxmox-tb4/creds/read');
|
|
83
|
+
assert.equal(seen[0]?.headers.get('x-vault-token'), TOKEN);
|
|
84
|
+
assert.equal(seen[0]?.headers.get('x-vault-namespace'), 'homeflare');
|
|
85
|
+
assert.equal(seen[0]?.headers.get('x-vault-request'), 'true');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ⛔ An unknown lifetime is 0, and lease-cache.ts never keeps a 0-second lease.
|
|
90
|
+
it('reads a missing lease_duration as 0', async () => {
|
|
91
|
+
const reply = { json: { data: { secret: SECRET, token_id: 'hf-read@pve!x' } }, status: 200 };
|
|
92
|
+
await withFake(reply, async (address) => {
|
|
93
|
+
assert.equal((await minted({ BAO_ADDR: address, BAO_TOKEN: TOKEN })).leaseSeconds, 0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ★ AGENT MODE: no BAO_TOKEN, no token header — the listener supplies its auto-auth token.
|
|
98
|
+
it('sends no token header when BAO_TOKEN is unset', async () => {
|
|
99
|
+
await withFake(CREDENTIAL, async (address, seen) => {
|
|
100
|
+
await minted({ BAO_ADDR: address });
|
|
101
|
+
assert.equal(seen[0]?.headers.has('x-vault-token'), false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('fails 403, 404 and 503 with OpenBao errors, never the token', async () => {
|
|
106
|
+
for (const status of [403, 404, 503]) {
|
|
107
|
+
await withFake(
|
|
108
|
+
{ json: { errors: [`status ${String(status)}`] }, status },
|
|
109
|
+
async (address) => {
|
|
110
|
+
const error = await refused({ BAO_ADDR: address, BAO_TOKEN: TOKEN });
|
|
111
|
+
assert.ok(error instanceof Error);
|
|
112
|
+
assert.match(
|
|
113
|
+
error.message,
|
|
114
|
+
new RegExp(`-> ${String(status)}: status ${String(status)}$`),
|
|
115
|
+
);
|
|
116
|
+
assert.ok(!error.message.includes(TOKEN));
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('refuses a 200 with no token_id, without quoting the secret', async () => {
|
|
123
|
+
await withFake({ json: { data: { secret: SECRET } }, status: 200 }, async (address) => {
|
|
124
|
+
const error = await refused({ BAO_ADDR: address, BAO_TOKEN: TOKEN });
|
|
125
|
+
assert.ok(error instanceof Error);
|
|
126
|
+
assert.match(error.message, /returned no token_id\/secret/);
|
|
127
|
+
assert.ok(!error.message.includes(SECRET));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('mints over a unix socket', async () => {
|
|
132
|
+
const socket = join(tmpdir(), `fake-bao-mint-${String(process.pid)}.sock`);
|
|
133
|
+
rmSync(socket, { force: true });
|
|
134
|
+
await withFake(
|
|
135
|
+
CREDENTIAL,
|
|
136
|
+
async (address, seen) => {
|
|
137
|
+
assert.equal((await minted({ BAO_ADDR: address })).leaseSeconds, 3600);
|
|
138
|
+
assert.equal(seen[0]?.path, '/v1/proxmox-tb4/creds/read');
|
|
139
|
+
},
|
|
140
|
+
socket,
|
|
141
|
+
);
|
|
142
|
+
rmSync(socket, { force: true });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PVE credential a reconcile runs with: minted from OpenBao, alive for five minutes.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THERE IS NO STATIC WRITE TOKEN IN THIS ESTATE, AND THAT IS DELIBERATE. Every PVE token stored
|
|
5
|
+
* in `kv/infra/proxmox/*` is read-only — PVEAuditor on media/ops/tb4, Audit on pbs, all as
|
|
6
|
+
* `monitoring@pve`. The MCP proxmox client says why in its own header: the auditor token bounds
|
|
7
|
+
* what a BUG can do, independently of what the code bounds. Adding a long-lived write token to
|
|
8
|
+
* that shelf would delete the outer lock for every reader of the shelf, not just for this
|
|
9
|
+
* provider.
|
|
10
|
+
*
|
|
11
|
+
* ★ SO WRITES COME FROM A DYNAMIC MOUNT INSTEAD, which the estate already built: `proxmox-tb4/`
|
|
12
|
+
* exposes two roles — `read` (ttl 3600s) and `provision` (ttl 300s, max 1800s, NOT renewable).
|
|
13
|
+
* A mint returns a PVE API token of its own, `hf-provision@pve!hf-provision-…-<timestamp>-<id>`,
|
|
14
|
+
* which expires on its own whether or not anything cleans up. Same shape as
|
|
15
|
+
* `cloudflare-<account>-<surface>/creds/<role>`, same reason.
|
|
16
|
+
* ⚠️ THAT PATH IS WRITTEN WITH `<surface>` RATHER THAN A GLOB ON PURPOSE: a `*` followed by
|
|
17
|
+
* a slash CLOSES THIS BLOCK COMMENT, and everything below it then parses as code. It cost
|
|
18
|
+
* six TS1005/TS1443 errors pointing at innocent lines twenty rows further down.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ FIVE MINUTES IS THE DESIGN, NOT AN OBSTACLE. A reconcile that cannot finish inside the lease
|
|
21
|
+
* should mint again rather than ask for a longer one: the short lease is what makes a leaked
|
|
22
|
+
* token uninteresting. `provision` is explicitly non-renewable, so there is no renew path to
|
|
23
|
+
* reach for.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ THE SECRET NEVER TOUCHES DISK, A LOG, OR ALCHEMY STATE. Alchemy persists resource attributes
|
|
26
|
+
* WITHOUT encryption — StateEncoding.ts writes `Redacted` as `{"@redacted": <plaintext>}` — and
|
|
27
|
+
* this estate's state store is the `alchemy` Postgres, which pg-backup.sh dumps nightly. A PVE
|
|
28
|
+
* secret that reached an attribute would outlive its 300s lease by months, in four places.
|
|
29
|
+
* Nothing here returns it to a resource; it is used to build a header and then dropped.
|
|
30
|
+
*/
|
|
31
|
+
import * as Effect from 'effect/Effect';
|
|
32
|
+
import * as Option from 'effect/Option';
|
|
33
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
34
|
+
import * as Headers from 'effect/unstable/http/Headers';
|
|
35
|
+
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
36
|
+
import * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ★ NO SESSION, ON PURPOSE, AND THIS IS WHAT UNBLOCKED `wip/bao-policy`.
|
|
40
|
+
* `CommandExecutor.run` requires a `ScopedPlanStatusSession`, and Alchemy gives one to
|
|
41
|
+
* `reconcile` and `delete` but NOT to `read` or `diff` — so a provider built on `run` can only
|
|
42
|
+
* ever write. Alchemy's OWN Docker provider does not use `CommandExecutor` for this at all: it
|
|
43
|
+
* takes `ChildProcessSpawner` and pipes `ChildProcess.make(...)` through `spawner.spawn`, which
|
|
44
|
+
* needs a `Scope` and nothing else (Docker.ts:476-553). That was the pattern here until
|
|
45
|
+
* 2026-09-14, when `mint` moved to `HttpClient` — which needs no session either, so `mint` is
|
|
46
|
+
* still callable from all four operations.
|
|
47
|
+
*
|
|
48
|
+
* ⛔ DO NOT SATISFY THE OLD SIGNATURE BY FAKING A SESSION. A session carries plan status Alchemy
|
|
49
|
+
* uses to report progress; inventing one would make read and diff report work they are not doing.
|
|
50
|
+
*/
|
|
51
|
+
/** Which role a call needs. `read` for read/diff, `provision` for reconcile/delete. */
|
|
52
|
+
export type PveRole = 'read' | 'provision';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Where credentials come from, and which cluster they are for.
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ A PARAMETER, NOT A CONSTANT, SO THIS PACKAGE CAN LEAVE THE ESTATE. Everything
|
|
58
|
+
* HomeFlare-specific — the mount name, the API host — belongs in the stack that declares
|
|
59
|
+
* resources, not in the provider. Anyone with an OpenBao mount that vends Proxmox API tokens can
|
|
60
|
+
* use this file unchanged; that is the whole difference between a provider and a script.
|
|
61
|
+
*/
|
|
62
|
+
export type PveTarget = {
|
|
63
|
+
/** OpenBao mount that vends API tokens for this host, e.g. `proxmox-tb4`. */
|
|
64
|
+
readonly mount: string;
|
|
65
|
+
/**
|
|
66
|
+
* mgmt hostnames for cluster members, e.g. `n2.mgmt.example.com`. Any member's :8006 API
|
|
67
|
+
* manages the whole cluster; `client.ts` failovers across them.
|
|
68
|
+
*/
|
|
69
|
+
readonly members: readonly string[];
|
|
70
|
+
/**
|
|
71
|
+
* Which product answers at `api`. Defaults to `pve`.
|
|
72
|
+
*
|
|
73
|
+
* ★ PBS IS THE SAME CLIENT WITH A DIFFERENT AUTHORIZATION HEADER, AND THAT IS WHY THERE IS NO
|
|
74
|
+
* SECOND CLIENT HERE. Proxmox Backup Server speaks the same `/api2/json` paths, wraps every
|
|
75
|
+
* answer in the same `{"data": …}` envelope, and its OpenBao mount vends the same
|
|
76
|
+
* `{token_id, secret}` shape — so `mint`, `pve()` and `pveOperations` all work against it
|
|
77
|
+
* unchanged. The ONE difference is the header scheme: PVE spells it
|
|
78
|
+
* `PVEAPIToken=<id>=<secret>` and PBS spells it `PBSAPIToken=<id>:<secret>` — a different
|
|
79
|
+
* prefix AND a different separator.
|
|
80
|
+
*
|
|
81
|
+
* ⛔ IT IS REQUIRED, NOT OPTIONAL, AND THAT IS THE WHOLE TYPE-SAFETY ARGUMENT. A PBS target and
|
|
82
|
+
* a PVE target are the same two strings, so with an OPTIONAL discriminant TypeScript's
|
|
83
|
+
* structural typing would accept a PBS host wherever a PVE one belongs — and `pve()` would
|
|
84
|
+
* then build `PVEAPIToken=<id>=<secret>` against a server wanting
|
|
85
|
+
* `PBSAPIToken=<id>:<secret>`. Every call 401s, `pveOperations.read` folds that into "absent",
|
|
86
|
+
* the plan says CREATE for an object that is plainly there, the POST is refused, and not one
|
|
87
|
+
* symptom points at the header. Required, the mistake is a compile error instead.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ REASONED FROM PBS'S DOCUMENTED SCHEME, NOT MEASURED. The estate has no PBS credential yet
|
|
90
|
+
* (there is no PBS mount in OpenBao), and an unauthenticated probe cannot tell the schemes
|
|
91
|
+
* apart — `https://pbs.example.com:8007/api2/json/nodes` answers 401 to a missing header, a
|
|
92
|
+
* PVE-shaped one and a PBS-shaped one alike. The first real token will confirm or correct it,
|
|
93
|
+
* and a wrong guess fails closed with a 401 rather than doing something odd.
|
|
94
|
+
*/
|
|
95
|
+
readonly scheme: 'pve';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The same client against a Proxmox Backup Server. See the ⛔ on `scheme` above. */
|
|
99
|
+
export type PbsTarget = {
|
|
100
|
+
readonly mount: string;
|
|
101
|
+
readonly api: string;
|
|
102
|
+
readonly scheme: 'pbs';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Either product. What `pve()` and `pveOperations` accept; what a RESOURCE accepts is narrower. */
|
|
106
|
+
export type ApiTarget = PbsTarget | PveTarget;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a mint returns.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ `secret` IS PRESENT AND MUST NOT BE PERSISTED. It is typed as a plain string rather than
|
|
112
|
+
* `Redacted` on purpose: Alchemy's Redacted is a STATE-ENCODING marker, not encryption, so
|
|
113
|
+
* wrapping it would imply a protection that does not exist. The protection here is that this
|
|
114
|
+
* value never leaves the function that builds the Authorization header.
|
|
115
|
+
*/
|
|
116
|
+
export type PveCredential = {
|
|
117
|
+
/** `hf-provision@pve!hf-provision-…` — an identifier, safe to log. */
|
|
118
|
+
readonly tokenId: string;
|
|
119
|
+
/** ⛔ NEVER LOG, NEVER PERSIST, NEVER RETURN FROM A RESOURCE. */
|
|
120
|
+
readonly secret: string;
|
|
121
|
+
/** Seconds the lease was granted for, as reported by OpenBao. */
|
|
122
|
+
readonly leaseSeconds: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The environment `mint` resolves OpenBao from — `process.env` unless a test passes its own. */
|
|
126
|
+
export type BaoEnvironment = Readonly<Record<string, string | undefined>>;
|
|
127
|
+
|
|
128
|
+
/** openbao v2.6.2 api/env.go:22-34 — a present BAO_* wins, even empty; else its VAULT_* twin. */
|
|
129
|
+
const baoVariable = (env: BaoEnvironment, name: string) =>
|
|
130
|
+
env[`BAO_${name}`] ?? env[`VAULT_${name}`] ?? '';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Mint one credential for `role`: `GET /v1/<mount>/creds/<role>` on OpenBao's HTTP API.
|
|
134
|
+
*
|
|
135
|
+
* 🔴 IT SHELLED OUT TO `bao read -format=json` UNTIL 2026-09-14. The case for the CLI was that one
|
|
136
|
+
* client could not disagree with `bao kv get` about what this machine may do. house/openbao then
|
|
137
|
+
* learned twice what deciding by stderr costs — every failure read as absent, and two commands
|
|
138
|
+
* said "absent" in different words — so this reads STATUS CODES: 2xx is a credential, and 403
|
|
139
|
+
* (denied), 404 (no such role or mount — a mint has no "absent"), 503 (sealed) all fail.
|
|
140
|
+
* ★ THE ENVIRONMENT CONTRACT IS STILL THE CLI's, READ THE CLI's WAY, which keeps the case above
|
|
141
|
+
* true (openbao v2.6.2 api/client.go:34-66, 370, 503-507, 761-762, 776-785): BAO_AGENT_ADDR over
|
|
142
|
+
* BAO_ADDR over https://127.0.0.1:8200; BAO_NAMESPACE as X-Vault-Namespace; BAO_TOKEN as
|
|
143
|
+
* X-Vault-Token; X-Vault-Request always. house/openbao/src/bao-address.ts has the long form.
|
|
144
|
+
* ★ AGENT MODE IS NO BAO_TOKEN AND NO TOKEN HEADER. An Agent or Proxy listener with
|
|
145
|
+
* `api_proxy { use_auto_auth_token = true }` (or the older `cache { … }`) substitutes its
|
|
146
|
+
* auto-auth token exactly when a request carries none — command/agentproxyshared/cache/handler.go
|
|
147
|
+
* :29-38, command/agent.go:517-541, command/agent/config/config.go:288-292, and
|
|
148
|
+
* website/content/docs/agent-and-proxy/agent/apiproxy.mdx:57-62. The CLI's token-file fallback
|
|
149
|
+
* is deliberately not read. ⚠️ So an unset BAO_TOKEN aimed at a SERVER rather than an agent is a
|
|
150
|
+
* 403, which reads like a missing grant rather than a missing variable.
|
|
151
|
+
* ★ UNIX SOCKETS: `unix:///path` is plain HTTP to `localhost` over that socket, as the CLI does it
|
|
152
|
+
* (api/client.go:654-667). Bun's fetch takes a `unix` option; `FetchHttpClient.RequestInit` is
|
|
153
|
+
* Effect's documented way to pass fetch options, read from the calling fiber (effect
|
|
154
|
+
* FetchHttpClient.ts:59-72). Off Bun it REFUSES, rather than reach http://localhost:80.
|
|
155
|
+
*/
|
|
156
|
+
export const mint = (target: ApiTarget, role: PveRole, env: BaoEnvironment = process.env) =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const path = `${target.mount}/creds/${role}`;
|
|
159
|
+
// ⛔ METHOD, PATH AND OpenBao's `errors` ONLY — never a header (the token), never a success
|
|
160
|
+
// body (the credential is IN it, so echoing it would print the secret into a log).
|
|
161
|
+
const refuse = (status: number, detail: string) =>
|
|
162
|
+
new Error(`OpenBao GET /v1/${path} -> ${status === 0 ? 'no response' : status}: ${detail}`);
|
|
163
|
+
const address =
|
|
164
|
+
baoVariable(env, 'AGENT_ADDR') || baoVariable(env, 'ADDR') || 'https://127.0.0.1:8200';
|
|
165
|
+
const socket = address.startsWith('unix://') ? address.slice('unix://'.length) : undefined;
|
|
166
|
+
const base = socket === undefined ? address.replace(/\/+$/, '') : 'http://localhost';
|
|
167
|
+
const headers: Record<string, string> = { 'X-Vault-Request': 'true' };
|
|
168
|
+
const namespace = baoVariable(env, 'NAMESPACE');
|
|
169
|
+
if (namespace !== '') headers['X-Vault-Namespace'] = namespace;
|
|
170
|
+
const token = baoVariable(env, 'TOKEN');
|
|
171
|
+
if (token !== '') headers['X-Vault-Token'] = token;
|
|
172
|
+
if (socket !== undefined && !('Bun' in globalThis)) {
|
|
173
|
+
return yield* Effect.fail(refuse(0, 'a unix:// address needs Bun fetch; this is not Bun'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const client = yield* HttpClient.HttpClient;
|
|
177
|
+
// ⛔ Effect records request headers on the client span, and its default redaction list does
|
|
178
|
+
// not name x-vault-token (effect Headers.ts:753-762). It is added for this call.
|
|
179
|
+
const redacted = yield* Headers.CurrentRedactedNames;
|
|
180
|
+
const defaults = Option.getOrElse(
|
|
181
|
+
yield* Effect.serviceOption(FetchHttpClient.RequestInit),
|
|
182
|
+
() => ({}),
|
|
183
|
+
);
|
|
184
|
+
const request = HttpClientRequest.get(`${base}/v1/${path}`).pipe(
|
|
185
|
+
HttpClientRequest.setHeaders(headers),
|
|
186
|
+
);
|
|
187
|
+
const exchange = client.execute(request).pipe(
|
|
188
|
+
Effect.flatMap((response) =>
|
|
189
|
+
Effect.map(response.text, (text) => ({ status: response.status, text })),
|
|
190
|
+
),
|
|
191
|
+
// ⚠️ The CLI's default client timeout (api/client.go:324) — a hung agent must not hang a plan.
|
|
192
|
+
Effect.timeout('60 seconds'),
|
|
193
|
+
Effect.provideService(Headers.CurrentRedactedNames, [...redacted, 'x-vault-token']),
|
|
194
|
+
);
|
|
195
|
+
// ⚠️ `| undefined` under exactOptionalPropertyTypes: `socket` is narrowed to a string
|
|
196
|
+
// only on the branch below, so the object literal genuinely carries
|
|
197
|
+
// `unix: string | undefined` and the type must say so. The alternative — building
|
|
198
|
+
// the key conditionally — hides that this request is the unix-socket path.
|
|
199
|
+
const init: RequestInit & { unix?: string | undefined } = { ...defaults, unix: socket };
|
|
200
|
+
const { status, text } = yield* (
|
|
201
|
+
socket === undefined
|
|
202
|
+
? exchange
|
|
203
|
+
: Effect.provideService(exchange, FetchHttpClient.RequestInit, init)
|
|
204
|
+
).pipe(
|
|
205
|
+
// ⛔ `.message` only — the error object holds the request, and the request holds the token.
|
|
206
|
+
Effect.mapError((cause) => refuse(0, cause.message)),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
let parsed:
|
|
210
|
+
| {
|
|
211
|
+
lease_duration?: unknown;
|
|
212
|
+
data?: { token_id?: unknown; secret?: unknown };
|
|
213
|
+
errors?: unknown;
|
|
214
|
+
}
|
|
215
|
+
| undefined;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(text) as typeof parsed;
|
|
218
|
+
} catch {
|
|
219
|
+
parsed = undefined;
|
|
220
|
+
}
|
|
221
|
+
if (status < 200 || status >= 300) {
|
|
222
|
+
const errors = parsed?.errors;
|
|
223
|
+
const detail = Array.isArray(errors)
|
|
224
|
+
? errors.map(String).join('; ')
|
|
225
|
+
: 'not an OpenBao error body';
|
|
226
|
+
return yield* Effect.fail(refuse(status, detail || '(no errors given)'));
|
|
227
|
+
}
|
|
228
|
+
const tokenId = parsed?.data?.token_id;
|
|
229
|
+
const secret = parsed?.data?.secret;
|
|
230
|
+
if (typeof tokenId !== 'string' || typeof secret !== 'string') {
|
|
231
|
+
// ⛔ REFUSE RATHER THAN RETURN A HALF CREDENTIAL, and again WITHOUT quoting the body.
|
|
232
|
+
return yield* Effect.fail(
|
|
233
|
+
new Error(
|
|
234
|
+
`${target.mount}/creds/${role} returned no token_id/secret. Either this approle lacks ` +
|
|
235
|
+
`the grant, or the mount is not ${target.mount}. Say which mount and role you needed ` +
|
|
236
|
+
`than falling back to a stored credential -- every stored PVE token is read-only.`,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const lease = parsed?.lease_duration;
|
|
241
|
+
const leaseSeconds = typeof lease === 'number' ? lease : 0;
|
|
242
|
+
return { leaseSeconds, secret, tokenId } satisfies PveCredential;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The header PVE wants. `PVEAPIToken=<tokenid>=<secret>`.
|
|
247
|
+
*
|
|
248
|
+
* ⚠️ THE SEPARATOR IS `=`, NOT `:`. Proxmox rejects the colon form with a 401 that says only
|
|
249
|
+
* "authentication failure", which reads as a bad credential rather than as a malformed header.
|
|
250
|
+
*/
|
|
251
|
+
export const authorization = (credential: PveCredential, target: ApiTarget): string =>
|
|
252
|
+
target.scheme === 'pbs'
|
|
253
|
+
? `PBSAPIToken=${credential.tokenId}:${credential.secret}`
|
|
254
|
+
: `PVEAPIToken=${credential.tokenId}=${credential.secret}`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A firewall alias on the wire: what PVE stores, and what it quietly changes on the way.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF firewall-alias.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the
|
|
5
|
+
* same one metric-server.ts and node-network.ts cut: this file answers "what does the cluster do
|
|
6
|
+
* to a value", firewall-alias.ts answers "what is an alias and when has it changed". Nothing here
|
|
7
|
+
* reads the cluster and nothing here decides a diff.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ THE PUT IS A FULL REPLACE, WHICH IS THE EXACT INVERSE OF metric-server.ts — DO NOT COPY THAT
|
|
10
|
+
* FILE'S RULE OVER HERE. A metric server is a SectionConfig section and a PUT merges into it, so
|
|
11
|
+
* an omitted field SURVIVES and a managed optional needs a matching `delete=` or it can never be
|
|
12
|
+
* cleared. An alias is not: `update_alias` builds `$data = { name, cidr }`, adds `comment` only
|
|
13
|
+
* when it is truthy, and assigns the whole hash over the old one. So an omitted `comment` is
|
|
14
|
+
* CLEARED here, there is no `delete` parameter on this endpoint at all, and sending
|
|
15
|
+
* `comment=''` would be a second spelling of the same thing.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ THE SAME REPLACE MAKES `cidr` REQUIRED ON EVERY UPDATE. PVE's PUT schema does not mark it
|
|
18
|
+
* optional, and the Perl would build an entry with an undefined cidr if it could — so `body`
|
|
19
|
+
* below sends it on update as well as on create, rather than treating it as "only send what
|
|
20
|
+
* changed".
|
|
21
|
+
*/
|
|
22
|
+
import type { FirewallAliasProps } from './firewall-alias.ts';
|
|
23
|
+
import { text } from './values.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The alias key, as PVE computes it.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ EVERY ENTRY POINT LOWERS THE NAME AND NOTHING LOWERS THE STORED ONE: create, read, update and
|
|
29
|
+
* delete all start `my $name = lc($param->{name})`, while the entry keeps `name => $param->{name}`
|
|
30
|
+
* as written. So this is identity, and the stored spelling is decoration. Used by `diff` to tell
|
|
31
|
+
* an edit from a different alias — a comparison of the raw names would call a re-capitalisation a
|
|
32
|
+
* replace, and delete an alias to put back the same one.
|
|
33
|
+
*/
|
|
34
|
+
export const fold = (name: string) => name.trim().toLowerCase();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A CIDR reduced to the one form PVE will hand back.
|
|
38
|
+
*
|
|
39
|
+
* ⛔ MEASURED by running the cluster's own `PVE::Firewall::parse_alias` on n2 (pve-manager 9.2.11,
|
|
40
|
+
* 2026-09-13) — a pure function over a string, no config read and none written:
|
|
41
|
+
* `10.1.1.5/32` -> `10.1.1.5` a /32 host suffix is STRIPPED
|
|
42
|
+
* `2001:0DB8::1/128`-> `2001:0DB8::1` a /128 is stripped, and the case is NOT folded
|
|
43
|
+
* `2001:db8::/32` -> `2001:db8::/32` a /32 on IPv6 is a real prefix and SURVIVES
|
|
44
|
+
* `parse_ip_or_cidr` is where it happens: `s|/32$||` under the IPv4 branch and `s|/128$||` under
|
|
45
|
+
* the IPv6 one. Unmatched, a declared `10.1.1.5/32` reads back as `10.1.1.5` and reports an
|
|
46
|
+
* update on every plan for the rest of time, each deploy writing back a value it already holds.
|
|
47
|
+
*
|
|
48
|
+
* ⛔ SO THE STRIP IS VERSION-AWARE, AND AN UNCONDITIONAL `/32` WOULD CORRUPT THE COMPARISON. A
|
|
49
|
+
* colon cannot appear in an IPv4 address and must appear in an IPv6 one, which is the whole
|
|
50
|
+
* discriminator PVE's own two regexes come down to here. Reading `2001:db8::/32` as
|
|
51
|
+
* `2001:db8::` would make a /32 prefix compare equal to a single host.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ AND IT STOPS THERE, ON PURPOSE. PVE does not lowercase, zero-pad or compress an IPv6 literal,
|
|
54
|
+
* so neither may this: it stores what it was handed and gives that back, which means any spelling
|
|
55
|
+
* is stable once written. Canonicalising further would make `matches` call two DIFFERENT stored
|
|
56
|
+
* strings equal and report noop over real drift — the same lie as missing a rewrite, pointed the
|
|
57
|
+
* other way.
|
|
58
|
+
*/
|
|
59
|
+
export const cidr = (value: unknown) => {
|
|
60
|
+
const raw = text(value).trim();
|
|
61
|
+
const host = raw.includes(':') ? '/128' : '/32';
|
|
62
|
+
return raw.endsWith(host) ? raw.slice(0, -host.length) : raw;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A comment reduced to the one form PVE will hand back.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ AN ALIAS COMMENT IS A TRAILING `#` ON ITS LINE IN `cluster.fw`, AND THE ROUND TRIP TRIMS IT.
|
|
69
|
+
* MEASURED with the same parser run: `Cmt 10.2.0.0/16 # spaced comment ` comes back as
|
|
70
|
+
* `spaced comment`. `parse_alias` strips the surrounding whitespace (`s/\s*#\s*(.*?)\s*$//`) and
|
|
71
|
+
* `format_aliases` refuses to write a blank one at all, so an untrimmed declaration would diff
|
|
72
|
+
* against its own stored form forever.
|
|
73
|
+
*
|
|
74
|
+
* ⛔ A COMMENT OF EXACTLY `0` CANNOT BE STORED, AND READING THAT AS DRIFT WOULD LOOP FOREVER.
|
|
75
|
+
* MEASURED: `Zero 10.3.0.0/16 # 0` parses to an entry with NO comment key. Both the create and
|
|
76
|
+
* the update guard with Perl's `if $param->{comment}`, and `'0'` is false in Perl — so the string
|
|
77
|
+
* is dropped before it ever reaches the file, on write as well as on read. Folded to `''` here
|
|
78
|
+
* because that is what the cluster will do with it: a declaration asking for something PVE will
|
|
79
|
+
* not keep should plan as noop and be documented, not diff on every plan and be unfixable by the
|
|
80
|
+
* very PUT the diff asks for.
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ A LINE FEED IS A 400, NOT A TRUNCATION. `pve_fw_verify_comment_spec` dies with "comment must
|
|
83
|
+
* not contain a line feed", which is the honest answer and is left to PVE rather than silently
|
|
84
|
+
* flattened here — flattening would store something the declaration did not say.
|
|
85
|
+
*/
|
|
86
|
+
export const comment = (value: unknown) => {
|
|
87
|
+
const trimmed = text(value).trim();
|
|
88
|
+
return trimmed === '0' ? '' : trimmed;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Everything mutable, in the form PVE wants. Create adds `name`; update sends exactly this.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ `name` IS NOT SENT ON UPDATE BECAUSE IT IS ALREADY THE LAST SEGMENT OF THE PATH BEING PUT TO,
|
|
95
|
+
* and a second copy can only disagree with it — the same reasoning metric-server.ts gives for
|
|
96
|
+
* omitting its `id`. PVE takes the path segment as `$param->{name}` and writes it back as the
|
|
97
|
+
* stored spelling, so a PUT for any other reason also re-capitalises the entry to match the
|
|
98
|
+
* declaration. That is a side effect worth knowing about and not worth causing on its own.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ THE NORMALISED CIDR IS WHAT GETS WRITTEN, NOT THE DECLARED SPELLING. PVE stores the parameter
|
|
101
|
+
* verbatim into `cluster.fw` and only strips the host suffix when it reads the file back, so
|
|
102
|
+
* sending `10.1.1.5` rather than `10.1.1.5/32` leaves the file saying exactly what the next read
|
|
103
|
+
* will answer. Both settle; this one leaves no pair of values that merely happen to agree.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ `rename` IS DELIBERATELY ABSENT — see the ⚠️ on renames in firewall-alias.ts. A `rename` in
|
|
106
|
+
* this form would be a second, invisible identity for the object, set from a field the resource
|
|
107
|
+
* does not have.
|
|
108
|
+
*/
|
|
109
|
+
export const body = (props: FirewallAliasProps): Record<string, string> => {
|
|
110
|
+
const note = comment(props.comment);
|
|
111
|
+
return { cidr: cidr(props.cidr), ...(note === '' ? {} : { comment: note }) };
|
|
112
|
+
};
|