@homeflare/alchemy 0.38.0 → 0.39.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 (165) hide show
  1. package/README.md +22 -0
  2. package/dist/forgejo/index.js +4 -2
  3. package/dist/forgejo/index.js.map +3 -3
  4. package/dist/forgejo/team-member.d.ts +25 -0
  5. package/dist/forgejo/team-member.d.ts.map +1 -1
  6. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  7. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  8. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  9. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  10. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  11. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  12. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  13. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  14. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  15. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  16. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  17. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  18. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  19. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  20. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  21. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  22. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  23. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  24. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  25. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  26. package/dist/proxmox/index.d.ts +14 -3
  27. package/dist/proxmox/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +565 -133
  29. package/dist/proxmox/index.js.map +33 -23
  30. package/dist/proxmox/qemu-form.d.ts +27 -18
  31. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  32. package/dist/proxmox/qemu-judge.d.ts +30 -0
  33. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  34. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  35. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  36. package/dist/proxmox/qemu-net.d.ts +9 -0
  37. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  38. package/dist/proxmox/qemu-props.d.ts +112 -0
  39. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  40. package/dist/proxmox/qemu-volume.d.ts +17 -0
  41. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  42. package/dist/proxmox/qemu.d.ts +14 -27
  43. package/dist/proxmox/qemu.d.ts.map +1 -1
  44. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  45. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  46. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  47. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  48. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  49. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  50. package/dist/proxmox/storage-download-props.d.ts +36 -0
  51. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  52. package/dist/proxmox/storage-download-task.d.ts +17 -0
  53. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  54. package/dist/proxmox/storage-download.d.ts +44 -0
  55. package/dist/proxmox/storage-download.d.ts.map +1 -0
  56. package/dist/telemetry/fake-collector.d.ts +36 -0
  57. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  58. package/dist/telemetry/index.d.ts +11 -0
  59. package/dist/telemetry/index.d.ts.map +1 -0
  60. package/dist/telemetry/index.js +223 -0
  61. package/dist/telemetry/index.js.map +12 -0
  62. package/dist/telemetry/layer.d.ts +86 -0
  63. package/dist/telemetry/layer.d.ts.map +1 -0
  64. package/dist/telemetry/redact-serialization.d.ts +21 -0
  65. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  66. package/dist/telemetry/redact.d.ts +46 -0
  67. package/dist/telemetry/redact.d.ts.map +1 -0
  68. package/dist/unifi/drift.d.ts +27 -0
  69. package/dist/unifi/drift.d.ts.map +1 -0
  70. package/dist/unifi/fake-unifi.d.ts +5 -1
  71. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  72. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  73. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  74. package/dist/unifi/firewall-zone.d.ts +1 -1
  75. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  76. package/dist/unifi/index.d.ts +4 -3
  77. package/dist/unifi/index.d.ts.map +1 -1
  78. package/dist/unifi/index.js +138 -58
  79. package/dist/unifi/index.js.map +10 -8
  80. package/dist/unifi/network-drift.d.ts +26 -0
  81. package/dist/unifi/network-drift.d.ts.map +1 -0
  82. package/dist/unifi/network-form.d.ts +40 -7
  83. package/dist/unifi/network-form.d.ts.map +1 -1
  84. package/dist/unifi/network.d.ts +1 -0
  85. package/dist/unifi/network.d.ts.map +1 -1
  86. package/dist/unifi/paginate.d.ts +36 -0
  87. package/dist/unifi/paginate.d.ts.map +1 -0
  88. package/dist/unifi/resource.d.ts +36 -1
  89. package/dist/unifi/resource.d.ts.map +1 -1
  90. package/docs/telemetry-spike.md +94 -0
  91. package/docs/telemetry.md +109 -0
  92. package/docs/unifi.md +56 -0
  93. package/package.json +6 -2
  94. package/src/forgejo/team-member.test.ts +14 -0
  95. package/src/forgejo/team-member.ts +28 -1
  96. package/src/proxmox/constraints-required.test.ts +1 -0
  97. package/src/proxmox/generated/constraints/index.ts +3 -1
  98. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  99. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  100. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  101. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  102. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  103. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  104. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  105. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  106. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  107. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  108. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  109. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  110. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  111. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  112. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  113. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  114. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  115. package/src/proxmox/index.test.ts +11 -6
  116. package/src/proxmox/index.ts +19 -3
  117. package/src/proxmox/qemu-form.ts +49 -13
  118. package/src/proxmox/qemu-identity.test.ts +30 -0
  119. package/src/proxmox/qemu-judge.test.ts +65 -0
  120. package/src/proxmox/qemu-judge.ts +84 -0
  121. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  122. package/src/proxmox/qemu-lifecycle.ts +131 -32
  123. package/src/proxmox/qemu-net.test.ts +48 -0
  124. package/src/proxmox/qemu-net.ts +78 -0
  125. package/src/proxmox/qemu-ownership.test.ts +44 -0
  126. package/src/proxmox/qemu-props.test.ts +87 -0
  127. package/src/proxmox/qemu-props.ts +157 -0
  128. package/src/proxmox/qemu-volume.test.ts +80 -0
  129. package/src/proxmox/qemu-volume.ts +121 -0
  130. package/src/proxmox/qemu.ts +15 -29
  131. package/src/proxmox/storage-download-errors.ts +7 -0
  132. package/src/proxmox/storage-download-identity.test.ts +54 -0
  133. package/src/proxmox/storage-download-identity.ts +50 -0
  134. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  135. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  136. package/src/proxmox/storage-download-props.ts +38 -0
  137. package/src/proxmox/storage-download-task.ts +72 -0
  138. package/src/proxmox/storage-download.test.ts +216 -0
  139. package/src/proxmox/storage-download.ts +58 -0
  140. package/src/telemetry/fake-collector.ts +59 -0
  141. package/src/telemetry/http-client-sealing.test.ts +82 -0
  142. package/src/telemetry/index.ts +10 -0
  143. package/src/telemetry/layer.test.ts +204 -0
  144. package/src/telemetry/layer.ts +152 -0
  145. package/src/telemetry/redact-serialization.ts +178 -0
  146. package/src/telemetry/redact.test.ts +124 -0
  147. package/src/telemetry/redact.ts +147 -0
  148. package/src/unifi/drift.test.ts +78 -0
  149. package/src/unifi/drift.ts +56 -0
  150. package/src/unifi/errors-and-secrets.test.ts +127 -0
  151. package/src/unifi/fake-unifi.ts +5 -1
  152. package/src/unifi/firewall-zone-form.ts +31 -2
  153. package/src/unifi/firewall-zone.test.ts +21 -1
  154. package/src/unifi/firewall-zone.ts +1 -1
  155. package/src/unifi/get-only-guard.test.ts +165 -0
  156. package/src/unifi/index.ts +15 -1
  157. package/src/unifi/network-drift.test.ts +207 -0
  158. package/src/unifi/network-drift.ts +80 -0
  159. package/src/unifi/network-form.ts +11 -39
  160. package/src/unifi/network.test.ts +7 -82
  161. package/src/unifi/network.ts +6 -6
  162. package/src/unifi/paginate.test.ts +228 -0
  163. package/src/unifi/paginate.ts +129 -0
  164. package/src/unifi/resource.ts +73 -3
  165. package/src/unifi/write-op-reference.test.ts +236 -0
