@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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire-and-form half of Bao.SshRole: normalisation, argv, and the equality that decides
|
|
3
|
+
* whether a plan says `noop`. Split from ssh-role.ts for the 250-line cap.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ EVERY FIELD TYPE BELOW WAS MEASURED, not assumed — OpenBao v2.6.2, read from the live
|
|
6
|
+
* engine's own `sys/internal/specs/openapi`, schema `SshWriteRoleRequest`:
|
|
7
|
+
* `allowed_users`, `allowed_extensions`, `allowed_critical_options`, `allowed_domains`
|
|
8
|
+
* → `string`. ONE COMMA-SEPARATED STRING. Not a list, however much it reads like one.
|
|
9
|
+
* `default_extensions`, `default_critical_options`, `allowed_user_key_lengths`
|
|
10
|
+
* → `object`, `format: map`.
|
|
11
|
+
* `ttl`, `max_ttl`, `not_before_duration` → `integer`, SECONDS.
|
|
12
|
+
* A role read echoes exactly those types back, which is why csvList and extMap exist.
|
|
13
|
+
*/
|
|
14
|
+
import { sha256 } from './digest.ts';
|
|
15
|
+
import { parseDuration, ttlSeconds } from './mount-form.ts';
|
|
16
|
+
|
|
17
|
+
export interface BaoSshRoleProps {
|
|
18
|
+
/** Role name, as `<mount>/roles/{name}` takes it. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Secrets engine mount, no trailing slash. Defaults to `ssh`; `ssh-host` also exists. */
|
|
21
|
+
mount?: string;
|
|
22
|
+
/** Principals a certificate may carry. `['*']` is OpenBao's wildcard. */
|
|
23
|
+
allowedUsers: readonly string[];
|
|
24
|
+
/** Principal used when a sign request does not name one. */
|
|
25
|
+
defaultUser?: string;
|
|
26
|
+
/** Sign user certificates. Defaults to false — OpenBao's own default. */
|
|
27
|
+
allowUserCertificates?: boolean;
|
|
28
|
+
/** Sign host certificates. Defaults to false — OpenBao's own default. */
|
|
29
|
+
allowHostCertificates?: boolean;
|
|
30
|
+
/** Host-cert roles only: the domains a host certificate may claim. */
|
|
31
|
+
allowedDomains?: readonly string[];
|
|
32
|
+
/** Host-cert roles only: allow `host.example.com` under an allowed `example.com`. */
|
|
33
|
+
allowSubdomains?: boolean;
|
|
34
|
+
/** Host-cert roles only: allow the bare allowed domain itself. */
|
|
35
|
+
allowBareDomains?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Extensions a REQUESTER may ask for — not the ones that get stamped on. `[]` means none
|
|
38
|
+
* may be requested, which is what all three live roles carry; `['*']` allows any.
|
|
39
|
+
*/
|
|
40
|
+
allowedExtensions?: readonly string[];
|
|
41
|
+
/** Extensions stamped on every certificate, e.g. `{ 'permit-pty': '' }`. */
|
|
42
|
+
defaultExtensions?: Readonly<Record<string, string>>;
|
|
43
|
+
/** Critical options a requester may ask for — `force-command`, `source-address`. */
|
|
44
|
+
allowedCriticalOptions?: readonly string[];
|
|
45
|
+
/** Critical options stamped on every certificate. See the ⛔ on erasure in ssh-role.ts. */
|
|
46
|
+
defaultCriticalOptions?: Readonly<Record<string, string>>;
|
|
47
|
+
/** ⛔ Skeleton-key flag — read the ⛔ in ssh-role.ts before setting this true. */
|
|
48
|
+
allowEmptyPrincipals?: boolean;
|
|
49
|
+
/** Certificate lifetime, e.g. `4h`. ⛔ `0` is NOT "no expiry" — see ssh-role.ts. */
|
|
50
|
+
ttl: string;
|
|
51
|
+
/** Ceiling on a requested lifetime, e.g. `8h`. */
|
|
52
|
+
maxTtl: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Every prop resolved to the value the full-replace write will actually produce.
|
|
57
|
+
*
|
|
58
|
+
* ★ Derived rather than retyped: `Required` is the whole difference between the two, and a
|
|
59
|
+
* hand-copied twin is the kind of thing that silently loses a field in review.
|
|
60
|
+
*/
|
|
61
|
+
export type BaoSshRoleForm = Required<BaoSshRoleProps>;
|
|
62
|
+
|
|
63
|
+
/** ⛔ NO SECRET HERE — names, flags, TTLs and a digest. The CA key is out of scope. */
|
|
64
|
+
export interface BaoSshRoleAttributes extends Omit<BaoSshRoleForm, 'maxTtl' | 'ttl'> {
|
|
65
|
+
/**
|
|
66
|
+
* ⚠️ SECONDS, NOT A DURATION STRING. OpenBao stores seconds and a plan should show what is
|
|
67
|
+
* stored; `4h` in the props and 14400 here are the same role. Rendering seconds back
|
|
68
|
+
* into `4h` needs a table of unit rules that mount-form.ts and auth-role-form.ts each
|
|
69
|
+
* already carry a private copy of — a third copy is how the three drift apart.
|
|
70
|
+
*/
|
|
71
|
+
ttlSeconds: number;
|
|
72
|
+
maxTtlSeconds: number;
|
|
73
|
+
/** Always `ca` on a healthy role. Stored so an otp/dynamic role reads as a mismatch. */
|
|
74
|
+
keyType: string;
|
|
75
|
+
/** SHA-256 of the managed fields — safe to persist; see policy.ts. */
|
|
76
|
+
digest: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Mount path without a trailing slash; `ssh` when the caller did not say. */
|
|
80
|
+
export const mountOf = (mount: string | undefined) => (mount ?? 'ssh').replace(/\/+$/, '');
|
|
81
|
+
|
|
82
|
+
export const readPath = (mount: string | undefined, name: string) =>
|
|
83
|
+
`${mountOf(mount)}/roles/${name}`;
|
|
84
|
+
|
|
85
|
+
const text = (value: unknown) => (typeof value === 'string' ? value : '');
|
|
86
|
+
|
|
87
|
+
const bool = (value: unknown, fallback: boolean) => (typeof value === 'boolean' ? value : fallback);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ⚠️ SETS, NOT SEQUENCES. OpenBao stores these as one comma-separated string and promises
|
|
91
|
+
* nothing about the order it hands back. Sorting and de-duplicating on BOTH sides is what
|
|
92
|
+
* stops every plan from reporting an update on a role nobody touched.
|
|
93
|
+
*/
|
|
94
|
+
export const csvList = (value: unknown): readonly string[] => {
|
|
95
|
+
const parts = Array.isArray(value)
|
|
96
|
+
? value.filter((entry): entry is string => typeof entry === 'string')
|
|
97
|
+
: text(value).split(',');
|
|
98
|
+
return [
|
|
99
|
+
...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0)),
|
|
100
|
+
].sort();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ⚠️ A MAP WITH NO PROMISED ORDER. Key order also decides the digest and the argv, so it is
|
|
105
|
+
* normalised once here and everything else reads the sorted copy.
|
|
106
|
+
*/
|
|
107
|
+
export const extMap = (value: unknown): Record<string, string> => {
|
|
108
|
+
const out: Record<string, string> = {};
|
|
109
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return out;
|
|
110
|
+
const source = value as Record<string, unknown>;
|
|
111
|
+
for (const key of Object.keys(source).sort()) out[key] = text(source[key]);
|
|
112
|
+
return out;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** ⛔ Every default here is what the WRITE produces, not what felt sensible. */
|
|
116
|
+
export const resolve = (props: BaoSshRoleProps): BaoSshRoleForm => ({
|
|
117
|
+
name: props.name,
|
|
118
|
+
mount: mountOf(props.mount),
|
|
119
|
+
allowedUsers: csvList(props.allowedUsers),
|
|
120
|
+
defaultUser: props.defaultUser ?? '',
|
|
121
|
+
allowUserCertificates: props.allowUserCertificates ?? false,
|
|
122
|
+
allowHostCertificates: props.allowHostCertificates ?? false,
|
|
123
|
+
allowedDomains: csvList(props.allowedDomains),
|
|
124
|
+
allowSubdomains: props.allowSubdomains ?? false,
|
|
125
|
+
allowBareDomains: props.allowBareDomains ?? false,
|
|
126
|
+
allowedExtensions: csvList(props.allowedExtensions),
|
|
127
|
+
defaultExtensions: extMap(props.defaultExtensions ?? {}),
|
|
128
|
+
allowedCriticalOptions: csvList(props.allowedCriticalOptions),
|
|
129
|
+
defaultCriticalOptions: extMap(props.defaultCriticalOptions ?? {}),
|
|
130
|
+
allowEmptyPrincipals: props.allowEmptyPrincipals ?? false,
|
|
131
|
+
ttl: props.ttl,
|
|
132
|
+
maxTtl: props.maxTtl,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export const attributesOf = (
|
|
136
|
+
form: BaoSshRoleForm,
|
|
137
|
+
live: Record<string, unknown>,
|
|
138
|
+
): BaoSshRoleAttributes => {
|
|
139
|
+
const attrs = {
|
|
140
|
+
name: form.name,
|
|
141
|
+
mount: form.mount,
|
|
142
|
+
allowedUsers: csvList(live['allowed_users']),
|
|
143
|
+
defaultUser: text(live['default_user']),
|
|
144
|
+
allowUserCertificates: bool(live['allow_user_certificates'], false),
|
|
145
|
+
allowHostCertificates: bool(live['allow_host_certificates'], false),
|
|
146
|
+
allowedDomains: csvList(live['allowed_domains']),
|
|
147
|
+
allowSubdomains: bool(live['allow_subdomains'], false),
|
|
148
|
+
allowBareDomains: bool(live['allow_bare_domains'], false),
|
|
149
|
+
allowedExtensions: csvList(live['allowed_extensions']),
|
|
150
|
+
defaultExtensions: extMap(live['default_extensions']),
|
|
151
|
+
allowedCriticalOptions: csvList(live['allowed_critical_options']),
|
|
152
|
+
defaultCriticalOptions: extMap(live['default_critical_options']),
|
|
153
|
+
allowEmptyPrincipals: bool(live['allow_empty_principals'], false),
|
|
154
|
+
keyType: text(live['key_type']) || 'ca',
|
|
155
|
+
ttlSeconds: ttlSeconds(live['ttl']) ?? 0,
|
|
156
|
+
maxTtlSeconds: ttlSeconds(live['max_ttl']) ?? 0,
|
|
157
|
+
};
|
|
158
|
+
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ⛔ THE MAPS CROSS THE WIRE AS A JSON STRING, AND THAT IS THE CORRECT SHAPE. MEASURED with
|
|
163
|
+
* `bao write -output-curl-string` against the live v2.6.2 (no write was sent): the CLI
|
|
164
|
+
* puts every `k=v` value in the body as a string, so `default_extensions` leaves as
|
|
165
|
+
* `"{\"permit-pty\":\"\"}"` and never as an object. OpenBao's own OpenAPI types that field
|
|
166
|
+
* `object`/`format: map` — a `framework` TypeMap, which JSON-decodes a string value.
|
|
167
|
+
* ⚠️ THAT DECODE IS REASONED, NOT MEASURED: writing to the live engine was out of bounds,
|
|
168
|
+
* so the round trip was never executed. If a first deploy lands a role whose
|
|
169
|
+
* default_extensions came out EMPTY, this is the line that was wrong, and the fix is to
|
|
170
|
+
* send the map as a JSON object in writeBody rather than as this string — the HTTP body
|
|
171
|
+
* can now carry either.
|
|
172
|
+
*/
|
|
173
|
+
const mapValue = (map: Readonly<Record<string, string>>) => JSON.stringify(extMap(map));
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* ⛔ EVERY MANAGED FIELD IS SENT ON EVERY WRITE, INCLUDING THE FALSE ONES. The role write is
|
|
177
|
+
* a full replace (see ssh-role.ts), so an omitted field is not "left alone" — it is reset.
|
|
178
|
+
* Sending the resolved form makes the body and the declaration the same statement. All
|
|
179
|
+
* strings, exactly the `k=v` pairs `bao write` sent, for `PUT <mount>/roles/<name>`.
|
|
180
|
+
*/
|
|
181
|
+
export const writeBody = (form: BaoSshRoleForm): Record<string, string> => ({
|
|
182
|
+
key_type: 'ca',
|
|
183
|
+
allowed_users: form.allowedUsers.join(','),
|
|
184
|
+
default_user: form.defaultUser,
|
|
185
|
+
allow_user_certificates: String(form.allowUserCertificates),
|
|
186
|
+
allow_host_certificates: String(form.allowHostCertificates),
|
|
187
|
+
allowed_domains: form.allowedDomains.join(','),
|
|
188
|
+
allow_subdomains: String(form.allowSubdomains),
|
|
189
|
+
allow_bare_domains: String(form.allowBareDomains),
|
|
190
|
+
allowed_extensions: form.allowedExtensions.join(','),
|
|
191
|
+
default_extensions: mapValue(form.defaultExtensions),
|
|
192
|
+
allowed_critical_options: form.allowedCriticalOptions.join(','),
|
|
193
|
+
default_critical_options: mapValue(form.defaultCriticalOptions),
|
|
194
|
+
allow_empty_principals: String(form.allowEmptyPrincipals),
|
|
195
|
+
ttl: form.ttl,
|
|
196
|
+
max_ttl: form.maxTtl,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const sameList = (want: readonly string[], have: readonly string[]) =>
|
|
200
|
+
want.length === have.length && want.every((entry, index) => entry === have[index]);
|
|
201
|
+
|
|
202
|
+
const sameMap = (
|
|
203
|
+
want: Readonly<Record<string, string>>,
|
|
204
|
+
have: Readonly<Record<string, string>>,
|
|
205
|
+
) => {
|
|
206
|
+
const keys = Object.keys(want);
|
|
207
|
+
return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Compare in seconds, never as text — `60m` and `1h` are the same role.
|
|
212
|
+
* ⚠️ An unparseable prop yields undefined, which equals no number, so it reports drift
|
|
213
|
+
* rather than a false noop. `badDurations` is what turns that into a readable refusal.
|
|
214
|
+
*/
|
|
215
|
+
const sameTtl = (want: string, have: number) => parseDuration(want) === have;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* ⛔ AN UNPARSEABLE TTL IS A PLAN THAT NEVER GOES GREEN. parseDuration understands `0` and
|
|
219
|
+
* `<n>[smhd]` and nothing else. A prop like `4h30m` writes fine, reads back as 16200,
|
|
220
|
+
* fails to parse on the want side, and reports `update` forever — and a resource that is
|
|
221
|
+
* always dirty is one nobody reads the plan for any more.
|
|
222
|
+
*/
|
|
223
|
+
export const badDurations = (form: BaoSshRoleForm): readonly string[] =>
|
|
224
|
+
(['ttl', 'maxTtl'] as const).filter((field) => parseDuration(form[field]) === undefined);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* True when live already matches the declaration on every managed field.
|
|
228
|
+
* ⛔ NOTHING IS SKIPPED FOR BEING UNDEFINED. mount.ts and auth-role.ts can treat an absent
|
|
229
|
+
* prop as "don't care" because their writes are partial tunes; this one cannot, because
|
|
230
|
+
* the write resets whatever it omits. Comparing the resolved form is what keeps a
|
|
231
|
+
* hand-edited role from surviving forever behind a green plan.
|
|
232
|
+
*/
|
|
233
|
+
export const matches = (attributes: BaoSshRoleAttributes, form: BaoSshRoleForm) =>
|
|
234
|
+
attributes.keyType === 'ca' &&
|
|
235
|
+
sameList(form.allowedUsers, attributes.allowedUsers) &&
|
|
236
|
+
attributes.defaultUser === form.defaultUser &&
|
|
237
|
+
attributes.allowUserCertificates === form.allowUserCertificates &&
|
|
238
|
+
attributes.allowHostCertificates === form.allowHostCertificates &&
|
|
239
|
+
sameList(form.allowedDomains, attributes.allowedDomains) &&
|
|
240
|
+
attributes.allowSubdomains === form.allowSubdomains &&
|
|
241
|
+
attributes.allowBareDomains === form.allowBareDomains &&
|
|
242
|
+
sameList(form.allowedExtensions, attributes.allowedExtensions) &&
|
|
243
|
+
sameMap(form.defaultExtensions, attributes.defaultExtensions) &&
|
|
244
|
+
sameList(form.allowedCriticalOptions, attributes.allowedCriticalOptions) &&
|
|
245
|
+
sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) &&
|
|
246
|
+
attributes.allowEmptyPrincipals === form.allowEmptyPrincipals &&
|
|
247
|
+
sameTtl(form.ttl, attributes.ttlSeconds) &&
|
|
248
|
+
sameTtl(form.maxTtl, attributes.maxTtlSeconds);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An OpenBao SSH secrets-engine role — `<mount>/roles/{name}`, `key_type=ca`. METADATA ONLY.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THE SIGNING KEY IS OUT OF SCOPE AND MUST STAY OUT. `ssh/config/ca` holds (or generates)
|
|
5
|
+
* the CA private key and `ssh/sign/<role>` issues certificates. Neither is declarable
|
|
6
|
+
* here, for the reason policy.ts spells out: Alchemy persists attributes to its state
|
|
7
|
+
* store WITHOUT encrypting them, that store is the `alchemy` Postgres, and pg-backup.sh
|
|
8
|
+
* dumps it nightly to CT100. A CA key there would outlive the vault that guards it. This
|
|
9
|
+
* resource declares the SHAPE a signature must satisfy — principals, extensions, TTLs.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ THE ROLE WRITE IS A FULL REPLACE, NOT A PATCH. OpenBao rebuilds the role entry from the
|
|
12
|
+
* request body, so any field the write omits reverts to its ZERO value. Two things follow:
|
|
13
|
+
* `resolve` in ssh-role-form.ts fills every optional prop with the default the write would
|
|
14
|
+
* produce and `matches` compares all of it (an omitted prop asserts the default, it never
|
|
15
|
+
* means "don't care"); and `wouldErase` below names the fields this resource does NOT
|
|
16
|
+
* declare, so reconcile can REFUSE instead of quietly dropping someone's key-length floor.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ `allowEmptyPrincipals: true` WITH AN EMPTY `allowedUsers` IS A SKELETON KEY. OpenBao
|
|
19
|
+
* will sign a certificate carrying no principals, and OpenSSH accepts such a certificate
|
|
20
|
+
* for ANY user on ANY host that trusts the CA. It defaults to false, and all three roles
|
|
21
|
+
* on the live engine carry false (MEASURED). Do not set it true without pinning
|
|
22
|
+
* `defaultCriticalOptions['force-command']` in the same declaration.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ `ttl: '0'` DOES NOT MEAN "NO EXPIRY" AND IT DOES NOT MEAN "SHORT". It means the mount's
|
|
25
|
+
* `default_lease_ttl`, and the live `ssh` mount leaves that at 0, which means the SYSTEM
|
|
26
|
+
* default — 768h. A 32-day SSH certificate is the opposite of what an SSH CA is for.
|
|
27
|
+
* State the duration. MEASURED: the live `node-admin` and `tim` roles are ttl 14400 /
|
|
28
|
+
* max_ttl 28800 (4h / 8h); `ssh-host/roles/host` is 31536000 (365d), as a host cert should.
|
|
29
|
+
*
|
|
30
|
+
* ★ `defaultRemovalPolicy: 'retain'`. Deleting this role locks every operator out of the
|
|
31
|
+
* estate — the memory note "operator SSH is homeflare-bot@<host>" is this CA. Opt in with
|
|
32
|
+
* `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
|
|
33
|
+
*/
|
|
34
|
+
import { Resource } from 'alchemy';
|
|
35
|
+
import { isResolved } from 'alchemy/Diff';
|
|
36
|
+
import * as Provider from 'alchemy/Provider';
|
|
37
|
+
import * as Effect from 'effect/Effect';
|
|
38
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
39
|
+
import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
|
|
40
|
+
import {
|
|
41
|
+
type BaoSshRoleAttributes,
|
|
42
|
+
type BaoSshRoleForm,
|
|
43
|
+
type BaoSshRoleProps,
|
|
44
|
+
attributesOf,
|
|
45
|
+
badDurations,
|
|
46
|
+
extMap,
|
|
47
|
+
matches,
|
|
48
|
+
readPath,
|
|
49
|
+
resolve,
|
|
50
|
+
writeBody,
|
|
51
|
+
} from './ssh-role-form.ts';
|
|
52
|
+
|
|
53
|
+
export type { BaoSshRoleAttributes, BaoSshRoleProps };
|
|
54
|
+
|
|
55
|
+
export interface BaoSshRole extends Resource<
|
|
56
|
+
'Bao.SshRole',
|
|
57
|
+
BaoSshRoleProps,
|
|
58
|
+
BaoSshRoleAttributes,
|
|
59
|
+
never,
|
|
60
|
+
HttpClient.HttpClient
|
|
61
|
+
> {}
|
|
62
|
+
|
|
63
|
+
export const BaoSshRole = Resource<BaoSshRole>('Bao.SshRole', {
|
|
64
|
+
defaultRemovalPolicy: 'retain',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** Live role, plus the raw body — reconcile needs the raw half to see what it would erase. */
|
|
68
|
+
const readRole = (form: BaoSshRoleForm) =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const live = yield* baoRead(readPath(form.mount, form.name));
|
|
71
|
+
if (live === undefined) return undefined;
|
|
72
|
+
return { attributes: attributesOf(form, live), live };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fields this resource deliberately does not model, mapped to the value OpenBao's own write
|
|
77
|
+
* puts there when nobody says otherwise. The templating flags change how a principal is
|
|
78
|
+
* DERIVED at signing time, which is a different kind of decision from which principals
|
|
79
|
+
* exist; the rest are issuance mechanics. MEASURED against all three live roles — every one
|
|
80
|
+
* of them sits on exactly these values, so the guard below fires on nothing that exists.
|
|
81
|
+
*/
|
|
82
|
+
const UNMANAGED_DEFAULTS: Readonly<Record<string, unknown>> = {
|
|
83
|
+
algorithm_signer: 'default',
|
|
84
|
+
allow_commas_in_identity_templates: false,
|
|
85
|
+
allow_user_key_ids: false,
|
|
86
|
+
allowed_domains_template: false,
|
|
87
|
+
allowed_users_template: false,
|
|
88
|
+
default_extensions_template: false,
|
|
89
|
+
default_user_template: false,
|
|
90
|
+
issuer_ref: 'default',
|
|
91
|
+
key_id_format: '',
|
|
92
|
+
not_before_duration: 30,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ⚠️ ABSENT AND EMPTY BOTH COUNT AS "NEVER SET". A role this resource wrote omits these
|
|
97
|
+
* fields entirely, so a stricter test would refuse on the provider's own output and
|
|
98
|
+
* deadlock the update after next.
|
|
99
|
+
*/
|
|
100
|
+
const isDefault = (live: Record<string, unknown>, field: string) =>
|
|
101
|
+
live[field] === undefined || live[field] === '' || live[field] === UNMANAGED_DEFAULTS[field];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ⛔ THE FIELDS A WRITE WOULD SILENTLY DESTROY. Because the write is a full replace, a role
|
|
105
|
+
* someone tuned by hand — `allowed_user_key_lengths` (a minimum RSA size), a
|
|
106
|
+
* `key_id_format` the audit trail is grepped by, a `not_before_duration` widened to
|
|
107
|
+
* survive clock skew, a non-default issuer — loses all of it the moment this resource
|
|
108
|
+
* decides to update. A vanished key-length floor is a security regression that no plan
|
|
109
|
+
* output would ever mention, and a vanished backdating window breaks signing on exactly
|
|
110
|
+
* the skewed host it was added for. So reconcile refuses instead of writing.
|
|
111
|
+
*/
|
|
112
|
+
const wouldErase = (live: Record<string, unknown>): readonly string[] => {
|
|
113
|
+
const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));
|
|
114
|
+
if (Object.keys(extMap(live['allowed_user_key_lengths'])).length > 0) {
|
|
115
|
+
lost.push('allowed_user_key_lengths');
|
|
116
|
+
}
|
|
117
|
+
return lost.sort();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const refuse = (message: string) => Effect.die(new Error(message));
|
|
121
|
+
|
|
122
|
+
export const BaoSshRoleProvider = () =>
|
|
123
|
+
Provider.effect(
|
|
124
|
+
BaoSshRole,
|
|
125
|
+
Effect.succeed(
|
|
126
|
+
BaoSshRole.Provider.of({
|
|
127
|
+
/**
|
|
128
|
+
* ⛔ `bao list ssh/roles` IS NOT A LIST OF THINGS THIS OWNS. Every role on the mount
|
|
129
|
+
* answers, including the break-glass ones cut by hand at bringup. Returning them
|
|
130
|
+
* would invite Alchemy to adopt — and then delete — the role that is the only way
|
|
131
|
+
* back into the estate.
|
|
132
|
+
*/
|
|
133
|
+
list: () => Effect.succeed([]),
|
|
134
|
+
|
|
135
|
+
read: Effect.fn(function* ({ olds }) {
|
|
136
|
+
const found = yield* readRole(resolve(olds));
|
|
137
|
+
return found?.attributes;
|
|
138
|
+
}),
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A role widened in the OpenBao
|
|
142
|
+
* UI — one extra principal in `allowed_users` — is exactly the drift the check-*
|
|
143
|
+
* gates exist to catch, and a provider that trusted its own state would report
|
|
144
|
+
* `noop` straight through it.
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ `wouldErase` is deliberately NOT consulted here. Extra hand-set fields are only
|
|
147
|
+
* at risk when something actually writes; if every managed field already matches,
|
|
148
|
+
* the honest answer is `noop` and nothing gets destroyed.
|
|
149
|
+
*/
|
|
150
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
151
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
152
|
+
const form = resolve(news);
|
|
153
|
+
/**
|
|
154
|
+
* ⛔ A RENAMED ROLE IS A NEW PATH, NOT AN EDIT. `ssh/roles/x` and `ssh-host/roles/x`
|
|
155
|
+
* are different mounts with different CAs. Without this, changing `name` or
|
|
156
|
+
* `mount` would write the new role and leave the old one signing certificates
|
|
157
|
+
* for as long as anyone remembered its name.
|
|
158
|
+
*/
|
|
159
|
+
if (output.mount !== form.mount || output.name !== form.name) {
|
|
160
|
+
return { action: 'replace' } as const;
|
|
161
|
+
}
|
|
162
|
+
const found = yield* readRole(form);
|
|
163
|
+
if (found === undefined) return { action: 'update' } as const;
|
|
164
|
+
return matches(found.attributes, form)
|
|
165
|
+
? ({ action: 'noop' } as const)
|
|
166
|
+
: ({ action: 'update' } as const);
|
|
167
|
+
}),
|
|
168
|
+
|
|
169
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
170
|
+
const form = resolve(news);
|
|
171
|
+
const path = readPath(form.mount, form.name);
|
|
172
|
+
|
|
173
|
+
const bad = badDurations(form);
|
|
174
|
+
if (bad.length > 0) {
|
|
175
|
+
return yield* refuse(
|
|
176
|
+
`Bao.SshRole ${path}: ${bad.join(', ')} is not a duration parseDuration understands ` +
|
|
177
|
+
'(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.',
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* ⛔ A USER-CERT ROLE WITH NO PRINCIPALS SIGNS NOTHING. It is not dangerous — it
|
|
182
|
+
* fails closed at sign time — but it is never what was meant, and the failure
|
|
183
|
+
* surfaces hours later as "ssh stopped working" rather than here. The opposite
|
|
184
|
+
* case, empty principals that are ALLOWED, is the skeleton key in the header.
|
|
185
|
+
*/
|
|
186
|
+
if (
|
|
187
|
+
form.allowUserCertificates &&
|
|
188
|
+
form.allowedUsers.length === 0 &&
|
|
189
|
+
!form.allowEmptyPrincipals
|
|
190
|
+
) {
|
|
191
|
+
return yield* refuse(
|
|
192
|
+
`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` +
|
|
193
|
+
'allowEmptyPrincipals false. Every sign request against it would be rejected.',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const found = yield* readRole(form);
|
|
198
|
+
if (found === undefined || !matches(found.attributes, form)) {
|
|
199
|
+
if (found !== undefined) {
|
|
200
|
+
const lost = wouldErase(found.live);
|
|
201
|
+
if (lost.length > 0) {
|
|
202
|
+
return yield* refuse(
|
|
203
|
+
`Bao.SshRole ${path}: live role carries ${lost.join(', ')}, which this ` +
|
|
204
|
+
'resource does not declare and the full-replace write would ERASE. ' +
|
|
205
|
+
'Fold those settings into the declaration, or clear them by hand first.',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
yield* baoWrite('PUT', path, writeBody(form));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* ⚠️ RE-READ RATHER THAN ECHO THE DECLARATION. The write is the only place the
|
|
213
|
+
* JSON-string encoding of `default_extensions` could be wrong (see the ⛔ on
|
|
214
|
+
* mapValue), and a provider that returned its own props would record a role it
|
|
215
|
+
* had never confirmed.
|
|
216
|
+
*/
|
|
217
|
+
const after = yield* readRole(form);
|
|
218
|
+
if (after === undefined) {
|
|
219
|
+
return yield* refuse(
|
|
220
|
+
`Bao.SshRole ${path}: write returned no error but the role is still absent.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return after.attributes;
|
|
224
|
+
}),
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* ⛔ DELETING THE ROLE DOES NOT REVOKE THE CERTIFICATES IT ALREADY SIGNED. OpenSSH
|
|
228
|
+
* has no CRL here: every outstanding certificate stays valid for the rest of its
|
|
229
|
+
* TTL, on every host trusting the CA, with no way to call it back short of a KRL
|
|
230
|
+
* on each host. On the live `ssh-host` role that TTL is 365 days. Deleting is
|
|
231
|
+
* therefore a change to who gets in NEXT, never a way to lock someone out now.
|
|
232
|
+
* Idempotent as Alchemy requires — already gone is success.
|
|
233
|
+
*/
|
|
234
|
+
delete: Effect.fn(function* ({ output }) {
|
|
235
|
+
yield* baoDelete(readPath(output.mount, output.name));
|
|
236
|
+
return undefined;
|
|
237
|
+
}),
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
);
|