@homeflare/alchemy 0.39.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 (207) hide show
  1. package/README.md +10 -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/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +15 -6
  29. package/dist/proxmox/index.js.map +3 -3
  30. package/dist/proxmox/qemu-volume.d.ts +12 -2
  31. package/dist/proxmox/qemu-volume.d.ts.map +1 -1
  32. package/dist/talos/credentials-write.d.ts +46 -0
  33. package/dist/talos/credentials-write.d.ts.map +1 -0
  34. package/dist/talos/credentials.d.ts +51 -9
  35. package/dist/talos/credentials.d.ts.map +1 -1
  36. package/dist/talos/fake-process.d.ts +56 -0
  37. package/dist/talos/fake-process.d.ts.map +1 -0
  38. package/dist/talos/index.d.ts +19 -4
  39. package/dist/talos/index.d.ts.map +1 -1
  40. package/dist/talos/index.js +357 -173
  41. package/dist/talos/index.js.map +16 -10
  42. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  43. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  44. package/dist/talos/kubeconfig.d.ts +41 -23
  45. package/dist/talos/kubeconfig.d.ts.map +1 -1
  46. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  47. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  48. package/dist/talos/machine-config-poll.d.ts +43 -0
  49. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  50. package/dist/talos/machine-config-read.d.ts +49 -0
  51. package/dist/talos/machine-config-read.d.ts.map +1 -0
  52. package/dist/talos/resource.d.ts +18 -0
  53. package/dist/talos/resource.d.ts.map +1 -1
  54. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  55. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  56. package/dist/talos/talos-bootstrap.d.ts +87 -0
  57. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  58. package/dist/talos/talos-cluster-health.d.ts +43 -1
  59. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  60. package/dist/talos/talos-errors.d.ts +68 -0
  61. package/dist/talos/talos-errors.d.ts.map +1 -0
  62. package/dist/talos/talos-machine-config.d.ts +51 -15
  63. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  64. package/dist/talos/talosctl.d.ts +7 -0
  65. package/dist/talos/talosctl.d.ts.map +1 -1
  66. package/dist/talos/values.d.ts +23 -3
  67. package/dist/talos/values.d.ts.map +1 -1
  68. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  69. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  70. package/dist/unifi/acl-rule-form.d.ts +90 -0
  71. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  72. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  73. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  74. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  75. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  76. package/dist/unifi/acl-rule.d.ts +49 -0
  77. package/dist/unifi/acl-rule.d.ts.map +1 -0
  78. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  79. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  80. package/dist/unifi/dns-policy-form.d.ts +97 -0
  81. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  82. package/dist/unifi/dns-policy.d.ts +55 -0
  83. package/dist/unifi/dns-policy.d.ts.map +1 -0
  84. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  85. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  86. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  87. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  89. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  90. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  91. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  92. package/dist/unifi/firewall-policy.d.ts +57 -0
  93. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  94. package/dist/unifi/index.d.ts +7 -0
  95. package/dist/unifi/index.d.ts.map +1 -1
  96. package/dist/unifi/index.js +588 -3
  97. package/dist/unifi/index.js.map +24 -4
  98. package/dist/unifi/paginate.d.ts +0 -12
  99. package/dist/unifi/paginate.d.ts.map +1 -1
  100. package/dist/unifi/providers.d.ts +1 -1
  101. package/dist/unifi/providers.d.ts.map +1 -1
  102. package/dist/unifi/scan-source.d.ts +11 -0
  103. package/dist/unifi/scan-source.d.ts.map +1 -0
  104. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  105. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  106. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  107. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  108. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  109. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  110. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  111. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  112. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  113. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  114. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  115. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  116. package/docs/unifi-acl-rule.md +122 -0
  117. package/docs/unifi-dns-policy.md +75 -0
  118. package/docs/unifi-firewall-policy.md +109 -0
  119. package/docs/unifi-traffic-matching-list.md +82 -0
  120. package/docs/unifi-wifi.md +120 -0
  121. package/docs/unifi.md +10 -5
  122. package/package.json +6 -2
  123. package/src/ceph/auth-entity.ts +74 -0
  124. package/src/ceph/ceph-argv-config.ts +78 -0
  125. package/src/ceph/ceph-argv.test.ts +226 -0
  126. package/src/ceph/ceph-argv.ts +189 -0
  127. package/src/ceph/ceph-auth-form.test.ts +103 -0
  128. package/src/ceph/ceph-auth-form.ts +107 -0
  129. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  130. package/src/ceph/ceph-auth-parse.ts +121 -0
  131. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  132. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  133. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  134. package/src/ceph/ceph-config-options.test.ts +44 -0
  135. package/src/ceph/ceph-config-options.ts +36 -0
  136. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  137. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  138. package/src/ceph/ceph-transport.test.ts +125 -0
  139. package/src/ceph/ceph-transport.ts +117 -0
  140. package/src/ceph/fake-ceph-dial.ts +55 -0
  141. package/src/ceph/index.ts +12 -0
  142. package/src/index.ts +1 -0
  143. package/src/proxmox/qemu-volume.test.ts +153 -4
  144. package/src/proxmox/qemu-volume.ts +71 -14
  145. package/src/talos/credentials-write.test.ts +81 -0
  146. package/src/talos/credentials-write.ts +95 -0
  147. package/src/talos/credentials.test.ts +157 -0
  148. package/src/talos/credentials.ts +160 -90
  149. package/src/talos/fake-process.ts +94 -0
  150. package/src/talos/index.ts +39 -4
  151. package/src/talos/kubeconfig-attrs.ts +55 -0
  152. package/src/talos/kubeconfig.test.ts +256 -0
  153. package/src/talos/kubeconfig.ts +170 -90
  154. package/src/talos/machine-config-fixtures.ts +86 -0
  155. package/src/talos/machine-config-poll.test.ts +57 -0
  156. package/src/talos/machine-config-poll.ts +82 -0
  157. package/src/talos/machine-config-read.ts +118 -0
  158. package/src/talos/resource.ts +19 -0
  159. package/src/talos/secrets-gate.test.ts +63 -0
  160. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  161. package/src/talos/talos-bootstrap.test.ts +218 -0
  162. package/src/talos/talos-bootstrap.ts +151 -45
  163. package/src/talos/talos-cluster-health.test.ts +143 -0
  164. package/src/talos/talos-cluster-health.ts +91 -28
  165. package/src/talos/talos-errors.ts +85 -0
  166. package/src/talos/talos-machine-config.test.ts +189 -0
  167. package/src/talos/talos-machine-config.ts +102 -88
  168. package/src/talos/talosctl.ts +8 -0
  169. package/src/talos/values.test.ts +34 -6
  170. package/src/talos/values.ts +41 -7
  171. package/src/unifi/acl-rule-drift.ts +57 -0
  172. package/src/unifi/acl-rule-form.ts +135 -0
  173. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  174. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  175. package/src/unifi/acl-rule-ordering.ts +70 -0
  176. package/src/unifi/acl-rule.test.ts +183 -0
  177. package/src/unifi/acl-rule.ts +61 -0
  178. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  179. package/src/unifi/dns-policy-drift.ts +48 -0
  180. package/src/unifi/dns-policy-form.ts +142 -0
  181. package/src/unifi/dns-policy.test.ts +154 -0
  182. package/src/unifi/dns-policy.ts +67 -0
  183. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  184. package/src/unifi/firewall-policy-drift.ts +55 -0
  185. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  186. package/src/unifi/firewall-policy-form.ts +146 -0
  187. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  188. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  189. package/src/unifi/firewall-policy-ordering.ts +87 -0
  190. package/src/unifi/firewall-policy.test.ts +183 -0
  191. package/src/unifi/firewall-policy.ts +73 -0
  192. package/src/unifi/index.ts +63 -0
  193. package/src/unifi/paginate.test.ts +24 -0
  194. package/src/unifi/paginate.ts +15 -3
  195. package/src/unifi/providers.ts +19 -1
  196. package/src/unifi/scan-source.ts +64 -0
  197. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  198. package/src/unifi/traffic-matching-list-form.ts +77 -0
  199. package/src/unifi/traffic-matching-list.test.ts +202 -0
  200. package/src/unifi/traffic-matching-list.ts +78 -0
  201. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  202. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  203. package/src/unifi/wifi-broadcast-form.ts +211 -0
  204. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  205. package/src/unifi/wifi-broadcast.test.ts +155 -0
  206. package/src/unifi/wifi-broadcast.ts +90 -0
  207. package/src/unifi/write-op-reference.test.ts +8 -54