@@ -1,4 +1,38 @@
1
+ /**
2
+ * `Unifi.Network`'s wire shape: the declarable subset of `NetworkDetails`, and how a live read
3
+ * becomes both plan attributes and a rendered declaration.
4
+ *
5
+ * ⚠️ PROPS MIRROR `UpdateNetworkRequest`, NOT `NetworkDetails`. `default` and `metadata` are
6
+ * server-derived — no writable endpoint in `networks.ts` accepts either — so they are
7
+ * attributes-only, reported but never compared or declared. Everything else in `NetworkProps`
8
+ * is exactly what `updateNetwork`/`createNetwork` would accept, even though this family never
9
+ * calls either: the shape is what a future write path would need, and what today's read-only
10
+ * `matches` compares against.
11
+ *
12
+ * ⛔ `ipv4Configuration` STAYS `unknown`, ON THE SDK'S OWN AUTHORITY. `networks.ts`'s own comment
13
+ * ("Shape varies by variant — widened by scripts/convert.ts; see README") says the OpenAPI
14
+ * discriminator this field varies over was flattened at generation time, not decoded per
15
+ * variant. Comparing it as an opaque JSON value (below) is honest about that; guessing a
16
+ * narrower shape here would silently stop matching the day a new variant's key order differs.
17
+ * ⚠️ It may ALSO hide an unordered array the way `dhcpGuarding` and `ipv6Configuration` do
18
+ * below — this family just can't know, because the shape is never decoded. If a future variant
19
+ * plans spurious `update`s, suspect this field first and widen it into a typed, order-normalized
20
+ * shape rather than adding another opaque comparison.
21
+ *
22
+ * ⛔ THREE FIELDS ARE SETS, NOT SEQUENCES: `dhcpGuarding.trustedDhcpServerIpAddresses`,
23
+ * `ipv6Configuration.additionalHostIpSubnets`, and `ipv6Configuration.dnsServerIpAddressesOverride`.
24
+ * The vendor document gives no indication any of the three is ordered — same reasoning
25
+ * `firewall-zone-form.ts` gives for `networkIds`, and `proxmox/user-wire.ts`'s `groupSet` gives
26
+ * for PVE's `groups` — but `matches` compares via alchemy's `deepEqual`, which sorts object KEYS
27
+ * but NOT ARRAY ELEMENTS (`node_modules/alchemy/lib/Diff.js`). Left alone, a console that
28
+ * returns the same set in a different order between calls plans a spurious `update`, and the
29
+ * read-only reconcile then refuses every deploy. `sortedSet` (dedupe + sort) neutralizes that
30
+ * before any of the three is attributed, declared, or compared. Every OTHER array field in this
31
+ * family (there are none today besides these three) would stay declaration-order as written.
32
+ */
1
33
  import type * as networks from '@distilled.cloud/unifi-network/networks';
34
+ export declare const normalizeDhcpGuarding: (value: networks.NetworkDHCPGuarding | undefined) => networks.NetworkDHCPGuarding | undefined;
35
+ export declare const normalizeIpv6Configuration: (value: networks.NetworkIPv6Configuration | undefined) => networks.NetworkIPv6Configuration | undefined;
2
36
  /**
3
37
  * ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE. `tsconfig.json` sets
4
38
  * `exactOptionalPropertyTypes`, which treats `field?: T` (absent-or-T) and
@@ -47,14 +81,13 @@ export interface NetworkAttributes {
47
81
  }
48
82
  export declare const attributesOf: (live: networks.NetworkDetails, props: NetworkProps) => NetworkAttributes;
49
83
  /**
50
- * ⚠️ `stripNullish: true` ON EVERY COMPARISON. UniFi's JSON answers `null` for an unset optional
51
- * field; a declaration that omits the same prop carries `undefined`. Without this option every
52
- * plan against an object with one unset optional field would report `update` forever — the
53
- * exact "read and write are different shapes" trap `netbox/values.ts` and `proxmox/user-wire.ts`
54
- * both work around, here solved once with the engine's own `deepEqual` instead of a per-field
55
- * coercion, because nothing here needs PVE's or NetBox's extra wire-format translation.
84
+ * MEDIUM-4 (red team, 2026-09-26): `matches` used to be a hand-written boolean expression here,
85
+ * kept in sync BY HAND with `network-drift.ts`'s separately-maintained field list — two comparisons
86
+ * that could (and, per a red-team mutant, silently did) drift apart. `matches` now lives in
87
+ * `network-drift.ts`, defined as `fieldDrift(...).length === 0` — the SAME comparison `driftOf`
88
+ * runs, not a second one. See that file's own header for the `stripNullish`/normalizer reasoning
89
+ * this expression used to carry.
56
90
  */
