@homeflare/alchemy 0.7.0 → 0.10.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 +45 -45
- package/dist/caddy/config-lifecycle.d.ts +49 -3
- package/dist/caddy/config-lifecycle.d.ts.map +1 -1
- package/dist/caddy/config.d.ts +6 -2
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/fake-caddy.d.ts +11 -0
- package/dist/caddy/fake-caddy.d.ts.map +1 -1
- package/dist/caddy/index.d.ts +2 -1
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +270 -44
- package/dist/caddy/index.js.map +15 -11
- package/dist/caddy/with-file.d.ts +7 -3
- package/dist/caddy/with-file.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts +1 -0
- package/dist/launchd/fake-runner.d.ts.map +1 -1
- package/dist/launchd/fake-sudo.d.ts +51 -0
- package/dist/launchd/fake-sudo.d.ts.map +1 -0
- package/dist/launchd/host-effect.d.ts +1 -1
- package/dist/launchd/host-file-form.d.ts +5 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -1
- package/dist/launchd/host-file-lifecycle.d.ts +14 -1
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
- package/dist/launchd/host-file.d.ts +2 -1
- package/dist/launchd/host-file.d.ts.map +1 -1
- package/dist/launchd/index.d.ts +2 -0
- package/dist/launchd/index.d.ts.map +1 -1
- package/dist/launchd/index.js +642 -49
- package/dist/launchd/index.js.map +24 -14
- package/dist/launchd/job-lifecycle.d.ts +8 -1
- package/dist/launchd/job-lifecycle.d.ts.map +1 -1
- package/dist/launchd/job-preflight.d.ts +13 -1
- package/dist/launchd/job-preflight.d.ts.map +1 -1
- package/dist/launchd/job-validate.d.ts +2 -0
- package/dist/launchd/job-validate.d.ts.map +1 -1
- package/dist/launchd/job.d.ts +2 -1
- package/dist/launchd/job.d.ts.map +1 -1
- package/dist/launchd/local-runner.d.ts +2 -0
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/providers.d.ts +2 -1
- package/dist/launchd/providers.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +14 -5
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-acl.d.ts +25 -0
- package/dist/launchd/sudo-acl.d.ts.map +1 -0
- package/dist/launchd/sudo-allowlist.d.ts +47 -0
- package/dist/launchd/sudo-allowlist.d.ts.map +1 -0
- package/dist/launchd/sudo-guard.d.ts +83 -0
- package/dist/launchd/sudo-guard.d.ts.map +1 -0
- package/dist/launchd/sudo-runner.d.ts +60 -0
- package/dist/launchd/sudo-runner.d.ts.map +1 -0
- package/dist/launchd/sudo-said.d.ts +10 -0
- package/dist/launchd/sudo-said.d.ts.map +1 -0
- package/dist/launchd/sudo-stage.d.ts +7 -0
- package/dist/launchd/sudo-stage.d.ts.map +1 -0
- package/dist/openbao/auth-method-reconcile.d.ts +6 -2
- package/dist/openbao/auth-method-reconcile.d.ts.map +1 -1
- package/dist/openbao/auth-method.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +0 -9
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role.d.ts +3 -2
- package/dist/openbao/auth-role.d.ts.map +1 -1
- package/dist/openbao/cloudflare-role.d.ts.map +1 -1
- package/dist/openbao/fake-bao.d.ts +18 -0
- package/dist/openbao/fake-bao.d.ts.map +1 -1
- package/dist/openbao/fake-core.d.ts +39 -0
- package/dist/openbao/fake-core.d.ts.map +1 -0
- package/dist/openbao/fake-engines-roles.d.ts +32 -0
- package/dist/openbao/fake-engines-roles.d.ts.map +1 -0
- package/dist/openbao/fake-engines.d.ts +8 -2
- package/dist/openbao/fake-engines.d.ts.map +1 -1
- package/dist/openbao/fake-families.d.ts +52 -0
- package/dist/openbao/fake-families.d.ts.map +1 -0
- package/dist/openbao/fake-mounts.d.ts +16 -0
- package/dist/openbao/fake-mounts.d.ts.map +1 -0
- package/dist/openbao/fake-stack.d.ts +5 -1
- package/dist/openbao/fake-stack.d.ts.map +1 -1
- package/dist/openbao/host-approles.d.ts.map +1 -1
- package/dist/openbao/index.js +709 -359
- package/dist/openbao/index.js.map +32 -25
- package/dist/openbao/jwt-config.d.ts +5 -0
- package/dist/openbao/jwt-config.d.ts.map +1 -1
- package/dist/openbao/jwt-role.d.ts +2 -1
- package/dist/openbao/jwt-role.d.ts.map +1 -1
- package/dist/openbao/kubernetes-role.d.ts +2 -1
- package/dist/openbao/kubernetes-role.d.ts.map +1 -1
- package/dist/openbao/mfa-enforcement.d.ts +12 -5
- package/dist/openbao/mfa-enforcement.d.ts.map +1 -1
- package/dist/openbao/mfa-totp.d.ts +6 -2
- package/dist/openbao/mfa-totp.d.ts.map +1 -1
- package/dist/openbao/mount-reconcile.d.ts +5 -1
- package/dist/openbao/mount-reconcile.d.ts.map +1 -1
- package/dist/openbao/mount.d.ts.map +1 -1
- package/dist/openbao/pki-role.d.ts.map +1 -1
- package/dist/openbao/plugin-reconcile.d.ts +6 -2
- package/dist/openbao/plugin-reconcile.d.ts.map +1 -1
- package/dist/openbao/plugin.d.ts.map +1 -1
- package/dist/openbao/policy.d.ts.map +1 -1
- package/dist/openbao/proxmox-role.d.ts.map +1 -1
- package/dist/openbao/rename-identity.d.ts +69 -0
- package/dist/openbao/rename-identity.d.ts.map +1 -0
- package/dist/openbao/rename.d.ts +13 -12
- package/dist/openbao/rename.d.ts.map +1 -1
- package/dist/openbao/role-reconcile.d.ts +11 -1
- package/dist/openbao/role-reconcile.d.ts.map +1 -1
- package/dist/openbao/ssh-role-erase.d.ts +11 -0
- package/dist/openbao/ssh-role-erase.d.ts.map +1 -0
- package/dist/openbao/ssh-role.d.ts.map +1 -1
- package/dist/ownership/adopt.d.ts +52 -0
- package/dist/ownership/adopt.d.ts.map +1 -0
- package/dist/ownership/probe.d.ts +13 -0
- package/dist/ownership/probe.d.ts.map +1 -0
- package/dist/ownership/resume.d.ts +40 -0
- package/dist/ownership/resume.d.ts.map +1 -0
- package/dist/ownership/rows.d.ts +33 -0
- package/dist/ownership/rows.d.ts.map +1 -0
- package/dist/ownership/whole.d.ts +14 -0
- package/dist/ownership/whole.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-form.d.ts +3 -3
- package/dist/proxmox/ceph-pool.d.ts.map +1 -1
- package/dist/proxmox/fake-bao-env.d.ts +3 -0
- package/dist/proxmox/fake-bao-env.d.ts.map +1 -0
- package/dist/proxmox/fake-pve-lxc.d.ts +42 -0
- package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -0
- package/dist/proxmox/fake-pve.d.ts +38 -0
- package/dist/proxmox/fake-pve.d.ts.map +1 -0
- package/dist/proxmox/index.d.ts +6 -2
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +970 -186
- package/dist/proxmox/index.js.map +22 -9
- package/dist/proxmox/lxc-create-form.d.ts +6 -0
- package/dist/proxmox/lxc-create-form.d.ts.map +1 -0
- package/dist/proxmox/lxc-harness.d.ts +34 -0
- package/dist/proxmox/lxc-harness.d.ts.map +1 -0
- package/dist/proxmox/lxc-identity.d.ts +18 -0
- package/dist/proxmox/lxc-identity.d.ts.map +1 -0
- package/dist/proxmox/lxc-judge.d.ts +46 -0
- package/dist/proxmox/lxc-judge.d.ts.map +1 -0
- package/dist/proxmox/lxc-lifecycle.d.ts +68 -0
- package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/lxc-props.d.ts +139 -0
- package/dist/proxmox/lxc-props.d.ts.map +1 -0
- package/dist/proxmox/lxc-volume.d.ts +29 -0
- package/dist/proxmox/lxc-volume.d.ts.map +1 -0
- package/dist/proxmox/lxc-wire.d.ts +51 -0
- package/dist/proxmox/lxc-wire.d.ts.map +1 -0
- package/dist/proxmox/lxc.d.ts +27 -47
- package/dist/proxmox/lxc.d.ts.map +1 -1
- package/dist/proxmox/network-apply-read.d.ts +1 -1
- package/dist/proxmox/network-apply-read.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/update-guard.d.ts +27 -0
- package/dist/proxmox/update-guard.d.ts.map +1 -0
- package/dist/verify/args.d.ts +16 -0
- package/dist/verify/args.d.ts.map +1 -0
- package/dist/verify/cli.d.ts +3 -0
- package/dist/verify/cli.d.ts.map +1 -0
- package/dist/verify/cli.js +320 -0
- package/dist/verify/cli.js.map +15 -0
- package/dist/verify/fake-engine.d.ts +57 -0
- package/dist/verify/fake-engine.d.ts.map +1 -0
- package/dist/verify/index.d.ts +16 -0
- package/dist/verify/index.d.ts.map +1 -0
- package/dist/verify/index.js +297 -0
- package/dist/verify/index.js.map +14 -0
- package/dist/verify/report.d.ts +6 -0
- package/dist/verify/report.d.ts.map +1 -0
- package/dist/verify/rows.d.ts +57 -0
- package/dist/verify/rows.d.ts.map +1 -0
- package/dist/verify/spy.d.ts +31 -0
- package/dist/verify/spy.d.ts.map +1 -0
- package/dist/verify/verify.d.ts +43 -0
- package/dist/verify/verify.d.ts.map +1 -0
- package/docs/adopt-verify.md +108 -0
- package/docs/adopted-deploys.md +85 -0
- package/docs/caddy-admin.md +24 -0
- package/docs/caddy.md +73 -54
- package/docs/credentials.md +15 -0
- package/docs/launchd-sudo.md +147 -0
- package/docs/launchd.md +14 -7
- package/docs/ownership.md +113 -0
- package/docs/peers.md +39 -0
- package/docs/proxmox-lxc.md +196 -0
- package/docs/proxmox.md +12 -0
- package/package.json +9 -2
- package/src/caddy/adopt-scope.test.ts +82 -0
- package/src/caddy/config-adopt.test.ts +235 -0
- package/src/caddy/config-lifecycle.test.ts +31 -53
- package/src/caddy/config-lifecycle.ts +98 -7
- package/src/caddy/config.ts +48 -10
- package/src/caddy/fake-caddy.ts +19 -0
- package/src/caddy/index.ts +2 -1
- package/src/caddy/providers.test.ts +8 -23
- package/src/caddy/with-file.ts +7 -3
- package/src/launchd/adopt-apply.test.ts +166 -0
- package/src/launchd/fake-runner.ts +26 -4
- package/src/launchd/fake-sudo.ts +136 -0
- package/src/launchd/host-effect.ts +1 -1
- package/src/launchd/host-file-form.ts +10 -2
- package/src/launchd/host-file-lifecycle.ts +41 -15
- package/src/launchd/host-file.ts +13 -4
- package/src/launchd/index.ts +2 -0
- package/src/launchd/job-lifecycle.ts +22 -14
- package/src/launchd/job-preflight.ts +21 -3
- package/src/launchd/job-validate.ts +1 -1
- package/src/launchd/job.ts +13 -4
- package/src/launchd/local-runner.ts +6 -3
- package/src/launchd/providers.test.ts +3 -0
- package/src/launchd/providers.ts +2 -1
- package/src/launchd/runner.ts +14 -5
- package/src/launchd/sudo-acl.ts +68 -0
- package/src/launchd/sudo-allowlist.test.ts +191 -0
- package/src/launchd/sudo-allowlist.ts +213 -0
- package/src/launchd/sudo-chain.test.ts +121 -0
- package/src/launchd/sudo-guard.test.ts +100 -0
- package/src/launchd/sudo-guard.ts +230 -0
- package/src/launchd/sudo-lifecycle.test.ts +172 -0
- package/src/launchd/sudo-modes.test.ts +135 -0
- package/src/launchd/sudo-runner-files.test.ts +240 -0
- package/src/launchd/sudo-runner.test.ts +126 -0
- package/src/launchd/sudo-runner.ts +216 -0
- package/src/launchd/sudo-said.ts +58 -0
- package/src/launchd/sudo-stage.ts +28 -0
- package/src/openbao/README.md +11 -2
- package/src/openbao/REPLACE.md +67 -37
- package/src/openbao/adopt-core.test.ts +119 -0
- package/src/openbao/adopt-families.test.ts +97 -0
- package/src/openbao/adopt-holes.test.ts +168 -0
- package/src/openbao/adopt-recovery.test.ts +71 -0
- package/src/openbao/auth-method-reconcile.ts +15 -3
- package/src/openbao/auth-method.ts +15 -6
- package/src/openbao/auth-role-form.test.ts +2 -16
- package/src/openbao/auth-role-form.ts +0 -11
- package/src/openbao/auth-role.ts +48 -17
- package/src/openbao/cloudflare-role.ts +23 -18
- package/src/openbao/fake-bao.ts +52 -0
- package/src/openbao/fake-core.ts +141 -0
- package/src/openbao/fake-engines-roles.ts +114 -0
- package/src/openbao/fake-engines.ts +18 -11
- package/src/openbao/fake-families.ts +188 -0
- package/src/openbao/fake-mounts.ts +74 -0
- package/src/openbao/fake-stack.ts +17 -4
- package/src/openbao/host-approles.test.ts +15 -2
- package/src/openbao/host-approles.ts +19 -4
- package/src/openbao/jwt-config.ts +35 -11
- package/src/openbao/jwt-role.ts +33 -14
- package/src/openbao/kubernetes-role.ts +32 -14
- package/src/openbao/mfa-enforcement.ts +42 -13
- package/src/openbao/mfa-totp.ts +42 -14
- package/src/openbao/mount-move.test.ts +2 -35
- package/src/openbao/mount-reconcile.ts +13 -2
- package/src/openbao/mount.ts +15 -6
- package/src/openbao/pki-role.ts +33 -10
- package/src/openbao/plugin-reconcile.ts +16 -5
- package/src/openbao/plugin.ts +57 -20
- package/src/openbao/policy.ts +24 -11
- package/src/openbao/proxmox-role.ts +20 -20
- package/src/openbao/rename-adoption.test.ts +95 -0
- package/src/openbao/rename-families.test.ts +122 -0
- package/src/openbao/rename-identity.test.ts +129 -0
- package/src/openbao/rename-identity.ts +117 -0
- package/src/openbao/rename.ts +19 -15
- package/src/openbao/role-reconcile.ts +23 -0
- package/src/openbao/ssh-role-erase.ts +50 -0
- package/src/openbao/ssh-role.ts +29 -57
- package/src/ownership/adopt.ts +122 -0
- package/src/ownership/ownership.test.ts +200 -0
- package/src/ownership/probe.ts +65 -0
- package/src/ownership/resume.ts +118 -0
- package/src/ownership/rows.ts +117 -0
- package/src/ownership/whole.test.ts +39 -0
- package/src/ownership/whole.ts +60 -0
- package/src/proxmox/adopt-noop.test.ts +112 -0
- package/src/proxmox/ceph-pool-adopt.test.ts +88 -0
- package/src/proxmox/ceph-pool-form.ts +3 -3
- package/src/proxmox/ceph-pool.ts +10 -3
- package/src/proxmox/fake-bao-env.ts +28 -0
- package/src/proxmox/fake-pve-lxc.ts +210 -0
- package/src/proxmox/fake-pve.ts +75 -0
- package/src/proxmox/index.test.ts +6 -6
- package/src/proxmox/index.ts +6 -2
- package/src/proxmox/lxc-adopt.test.ts +170 -0
- package/src/proxmox/lxc-create-form.ts +76 -0
- package/src/proxmox/lxc-create.test.ts +126 -0
- package/src/proxmox/lxc-guards.test.ts +158 -0
- package/src/proxmox/lxc-harness.ts +150 -0
- package/src/proxmox/lxc-identity.ts +43 -0
- package/src/proxmox/lxc-judge.test.ts +175 -0
- package/src/proxmox/lxc-judge.ts +196 -0
- package/src/proxmox/lxc-lifecycle.ts +200 -0
- package/src/proxmox/lxc-ownership.test.ts +159 -0
- package/src/proxmox/lxc-props.ts +215 -0
- package/src/proxmox/lxc-read.test.ts +60 -0
- package/src/proxmox/lxc-volume.test.ts +96 -0
- package/src/proxmox/lxc-volume.ts +153 -0
- package/src/proxmox/lxc-wire.ts +210 -0
- package/src/proxmox/lxc.ts +216 -93
- package/src/proxmox/network-apply-read.ts +3 -1
- package/src/proxmox/pbs-datastore.ts +4 -3
- package/src/proxmox/resource.ts +7 -7
- package/src/proxmox/update-guard.ts +31 -0
- package/src/verify/args.ts +82 -0
- package/src/verify/cli.test.ts +86 -0
- package/src/verify/cli.ts +49 -0
- package/src/verify/fake-engine.ts +158 -0
- package/src/verify/index.ts +36 -0
- package/src/verify/report.ts +52 -0
- package/src/verify/rows.ts +169 -0
- package/src/verify/spy.test.ts +141 -0
- package/src/verify/spy.ts +149 -0
- package/src/verify/verify.test.ts +124 -0
- package/src/verify/verify.ts +172 -0
package/dist/launchd/index.js
CHANGED
|
@@ -1,13 +1,190 @@
|
|
|
1
1
|
// src/launchd/host-file.ts
|
|
2
2
|
import { Resource } from "alchemy";
|
|
3
|
-
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
|
|
4
4
|
import { isResolved } from "alchemy/Diff";
|
|
5
5
|
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import * as Effect6 from "effect/Effect";
|
|
7
|
+
|
|
8
|
+
// src/ownership/adopt.ts
|
|
9
|
+
import { AdoptPolicy } from "alchemy/AdoptPolicy";
|
|
10
|
+
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
11
|
+
import { Stack as Stack3 } from "alchemy/Stack";
|
|
12
|
+
import * as Effect4 from "effect/Effect";
|
|
13
|
+
import * as Option4 from "effect/Option";
|
|
14
|
+
|
|
15
|
+
// src/ownership/resume.ts
|
|
16
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
17
|
+
import { Artifacts } from "alchemy/Artifacts";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Option from "effect/Option";
|
|
20
|
+
var PROVEN = "homeflare/ownership/resumes";
|
|
21
|
+
var UNFINISHED = "homeflare/ownership/unfinished";
|
|
22
|
+
var note = (key, instanceId) => Effect.gen(function* () {
|
|
23
|
+
const bag = yield* Effect.serviceOption(Artifacts);
|
|
24
|
+
if (Option.isSome(bag))
|
|
25
|
+
yield* bag.value.set(key, instanceId);
|
|
26
|
+
});
|
|
27
|
+
var noted = (key, instanceId) => Effect.gen(function* () {
|
|
28
|
+
const bag = yield* Effect.serviceOption(Artifacts);
|
|
29
|
+
if (Option.isNone(bag))
|
|
30
|
+
return false;
|
|
31
|
+
return (yield* bag.value.get(key)) === instanceId;
|
|
32
|
+
});
|
|
33
|
+
var noteUnfinished = (instanceId) => Effect.as(note(UNFINISHED, instanceId), undefined);
|
|
34
|
+
var noteResume = (instanceId, recovered) => Effect.gen(function* () {
|
|
35
|
+
yield* noteUnfinished(instanceId);
|
|
36
|
+
const unproven = () => Effect.succeed(undefined);
|
|
37
|
+
const found = yield* recovered.pipe(Effect.catch(unproven), Effect.catchDefect(unproven));
|
|
38
|
+
if (found === undefined || found === null || Unowned.is(found))
|
|
39
|
+
return;
|
|
40
|
+
yield* note(PROVEN, instanceId);
|
|
41
|
+
});
|
|
42
|
+
var resumes = (instanceId) => noted(PROVEN, instanceId);
|
|
43
|
+
var unfinished = (instanceId) => noted(UNFINISHED, instanceId);
|
|
44
|
+
var provingResumes = (service) => {
|
|
45
|
+
const { diff, read } = service;
|
|
46
|
+
if (diff === undefined || read === undefined)
|
|
47
|
+
return service;
|
|
48
|
+
const proving = (input) => Effect.gen(function* () {
|
|
49
|
+
if (input.output === undefined) {
|
|
50
|
+
yield* noteResume(input.instanceId, read({ ...input, output: undefined }));
|
|
51
|
+
}
|
|
52
|
+
return yield* diff(input);
|
|
53
|
+
});
|
|
54
|
+
return { ...service, diff: proving };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/ownership/rows.ts
|
|
58
|
+
import { Stack as Stack2 } from "alchemy/Stack";
|
|
59
|
+
import { State, isActionState } from "alchemy/State/State";
|
|
60
|
+
import * as Effect3 from "effect/Effect";
|
|
61
|
+
import * as Option3 from "effect/Option";
|
|
62
|
+
|
|
63
|
+
// src/ownership/whole.ts
|
|
64
|
+
import { Stack } from "alchemy/Stack";
|
|
6
65
|
import * as Effect2 from "effect/Effect";
|
|
66
|
+
import * as Option2 from "effect/Option";
|
|
67
|
+
var absent = (value) => value === undefined || value === null;
|
|
68
|
+
var isPlainObject = (value) => {
|
|
69
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
70
|
+
return false;
|
|
71
|
+
const proto = Object.getPrototypeOf(value);
|
|
72
|
+
return proto === Object.prototype || proto === null;
|
|
73
|
+
};
|
|
74
|
+
var carries = (declared, recorded) => {
|
|
75
|
+
if (absent(declared))
|
|
76
|
+
return true;
|
|
77
|
+
if (absent(recorded))
|
|
78
|
+
return false;
|
|
79
|
+
if (Array.isArray(declared)) {
|
|
80
|
+
if (!Array.isArray(recorded))
|
|
81
|
+
return false;
|
|
82
|
+
return declared.every((each, index) => carries(each, recorded[index]));
|
|
83
|
+
}
|
|
84
|
+
if (isPlainObject(declared)) {
|
|
85
|
+
if (!isPlainObject(recorded))
|
|
86
|
+
return false;
|
|
87
|
+
return Object.entries(declared).every(([key, each]) => carries(each, recorded[key]));
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
var wholeDeclaration = (fqn, props) => Effect2.map(Effect2.serviceOption(Stack), (stack) => {
|
|
92
|
+
const declared = Option2.isSome(stack) ? stack.value.resources[fqn] : undefined;
|
|
93
|
+
return declared !== undefined && carries(declared.Props, props);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/ownership/rows.ts
|
|
97
|
+
var storeOf = Effect3.gen(function* () {
|
|
98
|
+
const stack = yield* Effect3.serviceOption(Stack2);
|
|
99
|
+
const state = yield* Effect3.serviceOption(State);
|
|
100
|
+
if (Option3.isNone(stack) || Option3.isNone(state))
|
|
101
|
+
return;
|
|
102
|
+
const store = yield* state.value;
|
|
103
|
+
const where = { stack: stack.value.name, stage: stack.value.stage };
|
|
104
|
+
return { resources: stack.value.resources, store, where };
|
|
105
|
+
});
|
|
106
|
+
var rowAt = (store, where, fqn) => store.get({ ...where, fqn }).pipe(Effect3.orElseSucceed(() => {
|
|
107
|
+
return;
|
|
108
|
+
}));
|
|
109
|
+
var generationOf = (row, instanceId) => {
|
|
110
|
+
let at = row;
|
|
111
|
+
while (typeof at === "object" && at !== null && !isActionState(at)) {
|
|
112
|
+
const generation = at;
|
|
113
|
+
if (generation.instanceId === instanceId)
|
|
114
|
+
return generation;
|
|
115
|
+
at = generation.old;
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
};
|
|
119
|
+
var recordedGeneration = (fqn, instanceId) => Effect3.gen(function* () {
|
|
120
|
+
const found = yield* storeOf;
|
|
121
|
+
if (found === undefined)
|
|
122
|
+
return;
|
|
123
|
+
for (const at of [fqn, ...found.resources[fqn]?.FormerFqns ?? []]) {
|
|
124
|
+
const generation = generationOf(yield* rowAt(found.store, found.where, at), instanceId);
|
|
125
|
+
if (generation !== undefined)
|
|
126
|
+
return generation;
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
});
|
|
130
|
+
var recordedInstance = (fqn, instanceId) => Effect3.gen(function* () {
|
|
131
|
+
const generation = yield* recordedGeneration(fqn, instanceId);
|
|
132
|
+
if (generation === undefined)
|
|
133
|
+
return "absent";
|
|
134
|
+
return (yield* wholeDeclaration(fqn, generation.props)) ? "whole" : "partial";
|
|
135
|
+
});
|
|
136
|
+
var isCreate = (fqn, instanceId) => Effect3.map(recordedGeneration(fqn, instanceId), (generation) => generation?.status === "creating");
|
|
137
|
+
var forgetRefusedCreate = (fqn, instanceId) => Effect3.gen(function* () {
|
|
138
|
+
const found = yield* storeOf;
|
|
139
|
+
if (found === undefined)
|
|
140
|
+
return;
|
|
141
|
+
const row = yield* rowAt(found.store, found.where, fqn);
|
|
142
|
+
const mine = row;
|
|
143
|
+
if (mine?.status !== "creating" || mine.instanceId !== instanceId || mine.attr !== undefined) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
yield* found.store.delete({ ...found.where, fqn }).pipe(Effect3.ignore);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// src/ownership/adopt.ts
|
|
150
|
+
var adoptEnabled = (fqn) => Effect4.gen(function* () {
|
|
151
|
+
const stack = yield* Effect4.serviceOption(Stack3);
|
|
152
|
+
const scoped = Option4.isSome(stack) ? stack.value.resources[fqn]?.Adopt : undefined;
|
|
153
|
+
if (scoped !== undefined)
|
|
154
|
+
return scoped;
|
|
155
|
+
const policy = yield* Effect4.serviceOption(AdoptPolicy);
|
|
156
|
+
if (Option4.isSome(policy))
|
|
157
|
+
return policy.value;
|
|
158
|
+
const context = yield* Effect4.serviceOption(AlchemyContext);
|
|
159
|
+
return Option4.isSome(context) ? context.value.adopt : false;
|
|
160
|
+
});
|
|
161
|
+
var adoptable = (owner) => Effect4.gen(function* () {
|
|
162
|
+
if (yield* isCreate(owner.fqn, owner.instanceId))
|
|
163
|
+
return true;
|
|
164
|
+
return yield* unfinished(owner.instanceId);
|
|
165
|
+
});
|
|
166
|
+
var adoptsAtApply = (owner) => Effect4.gen(function* () {
|
|
167
|
+
if (!(yield* adoptEnabled(owner.fqn)))
|
|
168
|
+
return false;
|
|
169
|
+
return yield* adoptable(owner);
|
|
170
|
+
});
|
|
171
|
+
var refuseTakeover = (owner, what) => Effect4.gen(function* () {
|
|
172
|
+
if (owner.output !== undefined)
|
|
173
|
+
return;
|
|
174
|
+
if (yield* resumes(owner.instanceId))
|
|
175
|
+
return;
|
|
176
|
+
const may = yield* adoptable(owner);
|
|
177
|
+
if (may && (yield* adoptEnabled(owner.fqn)))
|
|
178
|
+
return;
|
|
179
|
+
yield* forgetRefusedCreate(owner.fqn, owner.instanceId);
|
|
180
|
+
const remedy = may ? "Deploy with --adopt (or wrap the resource in adopt(true)) to take it over, or remove it first." : "It is the new identity of a replace, which --adopt does not cover: remove it first.";
|
|
181
|
+
return yield* Effect4.die(new Error(`${what}: already exists, and this stack holds no state for it — the plan could not ask ` + "first (a prop was still an Output, a replace moved onto it, or it appeared after the " + `plan). Nothing was written. ${remedy}`));
|
|
182
|
+
});
|
|
183
|
+
var claimFor = (owner) => (what) => refuseTakeover(owner, what);
|
|
7
184
|
|
|
8
185
|
// src/launchd/host-effect.ts
|
|
9
|
-
import * as
|
|
10
|
-
var lift = (step) =>
|
|
186
|
+
import * as Effect5 from "effect/Effect";
|
|
187
|
+
var lift = (step) => Effect5.tryPromise({
|
|
11
188
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
|
|
12
189
|
try: step
|
|
13
190
|
});
|
|
@@ -69,9 +246,8 @@ var identityProblems = (name, value) => {
|
|
|
69
246
|
}
|
|
70
247
|
return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value) ? [] : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
|
|
71
248
|
};
|
|
72
|
-
var
|
|
249
|
+
var pathProblems = (path) => {
|
|
73
250
|
const found = [];
|
|
74
|
-
const { path } = props;
|
|
75
251
|
if (!path.startsWith("/"))
|
|
76
252
|
found.push("path must be absolute");
|
|
77
253
|
if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
|
|
@@ -79,6 +255,10 @@ var fileProblems = (props) => {
|
|
|
79
255
|
}
|
|
80
256
|
if (path.includes("\x00"))
|
|
81
257
|
found.push("path contains NUL");
|
|
258
|
+
return found;
|
|
259
|
+
};
|
|
260
|
+
var fileProblems = (props) => {
|
|
261
|
+
const found = pathProblems(props.path);
|
|
82
262
|
const mode = props.mode ?? DEFAULT_MODE;
|
|
83
263
|
if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
|
|
84
264
|
found.push("mode must be 0–0o7777");
|
|
@@ -114,7 +294,7 @@ var escapeText = (text, path) => {
|
|
|
114
294
|
}
|
|
115
295
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\r", " ");
|
|
116
296
|
};
|
|
117
|
-
var
|
|
297
|
+
var isPlainObject2 = (value) => {
|
|
118
298
|
const proto = Object.getPrototypeOf(value);
|
|
119
299
|
return proto === Object.prototype || proto === null;
|
|
120
300
|
};
|
|
@@ -149,7 +329,7 @@ var renderValue = (value, path, depth, out) => {
|
|
|
149
329
|
out.push(`${pad}</array>`);
|
|
150
330
|
return;
|
|
151
331
|
}
|
|
152
|
-
if (typeof value === "object" && value !== null &&
|
|
332
|
+
if (typeof value === "object" && value !== null && isPlainObject2(value)) {
|
|
153
333
|
renderDict(value, path, depth, out);
|
|
154
334
|
return;
|
|
155
335
|
}
|
|
@@ -172,7 +352,7 @@ var renderDict = (dict, path, depth, out) => {
|
|
|
172
352
|
out.push(`${pad}</dict>`);
|
|
173
353
|
};
|
|
174
354
|
var renderPlist = (dict) => {
|
|
175
|
-
if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !
|
|
355
|
+
if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !isPlainObject2(dict)) {
|
|
176
356
|
throw new PlistError("", "the top level of a launchd plist must be a dict");
|
|
177
357
|
}
|
|
178
358
|
const out = [];
|
|
@@ -328,20 +508,29 @@ var readFileAttributes = async (runner, path) => {
|
|
|
328
508
|
uid: stat.uid
|
|
329
509
|
};
|
|
330
510
|
};
|
|
511
|
+
var writeOptionsOf = (want) => ({
|
|
512
|
+
mode: want.mode,
|
|
513
|
+
...want.uid === undefined ? {} : { uid: want.uid },
|
|
514
|
+
...want.gid === undefined ? {} : { gid: want.gid }
|
|
515
|
+
});
|
|
331
516
|
var matches = (live, want) => live.sha256 === want.sha256 && live.mode === want.mode && (want.uid === undefined || live.uid === want.uid) && (want.gid === undefined || live.gid === want.gid);
|
|
332
517
|
var diffFile = async (runner, news, output) => {
|
|
333
|
-
if (news.path !== output.path)
|
|
334
|
-
return { action: "replace" };
|
|
335
518
|
const want = await desiredFile(runner, news);
|
|
519
|
+
const writes = async (diff) => {
|
|
520
|
+
await runner.checkWrite?.(news.path, writeOptionsOf(want));
|
|
521
|
+
return diff;
|
|
522
|
+
};
|
|
523
|
+
if (news.path !== output.path)
|
|
524
|
+
return writes({ action: "replace" });
|
|
336
525
|
if (want.sha256 !== output.sha256)
|
|
337
|
-
return { action: "update" };
|
|
526
|
+
return writes({ action: "update" });
|
|
338
527
|
const live = await readFileAttributes(runner, news.path);
|
|
339
|
-
return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
|
|
528
|
+
return live !== undefined && matches(live, want) ? { action: "noop" } : writes({ action: "update" });
|
|
340
529
|
};
|
|
341
|
-
var reconcileFile = async (runner, props, output) => {
|
|
530
|
+
var reconcileFile = async (runner, props, output, adopt = false) => {
|
|
342
531
|
const want = await desiredFile(runner, props);
|
|
343
532
|
if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
|
|
344
|
-
throw refuse(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (
|
|
533
|
+
throw refuse(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (sudoRunner() with this path under a prefix). This " + "provider never calls sudo itself.");
|
|
345
534
|
}
|
|
346
535
|
const stat = await runner.stat(props.path);
|
|
347
536
|
if (stat !== undefined && stat.kind !== "file") {
|
|
@@ -350,15 +539,12 @@ var reconcileFile = async (runner, props, output) => {
|
|
|
350
539
|
const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
|
|
351
540
|
const moved = output !== undefined && output.path !== props.path;
|
|
352
541
|
const prior = moved ? undefined : output;
|
|
353
|
-
|
|
542
|
+
const takeOver = adopt && output === undefined;
|
|
543
|
+
if (prior === undefined && before !== undefined && !matches(before, want) && !takeOver) {
|
|
354
544
|
throw refuse(props.path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
|
|
355
545
|
}
|
|
356
546
|
if (before === undefined || !matches(before, want)) {
|
|
357
|
-
await runner.writeFileAtomic(props.path, want.bytes,
|
|
358
|
-
mode: want.mode,
|
|
359
|
-
...want.uid === undefined ? {} : { uid: want.uid },
|
|
360
|
-
...want.gid === undefined ? {} : { gid: want.gid }
|
|
361
|
-
});
|
|
547
|
+
await runner.writeFileAtomic(props.path, want.bytes, writeOptionsOf(want));
|
|
362
548
|
}
|
|
363
549
|
const after = await readFileAttributes(runner, props.path);
|
|
364
550
|
if (after === undefined || !matches(after, want)) {
|
|
@@ -383,34 +569,34 @@ var deleteFile = async (runner, output) => {
|
|
|
383
569
|
|
|
384
570
|
// src/launchd/host-file.ts
|
|
385
571
|
var HostFile = Resource("Host.File");
|
|
386
|
-
var HostFileProvider = () => Provider.effect(HostFile,
|
|
572
|
+
var HostFileProvider = () => Provider.effect(HostFile, Effect6.gen(function* () {
|
|
387
573
|
const runner = yield* HostRunnerService;
|
|
388
574
|
return HostFile.Provider.of({
|
|
389
|
-
list: () =>
|
|
575
|
+
list: () => Effect6.succeed([]),
|
|
390
576
|
read: ({ olds, output }) => lift(async () => {
|
|
391
577
|
const found = await readFileAttributes(runner, olds.path);
|
|
392
578
|
if (found === undefined)
|
|
393
579
|
return;
|
|
394
|
-
return output === undefined ?
|
|
580
|
+
return output === undefined ? Unowned2(found) : found;
|
|
395
581
|
}),
|
|
396
|
-
diff: ({ news, output }) => {
|
|
582
|
+
diff: ({ instanceId, news, output }) => {
|
|
397
583
|
if (output === undefined)
|
|
398
|
-
return
|
|
584
|
+
return noteUnfinished(instanceId);
|
|
399
585
|
if (isResolved(news))
|
|
400
586
|
return lift(() => diffFile(runner, news, output));
|
|
401
587
|
const path = resolvedString(news, "path");
|
|
402
|
-
return
|
|
588
|
+
return Effect6.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
|
|
403
589
|
},
|
|
404
|
-
reconcile: ({ news, output }) => lift(() => reconcileFile(runner, news, output)),
|
|
590
|
+
reconcile: ({ fqn, instanceId, news, output }) => Effect6.flatMap(adoptsAtApply({ fqn, instanceId, output }), (adopt) => lift(() => reconcileFile(runner, news, output, adopt))),
|
|
405
591
|
delete: ({ output }) => lift(() => deleteFile(runner, output))
|
|
406
592
|
});
|
|
407
593
|
}));
|
|
408
594
|
// src/launchd/job.ts
|
|
409
595
|
import { Resource as Resource2 } from "alchemy";
|
|
410
|
-
import { Unowned as
|
|
596
|
+
import { Unowned as Unowned3 } from "alchemy/AdoptPolicy";
|
|
411
597
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
412
598
|
import * as Provider2 from "alchemy/Provider";
|
|
413
|
-
import * as
|
|
599
|
+
import * as Effect7 from "effect/Effect";
|
|
414
600
|
|
|
415
601
|
// src/launchd/job-validate.ts
|
|
416
602
|
var LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
@@ -605,6 +791,8 @@ var bootoutIfLoaded = async (runner, target) => {
|
|
|
605
791
|
};
|
|
606
792
|
|
|
607
793
|
// src/launchd/job-preflight.ts
|
|
794
|
+
var plistWriteOptions = (domain) => domain.kind === "system" ? { gid: 0, mode: 420, uid: 0 } : { mode: 420, uid: domain.uid };
|
|
795
|
+
var checkPlistWrite = async (runner, location) => runner.checkWrite?.(location.plistPath, plistWriteOptions(location.domain));
|
|
608
796
|
var refuse2 = (label, message) => new Error(`Launchd.Job ${label}: ${message}`);
|
|
609
797
|
var assertValid = (props) => {
|
|
610
798
|
const found = jobProblems(props);
|
|
@@ -626,7 +814,7 @@ var assertMayWrite = (runner, label, domain) => {
|
|
|
626
814
|
if (canActAsRoot(runner))
|
|
627
815
|
return;
|
|
628
816
|
if (domain.kind === "system") {
|
|
629
|
-
throw refuse2(label, "the system domain needs root. Run the deploy as root, or
|
|
817
|
+
throw refuse2(label, "the system domain needs root. Run the deploy as root, or pass a deliberately privileged " + "HostRunner: sudoRunner() elevates exactly the calls this needs. This provider never " + "calls sudo itself.");
|
|
630
818
|
}
|
|
631
819
|
if (domain.uid !== runner.effectiveUid()) {
|
|
632
820
|
throw refuse2(label, `gui/${String(domain.uid)} belongs to another user; only root may write it. ` + "Deploy as that user, as root, or through a privileged HostRunner.");
|
|
@@ -661,6 +849,7 @@ var assertReplaceable = async (runner, next, full, old, renderedSha256) => {
|
|
|
661
849
|
assertMayWrite(runner, old.label, oldDomain);
|
|
662
850
|
const { location, status } = await preflight(runner, next);
|
|
663
851
|
await assertUnclaimed(runner, next.label, location, status, renderedSha256);
|
|
852
|
+
await checkPlistWrite(runner, location);
|
|
664
853
|
};
|
|
665
854
|
|
|
666
855
|
// src/launchd/job-lifecycle.ts
|
|
@@ -696,20 +885,24 @@ var diffJob = async (runner, news, output) => {
|
|
|
696
885
|
}
|
|
697
886
|
assertValid(news);
|
|
698
887
|
const desired = renderJob(news).sha256;
|
|
699
|
-
|
|
888
|
+
const update = async () => {
|
|
889
|
+
await checkPlistWrite(runner, await locate(runner, news));
|
|
700
890
|
return { action: "update" };
|
|
891
|
+
};
|
|
892
|
+
if (desired !== output.plistSha256)
|
|
893
|
+
return update();
|
|
701
894
|
const live = await readJob(runner, news);
|
|
702
895
|
if (live === undefined || !live.loaded || live.plistSha256 !== desired)
|
|
703
|
-
return
|
|
896
|
+
return update();
|
|
704
897
|
return { action: "noop" };
|
|
705
898
|
};
|
|
706
|
-
var reconcileJob = async (runner, props, output) => {
|
|
899
|
+
var reconcileJob = async (runner, props, output, adopt = false) => {
|
|
707
900
|
assertValid(props);
|
|
708
901
|
const { location, status } = await preflight(runner, props);
|
|
709
902
|
const rendered = renderJob(props);
|
|
710
903
|
const renamed = output !== undefined && (output.label !== props.label || output.domain !== props.domain);
|
|
711
904
|
const prior = renamed ? undefined : output;
|
|
712
|
-
if (prior === undefined) {
|
|
905
|
+
if (prior === undefined && !(adopt && output === undefined)) {
|
|
713
906
|
await assertUnclaimed(runner, props.label, location, status, rendered.sha256);
|
|
714
907
|
}
|
|
715
908
|
if (renamed)
|
|
@@ -718,7 +911,7 @@ var reconcileJob = async (runner, props, output) => {
|
|
|
718
911
|
if (status.loaded && before !== undefined && sha256Hex(before) === rendered.sha256 && prior?.plistSha256 === rendered.sha256) {
|
|
719
912
|
return attributesOf(props, location, rendered.sha256, status);
|
|
720
913
|
}
|
|
721
|
-
await runner.writeFileAtomic(location.plistPath, rendered.bytes, location.domain
|
|
914
|
+
await runner.writeFileAtomic(location.plistPath, rendered.bytes, plistWriteOptions(location.domain));
|
|
722
915
|
await bootoutIfLoaded(runner, location.target);
|
|
723
916
|
const failed = async (detail) => {
|
|
724
917
|
const removed = before === undefined && await runner.removeFile(location.plistPath).then(() => true, () => false);
|
|
@@ -745,26 +938,26 @@ var deleteJob = async (runner, output) => {
|
|
|
745
938
|
|
|
746
939
|
// src/launchd/job.ts
|
|
747
940
|
var LaunchdJob = Resource2("Launchd.Job");
|
|
748
|
-
var LaunchdJobProvider = () => Provider2.effect(LaunchdJob,
|
|
941
|
+
var LaunchdJobProvider = () => Provider2.effect(LaunchdJob, Effect7.gen(function* () {
|
|
749
942
|
const runner = yield* HostRunnerService;
|
|
750
943
|
return LaunchdJob.Provider.of({
|
|
751
|
-
list: () =>
|
|
944
|
+
list: () => Effect7.succeed([]),
|
|
752
945
|
read: ({ olds, output }) => lift(async () => {
|
|
753
946
|
const found = await readJob(runner, olds);
|
|
754
947
|
if (found === undefined)
|
|
755
948
|
return;
|
|
756
|
-
return output === undefined ?
|
|
949
|
+
return output === undefined ? Unowned3(found) : found;
|
|
757
950
|
}),
|
|
758
|
-
diff: ({ news, output }) => {
|
|
951
|
+
diff: ({ instanceId, news, output }) => {
|
|
759
952
|
if (output === undefined)
|
|
760
|
-
return
|
|
953
|
+
return noteUnfinished(instanceId);
|
|
761
954
|
if (isResolved2(news))
|
|
762
955
|
return lift(() => diffJob(runner, news, output));
|
|
763
956
|
const label = resolvedString(news, "label");
|
|
764
957
|
const domain = resolvedString(news, "domain");
|
|
765
|
-
return label !== undefined && domain !== undefined && (label !== output.label || domain !== output.domain) ? lift(() => replaceDiff(runner, { domain, label }, undefined, output)) :
|
|
958
|
+
return label !== undefined && domain !== undefined && (label !== output.label || domain !== output.domain) ? lift(() => replaceDiff(runner, { domain, label }, undefined, output)) : Effect7.succeed(undefined);
|
|
766
959
|
},
|
|
767
|
-
reconcile: ({ news, output }) => lift(() => reconcileJob(runner, news, output)),
|
|
960
|
+
reconcile: ({ fqn, instanceId, news, output }) => Effect7.flatMap(adoptsAtApply({ fqn, instanceId, output }), (adopt) => lift(() => reconcileJob(runner, news, output, adopt))),
|
|
768
961
|
delete: ({ output }) => lift(() => deleteJob(runner, output))
|
|
769
962
|
});
|
|
770
963
|
}));
|
|
@@ -835,8 +1028,8 @@ var parseGroup = (platform, stdout) => {
|
|
|
835
1028
|
};
|
|
836
1029
|
|
|
837
1030
|
// src/launchd/local-runner.ts
|
|
838
|
-
var
|
|
839
|
-
var
|
|
1031
|
+
var errnoCode = (cause) => typeof cause === "object" && cause !== null && ("code" in cause) ? String(cause.code) : undefined;
|
|
1032
|
+
var absent2 = (cause) => errnoCode(cause) === "ENOENT" || errnoCode(cause) === "ENOTDIR";
|
|
840
1033
|
var run = (argv, timeoutMs) => new Promise((resolve, reject) => {
|
|
841
1034
|
const [program, ...args] = argv;
|
|
842
1035
|
if (program === undefined) {
|
|
@@ -915,7 +1108,7 @@ var localRunner = (options = {}) => {
|
|
|
915
1108
|
try {
|
|
916
1109
|
return new Uint8Array(await readFile(path));
|
|
917
1110
|
} catch (cause) {
|
|
918
|
-
if (
|
|
1111
|
+
if (absent2(cause))
|
|
919
1112
|
return;
|
|
920
1113
|
throw cause;
|
|
921
1114
|
}
|
|
@@ -924,7 +1117,7 @@ var localRunner = (options = {}) => {
|
|
|
924
1117
|
try {
|
|
925
1118
|
await unlink(path);
|
|
926
1119
|
} catch (cause) {
|
|
927
|
-
if (!
|
|
1120
|
+
if (!absent2(cause))
|
|
928
1121
|
throw cause;
|
|
929
1122
|
}
|
|
930
1123
|
},
|
|
@@ -940,7 +1133,7 @@ var localRunner = (options = {}) => {
|
|
|
940
1133
|
uid: stats.uid
|
|
941
1134
|
};
|
|
942
1135
|
} catch (cause) {
|
|
943
|
-
if (
|
|
1136
|
+
if (absent2(cause))
|
|
944
1137
|
return;
|
|
945
1138
|
throw cause;
|
|
946
1139
|
}
|
|
@@ -951,6 +1144,404 @@ var localRunner = (options = {}) => {
|
|
|
951
1144
|
// src/launchd/providers.ts
|
|
952
1145
|
import * as Layer2 from "effect/Layer";
|
|
953
1146
|
var launchdProviders = (runner = localRunner()) => Layer2.mergeAll(LaunchdJobProvider(), HostFileProvider()).pipe(Layer2.provide(hostRunnerLayer(runner)));
|
|
1147
|
+
// src/launchd/sudo-allowlist.ts
|
|
1148
|
+
class SudoRefusedError extends Error {
|
|
1149
|
+
constructor(message) {
|
|
1150
|
+
super(message);
|
|
1151
|
+
this.name = "SudoRefusedError";
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
var SUDO = "/usr/bin/sudo";
|
|
1155
|
+
var INSTALL = "/usr/bin/install";
|
|
1156
|
+
var RM = "/bin/rm";
|
|
1157
|
+
var WRITES = new Set(["bootstrap", "bootout", "kickstart"]);
|
|
1158
|
+
var octalMode = (mode) => Number.isSafeInteger(mode) && mode >= 0 && mode <= 4095 ? mode.toString(8).padStart(4, "0") : undefined;
|
|
1159
|
+
var checkPrefixes = (prefixes) => {
|
|
1160
|
+
if (prefixes.length === 0) {
|
|
1161
|
+
throw new Error("sudoRunner: no prefixes. Name the directories root may write, e.g. /Library/LaunchDaemons.");
|
|
1162
|
+
}
|
|
1163
|
+
for (const prefix of prefixes) {
|
|
1164
|
+
const found = prefix === "/" ? ['"/" would allow every path'] : pathProblems(prefix);
|
|
1165
|
+
if (found.length > 0) {
|
|
1166
|
+
throw new Error(`sudoRunner: prefix ${JSON.stringify(prefix)}: ${found.join("; ")}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return [...prefixes];
|
|
1170
|
+
};
|
|
1171
|
+
var prefixOf = (path, prefixes) => pathProblems(path).length > 0 ? undefined : prefixes.find((prefix) => path.startsWith(`${prefix}/`));
|
|
1172
|
+
var labelOk = (label) => LABEL.test(label) && !RESERVED_LABEL_PREFIXES.some((prefix) => label.startsWith(prefix));
|
|
1173
|
+
var systemLabel = (target) => {
|
|
1174
|
+
const label = target?.startsWith("system/") === true ? target.slice("system/".length) : undefined;
|
|
1175
|
+
return label !== undefined && labelOk(label) ? label : undefined;
|
|
1176
|
+
};
|
|
1177
|
+
var daemonPlist = (label, prefixes) => {
|
|
1178
|
+
const path = plistPathFor(label, { kind: "system" }, undefined);
|
|
1179
|
+
return prefixOf(path, prefixes) === undefined ? undefined : path;
|
|
1180
|
+
};
|
|
1181
|
+
var NO_DAEMON_PREFIX = "needs /Library/LaunchDaemons among the prefixes, where the job’s plist is written and removed";
|
|
1182
|
+
var launchctlProblem = (args, prefixes) => {
|
|
1183
|
+
const [sub, ...rest] = args;
|
|
1184
|
+
if (sub === "bootstrap") {
|
|
1185
|
+
const given = rest[1] ?? "";
|
|
1186
|
+
const name = given.slice(given.lastIndexOf("/") + 1);
|
|
1187
|
+
const label = name.endsWith(".plist") ? name.slice(0, -".plist".length) : "";
|
|
1188
|
+
const shaped = rest.length === 2 && rest[0] === "system" && labelOk(label);
|
|
1189
|
+
if (!shaped)
|
|
1190
|
+
return "bootstrap takes exactly `system /Library/LaunchDaemons/<label>.plist`";
|
|
1191
|
+
const plist = daemonPlist(label, prefixes);
|
|
1192
|
+
if (plist === undefined)
|
|
1193
|
+
return `bootstrap ${NO_DAEMON_PREFIX}`;
|
|
1194
|
+
return given === plist ? undefined : `bootstrap loads only ${plist}, never another path`;
|
|
1195
|
+
}
|
|
1196
|
+
if (sub === "bootout") {
|
|
1197
|
+
const label = rest.length === 1 ? systemLabel(rest[0]) : undefined;
|
|
1198
|
+
if (label === undefined)
|
|
1199
|
+
return "bootout takes exactly `system/<label>`, never a bare domain";
|
|
1200
|
+
return daemonPlist(label, prefixes) === undefined ? `bootout ${NO_DAEMON_PREFIX}` : undefined;
|
|
1201
|
+
}
|
|
1202
|
+
if (sub === "kickstart") {
|
|
1203
|
+
const flags = rest.slice(0, -1);
|
|
1204
|
+
const flagsOk = flags.every((flag) => flag === "-k" || flag === "-p") && new Set(flags).size === flags.length;
|
|
1205
|
+
return flagsOk && systemLabel(rest[rest.length - 1]) !== undefined ? undefined : "kickstart takes only -k / -p, then `system/<label>`";
|
|
1206
|
+
}
|
|
1207
|
+
return `launchctl ${String(sub)} is never run as root`;
|
|
1208
|
+
};
|
|
1209
|
+
var ID = /^\d+$/;
|
|
1210
|
+
var idsOk = (ids) => {
|
|
1211
|
+
const [flag, id, second, secondId] = ids;
|
|
1212
|
+
if (ids.length === 0)
|
|
1213
|
+
return true;
|
|
1214
|
+
if (ids.length === 2)
|
|
1215
|
+
return (flag === "-o" || flag === "-g") && ID.test(id ?? "");
|
|
1216
|
+
return ids.length === 4 && flag === "-o" && ID.test(id ?? "") && second === "-g" && ID.test(secondId ?? "");
|
|
1217
|
+
};
|
|
1218
|
+
var privilegedProblem = (argv, context) => {
|
|
1219
|
+
const [program, ...args] = argv;
|
|
1220
|
+
const under = (path) => path !== undefined && prefixOf(path, context.prefixes) !== undefined;
|
|
1221
|
+
if (program === LAUNCHCTL)
|
|
1222
|
+
return launchctlProblem(args, context.prefixes);
|
|
1223
|
+
if (program === INSTALL) {
|
|
1224
|
+
const [safe, m, mode, ...rest] = args;
|
|
1225
|
+
if (safe !== "-S" || m !== "-m" || mode === undefined || !/^[0-7]{4}$/.test(mode)) {
|
|
1226
|
+
return "install takes exactly `-S -m <4 octal digits> [-o <uid>] [-g <gid>] <staged> <dest>`";
|
|
1227
|
+
}
|
|
1228
|
+
if (rest.length < 2 || !idsOk(rest.slice(0, -2)))
|
|
1229
|
+
return "install takes only -o <uid> -g <gid>";
|
|
1230
|
+
const [source, dest] = rest.slice(-2);
|
|
1231
|
+
if (source === undefined || source !== context.staged) {
|
|
1232
|
+
return "install copies only the file this runner staged";
|
|
1233
|
+
}
|
|
1234
|
+
return under(dest) ? undefined : "install destination is not under a declared prefix";
|
|
1235
|
+
}
|
|
1236
|
+
if (program === RM) {
|
|
1237
|
+
const [force, end, path, ...extra] = args;
|
|
1238
|
+
return force === "-f" && end === "--" && extra.length === 0 && under(path) ? undefined : "rm takes exactly `-f -- <path under a declared prefix>`";
|
|
1239
|
+
}
|
|
1240
|
+
return `${String(program)} is not on the sudo allowlist`;
|
|
1241
|
+
};
|
|
1242
|
+
var routeExec = (argv, operatorUid) => {
|
|
1243
|
+
const [program, sub] = argv;
|
|
1244
|
+
if (program === "sudo" || program?.endsWith("/sudo") === true) {
|
|
1245
|
+
return {
|
|
1246
|
+
refuse: "sudoRunner never passes a sudo argv through; it elevates only its allowlist"
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
if (program !== LAUNCHCTL || sub === undefined || !WRITES.has(sub))
|
|
1250
|
+
return { as: "operator" };
|
|
1251
|
+
const target = sub === "kickstart" ? argv[argv.length - 1] : argv[2];
|
|
1252
|
+
if (target === "system" || target?.startsWith("system/") === true)
|
|
1253
|
+
return { as: "root" };
|
|
1254
|
+
const owner = /^(?:gui|user)\/(\d+)(?:\/|$)/.exec(target ?? "")?.[1];
|
|
1255
|
+
if (owner === undefined || Number(owner) === operatorUid)
|
|
1256
|
+
return { as: "operator" };
|
|
1257
|
+
return {
|
|
1258
|
+
refuse: `launchctl ${sub} ${String(target)}: that domain belongs to uid ${owner}, and sudoRunner ` + "elevates only the system domain. Deploy as that user, or as root."
|
|
1259
|
+
};
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// src/launchd/sudo-acl.ts
|
|
1263
|
+
var LS = "/bin/ls";
|
|
1264
|
+
var WRITE_RIGHTS = new Set([
|
|
1265
|
+
"add_file",
|
|
1266
|
+
"add_subdirectory",
|
|
1267
|
+
"append",
|
|
1268
|
+
"chown",
|
|
1269
|
+
"delete",
|
|
1270
|
+
"delete_child",
|
|
1271
|
+
"write",
|
|
1272
|
+
"writesecurity"
|
|
1273
|
+
]);
|
|
1274
|
+
var ENTRY = /^\s*\d+:\s+\S+\s+(?:inherited\s+)?allow\s+(\S+)/;
|
|
1275
|
+
var aclGrants = (listing) => listing.split(`
|
|
1276
|
+
`).filter((line) => {
|
|
1277
|
+
const rights = ENTRY.exec(line)?.[1];
|
|
1278
|
+
return rights !== undefined && rights.split(",").some((right) => WRITE_RIGHTS.has(right));
|
|
1279
|
+
}).map((line) => line.trim());
|
|
1280
|
+
var assertNoAclGrants = async (base, path, chain) => {
|
|
1281
|
+
const listed = await base.exec([LS, "-lden", "--", ...chain]);
|
|
1282
|
+
if (listed.exitCode !== 0) {
|
|
1283
|
+
throw new SudoRefusedError(`sudoRunner ${path}: could not read the ACLs of the directories from / down to it ` + `(${LS} -lden exit ${String(listed.exitCode)}). Nothing ran as root.`);
|
|
1284
|
+
}
|
|
1285
|
+
const grants = aclGrants(listed.stdout);
|
|
1286
|
+
if (grants.length === 0)
|
|
1287
|
+
return;
|
|
1288
|
+
throw new SudoRefusedError(`sudoRunner ${path}: a directory from / down to it carries an ACL that lets someone other ` + `than root change what it holds (${grants.join("; ")}). Remove the entry (chmod -a), or ` + "declare a prefix without one. Nothing ran as root.");
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/launchd/sudo-guard.ts
|
|
1292
|
+
var refuse3 = (path, message) => new SudoRefusedError(`sudoRunner ${path}: ${message}`);
|
|
1293
|
+
var betweenDirs = (prefix, path) => {
|
|
1294
|
+
const parts = path.slice(prefix.length + 1).split("/").slice(0, -1);
|
|
1295
|
+
return parts.map((_, index) => `${prefix}/${parts.slice(0, index + 1).join("/")}`);
|
|
1296
|
+
};
|
|
1297
|
+
var aboveDirs = (prefix) => {
|
|
1298
|
+
const parts = prefix.split("/").slice(1, -1);
|
|
1299
|
+
return ["/", ...parts.map((_, index) => `/${parts.slice(0, index + 1).join("/")}`)];
|
|
1300
|
+
};
|
|
1301
|
+
var rootOnlyProblem = (stat) => {
|
|
1302
|
+
if (stat === undefined)
|
|
1303
|
+
return "is missing";
|
|
1304
|
+
if (stat.kind !== "directory")
|
|
1305
|
+
return `is a ${stat.kind}, not a directory`;
|
|
1306
|
+
if (stat.uid !== 0)
|
|
1307
|
+
return `is owned by uid ${String(stat.uid)}, not root`;
|
|
1308
|
+
if ((stat.mode & 18) !== 0) {
|
|
1309
|
+
return `is writable by group or other (mode ${stat.mode.toString(8).padStart(4, "0")})`;
|
|
1310
|
+
}
|
|
1311
|
+
return;
|
|
1312
|
+
};
|
|
1313
|
+
var assertPlainPath = async (base, prefix, path, expect) => {
|
|
1314
|
+
const above = aboveDirs(prefix);
|
|
1315
|
+
for (const [index, stat] of (await Promise.all(above.map((dir) => base.stat(dir)))).entries()) {
|
|
1316
|
+
const up = stat?.kind === "symlink" && stat.uid === 0 ? undefined : rootOnlyProblem(stat);
|
|
1317
|
+
if (up !== undefined) {
|
|
1318
|
+
throw refuse3(path, `${String(above[index])}, above the declared prefix ${prefix}, ${up}. Whoever may change it ` + "may swap the prefix itself; every directory from / down must be root-only. Nothing ran as root.");
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const dirs = betweenDirs(prefix, path);
|
|
1322
|
+
const [prefixStat, ...stats] = await Promise.all([prefix, ...dirs].map((dir) => base.stat(dir)));
|
|
1323
|
+
const bad = rootOnlyProblem(prefixStat);
|
|
1324
|
+
if (bad !== undefined) {
|
|
1325
|
+
throw refuse3(path, `the declared prefix ${prefix} ${bad}. Declare only a real directory that root owns and ` + "only root may write. Nothing ran as root.");
|
|
1326
|
+
}
|
|
1327
|
+
for (const [index, stat] of stats.entries()) {
|
|
1328
|
+
const dir = String(dirs[index]);
|
|
1329
|
+
if (stat?.kind !== "directory") {
|
|
1330
|
+
throw refuse3(path, `${dir} is ${stat === undefined ? "missing" : `a ${stat.kind}`}; root ` + "writes only through real directories under a declared prefix.");
|
|
1331
|
+
}
|
|
1332
|
+
const below = rootOnlyProblem(stat);
|
|
1333
|
+
if (below !== undefined) {
|
|
1334
|
+
throw refuse3(path, `${dir} ${below}. Every directory from the prefix ${prefix} down to the file must be one ` + "that root owns and only root may write. Nothing ran as root.");
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
await assertNoAclGrants(base, path, [...above, prefix, ...dirs]);
|
|
1338
|
+
const target = await base.stat(path);
|
|
1339
|
+
if (target === undefined && expect === "file-or-absent")
|
|
1340
|
+
return "absent";
|
|
1341
|
+
if (target?.kind === "file")
|
|
1342
|
+
return "file";
|
|
1343
|
+
throw refuse3(path, `is ${target === undefined ? "missing" : `a ${target.kind}`}, not a file`);
|
|
1344
|
+
};
|
|
1345
|
+
var modeProblem = (options) => {
|
|
1346
|
+
if (options.uid !== undefined && options.uid !== 0)
|
|
1347
|
+
return;
|
|
1348
|
+
const found = [];
|
|
1349
|
+
if ((options.mode & 3072) !== 0)
|
|
1350
|
+
found.push("setuid/setgid");
|
|
1351
|
+
if ((options.mode & 18) !== 0)
|
|
1352
|
+
found.push("writable by group or other");
|
|
1353
|
+
if (found.length === 0)
|
|
1354
|
+
return;
|
|
1355
|
+
return `mode ${options.mode.toString(8).padStart(4, "0")} is ${found.join(" and ")} on a root-owned file`;
|
|
1356
|
+
};
|
|
1357
|
+
var assertInstallable = async (base, prefix, path, options, groups) => {
|
|
1358
|
+
const mode = modeProblem(options);
|
|
1359
|
+
if (mode !== undefined) {
|
|
1360
|
+
throw refuse3(path, `${mode}. Declare a mode without those bits (0644, 0640, 0755), or give the file to its ` + "real owner. Nothing ran as root.");
|
|
1361
|
+
}
|
|
1362
|
+
await assertPlainPath(base, prefix, path, "file-or-absent");
|
|
1363
|
+
await assertReadableBack(base, path, options, groups);
|
|
1364
|
+
};
|
|
1365
|
+
var canRead = (file, uid, groups) => {
|
|
1366
|
+
if (uid === 0)
|
|
1367
|
+
return true;
|
|
1368
|
+
if (file.uid === uid)
|
|
1369
|
+
return (file.mode & 256) !== 0;
|
|
1370
|
+
const groupRead = (file.mode & 32) !== 0;
|
|
1371
|
+
const otherRead = (file.mode & 4) !== 0;
|
|
1372
|
+
if (groupRead === otherRead)
|
|
1373
|
+
return groupRead;
|
|
1374
|
+
if (groups === undefined)
|
|
1375
|
+
return;
|
|
1376
|
+
return groups.includes(file.gid) ? groupRead : otherRead;
|
|
1377
|
+
};
|
|
1378
|
+
var assertReadableBack = async (base, path, options, groups) => {
|
|
1379
|
+
const operator = base.effectiveUid();
|
|
1380
|
+
const parent = path.slice(0, path.lastIndexOf("/")) || "/";
|
|
1381
|
+
const gid = options.gid ?? (await base.stat(parent))?.gid ?? 0;
|
|
1382
|
+
const file = { gid, mode: options.mode, uid: options.uid ?? 0 };
|
|
1383
|
+
if (canRead(file, operator, groups) !== false)
|
|
1384
|
+
return;
|
|
1385
|
+
throw refuse3(path, `owner ${String(file.uid)}, group ${String(gid)}, mode ${file.mode.toString(8)} would hide it ` + `from the deploying user (uid ${String(operator)}), who reads every file back. Declare a ` + "mode that user can read, or deploy as root. Nothing was written.");
|
|
1386
|
+
};
|
|
1387
|
+
var operatorGroups = async (base) => {
|
|
1388
|
+
const result = await base.exec(["/usr/bin/id", "-G"]);
|
|
1389
|
+
const ids = result.stdout.trim().split(/\s+/);
|
|
1390
|
+
return result.exitCode === 0 && ids.every((id) => /^\d+$/.test(id)) ? ids.map(Number) : undefined;
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
// src/launchd/sudo-said.ts
|
|
1394
|
+
var PASSWORD = /^sudo: (?:a password is required|a terminal is required|sorry, you must have a tty)/m;
|
|
1395
|
+
var NOT_ALLOWED = /^(?:Sorry, user \S+ (?:is not allowed to execute|may not run sudo)|\S+ is not (?:in the sudoers file|allowed to run sudo on ))/m;
|
|
1396
|
+
var undeclared = (path, prefixes) => (cause) => {
|
|
1397
|
+
const code = errnoCode(cause);
|
|
1398
|
+
if (code !== "EACCES" && code !== "EPERM")
|
|
1399
|
+
throw cause;
|
|
1400
|
+
throw new Error(`sudoRunner ${path}: ${code} as the deploying user, and the path is under no declared prefix ` + `(${prefixes.join(", ")}). If root owns its directory, declare that directory as a prefix.`, { cause });
|
|
1401
|
+
};
|
|
1402
|
+
var sudoRefusal = (shown, result) => {
|
|
1403
|
+
if (result.exitCode !== 1)
|
|
1404
|
+
return;
|
|
1405
|
+
if (PASSWORD.test(result.stderr)) {
|
|
1406
|
+
return new SudoRefusedError(`sudo -n ${shown}: a password is required, and this runner never prompts. Run \`sudo -v\` ` + "in the deploying terminal just before the deploy, or grant exactly these commands " + "NOPASSWD (docs/launchd-sudo.md). The command did not run.");
|
|
1407
|
+
}
|
|
1408
|
+
if (NOT_ALLOWED.test(result.stderr)) {
|
|
1409
|
+
return new SudoRefusedError(`sudo -n ${shown}: sudoers does not let the deploying user run this. The command did not run.`);
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
// src/launchd/sudo-stage.ts
|
|
1415
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1416
|
+
import { tmpdir } from "node:os";
|
|
1417
|
+
import { join as join2 } from "node:path";
|
|
1418
|
+
var stageFile = async (bytes) => {
|
|
1419
|
+
const dir = await mkdtemp(join2(tmpdir(), "hf-sudo-"));
|
|
1420
|
+
const dispose = () => rm(dir, { force: true, recursive: true });
|
|
1421
|
+
try {
|
|
1422
|
+
const path = join2(dir, "staged");
|
|
1423
|
+
await writeFile(path, bytes, { flag: "wx", mode: 384 });
|
|
1424
|
+
return { dispose, path };
|
|
1425
|
+
} catch (cause) {
|
|
1426
|
+
await dispose();
|
|
1427
|
+
throw cause;
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/launchd/sudo-runner.ts
|
|
1432
|
+
var makeSudoRunner = (prefixList, deps) => {
|
|
1433
|
+
const prefixes = checkPrefixes(prefixList);
|
|
1434
|
+
const { base } = deps;
|
|
1435
|
+
const elevate = async (argv, { before, staged } = {}) => {
|
|
1436
|
+
const shown = JSON.stringify(argv);
|
|
1437
|
+
const problem = privilegedProblem(argv, {
|
|
1438
|
+
prefixes,
|
|
1439
|
+
...staged === undefined ? {} : { staged }
|
|
1440
|
+
});
|
|
1441
|
+
if (problem !== undefined)
|
|
1442
|
+
throw new SudoRefusedError(`sudoRunner refused ${shown}: ${problem}`);
|
|
1443
|
+
await before?.();
|
|
1444
|
+
deps.log(`homeflare/launchd sudo -n ${shown}`);
|
|
1445
|
+
const result = await base.exec([SUDO, "-n", "--", ...argv]);
|
|
1446
|
+
const refused = sudoRefusal(shown, result);
|
|
1447
|
+
if (refused !== undefined)
|
|
1448
|
+
throw refused;
|
|
1449
|
+
return result;
|
|
1450
|
+
};
|
|
1451
|
+
const vetWrite = async (path, options) => {
|
|
1452
|
+
const prefix = prefixOf(path, prefixes);
|
|
1453
|
+
const operator = base.effectiveUid();
|
|
1454
|
+
if (prefix === undefined) {
|
|
1455
|
+
if (options.uid !== undefined && options.uid !== operator && operator !== 0) {
|
|
1456
|
+
throw new SudoRefusedError(`sudoRunner ${path}: owner ${String(options.uid)} needs root, and the path is under no ` + `declared prefix (${prefixes.join(", ")}). Declare its directory, or deploy as root. ` + "Nothing was written.");
|
|
1457
|
+
}
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const mode = octalMode(options.mode);
|
|
1461
|
+
if (mode === undefined)
|
|
1462
|
+
throw new SudoRefusedError(`sudoRunner ${path}: mode must be 0–0o7777`);
|
|
1463
|
+
await assertInstallable(base, prefix, path, options, await deps.groups());
|
|
1464
|
+
return { mode, prefix };
|
|
1465
|
+
};
|
|
1466
|
+
const mustSucceed = async (argv, staged) => {
|
|
1467
|
+
const result = await elevate(argv, staged === undefined ? {} : { staged });
|
|
1468
|
+
if (result.exitCode !== 0) {
|
|
1469
|
+
const stderr = result.stderr.trim().slice(0, 300);
|
|
1470
|
+
throw new Error(`sudo -n ${JSON.stringify(argv)} -> ${String(result.exitCode)}: ${stderr}`);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
return {
|
|
1474
|
+
checkWrite: async (path, options) => {
|
|
1475
|
+
await vetWrite(path, options);
|
|
1476
|
+
},
|
|
1477
|
+
effectiveUid: () => base.effectiveUid(),
|
|
1478
|
+
exec: async (argv) => {
|
|
1479
|
+
const route = routeExec(argv, base.effectiveUid());
|
|
1480
|
+
if ("refuse" in route)
|
|
1481
|
+
throw new SudoRefusedError(`sudoRunner: ${route.refuse}`);
|
|
1482
|
+
if (route.as === "operator")
|
|
1483
|
+
return base.exec(argv);
|
|
1484
|
+
const plist = argv[1] === "bootstrap" ? argv[3] : undefined;
|
|
1485
|
+
const prefix = plist === undefined ? undefined : prefixOf(plist, prefixes);
|
|
1486
|
+
return elevate(argv, {
|
|
1487
|
+
before: async () => {
|
|
1488
|
+
if (plist !== undefined && prefix !== undefined) {
|
|
1489
|
+
await assertPlainPath(base, prefix, plist, "file");
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
},
|
|
1494
|
+
lookupGroup: (nameOrId) => base.lookupGroup(nameOrId),
|
|
1495
|
+
lookupUser: (nameOrId) => base.lookupUser(nameOrId),
|
|
1496
|
+
privileged: true,
|
|
1497
|
+
readFile: (path) => base.readFile(path),
|
|
1498
|
+
removeFile: async (path) => {
|
|
1499
|
+
const prefix = prefixOf(path, prefixes);
|
|
1500
|
+
if (prefix === undefined)
|
|
1501
|
+
return base.removeFile(path).catch(undeclared(path, prefixes));
|
|
1502
|
+
if (await base.stat(path) === undefined)
|
|
1503
|
+
return;
|
|
1504
|
+
await assertPlainPath(base, prefix, path, "file");
|
|
1505
|
+
await mustSucceed([RM, "-f", "--", path]);
|
|
1506
|
+
},
|
|
1507
|
+
sleep: (ms) => base.sleep(ms),
|
|
1508
|
+
stat: (path) => base.stat(path),
|
|
1509
|
+
writeFileAtomic: async (path, bytes, options) => {
|
|
1510
|
+
const vetted = await vetWrite(path, options);
|
|
1511
|
+
if (vetted === undefined) {
|
|
1512
|
+
return base.writeFileAtomic(path, bytes, options).catch(undeclared(path, prefixes));
|
|
1513
|
+
}
|
|
1514
|
+
const { mode } = vetted;
|
|
1515
|
+
const staged = await deps.stage(bytes);
|
|
1516
|
+
try {
|
|
1517
|
+
await mustSucceed([
|
|
1518
|
+
INSTALL,
|
|
1519
|
+
"-S",
|
|
1520
|
+
"-m",
|
|
1521
|
+
mode,
|
|
1522
|
+
...options.uid === undefined ? [] : ["-o", String(options.uid)],
|
|
1523
|
+
...options.gid === undefined ? [] : ["-g", String(options.gid)],
|
|
1524
|
+
staged.path,
|
|
1525
|
+
path
|
|
1526
|
+
], staged.path);
|
|
1527
|
+
} finally {
|
|
1528
|
+
await staged.dispose();
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
};
|
|
1533
|
+
var sudoRunner = (options) => {
|
|
1534
|
+
const base = localRunner(options);
|
|
1535
|
+
return makeSudoRunner(options.prefixes, {
|
|
1536
|
+
base,
|
|
1537
|
+
groups: () => operatorGroups(base),
|
|
1538
|
+
log: options.log ?? ((line) => {
|
|
1539
|
+
process.stderr.write(`${line}
|
|
1540
|
+
`);
|
|
1541
|
+
}),
|
|
1542
|
+
stage: stageFile
|
|
1543
|
+
});
|
|
1544
|
+
};
|
|
954
1545
|
export {
|
|
955
1546
|
HostFile,
|
|
956
1547
|
HostFileProvider,
|
|
@@ -958,12 +1549,14 @@ export {
|
|
|
958
1549
|
LaunchdJob,
|
|
959
1550
|
LaunchdJobProvider,
|
|
960
1551
|
PlistError,
|
|
1552
|
+
SudoRefusedError,
|
|
961
1553
|
canActAsRoot,
|
|
962
1554
|
hostRunnerLayer,
|
|
963
1555
|
launchdProviders,
|
|
964
1556
|
localRunner,
|
|
965
|
-
renderPlist
|
|
1557
|
+
renderPlist,
|
|
1558
|
+
sudoRunner
|
|
966
1559
|
};
|
|
967
1560
|
|
|
968
|
-
//# debugId=
|
|
1561
|
+
//# debugId=59F08ED5EC139A6064756E2164756E21
|
|
969
1562
|
//# sourceMappingURL=index.js.map
|