@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,151 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy`'s `driftOf`/`matches` (`firewall-policy-drift.ts`, B6) — split out of
3
+ * `firewall-policy.test.ts` to keep that file under the house 250-line cap once this coverage grew,
4
+ * same split `network-drift.test.ts` is from `network.test.ts`, not a different family.
5
+ *
6
+ * The last `describe` closes red-team finding I2 (2026-09-26, PR 309 LAND): `firewall-policy-
7
+ * drift.ts`'s field list is untyped strings (`makeDriftOf`'s own contract) — deleting any 7 of its
8
+ * 11 entries left every test in `firewall-policy.test.ts` green. `DECLARABLE_FIELDS` below is a
9
+ * `Record<..., true>` keyed by `Exclude<keyof FirewallPolicyProps, 'siteId' | 'firewallPolicyId'>`
10
+ * — adding or removing a `FirewallPolicyProps` field without updating that record is a TYPE ERROR,
11
+ * so the list this test walks is complete BY CONSTRUCTION. Changing exactly one field at a time and
12
+ * asserting `driftOf` reports exactly that field's name means a field missing from the real
13
+ * `fieldDrift` array (as the red-team mutant produced) shows up here as "reports []" instead of
14
+ * "reports [field]" — the same failure the red team's own by-hand deletion produced, now a standing
15
+ * test instead of a one-off probe.
16
+ *
17
+ * I1 (the filter-bearing-policy round trip) lives in `firewall-policy-filter-roundtrip.test.ts` —
18
+ * a third split, not folded in here, to keep this file under the cap too.
19
+ */
20
+ import { describe, expect, test } from 'bun:test';
21
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
22
+ import { attributesOf } from './firewall-policy-form.ts';
23
+ import { driftOf, matches } from './firewall-policy-drift.ts';
24
+ import type { FirewallPolicyProps } from './firewall-policy.ts';
25
+
26
+ const liveFirewallPolicy = (
27
+ overrides: Partial<firewall.FirewallPolicy> = {},
28
+ ): firewall.FirewallPolicy => ({
29
+ action: { type: 'ALLOW' },
30
+ connectionStateFilter: ['ESTABLISHED', 'RELATED'],
31
+ description: 'Allow IoT devices outbound to the internet',
32
+ destination: { zoneId: 'zone-external' },
33
+ enabled: true,
34
+ id: 'fp-1',
35
+ index: 3,
36
+ ipProtocolScope: { ipVersion: 'IPV4' },
37
+ loggingEnabled: false,
38
+ metadata: { origin: 'USER' },
39
+ name: 'Allow IoT to Internet',
40
+ source: { zoneId: 'zone-iot' },
41
+ ...overrides,
42
+ });
43
+
44
+ const PROPS: FirewallPolicyProps = {
45
+ siteId: 'site-1',
46
+ firewallPolicyId: 'fp-1',
47
+ action: { type: 'ALLOW' },
48
+ connectionStateFilter: ['ESTABLISHED', 'RELATED'],
49
+ description: 'Allow IoT devices outbound to the internet',
50
+ destination: { zoneId: 'zone-external' },
51
+ enabled: true,
52
+ ipProtocolScope: { ipVersion: 'IPV4' },
53
+ loggingEnabled: false,
54
+ name: 'Allow IoT to Internet',
55
+ source: { zoneId: 'zone-iot' },
56
+ };
57
+
58
+ describe('driftOf -- B6 field-level drift, straight from one live read', () => {
59
+ test('a real name change reports its own field, live and declared', () => {
60
+ const live = liveFirewallPolicy({ name: 'Allow IoT outbound' });
61
+ expect(driftOf(live, PROPS)).toEqual([
62
+ { field: 'name', live: 'Allow IoT outbound', declared: 'Allow IoT to Internet' },
63
+ ]);
64
+ });
65
+
66
+ test('connectionStateFilter in a different order is a noop -- it is a SET, not a sequence', () => {
67
+ const live = liveFirewallPolicy({ connectionStateFilter: ['RELATED', 'ESTABLISHED'] });
68
+ expect(matches(attributesOf(live, PROPS), PROPS)).toBe(true);
69
+ expect(driftOf(live, PROPS)).toEqual([]);
70
+ });
71
+
72
+ test('the DECLARED side of connectionStateFilter is also normalized (MEDIUM-4 mutant)', () => {
73
+ const live = liveFirewallPolicy({ connectionStateFilter: ['ESTABLISHED', 'RELATED'] });
74
+ const props: FirewallPolicyProps = {
75
+ ...PROPS,
76
+ connectionStateFilter: ['RELATED', 'ESTABLISHED'],
77
+ };
78
+ expect(matches(attributesOf(live, props), props)).toBe(true);
79
+ expect(driftOf(live, props)).toEqual([]);
80
+ });
81
+
82
+ test('a genuine connectionStateFilter difference is caught', () => {
83
+ const live = liveFirewallPolicy({ connectionStateFilter: ['NEW'] });
84
+ expect(driftOf(live, PROPS)).toEqual([
85
+ { field: 'connectionStateFilter', live: ['NEW'], declared: ['ESTABLISHED', 'RELATED'] },
86
+ ]);
87
+ });
88
+
89
+ test('index is never compared -- it is attributes-only, owned by FirewallPolicyOrdering', () => {
90
+ const live = liveFirewallPolicy({ index: 41 });
91
+ expect(matches(attributesOf(live, PROPS), PROPS)).toBe(true);
92
+ expect(attributesOf(live, PROPS).index).toBe(41);
93
+ });
94
+
95
+ test('source/destination compare wholesale -- a real zone change is still caught', () => {
96
+ const live = liveFirewallPolicy({ destination: { zoneId: 'zone-guest' } });
97
+ expect(driftOf(live, PROPS)).toEqual([
98
+ {
99
+ field: 'destination',
100
+ live: { zoneId: 'zone-guest' },
101
+ declared: { zoneId: 'zone-external' },
102
+ },
103
+ ]);
104
+ });
105
+ });
106
+
107
+ type DeclarableField = Exclude<keyof FirewallPolicyProps, 'siteId' | 'firewallPolicyId'>;
108
+
109
+ const DECLARABLE_FIELDS: Record<DeclarableField, true> = {
110
+ action: true,
111
+ connectionStateFilter: true,
112
+ description: true,
113
+ destination: true,
114
+ enabled: true,
115
+ ipProtocolScope: true,
116
+ ipsecFilter: true,
117
+ loggingEnabled: true,
118
+ name: true,
119
+ schedule: true,
120
+ source: true,
121
+ };
122
+
123
+ /** One changed value per field -- distinct enough from PROPS' own that `deepEqual` calls it drift,
124
+ * and (for `enabled`/`loggingEnabled`) not merely `undefined` vs a value, which `stripNullish`
125
+ * would treat as a noop. */
126
+ const CHANGED_VALUE: { [K in DeclarableField]: FirewallPolicyProps[K] } = {
127
+ action: { type: 'BLOCK' },
128
+ connectionStateFilter: ['NEW'],
129
+ description: 'a different description',
130
+ destination: { zoneId: 'zone-different' },
131
+ enabled: false,
132
+ ipProtocolScope: { ipVersion: 'IPV6' },
133
+ ipsecFilter: 'MATCH_ENCRYPTED',
134
+ loggingEnabled: true,
135
+ name: 'a different name',
136
+ schedule: { mode: 'EVERY_DAY' },
137
+ source: { zoneId: 'zone-different-2' },
138
+ };
139
+
140
+ describe('I2 -- every declarable field is wired into the drift field list', () => {
141
+ // `liveFirewallPolicy()` (no overrides) matches PROPS field-for-field (proved above), so
142
+ // changing exactly one PROPS field per case isolates that field's own wire-up.
143
+ test.each(Object.keys(DECLARABLE_FIELDS) as DeclarableField[])(
144
+ 'changing only %s reports exactly that field',
145
+ (field) => {
146
+ const live = liveFirewallPolicy();
147
+ const changedProps: FirewallPolicyProps = { ...PROPS, [field]: CHANGED_VALUE[field] };
148
+ expect(driftOf(live, changedProps).map((d) => d.field)).toEqual([field]);
149
+ },
150
+ );
151
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy`'s `matches` AND `driftOf` (B6) — both built on the ONE field list below,
3
+ * the 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, never two hand-kept-in-sync comparisons.
6
+ *
7
+ * ⚠️ `stripNullish: true` ON EVERY FIELD BY DEFAULT (`drift.ts`'s `defaultEqual`) — same "UniFi's
8
+ * JSON answers `null`, a declaration omits" tolerance every other family here relies on.
9
+ * ⛔ `connectionStateFilter` RUNS THROUGH `firewall-policy-form.ts`'s normalizer on BOTH sides
10
+ * before comparing — never applied to only `live` or only `declared` (the specific gap MEDIUM-4's
11
+ * own mutant found in `network-drift.ts`; see its header, and `acl-rule-drift.ts`'s own repeat of
12
+ * this same rule for `protocolFilter`/`enforcingDeviceFilter`).
13
+ * ★ `action`/`destination`/`ipProtocolScope`/`schedule`/`source` compare with the plain default
14
+ * `equal` (whole-value `deepEqual`) — see `firewall-policy-form.ts`'s header for the accepted
15
+ * known gap this leaves in their own nested set-like arrays.
16
+ */
17
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
18
+ import { makeDriftOf } from './drift.ts';
19
+ import {
20
+ type FirewallPolicyAttributes,
21
+ type FirewallPolicyProps,
22
+ attributesOf,
23
+ normalizeConnectionStateFilter,
24
+ } from './firewall-policy-form.ts';
25
+
26
+ const fieldDrift = makeDriftOf<FirewallPolicyAttributes, FirewallPolicyProps>([
27
+ { field: 'action', live: (a) => a.action, declared: (p) => p.action },
28
+ {
29
+ field: 'connectionStateFilter',
30
+ live: (a) => normalizeConnectionStateFilter(a.connectionStateFilter),
31
+ declared: (p) => normalizeConnectionStateFilter(p.connectionStateFilter),
32
+ },
33
+ { field: 'description', live: (a) => a.description, declared: (p) => p.description },
34
+ { field: 'destination', live: (a) => a.destination, declared: (p) => p.destination },
35
+ { field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
36
+ { field: 'ipProtocolScope', live: (a) => a.ipProtocolScope, declared: (p) => p.ipProtocolScope },
37
+ { field: 'ipsecFilter', live: (a) => a.ipsecFilter, declared: (p) => p.ipsecFilter },
38
+ { field: 'loggingEnabled', live: (a) => a.loggingEnabled, declared: (p) => p.loggingEnabled },
39
+ { field: 'name', live: (a) => a.name, declared: (p) => p.name },
40
+ { field: 'schedule', live: (a) => a.schedule, declared: (p) => p.schedule },
41
+ { field: 'source', live: (a) => a.source, declared: (p) => p.source },
42
+ ]);
43
+
44
+ export const matches = (
45
+ attributes: FirewallPolicyAttributes,
46
+ props: FirewallPolicyProps,
47
+ ): boolean => fieldDrift(attributes, props).length === 0;
48
+
49
+ /**
50
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
51
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
52
+ * `attributesOf` itself just to call this.
53
+ */
54
+ export const driftOf = (live: firewall.FirewallPolicy, props: FirewallPolicyProps) =>
55
+ fieldDrift(attributesOf(live, props), props);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * I1 (red team, 2026-09-26, PR 309 LAND): every fixture in `firewall-policy.test.ts` and
3
+ * `firewall-policy-drift.test.ts` uses filter-LESS `source`/`destination`/`ipProtocolScope`
4
+ * (`{zoneId}`/`{ipVersion}` only). A mutant that made `attributesOf`/`declareFirewallPolicy` write
5
+ * only those two keys -- silently dropping every `trafficFilter`/`protocolFilter`/`schedule` --
6
+ * passed all of `firewall-policy.test.ts` AND `discriminated-filter-decode.test.ts` (17/17). The
7
+ * latter proves the SDK's OWN decode keeps every key; it never reads them back through THIS
8
+ * resource's `attributesOf`/`declareFirewallPolicy`, which is the actual path a Lane C import walks.
9
+ *
10
+ * This file is that missing link: a filter-bearing wire body, read through `spec.fetchLive`, then
11
+ * `declareFirewallPolicy` -- proving the filters (plus a key this pinned spec has never declared)
12
+ * survive THIS round trip, not just the SDK's own decode.
13
+ *
14
+ * Split from `firewall-policy-drift.test.ts` to keep that file under the house 250-line cap.
15
+ */
16
+ import { describe, expect, test } from 'bun:test';
17
+ import * as Effect from 'effect/Effect';
18
+ import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
19
+ import { attributesOf } from './firewall-policy-form.ts';
20
+ import { driftOf, matches } from './firewall-policy-drift.ts';
21
+ import { type FirewallPolicyProps, declareFirewallPolicy, spec } from './firewall-policy.ts';
22
+
23
+ const FILTER_POLICY_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/fp-2';
24
+
25
+ // Fabricated fixture (RFC 5737/2606-safe) -- kit is PUBLIC. Every compound field here actually
26
+ // carries a filter, plus one key ("futureVendorField") this pinned spec doesn't declare.
27
+ const filterBearingWireBody = () => ({
28
+ id: 'fp-2',
29
+ name: 'Allow specific IPs to the internet',
30
+ enabled: true,
31
+ index: 5,
32
+ action: { type: 'ALLOW' },
33
+ connectionStateFilter: null,
34
+ description: null,
35
+ ipsecFilter: null,
36
+ loggingEnabled: false,
37
+ metadata: { origin: 'USER' },
38
+ source: {
39
+ zoneId: 'zone-src',
40
+ trafficFilter: {
41
+ type: 'NETWORK',
42
+ networkFilter: { matchOpposite: false, networkIds: ['net-1', 'net-2'] },
43
+ portFilter: {
44
+ matchOpposite: false,
45
+ type: 'PORT',
46
+ items: [{ type: 'PORT_SINGLE', value: 443 }],
47
+ },
48
+ },
49
+ },
50
+ destination: {
51
+ zoneId: 'zone-dst',
52
+ trafficFilter: {
53
+ type: 'IP_ADDRESS',
54
+ ipAddressFilter: {
55
+ matchOpposite: false,
56
+ type: 'IP_ADDRESS',
57
+ items: [{ type: 'IP_SINGLE_ADDRESS', value: '203.0.113.5' }],
58
+ },
59
+ },
60
+ },
61
+ ipProtocolScope: {
62
+ ipVersion: 'IPV4_AND_IPV6',
63
+ protocolFilter: {
64
+ type: 'PRESET',
65
+ preset: 'ALL',
66
+ matchOpposite: false,
67
+ futureVendorField: 'unmodeled-by-this-spec-version',
68
+ },
69
+ },
70
+ schedule: {
71
+ mode: 'EVERY_WEEK',
72
+ repeatOnDays: ['MONDAY', 'FRIDAY'],
73
+ timeFilter: { startTime: '08:00', stopTime: '18:00' },
74
+ },
75
+ });
76
+
77
+ const BASE_PROPS: FirewallPolicyProps = {
78
+ siteId: 'site-1',
79
+ firewallPolicyId: 'fp-2',
80
+ action: { type: 'ALLOW' },
81
+ destination: { zoneId: 'zone-dst' },
82
+ enabled: true,
83
+ ipProtocolScope: { ipVersion: 'IPV4' },
84
+ loggingEnabled: false,
85
+ name: 'placeholder',
86
+ source: { zoneId: 'zone-src' },
87
+ };
88
+
89
+ describe('I1 -- a filter-bearing policy round-trips through declareFirewallPolicy', () => {
90
+ test('source.trafficFilter, destination.trafficFilter, ipProtocolScope.protocolFilter and schedule survive, unknown key included', async () => {
91
+ const wire = filterBearingWireBody();
92
+ const fake = fakeUnifi((method, url) =>
93
+ method === 'GET' && url.pathname === FILTER_POLICY_PATH
94
+ ? Response.json(wire)
95
+ : fakeFailure(400, 'unexpected request'),
96
+ );
97
+ const live = await Effect.runPromise(
98
+ spec.fetchLive(BASE_PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
99
+ );
100
+ if (live == null) throw new Error('expected a live FirewallPolicy');
101
+ const declared = declareFirewallPolicy(live, 'site-1');
102
+
103
+ // Cast both sides to `Record<string, unknown>` -- the declared side's real type is a narrow
104
+ // per-discriminator union (A3), and `wire`'s own object-literal type widens each string
105
+ // property; comparing them as unions vs literals is not the point of this assertion, VALUE
106
+ // equality (including the unmodeled key below) is.
107
+ const asRecord = (value: unknown) => value as Record<string, unknown>;
108
+ expect(asRecord(declared.source.trafficFilter)).toEqual(asRecord(wire.source.trafficFilter));
109
+ expect(asRecord(declared.destination.trafficFilter)).toEqual(
110
+ asRecord(wire.destination.trafficFilter),
111
+ );
112
+ expect(asRecord(declared.ipProtocolScope.protocolFilter)).toEqual(
113
+ asRecord(wire.ipProtocolScope.protocolFilter),
114
+ );
115
+ expect(asRecord(declared.schedule)).toEqual(asRecord(wire.schedule));
116
+ // The key this pinned spec has never declared -- proves the mutant this test targets (silently
117
+ // narrowing to `{zoneId}`/`{ipVersion}`) can't hide behind a fixture with nothing to drop.
118
+ expect(asRecord(declared.ipProtocolScope.protocolFilter).futureVendorField).toBe(
119
+ 'unmodeled-by-this-spec-version',
120
+ );
121
+
122
+ const attrs = attributesOf(live, declared);
123
+ expect(matches(attrs, declared)).toBe(true);
124
+ expect(driftOf(live, declared)).toEqual([]);
125
+
126
+ // A nested change inside ONE compound field reports that field, and only that field.
127
+ const changedDestination: FirewallPolicyProps = {
128
+ ...declared,
129
+ destination: {
130
+ zoneId: 'zone-dst',
131
+ trafficFilter: {
132
+ type: 'IP_ADDRESS',
133
+ ipAddressFilter: {
134
+ matchOpposite: false,
135
+ type: 'IP_ADDRESS',
136
+ items: [{ type: 'IP_SINGLE_ADDRESS', value: '203.0.113.99' }],
137
+ },
138
+ },
139
+ },
140
+ };
141
+ expect(driftOf(live, changedDestination).map((d) => d.field)).toEqual(['destination']);
142
+ });
143
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy`'s wire shape — the declarable subset of `FirewallPolicy` (`getFirewallPolicy`'s
3
+ * own decode target), and how a live read becomes both plan attributes and a rendered declaration.
4
+ * Rule ORDER is a separate resource (`firewall-policy-ordering.ts`, T5) — this file is one policy's
5
+ * own fields only.
6
+ *
7
+ * ★ `source`/`destination`/`ipProtocolScope`/`action`/`schedule` STAY COMPOUND, COMPARED WHOLESALE —
8
+ * post-A3 (`8ee11f1`, "type FirewallPolicy/ACL/TrafficMatchingList filters per discriminator"),
9
+ * these decode as real per-discriminator TS unions instead of `unknown`
10
+ * (`discriminated-filter-decode.test.ts` proves the wire path keeps every key, including one this
11
+ * pinned spec doesn't yet declare), so this file is honest about their real shape rather than
12
+ * casting to `unknown` the way `acl-rule-form.ts` still does for its own opaque filters. It does
13
+ * NOT follow that up with a leaf-by-leaf declarable schema for all 12 filter variants, though:
14
+ * that is new modelling work this PR does not attempt, so each field is compared with plain
15
+ * `deepEqual` (`stripNullish`) as ONE value, same proportion `acl-rule-form.ts`'s
16
+ * `enforcingDeviceFilter` gets (one level of structure, not exploded into scalars).
17
+ *
18
+ * ⚠️ KNOWN GAP (T15, same shape as `acl-rule-form.ts`'s own — accepted, not fixed, here either):
19
+ * several of the vendor's filter variants nested inside `source`/`destination`/`ipProtocolScope`
20
+ * are themselves SET-LIKE arrays with no documented order —
21
+ * `FirewallPolicyMACAddressFilter.macAddresses`, `FirewallPolicyNetworkFilter.networkIds`,
22
+ * `FirewallPolicyApplicationFilter.applicationIds`, `FirewallPolicyIPAddressFilter.items`,
23
+ * `FirewallPolicyRegionFilter.regions`, `FirewallPolicyVPNServerFilter.vpnServerIds`,
24
+ * `FirewallSchedule.repeatOnDays` — `deepEqual` compares every one of them order-sensitively, so a
25
+ * console-side reorder with no membership change would report a spurious drift this read-only
26
+ * family's `reconcile` then refuses forever. Not fixed here for the same reason ACL's own gap
27
+ * isn't: normalizing 12 filter variants' own nested arrays is a bigger, separate change, not a
28
+ * proportional extension of typing per-discriminator (A3) into set-aware comparison too.
29
+ *
30
+ * ⛔ `connectionStateFilter` IS THE ONE TOP-LEVEL PROPS ARRAY, AND IS A SET — "Match on firewall
31
+ * connection state. If null, matches all connection states" describes membership, not a sequence
32
+ * (unlike `firewall-policy-ordering-form.ts`'s own `beforeSystemDefined`/`afterSystemDefined`,
33
+ * where position IS the value). Normalized with the same `sortedSet` every other family in this
34
+ * directory uses for a set-shaped array (`firewall-zone-form.ts`'s `networkIds`,
35
+ * `acl-rule-form.ts`'s `protocolFilter`).
36
+ *
37
+ * ⛔ `index` IS ATTRIBUTES-ONLY, NEVER DECLARED. `CreateFirewallPolicyRequest` has no `index`
38
+ * field at all — the live object's current position among ALL policies on the site is reported,
39
+ * but a policy's PRIORITY relative to its own zone pair is `firewall-policy-ordering.ts`'s concern
40
+ * (`OrderedFirewallPolicyIDs`), same split `acl-rule-form.ts`'s own `index` documents.
41
+ *
42
+ * ⛔ `metadata.origin` IS ATTRIBUTES-ONLY, same split every other family here uses
43
+ * (`firewall-zone-form.ts`'s `metadataOrigin`) — `UserOrSystemDefinedOrDerivedEntityMetadata` is
44
+ * server-derived, no writable field accepts it.
45
+ */
46
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
47
+
48
+ const sortedSet = (values: readonly string[]): string[] => [...new Set(values)].sort();
49
+
50
+ /** ★ EXPORTED so `firewall-policy-drift.ts` normalizes both live and declared sides identically —
51
+ * see `acl-rule-form.ts`'s own header for why a normalizer applied to only one side is a bug
52
+ * class (MEDIUM-4). */
53
+ export const normalizeConnectionStateFilter = (
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 FirewallPolicyProps {
60
+ siteId: string;
61
+ firewallPolicyId: string;
62
+ action: firewall.FirewallPolicyAction;
63
+ /** Connection states this policy matches — compared as a SET, see the header. */
64
+ connectionStateFilter?: string[] | undefined;
65
+ description?: string | undefined;
66
+ /** Traffic destination — compound, compared wholesale; see the header's known gap. */
67
+ destination: firewall.FirewallPolicyDestination;
68
+ enabled: boolean;
69
+ /** IP version + protocol scope — compound, compared wholesale; see the header's known gap. */
70
+ ipProtocolScope: firewall.FirewallPolicyIPProtocolScope;
71
+ ipsecFilter?: string | undefined;
72
+ loggingEnabled: boolean;
73
+ name: string;
74
+ /** Schedule this policy is active on — compound, compared wholesale; see the header's known gap. */
75
+ schedule?: firewall.FirewallSchedule | undefined;
76
+ /** Traffic source — compound, compared wholesale; see the header's known gap. */
77
+ source: firewall.FirewallPolicySource;
78
+ }
79
+
80
+ export interface FirewallPolicyAttributes {
81
+ siteId: string;
82
+ firewallPolicyId: string;
83
+ action: firewall.FirewallPolicyAction;
84
+ connectionStateFilter: string[] | undefined;
85
+ description: string | undefined;
86
+ destination: firewall.FirewallPolicyDestination;
87
+ enabled: boolean;
88
+ /** Current position among ALL policies on the site — server-derived, never declared; see header. */
89
+ index: number;
90
+ ipProtocolScope: firewall.FirewallPolicyIPProtocolScope;
91
+ ipsecFilter: string | undefined;
92
+ loggingEnabled: boolean;
93
+ name: string;
94
+ schedule: firewall.FirewallSchedule | undefined;
95
+ source: firewall.FirewallPolicySource;
96
+ /** `metadata.origin` — server-derived, never declared; see `firewall-zone-form.ts`'s own. */
97
+ metadataOrigin: string;
98
+ }
99
+
100
+ export const attributesOf = (
101
+ live: firewall.FirewallPolicy,
102
+ props: FirewallPolicyProps,
103
+ ): FirewallPolicyAttributes => ({
104
+ siteId: props.siteId,
105
+ firewallPolicyId: live.id,
106
+ action: live.action,
107
+ connectionStateFilter: normalizeConnectionStateFilter(live.connectionStateFilter),
108
+ description: live.description,
109
+ destination: live.destination,
110
+ enabled: live.enabled,
111
+ index: live.index,
112
+ ipProtocolScope: live.ipProtocolScope,
113
+ ipsecFilter: live.ipsecFilter,
114
+ loggingEnabled: live.loggingEnabled,
115
+ name: live.name,
116
+ schedule: live.schedule,
117
+ source: live.source,
118
+ metadataOrigin: live.metadata.origin,
119
+ });
120
+
121
+ /**
122
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
123
+ * needs so its plan is noop"). A later import script feeds `getFirewallPolicy`'s own response
124
+ * straight in and writes the result as `firewallPolicy('<id>', declareFirewallPolicy(live, siteId))`
125
+ * — no field is invented or defaulted beyond what `attributesOf` already reports, so
126
+ * `matches(attributesOf(live, props), declareFirewallPolicy(live, siteId))` is `true` by
127
+ * construction (`firewall-policy.test.ts` proves it).
128
+ */
129
+ export const declareFirewallPolicy = (
130
+ live: firewall.FirewallPolicy,
131
+ siteId: string,
132
+ ): FirewallPolicyProps => ({
133
+ siteId,
134
+ firewallPolicyId: live.id,
135
+ action: live.action,
136
+ connectionStateFilter: normalizeConnectionStateFilter(live.connectionStateFilter),
137
+ description: live.description,
138
+ destination: live.destination,
139
+ enabled: live.enabled,
140
+ ipProtocolScope: live.ipProtocolScope,
141
+ ipsecFilter: live.ipsecFilter,
142
+ loggingEnabled: live.loggingEnabled,
143
+ name: live.name,
144
+ schedule: live.schedule,
145
+ source: live.source,
146
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `Unifi.FirewallPolicyOrdering`'s wire shape — ONE resource per `(sourceFirewallZoneId,
3
+ * destinationFirewallZoneId)` ZONE PAIR (`getFirewallPolicyOrdering`'s own scope, `GET
4
+ * /v1/sites/{siteId}/firewall/policies/ordering?sourceFirewallZoneId=…&destinationFirewallZoneId=…`),
5
+ * not per policy and not site-wide. Compare with `acl-rule-ordering-form.ts`'s own header: ACL rule
6
+ * order is ONE list for the whole site because ACL rules have no zone concept; firewall policies are
7
+ * scoped to a zone pair (`firewall-zone.ts`'s own `Unifi.FirewallZone`), so the vendor keys ordering
8
+ * the same way — a console can have as many `Unifi.FirewallPolicyOrdering` resources as it has
9
+ * zone pairs with at least one user-defined policy between them.
10
+ *
11
+ * ⛔ T5 — `beforeSystemDefined`/`afterSystemDefined` ARE EACH A SEQUENCE, NEVER RUN THROUGH
12
+ * `sortedSet`. `OrderedFirewallPolicyIDs`' own shape splits a zone pair's user-defined policies
13
+ * into the ones ordered BEFORE the system-defined policies and the ones ordered AFTER them —
14
+ * within each half, position is priority, the same "lower index/earlier position wins" reasoning
15
+ * `acl-rule-ordering-form.ts` gives for `orderedAclRuleIds`. `drift.ts`'s `defaultEqual`
16
+ * (`deepEqual`) already compares arrays element-by-element without sorting them, so leaving both
17
+ * fields un-normalized is what makes the comparison order-sensitive, not an oversight.
18
+ *
19
+ * ★ TWO SEQUENCES, NOT ONE — the one real difference from `acl-rule-ordering-form.ts`'s single
20
+ * `orderedAclRuleIds`. A policy ordered "before system-defined" and one ordered "after" can never
21
+ * swap sides through this endpoint (there is no single combined list position that would let one
22
+ * policy move from one half to the other) — `matches`/`driftOf` below compare each half
23
+ * independently, so a policy moving from `beforeSystemDefined` to `afterSystemDefined` reports as
24
+ * drift on BOTH fields, not a single reordering of one array.
25
+ *
26
+ * ★ `makeDriftOf`, NO CUSTOM `equal` — same framework every other family's `matches`/`driftOf`
27
+ * share (MEDIUM-4). Its default `deepEqual` is already order-sensitive for arrays, so the absence
28
+ * of a custom `equal` here is itself the "never sortedSet" rule in force, exactly as
29
+ * `acl-rule-ordering-form.ts`'s own header explains for its one field.
30
+ */
31
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
32
+ import { makeDriftOf } from './drift.ts';
33
+
34
+ export interface FirewallPolicyOrderingProps {
35
+ siteId: string;
36
+ sourceFirewallZoneId: string;
37
+ destinationFirewallZoneId: string;
38
+ /** Policy IDs ordered before the zone pair's system-defined policies, highest priority first —
39
+ * never compared as a set (see header). */
40
+ beforeSystemDefined: string[];
41
+ /** Policy IDs ordered after the zone pair's system-defined policies, highest priority first —
42
+ * never compared as a set (see header). */
43
+ afterSystemDefined: string[];
44
+ }
45
+
46
+ export interface FirewallPolicyOrderingAttributes {
47
+ siteId: string;
48
+ sourceFirewallZoneId: string;
49
+ destinationFirewallZoneId: string;
50
+ beforeSystemDefined: string[];
51
+ afterSystemDefined: string[];
52
+ }
53
+
54
+ export const attributesOf = (
55
+ live: firewall.IntegrationFirewallPolicyOrderingDto,
56
+ props: FirewallPolicyOrderingProps,
57
+ ): FirewallPolicyOrderingAttributes => ({
58
+ siteId: props.siteId,
59
+ sourceFirewallZoneId: props.sourceFirewallZoneId,
60
+ destinationFirewallZoneId: props.destinationFirewallZoneId,
61
+ beforeSystemDefined: live.orderedFirewallPolicyIds.beforeSystemDefined,
62
+ afterSystemDefined: live.orderedFirewallPolicyIds.afterSystemDefined,
63
+ });
64
+
65
+ /**
66
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
67
+ * needs so its plan is noop"). A later import script feeds `getFirewallPolicyOrdering`'s own
68
+ * response straight in and writes the result as `firewallPolicyOrdering('<id>',
69
+ * declareFirewallPolicyOrdering(live, siteId, sourceZoneId, destZoneId))` —
70
+ * `matches(attributesOf(live, props), declareFirewallPolicyOrdering(live, …))` is `true` by
71
+ * construction (`firewall-policy-ordering.test.ts` proves it).
72
+ */
73
+ export const declareFirewallPolicyOrdering = (
74
+ live: firewall.IntegrationFirewallPolicyOrderingDto,
75
+ siteId: string,
76
+ sourceFirewallZoneId: string,
77
+ destinationFirewallZoneId: string,
78
+ ): FirewallPolicyOrderingProps => ({
79
+ siteId,
80
+ sourceFirewallZoneId,
81
+ destinationFirewallZoneId,
82
+ beforeSystemDefined: live.orderedFirewallPolicyIds.beforeSystemDefined,
83
+ afterSystemDefined: live.orderedFirewallPolicyIds.afterSystemDefined,
84
+ });
85
+
86
+ const fieldDrift = makeDriftOf<FirewallPolicyOrderingAttributes, FirewallPolicyOrderingProps>([
87
+ {
88
+ field: 'beforeSystemDefined',
89
+ live: (a) => a.beforeSystemDefined,
90
+ declared: (p) => p.beforeSystemDefined,
91
+ },
92
+ {
93
+ field: 'afterSystemDefined',
94
+ live: (a) => a.afterSystemDefined,
95
+ declared: (p) => p.afterSystemDefined,
96
+ },
97
+ ]);
98
+
99
+ export const matches = (
100
+ attributes: FirewallPolicyOrderingAttributes,
101
+ props: FirewallPolicyOrderingProps,
102
+ ): boolean => fieldDrift(attributes, props).length === 0;
103
+
104
+ /**
105
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
106
+ * (`driftOf(live, props)`).
107
+ */
108
+ export const driftOf = (
109
+ live: firewall.IntegrationFirewallPolicyOrderingDto,
110
+ props: FirewallPolicyOrderingProps,
111
+ ) => fieldDrift(attributesOf(live, props), props);