57
- export declare const matches: (attributes: NetworkAttributes, props: NetworkProps) => boolean;
58
91
  /**
59
92
  * The declaration renderer (task spec: "given one live object, return the props a declaration
60
93
  * needs so its plan is noop"). A later import script feeds `getNetworkDetails`'s own response
@@ -1 +1 @@
1
- {"version":3,"file":"network-form.d.ts","sourceRoot":"","sources":["../../src/unifi/network-form.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAwCzE;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACxD,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,+BAA+B;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAClE,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACvD,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IACjE,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,cAAc,SACtB,YAAY,KAClB,iBAiBD,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,eAAgB,iBAAiB,SAAS,YAAY,KAAG,OA6BL,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SAAU,QAAQ,CAAC,cAAc,UAAU,MAAM,KAAG,YAgB7E,CAAC"}
1
+ {"version":3,"file":"network-form.d.ts","sourceRoot":"","sources":["../../src/unifi/network-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAgBzE,eAAO,MAAM,qBAAqB,UACzB,QAAQ,CAAC,mBAAmB,GAAG,SAAS,KAC9C,QAAQ,CAAC,mBAAmB,GAAG,SAG6D,CAAC;AAOhG,eAAO,MAAM,0BAA0B,UAC9B,QAAQ,CAAC,wBAAwB,GAAG,SAAS,KACnD,QAAQ,CAAC,wBAAwB,GAAG,SAWhC,CAAC;AAER;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACxD,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,+BAA+B;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAClE,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACvD,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IACjE,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,cAAc,SACtB,YAAY,KAClB,iBAiBD,CAAC;AAEH;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SAAU,QAAQ,CAAC,cAAc,UAAU,MAAM,KAAG,YAgB7E,CAAC"}
@@ -26,6 +26,7 @@ import { type NetworkAttributes, type NetworkProps } from './network-form.ts';
26
26
  import type { UnifiRequirements, UnifiSpec } from './resource.ts';
27
27
  export type { NetworkAttributes, NetworkProps } from './network-form.ts';
28
28
  export { declareNetwork } from './network-form.ts';
29
+ export { driftOf } from './network-drift.ts';
29
30
  export interface UnifiNetwork extends Resource<'Unifi.Network', NetworkProps, NetworkAttributes, never, UnifiRequirements> {
30
31
  }
31
32
  export declare const UnifiNetwork: import("alchemy").ResourceClass<UnifiNetwork>;
@@ -1 +1 @@
1
- {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/unifi/network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAGlB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,OAAQ,MAAM,SAAS,YAAY,6FACZ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,YAAY,EACZ,QAAQ,CAAC,cAAc,EACvB,iBAAiB,EACjB,QAAQ,CAAC,sBAAsB,CAUhC,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,oBAAoB,kIACkD,CAAC"}
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/unifi/network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAgB,MAAM,mBAAmB,CAAC;AAE5F,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAInD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,OAAQ,MAAM,SAAS,YAAY,6FACZ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,YAAY,EACZ,QAAQ,CAAC,cAAc,EACvB,iBAAiB,EACjB,QAAQ,CAAC,sBAAsB,CAUhC,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,oBAAoB,kIACkD,CAAC"}
@@ -0,0 +1,36 @@
1
+ import * as Effect from 'effect/Effect';
2
+ /** The one response shape every UniFi Network list operation already has (`protocol.ts`). */
3
+ export interface OffsetPage<T> {
4
+ readonly data: readonly T[];
5
+ readonly offset: number;
6
+ readonly totalCount: number;
7
+ }
8
+ /** The one request shape every UniFi Network list operation already accepts, plus its own fields. */
9
+ export type OffsetPageRequest = {
10
+ readonly offset?: number;
11
+ readonly limit?: number;
12
+ };
13
+ declare const UnifiPaginationInconsistent_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
14
+ readonly _tag: "UnifiPaginationInconsistent";
15
+ } & Readonly<A>;
16
+ export declare class UnifiPaginationInconsistent extends UnifiPaginationInconsistent_base<{
17
+ readonly reason: 'offset-mismatch' | 'total-count-changed' | 'row-count-mismatch' | 'page-ceiling';
18
+ readonly detail: string;
19
+ }> {
20
+ get message(): string;
21
+ }
22
+ /**
23
+ * Walks every page of `fetchPage`, starting at `request.offset ?? 0`, and returns every row in
24
+ * order. `request.limit`, if given, is kept as the page size for every call; the vendor's own
25
+ * default applies otherwise — this loop never invents one (T18: "pass ops only their own fields").
26
+ *
27
+ * Three self-consistency checks run against fields the vendor already sends with every page —
28
+ * detection, not a fix, for the "live console mutates mid-walk" case the header above accepts as
29
+ * unfixable from the client alone: a row added AND removed in the same window can still net out to
30
+ * a `totalCount` and final row count that agree by coincidence. What this closes is the SILENT
31
+ * version of every case actually observed in review: an ignored `offset`, a `totalCount` that
32
+ * drifts between calls, and an undercounted `totalCount` that truncates the walk early.
33
+ */
34
+ export declare const pageAll: <Req extends OffsetPageRequest, T, E, R>(fetchPage: (request: Req) => Effect.Effect<OffsetPage<T>, E, R>, request: Req) => Effect.Effect<T[], E | UnifiPaginationInconsistent, R>;
35
+ export {};
36
+ //# sourceMappingURL=paginate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginate.d.ts","sourceRoot":"","sources":["../../src/unifi/paginate.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,6FAA6F;AAC7F,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,qGAAqG;AACrG,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;;;;AAQtF,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,MAAM,EACX,iBAAiB,GACjB,qBAAqB,GACrB,oBAAoB,GACpB,cAAc,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,GAAG,SAAS,iBAAiB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,aACjD,CAAC,OAAO,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,WACtD,GAAG,KACX,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,2BAA2B,EAAE,CAAC,CA4CpD,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import type { Input } from 'alchemy/Input';
2
2
  import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
3
3
  import * as Effect from 'effect/Effect';
