@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,212 @@
|
|
|
1
|
+
import { Resource } from 'alchemy';
|
|
2
|
+
import { isResolved } from 'alchemy/Diff';
|
|
3
|
+
import * as Provider from 'alchemy/Provider';
|
|
4
|
+
import * as Effect from 'effect/Effect';
|
|
5
|
+
import * as FileSystem from 'effect/FileSystem';
|
|
6
|
+
import * as Path from 'effect/Path';
|
|
7
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
8
|
+
import { sha256 } from './digest.ts';
|
|
9
|
+
import { isEmptyAssembly } from './policy-assembly.ts';
|
|
10
|
+
import { deletePolicy, readPolicy, writePolicy } from './policy-wire.ts';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An OpenBao ACL policy, assembled from the HCL fragments that declare it.
|
|
14
|
+
*
|
|
15
|
+
* ★ WHY THIS EXISTS. The estate already hand-rolled both halves of what Alchemy provides:
|
|
16
|
+
* five `apply-*.sh`/`apply-*.py` scripts converge OpenBao, and separate `check-*.sh`
|
|
17
|
+
* gates compare live to repo and refuse on drift. That is `reconcile` and `diff` written
|
|
18
|
+
* twice, in two languages, with nothing recording what was applied — and one of those
|
|
19
|
+
* gates blocked a deploy today because its own path had rotted.
|
|
20
|
+
*
|
|
21
|
+
* ⛔ THERE IS NO `Bao.Secret`, AND THERE MUST NEVER BE ONE. Alchemy persists attributes to
|
|
22
|
+
* its state store and does NOT encrypt them: StateEncoding.ts unwraps `Redacted` to
|
|
23
|
+
* plaintext under a marker key, and nothing under alchemy/src/State encrypts. This
|
|
24
|
+
* estate's state store is the `alchemy` Postgres database, which pg-backup.sh dumps
|
|
25
|
+
* nightly (it enumerates EVERY non-template database), copies to CT100, and PBS backs up
|
|
26
|
+
* from there. A KV value here would exist in four places, none of them OpenBao.
|
|
27
|
+
*
|
|
28
|
+
* So this declares the SHAPE AROUND secrets. Every attribute it stores is a name, a
|
|
29
|
+
* count, or a hash.
|
|
30
|
+
*
|
|
31
|
+
* ⛔ EVERY PRIMITIVE IS ALCHEMY'S OR EFFECT'S, NOT NODE'S. An earlier cut of this file used
|
|
32
|
+
* `node:child_process` + `promisify(execFile)` and `node:fs/promises`, which is
|
|
33
|
+
* hand-rolling inside a bun + Effect codebase: it loses the Scope, the typed
|
|
34
|
+
* PlatformError, and the Layer that makes the provider testable without a real vault.
|
|
35
|
+
* ★ `CommandExecutor.run` was tried first and blocked read/diff: Alchemy passes a session
|
|
36
|
+
* to reconcile/delete but NOT to read/diff (Provider.ts:258-289), so the fix was Alchemy's
|
|
37
|
+
* OWN Docker pattern — `ChildProcessSpawner` + `ChildProcess.make`, which needs only a
|
|
38
|
+
* Scope (Docker.ts:476-553). ★ SINCE 2026-09-14 THE CALLS ARE OpenBao's HTTP API through
|
|
39
|
+
* Effect's `HttpClient` (bao-http.ts), which needs no session either, so read/diff still work.
|
|
40
|
+
* FileSystem and Path come from `effect/*` directly — in Effect 4 they moved into core.
|
|
41
|
+
*/
|
|
42
|
+
export interface BaoPolicyProps {
|
|
43
|
+
/** Policy name, as `sys/policies/acl/<name>` takes it. */
|
|
44
|
+
name: string;
|
|
45
|
+
/**
|
|
46
|
+
* Directory of `*.hcl` fragments.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ CONCATENATED IN SORTED ORDER, EACH FOLLOWED BY A NEWLINE — byte-identical to
|
|
49
|
+
* apply-agent-policy.sh, which is what wrote the live policy. Any other joining makes
|
|
50
|
+
* the first plan report an update on a policy nobody changed.
|
|
51
|
+
*/
|
|
52
|
+
fragments: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BaoPolicyAttributes {
|
|
56
|
+
/** Policy name. */
|
|
57
|
+
name: string;
|
|
58
|
+
/** SHA-256 of the assembled HCL. The whole diff, and safe to persist. */
|
|
59
|
+
digest: string;
|
|
60
|
+
/** `path "..."` grants — the number apply-agent-policy.sh prints. */
|
|
61
|
+
grants: number;
|
|
62
|
+
/** Fragment files that produced it, so a plan reads like the directory. */
|
|
63
|
+
parts: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BaoPolicy extends Resource<
|
|
67
|
+
'Bao.Policy',
|
|
68
|
+
BaoPolicyProps,
|
|
69
|
+
BaoPolicyAttributes,
|
|
70
|
+
never,
|
|
71
|
+
HttpClient.HttpClient | FileSystem.FileSystem | Path.Path
|
|
72
|
+
> {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ⛔ `retain` — DELETING A POLICY REVOKES EVERY GRANT IT CARRIES, and this is the ONLY family in
|
|
76
|
+
* this package with live instances, so it is the only one where the default could ever fire.
|
|
77
|
+
*
|
|
78
|
+
* 🔴 IT WAS THE ONLY FAMILY WITHOUT IT. Every sibling passes `{ defaultRemovalPolicy: 'retain' }`
|
|
79
|
+
* — auth-role.ts:37, mount.ts:40, pki-role.ts, proxmox-role.ts, ssh-role.ts — each with a ★
|
|
80
|
+
* explaining what deletion destroys. This file has the same warning on its own `delete` handler
|
|
81
|
+
* and did not carry the option, which is the worst possible combination: the danger is
|
|
82
|
+
* documented and the guard is missing.
|
|
83
|
+
* ⛔ WHAT THAT MEANT IN PRACTICE, on 2026-09-13: ten live policies are named in a `const` array
|
|
84
|
+
* in alchemy.run.ts. Removing a name from that array orphans the resource, and without
|
|
85
|
+
* `retain` the next deploy calls `bao policy delete` on it — including the 86-line policy that
|
|
86
|
+
* decides what the agent working in this repo can reach. A one-word edit, no plan line saying
|
|
87
|
+
* "revoke", and the grants are gone. The array grew from four names to ten that same day,
|
|
88
|
+
* which multiplied the blast radius of a mistake nobody had guarded against.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ `retain` DOES NOT WEAKEN `delete`, WHICH STAYS FULLY IMPLEMENTED. The engine skips it for an
|
|
91
|
+
* orphaned resource; a caller that means it opts in with `.pipe(RemovalPolicy.destroy())`. This
|
|
92
|
+
* is Terraform's `prevent_destroy`, not a stubbed operation — a delete that silently did nothing
|
|
93
|
+
* would lie to whoever read the plan.
|
|
94
|
+
*/
|
|
95
|
+
export const BaoPolicy = Resource<BaoPolicy>('Bao.Policy', {
|
|
96
|
+
defaultRemovalPolicy: 'retain',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** `path "..."` grant count — the same number the apply script prints. */
|
|
100
|
+
const grantsOf = (hcl: string) => (hcl.match(/^path /gm) ?? []).length;
|
|
101
|
+
|
|
102
|
+
const attributesOf = (name: string, hcl: string, parts: number): BaoPolicyAttributes => ({
|
|
103
|
+
name,
|
|
104
|
+
digest: sha256(hcl),
|
|
105
|
+
grants: grantsOf(hcl),
|
|
106
|
+
parts,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const BaoPolicyProvider = () =>
|
|
110
|
+
Provider.effect(
|
|
111
|
+
BaoPolicy,
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const fs = yield* FileSystem.FileSystem;
|
|
114
|
+
const path = yield* Path.Path;
|
|
115
|
+
|
|
116
|
+
/** Assemble fragments exactly as apply-agent-policy.sh does. */
|
|
117
|
+
const assemble = (dir: string) =>
|
|
118
|
+
Effect.gen(function* () {
|
|
119
|
+
const entries = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith('.hcl')).sort();
|
|
120
|
+
let joined = '';
|
|
121
|
+
for (const f of entries) joined += `${yield* fs.readFileString(path.join(dir, f))}\n`;
|
|
122
|
+
return { joined, parts: entries.length };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return BaoPolicy.Provider.of({
|
|
126
|
+
/**
|
|
127
|
+
* ⛔ `bao policy list` IS NOT A LIST OF THINGS THIS OWNS. Every policy in the
|
|
128
|
+
* namespace answers, including ones written by hand years ago. Returning them
|
|
129
|
+
* would invite Alchemy to adopt — and then delete — policies it never created.
|
|
130
|
+
*/
|
|
131
|
+
list: () => Effect.succeed([]),
|
|
132
|
+
|
|
133
|
+
// ★ '' means genuinely absent (a 404); a refusal fails. policy-wire.ts has the history.
|
|
134
|
+
read: Effect.fn(function* ({ olds }) {
|
|
135
|
+
const live = yield* readPolicy(olds.name);
|
|
136
|
+
return live.length === 0 ? undefined : attributesOf(olds.name, live, 0);
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* ⛔ IT COMPARES THE LIVE POLICY, NOT THE STORED DIGEST. A policy edited in the
|
|
141
|
+
* OpenBao UI is exactly the drift the check-* gates exist to catch, and a
|
|
142
|
+
* provider that trusted its own state would report `noop` straight through it.
|
|
143
|
+
*/
|
|
144
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
145
|
+
// ⚠️ A prop can still be an unresolved Output or Config at plan time. Docker's
|
|
146
|
+
// own providers guard with isResolved and skip rather than guess; a diff that
|
|
147
|
+
// read a Config as a string would compare a placeholder to real HCL.
|
|
148
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
149
|
+
const { joined } = yield* assemble(news.fragments);
|
|
150
|
+
const live = yield* readPolicy(news.name);
|
|
151
|
+
return live.length > 0 && sha256(live) === sha256(joined)
|
|
152
|
+
? ({ action: 'noop' } as const)
|
|
153
|
+
: ({ action: 'update' } as const);
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
157
|
+
const name = news.name;
|
|
158
|
+
const { joined, parts } = yield* assemble(news.fragments);
|
|
159
|
+
/**
|
|
160
|
+
* ⛔ AN EMPTY ASSEMBLY IS A REFUSAL, NOT AN EMPTY POLICY. `bao policy write` with
|
|
161
|
+
* no paths SILENTLY REVOKES every grant the policy carries, for every token
|
|
162
|
+
* bound to it — and a mistyped or moved fragments directory reads exactly like
|
|
163
|
+
* a policy with nothing in it. This estate lost a deploy today to a directory
|
|
164
|
+
* that moved; it must not lose a vault to one.
|
|
165
|
+
*/
|
|
166
|
+
// ⛔ No FILES was the first guard; no GRANT is the real one — see policy-assembly.ts.
|
|
167
|
+
if (isEmptyAssembly(parts, joined))
|
|
168
|
+
return yield* Effect.die(
|
|
169
|
+
new Error(
|
|
170
|
+
`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` +
|
|
171
|
+
`(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
/**
|
|
175
|
+
* ⛔ ADOPTING A POLICY THAT ALREADY MATCHES MUST NOT REWRITE IT. Alchemy's `adopted`
|
|
176
|
+
* action is NOT a read — Apply.ts routes it down the same branch as `update` and calls
|
|
177
|
+
* reconcile — so the first deploy after declaring the four policies this estate
|
|
178
|
+
* already has would have run `bao policy write` over all four. Byte-identical in the
|
|
179
|
+
* happy case, and a SILENT REWRITE OF LIVE GRANTS the moment the repo fragments have
|
|
180
|
+
* drifted from what is deployed.
|
|
181
|
+
*
|
|
182
|
+
* ★ THE GUARD IS THE SAME EXPRESSION `diff` USES, deliberately: one definition of
|
|
183
|
+
* "settled" for this resource, so the plan and the write can never disagree. The
|
|
184
|
+
* sibling PVE provider does exactly this in pveOperations.reconcile.
|
|
185
|
+
*/
|
|
186
|
+
const current = yield* readPolicy(name);
|
|
187
|
+
if (current.length > 0 && sha256(current) === sha256(joined)) {
|
|
188
|
+
return attributesOf(name, joined, parts);
|
|
189
|
+
}
|
|
190
|
+
// ⚠️ In the request body, never on argv — see `writePolicy` for why the temp file went.
|
|
191
|
+
yield* writePolicy(name, joined);
|
|
192
|
+
return attributesOf(name, joined, parts);
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* ⛔ DELETING A POLICY REVOKES EVERY GRANT IT CARRIES. Idempotent as Alchemy
|
|
197
|
+
* requires — already gone is success — but this is the one operation with teeth.
|
|
198
|
+
*/
|
|
199
|
+
delete: Effect.fn(function* ({ output }) {
|
|
200
|
+
/**
|
|
201
|
+
* ⛔ A REFUSED DELETE FAILS. This is "the one operation with teeth" the note above
|
|
202
|
+
* names, and it used to run through the read helper, which discarded the exit code —
|
|
203
|
+
* so a REFUSED policy delete reported success and the row left Alchemy's state while
|
|
204
|
+
* every grant it carried stayed live. Idempotence is preserved: a 404 is still
|
|
205
|
+
* success, and a 403 is not (bao-status.ts).
|
|
206
|
+
*/
|
|
207
|
+
yield* deletePolicy(output.name);
|
|
208
|
+
return undefined;
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire and comparison half of Bao.ProxmoxRole — the resource itself is proxmox-role.ts.
|
|
3
|
+
*
|
|
4
|
+
* MEASURED 2026-09-13, read-only, against the live server (BAO_ADDR=http://127.0.0.1:8200,
|
|
5
|
+
* BAO_NAMESPACE=homeflare). No write of any kind was issued to a role or a mount:
|
|
6
|
+
*
|
|
7
|
+
* bao read -format=json proxmox-tb4/roles/read
|
|
8
|
+
* → { max_ttl: 21600, mint_user: "hf-read@pve", name: "read", ttl: 3600 }
|
|
9
|
+
* bao read -format=json proxmox-tb4/roles/provision
|
|
10
|
+
* → { max_ttl: 1800, mint_user: "hf-provision@pve", name: "provision", ttl: 300 }
|
|
11
|
+
* GET /v1/proxmox-tb4/?help=1 → components.schemas.WriteRolesNameRequest:
|
|
12
|
+
* mint_user string — the ONLY required field, described as "The `user@realm` whose
|
|
13
|
+
* standing ACL is the minted token's entire privilege set. Created
|
|
14
|
+
* out-of-band; this engine never grants ACLs."
|
|
15
|
+
* ttl integer — "format": "seconds"
|
|
16
|
+
* max_ttl integer — "format": "seconds", "Narrows the mount's; can never widen it."
|
|
17
|
+
*
|
|
18
|
+
* ★ name + mint_user + ttl + max_ttl IS THE WHOLE ROLE. The plugin stores nothing else, so
|
|
19
|
+
* `matches` below covers 100% of the server-side state: there is no unmanaged field left to
|
|
20
|
+
* drift behind a green plan. That completeness is bought by making the TTLs REQUIRED props
|
|
21
|
+
* rather than the optional-means-unmanaged shape Bao.Mount uses — see the ⛔ on writeBody.
|
|
22
|
+
*/
|
|
23
|
+
import { sha256 } from './digest.ts';
|
|
24
|
+
import { mountPath, parseDuration, ttlSeconds } from './mount-form.ts';
|
|
25
|
+
|
|
26
|
+
export interface BaoProxmoxRoleProps {
|
|
27
|
+
/** Mount path of the proxmox secrets engine, e.g. `proxmox-tb4`. A trailing slash is fine. */
|
|
28
|
+
mount: string;
|
|
29
|
+
/**
|
|
30
|
+
* Role name, as `<mount>/roles/<name>` takes it.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ The plugin's own schema says the name "Appears in the minted token's id, so keep it
|
|
33
|
+
* short." Both live roles are one word (`read`, `provision`). A long name here becomes a
|
|
34
|
+
* long PVE token id on every mint.
|
|
35
|
+
*/
|
|
36
|
+
name: string;
|
|
37
|
+
/**
|
|
38
|
+
* ⛔ THE ENTIRE SECURITY BOUNDARY OF THIS RESOURCE. A minted token inherits this PVE user's
|
|
39
|
+
* standing ACL and nothing else — the engine never grants ACLs, it only borrows one. Change
|
|
40
|
+
* this string and every credential the role issues from then on carries a different
|
|
41
|
+
* privilege set, with no other field in the declaration changing to hint at it. That is why
|
|
42
|
+
* it is compared in `matches` below, and why reconcile refuses to move it without
|
|
43
|
+
* `allowMintUserChange`.
|
|
44
|
+
*/
|
|
45
|
+
mintUser: string;
|
|
46
|
+
/** Default lease TTL for minted tokens, e.g. `1h`. REQUIRED — see the ⛔ on writeBody. */
|
|
47
|
+
ttl: string;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum lease TTL, e.g. `6h`. REQUIRED — see the ⛔ on writeBody.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ "Narrows the mount's; can never widen it." A role declaring `6h` under a mount whose
|
|
52
|
+
* max_lease_ttl is `1h` still READS BACK as 21600, so this resource reports noop and the
|
|
53
|
+
* plan is green — while every actual mint is capped at 3600 by the mount. The role is not
|
|
54
|
+
* lying and neither is the plan; the ceiling simply lives one level up. If a consumer gets
|
|
55
|
+
* a shorter lease than declared here, check `bao read sys/mounts/<mount>` before touching
|
|
56
|
+
* the role.
|
|
57
|
+
*/
|
|
58
|
+
maxTtl: string;
|
|
59
|
+
/**
|
|
60
|
+
* Permission to re-scope `mintUser` on an EXISTING role: set it to the CURRENT live mint_user
|
|
61
|
+
* that this change is allowed to replace.
|
|
62
|
+
*
|
|
63
|
+
* ★ IT IS A STRING AND NOT A BOOLEAN, AND THAT IS THE WHOLE POINT. A `true` left behind after
|
|
64
|
+
* one legitimate rename would wave through every future re-scope of that role forever —
|
|
65
|
+
* including one pasted in by whoever copies this block as a template. Naming the value being
|
|
66
|
+
* replaced makes the permission SELF-EXPIRING: the moment the change lands, live equals
|
|
67
|
+
* `mintUser`, the guard stops consulting it, and the next re-scope finds a stale string that
|
|
68
|
+
* does not match live and is refused on its own merits.
|
|
69
|
+
*
|
|
70
|
+
* Not persisted, not in attributes, not in `matches` — it describes the operator's intent, not
|
|
71
|
+
* the server, so changing it alone is correctly a noop. See the ⛔ in proxmox-role.ts reconcile.
|
|
72
|
+
*/
|
|
73
|
+
allowMintUserChange?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BaoProxmoxRoleAttributes {
|
|
77
|
+
/** Mount path, trailing slash stripped. */
|
|
78
|
+
mount: string;
|
|
79
|
+
/** Role name. */
|
|
80
|
+
name: string;
|
|
81
|
+
/** The PVE `user@realm` live tokens are minted under. A name, never a credential. */
|
|
82
|
+
mintUser: string;
|
|
83
|
+
/** Live default lease TTL, rendered back into the duration shape props use. */
|
|
84
|
+
ttl: string;
|
|
85
|
+
/** Live maximum lease TTL, same shape. */
|
|
86
|
+
maxTtl: string;
|
|
87
|
+
/** SHA-256 of the managed fields — safe to persist; see policy.ts. */
|
|
88
|
+
digest: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const text = (value: unknown) => (typeof value === 'string' ? value : '');
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Seconds back into the duration shape the props use.
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ Third private copy of this function in the package (mount-form.ts and auth-role-form.ts
|
|
97
|
+
* each keep one). Exporting a single copy means editing a file this change does not own, so
|
|
98
|
+
* it is duplicated deliberately rather than silently. Worth collapsing in a follow-up.
|
|
99
|
+
*/
|
|
100
|
+
const ttlText = (seconds: number | undefined, fallback = '0') => {
|
|
101
|
+
if (seconds === undefined) return fallback;
|
|
102
|
+
if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;
|
|
103
|
+
if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;
|
|
104
|
+
if (seconds % 60 === 0) return `${String(seconds / 60)}m`;
|
|
105
|
+
return `${String(seconds)}s`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** `proxmox-tb4/roles/read` — read, write and delete all address the role here. */
|
|
109
|
+
export const rolePath = (mount: string, name: string) => `${mountPath(mount)}/roles/${name}`;
|
|
110
|
+
|
|
111
|
+
export const attributesOf = (
|
|
112
|
+
props: BaoProxmoxRoleProps,
|
|
113
|
+
live: Record<string, unknown>,
|
|
114
|
+
): BaoProxmoxRoleAttributes => {
|
|
115
|
+
const attrs = {
|
|
116
|
+
mount: mountPath(props.mount),
|
|
117
|
+
// ⚠️ The plugin echoes `name` in a role read, so it is taken from `live` with the prop only
|
|
118
|
+
// as a fallback. Every other value below is live-only: the same split the sibling
|
|
119
|
+
// resources use, so a read never launders a declaration back into itself as "current".
|
|
120
|
+
name: text(live['name']) || props.name,
|
|
121
|
+
mintUser: text(live['mint_user']),
|
|
122
|
+
ttl: ttlText(ttlSeconds(live['ttl'])),
|
|
123
|
+
maxTtl: ttlText(ttlSeconds(live['max_ttl'])),
|
|
124
|
+
};
|
|
125
|
+
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The body for `PUT <mount>/roles/<name>` — all strings, the `k=v` pairs `bao write` sent — or
|
|
130
|
+
* the prop names whose duration would not parse.
|
|
131
|
+
*
|
|
132
|
+
* ⛔ A WRITE TO roles/{name} REPLACES THE ROLE, IT DOES NOT MERGE. An omitted `ttl` is written
|
|
133
|
+
* as the field's zero value, so a resource that treated undeclared TTLs as "unmanaged" — the
|
|
134
|
+
* Bao.Mount shape — would silently reset a lease ceiling every time it rewrote the role for an
|
|
135
|
+
* unrelated reason. Both TTLs are therefore required props and EVERY write carries all three
|
|
136
|
+
* fields. REASONED from the Vault secrets-framework write semantics plus this plugin's schema
|
|
137
|
+
* marking only `mint_user` required; NOT measured, because this session was read-only and
|
|
138
|
+
* proving it would have meant writing to the live engine.
|
|
139
|
+
*
|
|
140
|
+
* ⚠️ Seconds go on the wire as integers, not as `1h`. The schema declares both TTLs
|
|
141
|
+
* `"format": "seconds"`, and an integer is accepted unambiguously by every path that reads
|
|
142
|
+
* them back — which is what keeps the round-trip through `matches` stable.
|
|
143
|
+
*/
|
|
144
|
+
export const writeBody = (
|
|
145
|
+
props: BaoProxmoxRoleProps,
|
|
146
|
+
): { ok: true; body: Record<string, string> } | { ok: false; bad: string[] } => {
|
|
147
|
+
const ttl = parseDuration(props.ttl);
|
|
148
|
+
const maxTtl = parseDuration(props.maxTtl);
|
|
149
|
+
const bad: string[] = [];
|
|
150
|
+
if (ttl === undefined) bad.push(`ttl=${props.ttl}`);
|
|
151
|
+
if (maxTtl === undefined) bad.push(`maxTtl=${props.maxTtl}`);
|
|
152
|
+
if (ttl === undefined || maxTtl === undefined) return { ok: false, bad };
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
body: { mint_user: props.mintUser, ttl: String(ttl), max_ttl: String(maxTtl) },
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* ⚠️ An unparseable declaration never equals anything, so it surfaces as an update that will
|
|
161
|
+
* never settle rather than as a false noop. reconcile turns that loop into one named error;
|
|
162
|
+
* see the `writeBody` refusal there.
|
|
163
|
+
*/
|
|
164
|
+
const sameTtl = (want: string, have: string) => {
|
|
165
|
+
const wantSeconds = parseDuration(want);
|
|
166
|
+
const haveSeconds = ttlSeconds(have);
|
|
167
|
+
return wantSeconds !== undefined && wantSeconds === haveSeconds;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** True when the live role already matches the declaration on every field the plugin stores. */
|
|
171
|
+
export const matches = (attributes: BaoProxmoxRoleAttributes, props: BaoProxmoxRoleProps) =>
|
|
172
|
+
attributes.mintUser === props.mintUser &&
|
|
173
|
+
sameTtl(props.ttl, attributes.ttl) &&
|
|
174
|
+
sameTtl(props.maxTtl, attributes.maxTtl);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A role on the estate's proxmox secrets plugin — the mapping from one consumer to one PVE
|
|
3
|
+
* mint user, plus the lease ceilings that consumer's tokens get. METADATA ONLY.
|
|
4
|
+
*
|
|
5
|
+
* ★ WHY THIS FAMILY MATTERS MORE THAN IT LOOKS. Declaring these roles is how the estate
|
|
6
|
+
* replaces HAND-MADE PERMANENT PVE TOKENS with short-lived minted ones. On TB4, SEVEN tokens
|
|
7
|
+
* carry expire=0 and privsep=0 — root@pam!proxbox-sync, mcp@pve!executor,
|
|
8
|
+
* monitoring@pve!exporter, sablier@pve!sablier, tofu@pve!ro, tofu@pve!apply and
|
|
9
|
+
* vaultmint@pve!engine. Six of those are one Bao.ProxmoxRole plus a scoped PVE user away from
|
|
10
|
+
* being a 5-minute lease instead of a forever key.
|
|
11
|
+
*
|
|
12
|
+
* The seventh cannot be. vaultmint@pve!engine is the credential the ENGINE ITSELF
|
|
13
|
+
* authenticates with in order to mint anything at all: the thing that issues short-lived
|
|
14
|
+
* tokens needs a standing one, and no role can resolve that chicken-and-egg. It stays
|
|
15
|
+
* permanent, which makes it the single most valuable token on the cluster and the one whose
|
|
16
|
+
* rotation must be a deliberate, hand-run procedure rather than a plan.
|
|
17
|
+
*
|
|
18
|
+
* MEASURED here: `bao read proxmox-tb4/config` returns parent_token_id
|
|
19
|
+
* `vaultmint@pve!engine`, parent_secret_set true, parent_secret_tail `…eb01` — the engine's
|
|
20
|
+
* own key, present and, by the schema's own "Write-only: never returned by a read", not
|
|
21
|
+
* retrievable. The seven-token inventory was handed to this change and NOT re-counted against
|
|
22
|
+
* the PVE API in this session.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for the full reasoning. Every attribute
|
|
25
|
+
* here is a mount path, a role name, a PVE username or a TTL. Nothing this resource stores
|
|
26
|
+
* would help anyone authenticate to anything.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ ISSUANCE IS NOT THIS RESOURCE, AND MUST NEVER BECOME ONE. `<mount>/creds/<name>` returns a
|
|
29
|
+
* LIVE PVE TOKEN. Alchemy persists attributes unencrypted (StateEncoding.ts tags Redacted
|
|
30
|
+
* values rather than encrypting them) into the `alchemy` Postgres, which pg-backup.sh dumps
|
|
31
|
+
* nightly to CT100, from where PBS backs it up. A minted token declared as a resource would
|
|
32
|
+
* outlive its own lease in at least four places, none of them OpenBao. This resource declares
|
|
33
|
+
* the DOOR; walking through it is a runtime call, not a plan.
|
|
34
|
+
*
|
|
35
|
+
* ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from
|
|
36
|
+
* it. Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
|
|
37
|
+
*/
|
|
38
|
+
import { Resource } from 'alchemy';
|
|
39
|
+
import { isResolved } from 'alchemy/Diff';
|
|
40
|
+
import * as Provider from 'alchemy/Provider';
|
|
41
|
+
import * as Effect from 'effect/Effect';
|
|
42
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
43
|
+
import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
|
|
44
|
+
import {
|
|
45
|
+
type BaoProxmoxRoleAttributes,
|
|
46
|
+
type BaoProxmoxRoleProps,
|
|
47
|
+
attributesOf,
|
|
48
|
+
matches,
|
|
49
|
+
rolePath,
|
|
50
|
+
writeBody,
|
|
51
|
+
} from './proxmox-role-form.ts';
|
|
52
|
+
|
|
53
|
+
export type { BaoProxmoxRoleAttributes, BaoProxmoxRoleProps };
|
|
54
|
+
|
|
55
|
+
export interface BaoProxmoxRole extends Resource<
|
|
56
|
+
'Bao.ProxmoxRole',
|
|
57
|
+
BaoProxmoxRoleProps,
|
|
58
|
+
BaoProxmoxRoleAttributes,
|
|
59
|
+
never,
|
|
60
|
+
HttpClient.HttpClient
|
|
61
|
+
> {}
|
|
62
|
+
|
|
63
|
+
export const BaoProxmoxRole = Resource<BaoProxmoxRole>('Bao.ProxmoxRole', {
|
|
64
|
+
defaultRemovalPolicy: 'retain',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* ⚠️ A missing MOUNT and a missing ROLE both read as undefined here, because both answer 404:
|
|
69
|
+
* an absent role is a read that found nothing, and a path under no mount is OpenBao's
|
|
70
|
+
* unsupported-path error, which is also a 404 (sdk/logical/response_util.go). They are not the
|
|
71
|
+
* same failure — one is a typo in `mount`, the other is a role that has yet to be created — so
|
|
72
|
+
* reconcile lets OpenBao's own `errors` through on the write rather than inventing a message.
|
|
73
|
+
* A bad mount says "no handler for route".
|
|
74
|
+
*/
|
|
75
|
+
const readRole = (props: BaoProxmoxRoleProps) =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const live = yield* baoRead(rolePath(props.mount, props.name));
|
|
78
|
+
if (live === undefined) return undefined;
|
|
79
|
+
return attributesOf(props, live);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const BaoProxmoxRoleProvider = () =>
|
|
83
|
+
Provider.effect(
|
|
84
|
+
BaoProxmoxRole,
|
|
85
|
+
Effect.succeed(
|
|
86
|
+
BaoProxmoxRole.Provider.of({
|
|
87
|
+
/**
|
|
88
|
+
* ⛔ `bao list <mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED today:
|
|
89
|
+
* proxmox-tb4/roles answers `provision, read` and proxmox-ops/roles answers `read` —
|
|
90
|
+
* four roles created by hand at bringup, none of them declared anywhere yet.
|
|
91
|
+
* Returning them would invite Alchemy to adopt, and then delete, roles it never
|
|
92
|
+
* created, and deleting a role is how every consumer of it stops being able to mint.
|
|
93
|
+
*/
|
|
94
|
+
list: () => Effect.succeed([]),
|
|
95
|
+
|
|
96
|
+
read: Effect.fn(function* ({ olds }) {
|
|
97
|
+
return yield* readRole(olds);
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A mint_user edited in the
|
|
102
|
+
* OpenBao UI re-scopes every future credential while the digest in Postgres still
|
|
103
|
+
* says everything is fine — exactly the drift the hand-written check-* gates exist to
|
|
104
|
+
* catch, and exactly what a provider that trusted its own state would walk past.
|
|
105
|
+
*/
|
|
106
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
107
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
108
|
+
const live = yield* readRole(news);
|
|
109
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
110
|
+
if (matches(live, news)) return { action: 'noop' } as const;
|
|
111
|
+
/**
|
|
112
|
+
* ⛔ NEVER `replace`, NOT EVEN FOR A mint_user CHANGE. A replace deletes the role and
|
|
113
|
+
* writes it back; between those steps `<mount>/creds/<name>` 404s, so anything
|
|
114
|
+
* minting in that window fails. The end state is byte-identical to the in-place
|
|
115
|
+
* write, because the plugin stores name, mint_user, ttl and max_ttl and nothing
|
|
116
|
+
* else — there is no immutable identity a recreate could reset. So a replace here
|
|
117
|
+
* buys an outage window in exchange for a louder-looking plan. The loudness a
|
|
118
|
+
* re-scope deserves comes from reconcile REFUSING it below, which blocks rather
|
|
119
|
+
* than destroys.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ CONSEQUENCE FOR THE READER OF A PLAN: a mint_user re-scope shows up here as a
|
|
122
|
+
* plain `update`, indistinguishable from a TTL tweak. The refusal lands at DEPLOY,
|
|
123
|
+
* not at plan. That is the safe ordering — nothing is written before the refusal —
|
|
124
|
+
* but do not read a green plan as evidence that a mint_user change is approved.
|
|
125
|
+
*/
|
|
126
|
+
return { action: 'update' } as const;
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
130
|
+
const live = yield* readRole(news);
|
|
131
|
+
/**
|
|
132
|
+
* ⛔ A mint_user CHANGE RE-SCOPES EVERY CREDENTIAL THE ROLE WILL EVER MINT, SILENTLY.
|
|
133
|
+
* The minted token inherits that PVE user's standing ACL and nothing else, so
|
|
134
|
+
* swapping `hf-read@pve` for `hf-provision@pve` turns a read-only consumer into a
|
|
135
|
+
* provisioning one with no other line of the declaration changing. A plan diff of
|
|
136
|
+
* one string is not proportionate warning for that, so an existing role refuses the
|
|
137
|
+
* move unless the declaration NAMES THE VALUE IT IS REPLACING. Creating a role never
|
|
138
|
+
* trips this — there is nothing to re-scope — and the permission expires by itself
|
|
139
|
+
* once the change lands, because live then equals the declaration.
|
|
140
|
+
*/
|
|
141
|
+
if (
|
|
142
|
+
live !== undefined &&
|
|
143
|
+
live.mintUser !== news.mintUser &&
|
|
144
|
+
news.allowMintUserChange !== live.mintUser
|
|
145
|
+
) {
|
|
146
|
+
return yield* Effect.die(
|
|
147
|
+
new Error(
|
|
148
|
+
`Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: live mint_user ` +
|
|
149
|
+
`${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` +
|
|
150
|
+
'inherits the ACL of that PVE user. To re-scope deliberately, declare ' +
|
|
151
|
+
`allowMintUserChange: '${live.mintUser}'.`,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (live === undefined || !matches(live, news)) {
|
|
156
|
+
const write = writeBody(news);
|
|
157
|
+
/**
|
|
158
|
+
* ⛔ AN UNPARSEABLE TTL IS A REFUSAL, NOT A ZERO. parseDuration accepts `3600s`,
|
|
159
|
+
* `1h`, `6h`, `1d` and bare `0`, and NOTHING ELSE — a bare `3600` or a compound
|
|
160
|
+
* `1h30m` returns undefined. Left alone that never equals the live value, so the
|
|
161
|
+
* plan would report an update forever and each deploy would write a zero TTL.
|
|
162
|
+
* Die with the offending props named instead.
|
|
163
|
+
*/
|
|
164
|
+
if (!write.ok) {
|
|
165
|
+
return yield* Effect.die(
|
|
166
|
+
new Error(
|
|
167
|
+
`Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: unparseable duration ` +
|
|
168
|
+
`${write.bad.join(', ')}. Use 30s / 5m / 1h / 1d, or 0.`,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
yield* baoWrite('PUT', rolePath(news.mount, news.name), write.body);
|
|
173
|
+
}
|
|
174
|
+
const after = yield* readRole(news);
|
|
175
|
+
if (after === undefined) {
|
|
176
|
+
return yield* Effect.die(
|
|
177
|
+
new Error(
|
|
178
|
+
`Bao.ProxmoxRole ${rolePath(news.mount, news.name)}: write returned no error ` +
|
|
179
|
+
'but the role is still absent.',
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* ⚠️ A GREEN RECONCILE DOES NOT MEAN THE ROLE CAN MINT. The engine never grants ACLs
|
|
185
|
+
* — it borrows the mint user's — so declaring `mint_user: hf-read@pve` neither
|
|
186
|
+
* creates that PVE user nor gives it a single permission. If the user does not
|
|
187
|
+
* exist, or exists with no ACL, the role writes and reads back perfectly and the
|
|
188
|
+
* failure surfaces at `creds/<name>` time, in whatever consumer is unlucky. Verify
|
|
189
|
+
* the PVE side out-of-band; this resource cannot and does not.
|
|
190
|
+
*/
|
|
191
|
+
return after;
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* ⛔ DELETING A ROLE DOES NOT REVOKE THE TOKENS IT ALREADY MINTED — it only stops the
|
|
196
|
+
* next mint. Outstanding leases run to their own expiry against the PVE user's ACL.
|
|
197
|
+
* So this is an availability change for every consumer of `creds/<name>` and NOT a
|
|
198
|
+
* containment action: to contain a leaked credential, revoke the lease (which deletes
|
|
199
|
+
* the PVE token) or delete the token in PVE. Deleting the role only hides the door it
|
|
200
|
+
* came through. REASONED from Vault lease semantics; not exercised here.
|
|
201
|
+
*
|
|
202
|
+
* ⚠️ IDEMPOTENT AS ALCHEMY REQUIRES — already gone is success. It used to get that by
|
|
203
|
+
* going through `baoReadText`, which discarded the exit code entirely, so a REFUSED
|
|
204
|
+
* delete was reported as a successful one and the resource left Alchemy's state while
|
|
205
|
+
* the role stayed live. `baoDelete` keeps the idempotence — a 404 is success — and
|
|
206
|
+
* fails on everything else, 403 and 503 included.
|
|
207
|
+
*/
|
|
208
|
+
delete: Effect.fn(function* ({ output }) {
|
|
209
|
+
yield* baoDelete(rolePath(output.mount, output.name));
|
|
210
|
+
return undefined;
|
|
211
|
+
}),
|
|
212
|
+
}),
|
|
213
|
+
),
|
|
214
|
+
);
|