@homeflare/alchemy 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -8
- package/dist/ceph/auth-entity.d.ts +34 -0
- package/dist/ceph/auth-entity.d.ts.map +1 -0
- package/dist/ceph/ceph-argv-config.d.ts +10 -0
- package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
- package/dist/ceph/ceph-argv.d.ts +19 -0
- package/dist/ceph/ceph-argv.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-form.d.ts +54 -0
- package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-parse.d.ts +56 -0
- package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
- package/dist/ceph/ceph-config-options.d.ts +23 -0
- package/dist/ceph/ceph-config-options.d.ts.map +1 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
- package/dist/ceph/ceph-transport.d.ts +37 -0
- package/dist/ceph/ceph-transport.d.ts.map +1 -0
- package/dist/ceph/fake-ceph-dial.d.ts +35 -0
- package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
- package/dist/ceph/index.d.ts +13 -0
- package/dist/ceph/index.d.ts.map +1 -0
- package/dist/ceph/index.js +1609 -0
- package/dist/ceph/index.js.map +38 -0
- package/dist/forgejo/index.js +4 -2
- package/dist/forgejo/index.js.map +3 -3
- package/dist/forgejo/team-member.d.ts +25 -0
- package/dist/forgejo/team-member.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +14 -3
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +574 -133
- package/dist/proxmox/index.js.map +33 -23
- package/dist/proxmox/qemu-form.d.ts +27 -18
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts +30 -0
- package/dist/proxmox/qemu-judge.d.ts.map +1 -0
- package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-net.d.ts +9 -0
- package/dist/proxmox/qemu-net.d.ts.map +1 -0
- package/dist/proxmox/qemu-props.d.ts +112 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -0
- package/dist/proxmox/qemu-volume.d.ts +27 -0
- package/dist/proxmox/qemu-volume.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +14 -27
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/storage-download-errors.d.ts +5 -0
- package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
- package/dist/proxmox/storage-download-identity.d.ts +27 -0
- package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/storage-download-props.d.ts +36 -0
- package/dist/proxmox/storage-download-props.d.ts.map +1 -0
- package/dist/proxmox/storage-download-task.d.ts +17 -0
- package/dist/proxmox/storage-download-task.d.ts.map +1 -0
- package/dist/proxmox/storage-download.d.ts +44 -0
- package/dist/proxmox/storage-download.d.ts.map +1 -0
- package/dist/talos/credentials-write.d.ts +46 -0
- package/dist/talos/credentials-write.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +51 -9
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/fake-process.d.ts +56 -0
- package/dist/talos/fake-process.d.ts.map +1 -0
- package/dist/talos/index.d.ts +19 -4
- package/dist/talos/index.d.ts.map +1 -1
- package/dist/talos/index.js +357 -173
- package/dist/talos/index.js.map +16 -10
- package/dist/talos/kubeconfig-attrs.d.ts +4 -0
- package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
- package/dist/talos/kubeconfig.d.ts +41 -23
- package/dist/talos/kubeconfig.d.ts.map +1 -1
- package/dist/talos/machine-config-fixtures.d.ts +60 -0
- package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
- package/dist/talos/machine-config-poll.d.ts +43 -0
- package/dist/talos/machine-config-poll.d.ts.map +1 -0
- package/dist/talos/machine-config-read.d.ts +49 -0
- package/dist/talos/machine-config-read.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +18 -0
- package/dist/talos/resource.d.ts.map +1 -1
- package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
- package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +87 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -1
- package/dist/talos/talos-cluster-health.d.ts +43 -1
- package/dist/talos/talos-cluster-health.d.ts.map +1 -1
- package/dist/talos/talos-errors.d.ts +68 -0
- package/dist/talos/talos-errors.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +51 -15
- package/dist/talos/talos-machine-config.d.ts.map +1 -1
- package/dist/talos/talosctl.d.ts +7 -0
- package/dist/talos/talosctl.d.ts.map +1 -1
- package/dist/talos/values.d.ts +23 -3
- package/dist/talos/values.d.ts.map +1 -1
- package/dist/telemetry/fake-collector.d.ts +36 -0
- package/dist/telemetry/fake-collector.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +223 -0
- package/dist/telemetry/index.js.map +12 -0
- package/dist/telemetry/layer.d.ts +86 -0
- package/dist/telemetry/layer.d.ts.map +1 -0
- package/dist/telemetry/redact-serialization.d.ts +21 -0
- package/dist/telemetry/redact-serialization.d.ts.map +1 -0
- package/dist/telemetry/redact.d.ts +46 -0
- package/dist/telemetry/redact.d.ts.map +1 -0
- package/dist/unifi/acl-rule-drift.d.ts +22 -0
- package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
- package/dist/unifi/acl-rule-form.d.ts +90 -0
- package/dist/unifi/acl-rule-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering.d.ts +54 -0
- package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
- package/dist/unifi/acl-rule.d.ts +49 -0
- package/dist/unifi/acl-rule.d.ts.map +1 -0
- package/dist/unifi/dns-policy-drift.d.ts +21 -0
- package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
- package/dist/unifi/dns-policy-form.d.ts +97 -0
- package/dist/unifi/dns-policy-form.d.ts.map +1 -0
- package/dist/unifi/dns-policy.d.ts +55 -0
- package/dist/unifi/dns-policy.d.ts.map +1 -0
- package/dist/unifi/drift.d.ts +27 -0
- package/dist/unifi/drift.d.ts.map +1 -0
- package/dist/unifi/fake-unifi.d.ts +5 -1
- package/dist/unifi/fake-unifi.d.ts.map +1 -1
- package/dist/unifi/firewall-policy-drift.d.ts +26 -0
- package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-form.d.ts +102 -0
- package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
- package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
- package/dist/unifi/firewall-policy.d.ts +57 -0
- package/dist/unifi/firewall-policy.d.ts.map +1 -0
- package/dist/unifi/firewall-zone-form.d.ts +6 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +1 -1
- package/dist/unifi/firewall-zone.d.ts.map +1 -1
- package/dist/unifi/index.d.ts +11 -3
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +725 -60
- package/dist/unifi/index.js.map +31 -9
- package/dist/unifi/network-drift.d.ts +26 -0
- package/dist/unifi/network-drift.d.ts.map +1 -0
- package/dist/unifi/network-form.d.ts +40 -7
- package/dist/unifi/network-form.d.ts.map +1 -1
- package/dist/unifi/network.d.ts +1 -0
- package/dist/unifi/network.d.ts.map +1 -1
- package/dist/unifi/paginate.d.ts +24 -0
- package/dist/unifi/paginate.d.ts.map +1 -0
- package/dist/unifi/providers.d.ts +1 -1
- package/dist/unifi/providers.d.ts.map +1 -1
- package/dist/unifi/resource.d.ts +36 -1
- package/dist/unifi/resource.d.ts.map +1 -1
- package/dist/unifi/scan-source.d.ts +11 -0
- package/dist/unifi/scan-source.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
- package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list.d.ts +51 -0
- package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
- package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast.d.ts +73 -0
- package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
- package/docs/telemetry-spike.md +94 -0
- package/docs/telemetry.md +109 -0
- package/docs/unifi-acl-rule.md +122 -0
- package/docs/unifi-dns-policy.md +75 -0
- package/docs/unifi-firewall-policy.md +109 -0
- package/docs/unifi-traffic-matching-list.md +82 -0
- package/docs/unifi-wifi.md +120 -0
- package/docs/unifi.md +64 -3
- package/package.json +10 -2
- package/src/ceph/auth-entity.ts +74 -0
- package/src/ceph/ceph-argv-config.ts +78 -0
- package/src/ceph/ceph-argv.test.ts +226 -0
- package/src/ceph/ceph-argv.ts +189 -0
- package/src/ceph/ceph-auth-form.test.ts +103 -0
- package/src/ceph/ceph-auth-form.ts +107 -0
- package/src/ceph/ceph-auth-parse.test.ts +155 -0
- package/src/ceph/ceph-auth-parse.ts +121 -0
- package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
- package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
- package/src/ceph/ceph-auth-reconcile.ts +119 -0
- package/src/ceph/ceph-config-options.test.ts +44 -0
- package/src/ceph/ceph-config-options.ts +36 -0
- package/src/ceph/ceph-key-leak-assert.ts +35 -0
- package/src/ceph/ceph-no-output-logging.test.ts +52 -0
- package/src/ceph/ceph-transport.test.ts +125 -0
- package/src/ceph/ceph-transport.ts +117 -0
- package/src/ceph/fake-ceph-dial.ts +55 -0
- package/src/ceph/index.ts +12 -0
- package/src/forgejo/team-member.test.ts +14 -0
- package/src/forgejo/team-member.ts +28 -1
- package/src/index.ts +1 -0
- package/src/proxmox/constraints-required.test.ts +1 -0
- package/src/proxmox/generated/constraints/index.ts +3 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.test.ts +11 -6
- package/src/proxmox/index.ts +19 -3
- package/src/proxmox/qemu-form.ts +49 -13
- package/src/proxmox/qemu-identity.test.ts +30 -0
- package/src/proxmox/qemu-judge.test.ts +65 -0
- package/src/proxmox/qemu-judge.ts +84 -0
- package/src/proxmox/qemu-lifecycle.test.ts +78 -19
- package/src/proxmox/qemu-lifecycle.ts +131 -32
- package/src/proxmox/qemu-net.test.ts +48 -0
- package/src/proxmox/qemu-net.ts +78 -0
- package/src/proxmox/qemu-ownership.test.ts +44 -0
- package/src/proxmox/qemu-props.test.ts +87 -0
- package/src/proxmox/qemu-props.ts +157 -0
- package/src/proxmox/qemu-volume.test.ts +229 -0
- package/src/proxmox/qemu-volume.ts +178 -0
- package/src/proxmox/qemu.ts +15 -29
- package/src/proxmox/storage-download-errors.ts +7 -0
- package/src/proxmox/storage-download-identity.test.ts +54 -0
- package/src/proxmox/storage-download-identity.ts +50 -0
- package/src/proxmox/storage-download-lifecycle.ts +226 -0
- package/src/proxmox/storage-download-ownership.test.ts +50 -0
- package/src/proxmox/storage-download-props.ts +38 -0
- package/src/proxmox/storage-download-task.ts +72 -0
- package/src/proxmox/storage-download.test.ts +216 -0
- package/src/proxmox/storage-download.ts +58 -0
- package/src/talos/credentials-write.test.ts +81 -0
- package/src/talos/credentials-write.ts +95 -0
- package/src/talos/credentials.test.ts +157 -0
- package/src/talos/credentials.ts +160 -90
- package/src/talos/fake-process.ts +94 -0
- package/src/talos/index.ts +39 -4
- package/src/talos/kubeconfig-attrs.ts +55 -0
- package/src/talos/kubeconfig.test.ts +256 -0
- package/src/talos/kubeconfig.ts +170 -90
- package/src/talos/machine-config-fixtures.ts +86 -0
- package/src/talos/machine-config-poll.test.ts +57 -0
- package/src/talos/machine-config-poll.ts +82 -0
- package/src/talos/machine-config-read.ts +118 -0
- package/src/talos/resource.ts +19 -0
- package/src/talos/secrets-gate.test.ts +63 -0
- package/src/talos/talos-bootstrap-etcd.ts +71 -0
- package/src/talos/talos-bootstrap.test.ts +218 -0
- package/src/talos/talos-bootstrap.ts +151 -45
- package/src/talos/talos-cluster-health.test.ts +143 -0
- package/src/talos/talos-cluster-health.ts +91 -28
- package/src/talos/talos-errors.ts +85 -0
- package/src/talos/talos-machine-config.test.ts +189 -0
- package/src/talos/talos-machine-config.ts +102 -88
- package/src/talos/talosctl.ts +8 -0
- package/src/talos/values.test.ts +34 -6
- package/src/talos/values.ts +41 -7
- package/src/telemetry/fake-collector.ts +59 -0
- package/src/telemetry/http-client-sealing.test.ts +82 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/layer.test.ts +204 -0
- package/src/telemetry/layer.ts +152 -0
- package/src/telemetry/redact-serialization.ts +178 -0
- package/src/telemetry/redact.test.ts +124 -0
- package/src/telemetry/redact.ts +147 -0
- package/src/unifi/acl-rule-drift.ts +57 -0
- package/src/unifi/acl-rule-form.ts +135 -0
- package/src/unifi/acl-rule-ordering-form.ts +79 -0
- package/src/unifi/acl-rule-ordering.test.ts +141 -0
- package/src/unifi/acl-rule-ordering.ts +70 -0
- package/src/unifi/acl-rule.test.ts +183 -0
- package/src/unifi/acl-rule.ts +61 -0
- package/src/unifi/discriminated-filter-decode.test.ts +134 -0
- package/src/unifi/dns-policy-drift.ts +48 -0
- package/src/unifi/dns-policy-form.ts +142 -0
- package/src/unifi/dns-policy.test.ts +154 -0
- package/src/unifi/dns-policy.ts +67 -0
- package/src/unifi/drift.test.ts +78 -0
- package/src/unifi/drift.ts +56 -0
- package/src/unifi/errors-and-secrets.test.ts +127 -0
- package/src/unifi/fake-unifi.ts +5 -1
- package/src/unifi/firewall-policy-drift.test.ts +151 -0
- package/src/unifi/firewall-policy-drift.ts +55 -0
- package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
- package/src/unifi/firewall-policy-form.ts +146 -0
- package/src/unifi/firewall-policy-ordering-form.ts +111 -0
- package/src/unifi/firewall-policy-ordering.test.ts +185 -0
- package/src/unifi/firewall-policy-ordering.ts +87 -0
- package/src/unifi/firewall-policy.test.ts +183 -0
- package/src/unifi/firewall-policy.ts +73 -0
- package/src/unifi/firewall-zone-form.ts +31 -2
- package/src/unifi/firewall-zone.test.ts +21 -1
- package/src/unifi/firewall-zone.ts +1 -1
- package/src/unifi/get-only-guard.test.ts +165 -0
- package/src/unifi/index.ts +78 -1
- package/src/unifi/network-drift.test.ts +207 -0
- package/src/unifi/network-drift.ts +80 -0
- package/src/unifi/network-form.ts +11 -39
- package/src/unifi/network.test.ts +7 -82
- package/src/unifi/network.ts +6 -6
- package/src/unifi/paginate.test.ts +252 -0
- package/src/unifi/paginate.ts +141 -0
- package/src/unifi/providers.ts +19 -1
- package/src/unifi/resource.ts +73 -3
- package/src/unifi/scan-source.ts +64 -0
- package/src/unifi/traffic-matching-list-drift.ts +40 -0
- package/src/unifi/traffic-matching-list-form.ts +77 -0
- package/src/unifi/traffic-matching-list.test.ts +202 -0
- package/src/unifi/traffic-matching-list.ts +78 -0
- package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
- package/src/unifi/wifi-broadcast-drift.ts +69 -0
- package/src/unifi/wifi-broadcast-form.ts +211 -0
- package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
- package/src/unifi/wifi-broadcast.test.ts +155 -0
- package/src/unifi/wifi-broadcast.ts +90 -0
- package/src/unifi/write-op-reference.test.ts +190 -0
|
@@ -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)));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `makeDriftOf` (B6) against a synthetic field list — no SDK, no HTTP. `network.test.ts` and
|
|
3
|
+
* `firewall-zone.test.ts` each prove their own family's `driftOf` on top of this; this file proves
|
|
4
|
+
* the framework itself: an exact match reports nothing, a drifted field reports its own live and
|
|
5
|
+
* declared values (never swallowing a real difference into a single boolean), several drifted
|
|
6
|
+
* fields all report, and a custom `equal` overrides the default `deepEqual`.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import { type DriftField, makeDriftOf } from './drift.ts';
|
|
10
|
+
|
|
11
|
+
interface Widget {
|
|
12
|
+
name: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
nested: { count: number } | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FIELDS: readonly DriftField<Widget, Widget>[] = [
|
|
18
|
+
{ field: 'name', live: (a) => a.name, declared: (p) => p.name },
|
|
19
|
+
{ field: 'nested', live: (a) => a.nested, declared: (p) => p.nested },
|
|
20
|
+
{
|
|
21
|
+
field: 'tags',
|
|
22
|
+
live: (a) => a.tags,
|
|
23
|
+
declared: (p) => p.tags,
|
|
24
|
+
equal: (live, declared) =>
|
|
25
|
+
[...(live as string[])].sort().join(',') === [...(declared as string[])].sort().join(','),
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const widget = (overrides: Partial<Widget> = {}): Widget => ({
|
|
30
|
+
name: 'lan',
|
|
31
|
+
nested: { count: 1 },
|
|
32
|
+
tags: ['a', 'b'],
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('makeDriftOf', () => {
|
|
37
|
+
const driftOf = makeDriftOf(FIELDS);
|
|
38
|
+
|
|
39
|
+
test('an exact match reports nothing', () => {
|
|
40
|
+
const attrs = widget();
|
|
41
|
+
expect(driftOf(attrs, widget())).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('a drifted field reports its own live and declared values', () => {
|
|
45
|
+
const attrs = widget({ name: 'lan' });
|
|
46
|
+
const props = widget({ name: 'guest' });
|
|
47
|
+
expect(driftOf(attrs, props)).toEqual([{ field: 'name', live: 'lan', declared: 'guest' }]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('several drifted fields all report, independently', () => {
|
|
51
|
+
const attrs = widget({ name: 'lan', nested: { count: 1 } });
|
|
52
|
+
const props = widget({ name: 'guest', nested: { count: 2 } });
|
|
53
|
+
expect(driftOf(attrs, props)).toEqual([
|
|
54
|
+
{ field: 'name', live: 'lan', declared: 'guest' },
|
|
55
|
+
{ field: 'nested', live: { count: 1 }, declared: { count: 2 } },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the default comparison strips nullish, matching every matches() in this directory', () => {
|
|
60
|
+
const attrs = widget({ nested: undefined });
|
|
61
|
+
const props = { ...widget(), nested: null as unknown as undefined };
|
|
62
|
+
expect(driftOf(attrs, props)).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a custom `equal` overrides the default -- an unordered set is not drift', () => {
|
|
66
|
+
const attrs = widget({ tags: ['b', 'a'] });
|
|
67
|
+
const props = widget({ tags: ['a', 'b'] });
|
|
68
|
+
expect(driftOf(attrs, props)).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a custom `equal` still reports a genuine difference', () => {
|
|
72
|
+
const attrs = widget({ tags: ['a', 'b'] });
|
|
73
|
+
const props = widget({ tags: ['a', 'c'] });
|
|
74
|
+
expect(driftOf(attrs, props)).toEqual([
|
|
75
|
+
{ field: 'tags', live: ['a', 'b'], declared: ['a', 'c'] },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B6: the field-level drift framework every family's own `driftOf(live, props)` is built on
|
|
3
|
+
* (`network-form.ts`, `firewall-zone-form.ts`, and whatever family follows). Consumed by
|
|
4
|
+
* `homeflare-network`'s pre-import drift check (its own C4, not built here) to show WHICH fields
|
|
5
|
+
* a live object and a committed declaration disagree on, before that script overwrites the
|
|
6
|
+
* committed module — never by a `Unifi.*` provider itself, which only ever needs `matches`'s
|
|
7
|
+
* boolean.
|
|
8
|
+
*
|
|
9
|
+
* ★ BUILT ON `matches`, NOT A SEPARATE COMPARISON. Each family's `driftOf` reuses the SAME
|
|
10
|
+
* normalizers (`sortedSet`, the `dhcpGuarding`/`ipv6Configuration` null-vs-undefined handling)
|
|
11
|
+
* its own `matches` already applies — so this reports NOTHING `matches` would call a noop, and
|
|
12
|
+
* everything `matches` would call `update`, broken down by field instead of collapsed to one
|
|
13
|
+
* boolean. Two comparisons drifting apart would be its own bug class; there is exactly one.
|
|
14
|
+
*/
|
|
15
|
+
import { deepEqual } from 'alchemy/Diff';
|
|
16
|
+
|
|
17
|
+
/** One field whose live and declared values differ, as `makeDriftOf` reports it. */
|
|
18
|
+
export interface FieldDrift {
|
|
19
|
+
readonly field: string;
|
|
20
|
+
readonly live: unknown;
|
|
21
|
+
readonly declared: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One field's comparison recipe for `makeDriftOf`. */
|
|
25
|
+
export interface DriftField<Attributes, Props> {
|
|
26
|
+
readonly field: string;
|
|
27
|
+
readonly live: (attributes: Attributes) => unknown;
|
|
28
|
+
readonly declared: (props: Props) => unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Defaults to `alchemy/Diff`'s `deepEqual` with `stripNullish: true` — the same tolerance
|
|
31
|
+
* every `matches` in this directory already gives UniFi's `null`-for-unset optionals (see
|
|
32
|
+
* `network-form.ts`'s header). Pass one explicitly for a field `matches` compares as a
|
|
33
|
+
* normalized SET (`firewall-zone-form.ts`'s `networkIds`), where plain `deepEqual` would
|
|
34
|
+
* report order as drift `matches` never would.
|
|
35
|
+
*/
|
|
36
|
+
readonly equal?: (live: unknown, declared: unknown) => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const defaultEqual = (live: unknown, declared: unknown) =>
|
|
40
|
+
deepEqual(live, declared, { stripNullish: true });
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a `(attributes, props) => FieldDrift[]` from a field list — the framework a family's own
|
|
44
|
+
* `*-form.ts` calls once, then wraps as `driftOf(live, props)` by feeding it `attributesOf(live,
|
|
45
|
+
* props)` (the task's own API: callers have a live SDK object, not `Attributes`, on hand).
|
|
46
|
+
*/
|
|
47
|
+
export const makeDriftOf =
|
|
48
|
+
<Attributes, Props>(fields: readonly DriftField<Attributes, Props>[]) =>
|
|
49
|
+
(attributes: Attributes, props: Props): FieldDrift[] =>
|
|
50
|
+
fields.flatMap(({ field, live, declared, equal = defaultEqual }) => {
|
|
51
|
+
const liveValue = live(attributes);
|
|
52
|
+
const declaredValue = declared(props);
|
|
53
|
+
return equal(liveValue, declaredValue)
|
|
54
|
+
? []
|
|
55
|
+
: [{ field, live: liveValue, declared: declaredValue }];
|
|
56
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime proof for A12 (T8/T23): the distilled regen's two behavioral changes actually decode
|
|
3
|
+
* through the real fake-fetch path every other `*.test.ts` in this family uses — not just typecheck.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ WITHOUT THESE, A FUTURE REGEN THAT DROPS `sensitivePatterns` OR `errorEnvelope` STAYS GREEN.
|
|
6
|
+
* Every other test here fixes response bodies before A2/A1 landed, so none of them would notice
|
|
7
|
+
* either regressing back to a plain string / an always-`undefined` envelope.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import * as wifiBroadcasts from '@distilled.cloud/unifi-network/wifi_broadcasts';
|
|
11
|
+
import * as applicationInfo from '@distilled.cloud/unifi-network/application_info';
|
|
12
|
+
import * as Retry from '@distilled.cloud/unifi-network/Retry';
|
|
13
|
+
import * as Effect from 'effect/Effect';
|
|
14
|
+
import * as Redacted from 'effect/Redacted';
|
|
15
|
+
import { fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
|
|
16
|
+
|
|
17
|
+
const WIFI_PATH = '/proxy/network/integration/v1/sites/site-1/wifi/broadcasts/wifi-1';
|
|
18
|
+
|
|
19
|
+
// A fabricated sentinel, not a real WiFi passphrase (kit is PUBLIC — see fake-unifi.ts).
|
|
20
|
+
const SENTINEL = 'sentinel-wpa-passphrase-do-not-log-me';
|
|
21
|
+
|
|
22
|
+
const liveWifiBroadcast = () => ({
|
|
23
|
+
channel2gLockedTo6: false,
|
|
24
|
+
clientIsolationEnabled: false,
|
|
25
|
+
dtimPeriod2gLockedTo3: false,
|
|
26
|
+
enabled: true,
|
|
27
|
+
hideName: false,
|
|
28
|
+
id: 'wifi-1',
|
|
29
|
+
metadata: { origin: 'USER' },
|
|
30
|
+
multicastToUnicastConversionEnabled: false,
|
|
31
|
+
name: 'Guest',
|
|
32
|
+
type: 'STANDARD',
|
|
33
|
+
securityConfiguration: {
|
|
34
|
+
type: 'WPA_PERSONAL',
|
|
35
|
+
passphrase: SENTINEL,
|
|
36
|
+
presharedKeys: [{ network: { type: 'VLAN', networkId: 'net-1' }, passphrase: SENTINEL }],
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('A2/T23 — WPA/PPSK passphrase decodes Redacted, not a plain string', () => {
|
|
41
|
+
test('both the single passphrase and every PPSK entry come back Redacted', async () => {
|
|
42
|
+
const fake = fakeUnifi((method, url) =>
|
|
43
|
+
method === 'GET' && url.pathname === WIFI_PATH
|
|
44
|
+
? Response.json(liveWifiBroadcast())
|
|
45
|
+
: new Response('unexpected request', { status: 400 }),
|
|
46
|
+
);
|
|
47
|
+
const live = await Effect.runPromise(
|
|
48
|
+
wifiBroadcasts
|
|
49
|
+
.getWifiBroadcastDetails({ siteId: 'site-1', wifiBroadcastId: 'wifi-1' })
|
|
50
|
+
.pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
51
|
+
);
|
|
52
|
+
const { passphrase, presharedKeys } = live.securityConfiguration;
|
|
53
|
+
expect(Redacted.isRedacted(passphrase)).toBe(true);
|
|
54
|
+
expect(Redacted.value(passphrase as Redacted.Redacted<string>)).toBe(SENTINEL);
|
|
55
|
+
expect(Redacted.isRedacted(presharedKeys?.[0]?.passphrase)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('neither JSON.stringify nor String() ever shows the sentinel value', async () => {
|
|
59
|
+
const fake = fakeUnifi(() => Response.json(liveWifiBroadcast()));
|
|
60
|
+
const live = await Effect.runPromise(
|
|
61
|
+
wifiBroadcasts
|
|
62
|
+
.getWifiBroadcastDetails({ siteId: 'site-1', wifiBroadcastId: 'wifi-1' })
|
|
63
|
+
.pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
64
|
+
);
|
|
65
|
+
expect(JSON.stringify(live)).not.toContain(SENTINEL);
|
|
66
|
+
expect(String(live.securityConfiguration.passphrase)).not.toContain(SENTINEL);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** The pinned spec's `Error Message` schema (T8) — see `src/protocol.ts`'s module doc. */
|
|
71
|
+
const envelopeBody = (over: Record<string, unknown> = {}) => ({
|
|
72
|
+
code: 'api.example.not-found',
|
|
73
|
+
message: 'vendor-provided failure text',
|
|
74
|
+
requestId: 'req-sentinel-1',
|
|
75
|
+
requestPath: '/v1/sites/site-1/secret-console-path',
|
|
76
|
+
statusCode: 404,
|
|
77
|
+
statusName: 'Not Found',
|
|
78
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
79
|
+
...over,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('A1/T8 — the vendor Error Message envelope actually decodes', () => {
|
|
83
|
+
test('a mapped status (404) carries the vendor message, not a bare "HTTP 404"', async () => {
|
|
84
|
+
const fake = fakeUnifi(() => Response.json(envelopeBody(), { status: 404 }));
|
|
85
|
+
const failure = await Effect.runPromise(
|
|
86
|
+
Effect.flip(applicationInfo.getInfo({}).pipe(Effect.provide(fakeUnifiLayer(fake.fetch)))),
|
|
87
|
+
);
|
|
88
|
+
expect(failure._tag).toBe('NotFound');
|
|
89
|
+
expect((failure as { message?: string }).message).toBe('vendor-provided failure text');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('an unmapped status (418) becomes UnknownUnifiNetworkError with the extra envelope fields', async () => {
|
|
93
|
+
// ⚠️ `Retry.none`: `UnknownUnifiNetworkError` is tagged a retryable server error by core's
|
|
94
|
+
// default policy regardless of the actual (client, 4xx) status — this test asserts the
|
|
95
|
+
// FAILURE, not the retry schedule (same reason `network.test.ts`'s 500 test needs it).
|
|
96
|
+
const fake = fakeUnifi(() =>
|
|
97
|
+
Response.json(envelopeBody({ statusCode: 418, statusName: "I'm a teapot" }), {
|
|
98
|
+
status: 418,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
const failure = await Effect.runPromise(
|
|
102
|
+
Effect.flip(
|
|
103
|
+
applicationInfo.getInfo({}).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
expect(failure._tag).toBe('UnknownUnifiNetworkError');
|
|
107
|
+
const err = failure as unknown as {
|
|
108
|
+
code?: string;
|
|
109
|
+
requestId?: string;
|
|
110
|
+
statusCode?: number;
|
|
111
|
+
statusName?: string;
|
|
112
|
+
timestamp?: string;
|
|
113
|
+
body: unknown;
|
|
114
|
+
};
|
|
115
|
+
expect(err.code).toBe('api.example.not-found');
|
|
116
|
+
expect(err.requestId).toBe('req-sentinel-1');
|
|
117
|
+
expect(err.statusCode).toBe(418);
|
|
118
|
+
expect(err.statusName).toBe("I'm a teapot");
|
|
119
|
+
expect(err.timestamp).toBe('2026-01-01T00:00:00.000Z');
|
|
120
|
+
// T3: requestPath is never plucked into its own field or into `message` …
|
|
121
|
+
expect('requestPath' in err).toBe(false);
|
|
122
|
+
// … but it IS still inside the raw body for anyone who reads that on purpose (README/errors.ts).
|
|
123
|
+
expect((err.body as { requestPath?: string }).requestPath).toBe(
|
|
124
|
+
'/v1/sites/site-1/secret-console-path',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/unifi/fake-unifi.ts
CHANGED
|
@@ -19,7 +19,11 @@ export const FAKE_KEY = 'placeholder-unifi-key';
|
|
|
19
19
|
|
|
20
20
|
export type Seen = { readonly method: string; readonly path: string };
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Plain-text failure. `protocol.ts`'s `errorEnvelope` also decodes a documented JSON shape
|
|
24
|
+
* (`components.schemas["Error Message"]`, A1/T8) when a scenario needs that instead — see
|
|
25
|
+
* `errors-and-secrets.test.ts`'s `envelopeBody` helper for a fixture in that shape.
|
|
26
|
+
*/
|
|
23
27
|
export const fakeFailure = (status: number, detail: string) => new Response(detail, { status });
|
|
24
28
|
|
|
25
29
|
/**
|