@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
@@ -230,7 +230,7 @@ var formToSend = (matches, live, props, form) => matches(live, props) || Object.
230
230
 
231
231
  // src/proxmox/resource.ts
232
232
  var pveOperations = (spec) => {
233
- const read = (props) => pve(props.target, spec.readRole ?? "read", "GET", spec.path(props)).pipe(Effect5.map((data) => data === undefined ? undefined : spec.attributes(data, props)), Effect5.orElseSucceed(() => {
233
+ const read = (props) => pve(props.target, spec.readRole ?? "read", "GET", spec.path(props)).pipe(Effect5.map((data) => data == null ? undefined : spec.attributes(data, props)), Effect5.orElseSucceed(() => {
234
234
  return;
235
235
  }));
236
236
  return {
@@ -336,6 +336,56 @@ var ProxmoxAclProvider = () => Provider.effect(ProxmoxAcl, Effect6.succeed(Proxm
336
336
  yield* pve(olds.target, "provision", "PUT", "access/acl", removal);
337
337
  })
338
338
  })));
339
+ // src/proxmox/alertmanager-body.ts
340
+ var literal = (label, value) => {
341
+ if (/[{}\\]/.test(value) || value === "") {
342
+ throw new Error(`alertmanagerAlertBody: ${label} must be non-empty and hold no '{', '}' or '\\'`);
343
+ }
344
+ return JSON.stringify(value);
345
+ };
346
+ var absoluteUrl = (value) => {
347
+ const text = literal("generatorURL", value);
348
+ let protocol = "";
349
+ try {
350
+ protocol = new URL(value).protocol;
351
+ } catch {}
352
+ if (protocol !== "https:" && protocol !== "http:") {
353
+ throw new Error("alertmanagerAlertBody: generatorURL must be an absolute http(s) URL");
354
+ }
355
+ return text;
356
+ };
357
+ var FIELD_LABELS = [
358
+ ["job_type", "type"],
359
+ ["job_id", "job-id"],
360
+ ["datastore", "datastore"],
361
+ ["hostname", "hostname"]
362
+ ];
363
+ var alertmanagerAlertBody = (options = {}) => {
364
+ const alertname = literal("alertname", options.alertname ?? "PbsNotification");
365
+ const source = literal("source", options.source ?? "pbs");
366
+ const labels = FIELD_LABELS.map(([label, field]) => ` {{#if fields.${field}}}"${label}": {{ json fields.${field} }},{{/if}}`);
367
+ const link = options.generatorURL === undefined ? "" : `,
368
+ "generatorURL": ${absoluteUrl(options.generatorURL)}`;
369
+ return [
370
+ "[",
371
+ " {",
372
+ ' "labels": {',
373
+ ...labels,
374
+ ' "severity": {{ json severity }},',
375
+ ` "alertname": ${alertname},`,
376
+ ` "source": ${source}`,
377
+ " },",
378
+ ' "annotations": {',
379
+ ' "summary": {{ json title }},',
380
+ ' "description": {{ json message }},',
381
+ ' "timestamp": "{{ timestamp }}"',
382
+ ` }${link}`,
383
+ " }",
384
+ "]",
385
+ ""
386
+ ].join(`
387
+ `);
388
+ };
339
389
  // src/proxmox/api-token.ts
340
390
  import { Resource as Resource2 } from "alchemy";
341
391
  import * as Provider2 from "alchemy/Provider";
@@ -422,7 +472,7 @@ var handlers = {
422
472
  reconcile: Effect7.fn(function* ({ news }) {
423
473
  const live = yield* ops2.read(news);
424
474
  if (live === undefined) {
425
- return yield* Effect7.die(new Error(`${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE returns ` + "the secret only in the create response and it cannot be stored, so a token made here " + "would be a live credential nobody holds. Create it with `pveum user token add` and " + "capture the value, or mint a short-lived one from the OpenBao proxmox mount " + "(`bao read proxmox-tb4/creds/<role>`) -- then declare it here to manage it."));
475
+ return yield* Effect7.die(new Error(`${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE returns ` + "the secret only in the create response and it cannot be stored, so a token made here " + "would be a live credential nobody holds. Create it with `pveum user token add` and " + "capture the value, or mint a short-lived one from the OpenBao proxmox mount " + "(`bao read proxmox-c1/creds/<role>`) -- then declare it here to manage it."));
426
476
  }
427
477
  return yield* ops2.reconcile(news);
428
478
  })
@@ -2184,11 +2234,101 @@ var handlers13 = pveHandlers({
2184
2234
  updateForm: updateBody2
2185
2235
  });
2186
2236
  var ProxmoxNodeNetworkProvider = () => Provider16.effect(ProxmoxNodeNetwork, Effect34.succeed(ProxmoxNodeNetwork.Provider.of(handlers13)));
2187
- // src/proxmox/notification-target.ts
2237
+ // src/proxmox/notification-matcher.ts
2188
2238
  import { Resource as Resource17 } from "alchemy";
2189
2239
  import * as Provider17 from "alchemy/Provider";
2190
2240
  import * as Effect35 from "effect/Effect";
2191
2241
 
2242
+ // src/proxmox/notification-matcher-form.ts
2243
+ var list = (value) => Array.isArray(value) ? value.map((item) => String(item)) : [];
2244
+ var items = (values, canonical) => values.map((item) => canonical(item.trim())).filter((item) => item !== "");
2245
+ var severity = (item) => item.split(",").map((part) => part.trim()).filter((part) => part !== "").sort().join(",");
2246
+ var field4 = (item) => {
2247
+ if (!item.startsWith("exact:"))
2248
+ return item;
2249
+ const at = item.indexOf("=");
2250
+ if (at < 0)
2251
+ return item;
2252
+ const values = item.slice(at + 1).split(",");
2253
+ return `${item.slice(0, at)}=${values.map((value) => value.trim()).join(",")}`;
2254
+ };
2255
+ var same3 = (item) => item;
2256
+ var canonicalRules = (fields) => ({
2257
+ "match-calendar": items(fields["match-calendar"] ?? [], same3).sort(),
2258
+ "match-field": items(fields["match-field"] ?? [], field4).sort(),
2259
+ "match-severity": items(fields["match-severity"] ?? [], severity).sort(),
2260
+ targets: items(fields.targets, same3).sort()
2261
+ });
2262
+ var matcherAttributes = (live, name) => ({
2263
+ comment: text(live["comment"]),
2264
+ disable: bool(live["disable"]),
2265
+ "invert-match": bool(live["invert-match"]),
2266
+ "match-calendar": items(list(live["match-calendar"]), same3).sort(),
2267
+ "match-field": items(list(live["match-field"]), field4).sort(),
2268
+ "match-severity": items(list(live["match-severity"]), severity).sort(),
2269
+ mode: text(live["mode"], "all") || "all",
2270
+ name,
2271
+ origin: text(live["origin"]),
2272
+ targets: items(list(live["target"]), same3).sort()
2273
+ });
2274
+ var equal = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
2275
+ var matcherMatches = (attributes, fields) => {
2276
+ const rules = canonicalRules(fields);
2277
+ return attributes.disable === (fields.disable === true) && attributes["invert-match"] === (fields["invert-match"] === true) && attributes.mode === (fields.mode ?? "all") && equal(attributes.targets, rules.targets) && equal(attributes["match-severity"], rules["match-severity"]) && equal(attributes["match-field"], rules["match-field"]) && equal(attributes["match-calendar"], rules["match-calendar"]) && (fields.comment === undefined || attributes.comment === fields.comment);
2278
+ };
2279
+ var always = (fields) => ({
2280
+ disable: fields.disable === true ? "1" : "0",
2281
+ "invert-match": fields["invert-match"] === true ? "1" : "0",
2282
+ mode: fields.mode ?? "all"
2283
+ });
2284
+ var lists = (fields) => {
2285
+ const rules = canonicalRules(fields);
2286
+ const out = {};
2287
+ if (rules["match-calendar"].length > 0)
2288
+ out["match-calendar"] = rules["match-calendar"];
2289
+ if (rules["match-field"].length > 0)
2290
+ out["match-field"] = rules["match-field"];
2291
+ if (rules["match-severity"].length > 0)
2292
+ out["match-severity"] = rules["match-severity"];
2293
+ if (rules.targets.length > 0)
2294
+ out.target = rules.targets;
2295
+ return out;
2296
+ };
2297
+ var matcherCreateForm = (fields) => ({
2298
+ ...always(fields),
2299
+ ...lists(fields),
2300
+ ...fields.comment === undefined || fields.comment === "" ? {} : { comment: fields.comment },
2301
+ name: fields.name
2302
+ });
2303
+ var matcherUpdateForm = (fields) => {
2304
+ const present = lists(fields);
2305
+ const cleared = ["match-calendar", "match-field", "match-severity", "target"].filter((key) => present[key] === undefined);
2306
+ if (fields.comment === "")
2307
+ cleared.push("comment");
2308
+ return {
2309
+ ...always(fields),
2310
+ ...present,
2311
+ ...fields.comment === undefined || fields.comment === "" ? {} : { comment: fields.comment },
2312
+ ...cleared.length === 0 ? {} : { delete: cleared }
2313
+ };
2314
+ };
2315
+
2316
+ // src/proxmox/notification-matcher.ts
2317
+ var ProxmoxNotificationMatcher = Resource17("Proxmox.NotificationMatcher");
2318
+ var handlers14 = pveHandlers({
2319
+ attributes: (live, props) => matcherAttributes(live, props.name),
2320
+ collection: () => "cluster/notifications/matchers",
2321
+ createForm: matcherCreateForm,
2322
+ matches: matcherMatches,
2323
+ path: (props) => `cluster/notifications/matchers/${props.name}`,
2324
+ updateForm: matcherUpdateForm
2325
+ });
2326
+ var ProxmoxNotificationMatcherProvider = () => Provider17.effect(ProxmoxNotificationMatcher, Effect35.succeed(ProxmoxNotificationMatcher.Provider.of(handlers14)));
2327
+ // src/proxmox/notification-target.ts
2328
+ import { Resource as Resource18 } from "alchemy";
2329
+ import * as Provider18 from "alchemy/Provider";
2330
+ import * as Effect36 from "effect/Effect";
2331
+
2192
2332
  // src/proxmox/notification-target-form.ts
2193
2333
  var REPEATED_KEY = "\x00";
2194
2334
  var portOf = (value) => {
@@ -2217,8 +2357,8 @@ var shape4 = (props) => ({
2217
2357
  });
2218
2358
 
2219
2359
  // src/proxmox/notification-target.ts
2220
- var ProxmoxNotificationTarget = Resource17("Proxmox.NotificationTarget");
2221
- var handlers14 = pveHandlers({
2360
+ var ProxmoxNotificationTarget = Resource18("Proxmox.NotificationTarget");
2361
+ var handlers15 = pveHandlers({
2222
2362
  attributes: (live, props) => ({
2223
2363
  author: text(live["author"]),
2224
2364
  body: text(live["body"]),
@@ -2243,15 +2383,15 @@ var handlers14 = pveHandlers({
2243
2383
  path: (props) => `cluster/notifications/endpoints/${props.type}/${props.name}`,
2244
2384
  updateForm: shape4
2245
2385
  });
2246
- var ProxmoxNotificationTargetProvider = () => Provider17.effect(ProxmoxNotificationTarget, Effect35.succeed(ProxmoxNotificationTarget.Provider.of(handlers14)));
2386
+ var ProxmoxNotificationTargetProvider = () => Provider18.effect(ProxmoxNotificationTarget, Effect36.succeed(ProxmoxNotificationTarget.Provider.of(handlers15)));
2247
2387
  // src/proxmox/pbs-datastore.ts
2248
- import { Resource as Resource18 } from "alchemy";
2388
+ import { Resource as Resource19 } from "alchemy";
2249
2389
  import { isResolved as isResolved8 } from "alchemy/Diff";
2250
- import * as Provider18 from "alchemy/Provider";
2251
- import * as Effect38 from "effect/Effect";
2390
+ import * as Provider19 from "alchemy/Provider";
2391
+ import * as Effect39 from "effect/Effect";
2252
2392
 
2253
2393
  // src/proxmox/pbs-datastore-form.ts
2254
- import * as Effect36 from "effect/Effect";
2394
+ import * as Effect37 from "effect/Effect";
2255
2395
  var attributes = (live, props) => ({
2256
2396
  backend: propertyString(live["backend"]),
2257
2397
  comment: text(live["comment"]),
@@ -2273,8 +2413,8 @@ var attributes = (live, props) => ({
2273
2413
  tuning: propertyString(live["tuning"]),
2274
2414
  "verify-new": bool(live["verify-new"])
2275
2415
  });
2276
- var same3 = (declared, live) => declared === undefined || declared === live;
2277
- var matches = (live, props) => same3(props.comment, live.comment) && same3(props["gc-schedule"]?.trim(), live["gc-schedule"]) && same3(props["prune-schedule"]?.trim(), live["prune-schedule"]) && same3(props["keep-last"], live["keep-last"]) && same3(props["keep-hourly"], live["keep-hourly"]) && same3(props["keep-daily"], live["keep-daily"]) && same3(props["keep-weekly"], live["keep-weekly"]) && same3(props["keep-monthly"], live["keep-monthly"]) && same3(props["keep-yearly"], live["keep-yearly"]) && same3(props["notify-user"], live["notify-user"]) && same3(props["counter-reset-schedule"]?.trim(), live["counter-reset-schedule"]) && (props["notification-thresholds"] === undefined || propertyString(props["notification-thresholds"]) === live["notification-thresholds"]) && same3(props["verify-new"], live["verify-new"]) && (props.notify === undefined || propertyString(props.notify) === live.notify) && (props.tuning === undefined || propertyString(props.tuning) === live.tuning) && (props["maintenance-mode"] === undefined || propertyString(props["maintenance-mode"], "type") === live["maintenance-mode"]);
2416
+ var same4 = (declared, live) => declared === undefined || declared === live;
2417
+ var matches = (live, props) => same4(props.comment, live.comment) && same4(props["gc-schedule"]?.trim(), live["gc-schedule"]) && same4(props["prune-schedule"]?.trim(), live["prune-schedule"]) && same4(props["keep-last"], live["keep-last"]) && same4(props["keep-hourly"], live["keep-hourly"]) && same4(props["keep-daily"], live["keep-daily"]) && same4(props["keep-weekly"], live["keep-weekly"]) && same4(props["keep-monthly"], live["keep-monthly"]) && same4(props["keep-yearly"], live["keep-yearly"]) && same4(props["notify-user"], live["notify-user"]) && same4(props["counter-reset-schedule"]?.trim(), live["counter-reset-schedule"]) && (props["notification-thresholds"] === undefined || propertyString(props["notification-thresholds"]) === live["notification-thresholds"]) && same4(props["verify-new"], live["verify-new"]) && (props.notify === undefined || propertyString(props.notify) === live.notify) && (props.tuning === undefined || propertyString(props.tuning) === live.tuning) && (props["maintenance-mode"] === undefined || propertyString(props["maintenance-mode"], "type") === live["maintenance-mode"]);
2278
2418
  var set2 = (key, value) => value === undefined ? {} : { [key]: value };
2279
2419
  var count = (value) => value === undefined ? undefined : String(value);
2280
2420
  var mutable = (props) => ({
@@ -2303,36 +2443,36 @@ var createForm4 = (props) => ({
2303
2443
  });
2304
2444
  var updateForm = (props) => mutable(props);
2305
2445
  var object2 = (props) => `config/datastore/${props.name}`;
2306
- var readOne = (props) => pve(props.target, "read", "GET", object2(props)).pipe(Effect36.map((live) => live === undefined || typeof live["name"] !== "string" ? undefined : attributes(live, props)), Effect36.orElseSucceed(() => {
2446
+ var readOne = (props) => pve(props.target, "read", "GET", object2(props)).pipe(Effect37.map((live) => live === undefined || typeof live["name"] !== "string" ? undefined : attributes(live, props)), Effect37.orElseSucceed(() => {
2307
2447
  return;
2308
2448
  }));
2309
2449
 
2310
2450
  // src/proxmox/pbs-datastore-guard.ts
2311
- import * as Effect37 from "effect/Effect";
2451
+ import * as Effect38 from "effect/Effect";
2312
2452
  var samePath = (left, right) => {
2313
2453
  const trim = (value) => value.length > 1 ? value.replace(/\/+$/, "") : value;
2314
2454
  return trim(left) === trim(right);
2315
2455
  };
2316
- var guardPath = (live, props) => live.path === "" || samePath(live.path, props.path) ? Effect37.void : Effect37.die(new Error(`${object2(props)}: declared path ${props.path} but PBS holds ${live.path}. \`path\` ` + "is create-only, so this cannot be reconciled and will NOT be planned as a silent " + "noop: backups would keep landing in the old directory behind a green plan. Correct " + "the declaration, or remove the datastore and declare a new one -- the contents live, " + "because `destroy-data` is never sent."));
2317
- var guardBackend = (live, props) => props.backend === undefined || propertyString(props.backend) === live.backend ? Effect37.void : Effect37.die(new Error(`${object2(props)}: declared backend ${props.backend} but PBS holds ` + `${live.backend === "" ? "(none — a local datastore)" : live.backend}. \`backend\` ` + "is create-only: PBS has no update for it, so this cannot be reconciled and will NOT " + "be planned as a silent noop. Correct the declaration to match what is really there, " + "or remove the datastore and declare a new one -- the contents live, because " + "`destroy-data` is never sent."));
2318
- var settle3 = (read3, done) => Effect37.gen(function* () {
2456
+ var guardPath = (live, props) => live.path === "" || samePath(live.path, props.path) ? Effect38.void : Effect38.die(new Error(`${object2(props)}: declared path ${props.path} but PBS holds ${live.path}. \`path\` ` + "is create-only, so this cannot be reconciled and will NOT be planned as a silent " + "noop: backups would keep landing in the old directory behind a green plan. Correct " + "the declaration, or remove the datastore and declare a new one -- the contents live, " + "because `destroy-data` is never sent."));
2457
+ var guardBackend = (live, props) => props.backend === undefined || propertyString(props.backend) === live.backend ? Effect38.void : Effect38.die(new Error(`${object2(props)}: declared backend ${props.backend} but PBS holds ` + `${live.backend === "" ? "(none — a local datastore)" : live.backend}. \`backend\` ` + "is create-only: PBS has no update for it, so this cannot be reconciled and will NOT " + "be planned as a silent noop. Correct the declaration to match what is really there, " + "or remove the datastore and declare a new one -- the contents live, because " + "`destroy-data` is never sent."));
2458
+ var settle3 = (read3, done) => Effect38.gen(function* () {
2319
2459
  for (let attempt = 0;attempt < 60; attempt += 1) {
2320
2460
  const live = yield* read3;
2321
2461
  if (done(live))
2322
2462
  return live;
2323
- yield* Effect37.sleep("2 seconds");
2463
+ yield* Effect38.sleep("2 seconds");
2324
2464
  }
2325
2465
  return yield* read3;
2326
2466
  });
2327
2467
 
2328
2468
  // src/proxmox/pbs-datastore.ts
2329
- var PbsDatastore = Resource18("Pbs.Datastore", {
2469
+ var PbsDatastore = Resource19("Pbs.Datastore", {
2330
2470
  defaultRemovalPolicy: "retain"
2331
2471
  });
2332
- var PbsDatastoreProvider = () => Provider18.effect(PbsDatastore, Effect38.succeed(PbsDatastore.Provider.of({
2333
- list: () => Effect38.succeed([]),
2472
+ var PbsDatastoreProvider = () => Provider19.effect(PbsDatastore, Effect39.succeed(PbsDatastore.Provider.of({
2473
+ list: () => Effect39.succeed([]),
2334
2474
  read: ({ olds }) => readOne(olds),
2335
- diff: ({ news, output }) => Effect38.gen(function* () {
2475
+ diff: ({ news, output }) => Effect39.gen(function* () {
2336
2476
  if (output === undefined || !isResolved8(news))
2337
2477
  return;
2338
2478
  const live = yield* readOne(news);
@@ -2342,7 +2482,7 @@ var PbsDatastoreProvider = () => Provider18.effect(PbsDatastore, Effect38.succee
2342
2482
  yield* guardBackend(live, news);
2343
2483
  return matches(live, news) ? { action: "noop" } : { action: "update" };
2344
2484
  }),
2345
- reconcile: Effect38.fn(function* ({ news }) {
2485
+ reconcile: Effect39.fn(function* ({ news }) {
2346
2486
  const live = yield* readOne(news);
2347
2487
  let upid;
2348
2488
  if (live === undefined) {
@@ -2357,22 +2497,373 @@ var PbsDatastoreProvider = () => Provider18.effect(PbsDatastore, Effect38.succee
2357
2497
  }
2358
2498
  const after = yield* settle3(readOne(news), (row) => row !== undefined && matches(row, news));
2359
2499
  if (after === undefined || !matches(after, news)) {
2360
- return yield* Effect38.die(new Error(`${object2(news)}: the write returned no error but the datastore still does not ` + `match the declaration after 120s. PBS answered with task ${upid ?? "(none)"} ` + "-- read it with `proxmox-backup-manager task log <upid>`. A slow disk is a " + "plausible cause (see `settle`); a create over an existing chunk store is not, " + "because PBS refuses that outright rather than timing out."));
2500
+ return yield* Effect39.die(new Error(`${object2(news)}: the write returned no error but the datastore still does not ` + `match the declaration after 120s. PBS answered with task ${upid ?? "(none)"} ` + "-- read it with `proxmox-backup-manager task log <upid>`. A slow disk is a " + "plausible cause (see `settle`); a create over an existing chunk store is not, " + "because PBS refuses that outright rather than timing out."));
2361
2501
  }
2362
2502
  return after;
2363
2503
  }),
2364
- delete: Effect38.fn(function* ({ olds }) {
2504
+ delete: Effect39.fn(function* ({ olds }) {
2365
2505
  yield* pve(olds.target, "provision", "DELETE", object2(olds));
2366
2506
  const left = yield* settle3(readOne(olds), (row) => row === undefined);
2367
2507
  if (left !== undefined) {
2368
- return yield* Effect38.die(new Error(`${object2(olds)}: the DELETE returned no error but the datastore is still in ` + "datastore.cfg after 120s. Either a backup, verify or GC task still holds it, " + "or the provision role lacks Datastore.Allocate on /datastore/" + `${olds.name}. Read the task log with \`proxmox-backup-manager task log <upid>\`.`));
2508
+ return yield* Effect39.die(new Error(`${object2(olds)}: the DELETE returned no error but the datastore is still in ` + "datastore.cfg after 120s. Either a backup, verify or GC task still holds it, " + "or the provision role lacks Datastore.Allocate on /datastore/" + `${olds.name}. Read the task log with \`proxmox-backup-manager task log <upid>\`.`));
2369
2509
  }
2370
2510
  })
2371
2511
  })));
2512
+ // src/proxmox/pbs-notification-matcher.ts
2513
+ import { Resource as Resource20 } from "alchemy";
2514
+ import * as Provider20 from "alchemy/Provider";
2515
+ import * as Effect40 from "effect/Effect";
2516
+ var PbsNotificationMatcher = Resource20("Pbs.NotificationMatcher");
2517
+ var handlers16 = pveHandlers({
2518
+ attributes: (live, props) => matcherAttributes(live, props.name),
2519
+ collection: () => "config/notifications/matchers",
2520
+ createForm: matcherCreateForm,
2521
+ matches: matcherMatches,
2522
+ path: (props) => `config/notifications/matchers/${props.name}`,
2523
+ updateForm: matcherUpdateForm
2524
+ });
2525
+ var PbsNotificationMatcherProvider = () => Provider20.effect(PbsNotificationMatcher, Effect40.succeed(PbsNotificationMatcher.Provider.of(handlers16)));
2526
+ // src/proxmox/pbs-notification-target.ts
2527
+ import { Resource as Resource21 } from "alchemy";
2528
+ import * as Provider21 from "alchemy/Provider";
2529
+ import * as Effect42 from "effect/Effect";
2530
+
2531
+ // src/proxmox/pbs-notification-target-lifecycle.ts
2532
+ import { isResolved as isResolved9 } from "alchemy/Diff";
2533
+ import * as Effect41 from "effect/Effect";
2534
+
2535
+ // src/proxmox/credential-literals.ts
2536
+ var READS_SECRET = /\{\{[^}]*\bsecrets\./;
2537
+ var CREDENTIAL_WORD = /(^|[-_])(api[-_]?key|apikey|key|token|secret|password|passwd|pwd|pass|auth|signature|sig|code)($|[-_])/i;
2538
+ var CREDENTIAL_HEADER = /^(proxy-authorization|authorization|cookie)$/i;
2539
+ var isCredentialName = (name) => CREDENTIAL_HEADER.test(name) || CREDENTIAL_WORD.test(name);
2540
+ var literal2 = (value) => value !== "" && !READS_SECRET.test(value);
2541
+ var literalCredentialHeaders = (header) => Object.entries(header ?? {}).filter(([name, value]) => typeof value === "string" && isCredentialName(name)).filter(([, value]) => literal2(value)).map(([name]) => name).sort();
2542
+ var literalCredentialsInUrl = (url) => {
2543
+ if (url === undefined)
2544
+ return [];
2545
+ const out = [];
2546
+ const authority = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(url)?.[1] ?? "";
2547
+ const at = authority.lastIndexOf("@");
2548
+ if (at >= 0) {
2549
+ const userinfo = authority.slice(0, at);
2550
+ const colon = userinfo.indexOf(":");
2551
+ if (colon >= 0 && literal2(userinfo.slice(colon + 1)))
2552
+ out.push("userinfo");
2553
+ }
2554
+ const params = url.replace(/^[^?#]*/, "");
2555
+ for (const pair of params.split(/[?&#]/)) {
2556
+ const eq = pair.indexOf("=");
2557
+ const name = eq < 0 ? pair : pair.slice(0, eq);
2558
+ if (eq >= 0 && isCredentialName(name) && literal2(pair.slice(eq + 1)))
2559
+ out.push(`?${name}`);
2560
+ }
2561
+ return out;
2562
+ };
2563
+
2564
+ // src/proxmox/pbs-notification-target-wire.ts
2565
+ import { Buffer as Buffer2 } from "node:buffer";
2566
+
2567
+ // src/proxmox/write-only.ts
2568
+ import { Buffer } from "node:buffer";
2569
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
2570
+ var resolveAll = (refs, env = process.env) => {
2571
+ const values = {};
2572
+ const missing = [];
2573
+ for (const [key, ref] of Object.entries(refs)) {
2574
+ const value = env[ref.fromEnv];
2575
+ if (value === undefined || value === "")
2576
+ missing.push(ref.fromEnv);
2577
+ else
2578
+ values[key] = value;
2579
+ }
2580
+ return { missing: [...new Set(missing)].sort(), values };
2581
+ };
2582
+ var COST = { N: 16384, maxmem: 64 * 1024 * 1024, p: 1, r: 8 };
2583
+ var PREFIX = "scrypt";
2584
+ var canonical = (values) => JSON.stringify(Object.keys(values).sort().map((key) => [key, values[key]]));
2585
+ var digest = (salt, values) => scryptSync(canonical(values), salt, 32, COST);
2586
+ var seal = (values, salt) => {
2587
+ if (Object.keys(values).length === 0)
2588
+ return "";
2589
+ const bytes = salt === undefined ? randomBytes(16) : Buffer.from(salt, "utf8");
2590
+ return `${PREFIX}:${bytes.toString("base64url")}:${digest(bytes, values).toString("base64url")}`;
2591
+ };
2592
+ var sealMatches = (sealed, values) => {
2593
+ if (sealed === "")
2594
+ return Object.keys(values).length === 0;
2595
+ const [prefix, salt, stored, extra] = sealed.split(":");
2596
+ if (prefix !== PREFIX || salt === undefined || stored === undefined || extra !== undefined) {
2597
+ return false;
2598
+ }
2599
+ const expected = Buffer.from(stored, "base64url");
2600
+ const actual = digest(Buffer.from(salt, "base64url"), values);
2601
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
2602
+ };
2603
+
2604
+ // src/proxmox/pbs-notification-target-wire.ts
2605
+ var base64 = (value) => Buffer2.from(value, "utf8").toString("base64");
2606
+ var unbase64 = (value) => Buffer2.from(value, "base64").toString("utf8");
2607
+ var keyAndValue = (item) => {
2608
+ const parts = {};
2609
+ for (const part of item.split(",")) {
2610
+ const at = part.indexOf("=");
2611
+ if (at > 0)
2612
+ parts[part.slice(0, at).trim()] = part.slice(at + 1).trim();
2613
+ }
2614
+ const value = parts["value"];
2615
+ return value === undefined ? { name: parts["name"] ?? "" } : { name: parts["name"] ?? "", value };
2616
+ };
2617
+ var wireItem = (name, value) => `name=${name},value=${base64(value)}`;
2618
+ var list2 = (value) => Array.isArray(value) ? value.map((item) => String(item)) : [];
2619
+ var names = (keys) => [...keys].filter((key) => key !== "").sort().join(",");
2620
+ var headerDigest = (target, pairs2) => seal(pairs2, `hf:pbs-notification-target:${target}:header`);
2621
+ var targetAttributes = (live, props) => {
2622
+ const headers = {};
2623
+ for (const item of list2(live["header"])) {
2624
+ const { name, value } = keyAndValue(item);
2625
+ if (name !== "")
2626
+ headers[name] = unbase64(value ?? "");
2627
+ }
2628
+ const secrets = list2(live["secret"]).map((item) => keyAndValue(item).name);
2629
+ return {
2630
+ author: text(live["author"]),
2631
+ body: unbase64(text(live["body"])),
2632
+ comment: text(live["comment"]),
2633
+ disable: bool(live["disable"]),
2634
+ "from-address": text(live["from-address"]),
2635
+ header: names(Object.keys(headers)),
2636
+ headerDigest: headerDigest(props.name, headers),
2637
+ mailto: addressList(live["mailto"]),
2638
+ "mailto-user": addressList(live["mailto-user"]),
2639
+ method: text(live["method"]),
2640
+ mode: text(live["mode"]),
2641
+ name: props.name,
2642
+ origin: text(live["origin"]),
2643
+ port: portOf(live["port"]),
2644
+ secret: names(secrets),
2645
+ sealed: "",
2646
+ server: text(live["server"]),
2647
+ type: props.type,
2648
+ url: text(live["url"]),
2649
+ username: text(live["username"])
2650
+ };
2651
+ };
2652
+
2653
+ // src/proxmox/pbs-notification-target-form.ts
2654
+ var OWNED = {
2655
+ author: ["sendmail", "smtp"],
2656
+ body: ["webhook"],
2657
+ "from-address": ["sendmail", "smtp"],
2658
+ header: ["webhook"],
2659
+ mailto: ["sendmail", "smtp"],
2660
+ "mailto-user": ["sendmail", "smtp"],
2661
+ method: ["webhook"],
2662
+ mode: ["smtp"],
2663
+ password: ["smtp"],
2664
+ port: ["smtp"],
2665
+ secret: ["webhook"],
2666
+ server: ["smtp"],
2667
+ url: ["webhook"],
2668
+ username: ["smtp"]
2669
+ };
2670
+ var REQUIRED = {
2671
+ sendmail: [],
2672
+ smtp: ["server", "from-address"],
2673
+ webhook: ["url", "method"]
2674
+ };
2675
+ var CLEAN_NAME = /^[^,=\s]+$/;
2676
+ var WRITE_ONLY = "declare it { fromEnv: 'NAME' }, or keep it in 'secret' and write {{ secrets.<name> }}";
2677
+ var refusals = (props) => {
2678
+ const out = [];
2679
+ for (const [field5, types] of Object.entries(OWNED)) {
2680
+ if (props[field5] !== undefined && !types.includes(props.type)) {
2681
+ out.push(`'${field5}' belongs to ${types.join("/")} targets, not ${props.type}`);
2682
+ }
2683
+ }
2684
+ for (const field5 of REQUIRED[props.type]) {
2685
+ if (props[field5] === undefined || props[field5] === "")
2686
+ out.push(`${props.type} needs '${field5}'`);
2687
+ }
2688
+ for (const key of [...Object.keys(props.header ?? {}), ...Object.keys(props.secret ?? {})]) {
2689
+ if (!CLEAN_NAME.test(key))
2690
+ out.push(`'${key}' cannot be a header or secret name`);
2691
+ }
2692
+ for (const name of literalCredentialHeaders(props.header)) {
2693
+ out.push(`header '${name}' holds a literal credential; ${WRITE_ONLY}`);
2694
+ }
2695
+ for (const where of literalCredentialsInUrl(props.url)) {
2696
+ out.push(`'url' ${where} holds a literal credential; ${WRITE_ONLY}`);
2697
+ }
2698
+ return out;
2699
+ };
2700
+ var isFromEnv = (value) => typeof value !== "string";
2701
+ var resolveGroups = (props, env = process.env) => {
2702
+ const headerRefs = {};
2703
+ const headers = {};
2704
+ for (const [name, value] of Object.entries(props.header ?? {})) {
2705
+ if (isFromEnv(value))
2706
+ headerRefs[name] = value;
2707
+ else
2708
+ headers[name] = value;
2709
+ }
2710
+ const header = resolveAll(headerRefs, env);
2711
+ const sealedRefs = {};
2712
+ for (const [name, ref] of Object.entries(props.secret ?? {}))
2713
+ sealedRefs[`secret:${name}`] = ref;
2714
+ if (props.password !== undefined)
2715
+ sealedRefs["password"] = props.password;
2716
+ const sealed = resolveAll(sealedRefs, env);
2717
+ return {
2718
+ header: { missing: header.missing, values: { ...headers, ...header.values } },
2719
+ sealed
2720
+ };
2721
+ };
2722
+ var same5 = (declared, live) => declared === undefined || declared === live;
2723
+ var plainMatches = (live, props) => live.disable === (props.disable === true) && same5(props.comment, live.comment) && same5(props.author, live.author) && same5(props["from-address"], live["from-address"]) && same5(props.server, live.server) && (props.port === undefined || props.port === live.port) && same5(props.mode, live.mode) && same5(props.username, live.username) && same5(props.url, live.url) && same5(props.method, live.method) && same5(props.body, live.body) && (props.mailto === undefined || addressList(props.mailto) === live.mailto) && (props["mailto-user"] === undefined || addressList(props["mailto-user"]) === live["mailto-user"]);
2724
+ var headerState = (live, props, groups) => {
2725
+ if (props.header === undefined)
2726
+ return "match";
2727
+ if (names(Object.keys(props.header)) !== live.header)
2728
+ return "stale";
2729
+ if (groups.header.missing.length > 0)
2730
+ return "unknown";
2731
+ return headerDigest(props.name, groups.header.values) === live.headerDigest ? "match" : "stale";
2732
+ };
2733
+ var sealedState = (live, props, groups, sealed) => {
2734
+ if (props.secret === undefined && props.password === undefined)
2735
+ return "match";
2736
+ if (props.secret !== undefined && names(Object.keys(props.secret)) !== live.secret)
2737
+ return "stale";
2738
+ if (groups.sealed.missing.length > 0 || sealed === "")
2739
+ return "unknown";
2740
+ return sealMatches(sealed, groups.sealed.values) ? "match" : "stale";
2741
+ };
2742
+ var secretItems = (props, groups) => Object.keys(props.secret ?? {}).map((name) => wireItem(name, groups.sealed.values[`secret:${name}`] ?? ""));
2743
+ var headerItems = (props, groups) => Object.keys(props.header ?? {}).map((name) => wireItem(name, groups.header.values[name] ?? ""));
2744
+ var plainPairs = (props) => [
2745
+ ["author", props.author ?? ""],
2746
+ ["body", props.body === undefined ? "" : base64(props.body)],
2747
+ ["comment", props.comment ?? ""],
2748
+ ["from-address", props["from-address"] ?? ""],
2749
+ ["method", props.method ?? ""],
2750
+ ["mode", props.mode ?? ""],
2751
+ ["port", props.port === undefined ? "" : String(props.port)],
2752
+ ["server", props.server ?? ""],
2753
+ ["url", props.url ?? ""],
2754
+ ["username", props.username ?? ""],
2755
+ ["mailto", props.mailto === undefined ? [] : addressList(props.mailto).split(",")],
2756
+ [
2757
+ "mailto-user",
2758
+ props["mailto-user"] === undefined ? [] : addressList(props["mailto-user"]).split(",")
2759
+ ]
2760
+ ];
2761
+ var declared = (props, key) => props[key] !== undefined;
2762
+ var empty = (value) => typeof value === "string" ? value === "" : value.filter((item) => item !== "").length === 0;
2763
+ var targetForm = (props, groups, carry, mode) => {
2764
+ const form = {
2765
+ disable: props.disable === true ? "1" : "0"
2766
+ };
2767
+ const cleared = [];
2768
+ const put = (key, value) => {
2769
+ if (!empty(value))
2770
+ form[key] = value;
2771
+ else if (mode === "update")
2772
+ cleared.push(key);
2773
+ };
2774
+ for (const [key, value] of plainPairs(props))
2775
+ if (declared(props, key))
2776
+ put(key, value);
2777
+ if (carry.header && props.header !== undefined)
2778
+ put("header", headerItems(props, groups));
2779
+ if (carry.sealed && props.secret !== undefined)
2780
+ put("secret", secretItems(props, groups));
2781
+ if (carry.sealed && props.password !== undefined) {
2782
+ form["password"] = groups.sealed.values["password"] ?? "";
2783
+ }
2784
+ if (mode === "create")
2785
+ form["name"] = props.name;
2786
+ if (cleared.length > 0)
2787
+ form["delete"] = cleared;
2788
+ return form;
2789
+ };
2790
+
2791
+ // src/proxmox/pbs-notification-target-lifecycle.ts
2792
+ var path = (props) => `config/notifications/endpoints/${props.type}/${props.name}`;
2793
+ var readLive2 = (props) => pve(props.target, "read", "GET", path(props)).pipe(Effect41.map((data) => data == null ? undefined : targetAttributes(data, props)), Effect41.orElseSucceed(() => {
2794
+ return;
2795
+ }));
2796
+ var refuse2 = (props) => {
2797
+ const reasons = refusals(props);
2798
+ return reasons.length === 0 ? Effect41.void : Effect41.die(new Error(`${path(props)}: ${reasons.join("; ")}.`));
2799
+ };
2800
+ var toCarry = (live, props, groups, sealed) => {
2801
+ const header = headerState(live, props, groups) === "stale";
2802
+ const sealedStale = sealedState(live, props, groups, sealed) === "stale";
2803
+ return plainMatches(live, props) && !header && !sealedStale ? undefined : { header, sealed: sealedStale };
2804
+ };
2805
+ var requireValues = (props, groups, carry) => {
2806
+ const missing = [
2807
+ ...carry.header ? groups.header.missing : [],
2808
+ ...carry.sealed ? groups.sealed.missing : []
2809
+ ];
2810
+ return missing.length === 0 ? Effect41.void : Effect41.die(new Error(`${path(props)}: this write must send write-only values, and ${missing.join(", ")} ` + "is unset or empty in the deploying environment. Export it and deploy again."));
2811
+ };
2812
+ var handlers17 = {
2813
+ list: () => Effect41.succeed([]),
2814
+ read: ({ olds, output }) => (output === undefined ? refuse2(olds) : Effect41.void).pipe(Effect41.andThen(readLive2(olds)), Effect41.map((live) => live === undefined ? undefined : { ...live, sealed: output?.sealed ?? "" })),
2815
+ diff: ({
2816
+ news,
2817
+ olds,
2818
+ output
2819
+ }) => Effect41.gen(function* () {
2820
+ if (output === undefined || !isResolved9(news))
2821
+ return;
2822
+ if (olds !== undefined && (olds.type !== news.type || olds.name !== news.name)) {
2823
+ return { action: "replace", deleteFirst: olds.name === news.name };
2824
+ }
2825
+ yield* refuse2(news);
2826
+ const live = yield* readLive2(news);
2827
+ if (live === undefined)
2828
+ return { action: "update" };
2829
+ return toCarry(live, news, resolveGroups(news), output.sealed) === undefined ? { action: "noop" } : { action: "update" };
2830
+ }),
2831
+ reconcile: ({ news, output }) => Effect41.gen(function* () {
2832
+ yield* refuse2(news);
2833
+ const groups = resolveGroups(news);
2834
+ const live = yield* readLive2(news);
2835
+ let sealed = output?.sealed ?? "";
2836
+ if (live === undefined) {
2837
+ const carry = { header: true, sealed: true };
2838
+ yield* requireValues(news, groups, carry);
2839
+ const collection2 = `config/notifications/endpoints/${news.type}`;
2840
+ yield* pve(news.target, "provision", "POST", collection2, targetForm(news, groups, carry, "create"));
2841
+ sealed = seal(groups.sealed.values);
2842
+ } else {
2843
+ const carry = toCarry(live, news, groups, sealed);
2844
+ if (carry !== undefined) {
2845
+ yield* requireValues(news, groups, carry);
2846
+ yield* pve(news.target, "provision", "PUT", path(news), targetForm(news, groups, carry, "update"));
2847
+ if (carry.sealed)
2848
+ sealed = seal(groups.sealed.values);
2849
+ }
2850
+ }
2851
+ const after = yield* readLive2(news);
2852
+ if (after === undefined) {
2853
+ return yield* Effect41.die(new Error(`${path(news)}: the write returned no error but the target is still absent.`));
2854
+ }
2855
+ return { ...after, sealed };
2856
+ }),
2857
+ delete: ({ olds }) => pve(olds.target, "provision", "DELETE", path(olds))
2858
+ };
2859
+
2860
+ // src/proxmox/pbs-notification-target.ts
2861
+ var PbsNotificationTarget = Resource21("Pbs.NotificationTarget");
2862
+ var PbsNotificationTargetProvider = () => Provider21.effect(PbsNotificationTarget, Effect42.succeed(PbsNotificationTarget.Provider.of(handlers17)));
2372
2863
  // src/proxmox/pbs-prune-job.ts
2373
- import { Resource as Resource19 } from "alchemy";
2374
- import * as Provider19 from "alchemy/Provider";
2375
- import * as Effect39 from "effect/Effect";
2864
+ import { Resource as Resource22 } from "alchemy";
2865
+ import * as Provider22 from "alchemy/Provider";
2866
+ import * as Effect43 from "effect/Effect";
2376
2867
 
2377
2868
  // src/proxmox/pbs-prune-job-form.ts
2378
2869
  var KEEP_KEYS = [
@@ -2407,14 +2898,14 @@ var requireKeeps = (props) => {
2407
2898
  return;
2408
2899
  throw new Error(`Pbs.PruneJob ${props.id}: declares no keep-* window, which tells PBS to keep NOTHING on ` + `datastore '${props.store}'. Every keep-* is optional on the wire, so an omission and a ` + 'deliberate "retain nothing" are indistinguishable; declare at least one window (keep-last, ' + "keep-hourly, keep-daily, keep-weekly, keep-monthly or keep-yearly) or delete the job.");
2409
2900
  };
2410
- var field4 = (name, value) => value === undefined ? {} : { [name]: value };
2901
+ var field5 = (name, value) => value === undefined ? {} : { [name]: value };
2411
2902
  var integer = (value) => value === undefined ? undefined : String(value);
2412
2903
  var shape5 = (props) => ({
2413
2904
  ...keepForm(props),
2414
- ...field4("comment", props.comment),
2415
- ...field4("disable", flag(props.disable)),
2416
- ...field4("max-depth", integer(props["max-depth"])),
2417
- ...field4("ns", props.ns),
2905
+ ...field5("comment", props.comment),
2906
+ ...field5("disable", flag(props.disable)),
2907
+ ...field5("max-depth", integer(props["max-depth"])),
2908
+ ...field5("ns", props.ns),
2418
2909
  schedule: props.schedule,
2419
2910
  store: props.store
2420
2911
  });
@@ -2428,9 +2919,9 @@ var updateBody3 = (props) => {
2428
2919
  };
2429
2920
 
2430
2921
  // src/proxmox/pbs-prune-job.ts
2431
- var PbsPruneJob = Resource19("Pbs.PruneJob");
2432
- var same4 = (declared, live) => declared === undefined || declared === live;
2433
- var handlers15 = pveHandlers({
2922
+ var PbsPruneJob = Resource22("Pbs.PruneJob");
2923
+ var same6 = (declared2, live) => declared2 === undefined || declared2 === live;
2924
+ var handlers18 = pveHandlers({
2434
2925
  attributes: (live, props) => ({
2435
2926
  comment: text(live["comment"], ""),
2436
2927
  disable: bool(live["disable"], false),
@@ -2443,15 +2934,15 @@ var handlers15 = pveHandlers({
2443
2934
  }),
2444
2935
  collection: () => "config/prune",
2445
2936
  createForm: createBody2,
2446
- matches: (attributes2, props) => attributes2.schedule === props.schedule && attributes2.store === props.store && same4(props.ns, attributes2.ns) && same4(props["max-depth"], attributes2["max-depth"]) && same4(props.disable, attributes2.disable) && same4(props.comment, attributes2.comment) && keepsMatch(attributes2, props),
2937
+ matches: (attributes2, props) => attributes2.schedule === props.schedule && attributes2.store === props.store && same6(props.ns, attributes2.ns) && same6(props["max-depth"], attributes2["max-depth"]) && same6(props.disable, attributes2.disable) && same6(props.comment, attributes2.comment) && keepsMatch(attributes2, props),
2447
2938
  path: (props) => `config/prune/${props.id}`,
2448
2939
  updateForm: updateBody3
2449
2940
  });
2450
- var PbsPruneJobProvider = () => Provider19.effect(PbsPruneJob, Effect39.succeed(PbsPruneJob.Provider.of(handlers15)));
2941
+ var PbsPruneJobProvider = () => Provider22.effect(PbsPruneJob, Effect43.succeed(PbsPruneJob.Provider.of(handlers18)));
2451
2942
  // src/proxmox/pbs-sync-job.ts
2452
- import { Resource as Resource20 } from "alchemy";
2453
- import * as Provider20 from "alchemy/Provider";
2454
- import * as Effect40 from "effect/Effect";
2943
+ import { Resource as Resource23 } from "alchemy";
2944
+ import * as Provider23 from "alchemy/Provider";
2945
+ import * as Effect44 from "effect/Effect";
2455
2946
 
2456
2947
  // src/proxmox/pbs-sync-job-form.ts
2457
2948
  var UNSET4 = -1;
@@ -2499,7 +2990,7 @@ var createBody3 = (props) => ({
2499
2990
  ...props["sync-direction"] === undefined ? {} : { "sync-direction": props["sync-direction"] }
2500
2991
  });
2501
2992
  var updateBody4 = (props) => shape6(props);
2502
- var handlers16 = pveHandlers({
2993
+ var handlers19 = pveHandlers({
2503
2994
  attributes: (live, props) => ({
2504
2995
  comment: text(live["comment"], ""),
2505
2996
  "group-filter": filters(live["group-filter"]),
@@ -2526,12 +3017,12 @@ var handlers16 = pveHandlers({
2526
3017
  });
2527
3018
 
2528
3019
  // src/proxmox/pbs-sync-job.ts
2529
- var PbsSyncJob = Resource20("Pbs.SyncJob");
2530
- var PbsSyncJobProvider = () => Provider20.effect(PbsSyncJob, Effect40.succeed(PbsSyncJob.Provider.of(handlers16)));
3020
+ var PbsSyncJob = Resource23("Pbs.SyncJob");
3021
+ var PbsSyncJobProvider = () => Provider23.effect(PbsSyncJob, Effect44.succeed(PbsSyncJob.Provider.of(handlers19)));
2531
3022
  // src/proxmox/pbs-verify-job.ts
2532
- import { Resource as Resource21 } from "alchemy";
2533
- import * as Provider21 from "alchemy/Provider";
2534
- import * as Effect41 from "effect/Effect";
3023
+ import { Resource as Resource24 } from "alchemy";
3024
+ import * as Provider24 from "alchemy/Provider";
3025
+ import * as Effect45 from "effect/Effect";
2535
3026
 
2536
3027
  // src/proxmox/pbs-verify-job-form.ts
2537
3028
  var rechecks = (ignoreVerified, outdatedAfter) => {
@@ -2552,8 +3043,8 @@ var shape7 = (props) => ({
2552
3043
  });
2553
3044
 
2554
3045
  // src/proxmox/pbs-verify-job.ts
2555
- var PbsVerifyJob = Resource21("Pbs.VerifyJob");
2556
- var handlers17 = pveHandlers({
3046
+ var PbsVerifyJob = Resource24("Pbs.VerifyJob");
3047
+ var handlers20 = pveHandlers({
2557
3048
  attributes: (live, props) => {
2558
3049
  const ignoreVerified = bool(live["ignore-verified"], true);
2559
3050
  const outdatedAfter = int(live["outdated-after"], -1);
@@ -2575,12 +3066,12 @@ var handlers17 = pveHandlers({
2575
3066
  path: (props) => `config/verify/${props.id}`,
2576
3067
  updateForm: shape7
2577
3068
  });
2578
- var PbsVerifyJobProvider = () => Provider21.effect(PbsVerifyJob, Effect41.succeed(PbsVerifyJob.Provider.of(handlers17)));
3069
+ var PbsVerifyJobProvider = () => Provider24.effect(PbsVerifyJob, Effect45.succeed(PbsVerifyJob.Provider.of(handlers20)));
2579
3070
  // src/proxmox/pool.ts
2580
- import { Resource as Resource22 } from "alchemy";
2581
- import * as Provider22 from "alchemy/Provider";
2582
- import * as Effect42 from "effect/Effect";
2583
- var ProxmoxPool = Resource22("Proxmox.Pool");
3071
+ import { Resource as Resource25 } from "alchemy";
3072
+ import * as Provider25 from "alchemy/Provider";
3073
+ import * as Effect46 from "effect/Effect";
3074
+ var ProxmoxPool = Resource25("Proxmox.Pool");
2584
3075
  var poolCreateForm = (props) => ({
2585
3076
  comment: props.comment ?? "",
2586
3077
  poolid: props.poolid
@@ -2588,7 +3079,7 @@ var poolCreateForm = (props) => ({
2588
3079
  var poolUpdateForm = (props) => ({
2589
3080
  comment: props.comment ?? ""
2590
3081
  });
2591
- var handlers18 = pveHandlers({
3082
+ var handlers21 = pveHandlers({
2592
3083
  attributes: (live, props) => {
2593
3084
  const row = live;
2594
3085
  return {
@@ -2603,13 +3094,287 @@ var handlers18 = pveHandlers({
2603
3094
  path: (props) => `pools/${props.poolid}`,
2604
3095
  updateForm: poolUpdateForm
2605
3096
  });
2606
- var ProxmoxPoolProvider = () => Provider22.effect(ProxmoxPool, Effect42.succeed(ProxmoxPool.Provider.of(handlers18)));
2607
- // src/proxmox/qemu.ts
2608
- import { Resource as Resource23 } from "alchemy";
2609
- import * as Provider23 from "alchemy/Provider";
2610
- import * as Effect43 from "effect/Effect";
2611
- var ProxmoxVm = Resource23("Proxmox.Vm");
3097
+ var ProxmoxPoolProvider = () => Provider25.effect(ProxmoxPool, Effect46.succeed(ProxmoxPool.Provider.of(handlers21)));
3098
+ // src/proxmox/provision-baseline.ts
3099
+ var PROVISION_PRIVILEGES = Object.freeze([
3100
+ "Datastore.Allocate",
3101
+ "Datastore.AllocateSpace",
3102
+ "Datastore.Audit",
3103
+ "Group.Allocate",
3104
+ "Mapping.Audit",
3105
+ "Mapping.Modify",
3106
+ "Permissions.Modify",
3107
+ "Pool.Allocate",
3108
+ "Pool.Audit",
3109
+ "Realm.AllocateUser",
3110
+ "SDN.Allocate",
3111
+ "SDN.Audit",
3112
+ "SDN.Use",
3113
+ "Sys.Audit",
3114
+ "Sys.Console",
3115
+ "Sys.Modify",
3116
+ "User.Modify",
3117
+ "VM.Allocate",
3118
+ "VM.Audit",
3119
+ "VM.Backup",
3120
+ "VM.Config.CPU",
3121
+ "VM.Config.Disk",
3122
+ "VM.Config.HWType",
3123
+ "VM.Config.Memory",
3124
+ "VM.Config.Network",
3125
+ "VM.Config.Options",
3126
+ "VM.PowerMgmt"
3127
+ ]);
3128
+ var PROVISION_DEFAULTS = Object.freeze({
3129
+ comment: "provision baseline from @homeflare/alchemy - OpenBao mints short-lived tokens for members",
3130
+ mintGroup: "hf-mint",
3131
+ provisionUser: "hf-provision@pve",
3132
+ readRole: "PVEAuditor",
3133
+ readUser: "hf-read@pve",
3134
+ role: "HfProvisioner"
3135
+ });
3136
+ var ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
3137
+ var USERID = /^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z][A-Za-z0-9._-]*$/;
3138
+ var COMMENT = /^[A-Za-z0-9 ._,()@/+-]*$/;
3139
+ var problems2 = (names2) => {
3140
+ const found = [];
3141
+ if (!ID.test(names2.role))
3142
+ found.push(`role \`${names2.role}\` is not a PVE role id`);
3143
+ if (!ID.test(names2.mintGroup))
3144
+ found.push(`mintGroup \`${names2.mintGroup}\` is not a group id`);
3145
+ if (!ID.test(names2.readRole))
3146
+ found.push(`readRole \`${names2.readRole}\` is not a PVE role id`);
3147
+ for (const [field6, userid] of [
3148
+ ["provisionUser", names2.provisionUser],
3149
+ ["readUser", names2.readUser]
3150
+ ]) {
3151
+ if (userid !== null && !USERID.test(userid)) {
3152
+ found.push(`${field6} \`${userid}\` is not a realm-qualified user id (name@realm)`);
3153
+ }
3154
+ }
3155
+ if (names2.readUser === names2.provisionUser)
3156
+ found.push("readUser and provisionUser are one user");
3157
+ if (!COMMENT.test(names2.comment))
3158
+ found.push("comment holds a character the bootstrap will not quote");
3159
+ return found;
3160
+ };
3161
+ var provisionBaseline = (names2 = {}) => {
3162
+ const given = Object.entries(names2).filter(([, value]) => value !== undefined);
3163
+ const resolved = {
3164
+ ...PROVISION_DEFAULTS,
3165
+ ...Object.fromEntries(given)
3166
+ };
3167
+ const found = problems2(resolved);
3168
+ if (found.length > 0)
3169
+ throw new Error(`provision baseline: ${found.join("; ")}`);
3170
+ const lanes = [
3171
+ ["provision", resolved.provisionUser, resolved.role]
3172
+ ];
3173
+ if (resolved.readUser !== null)
3174
+ lanes.push(["read", resolved.readUser, resolved.readRole]);
3175
+ return {
3176
+ grants: lanes.map(([lane, userid, roleid]) => ({ lane, roleid, userid })),
3177
+ group: { comment: resolved.comment, groupid: resolved.mintGroup },
3178
+ role: { privs: PROVISION_PRIVILEGES, roleid: resolved.role },
3179
+ users: lanes.map(([lane, userid]) => ({
3180
+ comment: resolved.comment,
3181
+ groups: [resolved.mintGroup],
3182
+ lane,
3183
+ userid
3184
+ }))
3185
+ };
3186
+ };
3187
+ // src/proxmox/provision-bootstrap.ts
3188
+ var HEADER = `#!/bin/sh
3189
+ # Provisioning baseline bootstrap, generated by @homeflare/alchemy (provisionBootstrap).
3190
+ # Run ONCE, as root, on one node of a NEW cluster (or a new standalone node). Every step reads
3191
+ # first and writes only what differs; a second run changes nothing. No token, no password.
3192
+ set -eu
3193
+ [ "$(id -u)" -eq 0 ] || { echo 'bootstrap: run this as root' >&2; exit 1; }
3194
+ command -v pvesh >/dev/null 2>&1 || { echo 'bootstrap: no pvesh; run this on a PVE node' >&2; exit 1; }
3195
+ perl -MJSON::PP -e 1 || { echo 'bootstrap: perl cannot load JSON::PP' >&2; exit 1; }
3196
+
3197
+ # get <api-path> <perl>: print <perl> evaluated over the object at <api-path> (as $d).
3198
+ # Fails, silently, when the object does not exist: the empty input is not JSON.
3199
+ get() {
3200
+ pvesh get "$1" --output-format json 2>/dev/null | perl -MJSON::PP -0777 -ne "\\$d = decode_json(\\$_); $2" 2>/dev/null
3201
+ }
3202
+ `;
3203
+ var perlText = (text3) => `q{${text3}}`;
3204
+ var USER_STATE = "my $g = $d->{groups} // []; $g = [split /,/, $g] unless ref $g; " + 'print join(";", "groups=" . join(",", sort @$g), "enable=" . (($d->{enable} // 1) ? 1 : 0), ' + '"expire=" . ($d->{expire} // 0), "email=" . ($d->{email} // ""), ' + '"firstname=" . ($d->{firstname} // ""), "lastname=" . ($d->{lastname} // ""), ' + '"comment=" . ($d->{comment} // ""))';
3205
+ var roleStep = ({ role }) => {
3206
+ const privs = [...role.privs].sort().join(",");
3207
+ return `
3208
+ # Role ${role.roleid}: exactly the provision privileges, on no path yet.
3209
+ want='${privs}'
3210
+ if have=$(get /access/roles/${role.roleid} 'print join(",", sort grep { /\\./ } keys %$d)'); then
3211
+ if [ "$have" = "$want" ]; then echo 'role ${role.roleid}: ok'
3212
+ else echo 'role ${role.roleid}: setting the baseline privileges'; pveum role modify ${role.roleid} --privs "$want"; fi
3213
+ else
3214
+ echo 'role ${role.roleid}: adding'; pveum role add ${role.roleid} --privs "$want"
3215
+ fi
3216
+ `;
3217
+ };
3218
+ var groupStep = ({ group }) => `
3219
+ # Group ${group.groupid}: OpenBao may mint only for its members.
3220
+ if have=$(get /access/groups/${group.groupid} 'print $d->{comment} // ""'); then
3221
+ if [ "$have" = '${group.comment}' ]; then echo 'group ${group.groupid}: ok'
3222
+ else echo 'group ${group.groupid}: setting the comment'; pveum group modify ${group.groupid} --comment '${group.comment}'; fi
3223
+ else
3224
+ echo 'group ${group.groupid}: adding'; pveum group add ${group.groupid} --comment '${group.comment}'
3225
+ fi
3226
+ `;
3227
+ var userStep = (user) => {
3228
+ const groups = [...user.groups].sort().join(",");
3229
+ const want = `groups=${groups};enable=1;expire=0;email=;firstname=;lastname=;comment=${user.comment}`;
3230
+ const fields = `--groups ${groups} --comment '${user.comment}'`;
3231
+ return `
3232
+ # User ${user.userid} (the ${user.lane} lane): in ${groups}, enabled, no password.
3233
+ if have=$(get /access/users/${user.userid} '${USER_STATE}'); then
3234
+ if [ "$have" = '${want}' ]; then echo 'user ${user.userid}: ok'
3235
+ else echo 'user ${user.userid}: setting groups, comment and account fields'
3236
+ pveum user modify ${user.userid} ${fields} --enable 1 --expire 0 --email '' --firstname '' --lastname ''; fi
3237
+ else
3238
+ echo 'user ${user.userid}: adding'; pveum user add ${user.userid} ${fields}
3239
+ fi
3240
+ `;
3241
+ };
3242
+ var grantStep = (grant) => {
3243
+ const match = `print scalar grep { $_->{path} eq "/" && $_->{type} eq "user" && ` + `$_->{ugid} eq ${perlText(grant.userid)} && $_->{roleid} eq ${perlText(grant.roleid)} && ` + `($_->{propagate} // 1) } @$d`;
3244
+ return `
3245
+ # Grant: ${grant.userid} holds ${grant.roleid} on /, propagated (the ${grant.lane} lane).
3246
+ if [ "$(get /access/acl '${match}')" = 1 ]; then echo 'grant ${grant.userid} ${grant.roleid} /: ok'
3247
+ else echo 'grant ${grant.userid} ${grant.roleid} /: adding'; pveum acl modify / --users ${grant.userid} --roles ${grant.roleid} --propagate 1; fi
3248
+ `;
3249
+ };
3250
+ var requireRole = (roleid) => `
3251
+ get /access/roles/${roleid} 'print 1' >/dev/null || { echo 'bootstrap: role ${roleid} does not exist; create it, or name another readRole' >&2; exit 1; }
3252
+ `;
3253
+ var provisionBootstrap = (names2 = {}) => {
3254
+ const baseline = provisionBaseline(names2);
3255
+ const borrowed = baseline.grants.map((grant) => grant.roleid).filter((roleid) => roleid !== baseline.role.roleid);
3256
+ return [
3257
+ HEADER,
3258
+ ...[...new Set(borrowed)].map(requireRole),
3259
+ roleStep(baseline),
3260
+ groupStep(baseline),
3261
+ ...baseline.users.map(userStep),
3262
+ ...baseline.grants.map(grantStep),
3263
+ `
3264
+ echo 'provision baseline: done. Next: point the OpenBao proxmox mount at these users, then deploy the stack that declares this baseline with --adopt.'
3265
+ `
3266
+ ].join("");
3267
+ };
3268
+ // src/proxmox/provision-declare.ts
3269
+ import { adopt } from "alchemy/AdoptPolicy";
3270
+ import * as RemovalPolicy from "alchemy/RemovalPolicy";
3271
+ import * as Effect49 from "effect/Effect";
3272
+
3273
+ // src/proxmox/role.ts
3274
+ import { Resource as Resource26 } from "alchemy";
3275
+ import * as Provider26 from "alchemy/Provider";
3276
+ import * as Effect47 from "effect/Effect";
3277
+ var ProxmoxRole = Resource26("Proxmox.Role");
3278
+ var canonical2 = (privs) => [...new Set(privs.map((priv) => priv.trim()).filter((priv) => priv.length > 0))].sort();
3279
+ var livePrivs = (live) => {
3280
+ const listed = live["privs"];
3281
+ if (typeof listed === "string")
3282
+ return canonical2(listed.split(","));
3283
+ return canonical2(Object.keys(live).filter((key) => key.includes(".")));
3284
+ };
3285
+ var handlers22 = pveHandlers({
3286
+ attributes: (live, props) => ({ privs: livePrivs(live), roleid: props.roleid }),
3287
+ collection: () => "access/roles",
3288
+ createForm: (props) => ({ privs: canonical2(props.privs).join(","), roleid: props.roleid }),
3289
+ matches: (attributes2, props) => canonical2(attributes2.privs).join(",") === canonical2(props.privs).join(","),
3290
+ path: (props) => `access/roles/${props.roleid}`,
3291
+ updateForm: (props) => ({ privs: canonical2(props.privs).join(",") })
3292
+ });
3293
+ var ProxmoxRoleProvider = () => Provider26.effect(ProxmoxRole, Effect47.succeed(ProxmoxRole.Provider.of(handlers22)));
3294
+
3295
+ // src/proxmox/user.ts
3296
+ import { Resource as Resource27 } from "alchemy";
3297
+ import * as Provider27 from "alchemy/Provider";
3298
+ import * as Effect48 from "effect/Effect";
3299
+ var ProxmoxUser = Resource27("Proxmox.User");
3300
+ var str2 = (value) => typeof value === "string" ? value : "";
3301
+ var int2 = (value) => {
3302
+ const parsed = typeof value === "number" ? value : Number(value ?? 0);
3303
+ return Number.isFinite(parsed) ? parsed : 0;
3304
+ };
3305
+ var groupSet = (value) => {
3306
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
3307
+ const names2 = raw.map((group) => str2(group).trim()).filter((group) => group !== "");
3308
+ return [...new Set(names2)].sort();
3309
+ };
3310
+ var tokenNames = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value).sort() : [];
2612
3311
  var shape8 = (props) => ({
3312
+ comment: props.comment ?? "",
3313
+ email: props.email ?? "",
3314
+ enable: props.enable === false ? "0" : "1",
3315
+ expire: String(props.expire ?? 0),
3316
+ firstname: props.firstname ?? "",
3317
+ groups: groupSet(props.groups).join(","),
3318
+ lastname: props.lastname ?? ""
3319
+ });
3320
+ var handlers23 = pveHandlers({
3321
+ attributes: (live, props) => ({
3322
+ comment: str2(live["comment"]),
3323
+ email: str2(live["email"]),
3324
+ enable: bool(live["enable"], true),
3325
+ expire: int2(live["expire"]),
3326
+ firstname: str2(live["firstname"]),
3327
+ groups: groupSet(live["groups"]),
3328
+ lastname: str2(live["lastname"]),
3329
+ tokens: tokenNames(live["tokens"]),
3330
+ userid: props.userid
3331
+ }),
3332
+ collection: () => "access/users",
3333
+ createForm: (props) => ({ ...shape8(props), userid: props.userid }),
3334
+ matches: (attributes2, props) => attributes2.comment === (props.comment ?? "") && attributes2.email === (props.email ?? "") && attributes2.enable === (props.enable !== false) && attributes2.expire === (props.expire ?? 0) && attributes2.firstname === (props.firstname ?? "") && attributes2.lastname === (props.lastname ?? "") && attributes2.groups.join(",") === groupSet(props.groups).join(","),
3335
+ path: (props) => `access/users/${props.userid}`,
3336
+ updateForm: shape8
3337
+ });
3338
+ var ProxmoxUserProvider = () => Provider27.effect(ProxmoxUser, Effect48.succeed(ProxmoxUser.Provider.of(handlers23)));
3339
+
3340
+ // src/proxmox/provision-declare.ts
3341
+ var declareProvisionBaseline = (id, target, names2 = {}, options = {}) => Effect49.gen(function* () {
3342
+ const baseline = yield* Effect49.try({
3343
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
3344
+ try: () => provisionBaseline(names2)
3345
+ });
3346
+ const owned = (resource) => {
3347
+ const kept = resource.pipe(RemovalPolicy.retain());
3348
+ return options.adopt === undefined ? kept : kept.pipe(adopt(options.adopt));
3349
+ };
3350
+ const role = yield* owned(ProxmoxRole(`${id}-role`, {
3351
+ privs: [...baseline.role.privs],
3352
+ roleid: baseline.role.roleid,
3353
+ target
3354
+ }));
3355
+ const group = yield* owned(ProxmoxGroup(`${id}-mint-group`, { ...baseline.group, target }));
3356
+ const users = yield* Effect49.all(baseline.users.map((user) => owned(ProxmoxUser(`${id}-${user.lane}-user`, {
3357
+ comment: user.comment,
3358
+ groups: [...user.groups],
3359
+ target,
3360
+ userid: user.userid
3361
+ }))));
3362
+ const grants = yield* Effect49.all(baseline.grants.map((grant) => owned(ProxmoxAcl(`${id}-${grant.lane}-grant`, {
3363
+ path: "/",
3364
+ propagate: true,
3365
+ roleid: grant.roleid,
3366
+ target,
3367
+ type: "user",
3368
+ ugid: grant.userid
3369
+ }))));
3370
+ return { baseline, grants, group, role, users };
3371
+ });
3372
+ // src/proxmox/qemu.ts
3373
+ import { Resource as Resource28 } from "alchemy";
3374
+ import * as Provider28 from "alchemy/Provider";
3375
+ import * as Effect50 from "effect/Effect";
3376
+ var ProxmoxVm = Resource28("Proxmox.Vm");
3377
+ var shape9 = (props) => ({
2613
3378
  cores: String(props.cores ?? 1),
2614
3379
  memory: String(props.memory ?? 512),
2615
3380
  name: props.name ?? `vm${String(props.vmid)}`,
@@ -2617,7 +3382,7 @@ var shape8 = (props) => ({
2617
3382
  sockets: String(props.sockets ?? 1),
2618
3383
  ...props.net0 === undefined ? {} : { net0: props.net0 }
2619
3384
  });
2620
- var handlers19 = pveHandlers({
3385
+ var handlers24 = pveHandlers({
2621
3386
  attributes: (live, props) => ({
2622
3387
  cores: num(live["cores"], 1),
2623
3388
  memory: num(live["memory"], 512),
@@ -2628,46 +3393,46 @@ var handlers19 = pveHandlers({
2628
3393
  vmid: props.vmid
2629
3394
  }),
2630
3395
  collection: (props) => `nodes/${props.node}/qemu`,
2631
- createForm: (props) => ({ ...shape8(props), vmid: String(props.vmid) }),
3396
+ createForm: (props) => ({ ...shape9(props), vmid: String(props.vmid) }),
2632
3397
  matches: (attributes2, props) => attributes2.name === (props.name ?? `vm${String(props.vmid)}`) && attributes2.memory === (props.memory ?? 512) && attributes2.cores === (props.cores ?? 1) && attributes2.sockets === (props.sockets ?? 1) && attributes2.onboot === (props.onboot === true),
2633
3398
  path: (props) => `nodes/${props.node}/qemu/${String(props.vmid)}/config`,
2634
- updateForm: shape8
3399
+ updateForm: shape9
2635
3400
  });
2636
- var ProxmoxVmProvider = () => Provider23.effect(ProxmoxVm, Effect43.succeed(ProxmoxVm.Provider.of(handlers19)));
3401
+ var ProxmoxVmProvider = () => Provider28.effect(ProxmoxVm, Effect50.succeed(ProxmoxVm.Provider.of(handlers24)));
2637
3402
  // src/proxmox/replication-job.ts
2638
- import { Resource as Resource24 } from "alchemy";
2639
- import * as Provider24 from "alchemy/Provider";
2640
- import * as Effect44 from "effect/Effect";
3403
+ import { Resource as Resource29 } from "alchemy";
3404
+ import * as Provider29 from "alchemy/Provider";
3405
+ import * as Effect51 from "effect/Effect";
2641
3406
 
2642
3407
  // src/proxmox/replication-job-form.ts
2643
3408
  var DEFAULT_SCHEDULE = "*/15";
2644
3409
  var jobId = (props) => `${String(props.guest)}-${String(props.jobnum)}`;
2645
- var shape9 = (props) => ({
3410
+ var shape10 = (props) => ({
2646
3411
  comment: props.comment ?? "",
2647
3412
  disable: props.disable === true ? "1" : "0",
2648
3413
  schedule: props.schedule ?? DEFAULT_SCHEDULE,
2649
3414
  ...props.rate === undefined ? {} : { rate: String(props.rate) }
2650
3415
  });
2651
3416
  var createBody4 = (props) => ({
2652
- ...shape9(props),
3417
+ ...shape10(props),
2653
3418
  id: jobId(props),
2654
3419
  target: props.targetNode,
2655
3420
  type: "local"
2656
3421
  });
2657
3422
  var updateBody5 = (props) => ({
2658
- ...shape9(props),
3423
+ ...shape10(props),
2659
3424
  delete: "remove_job"
2660
3425
  });
2661
3426
 
2662
3427
  // src/proxmox/replication-job.ts
2663
- var ProxmoxReplicationJob = Resource24("Proxmox.ReplicationJob");
3428
+ var ProxmoxReplicationJob = Resource29("Proxmox.ReplicationJob");
2664
3429
  var rateOf = (value) => {
2665
3430
  if (typeof value === "number")
2666
3431
  return value;
2667
3432
  const parsed = Number.parseFloat(text(value));
2668
3433
  return Number.isFinite(parsed) ? parsed : 0;
2669
3434
  };
2670
- var handlers20 = pveHandlers({
3435
+ var handlers25 = pveHandlers({
2671
3436
  attributes: (live, props) => ({
2672
3437
  comment: text(live["comment"], ""),
2673
3438
  disable: bool(live["disable"], false),
@@ -2687,36 +3452,15 @@ var handlers20 = pveHandlers({
2687
3452
  path: (props) => `cluster/replication/${jobId(props)}`,
2688
3453
  updateForm: updateBody5
2689
3454
  });
2690
- var ProxmoxReplicationJobProvider = () => Provider24.effect(ProxmoxReplicationJob, Effect44.succeed(ProxmoxReplicationJob.Provider.of(handlers20)));
2691
- // src/proxmox/role.ts
2692
- import { Resource as Resource25 } from "alchemy";
2693
- import * as Provider25 from "alchemy/Provider";
2694
- import * as Effect45 from "effect/Effect";
2695
- var ProxmoxRole = Resource25("Proxmox.Role");
2696
- var canonical = (privs) => [...new Set(privs.map((priv) => priv.trim()).filter((priv) => priv.length > 0))].sort();
2697
- var livePrivs = (live) => {
2698
- const listed = live["privs"];
2699
- if (typeof listed === "string")
2700
- return canonical(listed.split(","));
2701
- return canonical(Object.keys(live).filter((key) => key.includes(".")));
2702
- };
2703
- var handlers21 = pveHandlers({
2704
- attributes: (live, props) => ({ privs: livePrivs(live), roleid: props.roleid }),
2705
- collection: () => "access/roles",
2706
- createForm: (props) => ({ privs: canonical(props.privs).join(","), roleid: props.roleid }),
2707
- matches: (attributes2, props) => canonical(attributes2.privs).join(",") === canonical(props.privs).join(","),
2708
- path: (props) => `access/roles/${props.roleid}`,
2709
- updateForm: (props) => ({ privs: canonical(props.privs).join(",") })
2710
- });
2711
- var ProxmoxRoleProvider = () => Provider25.effect(ProxmoxRole, Effect45.succeed(ProxmoxRole.Provider.of(handlers21)));
3455
+ var ProxmoxReplicationJobProvider = () => Provider29.effect(ProxmoxReplicationJob, Effect51.succeed(ProxmoxReplicationJob.Provider.of(handlers25)));
2712
3456
  // src/proxmox/sdn-apply.ts
2713
- import { Resource as Resource26 } from "alchemy";
2714
- import { isResolved as isResolved9 } from "alchemy/Diff";
2715
- import * as Provider26 from "alchemy/Provider";
2716
- import * as Effect47 from "effect/Effect";
3457
+ import { Resource as Resource30 } from "alchemy";
3458
+ import { isResolved as isResolved10 } from "alchemy/Diff";
3459
+ import * as Provider30 from "alchemy/Provider";
3460
+ import * as Effect53 from "effect/Effect";
2717
3461
 
2718
3462
  // src/proxmox/sdn-apply-read.ts
2719
- import * as Effect46 from "effect/Effect";
3463
+ import * as Effect52 from "effect/Effect";
2720
3464
  var MARKED = [
2721
3465
  "cluster/sdn/zones",
2722
3466
  "cluster/sdn/vnets",
@@ -2725,7 +3469,7 @@ var MARKED = [
2725
3469
  "cluster/sdn/route-maps"
2726
3470
  ];
2727
3471
  var subsystemAbsent = (error) => error instanceof PveError && error.status === 501;
2728
- var markedPending = (target) => Effect46.all(MARKED.map((collection2) => pve(target, "read", "GET", `${collection2}?pending=1`).pipe(Effect46.map((rows) => (rows ?? []).filter((row) => row.state !== undefined).length), Effect46.catchIf(subsystemAbsent, () => Effect46.succeed(0)))), { concurrency: "unbounded" }).pipe(Effect46.map((counts) => counts.reduce((total, count2) => total + count2, 0)));
3472
+ var markedPending = (target) => Effect52.all(MARKED.map((collection2) => pve(target, "read", "GET", `${collection2}?pending=1`).pipe(Effect52.map((rows) => (rows ?? []).filter((row) => row.state !== undefined).length), Effect52.catchIf(subsystemAbsent, () => Effect52.succeed(0)))), { concurrency: "unbounded" }).pipe(Effect52.map((counts) => counts.reduce((total, count2) => total + count2, 0)));
2729
3473
  var fabricStaged = (staged, running) => {
2730
3474
  if (staged === undefined && running === undefined)
2731
3475
  return 0;
@@ -2733,11 +3477,11 @@ var fabricStaged = (staged, running) => {
2733
3477
  return "inconsistent";
2734
3478
  return staged === running ? 0 : 1;
2735
3479
  };
2736
- var fabricPending = (target) => Effect46.all(["pending", "running"].map((view) => pve(target, "read", "GET", `cluster/sdn/fabrics/all?${view}=1`).pipe(Effect46.map(canonical2), Effect46.catchIf(subsystemAbsent, () => Effect46.succeed(undefined)))), { concurrency: "unbounded" }).pipe(Effect46.flatMap(([staged, running]) => {
3480
+ var fabricPending = (target) => Effect52.all(["pending", "running"].map((view) => pve(target, "read", "GET", `cluster/sdn/fabrics/all?${view}=1`).pipe(Effect52.map(canonical3), Effect52.catchIf(subsystemAbsent, () => Effect52.succeed(undefined)))), { concurrency: "unbounded" }).pipe(Effect52.flatMap(([staged, running]) => {
2737
3481
  const verdict = fabricStaged(staged, running);
2738
- return verdict === "inconsistent" ? Effect46.die(new Error("cluster/sdn/fabrics/all answered 501 for one of ?pending=1 and ?running=1 and not " + "the other. Refusing to read that as a staged fabric change.")) : Effect46.succeed(verdict);
3482
+ return verdict === "inconsistent" ? Effect52.die(new Error("cluster/sdn/fabrics/all answered 501 for one of ?pending=1 and ?running=1 and not " + "the other. Refusing to read that as a staged fabric change.")) : Effect52.succeed(verdict);
2739
3483
  }));
2740
- var canonical2 = (value) => JSON.stringify(sortDeep(value));
3484
+ var canonical3 = (value) => JSON.stringify(sortDeep(value));
2741
3485
  var sortDeep = (value) => {
2742
3486
  if (Array.isArray(value)) {
2743
3487
  return value.map(sortDeep).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
@@ -2748,39 +3492,39 @@ var sortDeep = (value) => {
2748
3492
  }
2749
3493
  return value;
2750
3494
  };
2751
- var sdnPendingCount = (target) => Effect46.all([markedPending(target), fabricPending(target)], { concurrency: "unbounded" }).pipe(Effect46.map(([marked, fabric]) => marked + fabric));
3495
+ var sdnPendingCount = (target) => Effect52.all([markedPending(target), fabricPending(target)], { concurrency: "unbounded" }).pipe(Effect52.map(([marked, fabric]) => marked + fabric));
2752
3496
 
2753
3497
  // src/proxmox/sdn-apply.ts
2754
- var ProxmoxSdnApply = Resource26("Proxmox.SdnApply");
2755
- var read3 = (props) => sdnPendingCount(props.target).pipe(Effect47.map((pending) => ({ pending })));
2756
- var ProxmoxSdnApplyProvider = () => Provider26.effect(ProxmoxSdnApply, Effect47.succeed(ProxmoxSdnApply.Provider.of({
2757
- list: () => Effect47.succeed([]),
2758
- read: Effect47.fn(function* ({ olds }) {
3498
+ var ProxmoxSdnApply = Resource30("Proxmox.SdnApply");
3499
+ var read3 = (props) => sdnPendingCount(props.target).pipe(Effect53.map((pending) => ({ pending })));
3500
+ var ProxmoxSdnApplyProvider = () => Provider30.effect(ProxmoxSdnApply, Effect53.succeed(ProxmoxSdnApply.Provider.of({
3501
+ list: () => Effect53.succeed([]),
3502
+ read: Effect53.fn(function* ({ olds }) {
2759
3503
  return yield* read3(olds);
2760
3504
  }),
2761
- diff: Effect47.fn(function* ({ news }) {
2762
- if (!isResolved9(news))
3505
+ diff: Effect53.fn(function* ({ news }) {
3506
+ if (!isResolved10(news))
2763
3507
  return;
2764
3508
  const { pending } = yield* read3(news);
2765
3509
  return pending === 0 ? { action: "noop" } : { action: "update" };
2766
3510
  }),
2767
- reconcile: Effect47.fn(function* ({ news }) {
3511
+ reconcile: Effect53.fn(function* ({ news }) {
2768
3512
  const staged = yield* read3(news);
2769
3513
  if (staged.pending === 0)
2770
3514
  return staged;
2771
3515
  yield* pve(news.target, "provision", "PUT", "cluster/sdn");
2772
3516
  const after = yield* read3(news);
2773
3517
  if (after.pending !== 0) {
2774
- return yield* Effect47.die(new Error(`PUT /cluster/sdn returned no error but ${String(after.pending)} SDN object(s) ` + "are still staged. The running config does not match the declared one -- check " + "`pvesh get /cluster/sdn/zones --pending 1` for the objects PVE refused."));
3518
+ return yield* Effect53.die(new Error(`PUT /cluster/sdn returned no error but ${String(after.pending)} SDN object(s) ` + "are still staged. The running config does not match the declared one -- check " + "`pvesh get /cluster/sdn/zones --pending 1` for the objects PVE refused."));
2775
3519
  }
2776
3520
  return after;
2777
3521
  }),
2778
- delete: () => Effect47.void
3522
+ delete: () => Effect53.void
2779
3523
  })));
2780
3524
  // src/proxmox/sdn-subnet.ts
2781
- import { Resource as Resource27 } from "alchemy";
2782
- import * as Provider27 from "alchemy/Provider";
2783
- import * as Effect48 from "effect/Effect";
3525
+ import { Resource as Resource31 } from "alchemy";
3526
+ import * as Provider31 from "alchemy/Provider";
3527
+ import * as Effect54 from "effect/Effect";
2784
3528
 
2785
3529
  // src/proxmox/sdn-subnet-form.ts
2786
3530
  var subnetId = (props) => `${props.zone}-${props.cidr.replace("/", "-")}`;
@@ -2810,8 +3554,8 @@ var updateForm2 = (props) => {
2810
3554
  };
2811
3555
 
2812
3556
  // src/proxmox/sdn-subnet.ts
2813
- var ProxmoxSdnSubnet = Resource27("Proxmox.SdnSubnet");
2814
- var handlers22 = pveHandlers({
3557
+ var ProxmoxSdnSubnet = Resource31("Proxmox.SdnSubnet");
3558
+ var handlers26 = pveHandlers({
2815
3559
  attributes: (live, props) => {
2816
3560
  const liveVnet = text(live["vnet"]);
2817
3561
  if (liveVnet !== "" && liveVnet !== props.vnet)
@@ -2836,14 +3580,14 @@ var handlers22 = pveHandlers({
2836
3580
  path: (props) => `cluster/sdn/vnets/${props.vnet}/subnets/${subnetId(props)}`,
2837
3581
  updateForm: updateForm2
2838
3582
  });
2839
- var ProxmoxSdnSubnetProvider = () => Provider27.effect(ProxmoxSdnSubnet, Effect48.succeed(ProxmoxSdnSubnet.Provider.of(handlers22)));
3583
+ var ProxmoxSdnSubnetProvider = () => Provider31.effect(ProxmoxSdnSubnet, Effect54.succeed(ProxmoxSdnSubnet.Provider.of(handlers26)));
2840
3584
  // src/proxmox/sdn-vnet.ts
2841
- import { Resource as Resource28 } from "alchemy";
2842
- import * as Provider28 from "alchemy/Provider";
2843
- import * as Effect49 from "effect/Effect";
2844
- var ProxmoxSdnVnet = Resource28("Proxmox.SdnVnet");
3585
+ import { Resource as Resource32 } from "alchemy";
3586
+ import * as Provider32 from "alchemy/Provider";
3587
+ import * as Effect55 from "effect/Effect";
3588
+ var ProxmoxSdnVnet = Resource32("Proxmox.SdnVnet");
2845
3589
  var tagOf = (value) => typeof value === "number" ? value : undefined;
2846
- var shape10 = (props) => {
3590
+ var shape11 = (props) => {
2847
3591
  const alias = props.alias ?? "";
2848
3592
  return {
2849
3593
  "isolate-ports": props.isolatePorts === true ? "1" : "0",
@@ -2858,9 +3602,9 @@ var updateForm3 = (props) => {
2858
3602
  ...(props.alias ?? "") === "" ? ["alias"] : [],
2859
3603
  ...props.tag === undefined ? ["tag"] : []
2860
3604
  ];
2861
- return withClears(shape10(props), unset2);
3605
+ return withClears(shape11(props), unset2);
2862
3606
  };
2863
- var handlers23 = pveHandlers({
3607
+ var handlers27 = pveHandlers({
2864
3608
  readRole: "provision",
2865
3609
  attributes: (live, props) => ({
2866
3610
  alias: typeof live["alias"] === "string" ? live["alias"] : "",
@@ -2872,18 +3616,18 @@ var handlers23 = pveHandlers({
2872
3616
  zone: typeof live["zone"] === "string" ? live["zone"] : ""
2873
3617
  }),
2874
3618
  collection: () => "cluster/sdn/vnets",
2875
- createForm: (props) => ({ ...shape10(props), vnet: props.vnet }),
3619
+ createForm: (props) => ({ ...shape11(props), vnet: props.vnet }),
2876
3620
  matches: (attributes2, props) => attributes2.zone === props.zone && attributes2.alias === (props.alias ?? "") && attributes2.tag === props.tag && attributes2.vlanaware === (props.vlanaware === true) && attributes2.isolatePorts === (props.isolatePorts === true),
2877
3621
  path: (props) => `cluster/sdn/vnets/${props.vnet}`,
2878
3622
  updateForm: updateForm3
2879
3623
  });
2880
- var ProxmoxSdnVnetProvider = () => Provider28.effect(ProxmoxSdnVnet, Effect49.succeed(ProxmoxSdnVnet.Provider.of(handlers23)));
3624
+ var ProxmoxSdnVnetProvider = () => Provider32.effect(ProxmoxSdnVnet, Effect55.succeed(ProxmoxSdnVnet.Provider.of(handlers27)));
2881
3625
  // src/proxmox/sdn-zone.ts
2882
- import { Resource as Resource29 } from "alchemy";
2883
- import * as Provider29 from "alchemy/Provider";
2884
- import * as Effect50 from "effect/Effect";
2885
- var ProxmoxSdnZone = Resource29("Proxmox.SdnZone");
2886
- var sameIpam = (live, declared) => (live === "" ? "pve" : live) === (declared === undefined || declared === "" ? "pve" : declared);
3626
+ import { Resource as Resource33 } from "alchemy";
3627
+ import * as Provider33 from "alchemy/Provider";
3628
+ import * as Effect56 from "effect/Effect";
3629
+ var ProxmoxSdnZone = Resource33("Proxmox.SdnZone");
3630
+ var sameIpam = (live, declared2) => (live === "" ? "pve" : live) === (declared2 === undefined || declared2 === "" ? "pve" : declared2);
2887
3631
  var body6 = (props) => ({
2888
3632
  ...props.bridge === undefined ? {} : { bridge: props.bridge },
2889
3633
  ...props.dns === undefined ? {} : { dns: props.dns },
@@ -2902,7 +3646,7 @@ var clearable = {
2902
3646
  vlan: ["dns", "dnszone", "ipam", "mtu", "nodes"],
2903
3647
  vxlan: ["dns", "dnszone", "ipam", "mtu", "nodes"]
2904
3648
  };
2905
- var handlers24 = pveHandlers({
3649
+ var handlers28 = pveHandlers({
2906
3650
  readRole: "provision",
2907
3651
  attributes: (live, props) => {
2908
3652
  const liveType = text(live["type"]);
@@ -2933,43 +3677,43 @@ var handlers24 = pveHandlers({
2933
3677
  return withClears(fields, clear);
2934
3678
  }
2935
3679
  });
2936
- var ProxmoxSdnZoneProvider = () => Provider29.effect(ProxmoxSdnZone, Effect50.succeed(ProxmoxSdnZone.Provider.of(handlers24)));
3680
+ var ProxmoxSdnZoneProvider = () => Provider33.effect(ProxmoxSdnZone, Effect56.succeed(ProxmoxSdnZone.Provider.of(handlers28)));
2937
3681
  // src/proxmox/storage.ts
2938
- import { Resource as Resource30 } from "alchemy";
2939
- import * as Provider30 from "alchemy/Provider";
2940
- import * as Effect51 from "effect/Effect";
2941
- var ProxmoxStorage = Resource30("Proxmox.Storage", {
3682
+ import { Resource as Resource34 } from "alchemy";
3683
+ import * as Provider34 from "alchemy/Provider";
3684
+ import * as Effect57 from "effect/Effect";
3685
+ var ProxmoxStorage = Resource34("Proxmox.Storage", {
2942
3686
  defaultRemovalPolicy: "retain"
2943
3687
  });
2944
- var str2 = (value, fallback = "") => typeof value === "string" ? value : fallback;
2945
- var field5 = (name, value) => value === undefined ? {} : { [name]: value };
3688
+ var str3 = (value, fallback = "") => typeof value === "string" ? value : fallback;
3689
+ var field6 = (name, value) => value === undefined ? {} : { [name]: value };
2946
3690
  var locatorForm = (locator) => ({ ...locator });
2947
3691
  var tokens = (value) => value.split(",").map((part) => part.trim()).filter((part) => part !== "").sort().join(",");
2948
- var sameList2 = (declared, live) => declared === undefined || tokens(declared) === tokens(live);
2949
- var same5 = (declared, live) => declared === undefined || declared === live;
3692
+ var sameList2 = (declared2, live) => declared2 === undefined || tokens(declared2) === tokens(live);
3693
+ var same7 = (declared2, live) => declared2 === undefined || declared2 === live;
2950
3694
  var mutable2 = (props) => ({
2951
- ...field5("comment", props.comment),
2952
- ...field5("content", props.content),
2953
- ...field5("disable", flag(props.disable)),
2954
- ...field5("maxfiles", props.maxfiles === undefined ? undefined : String(props.maxfiles)),
2955
- ...field5("nodes", props.nodes),
2956
- ...field5("preallocation", props.preallocation),
2957
- ...field5("prune-backups", props["prune-backups"]),
2958
- ...field5("shared", flag(props.shared))
2959
- });
2960
- var handlers25 = pveHandlers({
3695
+ ...field6("comment", props.comment),
3696
+ ...field6("content", props.content),
3697
+ ...field6("disable", flag(props.disable)),
3698
+ ...field6("maxfiles", props.maxfiles === undefined ? undefined : String(props.maxfiles)),
3699
+ ...field6("nodes", props.nodes),
3700
+ ...field6("preallocation", props.preallocation),
3701
+ ...field6("prune-backups", props["prune-backups"]),
3702
+ ...field6("shared", flag(props.shared))
3703
+ });
3704
+ var handlers29 = pveHandlers({
2961
3705
  readRole: "provision",
2962
3706
  attributes: (live, props) => ({
2963
- comment: str2(live["comment"]),
2964
- content: str2(live["content"]),
3707
+ comment: str3(live["comment"]),
3708
+ content: str3(live["content"]),
2965
3709
  disable: bool(live["disable"]),
2966
3710
  maxfiles: typeof live["maxfiles"] === "number" ? live["maxfiles"] : -1,
2967
- nodes: str2(live["nodes"]),
2968
- preallocation: str2(live["preallocation"]),
2969
- "prune-backups": str2(live["prune-backups"]),
3711
+ nodes: str3(live["nodes"]),
3712
+ preallocation: str3(live["preallocation"]),
3713
+ "prune-backups": str3(live["prune-backups"]),
2970
3714
  shared: bool(live["shared"]),
2971
3715
  storage: props.storage,
2972
- type: str2(live["type"], props.type)
3716
+ type: str3(live["type"], props.type)
2973
3717
  }),
2974
3718
  collection: () => "storage",
2975
3719
  createForm: (props) => ({
@@ -2978,62 +3722,18 @@ var handlers25 = pveHandlers({
2978
3722
  storage: props.storage,
2979
3723
  type: props.type
2980
3724
  }),
2981
- matches: (attributes2, props) => sameList2(props.content, attributes2.content) && sameList2(props.nodes, attributes2.nodes) && sameList2(props["prune-backups"], attributes2["prune-backups"]) && same5(props.disable, attributes2.disable) && same5(props.shared, attributes2.shared) && same5(props.preallocation, attributes2.preallocation) && same5(props.comment, attributes2.comment) && same5(props.maxfiles, attributes2.maxfiles),
3725
+ matches: (attributes2, props) => sameList2(props.content, attributes2.content) && sameList2(props.nodes, attributes2.nodes) && sameList2(props["prune-backups"], attributes2["prune-backups"]) && same7(props.disable, attributes2.disable) && same7(props.shared, attributes2.shared) && same7(props.preallocation, attributes2.preallocation) && same7(props.comment, attributes2.comment) && same7(props.maxfiles, attributes2.maxfiles),
2982
3726
  path: (props) => `storage/${props.storage}`,
2983
3727
  updateForm: mutable2
2984
3728
  });
2985
- var ProxmoxStorageProvider = () => Provider30.effect(ProxmoxStorage, Effect51.succeed(ProxmoxStorage.Provider.of(handlers25)));
2986
- // src/proxmox/user.ts
2987
- import { Resource as Resource31 } from "alchemy";
2988
- import * as Provider31 from "alchemy/Provider";
2989
- import * as Effect52 from "effect/Effect";
2990
- var ProxmoxUser = Resource31("Proxmox.User");
2991
- var str3 = (value) => typeof value === "string" ? value : "";
2992
- var int2 = (value) => {
2993
- const parsed = typeof value === "number" ? value : Number(value ?? 0);
2994
- return Number.isFinite(parsed) ? parsed : 0;
2995
- };
2996
- var groupSet = (value) => {
2997
- const raw = Array.isArray(value) ? value : typeof value === "string" ? value.split(",") : [];
2998
- const names = raw.map((group) => str3(group).trim()).filter((group) => group !== "");
2999
- return [...new Set(names)].sort();
3000
- };
3001
- var tokenNames = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value).sort() : [];
3002
- var shape11 = (props) => ({
3003
- comment: props.comment ?? "",
3004
- email: props.email ?? "",
3005
- enable: props.enable === false ? "0" : "1",
3006
- expire: String(props.expire ?? 0),
3007
- firstname: props.firstname ?? "",
3008
- groups: groupSet(props.groups).join(","),
3009
- lastname: props.lastname ?? ""
3010
- });
3011
- var handlers26 = pveHandlers({
3012
- attributes: (live, props) => ({
3013
- comment: str3(live["comment"]),
3014
- email: str3(live["email"]),
3015
- enable: bool(live["enable"], true),
3016
- expire: int2(live["expire"]),
3017
- firstname: str3(live["firstname"]),
3018
- groups: groupSet(live["groups"]),
3019
- lastname: str3(live["lastname"]),
3020
- tokens: tokenNames(live["tokens"]),
3021
- userid: props.userid
3022
- }),
3023
- collection: () => "access/users",
3024
- createForm: (props) => ({ ...shape11(props), userid: props.userid }),
3025
- matches: (attributes2, props) => attributes2.comment === (props.comment ?? "") && attributes2.email === (props.email ?? "") && attributes2.enable === (props.enable !== false) && attributes2.expire === (props.expire ?? 0) && attributes2.firstname === (props.firstname ?? "") && attributes2.lastname === (props.lastname ?? "") && attributes2.groups.join(",") === groupSet(props.groups).join(","),
3026
- path: (props) => `access/users/${props.userid}`,
3027
- updateForm: shape11
3028
- });
3029
- var ProxmoxUserProvider = () => Provider31.effect(ProxmoxUser, Effect52.succeed(ProxmoxUser.Provider.of(handlers26)));
3729
+ var ProxmoxStorageProvider = () => Provider34.effect(ProxmoxStorage, Effect57.succeed(ProxmoxStorage.Provider.of(handlers29)));
3030
3730
  // src/proxmox/zfs-pool.ts
3031
- import { Resource as Resource32 } from "alchemy";
3032
- import * as Provider32 from "alchemy/Provider";
3033
- import * as Effect54 from "effect/Effect";
3731
+ import { Resource as Resource35 } from "alchemy";
3732
+ import * as Provider35 from "alchemy/Provider";
3733
+ import * as Effect59 from "effect/Effect";
3034
3734
 
3035
3735
  // src/proxmox/zfs-pool-write.ts
3036
- import * as Effect53 from "effect/Effect";
3736
+ import * as Effect58 from "effect/Effect";
3037
3737
  var createForm6 = (props) => ({
3038
3738
  devices: props.devices.join(","),
3039
3739
  name: props.name,
@@ -3043,28 +3743,28 @@ var createForm6 = (props) => ({
3043
3743
  ...props["draid-config"] === undefined ? {} : { "draid-config": props["draid-config"] }
3044
3744
  });
3045
3745
  var SETTLE_ATTEMPTS = 30;
3046
- var createPool = (props, collection2, path, read4) => Effect53.gen(function* () {
3746
+ var createPool = (props, collection2, path2, read4) => Effect58.gen(function* () {
3047
3747
  const existing = yield* read4(props);
3048
3748
  if (existing !== undefined)
3049
3749
  return existing;
3050
3750
  const upid = yield* pve(props.target, "provision", "POST", collection2, createForm6(props));
3051
- const settle4 = (attempts) => Effect53.gen(function* () {
3751
+ const settle4 = (attempts) => Effect58.gen(function* () {
3052
3752
  const live = yield* read4(props);
3053
3753
  if (live !== undefined || attempts <= 0)
3054
3754
  return live;
3055
- yield* Effect53.sleep("2 seconds");
3755
+ yield* Effect58.sleep("2 seconds");
3056
3756
  return yield* settle4(attempts - 1);
3057
3757
  });
3058
3758
  const after = yield* settle4(SETTLE_ATTEMPTS);
3059
3759
  if (after === undefined) {
3060
- return yield* Effect53.die(new Error(`${path}: POST returned ${text(upid, "(no UPID)")} but the pool has not appeared. That ` + "POST only forks a `zfscreate` worker, so the task holds the real error -- read it " + `with \`pvesh get /nodes/${props.node}/tasks/<upid>/log\`. NOTHING WAS RETRIED AND ` + "NOTHING WAS CLEANED UP: if the worker reached `zpool create`, the declared devices " + "have already been written to."));
3760
+ return yield* Effect58.die(new Error(`${path2}: POST returned ${text(upid, "(no UPID)")} but the pool has not appeared. That ` + "POST only forks a `zfscreate` worker, so the task holds the real error -- read it " + `with \`pvesh get /nodes/${props.node}/tasks/<upid>/log\`. NOTHING WAS RETRIED AND ` + "NOTHING WAS CLEANED UP: if the worker reached `zpool create`, the declared devices " + "have already been written to."));
3061
3761
  }
3062
3762
  return after;
3063
3763
  });
3064
- var destroyPool = (olds, path) => pve(olds.target, "provision", "DELETE", path);
3764
+ var destroyPool = (olds, path2) => pve(olds.target, "provision", "DELETE", path2);
3065
3765
 
3066
3766
  // src/proxmox/zfs-pool.ts
3067
- var ProxmoxZfsPool = Resource32("Proxmox.ZfsPool", {
3767
+ var ProxmoxZfsPool = Resource35("Proxmox.ZfsPool", {
3068
3768
  defaultRemovalPolicy: "retain"
3069
3769
  });
3070
3770
  var leafDevices = (children) => Array.isArray(children) ? children.flatMap((entry) => {
@@ -3090,17 +3790,23 @@ var spec2 = {
3090
3790
  path: (props) => `nodes/${props.node}/disks/zfs/${props.name}`
3091
3791
  };
3092
3792
  var ops5 = pveOperations(spec2);
3093
- var handlers27 = {
3793
+ var handlers30 = {
3094
3794
  ...pveHandlers(spec2),
3095
- delete: Effect54.fn(function* ({ olds }) {
3795
+ delete: Effect59.fn(function* ({ olds }) {
3096
3796
  yield* destroyPool(olds, spec2.path(olds));
3097
3797
  }),
3098
3798
  reconcile: ({ news }) => createPool(news, spec2.collection(news), spec2.path(news), ops5.read)
3099
3799
  };
3100
- var ProxmoxZfsPoolProvider = () => Provider32.effect(ProxmoxZfsPool, Effect54.succeed(ProxmoxZfsPool.Provider.of(handlers27)));
3800
+ var ProxmoxZfsPoolProvider = () => Provider35.effect(ProxmoxZfsPool, Effect59.succeed(ProxmoxZfsPool.Provider.of(handlers30)));
3101
3801
  export {
3802
+ PROVISION_DEFAULTS,
3803
+ PROVISION_PRIVILEGES,
3102
3804
  PbsDatastore,
3103
3805
  PbsDatastoreProvider,
3806
+ PbsNotificationMatcher,
3807
+ PbsNotificationMatcherProvider,
3808
+ PbsNotificationTarget,
3809
+ PbsNotificationTargetProvider,
3104
3810
  PbsPruneJob,
3105
3811
  PbsPruneJobProvider,
3106
3812
  PbsSyncJob,
@@ -3131,6 +3837,8 @@ export {
3131
3837
  ProxmoxMetricServerProvider,
3132
3838
  ProxmoxNetworkApplyProvider,
3133
3839
  ProxmoxNodeNetworkProvider,
3840
+ ProxmoxNotificationMatcher,
3841
+ ProxmoxNotificationMatcherProvider,
3134
3842
  ProxmoxNotificationTarget,
3135
3843
  ProxmoxNotificationTargetProvider,
3136
3844
  ProxmoxPool,
@@ -3151,8 +3859,12 @@ export {
3151
3859
  ProxmoxUser,
3152
3860
  ProxmoxUserProvider,
3153
3861
  ProxmoxVmProvider,
3154
- ProxmoxZfsPoolProvider
3862
+ ProxmoxZfsPoolProvider,
3863
+ alertmanagerAlertBody,
3864
+ declareProvisionBaseline,
3865
+ provisionBaseline,
3866
+ provisionBootstrap
3155
3867
  };
3156
3868
 
3157
- //# debugId=6EEA5191A9F0C54064756E2164756E21
3869
+ //# debugId=98691C143D2F953764756E2164756E21
3158
3870
  //# sourceMappingURL=index.js.map