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