@homeflare/alchemy 0.37.2 → 0.37.4

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 (178) hide show
  1. package/dist/linux/directory-lifecycle.d.ts.map +1 -1
  2. package/dist/linux/directory.d.ts +5 -3
  3. package/dist/linux/directory.d.ts.map +1 -1
  4. package/dist/linux/index.js +4 -3
  5. package/dist/linux/index.js.map +4 -4
  6. package/dist/litellm/fake-stack.d.ts +7 -7
  7. package/dist/litellm/fake-stack.d.ts.map +1 -1
  8. package/dist/litellm/index.js +2 -2
  9. package/dist/litellm/index.js.map +4 -4
  10. package/dist/litellm/pass-through-endpoint.d.ts +9 -8
  11. package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
  12. package/dist/litellm/providers.d.ts +8 -2
  13. package/dist/litellm/providers.d.ts.map +1 -1
  14. package/dist/proxmox/backup-job-config.d.ts +95 -0
  15. package/dist/proxmox/backup-job-config.d.ts.map +1 -0
  16. package/dist/proxmox/backup-job-distilled.d.ts +46 -0
  17. package/dist/proxmox/backup-job-distilled.d.ts.map +1 -0
  18. package/dist/proxmox/backup-job.d.ts +9 -26
  19. package/dist/proxmox/backup-job.d.ts.map +1 -1
  20. package/dist/proxmox/distilled-pbs.d.ts +12 -0
  21. package/dist/proxmox/distilled-pbs.d.ts.map +1 -0
  22. package/dist/proxmox/fake-pbs-jobs.d.ts +23 -0
  23. package/dist/proxmox/fake-pbs-jobs.d.ts.map +1 -0
  24. package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -1
  25. package/dist/proxmox/fake-pve.d.ts +1 -1
  26. package/dist/proxmox/fake-pve.d.ts.map +1 -1
  27. package/dist/proxmox/index.js +1501 -607
  28. package/dist/proxmox/index.js.map +48 -24
  29. package/dist/proxmox/live-core-fixtures.d.ts +40 -0
  30. package/dist/proxmox/live-core-fixtures.d.ts.map +1 -0
  31. package/dist/proxmox/metric-server-config.d.ts +49 -0
  32. package/dist/proxmox/metric-server-config.d.ts.map +1 -0
  33. package/dist/proxmox/metric-server-distilled.d.ts +43 -0
  34. package/dist/proxmox/metric-server-distilled.d.ts.map +1 -0
  35. package/dist/proxmox/metric-server.d.ts +2 -10
  36. package/dist/proxmox/metric-server.d.ts.map +1 -1
  37. package/dist/proxmox/notification-matcher-distilled.d.ts +9 -0
  38. package/dist/proxmox/notification-matcher-distilled.d.ts.map +1 -0
  39. package/dist/proxmox/notification-matcher-lifecycle.d.ts +27 -0
  40. package/dist/proxmox/notification-matcher-lifecycle.d.ts.map +1 -0
  41. package/dist/proxmox/notification-matcher.d.ts +5 -5
  42. package/dist/proxmox/notification-matcher.d.ts.map +1 -1
  43. package/dist/proxmox/notification-sdk-input.d.ts +22 -0
  44. package/dist/proxmox/notification-sdk-input.d.ts.map +1 -0
  45. package/dist/proxmox/notification-target-distilled.d.ts +12 -0
  46. package/dist/proxmox/notification-target-distilled.d.ts.map +1 -0
  47. package/dist/proxmox/notification-target-form.d.ts +2 -0
  48. package/dist/proxmox/notification-target-form.d.ts.map +1 -1
  49. package/dist/proxmox/notification-target-lifecycle.d.ts +29 -0
  50. package/dist/proxmox/notification-target-lifecycle.d.ts.map +1 -0
  51. package/dist/proxmox/notification-target-spec.d.ts +5 -0
  52. package/dist/proxmox/notification-target-spec.d.ts.map +1 -0
  53. package/dist/proxmox/notification-target.d.ts +4 -5
  54. package/dist/proxmox/notification-target.d.ts.map +1 -1
  55. package/dist/proxmox/pbs-datastore-endpoint.d.ts +1 -1
  56. package/dist/proxmox/pbs-datastore-form.d.ts +18 -42
  57. package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -1
  58. package/dist/proxmox/pbs-datastore-guard.d.ts +7 -4
  59. package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -1
  60. package/dist/proxmox/pbs-datastore-lifecycle.d.ts +56 -0
  61. package/dist/proxmox/pbs-datastore-lifecycle.d.ts.map +1 -0
  62. package/dist/proxmox/pbs-datastore-wire.d.ts +21 -0
  63. package/dist/proxmox/pbs-datastore-wire.d.ts.map +1 -0
  64. package/dist/proxmox/pbs-datastore.d.ts +5 -6
  65. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  66. package/dist/proxmox/pbs-notification-input.d.ts +18 -0
  67. package/dist/proxmox/pbs-notification-input.d.ts.map +1 -0
  68. package/dist/proxmox/pbs-notification-matcher-distilled.d.ts +11 -0
  69. package/dist/proxmox/pbs-notification-matcher-distilled.d.ts.map +1 -0
  70. package/dist/proxmox/pbs-notification-matcher.d.ts +4 -3
  71. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
  72. package/dist/proxmox/pbs-notification-target-distilled.d.ts +20 -0
  73. package/dist/proxmox/pbs-notification-target-distilled.d.ts.map +1 -0
  74. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +4 -4
  75. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
  76. package/dist/proxmox/pbs-notification-target.d.ts +1 -1
  77. package/dist/proxmox/pbs-prune-job-lifecycle.d.ts +22 -0
  78. package/dist/proxmox/pbs-prune-job-lifecycle.d.ts.map +1 -0
  79. package/dist/proxmox/pbs-prune-job-spec.d.ts +5 -0
  80. package/dist/proxmox/pbs-prune-job-spec.d.ts.map +1 -0
  81. package/dist/proxmox/pbs-prune-job-wire.d.ts +11 -0
  82. package/dist/proxmox/pbs-prune-job-wire.d.ts.map +1 -0
  83. package/dist/proxmox/pbs-prune-job.d.ts +9 -8
  84. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
  85. package/dist/proxmox/pbs-sync-job-attributes.d.ts +34 -0
  86. package/dist/proxmox/pbs-sync-job-attributes.d.ts.map +1 -0
  87. package/dist/proxmox/pbs-sync-job-form.d.ts +21 -22
  88. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
  89. package/dist/proxmox/pbs-sync-job-lifecycle.d.ts +22 -0
  90. package/dist/proxmox/pbs-sync-job-lifecycle.d.ts.map +1 -0
  91. package/dist/proxmox/pbs-sync-job-wire.d.ts +11 -0
  92. package/dist/proxmox/pbs-sync-job-wire.d.ts.map +1 -0
  93. package/dist/proxmox/pbs-sync-job.d.ts +9 -39
  94. package/dist/proxmox/pbs-sync-job.d.ts.map +1 -1
  95. package/dist/proxmox/pbs-verify-job-lifecycle.d.ts +22 -0
  96. package/dist/proxmox/pbs-verify-job-lifecycle.d.ts.map +1 -0
  97. package/dist/proxmox/pbs-verify-job-spec.d.ts +16 -0
  98. package/dist/proxmox/pbs-verify-job-spec.d.ts.map +1 -0
  99. package/dist/proxmox/pbs-verify-job-wire.d.ts +11 -0
  100. package/dist/proxmox/pbs-verify-job-wire.d.ts.map +1 -0
  101. package/dist/proxmox/pbs-verify-job.d.ts +12 -12
  102. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
  103. package/dist/proxmox/pool.d.ts +10 -1
  104. package/dist/proxmox/pool.d.ts.map +1 -1
  105. package/dist/release/archive.d.ts.map +1 -1
  106. package/dist/release/index.js.map +2 -2
  107. package/docs/pbs-distilled-jobs.md +31 -0
  108. package/docs/pbs-notifications.md +8 -2
  109. package/docs/proxmox-notification-sdk.md +28 -0
  110. package/docs/proxmox-sdk-migration.md +60 -0
  111. package/docs/upstream-conformance-families.md +6 -2
  112. package/package.json +1 -1
  113. package/src/linux/directory-lifecycle.ts +14 -2
  114. package/src/linux/directory.test.ts +24 -0
  115. package/src/linux/directory.ts +5 -3
  116. package/src/litellm/fake-stack.ts +13 -10
  117. package/src/litellm/pass-through-endpoint.ts +5 -6
  118. package/src/litellm/pass-through-stack.test.ts +54 -0
  119. package/src/litellm/providers.ts +8 -2
  120. package/src/proxmox/adopt-noop.test.ts +4 -5
  121. package/src/proxmox/backup-job-config.ts +95 -0
  122. package/src/proxmox/backup-job-distilled.ts +86 -0
  123. package/src/proxmox/backup-job.ts +78 -116
  124. package/src/proxmox/constraints.test.ts +9 -8
  125. package/src/proxmox/distilled-pbs.test.ts +87 -0
  126. package/src/proxmox/distilled-pbs.ts +50 -0
  127. package/src/proxmox/fake-pbs-jobs.ts +99 -0
  128. package/src/proxmox/fake-pbs-notify.ts +4 -1
  129. package/src/proxmox/fake-pve.ts +3 -2
  130. package/src/proxmox/live-core-fixtures.ts +126 -0
  131. package/src/proxmox/live-core-lifecycle.test.ts +115 -0
  132. package/src/proxmox/live-core-wire.test.ts +190 -0
  133. package/src/proxmox/metric-server-config.ts +87 -0
  134. package/src/proxmox/metric-server-distilled.ts +81 -0
  135. package/src/proxmox/metric-server.ts +77 -88
  136. package/src/proxmox/notification-distilled.test.ts +162 -0
  137. package/src/proxmox/notification-matcher-distilled.ts +60 -0
  138. package/src/proxmox/notification-matcher-lifecycle.ts +71 -0
  139. package/src/proxmox/notification-matcher.ts +9 -26
  140. package/src/proxmox/notification-replacement.test.ts +190 -0
  141. package/src/proxmox/notification-sdk-input.ts +39 -0
  142. package/src/proxmox/notification-target-distilled.ts +160 -0
  143. package/src/proxmox/notification-target-form.ts +2 -0
  144. package/src/proxmox/notification-target-lifecycle.ts +75 -0
  145. package/src/proxmox/notification-target-spec.ts +68 -0
  146. package/src/proxmox/notification-target.ts +6 -68
  147. package/src/proxmox/pbs-datastore-endpoint.ts +1 -1
  148. package/src/proxmox/pbs-datastore-form.ts +18 -49
  149. package/src/proxmox/pbs-datastore-guard.ts +7 -4
  150. package/src/proxmox/pbs-datastore-lifecycle.ts +114 -0
  151. package/src/proxmox/pbs-datastore-wire.ts +94 -0
  152. package/src/proxmox/pbs-datastore.ts +7 -133
  153. package/src/proxmox/pbs-jobs-delete.test.ts +40 -0
  154. package/src/proxmox/pbs-jobs-sdk.test.ts +215 -0
  155. package/src/proxmox/pbs-notification-errors.test.ts +93 -0
  156. package/src/proxmox/pbs-notification-input.ts +25 -0
  157. package/src/proxmox/pbs-notification-mail.test.ts +64 -0
  158. package/src/proxmox/pbs-notification-matcher-distilled.ts +65 -0
  159. package/src/proxmox/pbs-notification-matcher.ts +70 -17
  160. package/src/proxmox/pbs-notification-target-distilled.ts +124 -0
  161. package/src/proxmox/pbs-notification-target-lifecycle.ts +26 -17
  162. package/src/proxmox/pbs-notification-target.ts +1 -1
  163. package/src/proxmox/pbs-notifications-stack.test.ts +25 -0
  164. package/src/proxmox/pbs-prune-job-lifecycle.ts +52 -0
  165. package/src/proxmox/pbs-prune-job-spec.ts +53 -0
  166. package/src/proxmox/pbs-prune-job-wire.ts +68 -0
  167. package/src/proxmox/pbs-prune-job.ts +10 -58
  168. package/src/proxmox/pbs-sync-job-attributes.ts +33 -0
  169. package/src/proxmox/pbs-sync-job-form.ts +7 -6
  170. package/src/proxmox/pbs-sync-job-lifecycle.ts +46 -0
  171. package/src/proxmox/pbs-sync-job-wire.ts +71 -0
  172. package/src/proxmox/pbs-sync-job.ts +10 -41
  173. package/src/proxmox/pbs-verify-job-lifecycle.ts +52 -0
  174. package/src/proxmox/pbs-verify-job-spec.ts +76 -0
  175. package/src/proxmox/pbs-verify-job-wire.ts +58 -0
  176. package/src/proxmox/pbs-verify-job.ts +13 -85
  177. package/src/proxmox/pool.ts +92 -10
  178. package/src/release/archive.ts +5 -1
