@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,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The roles.yaml expansion — surfaces × accounts × zones into concrete roles — ported from
|
|
3
|
+
* cfroles.py `expand` and cfhf.py `policy_for`, with permission groups still by NAME.
|
|
4
|
+
*
|
|
5
|
+
* ★ PURE, AND NAMES STAY NAMES. The Python resolved names to IDs in the same pass
|
|
6
|
+
* (apply-roles.py:144-150), with a Cloudflare credential in hand. Agents may not hold that
|
|
7
|
+
* credential, so resolution is a separate step behind one interface
|
|
8
|
+
* (cloudflare-permission-groups.ts) and nothing here knows where IDs come from.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ THE NAMING AND THE ZONE RULES ARE CONTRACTS WITH EVERY CONSUMER. apply-consumers.py grants
|
|
11
|
+
* `creds/<slug(zone)>-*` by zone (apply-consumers.py:117-120) and agents mint by role name, so a
|
|
12
|
+
* role a port renames is a role nothing can mint. Each rule cites the line it came from, and
|
|
13
|
+
* cloudflare/parity.ts proves the whole expansion against production.
|
|
14
|
+
*/
|
|
15
|
+
import { accountResource, bucketResource, zoneResource } from './cloudflare-group-scope.ts';
|
|
16
|
+
import type { AccountSpec, RoleTemplate, RolesConfig } from './cloudflare-roles-config.ts';
|
|
17
|
+
|
|
18
|
+
/** A zone as the expansion needs it — what cfhf.py load_account listed (cfhf.py:134-146). */
|
|
19
|
+
export interface CloudflareZone {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** From the `type=internal` listing. ⚠️ Shares its NAME with the public zone, never its id. */
|
|
23
|
+
readonly internal: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One policy entry, groups by name.
|
|
28
|
+
*
|
|
29
|
+
* ★ `groupOrder` CARRIES A PYTHON RULE THAT ONLY EXISTS AFTER RESOLUTION. cfhf.py:180 sorts the
|
|
30
|
+
* account entry's group IDS — not its names — so that order cannot be computed here, only
|
|
31
|
+
* promised. A `declared` entry keeps roles.yaml order, exactly as cfhf.py:175 emitted it.
|
|
32
|
+
*/
|
|
33
|
+
export interface DeclaredPolicy {
|
|
34
|
+
readonly effect: 'allow';
|
|
35
|
+
readonly resource: string;
|
|
36
|
+
readonly groups: readonly string[];
|
|
37
|
+
readonly groupOrder: 'declared' | 'by-id';
|
|
38
|
+
/**
|
|
39
|
+
* The scope these NAMES resolve at, when it is not this entry's own resource namespace —
|
|
40
|
+
* roles.yaml `groups_scope:`, already a GROUP_SCOPES value. Undefined on every entry that does
|
|
41
|
+
* not say otherwise, which is 639 of the estate's 643 group references.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ IT MOVES THE LOOKUP, NEVER THE RESOURCE. The entry still binds the groups to the resource
|
|
44
|
+
* above; only the id chosen for each name changes. That is the shape Cloudflare calls "all
|
|
45
|
+
* zones from an account" — the ACCOUNT resource carrying ZONE-scoped groups
|
|
46
|
+
* (house/nix/homeflare-config/scripts/cf-mint-analytics-tokens.py:155-158, beside a working
|
|
47
|
+
* token). Without this field the resource is the only witness, and it says `account`.
|
|
48
|
+
*/
|
|
49
|
+
readonly scope?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What apply-roles.py wrote to the KV catalog for one role (apply-roles.py:58-62). */
|
|
53
|
+
export interface CatalogView {
|
|
54
|
+
readonly description: string;
|
|
55
|
+
readonly zone: string | null;
|
|
56
|
+
readonly ttl: string;
|
|
57
|
+
readonly maxTtl: string;
|
|
58
|
+
readonly permissions: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ExpandedRole {
|
|
62
|
+
readonly account: string;
|
|
63
|
+
readonly accountId: string;
|
|
64
|
+
readonly mount: string;
|
|
65
|
+
readonly surface: string;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** Exactly what apply-roles.py:160-161 wrote — the zone suffix included. */
|
|
68
|
+
readonly description: string;
|
|
69
|
+
readonly ttl: string;
|
|
70
|
+
readonly maxTtl: string;
|
|
71
|
+
readonly policies: readonly DeclaredPolicy[];
|
|
72
|
+
readonly catalog: CatalogView;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** cfroles.py:18-20 — `example.com` → `homeflare-dev`. */
|
|
76
|
+
export const slug = (name: string) =>
|
|
77
|
+
name
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
+
.replace(/^-+|-+$/g, '');
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* cfroles.py:23-38. ⛔ THE `-internal` SUFFIX IS WHAT KEEPS TWO ZONES OF ONE NAME APART. Slugging
|
|
84
|
+
* both example.com zones alike would leave two roles fighting for one name, and whichever
|
|
85
|
+
* wrote last would silently own it.
|
|
86
|
+
*/
|
|
87
|
+
export const zoneSlug = (zone: CloudflareZone) =>
|
|
88
|
+
`${slug(zone.name)}${zone.internal ? '-internal' : ''}`;
|
|
89
|
+
|
|
90
|
+
/** cfroles.py:46-47 — one mount, and so one parent token, per (account, surface). */
|
|
91
|
+
export const mountName = (account: string, surface: string) => `cloudflare-${account}-${surface}`;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* cfhf.py:151-181. ⛔ ONE ENTRY PER SCOPE, NEVER MERGED: Cloudflare gates a permission at one
|
|
95
|
+
* scope only, so a zone role that also needs account groups gets a SECOND entry on the account
|
|
96
|
+
* resource. ⚠️ `if account_group_ids:` — an empty list emits no second entry (cfhf.py:176).
|
|
97
|
+
*/
|
|
98
|
+
const policiesFor = (
|
|
99
|
+
tpl: RoleTemplate,
|
|
100
|
+
accountId: string,
|
|
101
|
+
zone: CloudflareZone | null,
|
|
102
|
+
accountGroups: readonly string[],
|
|
103
|
+
): DeclaredPolicy[] => {
|
|
104
|
+
/**
|
|
105
|
+
* ⛔ A BUCKET NARROWS THE ACCOUNT RESOURCE; IT NEVER NARROWS A ZONE. A zone role scoped to an R2
|
|
106
|
+
* bucket is not a narrower grant, it is an incoherent one — the groups a zone role carries are
|
|
107
|
+
* gated at the zone and would be refused against a bucket resource. Refusing here is the only
|
|
108
|
+
* place that can tell the difference; roles.yaml cannot express the contradiction as a type.
|
|
109
|
+
*/
|
|
110
|
+
if (tpl.bucket !== undefined && zone !== null) {
|
|
111
|
+
throw new Error(`${tpl.name}: a zone role cannot be scoped to R2 bucket ${tpl.bucket}`);
|
|
112
|
+
}
|
|
113
|
+
const accountScoped =
|
|
114
|
+
tpl.bucket === undefined
|
|
115
|
+
? accountResource(accountId)
|
|
116
|
+
: bucketResource(accountId, tpl.bucketJurisdiction, tpl.bucket);
|
|
117
|
+
const resource = zone === null ? accountScoped : zoneResource(zone.id);
|
|
118
|
+
const first: DeclaredPolicy = {
|
|
119
|
+
effect: 'allow',
|
|
120
|
+
groupOrder: 'declared',
|
|
121
|
+
groups: tpl.groups,
|
|
122
|
+
resource,
|
|
123
|
+
/**
|
|
124
|
+
* ⚠️ `groups_scope:` GOVERNS `groups:` AND NOTHING ELSE. The second entry below exists because
|
|
125
|
+
* `account_groups` are account-scoped BY CONSTRUCTION (cfhf.py:176-180) — overriding its
|
|
126
|
+
* scope from the same key would make one word mean two things in one template.
|
|
127
|
+
* ★ OMITTED, NOT SET TO THE DEFAULT, WHEN THE yaml IS SILENT: an always-present field would
|
|
128
|
+
* rewrite the stored props of all 594 declared roles to say what the resource already said.
|
|
129
|
+
*/
|
|
130
|
+
...(tpl.groupsScope === undefined ? {} : { scope: tpl.groupsScope }),
|
|
131
|
+
};
|
|
132
|
+
if (accountGroups.length === 0) return [first];
|
|
133
|
+
const second: DeclaredPolicy = {
|
|
134
|
+
effect: 'allow',
|
|
135
|
+
groupOrder: 'by-id',
|
|
136
|
+
groups: accountGroups,
|
|
137
|
+
resource: accountResource(accountId),
|
|
138
|
+
};
|
|
139
|
+
return [first, second];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const build = (
|
|
143
|
+
account: AccountSpec,
|
|
144
|
+
surface: string,
|
|
145
|
+
tpl: RoleTemplate,
|
|
146
|
+
zone: CloudflareZone | null,
|
|
147
|
+
accountGroups: readonly string[],
|
|
148
|
+
): ExpandedRole => ({
|
|
149
|
+
account: account.key,
|
|
150
|
+
accountId: account.id,
|
|
151
|
+
catalog: {
|
|
152
|
+
description: tpl.desc,
|
|
153
|
+
maxTtl: tpl.maxTtl,
|
|
154
|
+
// apply-roles.py:61 — groups then account_groups, names, in file order.
|
|
155
|
+
permissions: [...tpl.groups, ...accountGroups],
|
|
156
|
+
ttl: tpl.ttl,
|
|
157
|
+
zone: zone === null ? null : zone.name,
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* apply-roles.py:160-161. ⚠️ THE ZONE NAME, NOT ITS SLUG, so a public zone and its internal
|
|
161
|
+
* twin describe alike, and an empty `desc` still gains the suffix after a leading space.
|
|
162
|
+
*/
|
|
163
|
+
description: zone === null ? tpl.desc : `${tpl.desc} (zone: ${zone.name})`,
|
|
164
|
+
maxTtl: tpl.maxTtl,
|
|
165
|
+
mount: mountName(account.key, surface),
|
|
166
|
+
// cfroles.py:84 for zone roles, :98 for account roles.
|
|
167
|
+
name: zone === null ? tpl.name : `${zoneSlug(zone)}-${tpl.name}`,
|
|
168
|
+
policies: policiesFor(tpl, account.id, zone, accountGroups),
|
|
169
|
+
surface,
|
|
170
|
+
ttl: tpl.ttl,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/** cfroles.py:50-104 for one account, in the Python's order: per surface, zone roles first. */
|
|
174
|
+
export const expandAccount = (
|
|
175
|
+
config: RolesConfig,
|
|
176
|
+
account: AccountSpec,
|
|
177
|
+
zones: readonly CloudflareZone[],
|
|
178
|
+
): ExpandedRole[] => {
|
|
179
|
+
const roles: ExpandedRole[] = [];
|
|
180
|
+
for (const surface of account.surfaces) {
|
|
181
|
+
const spec = config.surfaces[surface];
|
|
182
|
+
if (spec === undefined) throw new Error(`surface ${surface} is not defined`);
|
|
183
|
+
for (const tpl of spec.zoneRoles) {
|
|
184
|
+
const only = tpl.zones;
|
|
185
|
+
/**
|
|
186
|
+
* ⛔ INTERNAL ZONES ARE OPT-IN PER TEMPLATE AND THE DEFAULT IS NO (cfroles.py:68-74). Only the
|
|
187
|
+
* word `internal` in zone_types matters; `full` and `secondary` are never checked, so a
|
|
188
|
+
* public zone of any type expands.
|
|
189
|
+
*/
|
|
190
|
+
const internalOk = tpl.zoneTypes.includes('internal');
|
|
191
|
+
for (const zone of zones) {
|
|
192
|
+
if (account.zones !== 'all' && !account.zones.includes(zone.name)) continue; // :76
|
|
193
|
+
if (only !== undefined && only.length > 0 && !only.includes(zone.name)) continue; // :78
|
|
194
|
+
if (zone.internal && !internalOk) continue; // :80
|
|
195
|
+
roles.push(build(account, surface, tpl, zone, tpl.accountGroups));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// cfroles.py:96-103 — `account_groups` is [] on an account role, whatever the yaml says.
|
|
199
|
+
for (const tpl of spec.accountRoles) roles.push(build(account, surface, tpl, null, []));
|
|
200
|
+
}
|
|
201
|
+
return roles;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Every account's roles, grouped by mount.
|
|
206
|
+
*
|
|
207
|
+
* ⛔ A DUPLICATE ROLE ON ONE MOUNT IS AN ERROR HERE. apply-roles.py wrote both and the last write
|
|
208
|
+
* won in silence (apply-roles.py:157-162); declared, they are two resources fighting over one
|
|
209
|
+
* object. ⚠️ A MISSING ZONE INVENTORY IS AN ERROR TOO — an empty one is not, because an account
|
|
210
|
+
* with no zones is real, and the Python expanded it to account roles only.
|
|
211
|
+
*/
|
|
212
|
+
export const expandAll = (
|
|
213
|
+
config: RolesConfig,
|
|
214
|
+
zonesByAccount: Readonly<Record<string, readonly CloudflareZone[]>>,
|
|
215
|
+
): ReadonlyMap<string, readonly ExpandedRole[]> => {
|
|
216
|
+
const byMount = new Map<string, ExpandedRole[]>();
|
|
217
|
+
const seen = new Set<string>();
|
|
218
|
+
for (const account of config.accounts) {
|
|
219
|
+
const zones = zonesByAccount[account.key];
|
|
220
|
+
if (zones === undefined) throw new Error(`no zone inventory for account ${account.key}`);
|
|
221
|
+
for (const role of expandAccount(config, account, zones)) {
|
|
222
|
+
const key = `${role.mount}/roles/${role.name}`;
|
|
223
|
+
if (seen.has(key)) throw new Error(`roles.yaml expands ${key} twice`);
|
|
224
|
+
seen.add(key);
|
|
225
|
+
const mount = byMount.get(role.mount) ?? [];
|
|
226
|
+
mount.push(role);
|
|
227
|
+
byMount.set(role.mount, mount);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return byMount;
|
|
231
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The digest every Bao.* family persists instead of a value — moved here from the retired
|
|
3
|
+
* bao-shell.ts, unchanged, when the package stopped shelling out to `bao`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** ⚠️ Bun's hasher, not node:crypto — this runs under `bun --bun alchemy`. */
|
|
7
|
+
/**
|
|
8
|
+
* The only form a policy body may be hashed or compared in.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ OPENBAO STRIPS THE TRAILING NEWLINE AND THAT ALONE WAS A FOREVER-DIFF. MEASURED on the live
|
|
11
|
+
* server: the assembler emits each fragment followed by a newline, so `homeflare-llm` goes in as
|
|
12
|
+
* 41 lines and `bao policy read` hands back 40 — identical text, one `\n` apart, two different
|
|
13
|
+
* digests. Every plan therefore reported `update`, every deploy rewrote the same policy, and the
|
|
14
|
+
* stack could never settle. It was invisible until the stack planned for the first time today.
|
|
15
|
+
* ★ THE HTTP API HANDS BACK THE SAME STRIPPED TEXT. `bao policy read` printed `data.policy` of
|
|
16
|
+
* `GET /v1/sys/policies/acl/<name>` (openbao v2.6.2 api/sys_policy.go:60-81), which is what
|
|
17
|
+
* policy-wire.ts now reads directly — so the canonicalisation is still load-bearing.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ `trimEnd` ONLY. Leading and interior whitespace are part of the HCL and must not be touched —
|
|
20
|
+
* a policy that differs by an indent really is a different document, and quietly folding that
|
|
21
|
+
* away would hide an edit rather than a formatting artifact.
|
|
22
|
+
*/
|
|
23
|
+
export const canonical = (text: string) => text.trimEnd();
|
|
24
|
+
|
|
25
|
+
export const sha256 = (text: string) =>
|
|
26
|
+
new Bun.CryptoHasher('sha256').update(canonical(text)).digest('hex');
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fake OpenBao for the tests beside it — `Bun.serve` on an ephemeral 127.0.0.1 port, or on a unix
|
|
3
|
+
* socket the way an agent listener can be.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ TEST-ONLY, AND NEVER A REAL VAULT. No provider imports this file, it forwards nowhere, and every
|
|
6
|
+
* helper below runs with an EXPLICIT environment rather than `process.env`, so a BAO_ADDR or
|
|
7
|
+
* BAO_TOKEN in the shell running the tests can never be read or contacted.
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
11
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
12
|
+
import type { BaoEnvironment } from './bao-address.ts';
|
|
13
|
+
import { BaoEnv } from './bao-http.ts';
|
|
14
|
+
|
|
15
|
+
/** One request the fake received. */
|
|
16
|
+
export type Seen = {
|
|
17
|
+
readonly method: string;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly headers: Headers;
|
|
20
|
+
readonly body: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** What the fake answers: a status and a JSON value, or raw text for a body that is not JSON. */
|
|
24
|
+
export type Reply = { readonly status: number; readonly json?: unknown; readonly text?: string };
|
|
25
|
+
|
|
26
|
+
export type Fake = { readonly address: string; readonly seen: Seen[]; stop(): void };
|
|
27
|
+
|
|
28
|
+
/** ★ An answer may be async, so a test can HOLD requests open and count how many overlap. */
|
|
29
|
+
type Answer = (seen: Seen) => Reply | Promise<Reply>;
|
|
30
|
+
|
|
31
|
+
export const fakeBao = (answer: Answer, unix?: string): Fake => {
|
|
32
|
+
const seen: Seen[] = [];
|
|
33
|
+
const fetch = async (request: Request) => {
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
const entry = {
|
|
36
|
+
body: await request.text(),
|
|
37
|
+
headers: request.headers,
|
|
38
|
+
method: request.method,
|
|
39
|
+
path: `${url.pathname}${url.search}`,
|
|
40
|
+
};
|
|
41
|
+
seen.push(entry);
|
|
42
|
+
const reply = await answer(entry);
|
|
43
|
+
const text = reply.text ?? (reply.json === undefined ? null : JSON.stringify(reply.json));
|
|
44
|
+
return new Response(reply.status === 204 ? null : text, {
|
|
45
|
+
headers: { 'content-type': 'application/json' },
|
|
46
|
+
status: reply.status,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
if (unix !== undefined) {
|
|
50
|
+
const server = Bun.serve({ fetch, unix });
|
|
51
|
+
return { address: `unix://${unix}`, seen, stop: () => void server.stop(true) };
|
|
52
|
+
}
|
|
53
|
+
const server = Bun.serve({ fetch, hostname: '127.0.0.1', port: 0 });
|
|
54
|
+
return { address: server.url.origin, seen, stop: () => void server.stop(true) };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Run `body` against a fresh fake, and stop it whatever happens. */
|
|
58
|
+
export const withFake = async (
|
|
59
|
+
answer: Answer,
|
|
60
|
+
body: (bao: Fake) => Promise<void>,
|
|
61
|
+
unix?: string,
|
|
62
|
+
) => {
|
|
63
|
+
const bao = fakeBao(answer, unix);
|
|
64
|
+
try {
|
|
65
|
+
await body(bao);
|
|
66
|
+
} finally {
|
|
67
|
+
bao.stop();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const provided = <A, E>(env: BaoEnvironment, effect: Effect.Effect<A, E, HttpClient.HttpClient>) =>
|
|
72
|
+
effect.pipe(Effect.provideService(BaoEnv, env), Effect.provide(FetchHttpClient.layer));
|
|
73
|
+
|
|
74
|
+
/** The effect's success, through the real FetchHttpClient, under `env` only. */
|
|
75
|
+
export const run = <A, E>(
|
|
76
|
+
env: BaoEnvironment,
|
|
77
|
+
effect: Effect.Effect<A, E, HttpClient.HttpClient>,
|
|
78
|
+
) => Effect.runPromise(provided(env, effect));
|
|
79
|
+
|
|
80
|
+
/** The effect's failure — rejects if it succeeded. */
|
|
81
|
+
export const runFailure = <A, E>(
|
|
82
|
+
env: BaoEnvironment,
|
|
83
|
+
effect: Effect.Effect<A, E, HttpClient.HttpClient>,
|
|
84
|
+
) => Effect.runPromise(provided(env, Effect.flip(effect)));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
readCreateOutput,
|
|
4
|
+
runningForgejoBinary,
|
|
5
|
+
usernamesFrom,
|
|
6
|
+
} from './forgejo-bootstrap-parse.ts';
|
|
7
|
+
|
|
8
|
+
const NIX = '/nix/store/2wagmmlb09s2r0gv839iwmdvrc18kg8h-forgejo-16.0.3/bin/forgejo';
|
|
9
|
+
|
|
10
|
+
describe('runningForgejoBinary', () => {
|
|
11
|
+
test('finds the web process the daemon runs, as ps prints it', () => {
|
|
12
|
+
const ps = [
|
|
13
|
+
'COMMAND',
|
|
14
|
+
'/usr/sbin/cfprefsd agent',
|
|
15
|
+
`${NIX} web --work-path /opt/homeflare/forgejo`,
|
|
16
|
+
`${NIX} admin user list --work-path /opt/homeflare/forgejo`,
|
|
17
|
+
].join('\n');
|
|
18
|
+
expect(runningForgejoBinary(ps)).toBe(NIX);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('ignores a forgejo that is not serving, and one off PATH', () => {
|
|
22
|
+
expect(runningForgejoBinary(`${NIX} admin user list`)).toBeUndefined();
|
|
23
|
+
expect(runningForgejoBinary('/opt/homebrew/bin/forgejo web')).toBeUndefined();
|
|
24
|
+
expect(runningForgejoBinary('')).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('usernamesFrom', () => {
|
|
29
|
+
test('reads the Username column of the measured table', () => {
|
|
30
|
+
const table = [
|
|
31
|
+
'ID Username Email IsActive IsAdmin 2FA',
|
|
32
|
+
'1 tim tim@example.invalid true true false',
|
|
33
|
+
'7 forgejo-provision forgejo-provision@noreply.example true false false',
|
|
34
|
+
'',
|
|
35
|
+
].join('\n');
|
|
36
|
+
expect(usernamesFrom(table)).toEqual(['tim', 'forgejo-provision']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('refuses a table it does not recognise rather than reporting nobody', () => {
|
|
40
|
+
expect(() => usernamesFrom('something else entirely')).toThrow(/unexpected header/);
|
|
41
|
+
expect(() => usernamesFrom('')).toThrow(/unexpected header/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('readCreateOutput', () => {
|
|
46
|
+
const user = 'forgejo-provision';
|
|
47
|
+
|
|
48
|
+
test('captures the password and keeps it out of the lines it echoes', () => {
|
|
49
|
+
const stdout = [
|
|
50
|
+
"generated random password is 'aB3dE5gH7jK9'",
|
|
51
|
+
"New user 'forgejo-provision' has been successfully created!",
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
const out = readCreateOutput(stdout, user);
|
|
55
|
+
expect(out.password).toBe('aB3dE5gH7jK9');
|
|
56
|
+
expect(out.created).toBe(true);
|
|
57
|
+
expect(out.rest.join('\n')).not.toContain('aB3dE5gH7jK9');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('keeps a quote inside the password instead of truncating at it', () => {
|
|
61
|
+
const out = readCreateOutput("generated random password is 'ab'c\"d e'", user);
|
|
62
|
+
expect(out.password).toBe('ab\'c"d e');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('reports not created when the success line names someone else or is missing', () => {
|
|
66
|
+
expect(readCreateOutput("New user 'tim' has been successfully created!", user).created).toBe(
|
|
67
|
+
false,
|
|
68
|
+
);
|
|
69
|
+
const noPassword = readCreateOutput(
|
|
70
|
+
"New user 'forgejo-provision' has been successfully created!",
|
|
71
|
+
user,
|
|
72
|
+
);
|
|
73
|
+
expect(noPassword.created).toBe(true);
|
|
74
|
+
expect(noPassword.password).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what the Forgejo CLI prints, for the forgejo-provision bootstrap (forgejo-bootstrap.ts).
|
|
3
|
+
*
|
|
4
|
+
* ★ PURE, SO THE ONE PART THAT TOUCHES THE PASSWORD IS TESTED WITHOUT ONE. Every format below was
|
|
5
|
+
* read off the running 16.0.3 daemon or its source on 2026-09-14, not assumed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Where the mini's Forgejo keeps its state (hosts/macmini/modules/forgejo.nix:235, app.ini:14). */
|
|
9
|
+
export const FORGEJO_WORK_PATH = '/opt/homeflare/forgejo';
|
|
10
|
+
/** The file the launchd runner renders at every start (forgejo.nix:78). Readable by tim, who runs Forgejo. */
|
|
11
|
+
export const FORGEJO_CONFIG = `${FORGEJO_WORK_PATH}/custom/conf/app.ini`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The binary of the RUNNING server, so the CLI writes the same schema as the daemon serving it.
|
|
15
|
+
*
|
|
16
|
+
* ★ MEASURED: `ps -axo command` shows
|
|
17
|
+
* `/nix/store/<hash>-forgejo-16.0.3/bin/forgejo web --work-path /opt/homeflare/forgejo`. A
|
|
18
|
+
* `forgejo` found on PATH could be a different version from the one holding the database.
|
|
19
|
+
*/
|
|
20
|
+
export const runningForgejoBinary = (psCommands: string): string | undefined => {
|
|
21
|
+
for (const line of psCommands.split('\n')) {
|
|
22
|
+
const [binary, subcommand] = line.trim().split(/\s+/);
|
|
23
|
+
if (
|
|
24
|
+
subcommand === 'web' &&
|
|
25
|
+
binary !== undefined &&
|
|
26
|
+
/^\/nix\/store\/[^/]+-forgejo-[\d.]+\/bin\/forgejo$/.test(binary)
|
|
27
|
+
) {
|
|
28
|
+
return binary;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Usernames from `forgejo admin user list`: a tabwriter table whose header is
|
|
36
|
+
* `ID Username Email IsActive IsAdmin 2FA` (measured, six columns).
|
|
37
|
+
*
|
|
38
|
+
* ⛔ AN UNEXPECTED HEADER THROWS. Parsing a changed table as "no such user" would let the bootstrap
|
|
39
|
+
* create over an account it failed to see.
|
|
40
|
+
*/
|
|
41
|
+
export const usernamesFrom = (table: string): string[] => {
|
|
42
|
+
const rows = table
|
|
43
|
+
.split('\n')
|
|
44
|
+
.map((row) => row.trim())
|
|
45
|
+
.filter((row) => row !== '');
|
|
46
|
+
const header = rows[0]?.split(/\s+/) ?? [];
|
|
47
|
+
if (header[0] !== 'ID' || header[1] !== 'Username') {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`forgejo admin user list printed an unexpected header: ${rows[0] ?? '(nothing)'}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return rows.slice(1).map((row) => row.split(/\s+/)[1] ?? '');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** What `admin user create --random-password` printed on stdout. */
|
|
56
|
+
export type CreateOutput = {
|
|
57
|
+
readonly created: boolean;
|
|
58
|
+
/** ⛔ Never logged, never returned past the one OpenBao write. */
|
|
59
|
+
readonly password: string | undefined;
|
|
60
|
+
/** Every other non-empty stdout line, safe to echo. */
|
|
61
|
+
readonly rest: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ★ THE TWO LINES, FROM forgejo v16.0 cmd/admin_user_create.go:
|
|
66
|
+
* generated random password is '%s'
|
|
67
|
+
* New user '%s' has been successfully created!
|
|
68
|
+
* ⚠️ THE PATTERN IS GREEDY TO THE LAST QUOTE ON PURPOSE. Forgejo's generator can draw a `'` when
|
|
69
|
+
* PASSWORD_COMPLEXITY includes `spec`, and a lazy match would silently store a truncated password
|
|
70
|
+
* that no mint could ever use. The generator never draws a newline, so one line is one password.
|
|
71
|
+
*/
|
|
72
|
+
export const readCreateOutput = (stdout: string, username: string): CreateOutput => {
|
|
73
|
+
let password: string | undefined;
|
|
74
|
+
let created = false;
|
|
75
|
+
const rest: string[] = [];
|
|
76
|
+
for (const line of stdout.split('\n')) {
|
|
77
|
+
const generated = /^generated random password is '(.*)'$/.exec(line);
|
|
78
|
+
if (generated !== null) {
|
|
79
|
+
password = generated[1];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (line === `New user '${username}' has been successfully created!`) created = true;
|
|
83
|
+
if (line.trim() !== '') rest.push(line);
|
|
84
|
+
}
|
|
85
|
+
return { created, password, rest };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** The last few lines of a CLI's stderr, for a failure message. Forgejo prints no secret there. */
|
|
89
|
+
export const tailOf = (text: string, lines = 5): string =>
|
|
90
|
+
text.trim().split('\n').slice(-lines).join(' | ') || '(no output)';
|