@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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-function coverage for `redactAttribute`/`wrapTracer`. layer.test.ts covers the same policy
|
|
3
|
+
* end to end, through a real `HttpClient` call and a real OTLP export — this file is the fast,
|
|
4
|
+
* exhaustive sweep of the redaction rules themselves.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
8
|
+
import type * as Tracer from 'effect/Tracer';
|
|
9
|
+
import { DROP, REDACTED, redactAttribute, wrapTracer } from './redact.ts';
|
|
10
|
+
|
|
11
|
+
describe('redactAttribute', () => {
|
|
12
|
+
test('drops every header attribute outright, request or response, regardless of value', () => {
|
|
13
|
+
expect(redactAttribute('http.request.header.authorization', 'Bearer x', {})).toBe(DROP);
|
|
14
|
+
expect(redactAttribute('http.request.header.x-api-key', 'k', {})).toBe(DROP);
|
|
15
|
+
expect(redactAttribute('http.response.header.set-cookie', 'a=b', {})).toBe(DROP);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('drops url.query outright — no per-parameter allowlist', () => {
|
|
19
|
+
expect(redactAttribute('url.query', 'token=SECRET&safe=1', {})).toBe(DROP);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('strips the query string and hash from url.full even with an empty policy', () => {
|
|
23
|
+
expect(redactAttribute('url.full', 'http://host/path?token=SECRET#frag', {})).toBe(
|
|
24
|
+
'http://host/path',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('leaves url.full, url.path and server.address untouched when nothing matches the denylist', () => {
|
|
29
|
+
expect(redactAttribute('url.full', 'http://host:9428/insert/opentelemetry/v1/traces', {})).toBe(
|
|
30
|
+
'http://host:9428/insert/opentelemetry/v1/traces',
|
|
31
|
+
);
|
|
32
|
+
expect(redactAttribute('url.path', '/insert/opentelemetry/v1/traces', {})).toBe(
|
|
33
|
+
'/insert/opentelemetry/v1/traces',
|
|
34
|
+
);
|
|
35
|
+
expect(redactAttribute('server.address', 'http://host:9428', {})).toBe('http://host:9428');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('blanks a denylisted path segment, exact match only — no substring match', () => {
|
|
39
|
+
const policy = { denylist: ['console-abc123'] };
|
|
40
|
+
expect(redactAttribute('url.path', '/proxy/network/console-abc123/site', policy)).toBe(
|
|
41
|
+
`/proxy/network/${REDACTED}/site`,
|
|
42
|
+
);
|
|
43
|
+
// "console-abc123-extra" must NOT match the denylisted "console-abc123" segment.
|
|
44
|
+
expect(redactAttribute('url.path', '/proxy/console-abc123-extra', policy)).toBe(
|
|
45
|
+
'/proxy/console-abc123-extra',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('blanks a denylisted host in both url.full and server.address', () => {
|
|
50
|
+
const policy = { denylist: ['private.example.test'] };
|
|
51
|
+
expect(redactAttribute('server.address', 'https://private.example.test:443', policy)).toBe(
|
|
52
|
+
REDACTED,
|
|
53
|
+
);
|
|
54
|
+
expect(redactAttribute('url.full', 'https://private.example.test/api/site/x?y=1', policy)).toBe(
|
|
55
|
+
`https://${REDACTED}/api/site/x`,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a non-string value (e.g. server.port) and an unrecognized key pass through untouched', () => {
|
|
60
|
+
expect(redactAttribute('server.port', 8428, { denylist: ['8428'] })).toBe(8428);
|
|
61
|
+
expect(redactAttribute('alchemy.resource.fqn', 'stack/thing', { denylist: ['thing'] })).toBe(
|
|
62
|
+
'stack/thing',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a url.full value that fails to parse as a URL is left verbatim, not guessed at', () => {
|
|
67
|
+
expect(redactAttribute('url.full', 'not a url', { denylist: ['not'] })).toBe('not a url');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const testSpanOptions = {
|
|
72
|
+
annotations: undefined as never,
|
|
73
|
+
kind: 'internal' as const,
|
|
74
|
+
links: [],
|
|
75
|
+
name: 'test',
|
|
76
|
+
parent: Option.none(),
|
|
77
|
+
root: true,
|
|
78
|
+
sampled: true,
|
|
79
|
+
startTime: 0n,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const recordingInner = (): { calls: Array<[string, unknown]>; tracer: Tracer.Tracer } => {
|
|
83
|
+
const calls: Array<[string, unknown]> = [];
|
|
84
|
+
const tracer: Tracer.Tracer = {
|
|
85
|
+
span: (options) => ({
|
|
86
|
+
_tag: 'Span',
|
|
87
|
+
addLinks: () => {},
|
|
88
|
+
annotations: options.annotations,
|
|
89
|
+
attribute: (key, value) => calls.push([key, value]),
|
|
90
|
+
attributes: new Map(),
|
|
91
|
+
end: () => {},
|
|
92
|
+
event: () => {},
|
|
93
|
+
kind: options.kind,
|
|
94
|
+
links: options.links,
|
|
95
|
+
name: options.name,
|
|
96
|
+
parent: options.parent,
|
|
97
|
+
sampled: options.sampled,
|
|
98
|
+
spanId: 'span-1',
|
|
99
|
+
status: { _tag: 'Started', startTime: options.startTime },
|
|
100
|
+
traceId: 'trace-1',
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
return { calls, tracer };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe('wrapTracer', () => {
|
|
107
|
+
test('forwards an allowed attribute and withholds a dropped one from the real span', () => {
|
|
108
|
+
const { calls, tracer } = recordingInner();
|
|
109
|
+
const wrapped = wrapTracer(tracer, {});
|
|
110
|
+
const span = wrapped.span(testSpanOptions);
|
|
111
|
+
span.attribute('http.request.header.cookie', 'secret');
|
|
112
|
+
span.attribute('alchemy.resource.fqn', 'stack/thing');
|
|
113
|
+
expect(calls).toEqual([['alchemy.resource.fqn', 'stack/thing']]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("passes the span's identity and lifecycle through unchanged", () => {
|
|
117
|
+
const { tracer } = recordingInner();
|
|
118
|
+
const wrapped = wrapTracer(tracer, {});
|
|
119
|
+
const span = wrapped.span(testSpanOptions);
|
|
120
|
+
expect(span.name).toBe('test');
|
|
121
|
+
expect(span.spanId).toBe('span-1');
|
|
122
|
+
expect(span.traceId).toBe('trace-1');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL/header redaction for outgoing spans — applied at the `Tracer.Tracer` boundary so it holds for
|
|
3
|
+
* EVERY span source (a provider's own `Effect.withSpan`, effect's `HttpClient` client spans, and
|
|
4
|
+
* alchemy's own plan/apply/resource spans), not just the ones a caller remembers to annotate.
|
|
5
|
+
*
|
|
6
|
+
* ★ WHY THE TRACER, NOT AN HTTPCLIENT MIDDLEWARE. effect's `HttpClient.make` (src/unstable/http/
|
|
7
|
+
* HttpClient.ts) hard-codes `span.attribute("url.full", …)`, `"url.query"` and
|
|
8
|
+
* `http.request.header.<name>` on the span it creates — there is no hook to stop it recording
|
|
9
|
+
* them. The one seam every span (HttpClient's included) passes through is `Tracer.span()`
|
|
10
|
+
* returning a `Tracer.Span`, so `wrapTracer` intercepts `Span.attribute()` there instead.
|
|
11
|
+
* ⛔ NEVER RECORD HEADERS. A header NAME can itself be sensitive (`x-api-key`, a session cookie
|
|
12
|
+
* name), so every `http.{request,response}.header.*` key is DROPPED, not value-redacted.
|
|
13
|
+
* ⚠️ QUERY STRINGS ARE STRIPPED WHOLE, not redacted per-parameter: an allowlist of "safe" params
|
|
14
|
+
* would silently leak the next secret param a provider adds. `url.query` is dropped outright and
|
|
15
|
+
* `url.full`/`url.path` never carry a `?…` past this point.
|
|
16
|
+
*/
|
|
17
|
+
import type * as Tracer from 'effect/Tracer';
|
|
18
|
+
|
|
19
|
+
/** Host or path segments to blank wherever they appear in a URL attribute — exact-segment match. */
|
|
20
|
+
export interface RedactionPolicy {
|
|
21
|
+
/**
|
|
22
|
+
* e.g. a UniFi console id embedded in a path (`/proxy/network/api/site/<id>/...`) or a private
|
|
23
|
+
* hostname a consumer never wants leaving its own process. Compared against the full hostname and
|
|
24
|
+
* against each `/`-separated path segment — never a substring match, so `"db"` cannot accidentally
|
|
25
|
+
* blank `"db1.example.test"`.
|
|
26
|
+
*/
|
|
27
|
+
readonly denylist?: ReadonlyArray<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const REDACTED = '<redacted>';
|
|
31
|
+
|
|
32
|
+
/** Returned by `redactAttribute` to mean "drop this attribute entirely — do not forward it". */
|
|
33
|
+
export const DROP: unique symbol = Symbol.for('homeflare/alchemy/telemetry/drop-attribute');
|
|
34
|
+
|
|
35
|
+
const HEADER_ATTRIBUTE = /^http\.(?:request|response)\.header\./;
|
|
36
|
+
|
|
37
|
+
const redactSegments = (value: string, denylist: ReadonlySet<string>): string =>
|
|
38
|
+
value
|
|
39
|
+
.split('/')
|
|
40
|
+
.map((segment) => (denylist.has(segment) ? REDACTED : segment))
|
|
41
|
+
.join('/');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Applies `policy` to one span attribute. Returns `DROP` when the key must never be recorded at
|
|
45
|
+
* all (every header); otherwise returns the value to record, with URLs stripped of their query
|
|
46
|
+
* string and hash and their denylisted host/path segments blanked. A value this function does not
|
|
47
|
+
* recognize (not a URL-shaped key, or not a string) passes through unchanged — alchemy's own
|
|
48
|
+
* engine attributes (`resource.fqn`, `plan.action`, …) are never URLs and are never touched.
|
|
49
|
+
*/
|
|
50
|
+
export const redactAttribute = (key: string, value: unknown, policy: RedactionPolicy): unknown => {
|
|
51
|
+
if (HEADER_ATTRIBUTE.test(key)) return DROP;
|
|
52
|
+
if (key === 'url.query') return DROP;
|
|
53
|
+
if (typeof value !== 'string') return value;
|
|
54
|
+
const denylist = new Set(policy.denylist ?? []);
|
|
55
|
+
if (denylist.size === 0 && key !== 'url.full' && key !== 'server.address') return value;
|
|
56
|
+
// ⚠️ MEASURED 2026-09-26 (docs/telemetry-spike.md): effect's own `HttpClient.make` sets
|
|
57
|
+
// `server.address` to `url.origin` (`http://host:port`), NOT a bare hostname — a denylist entry
|
|
58
|
+
// naming just a hostname would never match the raw value, so this parses it out the same way
|
|
59
|
+
// `url.full` does.
|
|
60
|
+
// ⛔ FIX (redteam PR 302 MINOR item 5, 2026-09-26): the OTel semantic convention's own default for
|
|
61
|
+
// `server.address` IS a bare hostname with no scheme (`new URL(value)` throws on that), and other
|
|
62
|
+
// instrumentation could emit exactly that even though effect's `HttpClient` doesn't today —
|
|
63
|
+
// `redactAttribute('server.address', 'db1.example.test', { denylist: ['db1.example.test'] })`
|
|
64
|
+
// returned the value verbatim before this fallback. Compare the raw string directly when it
|
|
65
|
+
// isn't a parseable URL, instead of giving up.
|
|
66
|
+
if (key === 'server.address') {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(value);
|
|
69
|
+
return denylist.has(url.hostname) ? REDACTED : value;
|
|
70
|
+
} catch {
|
|
71
|
+
return denylist.has(value) ? REDACTED : value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (key === 'url.path') return redactSegments(value, denylist);
|
|
75
|
+
if (key === 'url.full') {
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(value);
|
|
78
|
+
const host = denylist.has(url.hostname) ? REDACTED : url.host;
|
|
79
|
+
return `${url.protocol}//${host}${redactSegments(url.pathname, denylist)}`;
|
|
80
|
+
} catch {
|
|
81
|
+
// Not a URL we can parse (a relative path effect couldn't resolve) — leave it verbatim
|
|
82
|
+
// rather than guess; `url.full` is the one HttpClient always sets from a resolved `URL`, so
|
|
83
|
+
// this branch is a defensive fallback, not the expected path.
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const redactAttributes = (
|
|
91
|
+
attributes: Record<string, unknown>,
|
|
92
|
+
policy: RedactionPolicy,
|
|
93
|
+
): Record<string, unknown> => {
|
|
94
|
+
const out: Record<string, unknown> = {};
|
|
95
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
96
|
+
const redacted = redactAttribute(key, value, policy);
|
|
97
|
+
if (redacted !== DROP) out[key] = redacted;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Wraps a `Tracer.Tracer` so every span it creates redacts its attributes and event attributes
|
|
104
|
+
* through `redactAttribute`/`redactAttributes` before they reach the real span (and therefore
|
|
105
|
+
* before OTLP serialization ever sees them). Everything else — timing, status, links, the span's
|
|
106
|
+
* own identity — passes straight through to `inner`.
|
|
107
|
+
*/
|
|
108
|
+
export const wrapTracer = (inner: Tracer.Tracer, policy: RedactionPolicy): Tracer.Tracer => ({
|
|
109
|
+
context: inner.context,
|
|
110
|
+
span(options): Tracer.Span {
|
|
111
|
+
const real = inner.span(options);
|
|
112
|
+
return {
|
|
113
|
+
_tag: 'Span',
|
|
114
|
+
spanId: real.spanId,
|
|
115
|
+
traceId: real.traceId,
|
|
116
|
+
parent: real.parent,
|
|
117
|
+
annotations: real.annotations,
|
|
118
|
+
get name() {
|
|
119
|
+
return real.name;
|
|
120
|
+
},
|
|
121
|
+
get status() {
|
|
122
|
+
return real.status;
|
|
123
|
+
},
|
|
124
|
+
get attributes() {
|
|
125
|
+
return real.attributes;
|
|
126
|
+
},
|
|
127
|
+
get links() {
|
|
128
|
+
return real.links;
|
|
129
|
+
},
|
|
130
|
+
sampled: real.sampled,
|
|
131
|
+
kind: real.kind,
|
|
132
|
+
end: (endTime, exit) => {
|
|
133
|
+
real.end(endTime, exit);
|
|
134
|
+
},
|
|
135
|
+
attribute: (key, value) => {
|
|
136
|
+
const redacted = redactAttribute(key, value, policy);
|
|
137
|
+
if (redacted !== DROP) real.attribute(key, redacted);
|
|
138
|
+
},
|
|
139
|
+
event: (name, startTime, attributes) => {
|
|
140
|
+
real.event(name, startTime, attributes ? redactAttributes(attributes, policy) : attributes);
|
|
141
|
+
},
|
|
142
|
+
addLinks: (links) => {
|
|
143
|
+
real.addLinks(links);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.AclRule`'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 header explains (MEDIUM-4,
|
|
4
|
+
* red team, 2026-09-26): `matches(attrs, props) === (driftOf(live, props).length === 0)` is true
|
|
5
|
+
* BY CONSTRUCTION.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ `stripNullish: true` ON EVERY FIELD BY DEFAULT (`drift.ts`'s `defaultEqual`) — same "UniFi's
|
|
8
|
+
* JSON answers `null`, a declaration omits" tolerance `network-drift.ts` documents.
|
|
9
|
+
* ⛔ `enforcingDeviceFilter`/`protocolFilter` RUN THROUGH `acl-rule-form.ts`'s normalizers on BOTH
|
|
10
|
+
* sides before comparing — never applied to only `live` or only `declared` (that specific gap is
|
|
11
|
+
* what MEDIUM-4's own mutant found in `network-drift.ts`; see its header).
|
|
12
|
+
*/
|
|
13
|
+
import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
|
|
14
|
+
import { makeDriftOf } from './drift.ts';
|
|
15
|
+
import {
|
|
16
|
+
type AclRuleAttributes,
|
|
17
|
+
type AclRuleProps,
|
|
18
|
+
attributesOf,
|
|
19
|
+
normalizeDeviceFilter,
|
|
20
|
+
normalizeProtocolFilter,
|
|
21
|
+
} from './acl-rule-form.ts';
|
|
22
|
+
|
|
23
|
+
const fieldDrift = makeDriftOf<AclRuleAttributes, AclRuleProps>([
|
|
24
|
+
{ field: 'action', live: (a) => a.action, declared: (p) => p.action },
|
|
25
|
+
{ field: 'description', live: (a) => a.description, declared: (p) => p.description },
|
|
26
|
+
{
|
|
27
|
+
field: 'destinationFilter',
|
|
28
|
+
live: (a) => a.destinationFilter,
|
|
29
|
+
declared: (p) => p.destinationFilter,
|
|
30
|
+
},
|
|
31
|
+
{ field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
|
|
32
|
+
{
|
|
33
|
+
field: 'enforcingDeviceFilter',
|
|
34
|
+
live: (a) => normalizeDeviceFilter(a.enforcingDeviceFilter),
|
|
35
|
+
declared: (p) => normalizeDeviceFilter(p.enforcingDeviceFilter),
|
|
36
|
+
},
|
|
37
|
+
{ field: 'name', live: (a) => a.name, declared: (p) => p.name },
|
|
38
|
+
{ field: 'sourceFilter', live: (a) => a.sourceFilter, declared: (p) => p.sourceFilter },
|
|
39
|
+
{ field: 'type', live: (a) => a.type, declared: (p) => p.type },
|
|
40
|
+
{
|
|
41
|
+
field: 'protocolFilter',
|
|
42
|
+
live: (a) => normalizeProtocolFilter(a.protocolFilter),
|
|
43
|
+
declared: (p) => normalizeProtocolFilter(p.protocolFilter),
|
|
44
|
+
},
|
|
45
|
+
{ field: 'networkIdFilter', live: (a) => a.networkIdFilter, declared: (p) => p.networkIdFilter },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export const matches = (attributes: AclRuleAttributes, props: AclRuleProps): boolean =>
|
|
49
|
+
fieldDrift(attributes, props).length === 0;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-field live-vs-declared drift straight from one live read — the task's own API
|
|
53
|
+
* (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
|
|
54
|
+
* `attributesOf` itself just to call this.
|
|
55
|
+
*/
|
|
56
|
+
export const driftOf = (live: aclRules.ACLRule, props: AclRuleProps) =>
|
|
57
|
+
fieldDrift(attributesOf(live, props), props);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.AclRule`'s wire shape — the declarable subset of `ACLRule` (`getAclRule`'s own decode
|
|
3
|
+
* target — `ACLRuleObject` is the structurally-identical page-list item type, unused here), and
|
|
4
|
+
* how a live read
|
|
5
|
+
* becomes both plan attributes and a rendered declaration. Rule ORDER is a separate resource
|
|
6
|
+
* (`acl-rule-ordering.ts`, T5) — this file is one rule's own fields only.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ `sourceFilter`/`destinationFilter` STAY `unknown`, ON THE SDK'S OWN AUTHORITY.
|
|
9
|
+
* `access_control_acl_rules.ts`'s own comment on both ("Shape varies by variant — widened by
|
|
10
|
+
* scripts/convert.ts; see README") says the OpenAPI discriminator these vary over was flattened
|
|
11
|
+
* at generation time (T10), same as `network-form.ts`'s `ipv4Configuration`. A3 (typing these
|
|
12
|
+
* per-discriminator) is scoped OUT of this PR — see the plan's family inventory — so both stay
|
|
13
|
+
* opaque JSON, compared with plain `deepEqual`, honest about not being decoded.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ KNOWN GAP UNTIL A3 (T15, red team, Minor finding 4): the vendor's own variant DTOs for these
|
|
16
|
+
* filters carry SET-LIKE arrays too (`networkIds`, `ipAddressesOrSubnets`, `portFilter`,
|
|
17
|
+
* `macAddresses` — `docs/unifi-acl-rule.md`'s own field notes). Because the filter stays opaque,
|
|
18
|
+
* `deepEqual` compares those nested arrays order-sensitively, the same false-`update` risk this
|
|
19
|
+
* file already removes for `deviceIds`/`protocolFilter` by normalizing them — but here there is
|
|
20
|
+
* no decoded field to normalize. Accepted as a known gap, not fixed, until A3 types the filter
|
|
21
|
+
* per-discriminator and can apply `sortedSet` to its own array fields.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ `index` IS ATTRIBUTES-ONLY, NEVER DECLARED. `CreateAclRuleRequest`'s own field comment:
|
|
24
|
+
* "ACL rule index. This property is deprecated and has no effect. Use the dedicated ACL rule
|
|
25
|
+
* reordering endpoint." The live object's current position is `acl-rule-ordering.ts`'s concern
|
|
26
|
+
* (`ACLRuleOrdering.orderedAclRuleIds`), not this resource's declared identity — reporting it
|
|
27
|
+
* here without comparing it avoids a field that can never converge under this family's own
|
|
28
|
+
* `matches` (nothing here could ever change it) from silently pretending to be declarable.
|
|
29
|
+
*
|
|
30
|
+
* ⛔ `enforcingDeviceFilter.deviceIds` AND `protocolFilter` ARE SETS, NOT SEQUENCES — same
|
|
31
|
+
* reasoning `firewall-zone-form.ts` gives for `networkIds`: "IDs of the Switch-capable devices
|
|
32
|
+
* used to enforce the ACL rule" and "Protocols this ACL rule will be applied to" both describe
|
|
33
|
+
* membership, with no indication either list is ordered like `acl-rule-ordering.ts`'s rule-id
|
|
34
|
+
* sequence. Left alone, a console that returns either in a different order between calls would
|
|
35
|
+
* plan a spurious `update` that this read-only family's `reconcile` then refuses forever.
|
|
36
|
+
*/
|
|
37
|
+
import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
|
|
38
|
+
|
|
39
|
+
const sortedSet = (values: readonly string[]): string[] => [...new Set(values)].sort();
|
|
40
|
+
|
|
41
|
+
/** ★ EXPORTED so `acl-rule-drift.ts` normalizes both live and declared sides identically — see
|
|
42
|
+
* `network-form.ts`'s own header for why a normalizer applied to only one side is a bug class. */
|
|
43
|
+
export const normalizeDeviceFilter = (
|
|
44
|
+
value: aclRules.ACLRuleDeviceFilter | undefined,
|
|
45
|
+
): aclRules.ACLRuleDeviceFilter | undefined =>
|
|
46
|
+
value == null
|
|
47
|
+
? value
|
|
48
|
+
: {
|
|
49
|
+
...value,
|
|
50
|
+
...(value.deviceIds !== undefined ? { deviceIds: sortedSet(value.deviceIds) } : {}),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const normalizeProtocolFilter = (
|
|
54
|
+
value: readonly string[] | undefined,
|
|
55
|
+
): string[] | undefined => (value == null ? undefined : sortedSet(value));
|
|
56
|
+
|
|
57
|
+
/** ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` — `exactOptionalPropertyTypes`; see
|
|
58
|
+
* `network-form.ts`'s own header. */
|
|
59
|
+
export interface AclRuleProps {
|
|
60
|
+
siteId: string;
|
|
61
|
+
aclRuleId: string;
|
|
62
|
+
/** `ALLOW` or `BLOCK` (also accepts a future vendor value the spec did not enumerate). */
|
|
63
|
+
action: string;
|
|
64
|
+
description?: string | undefined;
|
|
65
|
+
/** Traffic destination filter — opaque, see the header. */
|
|
66
|
+
destinationFilter?: unknown;
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
/** Devices enforcing this rule — compared as a SET, see the header. */
|
|
69
|
+
enforcingDeviceFilter?: aclRules.ACLRuleDeviceFilter | undefined;
|
|
70
|
+
name: string;
|
|
71
|
+
/** Traffic source filter — opaque, see the header. */
|
|
72
|
+
sourceFilter?: unknown;
|
|
73
|
+
type: string;
|
|
74
|
+
/** Protocols this rule applies to — compared as a SET, see the header. */
|
|
75
|
+
protocolFilter?: string[] | undefined;
|
|
76
|
+
networkIdFilter?: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AclRuleAttributes {
|
|
80
|
+
siteId: string;
|
|
81
|
+
aclRuleId: string;
|
|
82
|
+
action: string;
|
|
83
|
+
description: string | undefined;
|
|
84
|
+
destinationFilter: unknown;
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
enforcingDeviceFilter: aclRules.ACLRuleDeviceFilter | undefined;
|
|
87
|
+
/** ACL rule index (current priority position) — server-derived, never declared; see header. */
|
|
88
|
+
index: number;
|
|
89
|
+
name: string;
|
|
90
|
+
sourceFilter: unknown;
|
|
91
|
+
type: string;
|
|
92
|
+
protocolFilter: string[] | undefined;
|
|
93
|
+
networkIdFilter: string | undefined;
|
|
94
|
+
/** `metadata.origin` — server-derived, never declared; see `firewall-zone-form.ts`'s own. */
|
|
95
|
+
metadataOrigin: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const attributesOf = (live: aclRules.ACLRule, props: AclRuleProps): AclRuleAttributes => ({
|
|
99
|
+
siteId: props.siteId,
|
|
100
|
+
aclRuleId: live.id,
|
|
101
|
+
action: live.action,
|
|
102
|
+
description: live.description,
|
|
103
|
+
destinationFilter: live.destinationFilter,
|
|
104
|
+
enabled: live.enabled,
|
|
105
|
+
enforcingDeviceFilter: normalizeDeviceFilter(live.enforcingDeviceFilter),
|
|
106
|
+
index: live.index,
|
|
107
|
+
name: live.name,
|
|
108
|
+
sourceFilter: live.sourceFilter,
|
|
109
|
+
type: live.type,
|
|
110
|
+
protocolFilter: normalizeProtocolFilter(live.protocolFilter),
|
|
111
|
+
networkIdFilter: live.networkIdFilter,
|
|
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 `getAclRule`'s own response straight in
|
|
118
|
+
* and writes the result as `aclRule('<id>', declareAclRule(live, siteId))` — no field is invented
|
|
119
|
+
* or defaulted beyond what `attributesOf` already reports, so `matches(attributesOf(live, props),
|
|
120
|
+
* declareAclRule(live, siteId))` is `true` by construction (`acl-rule.test.ts` proves it).
|
|
121
|
+
*/
|
|
122
|
+
export const declareAclRule = (live: aclRules.ACLRule, siteId: string): AclRuleProps => ({
|
|
123
|
+
siteId,
|
|
124
|
+
aclRuleId: live.id,
|
|
125
|
+
action: live.action,
|
|
126
|
+
description: live.description,
|
|
127
|
+
destinationFilter: live.destinationFilter,
|
|
128
|
+
enabled: live.enabled,
|
|
129
|
+
enforcingDeviceFilter: normalizeDeviceFilter(live.enforcingDeviceFilter),
|
|
130
|
+
name: live.name,
|
|
131
|
+
sourceFilter: live.sourceFilter,
|
|
132
|
+
type: live.type,
|
|
133
|
+
protocolFilter: normalizeProtocolFilter(live.protocolFilter),
|
|
134
|
+
networkIdFilter: live.networkIdFilter,
|
|
135
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.AclRuleOrdering`'s wire shape — ONE resource per site (`getAclRuleOrdering`'s own scope,
|
|
3
|
+
* `GET /v1/sites/{siteId}/acl-rules/ordering`), not per rule. Compare with `firewall-zone-form.ts`'s
|
|
4
|
+
* `networkIds`, which is a genuine SET: this file's `orderedAclRuleIds` is the opposite case.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ T5 — `orderedAclRuleIds` IS A SEQUENCE, NEVER RUN THROUGH `sortedSet`. The endpoint's own name
|
|
7
|
+
* (`updateAclRuleOrdering`) and its field's own vendor comment on `ACLRule.index` ("Lower index
|
|
8
|
+
* has higher priority") both say this list's POSITION is the entire value it carries — rule
|
|
9
|
+
* priority. `firewall-zone-form.ts`'s `sortedSet`/`network-form.ts`'s three set normalizers all
|
|
10
|
+
* exist to stop the OPPOSITE mistake (treating a set as if order mattered); applying the same
|
|
11
|
+
* fix here would silently discard the one thing this resource represents. `drift.ts`'s
|
|
12
|
+
* `defaultEqual` (`deepEqual`) already compares arrays element-by-element without sorting them
|
|
13
|
+
* (its header: "sorts object KEYS but NOT ARRAY ELEMENTS") — so leaving this field un-normalized
|
|
14
|
+
* is what makes the comparison order-sensitive, not an oversight to "fix" later.
|
|
15
|
+
*
|
|
16
|
+
* ★ `makeDriftOf` (`drift.ts`), NO CUSTOM `equal` — same framework every other family's
|
|
17
|
+
* `matches`/`driftOf` share (MEDIUM-4). Its default `deepEqual` is ALREADY order-sensitive for
|
|
18
|
+
* arrays (see the paragraph above), so the absence of a custom `equal` here, unlike
|
|
19
|
+
* `firewall-zone-form.ts`'s `networkIds` entry, is itself the "never sortedSet" rule in force —
|
|
20
|
+
* not an oversight.
|
|
21
|
+
*/
|
|
22
|
+
import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
|
|
23
|
+
import { makeDriftOf } from './drift.ts';
|
|
24
|
+
|
|
25
|
+
export interface AclRuleOrderingProps {
|
|
26
|
+
siteId: string;
|
|
27
|
+
/** Rule IDs in priority order, highest priority first — never compared as a set (see header). */
|
|
28
|
+
orderedAclRuleIds: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AclRuleOrderingAttributes {
|
|
32
|
+
siteId: string;
|
|
33
|
+
orderedAclRuleIds: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const attributesOf = (
|
|
37
|
+
live: aclRules.ACLRuleOrdering,
|
|
38
|
+
props: AclRuleOrderingProps,
|
|
39
|
+
): AclRuleOrderingAttributes => ({
|
|
40
|
+
siteId: props.siteId,
|
|
41
|
+
orderedAclRuleIds: live.orderedAclRuleIds,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The declaration renderer (task spec: "given one live object, return the props a declaration
|
|
46
|
+
* needs so its plan is noop"). A later import script feeds `getAclRuleOrdering`'s own response
|
|
47
|
+
* straight in and writes the result as `aclRuleOrdering('site-1', declareAclRuleOrdering(live,
|
|
48
|
+
* siteId))` — `matches(attributesOf(live, props), declareAclRuleOrdering(live, siteId))` is `true`
|
|
49
|
+
* by construction (`acl-rule-ordering.test.ts` proves it).
|
|
50
|
+
*/
|
|
51
|
+
export const declareAclRuleOrdering = (
|
|
52
|
+
live: aclRules.ACLRuleOrdering,
|
|
53
|
+
siteId: string,
|
|
54
|
+
): AclRuleOrderingProps => ({
|
|
55
|
+
siteId,
|
|
56
|
+
orderedAclRuleIds: live.orderedAclRuleIds,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const fieldDrift = makeDriftOf<AclRuleOrderingAttributes, AclRuleOrderingProps>([
|
|
60
|
+
{
|
|
61
|
+
field: 'orderedAclRuleIds',
|
|
62
|
+
live: (a) => a.orderedAclRuleIds,
|
|
63
|
+
declared: (p) => p.orderedAclRuleIds,
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const matches = (
|
|
68
|
+
attributes: AclRuleOrderingAttributes,
|
|
69
|
+
props: AclRuleOrderingProps,
|
|
70
|
+
): boolean => fieldDrift(attributes, props).length === 0;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Per-field live-vs-declared drift straight from one live read — the task's own API
|
|
74
|
+
* (`driftOf(live, props)`). Only one field exists, so this either reports nothing or the whole
|
|
75
|
+
* ordered pair, never a per-position breakdown -- a future consumer wanting "which rule moved"
|
|
76
|
+
* would need to diff the two arrays itself.
|
|
77
|
+
*/
|
|
78
|
+
export const driftOf = (live: aclRules.ACLRuleOrdering, props: AclRuleOrderingProps) =>
|
|
79
|
+
fieldDrift(attributesOf(live, props), props);
|