@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
@@ -1 +1 @@
1
- {"version":3,"file":"mount-form.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-form.ts"],"names":[],"mappings":"AAEA,0FAA0F;AAC1F,eAAO,MAAM,SAAS,SAAU,MAAM,WAA6B,CAAC;AAIpE,qEAAqE;AACrE,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,MAAM,GAAG,SAkBrD,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,UAAU,UAAW,OAAO,KAAG,MAAM,GAAG,SAIpD,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AAiBD,eAAO,MAAM,YAAY,UAChB,aAAa,QACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,kBAYF,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,QAAQ,SAAU,MAAM,WAAoC,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,SAAU,MAAM,WAA6B,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAW,aAAa,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACsB,CAAC;AAE/F;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,UAAW,aAAa,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAMpE,CAAC;AASF,6EAA6E;AAC7E,eAAO,MAAM,OAAO,eAAgB,kBAAkB,SAAS,aAAa,YAIxB,CAAC;AAErD,eAAO,MAAM,SAAS,UAAW,aAAa,YAGb,CAAC"}
1
+ {"version":3,"file":"mount-form.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-form.ts"],"names":[],"mappings":"AAGA,0FAA0F;AAC1F,eAAO,MAAM,SAAS,SAAU,MAAM,WAA8B,CAAC;AAIrE,qEAAqE;AACrE,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,MAAM,GAAG,SAkBrD,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,UAAU,UAAW,OAAO,KAAG,MAAM,GAAG,SAIpD,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AAiBD,eAAO,MAAM,YAAY,UAChB,aAAa,QACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,kBAYF,CAAC;AAEF,iFAAiF;AACjF,eAAO,MAAM,QAAQ,SAAU,MAAM,WAAoC,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,SAAU,MAAM,WAA6B,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAW,aAAa,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CACsB,CAAC;AAE/F;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,UAAW,aAAa,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAMpE,CAAC;AASF,6EAA6E;AAC7E,eAAO,MAAM,OAAO,eAAgB,kBAAkB,SAAS,aAAa,YAIxB,CAAC;AAErD,eAAO,MAAM,SAAS,UAAW,aAAa,YAGb,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What a changed path means for Bao.Mount and Bao.AuthMethod — and the move, when one is declared.
3
+ *
4
+ * ⛔ A CHANGED PATH USED TO PLAN `update` AND ENABLE AN EMPTY MOUNT AT THE NEW PATH. diff read the
5
+ * new path, found nothing, answered `update`, and reconcile ran `enable` there: a fresh, empty
6
+ * `kv2/` beside the retained `kv/` that still held every secret, with Alchemy's state now naming
7
+ * only the empty one. Nothing failed, so nothing said so (site-config plan, Guards).
8
+ * ★ NOW A CHANGED PATH FAILS AT PLAN TIME, unless the declaration says where it came from:
9
+ * `remountFrom: '<old path>'` moves the mount with `sys/remount` (remount-wire.ts), which keeps
10
+ * every secret. A genuinely new, empty mount is a new resource id, never an edited path.
11
+ * ★ THE MOVE IS DECIDED BY LIVE STATE, SO A RE-RUN CONVERGES. Source present and target absent is
12
+ * a move; target present and source absent is a move that already happened; both present, or
13
+ * neither, is a refusal — "neither" in particular is exactly the empty-mount failure above.
14
+ */
15
+ import * as Effect from 'effect/Effect';
16
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
17
+ import type { BaoError } from './bao-status.ts';
18
+ import { type RemountError, type RemountOptions } from './remount-wire.ts';
19
+ export type Move = {
20
+ readonly kind: 'stay';
21
+ } | {
22
+ readonly kind: 'move';
23
+ readonly from: string;
24
+ } | {
25
+ readonly kind: 'refuse';
26
+ readonly message: string;
27
+ };
28
+ /**
29
+ * @param family `Bao.Mount` or `Bao.AuthMethod`, for the message.
30
+ * @param stated the path Alchemy's state holds, or undefined on a first reconcile.
31
+ * @param path the declared path.
32
+ * @param remountFrom the declared source, if any.
33
+ */
34
+ export declare const planMove: (family: string, stated: string | undefined, path: string, remountFrom: string | undefined) => Move;
35
+ export interface MoveTarget {
36
+ readonly family: string;
37
+ /** Declared path, bare. */
38
+ readonly path: string;
39
+ readonly type: string;
40
+ /** Read one path's live entry — undefined when absent. */
41
+ readonly read: (path: string) => Effect.Effect<{
42
+ readonly type: string;
43
+ } | undefined, BaoError, HttpClient.HttpClient>;
44
+ /** The path as sys/remount takes it: the bare path for secrets, `auth/<path>` for auth. */
45
+ readonly wire: (path: string) => string;
46
+ }
47
+ /** Perform a planned move, deciding by live state. Returns once the target is in place. */
48
+ export declare const performMove: (target: MoveTarget, from: string, options?: RemountOptions) => Effect.Effect<void, BaoError | RemountError, HttpClient.HttpClient>;
49
+ //# sourceMappingURL=mount-move.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount-move.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-move.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE3F,MAAM,MAAM,IAAI,GACZ;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,WACX,MAAM,UACN,MAAM,GAAG,SAAS,QACpB,MAAM,eACC,MAAM,GAAG,SAAS,KAC9B,IAyBF,CAAC;AAEF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2BAA2B;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,CACb,IAAI,EAAE,MAAM,KACT,MAAM,CAAC,MAAM,CAAC;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;IAC3F,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACzC;AAID,2FAA2F;AAC3F,eAAO,MAAM,WAAW,WACd,UAAU,QACZ,MAAM,YACF,cAAc,KACvB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,YAAY,EAAE,UAAU,CAAC,UAAU,CA6BjE,CAAC"}
@@ -1,10 +1,14 @@
1
1
  /**
2
- * Slash trimming for caller-supplied mount paths.
2
+ * Trimming runs of one character from caller-supplied paths and names.
3
3
  *
4
- * ★ LOOPS, NOT `/^\/+|\/+$/g` OR `/\/+$/`. A trailing `\/+$` backtracks polynomially on a long run
5
- * of `/` followed by anything else (CodeQL js/polynomial-redos), and mount paths are caller input.
6
- * Both loops are linear in the input length.
4
+ * ★ LOOPS, NOT `/^\/+|\/+$/g`, `/\/+$/` OR `/^-+|-+$/g`. A trailing `x+$` backtracks polynomially
5
+ * on a long run of `x` followed by anything else (CodeQL js/polynomial-redos), and mount paths,
6
+ * namespaces and slugs are caller input. Every loop here is linear in the input length.
7
7
  */
