@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.
- package/README.md +17 -48
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts +36 -0
- package/dist/launchd/fake-runner.d.ts.map +1 -0
- package/dist/launchd/host-effect.d.ts +19 -0
- package/dist/launchd/host-effect.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +24 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -0
- package/dist/launchd/host-file-lifecycle.d.ts +29 -0
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
- package/dist/launchd/host-file.d.ts +25 -0
- package/dist/launchd/host-file.d.ts.map +1 -0
- package/dist/launchd/host-lookup.d.ts +27 -0
- package/dist/launchd/host-lookup.d.ts.map +1 -0
- package/dist/launchd/index.d.ts +21 -0
- package/dist/launchd/index.d.ts.map +1 -0
- package/dist/launchd/index.js +969 -0
- package/dist/launchd/index.js.map +25 -0
- package/dist/launchd/job-form.d.ts +90 -0
- package/dist/launchd/job-form.d.ts.map +1 -0
- package/dist/launchd/job-lifecycle.d.ts +34 -0
- package/dist/launchd/job-lifecycle.d.ts.map +1 -0
- package/dist/launchd/job-preflight.d.ts +50 -0
- package/dist/launchd/job-preflight.d.ts.map +1 -0
- package/dist/launchd/job-validate.d.ts +25 -0
- package/dist/launchd/job-validate.d.ts.map +1 -0
- package/dist/launchd/job.d.ts +34 -0
- package/dist/launchd/job.d.ts.map +1 -0
- package/dist/launchd/launchctl.d.ts +62 -0
- package/dist/launchd/launchctl.d.ts.map +1 -0
- package/dist/launchd/local-runner.d.ts +7 -0
- package/dist/launchd/local-runner.d.ts.map +1 -0
- package/dist/launchd/plist.d.ts +41 -0
- package/dist/launchd/plist.d.ts.map +1 -0
- package/dist/launchd/providers.d.ts +15 -0
- package/dist/launchd/providers.d.ts.map +1 -0
- package/dist/launchd/runner.d.ts +91 -0
- package/dist/launchd/runner.d.ts.map +1 -0
- package/dist/launchd/secret-tripwire.d.ts +23 -0
- package/dist/launchd/secret-tripwire.d.ts.map +1 -0
- package/dist/openbao/auth-method-form.d.ts +7 -0
- package/dist/openbao/auth-method-form.d.ts.map +1 -1
- package/dist/openbao/auth-method-reconcile.d.ts +15 -0
- package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
- package/dist/openbao/auth-method.d.ts +6 -0
- package/dist/openbao/auth-method.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role.d.ts.map +1 -1
- package/dist/openbao/auth-token-form.d.ts +48 -0
- package/dist/openbao/auth-token-form.d.ts.map +1 -0
- package/dist/openbao/bao-identity.d.ts +81 -0
- package/dist/openbao/bao-identity.d.ts.map +1 -0
- package/dist/openbao/bao-namespace.d.ts +20 -0
- package/dist/openbao/bao-namespace.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
- package/dist/openbao/host-approles.d.ts +62 -0
- package/dist/openbao/host-approles.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +19 -0
- package/dist/openbao/index.d.ts.map +1 -1
- package/dist/openbao/index.js +1433 -305
- package/dist/openbao/index.js.map +34 -13
- package/dist/openbao/jwt-config-form.d.ts +51 -0
- package/dist/openbao/jwt-config-form.d.ts.map +1 -0
- package/dist/openbao/jwt-config.d.ts +25 -0
- package/dist/openbao/jwt-config.d.ts.map +1 -0
- package/dist/openbao/jwt-role-form.d.ts +91 -0
- package/dist/openbao/jwt-role-form.d.ts.map +1 -0
- package/dist/openbao/jwt-role.d.ts +31 -0
- package/dist/openbao/jwt-role.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role-form.d.ts +68 -0
- package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role.d.ts +25 -0
- package/dist/openbao/kubernetes-role.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
- package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement.d.ts +41 -0
- package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
- package/dist/openbao/mfa-totp-form.d.ts +52 -0
- package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
- package/dist/openbao/mfa-totp.d.ts +33 -0
- package/dist/openbao/mfa-totp.d.ts.map +1 -0
- package/dist/openbao/mfa-wire.d.ts +63 -0
- package/dist/openbao/mfa-wire.d.ts.map +1 -0
- package/dist/openbao/mount-form.d.ts +7 -0
- package/dist/openbao/mount-form.d.ts.map +1 -1
- package/dist/openbao/mount-move.d.ts +49 -0
- package/dist/openbao/mount-move.d.ts.map +1 -0
- package/dist/openbao/mount-path.d.ts +8 -4
- package/dist/openbao/mount-path.d.ts.map +1 -1
- package/dist/openbao/mount-reconcile.d.ts +16 -0
- package/dist/openbao/mount-reconcile.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +9 -0
- package/dist/openbao/mount.d.ts.map +1 -1
- package/dist/openbao/pki-role-canonical.d.ts +46 -0
- package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +38 -30
- package/dist/openbao/pki-role-form.d.ts.map +1 -1
- package/dist/openbao/pki-role.d.ts.map +1 -1
- package/dist/openbao/remount-wire.d.ts +44 -0
- package/dist/openbao/remount-wire.d.ts.map +1 -0
- package/dist/openbao/role-reconcile.d.ts +40 -0
- package/dist/openbao/role-reconcile.d.ts.map +1 -0
- package/docs/launchd.md +167 -0
- package/package.json +7 -2
- package/src/index.ts +1 -0
- package/src/launchd/fake-runner.ts +176 -0
- package/src/launchd/host-effect.ts +28 -0
- package/src/launchd/host-file-form.ts +71 -0
- package/src/launchd/host-file-lifecycle.test.ts +198 -0
- package/src/launchd/host-file-lifecycle.ts +164 -0
- package/src/launchd/host-file.ts +64 -0
- package/src/launchd/host-lookup.test.ts +92 -0
- package/src/launchd/host-lookup.ts +94 -0
- package/src/launchd/index.ts +26 -0
- package/src/launchd/job-form.test.ts +171 -0
- package/src/launchd/job-form.ts +180 -0
- package/src/launchd/job-identity.test.ts +147 -0
- package/src/launchd/job-lifecycle.test.ts +220 -0
- package/src/launchd/job-lifecycle.ts +194 -0
- package/src/launchd/job-preflight.ts +152 -0
- package/src/launchd/job-validate.ts +134 -0
- package/src/launchd/job.ts +96 -0
- package/src/launchd/launchctl.test.ts +181 -0
- package/src/launchd/launchctl.ts +188 -0
- package/src/launchd/local-runner.test.ts +125 -0
- package/src/launchd/local-runner.ts +143 -0
- package/src/launchd/plist.test.ts +186 -0
- package/src/launchd/plist.ts +156 -0
- package/src/launchd/providers.test.ts +222 -0
- package/src/launchd/providers.ts +21 -0
- package/src/launchd/runner.ts +95 -0
- package/src/launchd/secret-tripwire.test.ts +110 -0
- package/src/launchd/secret-tripwire.ts +81 -0
- package/src/openbao/README.md +181 -0
- package/src/openbao/REPLACE.md +87 -0
- package/src/openbao/auth-method-form.ts +7 -0
- package/src/openbao/auth-method-reconcile.ts +74 -0
- package/src/openbao/auth-method.ts +20 -41
- package/src/openbao/auth-role-form.test.ts +16 -1
- package/src/openbao/auth-role-form.ts +11 -0
- package/src/openbao/auth-role.ts +12 -0
- package/src/openbao/auth-token-form.ts +161 -0
- package/src/openbao/bao-identity.test.ts +131 -0
- package/src/openbao/bao-identity.ts +188 -0
- package/src/openbao/bao-namespace.ts +32 -0
- package/src/openbao/cloudflare-roles-expand.ts +2 -5
- package/src/openbao/host-approles.test.ts +121 -0
- package/src/openbao/host-approles.ts +126 -0
- package/src/openbao/index.ts +19 -0
- package/src/openbao/jwt-config-form.ts +161 -0
- package/src/openbao/jwt-config.ts +85 -0
- package/src/openbao/jwt-role-form.test.ts +130 -0
- package/src/openbao/jwt-role-form.ts +231 -0
- package/src/openbao/jwt-role.ts +102 -0
- package/src/openbao/kubernetes-role-form.test.ts +82 -0
- package/src/openbao/kubernetes-role-form.ts +149 -0
- package/src/openbao/kubernetes-role.ts +94 -0
- package/src/openbao/mfa-enforcement-form.ts +147 -0
- package/src/openbao/mfa-enforcement.ts +143 -0
- package/src/openbao/mfa-totp-form.ts +155 -0
- package/src/openbao/mfa-totp.ts +118 -0
- package/src/openbao/mfa-wire.ts +117 -0
- package/src/openbao/mfa.test.ts +200 -0
- package/src/openbao/mount-form.ts +9 -1
- package/src/openbao/mount-move.test.ts +164 -0
- package/src/openbao/mount-move.ts +113 -0
- package/src/openbao/mount-path.test.ts +11 -1
- package/src/openbao/mount-path.ts +17 -11
- package/src/openbao/mount-reconcile.ts +75 -0
- package/src/openbao/mount.ts +26 -43
- package/src/openbao/pki-role-canonical.ts +163 -0
- package/src/openbao/pki-role-form.test.ts +112 -0
- package/src/openbao/pki-role-form.ts +76 -110
- package/src/openbao/pki-role.ts +12 -27
- package/src/openbao/remount-wire.test.ts +87 -0
- package/src/openbao/remount-wire.ts +112 -0
- package/src/openbao/role-reconcile.test.ts +104 -0
- 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":"
|
|
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
|
-
*
|
|
2
|
+
* Trimming runs of one character from caller-supplied paths and names.
|
|
3
3
|
*
|
|
4
|
-
* ★ LOOPS, NOT `/^\/+|\/+$/g
|
|
5
|
-
* of
|
|
6
|
-
*
|
|
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,
|
|
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"}
|
package/dist/openbao/mount.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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":"
|
|
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,
|
|
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"}
|
package/docs/launchd.md
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo and
|
|
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
|