@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.
Files changed (184) hide show
  1. package/README.md +39 -3
  2. package/dist/grafana/dashboard-model.d.ts +77 -0
  3. package/dist/grafana/dashboard-model.d.ts.map +1 -0
  4. package/dist/grafana/dashboard.d.ts +98 -0
  5. package/dist/grafana/dashboard.d.ts.map +1 -0
  6. package/dist/grafana/folder.d.ts +100 -0
  7. package/dist/grafana/folder.d.ts.map +1 -0
  8. package/dist/grafana/index.d.ts +9 -8
  9. package/dist/grafana/index.d.ts.map +1 -1
  10. package/dist/grafana/index.js +158 -6
  11. package/dist/grafana/index.js.map +8 -4
  12. package/dist/grafana/providers.d.ts +1 -1
  13. package/dist/grafana/providers.d.ts.map +1 -1
  14. package/dist/grafana/provisioned.d.ts +11 -0
  15. package/dist/grafana/provisioned.d.ts.map +1 -0
  16. package/dist/linux/container-form.d.ts +129 -0
  17. package/dist/linux/container-form.d.ts.map +1 -0
  18. package/dist/linux/container-generator.d.ts +74 -0
  19. package/dist/linux/container-generator.d.ts.map +1 -0
  20. package/dist/linux/container-handlers.d.ts +15 -0
  21. package/dist/linux/container-handlers.d.ts.map +1 -0
  22. package/dist/linux/container-lifecycle.d.ts +37 -0
  23. package/dist/linux/container-lifecycle.d.ts.map +1 -0
  24. package/dist/linux/container-preflight.d.ts +34 -0
  25. package/dist/linux/container-preflight.d.ts.map +1 -0
  26. package/dist/linux/container-secrets.d.ts +37 -0
  27. package/dist/linux/container-secrets.d.ts.map +1 -0
  28. package/dist/linux/container-settle.d.ts +34 -0
  29. package/dist/linux/container-settle.d.ts.map +1 -0
  30. package/dist/linux/container.d.ts +30 -0
  31. package/dist/linux/container.d.ts.map +1 -0
  32. package/dist/linux/fake-quadlet.d.ts +25 -0
  33. package/dist/linux/fake-quadlet.d.ts.map +1 -0
  34. package/dist/linux/index.d.ts +6 -3
  35. package/dist/linux/index.d.ts.map +1 -1
  36. package/dist/linux/index.js +641 -268
  37. package/dist/linux/index.js.map +15 -7
  38. package/dist/linux/providers.d.ts +1 -1
  39. package/dist/linux/providers.d.ts.map +1 -1
  40. package/dist/linux/systemctl.d.ts +7 -1
  41. package/dist/linux/systemctl.d.ts.map +1 -1
  42. package/dist/opnsense/alias-form.d.ts +29 -0
  43. package/dist/opnsense/alias-form.d.ts.map +1 -0
  44. package/dist/opnsense/alias.d.ts +81 -0
  45. package/dist/opnsense/alias.d.ts.map +1 -0
  46. package/dist/opnsense/category-form.d.ts +10 -0
  47. package/dist/opnsense/category-form.d.ts.map +1 -0
  48. package/dist/opnsense/category.d.ts +58 -0
  49. package/dist/opnsense/category.d.ts.map +1 -0
  50. package/dist/opnsense/credentials.d.ts +36 -0
  51. package/dist/opnsense/credentials.d.ts.map +1 -0
  52. package/dist/opnsense/fake-opnsense.d.ts +51 -0
  53. package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
  54. package/dist/opnsense/group-form.d.ts +11 -0
  55. package/dist/opnsense/group-form.d.ts.map +1 -0
  56. package/dist/opnsense/group.d.ts +62 -0
  57. package/dist/opnsense/group.d.ts.map +1 -0
  58. package/dist/opnsense/index.d.ts +16 -0
  59. package/dist/opnsense/index.d.ts.map +1 -0
  60. package/dist/opnsense/index.js +211 -0
  61. package/dist/opnsense/index.js.map +20 -0
  62. package/dist/opnsense/policy.d.ts +20 -0
  63. package/dist/opnsense/policy.d.ts.map +1 -0
  64. package/dist/opnsense/providers.d.ts +13 -0
  65. package/dist/opnsense/providers.d.ts.map +1 -0
  66. package/dist/opnsense/resource.d.ts +74 -0
  67. package/dist/opnsense/resource.d.ts.map +1 -0
  68. package/dist/opnsense/wire.d.ts +39 -0
  69. package/dist/opnsense/wire.d.ts.map +1 -0
  70. package/dist/proxmox/index.js +170 -80
  71. package/dist/proxmox/index.js.map +9 -7
  72. package/dist/proxmox/storage-form.d.ts +54 -0
  73. package/dist/proxmox/storage-form.d.ts.map +1 -0
  74. package/dist/proxmox/storage-wire.d.ts +56 -0
  75. package/dist/proxmox/storage-wire.d.ts.map +1 -0
  76. package/dist/proxmox/storage.d.ts +55 -60
  77. package/dist/proxmox/storage.d.ts.map +1 -1
  78. package/dist/proxmox/zfs-pool-form.d.ts +124 -0
  79. package/dist/proxmox/zfs-pool-form.d.ts.map +1 -0
  80. package/dist/proxmox/zfs-pool-wire.d.ts +78 -0
  81. package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -0
  82. package/dist/proxmox/zfs-pool.d.ts +27 -52
  83. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  84. package/dist/unifi/fake-unifi.d.ts +48 -0
  85. package/dist/unifi/fake-unifi.d.ts.map +1 -0
  86. package/dist/unifi/firewall-zone-form.d.ts +40 -0
  87. package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-zone.d.ts +50 -0
  89. package/dist/unifi/firewall-zone.d.ts.map +1 -0
  90. package/dist/unifi/index.d.ts +13 -0
  91. package/dist/unifi/index.d.ts.map +1 -0
  92. package/dist/unifi/index.js +190 -0
  93. package/dist/unifi/index.js.map +16 -0
  94. package/dist/unifi/network-form.d.ts +67 -0
  95. package/dist/unifi/network-form.d.ts.map +1 -0
  96. package/dist/unifi/network.d.ts +67 -0
  97. package/dist/unifi/network.d.ts.map +1 -0
  98. package/dist/unifi/policy.d.ts +20 -0
  99. package/dist/unifi/policy.d.ts.map +1 -0
  100. package/dist/unifi/providers.d.ts +13 -0
  101. package/dist/unifi/providers.d.ts.map +1 -0
  102. package/dist/unifi/resource.d.ts +64 -0
  103. package/dist/unifi/resource.d.ts.map +1 -0
  104. package/docs/grafana-folder-dashboard.md +145 -0
  105. package/docs/grafana.md +11 -10
  106. package/docs/opnsense.md +146 -0
  107. package/docs/provider-standard.md +1 -1
  108. package/docs/quadlet-container.md +132 -0
  109. package/docs/unifi.md +93 -0
  110. package/docs/upstream-conformance.md +9 -4
  111. package/package.json +12 -2
  112. package/src/grafana/dashboard-model.test.ts +111 -0
  113. package/src/grafana/dashboard-model.ts +111 -0
  114. package/src/grafana/dashboard.test.ts +232 -0
  115. package/src/grafana/dashboard.ts +161 -0
  116. package/src/grafana/folder.test.ts +212 -0
  117. package/src/grafana/folder.ts +161 -0
  118. package/src/grafana/index.ts +21 -8
  119. package/src/grafana/providers.ts +7 -1
  120. package/src/grafana/provisioned.ts +49 -0
  121. package/src/linux/container-fixture.test.ts +92 -0
  122. package/src/linux/container-form.ts +243 -0
  123. package/src/linux/container-generator.test.ts +195 -0
  124. package/src/linux/container-generator.ts +113 -0
  125. package/src/linux/container-handlers.ts +73 -0
  126. package/src/linux/container-lifecycle.test.ts +209 -0
  127. package/src/linux/container-lifecycle.ts +217 -0
  128. package/src/linux/container-preflight.ts +97 -0
  129. package/src/linux/container-secrets.test.ts +171 -0
  130. package/src/linux/container-secrets.ts +112 -0
  131. package/src/linux/container-settle.ts +97 -0
  132. package/src/linux/container.ts +60 -0
  133. package/src/linux/fake-quadlet.ts +127 -0
  134. package/src/linux/index.ts +11 -3
  135. package/src/linux/providers.ts +2 -0
  136. package/src/linux/systemctl.ts +9 -0
  137. package/src/opnsense/alias-form.test.ts +89 -0
  138. package/src/opnsense/alias-form.ts +53 -0
  139. package/src/opnsense/alias.test.ts +203 -0
  140. package/src/opnsense/alias.ts +117 -0
  141. package/src/opnsense/category-form.test.ts +54 -0
  142. package/src/opnsense/category-form.ts +20 -0
  143. package/src/opnsense/category.test.ts +138 -0
  144. package/src/opnsense/category.ts +82 -0
  145. package/src/opnsense/credentials.ts +41 -0
  146. package/src/opnsense/fake-opnsense.ts +79 -0
  147. package/src/opnsense/group-form.test.ts +75 -0
  148. package/src/opnsense/group-form.ts +25 -0
  149. package/src/opnsense/group.test.ts +141 -0
  150. package/src/opnsense/group.ts +78 -0
  151. package/src/opnsense/index.ts +45 -0
  152. package/src/opnsense/policy.ts +49 -0
  153. package/src/opnsense/providers.ts +21 -0
  154. package/src/opnsense/resource.ts +146 -0
  155. package/src/opnsense/wire.ts +56 -0
  156. package/src/opnsense/write-refusal.test.ts +148 -0
  157. package/src/proxmox/constraints-host-forms.test.ts +1 -1
  158. package/src/proxmox/storage-form.ts +144 -0
  159. package/src/proxmox/storage-read-failure.test.ts +214 -0
  160. package/src/proxmox/storage-wire.ts +124 -0
  161. package/src/proxmox/storage.ts +165 -175
  162. package/src/proxmox/zfs-pool-adopt.test.ts +26 -0
  163. package/src/proxmox/zfs-pool-form.ts +236 -0
  164. package/src/proxmox/zfs-pool-read-failure.test.ts +118 -0
  165. package/src/proxmox/zfs-pool-wire.ts +132 -0
  166. package/src/proxmox/zfs-pool.ts +106 -76
  167. package/src/unifi/fake-unifi.ts +60 -0
  168. package/src/unifi/firewall-zone-form.ts +65 -0
  169. package/src/unifi/firewall-zone.test.ts +88 -0
  170. package/src/unifi/firewall-zone.ts +66 -0
  171. package/src/unifi/index.ts +26 -0
  172. package/src/unifi/network-form.ts +207 -0
  173. package/src/unifi/network.test.ts +209 -0
  174. package/src/unifi/network.ts +83 -0
  175. package/src/unifi/policy.ts +37 -0
  176. package/src/unifi/providers.ts +15 -0
  177. package/src/unifi/resource.test.ts +133 -0
  178. package/src/unifi/resource.ts +131 -0
  179. package/dist/proxmox/zfs-pool-spec.d.ts +0 -27
  180. package/dist/proxmox/zfs-pool-spec.d.ts.map +0 -1
  181. package/dist/proxmox/zfs-pool-write.d.ts +0 -128
  182. package/dist/proxmox/zfs-pool-write.d.ts.map +0 -1
  183. package/src/proxmox/zfs-pool-spec.ts +0 -76
  184. package/src/proxmox/zfs-pool-write.ts +0 -196
