@homeflare/alchemy 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +17 -48
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/launchd/fake-runner.d.ts +36 -0
  5. package/dist/launchd/fake-runner.d.ts.map +1 -0
  6. package/dist/launchd/host-effect.d.ts +19 -0
  7. package/dist/launchd/host-effect.d.ts.map +1 -0
  8. package/dist/launchd/host-file-form.d.ts +24 -0
  9. package/dist/launchd/host-file-form.d.ts.map +1 -0
  10. package/dist/launchd/host-file-lifecycle.d.ts +29 -0
  11. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
  12. package/dist/launchd/host-file.d.ts +25 -0
  13. package/dist/launchd/host-file.d.ts.map +1 -0
  14. package/dist/launchd/host-lookup.d.ts +27 -0
  15. package/dist/launchd/host-lookup.d.ts.map +1 -0
  16. package/dist/launchd/index.d.ts +21 -0
  17. package/dist/launchd/index.d.ts.map +1 -0
  18. package/dist/launchd/index.js +969 -0
  19. package/dist/launchd/index.js.map +25 -0
  20. package/dist/launchd/job-form.d.ts +90 -0
  21. package/dist/launchd/job-form.d.ts.map +1 -0
  22. package/dist/launchd/job-lifecycle.d.ts +34 -0
  23. package/dist/launchd/job-lifecycle.d.ts.map +1 -0
  24. package/dist/launchd/job-preflight.d.ts +50 -0
  25. package/dist/launchd/job-preflight.d.ts.map +1 -0
  26. package/dist/launchd/job-validate.d.ts +25 -0
  27. package/dist/launchd/job-validate.d.ts.map +1 -0
  28. package/dist/launchd/job.d.ts +34 -0
  29. package/dist/launchd/job.d.ts.map +1 -0
  30. package/dist/launchd/launchctl.d.ts +62 -0
  31. package/dist/launchd/launchctl.d.ts.map +1 -0
  32. package/dist/launchd/local-runner.d.ts +7 -0
  33. package/dist/launchd/local-runner.d.ts.map +1 -0
  34. package/dist/launchd/plist.d.ts +41 -0
  35. package/dist/launchd/plist.d.ts.map +1 -0
  36. package/dist/launchd/providers.d.ts +15 -0
  37. package/dist/launchd/providers.d.ts.map +1 -0
  38. package/dist/launchd/runner.d.ts +91 -0
  39. package/dist/launchd/runner.d.ts.map +1 -0
  40. package/dist/launchd/secret-tripwire.d.ts +23 -0
  41. package/dist/launchd/secret-tripwire.d.ts.map +1 -0
  42. package/dist/openbao/auth-method-form.d.ts +7 -0
  43. package/dist/openbao/auth-method-form.d.ts.map +1 -1
  44. package/dist/openbao/auth-method-reconcile.d.ts +15 -0
  45. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
  46. package/dist/openbao/auth-method.d.ts +6 -0
  47. package/dist/openbao/auth-method.d.ts.map +1 -1
  48. package/dist/openbao/auth-role-form.d.ts +9 -0
  49. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  50. package/dist/openbao/auth-role.d.ts.map +1 -1
  51. package/dist/openbao/auth-token-form.d.ts +48 -0
  52. package/dist/openbao/auth-token-form.d.ts.map +1 -0
  53. package/dist/openbao/bao-identity.d.ts +81 -0
  54. package/dist/openbao/bao-identity.d.ts.map +1 -0
  55. package/dist/openbao/bao-namespace.d.ts +20 -0
  56. package/dist/openbao/bao-namespace.d.ts.map +1 -0
  57. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
  58. package/dist/openbao/host-approles.d.ts +62 -0
  59. package/dist/openbao/host-approles.d.ts.map +1 -0
  60. package/dist/openbao/index.d.ts +19 -0
  61. package/dist/openbao/index.d.ts.map +1 -1
  62. package/dist/openbao/index.js +1433 -305
  63. package/dist/openbao/index.js.map +34 -13
  64. package/dist/openbao/jwt-config-form.d.ts +51 -0
  65. package/dist/openbao/jwt-config-form.d.ts.map +1 -0
  66. package/dist/openbao/jwt-config.d.ts +25 -0
  67. package/dist/openbao/jwt-config.d.ts.map +1 -0
  68. package/dist/openbao/jwt-role-form.d.ts +91 -0
  69. package/dist/openbao/jwt-role-form.d.ts.map +1 -0
  70. package/dist/openbao/jwt-role.d.ts +31 -0
  71. package/dist/openbao/jwt-role.d.ts.map +1 -0
  72. package/dist/openbao/kubernetes-role-form.d.ts +68 -0
  73. package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
  74. package/dist/openbao/kubernetes-role.d.ts +25 -0
  75. package/dist/openbao/kubernetes-role.d.ts.map +1 -0
  76. package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
  77. package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
  78. package/dist/openbao/mfa-enforcement.d.ts +41 -0
  79. package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
  80. package/dist/openbao/mfa-totp-form.d.ts +52 -0
  81. package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
  82. package/dist/openbao/mfa-totp.d.ts +33 -0
  83. package/dist/openbao/mfa-totp.d.ts.map +1 -0
  84. package/dist/openbao/mfa-wire.d.ts +63 -0
  85. package/dist/openbao/mfa-wire.d.ts.map +1 -0
  86. package/dist/openbao/mount-form.d.ts +7 -0
  87. package/dist/openbao/mount-form.d.ts.map +1 -1
  88. package/dist/openbao/mount-move.d.ts +49 -0
  89. package/dist/openbao/mount-move.d.ts.map +1 -0
  90. package/dist/openbao/mount-path.d.ts +8 -4
  91. package/dist/openbao/mount-path.d.ts.map +1 -1
  92. package/dist/openbao/mount-reconcile.d.ts +16 -0
  93. package/dist/openbao/mount-reconcile.d.ts.map +1 -0
  94. package/dist/openbao/mount.d.ts +9 -0
  95. package/dist/openbao/mount.d.ts.map +1 -1
  96. package/dist/openbao/pki-role-canonical.d.ts +46 -0
  97. package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
  98. package/dist/openbao/pki-role-form.d.ts +38 -30
  99. package/dist/openbao/pki-role-form.d.ts.map +1 -1
  100. package/dist/openbao/pki-role.d.ts.map +1 -1
  101. package/dist/openbao/remount-wire.d.ts +44 -0
  102. package/dist/openbao/remount-wire.d.ts.map +1 -0
  103. package/dist/openbao/role-reconcile.d.ts +40 -0
  104. package/dist/openbao/role-reconcile.d.ts.map +1 -0
  105. package/docs/launchd.md +167 -0
  106. package/package.json +7 -2
  107. package/src/index.ts +1 -0
  108. package/src/launchd/fake-runner.ts +176 -0
  109. package/src/launchd/host-effect.ts +28 -0
  110. package/src/launchd/host-file-form.ts +71 -0
  111. package/src/launchd/host-file-lifecycle.test.ts +198 -0
  112. package/src/launchd/host-file-lifecycle.ts +164 -0
  113. package/src/launchd/host-file.ts +64 -0
  114. package/src/launchd/host-lookup.test.ts +92 -0
  115. package/src/launchd/host-lookup.ts +94 -0
  116. package/src/launchd/index.ts +26 -0
  117. package/src/launchd/job-form.test.ts +171 -0
  118. package/src/launchd/job-form.ts +180 -0
  119. package/src/launchd/job-identity.test.ts +147 -0
  120. package/src/launchd/job-lifecycle.test.ts +220 -0
  121. package/src/launchd/job-lifecycle.ts +194 -0
  122. package/src/launchd/job-preflight.ts +152 -0
  123. package/src/launchd/job-validate.ts +134 -0
  124. package/src/launchd/job.ts +96 -0
  125. package/src/launchd/launchctl.test.ts +181 -0
  126. package/src/launchd/launchctl.ts +188 -0
  127. package/src/launchd/local-runner.test.ts +125 -0
  128. package/src/launchd/local-runner.ts +143 -0
  129. package/src/launchd/plist.test.ts +186 -0
  130. package/src/launchd/plist.ts +156 -0
  131. package/src/launchd/providers.test.ts +222 -0
  132. package/src/launchd/providers.ts +21 -0
  133. package/src/launchd/runner.ts +95 -0
  134. package/src/launchd/secret-tripwire.test.ts +110 -0
  135. package/src/launchd/secret-tripwire.ts +81 -0
  136. package/src/openbao/README.md +181 -0
  137. package/src/openbao/REPLACE.md +87 -0
  138. package/src/openbao/auth-method-form.ts +7 -0
  139. package/src/openbao/auth-method-reconcile.ts +74 -0
  140. package/src/openbao/auth-method.ts +20 -41
  141. package/src/openbao/auth-role-form.test.ts +16 -1
  142. package/src/openbao/auth-role-form.ts +11 -0
  143. package/src/openbao/auth-role.ts +12 -0
  144. package/src/openbao/auth-token-form.ts +161 -0
  145. package/src/openbao/bao-identity.test.ts +131 -0
  146. package/src/openbao/bao-identity.ts +188 -0
  147. package/src/openbao/bao-namespace.ts +32 -0
  148. package/src/openbao/cloudflare-roles-expand.ts +2 -5
  149. package/src/openbao/host-approles.test.ts +121 -0
  150. package/src/openbao/host-approles.ts +126 -0
  151. package/src/openbao/index.ts +19 -0
  152. package/src/openbao/jwt-config-form.ts +161 -0
  153. package/src/openbao/jwt-config.ts +85 -0
  154. package/src/openbao/jwt-role-form.test.ts +130 -0
  155. package/src/openbao/jwt-role-form.ts +231 -0
  156. package/src/openbao/jwt-role.ts +102 -0
  157. package/src/openbao/kubernetes-role-form.test.ts +82 -0
  158. package/src/openbao/kubernetes-role-form.ts +149 -0
  159. package/src/openbao/kubernetes-role.ts +94 -0
  160. package/src/openbao/mfa-enforcement-form.ts +147 -0
  161. package/src/openbao/mfa-enforcement.ts +143 -0
  162. package/src/openbao/mfa-totp-form.ts +155 -0
  163. package/src/openbao/mfa-totp.ts +118 -0
  164. package/src/openbao/mfa-wire.ts +117 -0
  165. package/src/openbao/mfa.test.ts +200 -0
  166. package/src/openbao/mount-form.ts +9 -1
  167. package/src/openbao/mount-move.test.ts +164 -0
  168. package/src/openbao/mount-move.ts +113 -0
  169. package/src/openbao/mount-path.test.ts +11 -1
  170. package/src/openbao/mount-path.ts +17 -11
  171. package/src/openbao/mount-reconcile.ts +75 -0
  172. package/src/openbao/mount.ts +26 -43
  173. package/src/openbao/pki-role-canonical.ts +163 -0
  174. package/src/openbao/pki-role-form.test.ts +112 -0
  175. package/src/openbao/pki-role-form.ts +76 -110
  176. package/src/openbao/pki-role.ts +12 -27
  177. package/src/openbao/remount-wire.test.ts +87 -0
  178. package/src/openbao/remount-wire.ts +112 -0
  179. package/src/openbao/role-reconcile.test.ts +104 -0
  180. package/src/openbao/role-reconcile.ts +84 -0
