@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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forgejo-provision service account: created once, by a person, with its password handed
|
|
3
|
+
* straight to the Forgejo engine's role and nowhere else.
|
|
4
|
+
*
|
|
5
|
+
* ★ TIM'S CALL, 2026-09-14: "Random password, piped", and membership of the HomeFlare Owners team
|
|
6
|
+
* (house/forgejo, Forgejo.TeamMember). Forgejo has no service-account type and no app.ini setting
|
|
7
|
+
* that creates users — the bot type is database-only (codeberg issue 8136) — so this is an
|
|
8
|
+
* ordinary user made by `forgejo admin user create`.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ AN AGENT DOES NOT RUN THIS. Creating an account is a human step (32-forgejo-provision.hcl).
|
|
11
|
+
* Everything around it is not: the engine, its config, the team membership and the mint proof.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ THE PASSWORD EXISTS IN THIS PROCESS AND IN ONE OPENBAO WRITE, AND NOWHERE ELSE.
|
|
14
|
+
* - Forgejo generates it (`--random-password`), so it is never in argv, where `ps` shows it.
|
|
15
|
+
* - Its stdout line is parsed and never echoed (forgejo-bootstrap-parse.ts).
|
|
16
|
+
* - It lands in `forgejo/roles/provision` as `mint_password`, which the engine never returns on a
|
|
17
|
+
* read (plugin-forgejo path_roles.go:63-71, :219-227).
|
|
18
|
+
* - It is never a file, an environment variable, a log line or an Alchemy prop.
|
|
19
|
+
*
|
|
20
|
+
* ★ EVERYTHING THAT CAN FAIL IS CHECKED BEFORE THE ACCOUNT EXISTS. An account whose password was
|
|
21
|
+
* lost between the CLI and OpenBao is the one failure a rerun cannot fix. So the preflight runs
|
|
22
|
+
* first, and if the write still fails after that, the account is deleted again so a rerun starts
|
|
23
|
+
* clean.
|
|
24
|
+
*/
|
|
25
|
+
import * as Effect from 'effect/Effect';
|
|
26
|
+
import { baoCall, baoRead, baoWrite } from './bao-http.ts';
|
|
27
|
+
import {
|
|
28
|
+
FORGEJO_CONFIG,
|
|
29
|
+
FORGEJO_WORK_PATH,
|
|
30
|
+
readCreateOutput,
|
|
31
|
+
runningForgejoBinary,
|
|
32
|
+
tailOf,
|
|
33
|
+
usernamesFrom,
|
|
34
|
+
} from './forgejo-bootstrap-parse.ts';
|
|
35
|
+
|
|
36
|
+
export const PROVISION_USER = 'forgejo-provision';
|
|
37
|
+
/** `NO_REPLY_ADDRESS` in hosts/macmini/forgejo/app.ini:117 — a service account's mail goes nowhere. */
|
|
38
|
+
const PROVISION_EMAIL = `${PROVISION_USER}@noreply.lab.example.com`;
|
|
39
|
+
export const ROLE_PATH = 'forgejo/roles/provision';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ★ THE SCOPES ARE WHAT house/forgejo WRITES TODAY. Org labels, teams and org secrets need
|
|
43
|
+
* `write:organization`; repositories, webhooks and branch protection need `write:repository`.
|
|
44
|
+
* Membership decides what the tokens can reach, and the scopes cap it.
|
|
45
|
+
* ⚠️ FORGEJO TOKENS NEVER EXPIRE ON THE SERVER. The TTL is OpenBao's lease, and revoking the lease is
|
|
46
|
+
* the only kill switch (path_roles.go:206-207).
|
|
47
|
+
*/
|
|
48
|
+
const ROLE = { max_ttl: '1h', scopes: 'write:organization,write:repository', ttl: '15m' } as const;
|
|
49
|
+
|
|
50
|
+
const refuse = (message: string) => Effect.fail(new Error(message));
|
|
51
|
+
const say = (line: string) => Effect.sync(() => process.stdout.write(`${line}\n`));
|
|
52
|
+
|
|
53
|
+
/** Run a command and collect everything it printed. ⛔ argv must never carry a secret. */
|
|
54
|
+
const run = (argv: readonly string[]) =>
|
|
55
|
+
Effect.tryPromise({
|
|
56
|
+
catch: (cause) => new Error(`${argv.slice(0, 4).join(' ')} could not run: ${String(cause)}`),
|
|
57
|
+
try: async () => {
|
|
58
|
+
const child = Bun.spawn([...argv], { stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' });
|
|
59
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
60
|
+
new Response(child.stdout).text(),
|
|
61
|
+
new Response(child.stderr).text(),
|
|
62
|
+
child.exited,
|
|
63
|
+
]);
|
|
64
|
+
return { code, stderr, stdout };
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const bootstrapProvisionAccount = Effect.gen(function* () {
|
|
69
|
+
// ── 1. OpenBao: the engine is configured, the role is absent, and this token may create it.
|
|
70
|
+
const config = yield* baoRead('forgejo/config');
|
|
71
|
+
if (typeof config?.['api_base_url'] !== 'string') {
|
|
72
|
+
return yield* refuse('forgejo/config has no api_base_url. Configure the engine first.');
|
|
73
|
+
}
|
|
74
|
+
const existing = yield* baoRead(ROLE_PATH);
|
|
75
|
+
if (existing !== undefined) {
|
|
76
|
+
return yield* refuse(
|
|
77
|
+
`${ROLE_PATH} already exists (mint_user ${String(existing['mint_user'])}). Nothing to do.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const capabilities = yield* baoCall('write', 'POST', 'sys/capabilities-self', {
|
|
81
|
+
paths: [ROLE_PATH],
|
|
82
|
+
});
|
|
83
|
+
const granted = capabilities?.[ROLE_PATH];
|
|
84
|
+
if (!Array.isArray(granted) || !granted.some((c) => c === 'create' || c === 'root')) {
|
|
85
|
+
return yield* refuse(
|
|
86
|
+
`this BAO_TOKEN cannot create ${ROLE_PATH} (capabilities: ${JSON.stringify(granted)}).`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 2. Forgejo: the running binary, and no account by that name yet.
|
|
91
|
+
const ps = yield* run(['ps', '-axo', 'command']);
|
|
92
|
+
const binary = runningForgejoBinary(ps.stdout);
|
|
93
|
+
if (binary === undefined) {
|
|
94
|
+
return yield* refuse(
|
|
95
|
+
'no running `forgejo web` found. Run this on the mini, where Forgejo runs.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const cli = (subcommand: string, ...args: string[]) =>
|
|
99
|
+
run([
|
|
100
|
+
binary,
|
|
101
|
+
'admin',
|
|
102
|
+
'user',
|
|
103
|
+
subcommand,
|
|
104
|
+
'--work-path',
|
|
105
|
+
FORGEJO_WORK_PATH,
|
|
106
|
+
'--config',
|
|
107
|
+
FORGEJO_CONFIG,
|
|
108
|
+
...args,
|
|
109
|
+
]);
|
|
110
|
+
const listUsers = Effect.gen(function* () {
|
|
111
|
+
const listed = yield* cli('list');
|
|
112
|
+
if (listed.code !== 0) {
|
|
113
|
+
return yield* refuse(
|
|
114
|
+
`forgejo admin user list exited ${listed.code}: ${tailOf(listed.stderr)}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return yield* Effect.try({
|
|
118
|
+
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
|
|
119
|
+
try: () => usernamesFrom(listed.stdout),
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
if ((yield* listUsers).includes(PROVISION_USER)) {
|
|
123
|
+
return yield* refuse(
|
|
124
|
+
`Forgejo already has ${PROVISION_USER} but OpenBao has no role for it, so its password is ` +
|
|
125
|
+
'unknown. Delete that account deliberately, then rerun.',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** ⛔ Only an account THIS run just created is ever removed, and only when its password is lost. */
|
|
130
|
+
const rollback = (reason: string) =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
if (!(yield* listUsers).includes(PROVISION_USER)) return;
|
|
133
|
+
const removed = yield* cli('delete', '--username', PROVISION_USER);
|
|
134
|
+
yield* say(
|
|
135
|
+
removed.code === 0
|
|
136
|
+
? ` rolled back: deleted ${PROVISION_USER}, because ${reason}`
|
|
137
|
+
: ` ⛔ ROLLBACK FAILED (${tailOf(removed.stderr)}). ${PROVISION_USER} exists with a ` +
|
|
138
|
+
'password nobody holds. Delete it by hand before rerunning.',
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── 3. The one step that makes a secret.
|
|
143
|
+
const created = yield* cli(
|
|
144
|
+
'create',
|
|
145
|
+
'--username',
|
|
146
|
+
PROVISION_USER,
|
|
147
|
+
'--email',
|
|
148
|
+
PROVISION_EMAIL,
|
|
149
|
+
'--fullname',
|
|
150
|
+
'Forgejo provision (OpenBao engine)',
|
|
151
|
+
'--random-password',
|
|
152
|
+
'--random-password-length',
|
|
153
|
+
'40',
|
|
154
|
+
'--must-change-password=false',
|
|
155
|
+
);
|
|
156
|
+
const output = readCreateOutput(created.stdout, PROVISION_USER);
|
|
157
|
+
for (const line of output.rest) yield* say(` forgejo: ${line}`);
|
|
158
|
+
const password = output.password;
|
|
159
|
+
if (created.code !== 0 || !output.created || password === undefined) {
|
|
160
|
+
const reason = `create exited ${created.code} (${tailOf(created.stderr)})`;
|
|
161
|
+
yield* rollback(reason);
|
|
162
|
+
return yield* refuse(`${PROVISION_USER} was not created cleanly: ${reason}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── 4. Straight into the engine, then read back rather than trusting the write.
|
|
166
|
+
yield* baoWrite('PUT', ROLE_PATH, {
|
|
167
|
+
...ROLE,
|
|
168
|
+
mint_password: password,
|
|
169
|
+
mint_user: PROVISION_USER,
|
|
170
|
+
}).pipe(Effect.tapError((error) => rollback(`the OpenBao write failed: ${error.message}`)));
|
|
171
|
+
const role = yield* baoRead(ROLE_PATH);
|
|
172
|
+
if (role?.['mint_user'] !== PROVISION_USER || role['mint_password_set'] !== true) {
|
|
173
|
+
return yield* refuse(
|
|
174
|
+
`${ROLE_PATH} wrote cleanly but reads back ${JSON.stringify(role)}. Inspect it before retrying.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
yield* say(
|
|
178
|
+
`ok: ${PROVISION_USER} created, and ${ROLE_PATH} holds its password (write-only). ` +
|
|
179
|
+
`Scopes ${ROLE.scopes}, lease ${ROLE.ttl}, max ${ROLE.max_ttl}.`,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenBao providers for Alchemy.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY.
|
|
5
|
+
* These are the symbols a real stack consumes; the rest of the files are internals a
|
|
6
|
+
* provider needs but a consumer should not depend on. An `export *` here would publish
|
|
7
|
+
* every helper as API and make the next refactor a breaking change.
|
|
8
|
+
* ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
|
|
9
|
+
* deliberate, visible act rather than an accident of barrelling.
|
|
10
|
+
*/
|
|
11
|
+
export { BaoAuthRoleProvider } from './auth-role.ts';
|
|
12
|
+
export { BaoCloudflareRoleProvider } from './cloudflare-role.ts';
|
|
13
|
+
export { BaoMountProvider } from './mount.ts';
|
|
14
|
+
export { BaoPkiRoleProvider } from './pki-role.ts';
|
|
15
|
+
export { BaoPolicy, BaoPolicyProvider } from './policy.ts';
|
|
16
|
+
export { BaoProxmoxRole, BaoProxmoxRoleProvider } from './proxmox-role.ts';
|
|
17
|
+
export { BaoSshRoleProvider } from './ssh-role.ts';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { sha256 } from './digest.ts';
|
|
2
|
+
|
|
3
|
+
/** Mount path without a trailing slash — props and `sys/mounts/<path>` use this shape. */
|
|
4
|
+
export const mountPath = (path: string) => path.replace(/\/+$/, '');
|
|
5
|
+
|
|
6
|
+
const DURATION = /^(\d+)([smhd])$/;
|
|
7
|
+
|
|
8
|
+
/** Parse a Vault duration string (`15m`, `768h`, `0`) to seconds. */
|
|
9
|
+
export const parseDuration = (text: string): number | undefined => {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (trimmed === '0') return 0;
|
|
12
|
+
const match = DURATION.exec(trimmed);
|
|
13
|
+
if (match === null) return undefined;
|
|
14
|
+
const amount = Number(match[1]);
|
|
15
|
+
switch (match[2]) {
|
|
16
|
+
case 's':
|
|
17
|
+
return amount;
|
|
18
|
+
case 'm':
|
|
19
|
+
return amount * 60;
|
|
20
|
+
case 'h':
|
|
21
|
+
return amount * 3600;
|
|
22
|
+
case 'd':
|
|
23
|
+
return amount * 86400;
|
|
24
|
+
default:
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Seconds from live JSON (`2764800`) or a duration prop (`768h`). */
|
|
30
|
+
export const ttlSeconds = (value: unknown): number | undefined => {
|
|
31
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
32
|
+
if (typeof value === 'string') return parseDuration(value);
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface BaoMountProps {
|
|
37
|
+
/** Mount path without trailing slash, as `sys/mounts/<path>` takes it. */
|
|
38
|
+
path: string;
|
|
39
|
+
/** Secrets engine type, e.g. `kv`, `pki`, `openbao-plugin-secrets-cloudflare`. */
|
|
40
|
+
type: string;
|
|
41
|
+
/** Human description — tuned via `sys/mounts/<path>/tune`, as apply-roles.py describe_mounts does. */
|
|
42
|
+
description?: string;
|
|
43
|
+
/** Default lease TTL, e.g. `768h`. */
|
|
44
|
+
defaultLeaseTtl?: string;
|
|
45
|
+
/** Max lease TTL, e.g. `8760h`. */
|
|
46
|
+
maxLeaseTtl?: string;
|
|
47
|
+
/** KV only — sent as `options.version` on enable, as `-version=2` was. Immutable after create. */
|
|
48
|
+
version?: 1 | 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BaoMountAttributes {
|
|
52
|
+
path: string;
|
|
53
|
+
type: string;
|
|
54
|
+
description: string;
|
|
55
|
+
defaultLeaseTtl: string;
|
|
56
|
+
maxLeaseTtl: string;
|
|
57
|
+
/** SHA-256 of the managed fields — safe to persist; see policy.ts. */
|
|
58
|
+
digest: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const configOf = (live: Record<string, unknown>) =>
|
|
62
|
+
typeof live['config'] === 'object' && live['config'] !== null
|
|
63
|
+
? (live['config'] as Record<string, unknown>)
|
|
64
|
+
: {};
|
|
65
|
+
|
|
66
|
+
const text = (value: unknown) => (typeof value === 'string' ? value : '');
|
|
67
|
+
|
|
68
|
+
const ttlProp = (seconds: number | undefined, fallback = '') => {
|
|
69
|
+
if (seconds === undefined) return fallback;
|
|
70
|
+
if (seconds % 86400 === 0) return `${String(seconds / 86400)}d`;
|
|
71
|
+
if (seconds % 3600 === 0) return `${String(seconds / 3600)}h`;
|
|
72
|
+
if (seconds % 60 === 0) return `${String(seconds / 60)}m`;
|
|
73
|
+
return `${String(seconds)}s`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const attributesOf = (
|
|
77
|
+
props: BaoMountProps,
|
|
78
|
+
live: Record<string, unknown>,
|
|
79
|
+
): BaoMountAttributes => {
|
|
80
|
+
const config = configOf(live);
|
|
81
|
+
const defaultLeaseTtl = ttlProp(ttlSeconds(config['default_lease_ttl']));
|
|
82
|
+
const maxLeaseTtl = ttlProp(ttlSeconds(config['max_lease_ttl']));
|
|
83
|
+
const attrs = {
|
|
84
|
+
path: mountPath(props.path),
|
|
85
|
+
type: text(live['type']) || props.type,
|
|
86
|
+
description: text(live['description']),
|
|
87
|
+
defaultLeaseTtl,
|
|
88
|
+
maxLeaseTtl,
|
|
89
|
+
};
|
|
90
|
+
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** `sys/mounts/<path>` — read, enable and disable all address the mount here. */
|
|
94
|
+
export const readPath = (path: string) => `sys/mounts/${mountPath(path)}`;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* ★ NO TRAILING SLASH NEEDED ON THE WIRE. `bao secrets tune` appended one itself; the server's
|
|
98
|
+
* route `mounts/(?P<path>.+?)/tune$` captures the bare path and `sanitizePath` appends the slash
|
|
99
|
+
* OpenBao stores in sys/mounts (openbao v2.6.2 vault/logical_system_paths.go:4248,
|
|
100
|
+
* vault/logical_system.go:4825-4833).
|
|
101
|
+
*/
|
|
102
|
+
export const tunePath = (path: string) => `${readPath(path)}/tune`;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The enable body. `-version=2` on the CLI became `options.version = "2"` (openbao v2.6.2
|
|
106
|
+
* command/secrets_enable.go:278 into api MountInput.Options), so that is what is sent.
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ DESCRIPTION AND TTLs ARE NOT SENT HERE. reconcile tunes them straight after the enable,
|
|
109
|
+
* exactly as the CLI path did, so a create and an update converge through the same call.
|
|
110
|
+
*/
|
|
111
|
+
export const enableBody = (props: BaoMountProps): Record<string, unknown> =>
|
|
112
|
+
props.version === 2 ? { options: { version: '2' }, type: props.type } : { type: props.type };
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The tune body — only the fields the declaration names.
|
|
116
|
+
*
|
|
117
|
+
* ★ AN OMITTED FIELD IS LEFT ALONE. The server reads an absent TTL as "" and keeps the mount's
|
|
118
|
+
* current value for "" (vault/logical_system.go:1614-1630), and applies `description` only when
|
|
119
|
+
* present (:1725). The CLI sent unset TTL flags as "" and set description only when given
|
|
120
|
+
* (command/secrets_tune.go Visit), so omitting them is the same request.
|
|
121
|
+
*/
|
|
122
|
+
export const tuneBody = (props: BaoMountProps): Record<string, string> => {
|
|
123
|
+
const body: Record<string, string> = {};
|
|
124
|
+
if (props.description !== undefined) body['description'] = props.description;
|
|
125
|
+
if (props.defaultLeaseTtl !== undefined) body['default_lease_ttl'] = props.defaultLeaseTtl;
|
|
126
|
+
if (props.maxLeaseTtl !== undefined) body['max_lease_ttl'] = props.maxLeaseTtl;
|
|
127
|
+
return body;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const sameTtl = (want: string | undefined, have: string) => {
|
|
131
|
+
if (want === undefined) return true;
|
|
132
|
+
const wantSeconds = ttlSeconds(want);
|
|
133
|
+
const haveSeconds = ttlSeconds(have);
|
|
134
|
+
return wantSeconds !== undefined && wantSeconds === haveSeconds;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** True when live already matches the declaration on every managed field. */
|
|
138
|
+
export const matches = (attributes: BaoMountAttributes, props: BaoMountProps) =>
|
|
139
|
+
attributes.type === props.type &&
|
|
140
|
+
(props.description === undefined || attributes.description === props.description) &&
|
|
141
|
+
sameTtl(props.defaultLeaseTtl, attributes.defaultLeaseTtl) &&
|
|
142
|
+
sameTtl(props.maxLeaseTtl, attributes.maxLeaseTtl);
|
|
143
|
+
|
|
144
|
+
export const wantsTune = (props: BaoMountProps) =>
|
|
145
|
+
props.description !== undefined ||
|
|
146
|
+
props.defaultLeaseTtl !== undefined ||
|
|
147
|
+
props.maxLeaseTtl !== undefined;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bao.Mount's calls against a fake. The part worth pinning is the MISSING mount: OpenBao answers a
|
|
3
|
+
* read of one with 400, not 404 (vault/logical_system.go:1171-1178), and only the mount table may
|
|
4
|
+
* turn that into "absent".
|
|
5
|
+
*/
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import { BaoError } from './bao-status.ts';
|
|
9
|
+
import { type Reply, type Seen, run, runFailure, withFake } from './fake-bao.ts';
|
|
10
|
+
import { disableMount, enableMount, readMountData, tuneMount } from './mount-wire.ts';
|
|
11
|
+
|
|
12
|
+
const NO_MOUNT = { json: { errors: ['No secret engine mount at zz-new/'] }, status: 400 };
|
|
13
|
+
const table = (...paths: string[]) => ({
|
|
14
|
+
json: { data: Object.fromEntries(paths.map((path) => [path, { type: 'kv' }])) },
|
|
15
|
+
status: 200,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Answer the mount read with `mount`, and the mount-table listing with `listing`. */
|
|
19
|
+
const routes = (mount: Reply, listing: Reply) => (seen: Seen) =>
|
|
20
|
+
seen.path === '/v1/sys/mounts' ? listing : mount;
|
|
21
|
+
|
|
22
|
+
const statusOf = async (error: Promise<unknown>) => {
|
|
23
|
+
const settled = await error;
|
|
24
|
+
assert.ok(settled instanceof BaoError);
|
|
25
|
+
return settled.status;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('readMountData', () => {
|
|
29
|
+
it('reads a mount that exists, without consulting the table', async () => {
|
|
30
|
+
const reply = { json: { data: { config: {}, type: 'pki' } }, status: 200 };
|
|
31
|
+
await withFake(routes(reply, table()), async (bao) => {
|
|
32
|
+
const data = await run({ BAO_ADDR: bao.address }, readMountData('pki'));
|
|
33
|
+
assert.equal(data?.['type'], 'pki');
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
bao.seen.map((seen) => seen.path),
|
|
36
|
+
['/v1/sys/mounts/pki'],
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("reads a missing mount as absent once the table confirms OpenBao's 400", async () => {
|
|
42
|
+
await withFake(routes(NO_MOUNT, table('kv/', 'pki/')), async (bao) => {
|
|
43
|
+
assert.equal(await run({ BAO_ADDR: bao.address }, readMountData('zz-new/')), undefined);
|
|
44
|
+
assert.deepEqual(
|
|
45
|
+
bao.seen.map((seen) => seen.path),
|
|
46
|
+
['/v1/sys/mounts/zz-new', '/v1/sys/mounts'],
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps the 400 when the table lists the mount after all', async () => {
|
|
52
|
+
await withFake(routes(NO_MOUNT, table('zz-new/')), async (bao) => {
|
|
53
|
+
const error = runFailure({ BAO_ADDR: bao.address }, readMountData('zz-new'));
|
|
54
|
+
assert.equal(await statusOf(error), 400);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps the 400 when the table read is refused', async () => {
|
|
59
|
+
const refused = { json: { errors: ['permission denied'] }, status: 403 };
|
|
60
|
+
await withFake(routes(NO_MOUNT, refused), async (bao) => {
|
|
61
|
+
const error = runFailure({ BAO_ADDR: bao.address }, readMountData('zz-new'));
|
|
62
|
+
assert.equal(await statusOf(error), 400);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('fails a refused mount read (403) without consulting the table', async () => {
|
|
67
|
+
const refused = { json: { errors: ['permission denied'] }, status: 403 };
|
|
68
|
+
await withFake(routes(refused, table()), async (bao) => {
|
|
69
|
+
const error = runFailure({ BAO_ADDR: bao.address }, readMountData('kv'));
|
|
70
|
+
assert.equal(await statusOf(error), 403);
|
|
71
|
+
assert.equal(bao.seen.length, 1);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('enable, tune, disable', () => {
|
|
77
|
+
it('sends the endpoints and bodies the CLI client sent', async () => {
|
|
78
|
+
await withFake(
|
|
79
|
+
() => ({ status: 204 }),
|
|
80
|
+
async (bao) => {
|
|
81
|
+
const env = { BAO_ADDR: bao.address };
|
|
82
|
+
const props = { defaultLeaseTtl: '768h', path: 'kv/', type: 'kv', version: 2 as const };
|
|
83
|
+
await run(env, enableMount(props));
|
|
84
|
+
await run(env, tuneMount(props));
|
|
85
|
+
await run(env, disableMount('kv'));
|
|
86
|
+
const calls = bao.seen.map((seen) => [seen.method, seen.path, seen.body]);
|
|
87
|
+
assert.deepEqual(calls, [
|
|
88
|
+
['POST', '/v1/sys/mounts/kv', JSON.stringify({ options: { version: '2' }, type: 'kv' })],
|
|
89
|
+
['POST', '/v1/sys/mounts/kv/tune', JSON.stringify({ default_lease_ttl: '768h' })],
|
|
90
|
+
['DELETE', '/v1/sys/mounts/kv', ''],
|
|
91
|
+
]);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('fails a refused enable with OpenBao errors', async () => {
|
|
97
|
+
await withFake(
|
|
98
|
+
() => ({ json: { errors: ['path is already in use at kv/'] }, status: 400 }),
|
|
99
|
+
async (bao) => {
|
|
100
|
+
const error = runFailure(
|
|
101
|
+
{ BAO_ADDR: bao.address },
|
|
102
|
+
enableMount({ path: 'kv', type: 'kv' }),
|
|
103
|
+
);
|
|
104
|
+
assert.equal(await statusOf(error), 400);
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenBao calls Bao.Mount makes — read one mount, enable, tune, disable. Split from mount.ts so
|
|
3
|
+
* the classification of a MISSING mount can be tested against a fake server.
|
|
4
|
+
*
|
|
5
|
+
* ★ ENDPOINTS, READ FROM openbao v2.6.2 api/sys_mounts.go — the client the CLI used:
|
|
6
|
+
* read GET sys/mounts/<path> (MountInfo, :254) — what `bao read sys/mounts/<path>` hit
|
|
7
|
+
* enable POST sys/mounts/<path> (Mount, :57) — `bao secrets enable`
|
|
8
|
+
* tune POST sys/mounts/<path>/tune (TuneMount, :201) — `bao secrets tune`
|
|
9
|
+
* disable DELETE sys/mounts/<path> (Unmount, :79) — `bao secrets disable`
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from 'effect/Effect';
|
|
12
|
+
import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
|
|
13
|
+
import { BaoError } from './bao-status.ts';
|
|
14
|
+
import {
|
|
15
|
+
type BaoMountProps,
|
|
16
|
+
enableBody,
|
|
17
|
+
mountPath,
|
|
18
|
+
readPath,
|
|
19
|
+
tuneBody,
|
|
20
|
+
tunePath,
|
|
21
|
+
} from './mount-form.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Confirm a mount is absent from the mount table, or give back the error that asked.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ A MISSING MOUNT IS NOT A 404, IT IS A 400 — AND THAT MADE A NEW MOUNT UNCREATABLE, SILENTLY,
|
|
27
|
+
* THE SAME WAY A NEW POLICY WAS (b3c18a33e). `handleReadMount` answers a path with no mount with a
|
|
28
|
+
* bare error response, "No secret engine mount at <path>" (vault/logical_system.go:1171-1178), and
|
|
29
|
+
* RespondErrorCommon turns an error response with no coded error into 400
|
|
30
|
+
* (sdk/logical/response_util.go). The CLI therefore exited 2 with that text, which was not
|
|
31
|
+
* `No value found at`, so every read of an undeclared-yet mount FAILED and reconcile never
|
|
32
|
+
* reached `secrets enable`. All four declared mounts already exist, which is the only reason
|
|
33
|
+
* nothing broke. REASONED FROM SOURCE, NOT MEASURED — no read of a missing mount was run.
|
|
34
|
+
* ★ SO A 400 IS SETTLED BY A SECOND, STATUS-CLASSIFIED QUESTION rather than by matching that text:
|
|
35
|
+
* `GET sys/mounts` lists the namespace's mount table, keyed `<path>/`. Only a 200 table WITHOUT
|
|
36
|
+
* the key is absence. A listed mount, a refused listing, or a 404 listing leaves the original 400
|
|
37
|
+
* standing — "I could not tell" is an error, never a create.
|
|
38
|
+
* ⚠️ THE LISTING NEEDS `read` ON `sys/mounts`. The admin lane has it
|
|
39
|
+
* (policies/homeflare-admin/live-captured-2026-09-02.hcl:20). A narrower token still fails, which
|
|
40
|
+
* is what it did before.
|
|
41
|
+
*/
|
|
42
|
+
const absentFromTable = (path: string, asked: BaoError) =>
|
|
43
|
+
baoRead('sys/mounts').pipe(
|
|
44
|
+
// ⚠️ A listing that fails keeps the 400, and says why it could not settle it.
|
|
45
|
+
Effect.mapError(
|
|
46
|
+
(listing) =>
|
|
47
|
+
new BaoError(asked.status, asked.method, asked.path, [
|
|
48
|
+
...asked.errors,
|
|
49
|
+
`sys/mounts could not confirm the mount is absent: ${listing.message}`,
|
|
50
|
+
]),
|
|
51
|
+
),
|
|
52
|
+
Effect.flatMap((table) =>
|
|
53
|
+
table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`)
|
|
54
|
+
? Effect.succeed(undefined)
|
|
55
|
+
: Effect.fail(asked),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/** One mount's config (`type`, `description`, `config.*_lease_ttl`), or undefined when absent. */
|
|
60
|
+
export const readMountData = (path: string) =>
|
|
61
|
+
baoRead(readPath(path)).pipe(
|
|
62
|
+
Effect.catchIf(
|
|
63
|
+
(error) => error.status === 400,
|
|
64
|
+
(error) => absentFromTable(path, error),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
export const enableMount = (props: BaoMountProps) =>
|
|
69
|
+
baoWrite('POST', readPath(props.path), enableBody(props));
|
|
70
|
+
|
|
71
|
+
export const tuneMount = (props: BaoMountProps) =>
|
|
72
|
+
baoWrite('POST', tunePath(props.path), tuneBody(props));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ⚠️ THERE IS NO FORCE. The old delete ran `bao secrets disable -force`, and v2.6.2's
|
|
76
|
+
* command/secrets_disable.go defines no `-force` flag, so that argv would have been refused at
|
|
77
|
+
* flag parsing. `Unmount` is a plain DELETE (api/sys_mounts.go:79); nothing else was ever sent.
|
|
78
|
+
*/
|
|
79
|
+
export const disableMount = (path: string) => baoDelete(readPath(path));
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenBao secrets engine mount — path, type, description and lease TTLs.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see policy.ts for why. This declares mount
|
|
5
|
+
* SHAPE only: names, types, TTL strings and a digest. KV values and CA material live
|
|
6
|
+
* in OpenBao, not Alchemy state.
|
|
7
|
+
*
|
|
8
|
+
* ★ `defaultRemovalPolicy: 'retain'` — disabling a mount destroys every secret under it.
|
|
9
|
+
* Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
|
|
10
|
+
*/
|
|
11
|
+
import { Resource } from 'alchemy';
|
|
12
|
+
import { isResolved } from 'alchemy/Diff';
|
|
13
|
+
import * as Provider from 'alchemy/Provider';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
16
|
+
import {
|
|
17
|
+
type BaoMountAttributes,
|
|
18
|
+
type BaoMountProps,
|
|
19
|
+
attributesOf,
|
|
20
|
+
matches,
|
|
21
|
+
mountPath,
|
|
22
|
+
wantsTune,
|
|
23
|
+
} from './mount-form.ts';
|
|
24
|
+
import { disableMount, enableMount, readMountData, tuneMount } from './mount-wire.ts';
|
|
25
|
+
|
|
26
|
+
export type { BaoMountAttributes, BaoMountProps };
|
|
27
|
+
|
|
28
|
+
export interface BaoMount extends Resource<
|
|
29
|
+
'Bao.Mount',
|
|
30
|
+
BaoMountProps,
|
|
31
|
+
BaoMountAttributes,
|
|
32
|
+
never,
|
|
33
|
+
HttpClient.HttpClient
|
|
34
|
+
> {}
|
|
35
|
+
|
|
36
|
+
export const BaoMount = Resource<BaoMount>('Bao.Mount', {
|
|
37
|
+
defaultRemovalPolicy: 'retain',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const readMount = (props: BaoMountProps) =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const live = yield* readMountData(props.path);
|
|
43
|
+
if (live === undefined) return undefined;
|
|
44
|
+
return attributesOf(props, live);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const BaoMountProvider = () =>
|
|
48
|
+
Provider.effect(
|
|
49
|
+
BaoMount,
|
|
50
|
+
Effect.succeed(
|
|
51
|
+
BaoMount.Provider.of({
|
|
52
|
+
/**
|
|
53
|
+
* ⛔ `bao secrets list` IS NOT A LIST OF THINGS THIS OWNS. Every mount in the
|
|
54
|
+
* namespace answers, including ones created by hand at bringup. Returning them
|
|
55
|
+
* would invite Alchemy to adopt — and then disable — mounts it never created.
|
|
56
|
+
* ⚠️ mount-wire.ts does read that listing, for one question only: whether a mount a 400
|
|
57
|
+
* named is really absent. It never becomes a list of resources.
|
|
58
|
+
*/
|
|
59
|
+
list: () => Effect.succeed([]),
|
|
60
|
+
|
|
61
|
+
read: Effect.fn(function* ({ olds }) {
|
|
62
|
+
return yield* readMount(olds);
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
66
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
67
|
+
const live = yield* readMount(news);
|
|
68
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
69
|
+
/**
|
|
70
|
+
* ⛔ A CHANGED `type` IS A REPLACE, AND THIS FAMILY'S DELETE IS A MOUNT DISABLE — the
|
|
71
|
+
* destruction of every secret under the mount. `kv/` is the estate's entire
|
|
72
|
+
* secret shelf, so the question "does `retain` guard the delete half of a REPLACE, or
|
|
73
|
+
* only an orphan?" is the difference between a typo and a catastrophe.
|
|
74
|
+
* ★ IT GUARDS BOTH — VERIFIED IN ALCHEMY'S OWN SOURCE, not assumed. Apply.ts:2164-2173:
|
|
75
|
+
* "Honor `retain` for the old generation of a replacement, mirroring the orphan-delete
|
|
76
|
+
* path above", with `retainOldGeneration = node.removalPolicy === 'retain'`, and when
|
|
77
|
+
* it is true the engine logs "Retaining replaced resource (removal policy: retain)"
|
|
78
|
+
* and skips the delete entirely. Every mount this package declares carries the retain
|
|
79
|
+
* default (see the ★ on the resource below), so a wrong `type` string cannot disable a
|
|
80
|
+
* live mount. What it CAN do is try to enable a second mount at an occupied path,
|
|
81
|
+
* which OpenBao refuses loudly — a failed deploy, not a lost shelf.
|
|
82
|
+
* ⚠️ THAT SAFETY IS THE DECORATION'S, NOT THIS LINE'S. A caller who opts into
|
|
83
|
+
* `.pipe(RemovalPolicy.destroy())` gets the destructive replace, which is the correct
|
|
84
|
+
* meaning of opting in and is worth knowing before you type it on a mount.
|
|
85
|
+
*/
|
|
86
|
+
if (live.type !== news.type) return { action: 'replace' } as const;
|
|
87
|
+
if (matches(live, news)) return { action: 'noop' } as const;
|
|
88
|
+
return { action: 'update' } as const;
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* ★ A REFUSED ENABLE OR TUNE FAILS THE EFFECT WITH OpenBao's OWN `errors`, naming the
|
|
93
|
+
* method and path — the job the per-command exit-code checks used to do by hand.
|
|
94
|
+
*/
|
|
95
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
96
|
+
let live = yield* readMount(news);
|
|
97
|
+
if (live === undefined) {
|
|
98
|
+
yield* enableMount(news);
|
|
99
|
+
if (wantsTune(news)) yield* tuneMount(news);
|
|
100
|
+
} else if (!matches(live, news)) {
|
|
101
|
+
if (live.type !== news.type) {
|
|
102
|
+
return yield* Effect.die(
|
|
103
|
+
new Error(
|
|
104
|
+
`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` +
|
|
105
|
+
'Secrets engine type is immutable — replace manually.',
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
yield* tuneMount(news);
|
|
110
|
+
}
|
|
111
|
+
live = yield* readMount(news);
|
|
112
|
+
if (live === undefined) {
|
|
113
|
+
return yield* Effect.die(
|
|
114
|
+
new Error(
|
|
115
|
+
`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return live;
|
|
120
|
+
}),
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ⛔ DISABLING A MOUNT DESTROYS EVERY SECRET UNDER IT. Idempotent as Alchemy
|
|
124
|
+
* requires — already gone is success — but retain is the default for a reason.
|
|
125
|
+
*/
|
|
126
|
+
delete: Effect.fn(function* ({ output }) {
|
|
127
|
+
yield* disableMount(output.path);
|
|
128
|
+
return undefined;
|
|
129
|
+
}),
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
);
|