@homeflare/alchemy 0.4.0 → 0.5.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.
Files changed (53) hide show
  1. package/README.md +57 -0
  2. package/dist/openbao/approle-login-form.d.ts +72 -0
  3. package/dist/openbao/approle-login-form.d.ts.map +1 -0
  4. package/dist/openbao/approle-login-result.d.ts +39 -0
  5. package/dist/openbao/approle-login-result.d.ts.map +1 -0
  6. package/dist/openbao/approle-login.d.ts +77 -0
  7. package/dist/openbao/approle-login.d.ts.map +1 -0
  8. package/dist/openbao/bao-http.d.ts +2 -0
  9. package/dist/openbao/bao-http.d.ts.map +1 -1
  10. package/dist/openbao/index.d.ts +7 -0
  11. package/dist/openbao/index.d.ts.map +1 -1
  12. package/dist/openbao/index.js +589 -305
  13. package/dist/openbao/index.js.map +16 -8
  14. package/dist/openbao/mount-path.d.ts +14 -0
  15. package/dist/openbao/mount-path.d.ts.map +1 -0
  16. package/dist/openbao/plugin-form.d.ts +60 -0
  17. package/dist/openbao/plugin-form.d.ts.map +1 -0
  18. package/dist/openbao/plugin-reconcile.d.ts +18 -0
  19. package/dist/openbao/plugin-reconcile.d.ts.map +1 -0
  20. package/dist/openbao/plugin-wire.d.ts +45 -0
  21. package/dist/openbao/plugin-wire.d.ts.map +1 -0
  22. package/dist/openbao/plugin.d.ts +31 -0
  23. package/dist/openbao/plugin.d.ts.map +1 -0
  24. package/dist/openbao/ssh-role-form.d.ts +6 -5
  25. package/dist/openbao/ssh-role-form.d.ts.map +1 -1
  26. package/dist/openbao/ssh-role.d.ts.map +1 -1
  27. package/dist/proxmox/index.d.ts +23 -20
  28. package/dist/proxmox/index.d.ts.map +1 -1
  29. package/dist/proxmox/index.js +19 -1
  30. package/dist/proxmox/index.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/openbao/approle-login-form.test.ts +64 -0
  33. package/src/openbao/approle-login-form.ts +153 -0
  34. package/src/openbao/approle-login-result.ts +96 -0
  35. package/src/openbao/approle-login.test.ts +200 -0
  36. package/src/openbao/approle-login.ts +156 -0
  37. package/src/openbao/auth-role-form.test.ts +42 -0
  38. package/src/openbao/bao-http.ts +2 -0
  39. package/src/openbao/index.ts +13 -0
  40. package/src/openbao/mount-path.test.ts +23 -0
  41. package/src/openbao/mount-path.ts +25 -0
  42. package/src/openbao/mount-wire.test.ts +13 -0
  43. package/src/openbao/plugin-form.test.ts +86 -0
  44. package/src/openbao/plugin-form.ts +164 -0
  45. package/src/openbao/plugin-reconcile.ts +92 -0
  46. package/src/openbao/plugin-wire.test.ts +146 -0
  47. package/src/openbao/plugin-wire.ts +65 -0
  48. package/src/openbao/plugin.ts +100 -0
  49. package/src/openbao/ssh-role-form.test.ts +105 -0
  50. package/src/openbao/ssh-role-form.ts +18 -16
  51. package/src/openbao/ssh-role.ts +3 -4
  52. package/src/proxmox/index.test.ts +53 -0
  53. package/src/proxmox/index.ts +26 -20
