@homeflare/alchemy 0.31.1 → 0.32.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 +44 -7
- package/dist/grafana/index.d.ts +6 -5
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/linux/container-form.d.ts +129 -0
- package/dist/linux/container-form.d.ts.map +1 -0
- package/dist/linux/container-generator.d.ts +74 -0
- package/dist/linux/container-generator.d.ts.map +1 -0
- package/dist/linux/container-handlers.d.ts +15 -0
- package/dist/linux/container-handlers.d.ts.map +1 -0
- package/dist/linux/container-lifecycle.d.ts +37 -0
- package/dist/linux/container-lifecycle.d.ts.map +1 -0
- package/dist/linux/container-preflight.d.ts +34 -0
- package/dist/linux/container-preflight.d.ts.map +1 -0
- package/dist/linux/container-secrets.d.ts +37 -0
- package/dist/linux/container-secrets.d.ts.map +1 -0
- package/dist/linux/container-settle.d.ts +34 -0
- package/dist/linux/container-settle.d.ts.map +1 -0
- package/dist/linux/container.d.ts +30 -0
- package/dist/linux/container.d.ts.map +1 -0
- package/dist/linux/fake-quadlet.d.ts +25 -0
- package/dist/linux/fake-quadlet.d.ts.map +1 -0
- package/dist/linux/index.d.ts +6 -3
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +641 -268
- package/dist/linux/index.js.map +15 -7
- package/dist/linux/providers.d.ts +1 -1
- package/dist/linux/providers.d.ts.map +1 -1
- package/dist/linux/systemctl.d.ts +7 -1
- package/dist/linux/systemctl.d.ts.map +1 -1
- package/dist/opnsense/alias-form.d.ts +29 -0
- package/dist/opnsense/alias-form.d.ts.map +1 -0
- package/dist/opnsense/alias.d.ts +81 -0
- package/dist/opnsense/alias.d.ts.map +1 -0
- package/dist/opnsense/category-form.d.ts +10 -0
- package/dist/opnsense/category-form.d.ts.map +1 -0
- package/dist/opnsense/category.d.ts +58 -0
- package/dist/opnsense/category.d.ts.map +1 -0
- package/dist/opnsense/credentials.d.ts +36 -0
- package/dist/opnsense/credentials.d.ts.map +1 -0
- package/dist/opnsense/fake-opnsense.d.ts +51 -0
- package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
- package/dist/opnsense/group-form.d.ts +11 -0
- package/dist/opnsense/group-form.d.ts.map +1 -0
- package/dist/opnsense/group.d.ts +62 -0
- package/dist/opnsense/group.d.ts.map +1 -0
- package/dist/opnsense/index.d.ts +16 -0
- package/dist/opnsense/index.d.ts.map +1 -0
- package/dist/opnsense/index.js +211 -0
- package/dist/opnsense/index.js.map +20 -0
- package/dist/opnsense/policy.d.ts +20 -0
- package/dist/opnsense/policy.d.ts.map +1 -0
- package/dist/opnsense/providers.d.ts +13 -0
- package/dist/opnsense/providers.d.ts.map +1 -0
- package/dist/opnsense/resource.d.ts +74 -0
- package/dist/opnsense/resource.d.ts.map +1 -0
- package/dist/opnsense/wire.d.ts +39 -0
- package/dist/opnsense/wire.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +44 -5
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/group.d.ts.map +1 -1
- package/dist/proxmox/index.js +326 -264
- package/dist/proxmox/index.js.map +13 -11
- package/dist/proxmox/role.d.ts.map +1 -1
- package/dist/proxmox/storage-form.d.ts +54 -0
- package/dist/proxmox/storage-form.d.ts.map +1 -0
- package/dist/proxmox/storage-wire.d.ts +56 -0
- package/dist/proxmox/storage-wire.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +55 -60
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +34 -5
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +6 -6
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/unifi/fake-unifi.d.ts +48 -0
- package/dist/unifi/fake-unifi.d.ts.map +1 -0
- package/dist/unifi/firewall-zone-form.d.ts +40 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
- package/dist/unifi/firewall-zone.d.ts +50 -0
- package/dist/unifi/firewall-zone.d.ts.map +1 -0
- package/dist/unifi/index.d.ts +13 -0
- package/dist/unifi/index.d.ts.map +1 -0
- package/dist/unifi/index.js +190 -0
- package/dist/unifi/index.js.map +16 -0
- package/dist/unifi/network-form.d.ts +67 -0
- package/dist/unifi/network-form.d.ts.map +1 -0
- package/dist/unifi/network.d.ts +67 -0
- package/dist/unifi/network.d.ts.map +1 -0
- package/dist/unifi/policy.d.ts +20 -0
- package/dist/unifi/policy.d.ts.map +1 -0
- package/dist/unifi/providers.d.ts +13 -0
- package/dist/unifi/providers.d.ts.map +1 -0
- package/dist/unifi/resource.d.ts +64 -0
- package/dist/unifi/resource.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +14 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/grafana.md +47 -17
- package/docs/opnsense.md +146 -0
- package/docs/provider-standard.md +1 -1
- package/docs/quadlet-container.md +132 -0
- package/docs/unifi.md +93 -0
- package/docs/upstream-conformance.md +14 -4
- package/package.json +13 -3
- package/src/grafana/index.ts +6 -5
- package/src/linux/container-fixture.test.ts +92 -0
- package/src/linux/container-form.ts +243 -0
- package/src/linux/container-generator.test.ts +195 -0
- package/src/linux/container-generator.ts +113 -0
- package/src/linux/container-handlers.ts +73 -0
- package/src/linux/container-lifecycle.test.ts +209 -0
- package/src/linux/container-lifecycle.ts +217 -0
- package/src/linux/container-preflight.ts +97 -0
- package/src/linux/container-secrets.test.ts +171 -0
- package/src/linux/container-secrets.ts +112 -0
- package/src/linux/container-settle.ts +97 -0
- package/src/linux/container.ts +60 -0
- package/src/linux/fake-quadlet.ts +127 -0
- package/src/linux/index.ts +11 -3
- package/src/linux/providers.ts +2 -0
- package/src/linux/systemctl.ts +9 -0
- package/src/opnsense/alias-form.test.ts +89 -0
- package/src/opnsense/alias-form.ts +53 -0
- package/src/opnsense/alias.test.ts +203 -0
- package/src/opnsense/alias.ts +117 -0
- package/src/opnsense/category-form.test.ts +54 -0
- package/src/opnsense/category-form.ts +20 -0
- package/src/opnsense/category.test.ts +138 -0
- package/src/opnsense/category.ts +82 -0
- package/src/opnsense/credentials.ts +41 -0
- package/src/opnsense/fake-opnsense.ts +79 -0
- package/src/opnsense/group-form.test.ts +75 -0
- package/src/opnsense/group-form.ts +25 -0
- package/src/opnsense/group.test.ts +141 -0
- package/src/opnsense/group.ts +78 -0
- package/src/opnsense/index.ts +45 -0
- package/src/opnsense/policy.ts +49 -0
- package/src/opnsense/providers.ts +21 -0
- package/src/opnsense/resource.ts +146 -0
- package/src/opnsense/wire.ts +56 -0
- package/src/opnsense/write-refusal.test.ts +148 -0
- package/src/proxmox/acl.ts +10 -4
- package/src/proxmox/api-token.ts +20 -12
- package/src/proxmox/group-wire.ts +62 -8
- package/src/proxmox/group.ts +23 -35
- package/src/proxmox/read-failure-propagation.test.ts +183 -0
- package/src/proxmox/role.ts +9 -2
- package/src/proxmox/storage-form.ts +144 -0
- package/src/proxmox/storage-read-failure.test.ts +214 -0
- package/src/proxmox/storage-wire.ts +124 -0
- package/src/proxmox/storage.ts +165 -175
- package/src/proxmox/user-wire.ts +44 -8
- package/src/proxmox/user.ts +29 -27
- package/src/unifi/fake-unifi.ts +60 -0
- package/src/unifi/firewall-zone-form.ts +65 -0
- package/src/unifi/firewall-zone.test.ts +88 -0
- package/src/unifi/firewall-zone.ts +66 -0
- package/src/unifi/index.ts +26 -0
- package/src/unifi/network-form.ts +207 -0
- package/src/unifi/network.test.ts +209 -0
- package/src/unifi/network.ts +83 -0
- package/src/unifi/policy.ts +37 -0
- package/src/unifi/providers.ts +15 -0
- package/src/unifi/resource.test.ts +133 -0
- package/src/unifi/resource.ts +131 -0
- package/src/verify/fake-engine.ts +23 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/unifi/policy.ts
|
|
2
|
+
import * as Data from "effect/Data";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
var UNIFI_READ_ONLY_POLICY = "read-only by Tim's rule, 2026-09-24; lifting it is a kit change";
|
|
5
|
+
|
|
6
|
+
class UnifiWriteRefused extends Data.TaggedError("UnifiWriteRefused") {
|
|
7
|
+
get message() {
|
|
8
|
+
return `${this.type} ${this.identity}: refusing to ${this.action} -- ${UNIFI_READ_ONLY_POLICY}.`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
var refuseWrite = (type, identity, action) => Effect.fail(new UnifiWriteRefused({ type, identity, action }));
|
|
12
|
+
// src/unifi/network.ts
|
|
13
|
+
import { Resource } from "alchemy";
|
|
14
|
+
import { adopt } from "alchemy/AdoptPolicy";
|
|
15
|
+
import * as Provider from "alchemy/Provider";
|
|
16
|
+
import * as networks from "@distilled.cloud/unifi-network/networks";
|
|
17
|
+
import * as Effect3 from "effect/Effect";
|
|
18
|
+
|
|
19
|
+
// src/unifi/network-form.ts
|
|
20
|
+
import { deepEqual } from "alchemy/Diff";
|
|
21
|
+
var sortedSet = (values) => [...new Set(values)].sort();
|
|
22
|
+
var normalizeDhcpGuarding = (value) => value == null ? value : { ...value, trustedDhcpServerIpAddresses: sortedSet(value.trustedDhcpServerIpAddresses) };
|
|
23
|
+
var normalizeIpv6Configuration = (value) => value == null ? value : {
|
|
24
|
+
...value,
|
|
25
|
+
...value.additionalHostIpSubnets !== undefined ? { additionalHostIpSubnets: sortedSet(value.additionalHostIpSubnets) } : {},
|
|
26
|
+
...value.dnsServerIpAddressesOverride !== undefined ? { dnsServerIpAddressesOverride: sortedSet(value.dnsServerIpAddressesOverride) } : {}
|
|
27
|
+
};
|
|
28
|
+
var attributesOf = (live, props) => ({
|
|
29
|
+
siteId: props.siteId,
|
|
30
|
+
networkId: live.id,
|
|
31
|
+
default: live.default,
|
|
32
|
+
name: live.name,
|
|
33
|
+
enabled: live.enabled,
|
|
34
|
+
management: live.management,
|
|
35
|
+
vlanId: live.vlanId,
|
|
36
|
+
dhcpGuarding: normalizeDhcpGuarding(live.dhcpGuarding),
|
|
37
|
+
cellularBackupEnabled: live.cellularBackupEnabled,
|
|
38
|
+
internetAccessEnabled: live.internetAccessEnabled,
|
|
39
|
+
ipv4Configuration: live.ipv4Configuration,
|
|
40
|
+
ipv6Configuration: normalizeIpv6Configuration(live.ipv6Configuration),
|
|
41
|
+
isolationEnabled: live.isolationEnabled,
|
|
42
|
+
mdnsForwardingEnabled: live.mdnsForwardingEnabled,
|
|
43
|
+
zoneId: live.zoneId,
|
|
44
|
+
deviceId: live.deviceId
|
|
45
|
+
});
|
|
46
|
+
var matches = (attributes, props) => attributes.name === props.name && attributes.enabled === props.enabled && attributes.management === props.management && attributes.vlanId === props.vlanId && deepEqual(normalizeDhcpGuarding(attributes.dhcpGuarding), normalizeDhcpGuarding(props.dhcpGuarding), {
|
|
47
|
+
stripNullish: true
|
|
48
|
+
}) && deepEqual(attributes.cellularBackupEnabled, props.cellularBackupEnabled, {
|
|
49
|
+
stripNullish: true
|
|
50
|
+
}) && deepEqual(attributes.internetAccessEnabled, props.internetAccessEnabled, {
|
|
51
|
+
stripNullish: true
|
|
52
|
+
}) && deepEqual(attributes.ipv4Configuration, props.ipv4Configuration, { stripNullish: true }) && deepEqual(normalizeIpv6Configuration(attributes.ipv6Configuration), normalizeIpv6Configuration(props.ipv6Configuration), { stripNullish: true }) && deepEqual(attributes.isolationEnabled, props.isolationEnabled, { stripNullish: true }) && deepEqual(attributes.mdnsForwardingEnabled, props.mdnsForwardingEnabled, {
|
|
53
|
+
stripNullish: true
|
|
54
|
+
}) && deepEqual(attributes.zoneId, props.zoneId, { stripNullish: true }) && deepEqual(attributes.deviceId, props.deviceId, { stripNullish: true });
|
|
55
|
+
var declareNetwork = (live, siteId) => ({
|
|
56
|
+
siteId,
|
|
57
|
+
networkId: live.id,
|
|
58
|
+
name: live.name,
|
|
59
|
+
enabled: live.enabled,
|
|
60
|
+
management: live.management,
|
|
61
|
+
vlanId: live.vlanId,
|
|
62
|
+
dhcpGuarding: normalizeDhcpGuarding(live.dhcpGuarding),
|
|
63
|
+
cellularBackupEnabled: live.cellularBackupEnabled,
|
|
64
|
+
internetAccessEnabled: live.internetAccessEnabled,
|
|
65
|
+
ipv4Configuration: live.ipv4Configuration,
|
|
66
|
+
ipv6Configuration: normalizeIpv6Configuration(live.ipv6Configuration),
|
|
67
|
+
isolationEnabled: live.isolationEnabled,
|
|
68
|
+
mdnsForwardingEnabled: live.mdnsForwardingEnabled,
|
|
69
|
+
zoneId: live.zoneId,
|
|
70
|
+
deviceId: live.deviceId
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/unifi/resource.ts
|
|
74
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
75
|
+
import { isResolved } from "alchemy/Diff";
|
|
76
|
+
import { CredentialsFromEnv } from "@distilled.cloud/unifi-network/Credentials";
|
|
77
|
+
import * as Effect2 from "effect/Effect";
|
|
78
|
+
var unifiOperations = (spec) => {
|
|
79
|
+
const read = (props) => spec.fetchLive(props).pipe(Effect2.map((live) => live === undefined ? undefined : spec.attributes(live, props)));
|
|
80
|
+
return {
|
|
81
|
+
read,
|
|
82
|
+
readHandler: ({ olds, output }) => Effect2.gen(function* () {
|
|
83
|
+
const attrs = yield* read(olds);
|
|
84
|
+
if (attrs === undefined)
|
|
85
|
+
return;
|
|
86
|
+
return output !== undefined ? attrs : Unowned(attrs);
|
|
87
|
+
}),
|
|
88
|
+
diff: (news, output) => Effect2.gen(function* () {
|
|
89
|
+
if (output === undefined || !isResolved(news))
|
|
90
|
+
return;
|
|
91
|
+
const live = yield* read(news);
|
|
92
|
+
if (live === undefined)
|
|
93
|
+
return { action: "update" };
|
|
94
|
+
return spec.matches(live, news) ? { action: "noop" } : { action: "update" };
|
|
95
|
+
}),
|
|
96
|
+
reconcile: (news) => Effect2.gen(function* () {
|
|
97
|
+
const live = yield* read(news);
|
|
98
|
+
if (live === undefined)
|
|
99
|
+
return yield* refuseWrite(spec.type, spec.describe(news), "create");
|
|
100
|
+
if (!spec.matches(live, news)) {
|
|
101
|
+
return yield* refuseWrite(spec.type, spec.describe(news), "update");
|
|
102
|
+
}
|
|
103
|
+
return live;
|
|
104
|
+
}),
|
|
105
|
+
destroy: (olds) => refuseWrite(spec.type, spec.describe(olds), "delete")
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
var unifiHandlers = (spec) => {
|
|
109
|
+
const ops = unifiOperations(spec);
|
|
110
|
+
const withCredentials = (effect) => Effect2.provide(effect, CredentialsFromEnv);
|
|
111
|
+
return {
|
|
112
|
+
list: () => Effect2.succeed([]),
|
|
113
|
+
read: (args) => withCredentials(ops.readHandler(args)),
|
|
114
|
+
diff: (args) => withCredentials(ops.diff(args.news, args.output)),
|
|
115
|
+
reconcile: (args) => withCredentials(ops.reconcile(args.news)),
|
|
116
|
+
delete: (args) => withCredentials(ops.destroy(args.olds))
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/unifi/network.ts
|
|
121
|
+
var UnifiNetwork = Resource("Unifi.Network", {
|
|
122
|
+
defaultRemovalPolicy: "retain"
|
|
123
|
+
});
|
|
124
|
+
var network = (id, props) => UnifiNetwork(id, props).pipe(adopt(true));
|
|
125
|
+
var spec = {
|
|
126
|
+
type: "Unifi.Network",
|
|
127
|
+
describe: (props) => `sites/${props.siteId}/networks/${props.networkId}`,
|
|
128
|
+
fetchLive: (props) => networks.getNetworkDetails({ siteId: props.siteId, networkId: props.networkId }).pipe(Effect3.catchTag("NotFound", () => Effect3.succeed(undefined))),
|
|
129
|
+
attributes: attributesOf,
|
|
130
|
+
matches
|
|
131
|
+
};
|
|
132
|
+
var handlers = unifiHandlers(spec);
|
|
133
|
+
var UnifiNetworkProvider = () => Provider.effect(UnifiNetwork, Effect3.succeed(UnifiNetwork.Provider.of(handlers)));
|
|
134
|
+
// src/unifi/firewall-zone.ts
|
|
135
|
+
import { Resource as Resource2 } from "alchemy";
|
|
136
|
+
import { adopt as adopt2 } from "alchemy/AdoptPolicy";
|
|
137
|
+
import * as Provider2 from "alchemy/Provider";
|
|
138
|
+
import * as firewall from "@distilled.cloud/unifi-network/firewall";
|
|
139
|
+
import * as Effect4 from "effect/Effect";
|
|
140
|
+
|
|
141
|
+
// src/unifi/firewall-zone-form.ts
|
|
142
|
+
var sortedSet2 = (values) => [...new Set(values)].sort();
|
|
143
|
+
var attributesOf2 = (live, props) => ({
|
|
144
|
+
siteId: props.siteId,
|
|
145
|
+
firewallZoneId: live.id,
|
|
146
|
+
name: live.name,
|
|
147
|
+
networkIds: sortedSet2(live.networkIds),
|
|
148
|
+
metadataOrigin: live.metadata.origin
|
|
149
|
+
});
|
|
150
|
+
var matches2 = (attributes, props) => attributes.name === props.name && attributes.networkIds.join(",") === sortedSet2(props.networkIds).join(",");
|
|
151
|
+
var declareFirewallZone = (live, siteId) => ({
|
|
152
|
+
siteId,
|
|
153
|
+
firewallZoneId: live.id,
|
|
154
|
+
name: live.name,
|
|
155
|
+
networkIds: sortedSet2(live.networkIds)
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// src/unifi/firewall-zone.ts
|
|
159
|
+
var UnifiFirewallZone = Resource2("Unifi.FirewallZone", {
|
|
160
|
+
defaultRemovalPolicy: "retain"
|
|
161
|
+
});
|
|
162
|
+
var firewallZone = (id, props) => UnifiFirewallZone(id, props).pipe(adopt2(true));
|
|
163
|
+
var spec2 = {
|
|
164
|
+
type: "Unifi.FirewallZone",
|
|
165
|
+
describe: (props) => `sites/${props.siteId}/firewall/zones/${props.firewallZoneId}`,
|
|
166
|
+
fetchLive: (props) => firewall.getFirewallZone({ siteId: props.siteId, firewallZoneId: props.firewallZoneId }).pipe(Effect4.catchTag("NotFound", () => Effect4.succeed(undefined))),
|
|
167
|
+
attributes: attributesOf2,
|
|
168
|
+
matches: matches2
|
|
169
|
+
};
|
|
170
|
+
var handlers2 = unifiHandlers(spec2);
|
|
171
|
+
var UnifiFirewallZoneProvider = () => Provider2.effect(UnifiFirewallZone, Effect4.succeed(UnifiFirewallZone.Provider.of(handlers2)));
|
|
172
|
+
// src/unifi/providers.ts
|
|
173
|
+
import * as Layer from "effect/Layer";
|
|
174
|
+
var providers = () => Layer.mergeAll(UnifiNetworkProvider(), UnifiFirewallZoneProvider());
|
|
175
|
+
export {
|
|
176
|
+
UNIFI_READ_ONLY_POLICY,
|
|
177
|
+
UnifiFirewallZone,
|
|
178
|
+
UnifiFirewallZoneProvider,
|
|
179
|
+
UnifiNetwork,
|
|
180
|
+
UnifiNetworkProvider,
|
|
181
|
+
UnifiWriteRefused,
|
|
182
|
+
declareFirewallZone,
|
|
183
|
+
declareNetwork,
|
|
184
|
+
firewallZone,
|
|
185
|
+
network,
|
|
186
|
+
providers
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
//# debugId=033CFC419E398B0664756E2164756E21
|
|
190
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/unifi/policy.ts", "../src/unifi/network.ts", "../src/unifi/network-form.ts", "../src/unifi/resource.ts", "../src/unifi/firewall-zone.ts", "../src/unifi/firewall-zone-form.ts", "../src/unifi/providers.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The one refusal every `Unifi.*` write handler returns — never an SDK write call.\n *\n * ⛔ TIM'S RULE, 2026-09-24: UniFi (and OPNsense) are READ-ONLY FOR NOW. This family has no\n * `create`, no `update` body and no delete call anywhere in its source — `reconcile` and\n * `delete` both fail with this typed error instead, and `resource.test.ts` proves the point\n * with a fake `HttpClient` that fails the test on any request that is not a `GET`. Lifting the\n * policy is a kit change (a new PR that adds a write path, reviewed as one), not a flag a stack\n * can pass.\n *\n * ★ A TYPED TAG, NOT A THROWN STRING (S21). `Effect.catchTag('UnifiWriteRefused', …)` lets a\n * caller (a future import script, a test) branch on the refusal without parsing a message.\n */\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\n\nexport const UNIFI_READ_ONLY_POLICY =\n \"read-only by Tim's rule, 2026-09-24; lifting it is a kit change\";\n\n/** What kind of write was asked for and refused — for the message only, never acted on. */\nexport type UnifiWriteAction = 'create' | 'update' | 'delete';\n\nexport class UnifiWriteRefused extends Data.TaggedError('UnifiWriteRefused')<{\n /** The vendor type string, e.g. `Unifi.Network`. */\n readonly type: string;\n /** `spec.describe(props)` — the object the write would have touched. */\n readonly identity: string;\n readonly action: UnifiWriteAction;\n}> {\n override get message(): string {\n return `${this.type} ${this.identity}: refusing to ${this.action} -- ${UNIFI_READ_ONLY_POLICY}.`;\n }\n}\n\n/** The only thing `reconcile`/`delete` ever return for a write this family cannot make. */\nexport const refuseWrite = (type: string, identity: string, action: UnifiWriteAction) =>\n Effect.fail(new UnifiWriteRefused({ type, identity, action }));\n",
|
|
6
|
+
"/**\n * `Unifi.Network` — one network on a UniFi Network site, READ-ONLY (`policy.ts`).\n *\n * ★ WHY NETWORK IS IN THE FIRST IMPORT SET. `getNetworkDetails`/`getNetworksOverviewPage`\n * (`../../distilled-unifi-network/src/services/networks.ts`) is a list+get pair over\n * genuine configuration — VLAN, DHCP guarding, IPv4/IPv6 addressing, isolation — not runtime\n * state like a connected client, a device statistic or a hotspot voucher. It is also the SDK\n * package's own README recommendation for a first read/adopt-only resource, alongside `Site`\n * (skipped here — no `getSite`, only the list operation, so it fails this task's own \"both a\n * list and a get\" bar) and `FirewallZone`/`FirewallPolicy` (`firewall-zone.ts` ships\n * `FirewallZone`; `FirewallPolicy`'s several discriminated filter variants and its\n * list-replacing ordering endpoint are a bigger, separate PR).\n *\n * ⛔ THE WHOLE-OBJECT-PUT TRAP (`docs/unifi-api-notes.md`) NEVER FIRES HERE, BY CONSTRUCTION.\n * `updateNetwork` is a `PUT` that disables `dhcpGuarding`/`ipv4Configuration`/`ipv6Configuration`\n * outright when the caller omits them — the exact failure mode a read-only family cannot cause,\n * because `resource.ts`'s `reconcile` never calls `updateNetwork` at all. Recorded here for\n * whoever adds the write path this docs note anticipates: read-merge-write the WHOLE object,\n * never a props-only body.\n */\nimport { Resource } from 'alchemy';\nimport { adopt } from 'alchemy/AdoptPolicy';\nimport * as Provider from 'alchemy/Provider';\nimport * as networks from '@distilled.cloud/unifi-network/networks';\nimport * as Effect from 'effect/Effect';\nimport {\n type NetworkAttributes,\n type NetworkProps,\n attributesOf,\n matches,\n} from './network-form.ts';\nimport type { UnifiRequirements, UnifiSpec } from './resource.ts';\nimport { unifiHandlers } from './resource.ts';\n\nexport type { NetworkAttributes, NetworkProps } from './network-form.ts';\nexport { declareNetwork } from './network-form.ts';\n\nexport interface UnifiNetwork extends Resource<\n 'Unifi.Network',\n NetworkProps,\n NetworkAttributes,\n never,\n UnifiRequirements\n> {}\n\nexport const UnifiNetwork = Resource<UnifiNetwork>('Unifi.Network', {\n defaultRemovalPolicy: 'retain',\n});\n\n/**\n * `network('lan', props)` — `adopt(true)` piped on by default (H5). This family never writes, so\n * the only thing a first deploy against an existing network could ever do is bind to it or refuse\n * `OwnedBySomeoneElse`; piping `adopt(true)` here picks the former without a stack having to\n * remember `--adopt`, mirroring `discord/application-command.ts`'s `applicationCommand`.\n */\nexport const network = (id: string, props: NetworkProps) =>\n UnifiNetwork(id, props).pipe(adopt(true));\n\n/**\n * ★ EXPORTED for direct testing against a fake `Credentials` layer (`fake-unifi.ts`), the same\n * seam `netbox/prefix.ts`'s `spec` and `discord/application-command.ts`'s `spec` use — `handlers`\n * bakes in `CredentialsFromEnv`, which resolves environment variables a test cannot repoint.\n */\nexport const spec: UnifiSpec<\n NetworkProps,\n networks.NetworkDetails,\n NetworkAttributes,\n networks.GetNetworkDetailsError\n> = {\n type: 'Unifi.Network',\n describe: (props) => `sites/${props.siteId}/networks/${props.networkId}`,\n fetchLive: (props) =>\n networks\n .getNetworkDetails({ siteId: props.siteId, networkId: props.networkId })\n .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),\n attributes: attributesOf,\n matches,\n};\n\nexport const handlers = unifiHandlers(spec);\n\nexport const UnifiNetworkProvider = () =>\n Provider.effect(UnifiNetwork, Effect.succeed(UnifiNetwork.Provider.of(handlers)));\n",
|
|
7
|
+
"/**\n * `Unifi.Network`'s wire shape: the declarable subset of `NetworkDetails`, and how a live read\n * becomes both plan attributes and a rendered declaration.\n *\n * ⚠️ PROPS MIRROR `UpdateNetworkRequest`, NOT `NetworkDetails`. `default` and `metadata` are\n * server-derived — no writable endpoint in `networks.ts` accepts either — so they are\n * attributes-only, reported but never compared or declared. Everything else in `NetworkProps`\n * is exactly what `updateNetwork`/`createNetwork` would accept, even though this family never\n * calls either: the shape is what a future write path would need, and what today's read-only\n * `matches` compares against.\n *\n * ⛔ `ipv4Configuration` STAYS `unknown`, ON THE SDK'S OWN AUTHORITY. `networks.ts`'s own comment\n * (\"Shape varies by variant — widened by scripts/convert.ts; see README\") says the OpenAPI\n * discriminator this field varies over was flattened at generation time, not decoded per\n * variant. Comparing it as an opaque JSON value (below) is honest about that; guessing a\n * narrower shape here would silently stop matching the day a new variant's key order differs.\n * ⚠️ It may ALSO hide an unordered array the way `dhcpGuarding` and `ipv6Configuration` do\n * below — this family just can't know, because the shape is never decoded. If a future variant\n * plans spurious `update`s, suspect this field first and widen it into a typed, order-normalized\n * shape rather than adding another opaque comparison.\n *\n * ⛔ THREE FIELDS ARE SETS, NOT SEQUENCES: `dhcpGuarding.trustedDhcpServerIpAddresses`,\n * `ipv6Configuration.additionalHostIpSubnets`, and `ipv6Configuration.dnsServerIpAddressesOverride`.\n * The vendor document gives no indication any of the three is ordered — same reasoning\n * `firewall-zone-form.ts` gives for `networkIds`, and `proxmox/user-wire.ts`'s `groupSet` gives\n * for PVE's `groups` — but `matches` compares via alchemy's `deepEqual`, which sorts object KEYS\n * but NOT ARRAY ELEMENTS (`node_modules/alchemy/lib/Diff.js`). Left alone, a console that\n * returns the same set in a different order between calls plans a spurious `update`, and the\n * read-only reconcile then refuses every deploy. `sortedSet` (dedupe + sort) neutralizes that\n * before any of the three is attributed, declared, or compared. Every OTHER array field in this\n * family (there are none today besides these three) would stay declaration-order as written.\n */\nimport { deepEqual } from 'alchemy/Diff';\nimport type * as networks from '@distilled.cloud/unifi-network/networks';\n\nconst sortedSet = (values: readonly string[]): string[] => [...new Set(values)].sort();\n\n// ⚠️ `value == null` (NOT `=== undefined`) ON PURPOSE, IN BOTH NORMALIZERS BELOW. The SDK types\n// `dhcpGuarding`/`ipv6Configuration` `T | undefined`, but `matches`'s own header says UniFi's\n// JSON answers `null` for an unset optional field — a reality `deepEqual`'s `stripNullish`\n// already treats as equivalent to `undefined` (`value == null` in alchemy's `Diff.js`), which\n// this family's props/attributes types don't model at the TS level. Destructuring a `null` with\n// `=== undefined`'s false branch (`{ ...value, field: value.field }`) would throw\n// `TypeError: Cannot read properties of null` the first time a console omits either field —\n// `== null` catches both and returns the value through untouched, matching what the rest of\n// this file already does for every other optional field.\nconst normalizeDhcpGuarding = (\n value: networks.NetworkDHCPGuarding | undefined,\n): networks.NetworkDHCPGuarding | undefined =>\n value == null\n ? value\n : { ...value, trustedDhcpServerIpAddresses: sortedSet(value.trustedDhcpServerIpAddresses) };\n\n// ⚠️ `exactOptionalPropertyTypes` (see the props header) means these two subfields must be\n// OMITTED, not set to an explicit `undefined`, when the live/declared value has no override —\n// `NetworkIPv6Configuration` types them `field?: T`, not `field?: T | undefined`. Conditional\n// spread (rather than `field: value.field === undefined ? undefined : sortedSet(...)`) is what\n// keeps an absent key absent.\nconst normalizeIpv6Configuration = (\n value: networks.NetworkIPv6Configuration | undefined,\n): networks.NetworkIPv6Configuration | undefined =>\n value == null\n ? value\n : {\n ...value,\n ...(value.additionalHostIpSubnets !== undefined\n ? { additionalHostIpSubnets: sortedSet(value.additionalHostIpSubnets) }\n : {}),\n ...(value.dnsServerIpAddressesOverride !== undefined\n ? { dnsServerIpAddressesOverride: sortedSet(value.dnsServerIpAddressesOverride) }\n : {}),\n };\n\n/**\n * ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE. `tsconfig.json` sets\n * `exactOptionalPropertyTypes`, which treats `field?: T` (absent-or-T) and\n * `field?: T | undefined` (absent-or-explicitly-undefined) as different types. `declareNetwork`\n * below assigns `live.dhcpGuarding` (typed `T | undefined`) straight through — the whole point\n * of a renderer that echoes a live optional field structurally rather than defaulting it — so\n * the target has to admit an explicit `undefined`, not just omission.\n */\nexport interface NetworkProps {\n siteId: string;\n networkId: string;\n name: string;\n enabled: boolean;\n management: string;\n /** VLAN ID. Must be 1 for the default network and >= 2 for additional networks. */\n vlanId: number;\n dhcpGuarding?: networks.NetworkDHCPGuarding | undefined;\n cellularBackupEnabled?: boolean | undefined;\n internetAccessEnabled?: boolean | undefined;\n /** Opaque — see the header. */\n ipv4Configuration?: unknown;\n ipv6Configuration?: networks.NetworkIPv6Configuration | undefined;\n isolationEnabled?: boolean | undefined;\n mdnsForwardingEnabled?: boolean | undefined;\n zoneId?: string | undefined;\n deviceId?: string | undefined;\n}\n\nexport interface NetworkAttributes {\n siteId: string;\n networkId: string;\n /** Whether this is the site's undeletable default network — server-derived, never declared. */\n default: boolean;\n name: string;\n enabled: boolean;\n management: string;\n vlanId: number;\n dhcpGuarding: networks.NetworkDHCPGuarding | undefined;\n cellularBackupEnabled: boolean | undefined;\n internetAccessEnabled: boolean | undefined;\n ipv4Configuration: unknown;\n ipv6Configuration: networks.NetworkIPv6Configuration | undefined;\n isolationEnabled: boolean | undefined;\n mdnsForwardingEnabled: boolean | undefined;\n zoneId: string | undefined;\n deviceId: string | undefined;\n}\n\nexport const attributesOf = (\n live: networks.NetworkDetails,\n props: NetworkProps,\n): NetworkAttributes => ({\n siteId: props.siteId,\n networkId: live.id,\n default: live.default,\n name: live.name,\n enabled: live.enabled,\n management: live.management,\n vlanId: live.vlanId,\n dhcpGuarding: normalizeDhcpGuarding(live.dhcpGuarding),\n cellularBackupEnabled: live.cellularBackupEnabled,\n internetAccessEnabled: live.internetAccessEnabled,\n ipv4Configuration: live.ipv4Configuration,\n ipv6Configuration: normalizeIpv6Configuration(live.ipv6Configuration),\n isolationEnabled: live.isolationEnabled,\n mdnsForwardingEnabled: live.mdnsForwardingEnabled,\n zoneId: live.zoneId,\n deviceId: live.deviceId,\n});\n\n/**\n * ⚠️ `stripNullish: true` ON EVERY COMPARISON. UniFi's JSON answers `null` for an unset optional\n * field; a declaration that omits the same prop carries `undefined`. Without this option every\n * plan against an object with one unset optional field would report `update` forever — the\n * exact \"read and write are different shapes\" trap `netbox/values.ts` and `proxmox/user-wire.ts`\n * both work around, here solved once with the engine's own `deepEqual` instead of a per-field\n * coercion, because nothing here needs PVE's or NetBox's extra wire-format translation.\n */\nexport const matches = (attributes: NetworkAttributes, props: NetworkProps): boolean =>\n attributes.name === props.name &&\n attributes.enabled === props.enabled &&\n attributes.management === props.management &&\n attributes.vlanId === props.vlanId &&\n deepEqual(\n normalizeDhcpGuarding(attributes.dhcpGuarding),\n normalizeDhcpGuarding(props.dhcpGuarding),\n {\n stripNullish: true,\n },\n ) &&\n deepEqual(attributes.cellularBackupEnabled, props.cellularBackupEnabled, {\n stripNullish: true,\n }) &&\n deepEqual(attributes.internetAccessEnabled, props.internetAccessEnabled, {\n stripNullish: true,\n }) &&\n deepEqual(attributes.ipv4Configuration, props.ipv4Configuration, { stripNullish: true }) &&\n deepEqual(\n normalizeIpv6Configuration(attributes.ipv6Configuration),\n normalizeIpv6Configuration(props.ipv6Configuration),\n { stripNullish: true },\n ) &&\n deepEqual(attributes.isolationEnabled, props.isolationEnabled, { stripNullish: true }) &&\n deepEqual(attributes.mdnsForwardingEnabled, props.mdnsForwardingEnabled, {\n stripNullish: true,\n }) &&\n deepEqual(attributes.zoneId, props.zoneId, { stripNullish: true }) &&\n deepEqual(attributes.deviceId, props.deviceId, { stripNullish: true });\n\n/**\n * The declaration renderer (task spec: \"given one live object, return the props a declaration\n * needs so its plan is noop\"). A later import script feeds `getNetworkDetails`'s own response\n * straight in and writes the result into `alchemy.run.ts` as `network('<id>', declareNetwork(live,\n * siteId))` — no field is invented or defaulted here beyond what `attributesOf` already reports,\n * so `matches(attributesOf(live, props), declareNetwork(live, siteId))` is `true` by construction\n * (`network.test.ts` proves it) for any live object this SDK can decode.\n */\nexport const declareNetwork = (live: networks.NetworkDetails, siteId: string): NetworkProps => ({\n siteId,\n networkId: live.id,\n name: live.name,\n enabled: live.enabled,\n management: live.management,\n vlanId: live.vlanId,\n dhcpGuarding: normalizeDhcpGuarding(live.dhcpGuarding),\n cellularBackupEnabled: live.cellularBackupEnabled,\n internetAccessEnabled: live.internetAccessEnabled,\n ipv4Configuration: live.ipv4Configuration,\n ipv6Configuration: normalizeIpv6Configuration(live.ipv6Configuration),\n isolationEnabled: live.isolationEnabled,\n mdnsForwardingEnabled: live.mdnsForwardingEnabled,\n zoneId: live.zoneId,\n deviceId: live.deviceId,\n});\n",
|
|
8
|
+
"/**\n * One shape for every UniFi Network read-only object — `Unifi.Network`, `Unifi.FirewallZone`,\n * and whatever import adds next.\n *\n * ★ MIRRORS `../discord/resource.ts` (S7-S10, H1, H5), NOT `../netbox/resource.ts`. Both are\n * \"marker-less API\" families (S8): UniFi Network objects carry no tag or metadata field this\n * stack could stamp as ownership, so a cold read of a match is `Unowned(attrs)` exactly like\n * `Cloudflare/Snippets/Snippet.ts@v2.0.0-beta.79#read`, the reference implementation S8 and H1\n * both cite. `adopt(true)`, piped on by each resource's convenience constructor (H5), turns\n * that into a silent one-time bind instead of an `OwnedBySomeoneElse` refusal.\n *\n * ⛔ THE DIFFERENCE FROM EVERY OTHER FAMILY HERE: THERE IS NO `create`, `update` OR `destroy` IN\n * THIS SPEC AT ALL. Tim's rule, 2026-09-24 (`policy.ts`): UniFi and OPNsense are read-only. So\n * `reconcile` below can only ever refuse or report an exact match — it has no write path to\n * fall into by accident, and no resource file wired through this engine can reach one either.\n *\n * ⚠️ RECONCILE STILL HAS TO SUCCEED ON AN EXACT-MATCH ADOPTION, NOT JUST REFUSE UNCONDITIONALLY.\n * Measured against `Plan.ts@v2.0.0-beta.79` (`forceUpdateAfterAdoption`, H6): beta.79 forces\n * ONE reconcile call after every cold adoption, whether or not the diff said noop — so a\n * resource whose `reconcile` always failed would break the ordinary \"adopt an unchanged\n * object\" deploy, not just a real write. The fix, exactly like `discordOperations.reconcile`:\n * read the live object again, and refuse ONLY when it is missing (would need a create) or\n * drifted (would need a write to converge). An exact match returns the live attributes and\n * calls nothing — S10's \"zero write calls when there is no drift\" holds even under the forced\n * post-adoption call.\n *\n * ⚠️ `NotFound` IS FOLDED TO \"ABSENT\" INSIDE EACH RESOURCE FILE'S OWN `fetchLive`, NOT HERE —\n * same split netbox's `resource.ts` uses and for the same reason: `Effect.catchTag`'s\n * tag-literal inference needs a concrete error union to resolve `\"NotFound\"` against, and a\n * spec's `E` is only concrete at each resource file's own call site. Every OTHER failure (a\n * real 5xx, a network error, `Forbidden`) is NOT folded anywhere — it fails the plan loudly,\n * per the task's own rule: only a genuine not-found may mean absent.\n */\nimport { Unowned } from 'alchemy/AdoptPolicy';\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport { CredentialsFromEnv } from '@distilled.cloud/unifi-network/Credentials';\nimport type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { refuseWrite } from './policy.ts';\n\n/** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */\nexport type UnifiRequirements = HttpClient.HttpClient;\n\n/**\n * One UniFi Network object's read. `Live` is whatever the SDK decodes (a `NetworkDetails`, a\n * `FirewallZone`, …); `E` is left to each resource file to declare (its own `fetchLive`'s error\n * union, with `NotFound` already folded away) so the precise per-operation error union flows\n * through instead of being widened by hand here.\n */\nexport type UnifiSpec<Props extends object, Live, Attributes extends object, E> = {\n /** The vendor type string, e.g. `Unifi.Network` — for the refusal message only. */\n readonly type: string;\n /** Already folds a genuine not-found to `undefined`; nothing else is folded (see header). */\n readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, UnifiNetworkOpContext>;\n readonly attributes: (live: Live, props: Props) => Attributes;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n /** For the refusal message and read-back failures. */\n readonly describe: (props: Props) => string;\n};\n\nexport const unifiOperations = <Props extends object, Live, Attributes extends object, E>(\n spec: UnifiSpec<Props, Live, Attributes, E>,\n) => {\n const read = (props: Props) =>\n spec\n .fetchLive(props)\n .pipe(Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))));\n\n return {\n read,\n\n readHandler: ({ olds, output }: { olds: Props; output: Attributes | undefined }) =>\n Effect.gen(function* () {\n const attrs = yield* read(olds);\n if (attrs === undefined) return undefined;\n // Warm read (persisted `output` says a prior apply already adopted this exact object):\n // plain attributes. Cold read (fresh store, or state lost): no ownership marker on this\n // vendor exists to tell \"ours\" from \"someone else's\" apart, so Unowned until adopt(true).\n return output !== undefined ? attrs : Unowned(attrs);\n }),\n\n diff: (news: Input<Props>, output: Attributes | undefined) =>\n Effect.gen(function* () {\n if (output === undefined || !isResolved(news)) return undefined;\n const live = yield* read(news);\n if (live === undefined) return { action: 'update' } as const;\n return spec.matches(live, news)\n ? ({ action: 'noop' } as const)\n : ({ action: 'update' } as const);\n }),\n\n reconcile: (news: Props) =>\n Effect.gen(function* () {\n const live = yield* read(news);\n if (live === undefined) return yield* refuseWrite(spec.type, spec.describe(news), 'create');\n if (!spec.matches(live, news)) {\n return yield* refuseWrite(spec.type, spec.describe(news), 'update');\n }\n // Exact match: the object already is what is declared. Zero write calls (S10), correct\n // whether this is a routine deploy or beta.79's forced post-adoption reconcile (H6).\n return live;\n }),\n\n destroy: (olds: Props) => refuseWrite(spec.type, spec.describe(olds), 'delete'),\n };\n};\n\n/**\n * The four provider handlers for a spec'd read-only UniFi object, wired once.\n *\n * ⛔ `list` ANSWERS EMPTY. `GET /v1/sites/{siteId}/networks` answers every network on the site;\n * adoption stays explicit, the same reasoning `Proxmox.User`'s and NetBox's `list` give.\n */\nexport const unifiHandlers = <Props extends object, Live, Attributes extends object, E>(\n spec: UnifiSpec<Props, Live, Attributes, E>,\n) => {\n const ops = unifiOperations(spec);\n const withCredentials = <A, Err>(effect: Effect.Effect<A, Err, UnifiNetworkOpContext>) =>\n Effect.provide(effect, CredentialsFromEnv);\n return {\n list: () => Effect.succeed([]),\n read: (args: { olds: Props; output: Attributes | undefined }) =>\n withCredentials(ops.readHandler(args)),\n diff: (args: { news: Input<Props>; output: Attributes | undefined }) =>\n withCredentials(ops.diff(args.news, args.output)),\n reconcile: (args: { news: Props }) => withCredentials(ops.reconcile(args.news)),\n delete: (args: { olds: Props }) => withCredentials(ops.destroy(args.olds)),\n };\n};\n",
|
|
9
|
+
"/**\n * `Unifi.FirewallZone` — one firewall zone on a UniFi Network site, READ-ONLY (`policy.ts`).\n *\n * ★ WHY FIREWALL ZONE IS IN THE FIRST IMPORT SET AND `Unifi.FirewallPolicy` IS NOT.\n * `getFirewallZone`/`getFirewallZones` (`firewall.ts`) is a list+get pair over genuine,\n * simply-shaped configuration: `{id, metadata, name, networkIds}`, four fields, no nested\n * discriminated filter. `FirewallPolicy` (same file) is also list+get and also configuration,\n * but its `source`/`destination`/`ipProtocolScope` fields carry several of `docs/unifi-api-notes.md`'s\n * \"converter-flattened discriminator variants\" each, and `firewall/policies/ordering` replaces\n * the whole ordered list — modelling those correctly is a bigger, separate PR (`network.ts`'s\n * header note names it explicitly) rather than something to rush alongside this one.\n */\nimport { Resource } from 'alchemy';\nimport { adopt } from 'alchemy/AdoptPolicy';\nimport * as Provider from 'alchemy/Provider';\nimport * as firewall from '@distilled.cloud/unifi-network/firewall';\nimport * as Effect from 'effect/Effect';\nimport {\n type FirewallZoneAttributes,\n type FirewallZoneProps,\n attributesOf,\n matches,\n} from './firewall-zone-form.ts';\nimport type { UnifiRequirements, UnifiSpec } from './resource.ts';\nimport { unifiHandlers } from './resource.ts';\n\nexport type { FirewallZoneAttributes, FirewallZoneProps } from './firewall-zone-form.ts';\nexport { declareFirewallZone } from './firewall-zone-form.ts';\n\nexport interface UnifiFirewallZone extends Resource<\n 'Unifi.FirewallZone',\n FirewallZoneProps,\n FirewallZoneAttributes,\n never,\n UnifiRequirements\n> {}\n\nexport const UnifiFirewallZone = Resource<UnifiFirewallZone>('Unifi.FirewallZone', {\n defaultRemovalPolicy: 'retain',\n});\n\n/** `firewallZone('lan', props)` — `adopt(true)` piped on by default (H5); see `network.ts`. */\nexport const firewallZone = (id: string, props: FirewallZoneProps) =>\n UnifiFirewallZone(id, props).pipe(adopt(true));\n\n/** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */\nexport const spec: UnifiSpec<\n FirewallZoneProps,\n firewall.FirewallZone,\n FirewallZoneAttributes,\n firewall.GetFirewallZoneError\n> = {\n type: 'Unifi.FirewallZone',\n describe: (props) => `sites/${props.siteId}/firewall/zones/${props.firewallZoneId}`,\n fetchLive: (props) =>\n firewall\n .getFirewallZone({ siteId: props.siteId, firewallZoneId: props.firewallZoneId })\n .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),\n attributes: attributesOf,\n matches,\n};\n\nexport const handlers = unifiHandlers(spec);\n\nexport const UnifiFirewallZoneProvider = () =>\n Provider.effect(UnifiFirewallZone, Effect.succeed(UnifiFirewallZone.Provider.of(handlers)));\n",
|
|
10
|
+
"/**\n * `Unifi.FirewallZone`'s wire shape — the declarable subset of `FirewallZone`, and how a live\n * read becomes both plan attributes and a rendered declaration.\n *\n * ⚠️ ONLY `name` AND `networkIds` ARE DECLARABLE. `CreateFirewallZoneRequest`\n * (`firewall.ts`) accepts exactly those two beyond `siteId`; `metadata` (origin/source) and\n * `id` are server-derived, so — same split as `network-form.ts`'s `default`/`metadata` —\n * they are attributes-only, reported but never compared or declared.\n *\n * ⛔ `networkIds` ORDER IS NOT PART OF IDENTITY. The vendor document gives no indication the\n * list is ordered (it is a set of attached networks, not a sequence like the ordering\n * endpoints' rule lists), so `matches` compares it as a set — sorted before comparing —\n * the same reasoning `proxmox/user-wire.ts`'s `groupSet` gives for PVE's `groups`.\n */\nimport type * as firewall from '@distilled.cloud/unifi-network/firewall';\n\nexport interface FirewallZoneProps {\n siteId: string;\n firewallZoneId: string;\n /** Name of a firewall zone. */\n name: string;\n /** Network IDs attached to this zone — compared as a set, see the header. */\n networkIds: string[];\n}\n\nexport interface FirewallZoneAttributes {\n siteId: string;\n firewallZoneId: string;\n name: string;\n networkIds: string[];\n /** `origin`/`source` (e.g. `SDWAN`) — server-derived, never declared. */\n metadataOrigin: string;\n}\n\nconst sortedSet = (values: readonly string[]): string[] => [...new Set(values)].sort();\n\nexport const attributesOf = (\n live: firewall.FirewallZone,\n props: FirewallZoneProps,\n): FirewallZoneAttributes => ({\n siteId: props.siteId,\n firewallZoneId: live.id,\n name: live.name,\n networkIds: sortedSet(live.networkIds),\n metadataOrigin: live.metadata.origin,\n});\n\nexport const matches = (attributes: FirewallZoneAttributes, props: FirewallZoneProps): boolean =>\n attributes.name === props.name &&\n attributes.networkIds.join(',') === sortedSet(props.networkIds).join(',');\n\n/**\n * The declaration renderer (task spec: \"given one live object, return the props a declaration\n * needs so its plan is noop\"). `matches(attributesOf(live, props), declareFirewallZone(live,\n * siteId))` is `true` by construction (`firewall-zone.test.ts` proves it).\n */\nexport const declareFirewallZone = (\n live: firewall.FirewallZone,\n siteId: string,\n): FirewallZoneProps => ({\n siteId,\n firewallZoneId: live.id,\n name: live.name,\n networkIds: sortedSet(live.networkIds),\n});\n",
|
|
11
|
+
"/**\n * Every `Unifi.*` provider as one layer for a stack.\n *\n * Layer.mergeAll(unifiProviders(), …the stack's other providers)\n * .pipe(Layer.provide(FetchHttpClient.layer))\n *\n * S16: a subpath's `providers()` plays the role upstream's own `Providers.ts` does — one minimal\n * insertion per family. `FetchHttpClient.layer` is the stack's own responsibility to provide,\n * mirroring `../discord/providers.ts`.\n */\nimport * as Layer from 'effect/Layer';\nimport { UnifiFirewallZoneProvider } from './firewall-zone.ts';\nimport { UnifiNetworkProvider } from './network.ts';\n\nexport const providers = () => Layer.mergeAll(UnifiNetworkProvider(), UnifiFirewallZoneProvider());\n"
|
|
12
|
+
],
|
|
13
|
+
"mappings": ";AAaA;AACA;AAEO,IAAM,yBACX;AAAA;AAKK,MAAM,0BAA+B,iBAAY,mBAAmB,EAMxE;AAAA,MACY,OAAO,GAAW;AAAA,IAC7B,OAAO,GAAG,KAAK,QAAQ,KAAK,yBAAyB,KAAK,aAAa;AAAA;AAE3E;AAGO,IAAM,cAAc,CAAC,MAAc,UAAkB,WACnD,YAAK,IAAI,kBAAkB,EAAE,MAAM,UAAU,OAAO,CAAC,CAAC;;AChB/D;AACA;AACA;AACA;AACA;;;ACQA;AAGA,IAAM,YAAY,CAAC,WAAwC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,EAAE,KAAK;AAWrF,IAAM,wBAAwB,CAC5B,UAEA,SAAS,OACL,QACA,KAAK,OAAO,8BAA8B,UAAU,MAAM,4BAA4B,EAAE;AAO9F,IAAM,6BAA6B,CACjC,UAEA,SAAS,OACL,QACA;AAAA,KACK;AAAA,KACC,MAAM,4BAA4B,YAClC,EAAE,yBAAyB,UAAU,MAAM,uBAAuB,EAAE,IACpE,CAAC;AAAA,KACD,MAAM,iCAAiC,YACvC,EAAE,8BAA8B,UAAU,MAAM,4BAA4B,EAAE,IAC9E,CAAC;AACP;AAkDC,IAAM,eAAe,CAC1B,MACA,WACuB;AAAA,EACvB,QAAQ,MAAM;AAAA,EACd,WAAW,KAAK;AAAA,EAChB,SAAS,KAAK;AAAA,EACd,MAAM,KAAK;AAAA,EACX,SAAS,KAAK;AAAA,EACd,YAAY,KAAK;AAAA,EACjB,QAAQ,KAAK;AAAA,EACb,cAAc,sBAAsB,KAAK,YAAY;AAAA,EACrD,uBAAuB,KAAK;AAAA,EAC5B,uBAAuB,KAAK;AAAA,EAC5B,mBAAmB,KAAK;AAAA,EACxB,mBAAmB,2BAA2B,KAAK,iBAAiB;AAAA,EACpE,kBAAkB,KAAK;AAAA,EACvB,uBAAuB,KAAK;AAAA,EAC5B,QAAQ,KAAK;AAAA,EACb,UAAU,KAAK;AACjB;AAUO,IAAM,UAAU,CAAC,YAA+B,UACrD,WAAW,SAAS,MAAM,QAC1B,WAAW,YAAY,MAAM,WAC7B,WAAW,eAAe,MAAM,cAChC,WAAW,WAAW,MAAM,UAC5B,UACE,sBAAsB,WAAW,YAAY,GAC7C,sBAAsB,MAAM,YAAY,GACxC;AAAA,EACE,cAAc;AAChB,CACF,KACA,UAAU,WAAW,uBAAuB,MAAM,uBAAuB;AAAA,EACvE,cAAc;AAChB,CAAC,KACD,UAAU,WAAW,uBAAuB,MAAM,uBAAuB;AAAA,EACvE,cAAc;AAChB,CAAC,KACD,UAAU,WAAW,mBAAmB,MAAM,mBAAmB,EAAE,cAAc,KAAK,CAAC,KACvF,UACE,2BAA2B,WAAW,iBAAiB,GACvD,2BAA2B,MAAM,iBAAiB,GAClD,EAAE,cAAc,KAAK,CACvB,KACA,UAAU,WAAW,kBAAkB,MAAM,kBAAkB,EAAE,cAAc,KAAK,CAAC,KACrF,UAAU,WAAW,uBAAuB,MAAM,uBAAuB;AAAA,EACvE,cAAc;AAChB,CAAC,KACD,UAAU,WAAW,QAAQ,MAAM,QAAQ,EAAE,cAAc,KAAK,CAAC,KACjE,UAAU,WAAW,UAAU,MAAM,UAAU,EAAE,cAAc,KAAK,CAAC;AAUhE,IAAM,iBAAiB,CAAC,MAA+B,YAAkC;AAAA,EAC9F;AAAA,EACA,WAAW,KAAK;AAAA,EAChB,MAAM,KAAK;AAAA,EACX,SAAS,KAAK;AAAA,EACd,YAAY,KAAK;AAAA,EACjB,QAAQ,KAAK;AAAA,EACb,cAAc,sBAAsB,KAAK,YAAY;AAAA,EACrD,uBAAuB,KAAK;AAAA,EAC5B,uBAAuB,KAAK;AAAA,EAC5B,mBAAmB,KAAK;AAAA,EACxB,mBAAmB,2BAA2B,KAAK,iBAAiB;AAAA,EACpE,kBAAkB,KAAK;AAAA,EACvB,uBAAuB,KAAK;AAAA,EAC5B,QAAQ,KAAK;AAAA,EACb,UAAU,KAAK;AACjB;;;AC7KA;AACA;AAEA;AAEA;AAwBO,IAAM,kBAAkB,CAC7B,SACG;AAAA,EACH,MAAM,OAAO,CAAC,UACZ,KACG,UAAU,KAAK,EACf,KAAY,YAAI,CAAC,SAAU,SAAS,YAAY,YAAY,KAAK,WAAW,MAAM,KAAK,CAAE,CAAC;AAAA,EAE/F,OAAO;AAAA,IACL;AAAA,IAEA,aAAa,GAAG,MAAM,aACb,YAAI,UAAU,GAAG;AAAA,MACtB,MAAM,QAAQ,OAAO,KAAK,IAAI;AAAA,MAC9B,IAAI,UAAU;AAAA,QAAW;AAAA,MAIzB,OAAO,WAAW,YAAY,QAAQ,QAAQ,KAAK;AAAA,KACpD;AAAA,IAEH,MAAM,CAAC,MAAoB,WAClB,YAAI,UAAU,GAAG;AAAA,MACtB,IAAI,WAAW,aAAa,CAAC,WAAW,IAAI;AAAA,QAAG;AAAA,MAC/C,MAAM,OAAO,OAAO,KAAK,IAAI;AAAA,MAC7B,IAAI,SAAS;AAAA,QAAW,OAAO,EAAE,QAAQ,SAAS;AAAA,MAClD,OAAO,KAAK,QAAQ,MAAM,IAAI,IACzB,EAAE,QAAQ,OAAO,IACjB,EAAE,QAAQ,SAAS;AAAA,KACzB;AAAA,IAEH,WAAW,CAAC,SACH,YAAI,UAAU,GAAG;AAAA,MACtB,MAAM,OAAO,OAAO,KAAK,IAAI;AAAA,MAC7B,IAAI,SAAS;AAAA,QAAW,OAAO,OAAO,YAAY,KAAK,MAAM,KAAK,SAAS,IAAI,GAAG,QAAQ;AAAA,MAC1F,IAAI,CAAC,KAAK,QAAQ,MAAM,IAAI,GAAG;AAAA,QAC7B,OAAO,OAAO,YAAY,KAAK,MAAM,KAAK,SAAS,IAAI,GAAG,QAAQ;AAAA,MACpE;AAAA,MAGA,OAAO;AAAA,KACR;AAAA,IAEH,SAAS,CAAC,SAAgB,YAAY,KAAK,MAAM,KAAK,SAAS,IAAI,GAAG,QAAQ;AAAA,EAChF;AAAA;AASK,IAAM,gBAAgB,CAC3B,SACG;AAAA,EACH,MAAM,MAAM,gBAAgB,IAAI;AAAA,EAChC,MAAM,kBAAkB,CAAS,WACxB,gBAAQ,QAAQ,kBAAkB;AAAA,EAC3C,OAAO;AAAA,IACL,MAAM,MAAa,gBAAQ,CAAC,CAAC;AAAA,IAC7B,MAAM,CAAC,SACL,gBAAgB,IAAI,YAAY,IAAI,CAAC;AAAA,IACvC,MAAM,CAAC,SACL,gBAAgB,IAAI,KAAK,KAAK,MAAM,KAAK,MAAM,CAAC;AAAA,IAClD,WAAW,CAAC,SAA0B,gBAAgB,IAAI,UAAU,KAAK,IAAI,CAAC;AAAA,IAC9E,QAAQ,CAAC,SAA0B,gBAAgB,IAAI,QAAQ,KAAK,IAAI,CAAC;AAAA,EAC3E;AAAA;;;AFpFK,IAAM,eAAe,SAAuB,iBAAiB;AAAA,EAClE,sBAAsB;AACxB,CAAC;AAQM,IAAM,UAAU,CAAC,IAAY,UAClC,aAAa,IAAI,KAAK,EAAE,KAAK,MAAM,IAAI,CAAC;AAOnC,IAAM,OAKT;AAAA,EACF,MAAM;AAAA,EACN,UAAU,CAAC,UAAU,SAAS,MAAM,mBAAmB,MAAM;AAAA,EAC7D,WAAW,CAAC,UAEP,2BAAkB,EAAE,QAAQ,MAAM,QAAQ,WAAW,MAAM,UAAU,CAAC,EACtE,KAAY,iBAAS,YAAY,MAAa,gBAAQ,SAAS,CAAC,CAAC;AAAA,EACtE,YAAY;AAAA,EACZ;AACF;AAEO,IAAM,WAAW,cAAc,IAAI;AAEnC,IAAM,uBAAuB,MACzB,gBAAO,cAAqB,gBAAQ,aAAa,SAAS,GAAG,QAAQ,CAAC,CAAC;;AGtElF,qBAAS;AACT,kBAAS;AACT;AACA;AACA;;;ACkBA,IAAM,aAAY,CAAC,WAAwC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,EAAE,KAAK;AAE9E,IAAM,gBAAe,CAC1B,MACA,WAC4B;AAAA,EAC5B,QAAQ,MAAM;AAAA,EACd,gBAAgB,KAAK;AAAA,EACrB,MAAM,KAAK;AAAA,EACX,YAAY,WAAU,KAAK,UAAU;AAAA,EACrC,gBAAgB,KAAK,SAAS;AAChC;AAEO,IAAM,WAAU,CAAC,YAAoC,UAC1D,WAAW,SAAS,MAAM,QAC1B,WAAW,WAAW,KAAK,GAAG,MAAM,WAAU,MAAM,UAAU,EAAE,KAAK,GAAG;AAOnE,IAAM,sBAAsB,CACjC,MACA,YACuB;AAAA,EACvB;AAAA,EACA,gBAAgB,KAAK;AAAA,EACrB,MAAM,KAAK;AAAA,EACX,YAAY,WAAU,KAAK,UAAU;AACvC;;;AD3BO,IAAM,oBAAoB,UAA4B,sBAAsB;AAAA,EACjF,sBAAsB;AACxB,CAAC;AAGM,IAAM,eAAe,CAAC,IAAY,UACvC,kBAAkB,IAAI,KAAK,EAAE,KAAK,OAAM,IAAI,CAAC;AAGxC,IAAM,QAKT;AAAA,EACF,MAAM;AAAA,EACN,UAAU,CAAC,UAAU,SAAS,MAAM,yBAAyB,MAAM;AAAA,EACnE,WAAW,CAAC,UAEP,yBAAgB,EAAE,QAAQ,MAAM,QAAQ,gBAAgB,MAAM,eAAe,CAAC,EAC9E,KAAY,iBAAS,YAAY,MAAa,gBAAQ,SAAS,CAAC,CAAC;AAAA,EACtE,YAAY;AAAA,EACZ;AACF;AAEO,IAAM,YAAW,cAAc,KAAI;AAEnC,IAAM,4BAA4B,MAC9B,iBAAO,mBAA0B,gBAAQ,kBAAkB,SAAS,GAAG,SAAQ,CAAC,CAAC;;AEvD5F;AAIO,IAAM,YAAY,MAAY,eAAS,qBAAqB,GAAG,0BAA0B,CAAC;",
|
|
14
|
+
"debugId": "033CFC419E398B0664756E2164756E21",
|
|
15
|
+
"names": []
|
|
16
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type * as networks from '@distilled.cloud/unifi-network/networks';
|
|
2
|
+
/**
|
|
3
|
+
* ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE. `tsconfig.json` sets
|
|
4
|
+
* `exactOptionalPropertyTypes`, which treats `field?: T` (absent-or-T) and
|
|
5
|
+
* `field?: T | undefined` (absent-or-explicitly-undefined) as different types. `declareNetwork`
|
|
6
|
+
* below assigns `live.dhcpGuarding` (typed `T | undefined`) straight through — the whole point
|
|
7
|
+
* of a renderer that echoes a live optional field structurally rather than defaulting it — so
|
|
8
|
+
* the target has to admit an explicit `undefined`, not just omission.
|
|
9
|
+
*/
|
|
10
|
+
export interface NetworkProps {
|
|
11
|
+
siteId: string;
|
|
12
|
+
networkId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
management: string;
|
|
16
|
+
/** VLAN ID. Must be 1 for the default network and >= 2 for additional networks. */
|
|
17
|
+
vlanId: number;
|
|
18
|
+
dhcpGuarding?: networks.NetworkDHCPGuarding | undefined;
|
|
19
|
+
cellularBackupEnabled?: boolean | undefined;
|
|
20
|
+
internetAccessEnabled?: boolean | undefined;
|
|
21
|
+
/** Opaque — see the header. */
|
|
22
|
+
ipv4Configuration?: unknown;
|
|
23
|
+
ipv6Configuration?: networks.NetworkIPv6Configuration | undefined;
|
|
24
|
+
isolationEnabled?: boolean | undefined;
|
|
25
|
+
mdnsForwardingEnabled?: boolean | undefined;
|
|
26
|
+
zoneId?: string | undefined;
|
|
27
|
+
deviceId?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface NetworkAttributes {
|
|
30
|
+
siteId: string;
|
|
31
|
+
networkId: string;
|
|
32
|
+
/** Whether this is the site's undeletable default network — server-derived, never declared. */
|
|
33
|
+
default: boolean;
|
|
34
|
+
name: string;
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
management: string;
|
|
37
|
+
vlanId: number;
|
|
38
|
+
dhcpGuarding: networks.NetworkDHCPGuarding | undefined;
|
|
39
|
+
cellularBackupEnabled: boolean | undefined;
|
|
40
|
+
internetAccessEnabled: boolean | undefined;
|
|
41
|
+
ipv4Configuration: unknown;
|
|
42
|
+
ipv6Configuration: networks.NetworkIPv6Configuration | undefined;
|
|
43
|
+
isolationEnabled: boolean | undefined;
|
|
44
|
+
mdnsForwardingEnabled: boolean | undefined;
|
|
45
|
+
zoneId: string | undefined;
|
|
46
|
+
deviceId: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
export declare const attributesOf: (live: networks.NetworkDetails, props: NetworkProps) => NetworkAttributes;
|
|
49
|
+
/**
|
|
50
|
+
* ⚠️ `stripNullish: true` ON EVERY COMPARISON. UniFi's JSON answers `null` for an unset optional
|
|
51
|
+
* field; a declaration that omits the same prop carries `undefined`. Without this option every
|
|
52
|
+
* plan against an object with one unset optional field would report `update` forever — the
|
|
53
|
+
* exact "read and write are different shapes" trap `netbox/values.ts` and `proxmox/user-wire.ts`
|
|
54
|
+
* both work around, here solved once with the engine's own `deepEqual` instead of a per-field
|
|
55
|
+
* coercion, because nothing here needs PVE's or NetBox's extra wire-format translation.
|
|
56
|
+
*/
|
|
57
|
+
export declare const matches: (attributes: NetworkAttributes, props: NetworkProps) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The declaration renderer (task spec: "given one live object, return the props a declaration
|
|
60
|
+
* needs so its plan is noop"). A later import script feeds `getNetworkDetails`'s own response
|
|
61
|
+
* straight in and writes the result into `alchemy.run.ts` as `network('<id>', declareNetwork(live,
|
|
62
|
+
* siteId))` — no field is invented or defaulted here beyond what `attributesOf` already reports,
|
|
63
|
+
* so `matches(attributesOf(live, props), declareNetwork(live, siteId))` is `true` by construction
|
|
64
|
+
* (`network.test.ts` proves it) for any live object this SDK can decode.
|
|
65
|
+
*/
|
|
66
|
+
export declare const declareNetwork: (live: networks.NetworkDetails, siteId: string) => NetworkProps;
|
|
67
|
+
//# sourceMappingURL=network-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-form.d.ts","sourceRoot":"","sources":["../../src/unifi/network-form.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAwCzE;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACxD,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,+BAA+B;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IAClE,gBAAgB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5C,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,QAAQ,CAAC,mBAAmB,GAAG,SAAS,CAAC;IACvD,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,QAAQ,CAAC,wBAAwB,GAAG,SAAS,CAAC;IACjE,gBAAgB,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC,qBAAqB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,cAAc,SACtB,YAAY,KAClB,iBAiBD,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,eAAgB,iBAAiB,SAAS,YAAY,KAAG,OA6BL,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SAAU,QAAQ,CAAC,cAAc,UAAU,MAAM,KAAG,YAgB7E,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.Network` — one network on a UniFi Network site, READ-ONLY (`policy.ts`).
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY NETWORK IS IN THE FIRST IMPORT SET. `getNetworkDetails`/`getNetworksOverviewPage`
|
|
5
|
+
* (`../../distilled-unifi-network/src/services/networks.ts`) is a list+get pair over
|
|
6
|
+
* genuine configuration — VLAN, DHCP guarding, IPv4/IPv6 addressing, isolation — not runtime
|
|
7
|
+
* state like a connected client, a device statistic or a hotspot voucher. It is also the SDK
|
|
8
|
+
* package's own README recommendation for a first read/adopt-only resource, alongside `Site`
|
|
9
|
+
* (skipped here — no `getSite`, only the list operation, so it fails this task's own "both a
|
|
10
|
+
* list and a get" bar) and `FirewallZone`/`FirewallPolicy` (`firewall-zone.ts` ships
|
|
11
|
+
* `FirewallZone`; `FirewallPolicy`'s several discriminated filter variants and its
|
|
12
|
+
* list-replacing ordering endpoint are a bigger, separate PR).
|
|
13
|
+
*
|
|
14
|
+
* ⛔ THE WHOLE-OBJECT-PUT TRAP (`docs/unifi-api-notes.md`) NEVER FIRES HERE, BY CONSTRUCTION.
|
|
15
|
+
* `updateNetwork` is a `PUT` that disables `dhcpGuarding`/`ipv4Configuration`/`ipv6Configuration`
|
|
16
|
+
* outright when the caller omits them — the exact failure mode a read-only family cannot cause,
|
|
17
|
+
* because `resource.ts`'s `reconcile` never calls `updateNetwork` at all. Recorded here for
|
|
18
|
+
* whoever adds the write path this docs note anticipates: read-merge-write the WHOLE object,
|
|
19
|
+
* never a props-only body.
|
|
20
|
+
*/
|
|
21
|
+
import { Resource } from 'alchemy';
|
|
22
|
+
import * as Provider from 'alchemy/Provider';
|
|
23
|
+
import * as networks from '@distilled.cloud/unifi-network/networks';
|
|
24
|
+
import * as Effect from 'effect/Effect';
|
|
25
|
+
import { type NetworkAttributes, type NetworkProps } from './network-form.ts';
|
|
26
|
+
import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
27
|
+
export type { NetworkAttributes, NetworkProps } from './network-form.ts';
|
|
28
|
+
export { declareNetwork } from './network-form.ts';
|
|
29
|
+
export interface UnifiNetwork extends Resource<'Unifi.Network', NetworkProps, NetworkAttributes, never, UnifiRequirements> {
|
|
30
|
+
}
|
|
31
|
+
export declare const UnifiNetwork: import("alchemy").ResourceClass<UnifiNetwork>;
|
|
32
|
+
/**
|
|
33
|
+
* `network('lan', props)` — `adopt(true)` piped on by default (H5). This family never writes, so
|
|
34
|
+
* the only thing a first deploy against an existing network could ever do is bind to it or refuse
|
|
35
|
+
* `OwnedBySomeoneElse`; piping `adopt(true)` here picks the former without a stack having to
|
|
36
|
+
* remember `--adopt`, mirroring `discord/application-command.ts`'s `applicationCommand`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const network: (id: string, props: NetworkProps) => Effect.Effect<UnifiNetwork, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
39
|
+
/**
|
|
40
|
+
* ★ EXPORTED for direct testing against a fake `Credentials` layer (`fake-unifi.ts`), the same
|
|
41
|
+
* seam `netbox/prefix.ts`'s `spec` and `discord/application-command.ts`'s `spec` use — `handlers`
|
|
42
|
+
* bakes in `CredentialsFromEnv`, which resolves environment variables a test cannot repoint.
|
|
43
|
+
*/
|
|
44
|
+
export declare const spec: UnifiSpec<NetworkProps, networks.NetworkDetails, NetworkAttributes, networks.GetNetworkDetailsError>;
|
|
45
|
+
export declare const handlers: {
|
|
46
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
47
|
+
read: (args: {
|
|
48
|
+
olds: NetworkProps;
|
|
49
|
+
output: NetworkAttributes | undefined;
|
|
50
|
+
}) => Effect.Effect<NetworkAttributes | undefined, networks.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
51
|
+
diff: (args: {
|
|
52
|
+
news: import("alchemy").Input<NetworkProps>;
|
|
53
|
+
output: NetworkAttributes | undefined;
|
|
54
|
+
}) => Effect.Effect<{
|
|
55
|
+
readonly action: 'update';
|
|
56
|
+
} | {
|
|
57
|
+
readonly action: 'noop';
|
|
58
|
+
} | undefined, networks.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
59
|
+
reconcile: (args: {
|
|
60
|
+
news: NetworkProps;
|
|
61
|
+
}) => Effect.Effect<NetworkAttributes, import("./policy.ts").UnifiWriteRefused | networks.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
62
|
+
delete: (args: {
|
|
63
|
+
olds: NetworkProps;
|
|
64
|
+
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
65
|
+
};
|
|
66
|
+
export declare const UnifiNetworkProvider: () => import("effect/Layer").Layer<Provider.Provider<UnifiNetwork>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
67
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/unifi/network.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAGlB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,OAAQ,MAAM,SAAS,YAAY,6FACZ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,YAAY,EACZ,QAAQ,CAAC,cAAc,EACvB,iBAAiB,EACjB,QAAQ,CAAC,sBAAsB,CAUhC,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,oBAAoB,kIACkD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
export declare const UNIFI_READ_ONLY_POLICY = "read-only by Tim's rule, 2026-09-24; lifting it is a kit change";
|
|
3
|
+
/** What kind of write was asked for and refused — for the message only, never acted on. */
|
|
4
|
+
export type UnifiWriteAction = 'create' | 'update' | 'delete';
|
|
5
|
+
declare const UnifiWriteRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
6
|
+
readonly _tag: "UnifiWriteRefused";
|
|
7
|
+
} & Readonly<A>;
|
|
8
|
+
export declare class UnifiWriteRefused extends UnifiWriteRefused_base<{
|
|
9
|
+
/** The vendor type string, e.g. `Unifi.Network`. */
|
|
10
|
+
readonly type: string;
|
|
11
|
+
/** `spec.describe(props)` — the object the write would have touched. */
|
|
12
|
+
readonly identity: string;
|
|
13
|
+
readonly action: UnifiWriteAction;
|
|
14
|
+
}> {
|
|
15
|
+
get message(): string;
|
|
16
|
+
}
|
|
17
|
+
/** The only thing `reconcile`/`delete` ever return for a write this family cannot make. */
|
|
18
|
+
export declare const refuseWrite: (type: string, identity: string, action: UnifiWriteAction) => Effect.Effect<never, UnifiWriteRefused, never>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,eAAO,MAAM,sBAAsB,oEACgC,CAAC;AAEpE,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;;;;AAE9D,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED,2FAA2F;AAC3F,eAAO,MAAM,WAAW,SAAU,MAAM,YAAY,MAAM,UAAU,gBAAgB,mDACpB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `Unifi.*` provider as one layer for a stack.
|
|
3
|
+
*
|
|
4
|
+
* Layer.mergeAll(unifiProviders(), …the stack's other providers)
|
|
5
|
+
* .pipe(Layer.provide(FetchHttpClient.layer))
|
|
6
|
+
*
|
|
7
|
+
* S16: a subpath's `providers()` plays the role upstream's own `Providers.ts` does — one minimal
|
|
8
|
+
* insertion per family. `FetchHttpClient.layer` is the stack's own responsibility to provide,
|
|
9
|
+
* mirroring `../discord/providers.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
export declare const providers: () => Layer.Layer<import("alchemy").Provider<import("./firewall-zone.ts").UnifiFirewallZone> | import("alchemy").Provider<import("./network.ts").UnifiNetwork>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
13
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/unifi/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAItC,eAAO,MAAM,SAAS,8NAA4E,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Input } from 'alchemy/Input';
|
|
2
|
+
import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
5
|
+
/** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
|
|
6
|
+
export type UnifiRequirements = HttpClient.HttpClient;
|
|
7
|
+
/**
|
|
8
|
+
* One UniFi Network object's read. `Live` is whatever the SDK decodes (a `NetworkDetails`, a
|
|
9
|
+
* `FirewallZone`, …); `E` is left to each resource file to declare (its own `fetchLive`'s error
|
|
10
|
+
* union, with `NotFound` already folded away) so the precise per-operation error union flows
|
|
11
|
+
* through instead of being widened by hand here.
|
|
12
|
+
*/
|
|
13
|
+
export type UnifiSpec<Props extends object, Live, Attributes extends object, E> = {
|
|
14
|
+
/** The vendor type string, e.g. `Unifi.Network` — for the refusal message only. */
|
|
15
|
+
readonly type: string;
|
|
16
|
+
/** Already folds a genuine not-found to `undefined`; nothing else is folded (see header). */
|
|
17
|
+
readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, UnifiNetworkOpContext>;
|
|
18
|
+
readonly attributes: (live: Live, props: Props) => Attributes;
|
|
19
|
+
readonly matches: (attributes: Attributes, props: Props) => boolean;
|
|
20
|
+
/** For the refusal message and read-back failures. */
|
|
21
|
+
readonly describe: (props: Props) => string;
|
|
22
|
+
};
|
|
23
|
+
export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
|
|
24
|
+
read: (props: Props) => Effect.Effect<Attributes | undefined, E, UnifiNetworkOpContext>;
|
|
25
|
+
readHandler: ({ olds, output }: {
|
|
26
|
+
olds: Props;
|
|
27
|
+
output: Attributes | undefined;
|
|
28
|
+
}) => Effect.Effect<Attributes | undefined, E, UnifiNetworkOpContext>;
|
|
29
|
+
diff: (news: Input<Props>, output: Attributes | undefined) => Effect.Effect<{
|
|
30
|
+
readonly action: 'update';
|
|
31
|
+
} | {
|
|
32
|
+
readonly action: 'noop';
|
|
33
|
+
} | undefined, E, UnifiNetworkOpContext>;
|
|
34
|
+
reconcile: (news: Props) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
|
|
35
|
+
destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The four provider handlers for a spec'd read-only UniFi object, wired once.
|
|
39
|
+
*
|
|
40
|
+
* ⛔ `list` ANSWERS EMPTY. `GET /v1/sites/{siteId}/networks` answers every network on the site;
|
|
41
|
+
* adoption stays explicit, the same reasoning `Proxmox.User`'s and NetBox's `list` give.
|
|
42
|
+
*/
|
|
43
|
+
export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
|
|
44
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
45
|
+
read: (args: {
|
|
46
|
+
olds: Props;
|
|
47
|
+
output: Attributes | undefined;
|
|
48
|
+
}) => Effect.Effect<Attributes | undefined, E, HttpClient.HttpClient>;
|
|
49
|
+
diff: (args: {
|
|
50
|
+
news: Input<Props>;
|
|
51
|
+
output: Attributes | undefined;
|
|
52
|
+
}) => Effect.Effect<{
|
|
53
|
+
readonly action: 'update';
|
|
54
|
+
} | {
|
|
55
|
+
readonly action: 'noop';
|
|
56
|
+
} | undefined, E, HttpClient.HttpClient>;
|
|
57
|
+
reconcile: (args: {
|
|
58
|
+
news: Props;
|
|
59
|
+
}) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
|
|
60
|
+
delete: (args: {
|
|
61
|
+
olds: Props;
|
|
62
|
+
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAGnE,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAChF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAChF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;kBAEtB,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;sBAIP,KAAK;oBAYP,KAAK;CAExB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAC9E,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAMzC,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBAtCtB,QAAQ;;yBAElC,MAAM;;IAsCzB,SAAS,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACjC,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
|
|
@@ -48,11 +48,25 @@ export declare const writesOf: (cloud: Cloud) => string[];
|
|
|
48
48
|
export declare const thingProviders: (cloud: Cloud) => Layer.Layer<Alchemy.Provider<Blind> | Alchemy.Provider<Recorded> | Alchemy.Provider<Thing>, never, never>;
|
|
49
49
|
/** A stack body: resource declarations, as an `alchemy.run.ts` would write them. */
|
|
50
50
|
export type Body = Effect.Effect<unknown, unknown, unknown>;
|
|
51
|
+
/** One row of `Alchemy.Drift.detect`'s dry-run report — the fields `drift` below asserts on. */
|
|
52
|
+
export type DriftRow = {
|
|
53
|
+
readonly action: string;
|
|
54
|
+
};
|
|
51
55
|
export interface FakeEngine {
|
|
52
56
|
/** Plan and apply `body`, as `alchemy deploy --adopt` would; resolves to the planned actions. */
|
|
53
57
|
readonly deploy: (body: Body) => Promise<Record<string, string>>;
|
|
54
58
|
/** Verify `body` against what the earlier deploys recorded. */
|
|
55
59
|
readonly verify: (body: Body, options?: VerifyOptions) => Promise<AdoptReport>;
|
|
60
|
+
/**
|
|
61
|
+
* `alchemy drift`'s own dry-run (`Alchemy.Drift.detect`) over whatever `deploy` already
|
|
62
|
+
* persisted — `body` is used only to resolve the stack's name/stage/providers, exactly as
|
|
63
|
+
* `verify` does; drift itself reads the state store, not `body`'s declarations. ⚠️ A DIFFERENT
|
|
64
|
+
* ENGINE PATH FROM `verify`'s `--all` READ, proxmox/read-failure-propagation.test.ts's own ⛔:
|
|
65
|
+
* `Drift.ts` calls a provider's `read` with `output` set to the PERSISTED attributes, and folds
|
|
66
|
+
* a `read` that resolves `undefined` into `{action: 'missing'}` with no error — silent, unlike
|
|
67
|
+
* `verify.ts`'s `readWithState`, which already wraps that same call in `Effect.catchCause`.
|
|
68
|
+
*/
|
|
69
|
+
readonly drift: (body: Body) => Promise<Record<string, DriftRow>>;
|
|
56
70
|
/**
|
|
57
71
|
* Every state row the store holds, as JSON — what a Postgres store would persist. ★ For tests
|
|
58
72
|
* that must prove a value NEVER reaches state (proxmox/pbs-notification-target-state.test.ts).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-engine.d.ts","sourceRoot":"","sources":["../../src/verify/fake-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAKnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AACnG,eAAO,MAAM,QAAQ,iCAA8C,CAAC;AAEpE,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,KAI1D,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAC2C,CAAC;AAyB1F,4FAA4F;AAC5F,eAAO,MAAM,cAAc,UAAW,KAAK,8GAmCxC,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAK5D,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/E;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAG,
|
|
1
|
+
{"version":3,"file":"fake-engine.d.ts","sourceRoot":"","sources":["../../src/verify/fake-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAKnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AACnG,eAAO,MAAM,QAAQ,iCAA8C,CAAC;AAEpE,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,KAI1D,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAC2C,CAAC;AAyB1F,4FAA4F;AAC5F,eAAO,MAAM,cAAc,UAAW,KAAK,8GAmCxC,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAK5D,gGAAgG;AAChG,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/E;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAG,UAmD3E,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,UAAU,UAAW,KAAK,KAAG,UAA+C,CAAC"}
|