@homeflare/alchemy 0.38.0 → 0.39.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 +22 -0
- package/dist/forgejo/index.js +4 -2
- package/dist/forgejo/index.js.map +3 -3
- package/dist/forgejo/team-member.d.ts +25 -0
- package/dist/forgejo/team-member.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +14 -3
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +565 -133
- package/dist/proxmox/index.js.map +33 -23
- package/dist/proxmox/qemu-form.d.ts +27 -18
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts +30 -0
- package/dist/proxmox/qemu-judge.d.ts.map +1 -0
- package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-net.d.ts +9 -0
- package/dist/proxmox/qemu-net.d.ts.map +1 -0
- package/dist/proxmox/qemu-props.d.ts +112 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -0
- package/dist/proxmox/qemu-volume.d.ts +17 -0
- package/dist/proxmox/qemu-volume.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +14 -27
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/storage-download-errors.d.ts +5 -0
- package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
- package/dist/proxmox/storage-download-identity.d.ts +27 -0
- package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/storage-download-props.d.ts +36 -0
- package/dist/proxmox/storage-download-props.d.ts.map +1 -0
- package/dist/proxmox/storage-download-task.d.ts +17 -0
- package/dist/proxmox/storage-download-task.d.ts.map +1 -0
- package/dist/proxmox/storage-download.d.ts +44 -0
- package/dist/proxmox/storage-download.d.ts.map +1 -0
- package/dist/telemetry/fake-collector.d.ts +36 -0
- package/dist/telemetry/fake-collector.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +223 -0
- package/dist/telemetry/index.js.map +12 -0
- package/dist/telemetry/layer.d.ts +86 -0
- package/dist/telemetry/layer.d.ts.map +1 -0
- package/dist/telemetry/redact-serialization.d.ts +21 -0
- package/dist/telemetry/redact-serialization.d.ts.map +1 -0
- package/dist/telemetry/redact.d.ts +46 -0
- package/dist/telemetry/redact.d.ts.map +1 -0
- package/dist/unifi/drift.d.ts +27 -0
- package/dist/unifi/drift.d.ts.map +1 -0
- package/dist/unifi/fake-unifi.d.ts +5 -1
- package/dist/unifi/fake-unifi.d.ts.map +1 -1
- package/dist/unifi/firewall-zone-form.d.ts +6 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +1 -1
- package/dist/unifi/firewall-zone.d.ts.map +1 -1
- package/dist/unifi/index.d.ts +4 -3
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +138 -58
- package/dist/unifi/index.js.map +10 -8
- package/dist/unifi/network-drift.d.ts +26 -0
- package/dist/unifi/network-drift.d.ts.map +1 -0
- package/dist/unifi/network-form.d.ts +40 -7
- package/dist/unifi/network-form.d.ts.map +1 -1
- package/dist/unifi/network.d.ts +1 -0
- package/dist/unifi/network.d.ts.map +1 -1
- package/dist/unifi/paginate.d.ts +36 -0
- package/dist/unifi/paginate.d.ts.map +1 -0
- package/dist/unifi/resource.d.ts +36 -1
- package/dist/unifi/resource.d.ts.map +1 -1
- package/docs/telemetry-spike.md +94 -0
- package/docs/telemetry.md +109 -0
- package/docs/unifi.md +56 -0
- package/package.json +6 -2
- package/src/forgejo/team-member.test.ts +14 -0
- package/src/forgejo/team-member.ts +28 -1
- package/src/proxmox/constraints-required.test.ts +1 -0
- package/src/proxmox/generated/constraints/index.ts +3 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.test.ts +11 -6
- package/src/proxmox/index.ts +19 -3
- package/src/proxmox/qemu-form.ts +49 -13
- package/src/proxmox/qemu-identity.test.ts +30 -0
- package/src/proxmox/qemu-judge.test.ts +65 -0
- package/src/proxmox/qemu-judge.ts +84 -0
- package/src/proxmox/qemu-lifecycle.test.ts +78 -19
- package/src/proxmox/qemu-lifecycle.ts +131 -32
- package/src/proxmox/qemu-net.test.ts +48 -0
- package/src/proxmox/qemu-net.ts +78 -0
- package/src/proxmox/qemu-ownership.test.ts +44 -0
- package/src/proxmox/qemu-props.test.ts +87 -0
- package/src/proxmox/qemu-props.ts +157 -0
- package/src/proxmox/qemu-volume.test.ts +80 -0
- package/src/proxmox/qemu-volume.ts +121 -0
- package/src/proxmox/qemu.ts +15 -29
- package/src/proxmox/storage-download-errors.ts +7 -0
- package/src/proxmox/storage-download-identity.test.ts +54 -0
- package/src/proxmox/storage-download-identity.ts +50 -0
- package/src/proxmox/storage-download-lifecycle.ts +226 -0
- package/src/proxmox/storage-download-ownership.test.ts +50 -0
- package/src/proxmox/storage-download-props.ts +38 -0
- package/src/proxmox/storage-download-task.ts +72 -0
- package/src/proxmox/storage-download.test.ts +216 -0
- package/src/proxmox/storage-download.ts +58 -0
- package/src/telemetry/fake-collector.ts +59 -0
- package/src/telemetry/http-client-sealing.test.ts +82 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/layer.test.ts +204 -0
- package/src/telemetry/layer.ts +152 -0
- package/src/telemetry/redact-serialization.ts +178 -0
- package/src/telemetry/redact.test.ts +124 -0
- package/src/telemetry/redact.ts +147 -0
- package/src/unifi/drift.test.ts +78 -0
- package/src/unifi/drift.ts +56 -0
- package/src/unifi/errors-and-secrets.test.ts +127 -0
- package/src/unifi/fake-unifi.ts +5 -1
- package/src/unifi/firewall-zone-form.ts +31 -2
- package/src/unifi/firewall-zone.test.ts +21 -1
- package/src/unifi/firewall-zone.ts +1 -1
- package/src/unifi/get-only-guard.test.ts +165 -0
- package/src/unifi/index.ts +15 -1
- package/src/unifi/network-drift.test.ts +207 -0
- package/src/unifi/network-drift.ts +80 -0
- package/src/unifi/network-form.ts +11 -39
- package/src/unifi/network.test.ts +7 -82
- package/src/unifi/network.ts +6 -6
- package/src/unifi/paginate.test.ts +228 -0
- package/src/unifi/paginate.ts +129 -0
- package/src/unifi/resource.ts +73 -3
- package/src/unifi/write-op-reference.test.ts +236 -0
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.Network`'s wire shape: the declarable subset of `NetworkDetails`, and how a live read
|
|
3
|
+
* becomes both plan attributes and a rendered declaration.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ PROPS MIRROR `UpdateNetworkRequest`, NOT `NetworkDetails`. `default` and `metadata` are
|
|
6
|
+
* server-derived — no writable endpoint in `networks.ts` accepts either — so they are
|
|
7
|
+
* attributes-only, reported but never compared or declared. Everything else in `NetworkProps`
|
|
8
|
+
* is exactly what `updateNetwork`/`createNetwork` would accept, even though this family never
|
|
9
|
+
* calls either: the shape is what a future write path would need, and what today's read-only
|
|
10
|
+
* `matches` compares against.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ `ipv4Configuration` STAYS `unknown`, ON THE SDK'S OWN AUTHORITY. `networks.ts`'s own comment
|
|
13
|
+
* ("Shape varies by variant — widened by scripts/convert.ts; see README") says the OpenAPI
|
|
14
|
+
* discriminator this field varies over was flattened at generation time, not decoded per
|
|
15
|
+
* variant. Comparing it as an opaque JSON value (below) is honest about that; guessing a
|
|
16
|
+
* narrower shape here would silently stop matching the day a new variant's key order differs.
|
|
17
|
+
* ⚠️ It may ALSO hide an unordered array the way `dhcpGuarding` and `ipv6Configuration` do
|
|
18
|
+
* below — this family just can't know, because the shape is never decoded. If a future variant
|
|
19
|
+
* plans spurious `update`s, suspect this field first and widen it into a typed, order-normalized
|
|
20
|
+
* shape rather than adding another opaque comparison.
|
|
21
|
+
*
|
|
22
|
+
* ⛔ THREE FIELDS ARE SETS, NOT SEQUENCES: `dhcpGuarding.trustedDhcpServerIpAddresses`,
|
|
23
|
+
* `ipv6Configuration.additionalHostIpSubnets`, and `ipv6Configuration.dnsServerIpAddressesOverride`.
|
|
24
|
+
* The vendor document gives no indication any of the three is ordered — same reasoning
|
|
25
|
+
* `firewall-zone-form.ts` gives for `networkIds`, and `proxmox/user-wire.ts`'s `groupSet` gives
|
|
26
|
+
* for PVE's `groups` — but `matches` compares via alchemy's `deepEqual`, which sorts object KEYS
|
|
27
|
+
* but NOT ARRAY ELEMENTS (`node_modules/alchemy/lib/Diff.js`). Left alone, a console that
|
|
28
|
+
* returns the same set in a different order between calls plans a spurious `update`, and the
|
|
29
|
+
* read-only reconcile then refuses every deploy. `sortedSet` (dedupe + sort) neutralizes that
|
|
30
|
+
* before any of the three is attributed, declared, or compared. Every OTHER array field in this
|
|
31
|
+
* family (there are none today besides these three) would stay declaration-order as written.
|
|
32
|
+
*/
|
|
1
33
|
import type * as networks from '@distilled.cloud/unifi-network/networks';
|
|
34
|
+
export declare const normalizeDhcpGuarding: (value: networks.NetworkDHCPGuarding | undefined) => networks.NetworkDHCPGuarding | undefined;
|
|
35
|
+
export declare const normalizeIpv6Configuration: (value: networks.NetworkIPv6Configuration | undefined) => networks.NetworkIPv6Configuration | undefined;
|
|
2
36
|
/**
|
|
3
37
|
* ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` ON PURPOSE. `tsconfig.json` sets
|
|
4
38
|
* `exactOptionalPropertyTypes`, which treats `field?: T` (absent-or-T) and
|
|
@@ -47,14 +81,13 @@ export interface NetworkAttributes {
|
|
|
47
81
|
}
|
|
48
82
|
export declare const attributesOf: (live: networks.NetworkDetails, props: NetworkProps) => NetworkAttributes;
|
|
49
83
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
84
|
+
* MEDIUM-4 (red team, 2026-09-26): `matches` used to be a hand-written boolean expression here,
|
|
85
|
+
* kept in sync BY HAND with `network-drift.ts`'s separately-maintained field list — two comparisons
|
|
86
|
+
* that could (and, per a red-team mutant, silently did) drift apart. `matches` now lives in
|
|
87
|
+
* `network-drift.ts`, defined as `fieldDrift(...).length === 0` — the SAME comparison `driftOf`
|
|
88
|
+
* runs, not a second one. See that file's own header for the `stripNullish`/normalizer reasoning
|
|
89
|
+
* this expression used to carry.
|
|
56
90
|
*/
|
|
57
|
-
export declare const matches: (attributes: NetworkAttributes, props: NetworkProps) => boolean;
|
|
58
91
|
/**
|
|
59
92
|
* The declaration renderer (task spec: "given one live object, return the props a declaration
|
|
60
93
|
* needs so its plan is noop"). A later import script feeds `getNetworkDetails`'s own response
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network-form.d.ts","sourceRoot":"","sources":["../../src/unifi/network-form.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"network-form.d.ts","sourceRoot":"","sources":["../../src/unifi/network-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAgBzE,eAAO,MAAM,qBAAqB,UACzB,QAAQ,CAAC,mBAAmB,GAAG,SAAS,KAC9C,QAAQ,CAAC,mBAAmB,GAAG,SAG6D,CAAC;AAOhG,eAAO,MAAM,0BAA0B,UAC9B,QAAQ,CAAC,wBAAwB,GAAG,SAAS,KACnD,QAAQ,CAAC,wBAAwB,GAAG,SAWhC,CAAC;AAER;;;;;;;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;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,SAAU,QAAQ,CAAC,cAAc,UAAU,MAAM,KAAG,YAgB7E,CAAC"}
|
package/dist/unifi/network.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { type NetworkAttributes, type NetworkProps } from './network-form.ts';
|
|
|
26
26
|
import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
27
27
|
export type { NetworkAttributes, NetworkProps } from './network-form.ts';
|
|
28
28
|
export { declareNetwork } from './network-form.ts';
|
|
29
|
+
export { driftOf } from './network-drift.ts';
|
|
29
30
|
export interface UnifiNetwork extends Resource<'Unifi.Network', NetworkProps, NetworkAttributes, never, UnifiRequirements> {
|
|
30
31
|
}
|
|
31
32
|
export declare const UnifiNetwork: import("alchemy").ResourceClass<UnifiNetwork>;
|
|
@@ -1 +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,
|
|
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,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAgB,MAAM,mBAAmB,CAAC;AAE5F,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;AAInD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,OAAQ,MAAM,SAAS,YAAY,6FACZ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,YAAY,EACZ,QAAQ,CAAC,cAAc,EACvB,iBAAiB,EACjB,QAAQ,CAAC,sBAAsB,CAUhC,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,oBAAoB,kIACkD,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
/** The one response shape every UniFi Network list operation already has (`protocol.ts`). */
|
|
3
|
+
export interface OffsetPage<T> {
|
|
4
|
+
readonly data: readonly T[];
|
|
5
|
+
readonly offset: number;
|
|
6
|
+
readonly totalCount: number;
|
|
7
|
+
}
|
|
8
|
+
/** The one request shape every UniFi Network list operation already accepts, plus its own fields. */
|
|
9
|
+
export type OffsetPageRequest = {
|
|
10
|
+
readonly offset?: number;
|
|
11
|
+
readonly limit?: number;
|
|
12
|
+
};
|
|
13
|
+
declare const UnifiPaginationInconsistent_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
14
|
+
readonly _tag: "UnifiPaginationInconsistent";
|
|
15
|
+
} & Readonly<A>;
|
|
16
|
+
export declare class UnifiPaginationInconsistent extends UnifiPaginationInconsistent_base<{
|
|
17
|
+
readonly reason: 'offset-mismatch' | 'total-count-changed' | 'row-count-mismatch' | 'page-ceiling';
|
|
18
|
+
readonly detail: string;
|
|
19
|
+
}> {
|
|
20
|
+
get message(): string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walks every page of `fetchPage`, starting at `request.offset ?? 0`, and returns every row in
|
|
24
|
+
* order. `request.limit`, if given, is kept as the page size for every call; the vendor's own
|
|
25
|
+
* default applies otherwise — this loop never invents one (T18: "pass ops only their own fields").
|
|
26
|
+
*
|
|
27
|
+
* Three self-consistency checks run against fields the vendor already sends with every page —
|
|
28
|
+
* detection, not a fix, for the "live console mutates mid-walk" case the header above accepts as
|
|
29
|
+
* unfixable from the client alone: a row added AND removed in the same window can still net out to
|
|
30
|
+
* a `totalCount` and final row count that agree by coincidence. What this closes is the SILENT
|
|
31
|
+
* version of every case actually observed in review: an ignored `offset`, a `totalCount` that
|
|
32
|
+
* drifts between calls, and an undercounted `totalCount` that truncates the walk early.
|
|
33
|
+
*/
|
|
34
|
+
export declare const pageAll: <Req extends OffsetPageRequest, T, E, R>(fetchPage: (request: Req) => Effect.Effect<OffsetPage<T>, E, R>, request: Req) => Effect.Effect<T[], E | UnifiPaginationInconsistent, R>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=paginate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paginate.d.ts","sourceRoot":"","sources":["../../src/unifi/paginate.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,6FAA6F;AAC7F,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,qGAAqG;AACrG,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;;;;AAQtF,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,MAAM,EACX,iBAAiB,GACjB,qBAAqB,GACrB,oBAAoB,GACpB,cAAc,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,GAAG,SAAS,iBAAiB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,aACjD,CAAC,OAAO,EAAE,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,WACtD,GAAG,KACX,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,2BAA2B,EAAE,CAAC,CA4CpD,CAAC"}
|
package/dist/unifi/resource.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Input } from 'alchemy/Input';
|
|
2
2
|
import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
|
|
3
3
|
import * as Effect from 'effect/Effect';
|
|
4
|
-
import
|
|
4
|
+
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
5
5
|
/** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
|
|
6
6
|
export type UnifiRequirements = HttpClient.HttpClient;
|
|
7
7
|
/**
|
|
@@ -34,6 +34,41 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
|
|
|
34
34
|
reconcile: (news: Props) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
|
|
35
35
|
destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* B0a / T12 DEFENSE IN DEPTH. `policy.ts`'s `UnifiWriteRefused` stops write INTENT at
|
|
39
|
+
* `reconcile`/`destroy` — every path this engine exposes already returns that refusal instead of
|
|
40
|
+
* calling an SDK write op. This guard stops the same thing one layer lower, AT THE WIRE: it wraps
|
|
41
|
+
* whatever `HttpClient` the caller already provides (the stack's `FetchHttpClient.layer` in
|
|
42
|
+
* production, `fakeUnifiLayer` in tests — `openbao/bao-http.ts`'s header names this same
|
|
43
|
+
* "wrap, don't replace" seam) so a future resource file that, by mistake, called an SDK write
|
|
44
|
+
* operation directly — bypassing `unifiOperations` entirely — would still never reach the vendor
|
|
45
|
+
* API. `write-op-reference.test.ts` proves no such call exists today; this is what stops one from
|
|
46
|
+
* ever taking effect if one is ever added.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ `Effect.die`, NOT A TYPED FAILURE — classified per distilled-doctrine's "classify every
|
|
49
|
+
* die/orDie". Every generated SDK operation (`networks.getNetworkDetails`, …) declares its OWN
|
|
50
|
+
* closed error union from the pinned OpenAPI spec, and generated files are never hand-edited to
|
|
51
|
+
* add a member to it. A typed `Effect.fail` here would be a failure mode absent from every op's
|
|
52
|
+
* declared type the moment it occurred — a type-system lie. A defect is the sound way to add a
|
|
53
|
+
* NEW "this must be structurally impossible" failure underneath types this package does not
|
|
54
|
+
* own, exactly like `openbao`'s `refuse()` helpers' `Effect.die(new Error(...))` for a
|
|
55
|
+
* configuration move that should never be reachable.
|
|
56
|
+
*/
|
|
57
|
+
export declare class UnifiNonGetRequest extends Error {
|
|
58
|
+
readonly method: string;
|
|
59
|
+
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
60
|
+
readonly path: string;
|
|
61
|
+
constructor(method: string,
|
|
62
|
+
/** PATH ONLY, never the full URL — see `requestPath` below. */
|
|
63
|
+
path: string);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
|
|
67
|
+
* for everything downstream, with `guardGetOnly`'s wrapped version (`HttpClient.layerMergedContext`
|
|
68
|
+
* — see the guard above). Exported so `get-only-guard.test.ts` can prove the mechanism directly,
|
|
69
|
+
* against a bare `HttpClient` service, without needing `CredentialsFromEnv` or any env var at all.
|
|
70
|
+
*/
|
|
71
|
+
export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
|
|
37
72
|
/**
|
|
38
73
|
* The four provider handlers for a spec'd read-only UniFi object, wired once.
|
|
39
74
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAG9D,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAChF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAChF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;kBAEtB,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;sBAIP,KAAK;oBAYP,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM;IACvB,+DAA+D;IACtD,IAAI,EAAE,MAAM,EAStB;CACF;AA0BD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,mFAE7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAC9E,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAMzC,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBA5GtB,QAAQ;;yBAElC,MAAM;;IA4GzB,SAAS,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACjC,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Spike: what a stack's `providers` Layer captures (2026-09-26)
|
|
2
|
+
|
|
3
|
+
Offline, no live Victoria call, per the workflow's own rule. A throwaway script (deleted after this
|
|
4
|
+
was recorded — not committed; `layer.test.ts` and `http-client-sealing.test.ts` are what's kept) ran
|
|
5
|
+
`verify/fake-engine.ts`'s `engineOver` — the REAL `Alchemy.Stack` + `Plan.make` + `apply` over an
|
|
6
|
+
in-memory state store, alchemy `2.0.0-beta.79` — with:
|
|
7
|
+
|
|
8
|
+
- a fake OTLP collector (`fake-collector.ts`'s `Bun.serve`, capturing every POST's path/headers/body)
|
|
9
|
+
- `telemetryLayer({ endpoints: { traces: '<collector>/v1/traces' }, serviceName: 'spike' })` merged
|
|
10
|
+
into the stack's `providers`
|
|
11
|
+
- one fake resource (`Spike.Thing`) whose `reconcile` wraps an `Effect.withSpan('spike.provider.
|
|
12
|
+
custom-span')` around an `HttpClient` `GET` to a SECOND fake server (`target`, standing in for
|
|
13
|
+
whatever real endpoint a provider calls) at `<target>/probe?token=SECRET123` with an
|
|
14
|
+
`authorization: Bearer sekrit` header
|
|
15
|
+
- a real `FetchHttpClient.layer` merged in as `ambient`, standing in for the platform layer a real
|
|
16
|
+
`alchemy.run.ts`'s `Stack.ts` provides underneath the merged `providers` (admin-transport-
|
|
17
|
+
scope.test.ts's own pattern) — `telemetryLayer`'s own `FetchHttpClient` is sealed, so without this
|
|
18
|
+
the fake provider's `HttpClient.HttpClient` requirement has nothing to resolve to
|
|
19
|
+
|
|
20
|
+
## Question 1: which spans arrive when the tracer is provided via `providers`?
|
|
21
|
+
|
|
22
|
+
**All of them.** One `deploy()` (a create) produced exactly one POST to `/v1/traces` containing:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
state_store.init, plan.diff.resource, plan.make,
|
|
26
|
+
http.client GET, spike.provider.custom-span,
|
|
27
|
+
provider.create, apply.resource, apply
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **The provider's own span** (`spike.provider.custom-span`) arrived.
|
|
31
|
+
- **The `HttpClient` client span** (`http.client GET`, effect's own `HttpClient.make` instrumentation)
|
|
32
|
+
arrived — effect's tracing reads whatever `Tracer.Tracer` is ambient when the request runs, and
|
|
33
|
+
the stack's `providers` context is exactly that.
|
|
34
|
+
- **Alchemy's own engine spans arrived too**: `plan.make` (Plan.ts), `apply`/`apply.resource`
|
|
35
|
+
(Apply.ts's own `Effect.withSpan` calls), and `provider.create` — alchemy's OWN
|
|
36
|
+
`instrumentLifecycle` wrapper (Apply.ts) puts every provider lifecycle call (`read`/`reconcile`/
|
|
37
|
+
`delete`) in a `provider.${op}` span ALREADY, with `alchemy.resource.fqn`/`type`/`op` attributes —
|
|
38
|
+
a provider doesn't need to add its own span just to get lifecycle-level tracing.
|
|
39
|
+
- `state_store.init` and `plan.diff.resource` are alchemy internals not named in the facts this
|
|
40
|
+
workflow started from — recorded here since "measure which spans arrive" means all of them, not
|
|
41
|
+
only the three anticipated.
|
|
42
|
+
|
|
43
|
+
**Why**: `verify/fake-engine.ts`'s `engineOver` builds `compiled.services` from the `providers`
|
|
44
|
+
layer, then runs `Plan.make`/`apply` with `Effect.provide(Layer.succeedContext(compiled.services))`
|
|
45
|
+
around them (`fake-engine.ts`'s `run`). Since `Tracer.Tracer` is an `effect/Tracer` `Context.
|
|
46
|
+
Reference` (a value with a process-wide default that any `Effect.provide` can override for the
|
|
47
|
+
effects it wraps), and `telemetryLayer` sets that reference via `Layer.effect(Tracer.Tracer, …)`
|
|
48
|
+
(the same shape `OtlpTracer.layer` itself uses), every span created inside that `Effect.provide` —
|
|
49
|
+
alchemy's own internals included — reads the OTLP-backed, redacting tracer, not effect's default.
|
|
50
|
+
**This holds specifically because the tracer is installed through the stack's OWN `providers`
|
|
51
|
+
option**, the mechanism `alchemy.run.ts` already exposes to every consumer stack — not through
|
|
52
|
+
alchemy's separate CLI-wide `Telemetry/Layer.ts` install this workflow's decision explicitly does
|
|
53
|
+
not touch.
|
|
54
|
+
|
|
55
|
+
## Question 2: does redaction hold on a REAL span, not just the unit tests?
|
|
56
|
+
|
|
57
|
+
Yes — the captured `http.client GET` span's `url.full` was `http://127.0.0.1:<port>/probe`: no
|
|
58
|
+
`?token=SECRET123`. With a `redaction: { denylist: [<target's host>, 'secret-site-abc123'] }` policy
|
|
59
|
+
and a target URL of `<target>/secret-site-abc123/probe?token=SECRET`, the captured attributes were:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
server.address: "<redacted>"
|
|
63
|
+
url.full: "http://<redacted>/<redacted>/probe"
|
|
64
|
+
url.path: "/<redacted>/probe"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
with **no `http.request.header.*` key at all** — the `authorization: Bearer sekrit` header was
|
|
68
|
+
dropped outright, not value-redacted. One correction this made to the implementation: effect's
|
|
69
|
+
`HttpClient.make` sets `server.address` to `url.origin` (`http://host:port`), not a bare hostname —
|
|
70
|
+
`redactAttribute`'s `server.address` branch now parses it with `URL` before comparing to the
|
|
71
|
+
denylist, rather than comparing the raw scheme-prefixed string (which would never match a
|
|
72
|
+
consumer's hostname-only denylist entry).
|
|
73
|
+
|
|
74
|
+
## Question 3: does the sealed transport actually seal?
|
|
75
|
+
|
|
76
|
+
Confirmed twice, once by accident: the FIRST version of this spike's script had no `ambient`
|
|
77
|
+
`FetchHttpClient` merged in, on the assumption that a leaking `telemetryLayer` would supply one. It
|
|
78
|
+
crashed instead — `error: Service not found: effect/HttpClient` — which is direct, if accidental,
|
|
79
|
+
proof that `telemetryLayer`'s own `FetchHttpClient` never reaches a sibling provider.
|
|
80
|
+
`http-client-sealing.test.ts`'s second test makes that assertion explicit and permanent
|
|
81
|
+
(`.rejects.toThrow()` with no `ambient` layer present) rather than relying on stumbling into it
|
|
82
|
+
again; its first test is the positive case (a sibling gets the real `ambient` client, not
|
|
83
|
+
telemetry's).
|
|
84
|
+
|
|
85
|
+
## Question 4: does the shutdown flush actually deliver a short CLI run's spans?
|
|
86
|
+
|
|
87
|
+
Yes, with no extra wait beyond `engine.deploy()`'s own returned `Promise` resolving. `OtlpExporter.
|
|
88
|
+
make` (effect's shared batch exporter) registers a `Scope.addFinalizer` that forces one more export
|
|
89
|
+
and awaits it (bounded by `shutdownTimeout`, default 3s) — and `engineOver`'s `run` wraps the whole
|
|
90
|
+
plan+apply effect in `Effect.scoped`, so that finalizer runs, and is awaited, before `deploy()`'s
|
|
91
|
+
promise settles. The spike's default `exportInterval` ('1 second') never even had to fire for the
|
|
92
|
+
one-resource run above; the shutdown flush alone delivered the batch. Confirmed separately: with
|
|
93
|
+
`endpoints: {}`, the fake collector saw zero requests even after waiting 1.5s (longer than the
|
|
94
|
+
default interval) past `deploy()` returning — nothing fires later on some other timer.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# `@homeflare/alchemy/telemetry` — OTLP tracing/logging/metrics for a stack
|
|
2
|
+
|
|
3
|
+
`telemetryLayer` builds an OTLP/HTTP `Layer` for a stack's own `providers` — spans, logs and
|
|
4
|
+
metrics from `Effect.withSpan`, `HttpClient` calls and alchemy's own plan/apply engine, exported to
|
|
5
|
+
whatever OTLP collector a consumer names. Read 2026-09-26 against `alchemy@2.0.0-beta.79` and
|
|
6
|
+
`effect@4.0.0-rc.115`'s `effect/unstable/observability` module (this file names no estate host — see
|
|
7
|
+
below). See `telemetry-spike.md` for what was actually measured arriving at a collector.
|
|
8
|
+
|
|
9
|
+
Bodies are sent **protobuf-encoded** (`content-type: application/x-protobuf`), not JSON — measured
|
|
10
|
+
against the pinned binaries in homeflare-mini's own `src/obs`: VictoriaLogs 1.52.0 and
|
|
11
|
+
VictoriaMetrics 1.151.0 both answer OTLP/HTTP JSON with a `400`, and effect's exporter treats a 400
|
|
12
|
+
as non-transient and drops the batch silently at debug-only log level, so a JSON-encoded consumer
|
|
13
|
+
would see traces (VictoriaTraces alone tolerates JSON) but never logs or metrics, with no visible
|
|
14
|
+
error. There is no option to choose JSON instead.
|
|
15
|
+
|
|
16
|
+
## Off unless you give it endpoints
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { telemetryLayer } from '@homeflare/alchemy/telemetry';
|
|
20
|
+
|
|
21
|
+
export const providers = Layer.mergeAll(
|
|
22
|
+
myProviders(),
|
|
23
|
+
telemetryLayer({
|
|
24
|
+
endpoints: {
|
|
25
|
+
traces: 'http://victoria.example.test:10428/insert/opentelemetry/v1/traces',
|
|
26
|
+
logs: 'http://victoria.example.test:9428/insert/opentelemetry/v1/logs',
|
|
27
|
+
metrics: 'http://victoria.example.test:8428/opentelemetry/v1/metrics',
|
|
28
|
+
},
|
|
29
|
+
serviceName: 'my-stack',
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every entry in `endpoints` is optional and `undefined` by default — an entry you leave out sends
|
|
35
|
+
nothing for that signal, and `telemetryLayer({ endpoints: {} , … })` sends nothing at all
|
|
36
|
+
(`layer.test.ts`'s own "nothing sent when unconfigured"). There is no default collector anywhere in
|
|
37
|
+
this module; a consumer's own site config supplies real hosts. `victoria.example.test` above is an
|
|
38
|
+
RFC 2606 reserved name — put your actual Victoria host there, e.g. from a `bao read` or an
|
|
39
|
+
environment variable your site file already has.
|
|
40
|
+
|
|
41
|
+
The three paths above are VictoriaTraces/Logs/Metrics's own OTLP ingest paths (their upstream docs,
|
|
42
|
+
version-specific — verify against the versions you run, not this file, per this repo's own house
|
|
43
|
+
rule that a documented path is not true because it is written down). They are examples, not
|
|
44
|
+
defaults: `telemetryLayer` never assumes a path shape, since `OtlpTracer`/`OtlpLogger`/`OtlpMetrics`
|
|
45
|
+
each post to the exact `url` given, with nothing appended.
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
| option | type | notes |
|
|
50
|
+
| -------------------- | ------------------------------ | ------------------------------------------------------------------ |
|
|
51
|
+
| `endpoints` | `{ traces?, logs?, metrics? }` | full URLs, each independent — see above. |
|
|
52
|
+
| `serviceName` | `string` | required — OTLP `service.name`. |
|
|
53
|
+
| `serviceVersion` | `string?` | OTLP `service.version`. |
|
|
54
|
+
| `resourceAttributes` | `Record<string, unknown>?` | extra OTLP resource attributes, e.g. `deployment.environment`. |
|
|
55
|
+
| `redaction` | `{ denylist?: string[] }?` | see below. |
|
|
56
|
+
| `exportInterval` | `Duration.Input?` | default `'1 second'` — short on purpose, see `telemetry-spike.md`. |
|
|
57
|
+
| `logsMinimumLevel` | `LogLevel.LogLevel?` | default `'Info'` — floor for what `logs` exports; see below. |
|
|
58
|
+
|
|
59
|
+
## Redaction — what leaves this process
|
|
60
|
+
|
|
61
|
+
Two passes run, at two different points, because no single hook sees every field (redteam PR 302,
|
|
62
|
+
2026-09-26 — `redact.ts`/`redact-serialization.ts` have the full detail):
|
|
63
|
+
|
|
64
|
+
1. **`redactAttribute`** (`redact.ts`) wraps the `Tracer.Tracer` itself, so it runs on every span
|
|
65
|
+
attribute AS IT IS SET — provider spans, `HttpClient` client spans, and alchemy's own engine
|
|
66
|
+
spans:
|
|
67
|
+
- **Every HTTP header is dropped**, request or response, never merely value-redacted — a header
|
|
68
|
+
NAME can itself be sensitive.
|
|
69
|
+
- **Every query string is dropped** — `url.query` outright, and `url.full`'s own `?…`/`#…` — no
|
|
70
|
+
per-parameter allowlist, since the next secret param a provider adds would otherwise leak.
|
|
71
|
+
- **A denylisted host or path segment is blanked to `<redacted>`**, exact-segment match (never a
|
|
72
|
+
substring), in `url.full`, `url.path` and `server.address` (bare hostname or full origin, both).
|
|
73
|
+
2. **`scrubText`/`redactTraceData`/`redactLogsData`** (`redact-serialization.ts`) run at
|
|
74
|
+
`OtlpSerialization`, right before the wire body is built — the one place that also sees fields
|
|
75
|
+
`redactAttribute` never does: `status.message` and the `exception.*` event attributes
|
|
76
|
+
`OtlpTracer` builds from a failed span's `Cause` at export time, and a log line inside a span
|
|
77
|
+
(turned into a span event by effect's default `tracerLogger` — the log message becomes the event
|
|
78
|
+
NAME). These are free text, not a structured URL, so this pass matches a denylist entry as a
|
|
79
|
+
**substring anywhere** in the string, and strips the query from any embedded URL. **Logs get the
|
|
80
|
+
same pass** (`redactLogsData`) — a log record's body and its `log.error` (`Cause.pretty`, full
|
|
81
|
+
formatted error text) attribute.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
telemetryLayer({
|
|
85
|
+
endpoints: { traces: 'http://victoria.example.test:10428/insert/opentelemetry/v1/traces' },
|
|
86
|
+
redaction: { denylist: ['a1b2c3d4e5f6'] }, // e.g. a UniFi console id in the request path
|
|
87
|
+
serviceName: 'my-stack',
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Everything else — span names once scrubbed, timing, alchemy's own `alchemy.resource.*`/
|
|
92
|
+
`alchemy.stack` attributes, non-URL attributes a provider sets itself — passes through untouched. A
|
|
93
|
+
provider that puts a secret in a NON-url, non-header, non-denylisted attribute (e.g.
|
|
94
|
+
`Effect.withSpan('x', { attributes: { 'my.token': secret } })`) is not caught by either pass; that is
|
|
95
|
+
a provider bug this module cannot see, not a gap in the redaction rules above.
|
|
96
|
+
|
|
97
|
+
`logsMinimumLevel` (default `'Info'`) is a separate concern from redaction: the `alchemy` CLI sets
|
|
98
|
+
the GLOBAL minimum log level to Debug, and without a floor of its own the OTLP logger would ship
|
|
99
|
+
every Debug-level record. It applies only to what this layer exports — a consumer's console/file
|
|
100
|
+
loggers are unaffected. Metrics carry numeric data points, not provider-supplied free text, and pass
|
|
101
|
+
through both passes untouched.
|
|
102
|
+
|
|
103
|
+
## What this is not
|
|
104
|
+
|
|
105
|
+
This is **not** alchemy's own CLI telemetry (`alchemy`'s `src/Telemetry/Layer.ts`, hard-coded to
|
|
106
|
+
`otel.alchemy.run`, opted out of estate-wide via `~/.alchemy/telemetry-disabled`). That stays
|
|
107
|
+
retired for this decision (2026-09-26): this module is a per-stack, explicit-endpoint layer
|
|
108
|
+
each consumer opts into, not a CLI-wide install, and it never calls alchemy's endpoint or reads its
|
|
109
|
+
opt-out file.
|
package/docs/unifi.md
CHANGED
|
@@ -74,6 +74,56 @@ need a write); an exact match returns the live attributes and calls nothing.
|
|
|
74
74
|
`GET /v1/sites/{siteId}/networks` answers every network on the site. Adoption stays explicit —
|
|
75
75
|
the same reasoning `Proxmox.User`'s and NetBox's own `list` give.
|
|
76
76
|
|
|
77
|
+
## Defense in depth: GET-only at the wire (2026-09-26)
|
|
78
|
+
|
|
79
|
+
`policy.ts`'s `UnifiWriteRefused` stops write INTENT at `reconcile`/`delete` — this family calls
|
|
80
|
+
no SDK write op anywhere today. `GetOnlyHttpClient` (`resource.ts`, barrel-exported so
|
|
81
|
+
`homeflare-network`'s own import-layer guard can reuse it instead of re-implementing the same
|
|
82
|
+
wrap), installed in `unifiHandlers`'s `withCredentials`, stops the same thing one layer lower, AT
|
|
83
|
+
THE WIRE: it wraps whatever `HttpClient` the caller provides so ANY non-`GET` method dies with
|
|
84
|
+
`UnifiNonGetRequest` (its message carries the request PATH only, never the host — a cloud
|
|
85
|
+
connector's base URL embeds the account's Console ID) before the request reaches the transport —
|
|
86
|
+
a backstop for a future resource file that, by mistake, called an SDK write operation directly.
|
|
87
|
+
`write-op-reference.test.ts` is the matching static check: it harvests the SDK's real write-op
|
|
88
|
+
export names straight from its own service modules (every name starting
|
|
89
|
+
`create`/`update`/`delete`/`patch`/`execute`/`remove`/`adopt`) and fails any file that mentions
|
|
90
|
+
the SDK's package specifier and contains one of those names as a token anywhere — an identifier, a
|
|
91
|
+
bracket key, a destructured binding, a re-export or a dynamic-import property access all read the
|
|
92
|
+
same way, so one scan catches every syntax form without parsing which one it is.
|
|
93
|
+
`get-only-guard.test.ts` proves the wire guard's mechanism directly against a fake `HttpClient`,
|
|
94
|
+
AND that `unifiHandlers` actually installs it (a handler-level probe, not just the mechanism in
|
|
95
|
+
isolation — `network.test.ts`/`firewall-zone.test.ts` call the lower-level `unifiOperations(spec)`
|
|
96
|
+
directly and never exercise `withCredentials` at all).
|
|
97
|
+
|
|
98
|
+
## Pagination helper
|
|
99
|
+
|
|
100
|
+
`paginate.ts`'s `pageAll` is a CONSUMER-SIDE sequential offset pager for the SDK's list
|
|
101
|
+
operations (`getNetworksOverviewPage`, `getFirewallZones`, …), none of which paginate on their
|
|
102
|
+
own — no `smithy.api#paginated` trait exists in the pinned spec, and neither the SDK nor
|
|
103
|
+
`@distilled.cloud/core` gained one for this. It stops on the response's `totalCount`, never on
|
|
104
|
+
`data.length < limit` (a page can legitimately come back short of the requested limit without
|
|
105
|
+
being the last page). No family here has enough rows to need it yet; it exists ahead of
|
|
106
|
+
`Unifi.FirewallPolicy` (hundreds of rows on a real console), which does.
|
|
107
|
+
|
|
108
|
+
It also fails with a typed `UnifiPaginationInconsistent` — never silently returns a wrong row
|
|
109
|
+
set — the moment a paged response contradicts itself: a page's echoed `offset` doesn't match what
|
|
110
|
+
was requested, `totalCount` changes between calls, the final row count disagrees with
|
|
111
|
+
`totalCount - startOffset`, or the walk exceeds a hard page ceiling without ever converging. Each
|
|
112
|
+
is a vendor-data condition, not a defect (distilled-doctrine): the caller may legitimately want to
|
|
113
|
+
retry or report on one, so it is an `Effect.fail`, not an `Effect.die`.
|
|
114
|
+
|
|
115
|
+
## Field-level drift
|
|
116
|
+
|
|
117
|
+
`network.ts`/`firewall-zone.ts` each export a pure `driftOf(live, props)` (`network-drift.ts`,
|
|
118
|
+
`firewall-zone-form.ts`). Both `driftOf` and `matches` are now built on the SAME `fieldDrift` list
|
|
119
|
+
(`drift.ts`'s `makeDriftOf` framework) — `matches` is exactly `fieldDrift(...).length === 0`, not
|
|
120
|
+
a second hand-written comparison kept in sync by hand, so the two cannot quietly disagree (a
|
|
121
|
+
red-team mutant that deleted a `fieldDrift` entry outright, 2026-09-26, is what found the previous
|
|
122
|
+
version's gap). Where `matches` collapses a comparison to one boolean, `driftOf` returns one entry
|
|
123
|
+
per field that actually differs, each carrying its own live and declared value — the shape a
|
|
124
|
+
future pre-import drift check (`homeflare-network`) needs to show WHICH fields changed before
|
|
125
|
+
overwriting a committed declaration.
|
|
126
|
+
|
|
77
127
|
## The declaration renderer
|
|
78
128
|
|
|
79
129
|
`declareNetwork(live, siteId)` and `declareFirewallZone(live, siteId)` are pure functions: given
|
|
@@ -136,3 +186,9 @@ delete it cannot perform is moot. No `Effect.orDie`/`Effect.die` anywhere in the
|
|
|
136
186
|
attributed, declared or compared. Both declaration renderers and both `matches` use
|
|
137
187
|
`value == null` / `deepEqual(..., { stripNullish: true })` consistently, per `network-form.ts`'s
|
|
138
188
|
own header. No departure found — nothing in this family needed changing.
|
|
189
|
+
|
|
190
|
+
⚠️ **Correction (2026-09-26):** the line above is no longer true as written. `resource.ts` now
|
|
191
|
+
has exactly one `Effect.die` — `GetOnlyHttpClient`'s guard (see "Defense in depth" above) —
|
|
192
|
+
classified in its own comment per distilled-doctrine's "classify every die/orDie": it raises a
|
|
193
|
+
NEW defect for a failure mode outside every generated SDK operation's declared error union, never
|
|
194
|
+
converts an existing typed failure the way `orDie` would. Still no `Effect.orDie` anywhere.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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,10 @@
|
|
|
100
100
|
"types": "./dist/verify/index.d.ts",
|
|
101
101
|
"default": "./dist/verify/index.js"
|
|
102
102
|
},
|
|
103
|
+
"./telemetry": {
|
|
104
|
+
"types": "./dist/telemetry/index.d.ts",
|
|
105
|
+
"default": "./dist/telemetry/index.js"
|
|
106
|
+
},
|
|
103
107
|
"./discord": {
|
|
104
108
|
"types": "./dist/discord/index.d.ts",
|
|
105
109
|
"default": "./dist/discord/index.js"
|
|
@@ -127,7 +131,7 @@
|
|
|
127
131
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
128
132
|
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.3",
|
|
129
133
|
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.1",
|
|
130
|
-
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.
|
|
134
|
+
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.3.0"
|
|
131
135
|
},
|
|
132
136
|
"peerDependencies": {
|
|
133
137
|
"@distilled.cloud/argocd": "1.0.0-rc.12",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* alongside the SDK's own `NotFound`. Proves the two stay distinct: only the SDK's tag folds.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
7
8
|
import * as organization from '@distilled.cloud/forgejo/organization';
|
|
8
9
|
import * as Effect from 'effect/Effect';
|
|
9
10
|
import { fakeFailure, fakeForgejo, fakeForgejoLayer } from './fake-forgejo.ts';
|
|
@@ -95,3 +96,16 @@ describe('Forgejo.TeamMember spec — the actual exported production functions',
|
|
|
95
96
|
expect(await readThrough(fake.fetch)).toBeUndefined();
|
|
96
97
|
});
|
|
97
98
|
});
|
|
99
|
+
|
|
100
|
+
test('ForgejoTeamMember declares defaultRemovalPolicy: retain on the Resource() call itself', () => {
|
|
101
|
+
// ⛔ Anchored on the `Resource<ForgejoTeamMember>(...)` CALL, not on the substring appearing
|
|
102
|
+
// anywhere in the file — team-member.ts's own header comment also says the phrase in prose, so
|
|
103
|
+
// a plain `source.toContain(...)` (database-refusals.test.ts's simpler version, safe there
|
|
104
|
+
// because database.ts's header never repeats the phrase) would pass here even if the option
|
|
105
|
+
// were dropped from the actual call. mini PR 82's red team (970e8be) had to pipe
|
|
106
|
+
// `RemovalPolicy.retain()` onto every call site because the kit resource had no default; this
|
|
107
|
+
// proves the kit default carries the fix so a caller no longer has to remember its own.
|
|
108
|
+
const source = readFileSync(new URL('./team-member.ts', import.meta.url), 'utf8');
|
|
109
|
+
const call = source.match(/Resource<ForgejoTeamMember>\([\s\S]*?\);/);
|
|
110
|
+
expect(call?.[0]).toContain("defaultRemovalPolicy: 'retain'");
|
|
111
|
+
});
|
|
@@ -22,6 +22,31 @@
|
|
|
22
22
|
* cannot create a team.
|
|
23
23
|
*
|
|
24
24
|
* ⛔ TOKEN NEEDS `write:organization` TO ADD OR REMOVE, and an identity allowed to manage the team.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ `defaultRemovalPolicy: 'retain'` — FOUND 2026-09-25 by reading the source, not by measuring
|
|
27
|
+
* anything (mini PR 82's red team, `homeflare-mini` 970e8be): before this, `ForgejoRepository`/
|
|
28
|
+
* `ForgejoOrgLabel` defaulted to retain in the kit itself but this family did not, so alchemy's
|
|
29
|
+
* own engine fallback (`destroy`) applied to it — every caller had to remember its own
|
|
30
|
+
* `.pipe(RemovalPolicy.retain())`, the way mini's `src/forgejo/declare.ts` did for
|
|
31
|
+
* `forgejo-provision`'s row. Dropping a `TeamMember` declaration — toggling a feature gate off,
|
|
32
|
+
* renaming the resource id — must not silently call `organization.orgRemoveTeamMember` and
|
|
33
|
+
* revoke a real membership; that is the same "removal from the stack is not removal from
|
|
34
|
+
* Forgejo" posture `ForgejoOrgLabel` and `ForgejoBranchProtection` already take, this family
|
|
35
|
+
* exists to attach a stack's identity to a team it never creates or destroys (`teamIdOf` below
|
|
36
|
+
* only ever locates the team, never manages it), and the same trade-off alchemy's own
|
|
37
|
+
* `GitHub.Collaborator` and `GitHub.TeamAccess` make for the identical reason — "preventing
|
|
38
|
+
* accidental lockout" (`GitHub/Collaborator.ts:127`, `GitHub/TeamAccess.ts:145` in the pinned
|
|
39
|
+
* alchemy source). The trade-off, stated plainly: removing a declaration no longer revokes
|
|
40
|
+
* access on its own — the plan shows that row as `orphaned`, not `delete`. Opt into a real
|
|
41
|
+
* revoke with `.pipe(RemovalPolicy.destroy())`; `destroy` below is fully implemented.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ THE DEFAULT ONLY PROTECTS A ROW ALREADY IN STATE ONCE THIS RELEASE IS DEPLOYED. Alchemy's
|
|
44
|
+
* plan reads `removalPolicy` off the *persisted row* (`Plan.ts`'s delete-node builder), not off
|
|
45
|
+
* this default; the default only reaches an existing row's state on a deploy where the resource
|
|
46
|
+
* plans as a no-op (`Apply.ts`'s noop path rewrites it and logs "removal policy destroy →
|
|
47
|
+
* retain"). A consumer that bumps to this release and drops a `TeamMember` declaration in the
|
|
48
|
+
* SAME deploy still gets `delete`, not `orphaned` — see the changeset for the two-deploy shape
|
|
49
|
+
* this implies for both adopting retain and opting back into destroy.
|
|
25
50
|
*/
|
|
26
51
|
import { Resource } from 'alchemy';
|
|
27
52
|
import * as Provider from 'alchemy/Provider';
|
|
@@ -52,7 +77,9 @@ export interface ForgejoTeamMember extends Resource<
|
|
|
52
77
|
ForgejoRequirements
|
|
53
78
|
> {}
|
|
54
79
|
|
|
55
|
-
export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember'
|
|
80
|
+
export const ForgejoTeamMember = Resource<ForgejoTeamMember>('Forgejo.TeamMember', {
|
|
81
|
+
defaultRemovalPolicy: 'retain',
|
|
82
|
+
});
|
|
56
83
|
|
|
57
84
|
export class ForgejoTeamNotFoundError extends Data.TaggedError('ForgejoTeamNotFoundError')<{
|
|
58
85
|
readonly message: string;
|
|
@@ -80,6 +80,7 @@ describe('every create this package makes, and what the vendor requires of it',
|
|
|
80
80
|
'pve:POST /nodes/{node}/lxc': ['ostemplate', 'vmid'],
|
|
81
81
|
'pve:POST /nodes/{node}/network': ['iface', 'type'],
|
|
82
82
|
'pve:POST /nodes/{node}/qemu': ['vmid'],
|
|
83
|
+
'pve:POST /nodes/{node}/storage/{storage}/download-url': ['content', 'filename', 'url'],
|
|
83
84
|
'pve:POST /pools': ['poolid'],
|
|
84
85
|
'pve:POST /storage': ['storage', 'type'],
|
|
85
86
|
});
|
|
@@ -22,6 +22,7 @@ import { PVE_NODES_DISKS_CONSTRAINTS } from './pve-nodes-disks.ts';
|
|
|
22
22
|
import { PVE_NODES_LXC_CONSTRAINTS } from './pve-nodes-lxc.ts';
|
|
23
23
|
import { PVE_NODES_NETWORK_CONSTRAINTS } from './pve-nodes-network.ts';
|
|
24
24
|
import { PVE_NODES_QEMU_CONSTRAINTS } from './pve-nodes-qemu.ts';
|
|
25
|
+
import { PVE_NODES_STORAGE_CONSTRAINTS } from './pve-nodes-storage.ts';
|
|
25
26
|
import { PVE_POOLS_CONSTRAINTS } from './pve-pools.ts';
|
|
26
27
|
import { PVE_STORAGE_CONSTRAINTS } from './pve-storage.ts';
|
|
27
28
|
import { PBS_CONFIG_CONSTRAINTS } from './pbs-config.ts';
|
|
@@ -41,6 +42,7 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
|
|
|
41
42
|
...PVE_NODES_LXC_CONSTRAINTS,
|
|
42
43
|
...PVE_NODES_NETWORK_CONSTRAINTS,
|
|
43
44
|
...PVE_NODES_QEMU_CONSTRAINTS,
|
|
45
|
+
...PVE_NODES_STORAGE_CONSTRAINTS,
|
|
44
46
|
...PVE_POOLS_CONSTRAINTS,
|
|
45
47
|
...PVE_STORAGE_CONSTRAINTS,
|
|
46
48
|
...PBS_CONFIG_CONSTRAINTS,
|
|
@@ -52,4 +54,4 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
|
|
|
52
54
|
* ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while
|
|
53
55
|
* changing a 128 to a 129 by hand is. `tests/schema-manifest.test.ts` recomputes it.
|
|
54
56
|
*/
|
|
55
|
-
export const PROXMOX_CONSTRAINTS_DIGEST = '
|
|
57
|
+
export const PROXMOX_CONSTRAINTS_DIGEST = '8b6745a471839974';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|