@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,64 @@
1
+ /**
2
+ * Shared comment/string-aware source scanner for `src/unifi`'s static banned-token tests:
3
+ * `write-op-reference.test.ts` (T12 — no SDK write-op reference anywhere in this directory) and
4
+ * `wifi-broadcast-details-call.test.ts` (T23 — no reference to the passphrase-bearing
5
+ * `getWifiBroadcastDetails` GET). Extracted (red team, IMPORTANT-2, 2026-09-26) so both share the
6
+ * ONE left-to-right, single-state comment stripper instead of each maintaining its own — see the
7
+ * history below for why a naive or duplicated implementation is a real trap here, not a
8
+ * hypothetical one.
9
+ *
10
+ * ⚠️ COMMENTS ARE STRIPPED WITH A SINGLE LEFT-TO-RIGHT SCAN, NOT TWO INDEPENDENT REGEXES. An
11
+ * earlier "mask strings, then regex the comments" attempt had its own bug: an apostrophe in
12
+ * ordinary prose ("doesn't", "it's") reads as an unterminated string open to any regex that does
13
+ * not already know it is inside a comment, and can swallow real code up to the next quote
14
+ * anywhere later in the file. `stripComments` tracks ONE state (code / line comment / block
15
+ * comment / string) char by char left to right, so a comment can't be misread as a string and a
16
+ * string's contents (a banned token hidden in one, e.g. `alias['deleteNetwork']`, is still an
17
+ * offense) are never mistaken for a comment either.
18
+ */
19
+ import { readFileSync, readdirSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+
22
+ /**
23
+ * One left-to-right scan tracking a single state (code / `//` / `/* *\/` / string). Comments are
24
+ * blanked to spaces (newlines kept, so a caller's own line-number diagnostics still line up);
25
+ * string and template literal CONTENTS are left untouched, because a banned token hidden inside
26
+ * one is still an offense a caller needs to see.
27
+ */
28
+ export const stripComments = (src: string): string => {
29
+ let out = '';
30
+ for (let i = 0; i < src.length;) {
31
+ const two = src.slice(i, i + 2);
32
+ if (two === '//') {
33
+ const end = src.indexOf('\n', i);
34
+ const stop = end === -1 ? src.length : end;
35
+ out += ' '.repeat(stop - i);
36
+ i = stop;
37
+ } else if (two === '/*') {
38
+ const end = src.indexOf('*/', i + 2);
39
+ const stop = end === -1 ? src.length : end + 2;
40
+ out += src.slice(i, stop).replace(/[^\n]/g, ' ');
41
+ i = stop;
42
+ } else if (src[i] === '"' || src[i] === "'" || src[i] === '`') {
43
+ const quote = src[i];
44
+ let j = i + 1;
45
+ while (j < src.length && src[j] !== quote) j += src[j] === '\\' ? 2 : 1;
46
+ const stop = Math.min(j + 1, src.length);
47
+ out += src.slice(i, stop);
48
+ i = stop;
49
+ } else {
50
+ out += src[i];
51
+ i += 1;
52
+ }
53
+ }
54
+ return out;
55
+ };
56
+
57
+ /** Every `.ts` file under `dir`, recursively — a future `src/unifi/<subdir>/*.ts` is walked too. */
58
+ export const walk = (dir: string): string[] =>
59
+ readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
60
+ const full = join(dir, entry.name);
61
+ return entry.isDirectory() ? walk(full) : entry.name.endsWith('.ts') ? [full] : [];
62
+ });
63
+
64
+ export const readSource = (path: string): string => readFileSync(path, 'utf8');
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `Unifi.TrafficMatchingList`'s `matches` AND `driftOf` (B6) — both built on the ONE field list
3
+ * below, the same `fieldDrift`-is-the-only-comparison shape `network-drift.ts`'s header explains
4
+ * (MEDIUM-4, red team, 2026-09-26): `matches(attrs, props) === (driftOf(live, props).length === 0)`
5
+ * is true 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
+ * ★ `items` compares with the plain default `equal` (whole-value `deepEqual`) — see
10
+ * `traffic-matching-list-form.ts`'s header for the accepted known gap this leaves for a
11
+ * membership-preserving reorder.
12
+ */
13
+ import type * as trafficMatchingLists from '@distilled.cloud/unifi-network/traffic_matching_lists';
14
+ import { makeDriftOf } from './drift.ts';
15
+ import {
16
+ type TrafficMatchingListAttributes,
17
+ type TrafficMatchingListProps,
18
+ attributesOf,
19
+ } from './traffic-matching-list-form.ts';
20
+
21
+ const fieldDrift = makeDriftOf<TrafficMatchingListAttributes, TrafficMatchingListProps>([
22
+ { field: 'name', live: (a) => a.name, declared: (p) => p.name },
23
+ { field: 'type', live: (a) => a.type, declared: (p) => p.type },
24
+ { field: 'items', live: (a) => a.items, declared: (p) => p.items },
25
+ ]);
26
+
27
+ export const matches = (
28
+ attributes: TrafficMatchingListAttributes,
29
+ props: TrafficMatchingListProps,
30
+ ): boolean => fieldDrift(attributes, props).length === 0;
31
+
32
+ /**
33
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
34
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
35
+ * `attributesOf` itself just to call this.
36
+ */
37
+ export const driftOf = (
38
+ live: trafficMatchingLists.TrafficMatchingList,
39
+ props: TrafficMatchingListProps,
40
+ ) => fieldDrift(attributesOf(live, props), props);
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `Unifi.TrafficMatchingList`'s wire shape — the declarable subset of `TrafficMatchingList`
3
+ * (`getTrafficMatchingList`'s own decode target), and how a live read becomes both plan attributes
4
+ * and a rendered declaration.
5
+ *
6
+ * ★ NO `metadata` FIELD AT ALL — the simplest object shape in this directory. Unlike every other
7
+ * family here (`FirewallZone`/`DNSPolicy`/`ACLRule`/`FirewallPolicy` all carry a server-derived
8
+ * `metadata.origin`), `TrafficMatchingList` is just `{id, name, type, items?}` — nothing here is
9
+ * attributes-only the way `metadataOrigin`/`index` are elsewhere.
10
+ *
11
+ * ★ `items` IS A REAL TS UNION POST-A3, COMPARED WHOLESALE — `Array<IPv4Matching> |
12
+ * Array<IPv6Matching> | Array<PortMatching>`, keyed by the sibling `type` field
13
+ * (`traffic_matching_lists.ts`'s own comment: "Shape depends on the sibling discriminator value").
14
+ * The runtime schema is still a passthrough `S.Unknown` (A3's own changeset: narrower for a reader
15
+ * at the type level only, so decode keeps every key including one this pinned spec doesn't yet
16
+ * declare — same proof `discriminated-filter-decode.test.ts` runs for `FirewallPolicy`/`AclRule`).
17
+ *
18
+ * ⚠️ KNOWN GAP (T15, same shape `acl-rule-form.ts`/`firewall-policy-form.ts` both carry): a traffic
19
+ * matching list's `items` is semantically a SET of match entries (matching is "is this traffic
20
+ * covered by ANY entry," not order-dependent, unlike `firewall-policy-ordering-form.ts`'s own
21
+ * sequences), but each entry is an OBJECT (`{type, value?, start?, stop?}`), not a bare string --
22
+ * there is no cheap canonical sort key the way `sortedSet` gives a `string[]`. `deepEqual` compares
23
+ * `items` order-sensitively, so a console-side reorder with no membership change would report a
24
+ * spurious drift this read-only family's `reconcile` then refuses forever. Accepted, not fixed,
25
+ * same proportion as ACL rule's own nested-filter gap: a generic object-set normalizer is bigger,
26
+ * separate modelling work, not a mechanical extension of typing `items` per-discriminator (A3).
27
+ */
28
+ import type * as trafficMatchingLists from '@distilled.cloud/unifi-network/traffic_matching_lists';
29
+
30
+ export interface TrafficMatchingListProps {
31
+ siteId: string;
32
+ trafficMatchingListId: string;
33
+ name: string;
34
+ /** Discriminator for `items`' own shape (e.g. an IPv4/IPv6/port list) — the spec enumerates no
35
+ * fixed value set for it (also accepts a future vendor value it did not enumerate). */
36
+ type: string;
37
+ /** Match entries — a SET semantically, compared wholesale; see the header's known gap. */
38
+ items?: trafficMatchingLists.TrafficMatchingListItems | undefined;
39
+ }
40
+
41
+ export interface TrafficMatchingListAttributes {
42
+ siteId: string;
43
+ trafficMatchingListId: string;
44
+ name: string;
45
+ type: string;
46
+ items: trafficMatchingLists.TrafficMatchingListItems | undefined;
47
+ }
48
+
49
+ export const attributesOf = (
50
+ live: trafficMatchingLists.TrafficMatchingList,
51
+ props: TrafficMatchingListProps,
52
+ ): TrafficMatchingListAttributes => ({
53
+ siteId: props.siteId,
54
+ trafficMatchingListId: live.id,
55
+ name: live.name,
56
+ type: live.type,
57
+ items: live.items,
58
+ });
59
+
60
+ /**
61
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
62
+ * needs so its plan is noop"). A later import script feeds `getTrafficMatchingList`'s own response
63
+ * straight in and writes the result as `trafficMatchingList('<id>',
64
+ * declareTrafficMatchingList(live, siteId))` — no field is invented or defaulted beyond what
65
+ * `attributesOf` already reports, so `matches(attributesOf(live, props), declareTrafficMatchingList(
66
+ * live, siteId))` is `true` by construction (`traffic-matching-list.test.ts` proves it).
67
+ */
68
+ export const declareTrafficMatchingList = (
69
+ live: trafficMatchingLists.TrafficMatchingList,
70
+ siteId: string,
71
+ ): TrafficMatchingListProps => ({
72
+ siteId,
73
+ trafficMatchingListId: live.id,
74
+ name: live.name,
75
+ type: live.type,
76
+ items: live.items,
77
+ });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * `Unifi.TrafficMatchingList`'s `spec` against a fake UniFi Network API — mirrors
3
+ * `dns-policy.test.ts`. `driftOf`/`matches` coverage lives here too, same reasoning as that file's
4
+ * own header (a short field list, no separate need to split under the house cap).
5
+ *
6
+ * ⛔ I2 (red team, 2026-09-26, PR 309 LAND): `traffic-matching-list-drift.ts`'s field list is
7
+ * untyped strings (`makeDriftOf`'s own contract) — dropping `type` from it left every test above
8
+ * green. The last `describe` below closes that the same way `firewall-policy-drift.test.ts` does:
9
+ * `DECLARABLE_FIELDS` is a `Record<..., true>` keyed by `Exclude<keyof TrafficMatchingListProps,
10
+ * 'siteId' | 'trafficMatchingListId'>` — a field added to or removed from
11
+ * `TrafficMatchingListProps` without a matching edit there is a TYPE ERROR, so the list is
12
+ * complete BY CONSTRUCTION; the per-field test then proves each one is actually wired into
13
+ * `fieldDrift`, not merely present in the type.
14
+ */
15
+ import { describe, expect, test } from 'bun:test';
16
+ import type * as trafficMatchingLists from '@distilled.cloud/unifi-network/traffic_matching_lists';
17
+ import * as Retry from '@distilled.cloud/unifi-network/Retry';
18
+ import * as Effect from 'effect/Effect';
19
+ import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
20
+ import { attributesOf } from './traffic-matching-list-form.ts';
21
+ import { driftOf, matches } from './traffic-matching-list-drift.ts';
22
+ import {
23
+ type TrafficMatchingListProps,
24
+ declareTrafficMatchingList,
25
+ spec,
26
+ } from './traffic-matching-list.ts';
27
+ import { unifiOperations } from './resource.ts';
28
+
29
+ const LIST_PATH = '/proxy/network/integration/v1/sites/site-1/traffic-matching-lists/tml-1';
30
+
31
+ const liveTrafficMatchingList = (
32
+ overrides: Partial<trafficMatchingLists.TrafficMatchingList> = {},
33
+ ): trafficMatchingLists.TrafficMatchingList => ({
34
+ id: 'tml-1',
35
+ name: 'Office subnets',
36
+ type: 'IPV4',
37
+ items: [{ type: 'SUBNET', value: '192.0.2.0/24' }],
38
+ ...overrides,
39
+ });
40
+
41
+ const PROPS: TrafficMatchingListProps = {
42
+ siteId: 'site-1',
43
+ trafficMatchingListId: 'tml-1',
44
+ name: 'Office subnets',
45
+ type: 'IPV4',
46
+ items: [{ type: 'SUBNET', value: '192.0.2.0/24' }],
47
+ };
48
+
49
+ describe('Unifi.TrafficMatchingList spec.fetchLive', () => {
50
+ test('a real GET response decodes into the typed live object', async () => {
51
+ const fake = fakeUnifi((method, url) =>
52
+ method === 'GET' && url.pathname === LIST_PATH
53
+ ? Response.json(liveTrafficMatchingList())
54
+ : fakeFailure(400, 'unexpected request'),
55
+ );
56
+ const live = await Effect.runPromise(
57
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
58
+ );
59
+ expect(live?.id).toBe('tml-1');
60
+ expect(fake.seen).toEqual([{ method: 'GET', path: LIST_PATH }]);
61
+ });
62
+
63
+ test('a genuine 404 folds to undefined -- never a false "unreadable" on anything else', async () => {
64
+ const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
65
+ const live = await Effect.runPromise(
66
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
67
+ );
68
+ expect(live).toBeUndefined();
69
+ });
70
+
71
+ test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
72
+ const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
73
+ const failure = await Effect.runPromise(
74
+ Effect.flip(
75
+ spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
76
+ ),
77
+ );
78
+ expect(failure._tag).toBe('InternalServerError');
79
+ });
80
+ });
81
+
82
+ describe('declareTrafficMatchingList -- the declaration renderer', () => {
83
+ test('its output matches attributesOf(live) by construction, for any live object', () => {
84
+ // A different discriminator case than the default fixture (`PortMatching[]`, not
85
+ // `IPv4Matching[]`) -- a fresh literal, not the SUBNET fixture's overrides.
86
+ const live: trafficMatchingLists.TrafficMatchingList = {
87
+ id: 'tml-2',
88
+ name: 'Management ports',
89
+ type: 'PORT',
90
+ items: [{ type: 'RANGE', start: 8000, stop: 8100 }],
91
+ };
92
+ const declared = declareTrafficMatchingList(live, 'site-1');
93
+ const attrs = attributesOf(live, declared);
94
+ expect(matches(attrs, declared)).toBe(true);
95
+ expect(driftOf(live, declared)).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe('driftOf -- B6 field-level drift, straight from one live read', () => {
100
+ test('a real name change reports its own field, live and declared', () => {
101
+ const live = liveTrafficMatchingList({ name: 'Office /24s' });
102
+ expect(driftOf(live, PROPS)).toEqual([
103
+ { field: 'name', live: 'Office /24s', declared: 'Office subnets' },
104
+ ]);
105
+ });
106
+
107
+ test('items compares wholesale -- a real membership change is caught', () => {
108
+ const live = liveTrafficMatchingList({ items: [{ type: 'SUBNET', value: '203.0.113.0/24' }] });
109
+ expect(driftOf(live, PROPS)).toEqual([
110
+ {
111
+ field: 'items',
112
+ live: [{ type: 'SUBNET', value: '203.0.113.0/24' }],
113
+ declared: [{ type: 'SUBNET', value: '192.0.2.0/24' }],
114
+ },
115
+ ]);
116
+ });
117
+
118
+ test('a live object with no items key at all is a noop against declared undefined', () => {
119
+ // `exactOptionalPropertyTypes`: the SDK's own `items?` type has no explicit `| undefined`, so
120
+ // this omits the key entirely rather than setting it to `undefined` -- the real wire shape for
121
+ // a list with no match entries yet.
122
+ const live: trafficMatchingLists.TrafficMatchingList = {
123
+ id: 'tml-1',
124
+ name: 'Office subnets',
125
+ type: 'IPV4',
126
+ };
127
+ const props: TrafficMatchingListProps = { ...PROPS, items: undefined };
128
+ expect(matches(attributesOf(live, props), props)).toBe(true);
129
+ expect(driftOf(live, props)).toEqual([]);
130
+ });
131
+ });
132
+
133
+ describe('Unifi.TrafficMatchingList write paths never reach the vendor API', () => {
134
+ const ops = unifiOperations(spec);
135
+
136
+ test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
137
+ const fake = fakeUnifi((method, url) =>
138
+ method === 'GET' && url.pathname === LIST_PATH
139
+ ? Response.json(liveTrafficMatchingList())
140
+ : fakeFailure(400, 'unexpected request'),
141
+ );
142
+ const exit = await Effect.runPromiseExit(
143
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
144
+ );
145
+ expect(exit._tag).toBe('Success');
146
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
147
+ });
148
+
149
+ test('reconcile on drift refuses instead of PUTting a merged body', async () => {
150
+ const fake = fakeUnifi((method, url) =>
151
+ method === 'GET' && url.pathname === LIST_PATH
152
+ ? Response.json(liveTrafficMatchingList({ name: 'Renamed elsewhere' }))
153
+ : fakeFailure(400, 'unexpected request'),
154
+ );
155
+ const exit = await Effect.runPromiseExit(
156
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
157
+ );
158
+ expect(exit._tag).toBe('Failure');
159
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
160
+ });
161
+
162
+ test('delete always refuses -- no DELETE is ever sent', async () => {
163
+ const fake = fakeUnifi((method, url) =>
164
+ method === 'GET' && url.pathname === LIST_PATH
165
+ ? Response.json(liveTrafficMatchingList())
166
+ : fakeFailure(400, 'unexpected request'),
167
+ );
168
+ const exit = await Effect.runPromiseExit(
169
+ ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
170
+ );
171
+ expect(exit._tag).toBe('Failure');
172
+ expect(fake.seen).toEqual([]);
173
+ });
174
+ });
175
+
176
+ type DeclarableField = Exclude<keyof TrafficMatchingListProps, 'siteId' | 'trafficMatchingListId'>;
177
+
178
+ const DECLARABLE_FIELDS: Record<DeclarableField, true> = {
179
+ name: true,
180
+ type: true,
181
+ items: true,
182
+ };
183
+
184
+ /** One changed value per field -- distinct enough from PROPS' own that `deepEqual` calls it drift. */
185
+ const CHANGED_VALUE: { [K in DeclarableField]: TrafficMatchingListProps[K] } = {
186
+ name: 'a different name',
187
+ type: 'PORT',
188
+ items: [{ type: 'RANGE', start: 8000, stop: 8100 }],
189
+ };
190
+
191
+ describe('I2 -- every declarable field is wired into the drift field list', () => {
192
+ // `liveTrafficMatchingList()` (no overrides) matches PROPS field-for-field, so changing exactly
193
+ // one PROPS field per case isolates that field's own wire-up.
194
+ test.each(Object.keys(DECLARABLE_FIELDS) as DeclarableField[])(
195
+ 'changing only %s reports exactly that field',
196
+ (field) => {
197
+ const live = liveTrafficMatchingList();
198
+ const changedProps: TrafficMatchingListProps = { ...PROPS, [field]: CHANGED_VALUE[field] };
199
+ expect(driftOf(live, changedProps).map((d) => d.field)).toEqual([field]);
200
+ },
201
+ );
202
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `Unifi.TrafficMatchingList` — one traffic matching list (IPv4/IPv6/port match entries) on a UniFi
3
+ * Network site, READ-ONLY (`policy.ts`).
4
+ *
5
+ * ★ T11 GATE, CHECKED 2026-09-26: `Traffic Matching Lists`' 5 operations are byte-identical between
6
+ * 10.4.57 and 10.6.97, full raw operation objects; the 19-schema closure reachable from all 5
7
+ * (`$ref` + `discriminator.mapping`) shares zero names with the 14 schemas that DID change
8
+ * elsewhere in the document. Corroborated independently by the A3 changeset's own broader
9
+ * 25-operation/139-schema re-check (`.changeset/unifi-network-a3-typed-discriminator-filters.md`).
10
+ * Full breakdown: `docs/unifi-traffic-matching-list.md`.
11
+ */
12
+ import { Resource } from 'alchemy';
13
+ import { adopt } from 'alchemy/AdoptPolicy';
14
+ import * as Provider from 'alchemy/Provider';
15
+ import * as trafficMatchingLists from '@distilled.cloud/unifi-network/traffic_matching_lists';
16
+ import * as Effect from 'effect/Effect';
17
+ import {
18
+ type TrafficMatchingListAttributes,
19
+ type TrafficMatchingListProps,
20
+ attributesOf,
21
+ } from './traffic-matching-list-form.ts';
22
+ import { matches } from './traffic-matching-list-drift.ts';
23
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
24
+ import { unifiHandlers } from './resource.ts';
25
+
26
+ export type {
27
+ TrafficMatchingListAttributes,
28
+ TrafficMatchingListProps,
29
+ } from './traffic-matching-list-form.ts';
30
+ export { declareTrafficMatchingList } from './traffic-matching-list-form.ts';
31
+ export { driftOf } from './traffic-matching-list-drift.ts';
32
+
33
+ export interface UnifiTrafficMatchingList extends Resource<
34
+ 'Unifi.TrafficMatchingList',
35
+ TrafficMatchingListProps,
36
+ TrafficMatchingListAttributes,
37
+ never,
38
+ UnifiRequirements
39
+ > {}
40
+
41
+ export const UnifiTrafficMatchingList = Resource<UnifiTrafficMatchingList>(
42
+ 'Unifi.TrafficMatchingList',
43
+ { defaultRemovalPolicy: 'retain' },
44
+ );
45
+
46
+ /** `trafficMatchingList('office-subnets', props)` — `adopt(true)` piped on by default (H5); see
47
+ * `network.ts`. */
48
+ export const trafficMatchingList = (id: string, props: TrafficMatchingListProps) =>
49
+ UnifiTrafficMatchingList(id, props).pipe(adopt(true));
50
+
51
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
52
+ export const spec: UnifiSpec<
53
+ TrafficMatchingListProps,
54
+ trafficMatchingLists.TrafficMatchingList,
55
+ TrafficMatchingListAttributes,
56
+ trafficMatchingLists.GetTrafficMatchingListError
57
+ > = {
58
+ type: 'Unifi.TrafficMatchingList',
59
+ describe: (props) =>
60
+ `sites/${props.siteId}/traffic-matching-lists/${props.trafficMatchingListId}`,
61
+ fetchLive: (props) =>
62
+ trafficMatchingLists
63
+ .getTrafficMatchingList({
64
+ siteId: props.siteId,
65
+ trafficMatchingListId: props.trafficMatchingListId,
66
+ })
67
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
68
+ attributes: attributesOf,
69
+ matches,
70
+ };
71
+
72
+ export const handlers = unifiHandlers(spec);
73
+
74
+ export const UnifiTrafficMatchingListProvider = () =>
75
+ Provider.effect(
76
+ UnifiTrafficMatchingList,
77
+ Effect.succeed(UnifiTrafficMatchingList.Provider.of(handlers)),
78
+ );
@@ -0,0 +1,92 @@
1
+ /**
2
+ * T23 static guard (red team, IMPORTANT-2, 2026-09-26): no file under `src/unifi` may reference
3
+ * `getWifiBroadcastDetails` — the single-object WiFi Broadcast GET whose `securityConfiguration`
4
+ * carries the WPA passphrase and every PPSK entry's own passphrase (`wifi-broadcast.ts`'s header;
5
+ * SDK `wifi_broadcasts.ts`). Confirmed missing before this file existed: `write-op-reference.test.ts`
6
+ * only bans WRITE verbs (create/update/delete/patch/execute/remove/adopt) — nothing stopped a
7
+ * future edit from calling this GET, even though it is exactly as forbidden here as a write op is.
8
+ *
9
+ * ⛔ NOT MERGED INTO `write-op-reference.test.ts`'s OWN SCAN. That scan's `WRITE_OP_NAME` regex and
10
+ * its `sdkWriteOpNames` harvest are BOTH scoped to `BANNED_VERBS` — `getWifiBroadcastDetails`
11
+ * starts with `get`, so it can never match there, and widening that regex to catch it would also
12
+ * start matching ordinary read helpers this family and every other one legitimately call
13
+ * (`getNetworkDetails`, `getWifiBroadcastPage`, …). This is a single hand-named token, not a verb
14
+ * class, so it gets its own small scan built on the SAME comment/string-aware scanner
15
+ * (`scan-source.ts`) rather than a real design bent to fit inside the other one.
16
+ *
17
+ * ⚠️ NO `SDK_MARKER` GATE, UNLIKE `write-op-reference.test.ts`. That gate exists there because
18
+ * `create`/`delete`/… are common English words that need the "and this file imports the SDK" ANDed
19
+ * check to avoid false positives; `getWifiBroadcastDetails` is specific enough that its bare
20
+ * appearance anywhere in code (not a comment) already IS the offense, with no plausible unrelated
21
+ * source for that exact identifier.
22
+ *
23
+ * ⛔ ONE NAMED EXCEPTION, NOT A PATTERN. `errors-and-secrets.test.ts` (pre-dates this file — A2,
24
+ * kit PR #300) calls `getWifiBroadcastDetails` directly ON PURPOSE: it is the runtime proof that
25
+ * the SDK's OWN regen redacts the WPA/PPSK passphrase to `Redacted.Redacted<string>` at the wire
26
+ * (T23's `sensitivePatterns` fix), which is impossible to prove without actually calling the
27
+ * details endpoint at the SDK layer. That is a DIFFERENT claim from this ban's actual target —
28
+ * "no `Unifi.WifiBroadcast` RESOURCE/family file ever calls it" — so it is named here explicitly,
29
+ * not folded into a looser rule that would also excuse a future family file. A new exception
30
+ * needs the same caliber of justification (proving a lower-layer guarantee that cannot be proven
31
+ * without the call) and its own line here, not a widened pattern.
32
+ */
33
+ import { describe, expect, test } from 'bun:test';
34
+ import { dirname, join } from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+ import { readSource, stripComments, walk } from './scan-source.ts';
37
+
38
+ const SELF = fileURLToPath(import.meta.url);
39
+ const DIR = dirname(SELF);
40
+ const BANNED_TOKEN = 'getWifiBroadcastDetails';
41
+ const TOKEN_PATTERN = new RegExp(`\\b${BANNED_TOKEN}\\b`);
42
+
43
+ /** The one file allowed to reference the banned token, and why — see this file's header. */
44
+ const ALLOWED_REFERENCES: ReadonlySet<string> = new Set([join(DIR, 'errors-and-secrets.test.ts')]);
45
+
46
+ const sourceFiles = () =>
47
+ walk(DIR).filter((path) => path !== SELF && !ALLOWED_REFERENCES.has(path));
48
+
49
+ const referencesBannedToken = (rawSrc: string): boolean =>
50
+ TOKEN_PATTERN.test(stripComments(rawSrc));
51
+
52
+ describe('src/unifi never calls getWifiBroadcastDetails (T23, IMPORTANT-2)', () => {
53
+ test('every source file, except the one named exception, is clean of the details call', () => {
54
+ const offenders = sourceFiles().filter((path) => referencesBannedToken(readSource(path)));
55
+ expect(offenders).toEqual([]);
56
+ });
57
+
58
+ test('the named exception still exists and still is the one making the call (guards a silent rename)', () => {
59
+ const [exceptionPath] = [...ALLOWED_REFERENCES];
60
+ if (exceptionPath === undefined) throw new Error('ALLOWED_REFERENCES must not be empty');
61
+ expect(referencesBannedToken(readSource(exceptionPath))).toBe(true);
62
+ });
63
+
64
+ test('a comment or prose mention of the banned name never trips the scan', () => {
65
+ // This file's own header, and wifi-broadcast.ts's, both name the token in prose -- proving the
66
+ // scan is comment-aware IS the point: a naive substring scan would flag the very files that
67
+ // document why the call is forbidden.
68
+ const documented = `
69
+ /** Never call getWifiBroadcastDetails directly -- see T23. */
70
+ // getWifiBroadcastDetails is refused by policy.ts too.
71
+ export const read = () => 1;
72
+ `;
73
+ expect(referencesBannedToken(documented)).toBe(false);
74
+ });
75
+
76
+ test('a real reference to the banned call is caught regardless of syntax shape', () => {
77
+ const marker = '@distilled.cloud/unifi-network/wifi_broadcasts';
78
+ const cases = [
79
+ `import { getWifiBroadcastDetails } from '${marker}';`,
80
+ `import { getWifiBroadcastDetails as readOne } from '${marker}'; readOne;`,
81
+ `export { getWifiBroadcastDetails } from '${marker}';`,
82
+ `const fn = (m: Record<string, unknown>) => m['getWifiBroadcastDetails'];`,
83
+ `const fn = async () => (await import('${marker}')).getWifiBroadcastDetails;`,
84
+ ];
85
+ for (const src of cases) expect(referencesBannedToken(src)).toBe(true);
86
+ });
87
+
88
+ test('the walk is recursive, matching write-op-reference.test.ts', () => {
89
+ expect(sourceFiles().length).toBeGreaterThan(0);
90
+ expect(sourceFiles()).not.toContain(SELF);
91
+ });
92
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `Unifi.WifiBroadcast`'s `matches` AND `driftOf` (B6) — built on the ONE field list below, the
3
+ * same `fieldDrift`-is-the-only-comparison shape `dns-policy-drift.ts`'s header explains
4
+ * (MEDIUM-4): `matches(attrs, props) === (driftOf(live, props).length === 0)` is true BY
5
+ * CONSTRUCTION, never two hand-kept-in-sync comparisons.
6
+ *
7
+ * ★ THREE FIELDS REUSE `wifi-broadcast-form.ts`'s OWN NORMALIZERS ON THE `declared` SIDE TOO, not
8
+ * just the `live` side — `firewall-zone-form.ts`'s `networkIds` gives the same reason: comparing
9
+ * a freshly-normalized live value against an un-normalized declared value would report spurious
10
+ * drift the moment a committed declaration's array happens to be in a different (still valid)
11
+ * order than what `declareWifiBroadcast` would itself produce today.
12
+ */
13
+ import type * as wifiBroadcasts from '@distilled.cloud/unifi-network/wifi_broadcasts';
14
+ import { makeDriftOf } from './drift.ts';
15
+ import {
16
+ type WifiBroadcastAttributes,
17
+ type WifiBroadcastProps,
18
+ attributesOf,
19
+ normalizeDeviceFilter,
20
+ normalizeFrequencies,
21
+ normalizeHotspot,
22
+ normalizeRef,
23
+ normalizeSecurityConfiguration,
24
+ } from './wifi-broadcast-form.ts';
25
+
26
+ const fieldDrift = makeDriftOf<WifiBroadcastAttributes, WifiBroadcastProps>([
27
+ { field: 'name', live: (a) => a.name, declared: (p) => p.name },
28
+ { field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
29
+ { field: 'type', live: (a) => a.type, declared: (p) => p.type },
30
+ {
31
+ // IMPORTANT-1: `a.network` is already rebuilt field-by-field inside `attributesOf`; the
32
+ // declared side gets the same treatment so a hand-written declaration carrying a stray key
33
+ // cannot report phantom drift against a live side that never could.
34
+ field: 'network',
35
+ live: (a) => a.network,
36
+ declared: (p) => normalizeRef(p.network),
37
+ },
38
+ {
39
+ field: 'broadcastingDeviceFilter',
40
+ live: (a) => a.broadcastingDeviceFilter,
41
+ declared: (p) => normalizeDeviceFilter(p.broadcastingDeviceFilter),
42
+ },
43
+ {
44
+ field: 'broadcastingFrequenciesGHz',
45
+ live: (a) => a.broadcastingFrequenciesGHz,
46
+ declared: (p) => normalizeFrequencies(p.broadcastingFrequenciesGHz),
47
+ },
48
+ {
49
+ field: 'hotspotConfiguration',
50
+ live: (a) => a.hotspotConfiguration,
51
+ declared: (p) => normalizeHotspot(p.hotspotConfiguration),
52
+ },
53
+ {
54
+ field: 'securityConfiguration',
55
+ live: (a) => a.securityConfiguration,
56
+ declared: (p) => normalizeSecurityConfiguration(p.securityConfiguration),
57
+ },
58
+ ]);
59
+
60
+ export const matches = (attributes: WifiBroadcastAttributes, props: WifiBroadcastProps): boolean =>
61
+ fieldDrift(attributes, props).length === 0;
62
+
63
+ /**
64
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
65
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
66
+ * `attributesOf` itself just to call this.
67
+ */
68
+ export const driftOf = (live: wifiBroadcasts.WifiBroadcastOverview, props: WifiBroadcastProps) =>
69
+ fieldDrift(attributesOf(live, props), props);