@homeflare/alchemy 0.39.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/caddy/index.js.map +2 -2
- 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 +1610 -0
- package/dist/ceph/index.js.map +38 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts.map +1 -1
- package/dist/launchd/index.d.ts +1 -1
- package/dist/launchd/index.d.ts.map +1 -1
- package/dist/launchd/index.js +7 -1
- package/dist/launchd/index.js.map +5 -5
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +21 -0
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/linux/directory-lifecycle.d.ts.map +1 -1
- package/dist/linux/fake-linux-host.d.ts.map +1 -1
- package/dist/linux/index.d.ts +1 -1
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +16 -4
- package/dist/linux/index.js.map +7 -7
- package/dist/linux/ssh-runner.d.ts.map +1 -1
- package/dist/linux/sudo-runner.d.ts.map +1 -1
- package/dist/opnsense/alias-form.d.ts +14 -1
- package/dist/opnsense/alias-form.d.ts.map +1 -1
- package/dist/opnsense/alias.d.ts +1 -1
- package/dist/opnsense/alias.d.ts.map +1 -1
- package/dist/opnsense/group-form.d.ts +8 -2
- package/dist/opnsense/group-form.d.ts.map +1 -1
- package/dist/opnsense/group.d.ts +1 -1
- package/dist/opnsense/group.d.ts.map +1 -1
- package/dist/opnsense/index.js +15 -7
- package/dist/opnsense/index.js.map +7 -7
- package/dist/opnsense/wire.d.ts +42 -12
- package/dist/opnsense/wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +62 -39
- package/dist/proxmox/index.js.map +8 -7
- package/dist/proxmox/qemu-volume.d.ts +12 -2
- package/dist/proxmox/qemu-volume.d.ts.map +1 -1
- package/dist/proxmox/storage-form.d.ts +28 -3
- package/dist/proxmox/storage-form.d.ts.map +1 -1
- package/dist/proxmox/storage-plugin-options.d.ts +2 -0
- package/dist/proxmox/storage-plugin-options.d.ts.map +1 -0
- package/dist/proxmox/storage-wire.d.ts +28 -1
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts +6 -1
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js.map +2 -2
- 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/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/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/index.d.ts +7 -0
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +588 -3
- package/dist/unifi/index.js.map +24 -4
- package/dist/unifi/paginate.d.ts +0 -12
- package/dist/unifi/paginate.d.ts.map +1 -1
- package/dist/unifi/providers.d.ts +1 -1
- package/dist/unifi/providers.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/distilled-interim.md +7 -0
- package/docs/launchd.md +5 -3
- package/docs/provider-standard.md +5 -1
- package/docs/talos-argocd-dogfood.md +160 -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 +10 -5
- package/package.json +7 -3
- 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/index.ts +1 -0
- package/src/launchd/fake-runner.ts +2 -0
- package/src/launchd/index.ts +8 -1
- package/src/launchd/local-runner.ts +16 -1
- package/src/launchd/runner.ts +22 -0
- package/src/launchd/sudo-runner.ts +3 -0
- package/src/linux/directory-lifecycle.ts +18 -3
- package/src/linux/directory-sudo.test.ts +59 -0
- package/src/linux/directory.test.ts +34 -8
- package/src/linux/fake-linux-host.ts +2 -0
- package/src/linux/index.ts +1 -0
- package/src/linux/ssh-runner.ts +4 -0
- package/src/linux/sudo-runner.ts +3 -0
- package/src/opnsense/alias-form.test.ts +24 -12
- package/src/opnsense/alias-form.ts +20 -7
- package/src/opnsense/alias.test.ts +13 -6
- package/src/opnsense/alias.ts +16 -10
- package/src/opnsense/group-form.test.ts +12 -8
- package/src/opnsense/group-form.ts +13 -4
- package/src/opnsense/group.test.ts +18 -7
- package/src/opnsense/group.ts +10 -3
- package/src/opnsense/wire.ts +57 -12
- package/src/proxmox/qemu-volume.test.ts +153 -4
- package/src/proxmox/qemu-volume.ts +71 -14
- package/src/proxmox/storage-form.ts +48 -8
- package/src/proxmox/storage-plugin-options.ts +45 -0
- package/src/proxmox/storage-update.test.ts +183 -0
- package/src/proxmox/storage-wire.ts +54 -7
- package/src/proxmox/storage.ts +18 -6
- package/src/release/index.ts +1 -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/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/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/index.ts +63 -0
- package/src/unifi/paginate.test.ts +24 -0
- package/src/unifi/paginate.ts +15 -3
- package/src/unifi/providers.ts +19 -1
- 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 +8 -54
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# UniFi DNS Policies — `Unifi.DnsPolicy`
|
|
2
|
+
|
|
3
|
+
⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
|
|
4
|
+
`Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
|
|
5
|
+
`GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
|
|
6
|
+
transport. This family calls no SDK write op anywhere — `write-op-reference.test.ts` proves it
|
|
7
|
+
statically, and `dns-policy.test.ts`'s own write-path tests prove `reconcile`/`destroy` never send
|
|
8
|
+
anything but the one `GET` this spec's `fetchLive` makes.
|
|
9
|
+
|
|
10
|
+
## One flat DTO for every DNS record type
|
|
11
|
+
|
|
12
|
+
⚠️ **Corrected 2026-09-26 (red team, Important finding 1): the vendor spec DOES discriminate.**
|
|
13
|
+
The pinned spec's `DNS policy` schema carries a 7-way `discriminator.mapping` on `type`
|
|
14
|
+
(`A_RECORD`, `AAAA_RECORD`, `CNAME_RECORD`, `FORWARD_DOMAIN`, `MX_RECORD`, `SRV_RECORD`,
|
|
15
|
+
`TXT_RECORD`), each mapping to its own variant DTO (`IntegrationDnsARecordDto`, …) that carries the
|
|
16
|
+
record-specific fields (`ipv4Address`, `ttlSeconds`, `text`, …) — the base `DNS policy` schema
|
|
17
|
+
itself has only `domain`/`enabled`/`id`/`metadata`/`type`. None of those variant DTOs are reachable
|
|
18
|
+
from the base schema by a plain `$ref` walk; they exist only behind the `mapping` (T11 gate below).
|
|
19
|
+
|
|
20
|
+
What IS still true: `scripts/convert.ts` FLATTENS all 7 variants into one struct — every variant
|
|
21
|
+
field becomes optional on the same `DNSPolicy`/`CreateDnsPolicyRequest`/`UpdateDnsPolicyRequest`
|
|
22
|
+
type, rather than widening any of them to `unknown` (T10). No two variants collide on a field name,
|
|
23
|
+
so the flattening loses no information. **That means `dns-policy-form.ts` still has no opaque-JSON
|
|
24
|
+
field to flag or defer, unlike `acl-rule-form.ts`'s `sourceFilter`/`destinationFilter`
|
|
25
|
+
(`docs/unifi-acl-rule.md`)** — that conclusion holds; only the "never discriminates" reason given
|
|
26
|
+
for it originally was wrong.
|
|
27
|
+
|
|
28
|
+
⛔ **No array field exists anywhere in this shape**, so none of `network-form.ts`'s/
|
|
29
|
+
`firewall-zone-form.ts`'s/`acl-rule-form.ts`'s `sortedSet` set-normalizing applies here —
|
|
30
|
+
`drift.ts`'s default `stripNullish` tolerance for UniFi's `null`-for-unset-optional fields is the
|
|
31
|
+
whole story for `dns-policy-drift.ts`.
|
|
32
|
+
|
|
33
|
+
## `id`/`metadata` are attributes-only, same split as every other family here
|
|
34
|
+
|
|
35
|
+
`DnsPolicyProps` mirrors `UpdateDnsPolicyRequest`, not `DNSPolicy`: `id` and `metadata.origin` are
|
|
36
|
+
server-derived (no writable field accepts either), reported in `DnsPolicyAttributes` but never
|
|
37
|
+
compared or declared — the same split `network-form.ts` and `firewall-zone-form.ts` document for
|
|
38
|
+
their own server-derived fields.
|
|
39
|
+
|
|
40
|
+
## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26, corrected same day)
|
|
41
|
+
|
|
42
|
+
Run together with `Unifi.AclRule`'s own gate, since both tags were diffed in the same pass —
|
|
43
|
+
**full breakdown, pin/mirror checksums and mirror commit are in
|
|
44
|
+
[`docs/unifi-acl-rule.md`](./unifi-acl-rule.md), not repeated here.** Summary for this family: all
|
|
45
|
+
5 `DNS Policies` operations are byte-identical between 10.4.57 and 10.6.97 (parameters, request
|
|
46
|
+
body, responses, unresolved `$ref`s included). ⚠️ The schema closure is **46 schemas, not 17** — a
|
|
47
|
+
plain `$ref` walk misses everything reachable only through `discriminator.mapping` (see "One flat
|
|
48
|
+
DTO" above), which for this tag is all 7 DNS record variant DTOs plus their 7 create/update
|
|
49
|
+
siblings. Re-run with `$ref` + `mapping` both followed, the full 46-schema closure (both tags
|
|
50
|
+
together) shares zero names with the 14 schemas that changed anywhere in the document between the
|
|
51
|
+
two versions — all 14 are `Switching`-, `Filtering`- or one `UniFi Devices` enum-shaped, none of it
|
|
52
|
+
DNS- or ACL-shaped. **Answer unchanged: `Unifi.DnsPolicy` would not decode differently against a
|
|
53
|
+
10.6.97 console** — the conclusion survives the correction; only the closure method and count were
|
|
54
|
+
wrong. **Method note for the next family (B4+): always follow `discriminator.mapping`, not just
|
|
55
|
+
`$ref` — a `$ref`-only walk silently under-counts any tag with a discriminated schema.**
|
|
56
|
+
|
|
57
|
+
## The declaration renderer
|
|
58
|
+
|
|
59
|
+
`declareDnsPolicy(live, siteId)` is a pure function, same contract as `declareNetwork`/
|
|
60
|
+
`declareFirewallZone` (`docs/unifi.md`): given one `getDnsPolicy` response, it returns the `Props`
|
|
61
|
+
a declaration needs so its plan is a no-op. A later import script reads every DNS policy on a site
|
|
62
|
+
and writes one `dnsPolicy('<id>', declareDnsPolicy(live, siteId))` row per result — nothing here
|
|
63
|
+
performs that write.
|
|
64
|
+
|
|
65
|
+
## Field-level drift
|
|
66
|
+
|
|
67
|
+
`dns-policy.ts` exports a pure `driftOf(live, props)` (`dns-policy-drift.ts`), built on `drift.ts`'s
|
|
68
|
+
`makeDriftOf` framework — the same one `Unifi.Network`/`Unifi.FirewallZone`/`Unifi.AclRule` use.
|
|
69
|
+
`matches` is exactly `fieldDrift(...).length === 0`, never a second hand-written comparison
|
|
70
|
+
(MEDIUM-4, `network-drift.ts`'s own header).
|
|
71
|
+
|
|
72
|
+
## Vendor version
|
|
73
|
+
|
|
74
|
+
UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
|
|
75
|
+
`@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.2.0`.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# UniFi Firewall Policies — `Unifi.FirewallPolicy` / `Unifi.FirewallPolicyOrdering`
|
|
2
|
+
|
|
3
|
+
⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
|
|
4
|
+
`Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
|
|
5
|
+
`GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
|
|
6
|
+
transport. Neither family here calls `createFirewallPolicy`, `updateFirewallPolicy`,
|
|
7
|
+
`patchFirewallPolicy`, `deleteFirewallPolicy` or `updateFirewallPolicyOrdering` anywhere —
|
|
8
|
+
`write-op-reference.test.ts` proves it statically.
|
|
9
|
+
|
|
10
|
+
## Two resources, because policy order is not a policy's own property (T5)
|
|
11
|
+
|
|
12
|
+
`Unifi.FirewallPolicy` is one policy (`getFirewallPolicy`/`getFirewallPolicies`).
|
|
13
|
+
`Unifi.FirewallPolicyOrdering` is the ordered `before`/`afterSystemDefined` policy-id lists for ONE
|
|
14
|
+
`(sourceFirewallZoneId, destinationFirewallZoneId)` zone pair (`getFirewallPolicyOrdering`,
|
|
15
|
+
`GET /v1/sites/{siteId}/firewall/policies/ordering?sourceFirewallZoneId=…&destinationFirewallZoneId=…`)
|
|
16
|
+
— unlike `Unifi.AclRuleOrdering` (`docs/unifi-acl-rule.md`), which is site-wide, because firewall
|
|
17
|
+
policies are scoped to a zone pair and the vendor keys ordering the same way. There is no "list
|
|
18
|
+
every zone pair" endpoint; a caller has to already know which pairs exist, typically by walking
|
|
19
|
+
`getFirewallPolicies` (T9 — see below) and grouping by each policy's `source.zoneId`/
|
|
20
|
+
`destination.zoneId`. Neither resource performs that discovery.
|
|
21
|
+
|
|
22
|
+
⛔ **`beforeSystemDefined`/`afterSystemDefined` ARE NEVER RUN THROUGH `sortedSet`.** Both are
|
|
23
|
+
sequences — within each half, position is priority — the same "order IS the value" reasoning
|
|
24
|
+
`docs/unifi-acl-rule.md` gives for `orderedAclRuleIds`. `firewall-policy-ordering-form.ts` compares
|
|
25
|
+
each half independently with plain `deepEqual`, so a policy moving from one half to the other
|
|
26
|
+
reports as drift on BOTH fields rather than one combined reorder.
|
|
27
|
+
|
|
28
|
+
## `Unifi.FirewallPolicy`'s own fields
|
|
29
|
+
|
|
30
|
+
- **`action`/`destination`/`ipProtocolScope`/`schedule`/`source` are compound, compared wholesale.**
|
|
31
|
+
Post-A3 (`8ee11f1`), these decode as real per-discriminator TS unions instead of `unknown`
|
|
32
|
+
(`discriminated-filter-decode.test.ts` proves the wire path keeps every key, including one this
|
|
33
|
+
pinned spec doesn't yet declare) — but this PR does not go on to build a leaf-by-leaf declarable
|
|
34
|
+
schema for all 12 filter variants underneath them. Each is compared as ONE value with plain
|
|
35
|
+
`deepEqual` (`stripNullish`), the same proportion `acl-rule-form.ts`'s `enforcingDeviceFilter`
|
|
36
|
+
gets (one level of structure, not exploded into scalars).
|
|
37
|
+
⚠️ **Known gap (T15, same shape as `acl-rule-form.ts`'s own)**: several of the vendor's filter
|
|
38
|
+
variants nested inside these fields are themselves set-like arrays with no documented order
|
|
39
|
+
(`FirewallPolicyMACAddressFilter.macAddresses`, `FirewallPolicyNetworkFilter.networkIds`,
|
|
40
|
+
`FirewallPolicyApplicationFilter.applicationIds`, `FirewallPolicyIPAddressFilter.items`,
|
|
41
|
+
`FirewallPolicyRegionFilter.regions`, `FirewallPolicyVPNServerFilter.vpnServerIds`,
|
|
42
|
+
`FirewallSchedule.repeatOnDays`) — `deepEqual` compares them order-sensitively, so a console-side
|
|
43
|
+
reorder with no membership change would report a spurious drift this read-only family's
|
|
44
|
+
`reconcile` then refuses forever. Accepted, not fixed: normalizing 12 filter variants' own nested
|
|
45
|
+
arrays is bigger, separate modelling work, not a mechanical extension of A3.
|
|
46
|
+
- **`connectionStateFilter` is the one top-level Props array, and is a SET** — "matches all
|
|
47
|
+
connection states" if null describes membership, not a sequence (unlike the ordering resource's
|
|
48
|
+
own lists, where position IS the value). Normalized with the same `sortedSet` every other family
|
|
49
|
+
here uses for a set-shaped array.
|
|
50
|
+
- **`index` is attributes-only, never declared.** `CreateFirewallPolicyRequest` has no `index`
|
|
51
|
+
field — the live object's position among ALL policies on the site is reported, but its priority
|
|
52
|
+
relative to its own zone pair is `Unifi.FirewallPolicyOrdering`'s concern, same split
|
|
53
|
+
`acl-rule-form.ts`'s own `index` documents.
|
|
54
|
+
- **`metadata.origin` is attributes-only**, same split every family here uses.
|
|
55
|
+
|
|
56
|
+
## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26)
|
|
57
|
+
|
|
58
|
+
Same method `docs/unifi-acl-rule.md` used, run here for the `Firewall` tag's 13 operations (5
|
|
59
|
+
already covered for `Unifi.FirewallZone` in `docs/unifi.md`; 8 new here: `getFirewallPolicies`,
|
|
60
|
+
`createFirewallPolicy`, `getFirewallPolicyOrdering`, `updateFirewallPolicyOrdering`,
|
|
61
|
+
`deleteFirewallPolicy`, `getFirewallPolicy`, `patchFirewallPolicy`, `updateFirewallPolicy`).
|
|
62
|
+
|
|
63
|
+
- **Pin**: `~/.cache/homeflare/schemas/unifi/network_v10.4.57_openapi.json`, re-verified against
|
|
64
|
+
its own `SHA256SUMS` today (`3773947b…c9df59d`).
|
|
65
|
+
- **Mirror**: `raw.githubusercontent.com/beezly/unifi-apis/main/unifi-network/10.6.97.json`,
|
|
66
|
+
diffing aid only, never a spec of record — re-fetched today, still `info.version: "10.6.97"`,
|
|
67
|
+
`sha256: 44fd0e7f81603bb1279889a528a443d8f1a0102436e4a29675d8451adf32bd78`, unchanged since mirror
|
|
68
|
+
commit `f979dd64ec0ea6008c2a698c278fe49a10e102c8` (Andrew Beresford, 2026-08-20, "Add UniFi API
|
|
69
|
+
specs: Network 10.6.97", GitHub-verified signature) — the same file `docs/unifi-acl-rule.md` cites.
|
|
70
|
+
- **Operation count**: both versions carry exactly the same 13 operations for `Firewall` — zero
|
|
71
|
+
added, zero removed, either direction.
|
|
72
|
+
- **Full raw operation objects** (parameters, request body, responses, unresolved `$ref`s included):
|
|
73
|
+
byte-identical for all 13, both versions.
|
|
74
|
+
- **Reachability** (`$ref` AND `discriminator.mapping`, per `docs/unifi-acl-rule.md`'s corrected
|
|
75
|
+
method): the closure is **106 schema names**, identical set in both versions. Every one of the 12
|
|
76
|
+
filter variants' own sub-schemas (`IntegrationFirewallPolicySourceMacAddressFilterDto`, all three
|
|
77
|
+
IP-protocol-scope Dto families, both action Dtos, etc.) is in it. **Zero overlap** with the 14
|
|
78
|
+
schemas that changed elsewhere in the document (the same 9 `Switching`, 4 `Filtering`, 1 mDNS
|
|
79
|
+
schemas `docs/unifi.md`/`docs/unifi-acl-rule.md` already found) — re-diffed all 106 with an
|
|
80
|
+
order-independent structural compare: **zero differ**.
|
|
81
|
+
- **Corroboration**: the A3 changeset's own broader re-check
|
|
82
|
+
(`.changeset/unifi-network-a3-typed-discriminator-filters.md`) independently walked all 25
|
|
83
|
+
FirewallPolicy/ACL rule/TrafficMatchingList operations together — a 139-schema closure, also
|
|
84
|
+
byte-identical between versions — against a mirror fetch the same day citing commit
|
|
85
|
+
`2457b337173c83599c583993210218b1b6c0f8e7` (that commit only touches `README.md`; the spec file
|
|
86
|
+
itself has been untouched, hence unchanged, since `f979dd64` above).
|
|
87
|
+
|
|
88
|
+
**Answer: neither `FirewallPolicy` nor `FirewallPolicyOrdering` would decode differently against
|
|
89
|
+
10.6.97.** This does not generalize past 10.6.97 or to any other tag — re-run this check, following
|
|
90
|
+
BOTH `$ref` and `discriminator.mapping`, before importing or declaring against `Clients`,
|
|
91
|
+
`Switching`, or any tag not yet covered by an existing `docs/unifi-*.md`.
|
|
92
|
+
|
|
93
|
+
## The declaration renderers
|
|
94
|
+
|
|
95
|
+
`declareFirewallPolicy(live, siteId)` and `declareFirewallPolicyOrdering(live, siteId, sourceZoneId,
|
|
96
|
+
destZoneId)` are pure functions, same contract as every other family's own (`docs/unifi.md`): given
|
|
97
|
+
one live read, they return the `Props` a declaration needs so its plan is a no-op.
|
|
98
|
+
|
|
99
|
+
## Field-level drift
|
|
100
|
+
|
|
101
|
+
`firewall-policy.ts`/`firewall-policy-ordering.ts` each export a pure `driftOf(live, props)`
|
|
102
|
+
(`firewall-policy-drift.ts`, `firewall-policy-ordering-form.ts`), built on the same `drift.ts`'s
|
|
103
|
+
`makeDriftOf` framework as every other family — `matches` is exactly `fieldDrift(...).length === 0`,
|
|
104
|
+
never a second hand-written comparison (MEDIUM-4).
|
|
105
|
+
|
|
106
|
+
## Vendor version
|
|
107
|
+
|
|
108
|
+
UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
|
|
109
|
+
`@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.3.0`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# UniFi Traffic Matching Lists — `Unifi.TrafficMatchingList`
|
|
2
|
+
|
|
3
|
+
⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
|
|
4
|
+
`Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
|
|
5
|
+
`GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
|
|
6
|
+
transport. This family never calls `createTrafficMatchingList`, `updateTrafficMatchingList` or
|
|
7
|
+
`deleteTrafficMatchingList` anywhere — `write-op-reference.test.ts` proves it statically.
|
|
8
|
+
|
|
9
|
+
## The simplest object shape in this directory
|
|
10
|
+
|
|
11
|
+
`TrafficMatchingList` is `{id, name, type, items?}` — no `metadata` field at all, unlike every
|
|
12
|
+
other family here (`FirewallZone`/`DNSPolicy`/`ACLRule`/`FirewallPolicy` all carry a server-derived
|
|
13
|
+
`metadata.origin`). Nothing in `TrafficMatchingListAttributes` is attributes-only the way
|
|
14
|
+
`metadataOrigin`/`index` are elsewhere: every field this family reads is also declarable.
|
|
15
|
+
|
|
16
|
+
## `items` is a real TS union post-A3, compared wholesale
|
|
17
|
+
|
|
18
|
+
`items` decodes as `Array<IPv4Matching> | Array<IPv6Matching> | Array<PortMatching>`, keyed by the
|
|
19
|
+
sibling `type` field (A3, `8ee11f1`) — previously opaque `unknown`. The runtime schema is still a
|
|
20
|
+
passthrough `S.Unknown` (Argo CD's `union:` callback): narrower for a reader at the TYPE level only,
|
|
21
|
+
so decode keeps every key including one this pinned spec doesn't yet declare, the same proof
|
|
22
|
+
`discriminated-filter-decode.test.ts` runs for `FirewallPolicy`/`AclRule`.
|
|
23
|
+
|
|
24
|
+
⚠️ **Known gap (T15, same shape `acl-rule-form.ts`/`docs/unifi-firewall-policy.md` both carry)**: a
|
|
25
|
+
traffic matching list's `items` is semantically a SET of match entries — matching is "is this
|
|
26
|
+
traffic covered by ANY entry," not order-dependent — but each entry is an OBJECT
|
|
27
|
+
(`{type, value?, start?, stop?}`), not a bare string, so there is no cheap canonical sort key the
|
|
28
|
+
way `sortedSet` gives a `string[]`. `deepEqual` compares `items` order-sensitively, so a
|
|
29
|
+
console-side reorder with no membership change would report a spurious drift this read-only
|
|
30
|
+
family's `reconcile` then refuses forever. Accepted, not fixed: a generic object-set normalizer is
|
|
31
|
+
bigger, separate modelling work, not a mechanical extension of typing `items` per-discriminator.
|
|
32
|
+
|
|
33
|
+
## T11 gate: per-tag closure diff, 10.4.57 vs 10.6.97 (checked 2026-09-26)
|
|
34
|
+
|
|
35
|
+
Same method `docs/unifi-acl-rule.md`/`docs/unifi-firewall-policy.md` use, run here for the
|
|
36
|
+
`Traffic Matching Lists` tag's 5 operations (`getTrafficMatchingLists`, `createTrafficMatchingList`,
|
|
37
|
+
`deleteTrafficMatchingList`, `getTrafficMatchingList`, `updateTrafficMatchingList`).
|
|
38
|
+
|
|
39
|
+
- **Pin**: `~/.cache/homeflare/schemas/unifi/network_v10.4.57_openapi.json`, re-verified against
|
|
40
|
+
its own `SHA256SUMS` today (`3773947b…c9df59d`).
|
|
41
|
+
- **Mirror**: `raw.githubusercontent.com/beezly/unifi-apis/main/unifi-network/10.6.97.json`,
|
|
42
|
+
diffing aid only, never a spec of record — re-fetched today, still `info.version: "10.6.97"`,
|
|
43
|
+
`sha256: 44fd0e7f81603bb1279889a528a443d8f1a0102436e4a29675d8451adf32bd78`, unchanged since mirror
|
|
44
|
+
commit `f979dd64ec0ea6008c2a698c278fe49a10e102c8` (Andrew Beresford, 2026-08-20, "Add UniFi API
|
|
45
|
+
specs: Network 10.6.97", GitHub-verified signature) — the same file every other family doc cites.
|
|
46
|
+
- **Operation count**: both versions carry exactly the same 5 operations for `Traffic Matching
|
|
47
|
+
Lists` — zero added, zero removed, either direction.
|
|
48
|
+
- **Full raw operation objects** (parameters, request body, responses, unresolved `$ref`s included):
|
|
49
|
+
byte-identical for all 5, both versions.
|
|
50
|
+
- **Reachability** (`$ref` AND `discriminator.mapping`): the closure is **19 schema names**
|
|
51
|
+
(`IPv4Matching`/`IPv6Matching`/`Port matching` and each of the three list-shape Dto pairs, plus
|
|
52
|
+
their subnet/range/address variant Dtos), identical set in both versions. **Zero overlap** with
|
|
53
|
+
the 14 schemas that changed elsewhere in the document — re-diffed all 19 with an
|
|
54
|
+
order-independent structural compare: **zero differ**.
|
|
55
|
+
- **Corroboration**: the A3 changeset's own broader re-check
|
|
56
|
+
(`.changeset/unifi-network-a3-typed-discriminator-filters.md`) independently walked all 25
|
|
57
|
+
FirewallPolicy/ACL rule/TrafficMatchingList operations together — a 139-schema closure, also
|
|
58
|
+
byte-identical between versions — against a mirror fetch the same day; see
|
|
59
|
+
`docs/unifi-firewall-policy.md`'s own T11 section for why that citation's different-looking
|
|
60
|
+
mirror commit is the same unchanged file.
|
|
61
|
+
|
|
62
|
+
**Answer: `TrafficMatchingList` would not decode differently against 10.6.97.** This does not
|
|
63
|
+
generalize past 10.6.97 or to any other tag — re-run this check, following BOTH `$ref` and
|
|
64
|
+
`discriminator.mapping`, before importing or declaring against `Clients`, `Switching`, or any tag
|
|
65
|
+
not yet covered by an existing `docs/unifi-*.md`.
|
|
66
|
+
|
|
67
|
+
## The declaration renderer
|
|
68
|
+
|
|
69
|
+
`declareTrafficMatchingList(live, siteId)` is a pure function, same contract as every other
|
|
70
|
+
family's own (`docs/unifi.md`): given one `getTrafficMatchingList` response, it returns the `Props`
|
|
71
|
+
a declaration needs so its plan is a no-op.
|
|
72
|
+
|
|
73
|
+
## Field-level drift
|
|
74
|
+
|
|
75
|
+
`traffic-matching-list.ts` exports a pure `driftOf(live, props)` (`traffic-matching-list-drift.ts`),
|
|
76
|
+
built on the same `drift.ts`'s `makeDriftOf` framework as every other family — `matches` is exactly
|
|
77
|
+
`fieldDrift(...).length === 0`, never a second hand-written comparison (MEDIUM-4).
|
|
78
|
+
|
|
79
|
+
## Vendor version
|
|
80
|
+
|
|
81
|
+
UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
|
|
82
|
+
`@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.3.0`.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# UniFi WiFi Broadcasts — `Unifi.WifiBroadcast`
|
|
2
|
+
|
|
3
|
+
⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** Same posture as `docs/unifi.md`'s `Unifi.Network`/
|
|
4
|
+
`Unifi.FirewallZone`: `reconcile`/`delete` both fail with a typed `UnifiWriteRefused`, and the
|
|
5
|
+
`GetOnlyHttpClient` wire guard (`resource.ts`) refuses any non-`GET` request before it reaches the
|
|
6
|
+
transport. This family calls no SDK write op anywhere — `write-op-reference.test.ts` proves it
|
|
7
|
+
statically, and `wifi-broadcast.test.ts`'s own write-path tests prove `reconcile`/`destroy` never
|
|
8
|
+
send anything but `GET`s.
|
|
9
|
+
|
|
10
|
+
## T23: the passphrase-bearing details call is never made
|
|
11
|
+
|
|
12
|
+
The pinned spec's WiFi Broadcast "details" shape (`getWifiBroadcastDetails`, and the create/update
|
|
13
|
+
request bodies) — `WifiSecurityConfigurationDetailObject` — carries the WPA passphrase AND every
|
|
14
|
+
PPSK entry's own passphrase, with **no `writeOnly` flag on either** (verified: 4 `passphrase`
|
|
15
|
+
properties, 0 `writeOnly` flags, in the staged 10.4.57 spec). `README.md`'s claim that these fields
|
|
16
|
+
are "write-only" was wrong; A2 (kit PR #300) fixed the SDK layer by decoding them `Redacted`
|
|
17
|
+
instead of a plain string, proven at the wire in `errors-and-secrets.test.ts`.
|
|
18
|
+
|
|
19
|
+
This family goes further and **never calls that shape at all.** The only SDK call
|
|
20
|
+
`wifi-broadcast.ts` makes is `getWifiBroadcastPage` — the LIST endpoint, which answers
|
|
21
|
+
`WifiBroadcastOverview` rows. That type's own `securityConfiguration`
|
|
22
|
+
(`WifiSecurityConfigurationOverview`) is `{type, presharedKeyNetworkIds}`: no passphrase field
|
|
23
|
+
exists on it, so there is no DECLARED field for `attributesOf`/`declareWifiBroadcast` to name a
|
|
24
|
+
passphrase into. A static guard (`wifi-broadcast-details-call.test.ts`) additionally bans any
|
|
25
|
+
source reference to `getWifiBroadcastDetails` itself under `src/unifi` (red team, IMPORTANT-2),
|
|
26
|
+
with one named exception: `errors-and-secrets.test.ts` (A2) calls it directly on purpose, to prove
|
|
27
|
+
the SDK's own redaction happens at the wire — a different, lower-layer claim than this family ever
|
|
28
|
+
needing to make that call.
|
|
29
|
+
|
|
30
|
+
⛔ **This is NOT enforced by the type system alone** (red team, IMPORTANT-1) — corrected from an
|
|
31
|
+
earlier version of this doc that overclaimed it was. `@distilled.cloud/core`'s wire decode does not
|
|
32
|
+
strip a key a schema doesn't declare, so a stray key nested inside `network`, `hotspotConfiguration`,
|
|
33
|
+
`broadcastingDeviceFilter`, or one `presharedKeyNetworkIds` element could ride through a naive
|
|
34
|
+
pass-through exactly like the top-level `securityConfiguration` object could. See "The stray-key
|
|
35
|
+
defense" below — enumeration at runtime, not the TS annotation, is what actually stops it.
|
|
36
|
+
|
|
37
|
+
### No get-by-id call exists for the overview shape
|
|
38
|
+
|
|
39
|
+
Unlike every other family here (`getNetworkDetails`, `getFirewallZone`, `getDnsPolicy`,
|
|
40
|
+
`getAclRule`), `wifi_broadcasts.ts` has no single-object GET that returns the overview shape — only
|
|
41
|
+
the list (safe) and the details call (forbidden above). `wifi-broadcast.ts`'s `fetchLive` walks
|
|
42
|
+
every page with B0b's `pageAll` (`paginate.ts`) and finds the row matching `wifiBroadcastId` — the
|
|
43
|
+
first real resource-level consumer of that pager, previously exercised only in isolation by
|
|
44
|
+
`paginate.test.ts`. A genuine site-not-found 404 on ANY page of the walk — not only the first —
|
|
45
|
+
folds to `undefined` like every other family, discarding whatever rows the walk had already
|
|
46
|
+
collected (red team, MINOR-4: an earlier version of this doc said "on the first page call", which
|
|
47
|
+
overstated how narrowly the catch is scoped); the target id simply not being among the collected
|
|
48
|
+
rows folds to `undefined` too — there is no separate vendor signal to tell any of these cases apart,
|
|
49
|
+
and no other family's `fetchLive` has one either.
|
|
50
|
+
|
|
51
|
+
### The stray-key defense (why `normalizeSecurityConfiguration` never spreads)
|
|
52
|
+
|
|
53
|
+
Nothing in `@distilled.cloud/core`'s wire key-mapping strips an object key the schema does not
|
|
54
|
+
declare — it only renames declared ones (measured directly against the installed
|
|
55
|
+
`@distilled.cloud/core@1.0.0-rc.12`: a struct decode keeps every key from the raw response,
|
|
56
|
+
declared or not). So a stray `passphrase`-shaped key on the WIRE `securityConfiguration` object — a
|
|
57
|
+
vendor bug, or a future console version — physically survives into the `live` object `fetchLive`
|
|
58
|
+
returns, even though `WifiBroadcastOverview`'s own TS type has no such field.
|
|
59
|
+
|
|
60
|
+
`wifi-broadcast-form.ts`'s `normalizeSecurityConfiguration` is the one normalizer in this whole
|
|
61
|
+
`Unifi.*` tree that does **not** use `{...value}` (every sibling normalizer, `network-form.ts`'s
|
|
62
|
+
included, spreads its input). It enumerates exactly `{type, presharedKeyNetworkIds}` instead —
|
|
63
|
+
`attributesOf`/`declareWifiBroadcast` are built the same way, field by field, never `{...live}`.
|
|
64
|
+
|
|
65
|
+
⛔ **The same discipline applies to every OTHER nested object this family handles, not just
|
|
66
|
+
`securityConfiguration`** (red team, IMPORTANT-1) — `normalizeRef` rebuilds `network` and each
|
|
67
|
+
`presharedKeyNetworkIds` element as `{type, networkId?}`, `normalizeHotspot` rebuilds
|
|
68
|
+
`hotspotConfiguration` as `{type}`, and `normalizeDeviceFilter` no longer spreads
|
|
69
|
+
`broadcastingDeviceFilter` either. A `presharedKeyNetworkIds` element is the single most plausible
|
|
70
|
+
leak site of the four: `IntegrationWifiPresharedKeyDto`, the details-call shape for a PPSK entry, is
|
|
71
|
+
literally `{network: WifiNetworkReference, passphrase}` — the same reference shape this family's
|
|
72
|
+
overview also uses, just with one more field the overview type doesn't declare.
|
|
73
|
+
|
|
74
|
+
`wifi-broadcast-secrets.test.ts`'s sentinel tests prove the pipeline for all five locations
|
|
75
|
+
(`securityConfiguration`, a `presharedKeyNetworkIds` element, `network`, `hotspotConfiguration`,
|
|
76
|
+
`broadcastingDeviceFilter`): each plants a raw sentinel key that genuinely does survive into `live`
|
|
77
|
+
(asserted directly, not assumed away), then proves `attributesOf`, `declareWifiBroadcast`, and the
|
|
78
|
+
literal string a later import script would render into `alchemy.run.ts` none of them carry it
|
|
79
|
+
forward.
|
|
80
|
+
|
|
81
|
+
### The forced decode-failure test
|
|
82
|
+
|
|
83
|
+
`wifi-broadcast-secrets.test.ts`'s decode-failure test sends a non-JSON 200 body (a genuine decode
|
|
84
|
+
failure — this SDK's REST decode has no schema-validating step to reject a malformed body with; see
|
|
85
|
+
`@distilled.cloud/core`'s `protocol-rest.ts`) that happens to contain a sentinel value, the way a
|
|
86
|
+
garbled proxy or debug response might. `pageAll`'s own consistency check (`paginate.ts`) catches it
|
|
87
|
+
— the malformed "page" has no `.offset`, so the walk refuses to trust it — and fails typed as
|
|
88
|
+
`UnifiPaginationInconsistent`. That error's `reason`/`detail`/`message` are built from `offset`/
|
|
89
|
+
`totalCount` values run through `paginate.ts`'s `numOrPlaceholder` (red team, MINOR-3: an earlier
|
|
90
|
+
version of this doc said these fields were "never from the raw response body", but `offset`/
|
|
91
|
+
`totalCount` themselves ARE unvalidated wire values — `paginate.test.ts`'s own test proves a
|
|
92
|
+
non-numeric one renders as a fixed placeholder, never as itself), so none of them can ever echo
|
|
93
|
+
attacker- or vendor-controlled text regardless of what the server actually sent.
|
|
94
|
+
|
|
95
|
+
## Field-level drift
|
|
96
|
+
|
|
97
|
+
Three fields are membership SETS, not ordered sequences, by the vendor description's own wording
|
|
98
|
+
("Defines the custom scope of devices…") and unconfirmed against a live console (same caveat
|
|
99
|
+
`network-form.ts` carries for its own three): `broadcastingDeviceFilter.deviceIds`/`.deviceTagIds`
|
|
100
|
+
and `securityConfiguration.presharedKeyNetworkIds`. `wifi-broadcast-form.ts`'s
|
|
101
|
+
`normalizeDeviceFilter`/`normalizeSecurityConfiguration`/`normalizeFrequencies` sort+dedupe them
|
|
102
|
+
before comparison, on both the live AND declared sides (`wifi-broadcast-drift.ts`), the same
|
|
103
|
+
`firewall-zone-form.ts`'s `networkIds` pattern — otherwise a console returning the same set in a
|
|
104
|
+
different order would plan a spurious `update` that a read-only `reconcile` then refuses.
|
|
105
|
+
|
|
106
|
+
`wifi-broadcast.ts` exports a pure `driftOf(live, props)`, built on `drift.ts`'s `makeDriftOf`
|
|
107
|
+
framework. `matches` is exactly `fieldDrift(...).length === 0` (MEDIUM-4).
|
|
108
|
+
|
|
109
|
+
## The declaration renderer
|
|
110
|
+
|
|
111
|
+
`declareWifiBroadcast(live, siteId)` is a pure function: given one `WifiBroadcastOverview` row (as
|
|
112
|
+
`getWifiBroadcastPage` returns it, never `getWifiBroadcastDetails`), it returns the `Props` a
|
|
113
|
+
declaration needs so its plan is a no-op. A later import script pages every WiFi broadcast on a
|
|
114
|
+
site and writes one `wifiBroadcast('<id>', declareWifiBroadcast(live, siteId))` row per result —
|
|
115
|
+
nothing here performs that write.
|
|
116
|
+
|
|
117
|
+
## Vendor version
|
|
118
|
+
|
|
119
|
+
UniFi Network Integration API **10.4.57**, same pin as `docs/unifi.md`, via
|
|
120
|
+
`@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.3.0`.
|
package/docs/unifi.md
CHANGED
|
@@ -6,6 +6,11 @@ with a typed `UnifiWriteRefused` naming the policy rather than calling any SDK w
|
|
|
6
6
|
Lifting the rule is a kit change — a PR that adds a write path, reviewed as one — not a flag a
|
|
7
7
|
stack can pass. See `packages/alchemy/src/unifi/policy.ts`.
|
|
8
8
|
|
|
9
|
+
This page covers `Unifi.Network`/`Unifi.FirewallZone` only. Every other read-only family has its
|
|
10
|
+
own doc, per family (`docs/unifi.md` would breach the 200-line cap otherwise): [`Unifi.DnsPolicy`](./unifi-dns-policy.md), [`Unifi.AclRule`/`Unifi.AclRuleOrdering`](./unifi-acl-rule.md),
|
|
11
|
+
[`Unifi.WifiBroadcast`](./unifi-wifi.md), [`Unifi.FirewallPolicy`/`Unifi.FirewallPolicyOrdering`](./unifi-firewall-policy.md),
|
|
12
|
+
and [`Unifi.TrafficMatchingList`](./unifi-traffic-matching-list.md).
|
|
13
|
+
|
|
9
14
|
## What's declared, and why these two first
|
|
10
15
|
|
|
11
16
|
`Unifi.Network` (`getNetworkDetails`/`getNetworksOverviewPage`) and `Unifi.FirewallZone`
|
|
@@ -13,9 +18,8 @@ stack can pass. See `packages/alchemy/src/unifi/policy.ts`.
|
|
|
13
18
|
not runtime state (a connected client, a device statistic, a hotspot voucher, a switch stack).
|
|
14
19
|
`Site` was the SDK README's other first suggestion; it has no `getSite`, only the list operation,
|
|
15
20
|
so it fails this house's own "both a list and a get" bar for a first import. `FirewallPolicy` is
|
|
16
|
-
also a list+get config object
|
|
17
|
-
|
|
18
|
-
endpoint replaces the whole rule list — modelling that correctly is a bigger, separate PR.
|
|
21
|
+
also a list+get config object; modelling it correctly needed its own PR (now shipped — [`docs/unifi-firewall-policy.md`](./unifi-firewall-policy.md)) because its ordering endpoint replaces the whole
|
|
22
|
+
rule list per zone pair, and it was gated on A3 typing its filter fields' discriminator variants first.
|
|
19
23
|
|
|
20
24
|
## Credentials
|
|
21
25
|
|
|
@@ -102,8 +106,9 @@ operations (`getNetworksOverviewPage`, `getFirewallZones`, …), none of which p
|
|
|
102
106
|
own — no `smithy.api#paginated` trait exists in the pinned spec, and neither the SDK nor
|
|
103
107
|
`@distilled.cloud/core` gained one for this. It stops on the response's `totalCount`, never on
|
|
104
108
|
`data.length < limit` (a page can legitimately come back short of the requested limit without
|
|
105
|
-
being the last page).
|
|
106
|
-
`
|
|
109
|
+
being the last page). `Unifi.WifiBroadcast` (`docs/unifi-wifi.md`) is its first resource-level
|
|
110
|
+
consumer, walking every page because that family has no get-by-id call at all; `docs/unifi-firewall-policy.md`'s own decode-proof test exercises the same page shape for `getFirewallPolicies`
|
|
111
|
+
(hundreds of rows on a real console — T9), though `Unifi.FirewallPolicy` itself still reads by id.
|
|
107
112
|
|
|
108
113
|
It also fails with a typed `UnifiPaginationInconsistent` — never silently returns a wrong row
|
|
109
114
|
set — the moment a paged response contradicts itself: a page's echoed `offset` doesn't match what
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
"types": "./dist/proxmox/index.d.ts",
|
|
57
57
|
"default": "./dist/proxmox/index.js"
|
|
58
58
|
},
|
|
59
|
+
"./ceph": {
|
|
60
|
+
"types": "./dist/ceph/index.d.ts",
|
|
61
|
+
"default": "./dist/ceph/index.js"
|
|
62
|
+
},
|
|
59
63
|
"./launchd": {
|
|
60
64
|
"types": "./dist/launchd/index.d.ts",
|
|
61
65
|
"default": "./dist/launchd/index.js"
|
|
@@ -127,11 +131,11 @@
|
|
|
127
131
|
"@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.2.0",
|
|
128
132
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
129
133
|
"@distilled.cloud/openbao": "npm:@homeflare/distilled-openbao@0.2.2",
|
|
130
|
-
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.
|
|
134
|
+
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.3.0",
|
|
131
135
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
132
136
|
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.3",
|
|
133
137
|
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
|
|
134
|
-
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.
|
|
138
|
+
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.4.0"
|
|
135
139
|
},
|
|
136
140
|
"peerDependencies": {
|
|
137
141
|
"@distilled.cloud/argocd": "1.0.0-rc.12",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Ceph.AuthEntity` (K-A4) — a Ceph client auth entity (`client.k8s-*`), minted over the ssh
|
|
3
|
+
* mon-command transport and stored only in OpenBao. Design: docs/plans/2026-09-26-ceph-mon-
|
|
4
|
+
* transport.md, "`Ceph.AuthEntity` (K-A4)"; the loop itself lives in ceph-auth-reconcile.ts, the
|
|
5
|
+
* transport in ceph-transport.ts, the allowlist in ceph-argv.ts.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ `read` AND `diff` NEVER SSH. "A plan never elevates" (../linux/sudo-runner.ts's own header
|
|
8
|
+
* makes the same rule for the file family): this family's props/state comparison is the whole
|
|
9
|
+
* of `diff`, and `read` hands back exactly the attributes state already holds. Live drift is
|
|
10
|
+
* caught only at `reconcile` (which does elevate, on the admin lane) and by the post-deploy
|
|
11
|
+
* operator check — this family cannot prove live convergence at plan time, and says so rather
|
|
12
|
+
* than pretending an unprivileged probe exists where none does.
|
|
13
|
+
* ⛔ `delete` IS REFUSED UNCONDITIONALLY. `defaultRemovalPolicy: 'retain'` keeps Alchemy from ever
|
|
14
|
+
* calling it on an ordinary `destroy`, and the handler itself refuses too — belt and suspenders
|
|
15
|
+
* with ceph-argv.ts's own refusal of the `auth del` shape, so there is no path, `--adopt` or a
|
|
16
|
+
* resource-level removal-policy override included, that ever reaches it (D3).
|
|
17
|
+
* ⛔ NEVER ADOPTED. See ceph-auth-reconcile.ts's header for why the generic `refuseTakeover` is not
|
|
18
|
+
* used here.
|
|
19
|
+
* ⛔ ROWS ARE CREATES THROUGH THE FIRST-CREATE GATE, NO `adopt()` — `list` answers empty, the same
|
|
20
|
+
* as every Ceph family in this kit (ceph-pool.ts's header has the reason a non-empty list would
|
|
21
|
+
* invite exactly the takeover this family refuses).
|
|
22
|
+
*/
|
|
23
|
+
import { Resource } from 'alchemy';
|
|
24
|
+
import { isResolved } from 'alchemy/Diff';
|
|
25
|
+
import * as Provider from 'alchemy/Provider';
|
|
26
|
+
import * as Effect from 'effect/Effect';
|
|
27
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
28
|
+
import type { CephAuthEntityAttributes, CephAuthEntityProps } from './ceph-auth-form.ts';
|
|
29
|
+
import { planCephAuthEntity } from './ceph-auth-form.ts';
|
|
30
|
+
import { reconcileCephAuthEntity } from './ceph-auth-reconcile.ts';
|
|
31
|
+
|
|
32
|
+
export type { CephAuthEntityAttributes, CephAuthEntityProps } from './ceph-auth-form.ts';
|
|
33
|
+
export type { CephCaps } from './ceph-auth-parse.ts';
|
|
34
|
+
|
|
35
|
+
export interface CephAuthEntity extends Resource<
|
|
36
|
+
'Ceph.AuthEntity',
|
|
37
|
+
CephAuthEntityProps,
|
|
38
|
+
CephAuthEntityAttributes,
|
|
39
|
+
never,
|
|
40
|
+
HttpClient.HttpClient
|
|
41
|
+
> {}
|
|
42
|
+
|
|
43
|
+
/** ★ `retain` — the delete handler below refuses regardless, but this stops Alchemy calling it. */
|
|
44
|
+
export const CephAuthEntity = Resource<CephAuthEntity>('Ceph.AuthEntity', {
|
|
45
|
+
defaultRemovalPolicy: 'retain',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const refusedDelete = (entity: string): Error =>
|
|
49
|
+
new Error(
|
|
50
|
+
`Ceph.AuthEntity ${entity}: 'ceph auth del' is refused entirely — lockout safety, never ` +
|
|
51
|
+
'auto-delete (D3). Nothing was sent to any mon. If the entity truly must go, remove it by ' +
|
|
52
|
+
'hand, deliberately, along with its OpenBao key — this family will never do it for you.',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const CephAuthEntityProvider = () =>
|
|
56
|
+
Provider.effect(
|
|
57
|
+
CephAuthEntity,
|
|
58
|
+
Effect.succeed(
|
|
59
|
+
CephAuthEntity.Provider.of({
|
|
60
|
+
list: () => Effect.succeed([]),
|
|
61
|
+
|
|
62
|
+
/** State only — see the header. Never `Unowned`: this family is never adopted. */
|
|
63
|
+
read: ({ output }) => Effect.succeed(output),
|
|
64
|
+
|
|
65
|
+
/** State only — see the header. `output === undefined` lets reconcile decide (a create). */
|
|
66
|
+
diff: ({ news, output }) =>
|
|
67
|
+
Effect.succeed(isResolved(news) ? planCephAuthEntity(news, output) : undefined),
|
|
68
|
+
|
|
69
|
+
reconcile: ({ news, output }) => reconcileCephAuthEntity(news, output, {}),
|
|
70
|
+
|
|
71
|
+
delete: ({ output }) => Effect.die(refusedDelete(output.entity)),
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ceph config get/set/rm` half of the argv allowlist — split out of ceph-argv.ts (which
|
|
3
|
+
* re-exports `configProblem`) purely to keep that file under the house's 250-line file cap; the
|
|
4
|
+
* split follows the same line the design docs already draw (2026-09-26-ceph-config-options.md is
|
|
5
|
+
* its own companion to 2026-09-26-ceph-mon-transport.md).
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ `<who>` IS NOT YET BOUNDED TO K-D's "declared daemons" (the config-options design's own
|
|
8
|
+
* acceptance test #1). `client.admin` still matches this shape. Latent today:
|
|
9
|
+
* `NAMED_CONFIG_OPTIONS` starts empty, so `namedOptionProblem` refuses every `config get/set/rm`
|
|
10
|
+
* regardless of `who` — LAND red team, 2026-09-26, flagged this as armed only once K-D's first
|
|
11
|
+
* named option lands, and narrowing it needs that family's own declared-daemon list, which does
|
|
12
|
+
* not exist yet. Tracked for `CephConfigOption` (K-D), not fixed here.
|
|
13
|
+
*/
|
|
14
|
+
import { NAMED_CONFIG_OPTIONS, isLockoutConfigOption } from './ceph-config-options.ts';
|
|
15
|
+
|
|
16
|
+
/** `config`'s `<who>`: a daemon class, optionally `.id` — never a path, never free text. */
|
|
17
|
+
const WHO = /^(global|mon|osd|mgr|mds|client)(\.[A-Za-z0-9_-]{1,32})?$/;
|
|
18
|
+
/** A ceph option name's own shape — snake_case, lowercase. Independent of the named list below. */
|
|
19
|
+
const OPTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
20
|
+
/**
|
|
21
|
+
* No shell metacharacters; the named list (below) is what actually decides secrecy, not this.
|
|
22
|
+
* ⛔ NO LEADING `-` EXCEPT A NEGATIVE INTEGER, AND NEVER EMPTY. LAND red team (2026-09-26),
|
|
23
|
+
* CONFIRMED: the prior pattern let a config VALUE start with `-`, and this argv reaches
|
|
24
|
+
* `sudo -n /usr/bin/ceph` with no shell in between — no shell means no quoting boundary, so
|
|
25
|
+
* `ceph config set global x -o/etc/ceph/ceph.conf` parses as ceph's own `-o <file>` flag, not
|
|
26
|
+
* as the string `-o/etc/ceph/ceph.conf`. Accepted flag-shaped values included
|
|
27
|
+
* `--admin-daemon=/var/run/ceph/ceph-mon.a.asok`, `--conf=/tmp/x`, `-n`, and the empty string.
|
|
28
|
+
* The sudoers entry has no argument restriction of its own (that is D4, a later step), so this
|
|
29
|
+
* allowlist is the only thing standing between a config value and an arbitrary ceph CLI flag.
|
|
30
|
+
* Latent while `NAMED_CONFIG_OPTIONS` is empty; K-D's first entry arms it.
|
|
31
|
+
*/
|
|
32
|
+
const CONFIG_VALUE = /^(?:[A-Za-z0-9][A-Za-z0-9 ._:/=,-]{0,255}|-[0-9]{1,255})$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `undefined` when `value` is a safe ceph config value — exported (like `boundedEntityProblem`)
|
|
36
|
+
* so the shape itself is directly testable, since `NAMED_CONFIG_OPTIONS` being empty today means
|
|
37
|
+
* `cephCommandProblem` never reaches this check on any real argv (`namedOptionProblem` refuses
|
|
38
|
+
* first) — this stays exercised until K-D's first named option arms the path for real.
|
|
39
|
+
*/
|
|
40
|
+
export const configValueProblem = (value: string): string | undefined =>
|
|
41
|
+
CONFIG_VALUE.test(value)
|
|
42
|
+
? undefined
|
|
43
|
+
: `config value ${JSON.stringify(value)} contains characters outside the safe set`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `undefined` when `name` may be declared, adopted, read by value or written — see
|
|
47
|
+
* ceph-config-options.ts's header for why the list starts empty and stays reviewed line by line.
|
|
48
|
+
*/
|
|
49
|
+
const namedOptionProblem = (name: string): string | undefined => {
|
|
50
|
+
if (!OPTION_NAME.test(name)) return `${JSON.stringify(name)} is not a plain ceph option name`;
|
|
51
|
+
if (isLockoutConfigOption(name))
|
|
52
|
+
return `${name} is a lockout-class option and can never be declared`;
|
|
53
|
+
return NAMED_CONFIG_OPTIONS.has(name)
|
|
54
|
+
? undefined
|
|
55
|
+
: `${name} is not on the named option list (it starts empty — see ceph-config-options.ts)`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** `config get|set|rm <who> <name> [value]`, called with the operands after `config` stripped. */
|
|
59
|
+
export const configProblem = (rest: readonly string[]): string | undefined => {
|
|
60
|
+
const [verb, who, name, ...extra] = rest;
|
|
61
|
+
if (who === undefined || !WHO.test(who)) {
|
|
62
|
+
return 'config takes a bounded `<who>` (global/mon/osd/mgr/mds/client[.id])';
|
|
63
|
+
}
|
|
64
|
+
if (name === undefined) return 'config takes a `<name>` from the named option list';
|
|
65
|
+
if (verb === 'get' || verb === 'rm') {
|
|
66
|
+
return extra.length === 0
|
|
67
|
+
? namedOptionProblem(name)
|
|
68
|
+
: `config ${String(verb)} takes exactly \`<who> <name>\``;
|
|
69
|
+
}
|
|
70
|
+
if (verb === 'set') {
|
|
71
|
+
const [value, ...more] = extra;
|
|
72
|
+
if (value === undefined || more.length !== 0)
|
|
73
|
+
return 'config set takes exactly `<who> <name> <value>`';
|
|
74
|
+
const named = namedOptionProblem(name);
|
|
75
|
+
return named !== undefined ? named : configValueProblem(value);
|
|
76
|
+
}
|
|
77
|
+
return `ceph config ${String(verb)} is not on the allowlist`;
|
|
78
|
+
};
|