@@ -0,0 +1,76 @@
1
+ /** PBS 4.2.6-1 values and vendor form guards; SDK transport is in pbs-verify-job-wire.ts. */
2
+ import type { PbsVerifyJobAttributes, PbsVerifyJobProps } from './pbs-verify-job.ts';
3
+ import type { PveSpec } from './resource-spec.ts';
4
+ import { rechecks, shape } from './pbs-verify-job-form.ts';
5
+ import { bool, int, text } from './values.ts';
6
+
7
+ /**
8
+ * ⚠️ EVERY FIELD IS READ THROUGH `values.ts` THOUGH PBS RETURNS REAL JSON TYPES. Unlike PVE's
9
+ * SectionConfig round-trip, `GET /config/verify/{id}` serialises a Rust struct, so
10
+ * `ignore-verified` arrives as a JSON boolean and `max-depth` as a JSON number. `bool` and `int`
11
+ * accept both spellings, and using them costs nothing while covering the version that does not.
12
+ * ⚠️ `digest` IS NOT AN ATTRIBUTE AND CANNOT BECOME ONE BY ACCIDENT. PBS puts it on the rpcenv, and
13
+ * the JSON formatter adds rpcenv attributes as SIBLINGS of `data` — `{"data":{…},"digest":"…"}` —
14
+ * so both the old client and the SDK expose only the resource data. Attributes deliberately
15
+ * exclude it: the digest covers verification.cfg as a FILE, so keeping it would churn state
16
+ * whenever an unrelated verify job was edited.
17
+ */
18
+ export const spec: PveSpec<PbsVerifyJobProps, PbsVerifyJobAttributes> = {
19
+ attributes: (live, props) => {
20
+ const ignoreVerified = bool(live['ignore-verified'], true);
21
+ const outdatedAfter = int(live['outdated-after'], -1);
22
+ return {
23
+ comment: text(live['comment'], ''),
24
+ id: props.id,
25
+ 'ignore-verified': ignoreVerified,
26
+ 'max-depth': int(live['max-depth'], -1),
27
+ ns: text(live['ns'], ''),
28
+ 'outdated-after': outdatedAfter,
29
+ rechecks: rechecks(ignoreVerified, outdatedAfter),
30
+ schedule: text(live['schedule'], ''),
31
+ store: text(live['store'], props.store),
32
+ };
33
+ },
34
+ /**
35
+ * 🔴 THIS WAS `config/verification` AND PBS ANSWERS 404 FOR IT. MEASURED on the live 4.2 server:
36
+ * `GET /api2/json/config/verification` -> 404 "Path not found"; `GET /api2/json/config/verify`
37
+ * -> 200 with both jobs. The struct is `VerificationJobConfig` and the CLI subcommand is
38
+ * `verify-job`, so the long spelling reads right and is simply not the route.
39
+ * ⛔ THE OLD FAILURE MODE: `read` folded every failure into `undefined`, so a 404 from a
40
+ * WRONG PATH was indistinguishable from an object that was not
41
+ * there. The first plan against the live estate said `create` for two verification jobs that
42
+ * have existed for weeks — and a create would then have POSTed to a 404 as well, so it fails
43
+ * loudly rather than duplicating anything. Nothing caught it earlier because no verification
44
+ * job had ever been declared; registering the provider exercises no path at all.
45
+ */
46
+ collection: () => 'config/verify',
47
+ /** ⛔ `id` IS SENT. PBS refuses a duplicate id, and that refusal is this family's safety net. */
48
+ createForm: (props) => ({ ...shape(props), id: props.id }),
49
+ /**
50
+ * ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — the `Proxmox.BackupJob` trade, for the
51
+ * same reason and with one extra: this form carries NO `delete` list, so there is nothing it
52
+ * could clear even if it wanted to. See the ⛔ on `withClears` in pbs-verify-job-form.ts.
53
+ * ⚠️ `store` IS COMPARED AND IS MUTABLE, which is unusual and worth reading twice. PBS's update
54
+ * handler assigns it and re-checks privileges on both the old and the new path, so changing
55
+ * `store` MOVES the job — and the datastore it left is then verified by nothing. That is a
56
+ * one-word edit with the same effect as deleting the job, and it plans as a quiet `update`.
57
+ * ⚠️ `rechecks` IS ABSENT HERE ON PURPOSE. It is a rendering of the two fields on the lines
58
+ * above; comparing it too would report the same drift twice.
59
+ */
60
+ /** The vendor rules these forms are checked against at plan time — resource-spec.ts. */
61
+ endpoint: { create: 'pbs:POST /config/verify', update: 'pbs:PUT /config/verify/{id}' },
62
+ matches: (attributes, props) =>
63
+ attributes.store === props.store &&
64
+ // ⚠️ `null` IS COMPARED AGAINST `''`, NOT SKIPPED. A parked job is a declaration like any
65
+ // other, so declaring `null` against a job that HAS a schedule must plan an update — the
66
+ // form then omits `schedule`, which is a set-only form, so see the ⛔ on clearing below.
67
+ attributes.schedule === (props.schedule ?? '') &&
68
+ attributes['ignore-verified'] === (props['ignore-verified'] !== false) &&
69
+ (props['outdated-after'] === undefined ||
70
+ attributes['outdated-after'] === props['outdated-after']) &&
71
+ (props['max-depth'] === undefined || attributes['max-depth'] === props['max-depth']) &&
72
+ (props.ns === undefined || attributes.ns === props.ns) &&
73
+ (props.comment === undefined || attributes.comment === props.comment),
74
+ path: (props) => `config/verify/${props.id}`,
75
+ updateForm: shape,
76
+ };
@@ -0,0 +1,58 @@
1
+ /** PBS 4.2.6-1 operations through @distilled.cloud/proxmox-backup. */
2
+ import * as config from '@distilled.cloud/proxmox-backup/config';
3
+ import * as Effect from 'effect/Effect';
4
+ import { runPbs } from './distilled-pbs.ts';
5
+ import type { PbsVerifyJobProps } from './pbs-verify-job.ts';
6
+ import { shape } from './pbs-verify-job-form.ts';
7
+ import { spec } from './pbs-verify-job-spec.ts';
8
+
9
+ /** SDK member names differ from the vendor form names; omission stays unmanaged. */
10
+ const members = (form: Record<string, string>) => ({
11
+ ...(form['ignore-verified'] === undefined ? {} : { ignore_verified: form['ignore-verified'] }),
12
+ ...(form['store'] === undefined ? {} : { store: form['store'] }),
13
+ ...(form['schedule'] === undefined ? {} : { schedule: form['schedule'] }),
14
+ ...(form['comment'] === undefined ? {} : { comment: form['comment'] }),
15
+ ...(form['max-depth'] === undefined ? {} : { max_depth: form['max-depth'] }),
16
+ ...(form['ns'] === undefined ? {} : { ns: form['ns'] }),
17
+ ...(form['outdated-after'] === undefined ? {} : { outdated_after: form['outdated-after'] }),
18
+ });
19
+
20
+ const createRequest = (props: PbsVerifyJobProps): config.VerifyConfigRequest => ({
21
+ ...members({ ...shape(props), id: props.id }),
22
+ id: props.id,
23
+ store: props.store,
24
+ });
25
+
26
+ const updateRequest = (props: PbsVerifyJobProps): config.PutConfigVerifyRequest => ({
27
+ ...members(shape(props)),
28
+ id: props.id,
29
+ });
30
+
31
+ /** Preserve the state attribute shape while the SDK decodes its typed field names. */
32
+ const wire = (row: config.GetConfigVerifyResponse): Record<string, unknown> => ({
33
+ ...row,
34
+ 'ignore-verified': row.ignore_verified,
35
+ 'max-depth': row.max_depth,
36
+ 'outdated-after': row.outdated_after,
37
+ });
38
+
39
+ /** ⛔ Only the SDK VerifyJobNotFound is absence; permission, transport and malformed replies propagate. */
40
+ export const readOne = (props: PbsVerifyJobProps) =>
41
+ runPbs(props.target, 'read', config.getConfigVerify({ id: props.id })).pipe(
42
+ Effect.map((row) => spec.attributes(wire(row), props)),
43
+ Effect.catchTag('VerifyJobNotFound', () => Effect.succeed(undefined)),
44
+ );
45
+
46
+ export const createOne = (props: PbsVerifyJobProps) =>
47
+ runPbs(props.target, 'provision', config.verifyConfig(createRequest(props)));
48
+
49
+ export const updateOne = (props: PbsVerifyJobProps) =>
50
+ runPbs(props.target, 'provision', config.putConfigVerify(updateRequest(props)));
51
+
52
+ /** Deleting the schedule never deletes backups; already missing is a successful deletion. */
53
+ export const deleteOne = (props: PbsVerifyJobProps) =>
54
+ Effect.gen(function* () {
55
+ // ⛔ Missing-item tags are measured for GET only. Do not guess the DELETE error shape.
56
+ if ((yield* readOne(props)) === undefined) return;
57
+ return yield* runPbs(props.target, 'provision', config.deleteConfigVerify({ id: props.id }));
58
+ });
@@ -9,22 +9,22 @@
9
9
  * green `Proxmox.BackupJob` makes it worse rather than better, because a vzdump job that says
