@homeflare/alchemy 0.4.1 → 0.6.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 (215) hide show
  1. package/README.md +26 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/launchd/fake-runner.d.ts +36 -0
  5. package/dist/launchd/fake-runner.d.ts.map +1 -0
  6. package/dist/launchd/host-effect.d.ts +19 -0
  7. package/dist/launchd/host-effect.d.ts.map +1 -0
  8. package/dist/launchd/host-file-form.d.ts +24 -0
  9. package/dist/launchd/host-file-form.d.ts.map +1 -0
  10. package/dist/launchd/host-file-lifecycle.d.ts +29 -0
  11. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
  12. package/dist/launchd/host-file.d.ts +25 -0
  13. package/dist/launchd/host-file.d.ts.map +1 -0
  14. package/dist/launchd/host-lookup.d.ts +27 -0
  15. package/dist/launchd/host-lookup.d.ts.map +1 -0
  16. package/dist/launchd/index.d.ts +21 -0
  17. package/dist/launchd/index.d.ts.map +1 -0
  18. package/dist/launchd/index.js +969 -0
  19. package/dist/launchd/index.js.map +25 -0
  20. package/dist/launchd/job-form.d.ts +90 -0
  21. package/dist/launchd/job-form.d.ts.map +1 -0
  22. package/dist/launchd/job-lifecycle.d.ts +34 -0
  23. package/dist/launchd/job-lifecycle.d.ts.map +1 -0
  24. package/dist/launchd/job-preflight.d.ts +50 -0
  25. package/dist/launchd/job-preflight.d.ts.map +1 -0
  26. package/dist/launchd/job-validate.d.ts +25 -0
  27. package/dist/launchd/job-validate.d.ts.map +1 -0
  28. package/dist/launchd/job.d.ts +34 -0
  29. package/dist/launchd/job.d.ts.map +1 -0
  30. package/dist/launchd/launchctl.d.ts +62 -0
  31. package/dist/launchd/launchctl.d.ts.map +1 -0
  32. package/dist/launchd/local-runner.d.ts +7 -0
  33. package/dist/launchd/local-runner.d.ts.map +1 -0
  34. package/dist/launchd/plist.d.ts +41 -0
  35. package/dist/launchd/plist.d.ts.map +1 -0
  36. package/dist/launchd/providers.d.ts +15 -0
  37. package/dist/launchd/providers.d.ts.map +1 -0
  38. package/dist/launchd/runner.d.ts +91 -0
  39. package/dist/launchd/runner.d.ts.map +1 -0
  40. package/dist/launchd/secret-tripwire.d.ts +23 -0
  41. package/dist/launchd/secret-tripwire.d.ts.map +1 -0
  42. package/dist/openbao/approle-login-form.d.ts +72 -0
  43. package/dist/openbao/approle-login-form.d.ts.map +1 -0
  44. package/dist/openbao/approle-login-result.d.ts +39 -0
  45. package/dist/openbao/approle-login-result.d.ts.map +1 -0
  46. package/dist/openbao/approle-login.d.ts +77 -0
  47. package/dist/openbao/approle-login.d.ts.map +1 -0
  48. package/dist/openbao/auth-method-form.d.ts +7 -0
  49. package/dist/openbao/auth-method-form.d.ts.map +1 -1
  50. package/dist/openbao/auth-method-reconcile.d.ts +15 -0
  51. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
  52. package/dist/openbao/auth-method.d.ts +6 -0
  53. package/dist/openbao/auth-method.d.ts.map +1 -1
  54. package/dist/openbao/auth-role-form.d.ts +9 -0
  55. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  56. package/dist/openbao/auth-role.d.ts.map +1 -1
  57. package/dist/openbao/auth-token-form.d.ts +48 -0
  58. package/dist/openbao/auth-token-form.d.ts.map +1 -0
  59. package/dist/openbao/bao-http.d.ts +2 -0
  60. package/dist/openbao/bao-http.d.ts.map +1 -1
  61. package/dist/openbao/bao-identity.d.ts +81 -0
  62. package/dist/openbao/bao-identity.d.ts.map +1 -0
  63. package/dist/openbao/bao-namespace.d.ts +20 -0
  64. package/dist/openbao/bao-namespace.d.ts.map +1 -0
  65. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
  66. package/dist/openbao/host-approles.d.ts +62 -0
  67. package/dist/openbao/host-approles.d.ts.map +1 -0
  68. package/dist/openbao/index.d.ts +26 -0
  69. package/dist/openbao/index.d.ts.map +1 -1
  70. package/dist/openbao/index.js +1889 -477
  71. package/dist/openbao/index.js.map +46 -17
  72. package/dist/openbao/jwt-config-form.d.ts +51 -0
  73. package/dist/openbao/jwt-config-form.d.ts.map +1 -0
  74. package/dist/openbao/jwt-config.d.ts +25 -0
  75. package/dist/openbao/jwt-config.d.ts.map +1 -0
  76. package/dist/openbao/jwt-role-form.d.ts +91 -0
  77. package/dist/openbao/jwt-role-form.d.ts.map +1 -0
  78. package/dist/openbao/jwt-role.d.ts +31 -0
  79. package/dist/openbao/jwt-role.d.ts.map +1 -0
  80. package/dist/openbao/kubernetes-role-form.d.ts +68 -0
  81. package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
  82. package/dist/openbao/kubernetes-role.d.ts +25 -0
  83. package/dist/openbao/kubernetes-role.d.ts.map +1 -0
  84. package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
  85. package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
  86. package/dist/openbao/mfa-enforcement.d.ts +41 -0
  87. package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
  88. package/dist/openbao/mfa-totp-form.d.ts +52 -0
  89. package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
  90. package/dist/openbao/mfa-totp.d.ts +33 -0
  91. package/dist/openbao/mfa-totp.d.ts.map +1 -0
  92. package/dist/openbao/mfa-wire.d.ts +63 -0
  93. package/dist/openbao/mfa-wire.d.ts.map +1 -0
  94. package/dist/openbao/mount-form.d.ts +7 -0
  95. package/dist/openbao/mount-form.d.ts.map +1 -1
  96. package/dist/openbao/mount-move.d.ts +49 -0
  97. package/dist/openbao/mount-move.d.ts.map +1 -0
  98. package/dist/openbao/mount-path.d.ts +18 -0
  99. package/dist/openbao/mount-path.d.ts.map +1 -0
  100. package/dist/openbao/mount-reconcile.d.ts +16 -0
  101. package/dist/openbao/mount-reconcile.d.ts.map +1 -0
  102. package/dist/openbao/mount.d.ts +9 -0
  103. package/dist/openbao/mount.d.ts.map +1 -1
  104. package/dist/openbao/pki-role-canonical.d.ts +46 -0
  105. package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
  106. package/dist/openbao/pki-role-form.d.ts +38 -30
  107. package/dist/openbao/pki-role-form.d.ts.map +1 -1
  108. package/dist/openbao/pki-role.d.ts.map +1 -1
  109. package/dist/openbao/plugin-form.d.ts +60 -0
  110. package/dist/openbao/plugin-form.d.ts.map +1 -0
  111. package/dist/openbao/plugin-reconcile.d.ts +18 -0
  112. package/dist/openbao/plugin-reconcile.d.ts.map +1 -0
  113. package/dist/openbao/plugin-wire.d.ts +45 -0
  114. package/dist/openbao/plugin-wire.d.ts.map +1 -0
  115. package/dist/openbao/plugin.d.ts +31 -0
  116. package/dist/openbao/plugin.d.ts.map +1 -0
  117. package/dist/openbao/remount-wire.d.ts +44 -0
  118. package/dist/openbao/remount-wire.d.ts.map +1 -0
  119. package/dist/openbao/role-reconcile.d.ts +40 -0
  120. package/dist/openbao/role-reconcile.d.ts.map +1 -0
  121. package/dist/openbao/ssh-role-form.d.ts +6 -5
  122. package/dist/openbao/ssh-role-form.d.ts.map +1 -1
  123. package/dist/openbao/ssh-role.d.ts.map +1 -1
  124. package/docs/launchd.md +167 -0
  125. package/package.json +7 -2
  126. package/src/index.ts +1 -0
  127. package/src/launchd/fake-runner.ts +176 -0
  128. package/src/launchd/host-effect.ts +28 -0
  129. package/src/launchd/host-file-form.ts +71 -0
  130. package/src/launchd/host-file-lifecycle.test.ts +198 -0
  131. package/src/launchd/host-file-lifecycle.ts +164 -0
  132. package/src/launchd/host-file.ts +64 -0
  133. package/src/launchd/host-lookup.test.ts +92 -0
  134. package/src/launchd/host-lookup.ts +94 -0
  135. package/src/launchd/index.ts +26 -0
  136. package/src/launchd/job-form.test.ts +171 -0
  137. package/src/launchd/job-form.ts +180 -0
  138. package/src/launchd/job-identity.test.ts +147 -0
  139. package/src/launchd/job-lifecycle.test.ts +220 -0
  140. package/src/launchd/job-lifecycle.ts +194 -0
  141. package/src/launchd/job-preflight.ts +152 -0
  142. package/src/launchd/job-validate.ts +134 -0
  143. package/src/launchd/job.ts +96 -0
  144. package/src/launchd/launchctl.test.ts +181 -0
  145. package/src/launchd/launchctl.ts +188 -0
  146. package/src/launchd/local-runner.test.ts +125 -0
  147. package/src/launchd/local-runner.ts +143 -0
  148. package/src/launchd/plist.test.ts +186 -0
  149. package/src/launchd/plist.ts +156 -0
  150. package/src/launchd/providers.test.ts +222 -0
  151. package/src/launchd/providers.ts +21 -0
  152. package/src/launchd/runner.ts +95 -0
  153. package/src/launchd/secret-tripwire.test.ts +110 -0
  154. package/src/launchd/secret-tripwire.ts +81 -0
  155. package/src/openbao/README.md +181 -0
  156. package/src/openbao/REPLACE.md +87 -0
  157. package/src/openbao/approle-login-form.test.ts +64 -0
  158. package/src/openbao/approle-login-form.ts +153 -0
  159. package/src/openbao/approle-login-result.ts +96 -0
  160. package/src/openbao/approle-login.test.ts +200 -0
  161. package/src/openbao/approle-login.ts +156 -0
  162. package/src/openbao/auth-method-form.ts +7 -0
  163. package/src/openbao/auth-method-reconcile.ts +74 -0
  164. package/src/openbao/auth-method.ts +20 -41
  165. package/src/openbao/auth-role-form.test.ts +57 -0
  166. package/src/openbao/auth-role-form.ts +11 -0
  167. package/src/openbao/auth-role.ts +12 -0
  168. package/src/openbao/auth-token-form.ts +161 -0
  169. package/src/openbao/bao-http.ts +2 -0
  170. package/src/openbao/bao-identity.test.ts +131 -0
  171. package/src/openbao/bao-identity.ts +188 -0
  172. package/src/openbao/bao-namespace.ts +32 -0
  173. package/src/openbao/cloudflare-roles-expand.ts +2 -5
  174. package/src/openbao/host-approles.test.ts +121 -0
  175. package/src/openbao/host-approles.ts +126 -0
  176. package/src/openbao/index.ts +32 -0
  177. package/src/openbao/jwt-config-form.ts +161 -0
  178. package/src/openbao/jwt-config.ts +85 -0
  179. package/src/openbao/jwt-role-form.test.ts +130 -0
  180. package/src/openbao/jwt-role-form.ts +231 -0
  181. package/src/openbao/jwt-role.ts +102 -0
  182. package/src/openbao/kubernetes-role-form.test.ts +82 -0
  183. package/src/openbao/kubernetes-role-form.ts +149 -0
  184. package/src/openbao/kubernetes-role.ts +94 -0
  185. package/src/openbao/mfa-enforcement-form.ts +147 -0
  186. package/src/openbao/mfa-enforcement.ts +143 -0
  187. package/src/openbao/mfa-totp-form.ts +155 -0
  188. package/src/openbao/mfa-totp.ts +118 -0
  189. package/src/openbao/mfa-wire.ts +117 -0
  190. package/src/openbao/mfa.test.ts +200 -0
  191. package/src/openbao/mount-form.ts +9 -1
  192. package/src/openbao/mount-move.test.ts +164 -0
  193. package/src/openbao/mount-move.ts +113 -0
  194. package/src/openbao/mount-path.test.ts +33 -0
  195. package/src/openbao/mount-path.ts +31 -0
  196. package/src/openbao/mount-reconcile.ts +75 -0
  197. package/src/openbao/mount-wire.test.ts +13 -0
  198. package/src/openbao/mount.ts +26 -43
  199. package/src/openbao/pki-role-canonical.ts +163 -0
  200. package/src/openbao/pki-role-form.test.ts +112 -0
  201. package/src/openbao/pki-role-form.ts +76 -110
  202. package/src/openbao/pki-role.ts +12 -27
  203. package/src/openbao/plugin-form.test.ts +86 -0
  204. package/src/openbao/plugin-form.ts +164 -0
  205. package/src/openbao/plugin-reconcile.ts +92 -0
  206. package/src/openbao/plugin-wire.test.ts +146 -0
  207. package/src/openbao/plugin-wire.ts +65 -0
  208. package/src/openbao/plugin.ts +100 -0
  209. package/src/openbao/remount-wire.test.ts +87 -0
  210. package/src/openbao/remount-wire.ts +112 -0
  211. package/src/openbao/role-reconcile.test.ts +104 -0
  212. package/src/openbao/role-reconcile.ts +84 -0
  213. package/src/openbao/ssh-role-form.test.ts +105 -0
  214. package/src/openbao/ssh-role-form.ts +18 -16
  215. package/src/openbao/ssh-role.ts +3 -4
@@ -1,15 +1,310 @@
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 trimTrailing = (text, char) => {
144
+ let end = text.length;
145
+ while (end > 0 && text[end - 1] === char)
146
+ end -= 1;
147
+ return text.slice(0, end);
148
+ };
149
+ var trimRuns = (text, char) => {
150
+ let start = 0;
151
+ while (start < text.length && text[start] === char)
152
+ start += 1;
153
+ return trimTrailing(text.slice(start), char);
154
+ };
155
+ var trimTrailingSlashes = (text) => trimTrailing(text, "/");
156
+ var trimSlashes = (text) => trimRuns(text, "/");
157
+ var sshMountOf = (mount) => trimTrailingSlashes(mount ?? "ssh");
158
+
159
+ // src/openbao/approle-login-form.ts
160
+ var failureOf = (status) => status === 0 ? "unreachable" : status >= 200 && status < 300 ? "response" : "refused";
161
+
162
+ class BaoLoginError extends Error {
163
+ reason;
164
+ status;
165
+ errors;
166
+ constructor(reason, status, errors, operation) {
167
+ const code = status === 0 ? reason : `${reason} ${String(status)}`;
168
+ super(`${operation}: ${code}: ${errors.join("; ") || "(no errors given)"}`);
169
+ this.reason = reason;
170
+ this.status = status;
171
+ this.errors = errors;
172
+ this.name = "BaoLoginError";
173
+ }
174
+ }
175
+ var refusal = (label, value) => {
176
+ if (typeof value !== "string" || value === "")
177
+ return `${label} is empty`;
178
+ if (value.trim() === "")
179
+ return `${label} is only whitespace`;
180
+ if (value.trim() !== value) {
181
+ return `${label} has leading or trailing whitespace (a file read keeps its newline; trim it)`;
182
+ }
183
+ return;
184
+ };
185
+ var loginMount = (mount) => trimSlashes(mount ?? "approle");
186
+ var mountProblem = (mount) => {
187
+ const path = loginMount(mount);
188
+ if (path === "")
189
+ return "mount is empty";
190
+ const segments = path.split("/");
191
+ if (/[\s?#%\\]/.test(path) || segments.some((part) => part === "" || part === "." || part === "..")) {
192
+ return `mount ${JSON.stringify(path)} is not a plain mount path`;
193
+ }
194
+ return;
195
+ };
196
+ var loginPath = (mount) => `auth/${loginMount(mount)}/login`;
197
+ var GO_ESCAPES = {
198
+ 7: "a",
199
+ 8: "b",
200
+ 9: "t",
201
+ 10: "n",
202
+ 11: "v",
203
+ 12: "f",
204
+ 13: "r"
205
+ };
206
+ var goQuoted = (value) => [...value].map((char) => {
207
+ const code = char.codePointAt(0) ?? 0;
208
+ const named = GO_ESCAPES[code];
209
+ if (char === '"' || char === "\\")
210
+ return `\\${char}`;
211
+ if (named !== undefined)
212
+ return `\\${named}`;
213
+ if (code < 32 || code === 127)
214
+ return `\\x${code.toString(16).padStart(2, "0")}`;
215
+ return char;
216
+ }).join("");
217
+ var redact = (errors, secrets) => {
218
+ const forms = [...new Set(secrets.flatMap((secret) => [secret, goQuoted(secret)]))].filter((form) => form !== "").sort((a, b) => b.length - a.length);
219
+ return errors.map((entry) => forms.reduce((text, form) => text.split(form).join("[redacted]"), entry));
220
+ };
221
+
222
+ // src/openbao/approle-login-result.ts
223
+ class LoginResult {
224
+ #token;
225
+ accessor;
226
+ policies;
227
+ leaseDurationSeconds;
228
+ constructor(token, accessor, policies, lease) {
229
+ this.#token = token;
230
+ this.accessor = accessor;
231
+ this.policies = Object.freeze([...policies]);
232
+ this.leaseDurationSeconds = lease;
233
+ Object.freeze(this);
234
+ }
235
+ get clientToken() {
236
+ return this.#token;
237
+ }
238
+ }
239
+ var strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
240
+ var loginOf = (body) => {
241
+ const auth = body?.["auth"];
242
+ if (typeof auth !== "object" || auth === null || Array.isArray(auth)) {
243
+ return { problem: "the answer carried no auth block" };
244
+ }
245
+ const fields = auth;
246
+ const token = fields["client_token"];
247
+ if (typeof token !== "string" || token === "") {
248
+ const mfa = fields["mfa_requirement"];
249
+ return {
250
+ problem: mfa !== undefined && mfa !== null ? "the login requires MFA, which this helper does not complete" : "the auth block carried no client_token"
251
+ };
252
+ }
253
+ const lease = fields["lease_duration"];
254
+ return {
255
+ login: new LoginResult(token, typeof fields["accessor"] === "string" ? fields["accessor"] : "", strings(fields["policies"]), typeof lease === "number" && Number.isFinite(lease) ? lease : 0)
256
+ };
257
+ };
258
+
259
+ // src/openbao/approle-login.ts
260
+ var LOGIN = "OpenBao AppRole login";
261
+ var REVOKE = "OpenBao revoke-self";
262
+ var failed = (operation, secrets) => (error) => new BaoLoginError(failureOf(error.status), error.status, redact(error.errors, secrets), operation);
263
+ var appRoleLoginEffect = (input) => Effect2.gen(function* () {
264
+ const problems = [
265
+ refusal("roleId", input.roleId),
266
+ refusal("secretId", input.secretId),
267
+ mountProblem(input.mount)
268
+ ].filter((problem) => problem !== undefined);
269
+ if (problems.length > 0)
270
+ return yield* Effect2.fail(new BaoLoginError("input", 0, problems, LOGIN));
271
+ const env = yield* BaoEnv;
272
+ const body = yield* baoCall("write", "PUT", loginPath(input.mount), {
273
+ role_id: input.roleId,
274
+ secret_id: input.secretId
275
+ }).pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: "" }), Effect2.mapError(failed(LOGIN, [input.roleId, input.secretId])));
276
+ const read = loginOf(body);
277
+ if ("problem" in read) {
278
+ return yield* Effect2.fail(new BaoLoginError("response", 0, [read.problem], LOGIN));
279
+ }
280
+ return read.login;
281
+ });
282
+ var revokeSelfEffect = (token) => Effect2.gen(function* () {
283
+ const problem = refusal("token", token);
284
+ if (problem !== undefined) {
285
+ return yield* Effect2.fail(new BaoLoginError("input", 0, [problem], REVOKE));
286
+ }
287
+ const env = yield* BaoEnv;
288
+ yield* baoCall("write", "PUT", "auth/token/revoke-self").pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: token }), Effect2.mapError(failed(REVOKE, [token])));
289
+ });
290
+ var runWith = (env, effect) => {
291
+ const scoped = env === undefined ? effect : Effect2.provideService(effect, BaoEnv, env);
292
+ return Effect2.runPromise(scoped.pipe(Effect2.provide(FetchHttpClient2.layer)));
293
+ };
294
+ var appRoleLogin = (input) => runWith(input.env, appRoleLoginEffect(input));
295
+ var revokeSelf = (token, env) => runWith(env, revokeSelfEffect(token));
1
296
  // src/openbao/auth-method.ts