@@ -1,8 +1,301 @@
1
+ // src/openbao/approle-login.ts
2
+ import * as Effect2 from "effect/Effect";
3
+ import * as FetchHttpClient2 from "effect/unstable/http/FetchHttpClient";
4
+
5
+ // src/openbao/bao-http.ts
6
+ import * as Context from "effect/Context";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Option from "effect/Option";
9
+ import * as Schedule from "effect/Schedule";
10
+ import * as Semaphore from "effect/Semaphore";
11
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
12
+ import * as Headers from "effect/unstable/http/Headers";
13
+ import * as HttpClient from "effect/unstable/http/HttpClient";
14
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
15
+
16
+ // src/openbao/bao-address.ts
17
+ var DEFAULT_ADDRESS = "https://127.0.0.1:8200";
18
+ var UNIX = "unix://";
19
+ var readBaoVariable = (env, name) => env[name] ?? env[`VAULT_${name.slice("BAO_".length)}`] ?? "";
20
+ var resolveAddress = (env) => {
21
+ const agent = readBaoVariable(env, "BAO_AGENT_ADDR");
22
+ const direct = readBaoVariable(env, "BAO_ADDR");
23
+ const raw = agent !== "" ? agent : direct !== "" ? direct : DEFAULT_ADDRESS;
24
+ const namespace = readBaoVariable(env, "BAO_NAMESPACE");
25
+ const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
26
+ return {
27
+ base: socket === undefined ? raw.replace(/\/+$/, "") : "http://localhost",
28
+ namespace: namespace === "" ? undefined : namespace,
29
+ socket
30
+ };
31
+ };
32
+ var headersFor = (address, env) => {
33
+ const headers = { "X-Vault-Request": "true" };
34
+ if (address.namespace !== undefined)
35
+ headers["X-Vault-Namespace"] = address.namespace;
36
+ const token = readBaoVariable(env, "BAO_TOKEN");
37
+ if (token !== "")
38
+ headers["X-Vault-Token"] = token;
39
+ return headers;
40
+ };
41
+
42
+ // src/openbao/bao-status.ts
43
+ class BaoError extends Error {
44
+ status;
45
+ method;
46
+ path;
47
+ errors;
48
+ constructor(status, method, path, errors) {
49
+ const code = status === 0 ? "no response" : String(status);
50
+ super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join("; ") || "(no errors given)"}`);
51
+ this.status = status;
52
+ this.method = method;
53
+ this.path = path;
54
+ this.errors = errors;
55
+ this.name = "BaoError";
56
+ }
57
+ }
58
+ var objectOf = (text) => {
59
+ try {
60
+ const parsed = JSON.parse(text);
61
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
62
+ } catch {
63
+ return;
64
+ }
65
+ };
66
+ var errorsOf = (text) => {
67
+ const raw = objectOf(text)?.["errors"];
68
+ return Array.isArray(raw) && raw.every((entry) => typeof entry === "string") ? raw : undefined;
69
+ };
70
+ var settle = (intent, method, path, status, text) => {
71
+ const fail = (errors) => ({
72
+ error: new BaoError(status, method, path, errors)
73
+ });
74
+ if (status < 200 || status >= 300) {
75
+ const errors = errorsOf(text);
76
+ if (errors === undefined) {
77
+ return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);
78
+ }
79
+ if (status === 404 && intent !== "write")
80
+ return { body: undefined };
81
+ return fail(errors);
82
+ }
83
+ if (text.trim() === "") {
84
+ return intent === "read" ? fail(["a successful read with an empty body"]) : { body: undefined };
85
+ }
86
+ const body = objectOf(text);
87
+ return body === undefined ? fail(["a success body that is not a JSON object"]) : { body };
88
+ };
89
+
90
+ // src/openbao/bao-http.ts
91
+ var BaoEnv = Context.Reference("homeflare/openbao/BaoEnv", {
92
+ defaultValue: () => process.env
93
+ });
94
+ var MAX_IN_FLIGHT = 8;
95
+ var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
96
+ defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
97
+ });
98
+ var TIMEOUT = "60 seconds";
99
+ var retryTransport = (effect) => Effect.retry(effect, {
100
+ schedule: Schedule.spaced("750 millis"),
101
+ times: 2,
102
+ while: (error) => error.status === 0
103
+ });
104
+ var redactingToken = (effect) => Effect.gen(function* () {
105
+ const names = yield* Headers.CurrentRedactedNames;
106
+ return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
107
+ ...names,
108
+ "x-vault-token"
109
+ ]);
110
+ });
111
+ var overSocket = (socket, method, path) => (effect) => {
112
+ if (socket === undefined)
113
+ return effect;
114
+ if (typeof Bun === "undefined") {
115
+ return Effect.fail(new BaoError(0, method, path, ["a unix:// address needs Bun fetch; this is not Bun"]));
116
+ }
117
+ return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
118
+ const init = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
119
+ return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
120
+ });
121
+ };
122
+ var baoCall = (intent, method, path, body) => Effect.gen(function* () {
123
+ const env = yield* BaoEnv;
124
+ const address = resolveAddress(env);
125
+ const client = yield* HttpClient.HttpClient;
126
+ const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(HttpClientRequest.setHeaders(headersFor(address, env)), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
127
+ const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text2) => ({ status: response.status, text: text2 }))), Effect.timeout(TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
128
+ const gate = yield* BaoGate;
129
+ const { status, text } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
130
+ const outcome = settle(intent, method, path, status, text);
131
+ return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
132
+ });
133
+ var baoRead = (path) => Effect.map(baoCall("read", "GET", path), (body) => {
134
+ if (body === undefined)
135
+ return;
136
+ const data = body["data"];
137
+ return typeof data === "object" && data !== null ? data : body;
138
+ });
139
+ var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, path, body));
140
+ var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
141
+
142
+ // src/openbao/mount-path.ts
143
+ var trimTrailingSlashes = (text) => {
144
+ let end = text.length;
145
+ while (end > 0 && text[end - 1] === "/")
146
+ end -= 1;
147
+ return text.slice(0, end);
148
+ };
149
+ var trimSlashes = (text) => {
150
+ let start = 0;
151
+ while (start < text.length && text[start] === "/")
152
+ start += 1;
153
+ return trimTrailingSlashes(text.slice(start));
154
+ };
155
+ var sshMountOf = (mount) => trimTrailingSlashes(mount ?? "ssh");
156
+
157
+ // src/openbao/approle-login-form.ts
158
+ var failureOf = (status) => status === 0 ? "unreachable" : status >= 200 && status < 300 ? "response" : "refused";
159
+
160
+ class BaoLoginError extends Error {
161
+ reason;
162
+ status;
163
+ errors;
164
+ constructor(reason, status, errors, operation) {
165
+ const code = status === 0 ? reason : `${reason} ${String(status)}`;
166
+ super(`${operation}: ${code}: ${errors.join("; ") || "(no errors given)"}`);
167
+ this.reason = reason;
168
+ this.status = status;
169
+ this.errors = errors;
170
+ this.name = "BaoLoginError";
171
+ }
172
+ }
173
+ var refusal = (label, value) => {
174
+ if (typeof value !== "string" || value === "")
175
+ return `${label} is empty`;
176
+ if (value.trim() === "")
177
+ return `${label} is only whitespace`;
178
+ if (value.trim() !== value) {
179
+ return `${label} has leading or trailing whitespace (a file read keeps its newline; trim it)`;
180
+ }
181
+ return;
182
+ };
183
+ var loginMount = (mount) => trimSlashes(mount ?? "approle");
184
+ var mountProblem = (mount) => {
185
+ const path = loginMount(mount);
186
+ if (path === "")
187
+ return "mount is empty";
188
+ const segments = path.split("/");
189
+ if (/[\s?#%\\]/.test(path) || segments.some((part) => part === "" || part === "." || part === "..")) {
190
+ return `mount ${JSON.stringify(path)} is not a plain mount path`;
191
+ }
192
+ return;
193
+ };
194
+ var loginPath = (mount) => `auth/${loginMount(mount)}/login`;
195
+ var GO_ESCAPES = {
196
+ 7: "a",
197
+ 8: "b",
198
+ 9: "t",
199
+ 10: "n",
200
+ 11: "v",
201
+ 12: "f",
202
+ 13: "r"
203
+ };
204
+ var goQuoted = (value) => [...value].map((char) => {
205
+ const code = char.codePointAt(0) ?? 0;
206
+ const named = GO_ESCAPES[code];
207
+ if (char === '"' || char === "\\")
208
+ return `\\${char}`;
209
+ if (named !== undefined)
210
+ return `\\${named}`;
211
+ if (code < 32 || code === 127)
212
+ return `\\x${code.toString(16).padStart(2, "0")}`;
213
+ return char;
214
+ }).join("");
215
+ var redact = (errors, secrets) => {
216
+ const forms = [...new Set(secrets.flatMap((secret) => [secret, goQuoted(secret)]))].filter((form) => form !== "").sort((a, b) => b.length - a.length);
217
+ return errors.map((entry) => forms.reduce((text, form) => text.split(form).join("[redacted]"), entry));
218
+ };
219
+
220
+ // src/openbao/approle-login-result.ts
221
+ class LoginResult {
222
+ #token;
223
+ accessor;
224
+ policies;
225
+ leaseDurationSeconds;
226
+ constructor(token, accessor, policies, lease) {
227
+ this.#token = token;
228
+ this.accessor = accessor;
229
+ this.policies = Object.freeze([...policies]);
230
+ this.leaseDurationSeconds = lease;
231
+ Object.freeze(this);
232
+ }
233
+ get clientToken() {
234
+ return this.#token;
235
+ }
236
+ }
237
+ var strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
238
+ var loginOf = (body) => {
239
+ const auth = body?.["auth"];
240
+ if (typeof auth !== "object" || auth === null || Array.isArray(auth)) {
241
+ return { problem: "the answer carried no auth block" };
242
+ }
243
+ const fields = auth;
244
+ const token = fields["client_token"];
245
+ if (typeof token !== "string" || token === "") {
246
+ const mfa = fields["mfa_requirement"];
247
+ return {
248
+ problem: mfa !== undefined && mfa !== null ? "the login requires MFA, which this helper does not complete" : "the auth block carried no client_token"
249
+ };
250
+ }
251
+ const lease = fields["lease_duration"];
252
+ return {
253
+ login: new LoginResult(token, typeof fields["accessor"] === "string" ? fields["accessor"] : "", strings(fields["policies"]), typeof lease === "number" && Number.isFinite(lease) ? lease : 0)
254
+ };
255
+ };
256
+
257
+ // src/openbao/approle-login.ts
258
+ var LOGIN = "OpenBao AppRole login";
259
+ var REVOKE = "OpenBao revoke-self";
260
+ var failed = (operation, secrets) => (error) => new BaoLoginError(failureOf(error.status), error.status, redact(error.errors, secrets), operation);
261
+ var appRoleLoginEffect = (input) => Effect2.gen(function* () {
262
+ const problems = [
263
+ refusal("roleId", input.roleId),
264
+ refusal("secretId", input.secretId),
265
+ mountProblem(input.mount)
266
+ ].filter((problem) => problem !== undefined);
267
+ if (problems.length > 0)
268
+ return yield* Effect2.fail(new BaoLoginError("input", 0, problems, LOGIN));
269
+ const env = yield* BaoEnv;
270
+ const body = yield* baoCall("write", "PUT", loginPath(input.mount), {
271
+ role_id: input.roleId,
272
+ secret_id: input.secretId
273
+ }).pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: "" }), Effect2.mapError(failed(LOGIN, [input.roleId, input.secretId])));
274
+ const read = loginOf(body);
275
+ if ("problem" in read) {
276
+ return yield* Effect2.fail(new BaoLoginError("response", 0, [read.problem], LOGIN));
277
+ }
278
+ return read.login;
279
+ });
280
+ var revokeSelfEffect = (token) => Effect2.gen(function* () {
281
+ const problem = refusal("token", token);
282
+ if (problem !== undefined) {
283
+ return yield* Effect2.fail(new BaoLoginError("input", 0, [problem], REVOKE));
284
+ }
285
+ const env = yield* BaoEnv;
286
+ yield* baoCall("write", "PUT", "auth/token/revoke-self").pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: token }), Effect2.mapError(failed(REVOKE, [token])));
287
+ });
288
+ var runWith = (env, effect) => {
289
+ const scoped = env === undefined ? effect : Effect2.provideService(effect, BaoEnv, env);
290
+ return Effect2.runPromise(scoped.pipe(Effect2.provide(FetchHttpClient2.layer)));
291
+ };
292
+ var appRoleLogin = (input) => runWith(input.env, appRoleLoginEffect(input));
293
+ var revokeSelf = (token, env) => runWith(env, revokeSelfEffect(token));
1
294
  // src/openbao/auth-method.ts
2
295
  import { Resource } from "alchemy";
3
296
  import { isResolved } from "alchemy/Diff";
4
297
  import * as Provider from "alchemy/Provider";
5
- import * as Effect3 from "effect/Effect";
298
+ import * as Effect4 from "effect/Effect";
6
299
 
7
300
  // src/openbao/digest.ts
8
301
  var canonical = (text) => text.trimEnd();
@@ -143,151 +436,12 @@ var matches2 = (attributes, props) => attributes.type === props.type && (props.d
143
436
  var wantsTune2 = (props) => props.description !== undefined || props.defaultLeaseTtl !== undefined || props.maxLeaseTtl !== undefined;
144
437
 
145
438
  // src/openbao/auth-method-wire.ts
146
- import * as Effect2 from "effect/Effect";
147
-
148
- // src/openbao/bao-http.ts
149
- import * as Context from "effect/Context";
150
- import * as Effect from "effect/Effect";
151
- import * as Option from "effect/Option";
152
- import * as Schedule from "effect/Schedule";
153
- import * as Semaphore from "effect/Semaphore";
154
- import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
155
- import * as Headers from "effect/unstable/http/Headers";
156
- import * as HttpClient from "effect/unstable/http/HttpClient";
157
- import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
158
-
159
- // src/openbao/bao-address.ts
160
- var DEFAULT_ADDRESS = "https://127.0.0.1:8200";
161
- var UNIX = "unix://";
162
- var readBaoVariable = (env, name) => env[name] ?? env[`VAULT_${name.slice("BAO_".length)}`] ?? "";
163
- var resolveAddress = (env) => {
164
- const agent = readBaoVariable(env, "BAO_AGENT_ADDR");
165
- const direct = readBaoVariable(env, "BAO_ADDR");
166
- const raw = agent !== "" ? agent : direct !== "" ? direct : DEFAULT_ADDRESS;
167
- const namespace = readBaoVariable(env, "BAO_NAMESPACE");
168
- const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
169
- return {
170
- base: socket === undefined ? raw.replace(/\/+$/, "") : "http://localhost",
171
- namespace: namespace === "" ? undefined : namespace,
172
- socket
173
- };
174
- };
175
- var headersFor = (address, env) => {
176
- const headers = { "X-Vault-Request": "true" };
177
- if (address.namespace !== undefined)
178
- headers["X-Vault-Namespace"] = address.namespace;
179
- const token = readBaoVariable(env, "BAO_TOKEN");
180
- if (token !== "")
181
- headers["X-Vault-Token"] = token;
182
- return headers;
183
- };
184
-
185
- // src/openbao/bao-status.ts
186
- class BaoError extends Error {
187
- status;
188
- method;
189
- path;
190
- errors;
191
- constructor(status, method, path, errors) {
192
- const code = status === 0 ? "no response" : String(status);
193
- super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join("; ") || "(no errors given)"}`);
194
- this.status = status;
195
- this.method = method;
196
- this.path = path;
197
- this.errors = errors;
198
- this.name = "BaoError";
199
- }
200
- }
201
- var objectOf = (text3) => {
202
- try {
203
- const parsed = JSON.parse(text3);
204
- return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
205
- } catch {
206
- return;
207
- }
208
- };
209
- var errorsOf = (text3) => {
210
- const raw = objectOf(text3)?.["errors"];
211
- return Array.isArray(raw) && raw.every((entry) => typeof entry === "string") ? raw : undefined;
212
- };
213
- var settle = (intent, method, path, status, text3) => {
214
- const fail = (errors) => ({
215
- error: new BaoError(status, method, path, errors)
216
- });
217
- if (status < 200 || status >= 300) {
218
- const errors = errorsOf(text3);
219
- if (errors === undefined) {
220
- return fail([`not an OpenBao error body (${String(text3.length)} bytes)`]);
221
- }
222
- if (status === 404 && intent !== "write")
223
- return { body: undefined };
224
- return fail(errors);
225
- }
226
- if (text3.trim() === "") {
227
- return intent === "read" ? fail(["a successful read with an empty body"]) : { body: undefined };
228
- }
229
- const body = objectOf(text3);
230
- return body === undefined ? fail(["a success body that is not a JSON object"]) : { body };
231
- };
232
-
233
- // src/openbao/bao-http.ts
234
- var BaoEnv = Context.Reference("homeflare/openbao/BaoEnv", {
235
- defaultValue: () => process.env
236
- });
237
- var MAX_IN_FLIGHT = 8;
238
- var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
239
- defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
240
- });
241
- var TIMEOUT = "60 seconds";
242
- var retryTransport = (effect) => Effect.retry(effect, {
243
- schedule: Schedule.spaced("750 millis"),
244
- times: 2,
245
- while: (error) => error.status === 0
246
- });
247
- var redactingToken = (effect) => Effect.gen(function* () {
248
- const names = yield* Headers.CurrentRedactedNames;
249
- return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
250
- ...names,
251
- "x-vault-token"
252
- ]);
253
- });
254
- var overSocket = (socket, method, path) => (effect) => {
255
- if (socket === undefined)
256
- return effect;
257
- if (typeof Bun === "undefined") {
258
- return Effect.fail(new BaoError(0, method, path, ["a unix:// address needs Bun fetch; this is not Bun"]));
259
- }
260
- return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
261
- const init = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
262
- return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
263
- });
264
- };
265
- var baoCall = (intent, method, path, body) => Effect.gen(function* () {
266
- const env = yield* BaoEnv;
267
- const address = resolveAddress(env);
268
- const client = yield* HttpClient.HttpClient;
269
- const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(HttpClientRequest.setHeaders(headersFor(address, env)), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
270
- const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text4) => ({ status: response.status, text: text4 }))), Effect.timeout(TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
271
- const gate = yield* BaoGate;
272
- const { status, text: text3 } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
273
- const outcome = settle(intent, method, path, status, text3);
274
- return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
275
- });
276
- var baoRead = (path) => Effect.map(baoCall("read", "GET", path), (body) => {
277
- if (body === undefined)
278
- return;
279
- const data = body["data"];
280
- return typeof data === "object" && data !== null ? data : body;
281
- });
282
- var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, path, body));
283
- var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
284
-
285
- // src/openbao/auth-method-wire.ts
286
- var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect2.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
439
+ import * as Effect3 from "effect/Effect";
440
+ var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect3.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
287
441
  ...asked.errors,