10
10
  * "OK" every night is exactly the evidence people stop at.
11
11
  *
12
- * ⛔ THIS FILE CANNOT AUTHENTICATE UNTIL TWO THINGS OUTSIDE IT CHANGE, AND IT IS SHIPPED SAYING SO
13
- * RATHER THAN SHIPPED LOOKING FINISHED. Both are recorded on `PbsTarget` below:
12
+ * ⚠️ HISTORICAL BOOTSTRAP BLOCKER, 2026-09-13: TWO THINGS OUTSIDE THIS FILE WERE MISSING
13
+ * before PBS could authenticate. Both are now resolved by the PBS lane and SDK:
14
14
  * 1. `credentials.ts` has no PBS mint. MEASURED 2026-09-13 from this Mac: the agent's
15
15
  * approle is refused `sys/mounts`, `kv/infra/proxmox` and `proxmox-pbs/creds/read` alike —
16
16
  * 403 permission denied on all three — so there is no mount to name and none is invented.
17
17
  * 2. `client.ts`'s `authorization()` builds a PVE header, and PBS will not accept it. See the
18
18
  * ⛔ on `PbsTarget`.
19
- * Until both land, every call 401s, `pveOperations.read` folds that into "absent", and the plan
20
- * reports `create` for a job that is plainly there. The deploy then FAILS at the POST rather
19
+ * Before both landed, every call 401'd, `pveOperations.read` folded that into "absent", and the plan
20
+ * reported `create` for a job that was plainly there. The deploy then FAILED at the POST rather
21
21
  * than clobbering anything — an unauthenticated POST 401s, and an authenticated one is refused
