@homeflare/alchemy 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -7
- package/dist/grafana/index.d.ts +6 -5
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/linux/container-form.d.ts +129 -0
- package/dist/linux/container-form.d.ts.map +1 -0
- package/dist/linux/container-generator.d.ts +74 -0
- package/dist/linux/container-generator.d.ts.map +1 -0
- package/dist/linux/container-handlers.d.ts +15 -0
- package/dist/linux/container-handlers.d.ts.map +1 -0
- package/dist/linux/container-lifecycle.d.ts +37 -0
- package/dist/linux/container-lifecycle.d.ts.map +1 -0
- package/dist/linux/container-preflight.d.ts +34 -0
- package/dist/linux/container-preflight.d.ts.map +1 -0
- package/dist/linux/container-secrets.d.ts +37 -0
- package/dist/linux/container-secrets.d.ts.map +1 -0
- package/dist/linux/container-settle.d.ts +34 -0
- package/dist/linux/container-settle.d.ts.map +1 -0
- package/dist/linux/container.d.ts +30 -0
- package/dist/linux/container.d.ts.map +1 -0
- package/dist/linux/fake-quadlet.d.ts +25 -0
- package/dist/linux/fake-quadlet.d.ts.map +1 -0
- package/dist/linux/index.d.ts +6 -3
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +641 -268
- package/dist/linux/index.js.map +15 -7
- package/dist/linux/providers.d.ts +1 -1
- package/dist/linux/providers.d.ts.map +1 -1
- package/dist/linux/systemctl.d.ts +7 -1
- package/dist/linux/systemctl.d.ts.map +1 -1
- package/dist/opnsense/alias-form.d.ts +29 -0
- package/dist/opnsense/alias-form.d.ts.map +1 -0
- package/dist/opnsense/alias.d.ts +81 -0
- package/dist/opnsense/alias.d.ts.map +1 -0
- package/dist/opnsense/category-form.d.ts +10 -0
- package/dist/opnsense/category-form.d.ts.map +1 -0
- package/dist/opnsense/category.d.ts +58 -0
- package/dist/opnsense/category.d.ts.map +1 -0
- package/dist/opnsense/credentials.d.ts +36 -0
- package/dist/opnsense/credentials.d.ts.map +1 -0
- package/dist/opnsense/fake-opnsense.d.ts +51 -0
- package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
- package/dist/opnsense/group-form.d.ts +11 -0
- package/dist/opnsense/group-form.d.ts.map +1 -0
- package/dist/opnsense/group.d.ts +62 -0
- package/dist/opnsense/group.d.ts.map +1 -0
- package/dist/opnsense/index.d.ts +16 -0
- package/dist/opnsense/index.d.ts.map +1 -0
- package/dist/opnsense/index.js +211 -0
- package/dist/opnsense/index.js.map +20 -0
- package/dist/opnsense/policy.d.ts +20 -0
- package/dist/opnsense/policy.d.ts.map +1 -0
- package/dist/opnsense/providers.d.ts +13 -0
- package/dist/opnsense/providers.d.ts.map +1 -0
- package/dist/opnsense/resource.d.ts +74 -0
- package/dist/opnsense/resource.d.ts.map +1 -0
- package/dist/opnsense/wire.d.ts +39 -0
- package/dist/opnsense/wire.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +44 -5
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/group.d.ts.map +1 -1
- package/dist/proxmox/index.js +326 -264
- package/dist/proxmox/index.js.map +13 -11
- package/dist/proxmox/role.d.ts.map +1 -1
- package/dist/proxmox/storage-form.d.ts +54 -0
- package/dist/proxmox/storage-form.d.ts.map +1 -0
- package/dist/proxmox/storage-wire.d.ts +56 -0
- package/dist/proxmox/storage-wire.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +55 -60
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +34 -5
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts +6 -6
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/unifi/fake-unifi.d.ts +48 -0
- package/dist/unifi/fake-unifi.d.ts.map +1 -0
- package/dist/unifi/firewall-zone-form.d.ts +40 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
- package/dist/unifi/firewall-zone.d.ts +50 -0
- package/dist/unifi/firewall-zone.d.ts.map +1 -0
- package/dist/unifi/index.d.ts +13 -0
- package/dist/unifi/index.d.ts.map +1 -0
- package/dist/unifi/index.js +190 -0
- package/dist/unifi/index.js.map +16 -0
- package/dist/unifi/network-form.d.ts +67 -0
- package/dist/unifi/network-form.d.ts.map +1 -0
- package/dist/unifi/network.d.ts +67 -0
- package/dist/unifi/network.d.ts.map +1 -0
- package/dist/unifi/policy.d.ts +20 -0
- package/dist/unifi/policy.d.ts.map +1 -0
- package/dist/unifi/providers.d.ts +13 -0
- package/dist/unifi/providers.d.ts.map +1 -0
- package/dist/unifi/resource.d.ts +64 -0
- package/dist/unifi/resource.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +14 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/grafana.md +47 -17
- package/docs/opnsense.md +146 -0
- package/docs/provider-standard.md +1 -1
- package/docs/quadlet-container.md +132 -0
- package/docs/unifi.md +93 -0
- package/docs/upstream-conformance.md +14 -4
- package/package.json +13 -3
- package/src/grafana/index.ts +6 -5
- package/src/linux/container-fixture.test.ts +92 -0
- package/src/linux/container-form.ts +243 -0
- package/src/linux/container-generator.test.ts +195 -0
- package/src/linux/container-generator.ts +113 -0
- package/src/linux/container-handlers.ts +73 -0
- package/src/linux/container-lifecycle.test.ts +209 -0
- package/src/linux/container-lifecycle.ts +217 -0
- package/src/linux/container-preflight.ts +97 -0
- package/src/linux/container-secrets.test.ts +171 -0
- package/src/linux/container-secrets.ts +112 -0
- package/src/linux/container-settle.ts +97 -0
- package/src/linux/container.ts +60 -0
- package/src/linux/fake-quadlet.ts +127 -0
- package/src/linux/index.ts +11 -3
- package/src/linux/providers.ts +2 -0
- package/src/linux/systemctl.ts +9 -0
- package/src/opnsense/alias-form.test.ts +89 -0
- package/src/opnsense/alias-form.ts +53 -0
- package/src/opnsense/alias.test.ts +203 -0
- package/src/opnsense/alias.ts +117 -0
- package/src/opnsense/category-form.test.ts +54 -0
- package/src/opnsense/category-form.ts +20 -0
- package/src/opnsense/category.test.ts +138 -0
- package/src/opnsense/category.ts +82 -0
- package/src/opnsense/credentials.ts +41 -0
- package/src/opnsense/fake-opnsense.ts +79 -0
- package/src/opnsense/group-form.test.ts +75 -0
- package/src/opnsense/group-form.ts +25 -0
- package/src/opnsense/group.test.ts +141 -0
- package/src/opnsense/group.ts +78 -0
- package/src/opnsense/index.ts +45 -0
- package/src/opnsense/policy.ts +49 -0
- package/src/opnsense/providers.ts +21 -0
- package/src/opnsense/resource.ts +146 -0
- package/src/opnsense/wire.ts +56 -0
- package/src/opnsense/write-refusal.test.ts +148 -0
- package/src/proxmox/acl.ts +10 -4
- package/src/proxmox/api-token.ts +20 -12
- package/src/proxmox/group-wire.ts +62 -8
- package/src/proxmox/group.ts +23 -35
- package/src/proxmox/read-failure-propagation.test.ts +183 -0
- package/src/proxmox/role.ts +9 -2
- package/src/proxmox/storage-form.ts +144 -0
- package/src/proxmox/storage-read-failure.test.ts +214 -0
- package/src/proxmox/storage-wire.ts +124 -0
- package/src/proxmox/storage.ts +165 -175
- package/src/proxmox/user-wire.ts +44 -8
- package/src/proxmox/user.ts +29 -27
- package/src/unifi/fake-unifi.ts +60 -0
- package/src/unifi/firewall-zone-form.ts +65 -0
- package/src/unifi/firewall-zone.test.ts +88 -0
- package/src/unifi/firewall-zone.ts +66 -0
- package/src/unifi/index.ts +26 -0
- package/src/unifi/network-form.ts +207 -0
- package/src/unifi/network.test.ts +209 -0
- package/src/unifi/network.ts +83 -0
- package/src/unifi/policy.ts +37 -0
- package/src/unifi/providers.ts +15 -0
- package/src/unifi/resource.test.ts +133 -0
- package/src/unifi/resource.ts +131 -0
- package/src/verify/fake-engine.ts +23 -0
package/docs/grafana.md
CHANGED
|
@@ -8,32 +8,61 @@ publicly at `tesla.homeflare.dev/dash/`.
|
|
|
8
8
|
|
|
9
9
|
## The SDK gap — why only `Datasource` ships
|
|
10
10
|
|
|
11
|
+
✅ **SDK-level gap fixed 2026-09-24** (`@distilled.cloud/grafana`, aliased onto
|
|
12
|
+
`@homeflare/distilled-grafana` — 0.1.0 pre-release, 0.2.0 once its `minor` changeset publishes it;
|
|
13
|
+
see [docs/distilled-interim.md](./distilled-interim.md)).
|
|
14
|
+
The gap below is otherwise unchanged and left as evidence of what was measured and why; only its
|
|
15
|
+
status line moved. **Still true today:** only `Grafana.Datasource` ships as a house _resource_ —
|
|
16
|
+
`Grafana.Folder`, `Grafana.Dashboard`, `Grafana.AlertRule` and friends are follow-up work on top of
|
|
17
|
+
the now-available SDK operations, not part of this fix.
|
|
18
|
+
|
|
11
19
|
Measured 2026-09-24 against the published `@distilled.cloud/grafana@1.0.0-rc.12` tarball (its
|
|
12
|
-
`lib/services/grafana.d.ts`, 4,938 lines): the package
|
|
13
|
-
operations for folders, dashboards, alert rules or contact points**. It
|
|
20
|
+
`lib/services/grafana.d.ts`, 4,938 lines): the package had **no create/read/update/delete
|
|
21
|
+
operations for folders, dashboards, alert rules or contact points**. It covered datasources in
|
|
14
22
|
full (`addDataSource`, `getDataSourceByUID`, `updateDataSourceByUID`, `deleteDataSourceByUID`,
|
|
15
23
|
`getDataSources`), plus org/user/team/role/report/annotation/library-element/public-dashboard/
|
|
16
24
|
snapshot management — but:
|
|
17
25
|
|
|
18
|
-
- **Folders:** only `updateFolderPermissions`
|
|
19
|
-
`deleteFolder` — Grafana's `/api/folders` and `/api/folders/{uid}` routes
|
|
20
|
-
- **Dashboards:** only snapshot and public-dashboard routes
|
|
26
|
+
- **Folders:** only `updateFolderPermissions` existed. No `createFolder`, `getFolder` or
|
|
27
|
+
`deleteFolder` — Grafana's `/api/folders` and `/api/folders/{uid}` routes were entirely absent.
|
|
28
|
+
- **Dashboards:** only snapshot and public-dashboard routes existed (`createDashboardSnapshot`,
|
|
21
29
|
`getPublicDashboard`, …). No `POST /api/dashboards/db`, no `GET /api/dashboards/uid/{uid}`, no
|
|
22
|
-
`DELETE /api/dashboards/uid/{uid}` — the plain dashboard CRUD route family
|
|
30
|
+
`DELETE /api/dashboards/uid/{uid}` — the plain dashboard CRUD route family was absent.
|
|
23
31
|
- **Alert rules:** only `routeGetAlertRuleExport`/`routeGetAlertRuleGroupExport`/
|
|
24
|
-
`routeGetAlertRulesExport`
|
|
32
|
+
`routeGetAlertRulesExport` existed — export (read-only) endpoints under `/api/v1/provisioning/`.
|
|
25
33
|
No create/update/delete for `/api/v1/provisioning/alert-rules{,/{uid}}`.
|
|
26
|
-
- **Contact points:** only `routeGetContactpointsExport`
|
|
34
|
+
- **Contact points:** only `routeGetContactpointsExport` existed, same story — no
|
|
27
35
|
`/api/v1/provisioning/contact-points{,/{uid}}` CRUD.
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
**Root cause, measured (not the spec lacking the paths):** Grafana's own OpenAPI document has
|
|
38
|
+
every one of the routes above — the converter dropped them. `skipDeprecated: true` (the distilled
|
|
39
|
+
converter's v0 default) skips any operation the spec marks `deprecated: true`, and Grafana marks
|
|
40
|
+
its entire classic folder/dashboard/alerting-provisioning WRITE surface deprecated, each
|
|
41
|
+
operation's `description` pointing at a `/apis/<group>.grafana.app/v1/...` Kubernetes-apiserver
|
|
42
|
+
route that is a separate API server with its own OpenAPI document — absent from this spec
|
|
43
|
+
entirely, and not what this family's `Grafana.Datasource` (or any planned `Grafana.Folder`/
|
|
44
|
+
`Grafana.Dashboard`/etc.) targets. The fix patches `deprecated` back to `false` on exactly the 32
|
|
45
|
+
operations this gap named (leaving the other 32 deprecated operations — playlists, org/user
|
|
46
|
+
preferences, datasources-by-name, licensing, reports, dashboard stars/tags/versions — excluded, as
|
|
47
|
+
before): see the distilled clone's `packages/grafana/scripts/convert.ts` header and
|
|
48
|
+
`patches/*.patch.json` for the exact list and reasoning, and
|
|
49
|
+
`packages/distilled-grafana/README.md` in this kit for the copied result. `Grafana.Datasource`'s
|
|
50
|
+
own operations (`addDataSource`/`getDataSourceByUID`/`updateDataSourceByUID`/
|
|
51
|
+
`deleteDataSourceByUID`) were never deprecated and are untouched — same props, same attributes,
|
|
52
|
+
same generated code, confirmed by diffing the distilled clone's regenerated output against its
|
|
53
|
+
prior commit operation-by-operation.
|
|
54
|
+
|
|
55
|
+
This was recorded in [`docs/upstream-conformance.md`](./upstream-conformance.md) rather than worked
|
|
30
56
|
around. Per S23, the fix is a distilled PR (regenerate against a spec that includes these routes,
|
|
31
|
-
or patch them in per S22)
|
|
32
|
-
pieces alone
|
|
33
|
-
client, exactly what S23 exists to prevent.
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
57
|
+
or patch them in per S22) — not a hand-rolled `HttpClient` client in this package for the missing
|
|
58
|
+
pieces alone, which would put half a family through the SDK and half through a second, parallel
|
|
59
|
+
client, exactly what S23 exists to prevent. It shipped as a kit interim package
|
|
60
|
+
(`@homeflare/distilled-grafana`), the same S22 route `@homeflare/distilled-netbox` established,
|
|
61
|
+
because `alchemy-run/distilled` is upstream and this house never pushes to it (decision 42) — see
|
|
62
|
+
docs/distilled-interim.md.
|
|
63
|
+
|
|
64
|
+
`teslamate-grafana` itself is consistent with this gap having been real, not merely unexploited:
|
|
65
|
+
its one datasource is API-manageable and already exists; its dashboards ship baked into the
|
|
37
66
|
`teslamate/grafana` image and are provisioned by the container, not by any API call; it has no
|
|
38
67
|
folders, alert rules or contact points configured at all (measured over ssh, read-only, 2026-09-24).
|
|
39
68
|
|
|
@@ -96,7 +125,8 @@ export class TeslaMateDatasource extends GrafanaDatasource('teslamate-datasource
|
|
|
96
125
|
|
|
97
126
|
## Not covered
|
|
98
127
|
|
|
99
|
-
- **Folders, dashboards, alert rules, contact points
|
|
100
|
-
|
|
128
|
+
- **Folders, dashboards, alert rules, contact points, notification policies, mute timings.** The
|
|
129
|
+
SDK-level blocker is fixed (above); no house `Resource` calls these operations yet — follow-up
|
|
130
|
+
work, not blocked on anything.
|
|
101
131
|
- **`read` never answers `Unowned`** — the same open gap forgejo's and netbox's own families
|
|
102
132
|
still carry (see upstream-conformance.md); a maintainer decision on the ownership-check shape.
|
package/docs/opnsense.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# OPNsense — `@homeflare/alchemy/opnsense`
|
|
2
|
+
|
|
3
|
+
Declares `Opnsense.Firewall.Alias`, `.Category` and `.Group` against
|
|
4
|
+
`@distilled.cloud/opnsense` (aliased onto `@homeflare/distilled-opnsense@0.2.0` —
|
|
5
|
+
[`distilled-interim.md`](./distilled-interim.md)). Built 2026-09-24. **READ-ONLY BY
|
|
6
|
+
DESIGN** — see "The policy" below before reading anything else here. This is the
|
|
7
|
+
estate's **edge firewall**: nothing in this family's code, or in building it,
|
|
8
|
+
contacted the live box, and nothing about it may reconfigure one. (The vendor
|
|
9
|
+
version pin below WAS confirmed against the live box once, read-only, during
|
|
10
|
+
review — see "Vendor version" below; that probe used neither this family's
|
|
11
|
+
code nor this SDK's generated client.)
|
|
12
|
+
|
|
13
|
+
## Vendor version — measured 2026-09-24
|
|
14
|
+
|
|
15
|
+
`@homeflare/distilled-opnsense`'s own README carries its generation pin:
|
|
16
|
+
`opnsense/core`/`opnsense/plugins` tag `26.7.2`. That pin used to be an
|
|
17
|
+
**inference** from a captured package list, never measured live (SSH to the
|
|
18
|
+
edge is forbidden by house policy — a failed probe gets the caller's IP
|
|
19
|
+
CrowdSec-banned). This PR's review measured it instead (Probe 14, 2026-09-24),
|
|
20
|
+
live and read-only, over the HTTPS API only — no SSH:
|
|
21
|
+
|
|
22
|
+
- `firmware-info` returned `200` with `product_version=26.7.2_2`.
|
|
23
|
+
- `alias-search` returned `200` (keys: `current`, `rowCount`, `rows`, `total`).
|
|
24
|
+
|
|
25
|
+
So the pin is now **measured**, not inferred: the live edge runs `26.7.2_2`,
|
|
26
|
+
one patch ahead of the `26.7.2` tag the model/controller shape was generated
|
|
27
|
+
from — the same series the "patch releases rarely change shape" reasoning
|
|
28
|
+
already relied on, now confirmed rather than assumed. `alias-search`'s 200
|
|
29
|
+
also confirms the live box's Firewall Alias API answers in the shape this SDK
|
|
30
|
+
expects, even though this family reads through `get()`, not `search*` (below).
|
|
31
|
+
|
|
32
|
+
## Resource set, and why these three
|
|
33
|
+
|
|
34
|
+
The SDK covers 8 controllers / 83 operations (Firewall Alias/Category/Group/Filter,
|
|
35
|
+
Routing Gateways, and the `os-frr` plugin's Quagga/BGP). This first import pass
|
|
36
|
+
picks the three whose SDK module exports BOTH a `get()` (the whole model, keyed by
|
|
37
|
+
uuid) and a `search<Item>` operation (a paginated list, OPNsense's own POST-shaped
|
|
38
|
+
read) — genuine "list and get" configuration objects, not runtime state:
|
|
39
|
+
|
|
40
|
+
- **Built**: `firewall_alias`, `firewall_category`, `firewall_group` (interface
|
|
41
|
+
groups, not permission groups).
|
|
42
|
+
- **Skipped, and why**:
|
|
43
|
+
- `firewall_filter` (rules) and `routing_settings` (gateways) export `get()` but
|
|
44
|
+
no `search*` — no distinct list operation, so they miss this pass's own bar.
|
|
45
|
+
Rules are also the most consequential, highest-churn object in the model;
|
|
46
|
+
deferred to its own pass rather than folded in here.
|
|
47
|
+
- `quagga_general` is a global settings singleton, not a collection of items —
|
|
48
|
+
no uuid, nothing to adopt "by id".
|
|
49
|
+
- `quagga_bgp` mixes several item types (aspath, communitylist, neighbor,
|
|
50
|
+
peergroup, prefixlist, redistribution, routemap) with no `search*` for any of
|
|
51
|
+
them — deferred as a unit rather than picking one sub-type arbitrarily.
|
|
52
|
+
- `quagga_service` is pure runtime control (start/stop/restart/status) — the
|
|
53
|
+
task's own "not runtime state" exclusion, textbook case.
|
|
54
|
+
|
|
55
|
+
## Why `get()`, never `getItem`/`searchItem`, for the three that shipped
|
|
56
|
+
|
|
57
|
+
`get()` returns the WHOLE model tree in one GET —
|
|
58
|
+
`{ alias: { aliases: { alias: { <uuid>: AliasItem, ... } } } }` for Alias, and the
|
|
59
|
+
equivalent nesting for Category/Group (see each resource file's header for the
|
|
60
|
+
exact path). Finding one item by uuid is then an object-key lookup, never an
|
|
61
|
+
ambiguous request.
|
|
62
|
+
|
|
63
|
+
That sidesteps two real gaps MEASURED in this early SDK, not a stylistic choice:
|
|
64
|
+
|
|
65
|
+
1. **`firewall_alias` has no per-item get at all** — only `get` (whole model) and
|
|
66
|
+
`searchAlias` (paginated) exist.
|
|
67
|
+
2. **`firewall_category`/`firewall_group` DO export `getCategory`/`getGroup`**,
|
|
68
|
+
but their generated request schema carries no `uuid` path parameter at all
|
|
69
|
+
(`GetCategoryRequest`/`GetGroupRequest` are both `S.Struct({})` against a `uri`
|
|
70
|
+
with no `{uuid}` placeholder) — a generation gap in this SDK, not a design
|
|
71
|
+
choice. Filed as an SDK gap below rather than worked around with a raw path.
|
|
72
|
+
|
|
73
|
+
`searchAlias`/`searchCategory`/`searchGroup` were also passed over for `list()`
|
|
74
|
+
building (they stay unused) because their generated request schema exposes no
|
|
75
|
+
typed limit/offset/searchPhrase at all — a large object set could silently
|
|
76
|
+
page-truncate through a naive use of them, where `get()` has no pagination to
|
|
77
|
+
fall short of.
|
|
78
|
+
|
|
79
|
+
## The policy: reconcile and delete always refuse
|
|
80
|
+
|
|
81
|
+
Tim, 2026-09-24: UniFi and OPNsense are read-only for now. `policy.ts`'s
|
|
82
|
+
`OpnsenseWriteRefused` is the ONLY error `reconcile`/`delete` can raise on any of
|
|
83
|
+
the three resources — neither handler imports an `add`/`set`/`del`/`toggle`
|
|
84
|
+
operation from the SDK. `reconcile` still observes with one `get()` first, so the
|
|
85
|
+
refusal can say whether it would have created or updated; `delete` observes
|
|
86
|
+
nothing and refuses unconditionally. `write-refusal.test.ts` proves it with a fake
|
|
87
|
+
HTTP client that fails the test itself on any non-GET request, exercised through
|
|
88
|
+
the exact `handlers` object each resource's `Provider` wraps — not a lower-level
|
|
89
|
+
seam a real provider bypasses.
|
|
90
|
+
|
|
91
|
+
Lifting this is a kit change (edit `policy.ts`, write the create/update/delete
|
|
92
|
+
bodies these resources do not have), never a prop. There is no `allowWrite` flag.
|
|
93
|
+
|
|
94
|
+
## Adopt is `adopt(true)`, not silent (S7, S8, H1)
|
|
95
|
+
|
|
96
|
+
None of Alias, Category or Group carries an ownership marker OPNsense itself
|
|
97
|
+
understands. Following upstream's `Snippet.ts` reference for a marker-less API
|
|
98
|
+
(the same posture `../discord` documents, not NetBox's silent-adopt gap): a cold
|
|
99
|
+
read (no persisted `output`) that finds a live object at the declared uuid
|
|
100
|
+
returns `Unowned(attrs)`, so declaring it fails `OwnedBySomeoneElse` **unless**
|
|
101
|
+
`adopt(true)` is set. Each resource's convenience constructor
|
|
102
|
+
(`firewallAlias`/`firewallCategory`/`firewallGroup`) pipes `adopt(true)` on by
|
|
103
|
+
default (H5).
|
|
104
|
+
|
|
105
|
+
## The declaration renderer
|
|
106
|
+
|
|
107
|
+
`aliasPropsFromLive`/`categoryPropsFromLive`/`groupPropsFromLive` (the barrel's
|
|
108
|
+
names for each resource file's `propsFromLive`, itself the same function as
|
|
109
|
+
`attributesOf` — see each `*-form.ts`'s header) are pure: given the uuid `get()`'s
|
|
110
|
+
map key names and the item decoded at that key, they return exactly the props a
|
|
111
|
+
declaration needs so `matches` reports noop against it. A future import script
|
|
112
|
+
iterates `Object.entries(map)` from one `get()` call and calls the matching
|
|
113
|
+
renderer once per row to generate an `alchemy.run.ts` row — no such script ships
|
|
114
|
+
in this change.
|
|
115
|
+
|
|
116
|
+
## Credentials
|
|
117
|
+
|
|
118
|
+
`OPNSENSE_URL` / `OPNSENSE_API_KEY` / `OPNSENSE_API_SECRET`, read **at call time**
|
|
119
|
+
via `@distilled.cloud/opnsense/Credentials`'s own `CredentialsFromEnv` (Basic
|
|
120
|
+
auth, the vendor's own scheme), re-exported from `credentials.ts` rather than
|
|
121
|
+
re-implemented. No OpenBao mint yet (H8's target is unreleased); a deploy process
|
|
122
|
+
exports the three variables itself today. This package never reads, logs or
|
|
123
|
+
mints them.
|
|
124
|
+
|
|
125
|
+
The only key that exists today is a **ROOT API key** — not scoped to Firewall
|
|
126
|
+
Alias/Category/Group, or to read-only. That is one more reason "The policy"
|
|
127
|
+
above refuses every write unconditionally rather than trusting a props-level
|
|
128
|
+
flag: any write path this key could reach would carry root's full blast
|
|
129
|
+
radius on the estate's edge firewall, not just these three object types.
|
|
130
|
+
|
|
131
|
+
## SDK gaps
|
|
132
|
+
|
|
133
|
+
- 🔴 **`getCategory`/`getGroup`'s missing `uuid` path parameter** (above) — a
|
|
134
|
+
distilled generation gap, not exercised by this family, worth fixing upstream
|
|
135
|
+
in the distilled clone before a later pass tries to use either operation.
|
|
136
|
+
- **No vendor constraint table**, unlike NetBox/Paperless — this pass did not
|
|
137
|
+
walk `vendor-schema` for OPNsense's field limits (e.g. `CategoryItem.color`'s
|
|
138
|
+
`/^[0-9a-fA-F]{6}$/` pattern); a malformed declaration would only be caught if
|
|
139
|
+
writes were ever lifted, not at plan time.
|
|
140
|
+
- **`password`/`username`/`authtype`/`expire`** (the URL-fetch alias credential
|
|
141
|
+
fields) and **`path_expression`** (`urljson`-only) are left off `AliasProps`
|
|
142
|
+
entirely — S25 for the first four, narrow scope for the fifth. See
|
|
143
|
+
`alias-form.ts`'s header.
|
|
144
|
+
- **No live lifecycle test** (H12): tests run against `fake-opnsense.ts`, a
|
|
145
|
+
loopback fake, not the real edge. This task had no credentials and was told
|
|
146
|
+
not to look for any.
|
|
@@ -124,7 +124,7 @@ The files currently on the wrong side of the line are listed in
|
|
|
124
124
|
| `caddy` | Caddy 2.11.4 | source comments only ⚠️ |
|
|
125
125
|
| `forgejo` | Forgejo 16.0.3 | source comments only ⚠️ |
|
|
126
126
|
| `talos` | Talos v1.13 docs (never measured live) | source comments only ⚠️ |
|
|
127
|
-
| `launchd` / `linux` | macOS 27 / systemd 257
|
|
127
|
+
| `launchd` / `linux` | macOS 27 / systemd 257 / Podman 5.4.2 | source comments only ⚠️ |
|
|
128
128
|
|
|
129
129
|
⚠️ These rows record a version only in prose, so no test catches a drift. `schemas/manifest.json`
|
|
130
130
|
also repeats the PVE, PBS and UniFi entries, which means there are two manifests for one
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# `Podman.Container` — `@homeflare/alchemy/linux`
|
|
2
|
+
|
|
3
|
+
Status: active
|
|
4
|
+
Verified: 2026-09-24
|
|
5
|
+
|
|
6
|
+
One Podman [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
|
|
7
|
+
`.container` file, declared, plus the systemd unit Podman's own generator
|
|
8
|
+
turns it into — the container half of [linux-host.md](./linux-host.md)'s
|
|
9
|
+
`Systemd.Unit` family, over the same `HostRunner` seam.
|
|
10
|
+
|
|
11
|
+
★ **Why not `Docker.Container`.** Upstream Alchemy's resource was measured and
|
|
12
|
+
rejected for three reasons: no host-network prop (every container CT100 runs
|
|
13
|
+
uses `--network host`), `environment` puts `Redacted` secrets in Alchemy state,
|
|
14
|
+
and systemd already supervises the containers this stack declares. Quadlet
|
|
15
|
+
sidesteps all three: `Network=host` is a real value, `Environment=` is refused
|
|
16
|
+
if it looks like a secret (below), and the GENERATED unit is what systemd
|
|
17
|
+
supervises — this resource only ever declares the `.container` file that
|
|
18
|
+
produces it.
|
|
19
|
+
|
|
20
|
+
## Keys checked against the doc, not invented
|
|
21
|
+
|
|
22
|
+
Every `[Container]` key below is checked against `podman-systemd.unit(5)`,
|
|
23
|
+
**Podman 5.4** (`containers/podman` tag `v5.4.0`,
|
|
24
|
+
`docs/source/markdown/podman-systemd.unit.5.md`, read 2026-09-24) — CT100 runs
|
|
25
|
+
Podman 5.4.2 (`podman --version`, measured over `ssh ct100`, read-only,
|
|
26
|
+
2026-09-24). `[Container]` is typed, unlike `Systemd.Unit`'s freeform
|
|
27
|
+
`sections`, because Quadlet's own schema — unlike bare `systemd.unit(5)` — IS
|
|
28
|
+
one vendor's documented, versioned surface. Adding a key later means checking
|
|
29
|
+
it against the same doc and adding a field; there is no raw `[Container]`
|
|
30
|
+
escape hatch, on purpose (see secrets, below).
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
PodmanContainer('grafana-primary', {
|
|
34
|
+
name: 'grafana-primary',
|
|
35
|
+
container: {
|
|
36
|
+
containerName: 'grafana-primary',
|
|
37
|
+
environmentFile: ['/etc/grafana-primary/env'],
|
|
38
|
+
image: 'docker.io/grafana/grafana:12.2.1',
|
|
39
|
+
network: 'host',
|
|
40
|
+
volume: ['grafana-primary-data:/var/lib/grafana'],
|
|
41
|
+
},
|
|
42
|
+
install: { wantedBy: ['multi-user.target'] },
|
|
43
|
+
service: { restart: 'always', restartSec: '5s' },
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Supported `[Container]` keys: `Image`, `ContainerName`, `Network` (supports
|
|
48
|
+
`'host'`), `Volume` (repeatable), `EnvironmentFile` (repeatable), `Environment`
|
|
49
|
+
(repeatable `KEY=VALUE`, never a secret), `PublishPort` (repeatable, refused
|
|
50
|
+
together with `network: 'host'`), `Exec`, `User`, `AutoUpdate`
|
|
51
|
+
(`'registry' | 'local'`), `PodmanArgs` (repeatable). `[Unit]`
|
|
52
|
+
(`Description`/`Documentation` typed, everything else verbatim lines — same
|
|
53
|
+
approach `Systemd.Unit` uses for the whole file, because these three sections
|
|
54
|
+
stay systemd's own directive set even inside a `.container` file), `[Service]`
|
|
55
|
+
(`Restart`/`RestartSec` typed, everything else verbatim), and `[Install]`
|
|
56
|
+
(`WantedBy`/`RequiredBy`/`Alias` — the only three keys Quadlet honours for a
|
|
57
|
+
`.container` file; NOT `Also`, which plain `systemd.unit(5)` allows).
|
|
58
|
+
|
|
59
|
+
## Secrets: `EnvironmentFile=`, never `Environment=`
|
|
60
|
+
|
|
61
|
+
⛔ **A secret belongs in a host path an out-of-band renderer maintains**
|
|
62
|
+
(openbao-agent), named in `container.environmentFile` — never in
|
|
63
|
+
`container.environment`, which is rendered into a 0644 file and stored
|
|
64
|
+
unencrypted in Alchemy state, the same two reasons `Systemd.Unit`'s `content`
|
|
65
|
+
is never secret.
|
|
66
|
+
|
|
67
|
+
⚠️ **The refusal is best-effort, not a scanner** — `container-secrets.ts`. It
|
|
68
|
+
refuses an `Environment=` key whose NAME says what it holds (`SECRET`,
|
|
69
|
+
`TOKEN`, `PASSWORD`, `API_KEY`, …) and a handful of value shapes real vendor
|
|
70
|
+
tokens take (`Bearer `, `ghp_`, `AKIA`, a JWT's `eyJ`, …). It cannot prove a
|
|
71
|
+
value is safe, only refuse the obvious mistake — `container-secrets.test.ts`
|
|
72
|
+
proves the refusal fires before any write, and that the renderer itself has no
|
|
73
|
+
independent filter (validation is the only backstop, which is why every write
|
|
74
|
+
path calls it first).
|
|
75
|
+
|
|
76
|
+
## Lifecycle — where it differs from `Systemd.Unit`
|
|
77
|
+
|
|
78
|
+
Order is **write, `daemon-reload`, VERIFY the generator, then start or restart
|
|
79
|
+
the generated `<name>.service` only for a real change** — see
|
|
80
|
+
`container-lifecycle.ts` and `container-generator.ts` for the full reasoning,
|
|
81
|
+
each doc-cited. In brief:
|
|
82
|
+
|
|
83
|
+
- ⛔ **The generated unit is never `systemctl enable`d.** MEASURED on CT100,
|
|
84
|
+
2026-09-24: `caddy.container` declares `[Install] WantedBy=multi-user.target`
|
|
85
|
+
and nothing ever ran `systemctl enable caddy`; `systemctl is-enabled
|
|
86
|
+
caddy.service` prints `generated`, and
|
|
87
|
+
`/run/systemd/generator/multi-user.target.wants/caddy.service` is a symlink
|
|
88
|
+
the GENERATOR put there — doc-confirmed (`podman-systemd.unit(5)`): a
|
|
89
|
+
generated unit is transient, and `systemctl enable` "does not work" on one;
|
|
90
|
+
Quadlet applies `[Install]` itself, at every `daemon-reload`. So there is no
|
|
91
|
+
`enabled` prop: declaring `install.wantedBy` **is** the enablement.
|
|
92
|
+
- ⛔ **A generator failure is a typed `QuadletGeneratorError`, never "absent".**
|
|
93
|
+
`systemd.generator(7)` (tag `v257`): `daemon-reload` deletes ALL generator
|
|
94
|
+
output and reruns every generator from the files on disk NOW. So a
|
|
95
|
+
`.container` file Quadlet refuses has no generated unit at all — even one
|
|
96
|
+
that generated fine a moment ago. An UPDATE that fails verification
|
|
97
|
+
therefore restores the LAST-KNOWN-GOOD file and reloads again, rather than
|
|
98
|
+
leaving the bad file for the next deploy to retry (`Systemd.Unit`'s own
|
|
99
|
+
choice, unit-lifecycle.ts): here "the next deploy" might be a reboot, and by
|
|
100
|
+
then the container this resource already promised running has no unit at
|
|
101
|
+
all. A CREATE that fails removes the file it wrote, same as `Systemd.Unit`.
|
|
102
|
+
- ⚠️ **The one open question.** Whether an ALREADY-ACTIVE unit's `LoadState`
|
|
103
|
+
stays `loaded` through a failed regeneration (systemd need not
|
|
104
|
+
garbage-collect a referenced unit) was not measured — doing so needs writing
|
|
105
|
+
a broken file to a live host, which read-only access forbids. If it does,
|
|
106
|
+
`LoadState`/`SourcePath` alone would not catch a failed update to a
|
|
107
|
+
container that was already running; the restore-last-known-good step does
|
|
108
|
+
not depend on catching it, since it always leaves the host at a file known
|
|
109
|
+
to generate.
|
|
110
|
+
- `SourcePath` (added to `systemctl.ts`'s `SHOW_PROPERTIES`, additive for
|
|
111
|
+
`Systemd.Unit`) ties the generated unit back to the `.container` file —
|
|
112
|
+
MEASURED on CT100: `SourcePath=/etc/containers/systemd/caddy.container`. A
|
|
113
|
+
mismatch means a same-named file earlier in Quadlet's search path is
|
|
114
|
+
generating the unit instead of ours — a takeover, not a success.
|
|
115
|
+
- Directory allowlist is Quadlet's OWN rootful search path (doc, "Podman Unit
|
|
116
|
+
Search Path"): `/run/containers/systemd`, `/etc/containers/systemd`,
|
|
117
|
+
`/usr/share/containers/systemd` — CT100's `caddy.container` lives in the
|
|
118
|
+
middle one.
|
|
119
|
+
- Adopt, restart-on-change, delete (stop, remove the file, reload — no
|
|
120
|
+
`disable`, since it was never enabled through systemctl) and the rename
|
|
121
|
+
(delete-first replace) contract are otherwise identical to `Systemd.Unit`'s
|
|
122
|
+
— see [linux-host.md](./linux-host.md).
|
|
123
|
+
|
|
124
|
+
## The CT100 fixture
|
|
125
|
+
|
|
126
|
+
`container-fixture.test.ts` renders the same directives as CT100's live
|
|
127
|
+
`caddy.container` (read read-only over `ssh ct100`, 2026-09-24) and checks
|
|
128
|
+
every line matches. Not byte-for-byte: CT100's file interleaves long prose
|
|
129
|
+
comments between directives, and a typed `[Container]` has nowhere to attach
|
|
130
|
+
"this comment goes between these two keys" — the same limit `Systemd.Unit`'s
|
|
131
|
+
`renderUnit` has for the identical reason. The test proves directive parity;
|
|
132
|
+
the file explains why byte parity is not the right bar for a typed resource.
|
package/docs/unifi.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# UniFi Network — `@homeflare/alchemy/unifi`
|
|
2
|
+
|
|
3
|
+
⛔ **READ-ONLY, BY TIM'S RULE (2026-09-24).** `Unifi.Network` and `Unifi.FirewallZone` read and
|
|
4
|
+
adopt; neither has a create, an update body or a working delete. `reconcile`/`delete` both fail
|
|
5
|
+
with a typed `UnifiWriteRefused` naming the policy rather than calling any SDK write operation.
|
|
6
|
+
Lifting the rule is a kit change — a PR that adds a write path, reviewed as one — not a flag a
|
|
7
|
+
stack can pass. See `packages/alchemy/src/unifi/policy.ts`.
|
|
8
|
+
|
|
9
|
+
## What's declared, and why these two first
|
|
10
|
+
|
|
11
|
+
`Unifi.Network` (`getNetworkDetails`/`getNetworksOverviewPage`) and `Unifi.FirewallZone`
|
|
12
|
+
(`getFirewallZone`/`getFirewallZones`) — both list+get pairs over genuine, stable configuration,
|
|
13
|
+
not runtime state (a connected client, a device statistic, a hotspot voucher, a switch stack).
|
|
14
|
+
`Site` was the SDK README's other first suggestion; it has no `getSite`, only the list operation,
|
|
15
|
+
so it fails this house's own "both a list and a get" bar for a first import. `FirewallPolicy` is
|
|
16
|
+
also a list+get config object, but its `source`/`destination`/`ipProtocolScope` fields carry
|
|
17
|
+
several of the vendor's converter-flattened discriminator variants each, and its ordering
|
|
18
|
+
endpoint replaces the whole rule list — modelling that correctly is a bigger, separate PR.
|
|
19
|
+
|
|
20
|
+
## Credentials
|
|
21
|
+
|
|
22
|
+
Two environment variables, read **at call time**, never props — the SDK's own
|
|
23
|
+
`CredentialsFromEnv` (`@distilled.cloud/unifi-network/Credentials`):
|
|
24
|
+
|
|
25
|
+
| variable | holds |
|
|
26
|
+
| ---------------------------- | --------------------------------------------------- |
|
|
27
|
+
| `UNIFI_NETWORK_API_KEY` | an Integrations → API Key from the console's own UI |
|
|
28
|
+
| `UNIFI_NETWORK_API_BASE_URL` | the console's integration API root — see below |
|
|
29
|
+
|
|
30
|
+
⛔ **Neither has a default**, and neither is ever logged, committed or persisted in state.
|
|
31
|
+
|
|
32
|
+
⛔ **No console identifier, endpoint host or other live value ever appears in code, tests,
|
|
33
|
+
fixtures, commits or a PR.** The base URL shape depends on how the console is reached: a cloud
|
|
34
|
+
connector's is `https://api.ui.com/v1/connector/consoles/<consoleId>/proxy/network/integration`
|
|
35
|
+
(`<consoleId>` is an account identifier); a local console is reached directly,
|
|
36
|
+
`<endpoint>/proxy/network/integration`, where `<endpoint>` is a hostname behind a TLS certificate
|
|
37
|
+
the caller trusts. Treat any UniFi base URL as sensitive; this package's tests use only the RFC
|
|
38
|
+
2606 placeholder `https://unifi.example.com`.
|
|
39
|
+
|
|
40
|
+
The estate's own credential lives in OpenBao at `kv/infra/unifi/api` — fields `api_key`,
|
|
41
|
+
`connector_id`, `console`, `endpoint`, `site`, `transport` (`transport=local`: this console is
|
|
42
|
+
reached directly, not through the cloud connector). A separate `kv/infra/unifi/site-manager`
|
|
43
|
+
token is the _cloud_ Site Manager API credential, a different door (below) this family never
|
|
44
|
+
reads.
|
|
45
|
+
|
|
46
|
+
✅ **Auth was measured live, read-only, on 2026-09-24.** The OpenAPI document declares no
|
|
47
|
+
`securitySchemes` at all — `X-API-KEY` comes from Ubiquiti's own Integration API guide, not the
|
|
48
|
+
machine-readable spec (`@distilled.cloud/unifi-network/src/credentials.ts`'s own header) — but a
|
|
49
|
+
hand-run, read-only probe confirmed it works against the estate's local console: `X-API-KEY`
|
|
50
|
+
returns HTTP 200 on both `GET /v1/info` (key: `applicationVersion`) and `GET /v1/sites` (keys:
|
|
51
|
+
`count`, `data`, `limit`, `offset`, `totalCount`). The _same_ key against the `api.ui.com` cloud
|
|
52
|
+
connector returns 401 — the wrong door for a key minted on a local console, not evidence the
|
|
53
|
+
cloud shape rejects `X-API-KEY` outright; `kv/infra/unifi/site-manager` is the separate cloud
|
|
54
|
+
credential, untouched by this family. Only reads were probed, by hand, outside this package's own
|
|
55
|
+
test suite — nothing in this PR's code exercises the vendor API live, and a write path would still
|
|
56
|
+
need its own verification.
|
|
57
|
+
|
|
58
|
+
## Adopt is the default posture, and it never writes
|
|
59
|
+
|
|
60
|
+
`adopt(true)` is piped onto every resource by its convenience constructor (`network(...)`,
|
|
61
|
+
`firewallZone(...)`) — H5. A cold read of a live match answers `Unowned(attrs)` (H1, the
|
|
62
|
+
`Cloudflare/Snippets/Snippet.ts` reference every marker-less family here follows: UniFi Network
|
|
63
|
+
objects carry no tag or metadata field this stack could stamp), and `adopt(true)` turns that into
|
|
64
|
+
a silent bind instead of an `OwnedBySomeoneElse` refusal — the deploy never needs `--adopt`.
|
|
65
|
+
|
|
66
|
+
⚠️ **A forced post-adoption reconcile still runs once (H6), and it still makes no write.**
|
|
67
|
+
Beta.79 forces one `reconcile` call after every cold adoption whether or not `diff` said noop.
|
|
68
|
+
`resource.ts`'s `reconcile` handles this the same way `discord/resource.ts` does: it reads the
|
|
69
|
+
live object again and only refuses when it is missing (would need a create) or drifted (would
|
|
70
|
+
need a write); an exact match returns the live attributes and calls nothing.
|
|
71
|
+
|
|
72
|
+
## `list` answers `[]`
|
|
73
|
+
|
|
74
|
+
`GET /v1/sites/{siteId}/networks` answers every network on the site. Adoption stays explicit —
|
|
75
|
+
the same reasoning `Proxmox.User`'s and NetBox's own `list` give.
|
|
76
|
+
|
|
77
|
+
## The declaration renderer
|
|
78
|
+
|
|
79
|
+
`declareNetwork(live, siteId)` and `declareFirewallZone(live, siteId)` are pure functions: given
|
|
80
|
+
one `getNetworkDetails`/`getFirewallZone` response, they return the `Props` a declaration needs
|
|
81
|
+
so its plan is a no-op. A later import script reads every object on a site and writes one
|
|
82
|
+
`network('<id>', declareNetwork(live, siteId))` row per result into `alchemy.run.ts` — nothing
|
|
83
|
+
here performs that write; these two functions are what the script would call.
|
|
84
|
+
|
|
85
|
+
## Vendor version
|
|
86
|
+
|
|
87
|
+
UniFi Network Integration API **10.4.57** (OpenAPI 3.1.0, 44 paths), via
|
|
88
|
+
`@distilled.cloud/unifi-network` aliased onto `@homeflare/distilled-unifi-network@0.2.0`
|
|
89
|
+
(`docs/distilled-interim.md`). Traps in the underlying API that this read-only family cannot
|
|
90
|
+
trigger but a future write path must respect — whole-object `PUT`, ordering endpoints that
|
|
91
|
+
replace the whole list, `removeDevice` unadopting and factory-resetting hardware — are recorded
|
|
92
|
+
in the distilled package's own `README.md` and in the estate's `docs/unifi-api-notes.md`
|
|
93
|
+
(outside this repo).
|
|
@@ -249,10 +249,20 @@ HttpClient` client. The old status-carrying `NetboxError` and its `cause.status
|
|
|
249
249
|
`GET/DELETE /dashboards/uid/{uid}`), alert rules or contact points (both have only a
|
|
250
250
|
`routeGet*Export` read-only route under `/v1/provisioning/`, no create/update/delete). S1
|
|
251
251
|
found no upstream `Alchemy` family for any of these either. Full detail:
|
|
252
|
-
[grafana.md](./grafana.md#the-sdk-gap--why-only-datasource-ships).
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
[grafana.md](./grafana.md#the-sdk-gap--why-only-datasource-ships).
|
|
253
|
+
|
|
254
|
+
✅ **S22 fixed 2026-09-24** (branch `claude2/distilled-grafana`): root cause measured to be
|
|
255
|
+
`skipDeprecated: true` dropping 64 operations Grafana's own spec marks `deprecated: true` in
|
|
256
|
+
favor of a separate, undocumented-here Kubernetes-apiserver route — not a spec omission. RFC-6902
|
|
257
|
+
patches un-deprecate exactly the 32 operations this finding named (folders, dashboards,
|
|
258
|
+
alerting-provisioning writes); the other 32 stay excluded. Shipped as
|
|
259
|
+
`@homeflare/distilled-grafana` (0.1.0 pre-release, 0.2.0 once this PR's `minor` changeset
|
|
260
|
+
releases it), aliased onto `@distilled.cloud/grafana` the same S22 route
|
|
261
|
+
`@homeflare/distilled-netbox` established. `Grafana.Datasource`'s props/attributes/generated code
|
|
262
|
+
are unchanged (diffed operation-by-operation against the prior commit). **Still open, and not
|
|
263
|
+
this PR's scope:** no `Grafana.Folder`/`Dashboard`/`AlertRule`/`ContactPoint`/
|
|
264
|
+
`NotificationPolicy`/`MuteTiming` resource calls the newly-available operations yet — follow-up
|
|
265
|
+
PRs, no longer blocked on the SDK.
|
|
256
266
|
- ⛔ **Same open gaps as `forgejo/*` and `netbox/*` above:** `read` never answers `Unowned`
|
|
257
267
|
(H1), and credentials come from an explicit env var NAME at call time rather than an
|
|
258
268
|
`alchemy/Auth` provider (S24) — here the house's own `grafanaCredentials(target)`, not the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -100,6 +100,14 @@
|
|
|
100
100
|
"types": "./dist/discord/index.d.ts",
|
|
101
101
|
"default": "./dist/discord/index.js"
|
|
102
102
|
},
|
|
103
|
+
"./unifi": {
|
|
104
|
+
"types": "./dist/unifi/index.d.ts",
|
|
105
|
+
"default": "./dist/unifi/index.js"
|
|
106
|
+
},
|
|
107
|
+
"./opnsense": {
|
|
108
|
+
"types": "./dist/opnsense/index.d.ts",
|
|
109
|
+
"default": "./dist/opnsense/index.js"
|
|
110
|
+
},
|
|
103
111
|
"./package.json": "./package.json"
|
|
104
112
|
},
|
|
105
113
|
"publishConfig": {
|
|
@@ -107,18 +115,20 @@
|
|
|
107
115
|
},
|
|
108
116
|
"dependencies": {
|
|
109
117
|
"@distilled.cloud/caddy": "npm:@homeflare/distilled-caddy@0.2.2",
|
|
118
|
+
"@distilled.cloud/grafana": "npm:@homeflare/distilled-grafana@0.2.0",
|
|
110
119
|
"@distilled.cloud/litellm": "npm:@homeflare/distilled-litellm@0.2.0",
|
|
111
120
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
121
|
+
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.2.0",
|
|
112
122
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
113
123
|
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.2.0",
|
|
114
|
-
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.2.0"
|
|
124
|
+
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.2.0",
|
|
125
|
+
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
|
|
115
126
|
},
|
|
116
127
|
"peerDependencies": {
|
|
117
128
|
"@distilled.cloud/cloudflare": "1.0.0-rc.12",
|
|
118
129
|
"@distilled.cloud/discord": "1.0.0-rc.12",
|
|
119
130
|
"@distilled.cloud/forgejo": "1.0.0-rc.12",
|
|
120
131
|
"@distilled.cloud/google-workspace": "1.0.0-rc.12",
|
|
121
|
-
"@distilled.cloud/grafana": "1.0.0-rc.12",
|
|
122
132
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
123
133
|
"@effect/sql-pg": "4.0.0-rc.115",
|
|
124
134
|
"alchemy": "2.0.0-beta.79",
|
package/src/grafana/index.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* the symbols a real stack consumes; the rest of the files (resource.ts's engine, fake-grafana.ts)
|
|
6
6
|
* are internals a provider needs but a consumer should not depend on.
|
|
7
7
|
*
|
|
8
|
-
* ⛔ ONLY `Grafana.Datasource` SHIPS HERE. `@distilled.cloud/grafana
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* ⛔ ONLY `Grafana.Datasource` SHIPS HERE. `@distilled.cloud/grafana` (aliased onto
|
|
9
|
+
* `@homeflare/distilled-grafana` — see docs/distilled-interim.md) now HAS folder, dashboard and
|
|
10
|
+
* alerting-provisioning CRUD operations — the SDK-level gap docs/grafana.md and
|
|
11
|
+
* docs/upstream-conformance.md recorded is fixed. Building `Grafana.Folder`, `Grafana.Dashboard`,
|
|
12
|
+
* `Grafana.AlertRule` etc. on top of them is follow-up work this PR deliberately does not do —
|
|
13
|
+
* see docs/grafana.md for what changed and what is still open.
|
|
13
14
|
*/
|
|
14
15
|
export {
|
|
15
16
|
type DatasourceAttributes,
|