@homeflare/alchemy 0.39.0 → 0.41.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 (279) hide show
  1. package/README.md +10 -8
  2. package/dist/caddy/index.js.map +2 -2
  3. package/dist/ceph/auth-entity.d.ts +34 -0
  4. package/dist/ceph/auth-entity.d.ts.map +1 -0
  5. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  6. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  7. package/dist/ceph/ceph-argv.d.ts +19 -0
  8. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  9. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  10. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  12. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  14. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  15. package/dist/ceph/ceph-config-options.d.ts +23 -0
  16. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  18. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  19. package/dist/ceph/ceph-transport.d.ts +37 -0
  20. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  22. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  23. package/dist/ceph/index.d.ts +13 -0
  24. package/dist/ceph/index.d.ts.map +1 -0
  25. package/dist/ceph/index.js +1610 -0
  26. package/dist/ceph/index.js.map +38 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/launchd/fake-runner.d.ts.map +1 -1
  30. package/dist/launchd/index.d.ts +1 -1
  31. package/dist/launchd/index.d.ts.map +1 -1
  32. package/dist/launchd/index.js +7 -1
  33. package/dist/launchd/index.js.map +5 -5
  34. package/dist/launchd/local-runner.d.ts.map +1 -1
  35. package/dist/launchd/runner.d.ts +21 -0
  36. package/dist/launchd/runner.d.ts.map +1 -1
  37. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  38. package/dist/linux/directory-lifecycle.d.ts.map +1 -1
  39. package/dist/linux/fake-linux-host.d.ts.map +1 -1
  40. package/dist/linux/index.d.ts +1 -1
  41. package/dist/linux/index.d.ts.map +1 -1
  42. package/dist/linux/index.js +16 -4
  43. package/dist/linux/index.js.map +7 -7
  44. package/dist/linux/ssh-runner.d.ts.map +1 -1
  45. package/dist/linux/sudo-runner.d.ts.map +1 -1
  46. package/dist/opnsense/alias-form.d.ts +14 -1
  47. package/dist/opnsense/alias-form.d.ts.map +1 -1
  48. package/dist/opnsense/alias.d.ts +1 -1
  49. package/dist/opnsense/alias.d.ts.map +1 -1
  50. package/dist/opnsense/group-form.d.ts +8 -2
  51. package/dist/opnsense/group-form.d.ts.map +1 -1
  52. package/dist/opnsense/group.d.ts +1 -1
  53. package/dist/opnsense/group.d.ts.map +1 -1
  54. package/dist/opnsense/index.js +15 -7
  55. package/dist/opnsense/index.js.map +7 -7
  56. package/dist/opnsense/wire.d.ts +42 -12
  57. package/dist/opnsense/wire.d.ts.map +1 -1
  58. package/dist/proxmox/index.js +62 -39
  59. package/dist/proxmox/index.js.map +8 -7
  60. package/dist/proxmox/qemu-volume.d.ts +12 -2
  61. package/dist/proxmox/qemu-volume.d.ts.map +1 -1
  62. package/dist/proxmox/storage-form.d.ts +28 -3
  63. package/dist/proxmox/storage-form.d.ts.map +1 -1
  64. package/dist/proxmox/storage-plugin-options.d.ts +2 -0
  65. package/dist/proxmox/storage-plugin-options.d.ts.map +1 -0
  66. package/dist/proxmox/storage-wire.d.ts +28 -1
  67. package/dist/proxmox/storage-wire.d.ts.map +1 -1
  68. package/dist/proxmox/storage.d.ts +6 -1
  69. package/dist/proxmox/storage.d.ts.map +1 -1
  70. package/dist/release/index.d.ts +1 -1
  71. package/dist/release/index.d.ts.map +1 -1
  72. package/dist/release/index.js.map +2 -2
  73. package/dist/talos/credentials-write.d.ts +46 -0
  74. package/dist/talos/credentials-write.d.ts.map +1 -0
  75. package/dist/talos/credentials.d.ts +51 -9
  76. package/dist/talos/credentials.d.ts.map +1 -1
  77. package/dist/talos/fake-process.d.ts +56 -0
  78. package/dist/talos/fake-process.d.ts.map +1 -0
  79. package/dist/talos/index.d.ts +19 -4
  80. package/dist/talos/index.d.ts.map +1 -1
  81. package/dist/talos/index.js +357 -173
  82. package/dist/talos/index.js.map +16 -10
  83. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  84. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  85. package/dist/talos/kubeconfig.d.ts +41 -23
  86. package/dist/talos/kubeconfig.d.ts.map +1 -1
  87. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  88. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  89. package/dist/talos/machine-config-poll.d.ts +43 -0
  90. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  91. package/dist/talos/machine-config-read.d.ts +49 -0
  92. package/dist/talos/machine-config-read.d.ts.map +1 -0
  93. package/dist/talos/resource.d.ts +18 -0
  94. package/dist/talos/resource.d.ts.map +1 -1
  95. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  96. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  97. package/dist/talos/talos-bootstrap.d.ts +87 -0
  98. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  99. package/dist/talos/talos-cluster-health.d.ts +43 -1
  100. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  101. package/dist/talos/talos-errors.d.ts +68 -0
  102. package/dist/talos/talos-errors.d.ts.map +1 -0
  103. package/dist/talos/talos-machine-config.d.ts +51 -15
  104. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  105. package/dist/talos/talosctl.d.ts +7 -0
  106. package/dist/talos/talosctl.d.ts.map +1 -1
  107. package/dist/talos/values.d.ts +23 -3
  108. package/dist/talos/values.d.ts.map +1 -1
  109. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  110. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  111. package/dist/unifi/acl-rule-form.d.ts +90 -0
  112. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  113. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  114. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  115. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  116. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  117. package/dist/unifi/acl-rule.d.ts +49 -0
  118. package/dist/unifi/acl-rule.d.ts.map +1 -0
  119. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  120. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  121. package/dist/unifi/dns-policy-form.d.ts +97 -0
  122. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  123. package/dist/unifi/dns-policy.d.ts +55 -0
  124. package/dist/unifi/dns-policy.d.ts.map +1 -0
  125. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  126. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  127. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  128. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  129. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  130. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  131. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  132. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  133. package/dist/unifi/firewall-policy.d.ts +57 -0
  134. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  135. package/dist/unifi/index.d.ts +7 -0
  136. package/dist/unifi/index.d.ts.map +1 -1
  137. package/dist/unifi/index.js +588 -3
  138. package/dist/unifi/index.js.map +24 -4
  139. package/dist/unifi/paginate.d.ts +0 -12
  140. package/dist/unifi/paginate.d.ts.map +1 -1
  141. package/dist/unifi/providers.d.ts +1 -1
  142. package/dist/unifi/providers.d.ts.map +1 -1
  143. package/dist/unifi/scan-source.d.ts +11 -0
  144. package/dist/unifi/scan-source.d.ts.map +1 -0
  145. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  146. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  147. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  148. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  149. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  150. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  151. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  152. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  153. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  154. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  155. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  156. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  157. package/docs/distilled-interim.md +7 -0
  158. package/docs/launchd.md +5 -3
  159. package/docs/provider-standard.md +5 -1
  160. package/docs/talos-argocd-dogfood.md +160 -0
  161. package/docs/unifi-acl-rule.md +122 -0
  162. package/docs/unifi-dns-policy.md +75 -0
  163. package/docs/unifi-firewall-policy.md +109 -0
  164. package/docs/unifi-traffic-matching-list.md +82 -0
  165. package/docs/unifi-wifi.md +120 -0
  166. package/docs/unifi.md +10 -5
  167. package/package.json +7 -3
  168. package/src/ceph/auth-entity.ts +74 -0
  169. package/src/ceph/ceph-argv-config.ts +78 -0
  170. package/src/ceph/ceph-argv.test.ts +226 -0
  171. package/src/ceph/ceph-argv.ts +189 -0
  172. package/src/ceph/ceph-auth-form.test.ts +103 -0
  173. package/src/ceph/ceph-auth-form.ts +107 -0
  174. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  175. package/src/ceph/ceph-auth-parse.ts +121 -0
  176. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  177. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  178. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  179. package/src/ceph/ceph-config-options.test.ts +44 -0
  180. package/src/ceph/ceph-config-options.ts +36 -0
  181. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  182. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  183. package/src/ceph/ceph-transport.test.ts +125 -0
  184. package/src/ceph/ceph-transport.ts +117 -0
  185. package/src/ceph/fake-ceph-dial.ts +55 -0
  186. package/src/ceph/index.ts +12 -0
  187. package/src/index.ts +1 -0
  188. package/src/launchd/fake-runner.ts +2 -0
  189. package/src/launchd/index.ts +8 -1
  190. package/src/launchd/local-runner.ts +16 -1
  191. package/src/launchd/runner.ts +22 -0
  192. package/src/launchd/sudo-runner.ts +3 -0
  193. package/src/linux/directory-lifecycle.ts +18 -3
  194. package/src/linux/directory-sudo.test.ts +59 -0
  195. package/src/linux/directory.test.ts +34 -8
  196. package/src/linux/fake-linux-host.ts +2 -0
  197. package/src/linux/index.ts +1 -0
  198. package/src/linux/ssh-runner.ts +4 -0
  199. package/src/linux/sudo-runner.ts +3 -0
  200. package/src/opnsense/alias-form.test.ts +24 -12
  201. package/src/opnsense/alias-form.ts +20 -7
  202. package/src/opnsense/alias.test.ts +13 -6
  203. package/src/opnsense/alias.ts +16 -10
  204. package/src/opnsense/group-form.test.ts +12 -8
  205. package/src/opnsense/group-form.ts +13 -4
  206. package/src/opnsense/group.test.ts +18 -7
  207. package/src/opnsense/group.ts +10 -3
  208. package/src/opnsense/wire.ts +57 -12
  209. package/src/proxmox/qemu-volume.test.ts +153 -4
  210. package/src/proxmox/qemu-volume.ts +71 -14
  211. package/src/proxmox/storage-form.ts +48 -8
  212. package/src/proxmox/storage-plugin-options.ts +45 -0
  213. package/src/proxmox/storage-update.test.ts +183 -0
  214. package/src/proxmox/storage-wire.ts +54 -7
  215. package/src/proxmox/storage.ts +18 -6
  216. package/src/release/index.ts +1 -0
  217. package/src/talos/credentials-write.test.ts +81 -0
  218. package/src/talos/credentials-write.ts +95 -0
  219. package/src/talos/credentials.test.ts +157 -0
  220. package/src/talos/credentials.ts +160 -90
  221. package/src/talos/fake-process.ts +94 -0
  222. package/src/talos/index.ts +39 -4
  223. package/src/talos/kubeconfig-attrs.ts +55 -0
  224. package/src/talos/kubeconfig.test.ts +256 -0
  225. package/src/talos/kubeconfig.ts +170 -90
  226. package/src/talos/machine-config-fixtures.ts +86 -0
  227. package/src/talos/machine-config-poll.test.ts +57 -0
  228. package/src/talos/machine-config-poll.ts +82 -0
  229. package/src/talos/machine-config-read.ts +118 -0
  230. package/src/talos/resource.ts +19 -0
  231. package/src/talos/secrets-gate.test.ts +63 -0
  232. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  233. package/src/talos/talos-bootstrap.test.ts +218 -0
  234. package/src/talos/talos-bootstrap.ts +151 -45
  235. package/src/talos/talos-cluster-health.test.ts +143 -0
  236. package/src/talos/talos-cluster-health.ts +91 -28
  237. package/src/talos/talos-errors.ts +85 -0
  238. package/src/talos/talos-machine-config.test.ts +189 -0
  239. package/src/talos/talos-machine-config.ts +102 -88
  240. package/src/talos/talosctl.ts +8 -0
  241. package/src/talos/values.test.ts +34 -6
  242. package/src/talos/values.ts +41 -7
  243. package/src/unifi/acl-rule-drift.ts +57 -0
  244. package/src/unifi/acl-rule-form.ts +135 -0
  245. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  246. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  247. package/src/unifi/acl-rule-ordering.ts +70 -0
  248. package/src/unifi/acl-rule.test.ts +183 -0
  249. package/src/unifi/acl-rule.ts +61 -0
  250. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  251. package/src/unifi/dns-policy-drift.ts +48 -0
  252. package/src/unifi/dns-policy-form.ts +142 -0
  253. package/src/unifi/dns-policy.test.ts +154 -0
  254. package/src/unifi/dns-policy.ts +67 -0
  255. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  256. package/src/unifi/firewall-policy-drift.ts +55 -0
  257. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  258. package/src/unifi/firewall-policy-form.ts +146 -0
  259. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  260. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  261. package/src/unifi/firewall-policy-ordering.ts +87 -0
  262. package/src/unifi/firewall-policy.test.ts +183 -0
  263. package/src/unifi/firewall-policy.ts +73 -0
  264. package/src/unifi/index.ts +63 -0
  265. package/src/unifi/paginate.test.ts +24 -0
  266. package/src/unifi/paginate.ts +15 -3
  267. package/src/unifi/providers.ts +19 -1
  268. package/src/unifi/scan-source.ts +64 -0
  269. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  270. package/src/unifi/traffic-matching-list-form.ts +77 -0
  271. package/src/unifi/traffic-matching-list.test.ts +202 -0
  272. package/src/unifi/traffic-matching-list.ts +78 -0
  273. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  274. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  275. package/src/unifi/wifi-broadcast-form.ts +211 -0
  276. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  277. package/src/unifi/wifi-broadcast.test.ts +155 -0
  278. package/src/unifi/wifi-broadcast.ts +90 -0
  279. package/src/unifi/write-op-reference.test.ts +8 -54
