@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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenBao calls Bao.CloudflareRole and the live-roles bootstrap make — read, write and delete
|
|
3
|
+
* of `<mount>/roles/<name>`. Split out so each can be tested against fake-bao.
|
|
4
|
+
*/
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
7
|
+
import { baoDelete, baoRead, baoWrite } from './bao-http.ts';
|
|
8
|
+
import type { BaoError } from './bao-status.ts';
|
|
9
|
+
import { type LiveCloudflareRole, liveRoleOf, rolePath } from './cloudflare-role-form.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The live role, or undefined when the engine answers 404.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ A MISSING MOUNT AND A MISSING ROLE BOTH READ AS UNDEFINED — both are a 404 from OpenBao, the
|
|
15
|
+
* first as "no handler for route". That is proxmox-role.ts's ⚠️ in full: the WRITE is what tells
|
|
16
|
+
* them apart, and it lets OpenBao's own `errors` through. A 403 or 503 is never absence
|
|
17
|
+
* (bao-status.ts).
|
|
18
|
+
*/
|
|
19
|
+
export const readCloudflareRole = (
|
|
20
|
+
mount: string,
|
|
21
|
+
name: string,
|
|
22
|
+
): Effect.Effect<LiveCloudflareRole | undefined, BaoError, HttpClient.HttpClient> =>
|
|
23
|
+
Effect.map(baoRead(rolePath(mount, name)), (data) =>
|
|
24
|
+
data === undefined ? undefined : liveRoleOf(data),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** ★ `PUT`, which is what `bao write` sent (bao-http.ts). A 404 here fails — see readCloudflareRole. */
|
|
28
|
+
export const writeCloudflareRole = (
|
|
29
|
+
mount: string,
|
|
30
|
+
name: string,
|
|
31
|
+
body: Readonly<Record<string, string>>,
|
|
32
|
+
) => baoWrite('PUT', rolePath(mount, name), body);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Idempotent as Alchemy requires — already gone (404) is success, a refusal is not.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ THE ENGINE'S DELETE IS ONE STORAGE DELETE (path_roles.go:214-216). Tokens the role already
|
|
38
|
+
* minted are leases, and they live to their own expiry.
|
|
39
|
+
*/
|
|
40
|
+
export const deleteCloudflareRole = (mount: string, name: string) =>
|
|
41
|
+
baoDelete(rolePath(mount, name));
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A role on one of the six Cloudflare secrets-engine mounts — what one consumer may mint: the
|
|
3
|
+
* Cloudflare policy its tokens carry and the lease bounds they get. METADATA ONLY.
|
|
4
|
+
*
|
|
5
|
+
* ★ THE PORT OF apply-roles.py's WRITE. roles.yaml expands to these (cloudflare-roles-expand.ts),
|
|
6
|
+
* and what used to be `bao write <mount>/roles/<name> ttl= max_ttl= description= policies=` is
|
|
7
|
+
* reconcile below. The props name permission groups; CloudflarePermissionGroups turns them into
|
|
8
|
+
* IDs at diff and reconcile time.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ NO SECRET IN PROPS OR ATTRIBUTES — see cloudflare-role-form.ts. ⛔ AND ISSUANCE IS NOT THIS
|
|
11
|
+
* RESOURCE: `<mount>/creds/<name>` returns a LIVE Cloudflare token, and a token declared as a
|
|
12
|
+
* resource would outlive its lease in Alchemy's unencrypted state (proxmox-role.ts has the four
|
|
13
|
+
* places it would land). This declares the door; minting is a runtime call.
|
|
14
|
+
*
|
|
15
|
+
* ★ `defaultRemovalPolicy: 'retain'` — deleting a role breaks every consumer that mints from it,
|
|
16
|
+
* and apply-roles.py never deleted one either: an unexplained role was reported UNTRACKED and
|
|
17
|
+
* left alone (apply-roles.py:190-196). Opt in with `.pipe(RemovalPolicy.destroy())`.
|
|
18
|
+
*/
|
|
19
|
+
import { Resource } from 'alchemy';
|
|
20
|
+
import { isResolved } from 'alchemy/Diff';
|
|
21
|
+
import * as Provider from 'alchemy/Provider';
|
|
22
|
+
import * as Effect from 'effect/Effect';
|
|
23
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
24
|
+
import { groupKey, scopeOfResource } from './cloudflare-group-scope.ts';
|
|
25
|
+
import { CloudflarePermissionGroups, type GroupRef } from './cloudflare-permission-groups.ts';
|
|
26
|
+
import { resolvePolicies } from './cloudflare-policy.ts';
|
|
27
|
+
import {
|
|
28
|
+
type BaoCloudflareRoleAttributes,
|
|
29
|
+
type BaoCloudflareRoleProps,
|
|
30
|
+
attributesOf,
|
|
31
|
+
differences,
|
|
32
|
+
refusalOf,
|
|
33
|
+
rolePath,
|
|
34
|
+
writeBody,
|
|
35
|
+
} from './cloudflare-role-form.ts';
|
|
36
|
+
import {
|
|
37
|
+
deleteCloudflareRole,
|
|
38
|
+
readCloudflareRole,
|
|
39
|
+
writeCloudflareRole,
|
|
40
|
+
} from './cloudflare-role-wire.ts';
|
|
41
|
+
|
|
42
|
+
export type { BaoCloudflareRoleAttributes, BaoCloudflareRoleProps };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ⚠️ CloudflarePermissionGroups IS NOT A REQUIREMENT OF THE RESOURCE. The provider takes it once, when
|
|
46
|
+
* it is built (`yield* CloudflarePermissionGroups` below), and alchemy.run.ts supplies it to the
|
|
47
|
+
* provider layer with `Layer.provide`. Listing it here as well made it a requirement of the STACK
|
|
48
|
+
* body, which Alchemy only lets name its own provider and stack services — tsc refused the wiring.
|
|
49
|
+
*/
|
|
50
|
+
export interface BaoCloudflareRole extends Resource<
|
|
51
|
+
'Bao.CloudflareRole',
|
|
52
|
+
BaoCloudflareRoleProps,
|
|
53
|
+
BaoCloudflareRoleAttributes,
|
|
54
|
+
never,
|
|
55
|
+
HttpClient.HttpClient
|
|
56
|
+
> {}
|
|
57
|
+
|
|
58
|
+
export const BaoCloudflareRole = Resource<BaoCloudflareRole>('Bao.CloudflareRole', {
|
|
59
|
+
defaultRemovalPolicy: 'retain',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const BaoCloudflareRoleProvider = () =>
|
|
63
|
+
Provider.effect(
|
|
64
|
+
BaoCloudflareRole,
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const groups = yield* CloudflarePermissionGroups;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The declaration with IDs. ⛔ A name that does not resolve fails here — never a guess.
|
|
70
|
+
*
|
|
71
|
+
* ⛔ ONE REF PER (NAME, SCOPE), NOT PER NAME. The entry's resource is what says which scope
|
|
72
|
+
* its groups are gated at, so a role whose zone entry and account entry name one group asks
|
|
73
|
+
* for both ids; deduplicating by name alone would ask for one and use it for both.
|
|
74
|
+
*/
|
|
75
|
+
const resolved = (props: BaoCloudflareRoleProps) =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const refs = new Map<string, GroupRef>();
|
|
78
|
+
for (const policy of props.policies) {
|
|
79
|
+
/** ⛔ THE SAME PREFERENCE resolvePolicies MAKES — ask for the id the entry will use. */
|
|
80
|
+
const scope = policy.scope ?? scopeOfResource(policy.resource);
|
|
81
|
+
for (const name of policy.groups) refs.set(groupKey(name, scope), { name, scope });
|
|
82
|
+
}
|
|
83
|
+
const ids = yield* groups.resolve(props.mount, [...refs.values()]);
|
|
84
|
+
const { missing, policies } = resolvePolicies(props.policies, ids);
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
return yield* Effect.die(
|
|
87
|
+
new Error(
|
|
88
|
+
`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` +
|
|
89
|
+
`${missing.join(', ')} came back unresolved from a lookup that reported success.`,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return policies;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return BaoCloudflareRole.Provider.of({
|
|
97
|
+
/**
|
|
98
|
+
* ⛔ `<mount>/roles` IS NOT A LIST OF THINGS THIS OWNS. MEASURED 2026-09-14: 586 live roles,
|
|
99
|
+
* 585 of which roles.yaml declares — and the 586th, cloudflare-<account>-platform/
|
|
100
|
+
* alchemy-os-create, is exactly the role nobody has decided about. Returning it would
|
|
101
|
+
* invite Alchemy to adopt, then delete, a role that may be someone's live credential.
|
|
102
|
+
*/
|
|
103
|
+
list: () => Effect.succeed([]),
|
|
104
|
+
|
|
105
|
+
read: Effect.fn(function* ({ olds }) {
|
|
106
|
+
const live = yield* readCloudflareRole(olds.mount, olds.name);
|
|
107
|
+
return live === undefined ? undefined : attributesOf(olds, live);
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. A policy widened by hand in the
|
|
112
|
+
* OpenBao UI changes what every future token can do while the digest in Postgres still
|
|
113
|
+
* says everything is fine.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ NO `replace`, EVEN FOR A NAME OR MOUNT CHANGE. A different path is a different object,
|
|
116
|
+
* so diff reads the new path, finds it absent and plans `update`; reconcile creates it and
|
|
117
|
+
* the old role is left live, retained and unmanaged — the same outcome apply-roles.py's
|
|
118
|
+
* UNTRACKED report had. declareCloudflareRoles puts mount and name in the resource id, so
|
|
119
|
+
* a rename there is a new resource anyway.
|
|
120
|
+
*/
|
|
121
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
122
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
123
|
+
/**
|
|
124
|
+
* ⚠️ A DECLARATION reconcile WOULD REFUSE MUST NEVER PLAN AS noop — route it to reconcile,
|
|
125
|
+
* which says why (pki-role.ts has the same guard).
|
|
126
|
+
*/
|
|
127
|
+
if (refusalOf(news) !== undefined) return { action: 'update' } as const;
|
|
128
|
+
const policies = yield* resolved(news);
|
|
129
|
+
const live = yield* readCloudflareRole(news.mount, news.name);
|
|
130
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
131
|
+
return differences(news, policies, live).length === 0
|
|
132
|
+
? ({ action: 'noop' } as const)
|
|
133
|
+
: ({ action: 'update' } as const);
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
137
|
+
const path = rolePath(news.mount, news.name);
|
|
138
|
+
const refusal = refusalOf(news);
|
|
139
|
+
if (refusal !== undefined) {
|
|
140
|
+
return yield* Effect.die(new Error(`Bao.CloudflareRole ${path}: ${refusal}.`));
|
|
141
|
+
}
|
|
142
|
+
const policies = yield* resolved(news);
|
|
143
|
+
const live = yield* readCloudflareRole(news.mount, news.name);
|
|
144
|
+
/**
|
|
145
|
+
* ⛔ ADOPTING A ROLE THAT ALREADY MATCHES MUST NOT REWRITE IT. Alchemy's `adopted` action
|
|
146
|
+
* routes through reconcile (policy.ts has the Apply.ts reading), so without this guard
|
|
147
|
+
* the first deploy would PUT all 585 roles — byte-identical in the happy case, and a
|
|
148
|
+
* silent rewrite of live grants the moment the port disagreed with production.
|
|
149
|
+
*/
|
|
150
|
+
if (live === undefined || differences(news, policies, live).length > 0) {
|
|
151
|
+
yield* writeCloudflareRole(news.mount, news.name, writeBody(news, policies));
|
|
152
|
+
}
|
|
153
|
+
const after = yield* readCloudflareRole(news.mount, news.name);
|
|
154
|
+
if (after === undefined) {
|
|
155
|
+
return yield* Effect.die(
|
|
156
|
+
new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* ⛔ THE WRITE IS NOT TRUSTED, IT IS RE-READ. The engine re-marshals `policies` from structs
|
|
161
|
+
* (path_roles.go:199-207), so a document it stored differently from what was sent — a
|
|
162
|
+
* dropped group, a merged resource — shows up here as a named field, not as a green
|
|
163
|
+
* deploy followed by a 403 at mint time.
|
|
164
|
+
*/
|
|
165
|
+
const left = differences(news, policies, after);
|
|
166
|
+
if (left.length > 0) {
|
|
167
|
+
return yield* Effect.die(
|
|
168
|
+
new Error(
|
|
169
|
+
`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` +
|
|
170
|
+
`${left.map((d) => d.field).join(', ')}. Inspect the role before retrying.`,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return attributesOf(news, after);
|
|
175
|
+
}),
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ⛔ DELETING A ROLE DOES NOT REVOKE THE TOKENS IT MINTED. They are leases on Cloudflare API
|
|
179
|
+
* tokens and live to their own expiry, so this is an availability change for every
|
|
180
|
+
* consumer of `creds/<name>`, never a containment action — to contain a leak, revoke the
|
|
181
|
+
* lease. Idempotent: already gone is success (bao-status.ts).
|
|
182
|
+
*/
|
|
183
|
+
delete: Effect.fn(function* ({ output }) {
|
|
184
|
+
yield* deleteCloudflareRole(output.mount, output.name);
|
|
185
|
+
return undefined;
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roles.yaml, parsed and checked — the input half of the Cloudflare role expansion. The expansion
|
|
3
|
+
* itself is cloudflare-roles-expand.ts.
|
|
4
|
+
*
|
|
5
|
+
* ★ BUN's OWN PARSER, NO DEPENDENCY. `Bun.YAML.parse` ships in Bun 1.4 — MEASURED on 1.4.0:
|
|
6
|
+
* `typeof Bun.YAML` is `object` with `parse` and `stringify`, typed in bun-types 1.4.2
|
|
7
|
+
* (bun.d.ts:1479). The Python read the same file with PyYAML's `safe_load` (cfroles.py:41-43).
|
|
8
|
+
* ⚠️ THEY ARE DIFFERENT PARSERS, SO THE SHAPE IS CHECKED, NOT TRUSTED. The file leans on flow
|
|
9
|
+
* mappings (`- { name: dns-edit, groups: [DNS Write] }`), and a bare `5m` must stay a string.
|
|
10
|
+
* MEASURED against roles.yaml: both parsers give `ttl` as the string `5m`, and both keep
|
|
11
|
+
* `'Access: Apps and Policies Write'` as one name. parity.ts against the live snapshot is the
|
|
12
|
+
* end-to-end proof; a field of the wrong type fails loudly here instead of expanding to nonsense.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ A MISSING FIELD IS AN ERROR, NEVER A DEFAULT — except exactly where the Python defaulted:
|
|
15
|
+
* `desc` to '' (cfroles.py:86, :100), an account's `zones` to "all" (:57), `surfaces`,
|
|
16
|
+
* `zone_roles` and `account_roles` to empty (:59, :62, :96), `zone_types` to empty (:74) and
|
|
17
|
+
* `account_groups` to empty (:90). Everywhere else the Python raised KeyError, and so does this.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ `consumers:` IS NOT READ HERE. It belongs to apply-consumers.py, and declaring it is the next
|
|
20
|
+
* phase — see the report that came with this file.
|
|
21
|
+
*/
|
|
22
|
+
import { GROUP_SCOPES, groupScopeNamed } from './cloudflare-group-scope.ts';
|
|
23
|
+
|
|
24
|
+
/** One `zone_roles` or `account_roles` entry. */
|
|
25
|
+
export interface RoleTemplate {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly groups: readonly string[];
|
|
28
|
+
/** Duration text exactly as written (`5m`), which is what apply-roles.py sent. */
|
|
29
|
+
readonly ttl: string;
|
|
30
|
+
readonly maxTtl: string;
|
|
31
|
+
readonly desc: string;
|
|
32
|
+
/** `zones:` narrowing. Undefined or empty narrows nothing — `if only and …` (cfroles.py:78). */
|
|
33
|
+
readonly zones: readonly string[] | undefined;
|
|
34
|
+
readonly zoneTypes: readonly string[];
|
|
35
|
+
/** Account-scoped groups on a ZONE role. Ignored on account roles, as cfroles.py:101 ignores it. */
|
|
36
|
+
readonly accountGroups: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* One R2 bucket NAME, narrowing an account role's resource to that bucket alone.
|
|
39
|
+
*
|
|
40
|
+
* ⛔ BUCKET IS THE FINEST GRAIN R2 HAS — A PREFIX CANNOT BE SCOPED. So one repository per
|
|
41
|
+
* instance in its own bucket is the only least-privilege shape for a backup credential, and
|
|
42
|
+
* this field is what expresses it. Undefined leaves the role account-wide, which is what every
|
|
43
|
+
* role declared before 2026-09-15 is.
|
|
44
|
+
* ⚠️ IT ONLY MEANS ANYTHING WITH THE `Bucket Item` GROUPS. `Workers R2 Storage Write` is gated at
|
|
45
|
+
* the ACCOUNT scope, so pairing it with a bucket resource is a token Cloudflare will mint and
|
|
46
|
+
* that will then be refused on every call. roles.yaml says which pair goes with which.
|
|
47
|
+
*/
|
|
48
|
+
readonly bucket: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The bucket's jurisdiction, which is part of its resource id and not cosmetic — a bucket
|
|
51
|
+
* created in `default` is a different resource from the same name under `eu`. Defaults to
|
|
52
|
+
* `default`, which is what every bucket in this estate is (measured: `r2_bucket_get` reports
|
|
53
|
+
* `jurisdiction default` on all 34).
|
|
54
|
+
*/
|
|
55
|
+
readonly bucketJurisdiction: string;
|
|
56
|
+
/**
|
|
57
|
+
* roles.yaml `groups_scope:` — the scope this template's `groups:` NAMES resolve at, as a
|
|
58
|
+
* GROUP_SCOPES value. Undefined means the entry's own resource namespace, which is every
|
|
59
|
+
* template written before 2026-09-15.
|
|
60
|
+
*
|
|
61
|
+
* ⛔ IT SAYS WHICH ID, NOT WHICH RESOURCE. `groups_scope: zone` on an ACCOUNT role keeps the
|
|
62
|
+
* account resource and takes the zone-scoped id of each name — Cloudflare's "all zones from
|
|
63
|
+
* this account" (cf-mint-analytics-tokens.py:155-158). It is meaningful only for the seven
|
|
64
|
+
* names the engine lists at two scopes; anywhere else the name has one id and answers alike.
|
|
65
|
+
*/
|
|
66
|
+
readonly groupsScope: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SurfaceSpec {
|
|
70
|
+
readonly zoneRoles: readonly RoleTemplate[];
|
|
71
|
+
readonly accountRoles: readonly RoleTemplate[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AccountSpec {
|
|
75
|
+
/** The key under `accounts:` — the `<account>` in `cloudflare-<account>-<surface>`. */
|
|
76
|
+
readonly key: string;
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly zones: 'all' | readonly string[];
|
|
79
|
+
readonly surfaces: readonly string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RolesConfig {
|
|
83
|
+
readonly namespace: string;
|
|
84
|
+
readonly surfaces: Readonly<Record<string, SurfaceSpec>>;
|
|
85
|
+
/** In file order, which is the order apply-roles.py walked them (apply-roles.py:111). */
|
|
86
|
+
readonly accounts: readonly AccountSpec[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class RolesConfigError extends Error {
|
|
90
|
+
constructor(where: string, problem: string) {
|
|
91
|
+
super(`roles.yaml ${where}: ${problem}`);
|
|
92
|
+
this.name = 'RolesConfigError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type Raw = Readonly<Record<string, unknown>>;
|
|
97
|
+
|
|
98
|
+
const isRaw = (value: unknown): value is Raw =>
|
|
99
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
100
|
+
|
|
101
|
+
const record = (value: unknown, where: string): Raw => {
|
|
102
|
+
if (!isRaw(value)) throw new RolesConfigError(where, 'expected a mapping');
|
|
103
|
+
return value;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const text = (raw: Raw, key: string, where: string): string => {
|
|
107
|
+
const value = raw[key];
|
|
108
|
+
if (typeof value === 'string') return value;
|
|
109
|
+
/**
|
|
110
|
+
* ⚠️ STRICTER THAN THE PYTHON FOR A NUMBER. `ttl: 300` would have reached `bao write` as `300`
|
|
111
|
+
* and been read as seconds. Every duration in roles.yaml is unit-suffixed, and the comparison
|
|
112
|
+
* in cloudflare-role-form.ts only parses unit-suffixed text, so a bare number is refused here
|
|
113
|
+
* rather than planning an update that can never settle.
|
|
114
|
+
*/
|
|
115
|
+
throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const texts = (raw: Raw, key: string, where: string): readonly string[] | undefined => {
|
|
119
|
+
const value = raw[key];
|
|
120
|
+
if (value === undefined || value === null) return undefined;
|
|
121
|
+
if (Array.isArray(value) && value.every((item): item is string => typeof item === 'string')) {
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
throw new RolesConfigError(`${where}.${key}`, 'expected a list of strings');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const entries = (raw: Raw, key: string, where: string): readonly unknown[] => {
|
|
128
|
+
const value = raw[key];
|
|
129
|
+
if (value === undefined || value === null) return [];
|
|
130
|
+
if (!Array.isArray(value)) throw new RolesConfigError(`${where}.${key}`, 'expected a list');
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const template = (value: unknown, where: string): RoleTemplate => {
|
|
135
|
+
const raw = record(value, where);
|
|
136
|
+
const groups = texts(raw, 'groups', where);
|
|
137
|
+
if (groups === undefined) throw new RolesConfigError(`${where}.groups`, 'is required');
|
|
138
|
+
/**
|
|
139
|
+
* ⚠️ `bucket` IS OPTIONAL BUT NEVER EMPTY. `bucket: ''` would expand to the resource
|
|
140
|
+
* `…r2.bucket.<acct>_default_`, which is not a bucket and not the account either — a policy
|
|
141
|
+
* that names nothing. A missing key means account-wide; a present one must name a bucket.
|
|
142
|
+
*/
|
|
143
|
+
const bucket = raw['bucket'] === undefined ? undefined : text(raw, 'bucket', where);
|
|
144
|
+
if (bucket !== undefined && bucket.length === 0) {
|
|
145
|
+
throw new RolesConfigError(`${where}.bucket`, 'is empty; omit it to stay account-wide');
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* ⛔ AN UNKNOWN WORD IS AN ERROR, NEVER A PASS-THROUGH. A scope roles.yaml invents matches no
|
|
149
|
+
* group at all, so accepting it would turn a typo into "unknown group X at scope zonee" at
|
|
150
|
+
* deploy time — a failure naming the group rather than the line that is wrong.
|
|
151
|
+
*/
|
|
152
|
+
const groupsScope =
|
|
153
|
+
raw['groups_scope'] === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: groupScopeNamed(text(raw, 'groups_scope', where));
|
|
156
|
+
if (raw['groups_scope'] !== undefined && groupsScope === undefined) {
|
|
157
|
+
throw new RolesConfigError(
|
|
158
|
+
`${where}.groups_scope`,
|
|
159
|
+
`expected one of ${Object.keys(GROUP_SCOPES).join(', ')}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
accountGroups: texts(raw, 'account_groups', where) ?? [],
|
|
164
|
+
bucket,
|
|
165
|
+
bucketJurisdiction:
|
|
166
|
+
raw['bucket_jurisdiction'] === undefined
|
|
167
|
+
? 'default'
|
|
168
|
+
: text(raw, 'bucket_jurisdiction', where),
|
|
169
|
+
desc: raw['desc'] === undefined ? '' : text(raw, 'desc', where),
|
|
170
|
+
groups,
|
|
171
|
+
groupsScope,
|
|
172
|
+
maxTtl: text(raw, 'max_ttl', where),
|
|
173
|
+
name: text(raw, 'name', where),
|
|
174
|
+
ttl: text(raw, 'ttl', where),
|
|
175
|
+
zoneTypes: texts(raw, 'zone_types', where) ?? [],
|
|
176
|
+
zones: texts(raw, 'zones', where),
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const surface = (name: string, value: unknown): SurfaceSpec => {
|
|
181
|
+
const where = `surfaces.${name}`;
|
|
182
|
+
const raw = record(value, where);
|
|
183
|
+
return {
|
|
184
|
+
accountRoles: entries(raw, 'account_roles', where).map((item, i) =>
|
|
185
|
+
template(item, `${where}.account_roles[${String(i)}]`),
|
|
186
|
+
),
|
|
187
|
+
zoneRoles: entries(raw, 'zone_roles', where).map((item, i) =>
|
|
188
|
+
template(item, `${where}.zone_roles[${String(i)}]`),
|
|
189
|
+
),
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const account = (
|
|
194
|
+
key: string,
|
|
195
|
+
value: unknown,
|
|
196
|
+
surfaces: Readonly<Record<string, SurfaceSpec>>,
|
|
197
|
+
): AccountSpec => {
|
|
198
|
+
const where = `accounts.${key}`;
|
|
199
|
+
const raw = record(value, where);
|
|
200
|
+
const names = texts(raw, 'surfaces', where) ?? [];
|
|
201
|
+
for (const name of names) {
|
|
202
|
+
/** ⚠️ cfroles.py:60 would raise KeyError at expansion; this says which account named it. */
|
|
203
|
+
if (surfaces[name] === undefined) {
|
|
204
|
+
throw new RolesConfigError(
|
|
205
|
+
`${where}.surfaces`,
|
|
206
|
+
`names ${name}, which surfaces: never defines`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const zones =
|
|
211
|
+
raw['zones'] === undefined || raw['zones'] === 'all' ? 'all' : texts(raw, 'zones', where);
|
|
212
|
+
return { id: text(raw, 'id', where), key, surfaces: names, zones: zones ?? 'all' };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** Parse and check roles.yaml. Throws RolesConfigError naming the first bad field. */
|
|
216
|
+
export const parseRolesConfig = (yaml: string): RolesConfig => {
|
|
217
|
+
const root = record(Bun.YAML.parse(yaml), '(document)');
|
|
218
|
+
const surfaces: Record<string, SurfaceSpec> = {};
|
|
219
|
+
for (const [name, spec] of Object.entries(record(root['surfaces'], 'surfaces'))) {
|
|
220
|
+
surfaces[name] = surface(name, spec);
|
|
221
|
+
}
|
|
222
|
+
const accounts = Object.entries(record(root['accounts'], 'accounts')).map(([key, value]) =>
|
|
223
|
+
account(key, value, surfaces),
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
accounts,
|
|
227
|
+
namespace: text(record(root['defaults'], 'defaults'), 'namespace', 'defaults'),
|
|
228
|
+
surfaces,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 BUCKET-scoped roles: the third resource form, beside `zoneResource` and `accountResource`.
|
|
3
|
+
*
|
|
4
|
+
* ★ ITS OWN FILE, AND ITS OWN INLINE YAML, FOR TWO REASONS. The shared fixture
|
|
5
|
+
* (cloudflare-roles.fixture.yaml) is a miniature of the REAL roles.yaml and every assertion in
|
|
6
|
+
* cloudflare-roles-expand.test.ts counts its roles by name — adding two there moves four
|
|
7
|
+
* unrelated expectations for no gain. And a bucket role needs no zones at all, so the smallest
|
|
8
|
+
* thing that can prove it is three lines of yaml rather than a 64-line fixture.
|
|
9
|
+
* ⚠️ THE SPLIT IS ALSO THE CAP. The shared file is 229 lines against a 250 cap; these cases were
|
|
10
|
+
* written there first and pushed it to 302. AGENTS.md rule 5 says extract, never compress —
|
|
11
|
+
* the comments are the expensive part.
|
|
12
|
+
*/
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { describe, it } from 'node:test';
|
|
15
|
+
import { bucketResource } from './cloudflare-group-scope.ts';
|
|
16
|
+
import { RolesConfigError, parseRolesConfig } from './cloudflare-roles-config.ts';
|
|
17
|
+
import { type CloudflareZone, type ExpandedRole, expandAll } from './cloudflare-roles-expand.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ⚠️ ONE ZONE, AND IT EXISTS ONLY SO `expandAll` HAS AN INVENTORY. An account with no zone
|
|
21
|
+
* inventory is an ERROR there (a missing inventory is not the same as an empty one), so a bucket
|
|
22
|
+
* role — which is an account role and touches no zone — still needs this to be present.
|
|
23
|
+
*/
|
|
24
|
+
const ZONES: Record<string, readonly CloudflareZone[]> = {
|
|
25
|
+
acme: [{ id: 'zone-com', internal: false, name: 'example.com' }],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const yaml = (roles: string) => `
|
|
29
|
+
defaults: { namespace: homeflare }
|
|
30
|
+
surfaces:
|
|
31
|
+
platform:
|
|
32
|
+
${roles}
|
|
33
|
+
accounts:
|
|
34
|
+
acme: { id: acct-acme, zones: all, surfaces: [platform] }
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const ACCOUNT_ROLES = yaml(` account_roles:
|
|
38
|
+
- {
|
|
39
|
+
name: r2-backup-demo,
|
|
40
|
+
groups: [Workers R2 Storage Bucket Item Write, Workers R2 Storage Bucket Item Read],
|
|
41
|
+
bucket: demo-pg-bucket,
|
|
42
|
+
ttl: 12h,
|
|
43
|
+
max_ttl: 12h,
|
|
44
|
+
desc: 'Write one bucket and nothing else.',
|
|
45
|
+
}
|
|
46
|
+
- { name: r2-backup-eu, groups: [Workers R2 Storage Bucket Item Read],
|
|
47
|
+
bucket: demo-eu-bucket, bucket_jurisdiction: eu, ttl: 12h, max_ttl: 12h, desc: 'EU.' }
|
|
48
|
+
- { name: workers-deploy, groups: [Workers Scripts Write], ttl: 30m, max_ttl: 2h, desc: 'W.' }
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
const expanded = expandAll(parseRolesConfig(ACCOUNT_ROLES), ZONES);
|
|
52
|
+
|
|
53
|
+
const role = (name: string): ExpandedRole => {
|
|
54
|
+
const found = expanded.get('cloudflare-acme-platform')?.find((r) => r.name === name);
|
|
55
|
+
assert.ok(found, `${name} was not expanded`);
|
|
56
|
+
return found;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe('R2 bucket-scoped roles', () => {
|
|
60
|
+
/**
|
|
61
|
+
* ⛔ THE UNDERSCORES ARE THE ASSERTION, not the prefix. `com.cloudflare.edge.r2.bucket.` then
|
|
62
|
+
* `<accountId>_<jurisdiction>_<bucket>` as ONE dotted segment is what Cloudflare's R2 token
|
|
63
|
+
* documentation specifies. A dot where an underscore belongs still mints a token; that token
|
|
64
|
+
* matches no bucket and is refused on first use, which is indistinguishable at the call site
|
|
65
|
+
* from a missing permission group. This is the only place that difference is ever checked.
|
|
66
|
+
*/
|
|
67
|
+
it('scopes the policy to one bucket, in the edge.r2 namespace', () => {
|
|
68
|
+
assert.deepEqual(role('r2-backup-demo').policies, [
|
|
69
|
+
{
|
|
70
|
+
effect: 'allow',
|
|
71
|
+
groupOrder: 'declared',
|
|
72
|
+
groups: ['Workers R2 Storage Bucket Item Write', 'Workers R2 Storage Bucket Item Read'],
|
|
73
|
+
resource: 'com.cloudflare.edge.r2.bucket.acct-acme_default_demo-pg-bucket',
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/** ⚠️ THE DEFAULT IS UNCHANGED: every role declared before 2026-09-15 stays account-wide. */
|
|
79
|
+
it('leaves a role with no bucket on the account resource', () => {
|
|
80
|
+
assert.equal(
|
|
81
|
+
role('workers-deploy').policies[0]?.resource,
|
|
82
|
+
'com.cloudflare.api.account.acct-acme',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* ⛔ THE JURISDICTION IS PART OF THE IDENTITY, NOT DECORATION. A bucket of the same name under
|
|
88
|
+
* `eu` is a DIFFERENT resource, so defaulting it silently would hand out a token for a bucket
|
|
89
|
+
* that does not exist. Every bucket in this estate is `default` (measured on all 34), which is
|
|
90
|
+
* exactly why the non-default path needs a test rather than a comment.
|
|
91
|
+
*/
|
|
92
|
+
it('puts the jurisdiction in the middle segment', () => {
|
|
93
|
+
assert.equal(
|
|
94
|
+
role('r2-backup-eu').policies[0]?.resource,
|
|
95
|
+
'com.cloudflare.edge.r2.bucket.acct-acme_eu_demo-eu-bucket',
|
|
96
|
+
);
|
|
97
|
+
assert.equal(bucketResource('a', 'default', 'b'), 'com.cloudflare.edge.r2.bucket.a_default_b');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ⚠️ THE KV CATALOG SHAPE IS DELIBERATELY UNCHANGED. cloudflare-parity-catalog.ts proves the TS
|
|
102
|
+
* catalog against what apply-roles.py published, so a new field here would read as drift on
|
|
103
|
+
* every one of 555 roles. The bucket is stated in `desc` and shown by `--plan` instead.
|
|
104
|
+
*/
|
|
105
|
+
it('does not move the catalog shape', () => {
|
|
106
|
+
assert.deepEqual(role('r2-backup-demo').catalog, {
|
|
107
|
+
description: 'Write one bucket and nothing else.',
|
|
108
|
+
maxTtl: '12h',
|
|
109
|
+
permissions: ['Workers R2 Storage Bucket Item Write', 'Workers R2 Storage Bucket Item Read'],
|
|
110
|
+
ttl: '12h',
|
|
111
|
+
zone: null,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* ⛔ A ZONE ROLE SCOPED TO A BUCKET IS INCOHERENT, NOT NARROW. The groups a zone role carries are
|
|
117
|
+
* gated at the zone; against a bucket resource every one of them is refused. roles.yaml cannot
|
|
118
|
+
* express the contradiction as a type, so the expansion is the only place it can be caught.
|
|
119
|
+
*/
|
|
120
|
+
it('refuses a zone role that names a bucket', () => {
|
|
121
|
+
const bad = yaml(` zone_roles:
|
|
122
|
+
- { name: nope, groups: [DNS Read], bucket: b, ttl: 5m, max_ttl: 1h, desc: '' }
|
|
123
|
+
`);
|
|
124
|
+
assert.throws(() => expandAll(parseRolesConfig(bad), ZONES), /zone role cannot be scoped/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** ⛔ `bucket: ''` would expand to `…_default_` — neither a bucket nor the account. */
|
|
128
|
+
it('refuses an empty bucket rather than defaulting it', () => {
|
|
129
|
+
const bad = yaml(` account_roles:
|
|
130
|
+
- { name: nope, groups: [DNS Read], bucket: '', ttl: 5m, max_ttl: 1h, desc: '' }
|
|
131
|
+
`);
|
|
132
|
+
assert.throws(() => parseRolesConfig(bad), RolesConfigError);
|
|
133
|
+
});
|
|
134
|
+
});
|