22
22
  * because PBS will not take a duplicate section id — so the cost is a LYING PLAN and a broken
23
23
  * run, not a lost job. That is still reason enough not to wire it into a stack yet: a plan
24
24
  * nobody can trust is its own outage.
25
25
  *
26
- * ⚠️ THE SHAPES BELOW ARE READ FROM PUBLISHED SOURCE, NOT MEASURED ON THIS ESTATE. Everything is
27
- * taken from `pbs-api-types/src/jobs.rs`, `src/api2/config/verify.rs`, `src/server/verify_job.rs`
26
+ * ⚠️ THE ORIGINAL SHAPES WERE READ FROM PUBLISHED SOURCE, NOT MEASURED ON THIS ESTATE. Everything is
27
+ * originally taken from `pbs-api-types/src/jobs.rs`, `src/api2/config/verify.rs`, `src/server/verify_job.rs`
28
28
  * and `src/backup/verify.rs` at HEAD of the Proxmox git mirrors on 2026-09-13. What WAS measured
29
29
  * here: `pbs.example.com:8007` and `pbs.mgmt.example.com:8007` both answer HTTP 401 under
30
30
  * STRICT TLS (curl `ssl_verify_result=0`), so no `-k` is needed and `client.ts`'s plain `fetch`
@@ -34,10 +34,9 @@
34
34
  import { Resource } from 'alchemy';
