@homeflare/alchemy 0.38.0 → 0.40.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 (353) hide show
  1. package/README.md +32 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/forgejo/index.js +4 -2
  27. package/dist/forgejo/index.js.map +3 -3
  28. package/dist/forgejo/team-member.d.ts +25 -0
  29. package/dist/forgejo/team-member.d.ts.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  33. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  34. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  35. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  36. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  37. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  38. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  39. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  40. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  41. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  42. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  43. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  44. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  45. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  46. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  47. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  48. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
  49. package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
  50. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  51. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  52. package/dist/proxmox/index.d.ts +14 -3
  53. package/dist/proxmox/index.d.ts.map +1 -1
  54. package/dist/proxmox/index.js +574 -133
  55. package/dist/proxmox/index.js.map +33 -23
  56. package/dist/proxmox/qemu-form.d.ts +27 -18
  57. package/dist/proxmox/qemu-form.d.ts.map +1 -1
  58. package/dist/proxmox/qemu-judge.d.ts +30 -0
  59. package/dist/proxmox/qemu-judge.d.ts.map +1 -0
  60. package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
  61. package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
  62. package/dist/proxmox/qemu-net.d.ts +9 -0
  63. package/dist/proxmox/qemu-net.d.ts.map +1 -0
  64. package/dist/proxmox/qemu-props.d.ts +112 -0
  65. package/dist/proxmox/qemu-props.d.ts.map +1 -0
  66. package/dist/proxmox/qemu-volume.d.ts +27 -0
  67. package/dist/proxmox/qemu-volume.d.ts.map +1 -0
  68. package/dist/proxmox/qemu.d.ts +14 -27
  69. package/dist/proxmox/qemu.d.ts.map +1 -1
  70. package/dist/proxmox/storage-download-errors.d.ts +5 -0
  71. package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
  72. package/dist/proxmox/storage-download-identity.d.ts +27 -0
  73. package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
  74. package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
  75. package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
  76. package/dist/proxmox/storage-download-props.d.ts +36 -0
  77. package/dist/proxmox/storage-download-props.d.ts.map +1 -0
  78. package/dist/proxmox/storage-download-task.d.ts +17 -0
  79. package/dist/proxmox/storage-download-task.d.ts.map +1 -0
  80. package/dist/proxmox/storage-download.d.ts +44 -0
  81. package/dist/proxmox/storage-download.d.ts.map +1 -0
  82. package/dist/talos/credentials-write.d.ts +46 -0
  83. package/dist/talos/credentials-write.d.ts.map +1 -0
  84. package/dist/talos/credentials.d.ts +51 -9
  85. package/dist/talos/credentials.d.ts.map +1 -1
  86. package/dist/talos/fake-process.d.ts +56 -0
  87. package/dist/talos/fake-process.d.ts.map +1 -0
  88. package/dist/talos/index.d.ts +19 -4
  89. package/dist/talos/index.d.ts.map +1 -1
  90. package/dist/talos/index.js +357 -173
  91. package/dist/talos/index.js.map +16 -10
  92. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  93. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  94. package/dist/talos/kubeconfig.d.ts +41 -23
  95. package/dist/talos/kubeconfig.d.ts.map +1 -1
  96. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  97. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  98. package/dist/talos/machine-config-poll.d.ts +43 -0
  99. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  100. package/dist/talos/machine-config-read.d.ts +49 -0
  101. package/dist/talos/machine-config-read.d.ts.map +1 -0
  102. package/dist/talos/resource.d.ts +18 -0
  103. package/dist/talos/resource.d.ts.map +1 -1
  104. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  105. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  106. package/dist/talos/talos-bootstrap.d.ts +87 -0
  107. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  108. package/dist/talos/talos-cluster-health.d.ts +43 -1
  109. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  110. package/dist/talos/talos-errors.d.ts +68 -0
  111. package/dist/talos/talos-errors.d.ts.map +1 -0
  112. package/dist/talos/talos-machine-config.d.ts +51 -15
  113. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  114. package/dist/talos/talosctl.d.ts +7 -0
  115. package/dist/talos/talosctl.d.ts.map +1 -1
  116. package/dist/talos/values.d.ts +23 -3
  117. package/dist/talos/values.d.ts.map +1 -1
  118. package/dist/telemetry/fake-collector.d.ts +36 -0
  119. package/dist/telemetry/fake-collector.d.ts.map +1 -0
  120. package/dist/telemetry/index.d.ts +11 -0
  121. package/dist/telemetry/index.d.ts.map +1 -0
  122. package/dist/telemetry/index.js +223 -0
  123. package/dist/telemetry/index.js.map +12 -0
  124. package/dist/telemetry/layer.d.ts +86 -0
  125. package/dist/telemetry/layer.d.ts.map +1 -0
  126. package/dist/telemetry/redact-serialization.d.ts +21 -0
  127. package/dist/telemetry/redact-serialization.d.ts.map +1 -0
  128. package/dist/telemetry/redact.d.ts +46 -0
  129. package/dist/telemetry/redact.d.ts.map +1 -0
  130. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  131. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  132. package/dist/unifi/acl-rule-form.d.ts +90 -0
  133. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  134. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  135. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  136. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  137. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  138. package/dist/unifi/acl-rule.d.ts +49 -0
  139. package/dist/unifi/acl-rule.d.ts.map +1 -0
  140. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  141. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  142. package/dist/unifi/dns-policy-form.d.ts +97 -0
  143. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  144. package/dist/unifi/dns-policy.d.ts +55 -0
  145. package/dist/unifi/dns-policy.d.ts.map +1 -0
  146. package/dist/unifi/drift.d.ts +27 -0
  147. package/dist/unifi/drift.d.ts.map +1 -0
  148. package/dist/unifi/fake-unifi.d.ts +5 -1
  149. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  150. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  151. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  152. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  153. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  154. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  155. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  156. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  157. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  158. package/dist/unifi/firewall-policy.d.ts +57 -0
  159. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  160. package/dist/unifi/firewall-zone-form.d.ts +6 -0
  161. package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
  162. package/dist/unifi/firewall-zone.d.ts +1 -1
  163. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  164. package/dist/unifi/index.d.ts +11 -3
  165. package/dist/unifi/index.d.ts.map +1 -1
  166. package/dist/unifi/index.js +725 -60
  167. package/dist/unifi/index.js.map +31 -9
  168. package/dist/unifi/network-drift.d.ts +26 -0
  169. package/dist/unifi/network-drift.d.ts.map +1 -0
  170. package/dist/unifi/network-form.d.ts +40 -7
  171. package/dist/unifi/network-form.d.ts.map +1 -1
  172. package/dist/unifi/network.d.ts +1 -0
  173. package/dist/unifi/network.d.ts.map +1 -1
  174. package/dist/unifi/paginate.d.ts +24 -0
  175. package/dist/unifi/paginate.d.ts.map +1 -0
  176. package/dist/unifi/providers.d.ts +1 -1
  177. package/dist/unifi/providers.d.ts.map +1 -1
  178. package/dist/unifi/resource.d.ts +36 -1
  179. package/dist/unifi/resource.d.ts.map +1 -1
  180. package/dist/unifi/scan-source.d.ts +11 -0
  181. package/dist/unifi/scan-source.d.ts.map +1 -0
  182. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  183. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  184. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  185. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  186. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  187. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  188. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  189. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  190. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  191. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  192. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  193. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  194. package/docs/telemetry-spike.md +94 -0
  195. package/docs/telemetry.md +109 -0
  196. package/docs/unifi-acl-rule.md +122 -0
  197. package/docs/unifi-dns-policy.md +75 -0
  198. package/docs/unifi-firewall-policy.md +109 -0
  199. package/docs/unifi-traffic-matching-list.md +82 -0
  200. package/docs/unifi-wifi.md +120 -0
  201. package/docs/unifi.md +64 -3
  202. package/package.json +10 -2
  203. package/src/ceph/auth-entity.ts +74 -0
  204. package/src/ceph/ceph-argv-config.ts +78 -0
  205. package/src/ceph/ceph-argv.test.ts +226 -0
  206. package/src/ceph/ceph-argv.ts +189 -0
  207. package/src/ceph/ceph-auth-form.test.ts +103 -0
  208. package/src/ceph/ceph-auth-form.ts +107 -0
  209. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  210. package/src/ceph/ceph-auth-parse.ts +121 -0
  211. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  212. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  213. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  214. package/src/ceph/ceph-config-options.test.ts +44 -0
  215. package/src/ceph/ceph-config-options.ts +36 -0
  216. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  217. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  218. package/src/ceph/ceph-transport.test.ts +125 -0
  219. package/src/ceph/ceph-transport.ts +117 -0
  220. package/src/ceph/fake-ceph-dial.ts +55 -0
  221. package/src/ceph/index.ts +12 -0
  222. package/src/forgejo/team-member.test.ts +14 -0
  223. package/src/forgejo/team-member.ts +28 -1
  224. package/src/index.ts +1 -0
  225. package/src/proxmox/constraints-required.test.ts +1 -0
  226. package/src/proxmox/generated/constraints/index.ts +3 -1
  227. package/src/proxmox/generated/constraints/pve-access.ts +1 -1
  228. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  229. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  230. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  231. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  232. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  233. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  234. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  235. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  236. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  237. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  238. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  239. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  240. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  241. package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
  242. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  243. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  244. package/src/proxmox/index.test.ts +11 -6
  245. package/src/proxmox/index.ts +19 -3
  246. package/src/proxmox/qemu-form.ts +49 -13
  247. package/src/proxmox/qemu-identity.test.ts +30 -0
  248. package/src/proxmox/qemu-judge.test.ts +65 -0
  249. package/src/proxmox/qemu-judge.ts +84 -0
  250. package/src/proxmox/qemu-lifecycle.test.ts +78 -19
  251. package/src/proxmox/qemu-lifecycle.ts +131 -32
  252. package/src/proxmox/qemu-net.test.ts +48 -0
  253. package/src/proxmox/qemu-net.ts +78 -0
  254. package/src/proxmox/qemu-ownership.test.ts +44 -0
  255. package/src/proxmox/qemu-props.test.ts +87 -0
  256. package/src/proxmox/qemu-props.ts +157 -0
  257. package/src/proxmox/qemu-volume.test.ts +229 -0
  258. package/src/proxmox/qemu-volume.ts +178 -0
  259. package/src/proxmox/qemu.ts +15 -29
  260. package/src/proxmox/storage-download-errors.ts +7 -0
  261. package/src/proxmox/storage-download-identity.test.ts +54 -0
  262. package/src/proxmox/storage-download-identity.ts +50 -0
  263. package/src/proxmox/storage-download-lifecycle.ts +226 -0
  264. package/src/proxmox/storage-download-ownership.test.ts +50 -0
  265. package/src/proxmox/storage-download-props.ts +38 -0
  266. package/src/proxmox/storage-download-task.ts +72 -0
  267. package/src/proxmox/storage-download.test.ts +216 -0
  268. package/src/proxmox/storage-download.ts +58 -0
  269. package/src/talos/credentials-write.test.ts +81 -0
  270. package/src/talos/credentials-write.ts +95 -0
  271. package/src/talos/credentials.test.ts +157 -0
  272. package/src/talos/credentials.ts +160 -90
  273. package/src/talos/fake-process.ts +94 -0
  274. package/src/talos/index.ts +39 -4
  275. package/src/talos/kubeconfig-attrs.ts +55 -0
  276. package/src/talos/kubeconfig.test.ts +256 -0
  277. package/src/talos/kubeconfig.ts +170 -90
  278. package/src/talos/machine-config-fixtures.ts +86 -0
  279. package/src/talos/machine-config-poll.test.ts +57 -0
  280. package/src/talos/machine-config-poll.ts +82 -0
  281. package/src/talos/machine-config-read.ts +118 -0
  282. package/src/talos/resource.ts +19 -0
  283. package/src/talos/secrets-gate.test.ts +63 -0
  284. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  285. package/src/talos/talos-bootstrap.test.ts +218 -0
  286. package/src/talos/talos-bootstrap.ts +151 -45
  287. package/src/talos/talos-cluster-health.test.ts +143 -0
  288. package/src/talos/talos-cluster-health.ts +91 -28
  289. package/src/talos/talos-errors.ts +85 -0
  290. package/src/talos/talos-machine-config.test.ts +189 -0
  291. package/src/talos/talos-machine-config.ts +102 -88
  292. package/src/talos/talosctl.ts +8 -0
  293. package/src/talos/values.test.ts +34 -6
  294. package/src/talos/values.ts +41 -7
  295. package/src/telemetry/fake-collector.ts +59 -0
  296. package/src/telemetry/http-client-sealing.test.ts +82 -0
  297. package/src/telemetry/index.ts +10 -0
  298. package/src/telemetry/layer.test.ts +204 -0
  299. package/src/telemetry/layer.ts +152 -0
  300. package/src/telemetry/redact-serialization.ts +178 -0
  301. package/src/telemetry/redact.test.ts +124 -0
  302. package/src/telemetry/redact.ts +147 -0
  303. package/src/unifi/acl-rule-drift.ts +57 -0
  304. package/src/unifi/acl-rule-form.ts +135 -0
  305. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  306. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  307. package/src/unifi/acl-rule-ordering.ts +70 -0
  308. package/src/unifi/acl-rule.test.ts +183 -0
  309. package/src/unifi/acl-rule.ts +61 -0
  310. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  311. package/src/unifi/dns-policy-drift.ts +48 -0
  312. package/src/unifi/dns-policy-form.ts +142 -0
  313. package/src/unifi/dns-policy.test.ts +154 -0
  314. package/src/unifi/dns-policy.ts +67 -0
  315. package/src/unifi/drift.test.ts +78 -0
  316. package/src/unifi/drift.ts +56 -0
  317. package/src/unifi/errors-and-secrets.test.ts +127 -0
  318. package/src/unifi/fake-unifi.ts +5 -1
  319. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  320. package/src/unifi/firewall-policy-drift.ts +55 -0
  321. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  322. package/src/unifi/firewall-policy-form.ts +146 -0
  323. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  324. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  325. package/src/unifi/firewall-policy-ordering.ts +87 -0
  326. package/src/unifi/firewall-policy.test.ts +183 -0
  327. package/src/unifi/firewall-policy.ts +73 -0
  328. package/src/unifi/firewall-zone-form.ts +31 -2
  329. package/src/unifi/firewall-zone.test.ts +21 -1
  330. package/src/unifi/firewall-zone.ts +1 -1
  331. package/src/unifi/get-only-guard.test.ts +165 -0
  332. package/src/unifi/index.ts +78 -1
  333. package/src/unifi/network-drift.test.ts +207 -0
  334. package/src/unifi/network-drift.ts +80 -0
  335. package/src/unifi/network-form.ts +11 -39
  336. package/src/unifi/network.test.ts +7 -82
  337. package/src/unifi/network.ts +6 -6
  338. package/src/unifi/paginate.test.ts +252 -0
  339. package/src/unifi/paginate.ts +141 -0
  340. package/src/unifi/providers.ts +19 -1
  341. package/src/unifi/resource.ts +73 -3
  342. package/src/unifi/scan-source.ts +64 -0
  343. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  344. package/src/unifi/traffic-matching-list-form.ts +77 -0
  345. package/src/unifi/traffic-matching-list.test.ts +202 -0
  346. package/src/unifi/traffic-matching-list.ts +78 -0
  347. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  348. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  349. package/src/unifi/wifi-broadcast-form.ts +211 -0
  350. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  351. package/src/unifi/wifi-broadcast.test.ts +155 -0
  352. package/src/unifi/wifi-broadcast.ts +90 -0
  353. package/src/unifi/write-op-reference.test.ts +190 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pure-function coverage for `redactAttribute`/`wrapTracer`. layer.test.ts covers the same policy