4
- import type * as HttpClient from 'effect/unstable/http/HttpClient';
4
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
5
5
  /** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
6
6
  export type UnifiRequirements = HttpClient.HttpClient;
7
7
  /**
@@ -34,6 +34,41 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
34
34
  reconcile: (news: Props) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
35
35
  destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
36
36
  };
37
+ /**
38
+ * B0a / T12 DEFENSE IN DEPTH. `policy.ts`'s `UnifiWriteRefused` stops write INTENT at
39
+ * `reconcile`/`destroy` — every path this engine exposes already returns that refusal instead of
40
+ * calling an SDK write op. This guard stops the same thing one layer lower, AT THE WIRE: it wraps
41
+ * whatever `HttpClient` the caller already provides (the stack's `FetchHttpClient.layer` in
42
+ * production, `fakeUnifiLayer` in tests — `openbao/bao-http.ts`'s header names this same
43
+ * "wrap, don't replace" seam) so a future resource file that, by mistake, called an SDK write
44
+ * operation directly — bypassing `unifiOperations` entirely — would still never reach the vendor
45
+ * API. `write-op-reference.test.ts` proves no such call exists today; this is what stops one from
46
+ * ever taking effect if one is ever added.
47
+ *
48
+ * ⚠️ `Effect.die`, NOT A TYPED FAILURE — classified per distilled-doctrine's "classify every
49
+ * die/orDie". Every generated SDK operation (`networks.getNetworkDetails`, …) declares its OWN
50
+ * closed error union from the pinned OpenAPI spec, and generated files are never hand-edited to
51
+ * add a member to it. A typed `Effect.fail` here would be a failure mode absent from every op's
52
+ * declared type the moment it occurred — a type-system lie. A defect is the sound way to add a
53
+ * NEW "this must be structurally impossible" failure underneath types this package does not
54
+ * own, exactly like `openbao`'s `refuse()` helpers' `Effect.die(new Error(...))` for a
55
+ * configuration move that should never be reachable.
56
+ */
57
+ export declare class UnifiNonGetRequest extends Error {
58
+ readonly method: string;
59
+ /** PATH ONLY, never the full URL — see `requestPath` below. */
60
+ readonly path: string;
61
+ constructor(method: string,
62
+ /** PATH ONLY, never the full URL — see `requestPath` below. */
63
+ path: string);
64
+ }
65
+ /**
66
+ * A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
67
+ * for everything downstream, with `guardGetOnly`'s wrapped version (`HttpClient.layerMergedContext`
68
+ * — see the guard above). Exported so `get-only-guard.test.ts` can prove the mechanism directly,
69
+ * against a bare `HttpClient` service, without needing `CredentialsFromEnv` or any env var at all.
70
+ */
71
+ export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
37
72
  /**
38
73
  * The four provider handlers for a spec'd read-only UniFi object, wired once.
39
74
  *
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAGnE,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAChF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAChF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;kBAEtB,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;sBAIP,KAAK;oBAYP,KAAK;CAExB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAC9E,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAMzC,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBAtCtB,QAAQ;;yBAElC,MAAM;;IAsCzB,SAAS,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACjC,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAG9D,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAChF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAChF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;kBAEtB,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;sBAIP,KAAK;oBAYP,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM;IACvB,+DAA+D;IACtD,IAAI,EAAE,MAAM,EAStB;CACF;AA0BD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,mFAE7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAC9E,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAMzC,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBA5GtB,QAAQ;;yBAElC,MAAM;;IA4GzB,SAAS,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACjC,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
@@ -0,0 +1,94 @@
1
+ # Spike: what a stack's `providers` Layer captures (2026-09-26)
2
+
3
+ Offline, no live Victoria call, per the workflow's own rule. A throwaway script (deleted after this
4
+ was recorded — not committed; `layer.test.ts` and `http-client-sealing.test.ts` are what's kept) ran
5
+ `verify/fake-engine.ts`'s `engineOver` — the REAL `Alchemy.Stack` + `Plan.make` + `apply` over an
6
+ in-memory state store, alchemy `2.0.0-beta.79` — with:
7
+
8
+ - a fake OTLP collector (`fake-collector.ts`'s `Bun.serve`, capturing every POST's path/headers/body)
9
+ - `telemetryLayer({ endpoints: { traces: '<collector>/v1/traces' }, serviceName: 'spike' })` merged
10
+ into the stack's `providers`
11
+ - one fake resource (`Spike.Thing`) whose `reconcile` wraps an `Effect.withSpan('spike.provider.
12
+ custom-span')` around an `HttpClient` `GET` to a SECOND fake server (`target`, standing in for
13
+ whatever real endpoint a provider calls) at `<target>/probe?token=SECRET123` with an
14
+ `authorization: Bearer sekrit` header
15
+ - a real `FetchHttpClient.layer` merged in as `ambient`, standing in for the platform layer a real
16
+ `alchemy.run.ts`'s `Stack.ts` provides underneath the merged `providers` (admin-transport-
17
+ scope.test.ts's own pattern) — `telemetryLayer`'s own `FetchHttpClient` is sealed, so without this
18
+ the fake provider's `HttpClient.HttpClient` requirement has nothing to resolve to
19
+
20
+ ## Question 1: which spans arrive when the tracer is provided via `providers`?
21
+
22
+ **All of them.** One `deploy()` (a create) produced exactly one POST to `/v1/traces` containing:
23
+
24
+ ```
25
+ state_store.init, plan.diff.resource, plan.make,
26
+ http.client GET, spike.provider.custom-span,
27
+ provider.create, apply.resource, apply
28
+ ```
29
+
30
+ - **The provider's own span** (`spike.provider.custom-span`) arrived.
31
+ - **The `HttpClient` client span** (`http.client GET`, effect's own `HttpClient.make` instrumentation)
32
+ arrived — effect's tracing reads whatever `Tracer.Tracer` is ambient when the request runs, and
33
+ the stack's `providers` context is exactly that.
34
+ - **Alchemy's own engine spans arrived too**: `plan.make` (Plan.ts), `apply`/`apply.resource`
35
+ (Apply.ts's own `Effect.withSpan` calls), and `provider.create` — alchemy's OWN
36
+ `instrumentLifecycle` wrapper (Apply.ts) puts every provider lifecycle call (`read`/`reconcile`/
37
+ `delete`) in a `provider.${op}` span ALREADY, with `alchemy.resource.fqn`/`type`/`op` attributes —
38
+ a provider doesn't need to add its own span just to get lifecycle-level tracing.
39
+ - `state_store.init` and `plan.diff.resource` are alchemy internals not named in the facts this
40
+ workflow started from — recorded here since "measure which spans arrive" means all of them, not
41
+ only the three anticipated.
42
+
43
+ **Why**: `verify/fake-engine.ts`'s `engineOver` builds `compiled.services` from the `providers`
44
+ layer, then runs `Plan.make`/`apply` with `Effect.provide(Layer.succeedContext(compiled.services))`
45
+ around them (`fake-engine.ts`'s `run`). Since `Tracer.Tracer` is an `effect/Tracer` `Context.
46
+ Reference` (a value with a process-wide default that any `Effect.provide` can override for the
47
+ effects it wraps), and `telemetryLayer` sets that reference via `Layer.effect(Tracer.Tracer, …)`
48
+ (the same shape `OtlpTracer.layer` itself uses), every span created inside that `Effect.provide` —
49
+ alchemy's own internals included — reads the OTLP-backed, redacting tracer, not effect's default.
50
+ **This holds specifically because the tracer is installed through the stack's OWN `providers`
51
+ option**, the mechanism `alchemy.run.ts` already exposes to every consumer stack — not through
52
+ alchemy's separate CLI-wide `Telemetry/Layer.ts` install this workflow's decision explicitly does
53
+ not touch.
54
+
55
+ ## Question 2: does redaction hold on a REAL span, not just the unit tests?
56
+
57
+ Yes — the captured `http.client GET` span's `url.full` was `http://127.0.0.1:<port>/probe`: no
58
+ `?token=SECRET123`. With a `redaction: { denylist: [<target's host>, 'secret-site-abc123'] }` policy
59
+ and a target URL of `<target>/secret-site-abc123/probe?token=SECRET`, the captured attributes were:
60
+
61
+ ```
62
+ server.address: "<redacted>"
63
+ url.full: "http://<redacted>/<redacted>/probe"
64
+ url.path: "/<redacted>/probe"
65
+ ```
66
+
67
+ with **no `http.request.header.*` key at all** — the `authorization: Bearer sekrit` header was
68
+ dropped outright, not value-redacted. One correction this made to the implementation: effect's
69
+ `HttpClient.make` sets `server.address` to `url.origin` (`http://host:port`), not a bare hostname —
70
+ `redactAttribute`'s `server.address` branch now parses it with `URL` before comparing to the
71
+ denylist, rather than comparing the raw scheme-prefixed string (which would never match a
72
+ consumer's hostname-only denylist entry).
73
+
74
+ ## Question 3: does the sealed transport actually seal?
75
+
76
+ Confirmed twice, once by accident: the FIRST version of this spike's script had no `ambient`
77
+ `FetchHttpClient` merged in, on the assumption that a leaking `telemetryLayer` would supply one. It
78
+ crashed instead — `error: Service not found: effect/HttpClient` — which is direct, if accidental,
79
+ proof that `telemetryLayer`'s own `FetchHttpClient` never reaches a sibling provider.
80
+ `http-client-sealing.test.ts`'s second test makes that assertion explicit and permanent
81
+ (`.rejects.toThrow()` with no `ambient` layer present) rather than relying on stumbling into it
82
+ again; its first test is the positive case (a sibling gets the real `ambient` client, not
83
+ telemetry's).
84
+
85
+ ## Question 4: does the shutdown flush actually deliver a short CLI run's spans?
86
+
87
+ Yes, with no extra wait beyond `engine.deploy()`'s own returned `Promise` resolving. `OtlpExporter.
88
+ make` (effect's shared batch exporter) registers a `Scope.addFinalizer` that forces one more export
89
+ and awaits it (bounded by `shutdownTimeout`, default 3s) — and `engineOver`'s `run` wraps the whole
90
+ plan+apply effect in `Effect.scoped`, so that finalizer runs, and is awaited, before `deploy()`'s
91
+ promise settles. The spike's default `exportInterval` ('1 second') never even had to fire for the
92
+ one-resource run above; the shutdown flush alone delivered the batch. Confirmed separately: with
93
+ `endpoints: {}`, the fake collector saw zero requests even after waiting 1.5s (longer than the
94
+ default interval) past `deploy()` returning — nothing fires later on some other timer.
@@ -0,0 +1,109 @@
1
+ # `@homeflare/alchemy/telemetry` — OTLP tracing/logging/metrics for a stack
2
+
3
+ `telemetryLayer` builds an OTLP/HTTP `Layer` for a stack's own `providers` — spans, logs and
4
+ metrics from `Effect.withSpan`, `HttpClient` calls and alchemy's own plan/apply engine, exported to
5
+ whatever OTLP collector a consumer names. Read 2026-09-26 against `alchemy@2.0.0-beta.79` and
6
+ `effect@4.0.0-rc.115`'s `effect/unstable/observability` module (this file names no estate host — see
7
+ below). See `telemetry-spike.md` for what was actually measured arriving at a collector.
8
+
9
+ Bodies are sent **protobuf-encoded** (`content-type: application/x-protobuf`), not JSON — measured
10
+ against the pinned binaries in homeflare-mini's own `src/obs`: VictoriaLogs 1.52.0 and
11
+ VictoriaMetrics 1.151.0 both answer OTLP/HTTP JSON with a `400`, and effect's exporter treats a 400
12
+ as non-transient and drops the batch silently at debug-only log level, so a JSON-encoded consumer
13
+ would see traces (VictoriaTraces alone tolerates JSON) but never logs or metrics, with no visible
14
+ error. There is no option to choose JSON instead.
15
+
16
+ ## Off unless you give it endpoints
17
+
18
+ ```ts
19
+ import { telemetryLayer } from '@homeflare/alchemy/telemetry';
20
+
21
+ export const providers = Layer.mergeAll(
22
+ myProviders(),
23
+ telemetryLayer({
24
+ endpoints: {
25
+ traces: 'http://victoria.example.test:10428/insert/opentelemetry/v1/traces',
26
+ logs: 'http://victoria.example.test:9428/insert/opentelemetry/v1/logs',
27
+ metrics: 'http://victoria.example.test:8428/opentelemetry/v1/metrics',
28
+ },
29
+ serviceName: 'my-stack',
30
+ }),
31
+ );
32
+ ```
33
+
34
+ Every entry in `endpoints` is optional and `undefined` by default — an entry you leave out sends
35
+ nothing for that signal, and `telemetryLayer({ endpoints: {} , … })` sends nothing at all
36
+ (`layer.test.ts`'s own "nothing sent when unconfigured"). There is no default collector anywhere in
37
+ this module; a consumer's own site config supplies real hosts. `victoria.example.test` above is an
38
+ RFC 2606 reserved name — put your actual Victoria host there, e.g. from a `bao read` or an
39
+ environment variable your site file already has.
40
+
41
+ The three paths above are VictoriaTraces/Logs/Metrics's own OTLP ingest paths (their upstream docs,
42
+ version-specific — verify against the versions you run, not this file, per this repo's own house
43
+ rule that a documented path is not true because it is written down). They are examples, not
44
+ defaults: `telemetryLayer` never assumes a path shape, since `OtlpTracer`/`OtlpLogger`/`OtlpMetrics`
45
+ each post to the exact `url` given, with nothing appended.
46
+
47
+ ## Options
48
+
49
+ | option | type | notes |
50
+ | -------------------- | ------------------------------ | ------------------------------------------------------------------ |
51
+ | `endpoints` | `{ traces?, logs?, metrics? }` | full URLs, each independent — see above. |
52
+ | `serviceName` | `string` | required — OTLP `service.name`. |
53
+ | `serviceVersion` | `string?` | OTLP `service.version`. |
54
+ | `resourceAttributes` | `Record<string, unknown>?` | extra OTLP resource attributes, e.g. `deployment.environment`. |
55
+ | `redaction` | `{ denylist?: string[] }?` | see below. |
56
+ | `exportInterval` | `Duration.Input?` | default `'1 second'` — short on purpose, see `telemetry-spike.md`. |
57
+ | `logsMinimumLevel` | `LogLevel.LogLevel?` | default `'Info'` — floor for what `logs` exports; see below. |
58
+
59
+ ## Redaction — what leaves this process
60
+
61
+ Two passes run, at two different points, because no single hook sees every field (redteam PR 302,
62
+ 2026-09-26 — `redact.ts`/`redact-serialization.ts` have the full detail):
63
+
64
+ 1. **`redactAttribute`** (`redact.ts`) wraps the `Tracer.Tracer` itself, so it runs on every span
65
+ attribute AS IT IS SET — provider spans, `HttpClient` client spans, and alchemy's own engine
66
+ spans:
67
+ - **Every HTTP header is dropped**, request or response, never merely value-redacted — a header
68
+ NAME can itself be sensitive.
69
+ - **Every query string is dropped** — `url.query` outright, and `url.full`'s own `?…`/`#…` — no
70
+ per-parameter allowlist, since the next secret param a provider adds would otherwise leak.
71
+ - **A denylisted host or path segment is blanked to `<redacted>`**, exact-segment match (never a
72
+ substring), in `url.full`, `url.path` and `server.address` (bare hostname or full origin, both).
73
+ 2. **`scrubText`/`redactTraceData`/`redactLogsData`** (`redact-serialization.ts`) run at
74
+ `OtlpSerialization`, right before the wire body is built — the one place that also sees fields
75
+ `redactAttribute` never does: `status.message` and the `exception.*` event attributes
76
+ `OtlpTracer` builds from a failed span's `Cause` at export time, and a log line inside a span
77
+ (turned into a span event by effect's default `tracerLogger` — the log message becomes the event
78
+ NAME). These are free text, not a structured URL, so this pass matches a denylist entry as a
79
+ **substring anywhere** in the string, and strips the query from any embedded URL. **Logs get the
80
+ same pass** (`redactLogsData`) — a log record's body and its `log.error` (`Cause.pretty`, full
81
+ formatted error text) attribute.
82
+
83
+ ```ts
84
+ telemetryLayer({
85
+ endpoints: { traces: 'http://victoria.example.test:10428/insert/opentelemetry/v1/traces' },
86
+ redaction: { denylist: ['a1b2c3d4e5f6'] }, // e.g. a UniFi console id in the request path
87
+ serviceName: 'my-stack',
88
+ });
89
+ ```
90
+
91
+ Everything else — span names once scrubbed, timing, alchemy's own `alchemy.resource.*`/
92
+ `alchemy.stack` attributes, non-URL attributes a provider sets itself — passes through untouched. A
93
+ provider that puts a secret in a NON-url, non-header, non-denylisted attribute (e.g.
94
+ `Effect.withSpan('x', { attributes: { 'my.token': secret } })`) is not caught by either pass; that is
95
+ a provider bug this module cannot see, not a gap in the redaction rules above.
96
+
97
+ `logsMinimumLevel` (default `'Info'`) is a separate concern from redaction: the `alchemy` CLI sets
98
+ the GLOBAL minimum log level to Debug, and without a floor of its own the OTLP logger would ship
99
+ every Debug-level record. It applies only to what this layer exports — a consumer's console/file
100
+ loggers are unaffected. Metrics carry numeric data points, not provider-supplied free text, and pass
101
+ through both passes untouched.
102
+
103
+ ## What this is not
104
+
105
+ This is **not** alchemy's own CLI telemetry (`alchemy`'s `src/Telemetry/Layer.ts`, hard-coded to
106
+ `otel.alchemy.run`, opted out of estate-wide via `~/.alchemy/telemetry-disabled`). That stays
107
+ retired for this decision (2026-09-26): this module is a per-stack, explicit-endpoint layer
108
+ each consumer opts into, not a CLI-wide install, and it never calls alchemy's endpoint or reads its
109
+ opt-out file.
package/docs/unifi.md CHANGED
@@ -74,6 +74,56 @@ need a write); an exact match returns the live attributes and calls nothing.
74
74
  `GET /v1/sites/{siteId}/networks` answers every network on the site. Adoption stays explicit —
75
75
  the same reasoning `Proxmox.User`'s and NetBox's own `list` give.
76
76
 
77
+ ## Defense in depth: GET-only at the wire (2026-09-26)
78
+
79
+ `policy.ts`'s `UnifiWriteRefused` stops write INTENT at `reconcile`/`delete` — this family calls
80
+ no SDK write op anywhere today. `GetOnlyHttpClient` (`resource.ts`, barrel-exported so
81
+ `homeflare-network`'s own import-layer guard can reuse it instead of re-implementing the same
82
+ wrap), installed in `unifiHandlers`'s `withCredentials`, stops the same thing one layer lower, AT
83
+ THE WIRE: it wraps whatever `HttpClient` the caller provides so ANY non-`GET` method dies with
84
+ `UnifiNonGetRequest` (its message carries the request PATH only, never the host — a cloud
85
+ connector's base URL embeds the account's Console ID) before the request reaches the transport —
86
+ a backstop for a future resource file that, by mistake, called an SDK write operation directly.
87
+ `write-op-reference.test.ts` is the matching static check: it harvests the SDK's real write-op
88
+ export names straight from its own service modules (every name starting
89
+ `create`/`update`/`delete`/`patch`/`execute`/`remove`/`adopt`) and fails any file that mentions
90
+ the SDK's package specifier and contains one of those names as a token anywhere — an identifier, a
91
+ bracket key, a destructured binding, a re-export or a dynamic-import property access all read the
92
+ same way, so one scan catches every syntax form without parsing which one it is.
93
+ `get-only-guard.test.ts` proves the wire guard's mechanism directly against a fake `HttpClient`,
94
+ AND that `unifiHandlers` actually installs it (a handler-level probe, not just the mechanism in
95
+ isolation — `network.test.ts`/`firewall-zone.test.ts` call the lower-level `unifiOperations(spec)`
96
+ directly and never exercise `withCredentials` at all).
97
+
98
+ ## Pagination helper
99
+
100
+ `paginate.ts`'s `pageAll` is a CONSUMER-SIDE sequential offset pager for the SDK's list
101
+ operations (`getNetworksOverviewPage`, `getFirewallZones`, …), none of which paginate on their
102
+ own — no `smithy.api#paginated` trait exists in the pinned spec, and neither the SDK nor
103
+ `@distilled.cloud/core` gained one for this. It stops on the response's `totalCount`, never on
104
+ `data.length < limit` (a page can legitimately come back short of the requested limit without
105
+ being the last page). No family here has enough rows to need it yet; it exists ahead of
106
+ `Unifi.FirewallPolicy` (hundreds of rows on a real console), which does.
107
+
108
+ It also fails with a typed `UnifiPaginationInconsistent` — never silently returns a wrong row
109
+ set — the moment a paged response contradicts itself: a page's echoed `offset` doesn't match what
110
+ was requested, `totalCount` changes between calls, the final row count disagrees with
111
+ `totalCount - startOffset`, or the walk exceeds a hard page ceiling without ever converging. Each
112
+ is a vendor-data condition, not a defect (distilled-doctrine): the caller may legitimately want to
113
+ retry or report on one, so it is an `Effect.fail`, not an `Effect.die`.
114
+
115
+ ## Field-level drift
116
+
117
+ `network.ts`/`firewall-zone.ts` each export a pure `driftOf(live, props)` (`network-drift.ts`,
118
+ `firewall-zone-form.ts`). Both `driftOf` and `matches` are now built on the SAME `fieldDrift` list
119
+ (`drift.ts`'s `makeDriftOf` framework) — `matches` is exactly `fieldDrift(...).length === 0`, not
120
+ a second hand-written comparison kept in sync by hand, so the two cannot quietly disagree (a
121
+ red-team mutant that deleted a `fieldDrift` entry outright, 2026-09-26, is what found the previous
122
+ version's gap). Where `matches` collapses a comparison to one boolean, `driftOf` returns one entry
123
+ per field that actually differs, each carrying its own live and declared value — the shape a
124
+ future pre-import drift check (`homeflare-network`) needs to show WHICH fields changed before
125
+ overwriting a committed declaration.
126
+
77
127
  ## The declaration renderer
78
128
 
79
129
  `declareNetwork(live, siteId)` and `declareFirewallZone(live, siteId)` are pure functions: given
@@ -136,3 +186,9 @@ delete it cannot perform is moot. No `Effect.orDie`/`Effect.die` anywhere in the
136
186
  attributed, declared or compared. Both declaration renderers and both `matches` use
137
187
  `value == null` / `deepEqual(..., { stripNullish: true })` consistently, per `network-form.ts`'s
138
188
  own header. No departure found — nothing in this family needed changing.
189
+
190
+ ⚠️ **Correction (2026-09-26):** the line above is no longer true as written. `resource.ts` now
191
+ has exactly one `Effect.die` — `GetOnlyHttpClient`'s guard (see "Defense in depth" above) —
192
+ classified in its own comment per distilled-doctrine's "classify every die/orDie": it raises a
193
+ NEW defect for a failure mode outside every generated SDK operation's declared error union, never
194
+ converts an existing typed failure the way `orDie` would. Still no `Effect.orDie` anywhere.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -100,6 +100,10 @@
100
100
  "types": "./dist/verify/index.d.ts",
101
101
  "default": "./dist/verify/index.js"
102
102
  },
103
+ "./telemetry": {
104
+ "types": "./dist/telemetry/index.d.ts",
105
+ "default": "./dist/telemetry/index.js"
106
+ },
103
107
  "./discord": {
104
108
  "types": "./dist/discord/index.d.ts",
105
109
  "default": "./dist/discord/index.js"
@@ -127,7 +131,7 @@
127
131
  "@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
128
132
  "@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.3",
129
133
  "@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
130
- "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
134
+ "@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.3.0"
131
135
  },
132
136
  "peerDependencies": {
133
137
  "@distilled.cloud/argocd": "1.0.0-rc.12",
@@ -4,6 +4,7 @@
4
4
  * alongside the SDK's own `NotFound`. Proves the two stay distinct: only the SDK's tag folds.
5
5
  */