@@ -1 +1 @@
1
- {"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAA8B,MAAM,eAAe,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,2BAA2B,CAAC,WAAW,CAAC,CAAC,CAAC;AAEjF,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,2BAA2B,CAAC,aAAa,CAAC,CAAC,CAAC;AAErF;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,mGAAmG;AACnG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAqBH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
1
+ {"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,mGAAmG;AACnG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAEH,eAAO,MAAM,sBAAsB,oIAyEhC,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A minimal in-memory UniFi Network API for this family's tests — an in-memory `fetch` handed to
3
+ * Effect's real `FetchHttpClient` through its `Fetch` reference, mirroring `../netbox/fake-netbox.ts`
4
+ * (itself mirroring `../forgejo/fake-forgejo.ts`). Tests exercise distilled's REAL path assembly,
5
+ * JSON encode/decode and status→typed-error matching — nothing about that matching is
6
+ * re-implemented here, only the wire responses a scenario needs.
7
+ *
8
+ * ⛔ TEST-ONLY, AND NO NETWORK. No provider imports this file. The base URL is RFC 2606
9
+ * `unifi.example.com` and the key is a placeholder — never a real console address: a cloud
10
+ * connector's base URL embeds the account's Console ID, which must never appear in this repo
11
+ * (`docs/unifi.md`).
12
+ */
13
+ import { type Credentials } from '@distilled.cloud/unifi-network/Credentials';
14
+ import * as Layer from 'effect/Layer';
15
+ export declare const FAKE_BASE = "https://unifi.example.com/proxy/network/integration";
16
+ export declare const FAKE_KEY = "placeholder-unifi-key";
17
+ export type Seen = {
18
+ readonly method: string;
19
+ readonly path: string;
20
+ };
21
+ /** No documented failure envelope (see `protocol.ts`) — plain text is all a failure needs. */
22
+ export declare const fakeFailure: (status: number, detail: string) => Response;
23
+ /**
24
+ * `route` decides every response; this wrapper only records what was sent and adapts to
25
+ * `globalThis.fetch`'s signature.
26
+ *
27
+ * ★ THE WRITE-REFUSAL PROOF USES `seen` DIRECTLY, NOT A THROWING ROUTE. `resource.test.ts`'s
28
+ * "no SDK write operation is reachable" test asserts `seen.every(s => s.method === 'GET')`
29
+ * after driving every handler — a request this family's engine was never supposed to send
30
+ * shows up in `seen` as a fact, rather than depending on an exception surviving Effect's own
31
+ * error handling on its way back out of a fake `fetch`.
32
+ */
33
+ export declare const fakeUnifi: (route: (method: string, url: URL) => Response) => {
34
+ fetch: typeof fetch;
35
+ seen: Seen[];
36
+ };
37
+ /**
38
+ * distilled Credentials + the real FetchHttpClient over the fake — `UnifiNetworkOpContext`, what
39
+ * every resource file's exported `spec.fetchLive` needs.
40
+ *
41
+ * ⚠️ NOT PAIRED WITH `handlers`. `handlers` (each resource file's `unifiHandlers(spec)` export)
42
+ * bakes in `CredentialsFromEnv`, which resolves `UNIFI_NETWORK_API_KEY`/`_API_BASE_URL` through
43
+ * Effect's `Config` — whose default provider snapshots `process.env` once and never re-reads it
44
+ * (the same seam `netbox/fake-netbox.ts` documents). Tests call `spec.fetchLive`/`spec.attributes`
45
+ * directly instead, with this explicit layer.
46
+ */
47
+ export declare const fakeUnifiLayer: (fetchFn: typeof globalThis.fetch, creds?: Layer.Layer<Credentials>) => Layer.Layer<Credentials | import("effect/unstable/http/HttpClient").HttpClient, never, never>;
48
+ //# sourceMappingURL=fake-unifi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-unifi.d.ts","sourceRoot":"","sources":["../../src/unifi/fake-unifi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,4CAA4C,CAAC;AAC3F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,eAAO,MAAM,SAAS,wDAAwD,CAAC;AAC/E,eAAO,MAAM,QAAQ,0BAA0B,CAAC;AAEhD,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,8FAA8F;AAC9F,eAAO,MAAM,WAAW,WAAY,MAAM,UAAU,MAAM,aAAqC,CAAC;AAEhG;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,UAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ;;;CAUtE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,YAChB,OAAO,UAAU,CAAC,KAAK,UACzB,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,kGAC+D,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `Unifi.FirewallZone`'s wire shape — the declarable subset of `FirewallZone`, and how a live
3
+ * read becomes both plan attributes and a rendered declaration.
4
+ *
5
+ * ⚠️ ONLY `name` AND `networkIds` ARE DECLARABLE. `CreateFirewallZoneRequest`
6
+ * (`firewall.ts`) accepts exactly those two beyond `siteId`; `metadata` (origin/source) and
7
+ * `id` are server-derived, so — same split as `network-form.ts`'s `default`/`metadata` —
8
+ * they are attributes-only, reported but never compared or declared.
9
+ *
10
+ * ⛔ `networkIds` ORDER IS NOT PART OF IDENTITY. The vendor document gives no indication the
11
+ * list is ordered (it is a set of attached networks, not a sequence like the ordering
12
+ * endpoints' rule lists), so `matches` compares it as a set — sorted before comparing —
13
+ * the same reasoning `proxmox/user-wire.ts`'s `groupSet` gives for PVE's `groups`.
14
+ */
15
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
16
+ export interface FirewallZoneProps {
17
+ siteId: string;
18
+ firewallZoneId: string;
19
+ /** Name of a firewall zone. */
20
+ name: string;
21
+ /** Network IDs attached to this zone — compared as a set, see the header. */
22
+ networkIds: string[];
23
+ }
24
+ export interface FirewallZoneAttributes {
25
+ siteId: string;
26
+ firewallZoneId: string;
27
+ name: string;
28
+ networkIds: string[];
29
+ /** `origin`/`source` (e.g. `SDWAN`) — server-derived, never declared. */
30
+ metadataOrigin: string;
31
+ }
32
+ export declare const attributesOf: (live: firewall.FirewallZone, props: FirewallZoneProps) => FirewallZoneAttributes;
33
+ export declare const matches: (attributes: FirewallZoneAttributes, props: FirewallZoneProps) => boolean;
34
+ /**
35
+ * The declaration renderer (task spec: "given one live object, return the props a declaration
36
+ * needs so its plan is noop"). `matches(attributesOf(live, props), declareFirewallZone(live,
37
+ * siteId))` is `true` by construction (`firewall-zone.test.ts` proves it).
38
+ */
39
+ export declare const declareFirewallZone: (live: firewall.FirewallZone, siteId: string) => FirewallZoneProps;
40
+ //# sourceMappingURL=firewall-zone-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firewall-zone-form.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-zone-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAID,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,YAAY,SACpB,iBAAiB,KACvB,sBAMD,CAAC;AAEH,eAAO,MAAM,OAAO,eAAgB,sBAAsB,SAAS,iBAAiB,KAAG,OAEZ,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,QAAQ,CAAC,YAAY,UACnB,MAAM,KACb,iBAKD,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `Unifi.FirewallZone` — one firewall zone on a UniFi Network site, READ-ONLY (`policy.ts`).
3
+ *
4
+ * ★ WHY FIREWALL ZONE IS IN THE FIRST IMPORT SET AND `Unifi.FirewallPolicy` IS NOT.
5
+ * `getFirewallZone`/`getFirewallZones` (`firewall.ts`) is a list+get pair over genuine,
6
+ * simply-shaped configuration: `{id, metadata, name, networkIds}`, four fields, no nested
7
+ * discriminated filter. `FirewallPolicy` (same file) is also list+get and also configuration,
8
+ * but its `source`/`destination`/`ipProtocolScope` fields carry several of `docs/unifi-api-notes.md`'s
9
+ * "converter-flattened discriminator variants" each, and `firewall/policies/ordering` replaces
10
+ * the whole ordered list — modelling those correctly is a bigger, separate PR (`network.ts`'s
11
+ * header note names it explicitly) rather than something to rush alongside this one.
12
+ */
13
+ import { Resource } from 'alchemy';
14
+ import * as Provider from 'alchemy/Provider';
15
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
16
+ import * as Effect from 'effect/Effect';
17
+ import { type FirewallZoneAttributes, type FirewallZoneProps } from './firewall-zone-form.ts';
18
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
19
+ export type { FirewallZoneAttributes, FirewallZoneProps } from './firewall-zone-form.ts';
20
+ export { declareFirewallZone } from './firewall-zone-form.ts';
21
+ export interface UnifiFirewallZone extends Resource<'Unifi.FirewallZone', FirewallZoneProps, FirewallZoneAttributes, never, UnifiRequirements> {
22
+ }
23
+ export declare const UnifiFirewallZone: import("alchemy").ResourceClass<UnifiFirewallZone>;
24
+ /** `firewallZone('lan', props)` — `adopt(true)` piped on by default (H5); see `network.ts`. */
25
+ export declare const firewallZone: (id: string, props: FirewallZoneProps) => Effect.Effect<UnifiFirewallZone, never, import("effect/unstable/http/HttpClient").HttpClient>;
26
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
27
+ export declare const spec: UnifiSpec<FirewallZoneProps, firewall.FirewallZone, FirewallZoneAttributes, firewall.GetFirewallZoneError>;
28
+ export declare const handlers: {
29
+ list: () => Effect.Effect<never[], never, never>;
30
+ read: (args: {
31
+ olds: FirewallZoneProps;
32
+ output: FirewallZoneAttributes | undefined;
33
+ }) => Effect.Effect<FirewallZoneAttributes | undefined, firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
34
+ diff: (args: {
35
+ news: import("alchemy").Input<FirewallZoneProps>;
36
+ output: FirewallZoneAttributes | undefined;
37
+ }) => Effect.Effect<{
38
+ readonly action: 'update';
39
+ } | {
40
+ readonly action: 'noop';
41
+ } | undefined, firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
42
+ reconcile: (args: {
43
+ news: FirewallZoneProps;
44
+ }) => Effect.Effect<FirewallZoneAttributes, import("./policy.ts").UnifiWriteRefused | firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
45
+ delete: (args: {
46
+ olds: FirewallZoneProps;
47
+ }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/unstable/http/HttpClient").HttpClient>;
48
+ };
49
+ export declare const UnifiFirewallZoneProvider: () => import("effect/Layer").Layer<Provider.Provider<UnifiFirewallZone>, never, import("effect/unstable/http/HttpClient").HttpClient>;
50
+ //# sourceMappingURL=firewall-zone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firewall-zone.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-zone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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,sBAAsB,EAC3B,KAAK,iBAAiB,EAGvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAEH,+FAA+F;AAC/F,eAAO,MAAM,YAAY,OAAQ,MAAM,SAAS,iBAAiB,kGACjB,CAAC;AAEjD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,iBAAiB,EACjB,QAAQ,CAAC,YAAY,EACrB,sBAAsB,EACtB,QAAQ,CAAC,oBAAoB,CAU9B,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,yBAAyB,uIACuD,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * UniFi Network providers for Alchemy — READ-ONLY, by Tim's rule (2026-09-24; `policy.ts`).
3
+ *
4
+ * ⛔ THIS BARREL IS THE PUBLIC API, SMALLER THAN THE DIRECTORY. `resource.ts`'s generic engine
5
+ * and each `*-form.ts`'s wire helpers are internals a provider needs but a consumer should
6
+ * not depend on — mirrors `../netbox/index.ts`.
7
+ */
8
+ export { UNIFI_READ_ONLY_POLICY, UnifiWriteRefused, type UnifiWriteAction } from './policy.ts';
9
+ export type { UnifiRequirements, UnifiSpec } from './resource.ts';
10
+ export { UnifiNetwork, UnifiNetworkProvider, declareNetwork, network, type NetworkAttributes, type NetworkProps, } from './network.ts';
11
+ export { UnifiFirewallZone, UnifiFirewallZoneProvider, declareFirewallZone, firewallZone, type FirewallZoneAttributes, type FirewallZoneProps, } from './firewall-zone.ts';
12
+ export { providers } from './providers.ts';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/unifi/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/F,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,OAAO,EACP,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
@@ -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": "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",
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"}