@homeflare/alchemy 0.31.2 → 0.33.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 +39 -3
- package/dist/grafana/dashboard-model.d.ts +77 -0
- package/dist/grafana/dashboard-model.d.ts.map +1 -0
- package/dist/grafana/dashboard.d.ts +98 -0
- package/dist/grafana/dashboard.d.ts.map +1 -0
- package/dist/grafana/folder.d.ts +100 -0
- package/dist/grafana/folder.d.ts.map +1 -0
- package/dist/grafana/index.d.ts +9 -8
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +158 -6
- package/dist/grafana/index.js.map +8 -4
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/grafana/provisioned.d.ts +11 -0
- package/dist/grafana/provisioned.d.ts.map +1 -0
- 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/index.js +170 -80
- package/dist/proxmox/index.js.map +9 -7
- 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/zfs-pool-form.d.ts +124 -0
- package/dist/proxmox/zfs-pool-form.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-wire.d.ts +78 -0
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +27 -52
- package/dist/proxmox/zfs-pool.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/docs/grafana-folder-dashboard.md +145 -0
- package/docs/grafana.md +11 -10
- 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 +9 -4
- package/package.json +12 -2
- package/src/grafana/dashboard-model.test.ts +111 -0
- package/src/grafana/dashboard-model.ts +111 -0
- package/src/grafana/dashboard.test.ts +232 -0
- package/src/grafana/dashboard.ts +161 -0
- package/src/grafana/folder.test.ts +212 -0
- package/src/grafana/folder.ts +161 -0
- package/src/grafana/index.ts +21 -8
- package/src/grafana/providers.ts +7 -1
- package/src/grafana/provisioned.ts +49 -0
- 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/constraints-host-forms.test.ts +1 -1
- 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/zfs-pool-adopt.test.ts +26 -0
- package/src/proxmox/zfs-pool-form.ts +236 -0
- package/src/proxmox/zfs-pool-read-failure.test.ts +118 -0
- package/src/proxmox/zfs-pool-wire.ts +132 -0
- package/src/proxmox/zfs-pool.ts +106 -76
- 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/dist/proxmox/zfs-pool-spec.d.ts +0 -27
- package/dist/proxmox/zfs-pool-spec.d.ts.map +0 -1
- package/dist/proxmox/zfs-pool-write.d.ts +0 -128
- package/dist/proxmox/zfs-pool-write.d.ts.map +0 -1
- package/src/proxmox/zfs-pool-spec.ts +0 -76
- package/src/proxmox/zfs-pool-write.ts +0 -196
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// src/opnsense/alias.ts
|
|
2
|
+
import { adopt } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { Resource } from "alchemy";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import * as Alias from "@distilled.cloud/opnsense/firewall_alias";
|
|
6
|
+
import * as Effect3 from "effect/Effect";
|
|
7
|
+
|
|
8
|
+
// src/opnsense/wire.ts
|
|
9
|
+
var bool01 = (value, fallback) => value === undefined ? fallback : value === "1";
|
|
10
|
+
var csvSet = (value) => {
|
|
11
|
+
if (value === undefined || value.trim() === "")
|
|
12
|
+
return [];
|
|
13
|
+
const items = value.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
14
|
+
return [...new Set(items)].sort();
|
|
15
|
+
};
|
|
16
|
+
var csvSetOf = (value) => csvSet(value === undefined ? undefined : value.join(","));
|
|
17
|
+
var numField = (value, fallback) => {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return fallback;
|
|
20
|
+
const parsed = Number(value);
|
|
21
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/opnsense/alias-form.ts
|
|
25
|
+
var attributesOf = (uuid, live) => ({
|
|
26
|
+
categories: csvSet(live.categories),
|
|
27
|
+
content: live.content ?? "",
|
|
28
|
+
counters: bool01(live.counters, false),
|
|
29
|
+
description: live.description ?? "",
|
|
30
|
+
enabled: bool01(live.enabled, true),
|
|
31
|
+
interface: live.interface ?? "",
|
|
32
|
+
name: live.name,
|
|
33
|
+
proto: live.proto ?? "",
|
|
34
|
+
type: live.type,
|
|
35
|
+
updatefreq: numField(live.updatefreq, 0),
|
|
36
|
+
uuid
|
|
37
|
+
});
|
|
38
|
+
var matches = (attributes, props) => attributes.name === props.name && attributes.type === props.type && attributes.enabled === (props.enabled ?? true) && attributes.content === (props.content ?? "") && attributes.interface === (props.interface ?? "") && attributes.proto === (props.proto ?? "") && attributes.counters === (props.counters ?? false) && attributes.updatefreq === (props.updatefreq ?? 0) && attributes.description === (props.description ?? "") && attributes.categories.join(",") === csvSetOf(props.categories).join(",");
|
|
39
|
+
|
|
40
|
+
// src/opnsense/resource.ts
|
|
41
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
42
|
+
import { isResolved } from "alchemy/Diff";
|
|
43
|
+
import * as Effect2 from "effect/Effect";
|
|
44
|
+
|
|
45
|
+
// src/opnsense/credentials.ts
|
|
46
|
+
import {
|
|
47
|
+
Credentials,
|
|
48
|
+
CredentialsFromEnv,
|
|
49
|
+
credentials
|
|
50
|
+
} from "@distilled.cloud/opnsense/Credentials";
|
|
51
|
+
var fromEnv = () => CredentialsFromEnv;
|
|
52
|
+
|
|
53
|
+
// src/opnsense/policy.ts
|
|
54
|
+
import * as Data from "effect/Data";
|
|
55
|
+
import * as Effect from "effect/Effect";
|
|
56
|
+
|
|
57
|
+
class OpnsenseWriteRefused extends Data.TaggedError("OpnsenseWriteRefused") {
|
|
58
|
+
get message() {
|
|
59
|
+
return `${this.resourceType} ${this.id}: refusing to ${this.action} — read-only by Tim's rule, ` + "2026-09-24; lifting it is a kit change, not a prop.";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var refuseWrite = (resourceType, id, action) => Effect.fail(new OpnsenseWriteRefused({ action, id, resourceType }));
|
|
63
|
+
|
|
64
|
+
// src/opnsense/resource.ts
|
|
65
|
+
var opnsenseOperations = (spec) => {
|
|
66
|
+
const read = (props) => spec.fetchLive(props).pipe(Effect2.map((live) => live === undefined ? undefined : spec.attributes(props.uuid, live)));
|
|
67
|
+
return {
|
|
68
|
+
read: ({ olds, output }) => Effect2.gen(function* () {
|
|
69
|
+
const attrs = yield* read(olds);
|
|
70
|
+
if (attrs === undefined)
|
|
71
|
+
return;
|
|
72
|
+
if (output !== undefined)
|
|
73
|
+
return attrs;
|
|
74
|
+
return Unowned(attrs);
|
|
75
|
+
}),
|
|
76
|
+
diff: (news, output) => Effect2.gen(function* () {
|
|
77
|
+
if (output === undefined || !isResolved(news))
|
|
78
|
+
return;
|
|
79
|
+
const live = yield* read(news);
|
|
80
|
+
if (live === undefined)
|
|
81
|
+
return { action: "update" };
|
|
82
|
+
return spec.matches(live, news) ? { action: "noop" } : { action: "update" };
|
|
83
|
+
}),
|
|
84
|
+
reconcile: (news) => Effect2.gen(function* () {
|
|
85
|
+
const live = yield* read(news);
|
|
86
|
+
return yield* refuseWrite(spec.resourceType, news.uuid, live === undefined ? "create" : "update");
|
|
87
|
+
}),
|
|
88
|
+
destroy: (olds) => refuseWrite(spec.resourceType, olds.uuid, "delete")
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
var opnsenseHandlers = (spec) => {
|
|
92
|
+
const ops = opnsenseOperations(spec);
|
|
93
|
+
const withCredentials = (effect) => Effect2.provide(effect, CredentialsFromEnv);
|
|
94
|
+
return {
|
|
95
|
+
list: () => Effect2.succeed([]),
|
|
96
|
+
nuke: { skip: true },
|
|
97
|
+
read: ({ olds, output }) => withCredentials(ops.read({ olds, output })),
|
|
98
|
+
diff: ({ news, output }) => withCredentials(ops.diff(news, output)),
|
|
99
|
+
reconcile: ({ news }) => withCredentials(ops.reconcile(news)),
|
|
100
|
+
delete: ({ olds }) => withCredentials(ops.destroy(olds))
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/opnsense/alias.ts
|
|
105
|
+
var OpnsenseFirewallAlias = Resource("Opnsense.Firewall.Alias", {
|
|
106
|
+
defaultRemovalPolicy: "retain"
|
|
107
|
+
});
|
|
108
|
+
var isOpnsenseFirewallAlias = (value) => typeof value === "object" && value !== null && value.Type === "Opnsense.Firewall.Alias";
|
|
109
|
+
var spec = {
|
|
110
|
+
attributes: attributesOf,
|
|
111
|
+
fetchLive: (props) => Alias.get({}).pipe(Effect3.map((res) => res.alias?.aliases?.alias?.[props.uuid])),
|
|
112
|
+
matches,
|
|
113
|
+
resourceType: "Opnsense.Firewall.Alias"
|
|
114
|
+
};
|
|
115
|
+
var propsFromLive = attributesOf;
|
|
116
|
+
var handlers = opnsenseHandlers(spec);
|
|
117
|
+
var OpnsenseFirewallAliasProvider = () => Provider.effect(OpnsenseFirewallAlias, Effect3.succeed(OpnsenseFirewallAlias.Provider.of(handlers)));
|
|
118
|
+
var firewallAlias = (id, props) => OpnsenseFirewallAlias(id, props).pipe(adopt(true));
|
|
119
|
+
// src/opnsense/category.ts
|
|
120
|
+
import { adopt as adopt2 } from "alchemy/AdoptPolicy";
|
|
121
|
+
import { Resource as Resource2 } from "alchemy";
|
|
122
|
+
import * as Provider2 from "alchemy/Provider";
|
|
123
|
+
import * as Category from "@distilled.cloud/opnsense/firewall_category";
|
|
124
|
+
import * as Effect4 from "effect/Effect";
|
|
125
|
+
|
|
126
|
+
// src/opnsense/category-form.ts
|
|
127
|
+
var attributesOf2 = (uuid, live) => ({
|
|
128
|
+
auto: bool01(live.auto, false),
|
|
129
|
+
color: live.color ?? "",
|
|
130
|
+
name: live.name,
|
|
131
|
+
uuid
|
|
132
|
+
});
|
|
133
|
+
var matches2 = (attributes, props) => attributes.name === props.name && attributes.auto === (props.auto ?? false) && attributes.color === (props.color ?? "");
|
|
134
|
+
|
|
135
|
+
// src/opnsense/category.ts
|
|
136
|
+
var OpnsenseFirewallCategory = Resource2("Opnsense.Firewall.Category", {
|
|
137
|
+
defaultRemovalPolicy: "retain"
|
|
138
|
+
});
|
|
139
|
+
var isOpnsenseFirewallCategory = (value) => typeof value === "object" && value !== null && value.Type === "Opnsense.Firewall.Category";
|
|
140
|
+
var spec2 = {
|
|
141
|
+
attributes: attributesOf2,
|
|
142
|
+
fetchLive: (props) => Category.get({}).pipe(Effect4.map((res) => res.category?.categories?.category?.[props.uuid])),
|
|
143
|
+
matches: matches2,
|
|
144
|
+
resourceType: "Opnsense.Firewall.Category"
|
|
145
|
+
};
|
|
146
|
+
var propsFromLive2 = attributesOf2;
|
|
147
|
+
var handlers2 = opnsenseHandlers(spec2);
|
|
148
|
+
var OpnsenseFirewallCategoryProvider = () => Provider2.effect(OpnsenseFirewallCategory, Effect4.succeed(OpnsenseFirewallCategory.Provider.of(handlers2)));
|
|
149
|
+
var firewallCategory = (id, props) => OpnsenseFirewallCategory(id, props).pipe(adopt2(true));
|
|
150
|
+
// src/opnsense/group.ts
|
|
151
|
+
import { adopt as adopt3 } from "alchemy/AdoptPolicy";
|
|
152
|
+
import { Resource as Resource3 } from "alchemy";
|
|
153
|
+
import * as Provider3 from "alchemy/Provider";
|
|
154
|
+
import * as Group from "@distilled.cloud/opnsense/firewall_group";
|
|
155
|
+
import * as Effect5 from "effect/Effect";
|
|
156
|
+
|
|
157
|
+
// src/opnsense/group-form.ts
|
|
158
|
+
var attributesOf3 = (uuid, live) => ({
|
|
159
|
+
description: live.descr ?? "",
|
|
160
|
+
ifname: live.ifname,
|
|
161
|
+
members: csvSet(live.members),
|
|
162
|
+
nogroup: bool01(live.nogroup, false),
|
|
163
|
+
sequence: numField(live.sequence, 0),
|
|
164
|
+
uuid
|
|
165
|
+
});
|
|
166
|
+
var matches3 = (attributes, props) => attributes.ifname === props.ifname && attributes.nogroup === (props.nogroup ?? false) && attributes.sequence === (props.sequence ?? 0) && attributes.description === (props.description ?? "") && attributes.members.join(",") === csvSetOf(props.members).join(",");
|
|
167
|
+
|
|
168
|
+
// src/opnsense/group.ts
|
|
169
|
+
var OpnsenseFirewallGroup = Resource3("Opnsense.Firewall.Group", {
|
|
170
|
+
defaultRemovalPolicy: "retain"
|
|
171
|
+
});
|
|
172
|
+
var isOpnsenseFirewallGroup = (value) => typeof value === "object" && value !== null && value.Type === "Opnsense.Firewall.Group";
|
|
173
|
+
var spec3 = {
|
|
174
|
+
attributes: attributesOf3,
|
|
175
|
+
fetchLive: (props) => Group.get({}).pipe(Effect5.map((res) => res.group?.ifgroupentry?.[props.uuid])),
|
|
176
|
+
matches: matches3,
|
|
177
|
+
resourceType: "Opnsense.Firewall.Group"
|
|
178
|
+
};
|
|
179
|
+
var propsFromLive3 = attributesOf3;
|
|
180
|
+
var handlers3 = opnsenseHandlers(spec3);
|
|
181
|
+
var OpnsenseFirewallGroupProvider = () => Provider3.effect(OpnsenseFirewallGroup, Effect5.succeed(OpnsenseFirewallGroup.Provider.of(handlers3)));
|
|
182
|
+
var firewallGroup = (id, props) => OpnsenseFirewallGroup(id, props).pipe(adopt3(true));
|
|
183
|
+
// src/opnsense/providers.ts
|
|
184
|
+
import * as Layer from "effect/Layer";
|
|
185
|
+
var providers = () => Layer.mergeAll(OpnsenseFirewallAliasProvider(), OpnsenseFirewallCategoryProvider(), OpnsenseFirewallGroupProvider());
|
|
186
|
+
export {
|
|
187
|
+
Credentials,
|
|
188
|
+
CredentialsFromEnv,
|
|
189
|
+
OpnsenseFirewallAlias,
|
|
190
|
+
OpnsenseFirewallAliasProvider,
|
|
191
|
+
OpnsenseFirewallCategory,
|
|
192
|
+
OpnsenseFirewallCategoryProvider,
|
|
193
|
+
OpnsenseFirewallGroup,
|
|
194
|
+
OpnsenseFirewallGroupProvider,
|
|
195
|
+
OpnsenseWriteRefused,
|
|
196
|
+
propsFromLive as aliasPropsFromLive,
|
|
197
|
+
propsFromLive2 as categoryPropsFromLive,
|
|
198
|
+
credentials,
|
|
199
|
+
firewallAlias,
|
|
200
|
+
firewallCategory,
|
|
201
|
+
firewallGroup,
|
|
202
|
+
fromEnv,
|
|
203
|
+
propsFromLive3 as groupPropsFromLive,
|
|
204
|
+
isOpnsenseFirewallAlias,
|
|
205
|
+
isOpnsenseFirewallCategory,
|
|
206
|
+
isOpnsenseFirewallGroup,
|
|
207
|
+
providers
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
//# debugId=443068D95E23772E64756E2164756E21
|
|
211
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/opnsense/alias.ts", "../src/opnsense/wire.ts", "../src/opnsense/alias-form.ts", "../src/opnsense/resource.ts", "../src/opnsense/credentials.ts", "../src/opnsense/policy.ts", "../src/opnsense/category.ts", "../src/opnsense/category-form.ts", "../src/opnsense/group.ts", "../src/opnsense/group-form.ts", "../src/opnsense/providers.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `Opnsense.Firewall.Alias` — a read-only declaration of one `firewall/alias` item on the edge.\n * READ-ONLY BY DESIGN — see policy.ts's header. `reconcile`/`delete` always refuse.\n *\n * ★ READ GOES THROUGH `get()`, NEVER `getItem`/`searchItem`. `get()` (`firewall_alias.ts#get`)\n * returns the WHOLE model tree — `{ alias: { aliases: { alias: { <uuid>: AliasItem, ... } } } }`\n * — so finding one item by uuid is an object-key lookup, not a request the vendor could answer\n * ambiguously. That sidesteps two real gaps in this early SDK, both measured against the\n * generated `firewall_alias.ts`: (1) there is no per-item `getAlias` operation at all — only\n * `get` (whole model) and `searchAlias` (paginated, see below) exist; (2) `firewall_category.ts`\n * and `firewall_group.ts` DO export a `getCategory`/`getGroup`, but their generated request\n * schema carries no `uuid` path parameter (`GetCategoryRequest`/`GetGroupRequest` are both\n * `S.Struct({})` against a `uri` with no `{uuid}` placeholder) — a generation gap, not a design\n * choice, so this family does not rely on them for any of the three resources, uniformly.\n *\n * ⛔ NOT `searchAlias`. It is POST-shaped (a read, per OPNsense's own MVC convention — see the\n * family's docs page) and paginated with no typed limit/offset the generated request schema\n * exposes, so a large alias set could silently page-truncate through it. `get()` has neither\n * problem: GET, and the WHOLE model in one call, no pagination to fall short of.\n *\n * ⚠️ `uuid` IS THE ONLY STABLE IDENTITY THIS FAMILY DECLARES BY. A name is close to stable\n * (`AliasNameField` is unique) but is not what `get()`'s map is keyed on — obtain the live\n * uuid once (a future import script does this from a live read, via `propsFromLive` below)\n * and declare it from then on, the same \"adoption is explicit, one identity at a time\" posture\n * `../proxmox/user.ts` and `../netbox/resource.ts` document for their own families.\n */\nimport { adopt } from 'alchemy/AdoptPolicy';\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Alias from '@distilled.cloud/opnsense/firewall_alias';\nimport type { OpnsenseOpError } from '@distilled.cloud/opnsense/Protocol';\nimport * as Effect from 'effect/Effect';\nimport { attributesOf, matches } from './alias-form.ts';\nimport type { OpnsenseRequirements, OpnsenseSpec } from './resource.ts';\nimport { opnsenseHandlers } from './resource.ts';\n\nexport interface AliasProps {\n /** OPNsense-assigned uuid — see the file header on why this is the only identity to declare by. */\n readonly uuid: string;\n readonly name: string;\n /** e.g. `host`, `network`, `port`, `url`, `geoip`, `networkgroup` — OPNsense's own alias types. */\n readonly type: string;\n /** Default true, matching OPNsense's own `enabled=1`. */\n readonly enabled?: boolean;\n /** The alias's members — a host list, a CIDR, a URL, depending on `type`. */\n readonly content?: string;\n readonly interface?: string;\n /** `IPv4` or `IPv6`, for a `geoip`/`networkgroup` alias; comma-joined on the wire (wire.ts). */\n readonly proto?: string;\n readonly counters?: boolean;\n /** Minutes between refreshes, for a `url`/`urltable` alias. */\n readonly updatefreq?: number;\n readonly categories?: readonly string[];\n readonly description?: string;\n}\n\nexport interface AliasAttributes {\n readonly uuid: string;\n readonly name: string;\n readonly type: string;\n readonly enabled: boolean;\n readonly content: string;\n readonly interface: string;\n readonly proto: string;\n readonly counters: boolean;\n readonly updatefreq: number;\n readonly categories: readonly string[];\n readonly description: string;\n}\n\nexport interface OpnsenseFirewallAlias extends Resource<\n 'Opnsense.Firewall.Alias',\n AliasProps,\n AliasAttributes,\n never,\n OpnsenseRequirements\n> {}\n\nexport const OpnsenseFirewallAlias = Resource<OpnsenseFirewallAlias>('Opnsense.Firewall.Alias', {\n // The edge's own config, not this stack's to reclaim — see policy.ts.\n defaultRemovalPolicy: 'retain',\n});\n\nexport const isOpnsenseFirewallAlias = (value: unknown): value is OpnsenseFirewallAlias =>\n typeof value === 'object' &&\n value !== null &&\n (value as { Type?: unknown }).Type === 'Opnsense.Firewall.Alias';\n\n/** Exported for direct testing against `fake-opnsense.ts` — the same seam `../discord/*.ts` uses. */\nexport const spec: OpnsenseSpec<AliasProps, Alias.AliasItem, AliasAttributes, OpnsenseOpError> = {\n attributes: attributesOf,\n fetchLive: (props) =>\n Alias.get({}).pipe(Effect.map((res) => res.alias?.aliases?.alias?.[props.uuid])),\n matches,\n resourceType: 'Opnsense.Firewall.Alias',\n};\n\n/**\n * The pure declaration renderer: given the uuid `get()`'s map key names and the `AliasItem`\n * decoded at that key, return the props a declaration needs so `firewallAlias.spec.matches`\n * reports noop against it. A later import script iterates `Object.entries(map)` and calls this\n * once per row to generate an `alchemy.run.ts` row — see `alias-form.test.ts`'s round-trip test.\n */\nexport const propsFromLive = attributesOf;\n\nexport const handlers = opnsenseHandlers(spec);\n\nexport const OpnsenseFirewallAliasProvider = () =>\n Provider.effect(\n OpnsenseFirewallAlias,\n Effect.succeed(OpnsenseFirewallAlias.Provider.of(handlers)),\n );\n\n/** `firewallAlias('name-alias', { uuid, name, type })` — `adopt(true)` piped by default (H5), so\n * declaring a live alias's own uuid takes it over silently instead of failing `OwnedBySomeoneElse`. */\nexport const firewallAlias = (id: string, props: AliasProps) =>\n OpnsenseFirewallAlias(id, props).pipe(adopt(true));\n",
|
|
6
|
+
"/**\n * How OPNsense's MVC model spells a value on the wire, and how to get it back to something\n * comparable — the `../proxmox/values.ts` idea, scoped to this vendor's own spellings.\n *\n * ★ OPNSENSE HAS EXACTLY TWO WIRE SHAPES THIS FAMILY NEEDS. Every `BooleanField` is the string\n * `\"1\"` or `\"0\"` (never a JSON boolean — `AliasItem.enabled: string`, `GroupItem.nogroup?:\n * string`, see `firewall_alias.ts`/`firewall_group.ts`). Every multi-select `OptionField`/\n * `ModelRelationField` is a SINGLE comma-joined string, not a JSON array (`AliasItem.categories`,\n * `GroupItem.members` — both carry the doc comment \"wire value is a single comma-joined string,\n * not a JSON array\" in the generated SDK). Nothing here is specific to one resource.\n *\n * ⚠️ COMMA-LIST ORDER ROUND-TRIP IS NOT ASSUMED STABLE. OPNsense builds these from a PHP\n * associative array (`ModelRelationField`/`OptionField::getNodeData()`), and nothing in the\n * model XML promises the join order survives a save-then-read unchanged. Sorting and\n * deduplicating both sides before comparing is what makes `matches` (alias-form.ts,\n * group-form.ts) actually settle instead of reporting drift on a re-ordered list that changed\n * nothing — the same reasoning `../proxmox/user-wire.ts`'s `groupSet` documents for PVE's\n * `groups` field.\n */\n\n/** OPNsense's own boolean spelling. Absent takes the caller's stated default — OPNsense's own\n * default for a `BooleanField` is form-specific, so there is no house-wide fallback here. */\nexport const bool01 = (value: string | undefined, fallback: boolean): boolean =>\n value === undefined ? fallback : value === '1';\n\n/** The write direction, for symmetry — unused today (this family never writes), kept so a form\n * helper never has to guess the spelling if lifting the read-only policy adds one later. */\nexport const toBool01 = (value: boolean): string => (value ? '1' : '0');\n\n/**\n * A comma-joined multi-select column, normalised to a sorted, deduplicated set — see the module\n * header's ⚠️ for why normalising (not just splitting) is what makes a diff settle.\n */\nexport const csvSet = (value: string | undefined): readonly string[] => {\n if (value === undefined || value.trim() === '') return [];\n const items = value\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item !== '');\n return [...new Set(items)].sort();\n};\n\n/** A declared array, normalised the SAME way `csvSet` normalises the wire, so `matches` compares\n * two sets built by one function rather than trusting the caller's array to already be sorted. */\nexport const csvSetOf = (value: readonly string[] | undefined): readonly string[] =>\n csvSet(value === undefined ? undefined : value.join(','));\n\n/**\n * OPNsense's numeric-as-string fields (`\"60\"`, `\"0\"`), tolerant of an already-numeric input the\n * same way `../proxmox/user-wire.ts`'s `int` is tolerant of PVE's mixed number/string releases.\n */\nexport const numField = (value: string | undefined, fallback: number): number => {\n if (value === undefined) return fallback;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : fallback;\n};\n",
|
|
7
|
+
"/**\n * `Opnsense.Firewall.Alias`'s wire shape: how a live `AliasItem` becomes attributes, and the\n * comparison that decides whether a declaration already matches it. Split out of alias.ts to\n * keep that file under the house's 250-line cap, mirroring `../proxmox/user-wire.ts`.\n *\n * ⛔ SCOPE, STATED ONCE. `AliasItem` (firewall_alias.ts) carries far more fields than this props\n * type declares — three groups are left out on purpose:\n * 1. Runtime counters (`current_items`, `last_updated`, `eval_nomatch`, `eval_match`,\n * `in_block_p`/`_b`, `in_pass_p`/`_b`, `out_block_p`/`_b`, `out_pass_p`/`_b`) — these are\n * the alias's hit/block statistics, not configuration, exactly the \"stats\" the task that\n * opened this family says to exclude.\n * 2. `path_expression` — only meaningful for the `urljson` alias type; a niche field this\n * first import pass leaves for a later one rather than modelling one type's one field.\n * 3. `password`/`username`/`authtype`/`expire` — the URL-fetch credential fields for the\n * `url`/`urltable`/`urljson` alias types. S25 (secrets stay out of props/attributes):\n * `password` is a literal stored credential and Alchemy persists Attributes unencrypted,\n * the same reasoning `../proxmox/user.ts` gives for shipping no `password` prop at all.\n */\nimport type { AliasAttributes, AliasProps } from './alias.ts';\nimport type * as alias from '@distilled.cloud/opnsense/firewall_alias';\nimport { bool01, csvSet, csvSetOf, numField } from './wire.ts';\n\n/**\n * ★ ATTRIBUTES AND THE DECLARATION RENDERER ARE ONE FUNCTION. Attributes ARE exactly the props a\n * noop declaration needs — see the exported `propsFromLive` alias in alias.ts's barrel and\n * `alias-form.test.ts`'s round-trip test (`matches(attributesOf(uuid, live), attributesOf(uuid,\n * live))` is always true by construction, and so is the general case).\n */\nexport const attributesOf = (uuid: string, live: alias.AliasItem): AliasAttributes => ({\n categories: csvSet(live.categories),\n content: live.content ?? '',\n counters: bool01(live.counters, false),\n description: live.description ?? '',\n enabled: bool01(live.enabled, true),\n interface: live.interface ?? '',\n name: live.name,\n proto: live.proto ?? '',\n type: live.type,\n updatefreq: numField(live.updatefreq, 0),\n uuid,\n});\n\nexport const matches = (attributes: AliasAttributes, props: AliasProps): boolean =>\n attributes.name === props.name &&\n attributes.type === props.type &&\n attributes.enabled === (props.enabled ?? true) &&\n attributes.content === (props.content ?? '') &&\n attributes.interface === (props.interface ?? '') &&\n attributes.proto === (props.proto ?? '') &&\n attributes.counters === (props.counters ?? false) &&\n attributes.updatefreq === (props.updatefreq ?? 0) &&\n attributes.description === (props.description ?? '') &&\n attributes.categories.join(',') === csvSetOf(props.categories).join(',');\n",
|
|
8
|
+
"/**\n * One shape for every OPNsense object this family declares — `read`/`diff` mirroring\n * `../discord/resource.ts`'s marker-less-API posture exactly (S7–S9, H1), and `reconcile`/\n * `delete` replaced end to end with the read-only refusal `policy.ts` states.\n *\n * ★ WHY ONE ENGINE FOR THREE RESOURCES. `alias.ts`, `category.ts` and `group.ts` differ only in\n * which `firewall_*` module they call and how a live item maps to attributes — the read/diff/\n * reconcile/delete flow is identical, so it lives here once, the way `netbox/resource.ts` and\n * `discord/resource.ts` already do for their own families.\n *\n * ★ `Unowned(attrs)` ON A COLD READ, PLAIN ATTRIBUTES ON A WARM ONE (S7, S8). None of Alias,\n * Category or Group carries an ownership marker OPNsense itself understands — no tag, no\n * metadata field a stack could stamp — so this is the same \"marker-less API\" S8 describes, and\n * the same posture `../discord/resource.ts` documents against `Snippet.ts`'s reference read.\n * `adopt(true)` (H5), piped on by each resource file's convenience constructor, is what turns\n * an `Unowned` first read into a silent one-time takeover instead of an `OwnedBySomeoneElse`\n * refusal — exactly what an import script generating `alchemy.run.ts` rows from live reads\n * needs: declare what is live, adopt it, change nothing.\n *\n * ⛔ `reconcile` AND `delete` NEVER CALL AN SDK OPERATION. `reconcile` still OBSERVES first (one\n * GET, via `read`) so the refusal it raises can say whether it would have created or updated —\n * S9's \"observe before you write\" doctrine, kept for the error message even though there is no\n * write to guard. `delete` observes nothing: it refuses unconditionally, because under this\n * policy a delete never succeeds regardless of whether the object is still live. See policy.ts.\n */\nimport { Unowned } from 'alchemy/AdoptPolicy';\nimport { isResolved } from 'alchemy/Diff';\nimport type { Input } from 'alchemy/Input';\nimport type { OpnsenseOpContext } from '@distilled.cloud/opnsense/Protocol';\nimport * as Effect from 'effect/Effect';\nimport type * as HttpClient from 'effect/unstable/http/HttpClient';\nimport { CredentialsFromEnv } from './credentials.ts';\nimport { refuseWrite } from './policy.ts';\n\n/** What every stack must still provide after `opnsenseHandlers` bakes in `CredentialsFromEnv`. */\nexport type OpnsenseRequirements = HttpClient.HttpClient;\n\n/** Every resource in this family identifies its live object by OPNsense's own assigned uuid. */\nexport interface OpnsenseIdentity {\n readonly uuid: string;\n}\n\n/**\n * One OPNsense object's read path. `Live` is whatever the generated `get()` response decodes to\n * for one item (an `AliasItem`, `CategoryItem`, `GroupItem`); `E` is left to each resource file\n * to declare — in practice always `OpnsenseOpError`, since every generated operation shares one\n * error channel (protocol.ts).\n */\nexport type OpnsenseSpec<Props extends OpnsenseIdentity, Live, Attributes extends object, E> = {\n readonly resourceType: string;\n /** Absent when `props.uuid` is not a key in the live model's map — see each resource file. */\n readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, OpnsenseOpContext>;\n /** Also the declaration renderer (S.T. propsFromLive re-export) — see each `*-form.ts`. */\n readonly attributes: (uuid: string, live: Live) => Attributes;\n readonly matches: (attributes: Attributes, props: Props) => boolean;\n};\n\nexport const opnsenseOperations = <\n Props extends OpnsenseIdentity,\n Live,\n Attributes extends object,\n E,\n>(\n spec: OpnsenseSpec<Props, Live, Attributes, E>,\n) => {\n const read = (props: Props) =>\n spec\n .fetchLive(props)\n .pipe(\n Effect.map((live) => (live === undefined ? undefined : spec.attributes(props.uuid, live))),\n );\n\n return {\n read: ({ 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: a prior apply already adopted this exact uuid — ours.\n if (output !== undefined) return attrs;\n // Cold read (fresh store, or state lost): no ownership marker exists on this vendor.\n return 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 return yield* refuseWrite(\n spec.resourceType,\n news.uuid,\n live === undefined ? 'create' : 'update',\n );\n }),\n\n destroy: (olds: Props) => refuseWrite(spec.resourceType, olds.uuid, 'delete'),\n };\n};\n\n/**\n * The provider handlers for a spec'd OPNsense object, wired once. `list` answers empty — every\n * `get()` in this family returns the WHOLE model tree with no per-account/per-scope narrowing,\n * so a `list` that enumerated it would sweep the estate's entire alias/category/group set into\n * one plan; adoption stays explicit, one uuid at a time, the same reasoning netbox's and\n * discord's own `list` give for a shared or marker-less object space (S12).\n *\n * `nuke: { skip: true }` — `delete` refuses unconditionally (policy.ts), so `alchemy unsafe\n * nuke` could never do anything here but fail loudly on every object it found; skipping keeps\n * that refusal out of a nuke census entirely, matching `postgres/database.ts` and\n * `github/repository-ruleset.ts`'s own `nuke: { skip: true }` for the same reason.\n */\nexport const opnsenseHandlers = <\n Props extends OpnsenseIdentity,\n Live,\n Attributes extends object,\n E,\n>(\n spec: OpnsenseSpec<Props, Live, Attributes, E>,\n) => {\n const ops = opnsenseOperations(spec);\n // ⚠️ GENERIC OVER ITS OWN ERROR AND REQUIREMENT CHANNELS, NOT PINNED TO `E`/`OpnsenseOpContext`.\n // `read`/`diff` fail with `E` and need `OpnsenseOpContext`; `reconcile` fails with\n // `E | OpnsenseWriteRefused` (it observes, then refuses) and still needs `OpnsenseOpContext`\n // for that observe; `delete` fails with `OpnsenseWriteRefused` alone and needs NOTHING (it\n // never observes) — three different signatures sharing one credentials-providing wrapper.\n const withCredentials = <A, Err, R>(effect: Effect.Effect<A, Err, R>) =>\n Effect.provide(effect, CredentialsFromEnv);\n return {\n list: () => Effect.succeed([]),\n nuke: { skip: true },\n read: ({ olds, output }: { olds: Props; output: Attributes | undefined }) =>\n withCredentials(ops.read({ olds, output })),\n diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>\n withCredentials(ops.diff(news, output)),\n reconcile: ({ news }: { news: Props }) => withCredentials(ops.reconcile(news)),\n delete: ({ olds }: { olds: Props }) => withCredentials(ops.destroy(olds)),\n };\n};\n",
|
|
9
|
+
"/**\n * OPNsense credentials — a key/secret pair REFERENCE, resolved lazily (S24), never a prop.\n *\n * ⛔ NO KEY OR SECRET IS EVER A PROP OR AN ATTRIBUTE. Alchemy persists Attributes unencrypted\n * (`State/StateEncoding.ts@v2.0.0-beta.79#encodeState`), so nothing a stack file holds may be a\n * credential. `@distilled.cloud/opnsense/Credentials` already ships exactly the shape S24 asks\n * for: `Credentials` is a lazy `Context.Service<Credentials, Effect.Effect<Config>>`, resolved\n * on the calling fiber inside each operation (`OpnsenseProtocol`'s `encode`), never at layer\n * build. This file re-exports it rather than re-implementing it — the same choice\n * `../discord/credentials.ts` made for its own SDK's `CredentialsFromEnv`.\n *\n * ★ `CredentialsFromEnv` READS `OPNSENSE_URL` / `OPNSENSE_API_KEY` / `OPNSENSE_API_SECRET` via\n * Effect's `Config` (the SDK's own `credentials.ts` — no vendor-standard env var names exist\n * for OPNsense's API, unlike Forgejo's `FORGEJO_URL`). The resource never sees the values,\n * only the deploy process's environment does, and the secret is `Redacted` the moment it is\n * read — exactly the \"reference, never a value\" shape S25 asks for.\n *\n * ⚠️ NO OpenBao MINT YET, THE SAME GAP `../discord/credentials.ts` DOCUMENTS. H8's target\n * (`Alchemy.Stack({ secrets })`) is unreleased (PR 1728, `main` only). Until the estate is on a\n * release that carries it, the deploy process's own environment is the credential seam — a\n * human or CI job exports the three `OPNSENSE_*` variables from wherever it mints them. This\n * package never mints, reads or logs the key/secret itself; it only names the variables.\n */\nimport type { Config } from '@distilled.cloud/opnsense/Credentials';\nimport {\n Credentials,\n CredentialsFromEnv,\n credentials,\n} from '@distilled.cloud/opnsense/Credentials';\n\n/** Re-exported so a consumer never has to reach into `@distilled.cloud/opnsense` by hand. */\nexport { Credentials, CredentialsFromEnv, credentials };\nexport type { Config as OpnsenseCredentialsConfig };\n\n/**\n * `OpnsenseCredentials.fromEnv()` — the layer `opnsenseHandlers` (resource.ts) provides by\n * default. A thin, named alias over `CredentialsFromEnv` so a stack's import reads `Opnsense.*`\n * throughout, the way `../discord/credentials.ts`'s `fromEnv()` keeps `Discord.*` names at the\n * front door.\n */\nexport const fromEnv = () => CredentialsFromEnv;\n",
|
|
10
|
+
"/**\n * `OpnsenseWriteRefused` — the one error every resource in this family can raise for a write,\n * and the only thing standing between \"read-only for now\" and an actual change against the edge.\n *\n * ⛔ TIM, 2026-09-24: UniFi AND OPNSENSE ARE READ-ONLY FOR NOW. `Opnsense.Firewall.Alias`,\n * `.Category` and `.Group` (alias.ts, category.ts, group.ts) exist to `read`, `diff` and adopt\n * against the live edge — never to change it. This is the estate's EDGE firewall: a failed\n * probe gets the caller's IP CrowdSec-banned (`edge-crowdsec-bans-lan-ssh-probes` in the shared\n * estate memory), and nothing about that risk is worth a resource that can also reconfigure it.\n * Every `reconcile` and `delete` in this directory calls `refuseWrite` below and NOTHING ELSE —\n * none of them imports an `add`/`set`/`del`/`toggle` operation from `@distilled.cloud/opnsense`\n * at all, and `write-refusal.test.ts` proves it with a fake HTTP client that fails the test on\n * any request that is not a GET.\n *\n * ★ LIFTING THIS IS A KIT CHANGE, NEVER A PROP. There is no `allowWrite` flag on any resource\n * here — a flag is one bad `true` (or one bad default) away from a live edit to the box that\n * fronts the whole estate. Lifting the policy means editing this file, and writing the\n * create/update/delete bodies these resources do not have, a decision Tim makes explicitly by\n * changing code and review, not a runtime toggle a stack can set.\n */\nimport * as Data from 'effect/Data';\nimport * as Effect from 'effect/Effect';\n\nexport type OpnsenseWriteAction = 'create' | 'update' | 'delete';\n\nexport class OpnsenseWriteRefused extends Data.TaggedError('OpnsenseWriteRefused')<{\n readonly resourceType: string;\n readonly id: string;\n readonly action: OpnsenseWriteAction;\n}> {\n override get message(): string {\n return (\n `${this.resourceType} ${this.id}: refusing to ${this.action} — read-only by Tim's rule, ` +\n '2026-09-24; lifting it is a kit change, not a prop.'\n );\n }\n}\n\n/**\n * Every `reconcile`/`delete` handler in this family funnels through this one call — see\n * `resource.ts`'s `opnsenseOperations`. Nothing downstream of this function can reach a write:\n * it never calls an SDK operation, it only builds and fails with the typed refusal above.\n */\nexport const refuseWrite = (\n resourceType: string,\n id: string,\n action: OpnsenseWriteAction,\n): Effect.Effect<never, OpnsenseWriteRefused> =>\n Effect.fail(new OpnsenseWriteRefused({ action, id, resourceType }));\n",
|
|
11
|
+
"/**\n * `Opnsense.Firewall.Category` — a read-only declaration of one `firewall/category` item (the\n * tag OPNsense's UI groups aliases, rules etc. by) on the edge. READ-ONLY BY DESIGN — see\n * policy.ts's header. `reconcile`/`delete` always refuse. Mirrors alias.ts exactly; see that\n * file's header for why `get()` (whole model) is the read source rather than `getCategory` —\n * the same generation gap (no `uuid` in `GetCategoryRequest`'s schema) applies here too.\n */\nimport { adopt } from 'alchemy/AdoptPolicy';\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Category from '@distilled.cloud/opnsense/firewall_category';\nimport type { OpnsenseOpError } from '@distilled.cloud/opnsense/Protocol';\nimport * as Effect from 'effect/Effect';\nimport { attributesOf, matches } from './category-form.ts';\nimport type { OpnsenseRequirements, OpnsenseSpec } from './resource.ts';\nimport { opnsenseHandlers } from './resource.ts';\n\nexport interface CategoryProps {\n readonly uuid: string;\n /** Pattern `/[^,]+/` on the wire — a comma in the name would collide with the multi-select join. */\n readonly name: string;\n /** Whether OPNsense itself created this category (its own subsystems tag things this way); a\n * category a person made is `false`/absent. */\n readonly auto?: boolean;\n /** Six hex digits, no `#`, e.g. `0000FF`. */\n readonly color?: string;\n}\n\nexport interface CategoryAttributes {\n readonly uuid: string;\n readonly name: string;\n readonly auto: boolean;\n readonly color: string;\n}\n\nexport interface OpnsenseFirewallCategory extends Resource<\n 'Opnsense.Firewall.Category',\n CategoryProps,\n CategoryAttributes,\n never,\n OpnsenseRequirements\n> {}\n\nexport const OpnsenseFirewallCategory = Resource<OpnsenseFirewallCategory>(\n 'Opnsense.Firewall.Category',\n {\n defaultRemovalPolicy: 'retain',\n },\n);\n\nexport const isOpnsenseFirewallCategory = (value: unknown): value is OpnsenseFirewallCategory =>\n typeof value === 'object' &&\n value !== null &&\n (value as { Type?: unknown }).Type === 'Opnsense.Firewall.Category';\n\nexport const spec: OpnsenseSpec<\n CategoryProps,\n Category.CategoryItem,\n CategoryAttributes,\n OpnsenseOpError\n> = {\n attributes: attributesOf,\n fetchLive: (props) =>\n Category.get({}).pipe(Effect.map((res) => res.category?.categories?.category?.[props.uuid])),\n matches,\n resourceType: 'Opnsense.Firewall.Category',\n};\n\n/** The pure declaration renderer — see alias.ts's `propsFromLive` for the full contract note. */\nexport const propsFromLive = attributesOf;\n\nexport const handlers = opnsenseHandlers(spec);\n\nexport const OpnsenseFirewallCategoryProvider = () =>\n Provider.effect(\n OpnsenseFirewallCategory,\n Effect.succeed(OpnsenseFirewallCategory.Provider.of(handlers)),\n );\n\n/** `firewallCategory('name', { uuid, name })` — `adopt(true)` piped by default (H5). */\nexport const firewallCategory = (id: string, props: CategoryProps) =>\n OpnsenseFirewallCategory(id, props).pipe(adopt(true));\n",
|
|
12
|
+
"/**\n * `Opnsense.Firewall.Category`'s wire shape: how a live `CategoryItem` becomes attributes, and\n * the comparison that decides whether a declaration already matches it. Mirrors alias-form.ts.\n */\nimport type { CategoryAttributes, CategoryProps } from './category.ts';\nimport type * as category from '@distilled.cloud/opnsense/firewall_category';\nimport { bool01 } from './wire.ts';\n\n/** ★ Attributes and the declaration renderer are one function — see alias-form.ts's header note. */\nexport const attributesOf = (uuid: string, live: category.CategoryItem): CategoryAttributes => ({\n auto: bool01(live.auto, false),\n color: live.color ?? '',\n name: live.name,\n uuid,\n});\n\nexport const matches = (attributes: CategoryAttributes, props: CategoryProps): boolean =>\n attributes.name === props.name &&\n attributes.auto === (props.auto ?? false) &&\n attributes.color === (props.color ?? '');\n",
|
|
13
|
+
"/**\n * `Opnsense.Firewall.Group` — a read-only declaration of one `firewall/group` item (an\n * \"Interface Group\", not a permission group) on the edge. READ-ONLY BY DESIGN — see policy.ts's\n * header. `reconcile`/`delete` always refuse. Mirrors alias.ts exactly; see that file's header\n * for why `get()` (whole model) is the read source rather than `getGroup` — the same generation\n * gap (no `uuid` in `GetGroupRequest`'s schema) applies here too.\n */\nimport { adopt } from 'alchemy/AdoptPolicy';\nimport { Resource } from 'alchemy';\nimport * as Provider from 'alchemy/Provider';\nimport * as Group from '@distilled.cloud/opnsense/firewall_group';\nimport type { OpnsenseOpError } from '@distilled.cloud/opnsense/Protocol';\nimport * as Effect from 'effect/Effect';\nimport { attributesOf, matches } from './group-form.ts';\nimport type { OpnsenseRequirements, OpnsenseSpec } from './resource.ts';\nimport { opnsenseHandlers } from './resource.ts';\n\nexport interface GroupProps {\n readonly uuid: string;\n /** OPNsense's own `GroupNameField` — the interface group's name. */\n readonly ifname: string;\n /** Member interface names (`lan`, `opt1`, …), not their descriptions. */\n readonly members: readonly string[];\n /** OPNsense's own \"no group\" flag — excludes it from certain interface pickers. Default false. */\n readonly nogroup?: boolean;\n /** UI list position, `0`..`9999`. Carries no evaluation-order meaning for this object type. */\n readonly sequence?: number;\n readonly description?: string;\n}\n\nexport interface GroupAttributes {\n readonly uuid: string;\n readonly ifname: string;\n readonly members: readonly string[];\n readonly nogroup: boolean;\n readonly sequence: number;\n readonly description: string;\n}\n\nexport interface OpnsenseFirewallGroup extends Resource<\n 'Opnsense.Firewall.Group',\n GroupProps,\n GroupAttributes,\n never,\n OpnsenseRequirements\n> {}\n\nexport const OpnsenseFirewallGroup = Resource<OpnsenseFirewallGroup>('Opnsense.Firewall.Group', {\n defaultRemovalPolicy: 'retain',\n});\n\nexport const isOpnsenseFirewallGroup = (value: unknown): value is OpnsenseFirewallGroup =>\n typeof value === 'object' &&\n value !== null &&\n (value as { Type?: unknown }).Type === 'Opnsense.Firewall.Group';\n\nexport const spec: OpnsenseSpec<GroupProps, Group.GroupItem, GroupAttributes, OpnsenseOpError> = {\n attributes: attributesOf,\n fetchLive: (props) =>\n Group.get({}).pipe(Effect.map((res) => res.group?.ifgroupentry?.[props.uuid])),\n matches,\n resourceType: 'Opnsense.Firewall.Group',\n};\n\n/** The pure declaration renderer — see alias.ts's `propsFromLive` for the full contract note. */\nexport const propsFromLive = attributesOf;\n\nexport const handlers = opnsenseHandlers(spec);\n\nexport const OpnsenseFirewallGroupProvider = () =>\n Provider.effect(\n OpnsenseFirewallGroup,\n Effect.succeed(OpnsenseFirewallGroup.Provider.of(handlers)),\n );\n\n/** `firewallGroup('name', { uuid, ifname, members })` — `adopt(true)` piped by default (H5). */\nexport const firewallGroup = (id: string, props: GroupProps) =>\n OpnsenseFirewallGroup(id, props).pipe(adopt(true));\n",
|
|
14
|
+
"/**\n * `Opnsense.Firewall.Group`'s wire shape: how a live `GroupItem` (an interface group) becomes\n * attributes, and the comparison that decides whether a declaration already matches it. Mirrors\n * alias-form.ts.\n */\nimport type { GroupAttributes, GroupProps } from './group.ts';\nimport type * as group from '@distilled.cloud/opnsense/firewall_group';\nimport { bool01, csvSet, csvSetOf, numField } from './wire.ts';\n\n/** ★ Attributes and the declaration renderer are one function — see alias-form.ts's header note. */\nexport const attributesOf = (uuid: string, live: group.GroupItem): GroupAttributes => ({\n description: live.descr ?? '',\n ifname: live.ifname,\n members: csvSet(live.members),\n nogroup: bool01(live.nogroup, false),\n sequence: numField(live.sequence, 0),\n uuid,\n});\n\nexport const matches = (attributes: GroupAttributes, props: GroupProps): boolean =>\n attributes.ifname === props.ifname &&\n attributes.nogroup === (props.nogroup ?? false) &&\n attributes.sequence === (props.sequence ?? 0) &&\n attributes.description === (props.description ?? '') &&\n attributes.members.join(',') === csvSetOf(props.members).join(',');\n",
|
|
15
|
+
"/**\n * All three OPNsense firewall providers as one layer for a stack.\n *\n * Layer.mergeAll(opnsenseProviders(), …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, requirements that never contain `unknown`. `FetchHttpClient.layer` is\n * the stack's own responsibility to provide, mirroring `../discord/providers.ts`.\n */\nimport * as Layer from 'effect/Layer';\nimport { OpnsenseFirewallAliasProvider } from './alias.ts';\nimport { OpnsenseFirewallCategoryProvider } from './category.ts';\nimport { OpnsenseFirewallGroupProvider } from './group.ts';\n\nexport const providers = () =>\n Layer.mergeAll(\n OpnsenseFirewallAliasProvider(),\n OpnsenseFirewallCategoryProvider(),\n OpnsenseFirewallGroupProvider(),\n );\n"
|
|
16
|
+
],
|
|
17
|
+
"mappings": "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",
|
|
18
|
+
"debugId": "443068D95E23772E64756E2164756E21",
|
|
19
|
+
"names": []
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
export type OpnsenseWriteAction = 'create' | 'update' | 'delete';
|
|
3
|
+
declare const OpnsenseWriteRefused_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 & {
|
|
4
|
+
readonly _tag: "OpnsenseWriteRefused";
|
|
5
|
+
} & Readonly<A>;
|
|
6
|
+
export declare class OpnsenseWriteRefused extends OpnsenseWriteRefused_base<{
|
|
7
|
+
readonly resourceType: string;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly action: OpnsenseWriteAction;
|
|
10
|
+
}> {
|
|
11
|
+
get message(): string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every `reconcile`/`delete` handler in this family funnels through this one call — see
|
|
15
|
+
* `resource.ts`'s `opnsenseOperations`. Nothing downstream of this function can reach a write:
|
|
16
|
+
* it never calls an SDK operation, it only builds and fails with the typed refusal above.
|
|
17
|
+
*/
|
|
18
|
+
export declare const refuseWrite: (resourceType: string, id: string, action: OpnsenseWriteAction) => Effect.Effect<never, OpnsenseWriteRefused>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/opnsense/policy.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;;;;AAEjE,qBAAa,oBAAqB,SAAQ,0BAAyC;IACjF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;CACtC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,iBACR,MAAM,MAChB,MAAM,UACF,mBAAmB,KAC1B,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,oBAAoB,CACyB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All three OPNsense firewall providers as one layer for a stack.
|
|
3
|
+
*
|
|
4
|
+
* Layer.mergeAll(opnsenseProviders(), …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, requirements that never contain `unknown`. `FetchHttpClient.layer` is
|
|
9
|
+
* the stack's own responsibility to provide, mirroring `../discord/providers.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
export declare const providers: () => Layer.Layer<import("alchemy").Provider<import("./alias.ts").OpnsenseFirewallAlias> | import("alchemy").Provider<import("./category.ts").OpnsenseFirewallCategory> | import("alchemy").Provider<import("./group.ts").OpnsenseFirewallGroup>, 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/opnsense/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAKtC,eAAO,MAAM,SAAS,gTAKnB,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Input } from 'alchemy/Input';
|
|
2
|
+
import type { OpnsenseOpContext } from '@distilled.cloud/opnsense/Protocol';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
5
|
+
/** What every stack must still provide after `opnsenseHandlers` bakes in `CredentialsFromEnv`. */
|
|
6
|
+
export type OpnsenseRequirements = HttpClient.HttpClient;
|
|
7
|
+
/** Every resource in this family identifies its live object by OPNsense's own assigned uuid. */
|
|
8
|
+
export interface OpnsenseIdentity {
|
|
9
|
+
readonly uuid: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One OPNsense object's read path. `Live` is whatever the generated `get()` response decodes to
|
|
13
|
+
* for one item (an `AliasItem`, `CategoryItem`, `GroupItem`); `E` is left to each resource file
|
|
14
|
+
* to declare — in practice always `OpnsenseOpError`, since every generated operation shares one
|
|
15
|
+
* error channel (protocol.ts).
|
|
16
|
+
*/
|
|
17
|
+
export type OpnsenseSpec<Props extends OpnsenseIdentity, Live, Attributes extends object, E> = {
|
|
18
|
+
readonly resourceType: string;
|
|
19
|
+
/** Absent when `props.uuid` is not a key in the live model's map — see each resource file. */
|
|
20
|
+
readonly fetchLive: (props: Props) => Effect.Effect<Live | undefined, E, OpnsenseOpContext>;
|
|
21
|
+
/** Also the declaration renderer (S.T. propsFromLive re-export) — see each `*-form.ts`. */
|
|
22
|
+
readonly attributes: (uuid: string, live: Live) => Attributes;
|
|
23
|
+
readonly matches: (attributes: Attributes, props: Props) => boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare const opnsenseOperations: <Props extends OpnsenseIdentity, Live, Attributes extends object, E>(spec: OpnsenseSpec<Props, Live, Attributes, E>) => {
|
|
26
|
+
read: ({ olds, output }: {
|
|
27
|
+
olds: Props;
|
|
28
|
+
output: Attributes | undefined;
|
|
29
|
+
}) => Effect.Effect<Attributes | undefined, E, OpnsenseOpContext>;
|
|
30
|
+
diff: (news: Input<Props>, output: Attributes | undefined) => Effect.Effect<{
|
|
31
|
+
readonly action: 'update';
|
|
32
|
+
} | {
|
|
33
|
+
readonly action: 'noop';
|
|
34
|
+
} | undefined, E, OpnsenseOpContext>;
|
|
35
|
+
reconcile: (news: Props) => Effect.Effect<never, E | import("./policy.ts").OpnsenseWriteRefused, OpnsenseOpContext>;
|
|
36
|
+
destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").OpnsenseWriteRefused, never>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The provider handlers for a spec'd OPNsense object, wired once. `list` answers empty — every
|
|
40
|
+
* `get()` in this family returns the WHOLE model tree with no per-account/per-scope narrowing,
|
|
41
|
+
* so a `list` that enumerated it would sweep the estate's entire alias/category/group set into
|
|
42
|
+
* one plan; adoption stays explicit, one uuid at a time, the same reasoning netbox's and
|
|
43
|
+
* discord's own `list` give for a shared or marker-less object space (S12).
|
|
44
|
+
*
|
|
45
|
+
* `nuke: { skip: true }` — `delete` refuses unconditionally (policy.ts), so `alchemy unsafe
|
|
46
|
+
* nuke` could never do anything here but fail loudly on every object it found; skipping keeps
|
|
47
|
+
* that refusal out of a nuke census entirely, matching `postgres/database.ts` and
|
|
48
|
+
* `github/repository-ruleset.ts`'s own `nuke: { skip: true }` for the same reason.
|
|
49
|
+
*/
|
|
50
|
+
export declare const opnsenseHandlers: <Props extends OpnsenseIdentity, Live, Attributes extends object, E>(spec: OpnsenseSpec<Props, Live, Attributes, E>) => {
|
|
51
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
52
|
+
nuke: {
|
|
53
|
+
skip: boolean;
|
|
54
|
+
};
|
|
55
|
+
read: ({ olds, output }: {
|
|
56
|
+
olds: Props;
|
|
57
|
+
output: Attributes | undefined;
|
|
58
|
+
}) => Effect.Effect<Attributes | undefined, E, HttpClient.HttpClient>;
|
|
59
|
+
diff: ({ news, output }: {
|
|
60
|
+
news: Input<Props>;
|
|
61
|
+
output: Attributes | undefined;
|
|
62
|
+
}) => Effect.Effect<{
|
|
63
|
+
readonly action: 'update';
|
|
64
|
+
} | {
|
|
65
|
+
readonly action: 'noop';
|
|
66
|
+
} | undefined, E, HttpClient.HttpClient>;
|
|
67
|
+
reconcile: ({ news }: {
|
|
68
|
+
news: Props;
|
|
69
|
+
}) => Effect.Effect<never, E | import("./policy.ts").OpnsenseWriteRefused, HttpClient.HttpClient>;
|
|
70
|
+
delete: ({ olds }: {
|
|
71
|
+
olds: Props;
|
|
72
|
+
}) => Effect.Effect<never, import("./policy.ts").OpnsenseWriteRefused, never>;
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/opnsense/resource.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAInE,kGAAkG;AAClG,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEzD,gGAAgG;AAChG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,gBAAgB,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAC7F,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAC5F,2FAA2F;IAC3F,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;CACrE,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,KAAK,SAAS,gBAAgB,EAC9B,IAAI,EACJ,UAAU,SAAS,MAAM,EACzB,CAAC,QAEK,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAU5C,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAUxE,IAAI,SAAS,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;IAIzB,SAAS,SAAS,KAAK;IAUvB,OAAO,SAAS,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,SAAS,gBAAgB,EAC9B,IAAI,EACJ,UAAU,SAAS,MAAM,EACzB,CAAC,QAEK,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAW5C,IAAI;IACJ,IAAI;QAAI,IAAI;;IACZ,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAExE,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBArDlC,QAAQ;;yBAElC,MAAM;;IAqDzB,SAAS,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACrC,MAAM,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAErC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How OPNsense's MVC model spells a value on the wire, and how to get it back to something
|
|
3
|
+
* comparable — the `../proxmox/values.ts` idea, scoped to this vendor's own spellings.
|
|
4
|
+
*
|
|
5
|
+
* ★ OPNSENSE HAS EXACTLY TWO WIRE SHAPES THIS FAMILY NEEDS. Every `BooleanField` is the string
|
|
6
|
+
* `"1"` or `"0"` (never a JSON boolean — `AliasItem.enabled: string`, `GroupItem.nogroup?:
|
|
7
|
+
* string`, see `firewall_alias.ts`/`firewall_group.ts`). Every multi-select `OptionField`/
|
|
8
|
+
* `ModelRelationField` is a SINGLE comma-joined string, not a JSON array (`AliasItem.categories`,
|
|
9
|
+
* `GroupItem.members` — both carry the doc comment "wire value is a single comma-joined string,
|
|
10
|
+
* not a JSON array" in the generated SDK). Nothing here is specific to one resource.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ COMMA-LIST ORDER ROUND-TRIP IS NOT ASSUMED STABLE. OPNsense builds these from a PHP
|
|
13
|
+
* associative array (`ModelRelationField`/`OptionField::getNodeData()`), and nothing in the
|
|
14
|
+
* model XML promises the join order survives a save-then-read unchanged. Sorting and
|
|
15
|
+
* deduplicating both sides before comparing is what makes `matches` (alias-form.ts,
|
|
16
|
+
* group-form.ts) actually settle instead of reporting drift on a re-ordered list that changed
|
|
17
|
+
* nothing — the same reasoning `../proxmox/user-wire.ts`'s `groupSet` documents for PVE's
|
|
18
|
+
* `groups` field.
|
|
19
|
+
*/
|
|
20
|
+
/** OPNsense's own boolean spelling. Absent takes the caller's stated default — OPNsense's own
|
|
21
|
+
* default for a `BooleanField` is form-specific, so there is no house-wide fallback here. */
|
|
22
|
+
export declare const bool01: (value: string | undefined, fallback: boolean) => boolean;
|
|
23
|
+
/** The write direction, for symmetry — unused today (this family never writes), kept so a form
|
|
24
|
+
* helper never has to guess the spelling if lifting the read-only policy adds one later. */
|
|
25
|
+
export declare const toBool01: (value: boolean) => string;
|
|
26
|
+
/**
|
|
27
|
+
* A comma-joined multi-select column, normalised to a sorted, deduplicated set — see the module
|
|
28
|
+
* header's ⚠️ for why normalising (not just splitting) is what makes a diff settle.
|
|
29
|
+
*/
|
|
30
|
+
export declare const csvSet: (value: string | undefined) => readonly string[];
|
|
31
|
+
/** A declared array, normalised the SAME way `csvSet` normalises the wire, so `matches` compares
|
|
32
|
+
* two sets built by one function rather than trusting the caller's array to already be sorted. */
|
|
33
|
+
export declare const csvSetOf: (value: readonly string[] | undefined) => readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* OPNsense's numeric-as-string fields (`"60"`, `"0"`), tolerant of an already-numeric input the
|
|
36
|
+
* same way `../proxmox/user-wire.ts`'s `int` is tolerant of PVE's mixed number/string releases.
|
|
37
|
+
*/
|
|
38
|
+
export declare const numField: (value: string | undefined, fallback: number) => number;
|
|
39
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/opnsense/wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;6FAC6F;AAC7F,eAAO,MAAM,MAAM,UAAW,MAAM,GAAG,SAAS,YAAY,OAAO,KAAG,OACtB,CAAC;AAEjD;4FAC4F;AAC5F,eAAO,MAAM,QAAQ,UAAW,OAAO,KAAG,MAA6B,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,MAAM,UAAW,MAAM,GAAG,SAAS,KAAG,SAAS,MAAM,EAOjE,CAAC;AAEF;kGACkG;AAClG,eAAO,MAAM,QAAQ,UAAW,SAAS,MAAM,EAAE,GAAG,SAAS,KAAG,SAAS,MAAM,EACpB,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,QAAQ,UAAW,MAAM,GAAG,SAAS,YAAY,MAAM,KAAG,MAItE,CAAC"}
|