6
6
  import { describe, expect, test } from 'bun:test';
7
+ import { readFileSync } from 'node:fs';
7
8
  import * as organization from '@distilled.cloud/forgejo/organization';
8
9
  import * as Effect from 'effect/Effect';
9
10
  import { fakeFailure, fakeForgejo, fakeForgejoLayer } from './fake-forgejo.ts';
@@ -95,3 +96,16 @@ describe('Forgejo.TeamMember spec — the actual exported production functions',
95
96
  expect(await readThrough(fake.fetch)).toBeUndefined();
96
97
  });
97
98
  });
99
+
100
+ test('ForgejoTeamMember declares defaultRemovalPolicy: retain on the Resource() call itself', () => {
101
+ // ⛔ Anchored on the `Resource<ForgejoTeamMember>(...)` CALL, not on the substring appearing
102
+ // anywhere in the file — team-member.ts's own header comment also says the phrase in prose, so
103
+ // a plain `source.toContain(...)` (database-refusals.test.ts's simpler version, safe there
104
+ // because database.ts's header never repeats the phrase) would pass here even if the option
105
+ // were dropped from the actual call. mini PR 82's red team (970e8be) had to pipe
106
+ // `RemovalPolicy.retain()` onto every call site because the kit resource had no default; this
107
+ // proves the kit default carries the fix so a caller no longer has to remember its own.
108
+ const source = readFileSync(new URL('./team-member.ts', import.meta.url), 'utf8');
109
+ const call = source.match(/Resource<ForgejoTeamMember>\([\s\S]*?\);/);
110
+ expect(call?.[0]).toContain("defaultRemovalPolicy: 'retain'");
111
+ });
@@ -22,6 +22,31 @@
22
22
  * cannot create a team.
