@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/proxmox/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAa7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAStE,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;
|
|
1
|
+
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/proxmox/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAa7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAStE,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAwBjE,eAAO,MAAM,mBAAmB,iIA+E7B,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Storage`'s WRITE side: the locator bag, the create/update forms, and the translation
|
|
3
|
+
* into distilled's own typed request shape. Split out of storage-wire.ts (2026-09-24) to keep
|
|
4
|
+
* both files under the 250-line cap — the api-token.ts/api-token-form.ts seam, same reasoning.
|
|
5
|
+
*/
|
|
6
|
+
import type * as storage from '@distilled.cloud/proxmox/storage';
|
|
7
|
+
import type { StorageProps } from './storage.ts';
|
|
8
|
+
export declare const STORAGE_CREATE = "pve:POST /storage";
|
|
9
|
+
export declare const STORAGE_UPDATE = "pve:PUT /storage/{storage}";
|
|
10
|
+
/**
|
|
11
|
+
* The create-time, plugin-specific half of a storage: `dir` wants `path`, `nfs` `server`+`export`,
|
|
12
|
+
* `cifs` `server`+`share`, `pbs` `server`+`datastore`, `rbd` `pool`+`monhost`, `lvmthin`
|
|
13
|
+
* `vgname`+`thinpool`, `zfspool` `pool`. A bag rather than two dozen named fields, because PVE
|
|
14
|
+
* ships about that many plugins and each brings its own locator. ⚠️ Never diffed — see `matches`
|
|
15
|
+
* (storage-wire.ts).
|
|
16
|
+
*
|
|
17
|
+
* ⛔ `password`, `keyring` AND `encryption-key` ARE `never` ON PURPOSE — see the ⛔ in storage.ts's
|
|
18
|
+
* header. POSITIVE-CONTROLLED rather than assumed: all three are a compile error, `TS2322: Type
|
|
19
|
+
* 'string' is not assignable to type 'undefined'`.
|
|
20
|
+
* ⚠️ EVERY KEY IN THIS BAG GOES THROUGH `underscored` (below) BEFORE IT REACHES DISTILLED — see
|
|
21
|
+
* that function's own header. A caller writes PVE's own field names here (`fs-name`, matching
|
|
22
|
+
* `pvesm`/pve-docs), never distilled's renamed ones.
|
|
23
|
+
*/
|
|
24
|
+
export type StorageLocator = Record<string, string> & {
|
|
25
|
+
password?: never;
|
|
26
|
+
keyring?: never;
|
|
27
|
+
'encryption-key'?: never;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* ⚠️ THE BAG IS SPREAD FIRST SO NOTHING IN IT CAN SHADOW A FIELD THIS RESOURCE MANAGES. A stray
|
|
31
|
+
* `storage` or `type` in a locator would otherwise rename the object being created, and PVE
|
|
32
|
+
* would build the wrong thing under a name Alchemy then records as the declared one.
|
|
33
|
+
* ★ HYPHENATED, THE WIRE SHAPE — `guardWrite` (storage.ts) checks this form directly; see
|
|
34
|
+
* `underscored`'s header for why translating it here would blind that check.
|
|
35
|
+
*/
|
|
36
|
+
export declare const createForm: (props: StorageProps) => Record<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* ⚠️ `type` AND THE LOCATOR ARE NEVER SENT ON UPDATE — PVE's PUT refuses the create-only fields
|
|
39
|
+
* (`PutStorageRequest` has no `type`, and the plugin locator fields it does carry are for a
|
|
40
|
+
* different purpose: moving an existing volume, not re-pointing the definition), and comparing
|
|
41
|
+
* either could only plan an update that no write can apply: a plan that reports work forever.
|
|
42
|
+
*/
|
|
43
|
+
export declare const updateForm: (props: StorageProps) => Record<string, string>;
|
|
44
|
+
/**
|
|
45
|
+
* The actual `createStorage` call body — `createForm`, translated once. See `underscored`.
|
|
46
|
+
* ⚠️ `as unknown as` — same load-bearing cast as `locatorForm`'s own ⚠️: a plain
|
|
47
|
+
* `Record<string,string>` does not structurally overlap a named-field interface enough for
|
|
48
|
+
* TypeScript to accept a direct cast, even though every field it carries fits one of the
|
|
49
|
+
* interface's own (all-optional-but-two) properties.
|
|
50
|
+
*/
|
|
51
|
+
export declare const toDistilledCreate: (props: StorageProps) => storage.CreateStorageRequest;
|
|
52
|
+
/** The actual `putStorage` call body — `updateForm`, translated once. See `toDistilledCreate`. */
|
|
53
|
+
export declare const toDistilledUpdate: (props: StorageProps) => storage.PutStorageRequest;
|
|
54
|
+
//# sourceMappingURL=storage-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-form.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage-form.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,KAAK,OAAO,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGjD,eAAO,MAAM,cAAc,sBAAsB,CAAC;AAClD,eAAO,MAAM,cAAc,+BAA+B,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IACpD,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,gBAAgB,CAAC,EAAE,KAAK,CAAC;CAC1B,CAAC;AA4EF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAW,YAAY,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAKpE,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,UAAW,YAAY,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAGpE,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,UAAW,YAAY,KAAG,OAAO,CAAC,oBACW,CAAC;AAE5E,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,UAAW,YAAY,KAAG,OAAO,CAAC,iBACQ,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Effect from 'effect/Effect';
|
|
2
|
+
import type { StorageProps } from './storage.ts';
|
|
3
|
+
import { UNREADABLE, type Unreadable } from './unreadable-read.ts';
|
|
4
|
+
export interface StorageAttributes {
|
|
5
|
+
storage: string;
|
|
6
|
+
/** Reported so a plan can show what it points at; never diffed — storage.ts's `type` doc. */
|
|
7
|
+
type: string;
|
|
8
|
+
content: string;
|
|
9
|
+
nodes: string;
|
|
10
|
+
disable: boolean;
|
|
11
|
+
shared: boolean;
|
|
12
|
+
preallocation: string;
|
|
13
|
+
'prune-backups': string;
|
|
14
|
+
comment: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const sameList: (declared: string | undefined, live: string) => boolean;
|
|
17
|
+
export declare const same: <T>(declared: T | undefined, live: T) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* ★ Default `read` role WOULD READ 403 — see storage.ts's header on why this family reads with
|
|
20
|
+
* `provision`, the same as acl.ts and unlike group.ts/user.ts.
|
|
21
|
+
* ⛔ `{ storage: props.storage }`, NEVER THE WHOLE `props` — user.ts/group.ts's own ⛔, the
|
|
22
|
+
* kit 0.31.1 regression: `GetStorageRequest`'s schema declares only `storage` (a path label);
|
|
23
|
+
* any OTHER key on the object passed to `storage.getStorage` is treated by distilled's
|
|
24
|
+
* `buildRequest` as an "unknown key" and JSON-encoded onto this bodyless GET, which a stricter
|
|
25
|
+
* fetch client refuses outright.
|
|
26
|
+
* ⛔ A MISSING STORAGE IS A 500, NOT A 404 — MEASURED against the live cluster (TB4, 2026-09-24,
|
|
27
|
+
* admin lane, read-only probe): `GET /storage/hf-measure-nonexistent-probe` answers
|
|
28
|
+
* `{"message":"storage 'hf-measure-nonexistent-probe' does not exist\n","data":null}` at HTTP
|
|
29
|
+
* 500 — the SAME shape as user.ts's and group.ts's own measured 500, not the clean
|
|
30
|
+
* `{"data":null}` at 200 this file's pre-migration comment assumed for every read failure
|
|
31
|
+
* (client.ts's `pveOperations.read` could not tell the two apart either way, folding both into
|
|
32
|
+
* "absent"). `readStorage` below (used by `read`/`reconcile`) still folds it, exactly as
|
|
33
|
+
* `readUser`/`readGroup` do, so a brand-new declaration can still be created; `readStorageOrFail`
|
|
34
|
+
* (used by `diff`) does not.
|
|
35
|
+
*/
|
|
36
|
+
export declare const readStorageOrFail: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
37
|
+
/**
|
|
38
|
+
* ⛔ FOLDS A GENUINE READ FAILURE TO `undefined` TOO, AND ONLY `read`/`reconcile` MAY USE IT —
|
|
39
|
+
* user.ts's `readUser` has the full reasoning. Safe here because a wrongful fold costs at most a
|
|
40
|
+
* redundant `createStorage` POST, which PVE refuses loudly ("storage ID already exists") rather
|
|
41
|
+
* than silently corrupting anything.
|
|
42
|
+
* ⛔ `storage.ts`'s `diff` does **NOT** use this — the SAME bug class as the credential denial fix:
|
|
43
|
+
* folding a TRANSIENT failure into "absent" at PLAN TIME is what forced a false `update` with
|
|
44
|
+
* nothing compared (the ORIGINAL cries-wolf bug this family shipped with, per unreadable-read.ts's
|
|
45
|
+
* own header). `diff` calls `readStorageOrFail` directly, so a genuine failure there propagates
|
|
46
|
+
* and fails the whole plan loudly instead.
|
|
47
|
+
*/
|
|
48
|
+
export declare const readStorage: (props: StorageProps) => Effect.Effect<typeof UNREADABLE | StorageAttributes | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
49
|
+
/** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
|
|
50
|
+
export declare const dropUnreadable: (live: StorageAttributes | Unreadable | undefined) => StorageAttributes | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* ⚠️ `type` IS NEVER COMPARED — CREATE-ONLY (`storage.ts`'s own `StorageProps.type` doc), so a
|
|
53
|
+
* changed declaration plans `noop` on it, same as before the migration.
|
|
54
|
+
*/
|
|
55
|
+
export declare const matches: (attributes: StorageAttributes, props: StorageProps) => boolean;
|
|
56
|
+
//# sourceMappingURL=storage-wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage-wire.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAmBD,eAAO,MAAM,QAAQ,aAAc,MAAM,GAAG,SAAS,QAAQ,MAAM,YACN,CAAC;AAE9D,eAAO,MAAM,IAAI,GAAI,CAAC,YAAY,CAAC,GAAG,SAAS,QAAQ,CAAC,YACX,CAAC;AAwB9C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,iBAAiB,UAAW,YAAY,m2BAGyC,CAAC;AAE/F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,UAAW,YAAY,kIACuB,CAAC;AAEvE,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,iBAAiB,GAAG,UAAU,GAAG,SAAS,kCACvC,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,iBAAiB,SAAS,YAAY,YAOjC,CAAC"}
|
|
@@ -12,19 +12,48 @@
|
|
|
12
12
|
* ⛔ ONE PRIVILEGE COVERS THIS WHOLE FAMILY AND IT IS `Datastore.Allocate` ON `/storage`. Create,
|
|
13
13
|
* update, delete AND the single-object read all check it — `Datastore.Audit` is not enough for
|
|
14
14
|
* any of them. MEASURED: `GET /storage/local` under the auditor-scoped `read` lease answers 403
|
|
15
|
-
* "Permission check failed (/storage/local, Datastore.Allocate)".
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
15
|
+
* "Permission check failed (/storage/local, Datastore.Allocate)". So this family reads with
|
|
16
|
+
* `provision`, same as acl.ts and unlike group.ts/user.ts — `provision-baseline.ts` carries
|
|
17
|
+
* `Datastore.Allocate`.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ ON A LANE WHOSE CREDENTIAL CANNOT MINT `provision` (the agent's `read`-role lane), THIS
|
|
20
|
+
* FAMILY SHIPPED WITH THE ORIGINAL CRIES-WOLF BUG — measured live, 2026-09-24: `bun run plan`
|
|
21
|
+
* showed all 5 storages (`cephfs-tb4`, `cephtb4`, `local`, `local-zfs`, `pbs`) as `update`, with
|
|
22
|
+
* no warning, because `client.ts`'s `pveOperations.read` folded the REFUSED mint into `undefined`
|
|
23
|
+
* the same way it folds a genuine absence — `unreadable-read.ts`'s own header names this family
|
|
24
|
+
* by name as one of the two the fix was written for. Migrated here the way acl.ts/group.ts/
|
|
25
|
+
* user.ts were (kit 0.31.1/0.31.2): a refused `provision` mint now reports `noop` with a logged
|
|
26
|
+
* warning instead of forcing `update` with nothing compared.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ A MISSING STORAGE IS A 500, NOT A 404 — MEASURED against the live cluster (TB4, 2026-09-24,
|
|
29
|
+
* admin lane, read-only `GET /storage/hf-measure-nonexistent-probe`): `{"message":"storage
|
|
30
|
+
* '...' does not exist\n"}` at HTTP 500. `storage-wire.ts`'s `readStorageOrFail`/`readStorage`
|
|
31
|
+
* carry the same dual-path split as user.ts/group.ts for exactly this reason — see that file.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ `maxfiles` IS GONE — A MEASURED `@distilled.cloud/proxmox` SDK GAP, NOT A SCOPE TRIM. The
|
|
34
|
+
* vendor's generated `CreateStorageRequest`/`PutStorageRequest` (storage-wire.ts's `mutable`)
|
|
35
|
+
* have no `maxfiles` field at all — grepped across the whole package, not just this endpoint —
|
|
36
|
+
* where `prune-backups`, its documented replacement, is present and correctly wired
|
|
37
|
+
* (`T.Body("prune-backups")` on the generated `prune_backups` property). Silently dropping a
|
|
38
|
+
* declared `maxfiles` value would have been the same class of bug this whole migration exists to
|
|
39
|
+
* close: a write that looks like it landed and did not. So `maxfiles?: never` below, the same
|
|
40
|
+
* COMPILE-ERROR treatment `StorageLocator`'s `password`/`keyring`/`encryption-key` already get —
|
|
41
|
+
* a declaration that sets it is refused before a plan ever runs, not silently ignored by one.
|
|
42
|
+
* None of this cluster's 5 live storages set it (measured, live response bodies below). If a
|
|
43
|
+
* consumer genuinely needs it, the fix belongs in `@distilled.cloud/proxmox`'s generator, not a
|
|
44
|
+
* hand-rolled bypass here — see the provider-walk-down doctrine on vendor gaps.
|
|
45
|
+
*
|
|
46
|
+
* ⛔ EVERY HYPHENATED PVE FIELD NAME BECOMES AN UNDERSCORE IN DISTILLED'S GENERATED TYPESCRIPT —
|
|
47
|
+
* MEASURED across the WHOLE generated `CreateStorageRequest`/`PutStorageRequest`, not one field:
|
|
48
|
+
* `prune-backups` -> `prune_backups`, `fs-name` -> `fs_name`, `max-protected-backups` ->
|
|
49
|
+
* `max_protected_backups`, and so on for every plugin field `StorageLocator`'s free-form bag can
|
|
50
|
+
* carry (`cephfs-tb4`'s own live locator uses `fs-name`, measured below). No other family
|
|
51
|
+
* migrated so far has a hyphenated field at all, so this is new here. `storage-wire.ts`'s
|
|
52
|
+
* `underscored` translates a form BUILT with PVE's own names into distilled's shape, but ONLY
|
|
53
|
+
* for the actual SDK call (`toDistilledCreate`/`toDistilledUpdate`) — never for the form
|
|
54
|
+
* `guardWrite` checks (`createForm`/`updateForm`), which the generated vendor-constraint tables
|
|
55
|
+
* key by PVE's own hyphenated names. Running the translation before that check would make it
|
|
56
|
+
* silently stop inspecting the very fields it exists to catch.
|
|
28
57
|
*
|
|
29
58
|
* ⛔ THERE IS NO `password` PROP AND THERE MUST NEVER BE ONE. PBS and CIFS storages take one at
|
|
30
59
|
* create time, and Alchemy persists attributes UNENCRYPTED — a secret in props is one careless
|
|
@@ -33,25 +62,20 @@
|
|
|
33
62
|
* a COMPILE ERROR rather than a leak somebody finds in a backup months later. Set it once out of
|
|
34
63
|
* band (`pvesm set <id> --password`); PVE never returns it on read, so nothing here could diff it
|
|
35
64
|
* anyway.
|
|
65
|
+
*
|
|
66
|
+
* ★ MIGRATED OFF `client.ts`'s generic `pve()`/`pveHandlers` (`resource.ts`) ONTO
|
|
67
|
+
* `@distilled.cloud/proxmox`'s typed `storage.getStorage`/`createStorage`/`putStorage`/
|
|
68
|
+
* `deleteStorage` (2026-09-24, decision 43's proxmox walk-down, nodes/storage sub-area, first
|
|
69
|
+
* resource). `distilled-pve.ts`'s `runPve` replaces `pve()`; the cries-wolf fix is wired the
|
|
70
|
+
* same way as acl.ts/group.ts/user.ts.
|
|
36
71
|
*/
|
|
37
72
|
import { Resource } from 'alchemy';
|
|
38
73
|
import * as Provider from 'alchemy/Provider';
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* ships about that many plugins and each brings its own locator. ⚠️ Never diffed — see `matches`.
|
|
45
|
-
*
|
|
46
|
-
* ⛔ `password`, `keyring` AND `encryption-key` ARE `never` ON PURPOSE — see the ⛔ in the header.
|
|
47
|
-
* POSITIVE-CONTROLLED rather than assumed: all three are a compile error, `TS2322: Type 'string'
|
|
48
|
-
* is not assignable to type 'undefined'`.
|
|
49
|
-
*/
|
|
50
|
-
export type StorageLocator = Record<string, string> & {
|
|
51
|
-
password?: never;
|
|
52
|
-
keyring?: never;
|
|
53
|
-
'encryption-key'?: never;
|
|
54
|
-
};
|
|
74
|
+
import type { PveRequirements, WithTarget } from './resource-spec.ts';
|
|
75
|
+
import type { StorageAttributes } from './storage-wire.ts';
|
|
76
|
+
import type { StorageLocator } from './storage-form.ts';
|
|
77
|
+
/** Re-exported so a caller of this file still finds them here, unchanged by the split below. */
|
|
78
|
+
export type { StorageAttributes, StorageLocator };
|
|
55
79
|
export interface StorageProps extends WithTarget {
|
|
56
80
|
/** PVE's primary key, cluster-wide — the string an LXC's `storage` and `ostemplate` name. */
|
|
57
81
|
storage: string;
|
|
@@ -81,41 +105,12 @@ export interface StorageProps extends WithTarget {
|
|
|
81
105
|
*/
|
|
82
106
|
'prune-backups'?: string;
|
|
83
107
|
comment?: string;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
* other. `0` means UNLIMITED to PVE rather than "unset", hence the `-1` in the attribute.
|
|
87
|
-
*/
|
|
88
|
-
maxfiles?: number;
|
|
89
|
-
}
|
|
90
|
-
export interface StorageAttributes {
|
|
91
|
-
storage: string;
|
|
92
|
-
/** Reported so a plan can show what it points at; never diffed — see `type` above. */
|
|
93
|
-
type: string;
|
|
94
|
-
content: string;
|
|
95
|
-
nodes: string;
|
|
96
|
-
disable: boolean;
|
|
97
|
-
shared: boolean;
|
|
98
|
-
preallocation: string;
|
|
99
|
-
'prune-backups': string;
|
|
100
|
-
comment: string;
|
|
101
|
-
/** ⚠️ `-1` means "no maxfiles in storage.cfg". `0` is a real setting meaning unlimited. */
|
|
102
|
-
maxfiles: number;
|
|
108
|
+
/** ⛔ REMOVED — a measured `@distilled.cloud/proxmox` gap, not a scope trim. See the header. */
|
|
109
|
+
maxfiles?: never;
|
|
103
110
|
}
|
|
104
111
|
export interface ProxmoxStorage extends Resource<'Proxmox.Storage', StorageProps, StorageAttributes, never, PveRequirements> {
|
|
105
112
|
}
|
|
106
113
|
/** ★ `retain` by default — a storage holding volumes cannot be rebuilt. See the ★ in resource.ts. */
|
|
107
114
|
export declare const ProxmoxStorage: import("alchemy").ResourceClass<ProxmoxStorage>;
|
|
108
|
-
/**
|
|
109
|
-
* ⛔ EMPTY, AND HERE IT MATTERS MORE THAN ANYWHERE ELSE IN THIS PACKAGE. `GET /storage`
|
|
110
|
-
* returns every definition on the cluster, `local` and `local-lvm`/`local-zfs` included:
|
|
111
|
-
* the installer's own, that every guest's rootfs sits on. Adopting those would put
|
|
112
|
-
* Alchemy one `delete` away from the cluster's disks. Adoption is explicit, always.
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
* ⚠️ THIS REMOVES THE DEFINITION, NOT THE DATA. PVE drops the section from storage.cfg and
|
|
116
|
-
* leaves the volumes on disk untouched — so the loss is not the bytes, it is that guests
|
|
117
|
-
* whose config still names this storage can no longer resolve their own disks.
|
|
118
|
-
|
|
119
|
-
*/
|
|
120
115
|
export declare const ProxmoxStorageProvider: () => import("effect/Layer").Layer<Provider.Provider<ProxmoxStorage>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
121
116
|
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/proxmox/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAYtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,gGAAgG;AAChG,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,wFAAwF;IACxF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yFAAyF;IACzF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,qGAAqG;AACrG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAEH,eAAO,MAAM,sBAAsB,oIAgGhC,CAAC"}
|
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `Proxmox.User`'s wire shape: PVE's create/update forms
|
|
3
|
-
*
|
|
4
|
-
* under the 250-line cap — the api-token.ts/api-token-form.ts seam.
|
|
2
|
+
* `Proxmox.User`'s wire shape: PVE's create/update forms, how a live item becomes attributes,
|
|
3
|
+
* and the read itself. Split out of user.ts (2026-09-24, the distilled migration) to keep that
|
|
4
|
+
* file under the 250-line cap — the api-token.ts/api-token-form.ts seam.
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import * as access from '@distilled.cloud/proxmox/access';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
7
8
|
import type { UserAttributes, UserProps } from './user.ts';
|
|
9
|
+
import { UNREADABLE, type Unreadable } from './unreadable-read.ts';
|
|
8
10
|
export declare const USER_CREATE = "pve:POST /access/users";
|
|
9
11
|
export declare const USER_UPDATE = "pve:PUT /access/users/{userid}";
|
|
10
12
|
export declare const createForm: (props: UserProps) => access.CreateAccessUserRequest;
|
|
11
13
|
export declare const updateForm: (props: UserProps) => access.PutAccessUserRequest;
|
|
12
|
-
export declare const attributesOf: (live: access.GetAccessUserResponse, props: UserProps) => UserAttributes;
|
|
13
14
|
/**
|
|
14
15
|
* ⚠️ `tokens` IS NOT COMPARED, and neither is anything create-only. What is compared is exactly
|
|
15
16
|
* what a PUT can put back; a field PVE reports but will not accept is drift nobody can fix.
|
|
16
17
|
*/
|
|
17
18
|
export declare const matches: (attributes: UserAttributes, props: UserProps) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* ★ Default `read` role for this family — only the write side needs `provision`.
|
|
21
|
+
* ⛔ `{ userid: props.userid }`, NEVER THE WHOLE `props` — MEASURED 2026-09-24, the regression in
|
|
22
|
+
* kit 0.31.1. `GetAccessUserRequest`'s schema declares only `userid` (a path label); any OTHER
|
|
23
|
+
* key on the object passed to `access.getAccessUser` — `target`, `comment`, every field this
|
|
24
|
+
* family's own props carry — is treated by distilled's `buildRequest` as an "unknown key" and
|
|
25
|
+
* JSON-encoded onto the request as a BODY, on every one of these bodyless GETs. A stricter
|
|
26
|
+
* fetch client than a bare `bun run` of this file refuses a GET carrying any body at all
|
|
27
|
+
* (`TypeError [ERR_INVALID_ARG_VALUE]: fetch() request with GET/HEAD method cannot have body`),
|
|
28
|
+
* which `runPveWith` retries across every member and exhausts identically —
|
|
29
|
+
* `PveClusterExhausted`, then silently folded to "absent" by the old `orElseSucceed`, forcing a
|
|
30
|
+
* false `update` on every account.
|
|
31
|
+
*/
|
|
32
|
+
export declare const readUserOrFail: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes, import("@distilled.cloud/caddy").BadGateway | import("@distilled.cloud/caddy").BadRequest | import("@distilled.cloud/caddy").ConfigError | Error | import("@distilled.cloud/caddy").GatewayTimeout | import("effect/unstable/http/HttpClientError").HttpClientError | import("@distilled.cloud/caddy").InternalServerError | import("@distilled.cloud/proxmox/Errors").ParameterVerificationFailed | import("@distilled.cloud/proxmox/Errors").ProxmoxParseError | import("./distilled-pve.ts").PveClusterExhausted | import("@distilled.cloud/caddy").ServiceUnavailable | import("effect/Cause").TimeoutError | import("@distilled.cloud/caddy").TooManyRequests | import("@distilled.cloud/caddy").Unauthorized | import("@distilled.cloud/proxmox/Errors").UnknownProxmoxError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
33
|
+
/**
|
|
34
|
+
* ⛔ FOLDS A GENUINE READ FAILURE TO `undefined` TOO, AND ONLY `read`/`reconcile` MAY USE IT. A
|
|
35
|
+
* missing user is a thrown 500 (user.ts's own header), so `reconcile`'s create branch NEEDS
|
|
36
|
+
* "absent" from a failure to ever run — without this fold, a brand-new account could never be
|
|
37
|
+
* created. Safe here because a wrongful fold costs at most a redundant `createAccessUser` POST,
|
|
38
|
+
* which PVE refuses loudly rather than silently corrupting anything.
|
|
39
|
+
* ⛔ `user.ts`'s `diff` does **NOT** use this — the SAME bug class as the credential denial fix:
|
|
40
|
+
* folding a TRANSIENT failure into "absent" at PLAN TIME is what forced a false `update` with
|
|
41
|
+
* nothing compared. `diff` calls `readUserOrFail` directly, so a genuine failure there
|
|
42
|
+
* propagates and fails the whole plan loudly instead.
|
|
43
|
+
*/
|
|
44
|
+
export declare const readUser: (props: UserProps) => Effect.Effect<typeof UNREADABLE | UserAttributes | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
45
|
+
/** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
|
|
46
|
+
export declare const dropUnreadable: (live: UserAttributes | Unreadable | undefined) => UserAttributes | undefined;
|
|
18
47
|
//# sourceMappingURL=user-wire.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/user-wire.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,KAAK,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"user-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/user-wire.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,MAAM,MAAM,iCAAiC,CAAC;AAC1D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAGrF,eAAO,MAAM,WAAW,2BAA2B,CAAC;AACpD,eAAO,MAAM,WAAW,mCAAmC,CAAC;AA2D5D,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,uBAGnD,CAAC;AAEH,eAAO,MAAM,UAAU,UAAW,SAAS,KAAG,MAAM,CAAC,oBAGnD,CAAC;AAoBH;;;GAGG;AACH,eAAO,MAAM,OAAO,eAAgB,cAAc,SAAS,SAAS,YAOF,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,UAAW,SAAS,g2BAG+C,CAAC;AAE/F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,UAAW,SAAS,+HAC0B,CAAC;AAEpE,gGAAgG;AAChG,eAAO,MAAM,cAAc,SAAU,cAAc,GAAG,UAAU,GAAG,SAAS,+BACpC,CAAC"}
|
package/dist/proxmox/user.d.ts
CHANGED
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
* user, so a role wide enough to edit the account can still be refused for one group.
|
|
34
34
|
* · `Sys.Audit` (or `User.Modify`) for the read, which the `read` role's auditor already has.
|
|
35
35
|
*
|
|
36
|
-
* ⚠️ A MISSING USER IS A 500, NOT A 404 —
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
36
|
+
* ⚠️ A MISSING USER IS A 500, NOT A 404 — MEASURED against the live cluster, "no such user
|
|
37
|
+
* ('x@pve')". distilled turns that into an `InternalServerError`, with no distinct "not found"
|
|
38
|
+
* tag to catch. `user-wire.ts`'s `readUser` (used by `read`/`reconcile`, where the create
|
|
39
|
+
* workflow needs "absent" from exactly this failure) still folds it; `readUserOrFail` (used by
|
|
40
|
+
* `diff` below) does not — see that function's own header for why folding it there was the
|
|
41
|
+
* cries-wolf bug all over again, just for a genuine PVE quirk instead of a permission denial.
|
|
42
42
|
*
|
|
43
43
|
* ⚠️ THE WIRE URL FOR THIS FAMILY CHANGED, THOUGH NOTHING A CALLER SEES DID. MEASURED
|
|
44
44
|
* (user.test.ts): distilled's `{userid}` label substitution percent-encodes it —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/proxmox/user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AActE,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ,CAC3C,cAAc,EACd,SAAS,EACT,cAAc,EACd,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,WAAW,8CAAwC,CAAC;AAEjE,eAAO,MAAM,mBAAmB,iIAgG7B,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory UniFi Network API for this family's tests — an in-memory `fetch` handed to
|
|
3
|
+
* Effect's real `FetchHttpClient` through its `Fetch` reference, mirroring `../netbox/fake-netbox.ts`
|
|
4
|
+
* (itself mirroring `../forgejo/fake-forgejo.ts`). Tests exercise distilled's REAL path assembly,
|
|
5
|
+
* JSON encode/decode and status→typed-error matching — nothing about that matching is
|
|
6
|
+
* re-implemented here, only the wire responses a scenario needs.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ TEST-ONLY, AND NO NETWORK. No provider imports this file. The base URL is RFC 2606
|
|
9
|
+
* `unifi.example.com` and the key is a placeholder — never a real console address: a cloud
|
|
10
|
+
* connector's base URL embeds the account's Console ID, which must never appear in this repo
|
|
11
|
+
* (`docs/unifi.md`).
|
|
12
|
+
*/
|
|
13
|
+
import { type Credentials } from '@distilled.cloud/unifi-network/Credentials';
|
|
14
|
+
import * as Layer from 'effect/Layer';
|
|
15
|
+
export declare const FAKE_BASE = "https://unifi.example.com/proxy/network/integration";
|
|
16
|
+
export declare const FAKE_KEY = "placeholder-unifi-key";
|
|
17
|
+
export type Seen = {
|
|
18
|
+
readonly method: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
};
|
|
21
|
+
/** No documented failure envelope (see `protocol.ts`) — plain text is all a failure needs. */
|
|
22
|
+
export declare const fakeFailure: (status: number, detail: string) => Response;
|
|
23
|
+
/**
|
|
24
|
+
* `route` decides every response; this wrapper only records what was sent and adapts to
|
|
25
|
+
* `globalThis.fetch`'s signature.
|
|
26
|
+
*
|
|
27
|
+
* ★ THE WRITE-REFUSAL PROOF USES `seen` DIRECTLY, NOT A THROWING ROUTE. `resource.test.ts`'s
|
|
28
|
+
* "no SDK write operation is reachable" test asserts `seen.every(s => s.method === 'GET')`
|
|
29
|
+
* after driving every handler — a request this family's engine was never supposed to send
|
|
30
|
+
* shows up in `seen` as a fact, rather than depending on an exception surviving Effect's own
|
|
31
|
+
* error handling on its way back out of a fake `fetch`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const fakeUnifi: (route: (method: string, url: URL) => Response) => {
|
|
34
|
+
fetch: typeof fetch;
|
|
35
|
+
seen: Seen[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* distilled Credentials + the real FetchHttpClient over the fake — `UnifiNetworkOpContext`, what
|
|
39
|
+
* every resource file's exported `spec.fetchLive` needs.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ NOT PAIRED WITH `handlers`. `handlers` (each resource file's `unifiHandlers(spec)` export)
|
|
42
|
+
* bakes in `CredentialsFromEnv`, which resolves `UNIFI_NETWORK_API_KEY`/`_API_BASE_URL` through
|
|
43
|
+
* Effect's `Config` — whose default provider snapshots `process.env` once and never re-reads it
|
|
44
|
+
* (the same seam `netbox/fake-netbox.ts` documents). Tests call `spec.fetchLive`/`spec.attributes`
|
|
45
|
+
* directly instead, with this explicit layer.
|
|
46
|
+
*/
|
|
47
|
+
export declare const fakeUnifiLayer: (fetchFn: typeof globalThis.fetch, creds?: Layer.Layer<Credentials>) => Layer.Layer<Credentials | import("effect/unstable/http/HttpClient").HttpClient, never, never>;
|
|
48
|
+
//# sourceMappingURL=fake-unifi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-unifi.d.ts","sourceRoot":"","sources":["../../src/unifi/fake-unifi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,4CAA4C,CAAC;AAC3F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,eAAO,MAAM,SAAS,wDAAwD,CAAC;AAC/E,eAAO,MAAM,QAAQ,0BAA0B,CAAC;AAEhD,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,8FAA8F;AAC9F,eAAO,MAAM,WAAW,WAAY,MAAM,UAAU,MAAM,aAAqC,CAAC;AAEhG;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,UAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ;;;CAUtE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,YAChB,OAAO,UAAU,CAAC,KAAK,UACzB,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,kGAC+D,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallZone`'s wire shape — the declarable subset of `FirewallZone`, and how a live
|
|
3
|
+
* read becomes both plan attributes and a rendered declaration.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ ONLY `name` AND `networkIds` ARE DECLARABLE. `CreateFirewallZoneRequest`
|
|
6
|
+
* (`firewall.ts`) accepts exactly those two beyond `siteId`; `metadata` (origin/source) and
|
|
7
|
+
* `id` are server-derived, so — same split as `network-form.ts`'s `default`/`metadata` —
|
|
8
|
+
* they are attributes-only, reported but never compared or declared.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ `networkIds` ORDER IS NOT PART OF IDENTITY. The vendor document gives no indication the
|
|
11
|
+
* list is ordered (it is a set of attached networks, not a sequence like the ordering
|
|
12
|
+
* endpoints' rule lists), so `matches` compares it as a set — sorted before comparing —
|
|
13
|
+
* the same reasoning `proxmox/user-wire.ts`'s `groupSet` gives for PVE's `groups`.
|
|
14
|
+
*/
|
|
15
|
+
import type * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
16
|
+
export interface FirewallZoneProps {
|
|
17
|
+
siteId: string;
|
|
18
|
+
firewallZoneId: string;
|
|
19
|
+
/** Name of a firewall zone. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Network IDs attached to this zone — compared as a set, see the header. */
|
|
22
|
+
networkIds: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface FirewallZoneAttributes {
|
|
25
|
+
siteId: string;
|
|
26
|
+
firewallZoneId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
networkIds: string[];
|
|
29
|
+
/** `origin`/`source` (e.g. `SDWAN`) — server-derived, never declared. */
|
|
30
|
+
metadataOrigin: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const attributesOf: (live: firewall.FirewallZone, props: FirewallZoneProps) => FirewallZoneAttributes;
|
|
33
|
+
export declare const matches: (attributes: FirewallZoneAttributes, props: FirewallZoneProps) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The declaration renderer (task spec: "given one live object, return the props a declaration
|
|
36
|
+
* needs so its plan is noop"). `matches(attributesOf(live, props), declareFirewallZone(live,
|
|
37
|
+
* siteId))` is `true` by construction (`firewall-zone.test.ts` proves it).
|
|
38
|
+
*/
|
|
39
|
+
export declare const declareFirewallZone: (live: firewall.FirewallZone, siteId: string) => FirewallZoneProps;
|
|
40
|
+
//# sourceMappingURL=firewall-zone-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firewall-zone-form.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-zone-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAID,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,YAAY,SACpB,iBAAiB,KACvB,sBAMD,CAAC;AAEH,eAAO,MAAM,OAAO,eAAgB,sBAAsB,SAAS,iBAAiB,KAAG,OAEZ,CAAC;AAE5E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,SACxB,QAAQ,CAAC,YAAY,UACnB,MAAM,KACb,iBAKD,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.FirewallZone` — one firewall zone on a UniFi Network site, READ-ONLY (`policy.ts`).
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY FIREWALL ZONE IS IN THE FIRST IMPORT SET AND `Unifi.FirewallPolicy` IS NOT.
|
|
5
|
+
* `getFirewallZone`/`getFirewallZones` (`firewall.ts`) is a list+get pair over genuine,
|
|
6
|
+
* simply-shaped configuration: `{id, metadata, name, networkIds}`, four fields, no nested
|
|
7
|
+
* discriminated filter. `FirewallPolicy` (same file) is also list+get and also configuration,
|
|
8
|
+
* but its `source`/`destination`/`ipProtocolScope` fields carry several of `docs/unifi-api-notes.md`'s
|
|
9
|
+
* "converter-flattened discriminator variants" each, and `firewall/policies/ordering` replaces
|
|
10
|
+
* the whole ordered list — modelling those correctly is a bigger, separate PR (`network.ts`'s
|
|
11
|
+
* header note names it explicitly) rather than something to rush alongside this one.
|
|
12
|
+
*/
|
|
13
|
+
import { Resource } from 'alchemy';
|
|
14
|
+
import * as Provider from 'alchemy/Provider';
|
|
15
|
+
import * as firewall from '@distilled.cloud/unifi-network/firewall';
|
|
16
|
+
import * as Effect from 'effect/Effect';
|
|
17
|
+
import { type FirewallZoneAttributes, type FirewallZoneProps } from './firewall-zone-form.ts';
|
|
18
|
+
import type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
19
|
+
export type { FirewallZoneAttributes, FirewallZoneProps } from './firewall-zone-form.ts';
|
|
20
|
+
export { declareFirewallZone } from './firewall-zone-form.ts';
|
|
21
|
+
export interface UnifiFirewallZone extends Resource<'Unifi.FirewallZone', FirewallZoneProps, FirewallZoneAttributes, never, UnifiRequirements> {
|
|
22
|
+
}
|
|
23
|
+
export declare const UnifiFirewallZone: import("alchemy").ResourceClass<UnifiFirewallZone>;
|
|
24
|
+
/** `firewallZone('lan', props)` — `adopt(true)` piped on by default (H5); see `network.ts`. */
|
|
25
|
+
export declare const firewallZone: (id: string, props: FirewallZoneProps) => Effect.Effect<UnifiFirewallZone, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
26
|
+
/** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
|
|
27
|
+
export declare const spec: UnifiSpec<FirewallZoneProps, firewall.FirewallZone, FirewallZoneAttributes, firewall.GetFirewallZoneError>;
|
|
28
|
+
export declare const handlers: {
|
|
29
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
30
|
+
read: (args: {
|
|
31
|
+
olds: FirewallZoneProps;
|
|
32
|
+
output: FirewallZoneAttributes | undefined;
|
|
33
|
+
}) => Effect.Effect<FirewallZoneAttributes | undefined, firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
34
|
+
diff: (args: {
|
|
35
|
+
news: import("alchemy").Input<FirewallZoneProps>;
|
|
36
|
+
output: FirewallZoneAttributes | undefined;
|
|
37
|
+
}) => Effect.Effect<{
|
|
38
|
+
readonly action: 'update';
|
|
39
|
+
} | {
|
|
40
|
+
readonly action: 'noop';
|
|
41
|
+
} | undefined, firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
42
|
+
reconcile: (args: {
|
|
43
|
+
news: FirewallZoneProps;
|
|
44
|
+
}) => Effect.Effect<FirewallZoneAttributes, import("./policy.ts").UnifiWriteRefused | firewall.UnifiNetworkOpError, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
45
|
+
delete: (args: {
|
|
46
|
+
olds: FirewallZoneProps;
|
|
47
|
+
}) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
48
|
+
};
|
|
49
|
+
export declare const UnifiFirewallZoneProvider: () => import("effect/Layer").Layer<Provider.Provider<UnifiFirewallZone>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
50
|
+
//# sourceMappingURL=firewall-zone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firewall-zone.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-zone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAGvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,MAAM,WAAW,iBAAkB,SAAQ,QAAQ,CACjD,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,iBAAiB,oDAE5B,CAAC;AAEH,+FAA+F;AAC/F,eAAO,MAAM,YAAY,OAAQ,MAAM,SAAS,iBAAiB,kGACjB,CAAC;AAEjD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,iBAAiB,EACjB,QAAQ,CAAC,YAAY,EACrB,sBAAsB,EACtB,QAAQ,CAAC,oBAAoB,CAU9B,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,yBAAyB,uIACuD,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UniFi Network providers for Alchemy — READ-ONLY, by Tim's rule (2026-09-24; `policy.ts`).
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, SMALLER THAN THE DIRECTORY. `resource.ts`'s generic engine
|
|
5
|
+
* and each `*-form.ts`'s wire helpers are internals a provider needs but a consumer should
|
|
6
|
+
* not depend on — mirrors `../netbox/index.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export { UNIFI_READ_ONLY_POLICY, UnifiWriteRefused, type UnifiWriteAction } from './policy.ts';
|
|
9
|
+
export type { UnifiRequirements, UnifiSpec } from './resource.ts';
|
|
10
|
+
export { UnifiNetwork, UnifiNetworkProvider, declareNetwork, network, type NetworkAttributes, type NetworkProps, } from './network.ts';
|
|
11
|
+
export { UnifiFirewallZone, UnifiFirewallZoneProvider, declareFirewallZone, firewallZone, type FirewallZoneAttributes, type FirewallZoneProps, } from './firewall-zone.ts';
|
|
12
|
+
export { providers } from './providers.ts';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/unifi/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/F,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,cAAc,EACd,OAAO,EACP,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,mBAAmB,EACnB,YAAY,EACZ,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|