@@ -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 trimTrailingSlashes = (text) => {
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 trimSlashes = (text) => {
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 trimTrailingSlashes(text.slice(start));
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 Effect4 from "effect/Effect";
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.replace(/\/+$/, "");
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/auth-method.ts
450
- var BaoAuthMethod = Resource("Bao.AuthMethod", {
451
- defaultRemovalPolicy: "retain"
454
+ // src/openbao/mount-move.ts
455
+ import * as Effect5 from "effect/Effect";
456
+
457
+ // src/openbao/remount-wire.ts
458
+ import * as Effect4 from "effect/Effect";
459
+ class RemountError extends Error {
460
+ constructor(message) {
461
+ super(message);
462
+ this.name = "RemountError";
463
+ }
464
+ }
465
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
466
+ var startRemount = (from, to) => Effect4.flatMap(baoCall("write", "POST", "sys/remount", { from, to }), (body) => {
467
+ const id = record(body?.["data"])?.["migration_id"];
468
+ return typeof id === "string" && id !== "" ? Effect4.succeed(id) : Effect4.fail(new RemountError(`sys/remount ${from} -> ${to} answered no migration_id.`));
469
+ });
470
+ var remountStatus = (id) => Effect4.map(baoRead(`sys/remount/status/${encodeURIComponent(id)}`), (data) => {
471
+ if (data === undefined)
472
+ return;
473
+ const status = record(data["migration_info"])?.["status"];
474
+ return typeof status === "string" ? status : "unknown";
475
+ });
476
+ var remountAndWait = (from, to, confirm, options = {}) => Effect4.gen(function* () {
477
+ const interval = options.intervalMillis ?? 1000;
478
+ const attempts = Math.max(1, options.attempts ?? 120);
479
+ const id = yield* startRemount(from, to);
480
+ const named = `sys/remount ${from} -> ${to} (migration ${id})`;
481
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
482
+ const status = yield* remountStatus(id);
483
+ if (status === "success")
484
+ return;
485
+ if (status === "failure") {
486
+ return yield* Effect4.fail(new RemountError(`${named} FAILED. The server log carries the reason, tagged with that migration_id. ` + "Read sys/mounts (or sys/auth) before retrying: the source may be tainted."));
487
+ }
488
+ if (status === undefined) {
489
+ if (yield* confirm)
490
+ return;
491
+ return yield* Effect4.fail(new RemountError(`${named}: the server no longer knows the migration (its tracker is in memory, lost ` + "on a restart or leader change), and the mount table does not show the move done."));
492
+ }
493
+ if (attempt < attempts)
494
+ yield* Effect4.sleep(interval);
495
+ }
496
+ return yield* Effect4.fail(new RemountError(`${named} is still in progress after ${String(attempts)} status reads. Do not re-run ` + `until sys/remount/status/${id} settles.`));
497
+ });
498
+
499
+ // src/openbao/mount-move.ts
500
+ var planMove = (family, stated, path, remountFrom) => {
501
+ const to = mountPath(path);
502
+ const from = remountFrom === undefined ? undefined : mountPath(remountFrom);
503
+ if (stated === undefined) {
504
+ return from === undefined || from === to ? { kind: "stay" } : { kind: "move", from };
505
+ }
506
+ const was = mountPath(stated);
507
+ if (was === to)
508
+ return { kind: "stay" };
509
+ if (from === undefined) {
510
+ return {
511
+ kind: "refuse",
512
+ message: `${family}: path changed from ${was} to ${to}. That would enable an EMPTY mount at ${to} ` + `and strand everything under ${was}. To move it with its data, declare ` + `\`remountFrom: '${was}'\` (leases under it are revoked; policies are not rewritten). ` + "For a new, empty mount, give it a new resource id instead."
513
+ };
514
+ }
515
+ if (from !== was) {
516
+ return {
517
+ kind: "refuse",
518
+ message: `${family}: remountFrom is ${from}, but the mount in state is ${was}.`
519
+ };
520
+ }
521
+ return { kind: "move", from };
522
+ };
523
+ var refuse = (message) => Effect5.die(new Error(message));
524
+ var performMove = (target, from, options) => Effect5.gen(function* () {
525
+ const to = mountPath(target.path);
526
+ const source = yield* target.read(from);
527
+ const landed = yield* target.read(to);
528
+ if (source !== undefined && landed !== undefined) {
529
+ return yield* refuse(`${target.family}: both ${from} and ${to} exist, so remountFrom cannot move one onto the ` + "other. Decide which holds the data, and retire the other by hand.");
530
+ }
531
+ if (source === undefined && landed === undefined) {
532
+ return yield* refuse(`${target.family}: remountFrom ${from} names no mount, and ${to} does not exist either. ` + "Refusing to enable an empty mount where data was expected.");
533
+ }
534
+ if (source === undefined)
535
+ return;
536
+ if (source.type !== target.type) {
537
+ return yield* refuse(`${target.family}: ${from} is type ${source.type}, not ${target.type}. A remount keeps the ` + "type; fix the declaration first.");
538
+ }
539
+ const confirm = Effect5.map(Effect5.all([target.read(from), target.read(to)]), ([gone, there]) => gone === undefined && there !== undefined);
540
+ yield* remountAndWait(target.wire(from), target.wire(to), confirm, options);
452
541
  });
453
- var readMethod = (props) => Effect4.gen(function* () {
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 BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect4.succeed(BaoAuthMethod.Provider.of({
460
- list: () => Effect4.succeed([]),
461
- read: Effect4.fn(function* ({ olds }) {
550
+ var moveTarget = (props) => ({
551
+ family: "Bao.AuthMethod",
552
+ path: props.path,
553
+ read: (path) => readMethod({ ...props, path }),
554
+ type: props.type,
555
+ wire: (path) => `auth/${authPath(path)}`
556
+ });
557
+ var reconcileAuthMethod = (news, stated, options) => Effect6.gen(function* () {
558
+ const move = planMove("Bao.AuthMethod", stated, news.path, news.remountFrom);
559
+ if (move.kind === "refuse")
560
+ return yield* Effect6.die(new Error(move.message));
561
+ if (move.kind === "move")
562
+ yield* performMove(moveTarget(news), move.from, options);
563
+ let live = yield* readMethod(news);
564
+ if (live === undefined) {
565
+ yield* enableAuthMethod(news);
566
+ if (wantsTune2(news))
567
+ yield* tuneAuthMethod(news);
568
+ } else if (!matches2(live, news)) {
569
+ if (live.type !== news.type) {
570
+ return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
571
+ }
572
+ yield* tuneAuthMethod(news);
573
+ }
574
+ live = yield* readMethod(news);
575
+ if (live === undefined) {
576
+ return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
577
+ }
578
+ return live;
579
+ });
580
+
581
+ // src/openbao/auth-method.ts
582
+ var BaoAuthMethod = Resource("Bao.AuthMethod", {
583
+ defaultRemovalPolicy: "retain"
584
+ });
585
+ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect7.succeed(BaoAuthMethod.Provider.of({
586
+ list: () => Effect7.succeed([]),
587
+ read: Effect7.fn(function* ({ olds }) {
462
588
  return yield* readMethod(olds);
463
589
  }),
464
- diff: Effect4.fn(function* ({ news, output }) {
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: Effect4.fn(function* ({ news }) {
477
- let live = yield* readMethod(news);
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: Effect4.fn(function* ({ output }) {
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 Effect5 from "effect/Effect";
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) => Effect5.gen(function* () {
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, Effect5.succeed(BaoAuthRole.Provider.of({
574
- list: () => Effect5.succeed([]),
575
- read: Effect5.fn(function* ({ olds }) {
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: Effect5.fn(function* ({ news, output }) {
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: Effect5.fn(function* ({ news }) {
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* Effect5.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
714
+ return yield* Effect8.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
596
715
  }
597
716
  return after;
598
717
  }),
599
- delete: Effect5.fn(function* ({ output }) {
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 Effect8 from "effect/Effect";
807
+ import * as Effect12 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 Effect7 from "effect/Effect";
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 Effect6 from "effect/Effect";
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) => Effect6.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
1062
+ var readCloudflareRole = (mount, name) => Effect10.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
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 Effect7.fail(new CloudflareGroupsError(mount, problems));
1084
+ return Effect11.fail(new CloudflareGroupsError(mount, problems));
886
1085
  }
887
- return Effect7.succeed(new Map(refs.flatMap((ref) => {
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) => Effect7.catchIf(read, (error) => error.status === 403, () => Effect7.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
896
- var engineGroups = (mount) => Effect7.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
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 Effect7.fail(new CloudflareGroupsError(mount, [why]));
1099
+ return Effect11.fail(new CloudflareGroupsError(mount, [why]));
901
1100
  }
902
1101
  if (!Array.isArray(list)) {
903
- return Effect7.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
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 ? Effect7.fail(new CloudflareGroupsError(mount, problems)) : Effect7.succeed(new Map([...atEveryScope(sole), ...scoped]));
1129
+ return problems.length > 0 ? Effect11.fail(new CloudflareGroupsError(mount, problems)) : Effect11.succeed(new Map([...atEveryScope(sole), ...scoped]));
931
1130
  });
932
- var permissionGroupsFromEngine = () => Effect7.sync(() => {
1131
+ var permissionGroupsFromEngine = () => Effect11.sync(() => {
933
1132
  const cache = new Map;
934
- const cachedFor = (mount) => Effect7.suspend(() => {
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 Effect7.flatMap(Effect7.cached(engineGroups(mount)), (lookup) => {
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) => Effect7.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
1143
+ resolve: (mount, refs) => Effect11.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
945
1144
  });
946
1145
  });
947
- var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
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 = Effect7.gen(function* () {
1157
+ const solve = Effect11.gen(function* () {
959
1158
  const live = new Map;
960
- yield* Effect7.all(roles.map((role) => Effect7.map(readCloudflareRole(role.mount, role.name), (read) => {
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* Effect7.fail(new CloudflareGroupsError(`account ${account}`, problems));
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* Effect7.cached(solve));
1171
+ solved.set(account, yield* Effect11.cached(solve));
973
1172
  }
974
1173
  return CloudflarePermissionGroups.of({
975
1174
  resolve: (mount, refs) => {
@@ -977,10 +1176,10 @@ 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 Effect7.fail(new CloudflareGroupsError(mount, [why]));
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 Effect7.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
1182
+ return Effect11.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
984
1183
  }
985
1184
  });
986
1185
  });
@@ -989,9 +1188,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect7.gen(function* () {
989
1188
  var BaoCloudflareRole = Resource3("Bao.CloudflareRole", {
990
1189
  defaultRemovalPolicy: "retain"
991
1190
  });
992
- var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect8.gen(function* () {
1191
+ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect12.gen(function* () {
993
1192
  const groups = yield* CloudflarePermissionGroups;
994
- const resolved = (props) => Effect8.gen(function* () {
1193
+ const resolved = (props) => Effect12.gen(function* () {
995
1194
  const refs = new Map;
996
1195
  for (const policy of props.policies) {
997
1196
  const scope = policy.scope ?? scopeOfResource(policy.resource);
@@ -1001,17 +1200,17 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
1001
1200
  const ids = yield* groups.resolve(props.mount, [...refs.values()]);
1002
1201
  const { missing, policies } = resolvePolicies(props.policies, ids);
1003
1202
  if (missing.length > 0) {
1004
- return yield* Effect8.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
1203
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
1005
1204
  }
1006
1205
  return policies;
1007
1206
  });
1008
1207
  return BaoCloudflareRole.Provider.of({
1009
- list: () => Effect8.succeed([]),
1010
- read: Effect8.fn(function* ({ olds }) {
1208
+ list: () => Effect12.succeed([]),
1209
+ read: Effect12.fn(function* ({ olds }) {
1011
1210
  const live = yield* readCloudflareRole(olds.mount, olds.name);
1012
1211
  return live === undefined ? undefined : attributesOf4(olds, live);
1013
1212
  }),
1014
- diff: Effect8.fn(function* ({ news, output }) {
1213
+ diff: Effect12.fn(function* ({ news, output }) {
1015
1214
  if (output === undefined || !isResolved3(news))
1016
1215
  return;
1017
1216
  if (refusalOf(news) !== undefined)
@@ -1022,11 +1221,11 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
1022
1221
  return { action: "update" };
1023
1222
  return differences(news, policies, live).length === 0 ? { action: "noop" } : { action: "update" };
1024
1223
  }),
1025
- reconcile: Effect8.fn(function* ({ news }) {
1224
+ reconcile: Effect12.fn(function* ({ news }) {
1026
1225
  const path = rolePath(news.mount, news.name);
1027
1226
  const refusal2 = refusalOf(news);
1028
1227
  if (refusal2 !== undefined) {
1029
- return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
1228
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
1030
1229
  }
1031
1230
  const policies = yield* resolved(news);
1032
1231
  const live = yield* readCloudflareRole(news.mount, news.name);
@@ -1035,22 +1234,22 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
1035
1234
  }
1036
1235
  const after = yield* readCloudflareRole(news.mount, news.name);
1037
1236
  if (after === undefined) {
1038
- return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
1237
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
1039
1238
  }
1040
1239
  const left = differences(news, policies, after);
1041
1240
  if (left.length > 0) {
1042
- return yield* Effect8.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
1241
+ return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
1043
1242
  }
1044
1243
  return attributesOf4(news, after);
1045
1244
  }),
1046
- delete: Effect8.fn(function* ({ output }) {
1245
+ delete: Effect12.fn(function* ({ output }) {
1047
1246
  yield* deleteCloudflareRole(output.mount, output.name);
1048
1247
  return;
1049
1248
  })
1050
1249
  });
1051
1250
  }));
1052
1251
  // src/openbao/cloudflare-roles-expand.ts
1053
- var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1252
+ var slug = (name) => trimRuns(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-");
1054
1253
  var zoneSlug = (zone) => `${slug(zone.name)}${zone.internal ? "-internal" : ""}`;
1055
1254
  var mountName = (account, surface) => `cloudflare-${account}-${surface}`;
1056
1255
  var policiesFor = (tpl, accountId, zone, accountGroups) => {
@@ -1145,12 +1344,12 @@ class RolesConfigError extends Error {
1145
1344
  }
1146
1345
  }
1147
1346
  var isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1148
- var record = (value, where) => {
1347
+ var record2 = (value, where) => {
1149
1348
  if (!isRaw(value))
1150
1349
  throw new RolesConfigError(where, "expected a mapping");
1151
1350
  return value;
1152
1351
  };
1153
- var text3 = (raw, key, where) => {
1352
+ var text4 = (raw, key, where) => {
1154
1353
  const value = raw[key];
1155
1354
  if (typeof value === "string")
1156
1355
  return value;
@@ -1174,35 +1373,35 @@ var entries = (raw, key, where) => {
1174
1373
  return value;
1175
1374
  };
1176
1375
  var template = (value, where) => {
1177
- const raw = record(value, where);
1376
+ const raw = record2(value, where);
1178
1377
  const groups = texts(raw, "groups", where);
1179
1378
  if (groups === undefined)
1180
1379
  throw new RolesConfigError(`${where}.groups`, "is required");
1181
- const bucket = raw["bucket"] === undefined ? undefined : text3(raw, "bucket", where);
1380
+ const bucket = raw["bucket"] === undefined ? undefined : text4(raw, "bucket", where);
1182
1381
  if (bucket !== undefined && bucket.length === 0) {
1183
1382
  throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
1184
1383
  }
1185
- const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text3(raw, "groups_scope", where));
1384
+ const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text4(raw, "groups_scope", where));
1186
1385
  if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
1187
1386
  throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
1188
1387
  }
1189
1388
  return {
1190
1389
  accountGroups: texts(raw, "account_groups", where) ?? [],
1191
1390
  bucket,
1192
- bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text3(raw, "bucket_jurisdiction", where),
1193
- desc: raw["desc"] === undefined ? "" : text3(raw, "desc", where),
1391
+ bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text4(raw, "bucket_jurisdiction", where),
1392
+ desc: raw["desc"] === undefined ? "" : text4(raw, "desc", where),
1194
1393
  groups,
1195
1394
  groupsScope,
1196
- maxTtl: text3(raw, "max_ttl", where),
1197
- name: text3(raw, "name", where),
1198
- ttl: text3(raw, "ttl", where),
1395
+ maxTtl: text4(raw, "max_ttl", where),
1396
+ name: text4(raw, "name", where),
1397
+ ttl: text4(raw, "ttl", where),
1199
1398
  zoneTypes: texts(raw, "zone_types", where) ?? [],
1200
1399
  zones: texts(raw, "zones", where)
1201
1400
  };
1202
1401
  };
1203
1402
  var surface = (name, value) => {
1204
1403
  const where = `surfaces.${name}`;
1205
- const raw = record(value, where);
1404
+ const raw = record2(value, where);
1206
1405
  return {
1207
1406
  accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
1208
1407
  zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
@@ -1210,7 +1409,7 @@ var surface = (name, value) => {
1210
1409
  };
1211
1410
  var account = (key, value, surfaces) => {
1212
1411
  const where = `accounts.${key}`;
1213
- const raw = record(value, where);
1412
+ const raw = record2(value, where);
1214
1413
  const names = texts(raw, "surfaces", where) ?? [];
1215
1414
  for (const name of names) {
1216
1415
  if (surfaces[name] === undefined) {
@@ -1218,56 +1417,941 @@ var account = (key, value, surfaces) => {
1218
1417
  }
1219
1418
  }
1220
1419
  const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
1221
- return { id: text3(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
1420
+ return { id: text4(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
1222
1421
  };
1223
1422
  var parseRolesConfig = (yaml) => {
1224
- const root = record(Bun.YAML.parse(yaml), "(document)");
1423
+ const root = record2(Bun.YAML.parse(yaml), "(document)");
1225
1424
  const surfaces = {};
1226
- for (const [name, spec] of Object.entries(record(root["surfaces"], "surfaces"))) {
1425
+ for (const [name, spec] of Object.entries(record2(root["surfaces"], "surfaces"))) {
1227
1426
  surfaces[name] = surface(name, spec);
1228
1427
  }
1229
- const accounts = Object.entries(record(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
1428
+ const accounts = Object.entries(record2(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
1230
1429
  return {
1231
1430
  accounts,
1232
- namespace: text3(record(root["defaults"], "defaults"), "namespace", "defaults"),
1431
+ namespace: text4(record2(root["defaults"], "defaults"), "namespace", "defaults"),
1233
1432
  surfaces
1234
1433
  };
1235
1434
  };
1236
- // src/openbao/mount.ts
1435
+ // src/openbao/host-approles.ts
1436
+ var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1437
+ var HOST_ROLE_SEPARATOR = "--";
1438
+ var hostRoleName = (className, host) => `${className}${HOST_ROLE_SEPARATOR}${host}`;
1439
+ var classProblems = (name, spec) => {
1440
+ const found = [];
1441
+ if (!NAME.test(name))
1442
+ found.push(`class \`${name}\` is not lowercase letters, digits, single hyphens`);
1443
+ if (spec.policies.length === 0)
1444
+ found.push(`class \`${name}\` has no policies`);
1445
+ if (spec.policies.includes("root"))
1446
+ found.push(`class \`${name}\` grants \`root\``);
1447
+ const secretId = parseDuration(spec.secretIdTtl);
1448
+ if (secretId === undefined || secretId <= 0) {
1449
+ found.push(`class \`${name}\` secretIdTtl \`${spec.secretIdTtl}\` must be a duration above zero ` + "(0 never expires)");
1450
+ }
1451
+ const ttl = parseDuration(spec.tokenTtl);
1452
+ const max = parseDuration(spec.tokenMaxTtl);
1453
+ if (ttl === undefined || max === undefined) {
1454
+ found.push(`class \`${name}\` token TTLs must be durations like 15m or 1h`);
1455
+ } else if (max > 0 && ttl > max) {
1456
+ found.push(`class \`${name}\` tokenTtl is longer than tokenMaxTtl`);
1457
+ }
1458
+ return found;
1459
+ };
1460
+ var hostAppRoles = (input) => {
1461
+ const problems = Object.entries(input.classes).flatMap(([name, spec]) => classProblems(name, spec));
1462
+ const seen = new Set;
1463
+ const roles = [];
1464
+ for (const host of input.hosts) {
1465
+ if (!NAME.test(host.name)) {
1466
+ problems.push(`host \`${host.name}\` is not lowercase letters, digits, single hyphens`);
1467
+ }
1468
+ if (seen.has(host.name))
1469
+ problems.push(`host \`${host.name}\` is listed twice`);
1470
+ seen.add(host.name);
1471
+ const spec = input.classes[host.class];
1472
+ if (spec === undefined) {
1473
+ problems.push(`host \`${host.name}\` names unknown class \`${host.class}\``);
1474
+ continue;
1475
+ }
1476
+ roles.push({
1477
+ bindSecretId: true,
1478
+ name: hostRoleName(host.class, host.name),
1479
+ secretIdNumUses: spec.secretIdNumUses ?? 0,
1480
+ secretIdTtl: spec.secretIdTtl,
1481
+ tokenMaxTtl: spec.tokenMaxTtl,
1482
+ tokenPolicies: [...new Set(spec.policies)].sort(),
1483
+ tokenTtl: spec.tokenTtl
1484
+ });
1485
+ }
1486
+ if (problems.length > 0) {
1487
+ throw new Error(`hostAppRoles refused the input: ${problems.join("; ")}.`);
1488
+ }
1489
+ return roles.sort((a, b) => a.name.localeCompare(b.name));
1490
+ };
1491
+ // src/openbao/jwt-config.ts
1237
1492
  import { Resource as Resource4 } from "alchemy";
1238
1493
  import { isResolved as isResolved4 } from "alchemy/Diff";
1239
1494
  import * as Provider4 from "alchemy/Provider";
1240
- import * as Effect10 from "effect/Effect";
1495
+ import * as Effect14 from "effect/Effect";
1496
+
1497
+ // src/openbao/jwt-config-form.ts
1498
+ var text5 = (value) => typeof value === "string" ? value.trim() : "";
1499
+ var strings2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1500
+ var sortedSet = (values) => [...new Set(values.map((value) => value.trim()))].sort();
1501
+ var canonical2 = (input) => ({
1502
+ boundIssuer: input.boundIssuer.trim(),
1503
+ defaultRole: input.defaultRole.trim(),
1504
+ jwksCaPem: input.jwksCaPem.trim(),
1505
+ jwksUrl: input.jwksUrl.trim(),
1506
+ jwtSupportedAlgs: sortedSet(input.jwtSupportedAlgs),
1507
+ jwtValidationPubkeys: sortedSet(input.jwtValidationPubkeys),
1508
+ oidcDiscoveryCaPem: input.oidcDiscoveryCaPem.trim(),
1509
+ oidcDiscoveryUrl: input.oidcDiscoveryUrl.trim()
1510
+ });
1511
+ var canonicalFromProps = (props) => canonical2({
1512
+ boundIssuer: props.boundIssuer ?? "",
1513
+ defaultRole: props.defaultRole ?? "",
1514
+ jwksCaPem: props.jwksCaPem ?? "",
1515
+ jwksUrl: props.jwksUrl ?? "",
1516
+ jwtSupportedAlgs: props.jwtSupportedAlgs ?? [],
1517
+ jwtValidationPubkeys: props.jwtValidationPubkeys ?? [],
1518
+ oidcDiscoveryCaPem: props.oidcDiscoveryCaPem ?? "",
1519
+ oidcDiscoveryUrl: props.oidcDiscoveryUrl ?? ""
1520
+ });
1521
+ var canonicalFromLive = (live) => canonical2({
1522
+ boundIssuer: text5(live["bound_issuer"]),
1523
+ defaultRole: text5(live["default_role"]),
1524
+ jwksCaPem: text5(live["jwks_ca_pem"]),
1525
+ jwksUrl: text5(live["jwks_url"]),
1526
+ jwtSupportedAlgs: strings2(live["jwt_supported_algs"]),
1527
+ jwtValidationPubkeys: strings2(live["jwt_validation_pubkeys"]),
1528
+ oidcDiscoveryCaPem: text5(live["oidc_discovery_ca_pem"]),
1529
+ oidcDiscoveryUrl: text5(live["oidc_discovery_url"])
1530
+ });
1531
+ var mountOf = (props) => mountPath(props.mount ?? "jwt");
1532
+ var configPath = (props) => `auth/${mountOf(props)}/config`;
1533
+ var attributesOf5 = (props, live) => {
1534
+ const form = canonicalFromLive(live);
1535
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf(props) };
1536
+ };
1537
+ var matches4 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps(props)));
1538
+ var writeBody3 = (props) => ({
1539
+ bound_issuer: props.boundIssuer ?? "",
1540
+ default_role: props.defaultRole ?? "",
1541
+ jwks_ca_pem: props.jwksCaPem ?? "",
1542
+ jwks_url: props.jwksUrl ?? "",
1543
+ jwt_supported_algs: [...props.jwtSupportedAlgs ?? []],
1544
+ jwt_validation_pubkeys: [...props.jwtValidationPubkeys ?? []],
1545
+ oidc_discovery_ca_pem: props.oidcDiscoveryCaPem ?? "",
1546
+ oidc_discovery_url: props.oidcDiscoveryUrl ?? ""
1547
+ });
1548
+ var wouldErase = (live) => {
1549
+ const lost = [];
1550
+ if (text5(live["oidc_client_id"]) !== "")
1551
+ lost.push("oidc_client_id (and its unreadable secret)");
1552
+ const provider = live["provider_config"];
1553
+ if (typeof provider === "object" && provider !== null && Object.keys(provider).length > 0) {
1554
+ lost.push("provider_config");
1555
+ }
1556
+ if (text5(live["oidc_response_mode"]) !== "")
1557
+ lost.push("oidc_response_mode");
1558
+ if (strings2(live["oidc_response_types"]).length > 0)
1559
+ lost.push("oidc_response_types");
1560
+ if (strings2(live["override_allowed_server_names"]).length > 0) {
1561
+ lost.push("override_allowed_server_names");
1562
+ }
1563
+ return lost;
1564
+ };
1565
+ var problems = (props) => {
1566
+ const found = [];
1567
+ const sources = [
1568
+ (props.oidcDiscoveryUrl ?? "") !== "",
1569
+ (props.jwksUrl ?? "") !== "",
1570
+ (props.jwtValidationPubkeys?.length ?? 0) > 0
1571
+ ].filter(Boolean).length;
1572
+ if (sources !== 1) {
1573
+ found.push("exactly one of oidcDiscoveryUrl, jwksUrl or jwtValidationPubkeys must be set");
1574
+ }
1575
+ if ((props.oidcDiscoveryUrl ?? "").includes(".well-known")) {
1576
+ found.push("oidcDiscoveryUrl must be the issuer, without /.well-known/openid-configuration");
1577
+ }
1578
+ const role = props.defaultRole ?? "";
1579
+ if (role !== role.toLowerCase())
1580
+ found.push(`defaultRole \`${role}\` must be lower case`);
1581
+ return found;
1582
+ };
1583
+
1584
+ // src/openbao/role-reconcile.ts
1585
+ import * as Effect13 from "effect/Effect";
1586
+ var readRoleAt = (spec) => Effect13.map(baoRead(spec.path), (live) => live === undefined ? undefined : { attributes: spec.attributesOf(live), live });
1587
+ var planRole = (spec) => Effect13.gen(function* () {
1588
+ if (spec.problems.length > 0)
1589
+ return "update";
1590
+ const found = yield* readRoleAt(spec);
1591
+ return found !== undefined && spec.matches(found.attributes) ? "noop" : "update";
1592
+ });
1593
+ var refuse2 = (spec, message) => Effect13.die(new Error(`${spec.family} ${spec.path}: ${message}`));
1594
+ var reconcileRole = (spec) => Effect13.gen(function* () {
1595
+ if (spec.problems.length > 0)
1596
+ return yield* refuse2(spec, spec.problems.join("; "));
1597
+ const found = yield* readRoleAt(spec);
1598
+ if (found === undefined || !spec.matches(found.attributes)) {
1599
+ const lost = found === undefined ? [] : spec.wouldErase?.(found.live) ?? [];
1600
+ if (lost.length > 0) {
1601
+ return yield* refuse2(spec, `live carries ${lost.join(", ")}, which this resource does not declare and the write ` + "would reset. Fold it into the declaration, or clear it by hand first.");
1602
+ }
1603
+ yield* baoWrite("PUT", spec.path, spec.body);
1604
+ }
1605
+ const after = yield* readRoleAt(spec);
1606
+ if (after === undefined)
1607
+ return yield* refuse2(spec, "write reported success but it is absent.");
1608
+ if (!spec.matches(after.attributes)) {
1609
+ return yield* refuse2(spec, "wrote cleanly but read back different — OpenBao stored something other than what was " + "declared. Read it by hand before retrying.");
1610
+ }
1611
+ return after.attributes;
1612
+ });
1613
+
1614
+ // src/openbao/jwt-config.ts
1615
+ var BaoJwtAuthConfig = Resource4("Bao.JwtAuthConfig", {
1616
+ defaultRemovalPolicy: "retain"
1617
+ });
1618
+ var jwtConfigSpec = (props) => ({
1619
+ attributesOf: (live) => attributesOf5(props, live),
1620
+ body: writeBody3(props),
1621
+ family: "Bao.JwtAuthConfig",
1622
+ matches: (attributes) => matches4(attributes, props),
1623
+ path: configPath(props),
1624
+ problems: problems(props),
1625
+ wouldErase
1626
+ });
1627
+ var BaoJwtAuthConfigProvider = () => Provider4.effect(BaoJwtAuthConfig, Effect14.succeed(BaoJwtAuthConfig.Provider.of({
1628
+ list: () => Effect14.succeed([]),
1629
+ read: Effect14.fn(function* ({ olds }) {
1630
+ const found = yield* readRoleAt(jwtConfigSpec(olds));
1631
+ return found?.attributes;
1632
+ }),
1633
+ diff: Effect14.fn(function* ({ news, output }) {
1634
+ if (output === undefined || !isResolved4(news))
1635
+ return;
1636
+ if (mountOf(news) !== output.mount)
1637
+ return { action: "replace" };
1638
+ return { action: yield* planRole(jwtConfigSpec(news)) };
1639
+ }),
1640
+ reconcile: Effect14.fn(function* ({ news }) {
1641
+ return yield* reconcileRole(jwtConfigSpec(news));
1642
+ }),
1643
+ delete: () => Effect14.void
1644
+ })));
1645
+ // src/openbao/jwt-role.ts
1646
+ import { Resource as Resource5 } from "alchemy";
1647
+ import { isResolved as isResolved5 } from "alchemy/Diff";
1648
+ import * as Provider5 from "alchemy/Provider";
1649
+ import * as Effect15 from "effect/Effect";
1650
+
1651
+ // src/openbao/auth-token-form.ts
1652
+ var sortedSet2 = (values) => [...new Set(values)].sort();
1653
+ var cidr = (value) => value.trim().replace(/\/(32|128)$/, "");
1654
+ var strings3 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1655
+ var seconds2 = (text6) => parseDuration(text6 ?? "0") ?? -1;
1656
+ var tokenFields = (input) => ({
1657
+ tokenBoundCidrs: input.tokenBoundCidrs,
1658
+ tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
1659
+ tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
1660
+ tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
1661
+ tokenNumUses: input.tokenNumUses,
1662
+ tokenPeriodSeconds: input.tokenPeriodSeconds,
1663
+ tokenPolicies: input.tokenPolicies,
1664
+ tokenTtlSeconds: input.tokenTtlSeconds,
1665
+ tokenType: input.tokenType
1666
+ });
1667
+ var tokenForm = (input) => ({
1668
+ tokenBoundCidrs: sortedSet2(input.tokenBoundCidrs.map(cidr)),
1669
+ tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
1670
+ tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
1671
+ tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
1672
+ tokenNumUses: input.tokenNumUses,
1673
+ tokenPeriodSeconds: input.tokenPeriodSeconds,
1674
+ tokenPolicies: sortedSet2(input.tokenPolicies),
1675
+ tokenTtlSeconds: input.tokenTtlSeconds,
1676
+ tokenType: input.tokenType
1677
+ });
1678
+ var tokenFormOfProps = (props) => tokenForm({
1679
+ tokenBoundCidrs: props.tokenBoundCidrs ?? [],
1680
+ tokenExplicitMaxTtlSeconds: seconds2(props.tokenExplicitMaxTtl),
1681
+ tokenMaxTtlSeconds: seconds2(props.tokenMaxTtl),
1682
+ tokenNoDefaultPolicy: props.tokenNoDefaultPolicy ?? false,
1683
+ tokenNumUses: props.tokenNumUses ?? 0,
1684
+ tokenPeriodSeconds: seconds2(props.tokenPeriod),
1685
+ tokenPolicies: props.tokenPolicies,
1686
+ tokenTtlSeconds: seconds2(props.tokenTtl),
1687
+ tokenType: props.tokenType ?? "default"
1688
+ });
1689
+ var tokenFormOfLive = (live) => tokenForm({
1690
+ tokenBoundCidrs: strings3(live["token_bound_cidrs"]),
1691
+ tokenExplicitMaxTtlSeconds: ttlSeconds(live["token_explicit_max_ttl"]) ?? -1,
1692
+ tokenMaxTtlSeconds: ttlSeconds(live["token_max_ttl"]) ?? -1,
1693
+ tokenNoDefaultPolicy: live["token_no_default_policy"] === true,
1694
+ tokenNumUses: typeof live["token_num_uses"] === "number" ? live["token_num_uses"] : -1,
1695
+ tokenPeriodSeconds: ttlSeconds(live["token_period"]) ?? -1,
1696
+ tokenPolicies: strings3(live["token_policies"]),
1697
+ tokenTtlSeconds: ttlSeconds(live["token_ttl"]) ?? -1,
1698
+ tokenType: typeof live["token_type"] === "string" ? live["token_type"] : ""
1699
+ });
1700
+ var tokenBody = (props) => ({
1701
+ token_bound_cidrs: [...props.tokenBoundCidrs ?? []],
1702
+ token_explicit_max_ttl: props.tokenExplicitMaxTtl ?? "0",
1703
+ token_max_ttl: props.tokenMaxTtl ?? "0",
1704
+ token_no_default_policy: props.tokenNoDefaultPolicy ?? false,
1705
+ token_num_uses: props.tokenNumUses ?? 0,
1706
+ token_period: props.tokenPeriod ?? "0",
1707
+ token_policies: [...props.tokenPolicies],
1708
+ token_ttl: props.tokenTtl ?? "0",
1709
+ token_type: props.tokenType ?? "default"
1710
+ });
1711
+ var tokenProblems = (props) => {
1712
+ const found = [];
1713
+ const durations = {
1714
+ tokenExplicitMaxTtl: props.tokenExplicitMaxTtl,
1715
+ tokenMaxTtl: props.tokenMaxTtl,
1716
+ tokenPeriod: props.tokenPeriod,
1717
+ tokenTtl: props.tokenTtl
1718
+ };
1719
+ for (const [field, text6] of Object.entries(durations)) {
1720
+ if (seconds2(text6) < 0)
1721
+ found.push(`${field}=${text6 ?? ""} is not a duration (0 or <n>s|m|h|d)`);
1722
+ }
1723
+ const ttl = seconds2(props.tokenTtl);
1724
+ const max = seconds2(props.tokenMaxTtl);
1725
+ if (ttl > 0 && max > 0 && ttl > max)
1726
+ found.push("tokenTtl is longer than tokenMaxTtl");
1727
+ if (props.tokenType === "batch" && (seconds2(props.tokenPeriod) > 0 || (props.tokenNumUses ?? 0) > 0)) {
1728
+ found.push("a batch token cannot be periodic or use-limited (tokenutil.go:234-241)");
1729
+ }
1730
+ return found;
1731
+ };
1732
+
1733
+ // src/openbao/jwt-role-form.ts
1734
+ var sortedSet3 = (values) => [...new Set(values)].sort();
1735
+ var text6 = (value) => typeof value === "string" ? value : "";
1736
+ var strings4 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1737
+ var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1738
+ var claimsOf = (value) => {
1739
+ const source = record3(value);
1740
+ const out = {};
1741
+ for (const key of Object.keys(source).sort()) {
1742
+ const raw = source[key];
1743
+ out[key] = sortedSet3(typeof raw === "string" ? [raw] : strings4(raw));
1744
+ }
1745
+ return out;
1746
+ };
1747
+ var mappingsOf = (value) => {
1748
+ const source = record3(value);
1749
+ const out = {};
1750
+ for (const key of Object.keys(source).sort())
1751
+ out[key] = text6(source[key]);
1752
+ return out;
1753
+ };
1754
+ var canonical3 = (input) => ({
1755
+ allowedRedirectUris: sortedSet3(input.allowedRedirectUris),
1756
+ boundAudiences: sortedSet3(input.boundAudiences),
1757
+ boundClaims: claimsOf(input.boundClaims),
1758
+ boundClaimsType: input.boundClaimsType,
1759
+ boundSubject: input.boundSubject,
1760
+ callbackMode: input.callbackMode,
1761
+ claimMappings: mappingsOf(input.claimMappings),
1762
+ groupsClaim: input.groupsClaim,
1763
+ oidcDisableConfirmation: input.oidcDisableConfirmation,
1764
+ oidcScopes: sortedSet3(input.oidcScopes),
1765
+ roleType: input.roleType,
1766
+ userClaim: input.userClaim,
1767
+ verboseOidcLogging: input.verboseOidcLogging,
1768
+ ...tokenFields(input)
1769
+ });
1770
+ var canonicalFromProps2 = (props) => canonical3({
1771
+ ...tokenFormOfProps(props),
1772
+ allowedRedirectUris: props.allowedRedirectUris ?? [],
1773
+ boundAudiences: props.boundAudiences ?? [],
1774
+ boundClaims: claimsOf(props.boundClaims ?? {}),
1775
+ boundClaimsType: props.boundClaimsType ?? "string",
1776
+ boundSubject: props.boundSubject ?? "",
1777
+ callbackMode: props.callbackMode ?? "client",
1778
+ claimMappings: props.claimMappings ?? {},
1779
+ groupsClaim: props.groupsClaim ?? "",
1780
+ oidcDisableConfirmation: false,
1781
+ oidcScopes: props.oidcScopes ?? [],
1782
+ roleType: props.roleType,
1783
+ userClaim: props.userClaim,
1784
+ verboseOidcLogging: false
1785
+ });
1786
+ var canonicalFromLive2 = (live) => canonical3({
1787
+ ...tokenFormOfLive(live),
1788
+ allowedRedirectUris: strings4(live["allowed_redirect_uris"]),
1789
+ boundAudiences: strings4(live["bound_audiences"]),
1790
+ boundClaims: claimsOf(live["bound_claims"]),
1791
+ boundClaimsType: text6(live["bound_claims_type"]),
1792
+ boundSubject: text6(live["bound_subject"]),
1793
+ callbackMode: text6(live["callback_mode"]),
1794
+ claimMappings: mappingsOf(live["claim_mappings"]),
1795
+ groupsClaim: text6(live["groups_claim"]),
1796
+ oidcDisableConfirmation: live["oidc_disable_confirmation"] === true,
1797
+ oidcScopes: strings4(live["oidc_scopes"]),
1798
+ roleType: text6(live["role_type"]),
1799
+ userClaim: text6(live["user_claim"]),
1800
+ verboseOidcLogging: live["verbose_oidc_logging"] === true
1801
+ });
1802
+ var mountOf2 = (props) => mountPath(props.mount ?? "jwt");
1803
+ var rolePath2 = (props) => `auth/${mountOf2(props)}/role/${props.name}`;
1804
+ var attributesOf6 = (props, live) => {
1805
+ const form = canonicalFromLive2(live);
1806
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf2(props), name: props.name };
1807
+ };
1808
+ var matches5 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps2(props)));
1809
+ var writeBody4 = (props) => ({
1810
+ allowed_redirect_uris: [...props.allowedRedirectUris ?? []],
1811
+ bound_audiences: [...props.boundAudiences ?? []],
1812
+ bound_claims: { ...props.boundClaims },
1813
+ bound_claims_type: props.boundClaimsType ?? "string",
1814
+ bound_subject: props.boundSubject ?? "",
1815
+ callback_mode: props.callbackMode ?? "client",
1816
+ claim_mappings: { ...props.claimMappings },
1817
+ groups_claim: props.groupsClaim ?? "",
1818
+ oidc_disable_confirmation: false,
1819
+ oidc_scopes: [...props.oidcScopes ?? []],
1820
+ role_type: props.roleType,
1821
+ user_claim: props.userClaim,
1822
+ verbose_oidc_logging: false,
1823
+ ...tokenBody(props)
1824
+ });
1825
+ var NAME2 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
1826
+ var problems2 = (props) => {
1827
+ const found = [...tokenProblems(props)];
1828
+ if (!NAME2.test(props.name))
1829
+ found.push(`name \`${props.name}\` must be lower case (the server folds it)`);
1830
+ if (props.userClaim.trim() === "")
1831
+ found.push("userClaim is empty; the server requires one");
1832
+ const bound = (props.boundAudiences?.length ?? 0) > 0 || (props.boundSubject ?? "") !== "" || Object.keys(props.boundClaims ?? {}).length > 0 || (props.tokenBoundCidrs?.length ?? 0) > 0;
1833
+ if (props.roleType === "jwt" && !bound) {
1834
+ found.push("a jwt role needs a bound constraint: boundAudiences, boundSubject, boundClaims or tokenBoundCidrs");
1835
+ }
1836
+ if (props.roleType === "oidc" && (props.allowedRedirectUris?.length ?? 0) === 0) {
1837
+ found.push("an oidc role needs allowedRedirectUris");
1838
+ }
1839
+ const targets = Object.values(props.claimMappings ?? {});
1840
+ if (targets.includes("role"))
1841
+ found.push("claimMappings may not map to the reserved key `role`");
1842
+ if (new Set(targets).size !== targets.length)
1843
+ found.push("claimMappings maps two claims to one key");
1844
+ return found;
1845
+ };
1846
+
1847
+ // src/openbao/jwt-role.ts
1848
+ var BaoJwtRole = Resource5("Bao.JwtRole", {
1849
+ defaultRemovalPolicy: "retain"
1850
+ });
1851
+ var jwtRoleSpec = (props) => ({
1852
+ attributesOf: (live) => attributesOf6(props, live),
1853
+ body: writeBody4(props),
1854
+ family: "Bao.JwtRole",
1855
+ matches: (attributes) => matches5(attributes, props),
1856
+ path: rolePath2(props),
1857
+ problems: problems2(props)
1858
+ });
1859
+ var BaoJwtRoleProvider = () => Provider5.effect(BaoJwtRole, Effect15.succeed(BaoJwtRole.Provider.of({
1860
+ list: () => Effect15.succeed([]),
1861
+ read: Effect15.fn(function* ({ olds }) {
1862
+ const found = yield* readRoleAt(jwtRoleSpec(olds));
1863
+ return found?.attributes;
1864
+ }),
1865
+ diff: Effect15.fn(function* ({ news, output }) {
1866
+ if (output === undefined || !isResolved5(news))
1867
+ return;
1868
+ if (mountOf2(news) !== output.mount || news.name !== output.name) {
1869
+ return { action: "replace" };
1870
+ }
1871
+ return { action: yield* planRole(jwtRoleSpec(news)) };
1872
+ }),
1873
+ reconcile: Effect15.fn(function* ({ news }) {
1874
+ return yield* reconcileRole(jwtRoleSpec(news));
1875
+ }),
1876
+ delete: Effect15.fn(function* ({ output }) {
1877
+ yield* baoDelete(rolePath2(output));
1878
+ return;
1879
+ })
1880
+ })));
1881
+ // src/openbao/kubernetes-role.ts
1882
+ import { Resource as Resource6 } from "alchemy";
1883
+ import { isResolved as isResolved6 } from "alchemy/Diff";
1884
+ import * as Provider6 from "alchemy/Provider";
1885
+ import * as Effect16 from "effect/Effect";
1886
+
1887
+ // src/openbao/kubernetes-role-form.ts
1888
+ var sortedSet4 = (values) => [...new Set(values)].sort();
1889
+ var text7 = (value) => typeof value === "string" ? value : "";
1890
+ var strings5 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1891
+ var canonical4 = (input) => ({
1892
+ aliasNameSource: input.aliasNameSource,
1893
+ audience: input.audience,
1894
+ boundServiceAccountNames: sortedSet4(input.boundServiceAccountNames),
1895
+ boundServiceAccountNamespaceSelector: input.boundServiceAccountNamespaceSelector.trim(),
1896
+ boundServiceAccountNamespaces: sortedSet4(input.boundServiceAccountNamespaces),
1897
+ ...tokenFields(input)
1898
+ });
1899
+ var canonicalFromProps3 = (props) => canonical4({
1900
+ ...tokenFormOfProps(props),
1901
+ aliasNameSource: props.aliasNameSource,
1902
+ audience: props.audience ?? "",
1903
+ boundServiceAccountNames: props.boundServiceAccountNames,
1904
+ boundServiceAccountNamespaceSelector: props.boundServiceAccountNamespaceSelector ?? "",
1905
+ boundServiceAccountNamespaces: props.boundServiceAccountNamespaces ?? []
1906
+ });
1907
+ var canonicalFromLive3 = (live) => canonical4({
1908
+ ...tokenFormOfLive(live),
1909
+ aliasNameSource: text7(live["alias_name_source"]),
1910
+ audience: text7(live["audience"]),
1911
+ boundServiceAccountNames: strings5(live["bound_service_account_names"]),
1912
+ boundServiceAccountNamespaceSelector: text7(live["bound_service_account_namespace_selector"]),
1913
+ boundServiceAccountNamespaces: strings5(live["bound_service_account_namespaces"])
1914
+ });
1915
+ var mountOf3 = (props) => mountPath(props.mount ?? "kubernetes");
1916
+ var rolePath3 = (props) => `auth/${mountOf3(props)}/role/${props.name}`;
1917
+ var attributesOf7 = (props, live) => {
1918
+ const form = canonicalFromLive3(live);
1919
+ return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf3(props), name: props.name };
1920
+ };
1921
+ var matches6 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps3(props)));
1922
+ var writeBody5 = (props) => ({
1923
+ alias_name_source: props.aliasNameSource,
1924
+ audience: props.audience ?? "",
1925
+ bound_service_account_names: [...props.boundServiceAccountNames],
1926
+ bound_service_account_namespace_selector: props.boundServiceAccountNamespaceSelector ?? "",
1927
+ bound_service_account_namespaces: [...props.boundServiceAccountNamespaces ?? []],
1928
+ ...tokenBody(props)
1929
+ });
1930
+ var NAME3 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
1931
+ var starMixed = (values) => values.length > 1 && values.includes("*");
1932
+ var problems3 = (props) => {
1933
+ const found = [...tokenProblems(props)];
1934
+ const namespaces = props.boundServiceAccountNamespaces ?? [];
1935
+ const selector = (props.boundServiceAccountNamespaceSelector ?? "").trim();
1936
+ if (!NAME3.test(props.name))
1937
+ found.push(`name \`${props.name}\` must be lower case (stored folded)`);
1938
+ if (props.boundServiceAccountNames.length === 0)
1939
+ found.push("boundServiceAccountNames is empty");
1940
+ if (starMixed(props.boundServiceAccountNames))
1941
+ found.push("`*` mixed with other account names");
1942
+ if (namespaces.length === 0 && selector === "") {
1943
+ found.push("boundServiceAccountNamespaces is empty and no namespace selector is set");
1944
+ }
1945
+ if (starMixed(namespaces))
1946
+ found.push("`*` mixed with other namespaces");
1947
+ return found;
1948
+ };
1949
+
1950
+ // src/openbao/kubernetes-role.ts
1951
+ var BaoKubernetesRole = Resource6("Bao.KubernetesRole", {
1952
+ defaultRemovalPolicy: "retain"
1953
+ });
1954
+ var kubernetesRoleSpec = (props) => ({
1955
+ attributesOf: (live) => attributesOf7(props, live),
1956
+ body: writeBody5(props),
1957
+ family: "Bao.KubernetesRole",
1958
+ matches: (attributes) => matches6(attributes, props),
1959
+ path: rolePath3(props),
1960
+ problems: problems3(props)
1961
+ });
1962
+ var BaoKubernetesRoleProvider = () => Provider6.effect(BaoKubernetesRole, Effect16.succeed(BaoKubernetesRole.Provider.of({
1963
+ list: () => Effect16.succeed([]),
1964
+ read: Effect16.fn(function* ({ olds }) {
1965
+ const found = yield* readRoleAt(kubernetesRoleSpec(olds));
1966
+ return found?.attributes;
1967
+ }),
1968
+ diff: Effect16.fn(function* ({ news, output }) {
1969
+ if (output === undefined || !isResolved6(news))
1970
+ return;
1971
+ if (mountOf3(news) !== output.mount || news.name !== output.name) {
1972
+ return { action: "replace" };
1973
+ }
1974
+ return { action: yield* planRole(kubernetesRoleSpec(news)) };
1975
+ }),
1976
+ reconcile: Effect16.fn(function* ({ news }) {
1977
+ return yield* reconcileRole(kubernetesRoleSpec(news));
1978
+ }),
1979
+ delete: Effect16.fn(function* ({ output }) {
1980
+ yield* baoDelete(rolePath3(output));
1981
+ return;
1982
+ })
1983
+ })));
1984
+ // src/openbao/mfa-enforcement.ts
1985
+ import { Resource as Resource7 } from "alchemy";
1986
+ import { isResolved as isResolved7 } from "alchemy/Diff";
1987
+ import * as Provider7 from "alchemy/Provider";
1988
+ import * as Effect18 from "effect/Effect";
1989
+
1990
+ // src/openbao/mfa-enforcement-form.ts
1991
+ var sortedSet5 = (values) => [...new Set(values.map((value) => value.trim()))].filter((value) => value !== "").sort();
1992
+ var strings6 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1993
+ var text8 = (value) => typeof value === "string" ? value : "";
1994
+ var canonical5 = (input) => ({
1995
+ authMethodAccessors: sortedSet5(input.authMethodAccessors),
1996
+ authMethodTypes: sortedSet5(input.authMethodTypes),
1997
+ identityEntityIds: sortedSet5(input.identityEntityIds),
1998
+ identityGroupIds: sortedSet5(input.identityGroupIds),
1999
+ mfaMethodIds: sortedSet5(input.mfaMethodIds)
2000
+ });
2001
+ var canonicalFromProps4 = (props, resolved) => canonical5({
2002
+ authMethodAccessors: [...props.authMethodAccessors ?? [], ...resolved],
2003
+ authMethodTypes: props.authMethodTypes ?? [],
2004
+ identityEntityIds: props.identityEntityIds ?? [],
2005
+ identityGroupIds: props.identityGroupIds ?? [],
2006
+ mfaMethodIds: props.mfaMethodIds
2007
+ });
2008
+ var canonicalFromLive4 = (live) => canonical5({
2009
+ authMethodAccessors: strings6(live["auth_method_accessors"]),
2010
+ authMethodTypes: strings6(live["auth_method_types"]),
2011
+ identityEntityIds: strings6(live["identity_entity_ids"]),
2012
+ identityGroupIds: strings6(live["identity_group_ids"]),
2013
+ mfaMethodIds: strings6(live["mfa_method_ids"])
2014
+ });
2015
+ var attributesOf8 = (name, live) => {
2016
+ const form = canonicalFromLive4(live);
2017
+ return {
2018
+ ...form,
2019
+ digest: sha256(JSON.stringify(form)),
2020
+ enforcementId: text8(live["id"]),
2021
+ name,
2022
+ namespacePath: text8(live["namespace_path"])
2023
+ };
2024
+ };
2025
+ var matches7 = (attributes, props, resolved) => attributes.digest === sha256(JSON.stringify(canonicalFromProps4(props, resolved)));
2026
+ var writeBody6 = (form) => ({
2027
+ auth_method_accessors: [...form.authMethodAccessors],
2028
+ auth_method_types: [...form.authMethodTypes],
2029
+ identity_entity_ids: [...form.identityEntityIds],
2030
+ identity_group_ids: [...form.identityGroupIds],
2031
+ mfa_method_ids: [...form.mfaMethodIds]
2032
+ });
2033
+ var NAME4 = /^\w(?:[\w.-]*\w)?$/;
2034
+ var problems4 = (props, missingPaths) => {
2035
+ const found = [];
2036
+ if (!NAME4.test(props.name))
2037
+ found.push(`name \`${props.name}\` is not a valid enforcement name`);
2038
+ if (sortedSet5(props.mfaMethodIds).length === 0)
2039
+ found.push("mfaMethodIds is empty");
2040
+ const targets = [
2041
+ props.authMethodPaths,
2042
+ props.authMethodAccessors,
2043
+ props.authMethodTypes,
2044
+ props.identityGroupIds,
2045
+ props.identityEntityIds
2046
+ ].some((list) => sortedSet5(list ?? []).length > 0);
2047
+ if (!targets)
2048
+ found.push("no target: an enforcement with none is accepted and enforces NOTHING");
2049
+ if (missingPaths.length > 0) {
2050
+ found.push(`no auth mount at ${missingPaths.map((path) => mountPath(path)).join(", ")}`);
2051
+ }
2052
+ return found;
2053
+ };
2054
+
2055
+ // src/openbao/mfa-wire.ts
2056
+ import * as Effect17 from "effect/Effect";
2057
+ var TOTP_PATH = "identity/mfa/method/totp";
2058
+ var METHODS_PATH = "identity/mfa/method";
2059
+ var enforcementPath = (name) => `identity/mfa/login-enforcement/${name}`;
2060
+ var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
2061
+ var ownNamespacePath = Effect17.gen(function* () {
2062
+ const env = yield* BaoEnv;
2063
+ return namespacePath(envNamespace(env));
2064
+ });
2065
+ var findTotp = (name) => Effect17.gen(function* () {
2066
+ const listing = yield* baoRead(`${METHODS_PATH}?list=true`);
2067
+ const info = record4(listing?.["key_info"]) ?? {};
2068
+ const own = yield* ownNamespacePath;
2069
+ for (const [id, raw] of Object.entries(info)) {
2070
+ const live = record4(raw);
2071
+ if (live === undefined || live["name"] !== name || live["namespace_path"] !== own)
2072
+ continue;
2073
+ if (live["type"] !== "totp") {
2074
+ return yield* Effect17.die(new Error(`Bao.MfaTotpMethod ${name}: that name belongs to a ${String(live["type"])} method ` + `(${id}) in this namespace, and a TOTP write would convert it. Pick another name.`));
2075
+ }
2076
+ return { id, live };
2077
+ }
2078
+ return;
2079
+ });
2080
+ var writeTotp = (body) => Effect17.asVoid(baoCall("write", "POST", TOTP_PATH, body));
2081
+ var deleteTotp = (id) => baoDelete(`${TOTP_PATH}/${id}`);
2082
+ var readEnforcement = (name) => baoRead(enforcementPath(name));
2083
+ var writeEnforcement = (name, body) => Effect17.asVoid(baoCall("write", "POST", enforcementPath(name), body));
2084
+ var authAccessors = (paths) => Effect17.gen(function* () {
2085
+ if (paths.length === 0)
2086
+ return { accessors: [], missing: [] };
2087
+ const table = (yield* baoRead("sys/auth")) ?? {};
2088
+ const accessors = [];
2089
+ const missing = [];
2090
+ for (const path of paths) {
2091
+ const accessor = record4(table[`${mountPath(path)}/`])?.["accessor"];
2092
+ if (typeof accessor === "string" && accessor !== "")
2093
+ accessors.push(accessor);
2094
+ else
2095
+ missing.push(mountPath(path));
2096
+ }
2097
+ return { accessors, missing };
2098
+ });
2099
+
2100
+ // src/openbao/mfa-enforcement.ts
2101
+ var BaoMfaLoginEnforcement = Resource7("Bao.MfaLoginEnforcement", {
2102
+ defaultRemovalPolicy: "retain"
2103
+ });
2104
+ var refuse3 = (name, message) => Effect18.die(new Error(`Bao.MfaLoginEnforcement ${name}: ${message}`));
2105
+ var resolveTargets = (props) => Effect18.map(authAccessors(props.authMethodPaths ?? []), ({ accessors, missing }) => ({
2106
+ bad: problems4(props, missing),
2107
+ resolved: accessors
2108
+ }));
2109
+ var readLive = (name) => Effect18.map(readEnforcement(name), (live) => live === undefined ? undefined : attributesOf8(name, live));
2110
+ var planEnforcement = (props) => Effect18.gen(function* () {
2111
+ const { bad, resolved } = yield* resolveTargets(props);
2112
+ if (bad.length > 0)
2113
+ return "update";
2114
+ const live = yield* readLive(props.name);
2115
+ return live !== undefined && matches7(live, props, resolved) ? "noop" : "update";
2116
+ });
2117
+ var reconcileEnforcement = (props) => Effect18.gen(function* () {
2118
+ const { bad, resolved } = yield* resolveTargets(props);
2119
+ if (bad.length > 0)
2120
+ return yield* refuse3(props.name, bad.join("; "));
2121
+ const live = yield* readLive(props.name);
2122
+ if (live === undefined || !matches7(live, props, resolved)) {
2123
+ yield* writeEnforcement(props.name, writeBody6(canonicalFromProps4(props, resolved)));
2124
+ }
2125
+ const after = yield* readLive(props.name);
2126
+ if (after === undefined)
2127
+ return yield* refuse3(props.name, "the write succeeded but it is absent.");
2128
+ if (!matches7(after, props, resolved)) {
2129
+ return yield* refuse3(props.name, "the enforcement read back different.");
2130
+ }
2131
+ return after;
2132
+ });
2133
+ var BaoMfaLoginEnforcementProvider = () => Provider7.effect(BaoMfaLoginEnforcement, Effect18.succeed(BaoMfaLoginEnforcement.Provider.of({
2134
+ list: () => Effect18.succeed([]),
2135
+ read: Effect18.fn(function* ({ olds }) {
2136
+ return yield* readLive(olds.name);
2137
+ }),
2138
+ diff: Effect18.fn(function* ({ news, output }) {
2139
+ if (output === undefined || !isResolved7(news))
2140
+ return;
2141
+ if (news.name !== output.name)
2142
+ return { action: "replace" };
2143
+ return { action: yield* planEnforcement(news) };
2144
+ }),
2145
+ reconcile: Effect18.fn(function* ({ news }) {
2146
+ return yield* reconcileEnforcement(news);
2147
+ }),
2148
+ delete: Effect18.fn(function* ({ output }) {
2149
+ return yield* refuse3(output.name, "REFUSING to delete: OpenBao 2.6.2 drops a login enforcement from memory only, and it " + "returns on the next unseal (openbao/openbao#4030). Set this resource back to " + "retain to drop it from state; remove it for good only once the server is fixed.");
2150
+ })
2151
+ })));
2152
+ // src/openbao/mfa-totp.ts
2153
+ import { Resource as Resource8 } from "alchemy";
2154
+ import { isResolved as isResolved8 } from "alchemy/Diff";
2155
+ import * as Provider8 from "alchemy/Provider";
2156
+ import * as Effect19 from "effect/Effect";
2157
+
2158
+ // src/openbao/mfa-totp-form.ts
2159
+ var canonical6 = (input) => ({
2160
+ algorithm: input.algorithm,
2161
+ digits: input.digits,
2162
+ issuer: input.issuer,
2163
+ keySize: input.keySize,
2164
+ maxValidationAttempts: input.maxValidationAttempts,
2165
+ periodSeconds: input.periodSeconds,
2166
+ qrSize: input.qrSize,
2167
+ skew: input.skew
2168
+ });
2169
+ var canonicalFromProps5 = (props) => canonical6({
2170
+ algorithm: props.algorithm ?? "SHA1",
2171
+ digits: props.digits ?? 6,
2172
+ issuer: props.issuer,
2173
+ keySize: props.keySize ?? 20,
2174
+ maxValidationAttempts: props.maxValidationAttempts ?? 5,
2175
+ periodSeconds: parseDuration(props.period ?? "30s") ?? -1,
2176
+ qrSize: props.qrSize ?? 200,
2177
+ skew: props.skew ?? 1
2178
+ });
2179
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : -1;
2180
+ var text9 = (value) => typeof value === "string" ? value : "";
2181
+ var canonicalFromLive5 = (live) => canonical6({
2182
+ algorithm: text9(live["algorithm"]),
2183
+ digits: num(live["digits"]),
2184
+ issuer: text9(live["issuer"]),
2185
+ keySize: num(live["key_size"]),
2186
+ maxValidationAttempts: num(live["max_validation_attempts"]),
2187
+ periodSeconds: ttlSeconds(live["period"]) ?? -1,
2188
+ qrSize: num(live["qr_size"]),
2189
+ skew: num(live["skew"])
2190
+ });
2191
+ var attributesOf9 = (methodId, live) => {
2192
+ const form = canonicalFromLive5(live);
2193
+ return {
2194
+ ...form,
2195
+ digest: sha256(JSON.stringify(form)),
2196
+ methodId,
2197
+ name: text9(live["name"]),
2198
+ namespacePath: text9(live["namespace_path"])
2199
+ };
2200
+ };
2201
+ var matches8 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps5(props)));
2202
+ var writeBody7 = (props) => {
2203
+ const form = canonicalFromProps5(props);
2204
+ return {
2205
+ algorithm: form.algorithm,
2206
+ digits: form.digits,
2207
+ issuer: form.issuer,
2208
+ key_size: form.keySize,
2209
+ max_validation_attempts: form.maxValidationAttempts,
2210
+ method_name: props.name,
2211
+ period: props.period ?? "30s",
2212
+ qr_size: form.qrSize,
2213
+ skew: form.skew
2214
+ };
2215
+ };
2216
+ var renameProblem = (from, to) => from === to ? undefined : `renaming from \`${from}\` would strand every enrolled secret. Declare the new method as a ` + "second resource, list both ids on the enforcement, enrol, then remove the old one " + "(mfa-totp.ts).";
2217
+ var problems5 = (props) => {
2218
+ const found = [];
2219
+ const form = canonicalFromProps5(props);
2220
+ if (props.name.trim() === "")
2221
+ found.push("name is empty; it is the only key a declaration has");
2222
+ if (props.issuer.trim() === "")
2223
+ found.push("issuer is empty; the server requires one");
2224
+ if (form.periodSeconds <= 0)
2225
+ found.push(`period \`${props.period ?? ""}\` must be a duration above zero`);
2226
+ if (form.keySize <= 0)
2227
+ found.push("keySize must be above zero");
2228
+ if (form.maxValidationAttempts < 1)
2229
+ found.push("maxValidationAttempts must be at least 1 (0 reads back as 5)");
2230
+ return found;
2231
+ };
2232
+
2233
+ // src/openbao/mfa-totp.ts
2234
+ var BaoMfaTotpMethod = Resource8("Bao.MfaTotpMethod", {
2235
+ defaultRemovalPolicy: "retain"
2236
+ });
2237
+ var refuse4 = (props, message) => Effect19.die(new Error(`Bao.MfaTotpMethod ${props.name}: ${message}`));
2238
+ var readTotp = (props) => Effect19.map(findTotp(props.name), (found) => found === undefined ? undefined : attributesOf9(found.id, found.live));
2239
+ var planTotp = (props) => Effect19.gen(function* () {
2240
+ if (problems5(props).length > 0)
2241
+ return "update";
2242
+ const live = yield* readTotp(props);
2243
+ return live !== undefined && matches8(live, props) ? "noop" : "update";
2244
+ });
2245
+ var reconcileTotp = (props) => Effect19.gen(function* () {
2246
+ const bad = problems5(props);
2247
+ if (bad.length > 0)
2248
+ return yield* refuse4(props, bad.join("; "));
2249
+ const live = yield* readTotp(props);
2250
+ if (live === undefined || !matches8(live, props))
2251
+ yield* writeTotp(writeBody7(props));
2252
+ const after = yield* readTotp(props);
2253
+ if (after === undefined)
2254
+ return yield* refuse4(props, "the write succeeded but no method has that name.");
2255
+ if (!matches8(after, props))
2256
+ return yield* refuse4(props, "the method read back different.");
2257
+ return after;
2258
+ });
2259
+ var BaoMfaTotpMethodProvider = () => Provider8.effect(BaoMfaTotpMethod, Effect19.succeed(BaoMfaTotpMethod.Provider.of({
2260
+ list: () => Effect19.succeed([]),
2261
+ read: Effect19.fn(function* ({ olds }) {
2262
+ return yield* readTotp(olds);
2263
+ }),
2264
+ diff: Effect19.fn(function* ({ news, output }) {
2265
+ if (output === undefined || !isResolved8(news))
2266
+ return;
2267
+ const rename = renameProblem(output.name, news.name);
2268
+ if (rename !== undefined)
2269
+ return yield* refuse4(news, rename);
2270
+ return { action: yield* planTotp(news) };
2271
+ }),
2272
+ reconcile: Effect19.fn(function* ({ news }) {
2273
+ return yield* reconcileTotp(news);
2274
+ }),
2275
+ delete: Effect19.fn(function* ({ output }) {
2276
+ yield* deleteTotp(output.methodId);
2277
+ return;
2278
+ })
2279
+ })));
2280
+ // src/openbao/mount.ts
2281
+ import { Resource as Resource9 } from "alchemy";
2282
+ import { isResolved as isResolved9 } from "alchemy/Diff";
2283
+ import * as Provider9 from "alchemy/Provider";
2284
+ import * as Effect22 from "effect/Effect";
2285
+
2286
+ // src/openbao/mount-reconcile.ts
2287
+ import * as Effect21 from "effect/Effect";
1241
2288
 
1242
2289
  // src/openbao/mount-wire.ts
1243
- import * as Effect9 from "effect/Effect";
1244
- var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect9.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
2290
+ import * as Effect20 from "effect/Effect";
2291
+ var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect20.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
1245
2292
  ...asked.errors,
1246
2293
  `sys/mounts could not confirm the mount is absent: ${listing.message}`
1247
- ])), Effect9.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect9.succeed(undefined) : Effect9.fail(asked)));
1248
- var readMountData = (path) => baoRead(readPath(path)).pipe(Effect9.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
2294
+ ])), Effect20.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect20.succeed(undefined) : Effect20.fail(asked)));
2295
+ var readMountData = (path) => baoRead(readPath(path)).pipe(Effect20.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
1249
2296
  var enableMount = (props) => baoWrite("POST", readPath(props.path), enableBody(props));
1250
2297
  var tuneMount = (props) => baoWrite("POST", tunePath(props.path), tuneBody(props));
1251
2298
  var disableMount = (path) => baoDelete(readPath(path));
1252
2299
 
1253
- // src/openbao/mount.ts
1254
- var BaoMount = Resource4("Bao.Mount", {
1255
- defaultRemovalPolicy: "retain"
1256
- });
1257
- var readMount = (props) => Effect10.gen(function* () {
2300
+ // src/openbao/mount-reconcile.ts
2301
+ var readMount = (props) => Effect21.gen(function* () {
1258
2302
  const live = yield* readMountData(props.path);
1259
2303
  if (live === undefined)
1260
2304
  return;
1261
2305
  return attributesOf(props, live);
1262
2306
  });
1263
- var BaoMountProvider = () => Provider4.effect(BaoMount, Effect10.succeed(BaoMount.Provider.of({
1264
- list: () => Effect10.succeed([]),
1265
- read: Effect10.fn(function* ({ olds }) {
2307
+ var moveTarget2 = (props) => ({
2308
+ family: "Bao.Mount",
2309
+ path: props.path,
2310
+ read: (path) => readMount({ ...props, path }),
2311
+ type: props.type,
2312
+ wire: mountPath
2313
+ });
2314
+ var reconcileMount = (news, stated, options) => Effect21.gen(function* () {
2315
+ const move = planMove("Bao.Mount", stated, news.path, news.remountFrom);
2316
+ if (move.kind === "refuse")
2317
+ return yield* Effect21.die(new Error(move.message));
2318
+ if (move.kind === "move")
2319
+ yield* performMove(moveTarget2(news), move.from, options);
2320
+ let live = yield* readMount(news);
2321
+ if (live === undefined) {
2322
+ yield* enableMount(news);
2323
+ if (wantsTune(news))
2324
+ yield* tuneMount(news);
2325
+ } else if (!matches(live, news)) {
2326
+ if (live.type !== news.type) {
2327
+ return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
2328
+ }
2329
+ yield* tuneMount(news);
2330
+ }
2331
+ live = yield* readMount(news);
2332
+ if (live === undefined) {
2333
+ return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
2334
+ }
2335
+ return live;
2336
+ });
2337
+
2338
+ // src/openbao/mount.ts
2339
+ var BaoMount = Resource9("Bao.Mount", {
2340
+ defaultRemovalPolicy: "retain"
2341
+ });
2342
+ var BaoMountProvider = () => Provider9.effect(BaoMount, Effect22.succeed(BaoMount.Provider.of({
2343
+ list: () => Effect22.succeed([]),
2344
+ read: Effect22.fn(function* ({ olds }) {
1266
2345
  return yield* readMount(olds);
1267
2346
  }),
1268
- diff: Effect10.fn(function* ({ news, output }) {
1269
- if (output === undefined || !isResolved4(news))
2347
+ diff: Effect22.fn(function* ({ news, output }) {
2348
+ if (output === undefined || !isResolved9(news))
1270
2349
  return;
2350
+ const move = planMove("Bao.Mount", output.path, news.path, news.remountFrom);
2351
+ if (move.kind === "refuse")
2352
+ return yield* Effect22.die(new Error(move.message));
2353
+ if (move.kind === "move")
2354
+ return { action: "update" };
1271
2355
  const live = yield* readMount(news);
1272
2356
  if (live === undefined)
1273
2357
  return { action: "update" };
@@ -1277,39 +2361,29 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect10.succeed(BaoMoun
1277
2361
  return { action: "noop" };
1278
2362
  return { action: "update" };
1279
2363
  }),
1280
- reconcile: Effect10.fn(function* ({ news }) {
1281
- let live = yield* readMount(news);
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;
2364
+ reconcile: Effect22.fn(function* ({ news, output }) {
2365
+ return yield* reconcileMount(news, output?.path);
1297
2366
  }),
1298
- delete: Effect10.fn(function* ({ output }) {
2367
+ delete: Effect22.fn(function* ({ output }) {
1299
2368
  yield* disableMount(output.path);
1300
2369
  return;
1301
2370
  })
1302
2371
  })));
1303
2372
  // src/openbao/pki-role.ts
1304
- import { Resource as Resource5 } from "alchemy";
1305
- import { isResolved as isResolved5 } from "alchemy/Diff";
1306
- import * as Provider5 from "alchemy/Provider";
1307
- import * as Effect11 from "effect/Effect";
2373
+ import { Resource as Resource10 } from "alchemy";
2374
+ import { isResolved as isResolved10 } from "alchemy/Diff";
2375
+ import * as Provider10 from "alchemy/Provider";
2376
+ import * as Effect23 from "effect/Effect";
1308
2377
 
1309
- // src/openbao/pki-role-form.ts
2378
+ // src/openbao/pki-role-canonical.ts
2379
+ var DEFAULT_KEY_USAGE = [
2380
+ "DigitalSignature",
2381
+ "KeyAgreement",
2382
+ "KeyEncipherment"
2383
+ ];
1310
2384
  var sorted = (values) => [...values].sort();
1311
2385
  var foldedUsages = (values) => sorted(values.map((v) => v.toLowerCase()));
1312
- var canonical2 = (input) => ({
2386
+ var canonical7 = (input) => ({
1313
2387
  allowBareDomains: input.allowBareDomains,
1314
2388
  allowGlobDomains: input.allowGlobDomains,
1315
2389
  allowIpSans: input.allowIpSans,
@@ -1317,18 +2391,24 @@ var canonical2 = (input) => ({
1317
2391
  allowSubdomains: input.allowSubdomains,
1318
2392
  allowWildcardCertificates: input.allowWildcardCertificates,
1319
2393
  allowedDomains: sorted(input.allowedDomains),
2394
+ allowedDomainsTemplate: input.allowedDomainsTemplate,
1320
2395
  clientFlag: input.clientFlag,
1321
2396
  cnValidations: sorted(input.cnValidations),
2397
+ enforceHostnames: input.enforceHostnames,
1322
2398
  extKeyUsage: foldedUsages(input.extKeyUsage),
2399
+ generateLease: input.generateLease,
1323
2400
  keyBits: input.keyBits,
1324
2401
  keyType: input.keyType,
2402
+ keyUsage: foldedUsages(input.keyUsage),
1325
2403
  maxTtlSeconds: input.maxTtlSeconds,
2404
+ noStore: input.noStore,
2405
+ requireCn: input.requireCn,
1326
2406
  serverFlag: input.serverFlag,
1327
2407
  signatureBits: input.signatureBits,
1328
2408
  ttlSeconds: input.ttlSeconds
1329
2409
  });
1330
- var seconds2 = (text4) => parseDuration(text4) ?? -1;
1331
- var canonicalFromProps = (props) => canonical2({
2410
+ var seconds3 = (text10) => parseDuration(text10) ?? -1;
2411
+ var canonicalFromProps6 = (props) => canonical7({
1332
2412
  allowBareDomains: props.allowBareDomains ?? false,
1333
2413
  allowGlobDomains: props.allowGlobDomains ?? false,
1334
2414
  allowIpSans: props.allowIpSans ?? false,
@@ -1336,21 +2416,27 @@ var canonicalFromProps = (props) => canonical2({
1336
2416
  allowSubdomains: props.allowSubdomains ?? false,
1337
2417
  allowWildcardCertificates: props.allowWildcardCertificates ?? false,
1338
2418
  allowedDomains: props.allowedDomains,
2419
+ allowedDomainsTemplate: props.allowedDomainsTemplate ?? false,
1339
2420
  clientFlag: props.clientFlag ?? true,
1340
2421
  cnValidations: props.cnValidations ?? ["hostname"],
2422
+ enforceHostnames: props.enforceHostnames ?? true,
1341
2423
  extKeyUsage: props.extKeyUsage ?? [],
2424
+ generateLease: props.generateLease ?? false,
1342
2425
  keyBits: props.keyBits ?? 0,
1343
2426
  keyType: props.keyType ?? "rsa",
1344
- maxTtlSeconds: seconds2(props.maxTtl),
2427
+ keyUsage: props.keyUsage ?? DEFAULT_KEY_USAGE,
2428
+ maxTtlSeconds: seconds3(props.maxTtl),
2429
+ noStore: props.noStore ?? false,
2430
+ requireCn: props.requireCn ?? true,
1345
2431
  serverFlag: props.serverFlag ?? true,
1346
2432
  signatureBits: props.signatureBits ?? 0,
1347
- ttlSeconds: seconds2(props.ttl)
2433
+ ttlSeconds: seconds3(props.ttl)
1348
2434
  });
1349
2435
  var bool2 = (value, fallback) => typeof value === "boolean" ? value : fallback;
1350
2436
  var int2 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
1351
2437
  var list = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1352
2438
  var str = (value, fallback) => typeof value === "string" ? value : fallback;
1353
- var canonicalFromLive = (live) => canonical2({
2439
+ var canonicalFromLive6 = (live) => canonical7({
1354
2440
  allowBareDomains: bool2(live["allow_bare_domains"], false),
1355
2441
  allowGlobDomains: bool2(live["allow_glob_domains"], false),
1356
2442
  allowIpSans: bool2(live["allow_ip_sans"], false),
@@ -1358,16 +2444,25 @@ var canonicalFromLive = (live) => canonical2({
1358
2444
  allowSubdomains: bool2(live["allow_subdomains"], false),
1359
2445
  allowWildcardCertificates: bool2(live["allow_wildcard_certificates"], false),
1360
2446
  allowedDomains: list(live["allowed_domains"]),
2447
+ allowedDomainsTemplate: bool2(live["allowed_domains_template"], false),
1361
2448
  clientFlag: bool2(live["client_flag"], true),
1362
2449
  cnValidations: list(live["cn_validations"]),
2450
+ enforceHostnames: bool2(live["enforce_hostnames"], false),
1363
2451
  extKeyUsage: list(live["ext_key_usage"]),
2452
+ generateLease: bool2(live["generate_lease"], false),
1364
2453
  keyBits: int2(live["key_bits"], 0),
1365
2454
  keyType: str(live["key_type"], "rsa"),
2455
+ keyUsage: list(live["key_usage"]),
1366
2456
  maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? -1,
2457
+ noStore: bool2(live["no_store"], false),
2458
+ requireCn: bool2(live["require_cn"], false),
1367
2459
  serverFlag: bool2(live["server_flag"], true),
1368
2460
  signatureBits: int2(live["signature_bits"], 0),
1369
2461
  ttlSeconds: ttlSeconds(live["ttl"]) ?? -1
1370
2462
  });
2463
+ var digestOf = (form) => sha256(JSON.stringify(form));
2464
+
2465
+ // src/openbao/pki-role-form.ts
1371
2466
  var ttlText2 = (value) => {
1372
2467
  if (value < 0)
1373
2468
  return "";
@@ -1379,10 +2474,9 @@ var ttlText2 = (value) => {
1379
2474
  return `${String(value / 60)}m`;
1380
2475
  return `${String(value)}s`;
1381
2476
  };
1382
- var digestOf = (form) => sha256(JSON.stringify(form));
1383
- var rolePath2 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
1384
- var attributesOf5 = (props, live) => {
1385
- const form = canonicalFromLive(live);
2477
+ var rolePath4 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
2478
+ var attributesOf10 = (props, live) => {
2479
+ const form = canonicalFromLive6(live);
1386
2480
  return {
1387
2481
  ...form,
1388
2482
  name: props.name,
@@ -1392,8 +2486,8 @@ var attributesOf5 = (props, live) => {
1392
2486
  digest: digestOf(form)
1393
2487
  };
1394
2488
  };
1395
- var writeBody3 = (props) => {
1396
- const form = canonicalFromProps(props);
2489
+ var writeBody8 = (props) => {
2490
+ const form = canonicalFromProps6(props);
1397
2491
  const body = {
1398
2492
  allowed_domains: form.allowedDomains.join(","),
1399
2493
  allow_bare_domains: String(form.allowBareDomains),
@@ -1409,76 +2503,94 @@ var writeBody3 = (props) => {
1409
2503
  key_bits: String(form.keyBits),
1410
2504
  signature_bits: String(form.signatureBits),
1411
2505
  ttl: props.ttl,
1412
- max_ttl: props.maxTtl
2506
+ max_ttl: props.maxTtl,
2507
+ require_cn: String(form.requireCn),
2508
+ enforce_hostnames: String(form.enforceHostnames),
2509
+ key_usage: (props.keyUsage ?? DEFAULT_KEY_USAGE).join(","),
2510
+ allowed_domains_template: String(form.allowedDomainsTemplate),
2511
+ no_store: String(form.noStore),
2512
+ generate_lease: String(form.generateLease)
1413
2513
  };
1414
2514
  const usages = props.extKeyUsage ?? [];
1415
2515
  if (usages.length > 0)
1416
2516
  body["ext_key_usage"] = usages.join(",");
1417
2517
  return body;
1418
2518
  };
1419
- var matches4 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps(props));
2519
+ var matches9 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps6(props));
2520
+ var problems6 = (props) => {
2521
+ const found = [];
2522
+ if (props.allowedDomains.length === 0) {
2523
+ found.push("allowedDomains is empty. With allow_any_name false that role can issue no certificate at all.");
2524
+ }
2525
+ const bad = [
2526
+ ["ttl", props.ttl],
2527
+ ["maxTtl", props.maxTtl]
2528
+ ].filter(([, text10]) => parseDuration(text10 ?? "") === undefined);
2529
+ if (bad.length > 0) {
2530
+ found.push(`${bad.map(([k, v]) => `${k ?? ""}=${v ?? ""}`).join(", ")} is not a duration OpenBao ` + "parses (expect 30m, 720h, 8760h or 0).");
2531
+ }
2532
+ if (props.noStore === true && props.generateLease === true) {
2533
+ found.push("noStore and generateLease are both true; OpenBao keeps no_store and drops the lease.");
2534
+ }
2535
+ return found;
2536
+ };
1420
2537
 
1421
2538
  // src/openbao/pki-role.ts
1422
- var BaoPkiRole = Resource5("Bao.PkiRole", {
2539
+ var BaoPkiRole = Resource10("Bao.PkiRole", {
1423
2540
  defaultRemovalPolicy: "retain"
1424
2541
  });
1425
- var readRole2 = (props) => Effect11.gen(function* () {
1426
- const live = yield* baoRead(rolePath2(props));
2542
+ var readRole2 = (props) => Effect23.gen(function* () {
2543
+ const live = yield* baoRead(rolePath4(props));
1427
2544
  if (live === undefined)
1428
2545
  return;
1429
- return attributesOf5(props, live);
2546
+ return attributesOf10(props, live);
1430
2547
  });
1431
- var badDurations = (props) => [["ttl", props.ttl], ["maxTtl", props.maxTtl]].filter(([, text4]) => parseDuration(text4) === undefined);
1432
- var BaoPkiRoleProvider = () => Provider5.effect(BaoPkiRole, Effect11.succeed(BaoPkiRole.Provider.of({
1433
- list: () => Effect11.succeed([]),
1434
- read: Effect11.fn(function* ({ olds }) {
2548
+ var BaoPkiRoleProvider = () => Provider10.effect(BaoPkiRole, Effect23.succeed(BaoPkiRole.Provider.of({
2549
+ list: () => Effect23.succeed([]),
2550
+ read: Effect23.fn(function* ({ olds }) {
1435
2551
  return yield* readRole2(olds);
1436
2552
  }),
1437
- diff: Effect11.fn(function* ({ news, output }) {
1438
- if (output === undefined || !isResolved5(news))
2553
+ diff: Effect23.fn(function* ({ news, output }) {
2554
+ if (output === undefined || !isResolved10(news))
1439
2555
  return;
1440
- if (mountPath(news.mount ?? "pki") !== output.mount)
2556
+ if (mountPath(news.mount ?? "pki") !== output.mount || news.name !== output.name)
1441
2557
  return { action: "replace" };
1442
- if (news.allowedDomains.length === 0 || badDurations(news).length > 0) {
2558
+ if (problems6(news).length > 0)
1443
2559
  return { action: "update" };
1444
- }
1445
2560
  const live = yield* readRole2(news);
1446
2561
  if (live === undefined)
1447
2562
  return { action: "update" };
1448
- return matches4(live, news) ? { action: "noop" } : { action: "update" };
2563
+ return matches9(live, news) ? { action: "noop" } : { action: "update" };
1449
2564
  }),
1450
- reconcile: Effect11.fn(function* ({ news }) {
1451
- const path = rolePath2(news);
1452
- if (news.allowedDomains.length === 0) {
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);
2565
+ reconcile: Effect23.fn(function* ({ news }) {
2566
+ const path = rolePath4(news);
2567
+ const bad = problems6(news);
1456
2568
  if (bad.length > 0) {
1457
- return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: ${bad.map(([k, v]) => `${k}=${v}`).join(", ")} ` + "is not a duration OpenBao parses (expect 30m, 720h, 8760h or 0)."));
2569
+ return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: ${bad.join(" ")}`));
1458
2570
  }
1459
2571
  const live = yield* readRole2(news);
1460
- if (live === undefined || !matches4(live, news)) {
1461
- yield* baoWrite("PUT", path, writeBody3(news));
2572
+ if (live === undefined || !matches9(live, news)) {
2573
+ yield* baoWrite("PUT", path, writeBody8(news));
1462
2574
  }
1463
2575
  const after = yield* readRole2(news);
1464
2576
  if (after === undefined) {
1465
- return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
2577
+ return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
1466
2578
  }
1467
- if (!matches4(after, news)) {
1468
- return yield* Effect11.die(new Error(`Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` + "OpenBao stored something other than what was declared — inspect with " + `\`bao read ${path}\` before retrying.`));
2579
+ if (!matches9(after, news)) {
2580
+ return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` + "OpenBao stored something other than what was declared — inspect with " + `\`bao read ${path}\` before retrying.`));
1469
2581
  }
1470
2582
  return after;
1471
2583
  }),
1472
- delete: Effect11.fn(function* ({ output }) {
1473
- yield* baoDelete(rolePath2(output));
2584
+ delete: Effect23.fn(function* ({ output }) {
2585
+ yield* baoDelete(rolePath4(output));
1474
2586
  return;
1475
2587
  })
1476
2588
  })));
1477
2589
  // src/openbao/plugin.ts
1478
- import { Resource as Resource6 } from "alchemy";
1479
- import { isResolved as isResolved6 } from "alchemy/Diff";
1480
- import * as Provider6 from "alchemy/Provider";
1481
- import * as Effect13 from "effect/Effect";
2590
+ import { Resource as Resource11 } from "alchemy";
2591
+ import { isResolved as isResolved11 } from "alchemy/Diff";
2592
+ import * as Provider11 from "alchemy/Provider";
2593
+ import * as Effect25 from "effect/Effect";
1482
2594
 
1483
2595
  // src/openbao/plugin-form.ts
1484
2596
  var resolve = (props) => ({
@@ -1490,7 +2602,7 @@ var resolve = (props) => ({
1490
2602
  version: props.version ?? ""
1491
2603
  });
1492
2604
  var CANONICAL_VERSION = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
1493
- var problems = (form) => {
2605
+ var problems7 = (form) => {
1494
2606
  const found = [];
1495
2607
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(form.name) || form.name.includes("..")) {
1496
2608
  found.push(`name ${JSON.stringify(form.name)} is not a plain catalog name`);
@@ -1516,25 +2628,25 @@ var registerBody = (form) => ({
1516
2628
  ...form.args.length > 0 ? { args: form.args } : {},
1517
2629
  ...form.version === "" ? {} : { version: form.version }
1518
2630
  });
1519
- var text4 = (value) => typeof value === "string" ? value : "";
2631
+ var text10 = (value) => typeof value === "string" ? value : "";
1520
2632
  var list2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
1521
- var attributesOf6 = (form, live) => {
2633
+ var attributesOf11 = (form, live) => {
1522
2634
  const attrs = {
1523
2635
  name: form.name,
1524
2636
  type: form.type,
1525
- version: text4(live["version"]),
1526
- sha256: text4(live["sha256"]).toLowerCase(),
1527
- command: text4(live["command"]),
2637
+ version: text10(live["version"]),
2638
+ sha256: text10(live["sha256"]).toLowerCase(),
2639
+ command: text10(live["command"]),
1528
2640
  args: list2(live["args"]),
1529
2641
  builtin: live["builtin"] === true
1530
2642
  };
1531
2643
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1532
2644
  };
1533
2645
  var sameArgs = (want, have) => want.length === have.length && want.every((entry, index) => entry === have[index]);
1534
- var matches5 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
2646
+ var matches10 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
1535
2647
 
1536
2648
  // src/openbao/plugin-reconcile.ts
1537
- import * as Effect12 from "effect/Effect";
2649
+ import * as Effect24 from "effect/Effect";
1538
2650
 
1539
2651
  // src/openbao/plugin-wire.ts
1540
2652
  var readPluginData = (form) => baoRead(versionedPath(form.type, form.name, form.version));
@@ -1542,44 +2654,44 @@ var registerPlugin = (form) => baoWrite("PUT", catalogPath(form.type, form.name)
1542
2654
  var deregisterPlugin = (type, name, version) => baoDelete(versionedPath(type, name, version));
1543
2655
 
1544
2656
  // src/openbao/plugin-reconcile.ts
1545
- var readEntry = (form) => Effect12.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf6(form, live), live });
1546
- var refuse = (form, message) => Effect12.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
2657
+ var readEntry = (form) => Effect24.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf11(form, live), live });
2658
+ var refuse5 = (form, message) => Effect24.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
1547
2659
  var SELF_REPORTED = "the write succeeded but the read-back does not match. If the binary reports its own version, " + "OpenBao filed the registration under it (plugin_catalog.go setInternal) — declare `version` " + "as exactly the version the binary reports; that adopts the entry this write just made.";
1548
- var reconcilePlugin = (props) => Effect12.gen(function* () {
2660
+ var reconcilePlugin = (props) => Effect24.gen(function* () {
1549
2661
  const form = resolve(props);
1550
- const bad = problems(form);
2662
+ const bad = problems7(form);
1551
2663
  if (bad.length > 0)
1552
- return yield* refuse(form, bad.join("; "));
2664
+ return yield* refuse5(form, bad.join("; "));
1553
2665
  const found = yield* readEntry(form);
1554
- if (found !== undefined && !matches5(found.attributes, form)) {
2666
+ if (found !== undefined && !matches10(found.attributes, form)) {
1555
2667
  if (found.live["declarative"] === true || found.live["oci"] === true) {
1556
- return yield* refuse(form, "the live entry is declarative/OCI; manage it in server config.");
2668
+ return yield* refuse5(form, "the live entry is declarative/OCI; manage it in server config.");
1557
2669
  }
1558
2670
  if (found.attributes.builtin) {
1559
- return yield* refuse(form, "this name is a builtin; an unversioned write would shadow it.");
2671
+ return yield* refuse5(form, "this name is a builtin; an unversioned write would shadow it.");
1560
2672
  }
1561
2673
  }
1562
- if (found === undefined || !matches5(found.attributes, form))
2674
+ if (found === undefined || !matches10(found.attributes, form))
1563
2675
  yield* registerPlugin(form);
1564
2676
  const after = yield* readEntry(form);
1565
- if (after === undefined || !matches5(after.attributes, form)) {
1566
- return yield* refuse(form, SELF_REPORTED);
2677
+ if (after === undefined || !matches10(after.attributes, form)) {
2678
+ return yield* refuse5(form, SELF_REPORTED);
1567
2679
  }
1568
2680
  return after.attributes;
1569
2681
  });
1570
2682
 
1571
2683
  // src/openbao/plugin.ts
1572
- var BaoPlugin = Resource6("Bao.Plugin", {
2684
+ var BaoPlugin = Resource11("Bao.Plugin", {
1573
2685
  defaultRemovalPolicy: "retain"
1574
2686
  });
1575
- var BaoPluginProvider = () => Provider6.effect(BaoPlugin, Effect13.succeed(BaoPlugin.Provider.of({
1576
- list: () => Effect13.succeed([]),
1577
- read: Effect13.fn(function* ({ olds }) {
2687
+ var BaoPluginProvider = () => Provider11.effect(BaoPlugin, Effect25.succeed(BaoPlugin.Provider.of({
2688
+ list: () => Effect25.succeed([]),
2689
+ read: Effect25.fn(function* ({ olds }) {
1578
2690
  const found = yield* readEntry(resolve(olds));
1579
2691
  return found?.attributes;
1580
2692
  }),
1581
- diff: Effect13.fn(function* ({ news, output }) {
1582
- if (output === undefined || !isResolved6(news))
2693
+ diff: Effect25.fn(function* ({ news, output }) {
2694
+ if (output === undefined || !isResolved11(news))
1583
2695
  return;
1584
2696
  const form = resolve(news);
1585
2697
  if (output.name !== form.name || output.type !== form.type || output.version !== form.version) {
@@ -1588,21 +2700,21 @@ var BaoPluginProvider = () => Provider6.effect(BaoPlugin, Effect13.succeed(BaoPl
1588
2700
  const found = yield* readEntry(form);
1589
2701
  if (found === undefined)
1590
2702
  return { action: "update" };
1591
- return matches5(found.attributes, form) ? { action: "noop" } : { action: "update" };
2703
+ return matches10(found.attributes, form) ? { action: "noop" } : { action: "update" };
1592
2704
  }),
1593
- reconcile: Effect13.fn(function* ({ news }) {
2705
+ reconcile: Effect25.fn(function* ({ news }) {
1594
2706
  return yield* reconcilePlugin(news);
1595
2707
  }),
1596
- delete: Effect13.fn(function* ({ output }) {
2708
+ delete: Effect25.fn(function* ({ output }) {
1597
2709
  yield* deregisterPlugin(output.type, output.name, output.version);
1598
2710
  return;
1599
2711
  })
1600
2712
  })));
1601
2713
  // src/openbao/policy.ts
1602
- import { Resource as Resource7 } from "alchemy";
1603
- import { isResolved as isResolved7 } from "alchemy/Diff";
1604
- import * as Provider7 from "alchemy/Provider";
1605
- import * as Effect15 from "effect/Effect";
2714
+ import { Resource as Resource12 } from "alchemy";
2715
+ import { isResolved as isResolved12 } from "alchemy/Diff";
2716
+ import * as Provider12 from "alchemy/Provider";
2717
+ import * as Effect27 from "effect/Effect";
1606
2718
  import * as FileSystem from "effect/FileSystem";
1607
2719
  import * as Path from "effect/Path";
1608
2720
 
@@ -1610,32 +2722,32 @@ import * as Path from "effect/Path";
1610
2722
  var isEmptyAssembly = (parts, joined) => parts === 0 || !/^\s*path\s+"/m.test(joined);
1611
2723
 
1612
2724
  // src/openbao/policy-wire.ts
1613
- import * as Effect14 from "effect/Effect";
2725
+ import * as Effect26 from "effect/Effect";
1614
2726
  var policyPath = (name) => `sys/policies/acl/${name}`;
1615
- var readPolicy = (name) => Effect14.flatMap(baoRead(policyPath(name)), (data) => {
2727
+ var readPolicy = (name) => Effect26.flatMap(baoRead(policyPath(name)), (data) => {
1616
2728
  if (data === undefined)
1617
- return Effect14.succeed("");
1618
- const text5 = data["policy"];
1619
- return typeof text5 === "string" ? Effect14.succeed(text5) : Effect14.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
2729
+ return Effect26.succeed("");
2730
+ const text11 = data["policy"];
2731
+ return typeof text11 === "string" ? Effect26.succeed(text11) : Effect26.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
1620
2732
  });
1621
2733
  var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
1622
2734
  var deletePolicy = (name) => baoDelete(policyPath(name));
1623
2735
 
1624
2736
  // src/openbao/policy.ts
1625
- var BaoPolicy = Resource7("Bao.Policy", {
2737
+ var BaoPolicy = Resource12("Bao.Policy", {
1626
2738
  defaultRemovalPolicy: "retain"
1627
2739
  });
1628
2740
  var grantsOf = (hcl) => (hcl.match(/^path /gm) ?? []).length;
1629
- var attributesOf7 = (name, hcl, parts) => ({
2741
+ var attributesOf12 = (name, hcl, parts) => ({
1630
2742
  name,
1631
2743
  digest: sha256(hcl),
1632
2744
  grants: grantsOf(hcl),
1633
2745
  parts
1634
2746
  });
1635
- var BaoPolicyProvider = () => Provider7.effect(BaoPolicy, Effect15.gen(function* () {
2747
+ var BaoPolicyProvider = () => Provider12.effect(BaoPolicy, Effect27.gen(function* () {
1636
2748
  const fs = yield* FileSystem.FileSystem;
1637
2749
  const path = yield* Path.Path;
1638
- const assemble = (dir) => Effect15.gen(function* () {
2750
+ const assemble = (dir) => Effect27.gen(function* () {
1639
2751
  const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
1640
2752
  let joined = "";
1641
2753
  for (const f of entries2)
@@ -1644,67 +2756,67 @@ var BaoPolicyProvider = () => Provider7.effect(BaoPolicy, Effect15.gen(function*
1644
2756
  return { joined, parts: entries2.length };
1645
2757
  });
1646
2758
  return BaoPolicy.Provider.of({
1647
- list: () => Effect15.succeed([]),
1648
- read: Effect15.fn(function* ({ olds }) {
2759
+ list: () => Effect27.succeed([]),
2760
+ read: Effect27.fn(function* ({ olds }) {
1649
2761
  const live = yield* readPolicy(olds.name);
1650
- return live.length === 0 ? undefined : attributesOf7(olds.name, live, 0);
2762
+ return live.length === 0 ? undefined : attributesOf12(olds.name, live, 0);
1651
2763
  }),
1652
- diff: Effect15.fn(function* ({ news, output }) {
1653
- if (output === undefined || !isResolved7(news))
2764
+ diff: Effect27.fn(function* ({ news, output }) {
2765
+ if (output === undefined || !isResolved12(news))
1654
2766
  return;
1655
2767
  const { joined } = yield* assemble(news.fragments);
1656
2768
  const live = yield* readPolicy(news.name);
1657
2769
  return live.length > 0 && sha256(live) === sha256(joined) ? { action: "noop" } : { action: "update" };
1658
2770
  }),
1659
- reconcile: Effect15.fn(function* ({ news }) {
2771
+ reconcile: Effect27.fn(function* ({ news }) {
1660
2772
  const name = news.name;
1661
2773
  const { joined, parts } = yield* assemble(news.fragments);
1662
2774
  if (isEmptyAssembly(parts, joined))
1663
- return yield* Effect15.die(new Error(`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` + `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`));
2775
+ return yield* Effect27.die(new Error(`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` + `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`));
1664
2776
  const current = yield* readPolicy(name);
1665
2777
  if (current.length > 0 && sha256(current) === sha256(joined)) {
1666
- return attributesOf7(name, joined, parts);
2778
+ return attributesOf12(name, joined, parts);
1667
2779
  }
1668
2780
  yield* writePolicy(name, joined);
1669
- return attributesOf7(name, joined, parts);
2781
+ return attributesOf12(name, joined, parts);
1670
2782
  }),
1671
- delete: Effect15.fn(function* ({ output }) {
2783
+ delete: Effect27.fn(function* ({ output }) {
1672
2784
  yield* deletePolicy(output.name);
1673
2785
  return;
1674
2786
  })
1675
2787
  });
1676
2788
  }));
1677
2789
  // src/openbao/proxmox-role.ts
1678
- import { Resource as Resource8 } from "alchemy";
1679
- import { isResolved as isResolved8 } from "alchemy/Diff";
1680
- import * as Provider8 from "alchemy/Provider";
1681
- import * as Effect16 from "effect/Effect";
2790
+ import { Resource as Resource13 } from "alchemy";
2791
+ import { isResolved as isResolved13 } from "alchemy/Diff";
2792
+ import * as Provider13 from "alchemy/Provider";
2793
+ import * as Effect28 from "effect/Effect";
1682
2794
 
1683
2795
  // src/openbao/proxmox-role-form.ts
1684
- var text5 = (value) => typeof value === "string" ? value : "";
1685
- var ttlText3 = (seconds3, fallback = "0") => {
1686
- if (seconds3 === undefined)
2796
+ var text11 = (value) => typeof value === "string" ? value : "";
2797
+ var ttlText3 = (seconds4, fallback = "0") => {
2798
+ if (seconds4 === undefined)
1687
2799
  return fallback;
1688
- if (seconds3 % 86400 === 0)
1689
- return `${String(seconds3 / 86400)}d`;
1690
- if (seconds3 % 3600 === 0)
1691
- return `${String(seconds3 / 3600)}h`;
1692
- if (seconds3 % 60 === 0)
1693
- return `${String(seconds3 / 60)}m`;
1694
- return `${String(seconds3)}s`;
1695
- };
1696
- var rolePath3 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
1697
- var attributesOf8 = (props, live) => {
2800
+ if (seconds4 % 86400 === 0)
2801
+ return `${String(seconds4 / 86400)}d`;
2802
+ if (seconds4 % 3600 === 0)
2803
+ return `${String(seconds4 / 3600)}h`;
2804
+ if (seconds4 % 60 === 0)
2805
+ return `${String(seconds4 / 60)}m`;
2806
+ return `${String(seconds4)}s`;
2807
+ };
2808
+ var rolePath5 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
2809
+ var attributesOf13 = (props, live) => {
1698
2810
  const attrs = {
1699
2811
  mount: mountPath(props.mount),
1700
- name: text5(live["name"]) || props.name,
1701
- mintUser: text5(live["mint_user"]),
2812
+ name: text11(live["name"]) || props.name,
2813
+ mintUser: text11(live["mint_user"]),
1702
2814
  ttl: ttlText3(ttlSeconds(live["ttl"])),
1703
2815
  maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
1704
2816
  };
1705
2817
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1706
2818
  };
1707
- var writeBody4 = (props) => {
2819
+ var writeBody9 = (props) => {
1708
2820
  const ttl = parseDuration(props.ttl);
1709
2821
  const maxTtl = parseDuration(props.maxTtl);
1710
2822
  const bad = [];
@@ -1724,68 +2836,68 @@ var sameTtl5 = (want, have) => {
1724
2836
  const haveSeconds = ttlSeconds(have);
1725
2837
  return wantSeconds !== undefined && wantSeconds === haveSeconds;
1726
2838
  };
1727
- var matches6 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
2839
+ var matches11 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
1728
2840
 
1729
2841
  // src/openbao/proxmox-role.ts
1730
- var BaoProxmoxRole = Resource8("Bao.ProxmoxRole", {
2842
+ var BaoProxmoxRole = Resource13("Bao.ProxmoxRole", {
1731
2843
  defaultRemovalPolicy: "retain"
1732
2844
  });
1733
- var readRole3 = (props) => Effect16.gen(function* () {
1734
- const live = yield* baoRead(rolePath3(props.mount, props.name));
2845
+ var readRole3 = (props) => Effect28.gen(function* () {
2846
+ const live = yield* baoRead(rolePath5(props.mount, props.name));
1735
2847
  if (live === undefined)
1736
2848
  return;
1737
- return attributesOf8(props, live);
2849
+ return attributesOf13(props, live);
1738
2850
  });
1739
- var BaoProxmoxRoleProvider = () => Provider8.effect(BaoProxmoxRole, Effect16.succeed(BaoProxmoxRole.Provider.of({
1740
- list: () => Effect16.succeed([]),
1741
- read: Effect16.fn(function* ({ olds }) {
2851
+ var BaoProxmoxRoleProvider = () => Provider13.effect(BaoProxmoxRole, Effect28.succeed(BaoProxmoxRole.Provider.of({
2852
+ list: () => Effect28.succeed([]),
2853
+ read: Effect28.fn(function* ({ olds }) {
1742
2854
  return yield* readRole3(olds);
1743
2855
  }),
1744
- diff: Effect16.fn(function* ({ news, output }) {
1745
- if (output === undefined || !isResolved8(news))
2856
+ diff: Effect28.fn(function* ({ news, output }) {
2857
+ if (output === undefined || !isResolved13(news))
1746
2858
  return;
1747
2859
  const live = yield* readRole3(news);
1748
2860
  if (live === undefined)
1749
2861
  return { action: "update" };
1750
- if (matches6(live, news))
2862
+ if (matches11(live, news))
1751
2863
  return { action: "noop" };
1752
2864
  return { action: "update" };
1753
2865
  }),
1754
- reconcile: Effect16.fn(function* ({ news }) {
2866
+ reconcile: Effect28.fn(function* ({ news }) {
1755
2867
  const live = yield* readRole3(news);
1756
2868
  if (live !== undefined && live.mintUser !== news.mintUser && news.allowMintUserChange !== live.mintUser) {
1757
- return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: live mint_user ` + `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` + "inherits the ACL of that PVE user. To re-scope deliberately, declare " + `allowMintUserChange: '${live.mintUser}'.`));
2869
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: live mint_user ` + `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` + "inherits the ACL of that PVE user. To re-scope deliberately, declare " + `allowMintUserChange: '${live.mintUser}'.`));
1758
2870
  }
1759
- if (live === undefined || !matches6(live, news)) {
1760
- const write = writeBody4(news);
2871
+ if (live === undefined || !matches11(live, news)) {
2872
+ const write = writeBody9(news);
1761
2873
  if (!write.ok) {
1762
- return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
2874
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
1763
2875
  }
1764
- yield* baoWrite("PUT", rolePath3(news.mount, news.name), write.body);
2876
+ yield* baoWrite("PUT", rolePath5(news.mount, news.name), write.body);
1765
2877
  }
1766
2878
  const after = yield* readRole3(news);
1767
2879
  if (after === undefined) {
1768
- return yield* Effect16.die(new Error(`Bao.ProxmoxRole ${rolePath3(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
2880
+ return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
1769
2881
  }
1770
2882
  return after;
1771
2883
  }),
1772
- delete: Effect16.fn(function* ({ output }) {
1773
- yield* baoDelete(rolePath3(output.mount, output.name));
2884
+ delete: Effect28.fn(function* ({ output }) {
2885
+ yield* baoDelete(rolePath5(output.mount, output.name));
1774
2886
  return;
1775
2887
  })
1776
2888
  })));
1777
2889
  // src/openbao/ssh-role.ts
1778
- import { Resource as Resource9 } from "alchemy";
1779
- import { isResolved as isResolved9 } from "alchemy/Diff";
1780
- import * as Provider9 from "alchemy/Provider";
1781
- import * as Effect17 from "effect/Effect";
2890
+ import { Resource as Resource14 } from "alchemy";
2891
+ import { isResolved as isResolved14 } from "alchemy/Diff";
2892
+ import * as Provider14 from "alchemy/Provider";
2893
+ import * as Effect29 from "effect/Effect";
1782
2894
 
1783
2895
  // src/openbao/ssh-role-form.ts
1784
2896
  var readPath4 = (mount, name) => `${sshMountOf(mount)}/roles/${name}`;
1785
- var text6 = (value) => typeof value === "string" ? value : "";
2897
+ var text12 = (value) => typeof value === "string" ? value : "";
1786
2898
  var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
1787
2899
  var csvList = (value) => {
1788
- const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text6(value).split(",");
2900
+ const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text12(value).split(",");
1789
2901
  return [
1790
2902
  ...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
1791
2903
  ].sort();
@@ -1796,7 +2908,7 @@ var extMap = (value) => {
1796
2908
  return out;
1797
2909
  const source = value;
1798
2910
  for (const key of Object.keys(source).sort())
1799
- out[key] = text6(source[key]);
2911
+ out[key] = text12(source[key]);
1800
2912
  return out;
1801
2913
  };
1802
2914
  var resolve2 = (props) => ({
@@ -1817,12 +2929,12 @@ var resolve2 = (props) => ({
1817
2929
  ttl: props.ttl,
1818
2930
  maxTtl: props.maxTtl
1819
2931
  });
1820
- var attributesOf9 = (form, live) => {
2932
+ var attributesOf14 = (form, live) => {
1821
2933
  const attrs = {
1822
2934
  name: form.name,
1823
2935
  mount: form.mount,
1824
2936
  allowedUsers: csvList(live["allowed_users"]),
1825
- defaultUser: text6(live["default_user"]),
2937
+ defaultUser: text12(live["default_user"]),
1826
2938
  allowUserCertificates: bool3(live["allow_user_certificates"], false),
1827
2939
  allowHostCertificates: bool3(live["allow_host_certificates"], false),
1828
2940
  allowedDomains: csvList(live["allowed_domains"]),
@@ -1833,14 +2945,14 @@ var attributesOf9 = (form, live) => {
1833
2945
  allowedCriticalOptions: csvList(live["allowed_critical_options"]),
1834
2946
  defaultCriticalOptions: extMap(live["default_critical_options"]),
1835
2947
  allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
1836
- keyType: text6(live["key_type"]) || "ca",
2948
+ keyType: text12(live["key_type"]) || "ca",
1837
2949
  ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
1838
2950
  maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
1839
2951
  };
1840
2952
  return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
1841
2953
  };
1842
- var mapValue = (map5) => extMap(map5);
1843
- var writeBody5 = (form) => ({
2954
+ var mapValue = (map10) => extMap(map10);
2955
+ var writeBody10 = (form) => ({
1844
2956
  key_type: "ca",
1845
2957
  allowed_users: form.allowedUsers.join(","),
1846
2958
  default_user: form.defaultUser,
@@ -1863,18 +2975,18 @@ var sameMap = (want, have) => {
1863
2975
  return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);
1864
2976
  };
1865
2977
  var sameTtl6 = (want, have) => parseDuration(want) === have;
1866
- var badDurations2 = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
1867
- var matches7 = (attributes, form) => attributes.keyType === "ca" && sameList2(form.allowedUsers, attributes.allowedUsers) && attributes.defaultUser === form.defaultUser && attributes.allowUserCertificates === form.allowUserCertificates && attributes.allowHostCertificates === form.allowHostCertificates && sameList2(form.allowedDomains, attributes.allowedDomains) && attributes.allowSubdomains === form.allowSubdomains && attributes.allowBareDomains === form.allowBareDomains && sameList2(form.allowedExtensions, attributes.allowedExtensions) && sameMap(form.defaultExtensions, attributes.defaultExtensions) && sameList2(form.allowedCriticalOptions, attributes.allowedCriticalOptions) && sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) && attributes.allowEmptyPrincipals === form.allowEmptyPrincipals && sameTtl6(form.ttl, attributes.ttlSeconds) && sameTtl6(form.maxTtl, attributes.maxTtlSeconds);
2978
+ var badDurations = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
2979
+ var matches12 = (attributes, form) => attributes.keyType === "ca" && sameList2(form.allowedUsers, attributes.allowedUsers) && attributes.defaultUser === form.defaultUser && attributes.allowUserCertificates === form.allowUserCertificates && attributes.allowHostCertificates === form.allowHostCertificates && sameList2(form.allowedDomains, attributes.allowedDomains) && attributes.allowSubdomains === form.allowSubdomains && attributes.allowBareDomains === form.allowBareDomains && sameList2(form.allowedExtensions, attributes.allowedExtensions) && sameMap(form.defaultExtensions, attributes.defaultExtensions) && sameList2(form.allowedCriticalOptions, attributes.allowedCriticalOptions) && sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) && attributes.allowEmptyPrincipals === form.allowEmptyPrincipals && sameTtl6(form.ttl, attributes.ttlSeconds) && sameTtl6(form.maxTtl, attributes.maxTtlSeconds);
1868
2980
 
1869
2981
  // src/openbao/ssh-role.ts
1870
- var BaoSshRole = Resource9("Bao.SshRole", {
2982
+ var BaoSshRole = Resource14("Bao.SshRole", {
1871
2983
  defaultRemovalPolicy: "retain"
1872
2984
  });
1873
- var readRole4 = (form) => Effect17.gen(function* () {
2985
+ var readRole4 = (form) => Effect29.gen(function* () {
1874
2986
  const live = yield* baoRead(readPath4(form.mount, form.name));
1875
2987
  if (live === undefined)
1876
2988
  return;
1877
- return { attributes: attributesOf9(form, live), live };
2989
+ return { attributes: attributesOf14(form, live), live };
1878
2990
  });
1879
2991
  var UNMANAGED_DEFAULTS = {
1880
2992
  algorithm_signer: "default",
@@ -1889,22 +3001,22 @@ var UNMANAGED_DEFAULTS = {
1889
3001
  not_before_duration: 30
1890
3002
  };
1891
3003
  var isDefault = (live, field) => live[field] === undefined || live[field] === "" || live[field] === UNMANAGED_DEFAULTS[field];
1892
- var wouldErase = (live) => {
3004
+ var wouldErase2 = (live) => {
1893
3005
  const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));
1894
3006
  if (Object.keys(extMap(live["allowed_user_key_lengths"])).length > 0) {
1895
3007
  lost.push("allowed_user_key_lengths");
1896
3008
  }
1897
3009
  return lost.sort();
1898
3010
  };
1899
- var refuse2 = (message) => Effect17.die(new Error(message));
1900
- var BaoSshRoleProvider = () => Provider9.effect(BaoSshRole, Effect17.succeed(BaoSshRole.Provider.of({
1901
- list: () => Effect17.succeed([]),
1902
- read: Effect17.fn(function* ({ olds }) {
3011
+ var refuse6 = (message) => Effect29.die(new Error(message));
3012
+ var BaoSshRoleProvider = () => Provider14.effect(BaoSshRole, Effect29.succeed(BaoSshRole.Provider.of({
3013
+ list: () => Effect29.succeed([]),
3014
+ read: Effect29.fn(function* ({ olds }) {
1903
3015
  const found = yield* readRole4(resolve2(olds));
1904
3016
  return found?.attributes;
1905
3017
  }),
1906
- diff: Effect17.fn(function* ({ news, output }) {
1907
- if (output === undefined || !isResolved9(news))
3018
+ diff: Effect29.fn(function* ({ news, output }) {
3019
+ if (output === undefined || !isResolved14(news))
1908
3020
  return;
1909
3021
  const form = resolve2(news);
1910
3022
  if (output.mount !== form.mount || output.name !== form.name) {
@@ -1913,35 +3025,35 @@ var BaoSshRoleProvider = () => Provider9.effect(BaoSshRole, Effect17.succeed(Bao
1913
3025
  const found = yield* readRole4(form);
1914
3026
  if (found === undefined)
1915
3027
  return { action: "update" };
1916
- return matches7(found.attributes, form) ? { action: "noop" } : { action: "update" };
3028
+ return matches12(found.attributes, form) ? { action: "noop" } : { action: "update" };
1917
3029
  }),
1918
- reconcile: Effect17.fn(function* ({ news }) {
3030
+ reconcile: Effect29.fn(function* ({ news }) {
1919
3031
  const form = resolve2(news);
1920
3032
  const path = readPath4(form.mount, form.name);
1921
- const bad = badDurations2(form);
3033
+ const bad = badDurations(form);
1922
3034
  if (bad.length > 0) {
1923
- return yield* refuse2(`Bao.SshRole ${path}: ${bad.join(", ")} is not a duration parseDuration understands ` + "(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.");
3035
+ return yield* refuse6(`Bao.SshRole ${path}: ${bad.join(", ")} is not a duration parseDuration understands ` + "(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.");
1924
3036
  }
1925
3037
  if (form.allowUserCertificates && form.allowedUsers.length === 0 && !form.allowEmptyPrincipals) {
1926
- return yield* refuse2(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
3038
+ return yield* refuse6(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
1927
3039
  }
1928
3040
  const found = yield* readRole4(form);
1929
- if (found === undefined || !matches7(found.attributes, form)) {
3041
+ if (found === undefined || !matches12(found.attributes, form)) {
1930
3042
  if (found !== undefined) {
1931
- const lost = wouldErase(found.live);
3043
+ const lost = wouldErase2(found.live);
1932
3044
  if (lost.length > 0) {
1933
- return yield* refuse2(`Bao.SshRole ${path}: live role carries ${lost.join(", ")}, which this ` + "resource does not declare and the full-replace write would ERASE. " + "Fold those settings into the declaration, or clear them by hand first.");
3045
+ return yield* refuse6(`Bao.SshRole ${path}: live role carries ${lost.join(", ")}, which this ` + "resource does not declare and the full-replace write would ERASE. " + "Fold those settings into the declaration, or clear them by hand first.");
1934
3046
  }
1935
3047
  }
1936
- yield* baoWrite("PUT", path, writeBody5(form));
3048
+ yield* baoWrite("PUT", path, writeBody10(form));
1937
3049
  }
1938
3050
  const after = yield* readRole4(form);
1939
3051
  if (after === undefined) {
1940
- return yield* refuse2(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
3052
+ return yield* refuse6(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
1941
3053
  }
1942
3054
  return after.attributes;
1943
3055
  }),
1944
- delete: Effect17.fn(function* ({ output }) {
3056
+ delete: Effect29.fn(function* ({ output }) {
1945
3057
  yield* baoDelete(readPath4(output.mount, output.name));
1946
3058
  return;
1947
3059
  })
@@ -1953,7 +3065,18 @@ export {
1953
3065
  BaoAuthRoleProvider,
1954
3066
  BaoCloudflareRole,
1955
3067
  BaoCloudflareRoleProvider,
3068
+ BaoIdentityError,
3069
+ BaoJwtAuthConfig,
3070
+ BaoJwtAuthConfigProvider,
3071
+ BaoJwtRole,
3072
+ BaoJwtRoleProvider,
3073
+ BaoKubernetesRole,
3074
+ BaoKubernetesRoleProvider,
1956
3075
  BaoLoginError,
3076
+ BaoMfaLoginEnforcement,
3077
+ BaoMfaLoginEnforcementProvider,
3078
+ BaoMfaTotpMethod,
3079
+ BaoMfaTotpMethodProvider,
1957
3080
  BaoMount,
1958
3081
  BaoMountProvider,
1959
3082
  BaoPkiRole,
@@ -1967,10 +3090,15 @@ export {
1967
3090
  BaoSshRole,
1968
3091
  BaoSshRoleProvider,
1969
3092
  CloudflarePermissionGroups,
3093
+ HOST_ROLE_SEPARATOR,
1970
3094
  appRoleLogin,
1971
3095
  appRoleLoginEffect,
3096
+ assertBaoIdentity,
3097
+ assertBaoIdentityEffect,
1972
3098
  expandAccount,
1973
3099
  expandAll,
3100
+ hostAppRoles,
3101
+ hostRoleName,
1974
3102
  mountName,
1975
3103
  parseRolesConfig,
1976
3104
  permissionGroupsFromEngine,
@@ -1979,5 +3107,5 @@ export {
1979
3107
  revokeSelfEffect
1980
3108
  };
1981
3109
 
1982
- //# debugId=1C57EE110DEE3DA764756E2164756E21
3110
+ //# debugId=7E1DFE85CC398C5464756E2164756E21
1983
3111
  //# sourceMappingURL=index.js.map