8
+ /** `text` without a trailing run of `char` (one UTF-16 unit). */
9
+ export declare const trimTrailing: (text: string, char: string) => string;
10
+ /** `text` without a leading or trailing run of `char` (one UTF-16 unit). */
11
+ export declare const trimRuns: (text: string, char: string) => string;
8
12
  /** `text` without trailing `/`. */
9
13
  export declare const trimTrailingSlashes: (text: string) => string;
10
14
  /** `text` without leading or trailing `/`. */
@@ -1 +1 @@
1
- {"version":3,"file":"mount-path.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mCAAmC;AACnC,eAAO,MAAM,mBAAmB,SAAU,MAAM,KAAG,MAIlD,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,MAI1C,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,KAAG,MAClB,CAAC"}
1
+ {"version":3,"file":"mount-path.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,iEAAiE;AACjE,eAAO,MAAM,YAAY,SAAU,MAAM,QAAQ,MAAM,KAAG,MAIzD,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,SAAU,MAAM,QAAQ,MAAM,KAAG,MAIrD,CAAC;AAEF,mCAAmC;AACnC,eAAO,MAAM,mBAAmB,SAAU,MAAM,KAAG,MAAiC,CAAC;AAErF,8CAA8C;AAC9C,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,MAA6B,CAAC;AAEzE,kFAAkF;AAClF,eAAO,MAAM,UAAU,UAAW,MAAM,GAAG,SAAS,KAAG,MAClB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bao.Mount's read and reconcile as plain effects — split from mount.ts so the move (and its
3
+ * refusals) run against a fake server, not only in a reviewer's head.
4
+ */
5
+ import * as Effect from 'effect/Effect';
6
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
7
+ import type { BaoError } from './bao-status.ts';
8
+ import { type BaoMountAttributes, type BaoMountProps } from './mount-form.ts';
9
+ import type { RemountError, RemountOptions } from './remount-wire.ts';
10
+ export declare const readMount: (props: BaoMountProps) => Effect.Effect<BaoMountAttributes | undefined, BaoError, HttpClient.HttpClient>;
11
+ /**
12
+ * Converge one mount. `stated` is the path in Alchemy's state (undefined on a first reconcile).
13
+ * Dies on a refusal; fails with OpenBao's own error on a refused call.
14
+ */
15
+ export declare const reconcileMount: (news: BaoMountProps, stated: string | undefined, options?: RemountOptions) => Effect.Effect<BaoMountAttributes, BaoError | RemountError, HttpClient.HttpClient>;
16
+ //# sourceMappingURL=mount-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount-reconcile.d.ts","sourceRoot":"","sources":["../../src/openbao/mount-reconcile.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAKnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEtE,eAAO,MAAM,SAAS,UACb,aAAa,KACnB,MAAM,CAAC,MAAM,CAAC,kBAAkB,GAAG,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAK5E,CAAC;AAUL;;;GAGG;AACH,eAAO,MAAM,cAAc,SACnB,aAAa,UACX,MAAM,GAAG,SAAS,YAChB,cAAc,KACvB,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,QAAQ,GAAG,YAAY,EAAE,UAAU,CAAC,UAAU,CA8B/E,CAAC"}
@@ -7,6 +7,15 @@
7
7
  *
8
8
  * ★ `defaultRemovalPolicy: 'retain'` — disabling a mount destroys every secret under it.
9
9
  * Opt in with `.pipe(RemovalPolicy.destroy())`; see resource.ts in house/proxmox.
10
+ *
11
+ * ★ REPLACE SEMANTICS (audited 2026-09-21, see src/openbao/REPLACE.md):
12
+ * · `type` changed → plans `replace`, and the APPLY FAILS, destroying nothing: the new
13
+ * generation's path is the old one's, still occupied, so its reconcile dies "type is
14
+ * immutable" before any write (mount-reconcile.ts). Change a type by hand. ⛔ Not `deleteFirst`
15
+ * on purpose — under `destroy` that would disable the mount, every secret with it, on a typo.
16
+ * · `path` changed → FAILS the plan unless `remountFrom` names the old path, which is an
17
+ * in-place `update` that moves the mount and its data (mount-move.ts). It used to be an
18
+ * `update` that enabled an EMPTY mount at the new path.
10
19
  */
11
20
  import { Resource } from 'alchemy';
12
21
  import * as Provider from 'alchemy/Provider';
