@homeflare/alchemy 0.11.0 → 0.13.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.
Files changed (290) hide show
  1. package/README.md +3 -3
  2. package/dist/caddy/admin-guard.d.ts +1 -1
  3. package/dist/caddy/index.js.map +2 -2
  4. package/dist/cloudflare/client.d.ts +1 -1
  5. package/dist/cloudflare/index.js.map +2 -2
  6. package/dist/cloudflare/lock-rules.d.ts +1 -1
  7. package/dist/forgejo/branch-protection-form.d.ts +1 -1
  8. package/dist/forgejo/branch-protection.d.ts +1 -1
  9. package/dist/forgejo/index.js.map +10 -10
  10. package/dist/forgejo/org-actions-secrets.d.ts +1 -1
  11. package/dist/forgejo/org-label.d.ts +1 -1
  12. package/dist/forgejo/org-team.d.ts +1 -1
  13. package/dist/forgejo/repo-webhook.d.ts +1 -1
  14. package/dist/forgejo/repository.d.ts +2 -2
  15. package/dist/forgejo/repository.d.ts.map +1 -1
  16. package/dist/forgejo/resource.d.ts +2 -2
  17. package/dist/forgejo/team-member.d.ts +1 -1
  18. package/dist/openbao/auth-role.d.ts +1 -1
  19. package/dist/openbao/bao-http.d.ts +4 -4
  20. package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
  21. package/dist/openbao/cloudflare-role-form.d.ts +2 -2
  22. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
  23. package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
  24. package/dist/openbao/digest.d.ts +1 -1
  25. package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
  26. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
  27. package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
  28. package/dist/openbao/index.js.map +14 -14
  29. package/dist/openbao/mount.d.ts +1 -1
  30. package/dist/openbao/pki-role.d.ts +1 -1
  31. package/dist/openbao/policy.d.ts +2 -2
  32. package/dist/openbao/proxmox-role-form.d.ts +2 -2
  33. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  34. package/dist/openbao/proxmox-role.d.ts +10 -10
  35. package/dist/openbao/ssh-role.d.ts +3 -3
  36. package/dist/proxmox/acl.d.ts +2 -2
  37. package/dist/proxmox/acl.d.ts.map +1 -1
  38. package/dist/proxmox/alertmanager-body.d.ts +54 -0
  39. package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
  40. package/dist/proxmox/api-token-form.d.ts +2 -2
  41. package/dist/proxmox/api-token.d.ts +11 -11
  42. package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
  43. package/dist/proxmox/ceph-daemon.d.ts +19 -19
  44. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  45. package/dist/proxmox/ceph-flag.d.ts +5 -5
  46. package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
  47. package/dist/proxmox/ceph-fs.d.ts +16 -16
  48. package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
  49. package/dist/proxmox/ceph-osd.d.ts +5 -5
  50. package/dist/proxmox/ceph-pool-form.d.ts +13 -13
  51. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
  52. package/dist/proxmox/ceph-pool.d.ts +3 -3
  53. package/dist/proxmox/client.d.ts +2 -2
  54. package/dist/proxmox/credential-literals.d.ts +36 -0
  55. package/dist/proxmox/credential-literals.d.ts.map +1 -0
  56. package/dist/proxmox/credentials.d.ts +8 -8
  57. package/dist/proxmox/credentials.d.ts.map +1 -1
  58. package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
  59. package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
  60. package/dist/proxmox/fake-pve.d.ts +5 -0
  61. package/dist/proxmox/fake-pve.d.ts.map +1 -1
  62. package/dist/proxmox/firewall-alias-form.d.ts +1 -1
  63. package/dist/proxmox/firewall-alias.d.ts +1 -1
  64. package/dist/proxmox/generated/pbs.d.ts +279 -1
  65. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  66. package/dist/proxmox/generated/pve.d.ts +58 -1
  67. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  68. package/dist/proxmox/group.d.ts +8 -8
  69. package/dist/proxmox/ha-resource.d.ts +3 -2
  70. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  71. package/dist/proxmox/ha-rule-form.d.ts +5 -5
  72. package/dist/proxmox/ha-rule.d.ts +3 -3
  73. package/dist/proxmox/index.d.ts +14 -0
  74. package/dist/proxmox/index.d.ts.map +1 -1
  75. package/dist/proxmox/index.js +935 -223
  76. package/dist/proxmox/index.js.map +69 -56
  77. package/dist/proxmox/lease-cache.d.ts +4 -4
  78. package/dist/proxmox/lxc-harness.d.ts +1 -1
  79. package/dist/proxmox/lxc-props.d.ts +2 -2
  80. package/dist/proxmox/lxc.d.ts +3 -3
  81. package/dist/proxmox/members.d.ts +3 -3
  82. package/dist/proxmox/network-apply-read.d.ts +5 -5
  83. package/dist/proxmox/network-apply.d.ts +9 -9
  84. package/dist/proxmox/node-network-form.d.ts +6 -6
  85. package/dist/proxmox/node-network.d.ts +17 -17
  86. package/dist/proxmox/node-network.d.ts.map +1 -1
  87. package/dist/proxmox/notification-matcher-form.d.ts +100 -0
  88. package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
  89. package/dist/proxmox/notification-matcher.d.ts +46 -0
  90. package/dist/proxmox/notification-matcher.d.ts.map +1 -0
  91. package/dist/proxmox/notification-target.d.ts +3 -2
  92. package/dist/proxmox/notification-target.d.ts.map +1 -1
  93. package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
  94. package/dist/proxmox/pbs-datastore.d.ts +10 -10
  95. package/dist/proxmox/pbs-notification-matcher.d.ts +53 -0
  96. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
  97. package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
  98. package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
  99. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
  100. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
  101. package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
  102. package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
  103. package/dist/proxmox/pbs-notification-target.d.ts +123 -0
  104. package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
  105. package/dist/proxmox/pbs-prune-job.d.ts +2 -2
  106. package/dist/proxmox/pbs-sync-job.d.ts +4 -4
  107. package/dist/proxmox/pbs-verify-job.d.ts +1 -1
  108. package/dist/proxmox/pool.d.ts +4 -4
  109. package/dist/proxmox/provision-baseline.d.ts +74 -0
  110. package/dist/proxmox/provision-baseline.d.ts.map +1 -0
  111. package/dist/proxmox/provision-bootstrap.d.ts +33 -0
  112. package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
  113. package/dist/proxmox/provision-cli-fake.d.ts +38 -0
  114. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
  115. package/dist/proxmox/provision-declare.d.ts +25 -0
  116. package/dist/proxmox/provision-declare.d.ts.map +1 -0
  117. package/dist/proxmox/replication-job.d.ts +9 -8
  118. package/dist/proxmox/replication-job.d.ts.map +1 -1
  119. package/dist/proxmox/resource.d.ts +6 -5
  120. package/dist/proxmox/resource.d.ts.map +1 -1
  121. package/dist/proxmox/sdn-apply-read.d.ts +7 -7
  122. package/dist/proxmox/sdn-apply.d.ts +1 -1
  123. package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
  124. package/dist/proxmox/sdn-subnet.d.ts +5 -5
  125. package/dist/proxmox/sdn-vnet.d.ts +5 -6
  126. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  127. package/dist/proxmox/storage.d.ts +4 -3
  128. package/dist/proxmox/storage.d.ts.map +1 -1
  129. package/dist/proxmox/user.d.ts +3 -2
  130. package/dist/proxmox/user.d.ts.map +1 -1
  131. package/dist/proxmox/values.d.ts +1 -1
  132. package/dist/proxmox/write-only.d.ts +35 -0
  133. package/dist/proxmox/write-only.d.ts.map +1 -0
  134. package/dist/proxmox/zfs-pool-write.d.ts +1 -1
  135. package/dist/proxmox/zfs-pool.d.ts +5 -5
  136. package/dist/talos/credentials.d.ts +1 -1
  137. package/dist/talos/credentials.d.ts.map +1 -1
  138. package/dist/talos/index.js.map +1 -1
  139. package/dist/verify/fake-engine.d.ts +5 -0
  140. package/dist/verify/fake-engine.d.ts.map +1 -1
  141. package/docs/adopt-verify.md +21 -5
  142. package/docs/adopted-deploys.md +9 -1
  143. package/docs/ownership.md +26 -1
  144. package/docs/pbs-alertmanager-body.md +73 -0
  145. package/docs/pbs-notifications.md +163 -0
  146. package/docs/provision-baseline.md +106 -0
  147. package/docs/proxmox-lxc-adopt.md +2 -2
  148. package/docs/proxmox-lxc.md +5 -4
  149. package/docs/proxmox.md +9 -0
  150. package/package.json +1 -1
  151. package/src/caddy/admin-guard.ts +1 -1
  152. package/src/caddy/local-admin.ts +1 -1
  153. package/src/cloudflare/client.ts +1 -1
  154. package/src/cloudflare/lock-rules.ts +1 -1
  155. package/src/forgejo/branch-protection-form.ts +1 -1
  156. package/src/forgejo/branch-protection.ts +1 -1
  157. package/src/forgejo/client.ts +1 -1
  158. package/src/forgejo/org-actions-secrets.ts +1 -1
  159. package/src/forgejo/org-label.ts +1 -1
  160. package/src/forgejo/org-team.ts +1 -1
  161. package/src/forgejo/repo-webhook.ts +1 -1
  162. package/src/forgejo/repository.ts +2 -2
  163. package/src/forgejo/resource.ts +2 -2
  164. package/src/forgejo/team-member.ts +1 -1
  165. package/src/openbao/README.md +4 -4
  166. package/src/openbao/auth-role.ts +1 -1
  167. package/src/openbao/bao-http.test.ts +2 -2
  168. package/src/openbao/bao-http.ts +6 -6
  169. package/src/openbao/bao-status.test.ts +1 -1
  170. package/src/openbao/bao-status.ts +1 -1
  171. package/src/openbao/cloudflare-group-scope.test.ts +1 -1
  172. package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
  173. package/src/openbao/cloudflare-permission-groups.ts +3 -3
  174. package/src/openbao/cloudflare-policy.ts +1 -1
  175. package/src/openbao/cloudflare-role-form.ts +2 -2
  176. package/src/openbao/cloudflare-roles-expand.ts +1 -1
  177. package/src/openbao/digest.ts +1 -1
  178. package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
  179. package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
  180. package/src/openbao/forgejo-bootstrap.ts +4 -4
  181. package/src/openbao/kubernetes-role-form.test.ts +5 -5
  182. package/src/openbao/mount.ts +1 -1
  183. package/src/openbao/pki-role.ts +1 -1
  184. package/src/openbao/policy-wire.test.ts +3 -3
  185. package/src/openbao/policy.ts +2 -2
  186. package/src/openbao/proxmox-role-form.ts +5 -5
  187. package/src/openbao/proxmox-role.ts +11 -11
  188. package/src/openbao/ssh-role.ts +3 -3
  189. package/src/proxmox/acl.ts +4 -4
  190. package/src/proxmox/adopt-noop.test.ts +6 -6
  191. package/src/proxmox/alertmanager-body.ts +125 -0
  192. package/src/proxmox/api-token-form.ts +4 -4
  193. package/src/proxmox/api-token.ts +13 -13
  194. package/src/proxmox/ceph-daemon-form.ts +3 -3
  195. package/src/proxmox/ceph-daemon.ts +20 -20
  196. package/src/proxmox/ceph-flag.ts +7 -7
  197. package/src/proxmox/ceph-fs-wire.ts +7 -7
  198. package/src/proxmox/ceph-fs.ts +18 -18
  199. package/src/proxmox/ceph-osd-tree.ts +16 -16
  200. package/src/proxmox/ceph-osd.ts +6 -6
  201. package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
  202. package/src/proxmox/ceph-pool-form.ts +13 -13
  203. package/src/proxmox/ceph-pool.ts +6 -6
  204. package/src/proxmox/client.test.ts +3 -3
  205. package/src/proxmox/client.ts +3 -3
  206. package/src/proxmox/credential-literals.test.ts +148 -0
  207. package/src/proxmox/credential-literals.ts +74 -0
  208. package/src/proxmox/credentials.test.ts +6 -8
  209. package/src/proxmox/credentials.ts +8 -8
  210. package/src/proxmox/fake-pbs-notify.ts +108 -0
  211. package/src/proxmox/fake-pve.ts +12 -2
  212. package/src/proxmox/firewall-alias-form.ts +1 -1
  213. package/src/proxmox/firewall-alias.ts +1 -1
  214. package/src/proxmox/generated/pbs.ts +330 -1
  215. package/src/proxmox/generated/pve.ts +63 -1
  216. package/src/proxmox/group.ts +10 -10
  217. package/src/proxmox/ha-resource.ts +3 -2
  218. package/src/proxmox/ha-rule-form.ts +5 -5
  219. package/src/proxmox/ha-rule.ts +3 -3
  220. package/src/proxmox/index.test.ts +10 -0
  221. package/src/proxmox/index.ts +32 -0
  222. package/src/proxmox/lease-cache.test.ts +19 -19
  223. package/src/proxmox/lease-cache.ts +4 -4
  224. package/src/proxmox/lxc-adopt.test.ts +3 -3
  225. package/src/proxmox/lxc-create-form.ts +1 -1
  226. package/src/proxmox/lxc-guards.test.ts +4 -4
  227. package/src/proxmox/lxc-harness.ts +3 -3
  228. package/src/proxmox/lxc-judge.test.ts +5 -5
  229. package/src/proxmox/lxc-lifecycle.ts +1 -1
  230. package/src/proxmox/lxc-props.ts +2 -2
  231. package/src/proxmox/lxc-read.test.ts +3 -3
  232. package/src/proxmox/lxc-strict-adopt.test.ts +1 -1
  233. package/src/proxmox/lxc-volume.test.ts +9 -9
  234. package/src/proxmox/lxc-volume.ts +1 -1
  235. package/src/proxmox/lxc-wire.ts +1 -1
  236. package/src/proxmox/lxc.ts +3 -3
  237. package/src/proxmox/members.test.ts +1 -1
  238. package/src/proxmox/members.ts +3 -3
  239. package/src/proxmox/metric-server-otel.ts +2 -2
  240. package/src/proxmox/network-apply-read.ts +10 -10
  241. package/src/proxmox/network-apply.ts +10 -10
  242. package/src/proxmox/node-network-form.ts +7 -7
  243. package/src/proxmox/node-network.ts +18 -18
  244. package/src/proxmox/notification-matcher-form.ts +221 -0
  245. package/src/proxmox/notification-matcher.test.ts +234 -0
  246. package/src/proxmox/notification-matcher.ts +76 -0
  247. package/src/proxmox/notification-target.ts +3 -2
  248. package/src/proxmox/pbs-datastore-guard.ts +1 -1
  249. package/src/proxmox/pbs-datastore.ts +10 -10
  250. package/src/proxmox/pbs-notification-matcher.ts +81 -0
  251. package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
  252. package/src/proxmox/pbs-notification-target-form.ts +229 -0
  253. package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
  254. package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
  255. package/src/proxmox/pbs-notification-target-wire.ts +112 -0
  256. package/src/proxmox/pbs-notification-target.test.ts +226 -0
  257. package/src/proxmox/pbs-notification-target.ts +139 -0
  258. package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
  259. package/src/proxmox/pbs-prune-job.ts +2 -2
  260. package/src/proxmox/pbs-sync-job.ts +4 -4
  261. package/src/proxmox/pbs-verify-job.ts +1 -1
  262. package/src/proxmox/pool.test.ts +2 -2
  263. package/src/proxmox/pool.ts +4 -4
  264. package/src/proxmox/provision-baseline.test.ts +87 -0
  265. package/src/proxmox/provision-baseline.ts +162 -0
  266. package/src/proxmox/provision-bootstrap.test.ts +132 -0
  267. package/src/proxmox/provision-bootstrap.ts +135 -0
  268. package/src/proxmox/provision-cli-fake.ts +217 -0
  269. package/src/proxmox/provision-declare.test.ts +111 -0
  270. package/src/proxmox/provision-declare.ts +103 -0
  271. package/src/proxmox/replication-job.ts +9 -8
  272. package/src/proxmox/resource.ts +9 -9
  273. package/src/proxmox/sdn-apply-read.test.ts +16 -16
  274. package/src/proxmox/sdn-apply-read.ts +7 -7
  275. package/src/proxmox/sdn-apply.ts +3 -3
  276. package/src/proxmox/sdn-subnet-form.ts +1 -1
  277. package/src/proxmox/sdn-subnet.ts +5 -5
  278. package/src/proxmox/sdn-vnet.ts +5 -6
  279. package/src/proxmox/storage.ts +4 -3
  280. package/src/proxmox/user.ts +3 -2
  281. package/src/proxmox/values.test.ts +2 -2
  282. package/src/proxmox/values.ts +1 -1
  283. package/src/proxmox/write-only.test.ts +75 -0
  284. package/src/proxmox/write-only.ts +106 -0
  285. package/src/proxmox/zfs-pool-write.ts +1 -1
  286. package/src/proxmox/zfs-pool.ts +6 -6
  287. package/src/talos/credentials.ts +3 -3
  288. package/src/talos/values.test.ts +9 -9
  289. package/src/verify/cli.test.ts +1 -1
  290. package/src/verify/fake-engine.ts +8 -1