35
35
  import * as Provider from 'alchemy/Provider';
36
36
  import * as Effect from 'effect/Effect';
37
+ import { handlers } from './pbs-verify-job-lifecycle.ts';
37
38
  import type { PbsTarget } from './credentials.ts';
38
- import { rechecks, shape } from './pbs-verify-job-form.ts';
39
- import { type PveRequirements, pveHandlers } from './resource.ts';
40
- import { bool, int, text } from './values.ts';
39
+ import type { PveRequirements } from './resource-spec.ts';
41
40
 
42
41
  export interface PbsVerifyJobProps {
43
42
  /** ⛔ A PBS host, never a PVE cluster. See `PbsTarget`. */
@@ -141,78 +140,7 @@ export interface PbsVerifyJob extends Resource<
141
140
  export const PbsVerifyJob = Resource<PbsVerifyJob>('Pbs.VerifyJob');
142
141
 
143
142
  /**
144
- * ⚠️ EVERY FIELD IS READ THROUGH `values.ts` THOUGH PBS RETURNS REAL JSON TYPES. Unlike PVE's
145
- * SectionConfig round-trip, `GET /config/verify/{id}` serialises a Rust struct, so
146
- * `ignore-verified` arrives as a JSON boolean and `max-depth` as a JSON number. `bool` and `int`
147
- * accept both spellings, and using them costs nothing while covering the version that does not.
148
- * ⚠️ `digest` IS NOT AN ATTRIBUTE AND CANNOT BECOME ONE BY ACCIDENT. PBS puts it on the rpcenv, and
149
- * the JSON formatter adds rpcenv attributes as SIBLINGS of `data` — `{"data":{…},"digest":"…"}` —
150
- * so `client.ts`, which returns `body.data`, never sees it. That is lucky rather than designed:
151
- * the digest covers verification.cfg as a FILE, so keeping it would churn this resource's state
152
- * whenever an unrelated verify job was edited.
153
- */
154
- const handlers = pveHandlers<PbsVerifyJobProps, PbsVerifyJobAttributes>({
155
- attributes: (live, props) => {
156
- const ignoreVerified = bool(live['ignore-verified'], true);
157
- const outdatedAfter = int(live['outdated-after'], -1);
158
- return {
159
- comment: text(live['comment'], ''),
160
- id: props.id,
161
- 'ignore-verified': ignoreVerified,
162
- 'max-depth': int(live['max-depth'], -1),
163
- ns: text(live['ns'], ''),
164
- 'outdated-after': outdatedAfter,
165
- rechecks: rechecks(ignoreVerified, outdatedAfter),
166
- schedule: text(live['schedule'], ''),
167
- store: text(live['store'], props.store),
168
- };
169
- },
170
- /**
171
- * 🔴 THIS WAS `config/verification` AND PBS ANSWERS 404 FOR IT. MEASURED on the live 4.2 server:
172
- * `GET /api2/json/config/verification` -> 404 "Path not found"; `GET /api2/json/config/verify`
173
- * -> 200 with both jobs. The struct is `VerificationJobConfig` and the CLI subcommand is
174
- * `verify-job`, so the long spelling reads right and is simply not the route.
175
- * ⛔ THE FAILURE MODE IS THE ONE THIS PACKAGE KEEPS FINDING: `read` folds every failure into
176
- * `undefined`, so a 404 from a WRONG PATH is indistinguishable from an object that is not
177
- * there. The first plan against the live estate said `create` for two verification jobs that
178
- * have existed for weeks — and a create would then have POSTed to a 404 as well, so it fails
179
- * loudly rather than duplicating anything. Nothing caught it earlier because no verification
180
- * job had ever been declared; registering the provider exercises no path at all.
181
- */
182
- collection: () => 'config/verify',
183
- /** ⛔ `id` IS SENT. PBS refuses a duplicate id, and that refusal is this family's safety net. */
184
- createForm: (props) => ({ ...shape(props), id: props.id }),
185
- /**
186
- * ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — the `Proxmox.BackupJob` trade, for the
187
- * same reason and with one extra: this form carries NO `delete` list, so there is nothing it
188
- * could clear even if it wanted to. See the ⛔ on `withClears` in pbs-verify-job-form.ts.
189
- * ⚠️ `store` IS COMPARED AND IS MUTABLE, which is unusual and worth reading twice. PBS's update
190
- * handler assigns it and re-checks privileges on both the old and the new path, so changing
191
- * `store` MOVES the job — and the datastore it left is then verified by nothing. That is a
192
- * one-word edit with the same effect as deleting the job, and it plans as a quiet `update`.
193
- * ⚠️ `rechecks` IS ABSENT HERE ON PURPOSE. It is a rendering of the two fields on the lines
194
- * above; comparing it too would report the same drift twice.
195
- */
196
- /** The vendor rules these forms are checked against at plan time — resource-spec.ts. */
197
- endpoint: { create: 'pbs:POST /config/verify', update: 'pbs:PUT /config/verify/{id}' },
198
- matches: (attributes, props) =>
199
- attributes.store === props.store &&
200
- // ⚠️ `null` IS COMPARED AGAINST `''`, NOT SKIPPED. A parked job is a declaration like any
201
- // other, so declaring `null` against a job that HAS a schedule must plan an update — the
202
- // form then omits `schedule`, which is a set-only form, so see the ⛔ on clearing below.
203
- attributes.schedule === (props.schedule ?? '') &&
204
- attributes['ignore-verified'] === (props['ignore-verified'] !== false) &&
205
- (props['outdated-after'] === undefined ||
206
- attributes['outdated-after'] === props['outdated-after']) &&
207
- (props['max-depth'] === undefined || attributes['max-depth'] === props['max-depth']) &&
208
- (props.ns === undefined || attributes.ns === props.ns) &&
209
- (props.comment === undefined || attributes.comment === props.comment),
210
- path: (props) => `config/verify/${props.id}`,
211
- updateForm: shape,
212
- });
213
-
214
- /**
215
- * ⛔ `list` IS EMPTY, INHERITED FROM `pveHandlers`, AND THE REASON IS SHARPER HERE THAN ELSEWHERE.
143
+ * ⛔ `list` STAYS EXPLICITLY EMPTY, AND THE REASON IS SHARPER HERE THAN ELSEWHERE.
216
144
  * `GET /config/verify` returns every verification job the caller can see, including the
217
145
  * ones the PBS installer and a human made. Adopting those would put Alchemy one dropped line
218
146
  * away from DELETING a datastore's only verification schedule — and deleting it destroys nothing
@@ -220,10 +148,10 @@ const handlers = pveHandlers<PbsVerifyJobProps, PbsVerifyJobAttributes>({
220
148
  * to say when the rot started. Adoption stays explicit: declare the existing id.
221
149
  *
222
150
  * ⚠️ NO `digest` IS SENT ON UPDATE OR DELETE, SO THIS IS LAST-WRITER-WINS. PBS accepts an optional
223
- * `digest` on both and would refuse a write made against a stale read. Sending one would need a
224
- * read-then-write window this provider does not have — `pveOperations` reads and writes in two
225
- * separate calls with two separate mints — so a concurrent edit from the PBS UI is overwritten
226
- * rather than refused. Same behaviour as every PVE family here; stated because PBS offers better.
151
+ * `digest` on both and would refuse a write made against a stale read. This migration preserves
152
+ * the existing forms, which omit it; the SDK runner reuses leased credentials but does not add
153
+ * an optimistic-lock check. A concurrent edit from the PBS UI may therefore be overwritten
154
+ * rather than refused. PBS offers that stronger guarantee as a separate provider change.
227
155
  */
228
156
  export const PbsVerifyJobProvider = () =>
229
157
  Provider.effect(PbsVerifyJob, Effect.succeed(PbsVerifyJob.Provider.of(handlers)));
@@ -16,14 +16,17 @@
16
16
  * reconcile needs, so widening stays a deliberate act rather than a reaction to a 403.
17
17
  */
18
18
  import { Resource } from 'alchemy';
19
+ import { isResolved } from 'alchemy/Diff';
19
20
  import * as Provider from 'alchemy/Provider';
21
+ import * as pools from '@distilled.cloud/proxmox/pools';
22
+ import { ProxmoxParseError } from '@distilled.cloud/proxmox/Errors';
23
+ import * as Schema from 'effect/Schema';
20
24
  import * as Effect from 'effect/Effect';
21
- import type {
22
- PoolsPoolidGetReturn,
23
- PoolsPoolidPutParams,
24
- PoolsPostParams,
25
- } from './generated/pve.ts';
26
- import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
25
+ import type { PoolsPoolidPutParams, PoolsPostParams } from './generated/pve.ts';
26
+ import type { PveRequirements, PveSpec, WithTarget } from './resource-spec.ts';
27
+ import { runPve } from './distilled-pve.ts';
28
+ import { specGuards } from './resource-guard.ts';
29
+ import { formToSend } from './update-guard.ts';
27
30
 
28
31
  export interface PoolProps extends WithTarget {
29
32
  /** PVE's primary key for a pool. Changing it is a replace, not an update. */
@@ -60,9 +63,9 @@ export const poolUpdateForm = (props: PoolProps): Pick<PoolsPoolidPutParams, 'co
60
63
  comment: props.comment ?? '',
61
64
  });