2
297
  import { Resource } from "alchemy";
3
298
  import { isResolved } from "alchemy/Diff";
4
299
  import * as Provider from "alchemy/Provider";
5
- import * as Effect3 from "effect/Effect";
300
+ import * as Effect7 from "effect/Effect";
6
301
 
7
302
  // src/openbao/digest.ts
8
303
  var canonical = (text) => text.trimEnd();
9
304
  var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
10
305
 
11
306
  // src/openbao/mount-form.ts
12
- var mountPath = (path) => path.replace(/\/+$/, "");
307
+ var mountPath = (path) => trimTrailingSlashes(path);
13
308
  var DURATION = /^(\d+)([smhd])$/;
14
309
  var parseDuration = (text) => {
15
310
  const trimmed = text.trim();
@@ -142,174 +437,164 @@ var sameTtl2 = (want, have) => {
142
437
  var matches2 = (attributes, props) => attributes.type === props.type && (props.description === undefined || attributes.description === props.description) && sameTtl2(props.defaultLeaseTtl, attributes.defaultLeaseTtl) && sameTtl2(props.maxLeaseTtl, attributes.maxLeaseTtl);
143
438
  var wantsTune2 = (props) => props.description !== undefined || props.defaultLeaseTtl !== undefined || props.maxLeaseTtl !== undefined;
144
439
 
440
+ // src/openbao/auth-method-reconcile.ts
441
+ import * as Effect6 from "effect/Effect";
442
+
145
443
  // 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, [
444
+ import * as Effect3 from "effect/Effect";
445
+ var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect3.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
287
446
  ...asked.errors,
288
447
  `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)));
448
+ ])), Effect3.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${authPath(path)}/`) ? Effect3.succeed(undefined) : Effect3.fail(asked)));
449
+ var readAuthMethodData = (path) => baoRead(readPath2(path)).pipe(Effect3.catchIf((error) => error.status === 400, (error) => absentFromTable(path, error)));
291
450
  var enableAuthMethod = (props) => baoWrite("POST", readPath2(props.path), enableBody2(props));
292
451
  var tuneAuthMethod = (props) => baoWrite("POST", tunePath2(props.path), tuneBody2(props));
293
452
  var disableAuthMethod = (path) => baoDelete(readPath2(path));
294
453
 
