@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.
Files changed (157) hide show
  1. package/README.md +22 -0
  2. package/dist/discord/application-command.d.ts +52 -0
  3. package/dist/discord/application-command.d.ts.map +1 -0
  4. package/dist/discord/command-form.d.ts +62 -0
  5. package/dist/discord/command-form.d.ts.map +1 -0
  6. package/dist/discord/credentials.d.ts +35 -0
  7. package/dist/discord/credentials.d.ts.map +1 -0
  8. package/dist/discord/fake-discord.d.ts +44 -0
  9. package/dist/discord/fake-discord.d.ts.map +1 -0
  10. package/dist/discord/guild-application-command.d.ts +53 -0
  11. package/dist/discord/guild-application-command.d.ts.map +1 -0
  12. package/dist/discord/index.d.ts +12 -0
  13. package/dist/discord/index.d.ts.map +1 -0
  14. package/dist/discord/index.js +193 -0
  15. package/dist/discord/index.js.map +15 -0
  16. package/dist/discord/providers.d.ts +15 -0
  17. package/dist/discord/providers.d.ts.map +1 -0
  18. package/dist/discord/resource.d.ts +75 -0
  19. package/dist/discord/resource.d.ts.map +1 -0
  20. package/dist/proxmox/acl-wire.d.ts +26 -0
  21. package/dist/proxmox/acl-wire.d.ts.map +1 -0
  22. package/dist/proxmox/acl.d.ts.map +1 -1
  23. package/dist/proxmox/api-token-form.d.ts +28 -26
  24. package/dist/proxmox/api-token-form.d.ts.map +1 -1
  25. package/dist/proxmox/api-token.d.ts +48 -89
  26. package/dist/proxmox/api-token.d.ts.map +1 -1
  27. package/dist/proxmox/ceph-fs-wire.d.ts +2 -2
  28. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
  29. package/dist/proxmox/ceph-osd-tree.d.ts +1 -1
  30. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -1
  31. package/dist/proxmox/ceph-osd-write.d.ts +2 -2
  32. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  33. package/dist/proxmox/ceph-pool-settle.d.ts +1 -1
  34. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
  35. package/dist/proxmox/client.d.ts +2 -2
  36. package/dist/proxmox/client.d.ts.map +1 -1
  37. package/dist/proxmox/credential-errors.d.ts +27 -0
  38. package/dist/proxmox/credential-errors.d.ts.map +1 -0
  39. package/dist/proxmox/credentials.d.ts +0 -38
  40. package/dist/proxmox/credentials.d.ts.map +1 -1
  41. package/dist/proxmox/distilled-guard.d.ts +31 -0
  42. package/dist/proxmox/distilled-guard.d.ts.map +1 -0
  43. package/dist/proxmox/distilled-pve.d.ts +11 -4
  44. package/dist/proxmox/distilled-pve.d.ts.map +1 -1
  45. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  46. package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
  47. package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -1
  48. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
  49. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
  50. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
  51. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
  52. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
  53. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
  54. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
  55. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
  56. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
  57. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
  58. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
  59. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
  60. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
  61. package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
  62. package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
  63. package/dist/proxmox/group-wire.d.ts +29 -0
  64. package/dist/proxmox/group-wire.d.ts.map +1 -0
  65. package/dist/proxmox/group.d.ts +20 -24
  66. package/dist/proxmox/group.d.ts.map +1 -1
  67. package/dist/proxmox/index.js +725 -509
  68. package/dist/proxmox/index.js.map +40 -33
  69. package/dist/proxmox/lease-cache.d.ts +1 -1
  70. package/dist/proxmox/lease-cache.d.ts.map +1 -1
  71. package/dist/proxmox/lxc-lifecycle.d.ts +5 -5
  72. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
  73. package/dist/proxmox/mint.d.ts +50 -0
  74. package/dist/proxmox/mint.d.ts.map +1 -0
  75. package/dist/proxmox/network-apply-read.d.ts +2 -2
  76. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  77. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +2 -2
  78. package/dist/proxmox/pbs-sync-job-form.d.ts +2 -2
  79. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -1
  80. package/dist/proxmox/resource.d.ts +4 -4
  81. package/dist/proxmox/role-wire.d.ts +28 -0
  82. package/dist/proxmox/role-wire.d.ts.map +1 -0
  83. package/dist/proxmox/role.d.ts +36 -52
  84. package/dist/proxmox/role.d.ts.map +1 -1
  85. package/dist/proxmox/sdn-apply-read.d.ts +1 -1
  86. package/dist/proxmox/sdn-apply-read.d.ts.map +1 -1
  87. package/dist/proxmox/unreadable-read.d.ts +50 -0
  88. package/dist/proxmox/unreadable-read.d.ts.map +1 -0
  89. package/dist/proxmox/user-wire.d.ts +18 -0
  90. package/dist/proxmox/user-wire.d.ts.map +1 -0
  91. package/dist/proxmox/user.d.ts +25 -32
  92. package/dist/proxmox/user.d.ts.map +1 -1
  93. package/dist/proxmox/zfs-pool-write.d.ts +2 -2
  94. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
  95. package/docs/credentials.md +9 -0
  96. package/docs/discord.md +129 -0
  97. package/docs/upstream-conformance.md +17 -1
  98. package/package.json +5 -1
  99. package/src/discord/application-command.test.ts +103 -0
  100. package/src/discord/application-command.ts +109 -0
  101. package/src/discord/command-form.test.ts +144 -0
  102. package/src/discord/command-form.ts +133 -0
  103. package/src/discord/credentials.ts +36 -0
  104. package/src/discord/fake-discord.ts +70 -0
  105. package/src/discord/guild-application-command.test.ts +215 -0
  106. package/src/discord/guild-application-command.ts +129 -0
  107. package/src/discord/index.ts +31 -0
  108. package/src/discord/providers.ts +18 -0
  109. package/src/discord/resource.ts +164 -0
  110. package/src/discord/retry.test.ts +74 -0
  111. package/src/proxmox/acl-wire.ts +83 -0
  112. package/src/proxmox/acl.ts +29 -82
  113. package/src/proxmox/adopt-noop.test.ts +18 -0
  114. package/src/proxmox/api-token-adopt.test.ts +28 -12
  115. package/src/proxmox/api-token-form.ts +50 -93
  116. package/src/proxmox/api-token.ts +149 -135
  117. package/src/proxmox/client.ts +2 -7
  118. package/src/proxmox/constraints-forms.test.ts +7 -27
  119. package/src/proxmox/credential-errors.ts +33 -0
  120. package/src/proxmox/credentials.test.ts +29 -3
  121. package/src/proxmox/credentials.ts +4 -134
  122. package/src/proxmox/distilled-guard.ts +42 -0
  123. package/src/proxmox/distilled-pve.ts +15 -4
  124. package/src/proxmox/generated/constraints/index.ts +1 -1
  125. package/src/proxmox/generated/constraints/pve-access.ts +1 -4
  126. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
  127. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
  128. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
  129. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
  130. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
  131. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
  132. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
  133. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
  134. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
  135. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
  136. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
  137. package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
  138. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
  139. package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
  140. package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
  141. package/src/proxmox/group-wire.ts +63 -0
  142. package/src/proxmox/group.test.ts +110 -0
  143. package/src/proxmox/group.ts +135 -105
  144. package/src/proxmox/lease-cache.ts +2 -1
  145. package/src/proxmox/lxc-lifecycle.ts +2 -1
  146. package/src/proxmox/mint.ts +158 -0
  147. package/src/proxmox/network-apply.ts +1 -1
  148. package/src/proxmox/provision-cli-fake.ts +11 -0
  149. package/src/proxmox/provision-declare.test.ts +6 -1
  150. package/src/proxmox/role-wire.ts +49 -0
  151. package/src/proxmox/role.test.ts +89 -0
  152. package/src/proxmox/role.ts +147 -106
  153. package/src/proxmox/unreadable-read.test.ts +83 -0
  154. package/src/proxmox/unreadable-read.ts +60 -0
  155. package/src/proxmox/user-wire.ts +112 -0
  156. package/src/proxmox/user.test.ts +142 -0
  157. 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. Create and update send exactly this.
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: `${props.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
- export const apiTokenSpec: PveSpec<ApiTokenProps, ApiTokenAttributes> = {
84
- // ⛔ THE ITEM READ NEEDS `User.Modify`, WHICH THE AUDITOR-SHAPED READ LEASE DOES NOT HAVE. The
85
- // measurement and the consequence are in the header's last ⚠️; the mechanism — a silent
86
- // "absent" rather than a 403 — is the ⛔ on `readRole` in resource.ts.
87
- readRole: 'provision',
88
- /**
89
- * ★ NOTHING PVE REPORTS HERE IS UNWRITABLE, WHICH IS UNUSUAL IN THIS PACKAGE AND IS WHY
90
- * `matches` can compare everything it reads. The GET returns exactly `comment`, `expire` and
91
- * `privsep`; PUT accepts exactly those three. There is no autoscaled field, no server-assigned
92
- * id, no set whose order PVE reshuffles — the three killers the neighbouring files carry
93
- * ⚠️s about do not arise. The two identity fields and `fullTokenid` come from props and are
94
- * true by construction, so comparing them would be theatre.
95
- * ⚠️ `undefined` WHEN NEITHER FLAG IS THERE, BECAUSE BOTH ARE ALWAYS THERE. `user.cfg`
96
- * materialises `expire` and `privsep` for every token it stores, so an answer carrying neither
97
- * is not a token — and without this guard `bool`/`int` would invent one out of their fallbacks
98
- * and the read-back guard in `reconcile` could never fire.
99
- */
100
- attributes: (live, props) => {
101
- const expire = live['expire'];
102
- const privsep = live['privsep'];
103
- if (expire === undefined && privsep === undefined) return undefined;
104
- return {
105
- comment: text(live['comment']),
106
- expire: int(expire, 0),
107
- fullTokenid: fullTokenid(props),
108
- /** ⚠️ FALLBACK `true`, MATCHING PVE'S API DEFAULT — unreachable given the guard above, but
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;
@@ -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 (`/usr/share/pve-docs/api-viewer/apidoc.js` on node-b, 2026-09-13): POST returns
6
- * `["full-tokenid","info","value"]`; PUT returns `["comment","expire","full-tokenid","privsep",
7
- * "value"]` with `value` present ONLY when `regenerate` was set; GET returns
8
- * `["comment","expire","privsep"]` and never the secret. A live token agrees —
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` drops the
15
- * value on the floor, and what lands is a live credential in `/etc/pve/user.cfg` whose secret
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. Rotation belongs to the mount
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` — read from
40
- * `<estate>/platform/secrets/vault/plugin-proxmox/proxmox/`: `privsep=0` (client.go), `expire` set
41
- * to the lease deadline, and an id the plugin CHOOSES,
42
- * `hf-<role>-<actor>-<entity6>-<stamp>-<nonce>` (tokenname.go). Revocation and WAL rollback look
43
- * up that one exact id (wal.go); neither sweeps a prefix. So the two systems cannot collide on
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, unlike `GET /access/users/{userid}`, which has one. And
54
- * `pvesh get /access/permissions --userid hf-read@pve --path /access/groups` answers the seven
55
- * PVEAuditor audit privileges and no `User.Modify`, while `hf-provision@pve` has it. So the
56
- * 3600s read lease reaches at most its OWN account's tokens and is refused for every other
57
- * userid — which `read` in resource.ts folds into "absent", so the plan says create and the POST
58
- * then fails with "Token already exists". Hence `readRole: 'provision'` below, for the same
59
- * reason storage.ts, sdn-zone.ts and sdn-vnet.ts set it, at the same cost: every plan mints a
60
- * 300s non-renewable provision lease just to read.
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 { apiTokenSpec } from './api-token-form.ts';
66
- import { type PveRequirements, type WithTarget, pveHandlers, pveOperations } from './resource.ts';
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, AND HERE THAT COSTS MORE THAN IT DOES FOR A USER. Editing this prop (or
82
- * `userid`) points `path` at a DIFFERENT token, which reads as absent and is then created —
83
- * leaving the old token LIVE, with its secret still working, invisible to the plan, while the
84
- * new one is the unusable kind described at the top. Rename by declaring a delete and a
85
- * create, the way user.ts says for an account.
86
- * ⛔ `diff` IS NOT OVERRIDDEN TO SAY `replace` THE WAY acl.ts DOES, AND THE DIFFERENCE IS THE
87
- * OBJECT. There, replace removes a grant that can be rebuilt from its own tuple; here it
88
- * would DELETE a live token — destroying an irrecoverable secret, and everything using it —
89
- * to tidy up a name. Leaving the old token standing is the lesser harm, and saying so here
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, `user.cfg` stores a token as `token:<id>:<expire>:<privsep>:<comment>:`
110
- * with no absent state, and the parser does `$expire = 0 if !$expire` (AccessControl.pm:1624).
111
- * MEASURED end to end: every token on this cluster that was created without an expire reads
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. PVE's API default
124
- * is 1, every token on this cluster is 0 (measured), and `user.cfg` materialises it either way
125
- * — `$privsep = $privsep ? 1 : 0` (AccessControl.pm:1622), so there is no unset state to
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 — the exact
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, a replacement is a DIFFERENT
163
- * value, and every holder loses access the instant `cfs_write_file` returns — with no error
164
- * raised anywhere near them. The live cluster's tokens are `metrics@pve!exporter` (the PVE
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
- const ops = pveOperations(apiTokenSpec);
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
- * ⛔ THIS FAMILY ADOPTS AND MANAGES TOKENS; IT REFUSES TO MINT ONE, AND THAT IS A DECISION RATHER
180
- * THAN A GAP. The secret exists ONLY in the create response — MEASURED from the schema: `POST`
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
- * ⛔ `list` IS EMPTY, AND FOR THIS FAMILY THAT MATTERS MORE THAN THE GENERIC ARGUMENT IN
221
- * resource.ts. A token index handed to Alchemy would offer up `mint@pve!engine` and every
222
- * live OpenBao lease for adoption — and adoption is what makes a later plan willing to delete.
223
- * The mount's leases in particular appear and vanish on their own; anything that adopted one
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(ProxmoxApiToken, Effect.succeed(ProxmoxApiToken.Provider.of(handlers)));
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
+ );
@@ -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
- describe('Proxmox.ApiToken plans clean against POST /access/users/{userid}/token/{tokenid}', () => {
34
- /** ⚠️ `expire: 0` is "never", and PVE's `minimum: 0` accepts it — the bound the table carries. */
35
- test('the estate-shaped token declaration has no violations', () => {
36
- expect(
37
- onCreate(
38
- 'pve:POST /access/users/{userid}/token/{tokenid}',
39
- apiTokenForm({
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
  /**