23
23
  *
24
24
  * ⛔ TOKEN NEEDS `write:organization` TO ADD OR REMOVE, and an identity allowed to manage the team.
25
+ *
26
+ * ⛔ `defaultRemovalPolicy: 'retain'` — FOUND 2026-09-25 by reading the source, not by measuring
27
+ * anything (mini PR 82's red team, `homeflare-mini` 970e8be): before this, `ForgejoRepository`/
28
+ * `ForgejoOrgLabel` defaulted to retain in the kit itself but this family did not, so alchemy's
29
+ * own engine fallback (`destroy`) applied to it — every caller had to remember its own
30
+ * `.pipe(RemovalPolicy.retain())`, the way mini's `src/forgejo/declare.ts` did for
31
+ * `forgejo-provision`'s row. Dropping a `TeamMember` declaration — toggling a feature gate off,
32
+ * renaming the resource id — must not silently call `organization.orgRemoveTeamMember` and
33
+ * revoke a real membership; that is the same "removal from the stack is not removal from
34
+ * Forgejo" posture `ForgejoOrgLabel` and `ForgejoBranchProtection` already take, this family
35
+ * exists to attach a stack's identity to a team it never creates or destroys (`teamIdOf` below
36
+ * only ever locates the team, never manages it), and the same trade-off alchemy's own
37
+ * `GitHub.Collaborator` and `GitHub.TeamAccess` make for the identical reason — "preventing
38
+ * accidental lockout" (`GitHub/Collaborator.ts:127`, `GitHub/TeamAccess.ts:145` in the pinned
39
+ * alchemy source). The trade-off, stated plainly: removing a declaration no longer revokes
40
+ * access on its own — the plan shows that row as `orphaned`, not `delete`. Opt into a real
41
+ * revoke with `.pipe(RemovalPolicy.destroy())`; `destroy` below is fully implemented.
42
+ *
43
+ * ⚠️ THE DEFAULT ONLY PROTECTS A ROW ALREADY IN STATE ONCE THIS RELEASE IS DEPLOYED. Alchemy's
44
+ * plan reads `removalPolicy` off the *persisted row* (`Plan.ts`'s delete-node builder), not off
45
+ * this default; the default only reaches an existing row's state on a deploy where the resource
46
+ * plans as a no-op (`Apply.ts`'s noop path rewrites it and logs "removal policy destroy →
47
+ * retain"). A consumer that bumps to this release and drops a `TeamMember` declaration in the
48
+ * SAME deploy still gets `delete`, not `orphaned` — see the changeset for the two-deploy shape
49
+ * this implies for both adopting retain and opting back into destroy.
25
50
  */
26
51
  import { Resource } from 'alchemy';
27
52
  import * as Provider from 'alchemy/Provider';
@@ -52,7 +77,9 @@ export interface ForgejoTeamMember extends Resource<
52
77
  ForgejoRequirements
53
78
  > {}
54
79
 
55
- export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember');
80
+ export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember', {
81
+ defaultRemovalPolicy: 'retain',
82
+ });
56
83
 