@@ -0,0 +1,134 @@
1
+ /**
2
+ * A3/T26 regression: the SDK's per-discriminator-typed filters (FirewallPolicy's
3
+ * `protocolFilter`/`macAddressFilter`, an ACL rule's `sourceFilter`/`destinationFilter`) must
4
+ * decode a wire value UNCHANGED — including a key this pinned spec doesn't yet declare.
5
+ *
6
+ * ⛔ WITHOUT THIS TEST, A REGEN THAT SWITCHES `scripts/generate.ts` BACK TO
7
+ * `unionStyle: "opaque-cases"` STAYS GREEN. That style emits `S.Unknown.pipe(T.UnionCases(...))`,
8
+ * whose decode (`@distilled.cloud/core` `protocol-http.ts`'s `mapKeys`) picks the best-matching
9
+ * case by key set and keeps ONLY that case's keys — silently dropping a future vendor field, and
10
+ * in a same-key-set tie (an all-null MAC endpoint vs. the IP endpoint below) picking the WRONG
11
+ * case's keys outright. `scripts/generate.ts` now uses Argo CD's `union:` callback instead — a
12
+ * bare `S.Unknown` with no case annotation — which this repo's own `fake-unifi.ts` proves against
13
+ * the real wire path (path assembly + JSON decode), the same way `errors-and-secrets.test.ts`
14
+ * proves A1/A2. This family has no `Unifi.FirewallPolicy`/`Unifi.AclRule` provider yet (Lane B),
15
+ * so the SDK's own `getAclRule`/`getFirewallPolicy` operations are called directly.
16
+ */
17
+ import { describe, expect, test } from 'bun:test';
18
+ import * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
19
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
20
+ import * as Effect from 'effect/Effect';
21
+ import { fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
22
+
23
+ const ACL_PATH = '/proxy/network/integration/v1/sites/site-1/acl-rules/acl-1';
24
+ const FW_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/fp-1';
25
+
26
+ // Fabricated fixture (RFC 5737 address block, RFC 2606-safe ids) — kit is PUBLIC (fake-unifi.ts).
27
+ const aclBody = () => ({
28
+ type: 'IPV4',
29
+ id: 'acl-1',
30
+ name: 'example-rule',
31
+ enabled: true,
32
+ action: 'BLOCK',
33
+ index: 0,
34
+ metadata: { origin: 'USER' },
35
+ // A future console version's field this pinned spec has never declared.
36
+ sourceFilter: {
37
+ type: 'IP_ADDRESSES_OR_SUBNETS',
38
+ ipAddressesOrSubnets: ['192.0.2.0/24'],
39
+ futureVendorField: 'unmodeled-by-this-spec-version',
40
+ },
41
+ // Every member null (a same-key-set tie with the IP case under key-set matching).
42
+ destinationFilter: { type: 'MAC_ADDRESSES', macAddresses: null, prefixLength: null },
43
+ });
44
+
45
+ describe('A3/T26 — ACL rule filters decode unchanged, including an unmodeled field', () => {
46
+ test('sourceFilter keeps a key this spec version does not declare', async () => {
47
+ const fake = fakeUnifi((method, url) =>
48
+ method === 'GET' && url.pathname === ACL_PATH
49
+ ? Response.json(aclBody())
50
+ : new Response('unexpected request', { status: 400 }),
51
+ );
52
+ const live = await Effect.runPromise(
53
+ aclRules
54
+ .getAclRule({ siteId: 'site-1', aclRuleId: 'acl-1' })
55
+ .pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
56
+ );
57
+ const source = live.sourceFilter as unknown as Record<string, unknown>;
58
+ expect(source.futureVendorField).toBe('unmodeled-by-this-spec-version');
59
+ expect(source.ipAddressesOrSubnets).toEqual(['192.0.2.0/24']);
60
+ });
61
+
62
+ test('destinationFilter is not silently reassigned to the tied IP case', async () => {
63
+ const fake = fakeUnifi(() => Response.json(aclBody()));
64
+ const live = await Effect.runPromise(
65
+ aclRules
66
+ .getAclRule({ siteId: 'site-1', aclRuleId: 'acl-1' })
67
+ .pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
68
+ );
69
+ const dest = live.destinationFilter as unknown as Record<string, unknown>;
70
+ expect(dest.type).toBe('MAC_ADDRESSES');
71
+ expect('macAddresses' in dest).toBe(true);
72
+ // The IP case's own keys (`ipAddressesOrSubnets`, `portFilter`, `networkIds`) never appear —
73
+ // the case-narrowing mapKeys path this SDK no longer uses would rewrite them in on a key-set tie.
74
+ expect('ipAddressesOrSubnets' in dest).toBe(false);
75
+ });
76
+ });
77
+
78
+ const fwBody = () => ({
79
+ id: 'fp-1',
80
+ name: 'example-policy',
81
+ enabled: true,
82
+ index: 1,
83
+ action: { type: 'ALLOW' },
84
+ source: {
85
+ zoneId: 'zone-src',
86
+ trafficFilter: {
87
+ type: 'MAC_ADDRESS',
88
+ macAddressFilter: {
89
+ macAddresses: ['02:00:00:00:00:01'],
90
+ futureVendorField: 'unmodeled-by-this-spec-version',
91
+ },
92
+ },
93
+ },
94
+ destination: { zoneId: 'zone-dst' },
95
+ ipProtocolScope: {
96
+ ipVersion: 'IPV6',
97
+ protocolFilter: {
98
+ type: 'NAMED_PROTOCOL',
99
+ protocol: { name: 'icmpv6', typenameFilter: 'ECHO_REQUEST' },
100
+ matchOpposite: false,
101
+ futureVendorField: 'unmodeled-by-this-spec-version',
102
+ },
103
+ },
104
+ metadata: { origin: 'USER' },
105
+ loggingEnabled: false,
106
+ });
107
+
108
+ describe('A3/T26 — FirewallPolicy filters decode unchanged, including an unmodeled field', () => {
109
+ test('macAddressFilter and protocolFilter both keep a key this spec version does not declare', async () => {
110
+ const fake = fakeUnifi((method, url) =>
111
+ method === 'GET' && url.pathname === FW_PATH
112
+ ? Response.json(fwBody())
113
+ : new Response('unexpected request', { status: 400 }),
114
+ );
115
+ const live = await Effect.runPromise(
116
+ firewall
117
+ .getFirewallPolicy({ siteId: 'site-1', firewallPolicyId: 'fp-1' })
118
+ .pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
119
+ );
120
+ const macFilter = live.source.trafficFilter?.macAddressFilter as unknown as Record<
121
+ string,
122
+ unknown
123
+ >;
124
+ expect(macFilter.futureVendorField).toBe('unmodeled-by-this-spec-version');
125
+ expect(macFilter.macAddresses).toEqual(['02:00:00:00:00:01']);
126
+
127
+ const protocolFilter = live.ipProtocolScope.protocolFilter as unknown as Record<
128
+ string,
129
+ unknown
130
+ >;
131
+ expect(protocolFilter.futureVendorField).toBe('unmodeled-by-this-spec-version');
132
+ expect((protocolFilter.protocol as Record<string, unknown>).name).toBe('icmpv6');
133
+ });
134
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `Unifi.DnsPolicy`'s `matches` AND `driftOf` (B6) — both built on the ONE field list below, the
3
+ * same `fieldDrift`-is-the-only-comparison shape `network-drift.ts`'s own header explains (MEDIUM-4,
4
+ * red team, 2026-09-26): `matches(attrs, props) === (driftOf(live, props).length === 0)` is true
5
+ * BY CONSTRUCTION, never two hand-kept-in-sync comparisons.
6
+ *
7
+ * ★ NO CUSTOM `equal` ANYWHERE. Every field is a scalar and `drift.ts`'s `defaultEqual`
8
+ * (`deepEqual` with `stripNullish: true`) already gives UniFi's `null`-for-unset-optional
9
+ * reality the same tolerance `network-drift.ts`/`firewall-zone-form.ts` rely on — see
10
+ * `dns-policy-form.ts`'s own header for why no field here needs a set/order normalizer.
11
+ */
12
+ import type * as dnsPolicies from '@distilled.cloud/unifi-network/dns_policies';
13
+ import { makeDriftOf } from './drift.ts';
14
+ import { type DnsPolicyAttributes, type DnsPolicyProps, attributesOf } from './dns-policy-form.ts';
15
+
16
+ const fieldDrift = makeDriftOf<DnsPolicyAttributes, DnsPolicyProps>([
17
+ { field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
18
+ { field: 'type', live: (a) => a.type, declared: (p) => p.type },
19
+ { field: 'domain', live: (a) => a.domain, declared: (p) => p.domain },
20
+ { field: 'ipv6Address', live: (a) => a.ipv6Address, declared: (p) => p.ipv6Address },
21
+ { field: 'ttlSeconds', live: (a) => a.ttlSeconds, declared: (p) => p.ttlSeconds },
22
+ { field: 'ipv4Address', live: (a) => a.ipv4Address, declared: (p) => p.ipv4Address },
23
+ { field: 'targetDomain', live: (a) => a.targetDomain, declared: (p) => p.targetDomain },
24
+ { field: 'ipAddress', live: (a) => a.ipAddress, declared: (p) => p.ipAddress },
25
+ {
26
+ field: 'mailServerDomain',
27
+ live: (a) => a.mailServerDomain,
28
+ declared: (p) => p.mailServerDomain,
29
+ },
30
+ { field: 'priority', live: (a) => a.priority, declared: (p) => p.priority },
31
+ { field: 'port', live: (a) => a.port, declared: (p) => p.port },
32
+ { field: 'protocol', live: (a) => a.protocol, declared: (p) => p.protocol },
33
+ { field: 'serverDomain', live: (a) => a.serverDomain, declared: (p) => p.serverDomain },
34
+ { field: 'service', live: (a) => a.service, declared: (p) => p.service },
35
+ { field: 'weight', live: (a) => a.weight, declared: (p) => p.weight },
36
+ { field: 'text', live: (a) => a.text, declared: (p) => p.text },
37
+ ]);
38
+
39
+ export const matches = (attributes: DnsPolicyAttributes, props: DnsPolicyProps): boolean =>
40
+ fieldDrift(attributes, props).length === 0;
41
+
42
+ /**
43
+ * Per-field live-vs-declared drift straight from one live read — the task's own API
44
+ * (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
45
+ * `attributesOf` itself just to call this.
46
+ */
47
+ export const driftOf = (live: dnsPolicies.DNSPolicy, props: DnsPolicyProps) =>
48
+ fieldDrift(attributesOf(live, props), props);
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `Unifi.DnsPolicy`'s wire shape — the declarable subset of `DNSPolicy`, and how a live read
3
+ * becomes both plan attributes and a rendered declaration.
4
+ *
5
+ * ★ ONE FLAT DTO FOR EVERY DNS RECORD `type` (`A_RECORD`/`AAAA_RECORD`/`CNAME_RECORD`/
6
+ * `FORWARD_DOMAIN`/`MX_RECORD`/`SRV_RECORD`/`TXT_RECORD` — the pinned spec's `DNS policy` schema
7
+ * DOES discriminate on `type`, via `discriminator.mapping` to 7 variant DTOs; see
8
+ * `docs/unifi-dns-policy.md`'s "One flat DTO" section, corrected 2026-09-26). Unlike
9
+ * `access_control_acl_rules.ts`'s `sourceFilter`/`destinationFilter` or `firewall.ts`'s policy
10
+ * filters, NONE of `dns_policies.ts`'s fields were widened to `unknown` by the converter (T10) —
11
+ * `scripts/convert.ts` flattens the 7 variants into one struct instead, because no two variants
12
+ * collide on a field name, so every optional field still decodes as a plain scalar and there is
13
+ * no discriminated-variant trap here to flag or defer past this PR.
14
+ *
15
+ * ⚠️ PROPS MIRROR `UpdateDnsPolicyRequest`, NOT `DNSPolicy` — same split `network-form.ts` and
16
+ * `firewall-zone-form.ts` both use. `id` and `metadata` are server-derived (no writable field
17
+ * accepts either), so they are attributes-only, reported but never compared or declared.
18
+ * Everything else in `DnsPolicyProps` is exactly what `updateDnsPolicy`/`createDnsPolicy` would
19
+ * accept, even though this family never calls either.
20
+ *
21
+ * ⛔ NO ARRAY FIELD ANYWHERE IN THIS SHAPE. Every field below is a scalar (`string`/`number`/
22
+ * `boolean`), so none of `network-form.ts`'s/`firewall-zone-form.ts`'s `sortedSet` normalizing
23
+ * is needed — `deepEqual`'s default `stripNullish` handling (`drift.ts`) is the whole story.
24
+ */
25
+ import type * as dnsPolicies from '@distilled.cloud/unifi-network/dns_policies';
26
+
27
+ /**
28
+ * ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE (`exactOptionalPropertyTypes` —
29
+ * see `network-form.ts`'s own header for why `declareDnsPolicy` below needs it: it assigns a
30
+ * live optional field straight through, so the target type must admit an explicit `undefined`,
31
+ * not just omission).
32
+ */
33
+ export interface DnsPolicyProps {
34
+ siteId: string;
35
+ dnsPolicyId: string;
36
+ enabled: boolean;
37
+ /** DNS record type: `A_RECORD`, `AAAA_RECORD`, `CNAME_RECORD`, `FORWARD_DOMAIN`, `MX_RECORD`,
38
+ * `SRV_RECORD` or `TXT_RECORD` (the discriminator's own mapping keys, see the header). */
39
+ type: string;
40
+ domain?: string | undefined;
41
+ ipv6Address?: string | undefined;
42
+ /** Time to live in seconds. */
43
+ ttlSeconds?: number | undefined;
44
+ ipv4Address?: string | undefined;
45
+ targetDomain?: string | undefined;
46
+ /** IP address of the DNS Server that the DNS query is forwarded to. */
47
+ ipAddress?: string | undefined;
48
+ mailServerDomain?: string | undefined;
49
+ /** Priority. A lower number is preferred. */
50
+ priority?: number | undefined;
51
+ port?: number | undefined;
52
+ /** Protocol used by the service. */
53
+ protocol?: string | undefined;
54
+ /** Domain of the server that is running the service. */
55
+ serverDomain?: string | undefined;
56
+ /** Service associated with this SRV record. */
57
+ service?: string | undefined;
58
+ /** Weight. A relative value applicable for records with the same priority. A lower number is
59
+ * preferred. */
60
+ weight?: number | undefined;
61
+ /** The text value associated with this TXT DNS record. Text can contain up to four
62
+ * 255-character strings. Lines containing commas must be enclosed in double quotes ("). */
63
+ text?: string | undefined;
64
+ }
65
+
66
+ export interface DnsPolicyAttributes {
67
+ siteId: string;
68
+ dnsPolicyId: string;
69
+ enabled: boolean;
70
+ type: string;
71
+ domain: string | undefined;
72
+ ipv6Address: string | undefined;
73
+ ttlSeconds: number | undefined;
74
+ ipv4Address: string | undefined;
75
+ targetDomain: string | undefined;
76
+ ipAddress: string | undefined;
77
+ mailServerDomain: string | undefined;
78
+ priority: number | undefined;
79
+ port: number | undefined;
80
+ protocol: string | undefined;
81
+ serverDomain: string | undefined;
82
+ service: string | undefined;
83
+ weight: number | undefined;
84
+ text: string | undefined;
85
+ /** `metadata.origin` (e.g. `USER`) — server-derived, never declared; see
86
+ * `firewall-zone-form.ts`'s own `metadataOrigin` for the same split. */
87
+ metadataOrigin: string;
88
+ }
89
+
90
+ export const attributesOf = (
91
+ live: dnsPolicies.DNSPolicy,
92
+ props: DnsPolicyProps,
93
+ ): DnsPolicyAttributes => ({
94
+ siteId: props.siteId,
95
+ dnsPolicyId: live.id,
96
+ enabled: live.enabled,
97
+ type: live.type,
98
+ domain: live.domain,
99
+ ipv6Address: live.ipv6Address,
100
+ ttlSeconds: live.ttlSeconds,
101
+ ipv4Address: live.ipv4Address,
102
+ targetDomain: live.targetDomain,
103
+ ipAddress: live.ipAddress,
104
+ mailServerDomain: live.mailServerDomain,
105
+ priority: live.priority,
106
+ port: live.port,
107
+ protocol: live.protocol,
108
+ serverDomain: live.serverDomain,
109
+ service: live.service,
110
+ weight: live.weight,
111
+ text: live.text,
112
+ metadataOrigin: live.metadata.origin,
113
+ });
114
+
115
+ /**
116
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
117
+ * needs so its plan is noop"). A later import script feeds `getDnsPolicy`'s own response straight
118
+ * in and writes the result into `alchemy.run.ts` as `dnsPolicy('<id>', declareDnsPolicy(live,
119
+ * siteId))` — no field is invented or defaulted here beyond what `attributesOf` already reports,
120
+ * so `matches(attributesOf(live, props), declareDnsPolicy(live, siteId))` is `true` by
121
+ * construction (`dns-policy.test.ts` proves it) for any live object this SDK can decode.
122
+ */
123
+ export const declareDnsPolicy = (live: dnsPolicies.DNSPolicy, siteId: string): DnsPolicyProps => ({
124
+ siteId,
125
+ dnsPolicyId: live.id,
126
+ enabled: live.enabled,
127
+ type: live.type,
128
+ domain: live.domain,
129
+ ipv6Address: live.ipv6Address,
130
+ ttlSeconds: live.ttlSeconds,
131
+ ipv4Address: live.ipv4Address,
132
+ targetDomain: live.targetDomain,
133
+ ipAddress: live.ipAddress,
134
+ mailServerDomain: live.mailServerDomain,
135
+ priority: live.priority,
136
+ port: live.port,
137
+ protocol: live.protocol,
138
+ serverDomain: live.serverDomain,
139
+ service: live.service,
140
+ weight: live.weight,
141
+ text: live.text,
142
+ });
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `Unifi.DnsPolicy`'s `spec` against a fake UniFi Network API — mirrors `network.test.ts`/
3
+ * `firewall-zone.test.ts`. `driftOf`/`matches` coverage lives here too (not split into its own
4
+ * file, unlike `network-drift.test.ts`): this family's field list is flat scalars only, so there
5
+ * is no unordered-array normalizer to exercise and the whole file stays well under the house cap.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import type * as dnsPolicies from '@distilled.cloud/unifi-network/dns_policies';
9
+ import * as Retry from '@distilled.cloud/unifi-network/Retry';
10
+ import * as Effect from 'effect/Effect';
11
+ import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
12
+ import { attributesOf } from './dns-policy-form.ts';
13
+ import { driftOf, matches } from './dns-policy-drift.ts';
14
+ import { type DnsPolicyProps, declareDnsPolicy, spec } from './dns-policy.ts';
15
+ import { unifiOperations } from './resource.ts';
16
+
17
+ const POLICY_PATH = '/proxy/network/integration/v1/sites/site-1/dns/policies/dns-1';
18
+
19
+ const liveDnsPolicy = (overrides: Partial<dnsPolicies.DNSPolicy> = {}): dnsPolicies.DNSPolicy => ({
20
+ enabled: true,
21
+ id: 'dns-1',
22
+ metadata: { origin: 'USER' },
23
+ type: 'A_RECORD',
24
+ domain: 'cameras.example.test',
25
+ ipv4Address: '192.0.2.10',
26
+ ttlSeconds: 300,
27
+ ...overrides,
28
+ });
29
+
30
+ const PROPS: DnsPolicyProps = {
31
+ siteId: 'site-1',
32
+ dnsPolicyId: 'dns-1',
33
+ enabled: true,
34
+ type: 'A_RECORD',
35
+ domain: 'cameras.example.test',
36
+ ipv4Address: '192.0.2.10',
37
+ ttlSeconds: 300,
38
+ };
39
+
40
+ describe('Unifi.DnsPolicy spec.fetchLive', () => {
41
+ test('a real GET response decodes into the typed live object', async () => {
42
+ const fake = fakeUnifi((method, url) =>
43
+ method === 'GET' && url.pathname === POLICY_PATH
44
+ ? Response.json(liveDnsPolicy())
45
+ : fakeFailure(400, 'unexpected request'),
46
+ );
47
+ const live = await Effect.runPromise(
48
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
49
+ );
50
+ expect(live?.id).toBe('dns-1');
51
+ expect(fake.seen).toEqual([{ method: 'GET', path: POLICY_PATH }]);
52
+ });
53
+
54
+ test('a genuine 404 folds to undefined -- never a false "unreadable" on anything else', async () => {
55
+ const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
56
+ const live = await Effect.runPromise(
57
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
58
+ );
59
+ expect(live).toBeUndefined();
60
+ });
61
+
62
+ test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
63
+ // ⚠️ `Retry.none`: the default policy retries a 500 indefinitely with backoff -- this test
64
+ // asserts the FAILURE, not the retry schedule. Regression for red-team Important finding 2:
65
+ // a blanket `Effect.orElseSucceed(() => undefined)` fold would pass every test above too.
66
+ const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
67
+ const failure = await Effect.runPromise(
68
+ Effect.flip(
69
+ spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
70
+ ),
71
+ );
72
+ expect(failure._tag).toBe('InternalServerError');
73
+ });
74
+ });
75
+
76
+ describe('declareDnsPolicy -- the declaration renderer', () => {
77
+ test('its output matches attributesOf(live) by construction, for any live object', () => {
78
+ // A record shape with a DIFFERENT set of fields present than `liveDnsPolicy`'s default (a
79
+ // TXT record has no `ipv4Address`/`ttlSeconds`, but DOES require its own `domain` --
80
+ // `IntegrationDnsTxtRecordDto` lists `domain` as required, unlike the A-record-only fields) --
81
+ // a fresh literal, not the `A_RECORD` fixture's overrides, so no field is merely omitted from
82
+ // an override that would otherwise still carry the base fixture's value.
83
+ const live: dnsPolicies.DNSPolicy = {
84
+ enabled: true,
85
+ id: 'dns-2',
86
+ metadata: { origin: 'USER' },
87
+ type: 'TXT_RECORD',
88
+ domain: 'spf.example.test',
89
+ text: '"v=spf1 -all"',
90
+ };
91
+ const declared = declareDnsPolicy(live, 'site-1');
92
+ const attrs = attributesOf(live, declared);
93
+ expect(matches(attrs, declared)).toBe(true);
94
+ expect(driftOf(live, declared)).toEqual([]);
95
+ });
96
+ });
97
+
98
+ describe('driftOf -- B6 field-level drift, straight from one live read', () => {
99
+ test('a drifted field reports its own live and declared values, by name', () => {
100
+ const live = liveDnsPolicy({ ttlSeconds: 60 });
101
+ expect(driftOf(live, PROPS)).toEqual([{ field: 'ttlSeconds', live: 60, declared: 300 }]);
102
+ });
103
+
104
+ test('a live `null` for an unset optional field is a noop, not a spurious update', () => {
105
+ // UniFi's JSON answers `null` for an unset optional (see `dns-policy-drift.ts`'s header);
106
+ // the double cast lands `null` at runtime past what the SDK's `T | undefined` types allow.
107
+ const live = { ...liveDnsPolicy(), ipv6Address: null } as unknown as dnsPolicies.DNSPolicy;
108
+ expect(matches(attributesOf(live, PROPS), PROPS)).toBe(true);
109
+ expect(driftOf(live, PROPS)).toEqual([]);
110
+ });
111
+ });
112
+
113
+ describe('Unifi.DnsPolicy write paths never reach the vendor API', () => {
114
+ const ops = unifiOperations(spec);
115
+
116
+ test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
117
+ const fake = fakeUnifi((method, url) =>
118
+ method === 'GET' && url.pathname === POLICY_PATH
119
+ ? Response.json(liveDnsPolicy())
120
+ : fakeFailure(400, 'unexpected request'),
121
+ );
122
+ const exit = await Effect.runPromiseExit(
123
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
124
+ );
125
+ expect(exit._tag).toBe('Success');
126
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
127
+ });
128
+
129
+ test('reconcile on drift refuses instead of PUTting a merged body', async () => {
130
+ const fake = fakeUnifi((method, url) =>
131
+ method === 'GET' && url.pathname === POLICY_PATH
132
+ ? Response.json(liveDnsPolicy({ ttlSeconds: 999 }))
133
+ : fakeFailure(400, 'unexpected request'),
134
+ );
135
+ const exit = await Effect.runPromiseExit(
136
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
137
+ );
138
+ expect(exit._tag).toBe('Failure');
139
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
140
+ });
141
+
142
+ test('delete always refuses -- no DELETE is ever sent', async () => {
143
+ const fake = fakeUnifi((method, url) =>
144
+ method === 'GET' && url.pathname === POLICY_PATH
145
+ ? Response.json(liveDnsPolicy())
146
+ : fakeFailure(400, 'unexpected request'),
147
+ );
148
+ const exit = await Effect.runPromiseExit(
149
+ ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
150
+ );
151
+ expect(exit._tag).toBe('Failure');
152
+ expect(fake.seen).toEqual([]);
153
+ });
154
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `Unifi.DnsPolicy` — one DNS policy on a UniFi Network site, READ-ONLY (`policy.ts`).
3
+ *
4
+ * ★ T11 GATE, CHECKED 2026-09-26, CORRECTED SAME DAY (red team, Important finding 1): `DNS
5
+ * Policies` is 5 operations, `Access Control (ACL Rules)` (`acl-rule.ts`) is 7 — 12 total.
6
+ * Diffed the pinned 10.4.57 spec against a 10.6.97 mirror (`beezly/unifi-apis`, diffing aid
7
+ * only, `docs/unifi-acl-rule.md` records the sha256 + mirror commit): all 12 full operation
8
+ * objects (parameters, request body, responses, unresolved `$ref`s included) are byte-identical
9
+ * across versions. ⚠️ The schema closure is 46, not 17 — a `$ref`-only walk misses every schema
10
+ * reachable only via a `discriminator.mapping` (both `DNS policy` and `ACL rule` discriminate on
11
+ * `type`); the 46-schema closure, `$ref` + `mapping` together, shares zero names with the 14
12
+ * schemas that DID change between the two versions (all in `Switching`, `Filtering`, or one
13
+ * `UniFi Devices` enum — none of it DNS- or ACL-shaped). Same reachability method
14
+ * `spec-version-provenance.md` (distilled package) used for Networks/FirewallZones, extended to
15
+ * follow `mapping` too; full breakdown in `docs/unifi-acl-rule.md`, not repeated per family.
16
+ */
17
+ import { Resource } from 'alchemy';
18
+ import { adopt } from 'alchemy/AdoptPolicy';
19
+ import * as Provider from 'alchemy/Provider';
20
+ import * as dnsPolicies from '@distilled.cloud/unifi-network/dns_policies';
21
+ import * as Effect from 'effect/Effect';
22
+ import { type DnsPolicyAttributes, type DnsPolicyProps, attributesOf } from './dns-policy-form.ts';
23
+ import { matches } from './dns-policy-drift.ts';
24
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
25
+ import { unifiHandlers } from './resource.ts';
26
+
27
+ export type { DnsPolicyAttributes, DnsPolicyProps } from './dns-policy-form.ts';
28
+ export { declareDnsPolicy } from './dns-policy-form.ts';
29
+ export { driftOf } from './dns-policy-drift.ts';
30
+
31
+ export interface UnifiDnsPolicy extends Resource<
32
+ 'Unifi.DnsPolicy',
33
+ DnsPolicyProps,
34
+ DnsPolicyAttributes,
35
+ never,
36
+ UnifiRequirements
37
+ > {}
38
+
39
+ export const UnifiDnsPolicy = Resource<UnifiDnsPolicy>('Unifi.DnsPolicy', {
40
+ defaultRemovalPolicy: 'retain',
41
+ });
42
+
43
+ /** `dnsPolicy('lan-forwarding', props)` — `adopt(true)` piped on by default (H5); see `network.ts`. */
44
+ export const dnsPolicy = (id: string, props: DnsPolicyProps) =>
45
+ UnifiDnsPolicy(id, props).pipe(adopt(true));
46
+
47
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
48
+ export const spec: UnifiSpec<
49
+ DnsPolicyProps,
50
+ dnsPolicies.DNSPolicy,
51
+ DnsPolicyAttributes,
52
+ dnsPolicies.GetDnsPolicyError
53
+ > = {
54
+ type: 'Unifi.DnsPolicy',
55
+ describe: (props) => `sites/${props.siteId}/dns/policies/${props.dnsPolicyId}`,
56
+ fetchLive: (props) =>
57
+ dnsPolicies
58
+ .getDnsPolicy({ siteId: props.siteId, dnsPolicyId: props.dnsPolicyId })
59
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
60
+ attributes: attributesOf,
61
+ matches,
62
+ };
63
+
64
+ export const handlers = unifiHandlers(spec);
65
+
66
+ export const UnifiDnsPolicyProvider = () =>
67
+ Provider.effect(UnifiDnsPolicy, Effect.succeed(UnifiDnsPolicy.Provider.of(handlers)));