@@ -1 +1 @@
1
- {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../../src/openbao/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAKnB,MAAM,iBAAiB,CAAC;AAGzB,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAElD,MAAM,WAAW,QAAS,SAAQ,QAAQ,CACxC,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,KAAK,EACL,UAAU,CAAC,UAAU,CACtB;CAAG;AAEJ,eAAO,MAAM,QAAQ,2CAEnB,CAAC;AASH,eAAO,MAAM,gBAAgB,+FAqF1B,CAAC"}
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../../src/openbao/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAW,MAAM,iBAAiB,CAAC;AAKvF,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC;AAElD,MAAM,WAAW,QAAS,SAAQ,QAAQ,CACxC,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,KAAK,EACL,UAAU,CAAC,UAAU,CACtB;CAAG;AAEJ,eAAO,MAAM,QAAQ,2CAEnB,CAAC;AAEH,eAAO,MAAM,gBAAgB,+FAuE1B,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { BaoPkiRoleProps } from './pki-role-form.ts';
2
+ /**
3
+ * The managed field set, normalised so both sides of a diff are byte-comparable.
4
+ *
5
+ * ⚠️ KEY ORDER IS THE DIGEST. The props side and the live side are both built by the single
6
+ * canonical() literal below, so `JSON.stringify` emits the same key order for each. Two
7
+ * hand-written object literals would drift apart and produce a forever-diff nobody could
8
+ * read from the plan output.
9
+ */
10
+ export interface BaoPkiRoleCanonical {
11
+ allowBareDomains: boolean;
12
+ allowGlobDomains: boolean;
13
+ allowIpSans: boolean;
14
+ allowLocalhost: boolean;
15
+ allowSubdomains: boolean;
16
+ allowWildcardCertificates: boolean;
17
+ allowedDomains: readonly string[];
18
+ allowedDomainsTemplate: boolean;
19
+ clientFlag: boolean;
20
+ cnValidations: readonly string[];
21
+ enforceHostnames: boolean;
22
+ extKeyUsage: readonly string[];
23
+ generateLease: boolean;
24
+ keyBits: number;
25
+ keyType: string;
26
+ keyUsage: readonly string[];
27
+ /** ⚠️ `-1` means the duration prop did not parse — reconcile refuses rather than writing it. */
28
+ maxTtlSeconds: number;
29
+ noStore: boolean;
30
+ requireCn: boolean;
31
+ serverFlag: boolean;
32
+ signatureBits: number;
33
+ ttlSeconds: number;
34
+ }
35
+ /** ★ OpenBao's own `key_usage` default (path_roles.go:697-698) — what an omitted field becomes. */
36
+ export declare const DEFAULT_KEY_USAGE: readonly string[];
37
+ export declare const canonicalFromProps: (props: BaoPkiRoleProps) => BaoPkiRoleCanonical;
38
+ /**
39
+ * ⚠️ AN ABSENT NEW FIELD READS AS ITS ZERO VALUE, NOT ITS SCHEMA DEFAULT. A role read always carries
40
+ * the six new fields (ToResponseData, path_roles.go:1720-1780; `generate_lease` only when set,
41
+ * and getRole upgrades a missing one to false at :1034), so the fallback only answers a body
42
+ * that is not OpenBao's — and then "false"/"none" reads as drift rather than a false noop.
43
+ */
44
+ export declare const canonicalFromLive: (live: Record<string, unknown>) => BaoPkiRoleCanonical;
45
+ export declare const digestOf: (form: BaoPkiRoleCanonical) => string;
46
+ //# sourceMappingURL=pki-role-canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pki-role-canonical.d.ts","sourceRoot":"","sources":["../../src/openbao/pki-role-canonical.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,yBAAyB,EAAE,OAAO,CAAC;IACnC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,sBAAsB,EAAE,OAAO,CAAC;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,gGAAgG;IAChG,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAI9C,CAAC;AAqCF,eAAO,MAAM,kBAAkB,UAAW,eAAe,KAAG,mBAwBxD,CAAC;AAYL;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,SAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,mBAwB9D,CAAC;AAEL,eAAO,MAAM,QAAQ,SAAU,mBAAmB,KAAG,MAAsC,CAAC"}
@@ -1,3 +1,5 @@
1
+ import { type BaoPkiRoleCanonical } from './pki-role-canonical.ts';
2
+ export type { BaoPkiRoleCanonical };
1
3
  /** ★ MEASURED: the OpenAPI enum for `key_type` is exactly these four, lowercase. */
2
4
  export type BaoPkiKeyType = 'any' | 'ec' | 'ed25519' | 'rsa';
3
5
  /** ★ MEASURED: live roles carry `["hostname"]` or the default `["email","hostname"]`. */
@@ -51,33 +53,29 @@ export interface BaoPkiRoleProps {
51
53
  cnValidations?: readonly BaoPkiCnValidation[];
52
54
  /** Signature hash bits; `0` auto-detects from key length. One live role pins 256. */
53
55
  signatureBits?: number;
54
- }
55
- /**
56
- * The managed field set, normalised so both sides of a diff are byte-comparable.
57
- *
58
- * ⚠️ KEY ORDER IS THE DIGEST. The props side and the live side are both built by the single
59
- * canonical() literal below, so `JSON.stringify` emits the same key order for each. Two
60
- * hand-written object literals would drift apart and produce a forever-diff nobody could
61
- * read from the plan output.
62
- */
63
- export interface BaoPkiRoleCanonical {
64
- allowBareDomains: boolean;
65
- allowGlobDomains: boolean;
66
- allowIpSans: boolean;
67
- allowLocalhost: boolean;
68
- allowSubdomains: boolean;
69
- allowWildcardCertificates: boolean;
70
- allowedDomains: readonly string[];
71
- clientFlag: boolean;
72
- cnValidations: readonly string[];
73
- extKeyUsage: readonly string[];
74
- keyBits: number;
75
- keyType: string;
76
- /** ⚠️ `-1` means the duration prop did not parse — reconcile refuses rather than writing it. */
77
- maxTtlSeconds: number;
78
- serverFlag: boolean;
79
- signatureBits: number;
80
- ttlSeconds: number;
56
+ /**
57
+ * ★ THE SIX BELOW (added 2026-09-21) default to OpenBao's OWN values, not closed ones — a role this
58
+ * resource wrote before they were props carries exactly those, so it still plans `noop`. The
59
+ * write already sent them implicitly; see the header of pki-role-canonical.ts.
60
+ */
61
+ /** A request must carry a common_name. Default true. */
62
+ requireCn?: boolean;
63
+ /** Only valid host names in the CN and DNS SANs. Default true. */
64
+ enforceHostnames?: boolean;
65
+ /**
66
+ * Key usages minus the `KeyUsage` prefix. Default `DigitalSignature, KeyAgreement,
67
+ * KeyEncipherment`. ⚠️ `[]` is a real value — it removes every key usage — and is sent as such.
68
+ */
69
+ keyUsage?: readonly string[];
70
+ /** Allowed domains may use identity templates (`{{identity.entity.name}}`). Default false. */
71
+ allowedDomainsTemplate?: boolean;
72
+ /**
73
+ * Do not store issued certificates. Default false. ⛔ They can then be neither listed nor
74
+ * revoked. ⚠️ Implies `generateLease: false`; declaring both true is refused (see problems).
75
+ */
76
+ noStore?: boolean;
77
+ /** Attach an OpenBao lease to every issued certificate. Default false — leases slow startup. */
78
+ generateLease?: boolean;
81
79
  }
82
80
  export interface BaoPkiRoleAttributes extends BaoPkiRoleCanonical {
83
81
  name: string;
@@ -88,9 +86,6 @@ export interface BaoPkiRoleAttributes extends BaoPkiRoleCanonical {
88
86
  /** SHA-256 of the canonical form — safe to persist; see policy.ts. */
89
87
  digest: string;
90
88
  }
91
- export declare const canonicalFromProps: (props: BaoPkiRoleProps) => BaoPkiRoleCanonical;
92
- export declare const canonicalFromLive: (live: Record<string, unknown>) => BaoPkiRoleCanonical;
93
- export declare const digestOf: (form: BaoPkiRoleCanonical) => string;
94
89
  export declare const rolePath: (props: {
95
90
  name: string;
96
91
  mount?: string;
@@ -109,10 +104,23 @@ export declare const attributesOf: (props: BaoPkiRoleProps, live: Record<string,
109
104
  * ★ `ext_key_usage` is OMITTED when empty rather than sent as `ext_key_usage=`. The write
110
105
  * replaces the whole role, so an absent field already lands on the `[]` default, and
111
106
  * omitting sidesteps how an empty value would be encoded.
107
+ * ⚠️ `key_usage` CANNOT BE OMITTED THAT WAY — its default is NOT empty — so it is always sent, and
108
+ * an empty declaration goes over as `""`. That encoding is now READ, not guessed: for a
109
+ * comma-string-slice field, go-secure-stdlib parseutil.ParseCommaStringSlice (:389-392)
110
+ * answers `""` with an empty list.
112
111
  *
113
112
  * ★ THE BODY FOR `PUT <mount>/roles/<name>`, ALL STRINGS — the same `k=v` pairs `bao write` sent.
114
113
  */
115
114
  export declare const writeBody: (props: BaoPkiRoleProps) => Record<string, string>;
116
115
  /** True when live already matches the declaration on every managed field. */
117
116
  export declare const matches: (attributes: BaoPkiRoleAttributes, props: BaoPkiRoleProps) => boolean;
117
+ /**
118
+ * Declarations reconcile must refuse rather than write. diff routes any of these to `update` so the
119
+ * refusal is seen on the next deploy, never hidden behind a `noop`.
120
+ *
121
+ * ⚠️ `noStore` WITH `generateLease` IS NOT AN ERROR ON THE SERVER — pathRoleCreate stores
122
+ * generate_lease false and adds a warning (path_roles.go:1250-1256) — so the role would read back
123
+ * different from its declaration and plan `update` forever. Refused here instead.
124
+ */
125
+ export declare const problems: (props: BaoPkiRoleProps) => readonly string[];
118
126
  //# sourceMappingURL=pki-role-form.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pki-role-form.d.ts","sourceRoot":"","sources":["../../src/openbao/pki-role-form.ts"],"names":[],"mappings":"AAGA,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7D,yFAAyF;AACzF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oDAAoD;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oGAAoG;IACpG,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,gGAAgG;IAChG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,8FAA8F;IAC9F,aAAa,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,yBAAyB,EAAE,OAAO,CAAC;IACnC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,gGAAgG;IAChG,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AA8BD,eAAO,MAAM,kBAAkB,UAAW,eAAe,KAAG,mBAkBxD,CAAC;AAYL,eAAO,MAAM,iBAAiB,SAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,mBAkB9D,CAAC;AAWL,eAAO,MAAM,QAAQ,SAAU,mBAAmB,WAAiC,CAAC;AAEpF,eAAO,MAAM,QAAQ,UAAW;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,WACN,CAAC;AAE3D,eAAO,MAAM,YAAY,UAChB,eAAe,QAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,oBAUF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,SAAS,UAAW,eAAe,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAsBvE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,OAAO,eAAgB,oBAAoB,SAAS,eAAe,YACrB,CAAC"}
1
+ {"version":3,"file":"pki-role-form.d.ts","sourceRoot":"","sources":["../../src/openbao/pki-role-form.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAKzB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7D,yFAAyF;AACzF,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oDAAoD;IACpD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oGAAoG;IACpG,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,gGAAgG;IAChG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,8FAA8F;IAC9F,aAAa,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,8FAA8F;IAC9F,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gGAAgG;IAChG,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,oBAAqB,SAAQ,mBAAmB;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AAWD,eAAO,MAAM,QAAQ,UAAW;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,WACN,CAAC;AAE3D,eAAO,MAAM,YAAY,UAChB,eAAe,QAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,oBAUF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,SAAS,UAAW,eAAe,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CA4BvE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,OAAO,eAAgB,oBAAoB,SAAS,eAAe,YACrB,CAAC;AAE5D;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,UAAW,eAAe,KAAG,SAAS,MAAM,EAuBhE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"pki-role.d.ts","sourceRoot":"","sources":["../../src/openbao/pki-role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAGnE,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EAKrB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,CAAC;AAEtD,MAAM,WAAW,UAAW,SAAQ,QAAQ,CAC1C,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,KAAK,EACL,UAAU,CAAC,UAAU,CACtB;CAAG;AAEJ,eAAO,MAAM,UAAU,6CAErB,CAAC;AAeH,eAAO,MAAM,kBAAkB,iGAsH5B,CAAC"}
1
+ {"version":3,"file":"pki-role.d.ts","sourceRoot":"","sources":["../../src/openbao/pki-role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAGnE,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EAMrB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,CAAC;AAEtD,MAAM,WAAW,UAAW,SAAQ,QAAQ,CAC1C,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,KAAK,EACL,UAAU,CAAC,UAAU,CACtB;CAAG;AAEJ,eAAO,MAAM,UAAU,6CAErB,CAAC;AASH,eAAO,MAAM,kBAAkB,iGA4G5B,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `sys/remount` — move a secrets engine or auth method to a new path WITH its data — and the status
3
+ * polling it needs, because the move finishes after the call returns.
4
+ *
5
+ * ★ READ FROM openbao v2.6.2, NOT RECALLED:
6
+ * · api/sys_mounts.go:95-191 — the CLI's `bao secrets move` POSTs `{from, to}` to sys/remount,
7
+ * takes `data.migration_id`, then GETs sys/remount/status/<id> once a second until
8
+ * `migration_info.status` is `success` or `failure`. This is that loop, bounded.
9
+ * · vault/logical_system.go:1257-1357 — the handler validates, records the migration and
10
+ * answers AT ONCE; a goroutine does the move. So a 200 means "queued", never "moved".
11
+ * · vault/mount.go:630-735 — the mount keeps its UUID, and storage lives under the UUID, so every
12
+ * secret stays. ⛔ BUT EVERY LEASE UNDER THE SOURCE IS REVOKED (`RevokePrefix`, :684): a move of
13
+ * a dynamic-secrets mount kills every credential it has issued. KV has no leases.
14
+ * · vault/logical_system.go:67-69 — `remount` is a sudo path. The status path is not.
15
+ * ⚠️ THE STATUS TRACKER IS IN MEMORY (vault/mount.go:2303-2336, a sync.Map on the active node). A
16
+ * restart or leader change mid-move loses it, and the status read then answers 404. That is not a
17
+ * failure and not a success; `confirm` settles it from the mount tables, or the call fails.
18
+ * ⛔ POLICIES ARE NOT REWRITTEN. Every ACL path naming the old mount keeps naming it.
19
+ */
20
+ import * as Effect from 'effect/Effect';
21
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
22
+ import type { BaoError } from './bao-status.ts';
23
+ /** A move OpenBao reported failed, or one this could not confirm. Never carries a token. */
24
+ export declare class RemountError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ export interface RemountOptions {
28
+ /** Milliseconds between status reads. Default 1000 — the CLI's own pace. */
29
+ readonly intervalMillis?: number;
30
+ /** Status reads before giving up. Default 120, so about two minutes. */
31
+ readonly attempts?: number;
32
+ }
33
+ /** POST sys/remount and return the migration id. `from`/`to` are full paths (`auth/x` for auth). */
34
+ export declare const startRemount: (from: string, to: string) => Effect.Effect<string, BaoError | RemountError, HttpClient.HttpClient>;
35
+ /** `in-progress`, `success`, `failure` — or undefined when the server no longer knows the id. */
36
+ export declare const remountStatus: (id: string) => Effect.Effect<string | undefined, BaoError, HttpClient.HttpClient>;
37
+ /**
38
+ * Move `from` to `to` and wait until OpenBao says it is done.
39
+ *
40
+ * @param confirm answers "does the mount table show the move finished?" — asked only when the
41
+ * status tracker has forgotten the id. True is success; false fails.
42
+ */
43
+ export declare const remountAndWait: (from: string, to: string, confirm: Effect.Effect<boolean, BaoError, HttpClient.HttpClient>, options?: RemountOptions) => Effect.Effect<void, BaoError | RemountError, HttpClient.HttpClient>;
44
+ //# sourceMappingURL=remount-wire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remount-wire.d.ts","sourceRoot":"","sources":["../../src/openbao/remount-wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAEnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,4FAA4F;AAC5F,qBAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAOD,oGAAoG;AACpG,eAAO,MAAM,YAAY,SACjB,MAAM,MACR,MAAM,KACT,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,YAAY,EAAE,UAAU,CAAC,UAAU,CAMnE,CAAC;AAEL,iGAAiG;AACjG,eAAO,MAAM,aAAa,OACpB,MAAM,KACT,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAKhE,CAAC;AAEL;;;;;GAKG;AACH,eAAO,MAAM,cAAc,SACnB,MAAM,MACR,MAAM,WACD,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,YACvD,cAAc,KACtB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,YAAY,EAAE,UAAU,CAAC,UAAU,CAkCjE,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The read / plan / reconcile loop the newer role families share (Bao.JwtRole, Bao.KubernetesRole,
3
+ * Bao.JwtAuthConfig): one path, one body, one comparison. Each family supplies its own form; this
4
+ * owns the order of operations, which is where the estate's providers have gone wrong before.
5
+ *
6
+ * ★ THE ORDER, AND WHY EACH STEP IS THERE:
7
+ * 1. Refuse a declaration the family knows is wrong, before any call (`problems`).
8
+ * 2. Read live. Write only when it differs — an identical write is still an audit line.
9
+ * 3. Before writing over a live object, refuse if the write would silently destroy something
10
+ * this resource does not model (`wouldErase`, the ssh-role.ts pattern).
11
+ * 4. Re-read, and refuse unless live now MATCHES. A 2xx is not proof: the server can accept a
12
+ * field and store something else (pki-role.ts has the history).
13
+ * ⛔ A DECLARATION reconcile WOULD REFUSE NEVER PLANS `noop` — `planRole` routes it to `update`,
14
+ * so the refusal is seen at the next deploy instead of hiding behind a clean plan.
15
+ */
16
+ import * as Effect from 'effect/Effect';
17
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
18
+ import type { BaoError } from './bao-status.ts';
19
+ export interface RoleSpec<A> {
20
+ /** `Bao.JwtRole` etc., for messages. */
21
+ readonly family: string;
22
+ /** The API path, e.g. `auth/jwt/role/humans`. */
23
+ readonly path: string;
24
+ readonly problems: readonly string[];
25
+ readonly attributesOf: (live: Record<string, unknown>) => A;
26
+ readonly matches: (attributes: A) => boolean;
27
+ readonly body: Readonly<Record<string, unknown>>;
28
+ /** Live fields a write would reset that the declaration does not model. */
29
+ readonly wouldErase?: (live: Record<string, unknown>) => readonly string[];
30
+ }
31
+ type Found<A> = {
32
+ readonly attributes: A;
33
+ readonly live: Record<string, unknown>;
34
+ };
35
+ export declare const readRoleAt: <A>(spec: RoleSpec<A>) => Effect.Effect<Found<A> | undefined, BaoError, HttpClient.HttpClient>;
36
+ /** `update` or `noop` — the part of a diff after the identity checks. */
37
+ export declare const planRole: <A>(spec: RoleSpec<A>) => Effect.Effect<'noop' | 'update', BaoError, HttpClient.HttpClient>;
38
+ export declare const reconcileRole: <A>(spec: RoleSpec<A>) => Effect.Effect<A, BaoError, HttpClient.HttpClient>;
39
+ export {};
40
+ //# sourceMappingURL=role-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-reconcile.d.ts","sourceRoot":"","sources":["../../src/openbao/role-reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAEnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,CAAC,KAAK,OAAO,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC;CAC5E;AAED,KAAK,KAAK,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAEnF,eAAO,MAAM,UAAU,GAAI,CAAC,QACpB,QAAQ,CAAC,CAAC,CAAC,KAChB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAGnE,CAAC;AAEJ,yEAAyE;AACzE,eAAO,MAAM,QAAQ,GAAI,CAAC,QAClB,QAAQ,CAAC,CAAC,CAAC,KAChB,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAK/D,CAAC;AAKL,eAAO,MAAM,aAAa,GAAI,CAAC,QACvB,QAAQ,CAAC,CAAC,CAAC,KAChB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CA0B/C,CAAC"}
@@ -0,0 +1,167 @@
1
+ # launchd — `@homeflare/alchemy/launchd`
2
+
3
+ Declare a Mac host with Alchemy: its launchd jobs (`LaunchdJob`) and the config files they read
4
+ (`HostFile`). launchd has no SDK, so the provider renders the plist itself and drives
5
+ `launchctl`. Every file read, file write and `launchctl` call goes through one injectable
6
+ `HostRunner`.
7
+
8
+ ```ts
9
+ import * as Effect from 'effect/Effect';
10
+ import { HostFile, LaunchdJob, launchdProviders } from '@homeflare/alchemy/launchd';
11
+
12
+ export const exporter = Effect.gen(function* () {
13
+ const config = yield* HostFile('exporter-config', {
14
+ path: '/usr/local/etc/example-exporter.yml',
15
+ content: 'listen: 127.0.0.1:9100\n',
16
+ mode: 0o644,
17
+ owner: 'root',
18
+ group: 'wheel',
19
+ });
20
+ yield* LaunchdJob('exporter', {
21
+ label: 'com.example.exporter',
22
+ domain: 'system', // or 'gui/<uid>' for a LaunchAgent
23
+ // ★ config.path, not the literal: the Output orders the job after its file is written.
24
+ programArguments: ['/usr/local/bin/example-exporter', '--config', config.path],
25
+ environment: { TZ: 'UTC' }, // ⛔ non-secret values only
26
+ runAtLoad: true,
27
+ keepAlive: { successfulExit: false },
28
+ throttleInterval: 30,
29
+ standardOutPath: '/var/log/example-exporter.log',
30
+ standardErrorPath: '/var/log/example-exporter.log',
31
+ });
32
+ });
33
+ // Provide launchdProviders() alongside the stack's other provider layers.
34
+ ```
35
+
36
+ ## LaunchdJob
37
+
38
+ | prop | plist key | notes |
39
+ | ----------------------------- | -------------------------------- | ------------------------------------------------------ |
40
+ | `label` | `Label` | also the file name; `[A-Za-z0-9._-]`, no `/` |
41
+ | `domain` | — | `system` (LaunchDaemons) or `gui/<uid>` (LaunchAgents) |
42
+ | `programArguments` | `ProgramArguments` | first element must be an absolute path |
43
+ | `environment` | `EnvironmentVariables` | ⛔ non-secret only (see below) |
44
+ | `runAtLoad` | `RunAtLoad` | |
45
+ | `keepAlive` | `KeepAlive` | `true`/`false` or `{ successfulExit, crashed }` |
46
+ | `startInterval` | `StartInterval` | seconds, > 0 |
47
+ | `startCalendarInterval` | `StartCalendarInterval` | one entry or a list; fields are range-checked |
48
+ | `throttleInterval` | `ThrottleInterval` | seconds, ≥ 0 |
49
+ | `standardOutPath` / `…Error…` | `StandardOutPath` / `…ErrorPath` | absolute |
50
+ | `workingDirectory` | `WorkingDirectory` | absolute |
51
+ | `userName` / `groupName` | `UserName` / `GroupName` | system domain only — launchd ignores them for agents |
52
+ | `extraKeys` | anything else, verbatim | may not repeat a key a typed prop owns |
53
+
54
+ The plist path is derived, never declared: `/Library/LaunchDaemons/<label>.plist` for `system`,
55
+ `<home of uid>/Library/LaunchAgents/<label>.plist` for `gui/<uid>`.
56
+
57
+ | step | what happens |
58
+ | ------- | --------------------------------------------------------------------------------------------------- |
59
+ | create | write the plist atomically (system: `root:wheel 0644`), then `launchctl bootstrap` |
60
+ | update | write, `bootout` the running job (and wait until launchd lets go), `bootstrap` — **a restart** |
61
+ | read | `launchctl print` (loaded? state, pid, last exit) plus the SHA-256 of the plist on disk |
62
+ | diff | the rendered plist's SHA-256 against the stored digest **and** the on-disk one, plus "is it loaded" |
63
+ | replace | only when `label` or `domain` changes — **delete first**, so two copies never run at once |
64
+ | delete | `bootout` if loaded, remove the plist; idempotent |
65
+
66
+ - A converged job is not restarted: a retried deploy that finds the right plist loaded does nothing.
67
+ - ⛔ **A rename is checked at plan time.** Alchemy deletes the old job before it creates the new
68
+ one, so everything the new one would be refused for (an invalid or reserved label, a domain you
69
+ may not write, a disabled label, a label another job already holds) fails the **plan**, with the
70
+ old job still running. A rename is seen even while other props are unresolved Outputs.
71
+ - ⚠️ A failed `bootstrap` leaves the job **down**. On an update the new plist stays on disk, the
72
+ state keeps the old digest, and the next deploy retries. On a first create the plist is removed
73
+ again, so the next plan does not find its own file and call it someone else's.
74
+ There is no rollback to an older plist.
75
+ - ⛔ A label disabled with `launchctl disable` is refused, not re-enabled: that was someone's
76
+ decision. The error names the `launchctl enable` command to run if it is stale.
77
+ - ⚠️ A `gui/<uid>` job needs that user logged in: without a login session the domain does not
78
+ exist (`launchctl print` exits 112, "Could not find domain"; measured). Deploying refuses before
79
+ it writes anything. Deleting still removes the plist, which stops launchd loading it at login.
80
+ - ⚠️ The digest covers the rendered bytes, so a kit upgrade that changes the renderer's output
81
+ restarts every declared job on the next deploy. Such a release says so in its changelog.
82
+
83
+ ## HostFile
84
+
85
+ `path` (absolute, normalised), `content` (UTF-8 text), `mode` (default `0o644`), `owner` and
86
+ `group` (names or ids). Written as a temp file in the same directory — mode and owner set before
87
+ it becomes visible — then renamed over the path. Read back after every write. Diffed by SHA-256,
88
+ mode and owner. A new `path` is a create-before-delete replace. Delete removes the file.
89
+
90
+ - ⛔ A different file already at a new `path` is refused, not overwritten: the adoption probe
91
+ never looks at a replace's new path, so a typo there would otherwise overwrite a system file.
92
+
93
+ - ⛔ A symlink or directory at the path is refused, never replaced: rename over a symlink replaces
94
+ the link, which takes the path from whatever tool owns it (nix-darwin's `/etc` entries point
95
+ into `/nix/store`).
96
+ - ⚠️ The parent directory must exist. Creating it would invent an owner and mode nobody declared.
97
+
98
+ ## ⛔ No secrets in props
99
+
100
+ Alchemy stores props and attributes **unencrypted** in its state store. A job's `environment`,
101
+ `programArguments` and `extraKeys`, and a file's `content`, land there, in every plan diff, and on
102
+ disk.
103
+
104
+ **Keep secret files rendered by a secret renderer** — `openbao-agent` templates on a HomeFlare
105
+ host — and declare only the path: `FOO_TOKEN_FILE=/path`, `--token-file /path`. A tripwire refuses
106
+ the obvious mistakes (a `*_TOKEN`/`*_PASSWORD`-style variable, `--token=…`, a PEM private key); it
107
+ is not a scanner, and passing it does not make a value safe to declare.
108
+
109
+ ## ⛔ No silent sudo
110
+
111
+ The `system` domain, and another user's `gui/<uid>`, need root. The provider refuses up front
112
+ unless the runner is root (`effectiveUid() === 0`) or was deliberately built with
113
+ `privileged: true`. `localRunner()` — the default — is never privileged and never calls `sudo`, so:
114
+
115
+ - **system daemons:** start the whole deploy as root; or pass `launchdProviders(yourRunner)` where
116
+ `yourRunner` is a `HostRunner` you wrote that already holds root (a root helper, say).
117
+ - **your own agents** (`gui/<your uid>`): no root needed.
118
+
119
+ Reading (`launchctl print`, `print-disabled`) needs no root. ⚠️ A `HostFile` whose mode hides it
120
+ from the planning user (a root-owned `0600`, say) fails the plan with `EACCES`: plan as the user
121
+ you deploy as.
122
+
123
+ ## Adoption: nothing is taken over silently
124
+
125
+ `read` with no prior state reports anything it finds — a loaded label, a plist, a file — as
126
+ `Unowned`, so Alchemy refuses to take it over without `--adopt`.
127
+
128
+ Labels under `org.nixos.`, `com.apple.` and `homebrew.mxcl.` are **refused outright**. nix-darwin
129
+ rewrites its plists on every activation and unloads any it no longer lists, macOS owns
130
+ `com.apple.*`, and `brew services` owns `homebrew.mxcl.*`. Adopting one would give a file two
131
+ owners. Move the job instead.
132
+
133
+ ## Cutover from nix-darwin
134
+
135
+ Nix-managed `org.nixos.*` jobs are **not adopted**. The host stack declares a **new** label, and the
136
+ Nix job is booted out first. Per job:
137
+
138
+ 1. **Declare** the replacement in the host stack under a new label (`com.example.<job>`), with its
139
+ config as `HostFile`s. ⚠️ Nothing it references may live in `/nix/store` — the next garbage
140
+ collection deletes it. Plan it: expect creates only.
141
+ 2. **Remove the job from the nix-darwin config and activate.** Activation unloads and deletes every
142
+ daemon it no longer lists — checked 2026-09-21 in a nix-darwin activation script: it prints
143
+ `removing service <label>`, runs `launchctl unload`, then removes the plist. Confirm with
144
+ `launchctl print system/org.nixos.<job>` → exit 113 ("Could not find service").
145
+ 3. **Deploy** the host stack. The new label bootstraps.
146
+ 4. **Verify** `launchctl print system/com.example.<job>` shows `state = running`, then check the
147
+ service itself (port, metrics, logs).
148
+
149
+ - ★ **Why this order.** Both jobs usually bind the same port and write the same files; the old one
150
+ must be gone before the new one starts. Downtime is one deploy.
151
+ - ⚠️ **`sudo launchctl bootout system/org.nixos.<job>` is not a cutover.** The plist stays in
152
+ `/Library/LaunchDaemons`, so launchd loads it again at the next boot; and activation (checked in
153
+ the same script) copies back and `load -w`s any Nix plist that is missing or differs from the
154
+ store. Either way two copies end up fighting over the port.
155
+ - ⚠️ **Check the host's own activation for labels it removes.** An activation script can boot out,
156
+ `disable` and delete jobs by name on every run; a new label that collides with one is disabled
157
+ under you. `grep -n 'bootout\|disable' /run/current-system/activate` before choosing labels.
158
+ - **Rolling back** is the same three steps reversed: destroy (or remove) the resource, restore the
159
+ job in the Nix config, activate.
160
+
161
+ ## The runner
162
+
163
+ `HostRunner` (see `src/launchd/runner.ts`) is `exec(argv)` (never through a shell), `readFile`,
164
+ `stat` (lstat), `writeFileAtomic`, `removeFile`, `lookupUser`, `lookupGroup`, `sleep`, plus
165
+ `privileged` and `effectiveUid()`. `localRunner()` implements it with `node:fs` and
166
+ `node:child_process`; tests use an in-memory fake, so nothing in the test suite runs `launchctl` or
167
+ writes outside a temp directory.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.5.0",
4
- "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo and Talos.",
3
+ "version": "0.6.0",
4
+ "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos and launchd.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
7
7
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "docs",
16
17
  "src",
17
18
  "README.md",
18
19
  "LICENSE"
@@ -44,6 +45,10 @@
44
45
  "types": "./dist/proxmox/index.d.ts",
45
46
  "default": "./dist/proxmox/index.js"
46
47
  },
48
+ "./launchd": {
49
+ "types": "./dist/launchd/index.d.ts",
50
+ "default": "./dist/launchd/index.js"
51
+ },
47
52
  "./package.json": "./package.json"
48
53
  },
49
54
  "publishConfig": {
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * import { BaoMount } from '@homeflare/alchemy/openbao';
10
10
  * import { TalosCluster } from '@homeflare/alchemy/talos';
11
11
  * import { ProxmoxAcl } from '@homeflare/alchemy/proxmox';
12
+ * import { LaunchdJob } from '@homeflare/alchemy/launchd';
12
13
  *
13
14
  * ★ WHY THESE EXIST AT ALL. When Alchemy has no resource for something, the alternative
14
15
  * is a runbook step a human runs once — and a plan can never show a missing runbook