57
84
  export class ForgejoTeamNotFoundError extends Data.TaggedError('ForgejoTeamNotFoundError')<{
58
85
  readonly message: string;
@@ -80,6 +80,7 @@ describe('every create this package makes, and what the vendor requires of it',
80
80
  'pve:POST /nodes/{node}/lxc': ['ostemplate', 'vmid'],
81
81
  'pve:POST /nodes/{node}/network': ['iface', 'type'],
82
82
  'pve:POST /nodes/{node}/qemu': ['vmid'],
83
+ 'pve:POST /nodes/{node}/storage/{storage}/download-url': ['content', 'filename', 'url'],
83
84
  'pve:POST /pools': ['poolid'],
84
85
  'pve:POST /storage': ['storage', 'type'],
85
86
  });
@@ -22,6 +22,7 @@ import { PVE_NODES_DISKS_CONSTRAINTS } from './pve-nodes-disks.ts';
22
22
  import { PVE_NODES_LXC_CONSTRAINTS } from './pve-nodes-lxc.ts';
23
23
  import { PVE_NODES_NETWORK_CONSTRAINTS } from './pve-nodes-network.ts';
24
24
  import { PVE_NODES_QEMU_CONSTRAINTS } from './pve-nodes-qemu.ts';
25
+ import { PVE_NODES_STORAGE_CONSTRAINTS } from './pve-nodes-storage.ts';
25
26
  import { PVE_POOLS_CONSTRAINTS } from './pve-pools.ts';
