@homeflare/alchemy 0.8.0 → 0.10.1
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 +22 -11
- package/dist/caddy/config-lifecycle.d.ts +1 -1
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/index.js +221 -53
- package/dist/caddy/index.js.map +13 -9
- 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 +1 -0
- package/dist/launchd/fake-sudo.d.ts.map +1 -1
- package/dist/launchd/host-effect.d.ts +9 -18
- package/dist/launchd/host-effect.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.js +345 -86
- package/dist/launchd/index.js.map +18 -12
- 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.d.ts +2 -1
- package/dist/launchd/job.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +7 -0
- 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-guard.d.ts +31 -4
- package/dist/launchd/sudo-guard.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts +3 -3
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/launchd/sudo-said.d.ts +10 -0
- package/dist/launchd/sudo-said.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.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-families.d.ts +6 -3
- package/dist/openbao/fake-families.d.ts.map +1 -1
- 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/index.js +541 -270
- package/dist/openbao/index.js.map +26 -21
- package/dist/openbao/jwt-config.d.ts.map +1 -1
- package/dist/openbao/jwt-role.d.ts.map +1 -1
- package/dist/openbao/kubernetes-role.d.ts.map +1 -1
- package/dist/openbao/mfa-enforcement.d.ts +6 -2
- 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/role-reconcile.d.ts +11 -1
- package/dist/openbao/role-reconcile.d.ts.map +1 -1
- 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 +370 -0
- package/dist/verify/cli.js.map +17 -0
- package/dist/verify/fake-engine.d.ts +65 -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 +347 -0
- package/dist/verify/index.js.map +16 -0
- package/dist/verify/plan-scope.d.ts +14 -0
- package/dist/verify/plan-scope.d.ts.map +1 -0
- package/dist/verify/recheck.d.ts +42 -0
- package/dist/verify/recheck.d.ts.map +1 -0
- package/dist/verify/report.d.ts +6 -0
- package/dist/verify/report.d.ts.map +1 -0
- package/dist/verify/rows.d.ts +67 -0
- package/dist/verify/rows.d.ts.map +1 -0
- package/dist/verify/spy.d.ts +43 -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 +119 -0
- package/docs/adopted-deploys.md +85 -0
- package/docs/credentials.md +15 -0
- package/docs/launchd-sudo.md +31 -6
- package/docs/launchd.md +7 -3
- package/docs/ownership.md +113 -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 +1 -1
- package/src/caddy/config-lifecycle.ts +1 -1
- package/src/caddy/config.ts +2 -1
- package/src/launchd/adopt-apply.test.ts +166 -0
- package/src/launchd/fake-runner.ts +26 -4
- package/src/launchd/host-effect.ts +1 -33
- package/src/launchd/host-file-lifecycle.ts +39 -14
- package/src/launchd/host-file.ts +13 -4
- package/src/launchd/job-lifecycle.ts +22 -14
- package/src/launchd/job-preflight.ts +18 -1
- package/src/launchd/job.ts +13 -4
- package/src/launchd/runner.ts +7 -0
- package/src/launchd/sudo-acl.ts +68 -0
- package/src/launchd/sudo-chain.test.ts +121 -0
- package/src/launchd/sudo-guard.ts +89 -10
- package/src/launchd/sudo-modes.test.ts +135 -0
- package/src/launchd/sudo-runner.ts +40 -59
- package/src/launchd/sudo-said.ts +58 -0
- package/src/openbao/README.md +5 -2
- package/src/openbao/REPLACE.md +13 -11
- 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.ts +14 -4
- package/src/openbao/cloudflare-role.ts +17 -4
- package/src/openbao/fake-bao.ts +52 -0
- package/src/openbao/fake-core.ts +141 -0
- package/src/openbao/fake-families.ts +11 -5
- package/src/openbao/fake-mounts.ts +74 -0
- package/src/openbao/fake-stack.ts +17 -4
- package/src/openbao/jwt-config.ts +10 -7
- package/src/openbao/jwt-role.ts +10 -7
- package/src/openbao/kubernetes-role.ts +13 -7
- package/src/openbao/mfa-enforcement.ts +20 -6
- package/src/openbao/mfa-totp.ts +22 -7
- 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 +15 -4
- package/src/openbao/plugin-reconcile.ts +16 -5
- package/src/openbao/plugin.ts +17 -6
- package/src/openbao/policy.ts +17 -4
- package/src/openbao/proxmox-role.ts +13 -4
- package/src/openbao/rename-adoption.test.ts +32 -19
- package/src/openbao/role-reconcile.ts +23 -0
- package/src/openbao/ssh-role.ts +14 -4
- 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 +120 -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 +182 -0
- package/src/verify/index.ts +37 -0
- package/src/verify/plan-scope.ts +34 -0
- package/src/verify/recheck.test.ts +56 -0
- package/src/verify/recheck.ts +71 -0
- package/src/verify/report.ts +52 -0
- package/src/verify/rows.ts +210 -0
- package/src/verify/spy.test.ts +141 -0
- package/src/verify/spy.ts +160 -0
- package/src/verify/verify.test.ts +146 -0
- package/src/verify/verify.ts +185 -0
package/dist/launchd/index.js
CHANGED
|
@@ -1,17 +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
|
|
6
|
+
import * as Effect6 from "effect/Effect";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
8
|
+
// src/ownership/adopt.ts
|
|
9
9
|
import { AdoptPolicy } from "alchemy/AdoptPolicy";
|
|
10
10
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
11
|
-
import { Stack } from "alchemy/Stack";
|
|
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";
|
|
12
18
|
import * as Effect from "effect/Effect";
|
|
13
19
|
import * as Option from "effect/Option";
|
|
14
|
-
var
|
|
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";
|
|
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);
|
|
184
|
+
|
|
185
|
+
// src/launchd/host-effect.ts
|
|
186
|
+
import * as Effect5 from "effect/Effect";
|
|
187
|
+
var lift = (step) => Effect5.tryPromise({
|
|
15
188
|
catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
|
|
16
189
|
try: step
|
|
17
190
|
});
|
|
@@ -21,17 +194,6 @@ var resolvedString = (news, key) => {
|
|
|
21
194
|
const value = news[key];
|
|
22
195
|
return typeof value === "string" ? value : undefined;
|
|
23
196
|
};
|
|
24
|
-
var adoptEnabled = (fqn) => Effect.gen(function* () {
|
|
25
|
-
const stack = yield* Effect.serviceOption(Stack);
|
|
26
|
-
const scoped = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : undefined;
|
|
27
|
-
if (scoped !== undefined)
|
|
28
|
-
return scoped;
|
|
29
|
-
const policy = yield* Effect.serviceOption(AdoptPolicy);
|
|
30
|
-
if (Option.isSome(policy))
|
|
31
|
-
return policy.value;
|
|
32
|
-
const context = yield* Effect.serviceOption(AlchemyContext);
|
|
33
|
-
return Option.isSome(context) ? context.value.adopt : false;
|
|
34
|
-
});
|
|
35
197
|
|
|
36
198
|
// src/launchd/secret-tripwire.ts
|
|
37
199
|
var ALTERNATIVE = "props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) " + "write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).";
|
|
@@ -132,7 +294,7 @@ var escapeText = (text, path) => {
|
|
|
132
294
|
}
|
|
133
295
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\r", " ");
|
|
134
296
|
};
|
|
135
|
-
var
|
|
297
|
+
var isPlainObject2 = (value) => {
|
|
136
298
|
const proto = Object.getPrototypeOf(value);
|
|
137
299
|
return proto === Object.prototype || proto === null;
|
|
138
300
|
};
|
|
@@ -167,7 +329,7 @@ var renderValue = (value, path, depth, out) => {
|
|
|
167
329
|
out.push(`${pad}</array>`);
|
|
168
330
|
return;
|
|
169
331
|
}
|
|
170
|
-
if (typeof value === "object" && value !== null &&
|
|
332
|
+
if (typeof value === "object" && value !== null && isPlainObject2(value)) {
|
|
171
333
|
renderDict(value, path, depth, out);
|
|
172
334
|
return;
|
|
173
335
|
}
|
|
@@ -190,7 +352,7 @@ var renderDict = (dict, path, depth, out) => {
|
|
|
190
352
|
out.push(`${pad}</dict>`);
|
|
191
353
|
};
|
|
192
354
|
var renderPlist = (dict) => {
|
|
193
|
-
if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !
|
|
355
|
+
if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !isPlainObject2(dict)) {
|
|
194
356
|
throw new PlistError("", "the top level of a launchd plist must be a dict");
|
|
195
357
|
}
|
|
196
358
|
const out = [];
|
|
@@ -346,17 +508,26 @@ var readFileAttributes = async (runner, path) => {
|
|
|
346
508
|
uid: stat.uid
|
|
347
509
|
};
|
|
348
510
|
};
|
|
511
|
+
var writeOptionsOf = (want) => ({
|
|
512
|
+
mode: want.mode,
|
|
513
|
+
...want.uid === undefined ? {} : { uid: want.uid },
|
|
514
|
+
...want.gid === undefined ? {} : { gid: want.gid }
|
|
515
|
+
});
|
|
349
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);
|
|
350
517
|
var diffFile = async (runner, news, output) => {
|
|
351
|
-
if (news.path !== output.path)
|
|
352
|
-
return { action: "replace" };
|
|
353
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" });
|
|
354
525
|
if (want.sha256 !== output.sha256)
|
|
355
|
-
return { action: "update" };
|
|
526
|
+
return writes({ action: "update" });
|
|
356
527
|
const live = await readFileAttributes(runner, news.path);
|
|
357
|
-
return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
|
|
528
|
+
return live !== undefined && matches(live, want) ? { action: "noop" } : writes({ action: "update" });
|
|
358
529
|
};
|
|
359
|
-
var reconcileFile = async (runner, props, output) => {
|
|
530
|
+
var reconcileFile = async (runner, props, output, adopt = false) => {
|
|
360
531
|
const want = await desiredFile(runner, props);
|
|
361
532
|
if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
|
|
362
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.");
|
|
@@ -368,15 +539,12 @@ var reconcileFile = async (runner, props, output) => {
|
|
|
368
539
|
const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
|
|
369
540
|
const moved = output !== undefined && output.path !== props.path;
|
|
370
541
|
const prior = moved ? undefined : output;
|
|
371
|
-
|
|
542
|
+
const takeOver = adopt && output === undefined;
|
|
543
|
+
if (prior === undefined && before !== undefined && !matches(before, want) && !takeOver) {
|
|
372
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.");
|
|
373
545
|
}
|
|
374
546
|
if (before === undefined || !matches(before, want)) {
|
|
375
|
-
await runner.writeFileAtomic(props.path, want.bytes,
|
|
376
|
-
mode: want.mode,
|
|
377
|
-
...want.uid === undefined ? {} : { uid: want.uid },
|
|
378
|
-
...want.gid === undefined ? {} : { gid: want.gid }
|
|
379
|
-
});
|
|
547
|
+
await runner.writeFileAtomic(props.path, want.bytes, writeOptionsOf(want));
|
|
380
548
|
}
|
|
381
549
|
const after = await readFileAttributes(runner, props.path);
|
|
382
550
|
if (after === undefined || !matches(after, want)) {
|
|
@@ -401,34 +569,34 @@ var deleteFile = async (runner, output) => {
|
|
|
401
569
|
|
|
402
570
|
// src/launchd/host-file.ts
|
|
403
571
|
var HostFile = Resource("Host.File");
|
|
404
|
-
var HostFileProvider = () => Provider.effect(HostFile,
|
|
572
|
+
var HostFileProvider = () => Provider.effect(HostFile, Effect6.gen(function* () {
|
|
405
573
|
const runner = yield* HostRunnerService;
|
|
406
574
|
return HostFile.Provider.of({
|
|
407
|
-
list: () =>
|
|
575
|
+
list: () => Effect6.succeed([]),
|
|
408
576
|
read: ({ olds, output }) => lift(async () => {
|
|
409
577
|
const found = await readFileAttributes(runner, olds.path);
|
|
410
578
|
if (found === undefined)
|
|
411
579
|
return;
|
|
412
|
-
return output === undefined ?
|
|
580
|
+
return output === undefined ? Unowned2(found) : found;
|
|
413
581
|
}),
|
|
414
|
-
diff: ({ news, output }) => {
|
|
582
|
+
diff: ({ instanceId, news, output }) => {
|
|
415
583
|
if (output === undefined)
|
|
416
|
-
return
|
|
584
|
+
return noteUnfinished(instanceId);
|
|
417
585
|
if (isResolved(news))
|
|
418
586
|
return lift(() => diffFile(runner, news, output));
|
|
419
587
|
const path = resolvedString(news, "path");
|
|
420
|
-
return
|
|
588
|
+
return Effect6.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
|
|
421
589
|
},
|
|
422
|
-
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))),
|
|
423
591
|
delete: ({ output }) => lift(() => deleteFile(runner, output))
|
|
424
592
|
});
|
|
425
593
|
}));
|
|
426
594
|
// src/launchd/job.ts
|
|
427
595
|
import { Resource as Resource2 } from "alchemy";
|
|
428
|
-
import { Unowned as
|
|
596
|
+
import { Unowned as Unowned3 } from "alchemy/AdoptPolicy";
|
|
429
597
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
430
598
|
import * as Provider2 from "alchemy/Provider";
|
|
431
|
-
import * as
|
|
599
|
+
import * as Effect7 from "effect/Effect";
|
|
432
600
|
|
|
433
601
|
// src/launchd/job-validate.ts
|
|
434
602
|
var LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
|
|
@@ -623,6 +791,8 @@ var bootoutIfLoaded = async (runner, target) => {
|
|
|
623
791
|
};
|
|
624
792
|
|
|
625
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));
|
|
626
796
|
var refuse2 = (label, message) => new Error(`Launchd.Job ${label}: ${message}`);
|
|
627
797
|
var assertValid = (props) => {
|
|
628
798
|
const found = jobProblems(props);
|
|
@@ -679,6 +849,7 @@ var assertReplaceable = async (runner, next, full, old, renderedSha256) => {
|
|
|
679
849
|
assertMayWrite(runner, old.label, oldDomain);
|
|
680
850
|
const { location, status } = await preflight(runner, next);
|
|
681
851
|
await assertUnclaimed(runner, next.label, location, status, renderedSha256);
|
|
852
|
+
await checkPlistWrite(runner, location);
|
|
682
853
|
};
|
|
683
854
|
|
|
684
855
|
// src/launchd/job-lifecycle.ts
|
|
@@ -714,20 +885,24 @@ var diffJob = async (runner, news, output) => {
|
|
|
714
885
|
}
|
|
715
886
|
assertValid(news);
|
|
716
887
|
const desired = renderJob(news).sha256;
|
|
717
|
-
|
|
888
|
+
const update = async () => {
|
|
889
|
+
await checkPlistWrite(runner, await locate(runner, news));
|
|
718
890
|
return { action: "update" };
|
|
891
|
+
};
|
|
892
|
+
if (desired !== output.plistSha256)
|
|
893
|
+
return update();
|
|
719
894
|
const live = await readJob(runner, news);
|
|
720
895
|
if (live === undefined || !live.loaded || live.plistSha256 !== desired)
|
|
721
|
-
return
|
|
896
|
+
return update();
|
|
722
897
|
return { action: "noop" };
|
|
723
898
|
};
|
|
724
|
-
var reconcileJob = async (runner, props, output) => {
|
|
899
|
+
var reconcileJob = async (runner, props, output, adopt = false) => {
|
|
725
900
|
assertValid(props);
|
|
726
901
|
const { location, status } = await preflight(runner, props);
|
|
727
902
|
const rendered = renderJob(props);
|
|
728
903
|
const renamed = output !== undefined && (output.label !== props.label || output.domain !== props.domain);
|
|
729
904
|
const prior = renamed ? undefined : output;
|
|
730
|
-
if (prior === undefined) {
|
|
905
|
+
if (prior === undefined && !(adopt && output === undefined)) {
|
|
731
906
|
await assertUnclaimed(runner, props.label, location, status, rendered.sha256);
|
|
732
907
|
}
|
|
733
908
|
if (renamed)
|
|
@@ -736,7 +911,7 @@ var reconcileJob = async (runner, props, output) => {
|
|
|
736
911
|
if (status.loaded && before !== undefined && sha256Hex(before) === rendered.sha256 && prior?.plistSha256 === rendered.sha256) {
|
|
737
912
|
return attributesOf(props, location, rendered.sha256, status);
|
|
738
913
|
}
|
|
739
|
-
await runner.writeFileAtomic(location.plistPath, rendered.bytes, location.domain
|
|
914
|
+
await runner.writeFileAtomic(location.plistPath, rendered.bytes, plistWriteOptions(location.domain));
|
|
740
915
|
await bootoutIfLoaded(runner, location.target);
|
|
741
916
|
const failed = async (detail) => {
|
|
742
917
|
const removed = before === undefined && await runner.removeFile(location.plistPath).then(() => true, () => false);
|
|
@@ -763,26 +938,26 @@ var deleteJob = async (runner, output) => {
|
|
|
763
938
|
|
|
764
939
|
// src/launchd/job.ts
|
|
765
940
|
var LaunchdJob = Resource2("Launchd.Job");
|
|
766
|
-
var LaunchdJobProvider = () => Provider2.effect(LaunchdJob,
|
|
941
|
+
var LaunchdJobProvider = () => Provider2.effect(LaunchdJob, Effect7.gen(function* () {
|
|
767
942
|
const runner = yield* HostRunnerService;
|
|
768
943
|
return LaunchdJob.Provider.of({
|
|
769
|
-
list: () =>
|
|
944
|
+
list: () => Effect7.succeed([]),
|
|
770
945
|
read: ({ olds, output }) => lift(async () => {
|
|
771
946
|
const found = await readJob(runner, olds);
|
|
772
947
|
if (found === undefined)
|
|
773
948
|
return;
|
|
774
|
-
return output === undefined ?
|
|
949
|
+
return output === undefined ? Unowned3(found) : found;
|
|
775
950
|
}),
|
|
776
|
-
diff: ({ news, output }) => {
|
|
951
|
+
diff: ({ instanceId, news, output }) => {
|
|
777
952
|
if (output === undefined)
|
|
778
|
-
return
|
|
953
|
+
return noteUnfinished(instanceId);
|
|
779
954
|
if (isResolved2(news))
|
|
780
955
|
return lift(() => diffJob(runner, news, output));
|
|
781
956
|
const label = resolvedString(news, "label");
|
|
782
957
|
const domain = resolvedString(news, "domain");
|
|
783
|
-
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);
|
|
784
959
|
},
|
|
785
|
-
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))),
|
|
786
961
|
delete: ({ output }) => lift(() => deleteJob(runner, output))
|
|
787
962
|
});
|
|
788
963
|
}));
|
|
@@ -854,7 +1029,7 @@ var parseGroup = (platform, stdout) => {
|
|
|
854
1029
|
|
|
855
1030
|
// src/launchd/local-runner.ts
|
|
856
1031
|
var errnoCode = (cause) => typeof cause === "object" && cause !== null && ("code" in cause) ? String(cause.code) : undefined;
|
|
857
|
-
var
|
|
1032
|
+
var absent2 = (cause) => errnoCode(cause) === "ENOENT" || errnoCode(cause) === "ENOTDIR";
|
|
858
1033
|
var run = (argv, timeoutMs) => new Promise((resolve, reject) => {
|
|
859
1034
|
const [program, ...args] = argv;
|
|
860
1035
|
if (program === undefined) {
|
|
@@ -933,7 +1108,7 @@ var localRunner = (options = {}) => {
|
|
|
933
1108
|
try {
|
|
934
1109
|
return new Uint8Array(await readFile(path));
|
|
935
1110
|
} catch (cause) {
|
|
936
|
-
if (
|
|
1111
|
+
if (absent2(cause))
|
|
937
1112
|
return;
|
|
938
1113
|
throw cause;
|
|
939
1114
|
}
|
|
@@ -942,7 +1117,7 @@ var localRunner = (options = {}) => {
|
|
|
942
1117
|
try {
|
|
943
1118
|
await unlink(path);
|
|
944
1119
|
} catch (cause) {
|
|
945
|
-
if (!
|
|
1120
|
+
if (!absent2(cause))
|
|
946
1121
|
throw cause;
|
|
947
1122
|
}
|
|
948
1123
|
},
|
|
@@ -958,7 +1133,7 @@ var localRunner = (options = {}) => {
|
|
|
958
1133
|
uid: stats.uid
|
|
959
1134
|
};
|
|
960
1135
|
} catch (cause) {
|
|
961
|
-
if (
|
|
1136
|
+
if (absent2(cause))
|
|
962
1137
|
return;
|
|
963
1138
|
throw cause;
|
|
964
1139
|
}
|
|
@@ -1084,13 +1259,46 @@ var routeExec = (argv, operatorUid) => {
|
|
|
1084
1259
|
};
|
|
1085
1260
|
};
|
|
1086
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
|
+
|
|
1087
1291
|
// src/launchd/sudo-guard.ts
|
|
1088
1292
|
var refuse3 = (path, message) => new SudoRefusedError(`sudoRunner ${path}: ${message}`);
|
|
1089
1293
|
var betweenDirs = (prefix, path) => {
|
|
1090
1294
|
const parts = path.slice(prefix.length + 1).split("/").slice(0, -1);
|
|
1091
1295
|
return parts.map((_, index) => `${prefix}/${parts.slice(0, index + 1).join("/")}`);
|
|
1092
1296
|
};
|
|
1093
|
-
var
|
|
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) => {
|
|
1094
1302
|
if (stat === undefined)
|
|
1095
1303
|
return "is missing";
|
|
1096
1304
|
if (stat.kind !== "directory")
|
|
@@ -1103,17 +1311,30 @@ var prefixProblem = (stat) => {
|
|
|
1103
1311
|
return;
|
|
1104
1312
|
};
|
|
1105
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
|
+
}
|
|
1106
1321
|
const dirs = betweenDirs(prefix, path);
|
|
1107
1322
|
const [prefixStat, ...stats] = await Promise.all([prefix, ...dirs].map((dir) => base.stat(dir)));
|
|
1108
|
-
const bad =
|
|
1323
|
+
const bad = rootOnlyProblem(prefixStat);
|
|
1109
1324
|
if (bad !== undefined) {
|
|
1110
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.");
|
|
1111
1326
|
}
|
|
1112
1327
|
for (const [index, stat] of stats.entries()) {
|
|
1328
|
+
const dir = String(dirs[index]);
|
|
1113
1329
|
if (stat?.kind !== "directory") {
|
|
1114
|
-
throw refuse3(path, `${
|
|
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.");
|
|
1115
1335
|
}
|
|
1116
1336
|
}
|
|
1337
|
+
await assertNoAclGrants(base, path, [...above, prefix, ...dirs]);
|
|
1117
1338
|
const target = await base.stat(path);
|
|
1118
1339
|
if (target === undefined && expect === "file-or-absent")
|
|
1119
1340
|
return "absent";
|
|
@@ -1121,6 +1342,26 @@ var assertPlainPath = async (base, prefix, path, expect) => {
|
|
|
1121
1342
|
return "file";
|
|
1122
1343
|
throw refuse3(path, `is ${target === undefined ? "missing" : `a ${target.kind}`}, not a file`);
|
|
1123
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
|
+
};
|
|
1124
1365
|
var canRead = (file, uid, groups) => {
|
|
1125
1366
|
if (uid === 0)
|
|
1126
1367
|
return true;
|
|
@@ -1149,6 +1390,27 @@ var operatorGroups = async (base) => {
|
|
|
1149
1390
|
return result.exitCode === 0 && ids.every((id) => /^\d+$/.test(id)) ? ids.map(Number) : undefined;
|
|
1150
1391
|
};
|
|
1151
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
|
+
|
|
1152
1414
|
// src/launchd/sudo-stage.ts
|
|
1153
1415
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1154
1416
|
import { tmpdir } from "node:os";
|
|
@@ -1167,14 +1429,6 @@ var stageFile = async (bytes) => {
|
|
|
1167
1429
|
};
|
|
1168
1430
|
|
|
1169
1431
|
// src/launchd/sudo-runner.ts
|
|
1170
|
-
var PASSWORD = /^sudo: (?:a password is required|a terminal is required|sorry, you must have a tty)/m;
|
|
1171
|
-
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;
|
|
1172
|
-
var undeclared = (path, prefixes) => (cause) => {
|
|
1173
|
-
const code = errnoCode(cause);
|
|
1174
|
-
if (code !== "EACCES" && code !== "EPERM")
|
|
1175
|
-
throw cause;
|
|
1176
|
-
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 });
|
|
1177
|
-
};
|
|
1178
1432
|
var makeSudoRunner = (prefixList, deps) => {
|
|
1179
1433
|
const prefixes = checkPrefixes(prefixList);
|
|
1180
1434
|
const { base } = deps;
|
|
@@ -1189,16 +1443,26 @@ var makeSudoRunner = (prefixList, deps) => {
|
|
|
1189
1443
|
await before?.();
|
|
1190
1444
|
deps.log(`homeflare/launchd sudo -n ${shown}`);
|
|
1191
1445
|
const result = await base.exec([SUDO, "-n", "--", ...argv]);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
throw 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.");
|
|
1196
|
-
}
|
|
1197
|
-
if (NOT_ALLOWED.test(result.stderr)) {
|
|
1198
|
-
throw new SudoRefusedError(`sudo -n ${shown}: sudoers does not let the deploying user run this. The command did not run.`);
|
|
1199
|
-
}
|
|
1446
|
+
const refused = sudoRefusal(shown, result);
|
|
1447
|
+
if (refused !== undefined)
|
|
1448
|
+
throw refused;
|
|
1200
1449
|
return result;
|
|
1201
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
|
+
};
|
|
1202
1466
|
const mustSucceed = async (argv, staged) => {
|
|
1203
1467
|
const result = await elevate(argv, staged === undefined ? {} : { staged });
|
|
1204
1468
|
if (result.exitCode !== 0) {
|
|
@@ -1207,6 +1471,9 @@ var makeSudoRunner = (prefixList, deps) => {
|
|
|
1207
1471
|
}
|
|
1208
1472
|
};
|
|
1209
1473
|
return {
|
|
1474
|
+
checkWrite: async (path, options) => {
|
|
1475
|
+
await vetWrite(path, options);
|
|
1476
|
+
},
|
|
1210
1477
|
effectiveUid: () => base.effectiveUid(),
|
|
1211
1478
|
exec: async (argv) => {
|
|
1212
1479
|
const route = routeExec(argv, base.effectiveUid());
|
|
@@ -1240,19 +1507,11 @@ var makeSudoRunner = (prefixList, deps) => {
|
|
|
1240
1507
|
sleep: (ms) => base.sleep(ms),
|
|
1241
1508
|
stat: (path) => base.stat(path),
|
|
1242
1509
|
writeFileAtomic: async (path, bytes, options) => {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
if (prefix === undefined) {
|
|
1246
|
-
if (options.uid !== undefined && options.uid !== operator && operator !== 0) {
|
|
1247
|
-
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.");
|
|
1248
|
-
}
|
|
1510
|
+
const vetted = await vetWrite(path, options);
|
|
1511
|
+
if (vetted === undefined) {
|
|
1249
1512
|
return base.writeFileAtomic(path, bytes, options).catch(undeclared(path, prefixes));
|
|
1250
1513
|
}
|
|
1251
|
-
const mode =
|
|
1252
|
-
if (mode === undefined)
|
|
1253
|
-
throw new SudoRefusedError(`sudoRunner ${path}: mode must be 0–0o7777`);
|
|
1254
|
-
await assertPlainPath(base, prefix, path, "file-or-absent");
|
|
1255
|
-
await assertReadableBack(base, path, options, await deps.groups());
|
|
1514
|
+
const { mode } = vetted;
|
|
1256
1515
|
const staged = await deps.stage(bytes);
|
|
1257
1516
|
try {
|
|
1258
1517
|
await mustSucceed([
|
|
@@ -1299,5 +1558,5 @@ export {
|
|
|
1299
1558
|
sudoRunner
|
|
1300
1559
|
};
|
|
1301
1560
|
|
|
1302
|
-
//# debugId=
|
|
1561
|
+
//# debugId=59F08ED5EC139A6064756E2164756E21
|
|
1303
1562
|
//# sourceMappingURL=index.js.map
|