@homeflare/alchemy 0.30.0 → 0.31.1
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/discord/application-command.d.ts +52 -0
- package/dist/discord/application-command.d.ts.map +1 -0
- package/dist/discord/command-form.d.ts +62 -0
- package/dist/discord/command-form.d.ts.map +1 -0
- package/dist/discord/credentials.d.ts +35 -0
- package/dist/discord/credentials.d.ts.map +1 -0
- package/dist/discord/fake-discord.d.ts +44 -0
- package/dist/discord/fake-discord.d.ts.map +1 -0
- package/dist/discord/guild-application-command.d.ts +53 -0
- package/dist/discord/guild-application-command.d.ts.map +1 -0
- package/dist/discord/index.d.ts +12 -0
- package/dist/discord/index.d.ts.map +1 -0
- package/dist/discord/index.js +193 -0
- package/dist/discord/index.js.map +15 -0
- package/dist/discord/providers.d.ts +15 -0
- package/dist/discord/providers.d.ts.map +1 -0
- package/dist/discord/resource.d.ts +75 -0
- package/dist/discord/resource.d.ts.map +1 -0
- package/dist/proxmox/acl-wire.d.ts +26 -0
- package/dist/proxmox/acl-wire.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token-form.d.ts +28 -26
- package/dist/proxmox/api-token-form.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts +48 -89
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-wire.d.ts +2 -2
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-tree.d.ts +1 -1
- package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +2 -2
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool-settle.d.ts +1 -1
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
- package/dist/proxmox/client.d.ts +2 -2
- package/dist/proxmox/client.d.ts.map +1 -1
- package/dist/proxmox/credential-errors.d.ts +27 -0
- package/dist/proxmox/credential-errors.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +0 -38
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/distilled-guard.d.ts +31 -0
- package/dist/proxmox/distilled-guard.d.ts.map +1 -0
- package/dist/proxmox/distilled-pve.d.ts +11 -4
- package/dist/proxmox/distilled-pve.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts.map +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-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/group-wire.d.ts +29 -0
- package/dist/proxmox/group-wire.d.ts.map +1 -0
- package/dist/proxmox/group.d.ts +20 -24
- package/dist/proxmox/group.d.ts.map +1 -1
- package/dist/proxmox/index.js +725 -509
- package/dist/proxmox/index.js.map +40 -33
- package/dist/proxmox/lease-cache.d.ts +1 -1
- package/dist/proxmox/lease-cache.d.ts.map +1 -1
- package/dist/proxmox/lxc-lifecycle.d.ts +5 -5
- package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/mint.d.ts +50 -0
- package/dist/proxmox/mint.d.ts.map +1 -0
- package/dist/proxmox/network-apply-read.d.ts +2 -2
- package/dist/proxmox/network-apply-read.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +2 -2
- package/dist/proxmox/pbs-sync-job-form.d.ts +2 -2
- package/dist/proxmox/provision-cli-fake.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts +4 -4
- package/dist/proxmox/role-wire.d.ts +28 -0
- package/dist/proxmox/role-wire.d.ts.map +1 -0
- package/dist/proxmox/role.d.ts +36 -52
- package/dist/proxmox/role.d.ts.map +1 -1
- package/dist/proxmox/sdn-apply-read.d.ts +1 -1
- package/dist/proxmox/sdn-apply-read.d.ts.map +1 -1
- package/dist/proxmox/unreadable-read.d.ts +50 -0
- package/dist/proxmox/unreadable-read.d.ts.map +1 -0
- package/dist/proxmox/user-wire.d.ts +18 -0
- package/dist/proxmox/user-wire.d.ts.map +1 -0
- package/dist/proxmox/user.d.ts +25 -32
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-write.d.ts +2 -2
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
- package/docs/credentials.md +9 -0
- package/docs/discord.md +129 -0
- package/docs/upstream-conformance.md +17 -1
- package/package.json +5 -1
- package/src/discord/application-command.test.ts +103 -0
- package/src/discord/application-command.ts +109 -0
- package/src/discord/command-form.test.ts +144 -0
- package/src/discord/command-form.ts +133 -0
- package/src/discord/credentials.ts +36 -0
- package/src/discord/fake-discord.ts +70 -0
- package/src/discord/guild-application-command.test.ts +215 -0
- package/src/discord/guild-application-command.ts +129 -0
- package/src/discord/index.ts +31 -0
- package/src/discord/providers.ts +18 -0
- package/src/discord/resource.ts +164 -0
- package/src/discord/retry.test.ts +74 -0
- package/src/proxmox/acl-wire.ts +83 -0
- package/src/proxmox/acl.ts +29 -82
- package/src/proxmox/adopt-noop.test.ts +18 -0
- package/src/proxmox/api-token-adopt.test.ts +28 -12
- package/src/proxmox/api-token-form.ts +50 -93
- package/src/proxmox/api-token.ts +149 -135
- package/src/proxmox/client.ts +2 -7
- package/src/proxmox/constraints-forms.test.ts +7 -27
- package/src/proxmox/credential-errors.ts +33 -0
- package/src/proxmox/credentials.test.ts +29 -3
- package/src/proxmox/credentials.ts +4 -134
- package/src/proxmox/distilled-guard.ts +42 -0
- package/src/proxmox/distilled-pve.ts +15 -4
- package/src/proxmox/generated/constraints/index.ts +1 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -4
- 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-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/group-wire.ts +63 -0
- package/src/proxmox/group.test.ts +110 -0
- package/src/proxmox/group.ts +135 -105
- package/src/proxmox/lease-cache.ts +2 -1
- package/src/proxmox/lxc-lifecycle.ts +2 -1
- package/src/proxmox/mint.ts +158 -0
- package/src/proxmox/network-apply.ts +1 -1
- package/src/proxmox/provision-cli-fake.ts +11 -0
- package/src/proxmox/provision-declare.test.ts +6 -1
- package/src/proxmox/role-wire.ts +49 -0
- package/src/proxmox/role.test.ts +89 -0
- package/src/proxmox/role.ts +147 -106
- package/src/proxmox/unreadable-read.test.ts +83 -0
- package/src/proxmox/unreadable-read.ts +60 -0
- package/src/proxmox/user-wire.ts +112 -0
- package/src/proxmox/user.test.ts +142 -0
- package/src/proxmox/user.ts +132 -129
|
@@ -15,14 +15,17 @@ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
|
|
|
15
15
|
* `update_token_info`, read over SSH on node-b on 2026-09-13. The two disagree in exactly the place
|
|
16
16
|
* that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
|
|
17
17
|
* never implements it. api-token.ts records what that means.
|
|
18
|
+
*
|
|
19
|
+
* ★ MIGRATED OFF THE GENERATED `generated/pve.ts` PARAM TYPES ONTO `@distilled.cloud/proxmox`'s
|
|
20
|
+
* typed `access.PutAccessUserTokenRequest`/`access.GetAccessUserTokenResponse` (2026-09-24) —
|
|
21
|
+
* the same three fields, still form-urlencoded strings on the wire; distilled's generator
|
|
22
|
+
* reads the identical vendor schema `generated/pve.ts` came from.
|
|
18
23
|
*/
|
|
19
|
-
import type
|
|
20
|
-
AccessUsersUseridTokenTokenidPostParams,
|
|
21
|
-
AccessUsersUseridTokenTokenidPutParams,
|
|
22
|
-
} from './generated/pve.ts';
|
|
23
|
-
import type { PveSpec } from './resource.ts';
|
|
24
|
+
import type * as access from '@distilled.cloud/proxmox/access';
|
|
24
25
|
import { bool, int, text } from './values.ts';
|
|
25
26
|
|
|
27
|
+
export const API_TOKEN_UPDATE = 'pve:PUT /access/users/{userid}/token/{tokenid}';
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* `<userid>!<tokenid>` — the name the rest of PVE calls this object by.
|
|
28
31
|
*
|
|
@@ -38,12 +41,8 @@ import { bool, int, text } from './values.ts';
|
|
|
38
41
|
export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.tokenid}`;
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
|
-
* Everything a token has, in the form PVE wants.
|
|
42
|
-
*
|
|
43
|
-
* ⛔ `userid` AND `tokenid` ARE NOT IN IT, and that is not an omission. Both are path segments of
|
|
44
|
-
* `access/users/{userid}/token/{tokenid}` — which is the path the POST goes to as well as the
|
|
45
|
-
* PUT — so a second copy in the body can only ever disagree with the path it was sent to.
|
|
46
|
-
* metric-server.ts omits its `id` for the same reason and says so there.
|
|
44
|
+
* Everything a token has, in the form PVE wants. Update sends exactly this (this family never
|
|
45
|
+
* creates — see api-token.ts's header).
|
|
47
46
|
*
|
|
48
47
|
* ⚠️ EVERY FIELD IS SENT ON EVERY WRITE, THE EMPTY COMMENT INCLUDED, BECAUSE A PUT MERGES.
|
|
49
48
|
* MEASURED in `update_token_info`: `$token->{comment} = $param->{comment} if defined(...)`
|
|
@@ -54,91 +53,49 @@ export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.to
|
|
|
54
53
|
* literally `my $deletable = { comment => 1 };`, and anything else answers
|
|
55
54
|
* "unknown option '<k>'" — so `expire` and `privsep` have no clear path at all, which is the
|
|
56
55
|
* other half of why both are required props rather than optional ones.
|
|
57
|
-
*
|
|
58
|
-
* ⚠️ `flag()` FROM values.ts IS DELIBERATELY NOT USED FOR `privsep`. Its entire job is turning an
|
|
59
|
-
* UNDECLARED boolean into `undefined` so the field is omitted from the form; `privsep` is a
|
|
60
|
-
* required prop precisely so that it is never undeclared, so there is nothing for it to do and
|
|
61
|
-
* its `string | undefined` return would have to be asserted away to fit `Record<string,string>`.
|
|
62
|
-
* An assertion here would be a claim about the prop that the type already makes properly.
|
|
63
|
-
*
|
|
64
|
-
* ★ TYPED AGAINST THE GENERATED PUT AND POST PARAMS, NOT `Record<string, string>`, since
|
|
65
|
-
* 2026-09-23 — the `notification-matcher-form.ts` pattern: `Pick` rather than the whole type,
|
|
66
|
-
* because neither `AccessUsersUseridTokenTokenidPutParams` nor …`PostParams` (generated/pve.ts)
|
|
67
|
-
* carries `userid`/`tokenid` (they are path segments — see the ⛔ above) and Put alone also
|
|
68
|
-
* carries `delete`/`regenerate`, which this family never sends. A schema drift on any of the
|
|
69
|
-
* three fields this function DOES send fails `tsc` here rather than surfacing as a 400 on a live
|
|
70
|
-
* cluster; api-token.test.ts pins the assignment both ways (Put and Post) without a cast.
|
|
71
|
-
* `expire` is written as a template literal, not `String(...)`, because the generated type is
|
|
72
|
-
* `` `${number}` ``, not `string` — `String(n)` widens to plain `string` and would not typecheck.
|
|
73
56
|
*/
|
|
74
|
-
export const shape = (
|
|
75
|
-
props: ApiTokenProps,
|
|
76
|
-
): Pick<AccessUsersUseridTokenTokenidPutParams, 'comment' | 'expire' | 'privsep'> &
|
|
77
|
-
Pick<AccessUsersUseridTokenTokenidPostParams, 'comment' | 'expire' | 'privsep'> => ({
|
|
57
|
+
export const shape = (props: ApiTokenProps): access.PutAccessUserTokenRequest => ({
|
|
78
58
|
comment: props.comment ?? '',
|
|
79
|
-
expire:
|
|
59
|
+
expire: String(props.expire),
|
|
80
60
|
privsep: props.privsep ? '1' : '0',
|
|
61
|
+
tokenid: props.tokenid,
|
|
62
|
+
userid: props.userid,
|
|
81
63
|
});
|
|
82
64
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* wrong in the safe direction if a future release stops emitting the field. */
|
|
110
|
-
privsep: bool(privsep, true),
|
|
111
|
-
tokenid: props.tokenid,
|
|
112
|
-
userid: props.userid,
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
/**
|
|
116
|
-
* ⛔ THE SAME STRING AS `path`, AND NOT A TYPO. A token is POSTed to its OWN url, not to the
|
|
117
|
-
* collection: MEASURED, `/access/users/{userid}/token` carries a GET and nothing else, while
|
|
118
|
-
* POST lives on `{tokenid}`. metric-server.ts has the identical shape for the identical
|
|
119
|
-
* reason and records the experiment there.
|
|
120
|
-
*/
|
|
121
|
-
collection: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
|
|
122
|
-
createForm: shape,
|
|
123
|
-
/** The vendor rules both forms are checked against at plan time — resource-spec.ts. */
|
|
124
|
-
endpoint: {
|
|
125
|
-
create: 'pve:POST /access/users/{userid}/token/{tokenid}',
|
|
126
|
-
update: 'pve:PUT /access/users/{userid}/token/{tokenid}',
|
|
127
|
-
},
|
|
128
|
-
/**
|
|
129
|
-
* ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
|
|
130
|
-
* MEASURED, by replaying these two functions over the live GET bodies of all four token
|
|
131
|
-
* shapes this cluster has: `metrics@pve!exporter` (no comment), `iac@pve!apply` and
|
|
132
|
-
* `app@pve!app` (commented), and a live OpenBao lease under `hf-read@pve` (a real
|
|
133
|
-
* non-zero `expire`, offered as both `1789327175` and `"1789327175"` since `int` must not care
|
|
134
|
-
* which). All four answer noop; flipping `privsep` answers update; an empty body answers
|
|
135
|
-
* absent. ⚠️ THAT IS THE COMPARISON, NOT THE ENGINE — no `alchemy plan` was run against this
|
|
136
|
-
* family, and nothing here was written to the cluster.
|
|
137
|
-
*/
|
|
138
|
-
matches: (attributes, props) =>
|
|
139
|
-
attributes.comment === (props.comment ?? '') &&
|
|
140
|
-
attributes.expire === props.expire &&
|
|
141
|
-
attributes.privsep === props.privsep,
|
|
142
|
-
path: (props) => `access/users/${props.userid}/token/${props.tokenid}`,
|
|
143
|
-
updateForm: shape,
|
|
65
|
+
/**
|
|
66
|
+
* ★ NOTHING PVE REPORTS HERE IS UNWRITABLE, WHICH IS UNUSUAL IN THIS PACKAGE. The GET returns
|
|
67
|
+
* exactly `comment`, `expire` and `privsep`; PUT accepts exactly those three.
|
|
68
|
+
* ⛔ `undefined` WHEN NEITHER FLAG IS THERE, BECAUSE BOTH ARE ALWAYS THERE FOR A REAL TOKEN.
|
|
69
|
+
* `user.cfg` materialises `expire` and `privsep` for every token it stores, so an answer
|
|
70
|
+
* carrying neither is not a token — the same "distilled unwraps a missing object's `{"data":
|
|
71
|
+
* null}` to `{}`" trap group.ts's header measures, checked here the way this guard already was
|
|
72
|
+
* before the distilled migration (api-token.ts's ⛔ on the create refusal cites the same
|
|
73
|
+
* MEASURED schema). Without this guard `bool`/`int` would invent one out of their fallbacks and
|
|
74
|
+
* the read-back guard in `reconcile` could never fire.
|
|
75
|
+
*/
|
|
76
|
+
export const attributesOf = (
|
|
77
|
+
live: access.GetAccessUserTokenResponse,
|
|
78
|
+
props: ApiTokenProps,
|
|
79
|
+
): ApiTokenAttributes | undefined => {
|
|
80
|
+
if (live.expire === undefined && live.privsep === undefined) return undefined;
|
|
81
|
+
return {
|
|
82
|
+
comment: text(live.comment),
|
|
83
|
+
expire: int(live.expire, 0),
|
|
84
|
+
fullTokenid: fullTokenid(props),
|
|
85
|
+
/** ⚠️ FALLBACK `true`, MATCHING PVE'S API DEFAULT — unreachable given the guard above, but
|
|
86
|
+
* wrong in the safe direction if a future release stops emitting the field. */
|
|
87
|
+
privsep: bool(live.privsep, true),
|
|
88
|
+
tokenid: props.tokenid,
|
|
89
|
+
userid: props.userid,
|
|
90
|
+
};
|
|
144
91
|
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* ⚠️ EXACTLY THE THREE FIELDS A PUT CAN PUT BACK, WHICH IS ALSO EXACTLY WHAT THE GET REPORTS.
|
|
95
|
+
* MEASURED (pre-migration, replaying against four live token shapes — see the old PR): all
|
|
96
|
+
* answer noop; flipping `privsep` answers update; an empty body answers absent.
|
|
97
|
+
*/
|
|
98
|
+
export const matches = (attributes: ApiTokenAttributes, props: ApiTokenProps) =>
|
|
99
|
+
attributes.comment === (props.comment ?? '') &&
|
|
100
|
+
attributes.expire === props.expire &&
|
|
101
|
+
attributes.privsep === props.privsep;
|
package/src/proxmox/api-token.ts
CHANGED
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
* `Proxmox.ApiToken` — a PVE API token: the thing a service actually authenticates as.
|
|
3
3
|
*
|
|
4
4
|
* ⛔ THE SECRET EXISTS FOR ONE HTTP RESPONSE AND THEN NOWHERE. MEASURED from the cluster's own
|
|
5
|
-
* schema
|
|
6
|
-
*
|
|
7
|
-
* "
|
|
8
|
-
*
|
|
9
|
-
* `GET /access/users/metrics@pve/token/exporter` answers `{"expire":0,"privsep":0}`. PVE says
|
|
10
|
-
* it plainly on the POST: the value "needs to be stored as it cannot be retrieved afterwards".
|
|
5
|
+
* schema: POST returns `["full-tokenid","info","value"]`; PUT returns `["comment","expire",
|
|
6
|
+
* "full-tokenid","privsep","value"]` with `value` present ONLY when `regenerate` was set; GET
|
|
7
|
+
* returns `["comment","expire","privsep"]` and never the secret. PVE says it plainly on the
|
|
8
|
+
* POST: the value "needs to be stored as it cannot be retrieved afterwards".
|
|
11
9
|
*
|
|
12
10
|
* ⛔ SO A TOKEN THIS RESOURCE CREATES IS UNUSABLE, AND SAYING SO IS THE POINT OF THIS BLOCK. The
|
|
13
11
|
* secret may not become an attribute: Alchemy persists attributes UNENCRYPTED, and this estate's
|
|
14
|
-
* state store is a Postgres dumped nightly (the ⛔ in credentials.ts). So `reconcile`
|
|
15
|
-
*
|
|
12
|
+
* state store is a Postgres dumped nightly (the ⛔ in credentials.ts). So `reconcile` refuses
|
|
13
|
+
* before any POST, and what would land is a live credential in `/etc/pve/user.cfg` whose secret
|
|
16
14
|
* nobody holds. No `regenerate` rescues it, because a regenerated value is discarded the same
|
|
17
15
|
* way. The only escape is an operator who captured the value out of band — which this provider
|
|
18
16
|
* gives them no way to do.
|
|
@@ -20,50 +18,49 @@
|
|
|
20
18
|
* whole of a token's policy, and declaring them is real work: it is how `expire` stops being
|
|
21
19
|
* whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
|
|
22
20
|
* `proxmox-c1` mount, or a human at `pveum user token add`.
|
|
23
|
-
* ⛔ ★ CORRECTED 2026-09-23 — THIS PARAGRAPH PREVIOUSLY SAID "THE CREATE PATH IS LEFT REACHABLE
|
|
24
|
-
* RATHER THAN STUBBED… the honest arrangement is a create that works". THAT CONTRADICTED THE
|
|
25
|
-
* `reconcile` BELOW, WHICH REFUSES: `handlers.reconcile` calls `Effect.die` by name when `read`
|
|
26
|
-
* answers absent, before any POST — metadata-only per decision 9 (2026-09-23), exported on the
|
|
27
|
-
* barrel as an adopt-and-manage-only Resource. A create that silently did nothing WOULD be the
|
|
28
|
-
* lie resource.ts's ★ describes for a stubbed delete; refusing loudly, by name, with the two
|
|
29
|
-
* ways to get a usable token, is the honest arrangement instead — see the ★ further down and
|
|
30
|
-
* api-token-adopt.test.ts's absent-token row, which pins the refusal and zero writes.
|
|
31
21
|
*
|
|
32
22
|
* ⛔ THERE IS NO `regenerate` PROP, AND IT IS NOT AN OVERSIGHT. PVE's own description: "All users
|
|
33
23
|
* of the previous secret will lose access after this operation." A prop for it would revoke a
|
|
34
24
|
* live credential during a deploy whose plan said `update`, and hand back a replacement this
|
|
35
|
-
* provider is obliged to throw away — breakage with no recovery.
|
|
36
|
-
* that owns the lease, or to a human who is watching.
|
|
25
|
+
* provider is obliged to throw away — breakage with no recovery.
|
|
37
26
|
*
|
|
38
27
|
* ⚠️ THERE IS A SECOND WRITER TO THESE OBJECTS AND IT IS THE ONE THIS PROVIDER RUNS ON. OpenBao's
|
|
39
|
-
* `proxmox-c1` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve`
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* one object by accident — but they share the USER: `hf-read@pve` held three live leases when
|
|
45
|
-
* this was written, and `Proxmox.User` deleting that account takes every one of them with it.
|
|
46
|
-
* ⛔ DO NOT DECLARE A TOKEN UNDER A MINT USER. `expire` is what makes that dangerous rather than
|
|
47
|
-
* untidy: point this resource at a lease token with the wrong expire and a 300-second
|
|
48
|
-
* credential becomes a permanent one — and on `hf-provision@pve` that credential carries
|
|
49
|
-
* `Permissions.Modify` at `/`. docs/privileges.md records what those 27 privileges buy.
|
|
28
|
+
* `proxmox-c1` engine mints PVE tokens under `hf-read@pve` and `hf-provision@pve` with an id it
|
|
29
|
+
* CHOOSES (`hf-<role>-<actor>-<entity6>-<stamp>-<nonce>`), so the two systems cannot collide on
|
|
30
|
+
* one object by accident — but they share the USER, and `Proxmox.User` deleting that account
|
|
31
|
+
* takes every one of its tokens with it. ⛔ DO NOT DECLARE A TOKEN UNDER A MINT USER: `expire`
|
|
32
|
+
* is what makes that dangerous — a 300-second lease token becomes a permanent one.
|
|
50
33
|
*
|
|
51
34
|
* ⚠️ PRIVILEGES, AND THE READ LANE CANNOT DO IT. MEASURED: all four methods on this path check
|
|
52
35
|
* `['or', ['userid-param','self'], ['userid-group', ['User.Modify']]]` — with no `Sys.Audit`
|
|
53
|
-
* alternative
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
36
|
+
* alternative — so the 3600s read lease reaches at most its OWN account's tokens and is refused
|
|
37
|
+
* for every other userid. `readRole: 'provision'` below, for the same reason storage.ts,
|
|
38
|
+
* sdn-zone.ts and sdn-vnet.ts set it, at the same cost: every plan mints a 300s non-renewable
|
|
39
|
+
* provision lease just to read.
|
|
40
|
+
*
|
|
41
|
+
* ★ MIGRATED OFF `client.ts`'s generic `pve()`/`pveHandlers`/`pveOperations` ONTO
|
|
42
|
+
* `@distilled.cloud/proxmox`'s typed `access.getAccessUserToken`/`putAccessUserToken`/
|
|
43
|
+
* `deleteAccessUserToken` (2026-09-24, decision 43's proxmox walk-down, the last access-family
|
|
44
|
+
* resource). There is no `createAccessUserToken` call anywhere in this file — the create branch
|
|
45
|
+
* was already provably unreachable before the migration (`reconcile` dies on an absent read
|
|
46
|
+
* before any create path runs), so nothing calls distilled's POST-shaped `updateAccessUserToken`
|
|
47
|
+
* operation either; see api-token-form.ts for why the generator gave it that name.
|
|
61
48
|
*/
|
|
62
49
|
import { Resource } from 'alchemy';
|
|
50
|
+
import { isResolved } from 'alchemy/Diff';
|
|
63
51
|
import * as Provider from 'alchemy/Provider';
|
|
52
|
+
import * as access from '@distilled.cloud/proxmox/access';
|
|
64
53
|
import * as Effect from 'effect/Effect';
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
54
|
+
import { API_TOKEN_UPDATE, attributesOf, matches, shape } from './api-token-form.ts';
|
|
55
|
+
import { guardWrite } from './distilled-guard.ts';
|
|
56
|
+
import type { PveRequirements, WithTarget } from './resource-spec.ts';
|
|
57
|
+
import { runPve } from './distilled-pve.ts';
|
|
58
|
+
import {
|
|
59
|
+
UNREADABLE,
|
|
60
|
+
type Unreadable,
|
|
61
|
+
readOrUnreadable,
|
|
62
|
+
unreadableWarning,
|
|
63
|
+
} from './unreadable-read.ts';
|
|
67
64
|
|
|
68
65
|
export interface ApiTokenProps extends WithTarget {
|
|
69
66
|
/**
|
|
@@ -78,57 +75,36 @@ export interface ApiTokenProps extends WithTarget {
|
|
|
78
75
|
/**
|
|
79
76
|
* The token's own name, unique within the account. Identity.
|
|
80
77
|
*
|
|
81
|
-
* ⚠️ PVE HAS NO RENAME
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* is what keeps it from looking like an omission.
|
|
91
|
-
* ⚠️ PATTERN `[A-Za-z][A-Za-z0-9.\-_]+`: a letter first, two characters minimum. The schema
|
|
92
|
-
* declares no maxLength; the OpenBao plugin assumes a conservative 64 rather than finding the
|
|
93
|
-
* real limit in production, and so should anything else.
|
|
78
|
+
* ⚠️ PVE HAS NO RENAME. Editing this prop (or `userid`) points `path` at a DIFFERENT token,
|
|
79
|
+
* which reads as absent and is then created — leaving the old token LIVE, invisible to the
|
|
80
|
+
* plan, while the new one is the unusable kind described at the top. Rename by declaring a
|
|
81
|
+
* delete and a create.
|
|
82
|
+
* ⛔ `diff` IS NOT OVERRIDDEN TO SAY `replace` THE WAY acl.ts DOES: there, replace removes a
|
|
83
|
+
* grant that can be rebuilt from its own tuple; here it would DELETE a live token —
|
|
84
|
+
* destroying an irrecoverable secret — to tidy up a name. Leaving the old token standing is
|
|
85
|
+
* the lesser harm.
|
|
86
|
+
* ⚠️ PATTERN `[A-Za-z][A-Za-z0-9.\-_]+`: a letter first, two characters minimum.
|
|
94
87
|
*/
|
|
95
88
|
tokenid: string;
|
|
96
|
-
/**
|
|
97
|
-
* Free text in `pveum user token list` and the UI. Empty is how it is cleared — see `shape`.
|
|
98
|
-
* ⚠️ ANY CHARACTER IS SAFE HERE, MEASURED, and it was worth checking: `user.cfg` is a
|
|
99
|
-
* colon-delimited line, so a comment holding a colon or a newline is the obvious place for a
|
|
100
|
-
* round-trip to lose a character and diff forever. PVE escapes it — `encode_text` turns
|
|
101
|
-
* `a:b\nc%d` into `a%3Ab%0Ac%25d` and `decode_text` gives it back byte for byte.
|
|
102
|
-
*/
|
|
89
|
+
/** Free text in `pveum user token list` and the UI. Empty is how it is cleared — see `shape`. */
|
|
103
90
|
comment?: string;
|
|
104
91
|
/**
|
|
105
92
|
* Seconds since the epoch, or 0 for "never expires".
|
|
106
93
|
*
|
|
107
94
|
* ⛔ REQUIRED, AND THE SCHEMA WILL TELL YOU IT NEED NOT BE. It declares the default "same as
|
|
108
95
|
* user", and the code never implements it: `generate_token` writes `expire` only when the
|
|
109
|
-
* parameter is defined,
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* back `"expire":0`. So "same as user" is documentation of an intention, not behaviour.
|
|
113
|
-
* ⛔ WHICH IS WHY IT IS NOT OPTIONAL-DEFAULTING-TO-0. That spelling reads identically and is far
|
|
114
|
-
* worse: an omitted `expire` would then quietly WRITE 0 over a live deadline, turning an
|
|
115
|
-
* expiring credential into a permanent one, and the plan would call it an update. Requiring
|
|
116
|
-
* the field makes "this token never expires" a sentence somebody had to type.
|
|
96
|
+
* parameter is defined, and the parser does `$expire = 0 if !$expire`. Required, so "this
|
|
97
|
+
* token never expires" is a sentence somebody had to type, rather than an omission that would
|
|
98
|
+
* quietly WRITE 0 over a live deadline.
|
|
117
99
|
*/
|
|
118
100
|
expire: number;
|
|
119
101
|
/**
|
|
120
102
|
* True keeps the token's privileges separate from its owner's — it gets NOTHING until an ACL
|
|
121
103
|
* names `fullTokenid`. False gives it the owner's privileges entire.
|
|
122
104
|
*
|
|
123
|
-
* ⛔ REQUIRED FOR THE SAME REASON AS `expire`, AND THE BLAST RADIUS IS LARGER.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* preserve. Were this optional, adopting a live token without mentioning `privsep` would plan
|
|
127
|
-
* an update to 1 and STRIP a working credential of every privilege it has, silently: PVE
|
|
128
|
-
* returns 401/403 to the service, nothing errors here, and `iac@pve!apply` simply stops
|
|
129
|
-
* working. Required, so adopting a token is a sentence that states what it is.
|
|
130
|
-
* ⚠️ AND `false` IS NOT A SHRUG. It is a token with its owner's whole privilege set; under a
|
|
131
|
-
* provisioning account that is the account's full authority with a separate secret.
|
|
105
|
+
* ⛔ REQUIRED FOR THE SAME REASON AS `expire`, AND THE BLAST RADIUS IS LARGER. Were this
|
|
106
|
+
* optional, adopting a live token without mentioning `privsep` would plan an update to 1 and
|
|
107
|
+
* STRIP a working credential of every privilege it has, silently.
|
|
132
108
|
*/
|
|
133
109
|
privsep: boolean;
|
|
134
110
|
}
|
|
@@ -143,9 +119,7 @@ export interface ApiTokenAttributes {
|
|
|
143
119
|
privsep: boolean;
|
|
144
120
|
/**
|
|
145
121
|
* ⛔ THERE IS NO `value` FIELD HERE AND THERE NEVER MAY BE. Adding one would write a working PVE
|
|
146
|
-
* credential, in clear, into the state Postgres and into every nightly dump of it
|
|
147
|
-
* leak metric-server.ts types its `token` as `never` to prevent. The read cannot supply one
|
|
148
|
-
* anyway; only create and regenerate can, and both of those responses are dropped.
|
|
122
|
+
* credential, in clear, into the state Postgres and into every nightly dump of it.
|
|
149
123
|
*/
|
|
150
124
|
}
|
|
151
125
|
|
|
@@ -159,69 +133,109 @@ export interface ProxmoxApiToken extends Resource<
|
|
|
159
133
|
|
|
160
134
|
/**
|
|
161
135
|
* ★ `retain` BY DEFAULT, AND THIS IS THE PLAINEST CASE FOR IT IN THE PACKAGE. A token's contents
|
|
162
|
-
* are one irreplaceable secret: delete it and the value is gone,
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* exporter feeding VictoriaMetrics), `agent@pve!executor`, `iac@pve!apply`, `iac@pve!ro`,
|
|
166
|
-
* `app@pve!app` and `mint@pve!engine` — the last being the parent credential the
|
|
167
|
-
* OpenBao mount itself authenticates with, so orphaning that one would stop every plan in this
|
|
168
|
-
* package, this resource included. `delete` is FULLY IMPLEMENTED (DELETE is a real method on
|
|
169
|
-
* this path, measured) and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
170
|
-
* See the ★ in resource.ts, which explains the convention once.
|
|
136
|
+
* are one irreplaceable secret: delete it and the value is gone, and every holder loses access
|
|
137
|
+
* the instant `cfs_write_file` returns — with no error raised anywhere near them. `delete` is
|
|
138
|
+
* FULLY IMPLEMENTED and runs the moment a caller opts in with `.pipe(RemovalPolicy.destroy())`.
|
|
171
139
|
*/
|
|
172
140
|
export const ProxmoxApiToken = Resource<ProxmoxApiToken>('Proxmox.ApiToken', {
|
|
173
141
|
defaultRemovalPolicy: 'retain',
|
|
174
142
|
});
|
|
175
143
|
|
|
176
|
-
|
|
144
|
+
/** ⛔ `readRole: 'provision'` — see the header's ⚠️ on privileges. */
|
|
145
|
+
const readToken = (props: ApiTokenProps) =>
|
|
146
|
+
readOrUnreadable(runPve(props.target, 'provision', false, access.getAccessUserToken(props))).pipe(
|
|
147
|
+
Effect.map((live) => (live === UNREADABLE ? UNREADABLE : attributesOf(live, props))),
|
|
148
|
+
Effect.orElseSucceed(() => undefined),
|
|
149
|
+
);
|
|
177
150
|
|
|
178
|
-
/**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* returns `["full-tokenid","info","value"]`, where `value` is "API token value used for
|
|
182
|
-
* authentication", while `GET` returns `["comment","expire","privsep"]` and never the secret.
|
|
183
|
-
* It may not become an attribute, because Alchemy writes attributes to its state store
|
|
184
|
-
* UNENCRYPTED into a Postgres that is dumped nightly.
|
|
185
|
-
*
|
|
186
|
-
* So a token created here would be A LIVE CREDENTIAL ON THE CLUSTER THAT NOBODY HOLDS: valid,
|
|
187
|
-
* privileged, unusable, and indistinguishable from one somebody meant to keep. Regenerating does
|
|
188
|
-
* not rescue it — a `regenerate` PUT returns the new value down the same discarded path. So
|
|
189
|
-
* `reconcile` refuses by name and says what to do instead.
|
|
190
|
-
*
|
|
191
|
-
* ★ EVERYTHING ELSE STILL WORKS, AND IT IS THE HALF WORTH HAVING. Adopting an existing token and
|
|
192
|
-
* converging its `comment`, `expire` and `privsep` are real operations: C1 carries SEVEN tokens
|
|
193
|
-
* with `expire=0` and `privsep=0` made by clicks nobody recorded. Declaring those freezes the
|
|
194
|
-
* set, and an eighth appearing shows up as drift.
|
|
195
|
-
*
|
|
196
|
-
* ⚠️ IF DECLARATIVE MINTING IS EVER WANTED, the missing piece is a secret SINK — write the value
|
|
197
|
-
* straight into an OpenBao kv path and return only its address. That is a different resource
|
|
198
|
-
* with a different contract; it must not be bolted onto this one.
|
|
199
|
-
*/
|
|
200
|
-
const handlers = {
|
|
201
|
-
...pveHandlers(apiTokenSpec),
|
|
202
|
-
reconcile: Effect.fn(function* ({ news }: { news: ApiTokenProps }) {
|
|
203
|
-
const live = yield* ops.read(news);
|
|
204
|
-
if (live === undefined) {
|
|
205
|
-
return yield* Effect.die(
|
|
206
|
-
new Error(
|
|
207
|
-
`${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE returns ` +
|
|
208
|
-
'the secret only in the create response and it cannot be stored, so a token made here ' +
|
|
209
|
-
'would be a live credential nobody holds. Create it with `pveum user token add` and ' +
|
|
210
|
-
'capture the value, or mint a short-lived one from the OpenBao proxmox mount ' +
|
|
211
|
-
'(`bao read proxmox-c1/creds/<role>`) -- then declare it here to manage it.',
|
|
212
|
-
),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return yield* ops.reconcile(news);
|
|
216
|
-
}),
|
|
217
|
-
};
|
|
151
|
+
/** `read`/`reconcile` return `Attributes | undefined`; only `diff` tells `UNREADABLE` apart. */
|
|
152
|
+
const dropUnreadable = (live: ApiTokenAttributes | Unreadable | undefined) =>
|
|
153
|
+
live === UNREADABLE ? undefined : live;
|
|
218
154
|
|
|
219
155
|
/**
|
|
220
|
-
* ⛔
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* would report drift against a credential that was never its to hold.
|
|
156
|
+
* ⛔ THIS FAMILY ADOPTS AND MANAGES TOKENS; IT REFUSES TO MINT ONE — see the header. `reconcile`
|
|
157
|
+
* refuses by name before any write when the read finds nothing, so the create path this
|
|
158
|
+
* family's spec would otherwise need never runs, and no `createAccessUserToken`-shaped call
|
|
159
|
+
* exists in this file at all.
|
|
225
160
|
*/
|
|
226
161
|
export const ProxmoxApiTokenProvider = () =>
|
|
227
|
-
Provider.effect(
|
|
162
|
+
Provider.effect(
|
|
163
|
+
ProxmoxApiToken,
|
|
164
|
+
Effect.succeed(
|
|
165
|
+
ProxmoxApiToken.Provider.of({
|
|
166
|
+
/**
|
|
167
|
+
* ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT.
|
|
168
|
+
* A token index handed to Alchemy would offer up `mint@pve!engine` and every live
|
|
169
|
+
* OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
|
|
170
|
+
*/
|
|
171
|
+
list: () => Effect.succeed([]),
|
|
172
|
+
read: Effect.fn(function* ({ olds }) {
|
|
173
|
+
return dropUnreadable(yield* readToken(olds));
|
|
174
|
+
}),
|
|
175
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
176
|
+
if (!isResolved(news)) return undefined;
|
|
177
|
+
yield* guardWrite(API_TOKEN_UPDATE, shape(news), false);
|
|
178
|
+
if (output === undefined) return undefined;
|
|
179
|
+
const live = yield* readToken(news);
|
|
180
|
+
// ⛔ THE CRIES-WOLF FIX: a refused read used to fall into `undefined` below and force
|
|
181
|
+
// `update` on a token that was plainly there — see unreadable-read.ts.
|
|
182
|
+
if (live === UNREADABLE) {
|
|
183
|
+
yield* unreadableWarning('Proxmox.ApiToken', `${news.userid}!${news.tokenid}`);
|
|
184
|
+
return { action: 'noop' } as const;
|
|
185
|
+
}
|
|
186
|
+
// ⚠️ NO GUARDED-CREATE BRANCH HERE, UNLIKE EVERY OTHER FAMILY IN THIS SUB-AREA. There
|
|
187
|
+
// is nothing to guard a create form for — `reconcile` refuses instead, and `diff`
|
|
188
|
+
// answering `update` for an absent token is what routes a real deploy into that
|
|
189
|
+
// refusal rather than silently reporting `noop` over a token that never landed.
|
|
190
|
+
return live !== undefined && matches(live, news)
|
|
191
|
+
? ({ action: 'noop' } as const)
|
|
192
|
+
: ({ action: 'update' } as const);
|
|
193
|
+
}),
|
|
194
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
195
|
+
// ⚠️ `dropUnreadable`: reconcile only runs once `provision` already minted for the
|
|
196
|
+
// read that fed `diff`'s `update` verdict, so `UNREADABLE` here is a narrow race —
|
|
197
|
+
// but UNLIKE group/role/user, the message below assumes genuine absence and would
|
|
198
|
+
// misname a transient credential refusal as "this resource does not create tokens".
|
|
199
|
+
// Rare (the SAME role must mint twice, once for diff/once for reconcile, and only the
|
|
200
|
+
// second is denied) and not silently wrong — the operator still sees a die() and goes
|
|
201
|
+
// looking — but named here rather than left for the next reader to assume away.
|
|
202
|
+
const before = dropUnreadable(yield* readToken(news));
|
|
203
|
+
if (before === undefined) {
|
|
204
|
+
return yield* Effect.die(
|
|
205
|
+
new Error(
|
|
206
|
+
`${news.userid}!${news.tokenid}: this resource does not create API tokens. PVE ` +
|
|
207
|
+
'returns the secret only in the create response and it cannot be stored, so a ' +
|
|
208
|
+
'token made here would be a live credential nobody holds. Create it with ' +
|
|
209
|
+
'`pveum user token add` and capture the value, or mint a short-lived one from ' +
|
|
210
|
+
'the OpenBao proxmox mount (`bao read proxmox-c1/creds/<role>`) -- then declare ' +
|
|
211
|
+
'it here to manage it.',
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
yield* guardWrite(API_TOKEN_UPDATE, shape(news), false);
|
|
216
|
+
if (!matches(before, news)) {
|
|
217
|
+
yield* runPve(news.target, 'provision', true, access.putAccessUserToken(shape(news)));
|
|
218
|
+
}
|
|
219
|
+
const after = dropUnreadable(yield* readToken(news));
|
|
220
|
+
if (after === undefined) {
|
|
221
|
+
return yield* Effect.die(
|
|
222
|
+
new Error(
|
|
223
|
+
`${news.userid}!${news.tokenid}: the write returned no error but the token is ` +
|
|
224
|
+
'still absent. PVE wraps every answer in {"data":...} and can report success ' +
|
|
225
|
+
'on a call that did nothing -- read back rather than trusting the status code.',
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return after;
|
|
230
|
+
}),
|
|
231
|
+
delete: Effect.fn(function* ({ olds }) {
|
|
232
|
+
yield* runPve(
|
|
233
|
+
olds.target,
|
|
234
|
+
'provision',
|
|
235
|
+
true,
|
|
236
|
+
access.deleteAccessUserToken({ tokenid: olds.tokenid, userid: olds.userid }),
|
|
237
|
+
);
|
|
238
|
+
}),
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
);
|
package/src/proxmox/client.ts
CHANGED
|
@@ -15,14 +15,9 @@
|
|
|
15
15
|
import * as Effect from 'effect/Effect';
|
|
16
16
|
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
17
17
|
import * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
18
|
-
import {
|
|
19
|
-
type ApiTarget,
|
|
20
|
-
type PveCredential,
|
|
21
|
-
type PveRole,
|
|
22
|
-
type PveTarget,
|
|
23
|
-
authorization,
|
|
24
|
-
} from './credentials.ts';
|
|
18
|
+
import type { ApiTarget, PveCredential, PveRole, PveTarget } from './credentials.ts';
|
|
25
19
|
import { leased } from './lease-cache.ts';
|
|
20
|
+
import { authorization } from './mint.ts';
|
|
26
21
|
import { executeOnCluster } from './members.ts';
|
|
27
22
|
|
|
28
23
|
export class PveError extends Error {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* there).
|
|
17
17
|
*/
|
|
18
18
|
import { describe, expect, test } from 'bun:test';
|
|
19
|
-
import { shape as apiTokenForm } from './api-token-form.ts';
|
|
20
19
|
import { NETWORK_APPLY_ENDPOINT, SDN_APPLY_ENDPOINT } from './apply-endpoints.ts';
|
|
21
20
|
import { constraintsFor, formViolations } from './constraint-guard.ts';
|
|
22
21
|
import type { PveTarget } from './credentials.ts';
|
|
@@ -30,32 +29,13 @@ const TARGET: PveTarget = { members: ['pve.test:8006'], mount: 'pve-test', schem
|
|
|
30
29
|
const onCreate = (key: string, form: Record<string, string | readonly string[]>) =>
|
|
31
30
|
formViolations(key, form, true);
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
comment: 'Declared by Alchemy',
|
|
41
|
-
expire: 0,
|
|
42
|
-
privsep: false,
|
|
43
|
-
target: TARGET,
|
|
44
|
-
tokenid: 'apply',
|
|
45
|
-
userid: 'iac@pve',
|
|
46
|
-
}),
|
|
47
|
-
),
|
|
48
|
-
).toEqual([]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
/** ⛔ `{userid}` AND `{tokenid}` ARE PATH SEGMENTS. A table that kept them would refuse every
|
|
52
|
-
* create for a "missing" parameter that was never missing (codegen/emit.ts). */
|
|
53
|
-
test('neither path parameter is in the table', () => {
|
|
54
|
-
const table = constraintsFor('pve:POST /access/users/{userid}/token/{tokenid}');
|
|
55
|
-
expect(table['userid']).toBeUndefined();
|
|
56
|
-
expect(table['tokenid']).toBeUndefined();
|
|
57
|
-
});
|
|
58
|
-
});
|
|
32
|
+
// ⛔ Proxmox.ApiToken's own create-endpoint check was REMOVED HERE 2026-09-24, not merely
|
|
33
|
+
// updated: the distilled migration (api-token.ts) confirmed the create path was already
|
|
34
|
+
// unreachable (reconcile refuses before any POST — see that file's header) and stopped
|
|
35
|
+
// guarding a create form at all, so `'pve:POST /access/users/{userid}/token/{tokenid}'` is no
|
|
36
|
+
// longer named anywhere in this package's source. `codegen/constraints.ts` (its own `keysFromSource`)
|
|
37
|
+
// dropped the table for it on the next regeneration — there is nothing left here to test
|
|
38
|
+
// against, and keeping this block would fail with "no vendor constraint table" forever.
|
|
59
39
|
|
|
60
40
|
describe('Proxmox.MetricServer plans clean against POST /cluster/metrics/server/{id}', () => {
|
|
61
41
|
/**
|