288
442
  `sys/auth could not confirm the method is absent: ${listing.message}`
289
- ])), Effect2.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${authPath(path)}/`) ? Effect2.succeed(undefined) : Effect2.fail(asked)));
290
- var readAuthMethodData = (path) => baoRead(readPath2(path)).pipe(Effect2.catchIf((error) => error.status === 400, (error) => absentFromTable(path, error)));
443
+ ])), Effect3.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${authPath(path)}/`) ? Effect3.succeed(undefined) : Effect3.fail(asked)));
444
+ var readAuthMethodData = (path) => baoRead(readPath2(path)).pipe(Effect3.catchIf((error) => error.status === 400, (error) => absentFromTable(path, error)));
291
445
  var enableAuthMethod = (props) => baoWrite("POST", readPath2(props.path), enableBody2(props));
292
446
  var tuneAuthMethod = (props) => baoWrite("POST", tunePath2(props.path), tuneBody2(props));
293
447
  var disableAuthMethod = (path) => baoDelete(readPath2(path));
@@ -296,18 +450,18 @@ var disableAuthMethod = (path) => baoDelete(readPath2(path));
296
450
  var BaoAuthMethod = Resource("Bao.AuthMethod", {
297
451
  defaultRemovalPolicy: "retain"
298
452
  });