3
+ * end to end, through a real `HttpClient` call and a real OTLP export — this file is the fast,
4
+ * exhaustive sweep of the redaction rules themselves.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import * as Option from 'effect/Option';
8
+ import type * as Tracer from 'effect/Tracer';
9
+ import { DROP, REDACTED, redactAttribute, wrapTracer } from './redact.ts';
10
+
11
+ describe('redactAttribute', () => {
12
+ test('drops every header attribute outright, request or response, regardless of value', () => {
13
+ expect(redactAttribute('http.request.header.authorization', 'Bearer x', {})).toBe(DROP);
14
+ expect(redactAttribute('http.request.header.x-api-key', 'k', {})).toBe(DROP);
15
+ expect(redactAttribute('http.response.header.set-cookie', 'a=b', {})).toBe(DROP);
16
+ });
17
+
18
+ test('drops url.query outright — no per-parameter allowlist', () => {
19
+ expect(redactAttribute('url.query', 'token=SECRET&safe=1', {})).toBe(DROP);
20
+ });
21
+
22
+ test('strips the query string and hash from url.full even with an empty policy', () => {
23
+ expect(redactAttribute('url.full', 'http://host/path?token=SECRET#frag', {})).toBe(
24
+ 'http://host/path',
25
+ );
26
+ });
27
+
28
+ test('leaves url.full, url.path and server.address untouched when nothing matches the denylist', () => {
29
+ expect(redactAttribute('url.full', 'http://host:9428/insert/opentelemetry/v1/traces', {})).toBe(
30
+ 'http://host:9428/insert/opentelemetry/v1/traces',
31
+ );
32
+ expect(redactAttribute('url.path', '/insert/opentelemetry/v1/traces', {})).toBe(
33
+ '/insert/opentelemetry/v1/traces',
34
+ );
35
+ expect(redactAttribute('server.address', 'http://host:9428', {})).toBe('http://host:9428');
36
+ });
37
+
38
+ test('blanks a denylisted path segment, exact match only — no substring match', () => {
39
+ const policy = { denylist: ['console-abc123'] };
40
+ expect(redactAttribute('url.path', '/proxy/network/console-abc123/site', policy)).toBe(
41
+ `/proxy/network/${REDACTED}/site`,
42
+ );
43
+ // "console-abc123-extra" must NOT match the denylisted "console-abc123" segment.
44
+ expect(redactAttribute('url.path', '/proxy/console-abc123-extra', policy)).toBe(
45
+ '/proxy/console-abc123-extra',
46
+ );
47
+ });
48
+
49
+ test('blanks a denylisted host in both url.full and server.address', () => {
50
+ const policy = { denylist: ['private.example.test'] };
51
+ expect(redactAttribute('server.address', 'https://private.example.test:443', policy)).toBe(
52
+ REDACTED,
53
+ );
54
+ expect(redactAttribute('url.full', 'https://private.example.test/api/site/x?y=1', policy)).toBe(
55
+ `https://${REDACTED}/api/site/x`,
56
+ );
57
+ });
58
+
59
+ test('a non-string value (e.g. server.port) and an unrecognized key pass through untouched', () => {
60
+ expect(redactAttribute('server.port', 8428, { denylist: ['8428'] })).toBe(8428);
61
+ expect(redactAttribute('alchemy.resource.fqn', 'stack/thing', { denylist: ['thing'] })).toBe(
62
+ 'stack/thing',
63
+ );
64
+ });
65
+
66
+ test('a url.full value that fails to parse as a URL is left verbatim, not guessed at', () => {
67
+ expect(redactAttribute('url.full', 'not a url', { denylist: ['not'] })).toBe('not a url');
68
+ });
69
+ });
70
+
71
+ const testSpanOptions = {
72
+ annotations: undefined as never,
73
+ kind: 'internal' as const,
74
+ links: [],
75
+ name: 'test',
76
+ parent: Option.none(),
77
+ root: true,
78
+ sampled: true,
79
+ startTime: 0n,
80
+ };
81
+
82
+ const recordingInner = (): { calls: Array<[string, unknown]>; tracer: Tracer.Tracer } => {
83
+ const calls: Array<[string, unknown]> = [];
84
+ const tracer: Tracer.Tracer = {
85
+ span: (options) => ({
86
+ _tag: 'Span',
87
+ addLinks: () => {},
88
+ annotations: options.annotations,
89
+ attribute: (key, value) => calls.push([key, value]),
90
+ attributes: new Map(),
91
+ end: () => {},
92
+ event: () => {},
93
+ kind: options.kind,
94
+ links: options.links,
95
+ name: options.name,
96
+ parent: options.parent,
97
+ sampled: options.sampled,
98
+ spanId: 'span-1',
99
+ status: { _tag: 'Started', startTime: options.startTime },
100
+ traceId: 'trace-1',
101
+ }),
102
+ };
103
+ return { calls, tracer };
104
+ };
105
+
106
+ describe('wrapTracer', () => {
107
+ test('forwards an allowed attribute and withholds a dropped one from the real span', () => {
108
+ const { calls, tracer } = recordingInner();
109
+ const wrapped = wrapTracer(tracer, {});
110
+ const span = wrapped.span(testSpanOptions);
111
+ span.attribute('http.request.header.cookie', 'secret');
112
+ span.attribute('alchemy.resource.fqn', 'stack/thing');
113
+ expect(calls).toEqual([['alchemy.resource.fqn', 'stack/thing']]);
114
+ });
115
+
116
+ test("passes the span's identity and lifecycle through unchanged", () => {
117
+ const { tracer } = recordingInner();
118
+ const wrapped = wrapTracer(tracer, {});
119
+ const span = wrapped.span(testSpanOptions);
120
+ expect(span.name).toBe('test');
121
+ expect(span.spanId).toBe('span-1');
122
+ expect(span.traceId).toBe('trace-1');
123
+ });
124
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * URL/header redaction for outgoing spans — applied at the `Tracer.Tracer` boundary so it holds for
3
+ * EVERY span source (a provider's own `Effect.withSpan`, effect's `HttpClient` client spans, and
4
+ * alchemy's own plan/apply/resource spans), not just the ones a caller remembers to annotate.
5
+ *
6
+ * ★ WHY THE TRACER, NOT AN HTTPCLIENT MIDDLEWARE. effect's `HttpClient.make` (src/unstable/http/
7
+ * HttpClient.ts) hard-codes `span.attribute("url.full", …)`, `"url.query"` and
8
+ * `http.request.header.<name>` on the span it creates — there is no hook to stop it recording
9
+ * them. The one seam every span (HttpClient's included) passes through is `Tracer.span()`
10
+ * returning a `Tracer.Span`, so `wrapTracer` intercepts `Span.attribute()` there instead.
11
+ * ⛔ NEVER RECORD HEADERS. A header NAME can itself be sensitive (`x-api-key`, a session cookie
12
+ * name), so every `http.{request,response}.header.*` key is DROPPED, not value-redacted.
13
+ * ⚠️ QUERY STRINGS ARE STRIPPED WHOLE, not redacted per-parameter: an allowlist of "safe" params
14
+ * would silently leak the next secret param a provider adds. `url.query` is dropped outright and
15
+ * `url.full`/`url.path` never carry a `?…` past this point.
16
+ */
17
+ import type * as Tracer from 'effect/Tracer';
18
+
19
+ /** Host or path segments to blank wherever they appear in a URL attribute — exact-segment match. */
20
+ export interface RedactionPolicy {
21
+ /**
22
+ * e.g. a UniFi console id embedded in a path (`/proxy/network/api/site/<id>/...`) or a private
23
+ * hostname a consumer never wants leaving its own process. Compared against the full hostname and
24
+ * against each `/`-separated path segment — never a substring match, so `"db"` cannot accidentally
25
+ * blank `"db1.example.test"`.
26
+ */
27
+ readonly denylist?: ReadonlyArray<string>;
28
+ }
29
+
30
+ export const REDACTED = '<redacted>';
31
+
32
+ /** Returned by `redactAttribute` to mean "drop this attribute entirely — do not forward it". */
33
+ export const DROP: unique symbol = Symbol.for('homeflare/alchemy/telemetry/drop-attribute');
34
+
35
+ const HEADER_ATTRIBUTE = /^http\.(?:request|response)\.header\./;
36
+
37
+ const redactSegments = (value: string, denylist: ReadonlySet<string>): string =>
38
+ value
39
+ .split('/')
40
+ .map((segment) => (denylist.has(segment) ? REDACTED : segment))
41
+ .join('/');
42
+
43
+ /**
44
+ * Applies `policy` to one span attribute. Returns `DROP` when the key must never be recorded at
45
+ * all (every header); otherwise returns the value to record, with URLs stripped of their query
46
+ * string and hash and their denylisted host/path segments blanked. A value this function does not
47
+ * recognize (not a URL-shaped key, or not a string) passes through unchanged — alchemy's own
48
+ * engine attributes (`resource.fqn`, `plan.action`, …) are never URLs and are never touched.
49
+ */
50
+ export const redactAttribute = (key: string, value: unknown, policy: RedactionPolicy): unknown => {
51
+ if (HEADER_ATTRIBUTE.test(key)) return DROP;
52
+ if (key === 'url.query') return DROP;
53
+ if (typeof value !== 'string') return value;
54
+ const denylist = new Set(policy.denylist ?? []);
55
+ if (denylist.size === 0 && key !== 'url.full' && key !== 'server.address') return value;
56
+ // ⚠️ MEASURED 2026-09-26 (docs/telemetry-spike.md): effect's own `HttpClient.make` sets
57
+ // `server.address` to `url.origin` (`http://host:port`), NOT a bare hostname — a denylist entry
58
+ // naming just a hostname would never match the raw value, so this parses it out the same way
59
+ // `url.full` does.
60
+ // ⛔ FIX (redteam PR 302 MINOR item 5, 2026-09-26): the OTel semantic convention's own default for
61
+ // `server.address` IS a bare hostname with no scheme (`new URL(value)` throws on that), and other
62
+ // instrumentation could emit exactly that even though effect's `HttpClient` doesn't today —
63
+ // `redactAttribute('server.address', 'db1.example.test', { denylist: ['db1.example.test'] })`
64
+ // returned the value verbatim before this fallback. Compare the raw string directly when it
65
+ // isn't a parseable URL, instead of giving up.
66
+ if (key === 'server.address') {
67
+ try {
68
+ const url = new URL(value);
69
+ return denylist.has(url.hostname) ? REDACTED : value;
70
+ } catch {
71
+ return denylist.has(value) ? REDACTED : value;
72
+ }
73
+ }
74
+ if (key === 'url.path') return redactSegments(value, denylist);
75
+ if (key === 'url.full') {
76
+ try {
77
+ const url = new URL(value);
78
+ const host = denylist.has(url.hostname) ? REDACTED : url.host;
79
+ return `${url.protocol}//${host}${redactSegments(url.pathname, denylist)}`;
80
+ } catch {
81
+ // Not a URL we can parse (a relative path effect couldn't resolve) — leave it verbatim
82
+ // rather than guess; `url.full` is the one HttpClient always sets from a resolved `URL`, so
83
+ // this branch is a defensive fallback, not the expected path.
84
+ return value;
85
+ }
86
+ }
87
+ return value;
88
+ };
89
+
90
+ const redactAttributes = (
91
+ attributes: Record<string, unknown>,
92
+ policy: RedactionPolicy,
93
+ ): Record<string, unknown> => {
94
+ const out: Record<string, unknown> = {};
95
+ for (const [key, value] of Object.entries(attributes)) {
96
+ const redacted = redactAttribute(key, value, policy);
97
+ if (redacted !== DROP) out[key] = redacted;
98
+ }
99
+ return out;
100
+ };
101
+
102
+ /**
103
+ * Wraps a `Tracer.Tracer` so every span it creates redacts its attributes and event attributes
104
+ * through `redactAttribute`/`redactAttributes` before they reach the real span (and therefore
105
+ * before OTLP serialization ever sees them). Everything else — timing, status, links, the span's
106
+ * own identity — passes straight through to `inner`.
107
+ */
108
+ export const wrapTracer = (inner: Tracer.Tracer, policy: RedactionPolicy): Tracer.Tracer => ({
109
+ context: inner.context,
110
+ span(options): Tracer.Span {
111
+ const real = inner.span(options);
112
+ return {
113
+ _tag: 'Span',
114
+ spanId: real.spanId,
115
+ traceId: real.traceId,
116
+ parent: real.parent,
117
+ annotations: real.annotations,
118
+ get name() {
119
+ return real.name;
120
+ },
121
+ get status() {
122
+ return real.status;
123
+ },
124
+ get attributes() {
125
+ return real.attributes;
126
+ },
127
+ get links() {
128
+ return real.links;
129
+ },
130
+ sampled: real.sampled,
131
+ kind: real.kind,
132
+ end: (endTime, exit) => {
133
+ real.end(endTime, exit);
134
+ },
135
+ attribute: (key, value) => {
136
+ const redacted = redactAttribute(key, value, policy);
137
+ if (redacted !== DROP) real.attribute(key, redacted);
138
+ },
139
+ event: (name, startTime, attributes) => {
140
+ real.event(name, startTime, attributes ? redactAttributes(attributes, policy) : attributes);
141
+ },
142
+ addLinks: (links) => {
143
+ real.addLinks(links);
144
+ },
145
+ };
146
+ },
147
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `Unifi.AclRule`'s `matches` AND `driftOf` (B6) — both built on the ONE field list below, the
3
+ * same `fieldDrift`-is-the-only-comparison shape `network-drift.ts`'s header explains (MEDIUM-4,
4
+ * red team, 2026-09-26): `matches(attrs, props) === (driftOf(live, props).length === 0)` is true
5
+ * BY CONSTRUCTION.
6
+ *
7
+ * ⚠️ `stripNullish: true` ON EVERY FIELD BY DEFAULT (`drift.ts`'s `defaultEqual`) — same "UniFi's
8
+ * JSON answers `null`, a declaration omits" tolerance `network-drift.ts` documents.
9
+ * ⛔ `enforcingDeviceFilter`/`protocolFilter` RUN THROUGH `acl-rule-form.ts`'s normalizers on BOTH
10
+ * sides before comparing — never applied to only `live` or only `declared` (that specific gap is
11
+ * what MEDIUM-4's own mutant found in `network-drift.ts`; see its header).
12
+ */
13
+ import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
14
+ import { makeDriftOf } from './drift.ts';
15
+ import {
16
+ type AclRuleAttributes,
17
+ type AclRuleProps,
18
+ attributesOf,
19
+ normalizeDeviceFilter,
20
+ normalizeProtocolFilter,
21
+ } from './acl-rule-form.ts';
22
+
23
+ const fieldDrift = makeDriftOf<AclRuleAttributes, AclRuleProps>([
24
+ { field: 'action', live: (a) => a.action, declared: (p) => p.action },
25
+ { field: 'description', live: (a) => a.description, declared: (p) => p.description },
26
+ {
27
+ field: 'destinationFilter',
28
+ live: (a) => a.destinationFilter,
29
+ declared: (p) => p.destinationFilter,
30
+ },
31
+ { field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
32
+ {
33
+ field: 'enforcingDeviceFilter',
34
+ live: (a) => normalizeDeviceFilter(a.enforcingDeviceFilter),
35
+ declared: (p) => normalizeDeviceFilter(p.enforcingDeviceFilter),
36
+ },
37
+ { field: 'name', live: (a) => a.name, declared: (p) => p.name },
38
+ { field: 'sourceFilter', live: (a) => a.sourceFilter, declared: (p) => p.sourceFilter },
39
+ { field: 'type', live: (a) => a.type, declared: (p) => p.type },
40
+ {
41
+ field: 'protocolFilter',
42
+ live: (a) => normalizeProtocolFilter(a.protocolFilter),
43
+ declared: (p) => normalizeProtocolFilter(p.protocolFilter),
44
+ },
45
+ { field: 'networkIdFilter', live: (a) => a.networkIdFilter, declared: (p) => p.networkIdFilter },
46
+ ]);
47
+
48
+ export const matches = (attributes: AclRuleAttributes, props: AclRuleProps): boolean =>
49
+ fieldDrift(attributes, props).length === 0;
50
+
51
+ /**
52
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
53
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
54
+ * `attributesOf` itself just to call this.
55
+ */
56
+ export const driftOf = (live: aclRules.ACLRule, props: AclRuleProps) =>
57
+ fieldDrift(attributesOf(live, props), props);
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `Unifi.AclRule`'s wire shape — the declarable subset of `ACLRule` (`getAclRule`'s own decode
3
+ * target — `ACLRuleObject` is the structurally-identical page-list item type, unused here), and
4
+ * how a live read
5
+ * becomes both plan attributes and a rendered declaration. Rule ORDER is a separate resource
6
+ * (`acl-rule-ordering.ts`, T5) — this file is one rule's own fields only.
7
+ *
8
+ * ⚠️ `sourceFilter`/`destinationFilter` STAY `unknown`, ON THE SDK'S OWN AUTHORITY.
9
+ * `access_control_acl_rules.ts`'s own comment on both ("Shape varies by variant — widened by
10
+ * scripts/convert.ts; see README") says the OpenAPI discriminator these vary over was flattened
11
+ * at generation time (T10), same as `network-form.ts`'s `ipv4Configuration`. A3 (typing these
12
+ * per-discriminator) is scoped OUT of this PR — see the plan's family inventory — so both stay
13
+ * opaque JSON, compared with plain `deepEqual`, honest about not being decoded.
14
+ *
15
+ * ⚠️ KNOWN GAP UNTIL A3 (T15, red team, Minor finding 4): the vendor's own variant DTOs for these
16
+ * filters carry SET-LIKE arrays too (`networkIds`, `ipAddressesOrSubnets`, `portFilter`,
17
+ * `macAddresses` — `docs/unifi-acl-rule.md`'s own field notes). Because the filter stays opaque,
18
+ * `deepEqual` compares those nested arrays order-sensitively, the same false-`update` risk this
19
+ * file already removes for `deviceIds`/`protocolFilter` by normalizing them — but here there is
20
+ * no decoded field to normalize. Accepted as a known gap, not fixed, until A3 types the filter
21
+ * per-discriminator and can apply `sortedSet` to its own array fields.
22
+ *
23
+ * ⛔ `index` IS ATTRIBUTES-ONLY, NEVER DECLARED. `CreateAclRuleRequest`'s own field comment:
24
+ * "ACL rule index. This property is deprecated and has no effect. Use the dedicated ACL rule
25
+ * reordering endpoint." The live object's current position is `acl-rule-ordering.ts`'s concern
26
+ * (`ACLRuleOrdering.orderedAclRuleIds`), not this resource's declared identity — reporting it
27
+ * here without comparing it avoids a field that can never converge under this family's own
28
+ * `matches` (nothing here could ever change it) from silently pretending to be declarable.
29
+ *
30
+ * ⛔ `enforcingDeviceFilter.deviceIds` AND `protocolFilter` ARE SETS, NOT SEQUENCES — same
31
+ * reasoning `firewall-zone-form.ts` gives for `networkIds`: "IDs of the Switch-capable devices
32
+ * used to enforce the ACL rule" and "Protocols this ACL rule will be applied to" both describe
33
+ * membership, with no indication either list is ordered like `acl-rule-ordering.ts`'s rule-id
34
+ * sequence. Left alone, a console that returns either in a different order between calls would
35
+ * plan a spurious `update` that this read-only family's `reconcile` then refuses forever.
36
+ */
37
+ import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
38
+
39
+ const sortedSet = (values: readonly string[]): string[] => [...new Set(values)].sort();
40
+
41
+ /** ★ EXPORTED so `acl-rule-drift.ts` normalizes both live and declared sides identically — see
42
+ * `network-form.ts`'s own header for why a normalizer applied to only one side is a bug class. */
43
+ export const normalizeDeviceFilter = (
44
+ value: aclRules.ACLRuleDeviceFilter | undefined,
45
+ ): aclRules.ACLRuleDeviceFilter | undefined =>
46
+ value == null
47
+ ? value
48
+ : {
49
+ ...value,
50
+ ...(value.deviceIds !== undefined ? { deviceIds: sortedSet(value.deviceIds) } : {}),
51
+ };
52
+
53
+ export const normalizeProtocolFilter = (
54
+ value: readonly string[] | undefined,
55
+ ): string[] | undefined => (value == null ? undefined : sortedSet(value));
56
+
57
+ /** ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` — `exactOptionalPropertyTypes`; see
58
+ * `network-form.ts`'s own header. */
59
+ export interface AclRuleProps {
60
+ siteId: string;
61
+ aclRuleId: string;
62
+ /** `ALLOW` or `BLOCK` (also accepts a future vendor value the spec did not enumerate). */
63
+ action: string;
64
+ description?: string | undefined;
65
+ /** Traffic destination filter — opaque, see the header. */
66
+ destinationFilter?: unknown;
67
+ enabled: boolean;
68
+ /** Devices enforcing this rule — compared as a SET, see the header. */
69
+ enforcingDeviceFilter?: aclRules.ACLRuleDeviceFilter | undefined;
70
+ name: string;
71
+ /** Traffic source filter — opaque, see the header. */
72
+ sourceFilter?: unknown;
73
+ type: string;
74
+ /** Protocols this rule applies to — compared as a SET, see the header. */
75
+ protocolFilter?: string[] | undefined;
76
+ networkIdFilter?: string | undefined;
77
+ }
78
+
79
+ export interface AclRuleAttributes {
80
+ siteId: string;
81
+ aclRuleId: string;
82
+ action: string;
83
+ description: string | undefined;
84
+ destinationFilter: unknown;
85
+ enabled: boolean;
86
+ enforcingDeviceFilter: aclRules.ACLRuleDeviceFilter | undefined;
87
+ /** ACL rule index (current priority position) — server-derived, never declared; see header. */
88
+ index: number;
89
+ name: string;
90
+ sourceFilter: unknown;
91
+ type: string;
92
+ protocolFilter: string[] | undefined;
93
+ networkIdFilter: string | undefined;
94
+ /** `metadata.origin` — server-derived, never declared; see `firewall-zone-form.ts`'s own. */
95
+ metadataOrigin: string;
96
+ }
97
+
98
+ export const attributesOf = (live: aclRules.ACLRule, props: AclRuleProps): AclRuleAttributes => ({
99
+ siteId: props.siteId,
100
+ aclRuleId: live.id,
101
+ action: live.action,
102
+ description: live.description,
103
+ destinationFilter: live.destinationFilter,
104
+ enabled: live.enabled,
105
+ enforcingDeviceFilter: normalizeDeviceFilter(live.enforcingDeviceFilter),
106
+ index: live.index,
107
+ name: live.name,
108
+ sourceFilter: live.sourceFilter,
109
+ type: live.type,
110
+ protocolFilter: normalizeProtocolFilter(live.protocolFilter),
111
+ networkIdFilter: live.networkIdFilter,
112
+ metadataOrigin: live.metadata.origin,
113
+ });
114
+
115
+ /**
116
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
117
+ * needs so its plan is noop"). A later import script feeds `getAclRule`'s own response straight in
118
+ * and writes the result as `aclRule('<id>', declareAclRule(live, siteId))` — no field is invented
119
+ * or defaulted beyond what `attributesOf` already reports, so `matches(attributesOf(live, props),
120
+ * declareAclRule(live, siteId))` is `true` by construction (`acl-rule.test.ts` proves it).
121
+ */
122
+ export const declareAclRule = (live: aclRules.ACLRule, siteId: string): AclRuleProps => ({
123
+ siteId,
124
+ aclRuleId: live.id,
125
+ action: live.action,
126
+ description: live.description,
127
+ destinationFilter: live.destinationFilter,
128
+ enabled: live.enabled,
129
+ enforcingDeviceFilter: normalizeDeviceFilter(live.enforcingDeviceFilter),
130
+ name: live.name,
131
+ sourceFilter: live.sourceFilter,
132
+ type: live.type,
133
+ protocolFilter: normalizeProtocolFilter(live.protocolFilter),
134
+ networkIdFilter: live.networkIdFilter,
135
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `Unifi.AclRuleOrdering`'s wire shape — ONE resource per site (`getAclRuleOrdering`'s own scope,
3
+ * `GET /v1/sites/{siteId}/acl-rules/ordering`), not per rule. Compare with `firewall-zone-form.ts`'s
4
+ * `networkIds`, which is a genuine SET: this file's `orderedAclRuleIds` is the opposite case.
5
+ *
6
+ * ⛔ T5 — `orderedAclRuleIds` IS A SEQUENCE, NEVER RUN THROUGH `sortedSet`. The endpoint's own name
7
+ * (`updateAclRuleOrdering`) and its field's own vendor comment on `ACLRule.index` ("Lower index
8
+ * has higher priority") both say this list's POSITION is the entire value it carries — rule
9
+ * priority. `firewall-zone-form.ts`'s `sortedSet`/`network-form.ts`'s three set normalizers all
10
+ * exist to stop the OPPOSITE mistake (treating a set as if order mattered); applying the same
11
+ * fix here would silently discard the one thing this resource represents. `drift.ts`'s
12
+ * `defaultEqual` (`deepEqual`) already compares arrays element-by-element without sorting them
13
+ * (its header: "sorts object KEYS but NOT ARRAY ELEMENTS") — so leaving this field un-normalized
14
+ * is what makes the comparison order-sensitive, not an oversight to "fix" later.
15
+ *
16
+ * ★ `makeDriftOf` (`drift.ts`), NO CUSTOM `equal` — same framework every other family's
17
+ * `matches`/`driftOf` share (MEDIUM-4). Its default `deepEqual` is ALREADY order-sensitive for
18
+ * arrays (see the paragraph above), so the absence of a custom `equal` here, unlike
19
+ * `firewall-zone-form.ts`'s `networkIds` entry, is itself the "never sortedSet" rule in force —
20
+ * not an oversight.
21
+ */
22
+ import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
23
+ import { makeDriftOf } from './drift.ts';
24
+
25
+ export interface AclRuleOrderingProps {
26
+ siteId: string;
27
+ /** Rule IDs in priority order, highest priority first — never compared as a set (see header). */
28
+ orderedAclRuleIds: string[];
29
+ }
30
+
31
+ export interface AclRuleOrderingAttributes {
32
+ siteId: string;
33
+ orderedAclRuleIds: string[];
34
+ }
35
+
36
+ export const attributesOf = (
37
+ live: aclRules.ACLRuleOrdering,
38
+ props: AclRuleOrderingProps,
39
+ ): AclRuleOrderingAttributes => ({
40
+ siteId: props.siteId,
41
+ orderedAclRuleIds: live.orderedAclRuleIds,
42
+ });
43
+
44
+ /**
45
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
46
+ * needs so its plan is noop"). A later import script feeds `getAclRuleOrdering`'s own response
47
+ * straight in and writes the result as `aclRuleOrdering('site-1', declareAclRuleOrdering(live,
48
+ * siteId))` — `matches(attributesOf(live, props), declareAclRuleOrdering(live, siteId))` is `true`
49
+ * by construction (`acl-rule-ordering.test.ts` proves it).
50
+ */
51
+ export const declareAclRuleOrdering = (
52
+ live: aclRules.ACLRuleOrdering,
53
+ siteId: string,
54
+ ): AclRuleOrderingProps => ({
55
+ siteId,
56
+ orderedAclRuleIds: live.orderedAclRuleIds,
57
+ });
58
+
59
+ const fieldDrift = makeDriftOf<AclRuleOrderingAttributes, AclRuleOrderingProps>([
60
+ {
61
+ field: 'orderedAclRuleIds',
62
+ live: (a) => a.orderedAclRuleIds,
63
+ declared: (p) => p.orderedAclRuleIds,
64
+ },
65
+ ]);
66
+
67
+ export const matches = (
68
+ attributes: AclRuleOrderingAttributes,
69
+ props: AclRuleOrderingProps,
70
+ ): boolean => fieldDrift(attributes, props).length === 0;
71
+
72
+ /**
73
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
74
+ * (`driftOf(live, props)`). Only one field exists, so this either reports nothing or the whole
75
+ * ordered pair, never a per-position breakdown -- a future consumer wanting "which rule moved"
76
+ * would need to diff the two arrays itself.
77
+ */
78
+ export const driftOf = (live: aclRules.ACLRuleOrdering, props: AclRuleOrderingProps) =>
79
+ fieldDrift(attributesOf(live, props), props);