@@ -0,0 +1,51 @@
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 * as Provider from 'alchemy/Provider';
14
+ import * as trafficMatchingLists from '@distilled.cloud/unifi-network/traffic_matching_lists';
15
+ import * as Effect from 'effect/Effect';
16
+ import { type TrafficMatchingListAttributes, type TrafficMatchingListProps } from './traffic-matching-list-form.ts';
17
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
18
+ export type { TrafficMatchingListAttributes, TrafficMatchingListProps, } from './traffic-matching-list-form.ts';
19
+ export { declareTrafficMatchingList } from './traffic-matching-list-form.ts';
20
+ export { driftOf } from './traffic-matching-list-drift.ts';
21
+ export interface UnifiTrafficMatchingList extends Resource<'Unifi.TrafficMatchingList', TrafficMatchingListProps, TrafficMatchingListAttributes, never, UnifiRequirements> {
22
+ }
23
+ export declare const UnifiTrafficMatchingList: import("alchemy").ResourceClass<UnifiTrafficMatchingList>;
24
+ /** `trafficMatchingList('office-subnets', props)` — `adopt(true)` piped on by default (H5); see
25
+ * `network.ts`. */
26
+ export declare const trafficMatchingList: (id: string, props: TrafficMatchingListProps) => Effect.Effect<UnifiTrafficMatchingList, never, import("effect/unstable/http/HttpClient").HttpClient>;
27
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
28
+ export declare const spec: UnifiSpec<TrafficMatchingListProps, trafficMatchingLists.TrafficMatchingList, TrafficMatchingListAttributes, trafficMatchingLists.GetTrafficMatchingListError>;
29
+ export declare const handlers: {
30
+ list: () => Effect.Effect<never[], never, never>;
31
+ read: (args: {
32
+ olds: TrafficMatchingListProps;
33
+ output: TrafficMatchingListAttributes | undefined;
34
+ }) => Effect.Effect<TrafficMatchingListAttributes | undefined, trafficMatchingLists.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
35
+ diff: (args: {
36
+ news: import("alchemy").Input<TrafficMatchingListProps>;
37
+ output: TrafficMatchingListAttributes | undefined;
38
+ }) => Effect.Effect<{
39
+ readonly action: 'update';
40
+ } | {
41
+ readonly action: 'noop';
42
+ } | undefined, trafficMatchingLists.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
43
+ reconcile: (args: {
44
+ news: TrafficMatchingListProps;
45
+ }) => Effect.Effect<TrafficMatchingListAttributes, import("./policy.ts").UnifiWriteRefused | trafficMatchingLists.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
46
+ delete: (args: {
47
+ olds: TrafficMatchingListProps;
48
+ }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/unstable/http/HttpClient").HttpClient>;
49
+ };
50
+ export declare const UnifiTrafficMatchingListProvider: () => import("effect/Layer").Layer<Provider.Provider<UnifiTrafficMatchingList>, never, import("effect/unstable/http/HttpClient").HttpClient>;
51
+ //# sourceMappingURL=traffic-matching-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traffic-matching-list.d.ts","sourceRoot":"","sources":["../../src/unifi/traffic-matching-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,uDAAuD,CAAC;AAC9F,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE3D,MAAM,WAAW,wBAAyB,SAAQ,QAAQ,CACxD,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,wBAAwB,2DAGpC,CAAC;AAEF;oBACoB;AACpB,eAAO,MAAM,mBAAmB,OAAQ,MAAM,SAAS,wBAAwB,yGACxB,CAAC;AAExD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,wBAAwB,EACxB,oBAAoB,CAAC,mBAAmB,EACxC,6BAA6B,EAC7B,oBAAoB,CAAC,2BAA2B,CAcjD,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,gCAAgC,8IAI1C,CAAC"}
@@ -0,0 +1,22 @@
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 { type WifiBroadcastAttributes, type WifiBroadcastProps } from './wifi-broadcast-form.ts';
15
+ export declare const matches: (attributes: WifiBroadcastAttributes, props: WifiBroadcastProps) => boolean;
16
+ /**
17
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
18
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
19
+ * `attributesOf` itself just to call this.
20
+ */
21
+ export declare const driftOf: (live: wifiBroadcasts.WifiBroadcastOverview, props: WifiBroadcastProps) => import("./drift.ts").FieldDrift[];
22
+ //# sourceMappingURL=wifi-broadcast-drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wifi-broadcast-drift.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,KAAK,cAAc,MAAM,gDAAgD,CAAC;AAEtF,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAOxB,MAAM,0BAA0B,CAAC;AAoClC,eAAO,MAAM,OAAO,eAAgB,uBAAuB,SAAS,kBAAkB,KAAG,OAC7C,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,OAAO,SAAU,cAAc,CAAC,qBAAqB,SAAS,kBAAkB,sCAC/C,CAAC"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `Unifi.WifiBroadcast`'s wire shape — the declarable subset of `WifiBroadcastOverview`, and how
3
+ * a live read becomes both plan attributes and a rendered declaration.
4
+ *
5
+ * ⛔ T23, THE WHOLE REASON THIS SHAPE DIFFERS FROM EVERY OTHER FAMILY HERE. Every other family's
6
+ * `*-form.ts` (`network-form.ts`, `dns-policy-form.ts`, …) types its props against the vendor's
7
+ * CREATE/UPDATE request — the fullest declarable shape, even though this stack never calls it.
8
+ * `WifiBroadcastProps`/`WifiBroadcastAttributes` do NOT: `securityConfiguration` is typed
9
+ * `wifiBroadcasts.WifiSecurityConfigurationOverview` — the PAGE OVERVIEW's own type, `{type,
10
+ * presharedKeyNetworkIds}` — never `WifiSecurityConfigurationDetailObject` (the
11
+ * `getWifiBroadcastDetails`/create/update shape, which carries `passphrase` and
12
+ * `radiusConfiguration`). There is no DECLARED field on `WifiSecurityConfigurationOverview` for
13
+ * `attributesOf`/`declareWifiBroadcast` below to name a passphrase into. See `wifi-broadcast.ts`'s
14
+ * header for why `fetchLive` can only ever produce a `WifiBroadcastOverview` in the first place
15
+ * (no get-by-id call exists for it).
16
+ *
17
+ * ⛔ THE TYPE SYSTEM ALONE DOES NOT ENFORCE THIS (red team, IMPORTANT-1, 2026-09-26). `@distilled
18
+ * .cloud/core`'s wire decode does not strip a key a schema doesn't declare (this file's next
19
+ * header block); the TS types above describe what a CORRECTLY-shaped response looks like, not
20
+ * what the decoder actually returns at runtime, so a stray key nested inside `network`,
21
+ * `hotspotConfiguration`, `broadcastingDeviceFilter`, or an element of `presharedKeyNetworkIds`
22
+ * would ride along through a naive `{...live}`/pass-through copy exactly like the top-level
23
+ * `securityConfiguration` object does. `normalizeRef`/`normalizeHotspot`/`normalizeDeviceFilter`
24
+ * below rebuild EVERY nested object field-by-field for the same reason
25
+ * `normalizeSecurityConfiguration` does — enumeration, not the type annotation, is what actually
26
+ * stops it. `wifi-broadcast-secrets.test.ts` plants a sentinel inside each nested shape
27
+ * (including one `presharedKeyNetworkIds` element) to prove it, not just the top level.
28
+ *
29
+ * ⚠️ EVERY OTHER FIELD REUSES THE SDK'S OWN OVERVIEW TYPES DIRECTLY (`WifiNetworkReference`,
30
+ * `BroadcastingDeviceFilter`, `IntegrationWifiHotspotConfigurationOverviewDto`) rather than
31
+ * redeclaring them — same reasoning `network-form.ts` gives for reusing `NetworkDHCPGuarding`/
32
+ * `NetworkIPv6Configuration` verbatim: one shape to keep in sync with the SDK's own regen.
33
+ *
34
+ * ⛔ THREE FIELDS ARE SETS, NOT SEQUENCES, UNCONFIRMED AGAINST A LIVE CONSOLE (same caveat
35
+ * `network-form.ts`'s header gives for its own three): `broadcastingDeviceFilter.deviceIds`/
36
+ * `.deviceTagIds` and `securityConfiguration.presharedKeyNetworkIds` are membership sets by the
37
+ * vendor description's own wording ("Defines the custom scope of devices…"), not ordered
38
+ * sequences. `normalizeDeviceFilter`/`normalizeSecurityConfiguration`/`normalizeFrequencies`
39
+ * neutralize the same "spurious update, refused by a read-only reconcile" failure mode
40
+ * `network-form.ts`'s header describes, exactly like `firewall-zone-form.ts`'s `networkIds`.
41
+ */
42
+ import type * as wifiBroadcasts from '@distilled.cloud/unifi-network/wifi_broadcasts';
43
+ /**
44
+ * Rebuilds a reference field-by-field — never returns the object as received. IMPORTANT-1: this
45
+ * runs on EVERY reference this file handles (`network`, and each `presharedKeyNetworkIds`
46
+ * element) precisely because a reference is exactly the shape (`{network: WifiNetworkReference,
47
+ * passphrase}`) the details call uses to carry a PPSK passphrase (`IntegrationWifiPresharedKeyDto`,
48
+ * SDK `wifi_broadcasts.ts`); it is the most plausible place for a stray passphrase-shaped key to
49
+ * appear on the wire if the vendor ever leaked one onto the overview response.
50
+ */
51
+ export declare const normalizeRef: (ref: wifiBroadcasts.WifiNetworkReference | undefined) => wifiBroadcasts.WifiNetworkReference | undefined;
52
+ /**
53
+ * Rebuilds the hotspot configuration field-by-field. `IntegrationWifiHotspotConfigurationOverviewDto`
54
+ * declares only `type` (SDK `wifi_broadcasts.ts:743-753`) — a PASSPOINT/RADIUS secret would live on
55
+ * a wire key this type doesn't name, exactly the IMPORTANT-1 risk `normalizeRef` guards against.
56
+ */
57
+ export declare const normalizeHotspot: (value: wifiBroadcasts.IntegrationWifiHotspotConfigurationOverviewDto | undefined) => wifiBroadcasts.IntegrationWifiHotspotConfigurationOverviewDto | undefined;
58
+ /**
59
+ * ★ EXPORTED: `wifi-broadcast-drift.ts` reuses these exact normalizers so `matches`/`driftOf` can
60
+ * never quietly disagree about what counts as a change (MEDIUM-4, `network-drift.ts`'s header).
61
+ *
62
+ * ⛔ NO `{...value}` SPREAD (red team, IMPORTANT-1) — `BroadcastingDeviceFilter` declares exactly
63
+ * `type`/`deviceIds`/`deviceTagIds` (SDK `wifi_broadcasts.ts:125-129`); enumerating exactly those
64
+ * three is what stops a stray wire key here, the same reason `normalizeSecurityConfiguration`
65
+ * (below) never spreads its input.
66
+ */
67
+ export declare const normalizeDeviceFilter: (value: wifiBroadcasts.BroadcastingDeviceFilter | undefined) => wifiBroadcasts.BroadcastingDeviceFilter | undefined;
68
+ export declare const normalizeFrequencies: (value: readonly number[] | undefined) => number[] | undefined;
69
+ /**
70
+ * ⛔ NO `{...value}` SPREAD, UNLIKE EVERY OTHER NORMALIZER IN THIS DIRECTORY (`network-form.ts`'s
71
+ * `normalizeDhcpGuarding`/`normalizeIpv6Configuration` both spread). This is the one field in
72
+ * the whole `Unifi.*` tree with a spec-confirmed passphrase risk (T23): nothing in
73
+ * `@distilled.cloud/core`'s wire key-mapping strips an object key the schema doesn't declare —
74
+ * it only renames declared ones — so a stray extra key on the WIRE `securityConfiguration`
75
+ * object (a vendor bug, or a future console version) survives `getWifiBroadcastPage`'s own
76
+ * decode untyped, and `{...value}` would carry it straight through into `Attributes`/`Props`/the
77
+ * rendered declaration. Enumerating exactly the two fields
78
+ * `WifiSecurityConfigurationOverview` declares is what actually stops that — proved by
79
+ * `wifi-broadcast-secrets.test.ts`'s sentinel test, which injects exactly this kind of stray key.
80
+ */
81
+ export declare const normalizeSecurityConfiguration: (value: wifiBroadcasts.WifiSecurityConfigurationOverview) => wifiBroadcasts.WifiSecurityConfigurationOverview;
82
+ /**
83
+ * ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE — `exactOptionalPropertyTypes`; see
84
+ * `network-form.ts`'s own header. `attributesOf`/`declareWifiBroadcast` echo a live optional
85
+ * field structurally rather than defaulting it, so the target type must admit an explicit
86
+ * `undefined`.
87
+ */
88
+ export interface WifiBroadcastProps {
89
+ siteId: string;
90
+ wifiBroadcastId: string;
91
+ name: string;
92
+ enabled: boolean;
93
+ type: string;
94
+ network?: wifiBroadcasts.WifiNetworkReference | undefined;
95
+ broadcastingDeviceFilter?: wifiBroadcasts.BroadcastingDeviceFilter | undefined;
96
+ broadcastingFrequenciesGHz?: number[] | undefined;
97
+ hotspotConfiguration?: wifiBroadcasts.IntegrationWifiHotspotConfigurationOverviewDto | undefined;
98
+ /** ⛔ T23: the PAGE OVERVIEW's security shape ONLY — see this file's header. */
99
+ securityConfiguration: wifiBroadcasts.WifiSecurityConfigurationOverview;
100
+ }
101
+ export interface WifiBroadcastAttributes {
102
+ siteId: string;
103
+ wifiBroadcastId: string;
104
+ name: string;
105
+ enabled: boolean;
106
+ type: string;
107
+ network: wifiBroadcasts.WifiNetworkReference | undefined;
108
+ broadcastingDeviceFilter: wifiBroadcasts.BroadcastingDeviceFilter | undefined;
109
+ broadcastingFrequenciesGHz: number[] | undefined;
110
+ hotspotConfiguration: wifiBroadcasts.IntegrationWifiHotspotConfigurationOverviewDto | undefined;
111
+ securityConfiguration: wifiBroadcasts.WifiSecurityConfigurationOverview;
112
+ /** `metadata.origin` — server-derived, never declared; same split as every other family. */
113
+ metadataOrigin: string;
114
+ }
115
+ export declare const attributesOf: (live: wifiBroadcasts.WifiBroadcastOverview, props: WifiBroadcastProps) => WifiBroadcastAttributes;
116
+ /**
117
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
118
+ * needs so its plan is noop"). A later import script feeds `getWifiBroadcastPage`'s own per-row
119
+ * response straight in and writes the result into `alchemy.run.ts` as `wifiBroadcast('<id>',
120
+ * declareWifiBroadcast(live, siteId))` — no field is invented or defaulted here beyond what
121
+ * `attributesOf` already reports, so `matches(attributesOf(live, props), declareWifiBroadcast(live,
122
+ * siteId))` is `true` by construction (`wifi-broadcast.test.ts` proves it) for any live object
123
+ * this SDK can decode. Builds ONLY from fields `WifiBroadcastOverview` has — never a passphrase,
124
+ * which that type cannot express (see this file's header).
125
+ */
126
+ export declare const declareWifiBroadcast: (live: wifiBroadcasts.WifiBroadcastOverview, siteId: string) => WifiBroadcastProps;
127
+ //# sourceMappingURL=wifi-broadcast-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wifi-broadcast-form.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,KAAK,cAAc,MAAM,gDAAgD,CAAC;AAWtF;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,QAClB,cAAc,CAAC,oBAAoB,GAAG,SAAS,KACnD,cAAc,CAAC,oBAAoB,GAAG,SAGqD,CAAC;AAa/F;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,UACpB,cAAc,CAAC,8CAA8C,GAAG,SAAS,KAC/E,cAAc,CAAC,8CAA8C,GAAG,SACX,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,UACzB,cAAc,CAAC,wBAAwB,GAAG,SAAS,KACzD,cAAc,CAAC,wBAAwB,GAAG,SAStC,CAAC;AAER,eAAO,MAAM,oBAAoB,UAAW,SAAS,MAAM,EAAE,GAAG,SAAS,KAAG,MAAM,EAAE,GAAG,SAC/B,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,UAClC,cAAc,CAAC,iCAAiC,KACtD,cAAc,CAAC,iCAKhB,CAAC;AAEH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,cAAc,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAC1D,wBAAwB,CAAC,EAAE,cAAc,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAC/E,0BAA0B,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAClD,oBAAoB,CAAC,EAAE,cAAc,CAAC,8CAA8C,GAAG,SAAS,CAAC;IACjG,+EAA+E;IAC/E,qBAAqB,EAAE,cAAc,CAAC,iCAAiC,CAAC;CACzE;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,CAAC,oBAAoB,GAAG,SAAS,CAAC;IACzD,wBAAwB,EAAE,cAAc,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAC9E,0BAA0B,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD,oBAAoB,EAAE,cAAc,CAAC,8CAA8C,GAAG,SAAS,CAAC;IAChG,qBAAqB,EAAE,cAAc,CAAC,iCAAiC,CAAC;IACxE,4FAA4F;IAC5F,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,SACjB,cAAc,CAAC,qBAAqB,SACnC,kBAAkB,KACxB,uBAYD,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,SACzB,cAAc,CAAC,qBAAqB,UAClC,MAAM,KACb,kBAWD,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `Unifi.WifiBroadcast` — one WiFi network (SSID) broadcast on a UniFi Network site, READ-ONLY
3
+ * (`policy.ts`).
4
+ *
5
+ * ⛔ T23, THE REASON THIS FAMILY NEVER CALLS `getWifiBroadcastDetails`. That single-object GET
6
+ * returns `WifiBroadcastDetails`, whose `securityConfiguration`
7
+ * (`WifiSecurityConfigurationDetailObject`) carries the WPA passphrase AND every PPSK entry's
8
+ * passphrase — the pinned spec has NO `writeOnly` flag on either (verified: 4 `passphrase`
9
+ * props, 0 `writeOnly`; A2 already redacts them to `Redacted.Redacted<string>` at the SDK layer,
10
+ * `errors-and-secrets.test.ts`). This family goes further and never fetches that shape AT ALL:
11
+ * the only call below is `getWifiBroadcastPage`, which answers `WifiBroadcastOverview` instead —
12
+ * whose own `securityConfiguration` (`WifiSecurityConfigurationOverview`) is `{type,
13
+ * presharedKeyNetworkIds}`. No passphrase field exists in that type, so `WifiBroadcastProps`/
14
+ * `Attributes` (`wifi-broadcast-form.ts`) cannot carry one even by a future mistake.
15
+ *
16
+ * ⚠️ NO GET-BY-ID CALL EXISTS FOR THE OVERVIEW SHAPE — unlike every other family in this
17
+ * directory (`getNetworkDetails`, `getFirewallZone`, `getDnsPolicy`, `getAclRule`),
18
+ * `wifi_broadcasts.ts` only has `getWifiBroadcastPage` (list) and `getWifiBroadcastDetails`
19
+ * (single, passphrase-bearing — forbidden above). `fetchLive` below walks every page with B0b's
20
+ * `pageAll` and finds the one row matching `wifiBroadcastId` — the first real resource-level
21
+ * consumer of that pager (previously exercised only by `paginate.test.ts` in isolation).
22
+ *
23
+ * ⚠️ THE `NotFound` CATCH BELOW IS FOR THE WHOLE WALK, NOT JUST THE FIRST PAGE CALL (red team,
24
+ * MINOR-4, 2026-09-26 — corrected from an earlier claim that it only applied to the first call).
25
+ * `pageAll` makes its sequential page calls inside one Effect pipeline, and `catchTag` here wraps
26
+ * that entire pipeline; a 404 on ANY page of the walk — not only the first — folds to `undefined`
27
+ * and discards whatever rows the walk had already collected. In practice this means "the site
28
+ * vanished partway through listing its broadcasts" is indistinguishable from "the site was never
29
+ * there" and from "the target id just isn't among the collected rows" — none of these three cases
30
+ * has a separate vendor signal, and no other family's `fetchLive` distinguishes its own analogous
31
+ * cases either. Read-only reporting treats all three the same conservative way (absent, not an
32
+ * error); a write path would need to tell them apart.
33
+ */
34
+ import { Resource } from 'alchemy';
35
+ import * as Provider from 'alchemy/Provider';
36
+ import * as wifiBroadcasts from '@distilled.cloud/unifi-network/wifi_broadcasts';
37
+ import * as Effect from 'effect/Effect';
38
+ import { type WifiBroadcastAttributes, type WifiBroadcastProps } from './wifi-broadcast-form.ts';
39
+ import { type UnifiPaginationInconsistent } from './paginate.ts';
40
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
41
+ export type { WifiBroadcastAttributes, WifiBroadcastProps } from './wifi-broadcast-form.ts';
42
+ export { declareWifiBroadcast } from './wifi-broadcast-form.ts';
43
+ export { driftOf } from './wifi-broadcast-drift.ts';
44
+ export interface UnifiWifiBroadcast extends Resource<'Unifi.WifiBroadcast', WifiBroadcastProps, WifiBroadcastAttributes, never, UnifiRequirements> {
45
+ }
46
+ export declare const UnifiWifiBroadcast: import("alchemy").ResourceClass<UnifiWifiBroadcast>;
47
+ /** `wifiBroadcast('guest', props)` — `adopt(true)` piped on by default (H5); see `network.ts`. */
48
+ export declare const wifiBroadcast: (id: string, props: WifiBroadcastProps) => Effect.Effect<UnifiWifiBroadcast, never, import("effect/unstable/http/HttpClient").HttpClient>;
49
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
50
+ export declare const spec: UnifiSpec<WifiBroadcastProps, wifiBroadcasts.WifiBroadcastOverview, WifiBroadcastAttributes, wifiBroadcasts.GetWifiBroadcastPageError | UnifiPaginationInconsistent>;
51
+ export declare const handlers: {
52
+ list: () => Effect.Effect<never[], never, never>;
53
+ read: (args: {
54
+ olds: WifiBroadcastProps;
55
+ output: WifiBroadcastAttributes | undefined;
56
+ }) => Effect.Effect<WifiBroadcastAttributes | undefined, UnifiPaginationInconsistent | wifiBroadcasts.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
57
+ diff: (args: {
58
+ news: import("alchemy").Input<WifiBroadcastProps>;
59
+ output: WifiBroadcastAttributes | undefined;
60
+ }) => Effect.Effect<{
61
+ readonly action: 'update';
62
+ } | {
63
+ readonly action: 'noop';
64
+ } | undefined, UnifiPaginationInconsistent | wifiBroadcasts.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
65
+ reconcile: (args: {
66
+ news: WifiBroadcastProps;
67
+ }) => Effect.Effect<WifiBroadcastAttributes, UnifiPaginationInconsistent | import("./policy.ts").UnifiWriteRefused | wifiBroadcasts.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
68
+ delete: (args: {
69
+ olds: WifiBroadcastProps;
70
+ }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/unstable/http/HttpClient").HttpClient>;
71
+ };
72
+ export declare const UnifiWifiBroadcastProvider: () => import("effect/Layer").Layer<Provider.Provider<UnifiWifiBroadcast>, never, import("effect/unstable/http/HttpClient").HttpClient>;
73
+ //# sourceMappingURL=wifi-broadcast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wifi-broadcast.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,cAAc,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,2BAA2B,EAAW,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAQ,MAAM,SAAS,kBAAkB,mGAClB,CAAC;AAElD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,kBAAkB,EAClB,cAAc,CAAC,qBAAqB,EACpC,uBAAuB,EACvB,cAAc,CAAC,yBAAyB,GAAG,2BAA2B,CAWvE,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,0BAA0B,wIACwD,CAAC"}
@@ -0,0 +1,122 @@
1
+ # UniFi ACL Rules — `Unifi.AclRule` / `Unifi.AclRuleOrdering`
2
+
3
+ ⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
4
+ `Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
5
+ `GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
6
+ transport. Neither family here calls `createAclRule`, `updateAclRule`, `deleteAclRule` or
7
+ `updateAclRuleOrdering` anywhere — `write-op-reference.test.ts` proves it statically.
8
+
9
+ ## Two resources, because rule order is not a rule's own property (T5)
10
+
11
+ `Unifi.AclRule` is one user-defined ACL rule (`getAclRule`/`getAclRulePage`).
12
+ `Unifi.AclRuleOrdering` is the site's ONE ordered list of rule IDs (`getAclRuleOrdering`,
13
+ `GET /v1/sites/{siteId}/acl-rules/ordering`) — not one-per-rule and not one-per-zone-pair the way a
14
+ future `Unifi.FirewallPolicyOrdering` will be (`network.ts`'s header names that as a separate,
15
+ bigger PR), because ACL rule ordering is site-wide, not keyed by a source/destination zone pair.
16
+
17
+ ⛔ **`orderedAclRuleIds` IS NEVER RUN THROUGH `sortedSet`.** Every other multi-value field in this
18
+ directory that gets normalized (`firewall-zone-form.ts`'s `networkIds`, this family's own
19
+ `enforcingDeviceFilter.deviceIds`/`protocolFilter` below) is a SET with no documented order.
20
+ `orderedAclRuleIds` is the opposite: its position IS the value — `ACLRule.index`'s own field
21
+ comment says "Lower index has higher priority," and the whole point of `updateAclRuleOrdering` is
22
+ to change that priority by reordering the list. `acl-rule-ordering-form.ts`'s `matches`/`driftOf`
23
+ compare it with plain `deepEqual`, which — per `drift.ts`'s own header — sorts object keys but
24
+ never array elements, so leaving this one field un-normalized is what makes the resource actually
25
+ represent order.
26
+
27
+ ## `Unifi.AclRule`'s own fields
28
+
29
+ - **`sourceFilter`/`destinationFilter` stay opaque `unknown`**, on the SDK's own authority
30
+ (`access_control_acl_rules.ts`'s comment on both: "Shape varies by variant — widened by
31
+ scripts/convert.ts"). This is the same converter-flattening trap (T10) `network-form.ts` already
32
+ documents for `ipv4Configuration`; typing these per-discriminator (A3) is explicitly scoped OUT
33
+ of this PR. Compared with plain `deepEqual` — honest about not being decoded, not a guess at a
34
+ narrower shape. ⚠️ **Known gap until A3 (T15, red team, Minor finding 4)**: the vendor's own
35
+ filter variants (`IntegrationIpAclRuleNetworkEndpointFilterDto`'s `networkIds`,
36
+ `IntegrationIpAclRuleSubnetEndpointFilterDto`'s `ipAddressesOrSubnets`, both variants'
37
+ `portFilter`, `IntegrationMacAclRuleMacAddressEndpointFilterDto`'s `macAddresses`) are themselves
38
+ set-like arrays — `deepEqual` compares them order-sensitively, the same false-`update` risk
39
+ already fixed for `deviceIds`/`protocolFilter` below, just not fixable here without decoding the
40
+ filter first.
41
+ - **`enforcingDeviceFilter.deviceIds` and `protocolFilter` are SETS**, normalized with the same
42
+ `sortedSet` `firewall-zone-form.ts` uses for `networkIds`: neither field's own vendor description
43
+ ("IDs of the Switch-capable devices used to enforce the ACL rule," "Protocols this ACL rule will
44
+ be applied to") indicates order, and leaving either un-normalized would let a console-side
45
+ reorder plan a spurious `update` this read-only family's `reconcile` then refuses forever.
46
+ - **`index` is attributes-only, never declared.** `CreateAclRuleRequest`'s own comment on the
47
+ field: "This property is deprecated and has no effect. Use the dedicated ACL rule reordering
48
+ endpoint." Reported so a reader can see a rule's current position, but never compared —
49
+ `Unifi.AclRuleOrdering` above is where that position is actually declared.
50
+ - **`metadata.origin` is attributes-only**, same split `firewall-zone-form.ts`'s `metadataOrigin`
51
+ uses — `UserDefinedOrDerivedEntityMetadata` is server-derived, no writable field accepts it.
52
+
53
+ ## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26)
54
+
55
+ Same method `spec-version-provenance.md` (distilled package) used for Networks/FirewallZones,
56
+ run here for `DNS Policies` (5 ops) and `Access Control (ACL Rules)` (7 ops) — 12 operations total,
57
+ covering both families in this PR plus `Unifi.DnsPolicy` (`docs/unifi-dns-policy.md`).
58
+
59
+ - **Pin**: `~/.cache/homeflare/schemas/unifi/network_v10.4.57_openapi.json`, re-verified against
60
+ its own `SHA256SUMS` today (`3773947b…c9df59d`).
61
+ - **Mirror**: `raw.githubusercontent.com/beezly/unifi-apis/main/unifi-network/10.6.97.json`,
62
+ diffing aid only, never a spec of record (Q5/M9) — `info.version: "10.6.97"`,
63
+ `sha256: 44fd0e7f81603bb1279889a528a443d8f1a0102436e4a29675d8451adf32bd78`, mirror commit
64
+ `f979dd64ec0ea6008c2a698c278fe49a10e102c8` (Andrew Beresford, 2026-08-20, "Add UniFi API specs:
65
+ Network 10.6.97", GitHub-verified signature).
66
+ - **Operation count**: both versions carry exactly the same 12 operations for these two tags — zero
67
+ added, zero removed, in either direction.
68
+ - **Full raw operation objects** (parameters, request body, responses, unresolved `$ref`s
69
+ included — not just named schemas, the same reason `unifi.md`'s own check goes past a
70
+ schema-name diff): byte-identical for all 12, both versions.
71
+ - **14 component schemas differ somewhere in the whole document** (of 379/380 total) — the SAME 14
72
+ `unifi.md`'s own Networks/FirewallZones check found: 9 switch-stack/LAG schemas (`Switching`
73
+ tag), 4 generic `filter`-query-syntax schemas (`FilterExpression` and its three siblings,
74
+ matching the new `Filtering` tag), and one mDNS enum addition (`SHELLY`, `UniFi Devices` tag).
75
+ Full per-schema breakdown: the distilled package's own `docs/spec-version-provenance.md`.
76
+ - **Reachability, corrected 2026-09-26 (red team, Important finding 1)**: the original pass
77
+ resolved only plain `$ref`s reachable from the 12 operations' full parameter/body/response trees
78
+ — 17 schema names. ⛔ **That walk is wrong for any discriminated schema**: both `DNS policy`
79
+ (`type`: `A_RECORD`…`TXT_RECORD`, `FORWARD_DOMAIN` — 7-way) and `ACL rule` (`type`: `IPV4`, `MAC`
80
+ — 2-way) are `discriminator`-typed, and a variant reachable only via `discriminator.mapping`
81
+ carries no `$ref` from its parent at all. Re-walked following `$ref` **and** `mapping` targets:
82
+ the closure is **46 schema names**, identical set in both versions. The 29 names the `$ref`-only
83
+ walk missed: all 7 DNS record variant DTOs (`IntegrationDnsARecordDto`,
84
+ `IntegrationDnsAaaaRecordDto`, `IntegrationDnsCnameRecordDto`, `IntegrationDnsForwardDomainPolicyDto`,
85
+ `IntegrationDnsMxRecordDto`, `IntegrationDnsSrvRecordDto`, `IntegrationDnsTxtRecordDto`) plus their
86
+ 7 `*CreateUpdateDto` siblings; both ACL rule variant DTOs (`IntegrationIpAclRuleDto`,
87
+ `IntegrationMacAclRuleDto`) plus their 2 `*CreateUpdateDto` siblings; the IP/MAC endpoint-filter
88
+ DTOs (`IP ACL rule endpoint`, `MAC ACL rule endpoint`, `IntegrationIpAclRuleNetworkEndpointFilterDto`,
89
+ `IntegrationIpAclRulePortEndpointFilterDto`, `IntegrationIpAclRuleSubnetEndpointFilterDto`,
90
+ `IntegrationMacAclRuleMacAddressEndpointFilterDto`); `IntegrationAclRuleDevicesFilterDto`
91
+ (the device-filter's own `DEVICES` mapping target); and 2 more entity-metadata variants beyond the
92
+ five the original count already had. Re-diffed all 46 against the mirror with an order-independent
93
+ structural compare (not a byte/string compare — JSON key order differs between the pin and the
94
+ mirror file): **zero differ.** **Zero overlap** with the 14 schemas that changed elsewhere in the
95
+ document.
96
+
97
+ **Answer unchanged: neither `DnsPolicy` nor `AclRule`/`AclRuleOrdering` would decode differently
98
+ against 10.6.97** — the conclusion survives the correction; only the closure method and the schema
99
+ count were wrong. **This does not generalize past 10.6.97 or to any other tag — re-run this check,
100
+ following BOTH `$ref` and `discriminator.mapping`, before importing or declaring against `Clients`,
101
+ `WiFi Broadcasts`, `Traffic Matching Lists`, `Switching`, or `Firewall` (`FirewallPolicy`); a
102
+ `$ref`-only walk on `WiFi Broadcasts` finds 15 schemas and would miss
103
+ `IntegrationWifiMdnsProxyPredefinedServiceDto`, which DID change (10.6.97 added the `SHELLY` enum
104
+ member) and is reachable only through a `mapping`.**
105
+
106
+ ## The declaration renderers
107
+
108
+ `declareAclRule(live, siteId)` and `declareAclRuleOrdering(live, siteId)` are pure functions, same
109
+ contract as `declareNetwork`/`declareFirewallZone` (`docs/unifi.md`): given one live read, they
110
+ return the `Props` a declaration needs so its plan is a no-op. Neither performs the read itself.
111
+
112
+ ## Field-level drift
113
+
114
+ `acl-rule.ts`/`acl-rule-ordering.ts` each export a pure `driftOf(live, props)`
115
+ (`acl-rule-drift.ts`, `acl-rule-ordering-form.ts`), built on the same `drift.ts`'s `makeDriftOf`
116
+ framework as `Unifi.Network`/`Unifi.FirewallZone` — `matches` is exactly
117
+ `fieldDrift(...).length === 0`, never a second hand-written comparison (MEDIUM-4).
118
+
119
+ ## Vendor version
120
+
121
+ UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
122
+ `@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.2.0`.
@@ -0,0 +1,75 @@
1
+ # UniFi DNS Policies — `Unifi.DnsPolicy`
2
+
3
+ ⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
4
+ `Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
5
+ `GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
6
+ transport. This family calls no SDK write op anywhere — `write-op-reference.test.ts` proves it
7
+ statically, and `dns-policy.test.ts`'s own write-path tests prove `reconcile`/`destroy` never send
8
+ anything but the one `GET` this spec's `fetchLive` makes.
9
+
10
+ ## One flat DTO for every DNS record type
11
+
12
+ ⚠️ **Corrected 2026-09-26 (red team, Important finding 1): the vendor spec DOES discriminate.**
13
+ The pinned spec's `DNS policy` schema carries a 7-way `discriminator.mapping` on `type`
14
+ (`A_RECORD`, `AAAA_RECORD`, `CNAME_RECORD`, `FORWARD_DOMAIN`, `MX_RECORD`, `SRV_RECORD`,
15
+ `TXT_RECORD`), each mapping to its own variant DTO (`IntegrationDnsARecordDto`, …) that carries the
16
+ record-specific fields (`ipv4Address`, `ttlSeconds`, `text`, …) — the base `DNS policy` schema
17
+ itself has only `domain`/`enabled`/`id`/`metadata`/`type`. None of those variant DTOs are reachable
18
+ from the base schema by a plain `$ref` walk; they exist only behind the `mapping` (T11 gate below).
19
+
20
+ What IS still true: `scripts/convert.ts` FLATTENS all 7 variants into one struct — every variant
21
+ field becomes optional on the same `DNSPolicy`/`CreateDnsPolicyRequest`/`UpdateDnsPolicyRequest`
22
+ type, rather than widening any of them to `unknown` (T10). No two variants collide on a field name,
23
+ so the flattening loses no information. **That means `dns-policy-form.ts` still has no opaque-JSON
24
+ field to flag or defer, unlike `acl-rule-form.ts`'s `sourceFilter`/`destinationFilter`
25
+ (`docs/unifi-acl-rule.md`)** — that conclusion holds; only the "never discriminates" reason given
26
+ for it originally was wrong.
27
+
28
+ ⛔ **No array field exists anywhere in this shape**, so none of `network-form.ts`'s/
29
+ `firewall-zone-form.ts`'s/`acl-rule-form.ts`'s `sortedSet` set-normalizing applies here —
30
+ `drift.ts`'s default `stripNullish` tolerance for UniFi's `null`-for-unset-optional fields is the
31
+ whole story for `dns-policy-drift.ts`.
32
+
33
+ ## `id`/`metadata` are attributes-only, same split as every other family here
34
+
35
+ `DnsPolicyProps` mirrors `UpdateDnsPolicyRequest`, not `DNSPolicy`: `id` and `metadata.origin` are
36
+ server-derived (no writable field accepts either), reported in `DnsPolicyAttributes` but never
37
+ compared or declared — the same split `network-form.ts` and `firewall-zone-form.ts` document for
38
+ their own server-derived fields.
39
+
40
+ ## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26, corrected same day)
41
+
42
+ Run together with `Unifi.AclRule`'s own gate, since both tags were diffed in the same pass —
43
+ **full breakdown, pin/mirror checksums and mirror commit are in
44
+ [`docs/unifi-acl-rule.md`](./unifi-acl-rule.md), not repeated here.** Summary for this family: all
45
+ 5 `DNS Policies` operations are byte-identical between 10.4.57 and 10.6.97 (parameters, request
46
+ body, responses, unresolved `$ref`s included). ⚠️ The schema closure is **46 schemas, not 17** — a
47
+ plain `$ref` walk misses everything reachable only through `discriminator.mapping` (see "One flat
48
+ DTO" above), which for this tag is all 7 DNS record variant DTOs plus their 7 create/update
49
+ siblings. Re-run with `$ref` + `mapping` both followed, the full 46-schema closure (both tags
50
+ together) shares zero names with the 14 schemas that changed anywhere in the document between the
51
+ two versions — all 14 are `Switching`-, `Filtering`- or one `UniFi Devices` enum-shaped, none of it
52
+ DNS- or ACL-shaped. **Answer unchanged: `Unifi.DnsPolicy` would not decode differently against a
53
+ 10.6.97 console** — the conclusion survives the correction; only the closure method and count were
54
+ wrong. **Method note for the next family (B4+): always follow `discriminator.mapping`, not just
55
+ `$ref` — a `$ref`-only walk silently under-counts any tag with a discriminated schema.**
56
+
57
+ ## The declaration renderer
58
+
59
+ `declareDnsPolicy(live, siteId)` is a pure function, same contract as `declareNetwork`/
60
+ `declareFirewallZone` (`docs/unifi.md`): given one `getDnsPolicy` response, it returns the `Props`
61
+ a declaration needs so its plan is a no-op. A later import script reads every DNS policy on a site
62
+ and writes one `dnsPolicy('<id>', declareDnsPolicy(live, siteId))` row per result — nothing here
63
+ performs that write.
64
+
65
+ ## Field-level drift
66
+
67
+ `dns-policy.ts` exports a pure `driftOf(live, props)` (`dns-policy-drift.ts`), built on `drift.ts`'s
68
+ `makeDriftOf` framework — the same one `Unifi.Network`/`Unifi.FirewallZone`/`Unifi.AclRule` use.
69
+ `matches` is exactly `fieldDrift(...).length === 0`, never a second hand-written comparison
70
+ (MEDIUM-4, `network-drift.ts`'s own header).
71
+
72
+ ## Vendor version
73
+
74
+ UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
75
+ `@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.2.0`.
@@ -0,0 +1,109 @@
1
+ # UniFi Firewall Policies — `Unifi.FirewallPolicy` / `Unifi.FirewallPolicyOrdering`
2
+
3
+ ⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
4
+ `Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
5
+ `GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
6
+ transport. Neither family here calls `createFirewallPolicy`, `updateFirewallPolicy`,
7
+ `patchFirewallPolicy`, `deleteFirewallPolicy` or `updateFirewallPolicyOrdering` anywhere —
8
+ `write-op-reference.test.ts` proves it statically.
9
+
10
+ ## Two resources, because policy order is not a policy's own property (T5)
11
+
12
+ `Unifi.FirewallPolicy` is one policy (`getFirewallPolicy`/`getFirewallPolicies`).
13
+ `Unifi.FirewallPolicyOrdering` is the ordered `before`/`afterSystemDefined` policy-id lists for ONE
14
+ `(sourceFirewallZoneId, destinationFirewallZoneId)` zone pair (`getFirewallPolicyOrdering`,
15
+ `GET /v1/sites/{siteId}/firewall/policies/ordering?sourceFirewallZoneId=…&destinationFirewallZoneId=…`)
16
+ — unlike `Unifi.AclRuleOrdering` (`docs/unifi-acl-rule.md`), which is site-wide, because firewall
17
+ policies are scoped to a zone pair and the vendor keys ordering the same way. There is no "list
18
+ every zone pair" endpoint; a caller has to already know which pairs exist, typically by walking
19
+ `getFirewallPolicies` (T9 — see below) and grouping by each policy's `source.zoneId`/
20
+ `destination.zoneId`. Neither resource performs that discovery.
21
+
22
+ ⛔ **`beforeSystemDefined`/`afterSystemDefined` ARE NEVER RUN THROUGH `sortedSet`.** Both are
23
+ sequences — within each half, position is priority — the same "order IS the value" reasoning
24
+ `docs/unifi-acl-rule.md` gives for `orderedAclRuleIds`. `firewall-policy-ordering-form.ts` compares
25
+ each half independently with plain `deepEqual`, so a policy moving from one half to the other
26
+ reports as drift on BOTH fields rather than one combined reorder.
27
+
28
+ ## `Unifi.FirewallPolicy`'s own fields
29
+
30
+ - **`action`/`destination`/`ipProtocolScope`/`schedule`/`source` are compound, compared wholesale.**
31
+ Post-A3 (`8ee11f1`), these decode as real per-discriminator TS unions instead of `unknown`
32
+ (`discriminated-filter-decode.test.ts` proves the wire path keeps every key, including one this
33
+ pinned spec doesn't yet declare) — but this PR does not go on to build a leaf-by-leaf declarable
34
+ schema for all 12 filter variants underneath them. Each is compared as ONE value with plain
35
+ `deepEqual` (`stripNullish`), the same proportion `acl-rule-form.ts`'s `enforcingDeviceFilter`
36
+ gets (one level of structure, not exploded into scalars).
37
+ ⚠️ **Known gap (T15, same shape as `acl-rule-form.ts`'s own)**: several of the vendor's filter
38
+ variants nested inside these fields are themselves set-like arrays with no documented order
39
+ (`FirewallPolicyMACAddressFilter.macAddresses`, `FirewallPolicyNetworkFilter.networkIds`,
40
+ `FirewallPolicyApplicationFilter.applicationIds`, `FirewallPolicyIPAddressFilter.items`,
41
+ `FirewallPolicyRegionFilter.regions`, `FirewallPolicyVPNServerFilter.vpnServerIds`,
42
+ `FirewallSchedule.repeatOnDays`) — `deepEqual` compares them order-sensitively, so a console-side
43
+ reorder with no membership change would report a spurious drift this read-only family's
44
+ `reconcile` then refuses forever. Accepted, not fixed: normalizing 12 filter variants' own nested
45
+ arrays is bigger, separate modelling work, not a mechanical extension of A3.
46
+ - **`connectionStateFilter` is the one top-level Props array, and is a SET** — "matches all
47
+ connection states" if null describes membership, not a sequence (unlike the ordering resource's
48
+ own lists, where position IS the value). Normalized with the same `sortedSet` every other family
49
+ here uses for a set-shaped array.
50
+ - **`index` is attributes-only, never declared.** `CreateFirewallPolicyRequest` has no `index`
51
+ field — the live object's position among ALL policies on the site is reported, but its priority
52
+ relative to its own zone pair is `Unifi.FirewallPolicyOrdering`'s concern, same split
53
+ `acl-rule-form.ts`'s own `index` documents.
54
+ - **`metadata.origin` is attributes-only**, same split every family here uses.
55
+
56
+ ## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26)
57
+
58
+ Same method `docs/unifi-acl-rule.md` used, run here for the `Firewall` tag's 13 operations (5
59
+ already covered for `Unifi.FirewallZone` in `docs/unifi.md`; 8 new here: `getFirewallPolicies`,
60
+ `createFirewallPolicy`, `getFirewallPolicyOrdering`, `updateFirewallPolicyOrdering`,
61
+ `deleteFirewallPolicy`, `getFirewallPolicy`, `patchFirewallPolicy`, `updateFirewallPolicy`).
62
+
63
+ - **Pin**: `~/.cache/homeflare/schemas/unifi/network_v10.4.57_openapi.json`, re-verified against
64
+ its own `SHA256SUMS` today (`3773947b…c9df59d`).
65
+ - **Mirror**: `raw.githubusercontent.com/beezly/unifi-apis/main/unifi-network/10.6.97.json`,
66
+ diffing aid only, never a spec of record — re-fetched today, still `info.version: "10.6.97"`,
67
+ `sha256: 44fd0e7f81603bb1279889a528a443d8f1a0102436e4a29675d8451adf32bd78`, unchanged since mirror
68
+ commit `f979dd64ec0ea6008c2a698c278fe49a10e102c8` (Andrew Beresford, 2026-08-20, "Add UniFi API
69
+ specs: Network 10.6.97", GitHub-verified signature) — the same file `docs/unifi-acl-rule.md` cites.
70
+ - **Operation count**: both versions carry exactly the same 13 operations for `Firewall` — zero
71
+ added, zero removed, either direction.
72
+ - **Full raw operation objects** (parameters, request body, responses, unresolved `$ref`s included):
73
+ byte-identical for all 13, both versions.
74
+ - **Reachability** (`$ref` AND `discriminator.mapping`, per `docs/unifi-acl-rule.md`'s corrected
75
+ method): the closure is **106 schema names**, identical set in both versions. Every one of the 12
76
+ filter variants' own sub-schemas (`IntegrationFirewallPolicySourceMacAddressFilterDto`, all three
77
+ IP-protocol-scope Dto families, both action Dtos, etc.) is in it. **Zero overlap** with the 14
78
+ schemas that changed elsewhere in the document (the same 9 `Switching`, 4 `Filtering`, 1 mDNS
79
+ schemas `docs/unifi.md`/`docs/unifi-acl-rule.md` already found) — re-diffed all 106 with an
80
+ order-independent structural compare: **zero differ**.
81
+ - **Corroboration**: the A3 changeset's own broader re-check
82
+ (`.changeset/unifi-network-a3-typed-discriminator-filters.md`) independently walked all 25
83
+ FirewallPolicy/ACL rule/TrafficMatchingList operations together — a 139-schema closure, also
84
+ byte-identical between versions — against a mirror fetch the same day citing commit
85
+ `2457b337173c83599c583993210218b1b6c0f8e7` (that commit only touches `README.md`; the spec file
86
+ itself has been untouched, hence unchanged, since `f979dd64` above).
87
+
88
+ **Answer: neither `FirewallPolicy` nor `FirewallPolicyOrdering` would decode differently against
89
+ 10.6.97.** This does not generalize past 10.6.97 or to any other tag — re-run this check, following
90
+ BOTH `$ref` and `discriminator.mapping`, before importing or declaring against `Clients`,
91
+ `Switching`, or any tag not yet covered by an existing `docs/unifi-*.md`.
92
+
93
+ ## The declaration renderers
94
+
95
+ `declareFirewallPolicy(live, siteId)` and `declareFirewallPolicyOrdering(live, siteId, sourceZoneId,
96
+ destZoneId)` are pure functions, same contract as every other family's own (`docs/unifi.md`): given
97
+ one live read, they return the `Props` a declaration needs so its plan is a no-op.
98
+
99
+ ## Field-level drift
100
+
101
+ `firewall-policy.ts`/`firewall-policy-ordering.ts` each export a pure `driftOf(live, props)`
102
+ (`firewall-policy-drift.ts`, `firewall-policy-ordering-form.ts`), built on the same `drift.ts`'s
103
+ `makeDriftOf` framework as every other family — `matches` is exactly `fieldDrift(...).length === 0`,
104
+ never a second hand-written comparison (MEDIUM-4).
105
+
106
+ ## Vendor version
107
+
108
+ UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
109
+ `@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.3.0`.