299
- var readMethod = (props) => Effect3.gen(function* () {
453
+ var readMethod = (props) => Effect4.gen(function* () {
300
454
  const live = yield* readAuthMethodData(props.path);
301
455
  if (live === undefined)
302
456
  return;
303
457
  return attributesOf2(props, live);
304
458
  });
305
- var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed(BaoAuthMethod.Provider.of({
306
- list: () => Effect3.succeed([]),
307
- read: Effect3.fn(function* ({ olds }) {
459
+ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect4.succeed(BaoAuthMethod.Provider.of({
460
+ list: () => Effect4.succeed([]),
461
+ read: Effect4.fn(function* ({ olds }) {
308
462
  return yield* readMethod(olds);
309
463
  }),
310
- diff: Effect3.fn(function* ({ news, output }) {
464
+ diff: Effect4.fn(function* ({ news, output }) {
311
465
  if (output === undefined || !isResolved(news))
312
466
  return;
313
467
  const live = yield* readMethod(news);
@@ -319,7 +473,7 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
319
473
  return { action: "noop" };
320
474
  return { action: "update" };
321
475
  }),
322
- reconcile: Effect3.fn(function* ({ news }) {
476
+ reconcile: Effect4.fn(function* ({ news }) {
323
477
  let live = yield* readMethod(news);
324
478
  if (live === undefined) {
325
479
  yield* enableAuthMethod(news);
@@ -327,17 +481,17 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
327
481
  yield* tuneAuthMethod(news);
328
482
  } else if (!matches2(live, news)) {
329
483
  if (live.type !== news.type) {
330
- return yield* Effect3.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
484
+ return yield* Effect4.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
331
485
  }
332
486
  yield* tuneAuthMethod(news);
333
487
  }
334
488
  live = yield* readMethod(news);
335
489
  if (live === undefined) {
336
- return yield* Effect3.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
490
+ return yield* Effect4.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
337
491
  }
338
492
  return live;
339
493
  }),
340
- delete: Effect3.fn(function* ({ output }) {
494
+ delete: Effect4.fn(function* ({ output }) {
341
495
  yield* disableAuthMethod(output.path);
342
496
  return;
343
497
  })
@@ -346,7 +500,7 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
346
500
  import { Resource as Resource2 } from "alchemy";
347
501
  import { isResolved as isResolved2 } from "alchemy/Diff";
348
502
  import * as Provider2 from "alchemy/Provider";
349
- import * as Effect4 from "effect/Effect";
503
+ import * as Effect5 from "effect/Effect";
350
504
 
351
505
  // src/openbao/auth-role-form.ts
352
506
  var policiesOf = (live) => {
@@ -410,18 +564,18 @@ var matches3 = (attributes, props) => {
410
564
  var BaoAuthRole = Resource2("Bao.AuthRole", {
411
565
  defaultRemovalPolicy: "retain"
412
566
  });
413
- var readRole = (props) => Effect4.gen(function* () {
567
+ var readRole = (props) => Effect5.gen(function* () {
414
568
  const live = yield* baoRead(readPath3(props.name));
415
569
  if (live === undefined)
416
570
  return;
417
571
  return attributesOf3(props, live);
418
572
  });
419
- var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(BaoAuthRole.Provider.of({
420
- list: () => Effect4.succeed([]),
421
- read: Effect4.fn(function* ({ olds }) {
573
+ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect5.succeed(BaoAuthRole.Provider.of({
574
+ list: () => Effect5.succeed([]),
575
+ read: Effect5.fn(function* ({ olds }) {
422
576
  return yield* readRole(olds);
423
577
  }),
424
- diff: Effect4.fn(function* ({ news, output }) {
578
+ diff: Effect5.fn(function* ({ news, output }) {
425
579
  if (output === undefined || !isResolved2(news))
426
580
  return;
427
581
  const live = yield* readRole(news);
@@ -431,18 +585,18 @@ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(Ba
431
585
  return { action: "noop" };
432
586
  return { action: "update" };
433
587
  }),
434
- reconcile: Effect4.fn(function* ({ news }) {
588
+ reconcile: Effect5.fn(function* ({ news }) {
435
589
  const live = yield* readRole(news);
436
590
  if (live === undefined || !matches3(live, news)) {
437
591
  yield* baoWrite("PUT", readPath3(news.name), writeBody(news));
438
592
  }
439
593
  const after = yield* readRole(news);
440
594
  if (after === undefined) {
441
- return yield* Effect4.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
595
+ return yield* Effect5.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
442
596
  }
443
597
  return after;
444
598
  }),
445
- delete: Effect4.fn(function* ({ output }) {
599
+ delete: Effect5.fn(function* ({ output }) {
446
600
  yield* baoDelete(readPath3(output.name));
447
601
  return;
448
602
  })
@@ -451,7 +605,7 @@ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(Ba
451
605
  import { Resource as Resource3 } from "alchemy";
452
606
  import { isResolved as isResolved3 } from "alchemy/Diff";
453
607
  import * as Provider3 from "alchemy/Provider";
454
- import * as Effect7 from "effect/Effect";
608
+ import * as Effect8 from "effect/Effect";
455
609
 
456
610
  // src/openbao/cloudflare-group-scope.ts
457
611
  var zoneResource = (zoneId) => `com.cloudflare.api.account.zone.${zoneId}`;
@@ -471,7 +625,7 @@ var groupKey = (name, scope) => `${scope} ${name}`;
471
625
 
472
626
  // src/openbao/cloudflare-permission-groups.ts
473
627
  import * as Context2 from "effect/Context";
474
- import * as Effect6 from "effect/Effect";
628
+ import * as Effect7 from "effect/Effect";
475
629
 
476
630
  // src/openbao/cloudflare-permission-groups-solve.ts
477
631
  var observationsOf = (declared, live) => declared.flatMap((role) => {
@@ -641,7 +795,7 @@ var samePolicies2 = (want, have) => want.length === have.length && want.every((w
641
795
  });
642
796
 
643
797
  // src/openbao/cloudflare-role-wire.ts
644
- import * as Effect5 from "effect/Effect";
798
+ import * as Effect6 from "effect/Effect";
645
799
 
646
800
  // src/openbao/cloudflare-role-form.ts
647
801
  var rolePath = (mount, name) => `${mountPath(mount)}/roles/${name}`;
@@ -706,7 +860,7 @@ var differences = (props, policies, live) => {
706
860
  };
707
861
 
708
862
  // src/openbao/cloudflare-role-wire.ts
709
- var readCloudflareRole = (mount, name) => Effect5.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
863
+ var readCloudflareRole = (mount, name) => Effect6.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
710
864
  var writeCloudflareRole = (mount, name, body) => baoWrite("PUT", rolePath(mount, name), body);
711
865
  var deleteCloudflareRole = (mount, name) => baoDelete(rolePath(mount, name));
712
866
 
@@ -728,9 +882,9 @@ var pick = (mount, groups, refs, hint) => {
728
882
  const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));
729
883
  if (unknown.length > 0) {
730
884
  const problems = unknown.map((ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`);
731
- return Effect6.fail(new CloudflareGroupsError(mount, problems));
885
+ return Effect7.fail(new CloudflareGroupsError(mount, problems));
732
886
  }
733
- return Effect6.succeed(new Map(refs.flatMap((ref) => {
887
+ return Effect7.succeed(new Map(refs.flatMap((ref) => {
734
888
  const key = groupKey(ref.name, ref.scope);
735
889
  const id = groups.get(key);
736
890
  return id === undefined ? [] : [[key, id]];
@@ -738,15 +892,15 @@ var pick = (mount, groups, refs, hint) => {
738
892
  };
739
893
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
740
894
  var deniedHint = (mount) => `GET ${mountPath(mount)}/permission-groups answered 403 — this token's policy carries no ` + `\`path "${mountPath(mount)}/permission-groups" { capabilities = ["read"] }\` grant. ` + "It is declared in ../platform/secrets/vault/policies/homeflare-admin/" + "20-cloudflare-permission-groups.hcl and applies with `alchemy deploy` of this stack";
741
- var deniedNamed = (mount, read) => Effect6.catchIf(read, (error) => error.status === 403, () => Effect6.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
742
- var engineGroups = (mount) => Effect6.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
895
+ var deniedNamed = (mount, read) => Effect7.catchIf(read, (error) => error.status === 403, () => Effect7.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
896
+ var engineGroups = (mount) => Effect7.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
743
897
  const list = data?.["permission_groups"];
744
898
  if (data === undefined) {
745
899
  const why = "GET <mount>/permission-groups answered 404 — the endpoint is not deployed here";
746
- return Effect6.fail(new CloudflareGroupsError(mount, [why]));
900
+ return Effect7.fail(new CloudflareGroupsError(mount, [why]));
747
901
  }
748
902
  if (!Array.isArray(list)) {
749
- return Effect6.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
903
+ return Effect7.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
750
904
  }
751
905
  const scoped = new Map;
752
906
  const idsOf = new Map;
@@ -773,24 +927,24 @@ var engineGroups = (mount) => Effect6.flatMap(deniedNamed(mount, baoRead(`${moun
773
927
  const only = [...ids];
774
928
  return ids.size === 1 && only[0] !== undefined ? [[name, only[0]]] : [];
775
929
  }));
776
- return problems.length > 0 ? Effect6.fail(new CloudflareGroupsError(mount, problems)) : Effect6.succeed(new Map([...atEveryScope(sole), ...scoped]));
930
+ return problems.length > 0 ? Effect7.fail(new CloudflareGroupsError(mount, problems)) : Effect7.succeed(new Map([...atEveryScope(sole), ...scoped]));
777
931
  });
778
- var permissionGroupsFromEngine = () => Effect6.sync(() => {
932
+ var permissionGroupsFromEngine = () => Effect7.sync(() => {
779
933
  const cache = new Map;
780
- const cachedFor = (mount) => Effect6.suspend(() => {
934
+ const cachedFor = (mount) => Effect7.suspend(() => {
781
935
  const hit = cache.get(mountPath(mount));
782
936
  if (hit !== undefined)
783
937
  return hit;
784
- return Effect6.flatMap(Effect6.cached(engineGroups(mount)), (lookup) => {
938
+ return Effect7.flatMap(Effect7.cached(engineGroups(mount)), (lookup) => {
785
939
  cache.set(mountPath(mount), lookup);
786
940
  return lookup;
787
941
  });
788
942
  });
789
943
  return CloudflarePermissionGroups.of({
790
- resolve: (mount, refs) => Effect6.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
944
+ resolve: (mount, refs) => Effect7.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
791
945
  });
792
946
  });
793
- var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
947
+ var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
794
948
  const accountOf = new Map;
795
949
  const byAccount = new Map;
796
950
  for (const [mount, roles] of declared) {
@@ -801,9 +955,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
801
955
  }
802
956
  const solved = new Map;
803
957
  for (const [account, roles] of byAccount) {
804
- const solve = Effect6.gen(function* () {
958
+ const solve = Effect7.gen(function* () {
805
959
  const live = new Map;
806
- yield* Effect6.all(roles.map((role) => Effect6.map(readCloudflareRole(role.mount, role.name), (read) => {
960
+ yield* Effect7.all(roles.map((role) => Effect7.map(readCloudflareRole(role.mount, role.name), (read) => {
807
961
  if (read?.policies !== undefined) {
808
962
  live.set(`${role.mount}/${role.name}`, read.policies);
809
963
  }
@@ -811,11 +965,11 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
811
965
  const result = solveGroups(observationsOf(roles, live));
812
966
  const problems = describeProblems(result);
813
967
  if (problems.length > 0) {
814
- return yield* Effect6.fail(new CloudflareGroupsError(`account ${account}`, problems));
968
+ return yield* Effect7.fail(new CloudflareGroupsError(`account ${account}`, problems));
815
969
  }
816
970
  return atEveryScope(result.groups);
817
971
  });
818
- solved.set(account, yield* Effect6.cached(solve));
972
+ solved.set(account, yield* Effect7.cached(solve));
819
973
  }
820
974
  return CloudflarePermissionGroups.of({
821
975
  resolve: (mount, refs) => {
@@ -823,10 +977,10 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
823
977
  const lookup = account === undefined ? undefined : solved.get(account);
824
978
  if (account === undefined || lookup === undefined) {
825
979
  const why = "no declared role on this mount, so no live role can vouch for its groups";
826
- return Effect6.fail(new CloudflareGroupsError(mount, [why]));
980
+ return Effect7.fail(new CloudflareGroupsError(mount, [why]));
827
981
  }
828
982
  const hint = `no live role on account ${account} carries it; a NEW group needs the engine endpoint`;
829
- return Effect6.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
983
+ return Effect7.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
830
984
  }
831
985
  });
832
986
  });
@@ -835,9 +989,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
835
989
  var BaoCloudflareRole = Resource3("Bao.CloudflareRole", {
836
990
  defaultRemovalPolicy: "retain"
837
991
  });
838
- var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect7.gen(function* () {
992
+ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect8.gen(function* () {
839
993
  const groups = yield* CloudflarePermissionGroups;
840
- const resolved = (props) => Effect7.gen(function* () {
994
+ const resolved = (props) => Effect8.gen(function* () {
841
995
  const refs = new Map;
842
996
  for (const policy of props.policies) {
843
997
  const scope = policy.scope ?? scopeOfResource(policy.resource);
@@ -847,17 +1001,17 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
847
1001
  const ids = yield* groups.resolve(props.mount, [...refs.values()]);
848
1002
  const { missing, policies } = resolvePolicies(props.policies, ids);
849
1003
  if (missing.length > 0) {
850
- return yield* Effect7.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
1004
+ return yield* Effect8.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
851
1005
  }
852
1006
  return policies;
853
1007
  });
854
1008
  return BaoCloudflareRole.Provider.of({
855
- list: () => Effect7.succeed([]),
856
- read: Effect7.fn(function* ({ olds }) {
1009
+ list: () => Effect8.succeed([]),
1010
+ read: Effect8.fn(function* ({ olds }) {
857
1011
  const live = yield* readCloudflareRole(olds.mount, olds.name);
858
1012
  return live === undefined ? undefined : attributesOf4(olds, live);
859
1013
  }),
860
- diff: Effect7.fn(function* ({ news, output }) {
1014
+ diff: Effect8.fn(function* ({ news, output }) {
861
1015
  if (output === undefined || !isResolved3(news))
862
1016
  return;
863
1017
  if (refusalOf(news) !== undefined)
@@ -868,11 +1022,11 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
868
1022
  return { action: "update" };
869
1023
  return differences(news, policies, live).length === 0 ? { action: "noop" } : { action: "update" };
870
1024
  }),
871
- reconcile: Effect7.fn(function* ({ news }) {
1025
+ reconcile: Effect8.fn(function* ({ news }) {
872
1026
  const path = rolePath(news.mount, news.name);
873
- const refusal = refusalOf(news);
874
- if (refusal !== undefined) {
875
- return yield* Effect7.die(new Error(`Bao.CloudflareRole ${path}: ${refusal}.`));
1027
+ const refusal2 = refusalOf(news);
1028
+ if (refusal2 !== undefined) {
1029
+ return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
876
1030
  }
877
1031
  const policies = yield* resolved(news);
878
1032
  const live = yield* readCloudflareRole(news.mount, news.name);
@@ -881,15 +1035,15 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
881
1035
  }
882
1036
  const after = yield* readCloudflareRole(news.mount, news.name);
883
1037
  if (after === undefined) {
884
- return yield* Effect7.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
1038
+ return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
885
1039
  }
886
1040
  const left = differences(news, policies, after);
887
1041
  if (left.length > 0) {
888
- return yield* Effect7.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
1042
+ return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
889
1043
  }
890
1044
  return attributesOf4(news, after);
891
1045
  }),
892
- delete: Effect7.fn(function* ({ output }) {
1046
+ delete: Effect8.fn(function* ({ output }) {
893
1047
  yield* deleteCloudflareRole(output.mount, output.name);
894
1048
  return;
895
1049
  })
@@ -1083,15 +1237,15 @@ var parseRolesConfig = (yaml) => {
1083
1237
  import { Resource as Resource4 } from "alchemy";
1084
1238
  import { isResolved as isResolved4 } from "alchemy/Diff";
1085
1239
  import * as Provider4 from "alchemy/Provider";
1086
- import * as Effect9 from "effect/Effect";
1240
+ import * as Effect10 from "effect/Effect";
1087
1241
 
1088
1242
  // src/openbao/mount-wire.ts
1089
- import * as Effect8 from "effect/Effect";
1090
- var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect8.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
1243
+ import * as Effect9 from "effect/Effect";
1244
+ var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect9.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
1091
1245
  ...asked.errors,
1092
1246
  `sys/mounts could not confirm the mount is absent: ${listing.message}`
1093
- ])), Effect8.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect8.succeed(undefined) : Effect8.fail(asked)));
1094
- var readMountData = (path) => baoRead(readPath(path)).pipe(Effect8.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
1247
+ ])), Effect9.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect9.succeed(undefined) : Effect9.fail(asked)));
1248
+ var readMountData = (path) => baoRead(readPath(path)).pipe(Effect9.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
1095
1249
  var enableMount = (props) => baoWrite("POST", readPath(props.path), enableBody(props));
1096
1250
  var tuneMount = (props) => baoWrite("POST", tunePath(props.path), tuneBody(props));
1097
1251
  var disableMount = (path) => baoDelete(readPath(path));
@@ -1100,18 +1254,18 @@ var disableMount = (path) => baoDelete(readPath(path));
1100
1254
  var BaoMount = Resource4("Bao.Mount", {
1101
1255
  defaultRemovalPolicy: "retain"
1102
1256
  });
1103
- var readMount = (props) => Effect9.gen(function* () {
1257
+ var readMount = (props) => Effect10.gen(function* () {
1104
1258
  const live = yield* readMountData(props.path);
1105
1259
  if (live === undefined)
1106
1260
  return;
1107
1261
  return attributesOf(props, live);
1108
1262
  });
1109
- var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount.Provider.of({
1110
- list: () => Effect9.succeed([]),
1111
- read: Effect9.fn(function* ({ olds }) {
1263
+ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect10.succeed(BaoMount.Provider.of({
1264
+ list: () => Effect10.succeed([]),
1265
+ read: Effect10.fn(function* ({ olds }) {
1112
1266
  return yield* readMount(olds);
1113
1267
  }),
1114
- diff: Effect9.fn(function* ({ news, output }) {
1268
+ diff: Effect10.fn(function* ({ news, output }) {
1115
1269
  if (output === undefined || !isResolved4(news))
1116
1270
  return;
1117
1271
  const live = yield* readMount(news);
@@ -1123,7 +1277,7 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount
1123
1277
  return { action: "noop" };
1124
1278
  return { action: "update" };
1125
1279
  }),
1126
- reconcile: Effect9.fn(function* ({ news }) {
1280
+ reconcile: Effect10.fn(function* ({ news }) {
1127
1281
  let live = yield* readMount(news);
1128
1282
  if (live === undefined) {
1129
1283
  yield* enableMount(news);
@@ -1131,17 +1285,17 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount
1131
1285
  yield* tuneMount(news);
1132
1286
  } else if (!matches(live, news)) {
1133
1287
  if (live.type !== news.type) {
1134
- return yield* Effect9.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
1288
+ return yield* Effect10.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
1135
1289
  }
1136
1290
  yield* tuneMount(news);
1137
1291
  }
1138
1292
  live = yield* readMount(news);
1139
1293
  if (live === undefined) {
1140
- return yield* Effect9.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
1294
+ return yield* Effect10.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
1141
1295
  }
1142
1296
  return live;
1143
1297
  }),
1144
- delete: Effect9.fn(function* ({ output }) {
1298
+ delete: Effect10.fn(function* ({ output }) {
1145
1299
  yield* disableMount(output.path);
1146
1300
  return;
1147
1301
  })
@@ -1150,7 +1304,7 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount
1150
1304
  import { Resource as Resource5 } from "alchemy";
1151
1305
  import { isResolved as isResolved5 } from "alchemy/Diff";
1152
1306
  import * as Provider5 from "alchemy/Provider";
1153
- import * as Effect10 from "effect/Effect";
1307
+ import * as Effect11 from "effect/Effect";
1154
1308
 
1155
1309
  // src/openbao/pki-role-form.ts
1156
1310
  var sorted = (values) => [...values].sort();
@@ -1268,19 +1422,19 @@ var matches4 = (attributes, props) => attributes.digest === digestOf(canonicalFr
1268
1422
  var BaoPkiRole = Resource5("Bao.PkiRole", {
1269
1423
  defaultRemovalPolicy: "retain"
1270
1424
  });
1271
- var readRole2 = (props) => Effect10.gen(function* () {
1425
+ var readRole2 = (props) => Effect11.gen(function* () {
1272
1426
  const live = yield* baoRead(rolePath2(props));
1273
1427
  if (live === undefined)
1274
1428
  return;
1275
1429
  return attributesOf5(props, live);
1276
1430
  });
1277
1431
  var badDurations = (props) => [["ttl", props.ttl], ["maxTtl", props.maxTtl]].filter(([, text4]) => parseDuration(text4) === undefined);
1278
- var BaoPkiRoleProvider = () => Provider5.effect(BaoPkiRole, Effect10.succeed(BaoPkiRole.Provider.of({
1279
- list: () => Effect10.succeed([]),
1280
- read: Effect10.fn(function* ({ olds }) {
1432
+ var BaoPkiRoleProvider = () => Provider5.effect(BaoPkiRole, Effect11.succeed(BaoPkiRole.Provider.of({
1433
+ list: () => Effect11.succeed([]),
1434
+ read: Effect11.fn(function* ({ olds }) {
1281
1435
  return yield* readRole2(olds);
1282
1436
  }),
1283
- diff: Effect10.fn(function* ({ news, output }) {
1437
+ diff: Effect11.fn(function* ({ news, output }) {
1284
1438
  if (output === undefined || !isResolved5(news))
1285
1439
  return;
1286
1440
  if (mountPath(news.mount ?? "pki") !== output.mount)
@@ -1293,14 +1447,14 @@ var BaoPkiRoleProvider = () => Provider5.effect(BaoPkiRole, Effect10.succeed(Bao
1293
1447
  return { action: "update" };
1294
1448
  return matches4(live, news) ? { action: "noop" } : { action: "update" };
1295
1449
  }),
1296
- reconcile: Effect10.fn(function* ({ news }) {
1450
+ reconcile: Effect11.fn(function* ({ news }) {
1297
1451
  const path = rolePath2(news);
1298
1452
  if (news.allowedDomains.length === 0) {
1299
- return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` + "that role can issue no certificate at all."));
1453
+ return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` + "that role can issue no certificate at all."));
1300
1454
  }
1301
1455
  const bad = badDurations(news);
1302
1456
  if (bad.length > 0) {
1303
- return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: ${bad.map(([k, v]) => `${k}=${v}`).join(", ")} ` + "is not a duration OpenBao parses (expect 30m, 720h, 8760h or 0)."));
1457
+ return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: ${bad.map(([k, v]) => `${k}=${v}`).join(", ")} ` + "is not a duration OpenBao parses (expect 30m, 720h, 8760h or 0)."));
1304
1458
  }
1305
1459
  const live = yield* readRole2(news);
1306
1460
  if (live === undefined || !matches4(live, news)) {
@@ -1308,23 +1462,147 @@ var BaoPkiRoleProvider = () => Provider5.effect(BaoPkiRole, Effect10.succeed(Bao
1308
1462
  }
1309
1463
  const after = yield* readRole2(news);
1310
1464
  if (after === undefined) {
1311
- return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
1465
+ return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
1312
1466
  }
1313
1467
  if (!matches4(after, news)) {
1314
- return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` + "OpenBao stored something other than what was declared — inspect with " + `\`bao read ${path}\` before retrying.`));
1468
+ return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` + "OpenBao stored something other than what was declared — inspect with " + `\`bao read ${path}\` before retrying.`));
1315
1469
  }
1316
1470
  return after;
1317
1471
  }),
1318
- delete: Effect10.fn(function* ({ output }) {
1472
+ delete: Effect11.fn(function* ({ output }) {
1319
1473
  yield* baoDelete(rolePath2(output));
1320
1474
  return;
1321
1475
  })
1322
1476
  })));
1323
- // src/openbao/policy.ts
1477
+ // src/openbao/plugin.ts
1324
1478
  import { Resource as Resource6 } from "alchemy";
1325
1479
  import { isResolved as isResolved6 } from "alchemy/Diff";
1326
1480
  import * as Provider6 from "alchemy/Provider";
1481
+ import * as Effect13 from "effect/Effect";
1482
+
1483
+ // src/openbao/plugin-form.ts
1484
+ var resolve = (props) => ({
1485
+ args: props.args ?? [],
1486
+ command: props.command,
1487
+ name: props.name,
1488
+ sha256: props.sha256.toLowerCase(),
1489
+ type: props.type,
1490
+ version: props.version ?? ""
1491
+ });
1492
+ var CANONICAL_VERSION = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
1493
+ var problems = (form) => {
1494
+ const found = [];
1495
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(form.name) || form.name.includes("..")) {
1496
+ found.push(`name ${JSON.stringify(form.name)} is not a plain catalog name`);
1497
+ }
1498
+ if (!/^[0-9a-f]{64}$/.test(form.sha256))
1499
+ found.push("sha256 is not 64 hex characters");
1500
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(form.command) || form.command.includes("..")) {
1501
+ found.push(`command ${JSON.stringify(form.command)} is not a bare file name`);
1502
+ }
1503
+ if (form.version !== "" && !CANONICAL_VERSION.test(form.version)) {
1504
+ found.push(`version ${JSON.stringify(form.version)} is not canonical semver like v1.2.0`);
1505
+ }
1506
+ if (/\+builtin/.test(form.version))
1507
+ found.push("version metadata `builtin` is reserved");
1508
+ return found;
1509
+ };
1510
+ var versionQuery = (version) => version === "" ? "" : `?version=${encodeURIComponent(version)}`;
1511
+ var catalogPath = (type, name) => `sys/plugins/catalog/${type}/${name}`;
1512
+ var versionedPath = (type, name, version) => `${catalogPath(type, name)}${versionQuery(version)}`;
1513
+ var registerBody = (form) => ({
1514
+ command: form.command,
1515
+ sha256: form.sha256,
1516
+ ...form.args.length > 0 ? { args: form.args } : {},
1517
+ ...form.version === "" ? {} : { version: form.version }
1518
+ });
1519
+ var text4 = (value) => typeof value === "string" ? value : "";
1520
+ var list2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1521
+ var attributesOf6 = (form, live) => {
1522
+ const attrs = {
1523
+ name: form.name,
1524
+ type: form.type,
1525
+ version: text4(live["version"]),
1526
+ sha256: text4(live["sha256"]).toLowerCase(),
1527
+ command: text4(live["command"]),
1528
+ args: list2(live["args"]),
1529
+ builtin: live["builtin"] === true
1530
+ };
1531
+ return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1532
+ };
1533
+ var sameArgs = (want, have) => want.length === have.length && want.every((entry, index) => entry === have[index]);
1534
+ var matches5 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
1535
+
1536
+ // src/openbao/plugin-reconcile.ts
1327
1537
  import * as Effect12 from "effect/Effect";
1538
+
1539
+ // src/openbao/plugin-wire.ts
1540
+ var readPluginData = (form) => baoRead(versionedPath(form.type, form.name, form.version));
1541
+ var registerPlugin = (form) => baoWrite("PUT", catalogPath(form.type, form.name), registerBody(form));
1542
+ var deregisterPlugin = (type, name, version) => baoDelete(versionedPath(type, name, version));
1543
+
1544
+ // src/openbao/plugin-reconcile.ts
1545
+ var readEntry = (form) => Effect12.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf6(form, live), live });
1546
+ var refuse = (form, message) => Effect12.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
1547
+ var SELF_REPORTED = "the write succeeded but the read-back does not match. If the binary reports its own version, " + "OpenBao filed the registration under it (plugin_catalog.go setInternal) — declare `version` " + "as exactly the version the binary reports; that adopts the entry this write just made.";
1548
+ var reconcilePlugin = (props) => Effect12.gen(function* () {
1549
+ const form = resolve(props);
1550
+ const bad = problems(form);
1551
+ if (bad.length > 0)
1552
+ return yield* refuse(form, bad.join("; "));
1553
+ const found = yield* readEntry(form);
1554
+ if (found !== undefined && !matches5(found.attributes, form)) {
1555
+ if (found.live["declarative"] === true || found.live["oci"] === true) {
1556
+ return yield* refuse(form, "the live entry is declarative/OCI; manage it in server config.");
1557
+ }
1558
+ if (found.attributes.builtin) {
1559
+ return yield* refuse(form, "this name is a builtin; an unversioned write would shadow it.");
1560
+ }
1561
+ }
1562
+ if (found === undefined || !matches5(found.attributes, form))
1563
+ yield* registerPlugin(form);
1564
+ const after = yield* readEntry(form);
1565
+ if (after === undefined || !matches5(after.attributes, form)) {
1566
+ return yield* refuse(form, SELF_REPORTED);
1567
+ }
1568
+ return after.attributes;
1569
+ });
1570
+
1571
+ // src/openbao/plugin.ts
1572
+ var BaoPlugin = Resource6("Bao.Plugin", {
1573
+ defaultRemovalPolicy: "retain"
1574
+ });
1575
+ var BaoPluginProvider = () => Provider6.effect(BaoPlugin, Effect13.succeed(BaoPlugin.Provider.of({
1576
+ list: () => Effect13.succeed([]),
1577
+ read: Effect13.fn(function* ({ olds }) {
1578
+ const found = yield* readEntry(resolve(olds));
1579
+ return found?.attributes;
1580
+ }),
1581
+ diff: Effect13.fn(function* ({ news, output }) {
1582
+ if (output === undefined || !isResolved6(news))
1583
+ return;
1584
+ const form = resolve(news);
1585
+ if (output.name !== form.name || output.type !== form.type || output.version !== form.version) {
1586
+ return { action: "replace" };
1587
+ }
1588
+ const found = yield* readEntry(form);
1589
+ if (found === undefined)
1590
+ return { action: "update" };
1591
+ return matches5(found.attributes, form) ? { action: "noop" } : { action: "update" };
1592
+ }),
1593
+ reconcile: Effect13.fn(function* ({ news }) {
1594
+ return yield* reconcilePlugin(news);
1595
+ }),
1596
+ delete: Effect13.fn(function* ({ output }) {
1597
+ yield* deregisterPlugin(output.type, output.name, output.version);
1598
+ return;
1599
+ })
1600
+ })));
1601
+ // src/openbao/policy.ts
1602
+ import { Resource as Resource7 } from "alchemy";
1603
+ import { isResolved as isResolved7 } from "alchemy/Diff";
1604
+ import * as Provider7 from "alchemy/Provider";
1605
+ import * as Effect15 from "effect/Effect";
1328
1606
  import * as FileSystem from "effect/FileSystem";
1329
1607
  import * as Path from "effect/Path";
1330
1608
 
@@ -1332,32 +1610,32 @@ import * as Path from "effect/Path";
1332
1610
  var isEmptyAssembly = (parts, joined) => parts === 0 || !/^\s*path\s+"/m.test(joined);
1333
1611
 
1334
1612
  // src/openbao/policy-wire.ts
1335
- import * as Effect11 from "effect/Effect";
1613
+ import * as Effect14 from "effect/Effect";
1336
1614
  var policyPath = (name) => `sys/policies/acl/${name}`;
1337
- var readPolicy = (name) => Effect11.flatMap(baoRead(policyPath(name)), (data) => {
1615
+ var readPolicy = (name) => Effect14.flatMap(baoRead(policyPath(name)), (data) => {
1338
1616
  if (data === undefined)
1339
- return Effect11.succeed("");
1340
- const text4 = data["policy"];
1341
- return typeof text4 === "string" ? Effect11.succeed(text4) : Effect11.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
1617
+ return Effect14.succeed("");
1618
+ const text5 = data["policy"];
1619
+ return typeof text5 === "string" ? Effect14.succeed(text5) : Effect14.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
1342
1620
  });
1343
1621
  var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
1344
1622
  var deletePolicy = (name) => baoDelete(policyPath(name));
1345
1623
 
1346
1624
  // src/openbao/policy.ts
1347
- var BaoPolicy = Resource6("Bao.Policy", {
1625
+ var BaoPolicy = Resource7("Bao.Policy", {
1348
1626
  defaultRemovalPolicy: "retain"
1349
1627
  });
1350
1628
  var grantsOf = (hcl) => (hcl.match(/^path /gm) ?? []).length;
1351
- var attributesOf6 = (name, hcl, parts) => ({
1629
+ var attributesOf7 = (name, hcl, parts) => ({
1352
1630
  name,
1353
1631
  digest: sha256(hcl),
1354
1632
  grants: grantsOf(hcl),
1355
1633
  parts
1356
1634
  });
1357
- var BaoPolicyProvider = () => Provider6.effect(BaoPolicy, Effect12.gen(function* () {
1635
+ var BaoPolicyProvider = () => Provider7.effect(BaoPolicy, Effect15.gen(function* () {
1358
1636
  const fs = yield* FileSystem.FileSystem;
1359
1637
  const path = yield* Path.Path;
1360
- const assemble = (dir) => Effect12.gen(function* () {
1638
+ const assemble = (dir) => Effect15.gen(function* () {
1361
1639
  const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
1362
1640
  let joined = "";
1363
1641
  for (const f of entries2)
@@ -1366,44 +1644,44 @@ var BaoPolicyProvider = () => Provider6.effect(BaoPolicy, Effect12.gen(function*
1366
1644
  return { joined, parts: entries2.length };
1367
1645
  });
1368
1646
  return BaoPolicy.Provider.of({
1369
- list: () => Effect12.succeed([]),
1370
- read: Effect12.fn(function* ({ olds }) {
1647
+ list: () => Effect15.succeed([]),
1648
+ read: Effect15.fn(function* ({ olds }) {
1371
1649
  const live = yield* readPolicy(olds.name);
1372
- return live.length === 0 ? undefined : attributesOf6(olds.name, live, 0);
1650
+ return live.length === 0 ? undefined : attributesOf7(olds.name, live, 0);
1373
1651
  }),
1374
- diff: Effect12.fn(function* ({ news, output }) {
1375
- if (output === undefined || !isResolved6(news))
1652
+ diff: Effect15.fn(function* ({ news, output }) {
1653
+ if (output === undefined || !isResolved7(news))
1376
1654
  return;
1377
1655
  const { joined } = yield* assemble(news.fragments);
1378
1656
  const live = yield* readPolicy(news.name);
1379
1657
  return live.length > 0 && sha256(live) === sha256(joined) ? { action: "noop" } : { action: "update" };
1380
1658
  }),
1381
- reconcile: Effect12.fn(function* ({ news }) {
1659
+ reconcile: Effect15.fn(function* ({ news }) {
1382
1660
  const name = news.name;
1383
1661
  const { joined, parts } = yield* assemble(news.fragments);
1384
1662
  if (isEmptyAssembly(parts, joined))
1385
- return yield* Effect12.die(new Error(`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` + `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`));
1663
+ return yield* Effect15.die(new Error(`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` + `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`));
1386
1664
  const current = yield* readPolicy(name);
1387
1665
  if (current.length > 0 && sha256(current) === sha256(joined)) {
1388
- return attributesOf6(name, joined, parts);
1666
+ return attributesOf7(name, joined, parts);
1389
1667
  }
1390
1668
  yield* writePolicy(name, joined);
1391
- return attributesOf6(name, joined, parts);
1669
+ return attributesOf7(name, joined, parts);
1392
1670
  }),
1393
- delete: Effect12.fn(function* ({ output }) {
1671
+ delete: Effect15.fn(function* ({ output }) {
1394
1672
  yield* deletePolicy(output.name);
1395
1673
  return;
1396
1674
  })
1397
1675
  });
1398
1676
  }));
1399
1677
  // src/openbao/proxmox-role.ts
1400
- import { Resource as Resource7 } from "alchemy";
1401
- import { isResolved as isResolved7 } from "alchemy/Diff";
1402
- import * as Provider7 from "alchemy/Provider";
1403
- import * as Effect13 from "effect/Effect";
1678
+ import { Resource as Resource8 } from "alchemy";
1679
+ import { isResolved as isResolved8 } from "alchemy/Diff";
1680
+ import * as Provider8 from "alchemy/Provider";
1681
+ import * as Effect16 from "effect/Effect";
1404
1682
 
1405
1683
  // src/openbao/proxmox-role-form.ts
1406
- var text4 = (value) => typeof value === "string" ? value : "";
1684
+ var text5 = (value) => typeof value === "string" ? value : "";
1407
1685
  var ttlText3 = (seconds3, fallback = "0") => {
1408
1686
  if (seconds3 === undefined)
1409
1687
  return fallback;
@@ -1416,11 +1694,11 @@ var ttlText3 = (seconds3, fallback = "0") => {
1416
1694
  return `${String(seconds3)}s`;
1417
1695
  };
1418
1696
  var rolePath3 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
1419
- var attributesOf7 = (props, live) => {
1697
+ var attributesOf8 = (props, live) => {
1420
1698
  const attrs = {
1421
1699
  mount: mountPath(props.mount),
1422
- name: text4(live["name"]) || props.name,
1423
- mintUser: text4(live["mint_user"]),
1700
+ name: text5(live["name"]) || props.name,
1701
+ mintUser: text5(live["mint_user"]),
1424
1702
  ttl: ttlText3(ttlSeconds(live["ttl"])),
1425
1703
  maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
1426
1704
  };
@@ -1446,69 +1724,68 @@ var sameTtl5 = (want, have) => {
1446
1724
  const haveSeconds = ttlSeconds(have);
1447
1725
  return wantSeconds !== undefined && wantSeconds === haveSeconds;
1448
1726
  };
1449
- var matches5 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
1727
+ var matches6 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
1450
1728
 
1451
1729
  // src/openbao/proxmox-role.ts
1452
- var BaoProxmoxRole = Resource7("Bao.ProxmoxRole", {
1730
+ var BaoProxmoxRole = Resource8("Bao.ProxmoxRole", {
1453
1731
  defaultRemovalPolicy: "retain"
1454
1732
  });
1455
- var readRole3 = (props) => Effect13.gen(function* () {
1733
+ var readRole3 = (props) => Effect16.gen(function* () {
1456
1734
  const live = yield* baoRead(rolePath3(props.mount, props.name));
1457
1735
  if (live === undefined)
1458
1736
  return;
1459
- return attributesOf7(props, live);
1737
+ return attributesOf8(props, live);
1460
1738
  });
1461
- var BaoProxmoxRoleProvider = () => Provider7.effect(BaoProxmoxRole, Effect13.succeed(BaoProxmoxRole.Provider.of({
1462
- list: () => Effect13.succeed([]),
1463
- read: Effect13.fn(function* ({ olds }) {
1739
+ var BaoProxmoxRoleProvider = () => Provider8.effect(BaoProxmoxRole, Effect16.succeed(BaoProxmoxRole.Provider.of({
1740
+ list: () => Effect16.succeed([]),
1741
+ read: Effect16.fn(function* ({ olds }) {
1464
1742
  return yield* readRole3(olds);
1465
1743
  }),
1466
- diff: Effect13.fn(function* ({ news, output }) {
1467
- if (output === undefined || !isResolved7(news))
1744
+ diff: Effect16.fn(function* ({ news, output }) {
1745
+ if (output === undefined || !isResolved8(news))
1468
1746
  return;
1469
1747
  const live = yield* readRole3(news);
1470
1748
  if (live === undefined)
1471
1749
  return { action: "update" };
1472
- if (matches5(live, news))
1750
+ if (matches6(live, news))
1473
1751
  return { action: "noop" };
1474
1752
  return { action: "update" };
1475
1753
  }),
1476
- reconcile: Effect13.fn(function* ({ news }) {
1754
+ reconcile: Effect16.fn(function* ({ news }) {
1477
1755
  const live = yield* readRole3(news);
1478
1756
  if (live !== undefined && live.mintUser !== news.mintUser && news.allowMintUserChange !== live.mintUser) {
1479
- return yield* Effect13.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: live mint_user ` + `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` + "inherits the ACL of that PVE user. To re-scope deliberately, declare " + `allowMintUserChange: '${live.mintUser}'.`));
1757
+ return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: live mint_user ` + `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` + "inherits the ACL of that PVE user. To re-scope deliberately, declare " + `allowMintUserChange: '${live.mintUser}'.`));
1480
1758
  }
1481
- if (live === undefined || !matches5(live, news)) {
1759
+ if (live === undefined || !matches6(live, news)) {
1482
1760
  const write = writeBody4(news);
1483
1761
  if (!write.ok) {
1484
- return yield* Effect13.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
1762
+ return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
1485
1763
  }
1486
1764
  yield* baoWrite("PUT", rolePath3(news.mount, news.name), write.body);
1487
1765
  }
1488
1766
  const after = yield* readRole3(news);
1489
1767
  if (after === undefined) {
1490
- return yield* Effect13.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
1768
+ return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
1491
1769
  }
1492
1770
  return after;
1493
1771
  }),
1494
- delete: Effect13.fn(function* ({ output }) {
1772
+ delete: Effect16.fn(function* ({ output }) {
1495
1773
  yield* baoDelete(rolePath3(output.mount, output.name));
1496
1774
  return;
1497
1775
  })
1498
1776
  })));
1499
1777
  // src/openbao/ssh-role.ts
1500
- import { Resource as Resource8 } from "alchemy";
1501
- import { isResolved as isResolved8 } from "alchemy/Diff";
1502
- import * as Provider8 from "alchemy/Provider";
1503
- import * as Effect14 from "effect/Effect";
1778
+ import { Resource as Resource9 } from "alchemy";
1779
+ import { isResolved as isResolved9 } from "alchemy/Diff";
1780
+ import * as Provider9 from "alchemy/Provider";
1781
+ import * as Effect17 from "effect/Effect";
1504
1782
 
1505
1783
  // src/openbao/ssh-role-form.ts
1506
- var mountOf = (mount) => (mount ?? "ssh").replace(/\/+$/, "");
1507
- var readPath4 = (mount, name) => `${mountOf(mount)}/roles/${name}`;
1508
- var text5 = (value) => typeof value === "string" ? value : "";
1784
+ var readPath4 = (mount, name) => `${sshMountOf(mount)}/roles/${name}`;
1785
+ var text6 = (value) => typeof value === "string" ? value : "";
1509
1786
  var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
1510
1787
  var csvList = (value) => {
1511
- const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text5(value).split(",");
1788
+ const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text6(value).split(",");
1512
1789
  return [
1513
1790
  ...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
1514
1791
  ].sort();
@@ -1519,12 +1796,12 @@ var extMap = (value) => {
1519
1796
  return out;
1520
1797
  const source = value;
1521
1798
  for (const key of Object.keys(source).sort())
1522
- out[key] = text5(source[key]);
1799
+ out[key] = text6(source[key]);
1523
1800
  return out;
1524
1801
  };
1525
- var resolve = (props) => ({
1802
+ var resolve2 = (props) => ({
1526
1803
  name: props.name,
1527
- mount: mountOf(props.mount),
1804
+ mount: sshMountOf(props.mount),
1528
1805
  allowedUsers: csvList(props.allowedUsers),
1529
1806
  defaultUser: props.defaultUser ?? "",
1530
1807
  allowUserCertificates: props.allowUserCertificates ?? false,
@@ -1540,12 +1817,12 @@ var resolve = (props) => ({
1540
1817
  ttl: props.ttl,
1541
1818
  maxTtl: props.maxTtl
1542
1819
  });
1543
- var attributesOf8 = (form, live) => {
1820
+ var attributesOf9 = (form, live) => {
1544
1821
  const attrs = {
1545
1822
  name: form.name,
1546
1823
  mount: form.mount,
1547
1824
  allowedUsers: csvList(live["allowed_users"]),
1548
- defaultUser: text5(live["default_user"]),
1825
+ defaultUser: text6(live["default_user"]),
1549
1826
  allowUserCertificates: bool3(live["allow_user_certificates"], false),
1550
1827
  allowHostCertificates: bool3(live["allow_host_certificates"], false),
1551
1828
  allowedDomains: csvList(live["allowed_domains"]),
@@ -1556,13 +1833,13 @@ var attributesOf8 = (form, live) => {
1556
1833
  allowedCriticalOptions: csvList(live["allowed_critical_options"]),
1557
1834
  defaultCriticalOptions: extMap(live["default_critical_options"]),
1558
1835
  allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
1559
- keyType: text5(live["key_type"]) || "ca",
1836
+ keyType: text6(live["key_type"]) || "ca",
1560
1837
  ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
1561
1838
  maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
1562
1839
  };
1563
1840
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1564
1841
  };
1565
- var mapValue = (map4) => JSON.stringify(extMap(map4));
1842
+ var mapValue = (map5) => extMap(map5);
1566
1843
  var writeBody5 = (form) => ({
1567
1844
  key_type: "ca",
1568
1845
  allowed_users: form.allowedUsers.join(","),
@@ -1587,17 +1864,17 @@ var sameMap = (want, have) => {
1587
1864
  };
1588
1865
  var sameTtl6 = (want, have) => parseDuration(want) === have;
1589
1866
  var badDurations2 = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
1590
- var matches6 = (attributes, form) => attributes.keyType === "ca" && sameList2(form.allowedUsers, attributes.allowedUsers) && attributes.defaultUser === form.defaultUser && attributes.allowUserCertificates === form.allowUserCertificates && attributes.allowHostCertificates === form.allowHostCertificates && sameList2(form.allowedDomains, attributes.allowedDomains) && attributes.allowSubdomains === form.allowSubdomains && attributes.allowBareDomains === form.allowBareDomains && sameList2(form.allowedExtensions, attributes.allowedExtensions) && sameMap(form.defaultExtensions, attributes.defaultExtensions) && sameList2(form.allowedCriticalOptions, attributes.allowedCriticalOptions) && sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) && attributes.allowEmptyPrincipals === form.allowEmptyPrincipals && sameTtl6(form.ttl, attributes.ttlSeconds) && sameTtl6(form.maxTtl, attributes.maxTtlSeconds);
1867
+ var matches7 = (attributes, form) => attributes.keyType === "ca" && sameList2(form.allowedUsers, attributes.allowedUsers) && attributes.defaultUser === form.defaultUser && attributes.allowUserCertificates === form.allowUserCertificates && attributes.allowHostCertificates === form.allowHostCertificates && sameList2(form.allowedDomains, attributes.allowedDomains) && attributes.allowSubdomains === form.allowSubdomains && attributes.allowBareDomains === form.allowBareDomains && sameList2(form.allowedExtensions, attributes.allowedExtensions) && sameMap(form.defaultExtensions, attributes.defaultExtensions) && sameList2(form.allowedCriticalOptions, attributes.allowedCriticalOptions) && sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) && attributes.allowEmptyPrincipals === form.allowEmptyPrincipals && sameTtl6(form.ttl, attributes.ttlSeconds) && sameTtl6(form.maxTtl, attributes.maxTtlSeconds);
1591
1868
 
1592
1869
  // src/openbao/ssh-role.ts
1593
- var BaoSshRole = Resource8("Bao.SshRole", {
1870
+ var BaoSshRole = Resource9("Bao.SshRole", {
1594
1871
  defaultRemovalPolicy: "retain"
1595
1872
  });
1596
- var readRole4 = (form) => Effect14.gen(function* () {
1873
+ var readRole4 = (form) => Effect17.gen(function* () {
1597
1874
  const live = yield* baoRead(readPath4(form.mount, form.name));
1598
1875
  if (live === undefined)
1599
1876
  return;
1600
- return { attributes: attributesOf8(form, live), live };
1877
+ return { attributes: attributesOf9(form, live), live };
1601
1878
  });
1602
1879
  var UNMANAGED_DEFAULTS = {
1603
1880
  algorithm_signer: "default",
@@ -1619,52 +1896,52 @@ var wouldErase = (live) => {
1619
1896
  }
1620
1897
  return lost.sort();
1621
1898
  };
1622
- var refuse = (message) => Effect14.die(new Error(message));
1623
- var BaoSshRoleProvider = () => Provider8.effect(BaoSshRole, Effect14.succeed(BaoSshRole.Provider.of({
1624
- list: () => Effect14.succeed([]),
1625
- read: Effect14.fn(function* ({ olds }) {
1626
- const found = yield* readRole4(resolve(olds));
1899
+ var refuse2 = (message) => Effect17.die(new Error(message));
1900
+ var BaoSshRoleProvider = () => Provider9.effect(BaoSshRole, Effect17.succeed(BaoSshRole.Provider.of({
1901
+ list: () => Effect17.succeed([]),
1902
+ read: Effect17.fn(function* ({ olds }) {
1903
+ const found = yield* readRole4(resolve2(olds));
1627
1904
  return found?.attributes;
1628
1905
  }),
1629
- diff: Effect14.fn(function* ({ news, output }) {
1630
- if (output === undefined || !isResolved8(news))
1906
+ diff: Effect17.fn(function* ({ news, output }) {
1907
+ if (output === undefined || !isResolved9(news))
1631
1908
  return;
1632
- const form = resolve(news);
1909
+ const form = resolve2(news);
1633
1910
  if (output.mount !== form.mount || output.name !== form.name) {
1634
1911
  return { action: "replace" };
1635
1912
  }
1636
1913
  const found = yield* readRole4(form);
1637
1914
  if (found === undefined)
1638
1915
  return { action: "update" };
1639
- return matches6(found.attributes, form) ? { action: "noop" } : { action: "update" };
1916
+ return matches7(found.attributes, form) ? { action: "noop" } : { action: "update" };
1640
1917
  }),
1641
- reconcile: Effect14.fn(function* ({ news }) {
1642
- const form = resolve(news);
1918
+ reconcile: Effect17.fn(function* ({ news }) {
1919
+ const form = resolve2(news);
1643
1920
  const path = readPath4(form.mount, form.name);
1644
1921
  const bad = badDurations2(form);
1645
1922
  if (bad.length > 0) {
1646
- return yield* refuse(`Bao.SshRole ${path}: ${bad.join(", ")} is not a duration parseDuration understands ` + "(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.");
1923
+ return yield* refuse2(`Bao.SshRole ${path}: ${bad.join(", ")} is not a duration parseDuration understands ` + "(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.");
1647
1924
  }
1648
1925
  if (form.allowUserCertificates && form.allowedUsers.length === 0 && !form.allowEmptyPrincipals) {
1649
- return yield* refuse(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
1926
+ return yield* refuse2(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
1650
1927
  }
1651
1928
  const found = yield* readRole4(form);
1652
- if (found === undefined || !matches6(found.attributes, form)) {
1929
+ if (found === undefined || !matches7(found.attributes, form)) {
1653
1930
  if (found !== undefined) {
1654
1931
  const lost = wouldErase(found.live);
1655
1932
  if (lost.length > 0) {
1656
- return yield* refuse(`Bao.SshRole ${path}: live role carries ${lost.join(", ")}, which this ` + "resource does not declare and the full-replace write would ERASE. " + "Fold those settings into the declaration, or clear them by hand first.");
1933
+ return yield* refuse2(`Bao.SshRole ${path}: live role carries ${lost.join(", ")}, which this ` + "resource does not declare and the full-replace write would ERASE. " + "Fold those settings into the declaration, or clear them by hand first.");
1657
1934
  }
1658
1935
  }
1659
1936
  yield* baoWrite("PUT", path, writeBody5(form));
1660
1937
  }
1661
1938
  const after = yield* readRole4(form);
1662
1939
  if (after === undefined) {
1663
- return yield* refuse(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
1940
+ return yield* refuse2(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
1664
1941
  }
1665
1942
  return after.attributes;
1666
1943
  }),
1667
- delete: Effect14.fn(function* ({ output }) {
1944
+ delete: Effect17.fn(function* ({ output }) {
1668
1945
  yield* baoDelete(readPath4(output.mount, output.name));
1669
1946
  return;
1670
1947
  })
@@ -1676,10 +1953,13 @@ export {
1676
1953
  BaoAuthRoleProvider,
1677
1954
  BaoCloudflareRole,
1678
1955
  BaoCloudflareRoleProvider,
1956
+ BaoLoginError,
1679
1957
  BaoMount,
1680
1958
  BaoMountProvider,
1681
1959
  BaoPkiRole,
1682
1960
  BaoPkiRoleProvider,
1961
+ BaoPlugin,
1962
+ BaoPluginProvider,
1683
1963
  BaoPolicy,
1684
1964
  BaoPolicyProvider,
1685
1965
  BaoProxmoxRole,
@@ -1687,13 +1967,17 @@ export {
1687
1967
  BaoSshRole,
1688
1968
  BaoSshRoleProvider,
1689
1969
  CloudflarePermissionGroups,
1970
+ appRoleLogin,
1971
+ appRoleLoginEffect,
1690
1972
  expandAccount,
1691
1973
  expandAll,
1692
1974
  mountName,
1693
1975
  parseRolesConfig,
1694
1976
  permissionGroupsFromEngine,
1695
- permissionGroupsFromLiveRoles
1977
+ permissionGroupsFromLiveRoles,
1978
+ revokeSelf,
1979
+ revokeSelfEffect
1696
1980
  };
1697
1981
 
1698
- //# debugId=8FBBE010DA02328C64756E2164756E21
1982
+ //# debugId=1C57EE110DEE3DA764756E2164756E21
1699
1983
  //# sourceMappingURL=index.js.map