@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,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Acl` — one grant: a role bound to a subject at a path. The line that makes a role real.
|
|
3
|
+
*
|
|
4
|
+
* ★ USERS, GROUPS, ROLES AND TOKENS ARE ALL INERT UNTIL AN ACL BINDS THEM. A role is a named list
|
|
5
|
+
* of privileges that grants nothing by itself; a user can do nothing until
|
|
6
|
+
* {path, user|group|token, role} exists. That is what makes those four families worth declaring,
|
|
7
|
+
* and why the grants that let OpenBao mint a PVE token today are only clicks somebody made once.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ THE ONE FAMILY THAT DOES NOT FIT `pveOperations` AS WRITTEN, AND THE MISFIT IS MEASURED. The
|
|
10
|
+
* cluster's own schema — `/pve-docs/api-viewer/apidoc.js`, read unauthenticated 2026-09-13 —
|
|
11
|
+
* lists exactly TWO methods on `/access/acl`: GET and PUT. There is no DELETE (it answers
|
|
12
|
+
* "Method 'DELETE /access/acl' not implemented") and, less famously, NO POST either. So create
|
|
13
|
+
* and update are the SAME call, `PUT /access/acl`, and removal is that PUT with `delete=1` —
|
|
14
|
+
* which is why `delete` below does not use `ops.destroy`.
|
|
15
|
+
* ⚠️ THE REAL FIX BELONGS IN `resource.ts`: give `PveSpec` a `deleteForm` and the method to go
|
|
16
|
+
* with it, and this override disappears. Until then it is local and loud — do not "restore
|
|
17
|
+
* symmetry" by pointing `delete` at `ops.destroy`, which reports every removal as a 501 while
|
|
18
|
+
* the grant stays exactly where it was.
|
|
19
|
+
*
|
|
20
|
+
* ⛔ A SINGLE GRANT HAS NO URL OF ITS OWN. `GET /access/acl` answers ONE FLAT LIST for the whole
|
|
21
|
+
* cluster, so identity is the tuple (path, type, ugid, roleid), matched client-side in
|
|
22
|
+
* `attributes` below. Two resources declaring the SAME tuple are the SAME grant — Alchemy sees
|
|
23
|
+
* two resource ids, not one collision — and deleting either takes the access away from both,
|
|
24
|
+
* the same hazard as two guests declaring one vmid.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ AND THAT LIST IS FILTERED BY WHO IS ASKING. The schema's own words for GET: "The returned list
|
|
27
|
+
* is restricted to objects where you have rights to modify permissions." A credential that can
|
|
28
|
+
* read the cluster but not modify permissions is answered `[]` — not a 403, not an error — so
|
|
29
|
+
* the mount's `read` role needs permission-modify rights on the declared path exactly as
|
|
30
|
+
* `provision` does. Work reported on a grant that is plainly there means that role is too narrow.
|
|
31
|
+
*/
|
|
32
|
+
import { Resource } from 'alchemy';
|
|
33
|
+
import { isResolved } from 'alchemy/Diff';
|
|
34
|
+
import * as Provider from 'alchemy/Provider';
|
|
35
|
+
import * as Effect from 'effect/Effect';
|
|
36
|
+
import { pve } from './client.ts';
|
|
37
|
+
import { type PveRequirements, type WithTarget, pveOperations } from './resource.ts';
|
|
38
|
+
|
|
39
|
+
/** PVE's three kinds of subject. The read answers this word; the write wants its plural. */
|
|
40
|
+
export type AclSubjectType = 'user' | 'group' | 'token';
|
|
41
|
+
|
|
42
|
+
export interface AclProps extends WithTarget {
|
|
43
|
+
/**
|
|
44
|
+
* The PVE object path the grant is ON — `/`, `/pool/house`, `/vms/101`, `/storage/local-zfs`.
|
|
45
|
+
* ⚠️ NOT THE API PATH: `spec.path` below is the endpoint (`access/acl`). PVE's API overloads the
|
|
46
|
+
* word, so this file does too rather than renaming a field the cluster calls `path`.
|
|
47
|
+
*/
|
|
48
|
+
path: string;
|
|
49
|
+
/** Which kind of subject `ugid` names. Identity: changing it is a different grant. */
|
|
50
|
+
type: AclSubjectType;
|
|
51
|
+
/** `tim@pve`, `admins`, `hf-provision@pve!hf-provision-…`. Identity. */
|
|
52
|
+
ugid: string;
|
|
53
|
+
/** The role bound here, e.g. `PVEAuditor`. ⚠️ It must exist — PVE refuses an unknown roleid. */
|
|
54
|
+
roleid: string;
|
|
55
|
+
/** Inherit down the path. PVE's default is ON, and this is the grant's ONLY mutable field. */
|
|
56
|
+
propagate?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AclAttributes {
|
|
60
|
+
path: string;
|
|
61
|
+
type: AclSubjectType;
|
|
62
|
+
ugid: string;
|
|
63
|
+
roleid: string;
|
|
64
|
+
propagate: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* ⛔ PRESENCE AS AN ATTRIBUTE, AND IT IS WHAT MAKES THE FACTORY WORK HERE. `attributes` never
|
|
67
|
+
* returns undefined: if it did, `reconcile` would take the create branch and POST to
|
|
68
|
+
* `access/acl`, which PVE does not implement. Present-but-unbound sends every write down the
|
|
69
|
+
* PUT branch instead, the only branch PVE has. The cost: the factory's read-back guard cannot
|
|
70
|
+
* fire, so `reconcile` below re-checks this field itself and dies with the same honesty.
|
|
71
|
+
*/
|
|
72
|
+
bound: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ProxmoxAcl extends Resource<
|
|
76
|
+
'Proxmox.Acl',
|
|
77
|
+
AclProps,
|
|
78
|
+
AclAttributes,
|
|
79
|
+
never,
|
|
80
|
+
PveRequirements
|
|
81
|
+
> {}
|
|
82
|
+
|
|
83
|
+
export const ProxmoxAcl = Resource<ProxmoxAcl>('Proxmox.Acl');
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ⚠️ PVE NORMALISES ACL PATHS AND RETURNS THE NORMALISED FORM, a forever-update trap when only one
|
|
87
|
+
* side of the comparison is normalised. `PVE::AccessControl::normalize_path` collapses repeated
|
|
88
|
+
* slashes and strips the trailing one, so a declared `/pool/house/` reads back as `/pool/house`
|
|
89
|
+
* and a naive match never fires again. It also refuses a path with no leading slash, so one is
|
|
90
|
+
* added here rather than letting `pool/house` 400.
|
|
91
|
+
*/
|
|
92
|
+
const normalize = (raw: string) => `/${raw.split('/').filter(Boolean).join('/')}`;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* ⚠️ THE WRITE NAMES THE SUBJECT WITH A PLURAL KEY THAT DIFFERS PER KIND — `users`, `groups`,
|
|
96
|
+
* `tokens` — while the READ answers a singular `type`/`ugid` pair. The PUT declares
|
|
97
|
+
* `additionalProperties: 0`, so `user=` fails with a 400 rather than being ignored. The bad case
|
|
98
|
+
* is the WRONG plural: a valid parameter naming a subject kind you did not mean.
|
|
99
|
+
*/
|
|
100
|
+
const SUBJECT_FIELD = { group: 'groups', token: 'tokens', user: 'users' } as const;
|
|
101
|
+
|
|
102
|
+
/** The tuple PVE keys a grant by, as the form it takes. Every write to this family starts here. */
|
|
103
|
+
const tuple = (props: AclProps): Record<string, string> => ({
|
|
104
|
+
path: normalize(props.path),
|
|
105
|
+
roles: props.roleid,
|
|
106
|
+
[SUBJECT_FIELD[props.type]]: props.ugid,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Create and update are one call: the tuple plus the single mutable field. */
|
|
110
|
+
const bind = (props: AclProps): Record<string, string> => ({
|
|
111
|
+
propagate: props.propagate === false ? '0' : '1',
|
|
112
|
+
...tuple(props),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** What a change of this string means: not an edit, a different grant. Used by `diff` below. */
|
|
116
|
+
const identity = (grant: Pick<AclAttributes, 'path' | 'roleid' | 'type' | 'ugid'>) =>
|
|
117
|
+
[normalize(grant.path), grant.type, grant.ugid, grant.roleid].join(' ');
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* ⚠️ `GET /access/acl` ANSWERS AN ARRAY, while the factory hands `attributes` the
|
|
121
|
+
* `Record<string, unknown>` every other PVE read is shaped like. Rows are narrowed, not trusted,
|
|
122
|
+
* and an empty result is not evidence of an empty cluster — see the last ⛔ in the header.
|
|
123
|
+
*/
|
|
124
|
+
const find = (live: unknown, props: AclProps) =>
|
|
125
|
+
(Array.isArray(live) ? live : [])
|
|
126
|
+
.filter((row): row is Record<string, unknown> => typeof row === 'object' && row !== null)
|
|
127
|
+
.find(
|
|
128
|
+
(row) =>
|
|
129
|
+
row['path'] === normalize(props.path) &&
|
|
130
|
+
row['type'] === props.type &&
|
|
131
|
+
row['ugid'] === props.ugid &&
|
|
132
|
+
row['roleid'] === props.roleid,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* ⚠️ PVE ANSWERS `propagate` AS 1/0 RATHER THAN true/false, AND OMITS IT WHEN IT CARRIES THE API
|
|
137
|
+
* DEFAULT, WHICH IS ON. Reading an absent field as `false` would report drift on every plan,
|
|
138
|
+
* forever, for every grant this provider did not create itself.
|
|
139
|
+
*/
|
|
140
|
+
const propagates = (row: Record<string, unknown>) => {
|
|
141
|
+
const value = row['propagate'];
|
|
142
|
+
return value === undefined || value === 1 || value === true || value === '1';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const ops = pveOperations<AclProps, AclAttributes>({
|
|
146
|
+
attributes: (live, props) => {
|
|
147
|
+
const row = find(live, props);
|
|
148
|
+
return {
|
|
149
|
+
// ⚠️ `propagate` reads false when nothing is bound, because there is no grant to inherit.
|
|
150
|
+
// `bound` is the field that carries presence; this one means something only once it is set.
|
|
151
|
+
bound: row !== undefined,
|
|
152
|
+
path: normalize(props.path),
|
|
153
|
+
propagate: row !== undefined && propagates(row),
|
|
154
|
+
roleid: props.roleid,
|
|
155
|
+
type: props.type,
|
|
156
|
+
ugid: props.ugid,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* ⛔ THE CREATE PATH IS UNREACHABLE ON A HEALTHY CLUSTER, AND ITS ERROR WILL MISLEAD YOU. The
|
|
161
|
+
* factory POSTs `collection` only when the read came back undefined, and `attributes` never
|
|
162
|
+
* does — so a POST means the GET itself failed (an expired 300s lease, a node down), not that
|
|
163
|
+
* the grant is missing. PVE answers "Method 'POST /access/acl' not implemented": read that 501
|
|
164
|
+
* as "the read failed" and go and look at the credential, not at the ACL.
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* ⛔ `provision`, AND WITHOUT IT EVERY GRANT READS BACK AS ABSENT. `GET /access/acl` is
|
|
168
|
+
* FILTERED, NOT GATED — the schema's own words are "The returned list is restricted to objects
|
|
169
|
+
* where you have rights to modify permissions". A credential without Permissions.Modify gets
|
|
170
|
+
* HTTP 200 and an EMPTY ARRAY, never a 403, so there is no error for `read` to fold; it simply
|
|
171
|
+
* sees nothing.
|
|
172
|
+
* ⛔ THE READ LEASE IS EXACTLY SUCH A CREDENTIAL. `hf-read@pve` is PVEAuditor on `/`, whose
|
|
173
|
+
* privilege set is Datastore.Audit, Mapping.Audit, Pool.Audit, SDN.Audit, Sys.Audit,
|
|
174
|
+
* VM.Audit, VM.GuestAgent.Audit — MEASURED in the 2026-09-13 cluster read. No
|
|
175
|
+
* Permissions.Modify. So on the default lease all fourteen live grants read back unbound,
|
|
176
|
+
* `matches` is false for every one, the plan reports fourteen updates for grants that
|
|
177
|
+
* plainly exist, and the deploy PUTs each one before acl.ts's own read-back guard dies.
|
|
178
|
+
* ★ THIS IS THE SAME CLASS AS storage.ts, sdn-zone.ts, sdn-vnet.ts AND api-token.ts, and it is
|
|
179
|
+
* the worst instance of it: those three are gated and answer 403, which is at least an
|
|
180
|
+
* error. A filtered endpoint answers success with less data, which is indistinguishable from
|
|
181
|
+
* the object not being there.
|
|
182
|
+
*/
|
|
183
|
+
readRole: 'provision',
|
|
184
|
+
collection: () => 'access/acl',
|
|
185
|
+
createForm: bind,
|
|
186
|
+
/**
|
|
187
|
+
* ⚠️ ONLY `propagate` IS COMPARED, AND ONLY ONCE THE GRANT IS THERE. path/type/ugid/roleid are
|
|
188
|
+
* the FILTER that produced these attributes, not a reading of the cluster: comparing them with
|
|
189
|
+
* the props they came from is true by construction. An identity change is a DIFFERENT grant,
|
|
190
|
+
* which `diff` in the provider handles.
|
|
191
|
+
*/
|
|
192
|
+
matches: (attributes, props) =>
|
|
193
|
+
attributes.bound && attributes.propagate === (props.propagate !== false),
|
|
194
|
+
path: () => 'access/acl',
|
|
195
|
+
updateForm: bind,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const ProxmoxAclProvider = () =>
|
|
199
|
+
Provider.effect(
|
|
200
|
+
ProxmoxAcl,
|
|
201
|
+
Effect.succeed(
|
|
202
|
+
ProxmoxAcl.Provider.of({
|
|
203
|
+
/**
|
|
204
|
+
* ⛔ EMPTY LIKE EVERY OTHER RESOURCE HERE, AND MOST OF ALL THIS ONE. `GET /access/acl`
|
|
205
|
+
* hands back every grant a human ever clicked; adopting them would let a later plan
|
|
206
|
+
* DELETE somebody's access as tidy-up. Adoption is an explicit act.
|
|
207
|
+
*/
|
|
208
|
+
list: () => Effect.succeed([]),
|
|
209
|
+
read: Effect.fn(function* ({ olds }) {
|
|
210
|
+
return yield* ops.read(olds);
|
|
211
|
+
}),
|
|
212
|
+
/**
|
|
213
|
+
* ⛔ AN IDENTITY CHANGE IS A REPLACE, AND THE FACTORY CANNOT SAY SO ALONE — it answers
|
|
214
|
+
* `replace` only for objects with no update path, and this one has PUT. Left to delegate,
|
|
215
|
+
* editing `roleid` (or the subject, or the path) would PUT the NEW grant and LEAVE THE
|
|
216
|
+
* OLD ONE BOUND: access nobody declared, held indefinitely, invisible in the plan because
|
|
217
|
+
* the provider believes it converged. Replace makes Alchemy call `delete` with the OLD
|
|
218
|
+
* props, which is the only thing that removes it.
|
|
219
|
+
* ⚠️ CREATE-FIRST, DELIBERATELY (Alchemy's default; `deleteFirst: true` would invert it).
|
|
220
|
+
* Delete-first could remove the very grant the provision credential mints against and
|
|
221
|
+
* leave nothing able to put it back. The cost is that a NARROWING change leaves the
|
|
222
|
+
* wider grant bound until Phase 2 collects the old generation.
|
|
223
|
+
*/
|
|
224
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
225
|
+
if (output !== undefined && isResolved(news) && identity(news) !== identity(output)) {
|
|
226
|
+
return { action: 'replace' } as const;
|
|
227
|
+
}
|
|
228
|
+
return yield* ops.diff(news, output);
|
|
229
|
+
}),
|
|
230
|
+
/**
|
|
231
|
+
* ⚠️ THE READ-BACK GUARD, RESTORED FOR A FAMILY WHOSE "ABSENT" IS NOT `undefined`.
|
|
232
|
+
* `ops.reconcile` refuses when the object is still missing after a write, but missing
|
|
233
|
+
* here is `bound: false`, which that check cannot see. PVE answers 200 with
|
|
234
|
+
* `{"data":null}` on calls that did nothing: without this, a PUT that silently no-oped
|
|
235
|
+
* is recorded as a landed grant and the next plan reads noop over the gap.
|
|
236
|
+
*/
|
|
237
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
238
|
+
const after = yield* ops.reconcile(news);
|
|
239
|
+
if (!after.bound) {
|
|
240
|
+
return yield* Effect.die(
|
|
241
|
+
new Error(
|
|
242
|
+
`access/acl: the write returned no error but ${identity(news)} is still not ` +
|
|
243
|
+
'bound. Either the PUT did nothing, or the read credential cannot see the ' +
|
|
244
|
+
'grant -- GET /access/acl is filtered to paths you may MODIFY permissions on, ' +
|
|
245
|
+
'so a read-only role is answered [] rather than 403.',
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return after;
|
|
250
|
+
}),
|
|
251
|
+
/**
|
|
252
|
+
* ⛔ NOT `ops.destroy`, AND NOT AN OVERSIGHT. `DELETE /access/acl` is not implemented, so a
|
|
253
|
+
* factory delete would fail every destroy — while looking like a permissions problem —
|
|
254
|
+
* and leave the grant bound. Removal is the create call with `delete=1` and the identity
|
|
255
|
+
* tuple, no `propagate`, because removal is keyed on the tuple and nothing else.
|
|
256
|
+
*
|
|
257
|
+
* ⚠️ IDEMPOTENT, AND SHARED: removing a grant that is already gone is a no-op, but the
|
|
258
|
+
* tuple is cluster-global — re-read the second ⛔ in the header before assuming this
|
|
259
|
+
* only takes away what this stack declared.
|
|
260
|
+
*/
|
|
261
|
+
delete: Effect.fn(function* ({ olds }) {
|
|
262
|
+
const removal = { delete: '1', ...tuple(olds) };
|
|
263
|
+
yield* pve(olds.target, 'provision', 'PUT', 'access/acl', removal);
|
|
264
|
+
}),
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What a write to a PVE API token actually does, and the form that does it.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF api-token.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is the one
|
|
6
|
+
* metric-server-form.ts draws: this file answers "what does PVE store when this form arrives",
|
|
7
|
+
* api-token.ts answers "what a token is and when it has changed". Nothing here reads the
|
|
8
|
+
* cluster and nothing here decides a diff.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ THE `import type` BACK TO api-token.ts IS A CYCLE ON PAPER ONLY. It is type-only, so it is
|
|
11
|
+
* erased before anything runs and `ApiTokenProps` stays in the file that declares the resource.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ THE WRITE SEMANTICS BELOW ARE READ FROM THE CLUSTER'S OWN PERL RATHER THAN INFERRED FROM THE
|
|
14
|
+
* API SCHEMA — `/usr/share/perl5/PVE/API2/User.pm`, subs `generate_token` and
|
|
15
|
+
* `update_token_info`, read over SSH on n2 on 2026-09-13. The two disagree in exactly the place
|
|
16
|
+
* that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
|
|
17
|
+
* never implements it. api-token.ts records what that means.
|
|
18
|
+
*/
|
|
19
|
+
import type { PveSpec } from './resource.ts';
|
|
20
|
+
import { bool, int, text } from './values.ts';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `<userid>!<tokenid>` — the name the rest of PVE calls this object by.
|
|
24
|
+
*
|
|
25
|
+
* ★ IT IS WHAT AN ACL BINDS. `Proxmox.Acl` with `type: 'token'` wants exactly this string as its
|
|
26
|
+
* `ugid`, so reporting it as an attribute is what lets a privilege-separated token be granted
|
|
27
|
+
* anything at all without somebody retyping the join by hand.
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ DERIVED, NOT READ. MEASURED: `GET /access/users/monitoring@pve/token/exporter` answers
|
|
30
|
+
* `{"expire":0,"privsep":0}` and echoes back neither half of its own identity. So this is built
|
|
31
|
+
* from props, cannot disagree with the path the read used, and is therefore true by
|
|
32
|
+
* construction — which is why it is reported and never compared.
|
|
33
|
+
*/
|
|
34
|
+
export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.tokenid}`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Everything a token has, in the form PVE wants. Create and update send exactly this.
|
|
38
|
+
*
|
|
39
|
+
* ⛔ `userid` AND `tokenid` ARE NOT IN IT, and that is not an omission. Both are path segments of
|
|
40
|
+
* `access/users/{userid}/token/{tokenid}` — which is the path the POST goes to as well as the
|
|
41
|
+
* PUT — so a second copy in the body can only ever disagree with the path it was sent to.
|
|
42
|
+
* metric-server.ts omits its `id` for the same reason and says so there.
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ EVERY FIELD IS SENT ON EVERY WRITE, THE EMPTY COMMENT INCLUDED, BECAUSE A PUT MERGES.
|
|
45
|
+
* MEASURED in `update_token_info`: `$token->{comment} = $param->{comment} if defined(...)`
|
|
46
|
+
* followed by `delete $token->{comment} if (!length $token->{comment})`. So `comment=` is how a
|
|
47
|
+
* comment is CLEARED; omitting it leaves the old text in place while `matches` goes on asking
|
|
48
|
+
* for an update the update cannot make. user.ts writes the same reasoning out for `groups`.
|
|
49
|
+
* ⚠️ THE `delete=` PARAMETER IS NOT USED AND WOULD NOT HELP. Its whitelist in that sub is
|
|
50
|
+
* literally `my $deletable = { comment => 1 };`, and anything else answers
|
|
51
|
+
* "unknown option '<k>'" — so `expire` and `privsep` have no clear path at all, which is the
|
|
52
|
+
* other half of why both are required props rather than optional ones.
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ `flag()` FROM values.ts IS DELIBERATELY NOT USED FOR `privsep`. Its entire job is turning an
|
|
55
|
+
* UNDECLARED boolean into `undefined` so the field is omitted from the form; `privsep` is a
|
|
56
|
+
* required prop precisely so that it is never undeclared, so there is nothing for it to do and
|
|
57
|
+
* its `string | undefined` return would have to be asserted away to fit `Record<string,string>`.
|
|
58
|
+
* An assertion here would be a claim about the prop that the type already makes properly.
|
|
59
|
+
*/
|
|
60
|
+
export const shape = (props: ApiTokenProps): Record<string, string> => ({
|
|
61
|
+
comment: props.comment ?? '',
|
|
62
|
+
expire: String(props.expire),
|
|
63
|
+
privsep: props.privsep ? '1' : '0',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
|
|
67
|
+
// ⛔ THE ITEM READ NEEDS `User.Modify`, WHICH THE AUDITOR-SHAPED READ LEASE DOES NOT HAVE. The
|
|
68
|
+
// measurement and the consequence are in the header's last ⚠️; the mechanism — a silent
|
|
69
|
+
// "absent" rather than a 403 — is the ⛔ on `readRole` in resource.ts.
|
|
70
|
+
readRole: 'provision',
|
|
71
|
+
/**
|
|
72
|
+
* ★ NOTHING PVE REPORTS HERE IS UNWRITABLE, WHICH IS UNUSUAL IN THIS PACKAGE AND IS WHY
|
|
73
|
+
* `matches` can compare everything it reads. The GET returns exactly `comment`, `expire` and
|
|
74
|
+
* `privsep`; PUT accepts exactly those three. There is no autoscaled field, no server-assigned
|
|
75
|
+
* id, no set whose order PVE reshuffles — the three killers the neighbouring files carry
|
|
76
|
+
* ⚠️s about do not arise. The two identity fields and `fullTokenid` come from props and are
|
|
77
|
+
* true by construction, so comparing them would be theatre.
|
|
78
|
+
* ⚠️ `undefined` WHEN NEITHER FLAG IS THERE, BECAUSE BOTH ARE ALWAYS THERE. `user.cfg`
|
|
79
|
+
* materialises `expire` and `privsep` for every token it stores, so an answer carrying neither
|
|
80
|
+
* is not a token — and without this guard `bool`/`int` would invent one out of their fallbacks
|
|
81
|
+
* and the read-back guard in `reconcile` could never fire.
|
|
82
|
+
*/
|
|
83
|
+
attributes: (live, props) => {
|
|
84
|
+
const expire = live['expire'];
|
|
85
|
+
const privsep = live['privsep'];
|
|
86
|
+
if (expire === undefined && privsep === undefined) return undefined;
|
|
87
|
+
return {
|
|
88
|
+
comment: text(live['comment']),
|
|
89
|
+
expire: int(expire, 0),
|
|
90
|
+
fullTokenid: fullTokenid(props),
|
|
91
|
+
/** ⚠️ FALLBACK `true`, MATCHING PVE'S API DEFAULT — unreachable given the guard above, but
|
|
92
|
+
* wrong in the safe direction if a future release stops emitting the field. */
|
|
93
|
+
privsep: bool(privsep, true),
|
|
94
|
+
tokenid: props.tokenid,
|
|
95
|
+
userid: props.userid,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* ⛔ THE SAME STRING AS `path`, AND NOT A TYPO. A token is POSTed to its OWN url, not to the
|
|
100
|
+
* collection: MEASURED, `/access/users/{userid}/token` carries a GET and nothing else, while
|
|
101
|
+
* POST lives on `{tokenid}`. metric-server.ts has the identical shape for the identical
|
|
102
|
+
* reason and records the experiment there.
|
|
103
|
+
*/
|
|
104
|
+
collection: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
|
|
105
|
+
createForm: shape,
|
|
106
|
+
/**
|
|
107
|
+
* ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
|
|
108
|
+
* MEASURED, by replaying these two functions over the live GET bodies of all four token
|
|
109
|
+
* shapes this cluster has: `monitoring@pve!exporter` (no comment), `tofu@pve!apply` and
|
|
110
|
+
* `sablier@pve!sablier` (commented), and a live OpenBao lease under `hf-read@pve` (a real
|
|
111
|
+
* non-zero `expire`, offered as both `1789327175` and `"1789327175"` since `int` must not care
|
|
112
|
+
* which). All four answer noop; flipping `privsep` answers update; an empty body answers
|
|
113
|
+
* absent. ⚠️ THAT IS THE COMPARISON, NOT THE ENGINE — no `alchemy plan` was run against this
|
|
114
|
+
* family, and nothing here was written to the cluster.
|
|
115
|
+
*/
|
|
116
|
+
matches: (attributes, props) =>
|
|
117
|
+
attributes.comment === (props.comment ?? '') &&
|
|
118
|
+
attributes.expire === props.expire &&
|
|
119
|
+
attributes.privsep === props.privsep,
|
|
120
|
+
path: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
|
|
121
|
+
updateForm: shape,
|
|
122
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.ApiToken` — a PVE API token: the thing a service actually authenticates as.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THE SECRET EXISTS FOR ONE HTTP RESPONSE AND THEN NOWHERE. MEASURED from the cluster's own
|
|
5
|
+
* schema (`/usr/share/pve-docs/api-viewer/apidoc.js` on n2, 2026-09-13): POST returns
|
|
6
|
+
* `["full-tokenid","info","value"]`; PUT returns `["comment","expire","full-tokenid","privsep",
|
|
7
|
+
* "value"]` with `value` present ONLY when `regenerate` was set; GET returns
|
|
8
|
+
* `["comment","expire","privsep"]` and never the secret. A live token agrees —
|
|
9
|
+
* `GET /access/users/monitoring@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
|
|
10
|
+
* it plainly on the POST: the value "needs to be stored as it cannot be retrieved afterwards".
|
|
11
|
+
*
|
|
12
|
+
* ⛔ SO A TOKEN THIS RESOURCE CREATES IS UNUSABLE, AND SAYING SO IS THE POINT OF THIS BLOCK. The
|
|
13
|
+
* secret may not become an attribute: Alchemy persists attributes UNENCRYPTED, and this estate's
|
|
14
|
+
* state store is a Postgres dumped nightly (the ⛔ in credentials.ts). So `reconcile` drops the
|
|
15
|
+
* value on the floor, and what lands is a live credential in `/etc/pve/user.cfg` whose secret
|
|
16
|
+
* nobody holds. No `regenerate` rescues it, because a regenerated value is discarded the same
|
|
17
|
+
* way. The only escape is an operator who captured the value out of band — which this provider
|
|
18
|
+
* gives them no way to do.
|
|
19
|
+
* ★ SO POINT THIS FAMILY AT TOKENS THAT ALREADY EXIST. `comment`, `expire` and `privsep` are the
|
|
20
|
+
* whole of a token's policy, and declaring them is real work: it is how `expire` stops being
|
|
21
|
+
* whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
|
|
22
|
+
* `proxmox-tb4` mount, or a human at `pveum user token add`.
|
|
23
|
+
* ⚠️ THE CREATE PATH IS LEFT REACHABLE RATHER THAN STUBBED. A create that silently did nothing
|
|
24
|
+
* would be the same lie as the stubbed delete resource.ts's ★ refuses; the honest arrangement
|
|
25
|
+
* is a create that works and a header that says what it produces.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ THERE IS NO `regenerate` PROP, AND IT IS NOT AN OVERSIGHT. PVE's own description: "All users
|
|
28
|
+
* of the previous secret will lose access after this operation." A prop for it would revoke a
|
|
29
|
+
* live credential during a deploy whose plan said `update`, and hand back a replacement this
|
|
30
|
+
* provider is obliged to throw away — breakage with no recovery. Rotation belongs to the mount
|
|
31
|
+
* that owns the lease, or to a human who is watching.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ THERE IS A SECOND WRITER TO THESE OBJECTS AND IT IS THE ONE THIS PROVIDER RUNS ON. OpenBao's
|
|
34
|
+
* `proxmox-tb4` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` — read from
|
|
35
|
+
* `house/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
|
|
36
|
+
* to the lease deadline, and an id the plugin CHOOSES,
|
|
37
|
+
* `hf-<role>-<actor>-<entity6>-<stamp>-<nonce>` (tokenname.go). Revocation and WAL rollback look
|
|
38
|
+
* up that one exact id (wal.go); neither sweeps a prefix. So the two systems cannot collide on
|
|
39
|
+
* one object by accident — but they share the USER: `hf-read@pve` held three live leases when
|
|
40
|
+
* this was written, and `Proxmox.User` deleting that account takes every one of them with it.
|
|
41
|
+
* ⛔ DO NOT DECLARE A TOKEN UNDER A MINT USER. `expire` is what makes that dangerous rather than
|
|
42
|
+
* untidy: point this resource at a lease token with the wrong expire and a 300-second
|
|
43
|
+
* credential becomes a permanent one — and on `hf-provision@pve` that credential carries
|
|
44
|
+
* `Permissions.Modify` at `/`. docs/privileges.md records what those 27 privileges buy.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ PRIVILEGES, AND THE READ LANE CANNOT DO IT. MEASURED: all four methods on this path check
|
|
47
|
+
* `['or', ['userid-param','self'], ['userid-group', ['User.Modify']]]` — with no `Sys.Audit`
|
|
48
|
+
* alternative, unlike `GET /access/users/{userid}`, which has one. And
|
|
49
|
+
* `pvesh get /access/permissions --userid hf-read@pve --path /access/groups` answers the seven
|
|
50
|
+
* PVEAuditor audit privileges and no `User.Modify`, while `hf-provision@pve` has it. So the
|
|
51
|
+
* 3600s read lease reaches at most its OWN account's tokens and is refused for every other
|
|
52
|
+
* userid — which `read` in resource.ts folds into "absent", so the plan says create and the POST
|
|
53
|
+
* then fails with "Token already exists". Hence `readRole: 'provision'` below, for the same
|
|
54
|
+
* reason storage.ts, sdn-zone.ts and sdn-vnet.ts set it, at the same cost: every plan mints a
|
|
55
|
+
* 300s non-renewable provision lease just to read.
|
|
56
|
+
*/
|
|
57
|
+
import { Resource } from 'alchemy';
|
|
58
|
+
import * as Provider from 'alchemy/Provider';
|
|
59
|
+
import * as Effect from 'effect/Effect';
|
|
60
|
+
import { apiTokenSpec } from './api-token-form.ts';
|
|
61
|
+
import { type PveRequirements, type WithTarget, pveHandlers, pveOperations } from './resource.ts';
|
|
62
|
+
|
|
63
|
+
export interface ApiTokenProps extends WithTarget {
|
|
64
|
+
/**
|
|
65
|
+
* The account that owns the token, realm-qualified: `tofu@pve`, `root@pam`. Identity.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ THE ACCOUNT MUST ALREADY EXIST. Every method here runs `check_user_exist` first, so a
|
|
68
|
+
* token named under a missing user fails the READ (folded to "absent") and then fails the
|
|
69
|
+
* create with "no such user" — the honest error, arriving one step after the misleading plan.
|
|
70
|
+
* Sequence a `Proxmox.User` ahead of it and the ordering takes care of itself.
|
|
71
|
+
*/
|
|
72
|
+
userid: string;
|
|
73
|
+
/**
|
|
74
|
+
* The token's own name, unique within the account. Identity.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ PVE HAS NO RENAME, AND HERE THAT COSTS MORE THAN IT DOES FOR A USER. Editing this prop (or
|
|
77
|
+
* `userid`) points `path` at a DIFFERENT token, which reads as absent and is then created —
|
|
78
|
+
* leaving the old token LIVE, with its secret still working, invisible to the plan, while the
|
|
79
|
+
* new one is the unusable kind described at the top. Rename by declaring a delete and a
|
|
80
|
+
* create, the way user.ts says for an account.
|
|
81
|
+
* ⛔ `diff` IS NOT OVERRIDDEN TO SAY `replace` THE WAY acl.ts DOES, AND THE DIFFERENCE IS THE
|
|
82
|
+
* OBJECT. There, replace removes a grant that can be rebuilt from its own tuple; here it
|
|
83
|
+
* would DELETE a live token — destroying an irrecoverable secret, and everything using it —
|
|
84
|
+
* to tidy up a name. Leaving the old token standing is the lesser harm, and saying so here
|
|
85
|
+
* is what keeps it from looking like an omission.
|
|
86
|
+
* ⚠️ PATTERN `[A-Za-z][A-Za-z0-9.\-_]+`: a letter first, two characters minimum. The schema
|
|
87
|
+
* declares no maxLength; the OpenBao plugin assumes a conservative 64 rather than finding the
|
|
88
|
+
* real limit in production, and so should anything else.
|
|
89
|
+
*/
|
|
90
|
+
tokenid: string;
|
|
91
|
+
/**
|
|
92
|
+
* Free text in `pveum user token list` and the UI. Empty is how it is cleared — see `shape`.
|
|
93
|
+
* ⚠️ ANY CHARACTER IS SAFE HERE, MEASURED, and it was worth checking: `user.cfg` is a
|
|
94
|
+
* colon-delimited line, so a comment holding a colon or a newline is the obvious place for a
|
|
95
|
+
* round-trip to lose a character and diff forever. PVE escapes it — `encode_text` turns
|
|
96
|
+
* `a:b\nc%d` into `a%3Ab%0Ac%25d` and `decode_text` gives it back byte for byte.
|
|
97
|
+
*/
|
|
98
|
+
comment?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Seconds since the epoch, or 0 for "never expires".
|
|
101
|
+
*
|
|
102
|
+
* ⛔ REQUIRED, AND THE SCHEMA WILL TELL YOU IT NEED NOT BE. It declares the default "same as
|
|
103
|
+
* user", and the code never implements it: `generate_token` writes `expire` only when the
|
|
104
|
+
* parameter is defined, `user.cfg` stores a token as `token:<id>:<expire>:<privsep>:<comment>:`
|
|
105
|
+
* with no absent state, and the parser does `$expire = 0 if !$expire` (AccessControl.pm:1624).
|
|
106
|
+
* MEASURED end to end: every token on this cluster that was created without an expire reads
|
|
107
|
+
* back `"expire":0`. So "same as user" is documentation of an intention, not behaviour.
|
|
108
|
+
* ⛔ WHICH IS WHY IT IS NOT OPTIONAL-DEFAULTING-TO-0. That spelling reads identically and is far
|
|
109
|
+
* worse: an omitted `expire` would then quietly WRITE 0 over a live deadline, turning an
|
|
110
|
+
* expiring credential into a permanent one, and the plan would call it an update. Requiring
|
|
111
|
+
* the field makes "this token never expires" a sentence somebody had to type.
|
|
112
|
+
*/
|
|
113
|
+
expire: number;
|
|
114
|
+
/**
|
|
115
|
+
* True keeps the token's privileges separate from its owner's — it gets NOTHING until an ACL
|
|
116
|
+
* names `fullTokenid`. False gives it the owner's privileges entire.
|
|
117
|
+
*
|
|
118
|
+
* ⛔ REQUIRED FOR THE SAME REASON AS `expire`, AND THE BLAST RADIUS IS LARGER. PVE's API default
|
|
119
|
+
* is 1, every token on this cluster is 0 (measured), and `user.cfg` materialises it either way
|
|
120
|
+
* — `$privsep = $privsep ? 1 : 0` (AccessControl.pm:1622), so there is no unset state to
|
|
121
|
+
* preserve. Were this optional, adopting a live token without mentioning `privsep` would plan
|
|
122
|
+
* an update to 1 and STRIP a working credential of every privilege it has, silently: PVE
|
|
123
|
+
* returns 401/403 to the service, nothing errors here, and `tofu@pve!apply` simply stops
|
|
124
|
+
* working. Required, so adopting a token is a sentence that states what it is.
|
|
125
|
+
* ⚠️ AND `false` IS NOT A SHRUG. It is a token with its owner's whole privilege set; under a
|
|
126
|
+
* provisioning account that is the account's full authority with a separate secret.
|
|
127
|
+
*/
|
|
128
|
+
privsep: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ApiTokenAttributes {
|
|
132
|
+
userid: string;
|
|
133
|
+
tokenid: string;
|
|
134
|
+
/** `<userid>!<tokenid>` — what `Proxmox.Acl` binds as its `ugid`. Derived; never compared. */
|
|
135
|
+
fullTokenid: string;
|
|
136
|
+
comment: string;
|
|
137
|
+
expire: number;
|
|
138
|
+
privsep: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* ⛔ THERE IS NO `value` FIELD HERE AND THERE NEVER MAY BE. Adding one would write a working PVE
|
|
141
|
+
* credential, in clear, into the state Postgres and into every nightly dump of it — the exact
|
|
142
|
+
* leak metric-server.ts types its `token` as `never` to prevent. The read cannot supply one
|
|
143
|
+
* anyway; only create and regenerate can, and both of those responses are dropped.
|
|
144
|
+
*/
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ProxmoxApiToken extends Resource<
|
|
148
|
+
'Proxmox.ApiToken',
|
|
149
|
+
ApiTokenProps,
|
|
150
|
+
ApiTokenAttributes,
|
|
151
|
+
never,
|
|
152
|
+
PveRequirements
|
|
153
|
+
> {}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* ★ `retain` BY DEFAULT, AND THIS IS THE PLAINEST CASE FOR IT IN THE PACKAGE. A token's contents
|
|
157
|
+
* are one irreplaceable secret: delete it and the value is gone, a replacement is a DIFFERENT
|
|
158
|
+
* value, and every holder loses access the instant `cfs_write_file` returns — with no error
|
|
159
|
+
* raised anywhere near them. The live cluster's tokens are `monitoring@pve!exporter` (the PVE
|
|
160
|
+
* exporter feeding VictoriaMetrics), `mcp@pve!executor`, `tofu@pve!apply`, `tofu@pve!ro`,
|
|
161
|
+
* `sablier@pve!sablier` and `vaultmint@pve!engine` — the last being the parent credential the
|
|
162
|
+
* OpenBao mount itself authenticates with, so orphaning that one would stop every plan in this
|
|
163
|
+
* package, this resource included. `delete` is FULLY IMPLEMENTED (DELETE is a real method on
|
|
164
|
+
* this path, measured) and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
165
|
+
* See the ★ in resource.ts, which explains the convention once.
|
|
166
|
+
*/
|
|
167
|
+
export const ProxmoxApiToken = Resource<ProxmoxApiToken>('Proxmox.ApiToken', {
|
|
168
|
+
defaultRemovalPolicy: 'retain',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const ops = pveOperations(apiTokenSpec);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ⛔ THIS FAMILY ADOPTS AND MANAGES TOKENS; IT REFUSES TO MINT ONE, AND THAT IS A DECISION RATHER
|
|
175
|
+
* THAN A GAP. The secret exists ONLY in the create response — MEASURED from the schema: `POST`
|
|
176
|
+
* returns `["full-tokenid","info","value"]`, where `value` is "API token value used for
|
|
177
|
+
* authentication", while `GET` returns `["comment","expire","privsep"]` and never the secret.
|
|
178
|
+
* It may not become an attribute, because Alchemy writes attributes to its state store
|
|
179
|
+
* UNENCRYPTED into a Postgres that is dumped nightly.
|
|
180
|
+
*
|
|
181
|
+
* So a token created here would be A LIVE CREDENTIAL ON THE CLUSTER THAT NOBODY HOLDS: valid,
|
|
182
|
+
* privileged, unusable, and indistinguishable from one somebody meant to keep. Regenerating does
|
|
183
|
+
* not rescue it — a `regenerate` PUT returns the new value down the same discarded path. So
|
|
184
|
+
* `reconcile` refuses by name and says what to do instead.
|
|
185
|
+
*
|
|
186
|
+
* ★ EVERYTHING ELSE STILL WORKS, AND IT IS THE HALF WORTH HAVING. Adopting an existing token and
|
|
187
|
+
* converging its `comment`, `expire` and `privsep` are real operations: TB4 carries SEVEN tokens
|
|
188
|
+
* with `expire=0` and `privsep=0` made by clicks nobody recorded. Declaring those freezes the
|
|
189
|
+
* set, and an eighth appearing shows up as drift.
|
|
190
|
+
*
|
|
191
|
+
* ⚠️ IF DECLARATIVE MINTING IS EVER WANTED, the missing piece is a secret SINK — write the value
|
|
192
|
+
* straight into an OpenBao kv path and return only its address. That is a different resource
|
|
193
|
+
* with a different contract; it must not be bolted onto this one.
|
|
194
|
+
*/
|
|
195
|
+
const handlers = {
|
|
196
|
+
...pveHandlers(apiTokenSpec),
|
|
197
|
+
reconcile: Effect.fn(function* ({ news }: { news: ApiTokenProps }) {
|
|
198
|
+
const live = yield* ops.read(news);
|
|
199
|
+
if (live === undefined) {
|
|
200
|
+
return yield* Effect.die(
|
|
201
|
+
new Error(
|
|
202
|
+
`${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE returns ` +
|
|
203
|
+
'the secret only in the create response and it cannot be stored, so a token made here ' +
|
|
204
|
+
'would be a live credential nobody holds. Create it with `pveum user token add` and ' +
|
|
205
|
+
'capture the value, or mint a short-lived one from the OpenBao proxmox mount ' +
|
|
206
|
+
'(`bao read proxmox-tb4/creds/<role>`) -- then declare it here to manage it.',
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return yield* ops.reconcile(news);
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT IN
|
|
216
|
+
* resource.ts. A token index handed to Alchemy would offer up `vaultmint@pve!engine` and every
|
|
217
|
+
* live OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
|
|
218
|
+
* The mount's leases in particular appear and vanish on their own; anything that adopted one
|
|
219
|
+
* would report drift against a credential that was never its to hold.
|
|
220
|
+
*/
|
|
221
|
+
export const ProxmoxApiTokenProvider = () =>
|
|
222
|
+
Provider.effect(ProxmoxApiToken, Effect.succeed(ProxmoxApiToken.Provider.of(handlers)));
|