62
65
 
63
- const handlers = pveHandlers<PoolProps, PoolAttributes>({
66
+ const spec = {
64
67
  attributes: (live, props) => {
65
- const row = live as PoolsPoolidGetReturn;
68
+ const row = live;
66
69
  return {
67
70
  comment: typeof row.comment === 'string' ? row.comment : '',
68
71
  members: Array.isArray(row.members) ? row.members.length : 0,
@@ -76,7 +79,86 @@ const handlers = pveHandlers<PoolProps, PoolAttributes>({
76
79
  matches: (attributes, props) => attributes.comment === (props.comment ?? ''),
77
80
  path: (props) => `pools/${props.poolid}`,
78
81
  updateForm: poolUpdateForm,
79
- });
82
+ } satisfies PveSpec<PoolProps, PoolAttributes>;
83
+
84
+ const { guardCreate, guardUpdate } = specGuards(spec);
85
+
86
+ /** Pool absence is PVE's typed 500, not a catch-all; pve-manager 9.2.11. */
87
+ export const readPool = (props: PoolProps) =>
88
+ runPve(props.target, 'read', false, pools.getPool({ poolid: props.poolid })).pipe(
89
+ Effect.flatMap((live) =>
90
+ Schema.decodeUnknownEffect(Schema.toType(pools.GetPoolResponse))(live).pipe(
91
+ Effect.mapError(
92
+ () =>
93
+ new ProxmoxParseError({
94
+ body: undefined,
95
+ cause: 'Pool response does not match its vendor schema',
96
+ }),
97
+ ),
98
+ Effect.as(live),
99
+ ),
100
+ ),
101
+ Effect.map((live) => spec.attributes({ ...live }, props)),
102
+ Effect.catchTag('PoolNotFound', () => Effect.succeed(undefined)),
103
+ Effect.catchTag('NotFound', () => Effect.succeed(undefined)),
104
+ );
105
+
106
+ export const deletePool = (props: PoolProps) =>
107
+ runPve(props.target, 'provision', true, pools.deletePool({ poolid: props.poolid })).pipe(
108
+ Effect.catchTag('PoolNotFound', () => Effect.void),
109
+ Effect.catchTag('NotFound', () => Effect.void),
110
+ );
80
111
 
81
112
  export const ProxmoxPoolProvider = () =>
82
- Provider.effect(ProxmoxPool, Effect.succeed(ProxmoxPool.Provider.of(handlers)));
113
+ Provider.effect(
114
+ ProxmoxPool,
115
+ Effect.succeed(
116
+ ProxmoxPool.Provider.of({
117
+ /** Pools are adopted explicitly; an index must not silently claim every existing pool. */
118
+ list: () => Effect.succeed([]),
119
+ read: ({ olds }) => readPool(olds),
120
+ diff: Effect.fn(function* ({ news, output }) {
121
+ if (!isResolved(news)) return undefined;
122
+ yield* guardCreate(news, output === undefined);
123
+ yield* guardUpdate(news);
124
+ if (output === undefined) return undefined;
125
+ const live = yield* readPool(news);
126
+ if (live === undefined) {
127
+ yield* guardCreate(news, true);
128
+ return { action: 'update' } as const;
129
+ }
130
+ return { action: spec.matches(live, news) ? 'noop' : 'update' } as const;
131
+ }),
132
+ reconcile: Effect.fn(function* ({ news }) {
133
+ const live = yield* readPool(news);
134
+ yield* guardCreate(news, live === undefined);
135
+ yield* guardUpdate(news);
136
+ if (live === undefined) {
137
+ yield* runPve(news.target, 'provision', true, pools.createPool(poolCreateForm(news)));
138
+ } else {
139
+ const form = formToSend(spec.matches, live, news, poolUpdateForm(news));
140
+ if (form !== undefined)
141
+ yield* runPve(
142
+ news.target,
143
+ 'provision',
144
+ true,
145
+ pools.putPool({
146
+ ...form,
147
+ poolid: news.poolid,
148
+ }),
149
+ );
150
+ }
151
+ const after = yield* readPool(news);
152
+ if (after === undefined)
153
+ return yield* Effect.fail(
154
+ new Error(
155
+ `pools/${news.poolid}: write returned success but the pool is still absent`,
156
+ ),
157
+ );
158
+ return after;
159
+ }),
160
+ /** A nonempty-pool refusal still propagates; there is no force-delete fallback. */
161
+ delete: ({ olds }) => deletePool(olds),
162
+ }),
163
+ ),
164
+ );
@@ -40,7 +40,11 @@ export const extractMembers = async (
40
40
  // a peak RSS of 1116 MB for vmalert alone under bun 1.4.0. `pull` hands over the next slice only
41
41
  // when the stream asks, so backpressure bounds what is inflated ahead of the tar reader.
42
42
  let offset = 0;
43
- const source = new ReadableStream<Uint8Array>({
43
+ // ★ The writable side of DecompressionStream accepts BufferSource (ArrayBuffer or a
44
+ // view). pipeThrough is invariant there, so a Uint8Array source is rejected even
45
+ // though every slice enqueued is one. Measured as TS2345 under TypeScript 7. No
46
+ // zlib package: Bun and Node both ship this stream.
47
+ const source = new ReadableStream<ArrayBufferView | ArrayBuffer>({
44
48
  pull(controller) {
45
49
  if (offset >= gzipped.length) {
46
50
  controller.close();