26
27
  import { PVE_STORAGE_CONSTRAINTS } from './pve-storage.ts';
27
28
  import { PBS_CONFIG_CONSTRAINTS } from './pbs-config.ts';
@@ -41,6 +42,7 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
41
42
  ...PVE_NODES_LXC_CONSTRAINTS,
42
43
  ...PVE_NODES_NETWORK_CONSTRAINTS,
43
44
  ...PVE_NODES_QEMU_CONSTRAINTS,
45
+ ...PVE_NODES_STORAGE_CONSTRAINTS,
44
46
  ...PVE_POOLS_CONSTRAINTS,
45
47
  ...PVE_STORAGE_CONSTRAINTS,
46
48
  ...PBS_CONFIG_CONSTRAINTS,
@@ -52,4 +54,4 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
52
54
  * ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while
53
55
  * changing a 128 to a 129 by hand is. `tests/schema-manifest.test.ts` recomputes it.
54
56
  */
55
- export const PROXMOX_CONSTRAINTS_DIGEST = '56eba80750ddb1d2';
57
+ export const PROXMOX_CONSTRAINTS_DIGEST = '8b6745a471839974';
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  * sha256 9def8f13611184ee, read on a PVE cluster node from
7
7
  * /usr/share/pve-docs/api-viewer/apidoc.js
8
8
  *
9
- * 58 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
9
+ * 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
10
10
  * this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
11
11
  * and therefore unchecked at plan time.
12
12
  *