295
- // src/openbao/auth-method.ts
296
- var BaoAuthMethod = Resource("Bao.AuthMethod", {
297
- defaultRemovalPolicy: "retain"
454
+ // src/openbao/mount-move.ts
455
+ import * as Effect5 from "effect/Effect";
456
+
457
+ // src/openbao/remount-wire.ts
458
+ import * as Effect4 from "effect/Effect";
459
+ class RemountError extends Error {
460
+ constructor(message) {
461
+ super(message);
462
+ this.name = "RemountError";
463
+ }
464
+ }
465
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
466
+ var startRemount = (from, to) => Effect4.flatMap(baoCall("write", "POST", "sys/remount", { from, to }), (body) => {
467
+ const id = record(body?.["data"])?.["migration_id"];
468
+ return typeof id === "string" && id !== "" ? Effect4.succeed(id) : Effect4.fail(new RemountError(`sys/remount ${from} -> ${to} answered no migration_id.`));
469
+ });
470
+ var remountStatus = (id) => Effect4.map(baoRead(`sys/remount/status/${encodeURIComponent(id)}`), (data) => {
471
+ if (data === undefined)
472
+ return;
473
+ const status = record(data["migration_info"])?.["status"];
474
+ return typeof status === "string" ? status : "unknown";
475
+ });
476
+ var remountAndWait = (from, to, confirm, options = {}) => Effect4.gen(function* () {
477
+ const interval = options.intervalMillis ?? 1000;
478
+ const attempts = Math.max(1, options.attempts ?? 120);
479
+ const id = yield* startRemount(from, to);
480
+ const named = `sys/remount ${from} -> ${to} (migration ${id})`;
481
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
482
+ const status = yield* remountStatus(id);
483
+ if (status === "success")
484
+ return;
485
+ if (status === "failure") {
486
+ return yield* Effect4.fail(new RemountError(`${named} FAILED. The server log carries the reason, tagged with that migration_id. ` + "Read sys/mounts (or sys/auth) before retrying: the source may be tainted."));
487
+ }
488
+ if (status === undefined) {
489
+ if (yield* confirm)
490
+ return;
491
+ return yield* Effect4.fail(new RemountError(`${named}: the server no longer knows the migration (its tracker is in memory, lost ` + "on a restart or leader change), and the mount table does not show the move done."));
492
+ }
493
+ if (attempt < attempts)
494
+ yield* Effect4.sleep(interval);
495
+ }
496
+ return yield* Effect4.fail(new RemountError(`${named} is still in progress after ${String(attempts)} status reads. Do not re-run ` + `until sys/remount/status/${id} settles.`));
497
+ });
498
+
499
+ // src/openbao/mount-move.ts
500
+ var planMove = (family, stated, path, remountFrom) => {
501
+ const to = mountPath(path);
502
+ const from = remountFrom === undefined ? undefined : mountPath(remountFrom);
503
+ if (stated === undefined) {
504
+ return from === undefined || from === to ? { kind: "stay" } : { kind: "move", from };
505
+ }
506
+ const was = mountPath(stated);
507
+ if (was === to)
508
+ return { kind: "stay" };
509
+ if (from === undefined) {
510
+ return {
511
+ kind: "refuse",
512
+ message: `${family}: path changed from ${was} to ${to}. That would enable an EMPTY mount at ${to} ` + `and strand everything under ${was}. To move it with its data, declare ` + `\`remountFrom: '${was}'\` (leases under it are revoked; policies are not rewritten). ` + "For a new, empty mount, give it a new resource id instead."
513
+ };
514
+ }
515
+ if (from !== was) {
516
+ return {
517
+ kind: "refuse",
518
+ message: `${family}: remountFrom is ${from}, but the mount in state is ${was}.`
519
+ };
520
+ }
521
+ return { kind: "move", from };
522
+ };
523
+ var refuse = (message) => Effect5.die(new Error(message));
524
+ var performMove = (target, from, options) => Effect5.gen(function* () {
525
+ const to = mountPath(target.path);
526
+ const source = yield* target.read(from);
527
+ const landed = yield* target.read(to);
528
+ if (source !== undefined && landed !== undefined) {
529
+ return yield* refuse(`${target.family}: both ${from} and ${to} exist, so remountFrom cannot move one onto the ` + "other. Decide which holds the data, and retire the other by hand.");
530
+ }
531
+ if (source === undefined && landed === undefined) {
532
+ return yield* refuse(`${target.family}: remountFrom ${from} names no mount, and ${to} does not exist either. ` + "Refusing to enable an empty mount where data was expected.");
533
+ }
534
+ if (source === undefined)
535
+ return;
536
+ if (source.type !== target.type) {
537
+ return yield* refuse(`${target.family}: ${from} is type ${source.type}, not ${target.type}. A remount keeps the ` + "type; fix the declaration first.");
538
+ }
539
+ const confirm = Effect5.map(Effect5.all([target.read(from), target.read(to)]), ([gone, there]) => gone === undefined && there !== undefined);
540
+ yield* remountAndWait(target.wire(from), target.wire(to), confirm, options);
298
541
  });
299
- var readMethod = (props) => Effect3.gen(function* () {
542
+
543
+ // src/openbao/auth-method-reconcile.ts
544
+ var readMethod = (props) => Effect6.gen(function* () {
300
545
  const live = yield* readAuthMethodData(props.path);
301
546
  if (live === undefined)
302
547
  return;
303
548
  return attributesOf2(props, live);
304
549
  });
305
- var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed(BaoAuthMethod.Provider.of({
306
- list: () => Effect3.succeed([]),
307
- read: Effect3.fn(function* ({ olds }) {
550
+ var moveTarget = (props) => ({
551
+ family: "Bao.AuthMethod",
552
+ path: props.path,
553
+ read: (path) => readMethod({ ...props, path }),
554
+ type: props.type,
555
+ wire: (path) => `auth/${authPath(path)}`
556
+ });
557
+ var reconcileAuthMethod = (news, stated, options) => Effect6.gen(function* () {
558
+ const move = planMove("Bao.AuthMethod", stated, news.path, news.remountFrom);
559
+ if (move.kind === "refuse")
560
+ return yield* Effect6.die(new Error(move.message));
561
+ if (move.kind === "move")
562
+ yield* performMove(moveTarget(news), move.from, options);
563
+ let live = yield* readMethod(news);
564
+ if (live === undefined) {
565
+ yield* enableAuthMethod(news);
566
+ if (wantsTune2(news))
567
+ yield* tuneAuthMethod(news);
568
+ } else if (!matches2(live, news)) {
569
+ if (live.type !== news.type) {
570
+ return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
571
+ }
572
+ yield* tuneAuthMethod(news);
573
+ }
574
+ live = yield* readMethod(news);
575
+ if (live === undefined) {
576
+ return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
577
+ }
578
+ return live;
579
+ });
580
+
581
+ // src/openbao/auth-method.ts
582
+ var BaoAuthMethod = Resource("Bao.AuthMethod", {
583
+ defaultRemovalPolicy: "retain"
584
+ });
585
+ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect7.succeed(BaoAuthMethod.Provider.of({
586
+ list: () => Effect7.succeed([]),
587
+ read: Effect7.fn(function* ({ olds }) {
308
588
  return yield* readMethod(olds);
309
589
  }),
310
- diff: Effect3.fn(function* ({ news, output }) {
590
+ diff: Effect7.fn(function* ({ news, output }) {
311
591
  if (output === undefined || !isResolved(news))
312
592
  return;
593
+ const move = planMove("Bao.AuthMethod", output.path, news.path, news.remountFrom);
594
+ if (move.kind === "refuse")
595
+ return yield* Effect7.die(new Error(move.message));
596
+ if (move.kind === "move")
597
+ return { action: "update" };
313
598
  const live = yield* readMethod(news);
314
599
  if (live === undefined)
315
600
  return { action: "update" };
@@ -319,25 +604,10 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
319
604
  return { action: "noop" };
320
605
  return { action: "update" };
321
606
  }),
322
- reconcile: Effect3.fn(function* ({ news }) {
323
- let live = yield* readMethod(news);
324
- if (live === undefined) {
325
- yield* enableAuthMethod(news);
326
- if (wantsTune2(news))
327
- yield* tuneAuthMethod(news);
328
- } else if (!matches2(live, news)) {
329
- 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."));
331
- }
332
- yield* tuneAuthMethod(news);
333
- }
334
- live = yield* readMethod(news);
335
- 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.`));
337
- }
338
- return live;
607
+ reconcile: Effect7.fn(function* ({ news, output }) {
608
+ return yield* reconcileAuthMethod(news, output?.path);
339
609
  }),
340
- delete: Effect3.fn(function* ({ output }) {
610
+ delete: Effect7.fn(function* ({ output }) {
341
611
  yield* disableAuthMethod(output.path);
342
612
  return;
343
613
  })
@@ -346,7 +616,7 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
346
616
  import { Resource as Resource2 } from "alchemy";
347
617
  import { isResolved as isResolved2 } from "alchemy/Diff";
348
618
  import * as Provider2 from "alchemy/Provider";
349
- import * as Effect4 from "effect/Effect";
619
+ import * as Effect8 from "effect/Effect";
350
620
 
351
621
  // src/openbao/auth-role-form.ts
352
622
  var policiesOf = (live) => {
@@ -395,6 +665,7 @@ var writeBody = (props) => {
395
665
  }
396
666
  return body;
397
667
  };
668
+ var isRename = (from, to) => from.toLowerCase() !== to.toLowerCase();
398
669
  var samePolicies = (want, have) => want.length === have.length && want.every((policy, index) => policy === have[index]);
399
670
  var sameTtl3 = (want, have) => {
400
671
  const wantSeconds = parseDuration(want);
@@ -410,20 +681,22 @@ var matches3 = (attributes, props) => {
410
681
  var BaoAuthRole = Resource2("Bao.AuthRole", {
411
682
  defaultRemovalPolicy: "retain"
412
683
  });
413
- var readRole = (props) => Effect4.gen(function* () {
684
+ var readRole = (props) => Effect8.gen(function* () {
414
685
  const live = yield* baoRead(readPath3(props.name));
415
686
  if (live === undefined)
416
687
  return;
417
688
  return attributesOf3(props, live);
418
689
  });
419
- var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(BaoAuthRole.Provider.of({
420
- list: () => Effect4.succeed([]),
421
- read: Effect4.fn(function* ({ olds }) {
690
+ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect8.succeed(BaoAuthRole.Provider.of({
691
+ list: () => Effect8.succeed([]),
692
+ read: Effect8.fn(function* ({ olds }) {
422
693
  return yield* readRole(olds);
423
694
  }),
424
- diff: Effect4.fn(function* ({ news, output }) {
695
+ diff: Effect8.fn(function* ({ news, output }) {
425
696
  if (output === undefined || !isResolved2(news))
426
697
  return;
698
+ if (isRename(output.name, news.name))
699
+ return { action: "replace" };
427
700
  const live = yield* readRole(news);
428
701
  if (live === undefined)
429
702
  return { action: "update" };
@@ -431,27 +704,107 @@ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(Ba
431
704
  return { action: "noop" };
432
705
  return { action: "update" };
433
706
  }),
434
- reconcile: Effect4.fn(function* ({ news }) {
707
+ reconcile: Effect8.fn(function* ({ news }) {
435
708
  const live = yield* readRole(news);
436
709
  if (live === undefined || !matches3(live, news)) {
437
710
  yield* baoWrite("PUT", readPath3(news.name), writeBody(news));
438
711
  }
439
712
  const after = yield* readRole(news);
440
713
  if (after === undefined) {
441
- return yield* Effect4.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
714
+ return yield* Effect8.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
442
715
  }
443
716
  return after;
444
717
  }),
445
- delete: Effect4.fn(function* ({ output }) {
718
+ delete: Effect8.fn(function* ({ output }) {
446
719
  yield* baoDelete(readPath3(output.name));
447
720
  return;
448
721
  })
449
722
  })));
723
+ // src/openbao/bao-identity.ts
724
+ import * as Effect9 from "effect/Effect";
725
+ import * as FetchHttpClient3 from "effect/unstable/http/FetchHttpClient";
726
+
727
+ // src/openbao/bao-namespace.ts
728
+ var canonicalNamespace = (namespace) => {
729
+ const bare = trimSlashes((namespace ?? "").trim());
730
+ return bare === "root" ? "" : bare;
731
+ };
732
+ var namespacePath = (namespace) => {
733
+ const bare = canonicalNamespace(namespace);
734
+ return bare === "" ? "" : `${bare}/`;
735
+ };
736
+ var envNamespace = (env) => canonicalNamespace(resolveAddress(env).namespace);
737
+ var namespaceLabel = (namespace) => namespace === "" ? "root" : namespace;
738
+
739
+ // src/openbao/bao-identity.ts
740
+ class BaoIdentityError extends Error {
741
+ reason;
742
+ constructor(reason, detail) {
743
+ super(`REFUSING: ${detail}`);
744
+ this.reason = reason;
745
+ this.name = "BaoIdentityError";
746
+ }
747
+ }
748
+ var HEALTH_PATH = "sys/health?standbyok=true&sealedcode=200&uninitcode=200";
749
+ var NAMESPACE_PROBE_PATH = "sys/internal/ui/mounts";
750
+ var addressOf = (env) => {
751
+ const address = resolveAddress(env);
752
+ return address.socket === undefined ? address.base : `unix://${address.socket}`;
753
+ };
754
+ var unreachable = (address, what) => (error) => new BaoIdentityError("unreachable", `${address} did not answer ${what}: ${error.message}`);
755
+ var text3 = (value) => typeof value === "string" ? value : "";
756
+ var checkHealth = (address, expectedCluster, body) => {
757
+ if (body === undefined || typeof body["sealed"] !== "boolean") {
758
+ return new BaoIdentityError("unconfirmed", `${address} answered sys/health, but not as OpenBao.`);
759
+ }
760
+ if (body["initialized"] === false) {
761
+ return new BaoIdentityError("unconfirmed", `${address} is not initialised.`);
762
+ }
763
+ if (body["sealed"] === true) {
764
+ return new BaoIdentityError("unconfirmed", `${address} is sealed, and a sealed vault does not report its cluster_name.`);
765
+ }
766
+ const clusterName = text3(body["cluster_name"]);
767
+ if (clusterName !== expectedCluster) {
768
+ return new BaoIdentityError("mismatch", `${address} answers as cluster \`${clusterName || "(none)"}\`, but this stack expects ` + `\`${expectedCluster}\`. Check BAO_ADDR and BAO_AGENT_ADDR (the agent address wins).`);
769
+ }
770
+ return { clusterId: text3(body["cluster_id"]), version: text3(body["version"]) };
771
+ };
772
+ var assertBaoIdentityEffect = (expected) => Effect9.gen(function* () {
773
+ const env = yield* BaoEnv;
774
+ const address = addressOf(env);
775
+ const clusterName = expected.clusterName.trim();
776
+ if (clusterName === "") {
777
+ return yield* Effect9.fail(new BaoIdentityError("input", "clusterName is empty, so there is nothing to compare."));
778
+ }
779
+ const namespace = canonicalNamespace(expected.namespace);
780
+ const sent = envNamespace(env);
781
+ if (sent !== namespace) {
782
+ return yield* Effect9.fail(new BaoIdentityError("mismatch", `BAO_NAMESPACE resolves to \`${namespaceLabel(sent)}\`, but this stack expects ` + `\`${namespaceLabel(namespace)}\`.`));
783
+ }
784
+ const anonymous = { ...env, BAO_NAMESPACE: "", BAO_TOKEN: "" };
785
+ const body = yield* baoCall("read", "GET", HEALTH_PATH).pipe(Effect9.provideService(BaoEnv, anonymous), Effect9.mapError(unreachable(address, "sys/health")));
786
+ const health = checkHealth(address, clusterName, body);
787
+ if (health instanceof BaoIdentityError)
788
+ return yield* Effect9.fail(health);
789
+ if (namespace !== "") {
790
+ const scoped = { ...env, BAO_NAMESPACE: namespace, BAO_TOKEN: "" };
791
+ const probe = yield* baoCall("read", "GET", NAMESPACE_PROBE_PATH).pipe(Effect9.provideService(BaoEnv, scoped), Effect9.mapError(unreachable(address, `the namespace probe for \`${namespace}\``)));
792
+ if (probe === undefined) {
793
+ return yield* Effect9.fail(new BaoIdentityError("mismatch", `cluster \`${clusterName}\` at ${address} has no namespace \`${namespace}\`.`));
794
+ }
795
+ }
796
+ return { address, clusterName, namespace, ...health };
797
+ });
798
+ var assertBaoIdentity = (input) => {
799
+ const effect3 = assertBaoIdentityEffect(input);
800
+ const scoped = input.env === undefined ? effect3 : Effect9.provideService(effect3, BaoEnv, input.env);
801
+ return Effect9.runPromise(scoped.pipe(Effect9.provide(FetchHttpClient3.layer)));
802
+ };
450
803
  // src/openbao/cloudflare-role.ts
451
804
  import { Resource as Resource3 } from "alchemy";
452
805
  import { isResolved as isResolved3 } from "alchemy/Diff";
453
806
  import * as Provider3 from "alchemy/Provider";
454
- import * as Effect7 from "effect/Effect";
807
+ import * as Effect12 from "effect/Effect";
455
808
 
456
809
  // src/openbao/cloudflare-group-scope.ts
457
810
  var zoneResource = (zoneId) => `com.cloudflare.api.account.zone.${zoneId}`;
@@ -471,7 +824,7 @@ var groupKey = (name, scope) => `${scope} ${name}`;
471
824
 
472
825
  // src/openbao/cloudflare-permission-groups.ts
473
826
  import * as Context2 from "effect/Context";
474
- import * as Effect6 from "effect/Effect";
827
+ import * as Effect11 from "effect/Effect";
475
828
 
476
829
  // src/openbao/cloudflare-permission-groups-solve.ts
477
830
  var observationsOf = (declared, live) => declared.flatMap((role) => {
@@ -641,7 +994,7 @@ var samePolicies2 = (want, have) => want.length === have.length && want.every((w
641
994
  });
642
995
 
643
996
  // src/openbao/cloudflare-role-wire.ts
644
- import * as Effect5 from "effect/Effect";
997
+ import * as Effect10 from "effect/Effect";
645
998
 
646
999
  // src/openbao/cloudflare-role-form.ts
647
1000
  var rolePath = (mount, name) => `${mountPath(mount)}/roles/${name}`;
@@ -706,7 +1059,7 @@ var differences = (props, policies, live) => {
706
1059
  };
707
1060
 
708
1061
  // src/openbao/cloudflare-role-wire.ts
709
- var readCloudflareRole = (mount, name) => Effect5.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
1062
+ var readCloudflareRole = (mount, name) => Effect10.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
710
1063
  var writeCloudflareRole = (mount, name, body) => baoWrite("PUT", rolePath(mount, name), body);
711
1064
  var deleteCloudflareRole = (mount, name) => baoDelete(rolePath(mount, name));
712
1065
 
@@ -728,9 +1081,9 @@ var pick = (mount, groups, refs, hint) => {
728
1081
  const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));
729
1082
  if (unknown.length > 0) {
730
1083
  const problems = unknown.map((ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`);
731
- return Effect6.fail(new CloudflareGroupsError(mount, problems));
1084
+ return Effect11.fail(new CloudflareGroupsError(mount, problems));
732
1085
  }
733
- return Effect6.succeed(new Map(refs.flatMap((ref) => {
1086
+ return Effect11.succeed(new Map(refs.flatMap((ref) => {
734
1087
  const key = groupKey(ref.name, ref.scope);
735
1088
  const id = groups.get(key);
736
1089
  return id === undefined ? [] : [[key, id]];
@@ -738,15 +1091,15 @@ var pick = (mount, groups, refs, hint) => {
738
1091
  };
739
1092
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
740
1093
  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) => {
1094
+ var deniedNamed = (mount, read) => Effect11.catchIf(read, (error) => error.status === 403, () => Effect11.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
1095
+ var engineGroups = (mount) => Effect11.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
743
1096
  const list = data?.["permission_groups"];
744
1097
  if (data === undefined) {
745
1098
  const why = "GET <mount>/permission-groups answered 404 — the endpoint is not deployed here";
746
- return Effect6.fail(new CloudflareGroupsError(mount, [why]));
1099
+ return Effect11.fail(new CloudflareGroupsError(mount, [why]));
747
1100
  }
748
1101
  if (!Array.isArray(list)) {
749
- return Effect6.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
1102
+ return Effect11.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
750
1103
  }
751
1104
  const scoped = new Map;
752
1105
  const idsOf = new Map;
@@ -773,24 +1126,24 @@ var engineGroups = (mount) => Effect6.flatMap(deniedNamed(mount, baoRead(`${moun
773
1126
  const only = [...ids];
774
1127
  return ids.size === 1 && only[0] !== undefined ? [[name, only[0]]] : [];
775
1128
  }));
776
- return problems.length > 0 ? Effect6.fail(new CloudflareGroupsError(mount, problems)) : Effect6.succeed(new Map([...atEveryScope(sole), ...scoped]));
1129
+ return problems.length > 0 ? Effect11.fail(new CloudflareGroupsError(mount, problems)) : Effect11.succeed(new Map([...atEveryScope(sole), ...scoped]));
777
1130
  });
778
- var permissionGroupsFromEngine = () => Effect6.sync(() => {
1131
+ var permissionGroupsFromEngine = () => Effect11.sync(() => {
779
1132
  const cache = new Map;
780
- const cachedFor = (mount) => Effect6.suspend(() => {
1133
+ const cachedFor = (mount) => Effect11.suspend(() => {
781
1134
  const hit = cache.get(mountPath(mount));
782
1135
  if (hit !== undefined)
783
1136
  return hit;
784
- return Effect6.flatMap(Effect6.cached(engineGroups(mount)), (lookup) => {
1137
+ return Effect11.flatMap(Effect11.cached(engineGroups(mount)), (lookup) => {
785
1138
  cache.set(mountPath(mount), lookup);
786
1139
  return lookup;
787
1140
  });
788
1141
  });
789
1142
  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"))
1143
+ resolve: (mount, refs) => Effect11.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
791
1144
  });
792
1145
  });
793
- var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
1146
+ var permissionGroupsFromLiveRoles = (declared) => Effect11.gen(function* () {
794
1147
  const accountOf = new Map;
795
1148
  const byAccount = new Map;
796
1149
  for (const [mount, roles] of declared) {
@@ -801,9 +1154,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
801
1154
  }
802
1155
  const solved = new Map;
803
1156
  for (const [account, roles] of byAccount) {
804
- const solve = Effect6.gen(function* () {
1157
+ const solve = Effect11.gen(function* () {
805
1158
  const live = new Map;
806
- yield* Effect6.all(roles.map((role) => Effect6.map(readCloudflareRole(role.mount, role.name), (read) => {
1159
+ yield* Effect11.all(roles.map((role) => Effect11.map(readCloudflareRole(role.mount, role.name), (read) => {
807
1160
  if (read?.policies !== undefined) {
808
1161
  live.set(`${role.mount}/${role.name}`, read.policies);
809
1162
  }
@@ -811,11 +1164,11 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
811
1164
  const result = solveGroups(observationsOf(roles, live));
812
1165
  const problems = describeProblems(result);
813
1166
  if (problems.length > 0) {
814
- return yield* Effect6.fail(new CloudflareGroupsError(`account ${account}`, problems));
1167
+ return yield* Effect11.fail(new CloudflareGroupsError(`account ${account}`, problems));
815
1168
  }
816
1169
  return atEveryScope(result.groups);
817
1170
  });
818
- solved.set(account, yield* Effect6.cached(solve));
1171
+ solved.set(account, yield* Effect11.cached(solve));
819
1172
  }
820
1173
  return CloudflarePermissionGroups.of({
821
1174
  resolve: (mount, refs) => {
@@ -823,10 +1176,10 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
823
1176
  const lookup = account === undefined ? undefined : solved.get(account);
824
1177
  if (account === undefined || lookup === undefined) {
825
1178
  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]));
1179
+ return Effect11.fail(new CloudflareGroupsError(mount, [why]));
827
1180
  }
828
1181
  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));
1182
+ return Effect11.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
830
1183
  }
831
1184
  });
832
1185
  });
@@ -835,9 +1188,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
835
1188
  var BaoCloudflareRole = Resource3("Bao.CloudflareRole", {
836
1189
  defaultRemovalPolicy: "retain"
837
1190
  });
838
- var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect7.gen(function* () {
1191
+ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect12.gen(function* () {
839
1192
  const groups = yield* CloudflarePermissionGroups;
840
- const resolved = (props) => Effect7.gen(function* () {
1193
+ const resolved = (props) => Effect12.gen(function* () {
841
1194
  const refs = new Map;
842
1195
  for (const policy of props.policies) {
843
1196
  const scope = policy.scope ?? scopeOfResource(policy.resource);
@@ -847,17 +1200,17 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
847
1200
  const ids = yield* groups.resolve(props.mount, [...refs.values()]);
848
1201
  const { missing, policies } = resolvePolicies(props.policies, ids);
849
1202
  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.`));
1203
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
851
1204
  }
852
1205
  return policies;
853
1206
  });
854
1207
  return BaoCloudflareRole.Provider.of({
855
- list: () => Effect7.succeed([]),
856
- read: Effect7.fn(function* ({ olds }) {
1208
+ list: () => Effect12.succeed([]),
1209
+ read: Effect12.fn(function* ({ olds }) {
857
1210
  const live = yield* readCloudflareRole(olds.mount, olds.name);
858
1211
  return live === undefined ? undefined : attributesOf4(olds, live);
859
1212
  }),
860
- diff: Effect7.fn(function* ({ news, output }) {
1213
+ diff: Effect12.fn(function* ({ news, output }) {
861
1214
  if (output === undefined || !isResolved3(news))
862
1215
  return;
863
1216
  if (refusalOf(news) !== undefined)
@@ -868,11 +1221,11 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
868
1221
  return { action: "update" };
869
1222
  return differences(news, policies, live).length === 0 ? { action: "noop" } : { action: "update" };
870
1223
  }),
871
- reconcile: Effect7.fn(function* ({ news }) {
1224
+ reconcile: Effect12.fn(function* ({ news }) {
872
1225
  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}.`));
1226
+ const refusal2 = refusalOf(news);
1227
+ if (refusal2 !== undefined) {
1228
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
876
1229
  }
877
1230
  const policies = yield* resolved(news);
878
1231
  const live = yield* readCloudflareRole(news.mount, news.name);
@@ -881,22 +1234,22 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
881
1234
  }
882
1235
  const after = yield* readCloudflareRole(news.mount, news.name);
883
1236
  if (after === undefined) {
884
- return yield* Effect7.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
1237
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
885
1238
  }
886
1239
  const left = differences(news, policies, after);
887
1240
  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.`));
1241
+ return yield* Effect12.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
1242
  }
890
1243
  return attributesOf4(news, after);
891
1244
  }),
892
- delete: Effect7.fn(function* ({ output }) {
1245
+ delete: Effect12.fn(function* ({ output }) {
893
1246
  yield* deleteCloudflareRole(output.mount, output.name);
894
1247
  return;
895
1248
  })
896
1249
  });
897
1250
  }));
898
1251
  // src/openbao/cloudflare-roles-expand.ts
899
- var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1252
+ var slug = (name) => trimRuns(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-");
900
1253
  var zoneSlug = (zone) => `${slug(zone.name)}${zone.internal ? "-internal" : ""}`;
901
1254
  var mountName = (account, surface) => `cloudflare-${account}-${surface}`;
902
1255
  var policiesFor = (tpl, accountId, zone, accountGroups) => {
@@ -991,129 +1344,1014 @@ class RolesConfigError extends Error {
991
1344
  }
992
1345
  }
993
1346
  var isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
994
- var record = (value, where) => {
1347
+ var record2 = (value, where) => {
995
1348
  if (!isRaw(value))
996
1349
  throw new RolesConfigError(where, "expected a mapping");
997
1350
  return value;
998
1351
  };
999
- var text3 = (raw, key, where) => {
1000
- const value = raw[key];
1001
- if (typeof value === "string")
1002
- return value;
1003
- throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
1352
+ var text4 = (raw, key, where) => {
1353
+ const value = raw[key];
1354
+ if (typeof value === "string")
1355
+ return value;
1356
+ throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
1357
+ };
1358
+ var texts = (raw, key, where) => {
1359
+ const value = raw[key];
1360
+ if (value === undefined || value === null)
1361
+ return;
1362
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
1363
+ return value;
1364
+ }
1365
+ throw new RolesConfigError(`${where}.${key}`, "expected a list of strings");
1366
+ };
1367
+ var entries = (raw, key, where) => {
1368
+ const value = raw[key];
1369
+ if (value === undefined || value === null)
1370
+ return [];
1371
+ if (!Array.isArray(value))
1372
+ throw new RolesConfigError(`${where}.${key}`, "expected a list");
1373
+ return value;
1374
+ };
1375
+ var template = (value, where) => {
1376
+ const raw = record2(value, where);
1377
+ const groups = texts(raw, "groups", where);
1378
+ if (groups === undefined)
1379
+ throw new RolesConfigError(`${where}.groups`, "is required");
1380
+ const bucket = raw["bucket"] === undefined ? undefined : text4(raw, "bucket", where);
1381
+ if (bucket !== undefined && bucket.length === 0) {
1382
+ throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
1383
+ }
1384
+ const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text4(raw, "groups_scope", where));
1385
+ if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
1386
+ throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
1387
+ }
1388
+ return {
1389
+ accountGroups: texts(raw, "account_groups", where) ?? [],
1390
+ bucket,
1391
+ bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text4(raw, "bucket_jurisdiction", where),
1392
+ desc: raw["desc"] === undefined ? "" : text4(raw, "desc", where),
1393
+ groups,
1394
+ groupsScope,
1395
+ maxTtl: text4(raw, "max_ttl", where),
1396
+ name: text4(raw, "name", where),
1397
+ ttl: text4(raw, "ttl", where),
1398
+ zoneTypes: texts(raw, "zone_types", where) ?? [],
1399
+ zones: texts(raw, "zones", where)
1400
+ };
1401
+ };
1402
+ var surface = (name, value) => {
1403
+ const where = `surfaces.${name}`;
1404
+ const raw = record2(value, where);
1405
+ return {
1406
+ accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
1407
+ zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
1408
+ };
1409
+ };
1410
+ var account = (key, value, surfaces) => {
1411
+ const where = `accounts.${key}`;
1412
+ const raw = record2(value, where);
1413
+ const names = texts(raw, "surfaces", where) ?? [];
1414
+ for (const name of names) {
1415
+ if (surfaces[name] === undefined) {
1416
+ throw new RolesConfigError(`${where}.surfaces`, `names ${name}, which surfaces: never defines`);
1417
+ }
1418
+ }
1419
+ const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
1420
+ return { id: text4(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
1421
+ };
1422
+ var parseRolesConfig = (yaml) => {
1423
+ const root = record2(Bun.YAML.parse(yaml), "(document)");
1424
+ const surfaces = {};
1425
+ for (const [name, spec] of Object.entries(record2(root["surfaces"], "surfaces"))) {
1426
+ surfaces[name] = surface(name, spec);
1427
+ }
1428
+ const accounts = Object.entries(record2(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
1429
+ return {
1430
+ accounts,
1431
+ namespace: text4(record2(root["defaults"], "defaults"), "namespace", "defaults"),
1432
+ surfaces
1433
+ };
1434
+ };
1435
+ // src/openbao/host-approles.ts
1436
+ var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1437
+ var HOST_ROLE_SEPARATOR = "--";
1438
+ var hostRoleName = (className, host) => `${className}${HOST_ROLE_SEPARATOR}${host}`;
1439
+ var classProblems = (name, spec) => {
1440
+ const found = [];
1441
+ if (!NAME.test(name))
1442
+ found.push(`class \`${name}\` is not lowercase letters, digits, single hyphens`);
1443
+ if (spec.policies.length === 0)
1444
+ found.push(`class \`${name}\` has no policies`);
1445
+ if (spec.policies.includes("root"))
1446
+ found.push(`class \`${name}\` grants \`root\``);
1447
+ const secretId = parseDuration(spec.secretIdTtl);
1448
+ if (secretId === undefined || secretId <= 0) {
1449
+ found.push(`class \`${name}\` secretIdTtl \`${spec.secretIdTtl}\` must be a duration above zero ` + "(0 never expires)");
1450
+ }
1451
+ const ttl = parseDuration(spec.tokenTtl);
1452
+ const max = parseDuration(spec.tokenMaxTtl);
1453
+ if (ttl === undefined || max === undefined) {
1454
+ found.push(`class \`${name}\` token TTLs must be durations like 15m or 1h`);
1455
+ } else if (max > 0 && ttl > max) {
1456
+ found.push(`class \`${name}\` tokenTtl is longer than tokenMaxTtl`);
1457
+ }
1458
+ return found;
1459
+ };
1460
+ var hostAppRoles = (input) => {
1461
+ const problems = Object.entries(input.classes).flatMap(([name, spec]) => classProblems(name, spec));
1462
+ const seen = new Set;
1463
+ const roles = [];
1464
+ for (const host of input.hosts) {
1465
+ if (!NAME.test(host.name)) {
1466
+ problems.push(`host \`${host.name}\` is not lowercase letters, digits, single hyphens`);
1467
+ }
1468
+ if (seen.has(host.name))
1469
+ problems.push(`host \`${host.name}\` is listed twice`);
1470
+ seen.add(host.name);
1471
+ const spec = input.classes[host.class];
1472
+ if (spec === undefined) {
1473
+ problems.push(`host \`${host.name}\` names unknown class \`${host.class}\``);
1474
+ continue;
1475
+ }
1476
+ roles.push({
1477
+ bindSecretId: true,
1478
+ name: hostRoleName(host.class, host.name),
1479
+ secretIdNumUses: spec.secretIdNumUses ?? 0,
1480
+ secretIdTtl: spec.secretIdTtl,
1481
+ tokenMaxTtl: spec.tokenMaxTtl,
1482
+ tokenPolicies: [...new Set(spec.policies)].sort(),
1483
+ tokenTtl: spec.tokenTtl
1484
+ });
1485
+ }
1486
+ if (problems.length > 0) {
1487
+ throw new Error(`hostAppRoles refused the input: ${problems.join("; ")}.`);
1488
+ }
1489
+ return roles.sort((a, b) => a.name.localeCompare(b.name));
1490
+ };
1491
+ // src/openbao/jwt-config.ts
1492
+ import { Resource as Resource4 } from "alchemy";
1493
+ import { isResolved as isResolved4 } from "alchemy/Diff";
1494
+ import * as Provider4 from "alchemy/Provider";
1495
+ import * as Effect14 from "effect/Effect";
1496
+
1497
+ // src/openbao/jwt-config-form.ts
1498
+ var text5 = (value) => typeof value === "string" ? value.trim() : "";
1499
+ var strings2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1500
+ var sortedSet = (values) => [...new Set(values.map((value) => value.trim()))].sort();
1501
+ var canonical2 = (input) => ({
1502
+ boundIssuer: input.boundIssuer.trim(),
1503
+ defaultRole: input.defaultRole.trim(),
1504
+ jwksCaPem: input.jwksCaPem.trim(),
1505
+ jwksUrl: input.jwksUrl.trim(),
1506
+ jwtSupportedAlgs: sortedSet(input.jwtSupportedAlgs),
1507
+ jwtValidationPubkeys: sortedSet(input.jwtValidationPubkeys),
1508
+ oidcDiscoveryCaPem: input.oidcDiscoveryCaPem.trim(),
1509
+ oidcDiscoveryUrl: input.oidcDiscoveryUrl.trim()
1510
+ });
1511
+ var canonicalFromProps = (props) => canonical2({
1512
+ boundIssuer: props.boundIssuer ?? "",
1513
+ defaultRole: props.defaultRole ?? "",
1514
+ jwksCaPem: props.jwksCaPem ?? "",
1515
+ jwksUrl: props.jwksUrl ?? "",
1516
+ jwtSupportedAlgs: props.jwtSupportedAlgs ?? [],
1517
+ jwtValidationPubkeys: props.jwtValidationPubkeys ?? [],
1518
+ oidcDiscoveryCaPem: props.oidcDiscoveryCaPem ?? "",
1519
+ oidcDiscoveryUrl: props.oidcDiscoveryUrl ?? ""
1520
+ });
1521
+ var canonicalFromLive = (live) => canonical2({
1522
+ boundIssuer: text5(live["bound_issuer"]),
1523
+ defaultRole: text5(live["default_role"]),
1524
+ jwksCaPem: text5(live["jwks_ca_pem"]),
1525
+ jwksUrl: text5(live["jwks_url"]),
1526
+ jwtSupportedAlgs: strings2(live["jwt_supported_algs"]),
1527
+ jwtValidationPubkeys: strings2(live["jwt_validation_pubkeys"]),
1528
+ oidcDiscoveryCaPem: text5(live["oidc_discovery_ca_pem"]),
1529
+ oidcDiscoveryUrl: text5(live["oidc_discovery_url"])
1530
+ });
1531
+ var mountOf = (props) => mountPath(props.mount ?? "jwt");
1532
+ var configPath = (props) => `auth/${mountOf(props)}/config`;
1533
+ var attributesOf5 = (props, live) => {
1534
+ const form = canonicalFromLive(live);
1535
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf(props) };
1536
+ };
1537
+ var matches4 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps(props)));
1538
+ var writeBody3 = (props) => ({
1539
+ bound_issuer: props.boundIssuer ?? "",
1540
+ default_role: props.defaultRole ?? "",
1541
+ jwks_ca_pem: props.jwksCaPem ?? "",
1542
+ jwks_url: props.jwksUrl ?? "",
1543
+ jwt_supported_algs: [...props.jwtSupportedAlgs ?? []],
1544
+ jwt_validation_pubkeys: [...props.jwtValidationPubkeys ?? []],
1545
+ oidc_discovery_ca_pem: props.oidcDiscoveryCaPem ?? "",
1546
+ oidc_discovery_url: props.oidcDiscoveryUrl ?? ""
1547
+ });
1548
+ var wouldErase = (live) => {
1549
+ const lost = [];
1550
+ if (text5(live["oidc_client_id"]) !== "")
1551
+ lost.push("oidc_client_id (and its unreadable secret)");
1552
+ const provider = live["provider_config"];
1553
+ if (typeof provider === "object" && provider !== null && Object.keys(provider).length > 0) {
1554
+ lost.push("provider_config");
1555
+ }
1556
+ if (text5(live["oidc_response_mode"]) !== "")
1557
+ lost.push("oidc_response_mode");
1558
+ if (strings2(live["oidc_response_types"]).length > 0)
1559
+ lost.push("oidc_response_types");
1560
+ if (strings2(live["override_allowed_server_names"]).length > 0) {
1561
+ lost.push("override_allowed_server_names");
1562
+ }
1563
+ return lost;
1564
+ };
1565
+ var problems = (props) => {
1566
+ const found = [];
1567
+ const sources = [
1568
+ (props.oidcDiscoveryUrl ?? "") !== "",
1569
+ (props.jwksUrl ?? "") !== "",
1570
+ (props.jwtValidationPubkeys?.length ?? 0) > 0
1571
+ ].filter(Boolean).length;
1572
+ if (sources !== 1) {
1573
+ found.push("exactly one of oidcDiscoveryUrl, jwksUrl or jwtValidationPubkeys must be set");
1574
+ }
1575
+ if ((props.oidcDiscoveryUrl ?? "").includes(".well-known")) {
1576
+ found.push("oidcDiscoveryUrl must be the issuer, without /.well-known/openid-configuration");
1577
+ }
1578
+ const role = props.defaultRole ?? "";
1579
+ if (role !== role.toLowerCase())
1580
+ found.push(`defaultRole \`${role}\` must be lower case`);
1581
+ return found;
1582
+ };
1583
+
1584
+ // src/openbao/role-reconcile.ts
1585
+ import * as Effect13 from "effect/Effect";
1586
+ var readRoleAt = (spec) => Effect13.map(baoRead(spec.path), (live) => live === undefined ? undefined : { attributes: spec.attributesOf(live), live });
1587
+ var planRole = (spec) => Effect13.gen(function* () {
1588
+ if (spec.problems.length > 0)
1589
+ return "update";
1590
+ const found = yield* readRoleAt(spec);
1591
+ return found !== undefined && spec.matches(found.attributes) ? "noop" : "update";
1592
+ });
1593
+ var refuse2 = (spec, message) => Effect13.die(new Error(`${spec.family} ${spec.path}: ${message}`));
1594
+ var reconcileRole = (spec) => Effect13.gen(function* () {
1595
+ if (spec.problems.length > 0)
1596
+ return yield* refuse2(spec, spec.problems.join("; "));
1597
+ const found = yield* readRoleAt(spec);
1598
+ if (found === undefined || !spec.matches(found.attributes)) {
1599
+ const lost = found === undefined ? [] : spec.wouldErase?.(found.live) ?? [];
1600
+ if (lost.length > 0) {
1601
+ return yield* refuse2(spec, `live carries ${lost.join(", ")}, which this resource does not declare and the write ` + "would reset. Fold it into the declaration, or clear it by hand first.");
1602
+ }
1603
+ yield* baoWrite("PUT", spec.path, spec.body);
1604
+ }
1605
+ const after = yield* readRoleAt(spec);
1606
+ if (after === undefined)
1607
+ return yield* refuse2(spec, "write reported success but it is absent.");
1608
+ if (!spec.matches(after.attributes)) {
1609
+ return yield* refuse2(spec, "wrote cleanly but read back different — OpenBao stored something other than what was " + "declared. Read it by hand before retrying.");
1610
+ }
1611
+ return after.attributes;
1612
+ });
1613
+
1614
+ // src/openbao/jwt-config.ts
1615
+ var BaoJwtAuthConfig = Resource4("Bao.JwtAuthConfig", {
1616
+ defaultRemovalPolicy: "retain"
1617
+ });
1618
+ var jwtConfigSpec = (props) => ({
1619
+ attributesOf: (live) => attributesOf5(props, live),
1620
+ body: writeBody3(props),
1621
+ family: "Bao.JwtAuthConfig",
1622
+ matches: (attributes) => matches4(attributes, props),
1623
+ path: configPath(props),
1624
+ problems: problems(props),
1625
+ wouldErase
1626
+ });
1627
+ var BaoJwtAuthConfigProvider = () => Provider4.effect(BaoJwtAuthConfig, Effect14.succeed(BaoJwtAuthConfig.Provider.of({
1628
+ list: () => Effect14.succeed([]),
1629
+ read: Effect14.fn(function* ({ olds }) {
1630
+ const found = yield* readRoleAt(jwtConfigSpec(olds));
1631
+ return found?.attributes;
1632
+ }),
1633
+ diff: Effect14.fn(function* ({ news, output }) {
1634
+ if (output === undefined || !isResolved4(news))
1635
+ return;
1636
+ if (mountOf(news) !== output.mount)
1637
+ return { action: "replace" };
1638
+ return { action: yield* planRole(jwtConfigSpec(news)) };
1639
+ }),
1640
+ reconcile: Effect14.fn(function* ({ news }) {
1641
+ return yield* reconcileRole(jwtConfigSpec(news));
1642
+ }),
1643
+ delete: () => Effect14.void
1644
+ })));
1645
+ // src/openbao/jwt-role.ts
1646
+ import { Resource as Resource5 } from "alchemy";
1647
+ import { isResolved as isResolved5 } from "alchemy/Diff";
1648
+ import * as Provider5 from "alchemy/Provider";
1649
+ import * as Effect15 from "effect/Effect";
1650
+
1651
+ // src/openbao/auth-token-form.ts
1652
+ var sortedSet2 = (values) => [...new Set(values)].sort();
1653
+ var cidr = (value) => value.trim().replace(/\/(32|128)$/, "");
1654
+ var strings3 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1655
+ var seconds2 = (text6) => parseDuration(text6 ?? "0") ?? -1;
1656
+ var tokenFields = (input) => ({
1657
+ tokenBoundCidrs: input.tokenBoundCidrs,
1658
+ tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
1659
+ tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
1660
+ tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
1661
+ tokenNumUses: input.tokenNumUses,
1662
+ tokenPeriodSeconds: input.tokenPeriodSeconds,
1663
+ tokenPolicies: input.tokenPolicies,
1664
+ tokenTtlSeconds: input.tokenTtlSeconds,
1665
+ tokenType: input.tokenType
1666
+ });
1667
+ var tokenForm = (input) => ({
1668
+ tokenBoundCidrs: sortedSet2(input.tokenBoundCidrs.map(cidr)),
1669
+ tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
1670
+ tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
1671
+ tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
1672
+ tokenNumUses: input.tokenNumUses,
1673
+ tokenPeriodSeconds: input.tokenPeriodSeconds,
1674
+ tokenPolicies: sortedSet2(input.tokenPolicies),
1675
+ tokenTtlSeconds: input.tokenTtlSeconds,
1676
+ tokenType: input.tokenType
1677
+ });
1678
+ var tokenFormOfProps = (props) => tokenForm({
1679
+ tokenBoundCidrs: props.tokenBoundCidrs ?? [],
1680
+ tokenExplicitMaxTtlSeconds: seconds2(props.tokenExplicitMaxTtl),
1681
+ tokenMaxTtlSeconds: seconds2(props.tokenMaxTtl),
1682
+ tokenNoDefaultPolicy: props.tokenNoDefaultPolicy ?? false,
1683
+ tokenNumUses: props.tokenNumUses ?? 0,
1684
+ tokenPeriodSeconds: seconds2(props.tokenPeriod),
1685
+ tokenPolicies: props.tokenPolicies,
1686
+ tokenTtlSeconds: seconds2(props.tokenTtl),
1687
+ tokenType: props.tokenType ?? "default"
1688
+ });
1689
+ var tokenFormOfLive = (live) => tokenForm({
1690
+ tokenBoundCidrs: strings3(live["token_bound_cidrs"]),
1691
+ tokenExplicitMaxTtlSeconds: ttlSeconds(live["token_explicit_max_ttl"]) ?? -1,
1692
+ tokenMaxTtlSeconds: ttlSeconds(live["token_max_ttl"]) ?? -1,
1693
+ tokenNoDefaultPolicy: live["token_no_default_policy"] === true,
1694
+ tokenNumUses: typeof live["token_num_uses"] === "number" ? live["token_num_uses"] : -1,
1695
+ tokenPeriodSeconds: ttlSeconds(live["token_period"]) ?? -1,
1696
+ tokenPolicies: strings3(live["token_policies"]),
1697
+ tokenTtlSeconds: ttlSeconds(live["token_ttl"]) ?? -1,
1698
+ tokenType: typeof live["token_type"] === "string" ? live["token_type"] : ""
1699
+ });
1700
+ var tokenBody = (props) => ({
1701
+ token_bound_cidrs: [...props.tokenBoundCidrs ?? []],
1702
+ token_explicit_max_ttl: props.tokenExplicitMaxTtl ?? "0",
1703
+ token_max_ttl: props.tokenMaxTtl ?? "0",
1704
+ token_no_default_policy: props.tokenNoDefaultPolicy ?? false,
1705
+ token_num_uses: props.tokenNumUses ?? 0,
1706
+ token_period: props.tokenPeriod ?? "0",
1707
+ token_policies: [...props.tokenPolicies],
1708
+ token_ttl: props.tokenTtl ?? "0",
1709
+ token_type: props.tokenType ?? "default"
1710
+ });
1711
+ var tokenProblems = (props) => {
1712
+ const found = [];
1713
+ const durations = {
1714
+ tokenExplicitMaxTtl: props.tokenExplicitMaxTtl,
1715
+ tokenMaxTtl: props.tokenMaxTtl,
1716
+ tokenPeriod: props.tokenPeriod,
1717
+ tokenTtl: props.tokenTtl
1718
+ };
1719
+ for (const [field, text6] of Object.entries(durations)) {
1720
+ if (seconds2(text6) < 0)
1721
+ found.push(`${field}=${text6 ?? ""} is not a duration (0 or <n>s|m|h|d)`);
1722
+ }
1723
+ const ttl = seconds2(props.tokenTtl);
1724
+ const max = seconds2(props.tokenMaxTtl);
1725
+ if (ttl > 0 && max > 0 && ttl > max)
1726
+ found.push("tokenTtl is longer than tokenMaxTtl");
1727
+ if (props.tokenType === "batch" && (seconds2(props.tokenPeriod) > 0 || (props.tokenNumUses ?? 0) > 0)) {
1728
+ found.push("a batch token cannot be periodic or use-limited (tokenutil.go:234-241)");
1729
+ }
1730
+ return found;
1731
+ };
1732
+
1733
+ // src/openbao/jwt-role-form.ts
1734
+ var sortedSet3 = (values) => [...new Set(values)].sort();
1735
+ var text6 = (value) => typeof value === "string" ? value : "";
1736
+ var strings4 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1737
+ var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1738
+ var claimsOf = (value) => {
1739
+ const source = record3(value);
1740
+ const out = {};
1741
+ for (const key of Object.keys(source).sort()) {
1742
+ const raw = source[key];
1743
+ out[key] = sortedSet3(typeof raw === "string" ? [raw] : strings4(raw));
1744
+ }
1745
+ return out;
1746
+ };
1747
+ var mappingsOf = (value) => {
1748
+ const source = record3(value);
1749
+ const out = {};
1750
+ for (const key of Object.keys(source).sort())
1751
+ out[key] = text6(source[key]);
1752
+ return out;
1753
+ };
1754
+ var canonical3 = (input) => ({
1755
+ allowedRedirectUris: sortedSet3(input.allowedRedirectUris),
1756
+ boundAudiences: sortedSet3(input.boundAudiences),
1757
+ boundClaims: claimsOf(input.boundClaims),
1758
+ boundClaimsType: input.boundClaimsType,
1759
+ boundSubject: input.boundSubject,
1760
+ callbackMode: input.callbackMode,
1761
+ claimMappings: mappingsOf(input.claimMappings),
1762
+ groupsClaim: input.groupsClaim,
1763
+ oidcDisableConfirmation: input.oidcDisableConfirmation,
1764
+ oidcScopes: sortedSet3(input.oidcScopes),
1765
+ roleType: input.roleType,
1766
+ userClaim: input.userClaim,
1767
+ verboseOidcLogging: input.verboseOidcLogging,
1768
+ ...tokenFields(input)
1769
+ });
1770
+ var canonicalFromProps2 = (props) => canonical3({
1771
+ ...tokenFormOfProps(props),
1772
+ allowedRedirectUris: props.allowedRedirectUris ?? [],
1773
+ boundAudiences: props.boundAudiences ?? [],
1774
+ boundClaims: claimsOf(props.boundClaims ?? {}),
1775
+ boundClaimsType: props.boundClaimsType ?? "string",
1776
+ boundSubject: props.boundSubject ?? "",
1777
+ callbackMode: props.callbackMode ?? "client",
1778
+ claimMappings: props.claimMappings ?? {},
1779
+ groupsClaim: props.groupsClaim ?? "",
1780
+ oidcDisableConfirmation: false,
1781
+ oidcScopes: props.oidcScopes ?? [],
1782
+ roleType: props.roleType,
1783
+ userClaim: props.userClaim,
1784
+ verboseOidcLogging: false
1785
+ });
1786
+ var canonicalFromLive2 = (live) => canonical3({
1787
+ ...tokenFormOfLive(live),
1788
+ allowedRedirectUris: strings4(live["allowed_redirect_uris"]),
1789
+ boundAudiences: strings4(live["bound_audiences"]),
1790
+ boundClaims: claimsOf(live["bound_claims"]),
1791
+ boundClaimsType: text6(live["bound_claims_type"]),
1792
+ boundSubject: text6(live["bound_subject"]),
1793
+ callbackMode: text6(live["callback_mode"]),
1794
+ claimMappings: mappingsOf(live["claim_mappings"]),
1795
+ groupsClaim: text6(live["groups_claim"]),
1796
+ oidcDisableConfirmation: live["oidc_disable_confirmation"] === true,
1797
+ oidcScopes: strings4(live["oidc_scopes"]),
1798
+ roleType: text6(live["role_type"]),
1799
+ userClaim: text6(live["user_claim"]),
1800
+ verboseOidcLogging: live["verbose_oidc_logging"] === true
1801
+ });
1802
+ var mountOf2 = (props) => mountPath(props.mount ?? "jwt");
1803
+ var rolePath2 = (props) => `auth/${mountOf2(props)}/role/${props.name}`;
1804
+ var attributesOf6 = (props, live) => {
1805
+ const form = canonicalFromLive2(live);
1806
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf2(props), name: props.name };
1807
+ };
1808
+ var matches5 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps2(props)));
1809
+ var writeBody4 = (props) => ({
1810
+ allowed_redirect_uris: [...props.allowedRedirectUris ?? []],
1811
+ bound_audiences: [...props.boundAudiences ?? []],
1812
+ bound_claims: { ...props.boundClaims },
1813
+ bound_claims_type: props.boundClaimsType ?? "string",
1814
+ bound_subject: props.boundSubject ?? "",
1815
+ callback_mode: props.callbackMode ?? "client",
1816
+ claim_mappings: { ...props.claimMappings },
1817
+ groups_claim: props.groupsClaim ?? "",
1818
+ oidc_disable_confirmation: false,
1819
+ oidc_scopes: [...props.oidcScopes ?? []],
1820
+ role_type: props.roleType,
1821
+ user_claim: props.userClaim,
1822
+ verbose_oidc_logging: false,
1823
+ ...tokenBody(props)
1824
+ });
1825
+ var NAME2 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
1826
+ var problems2 = (props) => {
1827
+ const found = [...tokenProblems(props)];
1828
+ if (!NAME2.test(props.name))
1829
+ found.push(`name \`${props.name}\` must be lower case (the server folds it)`);
1830
+ if (props.userClaim.trim() === "")
1831
+ found.push("userClaim is empty; the server requires one");
1832
+ const bound = (props.boundAudiences?.length ?? 0) > 0 || (props.boundSubject ?? "") !== "" || Object.keys(props.boundClaims ?? {}).length > 0 || (props.tokenBoundCidrs?.length ?? 0) > 0;
1833
+ if (props.roleType === "jwt" && !bound) {
1834
+ found.push("a jwt role needs a bound constraint: boundAudiences, boundSubject, boundClaims or tokenBoundCidrs");
1835
+ }
1836
+ if (props.roleType === "oidc" && (props.allowedRedirectUris?.length ?? 0) === 0) {
1837
+ found.push("an oidc role needs allowedRedirectUris");
1838
+ }
1839
+ const targets = Object.values(props.claimMappings ?? {});
1840
+ if (targets.includes("role"))
1841
+ found.push("claimMappings may not map to the reserved key `role`");
1842
+ if (new Set(targets).size !== targets.length)
1843
+ found.push("claimMappings maps two claims to one key");
1844
+ return found;
1845
+ };
1846
+
1847
+ // src/openbao/jwt-role.ts
1848
+ var BaoJwtRole = Resource5("Bao.JwtRole", {
1849
+ defaultRemovalPolicy: "retain"
1850
+ });
1851
+ var jwtRoleSpec = (props) => ({
1852
+ attributesOf: (live) => attributesOf6(props, live),
1853
+ body: writeBody4(props),
1854
+ family: "Bao.JwtRole",
1855
+ matches: (attributes) => matches5(attributes, props),
1856
+ path: rolePath2(props),
1857
+ problems: problems2(props)
1858
+ });
1859
+ var BaoJwtRoleProvider = () => Provider5.effect(BaoJwtRole, Effect15.succeed(BaoJwtRole.Provider.of({
1860
+ list: () => Effect15.succeed([]),
1861
+ read: Effect15.fn(function* ({ olds }) {
1862
+ const found = yield* readRoleAt(jwtRoleSpec(olds));
1863
+ return found?.attributes;
1864
+ }),
1865
+ diff: Effect15.fn(function* ({ news, output }) {
1866
+ if (output === undefined || !isResolved5(news))
1867
+ return;
1868
+ if (mountOf2(news) !== output.mount || news.name !== output.name) {
1869
+ return { action: "replace" };
1870
+ }
1871
+ return { action: yield* planRole(jwtRoleSpec(news)) };
1872
+ }),
1873
+ reconcile: Effect15.fn(function* ({ news }) {
1874
+ return yield* reconcileRole(jwtRoleSpec(news));
1875
+ }),
1876
+ delete: Effect15.fn(function* ({ output }) {
1877
+ yield* baoDelete(rolePath2(output));
1878
+ return;
1879
+ })
1880
+ })));
1881
+ // src/openbao/kubernetes-role.ts
1882
+ import { Resource as Resource6 } from "alchemy";
1883
+ import { isResolved as isResolved6 } from "alchemy/Diff";
1884
+ import * as Provider6 from "alchemy/Provider";
1885
+ import * as Effect16 from "effect/Effect";
1886
+
1887
+ // src/openbao/kubernetes-role-form.ts
1888
+ var sortedSet4 = (values) => [...new Set(values)].sort();
1889
+ var text7 = (value) => typeof value === "string" ? value : "";
1890
+ var strings5 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1891
+ var canonical4 = (input) => ({
1892
+ aliasNameSource: input.aliasNameSource,
1893
+ audience: input.audience,
1894
+ boundServiceAccountNames: sortedSet4(input.boundServiceAccountNames),
1895
+ boundServiceAccountNamespaceSelector: input.boundServiceAccountNamespaceSelector.trim(),
1896
+ boundServiceAccountNamespaces: sortedSet4(input.boundServiceAccountNamespaces),
1897
+ ...tokenFields(input)
1898
+ });
1899
+ var canonicalFromProps3 = (props) => canonical4({
1900
+ ...tokenFormOfProps(props),
1901
+ aliasNameSource: props.aliasNameSource,
1902
+ audience: props.audience ?? "",
1903
+ boundServiceAccountNames: props.boundServiceAccountNames,
1904
+ boundServiceAccountNamespaceSelector: props.boundServiceAccountNamespaceSelector ?? "",
1905
+ boundServiceAccountNamespaces: props.boundServiceAccountNamespaces ?? []
1906
+ });
1907
+ var canonicalFromLive3 = (live) => canonical4({
1908
+ ...tokenFormOfLive(live),
1909
+ aliasNameSource: text7(live["alias_name_source"]),
1910
+ audience: text7(live["audience"]),
1911
+ boundServiceAccountNames: strings5(live["bound_service_account_names"]),
1912
+ boundServiceAccountNamespaceSelector: text7(live["bound_service_account_namespace_selector"]),
1913
+ boundServiceAccountNamespaces: strings5(live["bound_service_account_namespaces"])
1914
+ });
1915
+ var mountOf3 = (props) => mountPath(props.mount ?? "kubernetes");
1916
+ var rolePath3 = (props) => `auth/${mountOf3(props)}/role/${props.name}`;
1917
+ var attributesOf7 = (props, live) => {
1918
+ const form = canonicalFromLive3(live);
1919
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf3(props), name: props.name };
1920
+ };
1921
+ var matches6 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps3(props)));
1922
+ var writeBody5 = (props) => ({
1923
+ alias_name_source: props.aliasNameSource,
1924
+ audience: props.audience ?? "",
1925
+ bound_service_account_names: [...props.boundServiceAccountNames],
1926
+ bound_service_account_namespace_selector: props.boundServiceAccountNamespaceSelector ?? "",
1927
+ bound_service_account_namespaces: [...props.boundServiceAccountNamespaces ?? []],
1928
+ ...tokenBody(props)
1929
+ });
1930
+ var NAME3 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
1931
+ var starMixed = (values) => values.length > 1 && values.includes("*");
1932
+ var problems3 = (props) => {
1933
+ const found = [...tokenProblems(props)];
1934
+ const namespaces = props.boundServiceAccountNamespaces ?? [];
1935
+ const selector = (props.boundServiceAccountNamespaceSelector ?? "").trim();
1936
+ if (!NAME3.test(props.name))
1937
+ found.push(`name \`${props.name}\` must be lower case (stored folded)`);
1938
+ if (props.boundServiceAccountNames.length === 0)
1939
+ found.push("boundServiceAccountNames is empty");
1940
+ if (starMixed(props.boundServiceAccountNames))
1941
+ found.push("`*` mixed with other account names");
1942
+ if (namespaces.length === 0 && selector === "") {
1943
+ found.push("boundServiceAccountNamespaces is empty and no namespace selector is set");
1944
+ }
1945
+ if (starMixed(namespaces))
1946
+ found.push("`*` mixed with other namespaces");
1947
+ return found;
1004
1948
  };
1005
- var texts = (raw, key, where) => {
1006
- const value = raw[key];
1007
- if (value === undefined || value === null)
1949
+
1950
+ // src/openbao/kubernetes-role.ts
1951
+ var BaoKubernetesRole = Resource6("Bao.KubernetesRole", {
1952
+ defaultRemovalPolicy: "retain"
1953
+ });
1954
+ var kubernetesRoleSpec = (props) => ({
1955
+ attributesOf: (live) => attributesOf7(props, live),
1956
+ body: writeBody5(props),
1957
+ family: "Bao.KubernetesRole",
1958
+ matches: (attributes) => matches6(attributes, props),
1959
+ path: rolePath3(props),
1960
+ problems: problems3(props)
1961
+ });
1962
+ var BaoKubernetesRoleProvider = () => Provider6.effect(BaoKubernetesRole, Effect16.succeed(BaoKubernetesRole.Provider.of({
1963
+ list: () => Effect16.succeed([]),
1964
+ read: Effect16.fn(function* ({ olds }) {
1965
+ const found = yield* readRoleAt(kubernetesRoleSpec(olds));
1966
+ return found?.attributes;
1967
+ }),
1968
+ diff: Effect16.fn(function* ({ news, output }) {
1969
+ if (output === undefined || !isResolved6(news))
1970
+ return;
1971
+ if (mountOf3(news) !== output.mount || news.name !== output.name) {
1972
+ return { action: "replace" };
1973
+ }
1974
+ return { action: yield* planRole(kubernetesRoleSpec(news)) };
1975
+ }),
1976
+ reconcile: Effect16.fn(function* ({ news }) {
1977
+ return yield* reconcileRole(kubernetesRoleSpec(news));
1978
+ }),
1979
+ delete: Effect16.fn(function* ({ output }) {
1980
+ yield* baoDelete(rolePath3(output));
1008
1981
  return;
1009
- if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
1010
- return value;
1011
- }
1012
- throw new RolesConfigError(`${where}.${key}`, "expected a list of strings");
1982
+ })
1983
+ })));
1984
+ // src/openbao/mfa-enforcement.ts
1985
+ import { Resource as Resource7 } from "alchemy";
1986
+ import { isResolved as isResolved7 } from "alchemy/Diff";
1987
+ import * as Provider7 from "alchemy/Provider";
1988
+ import * as Effect18 from "effect/Effect";
1989
+
1990
+ // src/openbao/mfa-enforcement-form.ts
1991
+ var sortedSet5 = (values) => [...new Set(values.map((value) => value.trim()))].filter((value) => value !== "").sort();
1992
+ var strings6 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1993
+ var text8 = (value) => typeof value === "string" ? value : "";
1994
+ var canonical5 = (input) => ({
1995
+ authMethodAccessors: sortedSet5(input.authMethodAccessors),
1996
+ authMethodTypes: sortedSet5(input.authMethodTypes),
1997
+ identityEntityIds: sortedSet5(input.identityEntityIds),
1998
+ identityGroupIds: sortedSet5(input.identityGroupIds),
1999
+ mfaMethodIds: sortedSet5(input.mfaMethodIds)
2000
+ });
2001
+ var canonicalFromProps4 = (props, resolved) => canonical5({
2002
+ authMethodAccessors: [...props.authMethodAccessors ?? [], ...resolved],
2003
+ authMethodTypes: props.authMethodTypes ?? [],
2004
+ identityEntityIds: props.identityEntityIds ?? [],
2005
+ identityGroupIds: props.identityGroupIds ?? [],
2006
+ mfaMethodIds: props.mfaMethodIds
2007
+ });
2008
+ var canonicalFromLive4 = (live) => canonical5({
2009
+ authMethodAccessors: strings6(live["auth_method_accessors"]),
2010
+ authMethodTypes: strings6(live["auth_method_types"]),
2011
+ identityEntityIds: strings6(live["identity_entity_ids"]),
2012
+ identityGroupIds: strings6(live["identity_group_ids"]),
2013
+ mfaMethodIds: strings6(live["mfa_method_ids"])
2014
+ });
2015
+ var attributesOf8 = (name, live) => {
2016
+ const form = canonicalFromLive4(live);
2017
+ return {
2018
+ ...form,
2019
+ digest: sha256(JSON.stringify(form)),
2020
+ enforcementId: text8(live["id"]),
2021
+ name,
2022
+ namespacePath: text8(live["namespace_path"])
2023
+ };
1013
2024
  };
1014
- var entries = (raw, key, where) => {
1015
- const value = raw[key];
1016
- if (value === undefined || value === null)
1017
- return [];
1018
- if (!Array.isArray(value))
1019
- throw new RolesConfigError(`${where}.${key}`, "expected a list");
1020
- return value;
2025
+ var matches7 = (attributes, props, resolved) => attributes.digest === sha256(JSON.stringify(canonicalFromProps4(props, resolved)));
2026
+ var writeBody6 = (form) => ({
2027
+ auth_method_accessors: [...form.authMethodAccessors],
2028
+ auth_method_types: [...form.authMethodTypes],
2029
+ identity_entity_ids: [...form.identityEntityIds],
2030
+ identity_group_ids: [...form.identityGroupIds],
2031
+ mfa_method_ids: [...form.mfaMethodIds]
2032
+ });
2033
+ var NAME4 = /^\w(?:[\w.-]*\w)?$/;
2034
+ var problems4 = (props, missingPaths) => {
2035
+ const found = [];
2036
+ if (!NAME4.test(props.name))
2037
+ found.push(`name \`${props.name}\` is not a valid enforcement name`);
2038
+ if (sortedSet5(props.mfaMethodIds).length === 0)
2039
+ found.push("mfaMethodIds is empty");
2040
+ const targets = [
2041
+ props.authMethodPaths,
2042
+ props.authMethodAccessors,
2043
+ props.authMethodTypes,
2044
+ props.identityGroupIds,
2045
+ props.identityEntityIds
2046
+ ].some((list) => sortedSet5(list ?? []).length > 0);
2047
+ if (!targets)
2048
+ found.push("no target: an enforcement with none is accepted and enforces NOTHING");
2049
+ if (missingPaths.length > 0) {
2050
+ found.push(`no auth mount at ${missingPaths.map((path) => mountPath(path)).join(", ")}`);
2051
+ }
2052
+ return found;
1021
2053
  };
1022
- var template = (value, where) => {
1023
- const raw = record(value, where);
1024
- const groups = texts(raw, "groups", where);
1025
- if (groups === undefined)
1026
- throw new RolesConfigError(`${where}.groups`, "is required");
1027
- const bucket = raw["bucket"] === undefined ? undefined : text3(raw, "bucket", where);
1028
- if (bucket !== undefined && bucket.length === 0) {
1029
- throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
2054
+
2055
+ // src/openbao/mfa-wire.ts
2056
+ import * as Effect17 from "effect/Effect";
2057
+ var TOTP_PATH = "identity/mfa/method/totp";
2058
+ var METHODS_PATH = "identity/mfa/method";
2059
+ var enforcementPath = (name) => `identity/mfa/login-enforcement/${name}`;
2060
+ var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
2061
+ var ownNamespacePath = Effect17.gen(function* () {
2062
+ const env = yield* BaoEnv;
2063
+ return namespacePath(envNamespace(env));
2064
+ });
2065
+ var findTotp = (name) => Effect17.gen(function* () {
2066
+ const listing = yield* baoRead(`${METHODS_PATH}?list=true`);
2067
+ const info = record4(listing?.["key_info"]) ?? {};
2068
+ const own = yield* ownNamespacePath;
2069
+ for (const [id, raw] of Object.entries(info)) {
2070
+ const live = record4(raw);
2071
+ if (live === undefined || live["name"] !== name || live["namespace_path"] !== own)
2072
+ continue;
2073
+ if (live["type"] !== "totp") {
2074
+ return yield* Effect17.die(new Error(`Bao.MfaTotpMethod ${name}: that name belongs to a ${String(live["type"])} method ` + `(${id}) in this namespace, and a TOTP write would convert it. Pick another name.`));
2075
+ }
2076
+ return { id, live };
1030
2077
  }
1031
- const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text3(raw, "groups_scope", where));
1032
- if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
1033
- throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
2078
+ return;
2079
+ });
2080
+ var writeTotp = (body) => Effect17.asVoid(baoCall("write", "POST", TOTP_PATH, body));
2081
+ var deleteTotp = (id) => baoDelete(`${TOTP_PATH}/${id}`);
2082
+ var readEnforcement = (name) => baoRead(enforcementPath(name));
2083
+ var writeEnforcement = (name, body) => Effect17.asVoid(baoCall("write", "POST", enforcementPath(name), body));
2084
+ var authAccessors = (paths) => Effect17.gen(function* () {
2085
+ if (paths.length === 0)
2086
+ return { accessors: [], missing: [] };
2087
+ const table = (yield* baoRead("sys/auth")) ?? {};
2088
+ const accessors = [];
2089
+ const missing = [];
2090
+ for (const path of paths) {
2091
+ const accessor = record4(table[`${mountPath(path)}/`])?.["accessor"];
2092
+ if (typeof accessor === "string" && accessor !== "")
2093
+ accessors.push(accessor);
2094
+ else
2095
+ missing.push(mountPath(path));
2096
+ }
2097
+ return { accessors, missing };
2098
+ });
2099
+
2100
+ // src/openbao/mfa-enforcement.ts
2101
+ var BaoMfaLoginEnforcement = Resource7("Bao.MfaLoginEnforcement", {
2102
+ defaultRemovalPolicy: "retain"
2103
+ });
2104
+ var refuse3 = (name, message) => Effect18.die(new Error(`Bao.MfaLoginEnforcement ${name}: ${message}`));
2105
+ var resolveTargets = (props) => Effect18.map(authAccessors(props.authMethodPaths ?? []), ({ accessors, missing }) => ({
2106
+ bad: problems4(props, missing),
2107
+ resolved: accessors
2108
+ }));
2109
+ var readLive = (name) => Effect18.map(readEnforcement(name), (live) => live === undefined ? undefined : attributesOf8(name, live));
2110
+ var planEnforcement = (props) => Effect18.gen(function* () {
2111
+ const { bad, resolved } = yield* resolveTargets(props);
2112
+ if (bad.length > 0)
2113
+ return "update";
2114
+ const live = yield* readLive(props.name);
2115
+ return live !== undefined && matches7(live, props, resolved) ? "noop" : "update";
2116
+ });
2117
+ var reconcileEnforcement = (props) => Effect18.gen(function* () {
2118
+ const { bad, resolved } = yield* resolveTargets(props);
2119
+ if (bad.length > 0)
2120
+ return yield* refuse3(props.name, bad.join("; "));
2121
+ const live = yield* readLive(props.name);
2122
+ if (live === undefined || !matches7(live, props, resolved)) {
2123
+ yield* writeEnforcement(props.name, writeBody6(canonicalFromProps4(props, resolved)));
1034
2124
  }
2125
+ const after = yield* readLive(props.name);
2126
+ if (after === undefined)
2127
+ return yield* refuse3(props.name, "the write succeeded but it is absent.");
2128
+ if (!matches7(after, props, resolved)) {
2129
+ return yield* refuse3(props.name, "the enforcement read back different.");
2130
+ }
2131
+ return after;
2132
+ });
2133
+ var BaoMfaLoginEnforcementProvider = () => Provider7.effect(BaoMfaLoginEnforcement, Effect18.succeed(BaoMfaLoginEnforcement.Provider.of({
2134
+ list: () => Effect18.succeed([]),
2135
+ read: Effect18.fn(function* ({ olds }) {
2136
+ return yield* readLive(olds.name);
2137
+ }),
2138
+ diff: Effect18.fn(function* ({ news, output }) {
2139
+ if (output === undefined || !isResolved7(news))
2140
+ return;
2141
+ if (news.name !== output.name)
2142
+ return { action: "replace" };
2143
+ return { action: yield* planEnforcement(news) };
2144
+ }),
2145
+ reconcile: Effect18.fn(function* ({ news }) {
2146
+ return yield* reconcileEnforcement(news);
2147
+ }),
2148
+ delete: Effect18.fn(function* ({ output }) {
2149
+ return yield* refuse3(output.name, "REFUSING to delete: OpenBao 2.6.2 drops a login enforcement from memory only, and it " + "returns on the next unseal (openbao/openbao#4030). Set this resource back to " + "retain to drop it from state; remove it for good only once the server is fixed.");
2150
+ })
2151
+ })));
2152
+ // src/openbao/mfa-totp.ts
2153
+ import { Resource as Resource8 } from "alchemy";
2154
+ import { isResolved as isResolved8 } from "alchemy/Diff";
2155
+ import * as Provider8 from "alchemy/Provider";
2156
+ import * as Effect19 from "effect/Effect";
2157
+
2158
+ // src/openbao/mfa-totp-form.ts
2159
+ var canonical6 = (input) => ({
2160
+ algorithm: input.algorithm,
2161
+ digits: input.digits,
2162
+ issuer: input.issuer,
2163
+ keySize: input.keySize,
2164
+ maxValidationAttempts: input.maxValidationAttempts,
2165
+ periodSeconds: input.periodSeconds,
2166
+ qrSize: input.qrSize,
2167
+ skew: input.skew
2168
+ });
2169
+ var canonicalFromProps5 = (props) => canonical6({
2170
+ algorithm: props.algorithm ?? "SHA1",
2171
+ digits: props.digits ?? 6,
2172
+ issuer: props.issuer,
2173
+ keySize: props.keySize ?? 20,
2174
+ maxValidationAttempts: props.maxValidationAttempts ?? 5,
2175
+ periodSeconds: parseDuration(props.period ?? "30s") ?? -1,
2176
+ qrSize: props.qrSize ?? 200,
2177
+ skew: props.skew ?? 1
2178
+ });
2179
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : -1;
2180
+ var text9 = (value) => typeof value === "string" ? value : "";
2181
+ var canonicalFromLive5 = (live) => canonical6({
2182
+ algorithm: text9(live["algorithm"]),
2183
+ digits: num(live["digits"]),
2184
+ issuer: text9(live["issuer"]),
2185
+ keySize: num(live["key_size"]),
2186
+ maxValidationAttempts: num(live["max_validation_attempts"]),
2187
+ periodSeconds: ttlSeconds(live["period"]) ?? -1,
2188
+ qrSize: num(live["qr_size"]),
2189
+ skew: num(live["skew"])
2190
+ });
2191
+ var attributesOf9 = (methodId, live) => {
2192
+ const form = canonicalFromLive5(live);
1035
2193
  return {
1036
- accountGroups: texts(raw, "account_groups", where) ?? [],
1037
- bucket,
1038
- bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text3(raw, "bucket_jurisdiction", where),
1039
- desc: raw["desc"] === undefined ? "" : text3(raw, "desc", where),
1040
- groups,
1041
- groupsScope,
1042
- maxTtl: text3(raw, "max_ttl", where),
1043
- name: text3(raw, "name", where),
1044
- ttl: text3(raw, "ttl", where),
1045
- zoneTypes: texts(raw, "zone_types", where) ?? [],
1046
- zones: texts(raw, "zones", where)
2194
+ ...form,
2195
+ digest: sha256(JSON.stringify(form)),
2196
+ methodId,
2197
+ name: text9(live["name"]),
2198
+ namespacePath: text9(live["namespace_path"])
1047
2199
  };
1048
2200
  };
1049
- var surface = (name, value) => {
1050
- const where = `surfaces.${name}`;
1051
- const raw = record(value, where);
2201
+ var matches8 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps5(props)));
2202
+ var writeBody7 = (props) => {
2203
+ const form = canonicalFromProps5(props);
1052
2204
  return {
1053
- accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
1054
- zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
2205
+ algorithm: form.algorithm,
2206
+ digits: form.digits,
2207
+ issuer: form.issuer,
2208
+ key_size: form.keySize,
2209
+ max_validation_attempts: form.maxValidationAttempts,
2210
+ method_name: props.name,
2211
+ period: props.period ?? "30s",
2212
+ qr_size: form.qrSize,
2213
+ skew: form.skew
1055
2214
  };
1056
2215
  };
1057
- var account = (key, value, surfaces) => {
1058
- const where = `accounts.${key}`;
1059
- const raw = record(value, where);
1060
- const names = texts(raw, "surfaces", where) ?? [];
1061
- for (const name of names) {
1062
- if (surfaces[name] === undefined) {
1063
- throw new RolesConfigError(`${where}.surfaces`, `names ${name}, which surfaces: never defines`);
1064
- }
1065
- }
1066
- const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
1067
- return { id: text3(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
1068
- };
1069
- var parseRolesConfig = (yaml) => {
1070
- const root = record(Bun.YAML.parse(yaml), "(document)");
1071
- const surfaces = {};
1072
- for (const [name, spec] of Object.entries(record(root["surfaces"], "surfaces"))) {
1073
- surfaces[name] = surface(name, spec);
1074
- }
1075
- const accounts = Object.entries(record(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
1076
- return {
1077
- accounts,
1078
- namespace: text3(record(root["defaults"], "defaults"), "namespace", "defaults"),
1079
- surfaces
1080
- };
2216
+ var renameProblem = (from, to) => from === to ? undefined : `renaming from \`${from}\` would strand every enrolled secret. Declare the new method as a ` + "second resource, list both ids on the enforcement, enrol, then remove the old one " + "(mfa-totp.ts).";
2217
+ var problems5 = (props) => {
2218
+ const found = [];
2219
+ const form = canonicalFromProps5(props);
2220
+ if (props.name.trim() === "")
2221
+ found.push("name is empty; it is the only key a declaration has");
2222
+ if (props.issuer.trim() === "")
2223
+ found.push("issuer is empty; the server requires one");
2224
+ if (form.periodSeconds <= 0)
2225
+ found.push(`period \`${props.period ?? ""}\` must be a duration above zero`);
2226
+ if (form.keySize <= 0)
2227
+ found.push("keySize must be above zero");
2228
+ if (form.maxValidationAttempts < 1)
2229
+ found.push("maxValidationAttempts must be at least 1 (0 reads back as 5)");
2230
+ return found;
1081
2231
  };
2232
+
2233
+ // src/openbao/mfa-totp.ts
2234
+ var BaoMfaTotpMethod = Resource8("Bao.MfaTotpMethod", {
2235
+ defaultRemovalPolicy: "retain"
2236
+ });
2237
+ var refuse4 = (props, message) => Effect19.die(new Error(`Bao.MfaTotpMethod ${props.name}: ${message}`));
2238
+ var readTotp = (props) => Effect19.map(findTotp(props.name), (found) => found === undefined ? undefined : attributesOf9(found.id, found.live));
2239
+ var planTotp = (props) => Effect19.gen(function* () {
2240
+ if (problems5(props).length > 0)
2241
+ return "update";
2242
+ const live = yield* readTotp(props);
2243
+ return live !== undefined && matches8(live, props) ? "noop" : "update";
2244
+ });
2245
+ var reconcileTotp = (props) => Effect19.gen(function* () {
2246
+ const bad = problems5(props);
2247
+ if (bad.length > 0)
2248
+ return yield* refuse4(props, bad.join("; "));
2249
+ const live = yield* readTotp(props);
2250
+ if (live === undefined || !matches8(live, props))
2251
+ yield* writeTotp(writeBody7(props));
2252
+ const after = yield* readTotp(props);
2253
+ if (after === undefined)
2254
+ return yield* refuse4(props, "the write succeeded but no method has that name.");
2255
+ if (!matches8(after, props))
2256
+ return yield* refuse4(props, "the method read back different.");
2257
+ return after;
2258
+ });
2259
+ var BaoMfaTotpMethodProvider = () => Provider8.effect(BaoMfaTotpMethod, Effect19.succeed(BaoMfaTotpMethod.Provider.of({
2260
+ list: () => Effect19.succeed([]),
2261
+ read: Effect19.fn(function* ({ olds }) {
2262
+ return yield* readTotp(olds);
2263
+ }),
2264
+ diff: Effect19.fn(function* ({ news, output }) {
2265
+ if (output === undefined || !isResolved8(news))
2266
+ return;
2267
+ const rename = renameProblem(output.name, news.name);
2268
+ if (rename !== undefined)
2269
+ return yield* refuse4(news, rename);
2270
+ return { action: yield* planTotp(news) };
2271
+ }),
2272
+ reconcile: Effect19.fn(function* ({ news }) {
2273
+ return yield* reconcileTotp(news);
2274
+ }),
2275
+ delete: Effect19.fn(function* ({ output }) {
2276
+ yield* deleteTotp(output.methodId);
2277
+ return;
2278
+ })
2279
+ })));
1082
2280
  // src/openbao/mount.ts
1083
- import { Resource as Resource4 } from "alchemy";
1084
- import { isResolved as isResolved4 } from "alchemy/Diff";
1085
- import * as Provider4 from "alchemy/Provider";
1086
- import * as Effect9 from "effect/Effect";
2281
+ import { Resource as Resource9 } from "alchemy";
2282
+ import { isResolved as isResolved9 } from "alchemy/Diff";
2283
+ import * as Provider9 from "alchemy/Provider";
2284
+ import * as Effect22 from "effect/Effect";
2285
+
2286
+ // src/openbao/mount-reconcile.ts
2287
+ import * as Effect21 from "effect/Effect";
1087
2288
 
1088
2289
  // 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, [
2290
+ import * as Effect20 from "effect/Effect";
2291
+ var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect20.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
1091
2292
  ...asked.errors,
1092
2293
  `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)));
2294
+ ])), Effect20.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect20.succeed(undefined) : Effect20.fail(asked)));
2295
+ var readMountData = (path) => baoRead(readPath(path)).pipe(Effect20.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
1095
2296
  var enableMount = (props) => baoWrite("POST", readPath(props.path), enableBody(props));
1096
2297
  var tuneMount = (props) => baoWrite("POST", tunePath(props.path), tuneBody(props));
1097
2298
  var disableMount = (path) => baoDelete(readPath(path));
1098
2299
 
1099
- // src/openbao/mount.ts
1100
- var BaoMount = Resource4("Bao.Mount", {
1101
- defaultRemovalPolicy: "retain"
1102
- });
1103
- var readMount = (props) => Effect9.gen(function* () {
2300
+ // src/openbao/mount-reconcile.ts
2301
+ var readMount = (props) => Effect21.gen(function* () {
1104
2302
  const live = yield* readMountData(props.path);
1105
2303
  if (live === undefined)
1106
2304
  return;
1107
2305
  return attributesOf(props, live);
1108
2306
  });
1109
- var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount.Provider.of({
1110
- list: () => Effect9.succeed([]),
1111
- read: Effect9.fn(function* ({ olds }) {
2307
+ var moveTarget2 = (props) => ({
2308
+ family: "Bao.Mount",
2309
+ path: props.path,
2310
+ read: (path) => readMount({ ...props, path }),
2311
+ type: props.type,
2312
+ wire: mountPath
2313
+ });
2314
+ var reconcileMount = (news, stated, options) => Effect21.gen(function* () {
2315
+ const move = planMove("Bao.Mount", stated, news.path, news.remountFrom);
2316
+ if (move.kind === "refuse")
2317
+ return yield* Effect21.die(new Error(move.message));
2318
+ if (move.kind === "move")
2319
+ yield* performMove(moveTarget2(news), move.from, options);
2320
+ let live = yield* readMount(news);
2321
+ if (live === undefined) {
2322
+ yield* enableMount(news);
2323
+ if (wantsTune(news))
2324
+ yield* tuneMount(news);
2325
+ } else if (!matches(live, news)) {
2326
+ if (live.type !== news.type) {
2327
+ return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
2328
+ }
2329
+ yield* tuneMount(news);
2330
+ }
2331
+ live = yield* readMount(news);
2332
+ if (live === undefined) {
2333
+ return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
2334
+ }
2335
+ return live;
2336
+ });
2337
+
2338
+ // src/openbao/mount.ts
2339
+ var BaoMount = Resource9("Bao.Mount", {
2340
+ defaultRemovalPolicy: "retain"
2341
+ });
2342
+ var BaoMountProvider = () => Provider9.effect(BaoMount, Effect22.succeed(BaoMount.Provider.of({
2343
+ list: () => Effect22.succeed([]),
2344
+ read: Effect22.fn(function* ({ olds }) {
1112
2345
  return yield* readMount(olds);
1113
2346
  }),
1114
- diff: Effect9.fn(function* ({ news, output }) {
1115
- if (output === undefined || !isResolved4(news))
2347
+ diff: Effect22.fn(function* ({ news, output }) {
2348
+ if (output === undefined || !isResolved9(news))
1116
2349
  return;
2350
+ const move = planMove("Bao.Mount", output.path, news.path, news.remountFrom);
2351
+ if (move.kind === "refuse")
2352
+ return yield* Effect22.die(new Error(move.message));
2353
+ if (move.kind === "move")
2354
+ return { action: "update" };
1117
2355
  const live = yield* readMount(news);
1118
2356
  if (live === undefined)
1119
2357
  return { action: "update" };
@@ -1123,39 +2361,29 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount
1123
2361
  return { action: "noop" };
1124
2362
  return { action: "update" };
1125
2363
  }),
1126
- reconcile: Effect9.fn(function* ({ news }) {
1127
- let live = yield* readMount(news);
1128
- if (live === undefined) {
1129
- yield* enableMount(news);
1130
- if (wantsTune(news))
1131
- yield* tuneMount(news);
1132
- } else if (!matches(live, news)) {
1133
- 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."));
1135
- }
1136
- yield* tuneMount(news);
1137
- }
1138
- live = yield* readMount(news);
1139
- 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.`));
1141
- }
1142
- return live;
2364
+ reconcile: Effect22.fn(function* ({ news, output }) {
2365
+ return yield* reconcileMount(news, output?.path);
1143
2366
  }),
1144
- delete: Effect9.fn(function* ({ output }) {
2367
+ delete: Effect22.fn(function* ({ output }) {
1145
2368
  yield* disableMount(output.path);
1146
2369
  return;
1147
2370
  })
1148
2371
  })));
1149
2372
  // src/openbao/pki-role.ts
1150
- import { Resource as Resource5 } from "alchemy";
1151
- import { isResolved as isResolved5 } from "alchemy/Diff";
1152
- import * as Provider5 from "alchemy/Provider";
1153
- import * as Effect10 from "effect/Effect";
2373
+ import { Resource as Resource10 } from "alchemy";
2374
+ import { isResolved as isResolved10 } from "alchemy/Diff";
2375
+ import * as Provider10 from "alchemy/Provider";
2376
+ import * as Effect23 from "effect/Effect";
1154
2377
 
1155
- // src/openbao/pki-role-form.ts
2378
+ // src/openbao/pki-role-canonical.ts
2379
+ var DEFAULT_KEY_USAGE = [
2380
+ "DigitalSignature",
2381
+ "KeyAgreement",
2382
+ "KeyEncipherment"
2383
+ ];
1156
2384
  var sorted = (values) => [...values].sort();
1157
2385
  var foldedUsages = (values) => sorted(values.map((v) => v.toLowerCase()));
1158
- var canonical2 = (input) => ({
2386
+ var canonical7 = (input) => ({
1159
2387
  allowBareDomains: input.allowBareDomains,
1160
2388
  allowGlobDomains: input.allowGlobDomains,
1161
2389
  allowIpSans: input.allowIpSans,
@@ -1163,18 +2391,24 @@ var canonical2 = (input) => ({
1163
2391
  allowSubdomains: input.allowSubdomains,
1164
2392
  allowWildcardCertificates: input.allowWildcardCertificates,
1165
2393
  allowedDomains: sorted(input.allowedDomains),
2394
+ allowedDomainsTemplate: input.allowedDomainsTemplate,
1166
2395
  clientFlag: input.clientFlag,
1167
2396
  cnValidations: sorted(input.cnValidations),
2397
+ enforceHostnames: input.enforceHostnames,
1168
2398
  extKeyUsage: foldedUsages(input.extKeyUsage),
2399
+ generateLease: input.generateLease,
1169
2400
  keyBits: input.keyBits,
1170
2401
  keyType: input.keyType,
2402
+ keyUsage: foldedUsages(input.keyUsage),
1171
2403
  maxTtlSeconds: input.maxTtlSeconds,
2404
+ noStore: input.noStore,
2405
+ requireCn: input.requireCn,
1172
2406
  serverFlag: input.serverFlag,
1173
2407
  signatureBits: input.signatureBits,
1174
2408
  ttlSeconds: input.ttlSeconds
1175
2409
  });
1176
- var seconds2 = (text4) => parseDuration(text4) ?? -1;
1177
- var canonicalFromProps = (props) => canonical2({
2410
+ var seconds3 = (text10) => parseDuration(text10) ?? -1;
2411
+ var canonicalFromProps6 = (props) => canonical7({
1178
2412
  allowBareDomains: props.allowBareDomains ?? false,
1179
2413
  allowGlobDomains: props.allowGlobDomains ?? false,
1180
2414
  allowIpSans: props.allowIpSans ?? false,
@@ -1182,21 +2416,27 @@ var canonicalFromProps = (props) => canonical2({
1182
2416
  allowSubdomains: props.allowSubdomains ?? false,
1183
2417
  allowWildcardCertificates: props.allowWildcardCertificates ?? false,
1184
2418
  allowedDomains: props.allowedDomains,
2419
+ allowedDomainsTemplate: props.allowedDomainsTemplate ?? false,
1185
2420
  clientFlag: props.clientFlag ?? true,
1186
2421
  cnValidations: props.cnValidations ?? ["hostname"],
2422
+ enforceHostnames: props.enforceHostnames ?? true,
1187
2423
  extKeyUsage: props.extKeyUsage ?? [],
2424
+ generateLease: props.generateLease ?? false,
1188
2425
  keyBits: props.keyBits ?? 0,
1189
2426
  keyType: props.keyType ?? "rsa",
1190
- maxTtlSeconds: seconds2(props.maxTtl),
2427
+ keyUsage: props.keyUsage ?? DEFAULT_KEY_USAGE,
2428
+ maxTtlSeconds: seconds3(props.maxTtl),
2429
+ noStore: props.noStore ?? false,
2430
+ requireCn: props.requireCn ?? true,
1191
2431
  serverFlag: props.serverFlag ?? true,
1192
2432
  signatureBits: props.signatureBits ?? 0,
1193
- ttlSeconds: seconds2(props.ttl)
2433
+ ttlSeconds: seconds3(props.ttl)
1194
2434
  });
1195
2435
  var bool2 = (value, fallback) => typeof value === "boolean" ? value : fallback;
1196
2436
  var int2 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
1197
2437
  var list = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1198
2438
  var str = (value, fallback) => typeof value === "string" ? value : fallback;
1199
- var canonicalFromLive = (live) => canonical2({
2439
+ var canonicalFromLive6 = (live) => canonical7({
1200
2440
  allowBareDomains: bool2(live["allow_bare_domains"], false),
1201
2441
  allowGlobDomains: bool2(live["allow_glob_domains"], false),
1202
2442
  allowIpSans: bool2(live["allow_ip_sans"], false),
@@ -1204,16 +2444,25 @@ var canonicalFromLive = (live) => canonical2({
1204
2444
  allowSubdomains: bool2(live["allow_subdomains"], false),
1205
2445
  allowWildcardCertificates: bool2(live["allow_wildcard_certificates"], false),
1206
2446
  allowedDomains: list(live["allowed_domains"]),
2447
+ allowedDomainsTemplate: bool2(live["allowed_domains_template"], false),
1207
2448
  clientFlag: bool2(live["client_flag"], true),
1208
2449
  cnValidations: list(live["cn_validations"]),
2450
+ enforceHostnames: bool2(live["enforce_hostnames"], false),
1209
2451
  extKeyUsage: list(live["ext_key_usage"]),
2452
+ generateLease: bool2(live["generate_lease"], false),
1210
2453
  keyBits: int2(live["key_bits"], 0),
1211
2454
  keyType: str(live["key_type"], "rsa"),
2455
+ keyUsage: list(live["key_usage"]),
1212
2456
  maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? -1,
2457
+ noStore: bool2(live["no_store"], false),
2458
+ requireCn: bool2(live["require_cn"], false),
1213
2459
  serverFlag: bool2(live["server_flag"], true),
1214
2460
  signatureBits: int2(live["signature_bits"], 0),
1215
2461
  ttlSeconds: ttlSeconds(live["ttl"]) ?? -1
1216
2462
  });
2463
+ var digestOf = (form) => sha256(JSON.stringify(form));
2464
+
2465
+ // src/openbao/pki-role-form.ts
1217
2466
  var ttlText2 = (value) => {
1218
2467
  if (value < 0)
1219
2468
  return "";
@@ -1225,10 +2474,9 @@ var ttlText2 = (value) => {
1225
2474
  return `${String(value / 60)}m`;
1226
2475
  return `${String(value)}s`;
1227
2476
  };
1228
- var digestOf = (form) => sha256(JSON.stringify(form));
1229
- var rolePath2 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
1230
- var attributesOf5 = (props, live) => {
1231
- const form = canonicalFromLive(live);
2477
+ var rolePath4 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
2478
+ var attributesOf10 = (props, live) => {
2479
+ const form = canonicalFromLive6(live);
1232
2480
  return {
1233
2481
  ...form,
1234
2482
  name: props.name,
@@ -1238,8 +2486,8 @@ var attributesOf5 = (props, live) => {
1238
2486
  digest: digestOf(form)
1239
2487
  };
1240
2488
  };
1241
- var writeBody3 = (props) => {
1242
- const form = canonicalFromProps(props);
2489
+ var writeBody8 = (props) => {
2490
+ const form = canonicalFromProps6(props);
1243
2491
  const body = {
1244
2492
  allowed_domains: form.allowedDomains.join(","),
1245
2493
  allow_bare_domains: String(form.allowBareDomains),
@@ -1255,76 +2503,218 @@ var writeBody3 = (props) => {
1255
2503
  key_bits: String(form.keyBits),
1256
2504
  signature_bits: String(form.signatureBits),
1257
2505
  ttl: props.ttl,
1258
- max_ttl: props.maxTtl
2506
+ max_ttl: props.maxTtl,
2507
+ require_cn: String(form.requireCn),
2508
+ enforce_hostnames: String(form.enforceHostnames),
2509
+ key_usage: (props.keyUsage ?? DEFAULT_KEY_USAGE).join(","),
2510
+ allowed_domains_template: String(form.allowedDomainsTemplate),
2511
+ no_store: String(form.noStore),
2512
+ generate_lease: String(form.generateLease)
1259
2513
  };
1260
2514
  const usages = props.extKeyUsage ?? [];
1261
2515
  if (usages.length > 0)
1262
2516
  body["ext_key_usage"] = usages.join(",");
1263
2517
  return body;
1264
2518
  };
1265
- var matches4 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps(props));
2519
+ var matches9 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps6(props));
2520
+ var problems6 = (props) => {
2521
+ const found = [];
2522
+ if (props.allowedDomains.length === 0) {
2523
+ found.push("allowedDomains is empty. With allow_any_name false that role can issue no certificate at all.");
2524
+ }
2525
+ const bad = [
2526
+ ["ttl", props.ttl],
2527
+ ["maxTtl", props.maxTtl]
2528
+ ].filter(([, text10]) => parseDuration(text10 ?? "") === undefined);
2529
+ if (bad.length > 0) {
2530
+ found.push(`${bad.map(([k, v]) => `${k ?? ""}=${v ?? ""}`).join(", ")} is not a duration OpenBao ` + "parses (expect 30m, 720h, 8760h or 0).");
2531
+ }
2532
+ if (props.noStore === true && props.generateLease === true) {
2533
+ found.push("noStore and generateLease are both true; OpenBao keeps no_store and drops the lease.");
2534
+ }
2535
+ return found;
2536
+ };
1266
2537
 
1267
2538
  // src/openbao/pki-role.ts
1268
- var BaoPkiRole = Resource5("Bao.PkiRole", {
2539
+ var BaoPkiRole = Resource10("Bao.PkiRole", {
1269
2540
  defaultRemovalPolicy: "retain"
1270
2541
  });
1271
- var readRole2 = (props) => Effect10.gen(function* () {
1272
- const live = yield* baoRead(rolePath2(props));
2542
+ var readRole2 = (props) => Effect23.gen(function* () {
2543
+ const live = yield* baoRead(rolePath4(props));
1273
2544
  if (live === undefined)
1274
2545
  return;
1275
- return attributesOf5(props, live);
2546
+ return attributesOf10(props, live);
1276
2547
  });
1277
- 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 }) {
2548
+ var BaoPkiRoleProvider = () => Provider10.effect(BaoPkiRole, Effect23.succeed(BaoPkiRole.Provider.of({
2549
+ list: () => Effect23.succeed([]),
2550
+ read: Effect23.fn(function* ({ olds }) {
1281
2551
  return yield* readRole2(olds);
1282
2552
  }),
1283
- diff: Effect10.fn(function* ({ news, output }) {
1284
- if (output === undefined || !isResolved5(news))
2553
+ diff: Effect23.fn(function* ({ news, output }) {
2554
+ if (output === undefined || !isResolved10(news))
1285
2555
  return;
1286
- if (mountPath(news.mount ?? "pki") !== output.mount)
2556
+ if (mountPath(news.mount ?? "pki") !== output.mount || news.name !== output.name)
1287
2557
  return { action: "replace" };
1288
- if (news.allowedDomains.length === 0 || badDurations(news).length > 0) {
2558
+ if (problems6(news).length > 0)
1289
2559
  return { action: "update" };
1290
- }
1291
2560
  const live = yield* readRole2(news);
1292
2561
  if (live === undefined)
1293
2562
  return { action: "update" };
1294
- return matches4(live, news) ? { action: "noop" } : { action: "update" };
2563
+ return matches9(live, news) ? { action: "noop" } : { action: "update" };
1295
2564
  }),
1296
- reconcile: Effect10.fn(function* ({ news }) {
1297
- const path = rolePath2(news);
1298
- 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."));
1300
- }
1301
- const bad = badDurations(news);
2565
+ reconcile: Effect23.fn(function* ({ news }) {
2566
+ const path = rolePath4(news);
2567
+ const bad = problems6(news);
1302
2568
  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)."));
2569
+ return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: ${bad.join(" ")}`));
1304
2570
  }
1305
2571
  const live = yield* readRole2(news);
1306
- if (live === undefined || !matches4(live, news)) {
1307
- yield* baoWrite("PUT", path, writeBody3(news));
2572
+ if (live === undefined || !matches9(live, news)) {
2573
+ yield* baoWrite("PUT", path, writeBody8(news));
1308
2574
  }
1309
2575
  const after = yield* readRole2(news);
1310
2576
  if (after === undefined) {
1311
- return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
2577
+ return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
1312
2578
  }
1313
- 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.`));
2579
+ if (!matches9(after, news)) {
2580
+ return yield* Effect23.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
2581
  }
1316
2582
  return after;
1317
2583
  }),
1318
- delete: Effect10.fn(function* ({ output }) {
1319
- yield* baoDelete(rolePath2(output));
2584
+ delete: Effect23.fn(function* ({ output }) {
2585
+ yield* baoDelete(rolePath4(output));
2586
+ return;
2587
+ })
2588
+ })));
2589
+ // src/openbao/plugin.ts
2590
+ import { Resource as Resource11 } from "alchemy";
2591
+ import { isResolved as isResolved11 } from "alchemy/Diff";
2592
+ import * as Provider11 from "alchemy/Provider";
2593
+ import * as Effect25 from "effect/Effect";
2594
+
2595
+ // src/openbao/plugin-form.ts
2596
+ var resolve = (props) => ({
2597
+ args: props.args ?? [],
2598
+ command: props.command,
2599
+ name: props.name,
2600
+ sha256: props.sha256.toLowerCase(),
2601
+ type: props.type,
2602
+ version: props.version ?? ""
2603
+ });
2604
+ 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-]+)*)?$/;
2605
+ var problems7 = (form) => {
2606
+ const found = [];
2607
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(form.name) || form.name.includes("..")) {
2608
+ found.push(`name ${JSON.stringify(form.name)} is not a plain catalog name`);
2609
+ }
2610
+ if (!/^[0-9a-f]{64}$/.test(form.sha256))
2611
+ found.push("sha256 is not 64 hex characters");
2612
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(form.command) || form.command.includes("..")) {
2613
+ found.push(`command ${JSON.stringify(form.command)} is not a bare file name`);
2614
+ }
2615
+ if (form.version !== "" && !CANONICAL_VERSION.test(form.version)) {
2616
+ found.push(`version ${JSON.stringify(form.version)} is not canonical semver like v1.2.0`);
2617
+ }
2618
+ if (/\+builtin/.test(form.version))
2619
+ found.push("version metadata `builtin` is reserved");
2620
+ return found;
2621
+ };
2622
+ var versionQuery = (version) => version === "" ? "" : `?version=${encodeURIComponent(version)}`;
2623
+ var catalogPath = (type, name) => `sys/plugins/catalog/${type}/${name}`;
2624
+ var versionedPath = (type, name, version) => `${catalogPath(type, name)}${versionQuery(version)}`;
2625
+ var registerBody = (form) => ({
2626
+ command: form.command,
2627
+ sha256: form.sha256,
2628
+ ...form.args.length > 0 ? { args: form.args } : {},
2629
+ ...form.version === "" ? {} : { version: form.version }
2630
+ });
2631
+ var text10 = (value) => typeof value === "string" ? value : "";
2632
+ var list2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
2633
+ var attributesOf11 = (form, live) => {
2634
+ const attrs = {
2635
+ name: form.name,
2636
+ type: form.type,
2637
+ version: text10(live["version"]),
2638
+ sha256: text10(live["sha256"]).toLowerCase(),
2639
+ command: text10(live["command"]),
2640
+ args: list2(live["args"]),
2641
+ builtin: live["builtin"] === true
2642
+ };
2643
+ return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
2644
+ };
2645
+ var sameArgs = (want, have) => want.length === have.length && want.every((entry, index) => entry === have[index]);
2646
+ var matches10 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
2647
+
2648
+ // src/openbao/plugin-reconcile.ts
2649
+ import * as Effect24 from "effect/Effect";
2650
+
2651
+ // src/openbao/plugin-wire.ts
2652
+ var readPluginData = (form) => baoRead(versionedPath(form.type, form.name, form.version));
2653
+ var registerPlugin = (form) => baoWrite("PUT", catalogPath(form.type, form.name), registerBody(form));
2654
+ var deregisterPlugin = (type, name, version) => baoDelete(versionedPath(type, name, version));
2655
+
2656
+ // src/openbao/plugin-reconcile.ts
2657
+ var readEntry = (form) => Effect24.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf11(form, live), live });
2658
+ var refuse5 = (form, message) => Effect24.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
2659
+ 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.";
2660
+ var reconcilePlugin = (props) => Effect24.gen(function* () {
2661
+ const form = resolve(props);
2662
+ const bad = problems7(form);
2663
+ if (bad.length > 0)
2664
+ return yield* refuse5(form, bad.join("; "));
2665
+ const found = yield* readEntry(form);
2666
+ if (found !== undefined && !matches10(found.attributes, form)) {
2667
+ if (found.live["declarative"] === true || found.live["oci"] === true) {
2668
+ return yield* refuse5(form, "the live entry is declarative/OCI; manage it in server config.");
2669
+ }
2670
+ if (found.attributes.builtin) {
2671
+ return yield* refuse5(form, "this name is a builtin; an unversioned write would shadow it.");
2672
+ }
2673
+ }
2674
+ if (found === undefined || !matches10(found.attributes, form))
2675
+ yield* registerPlugin(form);
2676
+ const after = yield* readEntry(form);
2677
+ if (after === undefined || !matches10(after.attributes, form)) {
2678
+ return yield* refuse5(form, SELF_REPORTED);
2679
+ }
2680
+ return after.attributes;
2681
+ });
2682
+
2683
+ // src/openbao/plugin.ts
2684
+ var BaoPlugin = Resource11("Bao.Plugin", {
2685
+ defaultRemovalPolicy: "retain"
2686
+ });
2687
+ var BaoPluginProvider = () => Provider11.effect(BaoPlugin, Effect25.succeed(BaoPlugin.Provider.of({
2688
+ list: () => Effect25.succeed([]),
2689
+ read: Effect25.fn(function* ({ olds }) {
2690
+ const found = yield* readEntry(resolve(olds));
2691
+ return found?.attributes;
2692
+ }),
2693
+ diff: Effect25.fn(function* ({ news, output }) {
2694
+ if (output === undefined || !isResolved11(news))
2695
+ return;
2696
+ const form = resolve(news);
2697
+ if (output.name !== form.name || output.type !== form.type || output.version !== form.version) {
2698
+ return { action: "replace" };
2699
+ }
2700
+ const found = yield* readEntry(form);
2701
+ if (found === undefined)
2702
+ return { action: "update" };
2703
+ return matches10(found.attributes, form) ? { action: "noop" } : { action: "update" };
2704
+ }),
2705
+ reconcile: Effect25.fn(function* ({ news }) {
2706
+ return yield* reconcilePlugin(news);
2707
+ }),
2708
+ delete: Effect25.fn(function* ({ output }) {
2709
+ yield* deregisterPlugin(output.type, output.name, output.version);
1320
2710
  return;
1321
2711
  })
1322
2712
  })));
1323
2713
  // src/openbao/policy.ts
1324
- import { Resource as Resource6 } from "alchemy";
1325
- import { isResolved as isResolved6 } from "alchemy/Diff";
1326
- import * as Provider6 from "alchemy/Provider";
1327
- import * as Effect12 from "effect/Effect";
2714
+ import { Resource as Resource12 } from "alchemy";
2715
+ import { isResolved as isResolved12 } from "alchemy/Diff";
2716
+ import * as Provider12 from "alchemy/Provider";
2717
+ import * as Effect27 from "effect/Effect";
1328
2718
  import * as FileSystem from "effect/FileSystem";
1329
2719
  import * as Path from "effect/Path";
1330
2720
 
@@ -1332,32 +2722,32 @@ import * as Path from "effect/Path";
1332
2722
  var isEmptyAssembly = (parts, joined) => parts === 0 || !/^\s*path\s+"/m.test(joined);
1333
2723
 
1334
2724
  // src/openbao/policy-wire.ts
1335
- import * as Effect11 from "effect/Effect";
2725
+ import * as Effect26 from "effect/Effect";
1336
2726
  var policyPath = (name) => `sys/policies/acl/${name}`;
1337
- var readPolicy = (name) => Effect11.flatMap(baoRead(policyPath(name)), (data) => {
2727
+ var readPolicy = (name) => Effect26.flatMap(baoRead(policyPath(name)), (data) => {
1338
2728
  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"]));
2729
+ return Effect26.succeed("");
2730
+ const text11 = data["policy"];
2731
+ return typeof text11 === "string" ? Effect26.succeed(text11) : Effect26.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
1342
2732
  });
1343
2733
  var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
1344
2734
  var deletePolicy = (name) => baoDelete(policyPath(name));
1345
2735
 
1346
2736
  // src/openbao/policy.ts
1347
- var BaoPolicy = Resource6("Bao.Policy", {
2737
+ var BaoPolicy = Resource12("Bao.Policy", {
1348
2738
  defaultRemovalPolicy: "retain"
1349
2739
  });
1350
2740
  var grantsOf = (hcl) => (hcl.match(/^path /gm) ?? []).length;
1351
- var attributesOf6 = (name, hcl, parts) => ({
2741
+ var attributesOf12 = (name, hcl, parts) => ({
1352
2742
  name,
1353
2743
  digest: sha256(hcl),
1354
2744
  grants: grantsOf(hcl),
1355
2745
  parts
1356
2746
  });
1357
- var BaoPolicyProvider = () => Provider6.effect(BaoPolicy, Effect12.gen(function* () {
2747
+ var BaoPolicyProvider = () => Provider12.effect(BaoPolicy, Effect27.gen(function* () {
1358
2748
  const fs = yield* FileSystem.FileSystem;
1359
2749
  const path = yield* Path.Path;
1360
- const assemble = (dir) => Effect12.gen(function* () {
2750
+ const assemble = (dir) => Effect27.gen(function* () {
1361
2751
  const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
1362
2752
  let joined = "";
1363
2753
  for (const f of entries2)
@@ -1366,67 +2756,67 @@ var BaoPolicyProvider = () => Provider6.effect(BaoPolicy, Effect12.gen(function*
1366
2756
  return { joined, parts: entries2.length };
1367
2757
  });
1368
2758
  return BaoPolicy.Provider.of({
1369
- list: () => Effect12.succeed([]),
1370
- read: Effect12.fn(function* ({ olds }) {
2759
+ list: () => Effect27.succeed([]),
2760
+ read: Effect27.fn(function* ({ olds }) {
1371
2761
  const live = yield* readPolicy(olds.name);
1372
- return live.length === 0 ? undefined : attributesOf6(olds.name, live, 0);
2762
+ return live.length === 0 ? undefined : attributesOf12(olds.name, live, 0);
1373
2763
  }),
1374
- diff: Effect12.fn(function* ({ news, output }) {
1375
- if (output === undefined || !isResolved6(news))
2764
+ diff: Effect27.fn(function* ({ news, output }) {
2765
+ if (output === undefined || !isResolved12(news))
1376
2766
  return;
1377
2767
  const { joined } = yield* assemble(news.fragments);
1378
2768
  const live = yield* readPolicy(news.name);
1379
2769
  return live.length > 0 && sha256(live) === sha256(joined) ? { action: "noop" } : { action: "update" };
1380
2770
  }),
1381
- reconcile: Effect12.fn(function* ({ news }) {
2771
+ reconcile: Effect27.fn(function* ({ news }) {
1382
2772
  const name = news.name;
1383
2773
  const { joined, parts } = yield* assemble(news.fragments);
1384
2774
  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.`));
2775
+ return yield* Effect27.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
2776
  const current = yield* readPolicy(name);
1387
2777
  if (current.length > 0 && sha256(current) === sha256(joined)) {
1388
- return attributesOf6(name, joined, parts);
2778
+ return attributesOf12(name, joined, parts);
1389
2779
  }
1390
2780
  yield* writePolicy(name, joined);
1391
- return attributesOf6(name, joined, parts);
2781
+ return attributesOf12(name, joined, parts);
1392
2782
  }),
1393
- delete: Effect12.fn(function* ({ output }) {
2783
+ delete: Effect27.fn(function* ({ output }) {
1394
2784
  yield* deletePolicy(output.name);
1395
2785
  return;
1396
2786
  })
1397
2787
  });
1398
2788
  }));
1399
2789
  // 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";
2790
+ import { Resource as Resource13 } from "alchemy";
2791
+ import { isResolved as isResolved13 } from "alchemy/Diff";
2792
+ import * as Provider13 from "alchemy/Provider";
2793
+ import * as Effect28 from "effect/Effect";
1404
2794
 
1405
2795
  // src/openbao/proxmox-role-form.ts
1406
- var text4 = (value) => typeof value === "string" ? value : "";
1407
- var ttlText3 = (seconds3, fallback = "0") => {
1408
- if (seconds3 === undefined)
2796
+ var text11 = (value) => typeof value === "string" ? value : "";
2797
+ var ttlText3 = (seconds4, fallback = "0") => {
2798
+ if (seconds4 === undefined)
1409
2799
  return fallback;
1410
- if (seconds3 % 86400 === 0)
1411
- return `${String(seconds3 / 86400)}d`;
1412
- if (seconds3 % 3600 === 0)
1413
- return `${String(seconds3 / 3600)}h`;
1414
- if (seconds3 % 60 === 0)
1415
- return `${String(seconds3 / 60)}m`;
1416
- return `${String(seconds3)}s`;
1417
- };
1418
- var rolePath3 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
1419
- var attributesOf7 = (props, live) => {
2800
+ if (seconds4 % 86400 === 0)
2801
+ return `${String(seconds4 / 86400)}d`;
2802
+ if (seconds4 % 3600 === 0)
2803
+ return `${String(seconds4 / 3600)}h`;
2804
+ if (seconds4 % 60 === 0)
2805
+ return `${String(seconds4 / 60)}m`;
2806
+ return `${String(seconds4)}s`;
2807
+ };
2808
+ var rolePath5 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
2809
+ var attributesOf13 = (props, live) => {
1420
2810
  const attrs = {
1421
2811
  mount: mountPath(props.mount),
1422
- name: text4(live["name"]) || props.name,
1423
- mintUser: text4(live["mint_user"]),
2812
+ name: text11(live["name"]) || props.name,
2813
+ mintUser: text11(live["mint_user"]),
1424
2814
  ttl: ttlText3(ttlSeconds(live["ttl"])),
1425
2815
  maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
1426
2816
  };
1427
2817
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1428
2818
  };
1429
- var writeBody4 = (props) => {
2819
+ var writeBody9 = (props) => {
1430
2820
  const ttl = parseDuration(props.ttl);
1431
2821
  const maxTtl = parseDuration(props.maxTtl);
1432
2822
  const bad = [];
@@ -1446,69 +2836,68 @@ var sameTtl5 = (want, have) => {
1446
2836
  const haveSeconds = ttlSeconds(have);
1447
2837
  return wantSeconds !== undefined && wantSeconds === haveSeconds;
1448
2838
  };
1449
- var matches5 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
2839
+ var matches11 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
1450
2840
 
1451
2841
  // src/openbao/proxmox-role.ts
1452
- var BaoProxmoxRole = Resource7("Bao.ProxmoxRole", {
2842
+ var BaoProxmoxRole = Resource13("Bao.ProxmoxRole", {
1453
2843
  defaultRemovalPolicy: "retain"
1454
2844
  });
1455
- var readRole3 = (props) => Effect13.gen(function* () {
1456
- const live = yield* baoRead(rolePath3(props.mount, props.name));
2845
+ var readRole3 = (props) => Effect28.gen(function* () {
2846
+ const live = yield* baoRead(rolePath5(props.mount, props.name));
1457
2847
  if (live === undefined)
1458
2848
  return;
1459
- return attributesOf7(props, live);
2849
+ return attributesOf13(props, live);
1460
2850
  });
1461
- var BaoProxmoxRoleProvider = () => Provider7.effect(BaoProxmoxRole, Effect13.succeed(BaoProxmoxRole.Provider.of({
1462
- list: () => Effect13.succeed([]),
1463
- read: Effect13.fn(function* ({ olds }) {
2851
+ var BaoProxmoxRoleProvider = () => Provider13.effect(BaoProxmoxRole, Effect28.succeed(BaoProxmoxRole.Provider.of({
2852
+ list: () => Effect28.succeed([]),
2853
+ read: Effect28.fn(function* ({ olds }) {
1464
2854
  return yield* readRole3(olds);
1465
2855
  }),
1466
- diff: Effect13.fn(function* ({ news, output }) {
1467
- if (output === undefined || !isResolved7(news))
2856
+ diff: Effect28.fn(function* ({ news, output }) {
2857
+ if (output === undefined || !isResolved13(news))
1468
2858
  return;
1469
2859
  const live = yield* readRole3(news);
1470
2860
  if (live === undefined)
1471
2861
  return { action: "update" };
1472
- if (matches5(live, news))
2862
+ if (matches11(live, news))
1473
2863
  return { action: "noop" };
1474
2864
  return { action: "update" };
1475
2865
  }),
1476
- reconcile: Effect13.fn(function* ({ news }) {
2866
+ reconcile: Effect28.fn(function* ({ news }) {
1477
2867
  const live = yield* readRole3(news);
1478
2868
  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}'.`));
2869
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(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
2870
  }
1481
- if (live === undefined || !matches5(live, news)) {
1482
- const write = writeBody4(news);
2871
+ if (live === undefined || !matches11(live, news)) {
2872
+ const write = writeBody9(news);
1483
2873
  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.`));
2874
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
1485
2875
  }
1486
- yield* baoWrite("PUT", rolePath3(news.mount, news.name), write.body);
2876
+ yield* baoWrite("PUT", rolePath5(news.mount, news.name), write.body);
1487
2877
  }
1488
2878
  const after = yield* readRole3(news);
1489
2879
  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."));
2880
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
1491
2881
  }
1492
2882
  return after;
1493
2883
  }),
1494
- delete: Effect13.fn(function* ({ output }) {
1495
- yield* baoDelete(rolePath3(output.mount, output.name));
2884
+ delete: Effect28.fn(function* ({ output }) {
2885
+ yield* baoDelete(rolePath5(output.mount, output.name));
1496
2886
  return;
1497
2887
  })
1498
2888
  })));
1499
2889
  // 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";
2890
+ import { Resource as Resource14 } from "alchemy";
2891
+ import { isResolved as isResolved14 } from "alchemy/Diff";
2892
+ import * as Provider14 from "alchemy/Provider";
2893
+ import * as Effect29 from "effect/Effect";
1504
2894
 
1505
2895
  // 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 : "";
2896
+ var readPath4 = (mount, name) => `${sshMountOf(mount)}/roles/${name}`;
2897
+ var text12 = (value) => typeof value === "string" ? value : "";
1509
2898
  var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
1510
2899
  var csvList = (value) => {
1511
- const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text5(value).split(",");
2900
+ const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text12(value).split(",");
1512
2901
  return [
1513
2902
  ...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
1514
2903
  ].sort();
@@ -1519,12 +2908,12 @@ var extMap = (value) => {
1519
2908
  return out;
1520
2909
  const source = value;
1521
2910
  for (const key of Object.keys(source).sort())
1522
- out[key] = text5(source[key]);
2911
+ out[key] = text12(source[key]);
1523
2912
  return out;
1524
2913
  };
1525
- var resolve = (props) => ({
2914
+ var resolve2 = (props) => ({
1526
2915
  name: props.name,
1527
- mount: mountOf(props.mount),
2916
+ mount: sshMountOf(props.mount),
1528
2917
  allowedUsers: csvList(props.allowedUsers),
1529
2918
  defaultUser: props.defaultUser ?? "",
1530
2919
  allowUserCertificates: props.allowUserCertificates ?? false,
@@ -1540,12 +2929,12 @@ var resolve = (props) => ({
1540
2929
  ttl: props.ttl,
1541
2930
  maxTtl: props.maxTtl
1542
2931
  });
1543
- var attributesOf8 = (form, live) => {
2932
+ var attributesOf14 = (form, live) => {
1544
2933
  const attrs = {
1545
2934
  name: form.name,
1546
2935
  mount: form.mount,
1547
2936
  allowedUsers: csvList(live["allowed_users"]),
1548
- defaultUser: text5(live["default_user"]),
2937
+ defaultUser: text12(live["default_user"]),
1549
2938
  allowUserCertificates: bool3(live["allow_user_certificates"], false),
1550
2939
  allowHostCertificates: bool3(live["allow_host_certificates"], false),
1551
2940
  allowedDomains: csvList(live["allowed_domains"]),
@@ -1556,14 +2945,14 @@ var attributesOf8 = (form, live) => {
1556
2945
  allowedCriticalOptions: csvList(live["allowed_critical_options"]),
1557
2946
  defaultCriticalOptions: extMap(live["default_critical_options"]),
1558
2947
  allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
1559
- keyType: text5(live["key_type"]) || "ca",
2948
+ keyType: text12(live["key_type"]) || "ca",
1560
2949
  ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
1561
2950
  maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
1562
2951
  };
1563
2952
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1564
2953
  };
1565
- var mapValue = (map4) => JSON.stringify(extMap(map4));
1566
- var writeBody5 = (form) => ({
2954
+ var mapValue = (map10) => extMap(map10);
2955
+ var writeBody10 = (form) => ({
1567
2956
  key_type: "ca",
1568
2957
  allowed_users: form.allowedUsers.join(","),
1569
2958
  default_user: form.defaultUser,
@@ -1586,18 +2975,18 @@ var sameMap = (want, have) => {
1586
2975
  return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);
1587
2976
  };
1588
2977
  var sameTtl6 = (want, have) => parseDuration(want) === have;
1589
- 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);
2978
+ var badDurations = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
2979
+ var matches12 = (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
2980
 
1592
2981
  // src/openbao/ssh-role.ts
1593
- var BaoSshRole = Resource8("Bao.SshRole", {
2982
+ var BaoSshRole = Resource14("Bao.SshRole", {
1594
2983
  defaultRemovalPolicy: "retain"
1595
2984
  });
1596
- var readRole4 = (form) => Effect14.gen(function* () {
2985
+ var readRole4 = (form) => Effect29.gen(function* () {
1597
2986
  const live = yield* baoRead(readPath4(form.mount, form.name));
1598
2987
  if (live === undefined)
1599
2988
  return;
1600
- return { attributes: attributesOf8(form, live), live };
2989
+ return { attributes: attributesOf14(form, live), live };
1601
2990
  });
1602
2991
  var UNMANAGED_DEFAULTS = {
1603
2992
  algorithm_signer: "default",
@@ -1612,59 +3001,59 @@ var UNMANAGED_DEFAULTS = {
1612
3001
  not_before_duration: 30
1613
3002
  };
1614
3003
  var isDefault = (live, field) => live[field] === undefined || live[field] === "" || live[field] === UNMANAGED_DEFAULTS[field];
1615
- var wouldErase = (live) => {
3004
+ var wouldErase2 = (live) => {
1616
3005
  const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));
1617
3006
  if (Object.keys(extMap(live["allowed_user_key_lengths"])).length > 0) {
1618
3007
  lost.push("allowed_user_key_lengths");
1619
3008
  }
1620
3009
  return lost.sort();
1621
3010
  };
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));
3011
+ var refuse6 = (message) => Effect29.die(new Error(message));
3012
+ var BaoSshRoleProvider = () => Provider14.effect(BaoSshRole, Effect29.succeed(BaoSshRole.Provider.of({
3013
+ list: () => Effect29.succeed([]),
3014
+ read: Effect29.fn(function* ({ olds }) {
3015
+ const found = yield* readRole4(resolve2(olds));
1627
3016
  return found?.attributes;
1628
3017
  }),
1629
- diff: Effect14.fn(function* ({ news, output }) {
1630
- if (output === undefined || !isResolved8(news))
3018
+ diff: Effect29.fn(function* ({ news, output }) {
3019
+ if (output === undefined || !isResolved14(news))
1631
3020
  return;
1632
- const form = resolve(news);
3021
+ const form = resolve2(news);
1633
3022
  if (output.mount !== form.mount || output.name !== form.name) {
1634
3023
  return { action: "replace" };
1635
3024
  }
1636
3025
  const found = yield* readRole4(form);
1637
3026
  if (found === undefined)
1638
3027
  return { action: "update" };
1639
- return matches6(found.attributes, form) ? { action: "noop" } : { action: "update" };
3028
+ return matches12(found.attributes, form) ? { action: "noop" } : { action: "update" };
1640
3029
  }),
1641
- reconcile: Effect14.fn(function* ({ news }) {
1642
- const form = resolve(news);
3030
+ reconcile: Effect29.fn(function* ({ news }) {
3031
+ const form = resolve2(news);
1643
3032
  const path = readPath4(form.mount, form.name);
1644
- const bad = badDurations2(form);
3033
+ const bad = badDurations(form);
1645
3034
  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.");
3035
+ return yield* refuse6(`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
3036
  }
1648
3037
  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.");
3038
+ return yield* refuse6(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
1650
3039
  }
1651
3040
  const found = yield* readRole4(form);
1652
- if (found === undefined || !matches6(found.attributes, form)) {
3041
+ if (found === undefined || !matches12(found.attributes, form)) {
1653
3042
  if (found !== undefined) {
1654
- const lost = wouldErase(found.live);
3043
+ const lost = wouldErase2(found.live);
1655
3044
  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.");
3045
+ return yield* refuse6(`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
3046
  }
1658
3047
  }
1659
- yield* baoWrite("PUT", path, writeBody5(form));
3048
+ yield* baoWrite("PUT", path, writeBody10(form));
1660
3049
  }
1661
3050
  const after = yield* readRole4(form);
1662
3051
  if (after === undefined) {
1663
- return yield* refuse(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
3052
+ return yield* refuse6(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
1664
3053
  }
1665
3054
  return after.attributes;
1666
3055
  }),
1667
- delete: Effect14.fn(function* ({ output }) {
3056
+ delete: Effect29.fn(function* ({ output }) {
1668
3057
  yield* baoDelete(readPath4(output.mount, output.name));
1669
3058
  return;
1670
3059
  })
@@ -1676,10 +3065,24 @@ export {
1676
3065
  BaoAuthRoleProvider,
1677
3066
  BaoCloudflareRole,
1678
3067
  BaoCloudflareRoleProvider,
3068
+ BaoIdentityError,
3069
+ BaoJwtAuthConfig,
3070
+ BaoJwtAuthConfigProvider,
3071
+ BaoJwtRole,
3072
+ BaoJwtRoleProvider,
3073
+ BaoKubernetesRole,
3074
+ BaoKubernetesRoleProvider,
3075
+ BaoLoginError,
3076
+ BaoMfaLoginEnforcement,
3077
+ BaoMfaLoginEnforcementProvider,
3078
+ BaoMfaTotpMethod,
3079
+ BaoMfaTotpMethodProvider,
1679
3080
  BaoMount,
1680
3081
  BaoMountProvider,
1681
3082
  BaoPkiRole,
1682
3083
  BaoPkiRoleProvider,
3084
+ BaoPlugin,
3085
+ BaoPluginProvider,
1683
3086
  BaoPolicy,
1684
3087
  BaoPolicyProvider,
1685
3088
  BaoProxmoxRole,
@@ -1687,13 +3090,22 @@ export {
1687
3090
  BaoSshRole,
1688
3091
  BaoSshRoleProvider,
1689
3092
  CloudflarePermissionGroups,
3093
+ HOST_ROLE_SEPARATOR,
3094
+ appRoleLogin,
3095
+ appRoleLoginEffect,
3096
+ assertBaoIdentity,
3097
+ assertBaoIdentityEffect,
1690
3098
  expandAccount,
1691
3099
  expandAll,
3100
+ hostAppRoles,
3101
+ hostRoleName,
1692
3102
  mountName,
1693
3103
  parseRolesConfig,
1694
3104
  permissionGroupsFromEngine,
1695
- permissionGroupsFromLiveRoles
3105
+ permissionGroupsFromLiveRoles,
3106
+ revokeSelf,
3107
+ revokeSelfEffect
1696
3108
  };
1697
3109
 
1698
- //# debugId=8FBBE010DA02328C64756E2164756E21
3110
+ //# debugId=7E1DFE85CC398C5464756E2164756E21
1699
3111
  //# sourceMappingURL=index.js.map