@@ -0,0 +1,221 @@
1
+ /**
2
+ * A notification matcher — the rule that routes a notification to its targets — as ONE shape
3
+ * shared by PVE (`Proxmox.NotificationMatcher`) and PBS (`Pbs.NotificationMatcher`).
4
+ *
5
+ * ★ ONE FILE FOR BOTH PRODUCTS BECAUSE IT IS ONE IMPLEMENTATION. Both servers run the same Rust
6
+ * crate, `proxmox-notify` (matcher.rs), PVE through its Perl bindings; the schemas in
7
+ * generated/pve.ts and generated/pbs.ts carry the same nine fields, and the forms below are
8
+ * typed against BOTH, so a drift on either side fails `tsc` here rather than on a server.
9
+ *
10
+ * ⚠️ A MATCHER IS ITS WHOLE RULE, SO EVERY RULE FIELD IS COMPARED, DECLARED OR NOT. Most families
11
+ * here leave an undeclared field alone (backup-job.ts's trade). A matcher cannot: an undeclared
12
+ * `match-field` on a matcher that has one is a declaration claiming "route everything" over a
13
+ * server that routes one job type — and `invert-match` flips the meaning of every other line.
14
+ * So `match-*` default to empty, `mode` to `all`, `invert-match` and `disable` to false, all are
15
+ * compared, and a PUT sends all of them (an emptied list as `delete`). Only `comment` keeps the
16
+ * usual "undeclared is unmanaged" rule, because nothing routes on it.
17
+ * ★ THE COST IS THAT ADOPTING A MATCHER MEANS DECLARING IT AS IT IS. PBS ships `default-matcher`
18
+ * with `invert-match`, a `match-field` and a `match-severity` (measured on PBS 4.2, 2026-09-22),
19
+ * so adopting it as-is means writing those three down. A declaration that leaves them out
20
+ * plans an UPDATE, which is the plan telling the truth about what the deploy would do.
21
+ */
22
+ import type {
23
+ ClusterNotificationsMatchersNamePutParams,
24
+ ClusterNotificationsMatchersPostParams,
25
+ } from './generated/pve.ts';
26
+ import type {
27
+ ConfigNotificationsMatchersNamePutParams,
28
+ ConfigNotificationsMatchersPostParams,
29
+ } from './generated/pbs.ts';
30
+ import { bool, text } from './values.ts';
31
+
32
+ /** Everything a matcher declares except where it lives — the resource files add `target`. */
33
+ export interface NotificationMatcherFields {
34
+ /** The primary key, a safe id (2–32 chars on PBS). Changing it is a replace. */
35
+ name: string;
36
+ /**
37
+ * ⛔ THE TARGETS, BY NAME, AND REQUIRED: a matcher that notifies nobody is a routing rule for
38
+ * silence. `[]` is still declarable, and clears the list.
39
+ * ⚠️ `targets`, NOT THE SERVER'S `target`: every resource here already spends `target` on the
40
+ * HOST (resource.ts `WithTarget`), and one key cannot mean both. The wire still says `target`.
41
+ */
42
+ targets: readonly string[];
43
+ /**
44
+ * Severity rules. EACH ITEM IS ITSELF A COMMA LIST: `'warning,error'` matches either.
45
+ * ⛔ `['warning', 'error']` IS NOT THE SAME RULE AND UNDER `mode: 'all'` NEVER MATCHES. Items are
46
+ * combined with `mode`; the severities inside one item are always OR-ed (matcher.rs
47
+ * `SeverityMatcher`). A notification has one severity, so two items under AND ask for a
48
+ * notification that is both — and routing nothing is the quietest failure a matcher has.
49
+ * Severities: `info`, `notice`, `warning`, `error`, `unknown`.
50
+ */
51
+ 'match-severity'?: readonly string[];
52
+ /**
53
+ * Metadata rules, `exact:<field>=<v1>,<v2>` or `regex:<field>=<re>`. ⚠️ THE PREFIX IS REQUIRED:
54
+ * the schema's pattern makes it optional and the parser then refuses it. A notification WITHOUT
55
+ * the field never matches its rule — so `exact:job-id=…` excludes every garbage collection.
56
+ */
57
+ 'match-field'?: readonly string[];
58
+ /** Time windows, e.g. `mon-fri 9:00-17:00`. Compared as written; the server keeps the text. */
59
+ 'match-calendar'?: readonly string[];
60
+ /** How the rule items combine. The server's default, and this package's, is `all`. */
61
+ mode?: 'all' | 'any';
62
+ /** Invert the whole rule. ⚠️ IGNORED WHEN THERE ARE NO RULES — an empty matcher always matches. */
63
+ 'invert-match'?: boolean;
64
+ /** A disabled matcher routes nothing. */
65
+ disable?: boolean;
66
+ /** Free text. The one field left alone when undeclared; `''` clears it. */
67
+ comment?: string;
68
+ }
69
+
70
+ /** ⚠️ Lists are CANONICAL and SORTED, so a re-ordered declaration is not a diff. */
71
+ export interface NotificationMatcherAttributes {
72
+ name: string;
73
+ /**
74
+ * `builtin`, `modified-builtin` or `user-created`. REPORTED, NEVER COMPARED.
75
+ * ⛔ ON A BUILT-IN A DELETE REVERTS RATHER THAN REMOVES, and reports success either way — Alchemy
76
+ * drops the row while `default-matcher` goes on routing. Read this before destroying one.
77
+ * ★ MEASURED ON THE PER-OBJECT READ, which the published schema does not list: PVE 9.2.11 and
78
+ * PBS 4.2 both return it from `…/matchers/default-matcher` (2026-09-22).
79
+ */
80
+ origin: string;
81
+ comment: string;
82
+ disable: boolean;
83
+ 'invert-match': boolean;
84
+ mode: string;
85
+ targets: readonly string[];
86
+ 'match-severity': readonly string[];
87
+ 'match-field': readonly string[];
88
+ 'match-calendar': readonly string[];
89
+ }
90
+
91
+ const list = (value: unknown): readonly string[] =>
92
+ Array.isArray(value) ? value.map((item: unknown) => String(item)) : [];
93
+
94
+ /** Trimmed items, empty ones dropped. */
95
+ const items = (values: readonly string[], canonical: (item: string) => string): string[] =>
96
+ values.map((item) => canonical(item.trim())).filter((item) => item !== '');
97
+
98
+ /**
99
+ * ⚠️ THE SERVER WRITES A RULE BACK IN ITS OWN SPELLING, SO BOTH SIDES ARE SPELLED ITS WAY FIRST.
100
+ * matcher.rs parses `exact:type=gc, verify` into values and `Display`s them joined by a bare
101
+ * comma; a severity item is split, trimmed and re-joined the same way. Without this a
102
+ * declaration with a space after a comma would plan an update forever.
103
+ */
104
+ const severity = (item: string): string =>
105
+ item
106
+ .split(',')
107
+ .map((part) => part.trim())
108
+ .filter((part) => part !== '')
109
+ .sort()
110
+ .join(',');
111
+
112
+ const field = (item: string): string => {
113
+ if (!item.startsWith('exact:')) return item;
114
+ const at = item.indexOf('=');
115
+ if (at < 0) return item;
116
+ const values = item.slice(at + 1).split(',');
117
+ return `${item.slice(0, at)}=${values.map((value) => value.trim()).join(',')}`;
118
+ };
119
+
120
+ const same = (item: string): string => item;
121
+
122
+ /** A declaration's lists, canonical and sorted — the form `matches` compares. */
123
+ export const canonicalRules = (fields: NotificationMatcherFields) => ({
124
+ 'match-calendar': items(fields['match-calendar'] ?? [], same).sort(),
125
+ 'match-field': items(fields['match-field'] ?? [], field).sort(),
126
+ 'match-severity': items(fields['match-severity'] ?? [], severity).sort(),
127
+ targets: items(fields.targets, same).sort(),
128
+ });
129
+
130
+ export const matcherAttributes = (
131
+ live: Record<string, unknown>,
132
+ name: string,
133
+ ): NotificationMatcherAttributes => ({
134
+ comment: text(live['comment']),
135
+ disable: bool(live['disable']),
136
+ 'invert-match': bool(live['invert-match']),
137
+ 'match-calendar': items(list(live['match-calendar']), same).sort(),
138
+ 'match-field': items(list(live['match-field']), field).sort(),
139
+ 'match-severity': items(list(live['match-severity']), severity).sort(),
140
+ // ⚠️ ABSENT IS `all` — matcher.rs `mode.unwrap_or_default()`, and `All` is the default variant.
141
+ mode: text(live['mode'], 'all') || 'all',
142
+ name,
143
+ origin: text(live['origin']),
144
+ targets: items(list(live['target']), same).sort(),
145
+ });
146
+
147
+ const equal = (a: readonly string[], b: readonly string[]): boolean =>
148
+ a.length === b.length && a.every((item, index) => item === b[index]);
149
+
150
+ export const matcherMatches = (
151
+ attributes: NotificationMatcherAttributes,
152
+ fields: NotificationMatcherFields,
153
+ ): boolean => {
154
+ const rules = canonicalRules(fields);
155
+ return (
156
+ attributes.disable === (fields.disable === true) &&
157
+ attributes['invert-match'] === (fields['invert-match'] === true) &&
158
+ attributes.mode === (fields.mode ?? 'all') &&
159
+ equal(attributes.targets, rules.targets) &&
160
+ equal(attributes['match-severity'], rules['match-severity']) &&
161
+ equal(attributes['match-field'], rules['match-field']) &&
162
+ equal(attributes['match-calendar'], rules['match-calendar']) &&
163
+ (fields.comment === undefined || attributes.comment === fields.comment)
164
+ );
165
+ };
166
+
167
+ type Deletable = NonNullable<ConfigNotificationsMatchersNamePutParams['delete']>[number];
168
+ type Lists = 'match-calendar' | 'match-field' | 'match-severity' | 'target';
169
+
170
+ /**
171
+ * The fields both forms send. ⚠️ LISTS GO AS ARRAYS — REPEATED KEYS ON THE WIRE (client.ts
172
+ * `encode`) — because every list here is `type: array` on both servers, and a comma inside a
173
+ * severity item or an `exact:` rule means a comma-joined string could not carry them anyway.
174
+ */
175
+ const always = (fields: NotificationMatcherFields) => ({
176
+ disable: fields.disable === true ? ('1' as const) : ('0' as const),
177
+ 'invert-match': fields['invert-match'] === true ? ('1' as const) : ('0' as const),
178
+ mode: fields.mode ?? ('all' as const),
179
+ });
180
+
181
+ const lists = (fields: NotificationMatcherFields): Partial<Record<Lists, readonly string[]>> => {
182
+ const rules = canonicalRules(fields);
183
+ const out: Partial<Record<Lists, readonly string[]>> = {};
184
+ if (rules['match-calendar'].length > 0) out['match-calendar'] = rules['match-calendar'];
185
+ if (rules['match-field'].length > 0) out['match-field'] = rules['match-field'];
186
+ if (rules['match-severity'].length > 0) out['match-severity'] = rules['match-severity'];
187
+ if (rules.targets.length > 0) out.target = rules.targets;
188
+ return out;
189
+ };
190
+
191
+ export type MatcherCreateForm = ConfigNotificationsMatchersPostParams &
192
+ ClusterNotificationsMatchersPostParams;
193
+ export type MatcherUpdateForm = ConfigNotificationsMatchersNamePutParams &
194
+ ClusterNotificationsMatchersNamePutParams;
195
+
196
+ /** ⛔ `name` is create-only: the body carries it once and the path forever after. */
197
+ export const matcherCreateForm = (fields: NotificationMatcherFields): MatcherCreateForm => ({
198
+ ...always(fields),
199
+ ...lists(fields),
200
+ ...(fields.comment === undefined || fields.comment === '' ? {} : { comment: fields.comment }),
201
+ name: fields.name,
202
+ });
203
+
204
+ /**
205
+ * ⛔ AN EMPTIED LIST IS A `delete`, NOT AN EMPTY VALUE. A form cannot carry an empty array — no
206
+ * key is sent at all — so without the `delete` entry a declaration of `'match-field': []`
207
+ * would plan an update, PUT nothing for that field, and plan the same update forever.
208
+ */
209
+ export const matcherUpdateForm = (fields: NotificationMatcherFields): MatcherUpdateForm => {
210
+ const present = lists(fields);
211
+ const cleared: Deletable[] = (
212
+ ['match-calendar', 'match-field', 'match-severity', 'target'] as const
213
+ ).filter((key) => present[key] === undefined);
214
+ if (fields.comment === '') cleared.push('comment');
215
+ return {
216
+ ...always(fields),
217
+ ...present,
218
+ ...(fields.comment === undefined || fields.comment === '' ? {} : { comment: fields.comment }),
219
+ ...(cleared.length === 0 ? {} : { delete: cleared }),
220
+ };
221
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `Proxmox.NotificationMatcher` and `Pbs.NotificationMatcher` — forms against the generated
3
+ * schemas, the canonical spellings, and both built-in `default-matcher`s adopted as they are.
4
+ *
5
+ * ★ THE LIVE FIXTURES ARE THE MEASURED ONES (2026-09-22): PVE 9.2.11 `pvesh get
6
+ * /cluster/notifications/matchers/default-matcher` and PBS 4.2 `proxmox-backup-manager
7
+ * notification matcher show default-matcher`, comment text shortened.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import * as Layer from 'effect/Layer';
11
+ import { engineOver } from '../verify/fake-engine.ts';
12
+ import type { PbsTarget } from './credentials.ts';
13
+ import { fakeNotify } from './fake-pbs-notify.ts';
14
+ import { FAKE_TARGET, withoutBao } from './fake-pve.ts';
15
+ import type {
16
+ ClusterNotificationsMatchersNamePutParams,
17
+ ClusterNotificationsMatchersPostParams,
18
+ } from './generated/pve.ts';
19
+ import type {
20
+ ConfigNotificationsMatchersNamePutParams,
21
+ ConfigNotificationsMatchersPostParams,
22
+ } from './generated/pbs.ts';
23
+ import {
24
+ ProxmoxNotificationMatcher,
25
+ ProxmoxNotificationMatcherProvider,
26
+ } from './notification-matcher.ts';
27
+ import {
28
+ type NotificationMatcherFields,
29
+ matcherAttributes,
30
+ matcherCreateForm,
31
+ matcherMatches,
32
+ matcherUpdateForm,
33
+ } from './notification-matcher-form.ts';
34
+ import {
35
+ PbsNotificationMatcher,
36
+ PbsNotificationMatcherProvider,
37
+ } from './pbs-notification-matcher.ts';
38
+
39
+ const PBS: PbsTarget = { api: 'https://pbs.test:8007/api2/json', mount: 'pbs-test', scheme: 'pbs' };
40
+ const PVE_PATH = 'cluster/notifications/matchers/default-matcher';
41
+ const PBS_PATH = 'config/notifications/matchers/default-matcher';
42
+
43
+ /** ★ The live shape, `digest` included: the per-object GET carries one, the collection GET not. */
44
+ const PVE_DEFAULT = {
45
+ comment: 'Route all notifications to mail-to-root',
46
+ digest: 'd1'.repeat(32), // placeholder; a real one is a sha256 of notifications.cfg
47
+ mode: 'all',
48
+ name: 'default-matcher',
49
+ origin: 'builtin',
50
+ target: ['mail-to-root'],
51
+ };
52
+ const PBS_DEFAULT = {
53
+ comment: 'Route everything but successful prune jobs',
54
+ 'invert-match': true,
55
+ 'match-field': ['exact:type=prune'],
56
+ 'match-severity': ['info'],
57
+ mode: 'all',
58
+ name: 'default-matcher',
59
+ origin: 'builtin',
60
+ target: ['mail-to-root'],
61
+ };
62
+
63
+ const pbsDefault = (over: Partial<NotificationMatcherFields> = {}) =>
64
+ PbsNotificationMatcher('default-matcher', {
65
+ 'invert-match': true,
66
+ 'match-field': ['exact:type=prune'],
67
+ 'match-severity': ['info'],
68
+ name: 'default-matcher',
69
+ target: PBS,
70
+ targets: ['mail-to-root'],
71
+ ...over,
72
+ });
73
+
74
+ describe('forms are typed against BOTH generated schemas', () => {
75
+ const fields: NotificationMatcherFields = {
76
+ 'match-severity': ['error'],
77
+ name: 'pager',
78
+ targets: ['hook'],
79
+ };
80
+
81
+ test('create and update assign to PVE and PBS params without a cast', () => {
82
+ const pveCreate: ClusterNotificationsMatchersPostParams = matcherCreateForm(fields);
83
+ const pbsCreate: ConfigNotificationsMatchersPostParams = matcherCreateForm(fields);
84
+ const pveUpdate: ClusterNotificationsMatchersNamePutParams = matcherUpdateForm(fields);
85
+ const pbsUpdate: ConfigNotificationsMatchersNamePutParams = matcherUpdateForm(fields);
86
+ expect(pveCreate).toEqual(pbsCreate);
87
+ expect(pveUpdate).toEqual(pbsUpdate);
88
+ });
89
+
90
+ test('would fail tsc if `name` were dropped from a create', () => {
91
+ // @ts-expect-error name is required on POST .../matchers
92
+ const bad: ConfigNotificationsMatchersPostParams = { target: ['hook'] };
93
+ expect('name' in bad).toBe(false);
94
+ });
95
+
96
+ test('create sends lists as lists and the three always-compared flags', () => {
97
+ expect(matcherCreateForm(fields)).toEqual({
98
+ disable: '0',
99
+ 'invert-match': '0',
100
+ 'match-severity': ['error'],
101
+ mode: 'all',
102
+ name: 'pager',
103
+ target: ['hook'],
104
+ });
105
+ });
106
+
107
+ test('update clears every emptied rule list with `delete`, and an empty comment too', () => {
108
+ expect(matcherUpdateForm({ ...fields, comment: '', 'match-severity': [] })).toEqual({
109
+ delete: ['match-calendar', 'match-field', 'match-severity', 'comment'],
110
+ disable: '0',
111
+ 'invert-match': '0',
112
+ mode: 'all',
113
+ target: ['hook'],
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('matches reads both sides in the server spelling', () => {
119
+ const live = matcherAttributes(
120
+ {
121
+ 'match-field': ['exact:type=gc,verify'],
122
+ 'match-severity': ['error,warning'],
123
+ target: ['b', 'a'],
124
+ },
125
+ 'm',
126
+ );
127
+ const base: NotificationMatcherFields = { name: 'm', targets: ['a', 'b'] };
128
+
129
+ test('spaces after commas and list order are not a diff; absent mode is `all`', () => {
130
+ expect(
131
+ matcherMatches(live, {
132
+ ...base,
133
+ 'match-field': ['exact:type=gc, verify'],
134
+ 'match-severity': ['warning, error'],
135
+ mode: 'all',
136
+ }),
137
+ ).toBe(true);
138
+ });
139
+
140
+ test('an undeclared rule is compared as empty — a matcher is its whole rule', () => {
141
+ expect(matcherMatches(live, { ...base, 'match-severity': ['error,warning'] })).toBe(false);
142
+ });
143
+
144
+ test('invert-match, mode and disable are compared against their defaults', () => {
145
+ const rules = { 'match-field': ['exact:type=gc,verify'], 'match-severity': ['error,warning'] };
146
+ expect(matcherMatches(live, { ...base, ...rules })).toBe(true);
147
+ expect(matcherMatches(live, { ...base, ...rules, 'invert-match': true })).toBe(false);
148
+ expect(matcherMatches(live, { ...base, ...rules, mode: 'any' })).toBe(false);
149
+ expect(matcherMatches(live, { ...base, ...rules, disable: true })).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe('adopting a built-in default-matcher as it is only reads', () => {
154
+ test('PVE: target and name, nothing else, is a noop with no write', async () => {
155
+ const fake = fakeNotify({ [PVE_PATH]: PVE_DEFAULT });
156
+ const declare = () =>
157
+ ProxmoxNotificationMatcher('default-matcher', {
158
+ name: 'default-matcher',
159
+ target: FAKE_TARGET,
160
+ targets: ['mail-to-root'],
161
+ });
162
+ await withoutBao(async () => {
163
+ const engine = engineOver(
164
+ ProxmoxNotificationMatcherProvider().pipe(Layer.provideMerge(fake.layer)),
165
+ );
166
+ expect((await engine.verify(declare())).rows).toEqual([
167
+ expect.objectContaining({ diff: 'noop', ok: true }),
168
+ ]);
169
+ expect(Object.values(await engine.deploy(declare()))).toEqual(['adopted']);
170
+ expect((await engine.verify(declare(), { all: true })).rows[0]).toMatchObject({
171
+ diff: 'noop',
172
+ });
173
+ });
174
+ expect(fake.writes()).toEqual([]);
175
+ });
176
+
177
+ test('PBS: its three rule fields declared is a noop with no write', async () => {
178
+ const fake = fakeNotify({ [PBS_PATH]: PBS_DEFAULT });
179
+ await withoutBao(async () => {
180
+ const engine = engineOver(
181
+ PbsNotificationMatcherProvider().pipe(Layer.provideMerge(fake.layer)),
182
+ );
183
+ expect((await engine.verify(pbsDefault())).rows).toEqual([
184
+ expect.objectContaining({ diff: 'noop', ok: true }),
185
+ ]);
186
+ await engine.deploy(pbsDefault());
187
+ });
188
+ expect(fake.writes()).toEqual([]);
189
+ });
190
+
191
+ test('PBS: leaving the rules out plans an update, and the PUT clears them by name', async () => {
192
+ const fake = fakeNotify({ [PBS_PATH]: PBS_DEFAULT });
193
+ const bare = () =>
194
+ pbsDefault({ 'invert-match': false, 'match-field': [], 'match-severity': [] });
195
+ await withoutBao(async () => {
196
+ const engine = engineOver(
197
+ PbsNotificationMatcherProvider().pipe(Layer.provideMerge(fake.layer)),
198
+ );
199
+ expect((await engine.verify(bare())).rows[0]).toMatchObject({ diff: 'update', ok: false });
200
+ await engine.deploy(bare());
201
+ });
202
+ expect(fake.writes()).toEqual([`PUT ${PBS_PATH}`]);
203
+ const put = fake.calls.find((call) => call.method === 'PUT');
204
+ expect(put?.pairs).toContainEqual(['delete', 'match-field']);
205
+ expect(put?.pairs).toContainEqual(['delete', 'match-severity']);
206
+ expect(put?.pairs).toContainEqual(['invert-match', '0']);
207
+ expect(fake.objects.get(PBS_PATH)?.['match-field']).toBeUndefined();
208
+ });
209
+
210
+ test('PBS: a new matcher is POSTed with each list item as its own repeated key', async () => {
211
+ const fake = fakeNotify();
212
+ const pager = () =>
213
+ PbsNotificationMatcher('pager', {
214
+ 'match-field': ['exact:type=gc,sync,verify'],
215
+ 'match-severity': ['error'],
216
+ name: 'pager',
217
+ target: PBS,
218
+ targets: ['alertmanager', 'mail-to-root'],
219
+ });
220
+ await withoutBao(async () => {
221
+ const engine = engineOver(
222
+ PbsNotificationMatcherProvider().pipe(Layer.provideMerge(fake.layer)),
223
+ );
224
+ await engine.deploy(pager());
225
+ expect((await engine.verify(pager(), { all: true })).rows[0]).toMatchObject({ diff: 'noop' });
226
+ });
227
+ const post = fake.calls.find((call) => call.method === 'POST');
228
+ expect(post?.path).toBe('config/notifications/matchers');
229
+ expect(post?.pairs.filter(([key]) => key === 'target')).toEqual([
230
+ ['target', 'alertmanager'],
231
+ ['target', 'mail-to-root'],
232
+ ]);
233
+ });
234
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `Proxmox.NotificationMatcher` — which notifications a PVE cluster sends to which targets.
3
+ *
4
+ * ★ THE HALF OF THE NOTIFICATION SYSTEM notification-target.ts LEAVES OPEN. A target is where a
5
+ * message CAN go; a matcher decides whether it DOES. A cluster ships one, `default-matcher`,
6
+ * routing everything to `mail-to-root`, and a target no matcher names receives nothing — so a
7
+ * declared webhook with no matcher is a delivery path that is never used, and looks finished.
8
+ *
9
+ * ★ ADOPTING THE BUILT-IN AS IT IS: declare what it holds and nothing more. MEASURED on PVE
10
+ * 9.2.11 (2026-09-22, `pvesh get /cluster/notifications/matchers/default-matcher`): mode `all`,
11
+ * one target `mail-to-root`, no rules, the comment "Route all notifications to mail-to-root".
12
+ * ```ts
13
+ * ProxmoxNotificationMatcher('default-matcher', {
14
+ * target: cluster, name: 'default-matcher', targets: ['mail-to-root'],
15
+ * });
16
+ * ```
17
+ * That plans `noop` and deploys with no write (notification-matcher.test.ts). Leaving `comment`
18
+ * out leaves it alone; every rule field is compared — notification-matcher-form.ts says why.
19
+ *
20
+ * ★ WHAT THIS NEEDS, READ OFF THE CLUSTER'S OWN SCHEMA (generated/pve.ts source, 9.2.11):
21
+ * read GET matchers/{name} Mapping.Audit or Mapping.Modify on /mapping/notifications
22
+ * write POST, PUT, DELETE Mapping.Modify on /mapping/notifications
23
+ * The same pair as `Proxmox.NotificationTarget`, with that file's ⚠️ about granting it on `/`.
24
+ * ⛔ `Mapping.Use` IS NOT ENOUGH, though the COLLECTION GET accepts it: the per-object read this
25
+ * family makes lists only Audit and Modify (pve-manager API2/Cluster/Notifications.pm
26
+ * `get_matcher`, read at HEAD 2026-09-22 — corrected here; PR 95 said Use sufficed).
27
+ * ⛔ A MISSING READ PRIVILEGE LOOKS LIKE "ABSENT", not like a 403 — see the same file. For
28
+ * `default-matcher` that turns an adoption into a planned CREATE that PVE refuses as a
29
+ * duplicate: the noop below needs a read lease holding Mapping.Audit.
30
+ */
31
+ import { Resource } from 'alchemy';
32
+ import * as Provider from 'alchemy/Provider';
33
+ import * as Effect from 'effect/Effect';
34
+ import {
35
+ type NotificationMatcherAttributes,
36
+ type NotificationMatcherFields,
37
+ matcherAttributes,
38
+ matcherCreateForm,
39
+ matcherMatches,
40
+ matcherUpdateForm,
41
+ } from './notification-matcher-form.ts';
42
+ import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
43
+
44
+ export interface NotificationMatcherProps extends NotificationMatcherFields, WithTarget {}
45
+
46
+ export interface ProxmoxNotificationMatcher extends Resource<
47
+ 'Proxmox.NotificationMatcher',
48
+ NotificationMatcherProps,
49
+ NotificationMatcherAttributes,
50
+ never,
51
+ PveRequirements
52
+ > {}
53
+
54
+ /**
55
+ * ⚠️ NO `defaultRemovalPolicy: 'retain'`, for notification-target.ts's reason: a matcher holds no
56
+ * data, and a plan line is the only warning a delete gets. On `default-matcher` the delete
57
+ * REVERTS it to the shipped rule instead — see `origin` on the attributes.
58
+ */
59
+ export const ProxmoxNotificationMatcher = Resource<ProxmoxNotificationMatcher>(
60
+ 'Proxmox.NotificationMatcher',
61
+ );
62
+
63
+ const handlers = pveHandlers<NotificationMatcherProps, NotificationMatcherAttributes>({
64
+ attributes: (live, props) => matcherAttributes(live, props.name),
65
+ collection: () => 'cluster/notifications/matchers',
66
+ createForm: matcherCreateForm,
67
+ matches: matcherMatches,
68
+ path: (props) => `cluster/notifications/matchers/${props.name}`,
69
+ updateForm: matcherUpdateForm,
70
+ });
71
+
72
+ export const ProxmoxNotificationMatcherProvider = () =>
73
+ Provider.effect(
74
+ ProxmoxNotificationMatcher,
75
+ Effect.succeed(ProxmoxNotificationMatcher.Provider.of(handlers)),
76
+ );
@@ -39,8 +39,9 @@
39
39
  * reconcile POST + PUT Mapping.Modify on /mapping/notifications AND one of
40
40
  * Sys.Audit / Sys.Modify / Sys.AccessNetwork on /
41
41
  * delete DELETE Mapping.Modify on /mapping/notifications
42
- * `LXCProvisioner` already carries Sys.Audit on `/` (see lxc.ts), so the delta is the two Mapping
43
- * privileges — notably NOT the `Sys.Modify` a backup job costs.
42
+ * The provision role already carried Sys.Audit on `/` (see lxc.ts), so the delta was the two
43
+ * Mapping privileges — notably NOT the `Sys.Modify` a backup job costs. `PROVISION_PRIVILEGES`
44
+ * carries both on `/`: the baseline took the trade below knowingly.
44
45
  * ⚠️ PREFER A SECOND ROLE GRANTED AT `/mapping/notifications` OVER WIDENING THE PROVISIONING ROLE
45
46
  * ON `/`. `/mapping` also holds the PCI and USB passthrough maps, so Mapping.Modify at the root
46
47
  * lets a credential scoped to make containers rewire somebody's hardware.
@@ -54,7 +54,7 @@ export const guardPath = (live: PbsDatastoreAttributes, props: PbsDatastoreProps
54
54
  *
55
55
  * ⚠️ AN UNDECLARED `backend` IS NOT A DIVERGENCE. Undeclared is unmanaged everywhere in this
56
56
  * family, and this guard keeps that rule: it fires only when the declaration says something and
57
- * PBS says something else. Adopting `r2-offsite` without mentioning `backend` still plans noop.
57
+ * PBS says something else. Adopting `offsite` without mentioning `backend` still plans noop.
58
58
  *
59
59
  * ⚠️ COMPARED AS A CANONICAL PROPERTY STRING, so `bucket=x,type=s3` and `type=s3,bucket=x` are the
60
60
  * same backend and key order cannot deadlock a plan — the reason `samePath` trims a slash.
@@ -10,7 +10,7 @@
10
10
  * `{"data":{"pong":true}}` against a Let's Encrypt certificate for CN=pbs.example.com;
11
11
  * `/version` and `/config/datastore` answer 401 "authentication failed";
12
12
  * `pbs.mgmt.example.com:8007` answers 401 and its certificate verifies too; and
13
- * `/access/domains` lists the realms `pbs`, `pam` and an openid realm `Schenanigans`. So the
13
+ * `/access/domains` lists the realms `pbs`, `pam` and an openid realm. So the
14
14
  * prefix, the `{"data": …}` envelope and the strict-TLS story are PVE's exactly — and only the
15
15
  * credential differs. There is no `pve` realm here; a PVE token cannot authenticate at all.
16
16
  * The header differs too, which is why `pve()` exists rather than `pve()` being reused — see the
@@ -18,18 +18,18 @@
18
18
  *
19
19
  * ⛔ THERE IS NO OPENBAO MOUNT FOR PBS TODAY, SO NOTHING HERE CAN DEPLOY YET. `PbsTarget.mount` is
20
20
  * a parameter for the reason `PveTarget.mount` is — the provider must be able to leave this
21
- * estate — but the estate has nothing to put in it. MEASURED: under the `claude-code` approle,
22
- * `bao token capabilities` answers `deny` for `proxmox-tb4/creds/{read,provision}`,
23
- * `proxmox-ops/creds/read` and every `…pbs…` path tried, and `sys/mounts` is 403 — so I could
21
+ * estate — but the estate has nothing to put in it. MEASURED: under the agent's approle,
22
+ * `bao token capabilities` answers `deny` for `proxmox-c1/creds/{read,provision}`,
23
+ * `proxmox-c2/creds/read` and every `…pbs…` path tried, and `sys/mounts` is 403 — so I could
24
24
  * not enumerate the mounts to PROVE a PBS one is absent. That it is absent is the brief's
25
25
  * statement, not my measurement.
26
26
  * ★ WHAT THE MOUNT MUST VEND, so `mint()` is reused unchanged: `bao read -format=json
27
27
  * <mount>/creds/<role>` answering `{"data":{"token_id":…,"secret":…}}`, where `token_id` is a
28
28
  * PBS token id `user@realm!tokenname` — `hf-read@pbs!…` and `hf-provision@pbs!…`. Same short,
29
- * non-renewable lease contract as `proxmox-tb4`.
29
+ * non-renewable lease contract as `proxmox-c1`.
30
30
  * ⚠️ PBS HAS NO DYNAMIC-SECRETS PLUGIN OF ITS OWN — OpenBao's Proxmox support is a PVE thing — so
31
31
  * the mount is a small custom vendor against `POST /access/users/{id}/token/{name}`. Say which
32
- * mount and role you needed rather than reaching for the read-only `monitoring@pbs` token on
32
+ * mount and role you needed rather than reaching for the read-only `metrics@pbs` token on
33
33
  * the kv shelf: widening that would delete the outer lock for every reader of the shelf.
34
34
  *
35
35
  * ⛔ A DATASTORE HOLDS THE BACKUPS, SO `retain` IS THE DEFAULT AND `delete` IS FULLY IMPLEMENTED —
@@ -47,7 +47,7 @@
47
47
  * ⛔ NO SECRET IS A PROP OR AN ATTRIBUTE, AND THIS FAMILY IS CLEAN BY CONSTRUCTION: a datastore
48
48
  * section holds no password, token or key — encryption keys belong to the backup CLIENT. Every
49
49
  * field below is a name, a path, a schedule, a count or a policy string, all of which are safe in
50
- * a state store Alchemy writes UNENCRYPTED and this estate dumps to CT100 nightly.
50
+ * a state store Alchemy writes UNENCRYPTED and this estate copies to a backup guest nightly.
51
51
  */
52
52
  import { Resource } from 'alchemy';
53
53
  import { isResolved } from 'alchemy/Diff';
@@ -131,11 +131,11 @@ export interface PbsDatastoreProps {
131
131
  * `datastore create --help` offers `--backend`, `datastore update --help` does NOT, and
132
132
  * `backend` is absent from update's `--delete` enum. PBS will not move a datastore between
133
133
  * local disk and object storage, ever.
134
- * ★ IT IS A PROP AT ALL BECAUSE THE ESTATE HAS ONE. `r2-offsite` holds
135
- * `type=s3,client=cloudflare-r2,bucket=homeflare-pbs`. Leaving it undeclared would still plan
134
+ * ★ IT IS A PROP AT ALL BECAUSE THE ESTATE HAS ONE. `offsite` holds
135
+ * `type=s3,client=cloudflare-r2,bucket=example-pbs`. Leaving it undeclared would still plan
136
136
  * noop — undeclared is unmanaged here — but the declaration would then describe a LOCAL
137
137
  * datastore, and anyone recreating from it would get exactly that: an empty directory on the
138
- * mini's disk where the offsite copy used to be, behind a green plan the whole way.
138
+ * host's disk where the offsite copy used to be, behind a green plan the whole way.
139
139
  * ⚠️ A property string. Compared canonically, so key order is never a diff.
140
140
  */
141
141
  backend?: string;