@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,220 @@
|
|
|
1
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
2
|
+
import * as Provider from 'alchemy/Provider';
|
|
3
|
+
import { Resource, type ResourceClass } from 'alchemy/Resource';
|
|
4
|
+
/**
|
|
5
|
+
* `Cloudflare.R2BucketLock` — the lock rule set on one R2 bucket, declared rather than applied by
|
|
6
|
+
* hand. A bucket lock is a RETENTION FLOOR: while a rule covers an object, no API call, no
|
|
7
|
+
* lifecycle rule and no credential can delete it.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 WHY THIS EXISTS AT ALL, AND WHAT IT REPLACES. a consuming stack shipped the retention floor as a
|
|
10
|
+
* documented `wrangler r2 bucket lock add …` step in the runbook, on the measured ground that
|
|
11
|
+
* `alchemy@2.0.0-beta.77` has no lock property anywhere in its R2 namespace. That measurement is
|
|
12
|
+
* still true and it was the wrong conclusion: a step a human runs once is a step an Alchemy plan
|
|
13
|
+
* can never show missing, so removing the floor by hand leaves that stack, its plan and
|
|
14
|
+
* every gate completely green. The operator's rule (2026-09-15) is to cover such a gap with a
|
|
15
|
+
* custom resource over the vendor's SDK, which is what this is.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ THE SDK ALREADY HAD IT, AND THAT IS WORTH SAYING BECAUSE IT DECIDED THE SHAPE. Measured
|
|
18
|
+
* 2026-09-15 in `cloudflare@4.5.0`, `resources/r2/buckets/locks.d.ts`:
|
|
19
|
+
*
|
|
20
|
+
* update(bucketName: string, params: LockUpdateParams, options?): APIPromise<…>
|
|
21
|
+
* get(bucketName: string, params: LockGetParams, options?): APIPromise<LockGetResponse>
|
|
22
|
+
*
|
|
23
|
+
* over `PUT`/`GET /accounts/{account_id}/r2/buckets/{bucket_name}/lock` with the jurisdiction
|
|
24
|
+
* carried as the `cf-r2-jurisdiction` header (`locks.mjs:17` and `:41`). So there is no raw
|
|
25
|
+
* `client.put` fallback here and no path string this package invented — the fallback the rule
|
|
26
|
+
* allows was not needed.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ THE RULE SET IS A REPLACE, NOT A MERGE. `PUT` takes the WHOLE list, exactly like R2's
|
|
29
|
+
* lifecycle rules — which is the trap a consuming stack records for
|
|
30
|
+
* `homeflare-git`. Declaring this resource on a bucket whose live rules nobody has read would
|
|
31
|
+
* drop them, so `read` fetches them first and `reconcile` refuses to write when they already
|
|
32
|
+
* match.
|
|
33
|
+
*/
|
|
34
|
+
import type Cloudflare from 'cloudflare';
|
|
35
|
+
import { NotFoundError } from 'cloudflare';
|
|
36
|
+
import * as Effect from 'effect/Effect';
|
|
37
|
+
import { CloudflareApi } from './client.ts';
|
|
38
|
+
import {
|
|
39
|
+
type Jurisdiction,
|
|
40
|
+
type R2LockRule,
|
|
41
|
+
jurisdictionOf,
|
|
42
|
+
rulesEqual,
|
|
43
|
+
toBody,
|
|
44
|
+
} from './lock-rules.ts';
|
|
45
|
+
import type { Providers } from './providers.ts';
|
|
46
|
+
|
|
47
|
+
export interface R2BucketLockProps {
|
|
48
|
+
readonly accountId: string;
|
|
49
|
+
readonly bucketName: string;
|
|
50
|
+
/** ⚠️ Part of the bucket's IDENTITY, not a preference — a bucket in another jurisdiction is another bucket. */
|
|
51
|
+
readonly jurisdiction?: Jurisdiction;
|
|
52
|
+
/**
|
|
53
|
+
* ⛔ REQUIRED, AND ITS EMPTY VALUE IS THE DANGEROUS ONE. `rules: []` means NO FLOOR: it is what
|
|
54
|
+
* `delete` sends to take a lock off deliberately. An optional property would let a declaration
|
|
55
|
+
* that simply forgot it send `[]` and silently unlock the repository, reporting success — the
|
|
56
|
+
* same reasoning `LiteLLM.McpServer.allowedTools` is required for.
|
|
57
|
+
*/
|
|
58
|
+
readonly rules: readonly R2LockRule[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type R2BucketLockAttributes = {
|
|
62
|
+
readonly bucketName: string;
|
|
63
|
+
readonly jurisdiction: Jurisdiction;
|
|
64
|
+
readonly rules: readonly R2LockRule[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface R2BucketLock extends Resource<
|
|
68
|
+
'Cloudflare.R2BucketLock',
|
|
69
|
+
R2BucketLockProps,
|
|
70
|
+
R2BucketLockAttributes,
|
|
71
|
+
never,
|
|
72
|
+
Providers
|
|
73
|
+
> {}
|
|
74
|
+
|
|
75
|
+
// ⚠️ EXPLICIT ANNOTATION, REQUIRED BY isolatedDeclarations. This file was internal to a
|
|
76
|
+
// monorepo before it was published; a published package emits its .d.ts from a separate
|
|
77
|
+
// tsc pass, so every exported symbol must state its type rather than infer it.
|
|
78
|
+
export const R2BucketLock: ResourceClass<R2BucketLock> = Resource<R2BucketLock>(
|
|
79
|
+
'Cloudflare.R2BucketLock',
|
|
80
|
+
{
|
|
81
|
+
/**
|
|
82
|
+
* ⛔ RETAIN, AND ON THIS RESOURCE IT IS NOT THE ESTATE'S HABIT BUT THE WHOLE POINT. A lock is a
|
|
83
|
+
* safety floor: removing the declaration must not remove the floor, because the state it
|
|
84
|
+
* protects is the only physical backup of the estate's one Postgres. `delete` below is
|
|
85
|
+
* implemented — a provider that cannot delete cannot be tested or corrected — and it is
|
|
86
|
+
* reachable only by an operator running the CLI directly, past a deploy wrapper.
|
|
87
|
+
*/
|
|
88
|
+
defaultRemovalPolicy: 'retain',
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 🔴 A 404 HERE IS "NOTHING TO ADOPT", AND THE FIRST PLAN PROVED IT IS THE ORDINARY CASE.
|
|
94
|
+
* Measured 2026-09-15, an alchemy plan --detailed on a bucket that does not
|
|
95
|
+
* exist yet: `404 {"success":false,"errors":[{"code":10006,"message":"The specified bucket does
|
|
96
|
+
* not exist."}]}` — raised from `read`, during PLANNING, before anything is created. Alchemy
|
|
97
|
+
* calls `read` for a resource with no prior state, so on every first deploy the lock is asked
|
|
98
|
+
* about a bucket the same plan is about to create. Letting that throw makes the resource
|
|
99
|
+
* impossible to plan; swallowing it with `catchAll` would also hide a 403 from a mis-scoped
|
|
100
|
+
* token, which is the failure this estate spends the most time on. So: 404 → absent, everything
|
|
101
|
+
* else propagates.
|
|
102
|
+
*
|
|
103
|
+
* ⛔ AN EMPTY LIVE RULE SET IS "NO LOCK", NOT "A LOCK WITH NOTHING IN IT", so it reads as absent
|
|
104
|
+
* and the resource CREATES. The API answers `{}` for a bucket that has never been locked and
|
|
105
|
+
* `{ rules: [] }` for one whose rules were removed; both mean the floor is gone.
|
|
106
|
+
* ⚠️ `Unowned` UNTIL THIS STACK HAS WRITTEN IT. A lock someone applied by hand — which is exactly
|
|
107
|
+
* what the retired runbook step did — must not be silently rewritten by a mistyped declaration;
|
|
108
|
+
* the planner refuses until the resource is piped through `adopt(true)`, which is one line an
|
|
109
|
+
* operator reads in the diff. An adopted rule set that already MATCHES then costs no request at
|
|
110
|
+
* all, because `reconcile` below returns before writing.
|
|
111
|
+
*/
|
|
112
|
+
export const readLock = (
|
|
113
|
+
client: Cloudflare,
|
|
114
|
+
props: { accountId: string; bucketName: string; jurisdiction?: Jurisdiction },
|
|
115
|
+
written: boolean,
|
|
116
|
+
): Effect.Effect<R2BucketLockAttributes | undefined> =>
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const jurisdiction = jurisdictionOf(props);
|
|
119
|
+
const live = yield* Effect.tryPromise({
|
|
120
|
+
try: () =>
|
|
121
|
+
client.r2.buckets.locks.get(props.bucketName, {
|
|
122
|
+
account_id: props.accountId,
|
|
123
|
+
jurisdiction,
|
|
124
|
+
}),
|
|
125
|
+
catch: (cause) => cause,
|
|
126
|
+
}).pipe(
|
|
127
|
+
Effect.catchIf(
|
|
128
|
+
(cause) => cause instanceof NotFoundError,
|
|
129
|
+
() => Effect.succeed(undefined),
|
|
130
|
+
),
|
|
131
|
+
Effect.orDie,
|
|
132
|
+
);
|
|
133
|
+
if (live === undefined) return undefined;
|
|
134
|
+
const rules = (live.rules ?? []) as readonly R2LockRule[];
|
|
135
|
+
if (rules.length === 0) return undefined;
|
|
136
|
+
const attributes: R2BucketLockAttributes = {
|
|
137
|
+
bucketName: props.bucketName,
|
|
138
|
+
jurisdiction,
|
|
139
|
+
rules,
|
|
140
|
+
};
|
|
141
|
+
return written ? attributes : Unowned(attributes);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* ⛔ A NO-OP MAKES NO REQUEST, AND ON A LOCK THAT MATTERS MORE THAN TIDINESS. `PUT` replaces the
|
|
146
|
+
* rule set; re-sending an identical one on every deploy is a write against the only thing
|
|
147
|
+
* standing between this repository and a delete, for no change. `output` is what the last deploy
|
|
148
|
+
* persisted (or what `read` just adopted), so the comparison costs nothing either.
|
|
149
|
+
*/
|
|
150
|
+
export const reconcileLock = (
|
|
151
|
+
client: Cloudflare,
|
|
152
|
+
news: R2BucketLockProps,
|
|
153
|
+
output: R2BucketLockAttributes | undefined,
|
|
154
|
+
): Effect.Effect<R2BucketLockAttributes> =>
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
const jurisdiction = jurisdictionOf(news);
|
|
157
|
+
if (output !== undefined && rulesEqual(output.rules, news.rules)) return output;
|
|
158
|
+
yield* Effect.promise(() =>
|
|
159
|
+
client.r2.buckets.locks.update(news.bucketName, {
|
|
160
|
+
account_id: news.accountId,
|
|
161
|
+
jurisdiction,
|
|
162
|
+
...toBody(news.rules),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
return { bucketName: news.bucketName, jurisdiction, rules: news.rules };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* ⛔ DELETING THIS RESOURCE MEANS PUTTING AN EMPTY RULE SET, WHICH IS UNLOCKING THE BUCKET. It is
|
|
170
|
+
* implemented because the retirement path is real — R2 refuses to empty a bucket while any lock
|
|
171
|
+
* rule is configured, so retiring the repository has to start here — and it is behind
|
|
172
|
+
* `defaultRemovalPolicy: 'retain'` and the estate CLI's refusal of `destroy` precisely because
|
|
173
|
+
* no ordinary deploy should ever reach it.
|
|
174
|
+
*/
|
|
175
|
+
export const deleteLock = (
|
|
176
|
+
client: Cloudflare,
|
|
177
|
+
output: R2BucketLockAttributes,
|
|
178
|
+
accountId: string,
|
|
179
|
+
): Effect.Effect<void> =>
|
|
180
|
+
Effect.gen(function* () {
|
|
181
|
+
yield* Effect.promise(() =>
|
|
182
|
+
client.r2.buckets.locks.update(output.bucketName, {
|
|
183
|
+
account_id: accountId,
|
|
184
|
+
jurisdiction: output.jurisdiction,
|
|
185
|
+
rules: [],
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export const R2BucketLockProvider = () =>
|
|
191
|
+
Provider.effect(
|
|
192
|
+
R2BucketLock,
|
|
193
|
+
Effect.gen(function* () {
|
|
194
|
+
const client = yield* CloudflareApi;
|
|
195
|
+
return R2BucketLock.Provider.of({
|
|
196
|
+
/**
|
|
197
|
+
* ⛔ EMPTY, AND ALCHEMY'S OWN CONTRACT ASKS FOR EXACTLY THAT. `ProviderService.list`:
|
|
198
|
+
* "Resources with no native enumeration API (… sub-resources keyed entirely by a parent)
|
|
199
|
+
* should return an empty array rather than throwing." There is no list-locks endpoint —
|
|
200
|
+
* a lock is reachable only through the bucket that owns it — so enumerating would mean
|
|
201
|
+
* listing every bucket in the account and GETting each one's lock.
|
|
202
|
+
* ⛔⛔ AND `nuke.singleton` IS WHY THAT IS NOT MERELY UNAVAILABLE BUT UNWANTED. `list`
|
|
203
|
+
* feeds `alchemy unsafe nuke`, which lists and then deletes; `delete` here is an UNLOCK.
|
|
204
|
+
* A working enumeration would hand one command the ability to strip the retention floor
|
|
205
|
+
* off every bucket in the account — the precise event a floor exists to make impossible.
|
|
206
|
+
* `singleton` is the documented word for it: always-present configuration whose delete
|
|
207
|
+
* resets rather than removes.
|
|
208
|
+
*/
|
|
209
|
+
list: () => Effect.succeed([]),
|
|
210
|
+
nuke: { singleton: true },
|
|
211
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
212
|
+
const props = olds ?? undefined;
|
|
213
|
+
if (props === undefined) return undefined;
|
|
214
|
+
return yield* readLock(client, props, output !== undefined);
|
|
215
|
+
}),
|
|
216
|
+
reconcile: Effect.fn(({ news, output }) => reconcileLock(client, news, output)),
|
|
217
|
+
delete: Effect.fn(({ olds, output }) => deleteLock(client, output, olds.accountId)),
|
|
218
|
+
});
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create/PATCH bodies for `Forgejo.BranchProtection` — extracted for the 250-line cap.
|
|
3
|
+
*
|
|
4
|
+
* ★ READ OFF `house/mcp-servers/docs/api/upstream/forgejo.json` — create posts
|
|
5
|
+
* `CreateBranchProtectionOption`; update sends `EditBranchProtectionOption` fields only.
|
|
6
|
+
*/
|
|
7
|
+
import type { BranchProtectionProps } from './branch-protection.ts';
|
|
8
|
+
|
|
9
|
+
export const branchProtectionForm = (props: BranchProtectionProps) => ({
|
|
10
|
+
...(props.applyToAdmins === undefined ? {} : { apply_to_admins: props.applyToAdmins }),
|
|
11
|
+
...(props.dismissStaleApprovals === undefined
|
|
12
|
+
? {}
|
|
13
|
+
: { dismiss_stale_approvals: props.dismissStaleApprovals }),
|
|
14
|
+
...(props.enablePush === undefined ? {} : { enable_push: props.enablePush }),
|
|
15
|
+
...(props.enableStatusCheck === undefined
|
|
16
|
+
? {}
|
|
17
|
+
: { enable_status_check: props.enableStatusCheck }),
|
|
18
|
+
...(props.ignoreStaleApprovals === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { ignore_stale_approvals: props.ignoreStaleApprovals }),
|
|
21
|
+
...(props.requireSignedCommits === undefined
|
|
22
|
+
? {}
|
|
23
|
+
: { require_signed_commits: props.requireSignedCommits }),
|
|
24
|
+
...(props.requiredApprovals === undefined ? {} : { required_approvals: props.requiredApprovals }),
|
|
25
|
+
...(props.statusCheckContexts === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { status_check_contexts: props.statusCheckContexts }),
|
|
28
|
+
rule_name: props.branchName,
|
|
29
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Forgejo.BranchProtection` — one rule at `/repos/{owner}/{repo}/branch_protections/{name}`.
|
|
3
|
+
*
|
|
4
|
+
* ★ READ OFF `house/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection;
|
|
5
|
+
* read/update/delete use the branch/rule name in the path segment `{name}`. Response carries
|
|
6
|
+
* `rule_name` (preferred) and deprecated `branch_name`.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ `defaultRemovalPolicy: 'retain'` — dropping a protection rule from the stack must not silently
|
|
9
|
+
* unprotect a branch; opt into `.pipe(RemovalPolicy.destroy())` to DELETE.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ TOKEN NEEDS `write:repository` TO CREATE OR PATCH — get needs `read:repository`.
|
|
12
|
+
*/
|
|
13
|
+
import { Resource } from 'alchemy';
|
|
14
|
+
import * as Provider from 'alchemy/Provider';
|
|
15
|
+
import * as Effect from 'effect/Effect';
|
|
16
|
+
import { branchProtectionForm } from './branch-protection-form.ts';
|
|
17
|
+
import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
|
|
18
|
+
import { bool, int, stringArray, text } from './values.ts';
|
|
19
|
+
|
|
20
|
+
export interface BranchProtectionProps {
|
|
21
|
+
owner: string;
|
|
22
|
+
repo: string;
|
|
23
|
+
/** Branch or rule name — path key and `rule_name` on the wire. */
|
|
24
|
+
branchName: string;
|
|
25
|
+
requiredApprovals?: number;
|
|
26
|
+
enablePush?: boolean;
|
|
27
|
+
enableStatusCheck?: boolean;
|
|
28
|
+
requireSignedCommits?: boolean;
|
|
29
|
+
dismissStaleApprovals?: boolean;
|
|
30
|
+
ignoreStaleApprovals?: boolean;
|
|
31
|
+
applyToAdmins?: boolean;
|
|
32
|
+
statusCheckContexts?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BranchProtectionAttributes {
|
|
36
|
+
owner: string;
|
|
37
|
+
repo: string;
|
|
38
|
+
branchName: string;
|
|
39
|
+
requiredApprovals: number;
|
|
40
|
+
enablePush: boolean;
|
|
41
|
+
enableStatusCheck: boolean;
|
|
42
|
+
requireSignedCommits: boolean;
|
|
43
|
+
dismissStaleApprovals: boolean;
|
|
44
|
+
ignoreStaleApprovals: boolean;
|
|
45
|
+
applyToAdmins: boolean;
|
|
46
|
+
statusCheckContexts: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ForgejoBranchProtection extends Resource<
|
|
50
|
+
'Forgejo.BranchProtection',
|
|
51
|
+
BranchProtectionProps,
|
|
52
|
+
BranchProtectionAttributes,
|
|
53
|
+
never,
|
|
54
|
+
ForgejoRequirements
|
|
55
|
+
> {}
|
|
56
|
+
|
|
57
|
+
export const ForgejoBranchProtection = Resource<ForgejoBranchProtection>(
|
|
58
|
+
'Forgejo.BranchProtection',
|
|
59
|
+
{ defaultRemovalPolicy: 'retain' },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const handlers = forgejoHandlers<BranchProtectionProps, BranchProtectionAttributes>({
|
|
63
|
+
attributes: (live, props) => {
|
|
64
|
+
const ruleName = text(live['rule_name'], text(live['branch_name']));
|
|
65
|
+
if (ruleName === '') return undefined;
|
|
66
|
+
return {
|
|
67
|
+
applyToAdmins: bool(live['apply_to_admins']),
|
|
68
|
+
branchName: props.branchName,
|
|
69
|
+
dismissStaleApprovals: bool(live['dismiss_stale_approvals']),
|
|
70
|
+
enablePush: bool(live['enable_push'], true),
|
|
71
|
+
enableStatusCheck: bool(live['enable_status_check']),
|
|
72
|
+
ignoreStaleApprovals: bool(live['ignore_stale_approvals']),
|
|
73
|
+
owner: props.owner,
|
|
74
|
+
repo: props.repo,
|
|
75
|
+
requireSignedCommits: bool(live['require_signed_commits']),
|
|
76
|
+
requiredApprovals: int(live['required_approvals']),
|
|
77
|
+
statusCheckContexts: stringArray(live['status_check_contexts']),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
collection: (props) => `repos/${props.owner}/${props.repo}/branch_protections`,
|
|
81
|
+
createForm: branchProtectionForm,
|
|
82
|
+
matches: (attributes, props) =>
|
|
83
|
+
attributes.requiredApprovals === (props.requiredApprovals ?? 0) &&
|
|
84
|
+
attributes.enablePush === (props.enablePush ?? true) &&
|
|
85
|
+
attributes.enableStatusCheck === (props.enableStatusCheck ?? false) &&
|
|
86
|
+
attributes.requireSignedCommits === (props.requireSignedCommits ?? false) &&
|
|
87
|
+
attributes.dismissStaleApprovals === (props.dismissStaleApprovals ?? false) &&
|
|
88
|
+
attributes.ignoreStaleApprovals === (props.ignoreStaleApprovals ?? false) &&
|
|
89
|
+
attributes.applyToAdmins === (props.applyToAdmins ?? false) &&
|
|
90
|
+
(props.statusCheckContexts === undefined ||
|
|
91
|
+
stringArray(props.statusCheckContexts).join('\0') ===
|
|
92
|
+
attributes.statusCheckContexts.join('\0')),
|
|
93
|
+
path: (props) => `repos/${props.owner}/${props.repo}/branch_protections/${props.branchName}`,
|
|
94
|
+
updateForm: branchProtectionForm,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const ForgejoBranchProtectionProvider = () =>
|
|
98
|
+
Provider.effect(
|
|
99
|
+
ForgejoBranchProtection,
|
|
100
|
+
Effect.succeed(ForgejoBranchProtection.Provider.of(handlers)),
|
|
101
|
+
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One authenticated call to the Forgejo (Gitea) API at `/api/v1`.
|
|
3
|
+
*
|
|
4
|
+
* ★ PLAIN `fetch`, NO TLS ESCAPE HATCH — your Forgejo host serves a real certificate on :443,
|
|
5
|
+
* and the local daemon on :3000 is loopback. There is no `rejectUnauthorized` switch to leave
|
|
6
|
+
* on by accident elsewhere.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ GITEA DOES NOT WRAP ANSWERS IN `{"data": ...}` THE WAY PVE DOES. MEASURED 2026-09-13 on
|
|
9
|
+
* the live daemon: `GET /api/v1/version` returns `{"version":"16.0.3"}`; `GET /api/v1/user`
|
|
10
|
+
* returns the user object; `GET /api/v1/orgs/HomeFlare/repos` returns a JSON array. A caller
|
|
11
|
+
* that unwraps a non-existent envelope sees `undefined` and calls a healthy object "absent".
|
|
12
|
+
*
|
|
13
|
+
* ⛔ THE TOKEN IS NEVER A PROP. Read `FORGEJO_TOKEN` from the environment at call time only.
|
|
14
|
+
* Alchemy persists attributes unencrypted and this estate's state store is Postgres dumped
|
|
15
|
+
* nightly — nothing stored may be a credential.
|
|
16
|
+
*/
|
|
17
|
+
import * as Effect from 'effect/Effect';
|
|
18
|
+
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
19
|
+
import * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
20
|
+
|
|
21
|
+
const PATH_PREFIX = '/api/v1';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Forgejo instance, from `FORGEJO_URL`. Trailing slash stripped.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ NO DEFAULT HOSTNAME. This used to fall back to the author's own instance, which is
|
|
27
|
+
* fine inside one estate and wrong in a published package: a consumer who forgets the
|
|
28
|
+
* variable would silently point at someone else's server rather than being told.
|
|
29
|
+
* ⚠️ Read at CALL time, never at module scope — a value captured at import is pinned to
|
|
30
|
+
* whatever the first isolate saw.
|
|
31
|
+
*/
|
|
32
|
+
export const apiBase = (): string => {
|
|
33
|
+
const raw = process.env['FORGEJO_URL'];
|
|
34
|
+
if (raw === undefined || raw.trim() === '') {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'FORGEJO_URL is not set. Point it at your Forgejo instance, e.g. https://git.example.com',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return raw.trim().replace(/\/$/, '');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const token = (): Effect.Effect<string, ForgejoError> =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const raw = process.env['FORGEJO_TOKEN'];
|
|
45
|
+
if (raw === undefined || raw.trim() === '') {
|
|
46
|
+
return yield* Effect.fail(
|
|
47
|
+
new ForgejoError(
|
|
48
|
+
0,
|
|
49
|
+
'GET',
|
|
50
|
+
'',
|
|
51
|
+
'FORGEJO_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props.',
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return raw.trim();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export class ForgejoError extends Error {
|
|
59
|
+
constructor(
|
|
60
|
+
readonly status: number,
|
|
61
|
+
readonly method: string,
|
|
62
|
+
readonly path: string,
|
|
63
|
+
detail: string,
|
|
64
|
+
) {
|
|
65
|
+
super(`Forgejo ${method} ${path} -> ${String(status)}: ${detail}`);
|
|
66
|
+
this.name = 'ForgejoError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* ⚠️ `Authorization: token <value>`, NOT `Bearer`. Gitea docs and MEASURED behaviour on this
|
|
72
|
+
* estate: Bearer is treated as OAuth2 and rejected; `token` is the documented API-key form.
|
|
73
|
+
*/
|
|
74
|
+
const authorization = (value: string) => ({ Authorization: `token ${value}` });
|
|
75
|
+
|
|
76
|
+
/** One JSON call. Returns the parsed body, or `undefined` when the response is empty (DELETE). */
|
|
77
|
+
export const forgejo = <T>(
|
|
78
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
79
|
+
path: string,
|
|
80
|
+
body?: Record<string, unknown>,
|
|
81
|
+
) =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
const credential = yield* token();
|
|
84
|
+
const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\//, '')}`;
|
|
85
|
+
/**
|
|
86
|
+
* ★ THROUGH `HttpClient`, NOT THE GLOBAL `fetch` — the idiom Alchemy's own providers follow
|
|
87
|
+
* (src/Hetzner/Providers.ts builds on `FetchHttpClient.layer`; there is no bare fetch in its
|
|
88
|
+
* Docker, Kubernetes or GitHub providers). It buys a typed error channel, interruption when
|
|
89
|
+
* a plan is cancelled, the runtime's tracing, and a client a test can replace without
|
|
90
|
+
* monkey-patching a global. house/proxmox/src/resource.ts carries the same note.
|
|
91
|
+
*/
|
|
92
|
+
const client = yield* HttpClient.HttpClient;
|
|
93
|
+
/**
|
|
94
|
+
* ⛔ THE CONTENT TYPE GOES ON `bodyText`, NOT IN THE HEADER MAP, AND PUTTING IT IN BOTH IS HOW
|
|
95
|
+
* THIS PACKAGE SHIPPED UNABLE TO WRITE AT ALL.
|
|
96
|
+
*
|
|
97
|
+
* 🔴 THE BUG, MEASURED THE FIRST TIME A WRITE WAS EVER ATTEMPTED. The header map DID carry
|
|
98
|
+
* `Content-Type: application/json` — and `bodyText(s)` with no second argument sets
|
|
99
|
+
* `text/plain` and OVERWRITES it, because it is piped AFTER `setHeaders`. Forgejo answered
|
|
100
|
+
* 422 {"message":"[]: Unsupported Content-Type"}
|
|
101
|
+
* on `PATCH orgs/homeflare/labels/1`. Every create and every update in every family here was
|
|
102
|
+
* broken, identically, from the day the client was written.
|
|
103
|
+
*
|
|
104
|
+
* ⛔ NOTHING CAUGHT IT BECAUSE NOTHING HAD EVER WRITTEN. The only credential on the shelf was
|
|
105
|
+
* `mcp-read`, which answers 403 before a request body is ever parsed — so read, adopt, diff
|
|
106
|
+
* and plan all worked perfectly and the write path had literally never executed. A provider
|
|
107
|
+
* that has never performed its own writes is not a tested provider, whatever the plan says.
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ `bodyJsonUnsafe` WOULD ALSO WORK. `bodyText` with an explicit type is used instead
|
|
110
|
+
* because it makes the content type visible at the call site, which is the thing that was
|
|
111
|
+
* invisible before.
|
|
112
|
+
*/
|
|
113
|
+
const request = HttpClientRequest.make(method)(url).pipe(
|
|
114
|
+
HttpClientRequest.setHeaders({ ...authorization(credential), Accept: 'application/json' }),
|
|
115
|
+
body === undefined
|
|
116
|
+
? (self) => self
|
|
117
|
+
: HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),
|
|
118
|
+
);
|
|
119
|
+
const response = yield* client
|
|
120
|
+
.execute(request)
|
|
121
|
+
.pipe(Effect.mapError((cause) => new ForgejoError(0, method, path, String(cause))));
|
|
122
|
+
// ⛔ Status first, body once — a Gitea error body is JSON but a proxy's 502 is not.
|
|
123
|
+
if (response.status < 200 || response.status >= 300) {
|
|
124
|
+
const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ''));
|
|
125
|
+
return yield* Effect.fail(
|
|
126
|
+
new ForgejoError(response.status, method, path, detail.slice(0, 300)),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
// ⚠️ 204 AND AN EMPTY BODY ARE REAL: a label DELETE answers no content, and `json` on an empty
|
|
130
|
+
// body fails. Read text once and parse only when there is something to parse.
|
|
131
|
+
const text = yield* response.text.pipe(
|
|
132
|
+
Effect.mapError((cause) => new ForgejoError(0, method, path, String(cause))),
|
|
133
|
+
);
|
|
134
|
+
// ⚠️ PUT/DELETE on actions secrets and hooks often answer 201/204 with no body — same trap as DELETE.
|
|
135
|
+
return (text === '' ? undefined : JSON.parse(text)) as T;
|
|
136
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgejo providers for Alchemy.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY.
|
|
5
|
+
* These are the symbols a real stack consumes; the rest of the files are internals a
|
|
6
|
+
* provider needs but a consumer should not depend on. An `export *` here would publish
|
|
7
|
+
* every helper as API and make the next refactor a breaking change.
|
|
8
|
+
* ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
|
|
9
|
+
* deliberate, visible act rather than an accident of barrelling.
|
|
10
|
+
*/
|
|
11
|
+
export { ForgejoBranchProtectionProvider } from './branch-protection.ts';
|
|
12
|
+
export { ForgejoOrgSecretProvider } from './org-actions-secrets.ts';
|
|
13
|
+
export { ForgejoOrgLabel, ForgejoOrgLabelProvider, type OrgLabelProps } from './org-label.ts';
|
|
14
|
+
export { ForgejoOrgTeamProvider } from './org-team.ts';
|
|
15
|
+
export { ForgejoRepoWebhookProvider } from './repo-webhook.ts';
|
|
16
|
+
export { ForgejoRepository, ForgejoRepositoryProvider } from './repository.ts';
|
|
17
|
+
export { ForgejoTeamMember, ForgejoTeamMemberProvider } from './team-member.ts';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Forgejo.OrgSecret` — org-level Actions secret metadata at `/orgs/{org}/actions/secrets/{name}`.
|
|
3
|
+
*
|
|
4
|
+
* ★ CHOICE: AUTHORED WITH WRITE-ONLY VALUE VIA ENV, NOT REFUSED. The OpenAPI in
|
|
5
|
+
* `house/mcp-servers/docs/api/upstream/forgejo.json` shows `Secret` returns only `name` and
|
|
6
|
+
* `created_at` — never the `data` field — so attributes stay metadata-only. Create/update is PUT
|
|
7
|
+
* with body `{ data }`, which does not fit POST/PATCH; this family uses the `upsert` seam on
|
|
8
|
+
* `ForgejoSpec` instead of hand-rolling provider handlers.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ THE SECRET VALUE IS NEVER A PROP OR AN ATTRIBUTE. It is read from `FORGEJO_ORG_SECRET_{org}_{name}`
|
|
11
|
+
* (see orgSecretEnvKey) at PUT time when the name is absent from the list. Once present, reconcile
|
|
12
|
+
* noops — the API cannot confirm the value matches, and re-PUT on every deploy would rotate
|
|
13
|
+
* unintentionally. Rotate by destroy-then-create or a manual PUT outside Alchemy.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ PUT ANSWERS 201/204 WITH NO BODY — client.ts treats empty responses as `undefined`; read-back
|
|
16
|
+
* uses the list endpoint, not the PUT response.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ TOKEN NEEDS `write:organization` TO PUT — list needs `read:organization`.
|
|
19
|
+
*
|
|
20
|
+
* ⛔ THE FILE NAME IS PLURAL ON PURPOSE AND MUST STAY THAT WAY. `.gitignore:198` carries
|
|
21
|
+
* `*-secret.*` — the rule that stops `client-secret.json` and friends being committed — and it
|
|
22
|
+
* matches a SOURCE file called `org-secret.ts` just as happily. This family was first authored
|
|
23
|
+
* under that name: it existed on disk, typechecked, passed oxlint, was imported by alchemy.run.ts
|
|
24
|
+
* and appeared in NO `git status`. It would have been committed as a broken import for everyone
|
|
25
|
+
* else, with the local tree green the whole time. `scripts/husky/ignored-imports.ts` is the gate
|
|
26
|
+
* that now catches the class; this comment is for whoever is tempted to rename it back.
|
|
27
|
+
*/
|
|
28
|
+
import { Resource } from 'alchemy';
|
|
29
|
+
import * as Provider from 'alchemy/Provider';
|
|
30
|
+
import * as Effect from 'effect/Effect';
|
|
31
|
+
import { ForgejoError, forgejo } from './client.ts';
|
|
32
|
+
import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
|
|
33
|
+
import { orgSecretEnvKey, text } from './values.ts';
|
|
34
|
+
|
|
35
|
+
export interface OrgSecretProps {
|
|
36
|
+
org: string;
|
|
37
|
+
/** Secret name — path key; value lives in env, not here. */
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface OrgSecretAttributes {
|
|
42
|
+
org: string;
|
|
43
|
+
name: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ForgejoOrgSecret extends Resource<
|
|
48
|
+
'Forgejo.OrgSecret',
|
|
49
|
+
OrgSecretProps,
|
|
50
|
+
OrgSecretAttributes,
|
|
51
|
+
never,
|
|
52
|
+
ForgejoRequirements
|
|
53
|
+
> {}
|
|
54
|
+
|
|
55
|
+
export const ForgejoOrgSecret = Resource<ForgejoOrgSecret>('Forgejo.OrgSecret');
|
|
56
|
+
|
|
57
|
+
const secretData = (org: string, name: string) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const key = orgSecretEnvKey(org, name);
|
|
60
|
+
const raw = process.env[key];
|
|
61
|
+
if (raw === undefined || raw.trim() === '') {
|
|
62
|
+
return yield* Effect.fail(
|
|
63
|
+
new ForgejoError(
|
|
64
|
+
0,
|
|
65
|
+
'PUT',
|
|
66
|
+
`orgs/${org}/actions/secrets/${name}`,
|
|
67
|
+
`${key} is unset. Export the secret value there — never as an Alchemy prop.`,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return raw.trim();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const handlers = forgejoHandlers<OrgSecretProps, OrgSecretAttributes>({
|
|
75
|
+
attributes: (live, props) => ({
|
|
76
|
+
createdAt: text(live['created_at']),
|
|
77
|
+
name: props.name,
|
|
78
|
+
org: props.org,
|
|
79
|
+
}),
|
|
80
|
+
collection: (props) => `orgs/${props.org}/actions/secrets/${props.name}`,
|
|
81
|
+
createForm: () => ({}),
|
|
82
|
+
locate: (props) =>
|
|
83
|
+
forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/actions/secrets?limit=200`).pipe(
|
|
84
|
+
Effect.map((rows) =>
|
|
85
|
+
Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
matches: () => true,
|
|
89
|
+
path: (props) => `orgs/${props.org}/actions/secrets/${props.name}`,
|
|
90
|
+
upsert: (props, before) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
if (before !== undefined) return;
|
|
93
|
+
const data = yield* secretData(props.org, props.name);
|
|
94
|
+
yield* forgejo('PUT', `orgs/${props.org}/actions/secrets/${props.name}`, { data });
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const ForgejoOrgSecretProvider = () =>
|
|
99
|
+
Provider.effect(ForgejoOrgSecret, Effect.succeed(ForgejoOrgSecret.Provider.of(handlers)));
|