@homeflare/alchemy 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -55
- package/dist/caddy/admin-calls.d.ts +48 -0
- package/dist/caddy/admin-calls.d.ts.map +1 -0
- package/dist/caddy/admin-guard.d.ts +40 -0
- package/dist/caddy/admin-guard.d.ts.map +1 -0
- package/dist/caddy/admin.d.ts +73 -0
- package/dist/caddy/admin.d.ts.map +1 -0
- package/dist/caddy/caddyfile-tripwire.d.ts +5 -0
- package/dist/caddy/caddyfile-tripwire.d.ts.map +1 -0
- package/dist/caddy/config-form.d.ts +27 -0
- package/dist/caddy/config-form.d.ts.map +1 -0
- package/dist/caddy/config-lifecycle.d.ts +45 -0
- package/dist/caddy/config-lifecycle.d.ts.map +1 -0
- package/dist/caddy/config.d.ts +30 -0
- package/dist/caddy/config.d.ts.map +1 -0
- package/dist/caddy/digest.d.ts +7 -0
- package/dist/caddy/digest.d.ts.map +1 -0
- package/dist/caddy/fake-caddy.d.ts +43 -0
- package/dist/caddy/fake-caddy.d.ts.map +1 -0
- package/dist/caddy/index.d.ts +21 -0
- package/dist/caddy/index.d.ts.map +1 -0
- package/dist/caddy/index.js +912 -0
- package/dist/caddy/index.js.map +28 -0
- package/dist/caddy/local-admin.d.ts +45 -0
- package/dist/caddy/local-admin.d.ts.map +1 -0
- package/dist/caddy/providers.d.ts +14 -0
- package/dist/caddy/providers.d.ts.map +1 -0
- package/dist/caddy/with-file.d.ts +47 -0
- package/dist/caddy/with-file.d.ts.map +1 -0
- package/dist/cloudflare/fake-mesh.d.ts +58 -0
- package/dist/cloudflare/fake-mesh.d.ts.map +1 -0
- package/dist/cloudflare/index.d.ts +9 -3
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +218 -7
- package/dist/cloudflare/index.js.map +9 -4
- package/dist/cloudflare/mesh-node-api.d.ts +75 -0
- package/dist/cloudflare/mesh-node-api.d.ts.map +1 -0
- package/dist/cloudflare/mesh-node-form.d.ts +82 -0
- package/dist/cloudflare/mesh-node-form.d.ts.map +1 -0
- package/dist/cloudflare/mesh-node-harness.d.ts +46 -0
- package/dist/cloudflare/mesh-node-harness.d.ts.map +1 -0
- package/dist/cloudflare/mesh-node-lifecycle.d.ts +22 -0
- package/dist/cloudflare/mesh-node-lifecycle.d.ts.map +1 -0
- package/dist/cloudflare/mesh-node-token.d.ts +23 -0
- package/dist/cloudflare/mesh-node-token.d.ts.map +1 -0
- package/dist/cloudflare/mesh-node.d.ts +48 -0
- package/dist/cloudflare/mesh-node.d.ts.map +1 -0
- package/dist/cloudflare/providers.d.ts +1 -12
- package/dist/cloudflare/providers.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts +36 -0
- package/dist/launchd/fake-runner.d.ts.map +1 -0
- package/dist/launchd/host-effect.d.ts +19 -0
- package/dist/launchd/host-effect.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +24 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -0
- package/dist/launchd/host-file-lifecycle.d.ts +29 -0
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
- package/dist/launchd/host-file.d.ts +25 -0
- package/dist/launchd/host-file.d.ts.map +1 -0
- package/dist/launchd/host-lookup.d.ts +27 -0
- package/dist/launchd/host-lookup.d.ts.map +1 -0
- package/dist/launchd/index.d.ts +21 -0
- package/dist/launchd/index.d.ts.map +1 -0
- package/dist/launchd/index.js +969 -0
- package/dist/launchd/index.js.map +25 -0
- package/dist/launchd/job-form.d.ts +90 -0
- package/dist/launchd/job-form.d.ts.map +1 -0
- package/dist/launchd/job-lifecycle.d.ts +34 -0
- package/dist/launchd/job-lifecycle.d.ts.map +1 -0
- package/dist/launchd/job-preflight.d.ts +50 -0
- package/dist/launchd/job-preflight.d.ts.map +1 -0
- package/dist/launchd/job-validate.d.ts +25 -0
- package/dist/launchd/job-validate.d.ts.map +1 -0
- package/dist/launchd/job.d.ts +34 -0
- package/dist/launchd/job.d.ts.map +1 -0
- package/dist/launchd/launchctl.d.ts +62 -0
- package/dist/launchd/launchctl.d.ts.map +1 -0
- package/dist/launchd/local-runner.d.ts +7 -0
- package/dist/launchd/local-runner.d.ts.map +1 -0
- package/dist/launchd/plist.d.ts +41 -0
- package/dist/launchd/plist.d.ts.map +1 -0
- package/dist/launchd/providers.d.ts +15 -0
- package/dist/launchd/providers.d.ts.map +1 -0
- package/dist/launchd/runner.d.ts +91 -0
- package/dist/launchd/runner.d.ts.map +1 -0
- package/dist/launchd/secret-tripwire.d.ts +24 -0
- package/dist/launchd/secret-tripwire.d.ts.map +1 -0
- package/dist/openbao/auth-method-form.d.ts +7 -0
- package/dist/openbao/auth-method-form.d.ts.map +1 -1
- package/dist/openbao/auth-method-reconcile.d.ts +15 -0
- package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
- package/dist/openbao/auth-method.d.ts +6 -0
- package/dist/openbao/auth-method.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role.d.ts.map +1 -1
- package/dist/openbao/auth-token-form.d.ts +48 -0
- package/dist/openbao/auth-token-form.d.ts.map +1 -0
- package/dist/openbao/bao-identity.d.ts +81 -0
- package/dist/openbao/bao-identity.d.ts.map +1 -0
- package/dist/openbao/bao-namespace.d.ts +20 -0
- package/dist/openbao/bao-namespace.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role.d.ts.map +1 -1
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
- package/dist/openbao/fake-engines.d.ts +30 -0
- package/dist/openbao/fake-engines.d.ts.map +1 -0
- package/dist/openbao/fake-stack.d.ts +29 -0
- package/dist/openbao/fake-stack.d.ts.map +1 -0
- package/dist/openbao/host-approles.d.ts +62 -0
- package/dist/openbao/host-approles.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +19 -0
- package/dist/openbao/index.d.ts.map +1 -1
- package/dist/openbao/index.js +1518 -310
- package/dist/openbao/index.js.map +40 -17
- package/dist/openbao/jwt-config-form.d.ts +51 -0
- package/dist/openbao/jwt-config-form.d.ts.map +1 -0
- package/dist/openbao/jwt-config.d.ts +25 -0
- package/dist/openbao/jwt-config.d.ts.map +1 -0
- package/dist/openbao/jwt-role-form.d.ts +91 -0
- package/dist/openbao/jwt-role-form.d.ts.map +1 -0
- package/dist/openbao/jwt-role.d.ts +31 -0
- package/dist/openbao/jwt-role.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role-form.d.ts +68 -0
- package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role.d.ts +25 -0
- package/dist/openbao/kubernetes-role.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
- package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement.d.ts +41 -0
- package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
- package/dist/openbao/mfa-totp-form.d.ts +52 -0
- package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
- package/dist/openbao/mfa-totp.d.ts +33 -0
- package/dist/openbao/mfa-totp.d.ts.map +1 -0
- package/dist/openbao/mfa-wire.d.ts +63 -0
- package/dist/openbao/mfa-wire.d.ts.map +1 -0
- package/dist/openbao/mount-form.d.ts +7 -0
- package/dist/openbao/mount-form.d.ts.map +1 -1
- package/dist/openbao/mount-move.d.ts +49 -0
- package/dist/openbao/mount-move.d.ts.map +1 -0
- package/dist/openbao/mount-path.d.ts +8 -4
- package/dist/openbao/mount-path.d.ts.map +1 -1
- package/dist/openbao/mount-reconcile.d.ts +16 -0
- package/dist/openbao/mount-reconcile.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +9 -0
- package/dist/openbao/mount.d.ts.map +1 -1
- package/dist/openbao/pki-role-canonical.d.ts +46 -0
- package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +38 -30
- package/dist/openbao/pki-role-form.d.ts.map +1 -1
- package/dist/openbao/pki-role.d.ts.map +1 -1
- package/dist/openbao/policy.d.ts +2 -0
- package/dist/openbao/policy.d.ts.map +1 -1
- package/dist/openbao/proxmox-role-form.d.ts +18 -8
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
- package/dist/openbao/proxmox-role-wire.d.ts +18 -0
- package/dist/openbao/proxmox-role-wire.d.ts.map +1 -0
- package/dist/openbao/proxmox-role.d.ts.map +1 -1
- package/dist/openbao/remount-wire.d.ts +44 -0
- package/dist/openbao/remount-wire.d.ts.map +1 -0
- package/dist/openbao/rename.d.ts +106 -0
- package/dist/openbao/rename.d.ts.map +1 -0
- package/dist/openbao/role-reconcile.d.ts +40 -0
- package/dist/openbao/role-reconcile.d.ts.map +1 -0
- package/docs/caddy.md +180 -0
- package/docs/launchd.md +167 -0
- package/docs/mesh-node.md +188 -0
- package/package.json +12 -2
- package/src/caddy/admin-calls.ts +128 -0
- package/src/caddy/admin-guard.ts +170 -0
- package/src/caddy/admin.ts +74 -0
- package/src/caddy/caddyfile-tripwire.ts +133 -0
- package/src/caddy/config-form.ts +64 -0
- package/src/caddy/config-lifecycle.test.ts +218 -0
- package/src/caddy/config-lifecycle.ts +148 -0
- package/src/caddy/config.ts +116 -0
- package/src/caddy/digest.ts +44 -0
- package/src/caddy/fake-caddy.ts +160 -0
- package/src/caddy/guards.test.ts +156 -0
- package/src/caddy/index.ts +25 -0
- package/src/caddy/local-admin.test.ts +160 -0
- package/src/caddy/local-admin.ts +189 -0
- package/src/caddy/providers.test.ts +244 -0
- package/src/caddy/providers.ts +17 -0
- package/src/caddy/with-file.ts +62 -0
- package/src/cloudflare/fake-mesh.ts +181 -0
- package/src/cloudflare/index.ts +9 -3
- package/src/cloudflare/mesh-node-api.ts +127 -0
- package/src/cloudflare/mesh-node-edges.test.ts +103 -0
- package/src/cloudflare/mesh-node-form.test.ts +80 -0
- package/src/cloudflare/mesh-node-form.ts +115 -0
- package/src/cloudflare/mesh-node-harness.ts +124 -0
- package/src/cloudflare/mesh-node-lifecycle.ts +187 -0
- package/src/cloudflare/mesh-node-policy.test.ts +151 -0
- package/src/cloudflare/mesh-node-token.test.ts +117 -0
- package/src/cloudflare/mesh-node-token.ts +125 -0
- package/src/cloudflare/mesh-node.test.ts +172 -0
- package/src/cloudflare/mesh-node.ts +120 -0
- package/src/cloudflare/providers.ts +20 -2
- package/src/index.ts +2 -0
- package/src/launchd/fake-runner.ts +176 -0
- package/src/launchd/host-effect.ts +28 -0
- package/src/launchd/host-file-form.ts +71 -0
- package/src/launchd/host-file-lifecycle.test.ts +198 -0
- package/src/launchd/host-file-lifecycle.ts +164 -0
- package/src/launchd/host-file.ts +64 -0
- package/src/launchd/host-lookup.test.ts +92 -0
- package/src/launchd/host-lookup.ts +94 -0
- package/src/launchd/index.ts +26 -0
- package/src/launchd/job-form.test.ts +171 -0
- package/src/launchd/job-form.ts +180 -0
- package/src/launchd/job-identity.test.ts +147 -0
- package/src/launchd/job-lifecycle.test.ts +220 -0
- package/src/launchd/job-lifecycle.ts +194 -0
- package/src/launchd/job-preflight.ts +152 -0
- package/src/launchd/job-validate.ts +134 -0
- package/src/launchd/job.ts +96 -0
- package/src/launchd/launchctl.test.ts +181 -0
- package/src/launchd/launchctl.ts +188 -0
- package/src/launchd/local-runner.test.ts +125 -0
- package/src/launchd/local-runner.ts +143 -0
- package/src/launchd/plist.test.ts +186 -0
- package/src/launchd/plist.ts +156 -0
- package/src/launchd/providers.test.ts +222 -0
- package/src/launchd/providers.ts +21 -0
- package/src/launchd/runner.ts +95 -0
- package/src/launchd/secret-tripwire.test.ts +110 -0
- package/src/launchd/secret-tripwire.ts +81 -0
- package/src/openbao/README.md +181 -0
- package/src/openbao/REPLACE.md +163 -0
- package/src/openbao/auth-method-form.ts +7 -0
- package/src/openbao/auth-method-reconcile.ts +74 -0
- package/src/openbao/auth-method.ts +20 -41
- package/src/openbao/auth-role-form.test.ts +16 -1
- package/src/openbao/auth-role-form.ts +11 -0
- package/src/openbao/auth-role.ts +12 -0
- package/src/openbao/auth-token-form.ts +161 -0
- package/src/openbao/bao-identity.test.ts +131 -0
- package/src/openbao/bao-identity.ts +188 -0
- package/src/openbao/bao-namespace.ts +32 -0
- package/src/openbao/cloudflare-role-wire.test.ts +8 -34
- package/src/openbao/cloudflare-role.ts +30 -8
- package/src/openbao/cloudflare-roles-expand.ts +2 -5
- package/src/openbao/fake-engines.ts +119 -0
- package/src/openbao/fake-stack.ts +112 -0
- package/src/openbao/host-approles.test.ts +121 -0
- package/src/openbao/host-approles.ts +126 -0
- package/src/openbao/index.ts +19 -0
- package/src/openbao/jwt-config-form.ts +161 -0
- package/src/openbao/jwt-config.ts +85 -0
- package/src/openbao/jwt-role-form.test.ts +130 -0
- package/src/openbao/jwt-role-form.ts +231 -0
- package/src/openbao/jwt-role.ts +102 -0
- package/src/openbao/kubernetes-role-form.test.ts +82 -0
- package/src/openbao/kubernetes-role-form.ts +149 -0
- package/src/openbao/kubernetes-role.ts +94 -0
- package/src/openbao/mfa-enforcement-form.ts +147 -0
- package/src/openbao/mfa-enforcement.ts +143 -0
- package/src/openbao/mfa-totp-form.ts +155 -0
- package/src/openbao/mfa-totp.ts +118 -0
- package/src/openbao/mfa-wire.ts +117 -0
- package/src/openbao/mfa.test.ts +200 -0
- package/src/openbao/mount-form.ts +9 -1
- package/src/openbao/mount-move.test.ts +164 -0
- package/src/openbao/mount-move.ts +113 -0
- package/src/openbao/mount-path.test.ts +11 -1
- package/src/openbao/mount-path.ts +17 -11
- package/src/openbao/mount-reconcile.ts +75 -0
- package/src/openbao/mount.ts +26 -43
- package/src/openbao/pki-role-canonical.ts +163 -0
- package/src/openbao/pki-role-form.test.ts +112 -0
- package/src/openbao/pki-role-form.ts +76 -110
- package/src/openbao/pki-role.ts +12 -27
- package/src/openbao/policy-rename.test.ts +112 -0
- package/src/openbao/policy-wire.test.ts +7 -26
- package/src/openbao/policy.ts +27 -4
- package/src/openbao/proxmox-role-form.ts +27 -8
- package/src/openbao/proxmox-role-wire.ts +24 -0
- package/src/openbao/proxmox-role.ts +62 -37
- package/src/openbao/remount-wire.test.ts +87 -0
- package/src/openbao/remount-wire.ts +112 -0
- package/src/openbao/rename-occupied.test.ts +166 -0
- package/src/openbao/rename.ts +162 -0
- package/src/openbao/role-reconcile.test.ts +104 -0
- package/src/openbao/role-reconcile.ts +84 -0
- package/src/openbao/role-rename.test.ts +205 -0
package/dist/openbao/index.js
CHANGED
|
@@ -140,18 +140,20 @@ var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, pa
|
|
|
140
140
|
var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
|
|
141
141
|
|
|
142
142
|
// src/openbao/mount-path.ts
|
|
143
|
-
var
|
|
143
|
+
var trimTrailing = (text, char) => {
|
|
144
144
|
let end = text.length;
|
|
145
|
-
while (end > 0 && text[end - 1] ===
|
|
145
|
+
while (end > 0 && text[end - 1] === char)
|
|
146
146
|
end -= 1;
|
|
147
147
|
return text.slice(0, end);
|
|
148
148
|
};
|
|
149
|
-
var
|
|
149
|
+
var trimRuns = (text, char) => {
|
|
150
150
|
let start = 0;
|
|
151
|
-
while (start < text.length && text[start] ===
|
|
151
|
+
while (start < text.length && text[start] === char)
|
|
152
152
|
start += 1;
|
|
153
|
-
return
|
|
153
|
+
return trimTrailing(text.slice(start), char);
|
|
154
154
|
};
|
|
155
|
+
var trimTrailingSlashes = (text) => trimTrailing(text, "/");
|
|
156
|
+
var trimSlashes = (text) => trimRuns(text, "/");
|
|
155
157
|
var sshMountOf = (mount) => trimTrailingSlashes(mount ?? "ssh");
|
|
156
158
|
|
|
157
159
|
// src/openbao/approle-login-form.ts
|
|
@@ -295,14 +297,14 @@ var revokeSelf = (token, env) => runWith(env, revokeSelfEffect(token));
|
|
|
295
297
|
import { Resource } from "alchemy";
|
|
296
298
|
import { isResolved } from "alchemy/Diff";
|
|
297
299
|
import * as Provider from "alchemy/Provider";
|
|
298
|
-
import * as
|
|
300
|
+
import * as Effect7 from "effect/Effect";
|
|
299
301
|
|
|
300
302
|
// src/openbao/digest.ts
|
|
301
303
|
var canonical = (text) => text.trimEnd();
|
|
302
304
|
var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
|
|
303
305
|
|
|
304
306
|
// src/openbao/mount-form.ts
|
|
305
|
-
var mountPath = (path) => path
|
|
307
|
+
var mountPath = (path) => trimTrailingSlashes(path);
|
|
306
308
|
var DURATION = /^(\d+)([smhd])$/;
|
|
307
309
|
var parseDuration = (text) => {
|
|
308
310
|
const trimmed = text.trim();
|
|
@@ -435,6 +437,9 @@ var sameTtl2 = (want, have) => {
|
|
|
435
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);
|
|
436
438
|
var wantsTune2 = (props) => props.description !== undefined || props.defaultLeaseTtl !== undefined || props.maxLeaseTtl !== undefined;
|
|
437
439
|
|
|
440
|
+
// src/openbao/auth-method-reconcile.ts
|
|
441
|
+
import * as Effect6 from "effect/Effect";
|
|
442
|
+
|
|
438
443
|
// src/openbao/auth-method-wire.ts
|
|
439
444
|
import * as Effect3 from "effect/Effect";
|
|
440
445
|
var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect3.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
|
|
@@ -446,24 +451,150 @@ var enableAuthMethod = (props) => baoWrite("POST", readPath2(props.path), enable
|
|
|
446
451
|
var tuneAuthMethod = (props) => baoWrite("POST", tunePath2(props.path), tuneBody2(props));
|
|
447
452
|
var disableAuthMethod = (path) => baoDelete(readPath2(path));
|
|
448
453
|
|
|
449
|
-
// src/openbao/
|
|
450
|
-
|
|
451
|
-
|
|
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);
|
|
452
541
|
});
|
|
453
|
-
|
|
542
|
+
|
|
543
|
+
// src/openbao/auth-method-reconcile.ts
|
|
544
|
+
var readMethod = (props) => Effect6.gen(function* () {
|
|
454
545
|
const live = yield* readAuthMethodData(props.path);
|
|
455
546
|
if (live === undefined)
|
|
456
547
|
return;
|
|
457
548
|
return attributesOf2(props, live);
|
|
458
549
|
});
|
|
459
|
-
var
|
|
460
|
-
|
|
461
|
-
|
|
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 }) {
|
|
462
588
|
return yield* readMethod(olds);
|
|
463
589
|
}),
|
|
464
|
-
diff:
|
|
590
|
+
diff: Effect7.fn(function* ({ news, output }) {
|
|
465
591
|
if (output === undefined || !isResolved(news))
|
|
466
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" };
|
|
467
598
|
const live = yield* readMethod(news);
|
|
468
599
|
if (live === undefined)
|
|
469
600
|
return { action: "update" };
|
|
@@ -473,25 +604,10 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect4.succeed
|
|
|
473
604
|
return { action: "noop" };
|
|
474
605
|
return { action: "update" };
|
|
475
606
|
}),
|
|
476
|
-
reconcile:
|
|
477
|
-
|
|
478
|
-
if (live === undefined) {
|
|
479
|
-
yield* enableAuthMethod(news);
|
|
480
|
-
if (wantsTune2(news))
|
|
481
|
-
yield* tuneAuthMethod(news);
|
|
482
|
-
} else if (!matches2(live, news)) {
|
|
483
|
-
if (live.type !== news.type) {
|
|
484
|
-
return yield* Effect4.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
|
|
485
|
-
}
|
|
486
|
-
yield* tuneAuthMethod(news);
|
|
487
|
-
}
|
|
488
|
-
live = yield* readMethod(news);
|
|
489
|
-
if (live === undefined) {
|
|
490
|
-
return yield* Effect4.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
|
|
491
|
-
}
|
|
492
|
-
return live;
|
|
607
|
+
reconcile: Effect7.fn(function* ({ news, output }) {
|
|
608
|
+
return yield* reconcileAuthMethod(news, output?.path);
|
|
493
609
|
}),
|
|
494
|
-
delete:
|
|
610
|
+
delete: Effect7.fn(function* ({ output }) {
|
|
495
611
|
yield* disableAuthMethod(output.path);
|
|
496
612
|
return;
|
|
497
613
|
})
|
|
@@ -500,7 +616,7 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect4.succeed
|
|
|
500
616
|
import { Resource as Resource2 } from "alchemy";
|
|
501
617
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
502
618
|
import * as Provider2 from "alchemy/Provider";
|
|
503
|
-
import * as
|
|
619
|
+
import * as Effect8 from "effect/Effect";
|
|
504
620
|
|
|
505
621
|
// src/openbao/auth-role-form.ts
|
|
506
622
|
var policiesOf = (live) => {
|
|
@@ -549,6 +665,7 @@ var writeBody = (props) => {
|
|
|
549
665
|
}
|
|
550
666
|
return body;
|
|
551
667
|
};
|
|
668
|
+
var isRename = (from, to) => from.toLowerCase() !== to.toLowerCase();
|
|
552
669
|
var samePolicies = (want, have) => want.length === have.length && want.every((policy, index) => policy === have[index]);
|
|
553
670
|
var sameTtl3 = (want, have) => {
|
|
554
671
|
const wantSeconds = parseDuration(want);
|
|
@@ -564,20 +681,22 @@ var matches3 = (attributes, props) => {
|
|
|
564
681
|
var BaoAuthRole = Resource2("Bao.AuthRole", {
|
|
565
682
|
defaultRemovalPolicy: "retain"
|
|
566
683
|
});
|
|
567
|
-
var readRole = (props) =>
|
|
684
|
+
var readRole = (props) => Effect8.gen(function* () {
|
|
568
685
|
const live = yield* baoRead(readPath3(props.name));
|
|
569
686
|
if (live === undefined)
|
|
570
687
|
return;
|
|
571
688
|
return attributesOf3(props, live);
|
|
572
689
|
});
|
|
573
|
-
var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole,
|
|
574
|
-
list: () =>
|
|
575
|
-
read:
|
|
690
|
+
var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect8.succeed(BaoAuthRole.Provider.of({
|
|
691
|
+
list: () => Effect8.succeed([]),
|
|
692
|
+
read: Effect8.fn(function* ({ olds }) {
|
|
576
693
|
return yield* readRole(olds);
|
|
577
694
|
}),
|
|
578
|
-
diff:
|
|
695
|
+
diff: Effect8.fn(function* ({ news, output }) {
|
|
579
696
|
if (output === undefined || !isResolved2(news))
|
|
580
697
|
return;
|
|
698
|
+
if (isRename(output.name, news.name))
|
|
699
|
+
return { action: "replace" };
|
|
581
700
|
const live = yield* readRole(news);
|
|
582
701
|
if (live === undefined)
|
|
583
702
|
return { action: "update" };
|
|
@@ -585,27 +704,107 @@ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect5.succeed(Ba
|
|
|
585
704
|
return { action: "noop" };
|
|
586
705
|
return { action: "update" };
|
|
587
706
|
}),
|
|
588
|
-
reconcile:
|
|
707
|
+
reconcile: Effect8.fn(function* ({ news }) {
|
|
589
708
|
const live = yield* readRole(news);
|
|
590
709
|
if (live === undefined || !matches3(live, news)) {
|
|
591
710
|
yield* baoWrite("PUT", readPath3(news.name), writeBody(news));
|
|
592
711
|
}
|
|
593
712
|
const after = yield* readRole(news);
|
|
594
713
|
if (after === undefined) {
|
|
595
|
-
return yield*
|
|
714
|
+
return yield* Effect8.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
|
|
596
715
|
}
|
|
597
716
|
return after;
|
|
598
717
|
}),
|
|
599
|
-
delete:
|
|
718
|
+
delete: Effect8.fn(function* ({ output }) {
|
|
600
719
|
yield* baoDelete(readPath3(output.name));
|
|
601
720
|
return;
|
|
602
721
|
})
|
|
603
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
|
+
};
|
|
604
803
|
// src/openbao/cloudflare-role.ts
|
|
605
804
|
import { Resource as Resource3 } from "alchemy";
|
|
606
805
|
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
607
806
|
import * as Provider3 from "alchemy/Provider";
|
|
608
|
-
import * as
|
|
807
|
+
import * as Effect13 from "effect/Effect";
|
|
609
808
|
|
|
610
809
|
// src/openbao/cloudflare-group-scope.ts
|
|
611
810
|
var zoneResource = (zoneId) => `com.cloudflare.api.account.zone.${zoneId}`;
|
|
@@ -625,7 +824,7 @@ var groupKey = (name, scope) => `${scope} ${name}`;
|
|
|
625
824
|
|
|
626
825
|
// src/openbao/cloudflare-permission-groups.ts
|
|
627
826
|
import * as Context2 from "effect/Context";
|
|
628
|
-
import * as
|
|
827
|
+
import * as Effect11 from "effect/Effect";
|
|
629
828
|
|
|
630
829
|
// src/openbao/cloudflare-permission-groups-solve.ts
|
|
631
830
|
var observationsOf = (declared, live) => declared.flatMap((role) => {
|
|
@@ -795,7 +994,7 @@ var samePolicies2 = (want, have) => want.length === have.length && want.every((w
|
|
|
795
994
|
});
|
|
796
995
|
|
|
797
996
|
// src/openbao/cloudflare-role-wire.ts
|
|
798
|
-
import * as
|
|
997
|
+
import * as Effect10 from "effect/Effect";
|
|
799
998
|
|
|
800
999
|
// src/openbao/cloudflare-role-form.ts
|
|
801
1000
|
var rolePath = (mount, name) => `${mountPath(mount)}/roles/${name}`;
|
|
@@ -860,7 +1059,7 @@ var differences = (props, policies, live) => {
|
|
|
860
1059
|
};
|
|
861
1060
|
|
|
862
1061
|
// src/openbao/cloudflare-role-wire.ts
|
|
863
|
-
var readCloudflareRole = (mount, name) =>
|
|
1062
|
+
var readCloudflareRole = (mount, name) => Effect10.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
|
|
864
1063
|
var writeCloudflareRole = (mount, name, body) => baoWrite("PUT", rolePath(mount, name), body);
|
|
865
1064
|
var deleteCloudflareRole = (mount, name) => baoDelete(rolePath(mount, name));
|
|
866
1065
|
|
|
@@ -882,9 +1081,9 @@ var pick = (mount, groups, refs, hint) => {
|
|
|
882
1081
|
const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));
|
|
883
1082
|
if (unknown.length > 0) {
|
|
884
1083
|
const problems = unknown.map((ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`);
|
|
885
|
-
return
|
|
1084
|
+
return Effect11.fail(new CloudflareGroupsError(mount, problems));
|
|
886
1085
|
}
|
|
887
|
-
return
|
|
1086
|
+
return Effect11.succeed(new Map(refs.flatMap((ref) => {
|
|
888
1087
|
const key = groupKey(ref.name, ref.scope);
|
|
889
1088
|
const id = groups.get(key);
|
|
890
1089
|
return id === undefined ? [] : [[key, id]];
|
|
@@ -892,15 +1091,15 @@ var pick = (mount, groups, refs, hint) => {
|
|
|
892
1091
|
};
|
|
893
1092
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
894
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";
|
|
895
|
-
var deniedNamed = (mount, read) =>
|
|
896
|
-
var engineGroups = (mount) =>
|
|
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) => {
|
|
897
1096
|
const list = data?.["permission_groups"];
|
|
898
1097
|
if (data === undefined) {
|
|
899
1098
|
const why = "GET <mount>/permission-groups answered 404 — the endpoint is not deployed here";
|
|
900
|
-
return
|
|
1099
|
+
return Effect11.fail(new CloudflareGroupsError(mount, [why]));
|
|
901
1100
|
}
|
|
902
1101
|
if (!Array.isArray(list)) {
|
|
903
|
-
return
|
|
1102
|
+
return Effect11.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
|
|
904
1103
|
}
|
|
905
1104
|
const scoped = new Map;
|
|
906
1105
|
const idsOf = new Map;
|
|
@@ -927,24 +1126,24 @@ var engineGroups = (mount) => Effect7.flatMap(deniedNamed(mount, baoRead(`${moun
|
|
|
927
1126
|
const only = [...ids];
|
|
928
1127
|
return ids.size === 1 && only[0] !== undefined ? [[name, only[0]]] : [];
|
|
929
1128
|
}));
|
|
930
|
-
return problems.length > 0 ?
|
|
1129
|
+
return problems.length > 0 ? Effect11.fail(new CloudflareGroupsError(mount, problems)) : Effect11.succeed(new Map([...atEveryScope(sole), ...scoped]));
|
|
931
1130
|
});
|
|
932
|
-
var permissionGroupsFromEngine = () =>
|
|
1131
|
+
var permissionGroupsFromEngine = () => Effect11.sync(() => {
|
|
933
1132
|
const cache = new Map;
|
|
934
|
-
const cachedFor = (mount) =>
|
|
1133
|
+
const cachedFor = (mount) => Effect11.suspend(() => {
|
|
935
1134
|
const hit = cache.get(mountPath(mount));
|
|
936
1135
|
if (hit !== undefined)
|
|
937
1136
|
return hit;
|
|
938
|
-
return
|
|
1137
|
+
return Effect11.flatMap(Effect11.cached(engineGroups(mount)), (lookup) => {
|
|
939
1138
|
cache.set(mountPath(mount), lookup);
|
|
940
1139
|
return lookup;
|
|
941
1140
|
});
|
|
942
1141
|
});
|
|
943
1142
|
return CloudflarePermissionGroups.of({
|
|
944
|
-
resolve: (mount, refs) =>
|
|
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"))
|
|
945
1144
|
});
|
|
946
1145
|
});
|
|
947
|
-
var permissionGroupsFromLiveRoles = (declared) =>
|
|
1146
|
+
var permissionGroupsFromLiveRoles = (declared) => Effect11.gen(function* () {
|
|
948
1147
|
const accountOf = new Map;
|
|
949
1148
|
const byAccount = new Map;
|
|
950
1149
|
for (const [mount, roles] of declared) {
|
|
@@ -955,9 +1154,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
|
|
|
955
1154
|
}
|
|
956
1155
|
const solved = new Map;
|
|
957
1156
|
for (const [account, roles] of byAccount) {
|
|
958
|
-
const solve =
|
|
1157
|
+
const solve = Effect11.gen(function* () {
|
|
959
1158
|
const live = new Map;
|
|
960
|
-
yield*
|
|
1159
|
+
yield* Effect11.all(roles.map((role) => Effect11.map(readCloudflareRole(role.mount, role.name), (read) => {
|
|
961
1160
|
if (read?.policies !== undefined) {
|
|
962
1161
|
live.set(`${role.mount}/${role.name}`, read.policies);
|
|
963
1162
|
}
|
|
@@ -965,11 +1164,11 @@ var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
|
|
|
965
1164
|
const result = solveGroups(observationsOf(roles, live));
|
|
966
1165
|
const problems = describeProblems(result);
|
|
967
1166
|
if (problems.length > 0) {
|
|
968
|
-
return yield*
|
|
1167
|
+
return yield* Effect11.fail(new CloudflareGroupsError(`account ${account}`, problems));
|
|
969
1168
|
}
|
|
970
1169
|
return atEveryScope(result.groups);
|
|
971
1170
|
});
|
|
972
|
-
solved.set(account, yield*
|
|
1171
|
+
solved.set(account, yield* Effect11.cached(solve));
|
|
973
1172
|
}
|
|
974
1173
|
return CloudflarePermissionGroups.of({
|
|
975
1174
|
resolve: (mount, refs) => {
|
|
@@ -977,21 +1176,54 @@ var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
|
|
|
977
1176
|
const lookup = account === undefined ? undefined : solved.get(account);
|
|
978
1177
|
if (account === undefined || lookup === undefined) {
|
|
979
1178
|
const why = "no declared role on this mount, so no live role can vouch for its groups";
|
|
980
|
-
return
|
|
1179
|
+
return Effect11.fail(new CloudflareGroupsError(mount, [why]));
|
|
981
1180
|
}
|
|
982
1181
|
const hint = `no live role on account ${account} carries it; a NEW group needs the engine endpoint`;
|
|
983
|
-
return
|
|
1182
|
+
return Effect11.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
|
|
984
1183
|
}
|
|
985
1184
|
});
|
|
986
1185
|
});
|
|
987
1186
|
|
|
1187
|
+
// src/openbao/rename.ts
|
|
1188
|
+
import * as Effect12 from "effect/Effect";
|
|
1189
|
+
var exact = (identity) => identity;
|
|
1190
|
+
var declaredString = (news, key) => {
|
|
1191
|
+
if (typeof news !== "object" || news === null)
|
|
1192
|
+
return;
|
|
1193
|
+
const value = news[key];
|
|
1194
|
+
return typeof value === "string" ? value : undefined;
|
|
1195
|
+
};
|
|
1196
|
+
var isPendingProp = (news, key) => {
|
|
1197
|
+
if (typeof news !== "object" || news === null)
|
|
1198
|
+
return false;
|
|
1199
|
+
const value = news[key];
|
|
1200
|
+
return value !== undefined && typeof value !== "string";
|
|
1201
|
+
};
|
|
1202
|
+
var declaredRolePath = (news, rolePath2) => {
|
|
1203
|
+
const mount = declaredString(news, "mount");
|
|
1204
|
+
const name = declaredString(news, "name");
|
|
1205
|
+
return mount === undefined || name === undefined ? undefined : rolePath2(mount, name);
|
|
1206
|
+
};
|
|
1207
|
+
var policyKey = (name) => name.trim().toLowerCase();
|
|
1208
|
+
var isMoved = (stored, declared, key = exact) => declared === undefined ? undefined : key(stored) !== key(declared);
|
|
1209
|
+
var triedRolePath = (output, olds, rolePath2) => output === undefined ? declaredRolePath(olds, rolePath2) : rolePath2(output.mount, output.name);
|
|
1210
|
+
var judgeMove = (family, tried, declared, pathOf, key = exact) => Effect12.gen(function* () {
|
|
1211
|
+
if (tried === undefined || declared === undefined || key(tried) === key(declared)) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if ((yield* baoRead(pathOf(declared))) === undefined)
|
|
1215
|
+
return { from: tried, to: declared };
|
|
1216
|
+
return yield* Effect12.die(new Error(`${family}: ${tried} → ${declared} would land on an object that already exists at ` + `${declared}. Under RemovalPolicy.destroy() a resource moving off that name (a swap, a ` + "shift, or a reverted move that did not finish) deletes it after this one writes it. " + "Nothing was written. Move through a name nothing holds, in two deploys, or remove " + `${declared} by hand first.`));
|
|
1217
|
+
});
|
|
1218
|
+
var refuseMovedUpdate = (family, from, to) => Effect12.die(new Error(`${family}: planned as an update, but its identity moved from ${from} to ${to}. Writing ` + `${to} now would leave ${from} live with no state record. Nothing was written. Run the ` + "deploy again: once the value resolves at plan time it plans as a replace."));
|
|
1219
|
+
|
|
988
1220
|
// src/openbao/cloudflare-role.ts
|
|
989
1221
|
var BaoCloudflareRole = Resource3("Bao.CloudflareRole", {
|
|
990
1222
|
defaultRemovalPolicy: "retain"
|
|
991
1223
|
});
|
|
992
|
-
var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole,
|
|
1224
|
+
var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect13.gen(function* () {
|
|
993
1225
|
const groups = yield* CloudflarePermissionGroups;
|
|
994
|
-
const resolved = (props) =>
|
|
1226
|
+
const resolved = (props) => Effect13.gen(function* () {
|
|
995
1227
|
const refs = new Map;
|
|
996
1228
|
for (const policy of props.policies) {
|
|
997
1229
|
const scope = policy.scope ?? scopeOfResource(policy.resource);
|
|
@@ -1001,18 +1233,25 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
1001
1233
|
const ids = yield* groups.resolve(props.mount, [...refs.values()]);
|
|
1002
1234
|
const { missing, policies } = resolvePolicies(props.policies, ids);
|
|
1003
1235
|
if (missing.length > 0) {
|
|
1004
|
-
return yield*
|
|
1236
|
+
return yield* Effect13.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
|
|
1005
1237
|
}
|
|
1006
1238
|
return policies;
|
|
1007
1239
|
});
|
|
1008
1240
|
return BaoCloudflareRole.Provider.of({
|
|
1009
|
-
list: () =>
|
|
1010
|
-
read:
|
|
1241
|
+
list: () => Effect13.succeed([]),
|
|
1242
|
+
read: Effect13.fn(function* ({ olds }) {
|
|
1011
1243
|
const live = yield* readCloudflareRole(olds.mount, olds.name);
|
|
1012
1244
|
return live === undefined ? undefined : attributesOf4(olds, live);
|
|
1013
1245
|
}),
|
|
1014
|
-
diff:
|
|
1015
|
-
|
|
1246
|
+
diff: Effect13.fn(function* ({ news, olds, output }) {
|
|
1247
|
+
const tried = triedRolePath(output, olds, rolePath);
|
|
1248
|
+
const declared = declaredRolePath(news, rolePath);
|
|
1249
|
+
const move = yield* judgeMove("Bao.CloudflareRole", tried, declared, (path) => path);
|
|
1250
|
+
if (output === undefined)
|
|
1251
|
+
return;
|
|
1252
|
+
if (move !== undefined)
|
|
1253
|
+
return { action: "replace" };
|
|
1254
|
+
if (!isResolved3(news))
|
|
1016
1255
|
return;
|
|
1017
1256
|
if (refusalOf(news) !== undefined)
|
|
1018
1257
|
return { action: "update" };
|
|
@@ -1022,11 +1261,15 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
1022
1261
|
return { action: "update" };
|
|
1023
1262
|
return differences(news, policies, live).length === 0 ? { action: "noop" } : { action: "update" };
|
|
1024
1263
|
}),
|
|
1025
|
-
reconcile:
|
|
1264
|
+
reconcile: Effect13.fn(function* ({ news, output }) {
|
|
1026
1265
|
const path = rolePath(news.mount, news.name);
|
|
1266
|
+
const before = output === undefined ? path : rolePath(output.mount, output.name);
|
|
1267
|
+
if (isMoved(before, path) === true) {
|
|
1268
|
+
return yield* refuseMovedUpdate("Bao.CloudflareRole", before, path);
|
|
1269
|
+
}
|
|
1027
1270
|
const refusal2 = refusalOf(news);
|
|
1028
1271
|
if (refusal2 !== undefined) {
|
|
1029
|
-
return yield*
|
|
1272
|
+
return yield* Effect13.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
|
|
1030
1273
|
}
|
|
1031
1274
|
const policies = yield* resolved(news);
|
|
1032
1275
|
const live = yield* readCloudflareRole(news.mount, news.name);
|
|
@@ -1035,22 +1278,22 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
1035
1278
|
}
|
|
1036
1279
|
const after = yield* readCloudflareRole(news.mount, news.name);
|
|
1037
1280
|
if (after === undefined) {
|
|
1038
|
-
return yield*
|
|
1281
|
+
return yield* Effect13.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
|
|
1039
1282
|
}
|
|
1040
1283
|
const left = differences(news, policies, after);
|
|
1041
1284
|
if (left.length > 0) {
|
|
1042
|
-
return yield*
|
|
1285
|
+
return yield* Effect13.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
|
|
1043
1286
|
}
|
|
1044
1287
|
return attributesOf4(news, after);
|
|
1045
1288
|
}),
|
|
1046
|
-
delete:
|
|
1289
|
+
delete: Effect13.fn(function* ({ output }) {
|
|
1047
1290
|
yield* deleteCloudflareRole(output.mount, output.name);
|
|
1048
1291
|
return;
|
|
1049
1292
|
})
|
|
1050
1293
|
});
|
|
1051
1294
|
}));
|
|
1052
1295
|
// src/openbao/cloudflare-roles-expand.ts
|
|
1053
|
-
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-")
|
|
1296
|
+
var slug = (name) => trimRuns(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-");
|
|
1054
1297
|
var zoneSlug = (zone) => `${slug(zone.name)}${zone.internal ? "-internal" : ""}`;
|
|
1055
1298
|
var mountName = (account, surface) => `cloudflare-${account}-${surface}`;
|
|
1056
1299
|
var policiesFor = (tpl, accountId, zone, accountGroups) => {
|
|
@@ -1145,12 +1388,12 @@ class RolesConfigError extends Error {
|
|
|
1145
1388
|
}
|
|
1146
1389
|
}
|
|
1147
1390
|
var isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1148
|
-
var
|
|
1391
|
+
var record2 = (value, where) => {
|
|
1149
1392
|
if (!isRaw(value))
|
|
1150
1393
|
throw new RolesConfigError(where, "expected a mapping");
|
|
1151
1394
|
return value;
|
|
1152
1395
|
};
|
|
1153
|
-
var
|
|
1396
|
+
var text4 = (raw, key, where) => {
|
|
1154
1397
|
const value = raw[key];
|
|
1155
1398
|
if (typeof value === "string")
|
|
1156
1399
|
return value;
|
|
@@ -1174,35 +1417,35 @@ var entries = (raw, key, where) => {
|
|
|
1174
1417
|
return value;
|
|
1175
1418
|
};
|
|
1176
1419
|
var template = (value, where) => {
|
|
1177
|
-
const raw =
|
|
1420
|
+
const raw = record2(value, where);
|
|
1178
1421
|
const groups = texts(raw, "groups", where);
|
|
1179
1422
|
if (groups === undefined)
|
|
1180
1423
|
throw new RolesConfigError(`${where}.groups`, "is required");
|
|
1181
|
-
const bucket = raw["bucket"] === undefined ? undefined :
|
|
1424
|
+
const bucket = raw["bucket"] === undefined ? undefined : text4(raw, "bucket", where);
|
|
1182
1425
|
if (bucket !== undefined && bucket.length === 0) {
|
|
1183
1426
|
throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
|
|
1184
1427
|
}
|
|
1185
|
-
const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(
|
|
1428
|
+
const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text4(raw, "groups_scope", where));
|
|
1186
1429
|
if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
|
|
1187
1430
|
throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
|
|
1188
1431
|
}
|
|
1189
1432
|
return {
|
|
1190
1433
|
accountGroups: texts(raw, "account_groups", where) ?? [],
|
|
1191
1434
|
bucket,
|
|
1192
|
-
bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" :
|
|
1193
|
-
desc: raw["desc"] === undefined ? "" :
|
|
1435
|
+
bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text4(raw, "bucket_jurisdiction", where),
|
|
1436
|
+
desc: raw["desc"] === undefined ? "" : text4(raw, "desc", where),
|
|
1194
1437
|
groups,
|
|
1195
1438
|
groupsScope,
|
|
1196
|
-
maxTtl:
|
|
1197
|
-
name:
|
|
1198
|
-
ttl:
|
|
1439
|
+
maxTtl: text4(raw, "max_ttl", where),
|
|
1440
|
+
name: text4(raw, "name", where),
|
|
1441
|
+
ttl: text4(raw, "ttl", where),
|
|
1199
1442
|
zoneTypes: texts(raw, "zone_types", where) ?? [],
|
|
1200
1443
|
zones: texts(raw, "zones", where)
|
|
1201
1444
|
};
|
|
1202
1445
|
};
|
|
1203
1446
|
var surface = (name, value) => {
|
|
1204
1447
|
const where = `surfaces.${name}`;
|
|
1205
|
-
const raw =
|
|
1448
|
+
const raw = record2(value, where);
|
|
1206
1449
|
return {
|
|
1207
1450
|
accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
|
|
1208
1451
|
zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
|
|
@@ -1210,7 +1453,7 @@ var surface = (name, value) => {
|
|
|
1210
1453
|
};
|
|
1211
1454
|
var account = (key, value, surfaces) => {
|
|
1212
1455
|
const where = `accounts.${key}`;
|
|
1213
|
-
const raw =
|
|
1456
|
+
const raw = record2(value, where);
|
|
1214
1457
|
const names = texts(raw, "surfaces", where) ?? [];
|
|
1215
1458
|
for (const name of names) {
|
|
1216
1459
|
if (surfaces[name] === undefined) {
|
|
@@ -1218,56 +1461,941 @@ var account = (key, value, surfaces) => {
|
|
|
1218
1461
|
}
|
|
1219
1462
|
}
|
|
1220
1463
|
const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
|
|
1221
|
-
return { id:
|
|
1464
|
+
return { id: text4(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
|
|
1222
1465
|
};
|
|
1223
1466
|
var parseRolesConfig = (yaml) => {
|
|
1224
|
-
const root =
|
|
1467
|
+
const root = record2(Bun.YAML.parse(yaml), "(document)");
|
|
1225
1468
|
const surfaces = {};
|
|
1226
|
-
for (const [name, spec] of Object.entries(
|
|
1469
|
+
for (const [name, spec] of Object.entries(record2(root["surfaces"], "surfaces"))) {
|
|
1227
1470
|
surfaces[name] = surface(name, spec);
|
|
1228
1471
|
}
|
|
1229
|
-
const accounts = Object.entries(
|
|
1472
|
+
const accounts = Object.entries(record2(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
|
|
1230
1473
|
return {
|
|
1231
1474
|
accounts,
|
|
1232
|
-
namespace:
|
|
1475
|
+
namespace: text4(record2(root["defaults"], "defaults"), "namespace", "defaults"),
|
|
1233
1476
|
surfaces
|
|
1234
1477
|
};
|
|
1235
1478
|
};
|
|
1236
|
-
// src/openbao/
|
|
1479
|
+
// src/openbao/host-approles.ts
|
|
1480
|
+
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1481
|
+
var HOST_ROLE_SEPARATOR = "--";
|
|
1482
|
+
var hostRoleName = (className, host) => `${className}${HOST_ROLE_SEPARATOR}${host}`;
|
|
1483
|
+
var classProblems = (name, spec) => {
|
|
1484
|
+
const found = [];
|
|
1485
|
+
if (!NAME.test(name))
|
|
1486
|
+
found.push(`class \`${name}\` is not lowercase letters, digits, single hyphens`);
|
|
1487
|
+
if (spec.policies.length === 0)
|
|
1488
|
+
found.push(`class \`${name}\` has no policies`);
|
|
1489
|
+
if (spec.policies.includes("root"))
|
|
1490
|
+
found.push(`class \`${name}\` grants \`root\``);
|
|
1491
|
+
const secretId = parseDuration(spec.secretIdTtl);
|
|
1492
|
+
if (secretId === undefined || secretId <= 0) {
|
|
1493
|
+
found.push(`class \`${name}\` secretIdTtl \`${spec.secretIdTtl}\` must be a duration above zero ` + "(0 never expires)");
|
|
1494
|
+
}
|
|
1495
|
+
const ttl = parseDuration(spec.tokenTtl);
|
|
1496
|
+
const max = parseDuration(spec.tokenMaxTtl);
|
|
1497
|
+
if (ttl === undefined || max === undefined) {
|
|
1498
|
+
found.push(`class \`${name}\` token TTLs must be durations like 15m or 1h`);
|
|
1499
|
+
} else if (max > 0 && ttl > max) {
|
|
1500
|
+
found.push(`class \`${name}\` tokenTtl is longer than tokenMaxTtl`);
|
|
1501
|
+
}
|
|
1502
|
+
return found;
|
|
1503
|
+
};
|
|
1504
|
+
var hostAppRoles = (input) => {
|
|
1505
|
+
const problems = Object.entries(input.classes).flatMap(([name, spec]) => classProblems(name, spec));
|
|
1506
|
+
const seen = new Set;
|
|
1507
|
+
const roles = [];
|
|
1508
|
+
for (const host of input.hosts) {
|
|
1509
|
+
if (!NAME.test(host.name)) {
|
|
1510
|
+
problems.push(`host \`${host.name}\` is not lowercase letters, digits, single hyphens`);
|
|
1511
|
+
}
|
|
1512
|
+
if (seen.has(host.name))
|
|
1513
|
+
problems.push(`host \`${host.name}\` is listed twice`);
|
|
1514
|
+
seen.add(host.name);
|
|
1515
|
+
const spec = input.classes[host.class];
|
|
1516
|
+
if (spec === undefined) {
|
|
1517
|
+
problems.push(`host \`${host.name}\` names unknown class \`${host.class}\``);
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
roles.push({
|
|
1521
|
+
bindSecretId: true,
|
|
1522
|
+
name: hostRoleName(host.class, host.name),
|
|
1523
|
+
secretIdNumUses: spec.secretIdNumUses ?? 0,
|
|
1524
|
+
secretIdTtl: spec.secretIdTtl,
|
|
1525
|
+
tokenMaxTtl: spec.tokenMaxTtl,
|
|
1526
|
+
tokenPolicies: [...new Set(spec.policies)].sort(),
|
|
1527
|
+
tokenTtl: spec.tokenTtl
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if (problems.length > 0) {
|
|
1531
|
+
throw new Error(`hostAppRoles refused the input: ${problems.join("; ")}.`);
|
|
1532
|
+
}
|
|
1533
|
+
return roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1534
|
+
};
|
|
1535
|
+
// src/openbao/jwt-config.ts
|
|
1237
1536
|
import { Resource as Resource4 } from "alchemy";
|
|
1238
1537
|
import { isResolved as isResolved4 } from "alchemy/Diff";
|
|
1239
1538
|
import * as Provider4 from "alchemy/Provider";
|
|
1240
|
-
import * as
|
|
1539
|
+
import * as Effect15 from "effect/Effect";
|
|
1540
|
+
|
|
1541
|
+
// src/openbao/jwt-config-form.ts
|
|
1542
|
+
var text5 = (value) => typeof value === "string" ? value.trim() : "";
|
|
1543
|
+
var strings2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1544
|
+
var sortedSet = (values) => [...new Set(values.map((value) => value.trim()))].sort();
|
|
1545
|
+
var canonical2 = (input) => ({
|
|
1546
|
+
boundIssuer: input.boundIssuer.trim(),
|
|
1547
|
+
defaultRole: input.defaultRole.trim(),
|
|
1548
|
+
jwksCaPem: input.jwksCaPem.trim(),
|
|
1549
|
+
jwksUrl: input.jwksUrl.trim(),
|
|
1550
|
+
jwtSupportedAlgs: sortedSet(input.jwtSupportedAlgs),
|
|
1551
|
+
jwtValidationPubkeys: sortedSet(input.jwtValidationPubkeys),
|
|
1552
|
+
oidcDiscoveryCaPem: input.oidcDiscoveryCaPem.trim(),
|
|
1553
|
+
oidcDiscoveryUrl: input.oidcDiscoveryUrl.trim()
|
|
1554
|
+
});
|
|
1555
|
+
var canonicalFromProps = (props) => canonical2({
|
|
1556
|
+
boundIssuer: props.boundIssuer ?? "",
|
|
1557
|
+
defaultRole: props.defaultRole ?? "",
|
|
1558
|
+
jwksCaPem: props.jwksCaPem ?? "",
|
|
1559
|
+
jwksUrl: props.jwksUrl ?? "",
|
|
1560
|
+
jwtSupportedAlgs: props.jwtSupportedAlgs ?? [],
|
|
1561
|
+
jwtValidationPubkeys: props.jwtValidationPubkeys ?? [],
|
|
1562
|
+
oidcDiscoveryCaPem: props.oidcDiscoveryCaPem ?? "",
|
|
1563
|
+
oidcDiscoveryUrl: props.oidcDiscoveryUrl ?? ""
|
|
1564
|
+
});
|
|
1565
|
+
var canonicalFromLive = (live) => canonical2({
|
|
1566
|
+
boundIssuer: text5(live["bound_issuer"]),
|
|
1567
|
+
defaultRole: text5(live["default_role"]),
|
|
1568
|
+
jwksCaPem: text5(live["jwks_ca_pem"]),
|
|
1569
|
+
jwksUrl: text5(live["jwks_url"]),
|
|
1570
|
+
jwtSupportedAlgs: strings2(live["jwt_supported_algs"]),
|
|
1571
|
+
jwtValidationPubkeys: strings2(live["jwt_validation_pubkeys"]),
|
|
1572
|
+
oidcDiscoveryCaPem: text5(live["oidc_discovery_ca_pem"]),
|
|
1573
|
+
oidcDiscoveryUrl: text5(live["oidc_discovery_url"])
|
|
1574
|
+
});
|
|
1575
|
+
var mountOf = (props) => mountPath(props.mount ?? "jwt");
|
|
1576
|
+
var configPath = (props) => `auth/${mountOf(props)}/config`;
|
|
1577
|
+
var attributesOf5 = (props, live) => {
|
|
1578
|
+
const form = canonicalFromLive(live);
|
|
1579
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf(props) };
|
|
1580
|
+
};
|
|
1581
|
+
var matches4 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps(props)));
|
|
1582
|
+
var writeBody3 = (props) => ({
|
|
1583
|
+
bound_issuer: props.boundIssuer ?? "",
|
|
1584
|
+
default_role: props.defaultRole ?? "",
|
|
1585
|
+
jwks_ca_pem: props.jwksCaPem ?? "",
|
|
1586
|
+
jwks_url: props.jwksUrl ?? "",
|
|
1587
|
+
jwt_supported_algs: [...props.jwtSupportedAlgs ?? []],
|
|
1588
|
+
jwt_validation_pubkeys: [...props.jwtValidationPubkeys ?? []],
|
|
1589
|
+
oidc_discovery_ca_pem: props.oidcDiscoveryCaPem ?? "",
|
|
1590
|
+
oidc_discovery_url: props.oidcDiscoveryUrl ?? ""
|
|
1591
|
+
});
|
|
1592
|
+
var wouldErase = (live) => {
|
|
1593
|
+
const lost = [];
|
|
1594
|
+
if (text5(live["oidc_client_id"]) !== "")
|
|
1595
|
+
lost.push("oidc_client_id (and its unreadable secret)");
|
|
1596
|
+
const provider = live["provider_config"];
|
|
1597
|
+
if (typeof provider === "object" && provider !== null && Object.keys(provider).length > 0) {
|
|
1598
|
+
lost.push("provider_config");
|
|
1599
|
+
}
|
|
1600
|
+
if (text5(live["oidc_response_mode"]) !== "")
|
|
1601
|
+
lost.push("oidc_response_mode");
|
|
1602
|
+
if (strings2(live["oidc_response_types"]).length > 0)
|
|
1603
|
+
lost.push("oidc_response_types");
|
|
1604
|
+
if (strings2(live["override_allowed_server_names"]).length > 0) {
|
|
1605
|
+
lost.push("override_allowed_server_names");
|
|
1606
|
+
}
|
|
1607
|
+
return lost;
|
|
1608
|
+
};
|
|
1609
|
+
var problems = (props) => {
|
|
1610
|
+
const found = [];
|
|
1611
|
+
const sources = [
|
|
1612
|
+
(props.oidcDiscoveryUrl ?? "") !== "",
|
|
1613
|
+
(props.jwksUrl ?? "") !== "",
|
|
1614
|
+
(props.jwtValidationPubkeys?.length ?? 0) > 0
|
|
1615
|
+
].filter(Boolean).length;
|
|
1616
|
+
if (sources !== 1) {
|
|
1617
|
+
found.push("exactly one of oidcDiscoveryUrl, jwksUrl or jwtValidationPubkeys must be set");
|
|
1618
|
+
}
|
|
1619
|
+
if ((props.oidcDiscoveryUrl ?? "").includes(".well-known")) {
|
|
1620
|
+
found.push("oidcDiscoveryUrl must be the issuer, without /.well-known/openid-configuration");
|
|
1621
|
+
}
|
|
1622
|
+
const role = props.defaultRole ?? "";
|
|
1623
|
+
if (role !== role.toLowerCase())
|
|
1624
|
+
found.push(`defaultRole \`${role}\` must be lower case`);
|
|
1625
|
+
return found;
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// src/openbao/role-reconcile.ts
|
|
1629
|
+
import * as Effect14 from "effect/Effect";
|
|
1630
|
+
var readRoleAt = (spec) => Effect14.map(baoRead(spec.path), (live) => live === undefined ? undefined : { attributes: spec.attributesOf(live), live });
|
|
1631
|
+
var planRole = (spec) => Effect14.gen(function* () {
|
|
1632
|
+
if (spec.problems.length > 0)
|
|
1633
|
+
return "update";
|
|
1634
|
+
const found = yield* readRoleAt(spec);
|
|
1635
|
+
return found !== undefined && spec.matches(found.attributes) ? "noop" : "update";
|
|
1636
|
+
});
|
|
1637
|
+
var refuse2 = (spec, message) => Effect14.die(new Error(`${spec.family} ${spec.path}: ${message}`));
|
|
1638
|
+
var reconcileRole = (spec) => Effect14.gen(function* () {
|
|
1639
|
+
if (spec.problems.length > 0)
|
|
1640
|
+
return yield* refuse2(spec, spec.problems.join("; "));
|
|
1641
|
+
const found = yield* readRoleAt(spec);
|
|
1642
|
+
if (found === undefined || !spec.matches(found.attributes)) {
|
|
1643
|
+
const lost = found === undefined ? [] : spec.wouldErase?.(found.live) ?? [];
|
|
1644
|
+
if (lost.length > 0) {
|
|
1645
|
+
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.");
|
|
1646
|
+
}
|
|
1647
|
+
yield* baoWrite("PUT", spec.path, spec.body);
|
|
1648
|
+
}
|
|
1649
|
+
const after = yield* readRoleAt(spec);
|
|
1650
|
+
if (after === undefined)
|
|
1651
|
+
return yield* refuse2(spec, "write reported success but it is absent.");
|
|
1652
|
+
if (!spec.matches(after.attributes)) {
|
|
1653
|
+
return yield* refuse2(spec, "wrote cleanly but read back different — OpenBao stored something other than what was " + "declared. Read it by hand before retrying.");
|
|
1654
|
+
}
|
|
1655
|
+
return after.attributes;
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
// src/openbao/jwt-config.ts
|
|
1659
|
+
var BaoJwtAuthConfig = Resource4("Bao.JwtAuthConfig", {
|
|
1660
|
+
defaultRemovalPolicy: "retain"
|
|
1661
|
+
});
|
|
1662
|
+
var jwtConfigSpec = (props) => ({
|
|
1663
|
+
attributesOf: (live) => attributesOf5(props, live),
|
|
1664
|
+
body: writeBody3(props),
|
|
1665
|
+
family: "Bao.JwtAuthConfig",
|
|
1666
|
+
matches: (attributes) => matches4(attributes, props),
|
|
1667
|
+
path: configPath(props),
|
|
1668
|
+
problems: problems(props),
|
|
1669
|
+
wouldErase
|
|
1670
|
+
});
|
|
1671
|
+
var BaoJwtAuthConfigProvider = () => Provider4.effect(BaoJwtAuthConfig, Effect15.succeed(BaoJwtAuthConfig.Provider.of({
|
|
1672
|
+
list: () => Effect15.succeed([]),
|
|
1673
|
+
read: Effect15.fn(function* ({ olds }) {
|
|
1674
|
+
const found = yield* readRoleAt(jwtConfigSpec(olds));
|
|
1675
|
+
return found?.attributes;
|
|
1676
|
+
}),
|
|
1677
|
+
diff: Effect15.fn(function* ({ news, output }) {
|
|
1678
|
+
if (output === undefined || !isResolved4(news))
|
|
1679
|
+
return;
|
|
1680
|
+
if (mountOf(news) !== output.mount)
|
|
1681
|
+
return { action: "replace" };
|
|
1682
|
+
return { action: yield* planRole(jwtConfigSpec(news)) };
|
|
1683
|
+
}),
|
|
1684
|
+
reconcile: Effect15.fn(function* ({ news }) {
|
|
1685
|
+
return yield* reconcileRole(jwtConfigSpec(news));
|
|
1686
|
+
}),
|
|
1687
|
+
delete: () => Effect15.void
|
|
1688
|
+
})));
|
|
1689
|
+
// src/openbao/jwt-role.ts
|
|
1690
|
+
import { Resource as Resource5 } from "alchemy";
|
|
1691
|
+
import { isResolved as isResolved5 } from "alchemy/Diff";
|
|
1692
|
+
import * as Provider5 from "alchemy/Provider";
|
|
1693
|
+
import * as Effect16 from "effect/Effect";
|
|
1694
|
+
|
|
1695
|
+
// src/openbao/auth-token-form.ts
|
|
1696
|
+
var sortedSet2 = (values) => [...new Set(values)].sort();
|
|
1697
|
+
var cidr = (value) => value.trim().replace(/\/(32|128)$/, "");
|
|
1698
|
+
var strings3 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1699
|
+
var seconds2 = (text6) => parseDuration(text6 ?? "0") ?? -1;
|
|
1700
|
+
var tokenFields = (input) => ({
|
|
1701
|
+
tokenBoundCidrs: input.tokenBoundCidrs,
|
|
1702
|
+
tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
|
|
1703
|
+
tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
|
|
1704
|
+
tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
|
|
1705
|
+
tokenNumUses: input.tokenNumUses,
|
|
1706
|
+
tokenPeriodSeconds: input.tokenPeriodSeconds,
|
|
1707
|
+
tokenPolicies: input.tokenPolicies,
|
|
1708
|
+
tokenTtlSeconds: input.tokenTtlSeconds,
|
|
1709
|
+
tokenType: input.tokenType
|
|
1710
|
+
});
|
|
1711
|
+
var tokenForm = (input) => ({
|
|
1712
|
+
tokenBoundCidrs: sortedSet2(input.tokenBoundCidrs.map(cidr)),
|
|
1713
|
+
tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
|
|
1714
|
+
tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
|
|
1715
|
+
tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
|
|
1716
|
+
tokenNumUses: input.tokenNumUses,
|
|
1717
|
+
tokenPeriodSeconds: input.tokenPeriodSeconds,
|
|
1718
|
+
tokenPolicies: sortedSet2(input.tokenPolicies),
|
|
1719
|
+
tokenTtlSeconds: input.tokenTtlSeconds,
|
|
1720
|
+
tokenType: input.tokenType
|
|
1721
|
+
});
|
|
1722
|
+
var tokenFormOfProps = (props) => tokenForm({
|
|
1723
|
+
tokenBoundCidrs: props.tokenBoundCidrs ?? [],
|
|
1724
|
+
tokenExplicitMaxTtlSeconds: seconds2(props.tokenExplicitMaxTtl),
|
|
1725
|
+
tokenMaxTtlSeconds: seconds2(props.tokenMaxTtl),
|
|
1726
|
+
tokenNoDefaultPolicy: props.tokenNoDefaultPolicy ?? false,
|
|
1727
|
+
tokenNumUses: props.tokenNumUses ?? 0,
|
|
1728
|
+
tokenPeriodSeconds: seconds2(props.tokenPeriod),
|
|
1729
|
+
tokenPolicies: props.tokenPolicies,
|
|
1730
|
+
tokenTtlSeconds: seconds2(props.tokenTtl),
|
|
1731
|
+
tokenType: props.tokenType ?? "default"
|
|
1732
|
+
});
|
|
1733
|
+
var tokenFormOfLive = (live) => tokenForm({
|
|
1734
|
+
tokenBoundCidrs: strings3(live["token_bound_cidrs"]),
|
|
1735
|
+
tokenExplicitMaxTtlSeconds: ttlSeconds(live["token_explicit_max_ttl"]) ?? -1,
|
|
1736
|
+
tokenMaxTtlSeconds: ttlSeconds(live["token_max_ttl"]) ?? -1,
|
|
1737
|
+
tokenNoDefaultPolicy: live["token_no_default_policy"] === true,
|
|
1738
|
+
tokenNumUses: typeof live["token_num_uses"] === "number" ? live["token_num_uses"] : -1,
|
|
1739
|
+
tokenPeriodSeconds: ttlSeconds(live["token_period"]) ?? -1,
|
|
1740
|
+
tokenPolicies: strings3(live["token_policies"]),
|
|
1741
|
+
tokenTtlSeconds: ttlSeconds(live["token_ttl"]) ?? -1,
|
|
1742
|
+
tokenType: typeof live["token_type"] === "string" ? live["token_type"] : ""
|
|
1743
|
+
});
|
|
1744
|
+
var tokenBody = (props) => ({
|
|
1745
|
+
token_bound_cidrs: [...props.tokenBoundCidrs ?? []],
|
|
1746
|
+
token_explicit_max_ttl: props.tokenExplicitMaxTtl ?? "0",
|
|
1747
|
+
token_max_ttl: props.tokenMaxTtl ?? "0",
|
|
1748
|
+
token_no_default_policy: props.tokenNoDefaultPolicy ?? false,
|
|
1749
|
+
token_num_uses: props.tokenNumUses ?? 0,
|
|
1750
|
+
token_period: props.tokenPeriod ?? "0",
|
|
1751
|
+
token_policies: [...props.tokenPolicies],
|
|
1752
|
+
token_ttl: props.tokenTtl ?? "0",
|
|
1753
|
+
token_type: props.tokenType ?? "default"
|
|
1754
|
+
});
|
|
1755
|
+
var tokenProblems = (props) => {
|
|
1756
|
+
const found = [];
|
|
1757
|
+
const durations = {
|
|
1758
|
+
tokenExplicitMaxTtl: props.tokenExplicitMaxTtl,
|
|
1759
|
+
tokenMaxTtl: props.tokenMaxTtl,
|
|
1760
|
+
tokenPeriod: props.tokenPeriod,
|
|
1761
|
+
tokenTtl: props.tokenTtl
|
|
1762
|
+
};
|
|
1763
|
+
for (const [field, text6] of Object.entries(durations)) {
|
|
1764
|
+
if (seconds2(text6) < 0)
|
|
1765
|
+
found.push(`${field}=${text6 ?? ""} is not a duration (0 or <n>s|m|h|d)`);
|
|
1766
|
+
}
|
|
1767
|
+
const ttl = seconds2(props.tokenTtl);
|
|
1768
|
+
const max = seconds2(props.tokenMaxTtl);
|
|
1769
|
+
if (ttl > 0 && max > 0 && ttl > max)
|
|
1770
|
+
found.push("tokenTtl is longer than tokenMaxTtl");
|
|
1771
|
+
if (props.tokenType === "batch" && (seconds2(props.tokenPeriod) > 0 || (props.tokenNumUses ?? 0) > 0)) {
|
|
1772
|
+
found.push("a batch token cannot be periodic or use-limited (tokenutil.go:234-241)");
|
|
1773
|
+
}
|
|
1774
|
+
return found;
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
// src/openbao/jwt-role-form.ts
|
|
1778
|
+
var sortedSet3 = (values) => [...new Set(values)].sort();
|
|
1779
|
+
var text6 = (value) => typeof value === "string" ? value : "";
|
|
1780
|
+
var strings4 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1781
|
+
var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1782
|
+
var claimsOf = (value) => {
|
|
1783
|
+
const source = record3(value);
|
|
1784
|
+
const out = {};
|
|
1785
|
+
for (const key of Object.keys(source).sort()) {
|
|
1786
|
+
const raw = source[key];
|
|
1787
|
+
out[key] = sortedSet3(typeof raw === "string" ? [raw] : strings4(raw));
|
|
1788
|
+
}
|
|
1789
|
+
return out;
|
|
1790
|
+
};
|
|
1791
|
+
var mappingsOf = (value) => {
|
|
1792
|
+
const source = record3(value);
|
|
1793
|
+
const out = {};
|
|
1794
|
+
for (const key of Object.keys(source).sort())
|
|
1795
|
+
out[key] = text6(source[key]);
|
|
1796
|
+
return out;
|
|
1797
|
+
};
|
|
1798
|
+
var canonical3 = (input) => ({
|
|
1799
|
+
allowedRedirectUris: sortedSet3(input.allowedRedirectUris),
|
|
1800
|
+
boundAudiences: sortedSet3(input.boundAudiences),
|
|
1801
|
+
boundClaims: claimsOf(input.boundClaims),
|
|
1802
|
+
boundClaimsType: input.boundClaimsType,
|
|
1803
|
+
boundSubject: input.boundSubject,
|
|
1804
|
+
callbackMode: input.callbackMode,
|
|
1805
|
+
claimMappings: mappingsOf(input.claimMappings),
|
|
1806
|
+
groupsClaim: input.groupsClaim,
|
|
1807
|
+
oidcDisableConfirmation: input.oidcDisableConfirmation,
|
|
1808
|
+
oidcScopes: sortedSet3(input.oidcScopes),
|
|
1809
|
+
roleType: input.roleType,
|
|
1810
|
+
userClaim: input.userClaim,
|
|
1811
|
+
verboseOidcLogging: input.verboseOidcLogging,
|
|
1812
|
+
...tokenFields(input)
|
|
1813
|
+
});
|
|
1814
|
+
var canonicalFromProps2 = (props) => canonical3({
|
|
1815
|
+
...tokenFormOfProps(props),
|
|
1816
|
+
allowedRedirectUris: props.allowedRedirectUris ?? [],
|
|
1817
|
+
boundAudiences: props.boundAudiences ?? [],
|
|
1818
|
+
boundClaims: claimsOf(props.boundClaims ?? {}),
|
|
1819
|
+
boundClaimsType: props.boundClaimsType ?? "string",
|
|
1820
|
+
boundSubject: props.boundSubject ?? "",
|
|
1821
|
+
callbackMode: props.callbackMode ?? "client",
|
|
1822
|
+
claimMappings: props.claimMappings ?? {},
|
|
1823
|
+
groupsClaim: props.groupsClaim ?? "",
|
|
1824
|
+
oidcDisableConfirmation: false,
|
|
1825
|
+
oidcScopes: props.oidcScopes ?? [],
|
|
1826
|
+
roleType: props.roleType,
|
|
1827
|
+
userClaim: props.userClaim,
|
|
1828
|
+
verboseOidcLogging: false
|
|
1829
|
+
});
|
|
1830
|
+
var canonicalFromLive2 = (live) => canonical3({
|
|
1831
|
+
...tokenFormOfLive(live),
|
|
1832
|
+
allowedRedirectUris: strings4(live["allowed_redirect_uris"]),
|
|
1833
|
+
boundAudiences: strings4(live["bound_audiences"]),
|
|
1834
|
+
boundClaims: claimsOf(live["bound_claims"]),
|
|
1835
|
+
boundClaimsType: text6(live["bound_claims_type"]),
|
|
1836
|
+
boundSubject: text6(live["bound_subject"]),
|
|
1837
|
+
callbackMode: text6(live["callback_mode"]),
|
|
1838
|
+
claimMappings: mappingsOf(live["claim_mappings"]),
|
|
1839
|
+
groupsClaim: text6(live["groups_claim"]),
|
|
1840
|
+
oidcDisableConfirmation: live["oidc_disable_confirmation"] === true,
|
|
1841
|
+
oidcScopes: strings4(live["oidc_scopes"]),
|
|
1842
|
+
roleType: text6(live["role_type"]),
|
|
1843
|
+
userClaim: text6(live["user_claim"]),
|
|
1844
|
+
verboseOidcLogging: live["verbose_oidc_logging"] === true
|
|
1845
|
+
});
|
|
1846
|
+
var mountOf2 = (props) => mountPath(props.mount ?? "jwt");
|
|
1847
|
+
var rolePath2 = (props) => `auth/${mountOf2(props)}/role/${props.name}`;
|
|
1848
|
+
var attributesOf6 = (props, live) => {
|
|
1849
|
+
const form = canonicalFromLive2(live);
|
|
1850
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf2(props), name: props.name };
|
|
1851
|
+
};
|
|
1852
|
+
var matches5 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps2(props)));
|
|
1853
|
+
var writeBody4 = (props) => ({
|
|
1854
|
+
allowed_redirect_uris: [...props.allowedRedirectUris ?? []],
|
|
1855
|
+
bound_audiences: [...props.boundAudiences ?? []],
|
|
1856
|
+
bound_claims: { ...props.boundClaims },
|
|
1857
|
+
bound_claims_type: props.boundClaimsType ?? "string",
|
|
1858
|
+
bound_subject: props.boundSubject ?? "",
|
|
1859
|
+
callback_mode: props.callbackMode ?? "client",
|
|
1860
|
+
claim_mappings: { ...props.claimMappings },
|
|
1861
|
+
groups_claim: props.groupsClaim ?? "",
|
|
1862
|
+
oidc_disable_confirmation: false,
|
|
1863
|
+
oidc_scopes: [...props.oidcScopes ?? []],
|
|
1864
|
+
role_type: props.roleType,
|
|
1865
|
+
user_claim: props.userClaim,
|
|
1866
|
+
verbose_oidc_logging: false,
|
|
1867
|
+
...tokenBody(props)
|
|
1868
|
+
});
|
|
1869
|
+
var NAME2 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
|
|
1870
|
+
var problems2 = (props) => {
|
|
1871
|
+
const found = [...tokenProblems(props)];
|
|
1872
|
+
if (!NAME2.test(props.name))
|
|
1873
|
+
found.push(`name \`${props.name}\` must be lower case (the server folds it)`);
|
|
1874
|
+
if (props.userClaim.trim() === "")
|
|
1875
|
+
found.push("userClaim is empty; the server requires one");
|
|
1876
|
+
const bound = (props.boundAudiences?.length ?? 0) > 0 || (props.boundSubject ?? "") !== "" || Object.keys(props.boundClaims ?? {}).length > 0 || (props.tokenBoundCidrs?.length ?? 0) > 0;
|
|
1877
|
+
if (props.roleType === "jwt" && !bound) {
|
|
1878
|
+
found.push("a jwt role needs a bound constraint: boundAudiences, boundSubject, boundClaims or tokenBoundCidrs");
|
|
1879
|
+
}
|
|
1880
|
+
if (props.roleType === "oidc" && (props.allowedRedirectUris?.length ?? 0) === 0) {
|
|
1881
|
+
found.push("an oidc role needs allowedRedirectUris");
|
|
1882
|
+
}
|
|
1883
|
+
const targets = Object.values(props.claimMappings ?? {});
|
|
1884
|
+
if (targets.includes("role"))
|
|
1885
|
+
found.push("claimMappings may not map to the reserved key `role`");
|
|
1886
|
+
if (new Set(targets).size !== targets.length)
|
|
1887
|
+
found.push("claimMappings maps two claims to one key");
|
|
1888
|
+
return found;
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
// src/openbao/jwt-role.ts
|
|
1892
|
+
var BaoJwtRole = Resource5("Bao.JwtRole", {
|
|
1893
|
+
defaultRemovalPolicy: "retain"
|
|
1894
|
+
});
|
|
1895
|
+
var jwtRoleSpec = (props) => ({
|
|
1896
|
+
attributesOf: (live) => attributesOf6(props, live),
|
|
1897
|
+
body: writeBody4(props),
|
|
1898
|
+
family: "Bao.JwtRole",
|
|
1899
|
+
matches: (attributes) => matches5(attributes, props),
|
|
1900
|
+
path: rolePath2(props),
|
|
1901
|
+
problems: problems2(props)
|
|
1902
|
+
});
|
|
1903
|
+
var BaoJwtRoleProvider = () => Provider5.effect(BaoJwtRole, Effect16.succeed(BaoJwtRole.Provider.of({
|
|
1904
|
+
list: () => Effect16.succeed([]),
|
|
1905
|
+
read: Effect16.fn(function* ({ olds }) {
|
|
1906
|
+
const found = yield* readRoleAt(jwtRoleSpec(olds));
|
|
1907
|
+
return found?.attributes;
|
|
1908
|
+
}),
|
|
1909
|
+
diff: Effect16.fn(function* ({ news, output }) {
|
|
1910
|
+
if (output === undefined || !isResolved5(news))
|
|
1911
|
+
return;
|
|
1912
|
+
if (mountOf2(news) !== output.mount || news.name !== output.name) {
|
|
1913
|
+
return { action: "replace" };
|
|
1914
|
+
}
|
|
1915
|
+
return { action: yield* planRole(jwtRoleSpec(news)) };
|
|
1916
|
+
}),
|
|
1917
|
+
reconcile: Effect16.fn(function* ({ news }) {
|
|
1918
|
+
return yield* reconcileRole(jwtRoleSpec(news));
|
|
1919
|
+
}),
|
|
1920
|
+
delete: Effect16.fn(function* ({ output }) {
|
|
1921
|
+
yield* baoDelete(rolePath2(output));
|
|
1922
|
+
return;
|
|
1923
|
+
})
|
|
1924
|
+
})));
|
|
1925
|
+
// src/openbao/kubernetes-role.ts
|
|
1926
|
+
import { Resource as Resource6 } from "alchemy";
|
|
1927
|
+
import { isResolved as isResolved6 } from "alchemy/Diff";
|
|
1928
|
+
import * as Provider6 from "alchemy/Provider";
|
|
1929
|
+
import * as Effect17 from "effect/Effect";
|
|
1930
|
+
|
|
1931
|
+
// src/openbao/kubernetes-role-form.ts
|
|
1932
|
+
var sortedSet4 = (values) => [...new Set(values)].sort();
|
|
1933
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
1934
|
+
var strings5 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1935
|
+
var canonical4 = (input) => ({
|
|
1936
|
+
aliasNameSource: input.aliasNameSource,
|
|
1937
|
+
audience: input.audience,
|
|
1938
|
+
boundServiceAccountNames: sortedSet4(input.boundServiceAccountNames),
|
|
1939
|
+
boundServiceAccountNamespaceSelector: input.boundServiceAccountNamespaceSelector.trim(),
|
|
1940
|
+
boundServiceAccountNamespaces: sortedSet4(input.boundServiceAccountNamespaces),
|
|
1941
|
+
...tokenFields(input)
|
|
1942
|
+
});
|
|
1943
|
+
var canonicalFromProps3 = (props) => canonical4({
|
|
1944
|
+
...tokenFormOfProps(props),
|
|
1945
|
+
aliasNameSource: props.aliasNameSource,
|
|
1946
|
+
audience: props.audience ?? "",
|
|
1947
|
+
boundServiceAccountNames: props.boundServiceAccountNames,
|
|
1948
|
+
boundServiceAccountNamespaceSelector: props.boundServiceAccountNamespaceSelector ?? "",
|
|
1949
|
+
boundServiceAccountNamespaces: props.boundServiceAccountNamespaces ?? []
|
|
1950
|
+
});
|
|
1951
|
+
var canonicalFromLive3 = (live) => canonical4({
|
|
1952
|
+
...tokenFormOfLive(live),
|
|
1953
|
+
aliasNameSource: text7(live["alias_name_source"]),
|
|
1954
|
+
audience: text7(live["audience"]),
|
|
1955
|
+
boundServiceAccountNames: strings5(live["bound_service_account_names"]),
|
|
1956
|
+
boundServiceAccountNamespaceSelector: text7(live["bound_service_account_namespace_selector"]),
|
|
1957
|
+
boundServiceAccountNamespaces: strings5(live["bound_service_account_namespaces"])
|
|
1958
|
+
});
|
|
1959
|
+
var mountOf3 = (props) => mountPath(props.mount ?? "kubernetes");
|
|
1960
|
+
var rolePath3 = (props) => `auth/${mountOf3(props)}/role/${props.name}`;
|
|
1961
|
+
var attributesOf7 = (props, live) => {
|
|
1962
|
+
const form = canonicalFromLive3(live);
|
|
1963
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf3(props), name: props.name };
|
|
1964
|
+
};
|
|
1965
|
+
var matches6 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps3(props)));
|
|
1966
|
+
var writeBody5 = (props) => ({
|
|
1967
|
+
alias_name_source: props.aliasNameSource,
|
|
1968
|
+
audience: props.audience ?? "",
|
|
1969
|
+
bound_service_account_names: [...props.boundServiceAccountNames],
|
|
1970
|
+
bound_service_account_namespace_selector: props.boundServiceAccountNamespaceSelector ?? "",
|
|
1971
|
+
bound_service_account_namespaces: [...props.boundServiceAccountNamespaces ?? []],
|
|
1972
|
+
...tokenBody(props)
|
|
1973
|
+
});
|
|
1974
|
+
var NAME3 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
|
|
1975
|
+
var starMixed = (values) => values.length > 1 && values.includes("*");
|
|
1976
|
+
var problems3 = (props) => {
|
|
1977
|
+
const found = [...tokenProblems(props)];
|
|
1978
|
+
const namespaces = props.boundServiceAccountNamespaces ?? [];
|
|
1979
|
+
const selector = (props.boundServiceAccountNamespaceSelector ?? "").trim();
|
|
1980
|
+
if (!NAME3.test(props.name))
|
|
1981
|
+
found.push(`name \`${props.name}\` must be lower case (stored folded)`);
|
|
1982
|
+
if (props.boundServiceAccountNames.length === 0)
|
|
1983
|
+
found.push("boundServiceAccountNames is empty");
|
|
1984
|
+
if (starMixed(props.boundServiceAccountNames))
|
|
1985
|
+
found.push("`*` mixed with other account names");
|
|
1986
|
+
if (namespaces.length === 0 && selector === "") {
|
|
1987
|
+
found.push("boundServiceAccountNamespaces is empty and no namespace selector is set");
|
|
1988
|
+
}
|
|
1989
|
+
if (starMixed(namespaces))
|
|
1990
|
+
found.push("`*` mixed with other namespaces");
|
|
1991
|
+
return found;
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
// src/openbao/kubernetes-role.ts
|
|
1995
|
+
var BaoKubernetesRole = Resource6("Bao.KubernetesRole", {
|
|
1996
|
+
defaultRemovalPolicy: "retain"
|
|
1997
|
+
});
|
|
1998
|
+
var kubernetesRoleSpec = (props) => ({
|
|
1999
|
+
attributesOf: (live) => attributesOf7(props, live),
|
|
2000
|
+
body: writeBody5(props),
|
|
2001
|
+
family: "Bao.KubernetesRole",
|
|
2002
|
+
matches: (attributes) => matches6(attributes, props),
|
|
2003
|
+
path: rolePath3(props),
|
|
2004
|
+
problems: problems3(props)
|
|
2005
|
+
});
|
|
2006
|
+
var BaoKubernetesRoleProvider = () => Provider6.effect(BaoKubernetesRole, Effect17.succeed(BaoKubernetesRole.Provider.of({
|
|
2007
|
+
list: () => Effect17.succeed([]),
|
|
2008
|
+
read: Effect17.fn(function* ({ olds }) {
|
|
2009
|
+
const found = yield* readRoleAt(kubernetesRoleSpec(olds));
|
|
2010
|
+
return found?.attributes;
|
|
2011
|
+
}),
|
|
2012
|
+
diff: Effect17.fn(function* ({ news, output }) {
|
|
2013
|
+
if (output === undefined || !isResolved6(news))
|
|
2014
|
+
return;
|
|
2015
|
+
if (mountOf3(news) !== output.mount || news.name !== output.name) {
|
|
2016
|
+
return { action: "replace" };
|
|
2017
|
+
}
|
|
2018
|
+
return { action: yield* planRole(kubernetesRoleSpec(news)) };
|
|
2019
|
+
}),
|
|
2020
|
+
reconcile: Effect17.fn(function* ({ news }) {
|
|
2021
|
+
return yield* reconcileRole(kubernetesRoleSpec(news));
|
|
2022
|
+
}),
|
|
2023
|
+
delete: Effect17.fn(function* ({ output }) {
|
|
2024
|
+
yield* baoDelete(rolePath3(output));
|
|
2025
|
+
return;
|
|
2026
|
+
})
|
|
2027
|
+
})));
|
|
2028
|
+
// src/openbao/mfa-enforcement.ts
|
|
2029
|
+
import { Resource as Resource7 } from "alchemy";
|
|
2030
|
+
import { isResolved as isResolved7 } from "alchemy/Diff";
|
|
2031
|
+
import * as Provider7 from "alchemy/Provider";
|
|
2032
|
+
import * as Effect19 from "effect/Effect";
|
|
2033
|
+
|
|
2034
|
+
// src/openbao/mfa-enforcement-form.ts
|
|
2035
|
+
var sortedSet5 = (values) => [...new Set(values.map((value) => value.trim()))].filter((value) => value !== "").sort();
|
|
2036
|
+
var strings6 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
2037
|
+
var text8 = (value) => typeof value === "string" ? value : "";
|
|
2038
|
+
var canonical5 = (input) => ({
|
|
2039
|
+
authMethodAccessors: sortedSet5(input.authMethodAccessors),
|
|
2040
|
+
authMethodTypes: sortedSet5(input.authMethodTypes),
|
|
2041
|
+
identityEntityIds: sortedSet5(input.identityEntityIds),
|
|
2042
|
+
identityGroupIds: sortedSet5(input.identityGroupIds),
|
|
2043
|
+
mfaMethodIds: sortedSet5(input.mfaMethodIds)
|
|
2044
|
+
});
|
|
2045
|
+
var canonicalFromProps4 = (props, resolved) => canonical5({
|
|
2046
|
+
authMethodAccessors: [...props.authMethodAccessors ?? [], ...resolved],
|
|
2047
|
+
authMethodTypes: props.authMethodTypes ?? [],
|
|
2048
|
+
identityEntityIds: props.identityEntityIds ?? [],
|
|
2049
|
+
identityGroupIds: props.identityGroupIds ?? [],
|
|
2050
|
+
mfaMethodIds: props.mfaMethodIds
|
|
2051
|
+
});
|
|
2052
|
+
var canonicalFromLive4 = (live) => canonical5({
|
|
2053
|
+
authMethodAccessors: strings6(live["auth_method_accessors"]),
|
|
2054
|
+
authMethodTypes: strings6(live["auth_method_types"]),
|
|
2055
|
+
identityEntityIds: strings6(live["identity_entity_ids"]),
|
|
2056
|
+
identityGroupIds: strings6(live["identity_group_ids"]),
|
|
2057
|
+
mfaMethodIds: strings6(live["mfa_method_ids"])
|
|
2058
|
+
});
|
|
2059
|
+
var attributesOf8 = (name, live) => {
|
|
2060
|
+
const form = canonicalFromLive4(live);
|
|
2061
|
+
return {
|
|
2062
|
+
...form,
|
|
2063
|
+
digest: sha256(JSON.stringify(form)),
|
|
2064
|
+
enforcementId: text8(live["id"]),
|
|
2065
|
+
name,
|
|
2066
|
+
namespacePath: text8(live["namespace_path"])
|
|
2067
|
+
};
|
|
2068
|
+
};
|
|
2069
|
+
var matches7 = (attributes, props, resolved) => attributes.digest === sha256(JSON.stringify(canonicalFromProps4(props, resolved)));
|
|
2070
|
+
var writeBody6 = (form) => ({
|
|
2071
|
+
auth_method_accessors: [...form.authMethodAccessors],
|
|
2072
|
+
auth_method_types: [...form.authMethodTypes],
|
|
2073
|
+
identity_entity_ids: [...form.identityEntityIds],
|
|
2074
|
+
identity_group_ids: [...form.identityGroupIds],
|
|
2075
|
+
mfa_method_ids: [...form.mfaMethodIds]
|
|
2076
|
+
});
|
|
2077
|
+
var NAME4 = /^\w(?:[\w.-]*\w)?$/;
|
|
2078
|
+
var problems4 = (props, missingPaths) => {
|
|
2079
|
+
const found = [];
|
|
2080
|
+
if (!NAME4.test(props.name))
|
|
2081
|
+
found.push(`name \`${props.name}\` is not a valid enforcement name`);
|
|
2082
|
+
if (sortedSet5(props.mfaMethodIds).length === 0)
|
|
2083
|
+
found.push("mfaMethodIds is empty");
|
|
2084
|
+
const targets = [
|
|
2085
|
+
props.authMethodPaths,
|
|
2086
|
+
props.authMethodAccessors,
|
|
2087
|
+
props.authMethodTypes,
|
|
2088
|
+
props.identityGroupIds,
|
|
2089
|
+
props.identityEntityIds
|
|
2090
|
+
].some((list) => sortedSet5(list ?? []).length > 0);
|
|
2091
|
+
if (!targets)
|
|
2092
|
+
found.push("no target: an enforcement with none is accepted and enforces NOTHING");
|
|
2093
|
+
if (missingPaths.length > 0) {
|
|
2094
|
+
found.push(`no auth mount at ${missingPaths.map((path) => mountPath(path)).join(", ")}`);
|
|
2095
|
+
}
|
|
2096
|
+
return found;
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
// src/openbao/mfa-wire.ts
|
|
2100
|
+
import * as Effect18 from "effect/Effect";
|
|
2101
|
+
var TOTP_PATH = "identity/mfa/method/totp";
|
|
2102
|
+
var METHODS_PATH = "identity/mfa/method";
|
|
2103
|
+
var enforcementPath = (name) => `identity/mfa/login-enforcement/${name}`;
|
|
2104
|
+
var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
2105
|
+
var ownNamespacePath = Effect18.gen(function* () {
|
|
2106
|
+
const env = yield* BaoEnv;
|
|
2107
|
+
return namespacePath(envNamespace(env));
|
|
2108
|
+
});
|
|
2109
|
+
var findTotp = (name) => Effect18.gen(function* () {
|
|
2110
|
+
const listing = yield* baoRead(`${METHODS_PATH}?list=true`);
|
|
2111
|
+
const info = record4(listing?.["key_info"]) ?? {};
|
|
2112
|
+
const own = yield* ownNamespacePath;
|
|
2113
|
+
for (const [id, raw] of Object.entries(info)) {
|
|
2114
|
+
const live = record4(raw);
|
|
2115
|
+
if (live === undefined || live["name"] !== name || live["namespace_path"] !== own)
|
|
2116
|
+
continue;
|
|
2117
|
+
if (live["type"] !== "totp") {
|
|
2118
|
+
return yield* Effect18.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.`));
|
|
2119
|
+
}
|
|
2120
|
+
return { id, live };
|
|
2121
|
+
}
|
|
2122
|
+
return;
|
|
2123
|
+
});
|
|
2124
|
+
var writeTotp = (body) => Effect18.asVoid(baoCall("write", "POST", TOTP_PATH, body));
|
|
2125
|
+
var deleteTotp = (id) => baoDelete(`${TOTP_PATH}/${id}`);
|
|
2126
|
+
var readEnforcement = (name) => baoRead(enforcementPath(name));
|
|
2127
|
+
var writeEnforcement = (name, body) => Effect18.asVoid(baoCall("write", "POST", enforcementPath(name), body));
|
|
2128
|
+
var authAccessors = (paths) => Effect18.gen(function* () {
|
|
2129
|
+
if (paths.length === 0)
|
|
2130
|
+
return { accessors: [], missing: [] };
|
|
2131
|
+
const table = (yield* baoRead("sys/auth")) ?? {};
|
|
2132
|
+
const accessors = [];
|
|
2133
|
+
const missing = [];
|
|
2134
|
+
for (const path of paths) {
|
|
2135
|
+
const accessor = record4(table[`${mountPath(path)}/`])?.["accessor"];
|
|
2136
|
+
if (typeof accessor === "string" && accessor !== "")
|
|
2137
|
+
accessors.push(accessor);
|
|
2138
|
+
else
|
|
2139
|
+
missing.push(mountPath(path));
|
|
2140
|
+
}
|
|
2141
|
+
return { accessors, missing };
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
// src/openbao/mfa-enforcement.ts
|
|
2145
|
+
var BaoMfaLoginEnforcement = Resource7("Bao.MfaLoginEnforcement", {
|
|
2146
|
+
defaultRemovalPolicy: "retain"
|
|
2147
|
+
});
|
|
2148
|
+
var refuse3 = (name, message) => Effect19.die(new Error(`Bao.MfaLoginEnforcement ${name}: ${message}`));
|
|
2149
|
+
var resolveTargets = (props) => Effect19.map(authAccessors(props.authMethodPaths ?? []), ({ accessors, missing }) => ({
|
|
2150
|
+
bad: problems4(props, missing),
|
|
2151
|
+
resolved: accessors
|
|
2152
|
+
}));
|
|
2153
|
+
var readLive = (name) => Effect19.map(readEnforcement(name), (live) => live === undefined ? undefined : attributesOf8(name, live));
|
|
2154
|
+
var planEnforcement = (props) => Effect19.gen(function* () {
|
|
2155
|
+
const { bad, resolved } = yield* resolveTargets(props);
|
|
2156
|
+
if (bad.length > 0)
|
|
2157
|
+
return "update";
|
|
2158
|
+
const live = yield* readLive(props.name);
|
|
2159
|
+
return live !== undefined && matches7(live, props, resolved) ? "noop" : "update";
|
|
2160
|
+
});
|
|
2161
|
+
var reconcileEnforcement = (props) => Effect19.gen(function* () {
|
|
2162
|
+
const { bad, resolved } = yield* resolveTargets(props);
|
|
2163
|
+
if (bad.length > 0)
|
|
2164
|
+
return yield* refuse3(props.name, bad.join("; "));
|
|
2165
|
+
const live = yield* readLive(props.name);
|
|
2166
|
+
if (live === undefined || !matches7(live, props, resolved)) {
|
|
2167
|
+
yield* writeEnforcement(props.name, writeBody6(canonicalFromProps4(props, resolved)));
|
|
2168
|
+
}
|
|
2169
|
+
const after = yield* readLive(props.name);
|
|
2170
|
+
if (after === undefined)
|
|
2171
|
+
return yield* refuse3(props.name, "the write succeeded but it is absent.");
|
|
2172
|
+
if (!matches7(after, props, resolved)) {
|
|
2173
|
+
return yield* refuse3(props.name, "the enforcement read back different.");
|
|
2174
|
+
}
|
|
2175
|
+
return after;
|
|
2176
|
+
});
|
|
2177
|
+
var BaoMfaLoginEnforcementProvider = () => Provider7.effect(BaoMfaLoginEnforcement, Effect19.succeed(BaoMfaLoginEnforcement.Provider.of({
|
|
2178
|
+
list: () => Effect19.succeed([]),
|
|
2179
|
+
read: Effect19.fn(function* ({ olds }) {
|
|
2180
|
+
return yield* readLive(olds.name);
|
|
2181
|
+
}),
|
|
2182
|
+
diff: Effect19.fn(function* ({ news, output }) {
|
|
2183
|
+
if (output === undefined || !isResolved7(news))
|
|
2184
|
+
return;
|
|
2185
|
+
if (news.name !== output.name)
|
|
2186
|
+
return { action: "replace" };
|
|
2187
|
+
return { action: yield* planEnforcement(news) };
|
|
2188
|
+
}),
|
|
2189
|
+
reconcile: Effect19.fn(function* ({ news }) {
|
|
2190
|
+
return yield* reconcileEnforcement(news);
|
|
2191
|
+
}),
|
|
2192
|
+
delete: Effect19.fn(function* ({ output }) {
|
|
2193
|
+
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.");
|
|
2194
|
+
})
|
|
2195
|
+
})));
|
|
2196
|
+
// src/openbao/mfa-totp.ts
|
|
2197
|
+
import { Resource as Resource8 } from "alchemy";
|
|
2198
|
+
import { isResolved as isResolved8 } from "alchemy/Diff";
|
|
2199
|
+
import * as Provider8 from "alchemy/Provider";
|
|
2200
|
+
import * as Effect20 from "effect/Effect";
|
|
2201
|
+
|
|
2202
|
+
// src/openbao/mfa-totp-form.ts
|
|
2203
|
+
var canonical6 = (input) => ({
|
|
2204
|
+
algorithm: input.algorithm,
|
|
2205
|
+
digits: input.digits,
|
|
2206
|
+
issuer: input.issuer,
|
|
2207
|
+
keySize: input.keySize,
|
|
2208
|
+
maxValidationAttempts: input.maxValidationAttempts,
|
|
2209
|
+
periodSeconds: input.periodSeconds,
|
|
2210
|
+
qrSize: input.qrSize,
|
|
2211
|
+
skew: input.skew
|
|
2212
|
+
});
|
|
2213
|
+
var canonicalFromProps5 = (props) => canonical6({
|
|
2214
|
+
algorithm: props.algorithm ?? "SHA1",
|
|
2215
|
+
digits: props.digits ?? 6,
|
|
2216
|
+
issuer: props.issuer,
|
|
2217
|
+
keySize: props.keySize ?? 20,
|
|
2218
|
+
maxValidationAttempts: props.maxValidationAttempts ?? 5,
|
|
2219
|
+
periodSeconds: parseDuration(props.period ?? "30s") ?? -1,
|
|
2220
|
+
qrSize: props.qrSize ?? 200,
|
|
2221
|
+
skew: props.skew ?? 1
|
|
2222
|
+
});
|
|
2223
|
+
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : -1;
|
|
2224
|
+
var text9 = (value) => typeof value === "string" ? value : "";
|
|
2225
|
+
var canonicalFromLive5 = (live) => canonical6({
|
|
2226
|
+
algorithm: text9(live["algorithm"]),
|
|
2227
|
+
digits: num(live["digits"]),
|
|
2228
|
+
issuer: text9(live["issuer"]),
|
|
2229
|
+
keySize: num(live["key_size"]),
|
|
2230
|
+
maxValidationAttempts: num(live["max_validation_attempts"]),
|
|
2231
|
+
periodSeconds: ttlSeconds(live["period"]) ?? -1,
|
|
2232
|
+
qrSize: num(live["qr_size"]),
|
|
2233
|
+
skew: num(live["skew"])
|
|
2234
|
+
});
|
|
2235
|
+
var attributesOf9 = (methodId, live) => {
|
|
2236
|
+
const form = canonicalFromLive5(live);
|
|
2237
|
+
return {
|
|
2238
|
+
...form,
|
|
2239
|
+
digest: sha256(JSON.stringify(form)),
|
|
2240
|
+
methodId,
|
|
2241
|
+
name: text9(live["name"]),
|
|
2242
|
+
namespacePath: text9(live["namespace_path"])
|
|
2243
|
+
};
|
|
2244
|
+
};
|
|
2245
|
+
var matches8 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps5(props)));
|
|
2246
|
+
var writeBody7 = (props) => {
|
|
2247
|
+
const form = canonicalFromProps5(props);
|
|
2248
|
+
return {
|
|
2249
|
+
algorithm: form.algorithm,
|
|
2250
|
+
digits: form.digits,
|
|
2251
|
+
issuer: form.issuer,
|
|
2252
|
+
key_size: form.keySize,
|
|
2253
|
+
max_validation_attempts: form.maxValidationAttempts,
|
|
2254
|
+
method_name: props.name,
|
|
2255
|
+
period: props.period ?? "30s",
|
|
2256
|
+
qr_size: form.qrSize,
|
|
2257
|
+
skew: form.skew
|
|
2258
|
+
};
|
|
2259
|
+
};
|
|
2260
|
+
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).";
|
|
2261
|
+
var problems5 = (props) => {
|
|
2262
|
+
const found = [];
|
|
2263
|
+
const form = canonicalFromProps5(props);
|
|
2264
|
+
if (props.name.trim() === "")
|
|
2265
|
+
found.push("name is empty; it is the only key a declaration has");
|
|
2266
|
+
if (props.issuer.trim() === "")
|
|
2267
|
+
found.push("issuer is empty; the server requires one");
|
|
2268
|
+
if (form.periodSeconds <= 0)
|
|
2269
|
+
found.push(`period \`${props.period ?? ""}\` must be a duration above zero`);
|
|
2270
|
+
if (form.keySize <= 0)
|
|
2271
|
+
found.push("keySize must be above zero");
|
|
2272
|
+
if (form.maxValidationAttempts < 1)
|
|
2273
|
+
found.push("maxValidationAttempts must be at least 1 (0 reads back as 5)");
|
|
2274
|
+
return found;
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
// src/openbao/mfa-totp.ts
|
|
2278
|
+
var BaoMfaTotpMethod = Resource8("Bao.MfaTotpMethod", {
|
|
2279
|
+
defaultRemovalPolicy: "retain"
|
|
2280
|
+
});
|
|
2281
|
+
var refuse4 = (props, message) => Effect20.die(new Error(`Bao.MfaTotpMethod ${props.name}: ${message}`));
|
|
2282
|
+
var readTotp = (props) => Effect20.map(findTotp(props.name), (found) => found === undefined ? undefined : attributesOf9(found.id, found.live));
|
|
2283
|
+
var planTotp = (props) => Effect20.gen(function* () {
|
|
2284
|
+
if (problems5(props).length > 0)
|
|
2285
|
+
return "update";
|
|
2286
|
+
const live = yield* readTotp(props);
|
|
2287
|
+
return live !== undefined && matches8(live, props) ? "noop" : "update";
|
|
2288
|
+
});
|
|
2289
|
+
var reconcileTotp = (props) => Effect20.gen(function* () {
|
|
2290
|
+
const bad = problems5(props);
|
|
2291
|
+
if (bad.length > 0)
|
|
2292
|
+
return yield* refuse4(props, bad.join("; "));
|
|
2293
|
+
const live = yield* readTotp(props);
|
|
2294
|
+
if (live === undefined || !matches8(live, props))
|
|
2295
|
+
yield* writeTotp(writeBody7(props));
|
|
2296
|
+
const after = yield* readTotp(props);
|
|
2297
|
+
if (after === undefined)
|
|
2298
|
+
return yield* refuse4(props, "the write succeeded but no method has that name.");
|
|
2299
|
+
if (!matches8(after, props))
|
|
2300
|
+
return yield* refuse4(props, "the method read back different.");
|
|
2301
|
+
return after;
|
|
2302
|
+
});
|
|
2303
|
+
var BaoMfaTotpMethodProvider = () => Provider8.effect(BaoMfaTotpMethod, Effect20.succeed(BaoMfaTotpMethod.Provider.of({
|
|
2304
|
+
list: () => Effect20.succeed([]),
|
|
2305
|
+
read: Effect20.fn(function* ({ olds }) {
|
|
2306
|
+
return yield* readTotp(olds);
|
|
2307
|
+
}),
|
|
2308
|
+
diff: Effect20.fn(function* ({ news, output }) {
|
|
2309
|
+
if (output === undefined || !isResolved8(news))
|
|
2310
|
+
return;
|
|
2311
|
+
const rename = renameProblem(output.name, news.name);
|
|
2312
|
+
if (rename !== undefined)
|
|
2313
|
+
return yield* refuse4(news, rename);
|
|
2314
|
+
return { action: yield* planTotp(news) };
|
|
2315
|
+
}),
|
|
2316
|
+
reconcile: Effect20.fn(function* ({ news }) {
|
|
2317
|
+
return yield* reconcileTotp(news);
|
|
2318
|
+
}),
|
|
2319
|
+
delete: Effect20.fn(function* ({ output }) {
|
|
2320
|
+
yield* deleteTotp(output.methodId);
|
|
2321
|
+
return;
|
|
2322
|
+
})
|
|
2323
|
+
})));
|
|
2324
|
+
// src/openbao/mount.ts
|
|
2325
|
+
import { Resource as Resource9 } from "alchemy";
|
|
2326
|
+
import { isResolved as isResolved9 } from "alchemy/Diff";
|
|
2327
|
+
import * as Provider9 from "alchemy/Provider";
|
|
2328
|
+
import * as Effect23 from "effect/Effect";
|
|
2329
|
+
|
|
2330
|
+
// src/openbao/mount-reconcile.ts
|
|
2331
|
+
import * as Effect22 from "effect/Effect";
|
|
1241
2332
|
|
|
1242
2333
|
// src/openbao/mount-wire.ts
|
|
1243
|
-
import * as
|
|
1244
|
-
var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(
|
|
2334
|
+
import * as Effect21 from "effect/Effect";
|
|
2335
|
+
var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect21.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
|
|
1245
2336
|
...asked.errors,
|
|
1246
2337
|
`sys/mounts could not confirm the mount is absent: ${listing.message}`
|
|
1247
|
-
])),
|
|
1248
|
-
var readMountData = (path) => baoRead(readPath(path)).pipe(
|
|
2338
|
+
])), Effect21.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect21.succeed(undefined) : Effect21.fail(asked)));
|
|
2339
|
+
var readMountData = (path) => baoRead(readPath(path)).pipe(Effect21.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
|
|
1249
2340
|
var enableMount = (props) => baoWrite("POST", readPath(props.path), enableBody(props));
|
|
1250
2341
|
var tuneMount = (props) => baoWrite("POST", tunePath(props.path), tuneBody(props));
|
|
1251
2342
|
var disableMount = (path) => baoDelete(readPath(path));
|
|
1252
2343
|
|
|
1253
|
-
// src/openbao/mount.ts
|
|
1254
|
-
var
|
|
1255
|
-
defaultRemovalPolicy: "retain"
|
|
1256
|
-
});
|
|
1257
|
-
var readMount = (props) => Effect10.gen(function* () {
|
|
2344
|
+
// src/openbao/mount-reconcile.ts
|
|
2345
|
+
var readMount = (props) => Effect22.gen(function* () {
|
|
1258
2346
|
const live = yield* readMountData(props.path);
|
|
1259
2347
|
if (live === undefined)
|
|
1260
2348
|
return;
|
|
1261
2349
|
return attributesOf(props, live);
|
|
1262
2350
|
});
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
|
|
2351
|
+
var moveTarget2 = (props) => ({
|
|
2352
|
+
family: "Bao.Mount",
|
|
2353
|
+
path: props.path,
|
|
2354
|
+
read: (path) => readMount({ ...props, path }),
|
|
2355
|
+
type: props.type,
|
|
2356
|
+
wire: mountPath
|
|
2357
|
+
});
|
|
2358
|
+
var reconcileMount = (news, stated, options) => Effect22.gen(function* () {
|
|
2359
|
+
const move = planMove("Bao.Mount", stated, news.path, news.remountFrom);
|
|
2360
|
+
if (move.kind === "refuse")
|
|
2361
|
+
return yield* Effect22.die(new Error(move.message));
|
|
2362
|
+
if (move.kind === "move")
|
|
2363
|
+
yield* performMove(moveTarget2(news), move.from, options);
|
|
2364
|
+
let live = yield* readMount(news);
|
|
2365
|
+
if (live === undefined) {
|
|
2366
|
+
yield* enableMount(news);
|
|
2367
|
+
if (wantsTune(news))
|
|
2368
|
+
yield* tuneMount(news);
|
|
2369
|
+
} else if (!matches(live, news)) {
|
|
2370
|
+
if (live.type !== news.type) {
|
|
2371
|
+
return yield* Effect22.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
|
|
2372
|
+
}
|
|
2373
|
+
yield* tuneMount(news);
|
|
2374
|
+
}
|
|
2375
|
+
live = yield* readMount(news);
|
|
2376
|
+
if (live === undefined) {
|
|
2377
|
+
return yield* Effect22.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
|
|
2378
|
+
}
|
|
2379
|
+
return live;
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
// src/openbao/mount.ts
|
|
2383
|
+
var BaoMount = Resource9("Bao.Mount", {
|
|
2384
|
+
defaultRemovalPolicy: "retain"
|
|
2385
|
+
});
|
|
2386
|
+
var BaoMountProvider = () => Provider9.effect(BaoMount, Effect23.succeed(BaoMount.Provider.of({
|
|
2387
|
+
list: () => Effect23.succeed([]),
|
|
2388
|
+
read: Effect23.fn(function* ({ olds }) {
|
|
1266
2389
|
return yield* readMount(olds);
|
|
1267
2390
|
}),
|
|
1268
|
-
diff:
|
|
1269
|
-
if (output === undefined || !
|
|
2391
|
+
diff: Effect23.fn(function* ({ news, output }) {
|
|
2392
|
+
if (output === undefined || !isResolved9(news))
|
|
1270
2393
|
return;
|
|
2394
|
+
const move = planMove("Bao.Mount", output.path, news.path, news.remountFrom);
|
|
2395
|
+
if (move.kind === "refuse")
|
|
2396
|
+
return yield* Effect23.die(new Error(move.message));
|
|
2397
|
+
if (move.kind === "move")
|
|
2398
|
+
return { action: "update" };
|
|
1271
2399
|
const live = yield* readMount(news);
|
|
1272
2400
|
if (live === undefined)
|
|
1273
2401
|
return { action: "update" };
|
|
@@ -1277,39 +2405,29 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect10.succeed(BaoMoun
|
|
|
1277
2405
|
return { action: "noop" };
|
|
1278
2406
|
return { action: "update" };
|
|
1279
2407
|
}),
|
|
1280
|
-
reconcile:
|
|
1281
|
-
|
|
1282
|
-
if (live === undefined) {
|
|
1283
|
-
yield* enableMount(news);
|
|
1284
|
-
if (wantsTune(news))
|
|
1285
|
-
yield* tuneMount(news);
|
|
1286
|
-
} else if (!matches(live, news)) {
|
|
1287
|
-
if (live.type !== news.type) {
|
|
1288
|
-
return yield* Effect10.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
|
|
1289
|
-
}
|
|
1290
|
-
yield* tuneMount(news);
|
|
1291
|
-
}
|
|
1292
|
-
live = yield* readMount(news);
|
|
1293
|
-
if (live === undefined) {
|
|
1294
|
-
return yield* Effect10.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
|
|
1295
|
-
}
|
|
1296
|
-
return live;
|
|
2408
|
+
reconcile: Effect23.fn(function* ({ news, output }) {
|
|
2409
|
+
return yield* reconcileMount(news, output?.path);
|
|
1297
2410
|
}),
|
|
1298
|
-
delete:
|
|
2411
|
+
delete: Effect23.fn(function* ({ output }) {
|
|
1299
2412
|
yield* disableMount(output.path);
|
|
1300
2413
|
return;
|
|
1301
2414
|
})
|
|
1302
2415
|
})));
|
|
1303
2416
|
// src/openbao/pki-role.ts
|
|
1304
|
-
import { Resource as
|
|
1305
|
-
import { isResolved as
|
|
1306
|
-
import * as
|
|
1307
|
-
import * as
|
|
2417
|
+
import { Resource as Resource10 } from "alchemy";
|
|
2418
|
+
import { isResolved as isResolved10 } from "alchemy/Diff";
|
|
2419
|
+
import * as Provider10 from "alchemy/Provider";
|
|
2420
|
+
import * as Effect24 from "effect/Effect";
|
|
1308
2421
|
|
|
1309
|
-
// src/openbao/pki-role-
|
|
2422
|
+
// src/openbao/pki-role-canonical.ts
|
|
2423
|
+
var DEFAULT_KEY_USAGE = [
|
|
2424
|
+
"DigitalSignature",
|
|
2425
|
+
"KeyAgreement",
|
|
2426
|
+
"KeyEncipherment"
|
|
2427
|
+
];
|
|
1310
2428
|
var sorted = (values) => [...values].sort();
|
|
1311
2429
|
var foldedUsages = (values) => sorted(values.map((v) => v.toLowerCase()));
|
|
1312
|
-
var
|
|
2430
|
+
var canonical7 = (input) => ({
|
|
1313
2431
|
allowBareDomains: input.allowBareDomains,
|
|
1314
2432
|
allowGlobDomains: input.allowGlobDomains,
|
|
1315
2433
|
allowIpSans: input.allowIpSans,
|
|
@@ -1317,18 +2435,24 @@ var canonical2 = (input) => ({
|
|
|
1317
2435
|
allowSubdomains: input.allowSubdomains,
|
|
1318
2436
|
allowWildcardCertificates: input.allowWildcardCertificates,
|
|
1319
2437
|
allowedDomains: sorted(input.allowedDomains),
|
|
2438
|
+
allowedDomainsTemplate: input.allowedDomainsTemplate,
|
|
1320
2439
|
clientFlag: input.clientFlag,
|
|
1321
2440
|
cnValidations: sorted(input.cnValidations),
|
|
2441
|
+
enforceHostnames: input.enforceHostnames,
|
|
1322
2442
|
extKeyUsage: foldedUsages(input.extKeyUsage),
|
|
2443
|
+
generateLease: input.generateLease,
|
|
1323
2444
|
keyBits: input.keyBits,
|
|
1324
2445
|
keyType: input.keyType,
|
|
2446
|
+
keyUsage: foldedUsages(input.keyUsage),
|
|
1325
2447
|
maxTtlSeconds: input.maxTtlSeconds,
|
|
2448
|
+
noStore: input.noStore,
|
|
2449
|
+
requireCn: input.requireCn,
|
|
1326
2450
|
serverFlag: input.serverFlag,
|
|
1327
2451
|
signatureBits: input.signatureBits,
|
|
1328
2452
|
ttlSeconds: input.ttlSeconds
|
|
1329
2453
|
});
|
|
1330
|
-
var
|
|
1331
|
-
var
|
|
2454
|
+
var seconds3 = (text10) => parseDuration(text10) ?? -1;
|
|
2455
|
+
var canonicalFromProps6 = (props) => canonical7({
|
|
1332
2456
|
allowBareDomains: props.allowBareDomains ?? false,
|
|
1333
2457
|
allowGlobDomains: props.allowGlobDomains ?? false,
|
|
1334
2458
|
allowIpSans: props.allowIpSans ?? false,
|
|
@@ -1336,21 +2460,27 @@ var canonicalFromProps = (props) => canonical2({
|
|
|
1336
2460
|
allowSubdomains: props.allowSubdomains ?? false,
|
|
1337
2461
|
allowWildcardCertificates: props.allowWildcardCertificates ?? false,
|
|
1338
2462
|
allowedDomains: props.allowedDomains,
|
|
2463
|
+
allowedDomainsTemplate: props.allowedDomainsTemplate ?? false,
|
|
1339
2464
|
clientFlag: props.clientFlag ?? true,
|
|
1340
2465
|
cnValidations: props.cnValidations ?? ["hostname"],
|
|
2466
|
+
enforceHostnames: props.enforceHostnames ?? true,
|
|
1341
2467
|
extKeyUsage: props.extKeyUsage ?? [],
|
|
2468
|
+
generateLease: props.generateLease ?? false,
|
|
1342
2469
|
keyBits: props.keyBits ?? 0,
|
|
1343
2470
|
keyType: props.keyType ?? "rsa",
|
|
1344
|
-
|
|
2471
|
+
keyUsage: props.keyUsage ?? DEFAULT_KEY_USAGE,
|
|
2472
|
+
maxTtlSeconds: seconds3(props.maxTtl),
|
|
2473
|
+
noStore: props.noStore ?? false,
|
|
2474
|
+
requireCn: props.requireCn ?? true,
|
|
1345
2475
|
serverFlag: props.serverFlag ?? true,
|
|
1346
2476
|
signatureBits: props.signatureBits ?? 0,
|
|
1347
|
-
ttlSeconds:
|
|
2477
|
+
ttlSeconds: seconds3(props.ttl)
|
|
1348
2478
|
});
|
|
1349
2479
|
var bool2 = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
1350
2480
|
var int2 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1351
2481
|
var list = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1352
2482
|
var str = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
1353
|
-
var
|
|
2483
|
+
var canonicalFromLive6 = (live) => canonical7({
|
|
1354
2484
|
allowBareDomains: bool2(live["allow_bare_domains"], false),
|
|
1355
2485
|
allowGlobDomains: bool2(live["allow_glob_domains"], false),
|
|
1356
2486
|
allowIpSans: bool2(live["allow_ip_sans"], false),
|
|
@@ -1358,16 +2488,25 @@ var canonicalFromLive = (live) => canonical2({
|
|
|
1358
2488
|
allowSubdomains: bool2(live["allow_subdomains"], false),
|
|
1359
2489
|
allowWildcardCertificates: bool2(live["allow_wildcard_certificates"], false),
|
|
1360
2490
|
allowedDomains: list(live["allowed_domains"]),
|
|
2491
|
+
allowedDomainsTemplate: bool2(live["allowed_domains_template"], false),
|
|
1361
2492
|
clientFlag: bool2(live["client_flag"], true),
|
|
1362
2493
|
cnValidations: list(live["cn_validations"]),
|
|
2494
|
+
enforceHostnames: bool2(live["enforce_hostnames"], false),
|
|
1363
2495
|
extKeyUsage: list(live["ext_key_usage"]),
|
|
2496
|
+
generateLease: bool2(live["generate_lease"], false),
|
|
1364
2497
|
keyBits: int2(live["key_bits"], 0),
|
|
1365
2498
|
keyType: str(live["key_type"], "rsa"),
|
|
2499
|
+
keyUsage: list(live["key_usage"]),
|
|
1366
2500
|
maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? -1,
|
|
2501
|
+
noStore: bool2(live["no_store"], false),
|
|
2502
|
+
requireCn: bool2(live["require_cn"], false),
|
|
1367
2503
|
serverFlag: bool2(live["server_flag"], true),
|
|
1368
2504
|
signatureBits: int2(live["signature_bits"], 0),
|
|
1369
2505
|
ttlSeconds: ttlSeconds(live["ttl"]) ?? -1
|
|
1370
2506
|
});
|
|
2507
|
+
var digestOf = (form) => sha256(JSON.stringify(form));
|
|
2508
|
+
|
|
2509
|
+
// src/openbao/pki-role-form.ts
|
|
1371
2510
|
var ttlText2 = (value) => {
|
|
1372
2511
|
if (value < 0)
|
|
1373
2512
|
return "";
|
|
@@ -1379,10 +2518,9 @@ var ttlText2 = (value) => {
|
|
|
1379
2518
|
return `${String(value / 60)}m`;
|
|
1380
2519
|
return `${String(value)}s`;
|
|
1381
2520
|
};
|
|
1382
|
-
var
|
|
1383
|
-
var
|
|
1384
|
-
|
|
1385
|
-
const form = canonicalFromLive(live);
|
|
2521
|
+
var rolePath4 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
|
|
2522
|
+
var attributesOf10 = (props, live) => {
|
|
2523
|
+
const form = canonicalFromLive6(live);
|
|
1386
2524
|
return {
|
|
1387
2525
|
...form,
|
|
1388
2526
|
name: props.name,
|
|
@@ -1392,8 +2530,8 @@ var attributesOf5 = (props, live) => {
|
|
|
1392
2530
|
digest: digestOf(form)
|
|
1393
2531
|
};
|
|
1394
2532
|
};
|
|
1395
|
-
var
|
|
1396
|
-
const form =
|
|
2533
|
+
var writeBody8 = (props) => {
|
|
2534
|
+
const form = canonicalFromProps6(props);
|
|
1397
2535
|
const body = {
|
|
1398
2536
|
allowed_domains: form.allowedDomains.join(","),
|
|
1399
2537
|
allow_bare_domains: String(form.allowBareDomains),
|
|
@@ -1409,76 +2547,94 @@ var writeBody3 = (props) => {
|
|
|
1409
2547
|
key_bits: String(form.keyBits),
|
|
1410
2548
|
signature_bits: String(form.signatureBits),
|
|
1411
2549
|
ttl: props.ttl,
|
|
1412
|
-
max_ttl: props.maxTtl
|
|
2550
|
+
max_ttl: props.maxTtl,
|
|
2551
|
+
require_cn: String(form.requireCn),
|
|
2552
|
+
enforce_hostnames: String(form.enforceHostnames),
|
|
2553
|
+
key_usage: (props.keyUsage ?? DEFAULT_KEY_USAGE).join(","),
|
|
2554
|
+
allowed_domains_template: String(form.allowedDomainsTemplate),
|
|
2555
|
+
no_store: String(form.noStore),
|
|
2556
|
+
generate_lease: String(form.generateLease)
|
|
1413
2557
|
};
|
|
1414
2558
|
const usages = props.extKeyUsage ?? [];
|
|
1415
2559
|
if (usages.length > 0)
|
|
1416
2560
|
body["ext_key_usage"] = usages.join(",");
|
|
1417
2561
|
return body;
|
|
1418
2562
|
};
|
|
1419
|
-
var
|
|
2563
|
+
var matches9 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps6(props));
|
|
2564
|
+
var problems6 = (props) => {
|
|
2565
|
+
const found = [];
|
|
2566
|
+
if (props.allowedDomains.length === 0) {
|
|
2567
|
+
found.push("allowedDomains is empty. With allow_any_name false that role can issue no certificate at all.");
|
|
2568
|
+
}
|
|
2569
|
+
const bad = [
|
|
2570
|
+
["ttl", props.ttl],
|
|
2571
|
+
["maxTtl", props.maxTtl]
|
|
2572
|
+
].filter(([, text10]) => parseDuration(text10 ?? "") === undefined);
|
|
2573
|
+
if (bad.length > 0) {
|
|
2574
|
+
found.push(`${bad.map(([k, v]) => `${k ?? ""}=${v ?? ""}`).join(", ")} is not a duration OpenBao ` + "parses (expect 30m, 720h, 8760h or 0).");
|
|
2575
|
+
}
|
|
2576
|
+
if (props.noStore === true && props.generateLease === true) {
|
|
2577
|
+
found.push("noStore and generateLease are both true; OpenBao keeps no_store and drops the lease.");
|
|
2578
|
+
}
|
|
2579
|
+
return found;
|
|
2580
|
+
};
|
|
1420
2581
|
|
|
1421
2582
|
// src/openbao/pki-role.ts
|
|
1422
|
-
var BaoPkiRole =
|
|
2583
|
+
var BaoPkiRole = Resource10("Bao.PkiRole", {
|
|
1423
2584
|
defaultRemovalPolicy: "retain"
|
|
1424
2585
|
});
|
|
1425
|
-
var readRole2 = (props) =>
|
|
1426
|
-
const live = yield* baoRead(
|
|
2586
|
+
var readRole2 = (props) => Effect24.gen(function* () {
|
|
2587
|
+
const live = yield* baoRead(rolePath4(props));
|
|
1427
2588
|
if (live === undefined)
|
|
1428
2589
|
return;
|
|
1429
|
-
return
|
|
2590
|
+
return attributesOf10(props, live);
|
|
1430
2591
|
});
|
|
1431
|
-
var
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
read: Effect11.fn(function* ({ olds }) {
|
|
2592
|
+
var BaoPkiRoleProvider = () => Provider10.effect(BaoPkiRole, Effect24.succeed(BaoPkiRole.Provider.of({
|
|
2593
|
+
list: () => Effect24.succeed([]),
|
|
2594
|
+
read: Effect24.fn(function* ({ olds }) {
|
|
1435
2595
|
return yield* readRole2(olds);
|
|
1436
2596
|
}),
|
|
1437
|
-
diff:
|
|
1438
|
-
if (output === undefined || !
|
|
2597
|
+
diff: Effect24.fn(function* ({ news, output }) {
|
|
2598
|
+
if (output === undefined || !isResolved10(news))
|
|
1439
2599
|
return;
|
|
1440
|
-
if (mountPath(news.mount ?? "pki") !== output.mount)
|
|
2600
|
+
if (mountPath(news.mount ?? "pki") !== output.mount || news.name !== output.name)
|
|
1441
2601
|
return { action: "replace" };
|
|
1442
|
-
if (
|
|
2602
|
+
if (problems6(news).length > 0)
|
|
1443
2603
|
return { action: "update" };
|
|
1444
|
-
}
|
|
1445
2604
|
const live = yield* readRole2(news);
|
|
1446
2605
|
if (live === undefined)
|
|
1447
2606
|
return { action: "update" };
|
|
1448
|
-
return
|
|
2607
|
+
return matches9(live, news) ? { action: "noop" } : { action: "update" };
|
|
1449
2608
|
}),
|
|
1450
|
-
reconcile:
|
|
1451
|
-
const path =
|
|
1452
|
-
|
|
1453
|
-
return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` + "that role can issue no certificate at all."));
|
|
1454
|
-
}
|
|
1455
|
-
const bad = badDurations(news);
|
|
2609
|
+
reconcile: Effect24.fn(function* ({ news }) {
|
|
2610
|
+
const path = rolePath4(news);
|
|
2611
|
+
const bad = problems6(news);
|
|
1456
2612
|
if (bad.length > 0) {
|
|
1457
|
-
return yield*
|
|
2613
|
+
return yield* Effect24.die(new Error(`Bao.PkiRole ${path}: ${bad.join(" ")}`));
|
|
1458
2614
|
}
|
|
1459
2615
|
const live = yield* readRole2(news);
|
|
1460
|
-
if (live === undefined || !
|
|
1461
|
-
yield* baoWrite("PUT", path,
|
|
2616
|
+
if (live === undefined || !matches9(live, news)) {
|
|
2617
|
+
yield* baoWrite("PUT", path, writeBody8(news));
|
|
1462
2618
|
}
|
|
1463
2619
|
const after = yield* readRole2(news);
|
|
1464
2620
|
if (after === undefined) {
|
|
1465
|
-
return yield*
|
|
2621
|
+
return yield* Effect24.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
|
|
1466
2622
|
}
|
|
1467
|
-
if (!
|
|
1468
|
-
return yield*
|
|
2623
|
+
if (!matches9(after, news)) {
|
|
2624
|
+
return yield* Effect24.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.`));
|
|
1469
2625
|
}
|
|
1470
2626
|
return after;
|
|
1471
2627
|
}),
|
|
1472
|
-
delete:
|
|
1473
|
-
yield* baoDelete(
|
|
2628
|
+
delete: Effect24.fn(function* ({ output }) {
|
|
2629
|
+
yield* baoDelete(rolePath4(output));
|
|
1474
2630
|
return;
|
|
1475
2631
|
})
|
|
1476
2632
|
})));
|
|
1477
2633
|
// src/openbao/plugin.ts
|
|
1478
|
-
import { Resource as
|
|
1479
|
-
import { isResolved as
|
|
1480
|
-
import * as
|
|
1481
|
-
import * as
|
|
2634
|
+
import { Resource as Resource11 } from "alchemy";
|
|
2635
|
+
import { isResolved as isResolved11 } from "alchemy/Diff";
|
|
2636
|
+
import * as Provider11 from "alchemy/Provider";
|
|
2637
|
+
import * as Effect26 from "effect/Effect";
|
|
1482
2638
|
|
|
1483
2639
|
// src/openbao/plugin-form.ts
|
|
1484
2640
|
var resolve = (props) => ({
|
|
@@ -1490,7 +2646,7 @@ var resolve = (props) => ({
|
|
|
1490
2646
|
version: props.version ?? ""
|
|
1491
2647
|
});
|
|
1492
2648
|
var CANONICAL_VERSION = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
|
|
1493
|
-
var
|
|
2649
|
+
var problems7 = (form) => {
|
|
1494
2650
|
const found = [];
|
|
1495
2651
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(form.name) || form.name.includes("..")) {
|
|
1496
2652
|
found.push(`name ${JSON.stringify(form.name)} is not a plain catalog name`);
|
|
@@ -1516,25 +2672,25 @@ var registerBody = (form) => ({
|
|
|
1516
2672
|
...form.args.length > 0 ? { args: form.args } : {},
|
|
1517
2673
|
...form.version === "" ? {} : { version: form.version }
|
|
1518
2674
|
});
|
|
1519
|
-
var
|
|
2675
|
+
var text10 = (value) => typeof value === "string" ? value : "";
|
|
1520
2676
|
var list2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1521
|
-
var
|
|
2677
|
+
var attributesOf11 = (form, live) => {
|
|
1522
2678
|
const attrs = {
|
|
1523
2679
|
name: form.name,
|
|
1524
2680
|
type: form.type,
|
|
1525
|
-
version:
|
|
1526
|
-
sha256:
|
|
1527
|
-
command:
|
|
2681
|
+
version: text10(live["version"]),
|
|
2682
|
+
sha256: text10(live["sha256"]).toLowerCase(),
|
|
2683
|
+
command: text10(live["command"]),
|
|
1528
2684
|
args: list2(live["args"]),
|
|
1529
2685
|
builtin: live["builtin"] === true
|
|
1530
2686
|
};
|
|
1531
2687
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
1532
2688
|
};
|
|
1533
2689
|
var sameArgs = (want, have) => want.length === have.length && want.every((entry, index) => entry === have[index]);
|
|
1534
|
-
var
|
|
2690
|
+
var matches10 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
|
|
1535
2691
|
|
|
1536
2692
|
// src/openbao/plugin-reconcile.ts
|
|
1537
|
-
import * as
|
|
2693
|
+
import * as Effect25 from "effect/Effect";
|
|
1538
2694
|
|
|
1539
2695
|
// src/openbao/plugin-wire.ts
|
|
1540
2696
|
var readPluginData = (form) => baoRead(versionedPath(form.type, form.name, form.version));
|
|
@@ -1542,44 +2698,44 @@ var registerPlugin = (form) => baoWrite("PUT", catalogPath(form.type, form.name)
|
|
|
1542
2698
|
var deregisterPlugin = (type, name, version) => baoDelete(versionedPath(type, name, version));
|
|
1543
2699
|
|
|
1544
2700
|
// src/openbao/plugin-reconcile.ts
|
|
1545
|
-
var readEntry = (form) =>
|
|
1546
|
-
var
|
|
2701
|
+
var readEntry = (form) => Effect25.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf11(form, live), live });
|
|
2702
|
+
var refuse5 = (form, message) => Effect25.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
|
|
1547
2703
|
var SELF_REPORTED = "the write succeeded but the read-back does not match. If the binary reports its own version, " + "OpenBao filed the registration under it (plugin_catalog.go setInternal) — declare `version` " + "as exactly the version the binary reports; that adopts the entry this write just made.";
|
|
1548
|
-
var reconcilePlugin = (props) =>
|
|
2704
|
+
var reconcilePlugin = (props) => Effect25.gen(function* () {
|
|
1549
2705
|
const form = resolve(props);
|
|
1550
|
-
const bad =
|
|
2706
|
+
const bad = problems7(form);
|
|
1551
2707
|
if (bad.length > 0)
|
|
1552
|
-
return yield*
|
|
2708
|
+
return yield* refuse5(form, bad.join("; "));
|
|
1553
2709
|
const found = yield* readEntry(form);
|
|
1554
|
-
if (found !== undefined && !
|
|
2710
|
+
if (found !== undefined && !matches10(found.attributes, form)) {
|
|
1555
2711
|
if (found.live["declarative"] === true || found.live["oci"] === true) {
|
|
1556
|
-
return yield*
|
|
2712
|
+
return yield* refuse5(form, "the live entry is declarative/OCI; manage it in server config.");
|
|
1557
2713
|
}
|
|
1558
2714
|
if (found.attributes.builtin) {
|
|
1559
|
-
return yield*
|
|
2715
|
+
return yield* refuse5(form, "this name is a builtin; an unversioned write would shadow it.");
|
|
1560
2716
|
}
|
|
1561
2717
|
}
|
|
1562
|
-
if (found === undefined || !
|
|
2718
|
+
if (found === undefined || !matches10(found.attributes, form))
|
|
1563
2719
|
yield* registerPlugin(form);
|
|
1564
2720
|
const after = yield* readEntry(form);
|
|
1565
|
-
if (after === undefined || !
|
|
1566
|
-
return yield*
|
|
2721
|
+
if (after === undefined || !matches10(after.attributes, form)) {
|
|
2722
|
+
return yield* refuse5(form, SELF_REPORTED);
|
|
1567
2723
|
}
|
|
1568
2724
|
return after.attributes;
|
|
1569
2725
|
});
|
|
1570
2726
|
|
|
1571
2727
|
// src/openbao/plugin.ts
|
|
1572
|
-
var BaoPlugin =
|
|
2728
|
+
var BaoPlugin = Resource11("Bao.Plugin", {
|
|
1573
2729
|
defaultRemovalPolicy: "retain"
|
|
1574
2730
|
});
|
|
1575
|
-
var BaoPluginProvider = () =>
|
|
1576
|
-
list: () =>
|
|
1577
|
-
read:
|
|
2731
|
+
var BaoPluginProvider = () => Provider11.effect(BaoPlugin, Effect26.succeed(BaoPlugin.Provider.of({
|
|
2732
|
+
list: () => Effect26.succeed([]),
|
|
2733
|
+
read: Effect26.fn(function* ({ olds }) {
|
|
1578
2734
|
const found = yield* readEntry(resolve(olds));
|
|
1579
2735
|
return found?.attributes;
|
|
1580
2736
|
}),
|
|
1581
|
-
diff:
|
|
1582
|
-
if (output === undefined || !
|
|
2737
|
+
diff: Effect26.fn(function* ({ news, output }) {
|
|
2738
|
+
if (output === undefined || !isResolved11(news))
|
|
1583
2739
|
return;
|
|
1584
2740
|
const form = resolve(news);
|
|
1585
2741
|
if (output.name !== form.name || output.type !== form.type || output.version !== form.version) {
|
|
@@ -1588,21 +2744,21 @@ var BaoPluginProvider = () => Provider6.effect(BaoPlugin, Effect13.succeed(BaoPl
|
|
|
1588
2744
|
const found = yield* readEntry(form);
|
|
1589
2745
|
if (found === undefined)
|
|
1590
2746
|
return { action: "update" };
|
|
1591
|
-
return
|
|
2747
|
+
return matches10(found.attributes, form) ? { action: "noop" } : { action: "update" };
|
|
1592
2748
|
}),
|
|
1593
|
-
reconcile:
|
|
2749
|
+
reconcile: Effect26.fn(function* ({ news }) {
|
|
1594
2750
|
return yield* reconcilePlugin(news);
|
|
1595
2751
|
}),
|
|
1596
|
-
delete:
|
|
2752
|
+
delete: Effect26.fn(function* ({ output }) {
|
|
1597
2753
|
yield* deregisterPlugin(output.type, output.name, output.version);
|
|
1598
2754
|
return;
|
|
1599
2755
|
})
|
|
1600
2756
|
})));
|
|
1601
2757
|
// src/openbao/policy.ts
|
|
1602
|
-
import { Resource as
|
|
1603
|
-
import { isResolved as
|
|
1604
|
-
import * as
|
|
1605
|
-
import * as
|
|
2758
|
+
import { Resource as Resource12 } from "alchemy";
|
|
2759
|
+
import { isResolved as isResolved12 } from "alchemy/Diff";
|
|
2760
|
+
import * as Provider12 from "alchemy/Provider";
|
|
2761
|
+
import * as Effect28 from "effect/Effect";
|
|
1606
2762
|
import * as FileSystem from "effect/FileSystem";
|
|
1607
2763
|
import * as Path from "effect/Path";
|
|
1608
2764
|
|
|
@@ -1610,32 +2766,32 @@ import * as Path from "effect/Path";
|
|
|
1610
2766
|
var isEmptyAssembly = (parts, joined) => parts === 0 || !/^\s*path\s+"/m.test(joined);
|
|
1611
2767
|
|
|
1612
2768
|
// src/openbao/policy-wire.ts
|
|
1613
|
-
import * as
|
|
2769
|
+
import * as Effect27 from "effect/Effect";
|
|
1614
2770
|
var policyPath = (name) => `sys/policies/acl/${name}`;
|
|
1615
|
-
var readPolicy = (name) =>
|
|
2771
|
+
var readPolicy = (name) => Effect27.flatMap(baoRead(policyPath(name)), (data) => {
|
|
1616
2772
|
if (data === undefined)
|
|
1617
|
-
return
|
|
1618
|
-
const
|
|
1619
|
-
return typeof
|
|
2773
|
+
return Effect27.succeed("");
|
|
2774
|
+
const text11 = data["policy"];
|
|
2775
|
+
return typeof text11 === "string" ? Effect27.succeed(text11) : Effect27.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
|
|
1620
2776
|
});
|
|
1621
2777
|
var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
|
|
1622
2778
|
var deletePolicy = (name) => baoDelete(policyPath(name));
|
|
1623
2779
|
|
|
1624
2780
|
// src/openbao/policy.ts
|
|
1625
|
-
var BaoPolicy =
|
|
2781
|
+
var BaoPolicy = Resource12("Bao.Policy", {
|
|
1626
2782
|
defaultRemovalPolicy: "retain"
|
|
1627
2783
|
});
|
|
1628
2784
|
var grantsOf = (hcl) => (hcl.match(/^path /gm) ?? []).length;
|
|
1629
|
-
var
|
|
2785
|
+
var attributesOf12 = (name, hcl, parts) => ({
|
|
1630
2786
|
name,
|
|
1631
2787
|
digest: sha256(hcl),
|
|
1632
2788
|
grants: grantsOf(hcl),
|
|
1633
2789
|
parts
|
|
1634
2790
|
});
|
|
1635
|
-
var BaoPolicyProvider = () =>
|
|
2791
|
+
var BaoPolicyProvider = () => Provider12.effect(BaoPolicy, Effect28.gen(function* () {
|
|
1636
2792
|
const fs = yield* FileSystem.FileSystem;
|
|
1637
2793
|
const path = yield* Path.Path;
|
|
1638
|
-
const assemble = (dir) =>
|
|
2794
|
+
const assemble = (dir) => Effect28.gen(function* () {
|
|
1639
2795
|
const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
|
|
1640
2796
|
let joined = "";
|
|
1641
2797
|
for (const f of entries2)
|
|
@@ -1644,67 +2800,77 @@ var BaoPolicyProvider = () => Provider7.effect(BaoPolicy, Effect15.gen(function*
|
|
|
1644
2800
|
return { joined, parts: entries2.length };
|
|
1645
2801
|
});
|
|
1646
2802
|
return BaoPolicy.Provider.of({
|
|
1647
|
-
list: () =>
|
|
1648
|
-
read:
|
|
2803
|
+
list: () => Effect28.succeed([]),
|
|
2804
|
+
read: Effect28.fn(function* ({ olds }) {
|
|
1649
2805
|
const live = yield* readPolicy(olds.name);
|
|
1650
|
-
return live.length === 0 ? undefined :
|
|
2806
|
+
return live.length === 0 ? undefined : attributesOf12(olds.name, live, 0);
|
|
1651
2807
|
}),
|
|
1652
|
-
diff:
|
|
1653
|
-
|
|
2808
|
+
diff: Effect28.fn(function* ({ news, olds, output }) {
|
|
2809
|
+
const tried = output?.name ?? declaredString(olds, "name");
|
|
2810
|
+
const declared = declaredString(news, "name");
|
|
2811
|
+
const move = yield* judgeMove("Bao.Policy", tried, declared, policyPath, policyKey);
|
|
2812
|
+
if (output === undefined)
|
|
2813
|
+
return;
|
|
2814
|
+
if (move !== undefined)
|
|
2815
|
+
return { action: "replace" };
|
|
2816
|
+
if (!isResolved12(news))
|
|
1654
2817
|
return;
|
|
1655
2818
|
const { joined } = yield* assemble(news.fragments);
|
|
1656
2819
|
const live = yield* readPolicy(news.name);
|
|
1657
2820
|
return live.length > 0 && sha256(live) === sha256(joined) ? { action: "noop" } : { action: "update" };
|
|
1658
2821
|
}),
|
|
1659
|
-
reconcile:
|
|
2822
|
+
reconcile: Effect28.fn(function* ({ news, output }) {
|
|
1660
2823
|
const name = news.name;
|
|
2824
|
+
if (output !== undefined && isMoved(output.name, name, policyKey) === true) {
|
|
2825
|
+
return yield* refuseMovedUpdate(`Bao.Policy ${name}`, output.name, name);
|
|
2826
|
+
}
|
|
1661
2827
|
const { joined, parts } = yield* assemble(news.fragments);
|
|
1662
2828
|
if (isEmptyAssembly(parts, joined))
|
|
1663
|
-
return yield*
|
|
2829
|
+
return yield* Effect28.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.`));
|
|
1664
2830
|
const current = yield* readPolicy(name);
|
|
1665
2831
|
if (current.length > 0 && sha256(current) === sha256(joined)) {
|
|
1666
|
-
return
|
|
2832
|
+
return attributesOf12(name, joined, parts);
|
|
1667
2833
|
}
|
|
1668
2834
|
yield* writePolicy(name, joined);
|
|
1669
|
-
return
|
|
2835
|
+
return attributesOf12(name, joined, parts);
|
|
1670
2836
|
}),
|
|
1671
|
-
delete:
|
|
2837
|
+
delete: Effect28.fn(function* ({ output }) {
|
|
1672
2838
|
yield* deletePolicy(output.name);
|
|
1673
2839
|
return;
|
|
1674
2840
|
})
|
|
1675
2841
|
});
|
|
1676
2842
|
}));
|
|
1677
2843
|
// src/openbao/proxmox-role.ts
|
|
1678
|
-
import { Resource as
|
|
1679
|
-
import { isResolved as
|
|
1680
|
-
import * as
|
|
1681
|
-
import * as
|
|
2844
|
+
import { Resource as Resource13 } from "alchemy";
|
|
2845
|
+
import { isResolved as isResolved13 } from "alchemy/Diff";
|
|
2846
|
+
import * as Provider13 from "alchemy/Provider";
|
|
2847
|
+
import * as Effect30 from "effect/Effect";
|
|
1682
2848
|
|
|
1683
2849
|
// src/openbao/proxmox-role-form.ts
|
|
1684
|
-
var
|
|
1685
|
-
var ttlText3 = (
|
|
1686
|
-
if (
|
|
2850
|
+
var text11 = (value) => typeof value === "string" ? value : "";
|
|
2851
|
+
var ttlText3 = (seconds4, fallback = "0") => {
|
|
2852
|
+
if (seconds4 === undefined)
|
|
1687
2853
|
return fallback;
|
|
1688
|
-
if (
|
|
1689
|
-
return `${String(
|
|
1690
|
-
if (
|
|
1691
|
-
return `${String(
|
|
1692
|
-
if (
|
|
1693
|
-
return `${String(
|
|
1694
|
-
return `${String(
|
|
1695
|
-
};
|
|
1696
|
-
var
|
|
1697
|
-
var
|
|
2854
|
+
if (seconds4 % 86400 === 0)
|
|
2855
|
+
return `${String(seconds4 / 86400)}d`;
|
|
2856
|
+
if (seconds4 % 3600 === 0)
|
|
2857
|
+
return `${String(seconds4 / 3600)}h`;
|
|
2858
|
+
if (seconds4 % 60 === 0)
|
|
2859
|
+
return `${String(seconds4 / 60)}m`;
|
|
2860
|
+
return `${String(seconds4)}s`;
|
|
2861
|
+
};
|
|
2862
|
+
var rolePath5 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
|
|
2863
|
+
var attributesOf13 = (props, live) => {
|
|
1698
2864
|
const attrs = {
|
|
1699
2865
|
mount: mountPath(props.mount),
|
|
1700
|
-
name:
|
|
1701
|
-
mintUser:
|
|
2866
|
+
name: text11(live["name"]) || props.name,
|
|
2867
|
+
mintUser: text11(live["mint_user"]),
|
|
1702
2868
|
ttl: ttlText3(ttlSeconds(live["ttl"])),
|
|
1703
2869
|
maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
|
|
1704
2870
|
};
|
|
1705
2871
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
1706
2872
|
};
|
|
1707
|
-
var
|
|
2873
|
+
var writeBody9 = (props) => {
|
|
1708
2874
|
const ttl = parseDuration(props.ttl);
|
|
1709
2875
|
const maxTtl = parseDuration(props.maxTtl);
|
|
1710
2876
|
const bad = [];
|
|
@@ -1724,68 +2890,94 @@ var sameTtl5 = (want, have) => {
|
|
|
1724
2890
|
const haveSeconds = ttlSeconds(have);
|
|
1725
2891
|
return wantSeconds !== undefined && wantSeconds === haveSeconds;
|
|
1726
2892
|
};
|
|
1727
|
-
var
|
|
2893
|
+
var rescopeRefusal = (before, mintUser, allowMintUserChange) => before === mintUser || allowMintUserChange === before ? undefined : `re-scopes mint_user ${before} → ${mintUser}. Every token this role mints inherits ` + `the ACL of that PVE user. To re-scope deliberately, declare allowMintUserChange: '${before}'.`;
|
|
2894
|
+
var matches11 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
|
|
1728
2895
|
|
|
1729
|
-
// src/openbao/proxmox-role.ts
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
var readRole3 = (props) => Effect16.gen(function* () {
|
|
1734
|
-
const live = yield* baoRead(rolePath3(props.mount, props.name));
|
|
2896
|
+
// src/openbao/proxmox-role-wire.ts
|
|
2897
|
+
import * as Effect29 from "effect/Effect";
|
|
2898
|
+
var readRole3 = (props) => Effect29.gen(function* () {
|
|
2899
|
+
const live = yield* baoRead(rolePath5(props.mount, props.name));
|
|
1735
2900
|
if (live === undefined)
|
|
1736
2901
|
return;
|
|
1737
|
-
return
|
|
2902
|
+
return attributesOf13(props, live);
|
|
1738
2903
|
});
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2904
|
+
|
|
2905
|
+
// src/openbao/proxmox-role.ts
|
|
2906
|
+
var BaoProxmoxRole = Resource13("Bao.ProxmoxRole", {
|
|
2907
|
+
defaultRemovalPolicy: "retain"
|
|
2908
|
+
});
|
|
2909
|
+
var BaoProxmoxRoleProvider = () => Provider13.effect(BaoProxmoxRole, Effect30.succeed(BaoProxmoxRole.Provider.of({
|
|
2910
|
+
list: () => Effect30.succeed([]),
|
|
2911
|
+
read: Effect30.fn(function* ({ olds }) {
|
|
1742
2912
|
return yield* readRole3(olds);
|
|
1743
2913
|
}),
|
|
1744
|
-
diff:
|
|
1745
|
-
|
|
2914
|
+
diff: Effect30.fn(function* ({ news, olds, output }) {
|
|
2915
|
+
const tried = triedRolePath(output, olds, rolePath5);
|
|
2916
|
+
const declared = declaredRolePath(news, rolePath5);
|
|
2917
|
+
const move = yield* judgeMove("Bao.ProxmoxRole", tried, declared, (path) => path);
|
|
2918
|
+
if (output === undefined)
|
|
2919
|
+
return;
|
|
2920
|
+
if (move !== undefined) {
|
|
2921
|
+
const mintUser = declaredString(news, "mintUser");
|
|
2922
|
+
if (mintUser === undefined || isPendingProp(news, "allowMintUserChange"))
|
|
2923
|
+
return;
|
|
2924
|
+
const from = (yield* readRole3(output))?.mintUser ?? output.mintUser;
|
|
2925
|
+
const allow = declaredString(news, "allowMintUserChange");
|
|
2926
|
+
const refusal2 = rescopeRefusal(from, mintUser, allow);
|
|
2927
|
+
if (refusal2 === undefined)
|
|
2928
|
+
return { action: "replace" };
|
|
2929
|
+
return yield* Effect30.die(new Error(`Bao.ProxmoxRole ${move.from} → ${move.to}: the rename ${refusal2}`));
|
|
2930
|
+
}
|
|
2931
|
+
if (!isResolved13(news))
|
|
1746
2932
|
return;
|
|
1747
2933
|
const live = yield* readRole3(news);
|
|
1748
2934
|
if (live === undefined)
|
|
1749
2935
|
return { action: "update" };
|
|
1750
|
-
if (
|
|
2936
|
+
if (matches11(live, news))
|
|
1751
2937
|
return { action: "noop" };
|
|
1752
2938
|
return { action: "update" };
|
|
1753
2939
|
}),
|
|
1754
|
-
reconcile:
|
|
2940
|
+
reconcile: Effect30.fn(function* ({ news, output }) {
|
|
2941
|
+
const path = rolePath5(news.mount, news.name);
|
|
2942
|
+
const before = output === undefined ? path : rolePath5(output.mount, output.name);
|
|
2943
|
+
if (isMoved(before, path) === true) {
|
|
2944
|
+
return yield* refuseMovedUpdate("Bao.ProxmoxRole", before, path);
|
|
2945
|
+
}
|
|
1755
2946
|
const live = yield* readRole3(news);
|
|
1756
|
-
|
|
1757
|
-
|
|
2947
|
+
const rescope = live === undefined ? undefined : rescopeRefusal(live.mintUser, news.mintUser, news.allowMintUserChange);
|
|
2948
|
+
if (rescope !== undefined) {
|
|
2949
|
+
return yield* Effect30.die(new Error(`Bao.ProxmoxRole ${path}: ${rescope}`));
|
|
1758
2950
|
}
|
|
1759
|
-
if (live === undefined || !
|
|
1760
|
-
const write =
|
|
2951
|
+
if (live === undefined || !matches11(live, news)) {
|
|
2952
|
+
const write = writeBody9(news);
|
|
1761
2953
|
if (!write.ok) {
|
|
1762
|
-
return yield*
|
|
2954
|
+
return yield* Effect30.die(new Error(`Bao.ProxmoxRole ${path}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
|
|
1763
2955
|
}
|
|
1764
|
-
yield* baoWrite("PUT",
|
|
2956
|
+
yield* baoWrite("PUT", path, write.body);
|
|
1765
2957
|
}
|
|
1766
2958
|
const after = yield* readRole3(news);
|
|
1767
2959
|
if (after === undefined) {
|
|
1768
|
-
return yield*
|
|
2960
|
+
return yield* Effect30.die(new Error(`Bao.ProxmoxRole ${path}: write returned no error ` + "but the role is still absent."));
|
|
1769
2961
|
}
|
|
1770
2962
|
return after;
|
|
1771
2963
|
}),
|
|
1772
|
-
delete:
|
|
1773
|
-
yield* baoDelete(
|
|
2964
|
+
delete: Effect30.fn(function* ({ output }) {
|
|
2965
|
+
yield* baoDelete(rolePath5(output.mount, output.name));
|
|
1774
2966
|
return;
|
|
1775
2967
|
})
|
|
1776
2968
|
})));
|
|
1777
2969
|
// src/openbao/ssh-role.ts
|
|
1778
|
-
import { Resource as
|
|
1779
|
-
import { isResolved as
|
|
1780
|
-
import * as
|
|
1781
|
-
import * as
|
|
2970
|
+
import { Resource as Resource14 } from "alchemy";
|
|
2971
|
+
import { isResolved as isResolved14 } from "alchemy/Diff";
|
|
2972
|
+
import * as Provider14 from "alchemy/Provider";
|
|
2973
|
+
import * as Effect31 from "effect/Effect";
|
|
1782
2974
|
|
|
1783
2975
|
// src/openbao/ssh-role-form.ts
|
|
1784
2976
|
var readPath4 = (mount, name) => `${sshMountOf(mount)}/roles/${name}`;
|
|
1785
|
-
var
|
|
2977
|
+
var text12 = (value) => typeof value === "string" ? value : "";
|
|
1786
2978
|
var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
1787
2979
|
var csvList = (value) => {
|
|
1788
|
-
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") :
|
|
2980
|
+
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text12(value).split(",");
|
|
1789
2981
|
return [
|
|
1790
2982
|
...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
|
|
1791
2983
|
].sort();
|
|
@@ -1796,7 +2988,7 @@ var extMap = (value) => {
|
|
|
1796
2988
|
return out;
|
|
1797
2989
|
const source = value;
|
|
1798
2990
|
for (const key of Object.keys(source).sort())
|
|
1799
|
-
out[key] =
|
|
2991
|
+
out[key] = text12(source[key]);
|
|
1800
2992
|
return out;
|
|
1801
2993
|
};
|
|
1802
2994
|
var resolve2 = (props) => ({
|
|
@@ -1817,12 +3009,12 @@ var resolve2 = (props) => ({
|
|
|
1817
3009
|
ttl: props.ttl,
|
|
1818
3010
|
maxTtl: props.maxTtl
|
|
1819
3011
|
});
|
|
1820
|
-
var
|
|
3012
|
+
var attributesOf14 = (form, live) => {
|
|
1821
3013
|
const attrs = {
|
|
1822
3014
|
name: form.name,
|
|
1823
3015
|
mount: form.mount,
|
|
1824
3016
|
allowedUsers: csvList(live["allowed_users"]),
|
|
1825
|
-
defaultUser:
|
|
3017
|
+
defaultUser: text12(live["default_user"]),
|
|
1826
3018
|
allowUserCertificates: bool3(live["allow_user_certificates"], false),
|
|
1827
3019
|
allowHostCertificates: bool3(live["allow_host_certificates"], false),
|
|
1828
3020
|
allowedDomains: csvList(live["allowed_domains"]),
|
|
@@ -1833,14 +3025,14 @@ var attributesOf9 = (form, live) => {
|
|
|
1833
3025
|
allowedCriticalOptions: csvList(live["allowed_critical_options"]),
|
|
1834
3026
|
defaultCriticalOptions: extMap(live["default_critical_options"]),
|
|
1835
3027
|
allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
|
|
1836
|
-
keyType:
|
|
3028
|
+
keyType: text12(live["key_type"]) || "ca",
|
|
1837
3029
|
ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
|
|
1838
3030
|
maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
|
|
1839
3031
|
};
|
|
1840
3032
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
1841
3033
|
};
|
|
1842
|
-
var mapValue = (
|
|
1843
|
-
var
|
|
3034
|
+
var mapValue = (map10) => extMap(map10);
|
|
3035
|
+
var writeBody10 = (form) => ({
|
|
1844
3036
|
key_type: "ca",
|
|
1845
3037
|
allowed_users: form.allowedUsers.join(","),
|
|
1846
3038
|
default_user: form.defaultUser,
|
|
@@ -1863,18 +3055,18 @@ var sameMap = (want, have) => {
|
|
|
1863
3055
|
return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);
|
|
1864
3056
|
};
|
|
1865
3057
|
var sameTtl6 = (want, have) => parseDuration(want) === have;
|
|
1866
|
-
var
|
|
1867
|
-
var
|
|
3058
|
+
var badDurations = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
|
|
3059
|
+
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);
|
|
1868
3060
|
|
|
1869
3061
|
// src/openbao/ssh-role.ts
|
|
1870
|
-
var BaoSshRole =
|
|
3062
|
+
var BaoSshRole = Resource14("Bao.SshRole", {
|
|
1871
3063
|
defaultRemovalPolicy: "retain"
|
|
1872
3064
|
});
|
|
1873
|
-
var readRole4 = (form) =>
|
|
3065
|
+
var readRole4 = (form) => Effect31.gen(function* () {
|
|
1874
3066
|
const live = yield* baoRead(readPath4(form.mount, form.name));
|
|
1875
3067
|
if (live === undefined)
|
|
1876
3068
|
return;
|
|
1877
|
-
return { attributes:
|
|
3069
|
+
return { attributes: attributesOf14(form, live), live };
|
|
1878
3070
|
});
|
|
1879
3071
|
var UNMANAGED_DEFAULTS = {
|
|
1880
3072
|
algorithm_signer: "default",
|
|
@@ -1889,22 +3081,22 @@ var UNMANAGED_DEFAULTS = {
|
|
|
1889
3081
|
not_before_duration: 30
|
|
1890
3082
|
};
|
|
1891
3083
|
var isDefault = (live, field) => live[field] === undefined || live[field] === "" || live[field] === UNMANAGED_DEFAULTS[field];
|
|
1892
|
-
var
|
|
3084
|
+
var wouldErase2 = (live) => {
|
|
1893
3085
|
const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));
|
|
1894
3086
|
if (Object.keys(extMap(live["allowed_user_key_lengths"])).length > 0) {
|
|
1895
3087
|
lost.push("allowed_user_key_lengths");
|
|
1896
3088
|
}
|
|
1897
3089
|
return lost.sort();
|
|
1898
3090
|
};
|
|
1899
|
-
var
|
|
1900
|
-
var BaoSshRoleProvider = () =>
|
|
1901
|
-
list: () =>
|
|
1902
|
-
read:
|
|
3091
|
+
var refuse6 = (message) => Effect31.die(new Error(message));
|
|
3092
|
+
var BaoSshRoleProvider = () => Provider14.effect(BaoSshRole, Effect31.succeed(BaoSshRole.Provider.of({
|
|
3093
|
+
list: () => Effect31.succeed([]),
|
|
3094
|
+
read: Effect31.fn(function* ({ olds }) {
|
|
1903
3095
|
const found = yield* readRole4(resolve2(olds));
|
|
1904
3096
|
return found?.attributes;
|
|
1905
3097
|
}),
|
|
1906
|
-
diff:
|
|
1907
|
-
if (output === undefined || !
|
|
3098
|
+
diff: Effect31.fn(function* ({ news, output }) {
|
|
3099
|
+
if (output === undefined || !isResolved14(news))
|
|
1908
3100
|
return;
|
|
1909
3101
|
const form = resolve2(news);
|
|
1910
3102
|
if (output.mount !== form.mount || output.name !== form.name) {
|
|
@@ -1913,35 +3105,35 @@ var BaoSshRoleProvider = () => Provider9.effect(BaoSshRole, Effect17.succeed(Bao
|
|
|
1913
3105
|
const found = yield* readRole4(form);
|
|
1914
3106
|
if (found === undefined)
|
|
1915
3107
|
return { action: "update" };
|
|
1916
|
-
return
|
|
3108
|
+
return matches12(found.attributes, form) ? { action: "noop" } : { action: "update" };
|
|
1917
3109
|
}),
|
|
1918
|
-
reconcile:
|
|
3110
|
+
reconcile: Effect31.fn(function* ({ news }) {
|
|
1919
3111
|
const form = resolve2(news);
|
|
1920
3112
|
const path = readPath4(form.mount, form.name);
|
|
1921
|
-
const bad =
|
|
3113
|
+
const bad = badDurations(form);
|
|
1922
3114
|
if (bad.length > 0) {
|
|
1923
|
-
return yield*
|
|
3115
|
+
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.");
|
|
1924
3116
|
}
|
|
1925
3117
|
if (form.allowUserCertificates && form.allowedUsers.length === 0 && !form.allowEmptyPrincipals) {
|
|
1926
|
-
return yield*
|
|
3118
|
+
return yield* refuse6(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
|
|
1927
3119
|
}
|
|
1928
3120
|
const found = yield* readRole4(form);
|
|
1929
|
-
if (found === undefined || !
|
|
3121
|
+
if (found === undefined || !matches12(found.attributes, form)) {
|
|
1930
3122
|
if (found !== undefined) {
|
|
1931
|
-
const lost =
|
|
3123
|
+
const lost = wouldErase2(found.live);
|
|
1932
3124
|
if (lost.length > 0) {
|
|
1933
|
-
return yield*
|
|
3125
|
+
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.");
|
|
1934
3126
|
}
|
|
1935
3127
|
}
|
|
1936
|
-
yield* baoWrite("PUT", path,
|
|
3128
|
+
yield* baoWrite("PUT", path, writeBody10(form));
|
|
1937
3129
|
}
|
|
1938
3130
|
const after = yield* readRole4(form);
|
|
1939
3131
|
if (after === undefined) {
|
|
1940
|
-
return yield*
|
|
3132
|
+
return yield* refuse6(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
|
|
1941
3133
|
}
|
|
1942
3134
|
return after.attributes;
|
|
1943
3135
|
}),
|
|
1944
|
-
delete:
|
|
3136
|
+
delete: Effect31.fn(function* ({ output }) {
|
|
1945
3137
|
yield* baoDelete(readPath4(output.mount, output.name));
|
|
1946
3138
|
return;
|
|
1947
3139
|
})
|
|
@@ -1953,7 +3145,18 @@ export {
|
|
|
1953
3145
|
BaoAuthRoleProvider,
|
|
1954
3146
|
BaoCloudflareRole,
|
|
1955
3147
|
BaoCloudflareRoleProvider,
|
|
3148
|
+
BaoIdentityError,
|
|
3149
|
+
BaoJwtAuthConfig,
|
|
3150
|
+
BaoJwtAuthConfigProvider,
|
|
3151
|
+
BaoJwtRole,
|
|
3152
|
+
BaoJwtRoleProvider,
|
|
3153
|
+
BaoKubernetesRole,
|
|
3154
|
+
BaoKubernetesRoleProvider,
|
|
1956
3155
|
BaoLoginError,
|
|
3156
|
+
BaoMfaLoginEnforcement,
|
|
3157
|
+
BaoMfaLoginEnforcementProvider,
|
|
3158
|
+
BaoMfaTotpMethod,
|
|
3159
|
+
BaoMfaTotpMethodProvider,
|
|
1957
3160
|
BaoMount,
|
|
1958
3161
|
BaoMountProvider,
|
|
1959
3162
|
BaoPkiRole,
|
|
@@ -1967,10 +3170,15 @@ export {
|
|
|
1967
3170
|
BaoSshRole,
|
|
1968
3171
|
BaoSshRoleProvider,
|
|
1969
3172
|
CloudflarePermissionGroups,
|
|
3173
|
+
HOST_ROLE_SEPARATOR,
|
|
1970
3174
|
appRoleLogin,
|
|
1971
3175
|
appRoleLoginEffect,
|
|
3176
|
+
assertBaoIdentity,
|
|
3177
|
+
assertBaoIdentityEffect,
|
|
1972
3178
|
expandAccount,
|
|
1973
3179
|
expandAll,
|
|
3180
|
+
hostAppRoles,
|
|
3181
|
+
hostRoleName,
|
|
1974
3182
|
mountName,
|
|
1975
3183
|
parseRolesConfig,
|
|
1976
3184
|
permissionGroupsFromEngine,
|
|
@@ -1979,5 +3187,5 @@ export {
|
|
|
1979
3187
|
revokeSelfEffect
|
|
1980
3188
|
};
|
|
1981
3189
|
|
|
1982
|
-
//# debugId=
|
|
3190
|
+
//# debugId=81D869587FC0B7AF64756E2164756E21
|
|
1983
3191
|
//# sourceMappingURL=index.js.map
|