@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/src/openbao/jwt-role.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ★ REPLACE SEMANTICS (REPLACE.md): `mount` or `name` changed → `replace`, create-first. The new
|
|
16
16
|
* path cannot collide with the old, so no `deleteFirst`. Under the default `retain` the old role
|
|
17
|
-
* stays live, and still admits logins, until removed by hand.
|
|
17
|
+
* stays live, and still admits logins, until removed by hand. ⛔ A move onto a role that already
|
|
18
|
+
* exists fails the plan (rename-identity.ts).
|
|
18
19
|
* ★ `defaultRemovalPolicy: 'retain'`, like every Bao.* family.
|
|
19
20
|
*/
|
|
20
21
|
import { Resource } from 'alchemy';
|
|
@@ -22,18 +23,20 @@ import { isResolved } from 'alchemy/Diff';
|
|
|
22
23
|
import * as Provider from 'alchemy/Provider';
|
|
23
24
|
import * as Effect from 'effect/Effect';
|
|
24
25
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
26
|
+
import { claimFor } from '../ownership/adopt.ts';
|
|
27
|
+
import { provingResumes } from '../ownership/resume.ts';
|
|
25
28
|
import { baoDelete } from './bao-http.ts';
|
|
26
29
|
import {
|
|
27
30
|
type BaoJwtRoleAttributes,
|
|
28
31
|
type BaoJwtRoleProps,
|
|
29
32
|
attributesOf,
|
|
30
33
|
matches,
|
|
31
|
-
mountOf,
|
|
32
34
|
problems,
|
|
33
35
|
rolePath,
|
|
34
36
|
writeBody,
|
|
35
37
|
} from './jwt-role-form.ts';
|
|
36
|
-
import {
|
|
38
|
+
import { foldName, guardRename, judgeRename, roleIdentity } from './rename-identity.ts';
|
|
39
|
+
import { type RoleSpec, planRole, readOwnedRole, reconcileRole } from './role-reconcile.ts';
|
|
37
40
|
|
|
38
41
|
export type {
|
|
39
42
|
BaoJwtCallbackMode,
|
|
@@ -54,6 +57,18 @@ export const BaoJwtRole = Resource<BaoJwtRole>('Bao.JwtRole', {
|
|
|
54
57
|
defaultRemovalPolicy: 'retain',
|
|
55
58
|
});
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The name folded, as the server keys it (`foldName`): `name` is a TypeLowerCaseString
|
|
62
|
+
* (builtin/credential/jwt/path_role.go:77), so the handler reads it lowercased before it ever builds
|
|
63
|
+
* `role/<name>` (:289). `App` → `app` is the same role, never a move; `problems` refuses the case.
|
|
64
|
+
*/
|
|
65
|
+
const IDENTITY = roleIdentity<BaoJwtRoleAttributes>(
|
|
66
|
+
'Bao.JwtRole',
|
|
67
|
+
(mount, name) => rolePath({ mount, name }),
|
|
68
|
+
'jwt',
|
|
69
|
+
foldName,
|
|
70
|
+
);
|
|
71
|
+
|
|
57
72
|
export const jwtRoleSpec = (props: BaoJwtRoleProps): RoleSpec<BaoJwtRoleAttributes> => ({
|
|
58
73
|
attributesOf: (live) => attributesOf(props, live),
|
|
59
74
|
body: writeBody(props),
|
|
@@ -74,22 +89,26 @@ export const BaoJwtRoleProvider = () =>
|
|
|
74
89
|
*/
|
|
75
90
|
list: () => Effect.succeed([]),
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
92
|
+
/** ⛔ Stateless: `Unowned` unless our own interrupted create made it (ownership/probe.ts). */
|
|
93
|
+
read: Effect.fn(function* ({ fqn, instanceId, olds, output }) {
|
|
94
|
+
return yield* readOwnedRole({ fqn, instanceId, output }, jwtRoleSpec(olds));
|
|
80
95
|
}),
|
|
81
96
|
|
|
82
97
|
/** ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST — a hand-widened audience is drift. */
|
|
83
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
99
|
+
// ⛔ The identity first, before isResolved; onto a role that exists fails the plan.
|
|
100
|
+
const move = yield* judgeRename(IDENTITY, olds, news, output);
|
|
101
|
+
// ★ No attributes: an unfinished generation, proven ours or not by provingResumes.
|
|
102
|
+
if (output === undefined) return undefined;
|
|
103
|
+
if (move !== undefined) return { action: 'replace' } as const;
|
|
104
|
+
if (!isResolved(news)) return undefined;
|
|
88
105
|
return { action: yield* planRole(jwtRoleSpec(news)) } as const;
|
|
89
106
|
}),
|
|
90
107
|
|
|
91
|
-
reconcile: Effect.fn(function* ({ news }) {
|
|
92
|
-
|
|
108
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
109
|
+
// ⛔ An `update` across a move the diff could not see — refused before any write.
|
|
110
|
+
yield* guardRename(IDENTITY, news, output);
|
|
111
|
+
return yield* reconcileRole(jwtRoleSpec(news), claimFor({ fqn, instanceId, output }));
|
|
93
112
|
}),
|
|
94
113
|
|
|
95
114
|
/** Idempotent as Alchemy requires — a missing role deletes as success. */
|
|
@@ -98,5 +117,5 @@ export const BaoJwtRoleProvider = () =>
|
|
|
98
117
|
return undefined;
|
|
99
118
|
}),
|
|
100
119
|
}),
|
|
101
|
-
),
|
|
120
|
+
).pipe(Effect.map(provingResumes)),
|
|
102
121
|
);
|
|
@@ -9,25 +9,28 @@
|
|
|
9
9
|
* and nothing else (path_role.go:239-256).
|
|
10
10
|
*
|
|
11
11
|
* ★ REPLACE SEMANTICS (REPLACE.md): `mount` or `name` changed → `replace`, create-first (the new
|
|
12
|
-
* path cannot collide). Under the default `retain` the old role stays live until
|
|
12
|
+
* path cannot collide with the old). Under the default `retain` the old role stays live until
|
|
13
|
+
* removed by hand. ⛔ A move onto a role that already exists fails the plan (rename-identity.ts).
|
|
13
14
|
*/
|
|
14
15
|
import { Resource } from 'alchemy';
|
|
15
16
|
import { isResolved } from 'alchemy/Diff';
|
|
16
17
|
import * as Provider from 'alchemy/Provider';
|
|
17
18
|
import * as Effect from 'effect/Effect';
|
|
18
19
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
20
|
+
import { claimFor } from '../ownership/adopt.ts';
|
|
21
|
+
import { provingResumes } from '../ownership/resume.ts';
|
|
19
22
|
import { baoDelete } from './bao-http.ts';
|
|
20
23
|
import {
|
|
21
24
|
type BaoKubernetesRoleAttributes,
|
|
22
25
|
type BaoKubernetesRoleProps,
|
|
23
26
|
attributesOf,
|
|
24
27
|
matches,
|
|
25
|
-
mountOf,
|
|
26
28
|
problems,
|
|
27
29
|
rolePath,
|
|
28
30
|
writeBody,
|
|
29
31
|
} from './kubernetes-role-form.ts';
|
|
30
|
-
import {
|
|
32
|
+
import { foldName, guardRename, judgeRename, roleIdentity } from './rename-identity.ts';
|
|
33
|
+
import { type RoleSpec, planRole, readOwnedRole, reconcileRole } from './role-reconcile.ts';
|
|
31
34
|
|
|
32
35
|
export type {
|
|
33
36
|
BaoKubernetesAliasSource,
|
|
@@ -47,6 +50,14 @@ export const BaoKubernetesRole = Resource<BaoKubernetesRole>('Bao.KubernetesRole
|
|
|
47
50
|
defaultRemovalPolicy: 'retain',
|
|
48
51
|
});
|
|
49
52
|
|
|
53
|
+
/** The name folded, as the server stores it (`foldName`). */
|
|
54
|
+
const IDENTITY = roleIdentity<BaoKubernetesRoleAttributes>(
|
|
55
|
+
'Bao.KubernetesRole',
|
|
56
|
+
(mount, name) => rolePath({ mount, name }),
|
|
57
|
+
'kubernetes',
|
|
58
|
+
foldName,
|
|
59
|
+
);
|
|
60
|
+
|
|
50
61
|
export const kubernetesRoleSpec = (
|
|
51
62
|
props: BaoKubernetesRoleProps,
|
|
52
63
|
): RoleSpec<BaoKubernetesRoleAttributes> => ({
|
|
@@ -66,22 +77,29 @@ export const BaoKubernetesRoleProvider = () =>
|
|
|
66
77
|
/** ⛔ The mount's role listing is not a list of things this owns. */
|
|
67
78
|
list: () => Effect.succeed([]),
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
80
|
+
/** ⛔ Stateless: `Unowned` unless our own interrupted create made it (ownership/probe.ts). */
|
|
81
|
+
read: Effect.fn(function* ({ fqn, instanceId, olds, output }) {
|
|
82
|
+
return yield* readOwnedRole({ fqn, instanceId, output }, kubernetesRoleSpec(olds));
|
|
72
83
|
}),
|
|
73
84
|
|
|
74
85
|
/** ⛔ IT COMPARES THE LIVE ROLE, NOT THE STORED DIGEST. */
|
|
75
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
87
|
+
// ⛔ The identity first, before isResolved; onto a role that exists fails the plan.
|
|
88
|
+
const move = yield* judgeRename(IDENTITY, olds, news, output);
|
|
89
|
+
// ★ No attributes: an unfinished generation, proven ours or not by provingResumes.
|
|
90
|
+
if (output === undefined) return undefined;
|
|
91
|
+
if (move !== undefined) return { action: 'replace' } as const;
|
|
92
|
+
if (!isResolved(news)) return undefined;
|
|
80
93
|
return { action: yield* planRole(kubernetesRoleSpec(news)) } as const;
|
|
81
94
|
}),
|
|
82
95
|
|
|
83
|
-
reconcile: Effect.fn(function* ({ news }) {
|
|
84
|
-
|
|
96
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
97
|
+
// ⛔ An `update` across a move the diff could not see — refused before any write.
|
|
98
|
+
yield* guardRename(IDENTITY, news, output);
|
|
99
|
+
return yield* reconcileRole(
|
|
100
|
+
kubernetesRoleSpec(news),
|
|
101
|
+
claimFor({ fqn, instanceId, output }),
|
|
102
|
+
);
|
|
85
103
|
}),
|
|
86
104
|
|
|
87
105
|
/** Idempotent as Alchemy requires — a missing role deletes as success. */
|
|
@@ -90,5 +108,5 @@ export const BaoKubernetesRoleProvider = () =>
|
|
|
90
108
|
return undefined;
|
|
91
109
|
}),
|
|
92
110
|
}),
|
|
93
|
-
),
|
|
111
|
+
).pipe(Effect.map(provingResumes)),
|
|
94
112
|
);
|
|
@@ -19,15 +19,21 @@
|
|
|
19
19
|
* default `retain` it never runs; with `destroy`, set `retain` to drop it from state and leave it
|
|
20
20
|
* live — which is what the server would do anyway.
|
|
21
21
|
*
|
|
22
|
-
* ★ REPLACE SEMANTICS (REPLACE.md): `name` changed → `replace`, create-first
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* ★ REPLACE SEMANTICS (REPLACE.md): `name` changed → `replace`, create-first. Under `retain` BOTH
|
|
23
|
+
* enforcements then apply — fail closed. Under `destroy` the old one's delete is refused, so the
|
|
24
|
+
* deploy stops after creating. ⛔ A rename onto an enforcement that exists fails the plan
|
|
25
|
+
* (rename-identity.ts): the upsert by name would otherwise rewrite, in place, what someone else's
|
|
26
|
+
* enforcement demands. MEASURED 2026-09-21: a swap of two under `destroy` wrote each over the
|
|
27
|
+
* other, then stopped on the two refused deletes.
|
|
25
28
|
*/
|
|
26
29
|
import { Resource } from 'alchemy';
|
|
27
30
|
import { isResolved } from 'alchemy/Diff';
|
|
28
31
|
import * as Provider from 'alchemy/Provider';
|
|
29
32
|
import * as Effect from 'effect/Effect';
|
|
30
33
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
34
|
+
import { type Claim, claimFor } from '../ownership/adopt.ts';
|
|
35
|
+
import { ownedRead } from '../ownership/probe.ts';
|
|
36
|
+
import { provingResumes } from '../ownership/resume.ts';
|
|
31
37
|
import type { BaoError } from './bao-status.ts';
|
|
32
38
|
import {
|
|
33
39
|
type BaoMfaLoginEnforcementAttributes,
|
|
@@ -38,7 +44,8 @@ import {
|
|
|
38
44
|
problems,
|
|
39
45
|
writeBody,
|
|
40
46
|
} from './mfa-enforcement-form.ts';
|
|
41
|
-
import { authAccessors, readEnforcement, writeEnforcement } from './mfa-wire.ts';
|
|
47
|
+
import { authAccessors, enforcementPath, readEnforcement, writeEnforcement } from './mfa-wire.ts';
|
|
48
|
+
import { guardRename, judgeRename, nameIdentity } from './rename-identity.ts';
|
|
42
49
|
|
|
43
50
|
export type {
|
|
44
51
|
BaoMfaLoginEnforcementAttributes,
|
|
@@ -59,6 +66,12 @@ export const BaoMfaLoginEnforcement = Resource<BaoMfaLoginEnforcement>('Bao.MfaL
|
|
|
59
66
|
|
|
60
67
|
type Env = HttpClient.HttpClient;
|
|
61
68
|
|
|
69
|
+
/** Exact: the identity store indexes enforcements by (namespace, name) as given. */
|
|
70
|
+
const IDENTITY = nameIdentity<BaoMfaLoginEnforcementAttributes>(
|
|
71
|
+
'Bao.MfaLoginEnforcement',
|
|
72
|
+
enforcementPath,
|
|
73
|
+
);
|
|
74
|
+
|
|
62
75
|
const refuse = (name: string, message: string): Effect.Effect<never> =>
|
|
63
76
|
Effect.die(new Error(`Bao.MfaLoginEnforcement ${name}: ${message}`));
|
|
64
77
|
|
|
@@ -86,14 +99,21 @@ export const planEnforcement = (
|
|
|
86
99
|
return live !== undefined && matches(live, props, resolved) ? 'noop' : 'update';
|
|
87
100
|
});
|
|
88
101
|
|
|
89
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Write when live differs, then prove it by reading back.
|
|
104
|
+
* ⛔ `claim` (the provider passes it): a create never takes over an enforcement it finds by name.
|
|
105
|
+
*/
|
|
90
106
|
export const reconcileEnforcement = (
|
|
91
107
|
props: BaoMfaLoginEnforcementProps,
|
|
108
|
+
claim?: Claim,
|
|
92
109
|
): Effect.Effect<BaoMfaLoginEnforcementAttributes, BaoError, Env> =>
|
|
93
110
|
Effect.gen(function* () {
|
|
94
111
|
const { bad, resolved } = yield* resolveTargets(props);
|
|
95
112
|
if (bad.length > 0) return yield* refuse(props.name, bad.join('; '));
|
|
96
113
|
const live = yield* readLive(props.name);
|
|
114
|
+
if (live !== undefined && claim !== undefined) {
|
|
115
|
+
yield* claim(`Bao.MfaLoginEnforcement ${props.name}`);
|
|
116
|
+
}
|
|
97
117
|
if (live === undefined || !matches(live, props, resolved)) {
|
|
98
118
|
yield* writeEnforcement(props.name, writeBody(canonicalFromProps(props, resolved)));
|
|
99
119
|
}
|
|
@@ -114,19 +134,28 @@ export const BaoMfaLoginEnforcementProvider = () =>
|
|
|
114
134
|
/** ⛔ The namespace's enforcement listing is not a list of things this owns. */
|
|
115
135
|
list: () => Effect.succeed([]),
|
|
116
136
|
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
/** ⛔ Stateless: `Unowned` unless our own interrupted create made it (ownership/probe.ts). */
|
|
138
|
+
read: Effect.fn(function* ({ fqn, instanceId, olds, output }) {
|
|
139
|
+
const found = yield* readLive(olds.name);
|
|
140
|
+
const ours = Effect.map(planEnforcement(olds), (plan) => plan === 'noop');
|
|
141
|
+
return yield* ownedRead({ fqn, instanceId, output }, found, ours);
|
|
119
142
|
}),
|
|
120
143
|
|
|
121
144
|
/** ⛔ IT COMPARES THE LIVE ENFORCEMENT — a target removed by hand is drift, not a noop. */
|
|
122
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
146
|
+
// ⛔ The name first, before isResolved; onto an enforcement that exists fails the plan.
|
|
147
|
+
const move = yield* judgeRename(IDENTITY, olds, news, output);
|
|
148
|
+
// ★ No attributes: an unfinished generation, proven ours or not by provingResumes.
|
|
149
|
+
if (output === undefined) return undefined;
|
|
150
|
+
if (move !== undefined) return { action: 'replace' } as const;
|
|
151
|
+
if (!isResolved(news)) return undefined;
|
|
125
152
|
return { action: yield* planEnforcement(news) } as const;
|
|
126
153
|
}),
|
|
127
154
|
|
|
128
|
-
reconcile: Effect.fn(function* ({ news }) {
|
|
129
|
-
|
|
155
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
156
|
+
// ⛔ An `update` across a rename the diff could not see — refused before any write.
|
|
157
|
+
yield* guardRename(IDENTITY, news, output);
|
|
158
|
+
return yield* reconcileEnforcement(news, claimFor({ fqn, instanceId, output }));
|
|
130
159
|
}),
|
|
131
160
|
|
|
132
161
|
/** ⛔ REFUSED — read the header: in 2.6.2 the delete does not survive a restart. */
|
|
@@ -139,5 +168,5 @@ export const BaoMfaLoginEnforcementProvider = () =>
|
|
|
139
168
|
);
|
|
140
169
|
}),
|
|
141
170
|
}),
|
|
142
|
-
),
|
|
171
|
+
).pipe(Effect.map(provingResumes)),
|
|
143
172
|
);
|
package/src/openbao/mfa-totp.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { isResolved } from 'alchemy/Diff';
|
|
|
20
20
|
import * as Provider from 'alchemy/Provider';
|
|
21
21
|
import * as Effect from 'effect/Effect';
|
|
22
22
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
23
|
+
import { type Claim, claimFor } from '../ownership/adopt.ts';
|
|
24
|
+
import { ownedRead } from '../ownership/probe.ts';
|
|
25
|
+
import { provingResumes } from '../ownership/resume.ts';
|
|
23
26
|
import type { BaoError } from './bao-status.ts';
|
|
24
27
|
import {
|
|
25
28
|
type BaoMfaTotpMethodAttributes,
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
writeBody,
|
|
32
35
|
} from './mfa-totp-form.ts';
|
|
33
36
|
import { deleteTotp, findTotp, writeTotp } from './mfa-wire.ts';
|
|
37
|
+
import { declaredString } from './rename.ts';
|
|
34
38
|
|
|
35
39
|
export type {
|
|
36
40
|
BaoMfaTotpMethodAttributes,
|
|
@@ -50,8 +54,8 @@ export const BaoMfaTotpMethod = Resource<BaoMfaTotpMethod>('Bao.MfaTotpMethod',
|
|
|
50
54
|
defaultRemovalPolicy: 'retain',
|
|
51
55
|
});
|
|
52
56
|
|
|
53
|
-
const refuse = (
|
|
54
|
-
Effect.die(new Error(`Bao.MfaTotpMethod ${
|
|
57
|
+
const refuse = (name: string, message: string): Effect.Effect<never> =>
|
|
58
|
+
Effect.die(new Error(`Bao.MfaTotpMethod ${name}: ${message}`));
|
|
55
59
|
|
|
56
60
|
export const readTotp = (
|
|
57
61
|
props: BaoMfaTotpMethodProps,
|
|
@@ -69,19 +73,24 @@ export const planTotp = (
|
|
|
69
73
|
return live !== undefined && matches(live, props) ? 'noop' : 'update';
|
|
70
74
|
});
|
|
71
75
|
|
|
72
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Upsert by name when live differs, then prove it by reading back.
|
|
78
|
+
* ⛔ `claim` (the provider passes it): a create never takes over a method it finds by that name.
|
|
79
|
+
*/
|
|
73
80
|
export const reconcileTotp = (
|
|
74
81
|
props: BaoMfaTotpMethodProps,
|
|
82
|
+
claim?: Claim,
|
|
75
83
|
): Effect.Effect<BaoMfaTotpMethodAttributes, BaoError, HttpClient.HttpClient> =>
|
|
76
84
|
Effect.gen(function* () {
|
|
77
85
|
const bad = problems(props);
|
|
78
|
-
if (bad.length > 0) return yield* refuse(props, bad.join('; '));
|
|
86
|
+
if (bad.length > 0) return yield* refuse(props.name, bad.join('; '));
|
|
79
87
|
const live = yield* readTotp(props);
|
|
88
|
+
if (live !== undefined && claim !== undefined) yield* claim(`Bao.MfaTotpMethod ${props.name}`);
|
|
80
89
|
if (live === undefined || !matches(live, props)) yield* writeTotp(writeBody(props));
|
|
81
90
|
const after = yield* readTotp(props);
|
|
82
91
|
if (after === undefined)
|
|
83
|
-
return yield* refuse(props, 'the write succeeded but no method has that name.');
|
|
84
|
-
if (!matches(after, props)) return yield* refuse(props, 'the method read back different.');
|
|
92
|
+
return yield* refuse(props.name, 'the write succeeded but no method has that name.');
|
|
93
|
+
if (!matches(after, props)) return yield* refuse(props.name, 'the method read back different.');
|
|
85
94
|
return after;
|
|
86
95
|
});
|
|
87
96
|
|
|
@@ -93,19 +102,38 @@ export const BaoMfaTotpMethodProvider = () =>
|
|
|
93
102
|
/** ⛔ The namespace's method listing is not a list of things this owns. */
|
|
94
103
|
list: () => Effect.succeed([]),
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
/** ⛔ Stateless: `Unowned` unless our own interrupted create made it (ownership/probe.ts). */
|
|
106
|
+
read: Effect.fn(function* ({ fqn, instanceId, olds, output }) {
|
|
107
|
+
const found = yield* readTotp(olds);
|
|
108
|
+
const ours = Effect.sync(
|
|
109
|
+
() => found !== undefined && problems(olds).length === 0 && matches(found, olds),
|
|
110
|
+
);
|
|
111
|
+
return yield* ownedRead({ fqn, instanceId, output }, found, ours);
|
|
98
112
|
}),
|
|
99
113
|
|
|
114
|
+
/**
|
|
115
|
+
* ⛔ THE NAME IS JUDGED BEFORE `isResolved(news)`. Until 2026-09-21 it came after, so a
|
|
116
|
+
* rename landing with a pending `issuer` (or any other Output) planned `update`, and the
|
|
117
|
+
* upsert by name wrote a SECOND method: the header's stranding, reached through a green
|
|
118
|
+
* plan. MEASURED through the engine (rename-families.test.ts).
|
|
119
|
+
*/
|
|
100
120
|
diff: Effect.fn(function* ({ news, output }) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
const name = declaredString(news, 'name');
|
|
122
|
+
if (output !== undefined && name !== undefined) {
|
|
123
|
+
const rename = renameProblem(output.name, name);
|
|
124
|
+
if (rename !== undefined) return yield* refuse(name, rename);
|
|
125
|
+
}
|
|
126
|
+
// ★ No attributes: an unfinished generation, proven ours or not by provingResumes.
|
|
127
|
+
if (output === undefined) return undefined;
|
|
128
|
+
if (!isResolved(news)) return undefined;
|
|
104
129
|
return { action: yield* planTotp(news) } as const;
|
|
105
130
|
}),
|
|
106
131
|
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
/** ⛔ A rename the diff could not see (the name was an Output), refused before any write. */
|
|
133
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
134
|
+
const rename = output === undefined ? undefined : renameProblem(output.name, news.name);
|
|
135
|
+
if (rename !== undefined) return yield* refuse(news.name, rename);
|
|
136
|
+
return yield* reconcileTotp(news, claimFor({ fqn, instanceId, output }));
|
|
109
137
|
}),
|
|
110
138
|
|
|
111
139
|
/** ⛔ Read the header first. Idempotent: an unknown id deletes as success. */
|
|
@@ -114,5 +142,5 @@ export const BaoMfaTotpMethodProvider = () =>
|
|
|
114
142
|
return undefined;
|
|
115
143
|
}),
|
|
116
144
|
}),
|
|
117
|
-
),
|
|
145
|
+
).pipe(Effect.map(provingResumes)),
|
|
118
146
|
);
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { describe, it } from 'node:test';
|
|
7
7
|
import { reconcileAuthMethod } from './auth-method-reconcile.ts';
|
|
8
|
-
import { type
|
|
8
|
+
import { type Seen, run, withFake } from './fake-bao.ts';
|
|
9
|
+
import { liveTable } from './fake-mounts.ts';
|
|
9
10
|
import { planMove } from './mount-move.ts';
|
|
10
11
|
import { reconcileMount } from './mount-reconcile.ts';
|
|
11
12
|
|
|
@@ -32,40 +33,6 @@ describe('planMove', () => {
|
|
|
32
33
|
});
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
/** A fake with a live table under `prefix` (`sys/mounts` or `sys/auth`) that sys/remount edits. */
|
|
36
|
-
const liveTable = (prefix: string, initial: Record<string, string>) => {
|
|
37
|
-
const table = new Map(Object.entries(initial).map(([path, type]) => [`${path}/`, type]));
|
|
38
|
-
const entry = (type: string) => ({ config: { default_lease_ttl: 0, max_lease_ttl: 0 }, type });
|
|
39
|
-
const answer = (seen: Seen): Reply => {
|
|
40
|
-
const path = seen.path.replace(/^\/v1\//, '');
|
|
41
|
-
if (path === 'sys/remount' && seen.method === 'POST') {
|
|
42
|
-
const { from, to } = JSON.parse(seen.body) as { from: string; to: string };
|
|
43
|
-
const key = `${from.replace(/^auth\//, '')}/`;
|
|
44
|
-
const type = table.get(key);
|
|
45
|
-
if (type === undefined) return { json: { errors: ['no matching mount'] }, status: 400 };
|
|
46
|
-
table.delete(key);
|
|
47
|
-
table.set(`${to.replace(/^auth\//, '')}/`, type);
|
|
48
|
-
return { json: { data: { migration_id: 'm-1' } }, status: 200 };
|
|
49
|
-
}
|
|
50
|
-
if (path.startsWith('sys/remount/status/')) {
|
|
51
|
-
return { json: { data: { migration_info: { status: 'success' } } }, status: 200 };
|
|
52
|
-
}
|
|
53
|
-
if (path === prefix) {
|
|
54
|
-
return { json: { data: Object.fromEntries(table) }, status: 200 };
|
|
55
|
-
}
|
|
56
|
-
const mount = path.slice(prefix.length + 1).replace(/\/tune$/, '');
|
|
57
|
-
if (seen.method === 'POST') {
|
|
58
|
-
if (!path.endsWith('/tune')) table.set(`${mount}/`, 'enabled-by-test');
|
|
59
|
-
return { status: 204 };
|
|
60
|
-
}
|
|
61
|
-
const type = table.get(`${mount}/`);
|
|
62
|
-
return type === undefined
|
|
63
|
-
? { json: { errors: [`No secret engine mount at ${mount}/`] }, status: 400 }
|
|
64
|
-
: { json: { data: entry(type) }, status: 200 };
|
|
65
|
-
};
|
|
66
|
-
return { answer, table };
|
|
67
|
-
};
|
|
68
|
-
|
|
69
36
|
const posts = (seen: Seen[]) =>
|
|
70
37
|
seen.filter((each) => each.method === 'POST').map((each) => each.path);
|
|
71
38
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as Effect from 'effect/Effect';
|
|
6
6
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
7
|
+
import type { Claim } from '../ownership/adopt.ts';
|
|
7
8
|
import type { BaoError } from './bao-status.ts';
|
|
8
9
|
import {
|
|
9
10
|
type BaoMountAttributes,
|
|
@@ -37,22 +38,31 @@ const moveTarget = (props: BaoMountProps): MoveTarget => ({
|
|
|
37
38
|
/**
|
|
38
39
|
* Converge one mount. `stated` is the path in Alchemy's state (undefined on a first reconcile).
|
|
39
40
|
* Dies on a refusal; fails with OpenBao's own error on a refused call.
|
|
41
|
+
* ⛔ `claim` (mount.ts always passes it) refuses, before any write, a first reconcile that would take
|
|
42
|
+
* over a live mount: the one at the path, or the `remountFrom` source it would move
|
|
43
|
+
* (ownership/adopt.ts).
|
|
40
44
|
*/
|
|
41
45
|
export const reconcileMount = (
|
|
42
46
|
news: BaoMountProps,
|
|
43
47
|
stated: string | undefined,
|
|
44
48
|
options?: RemountOptions,
|
|
49
|
+
claim?: Claim,
|
|
45
50
|
): Effect.Effect<BaoMountAttributes, BaoError | RemountError, HttpClient.HttpClient> =>
|
|
46
51
|
Effect.gen(function* () {
|
|
47
52
|
const move = planMove('Bao.Mount', stated, news.path, news.remountFrom);
|
|
48
53
|
if (move.kind === 'refuse') return yield* Effect.die(new Error(move.message));
|
|
54
|
+
const first = stated === undefined ? claim : undefined;
|
|
55
|
+
if (first !== undefined && move.kind === 'move') {
|
|
56
|
+
const source = yield* readMount({ ...news, path: move.from });
|
|
57
|
+
if (source !== undefined) yield* first(`Bao.Mount ${move.from} (the remountFrom source)`);
|
|
58
|
+
}
|
|
49
59
|
if (move.kind === 'move') yield* performMove(moveTarget(news), move.from, options);
|
|
50
60
|
|
|
51
61
|
let live = yield* readMount(news);
|
|
52
62
|
if (live === undefined) {
|
|
53
63
|
yield* enableMount(news);
|
|
54
64
|
if (wantsTune(news)) yield* tuneMount(news);
|
|
55
|
-
} else
|
|
65
|
+
} else {
|
|
56
66
|
if (live.type !== news.type) {
|
|
57
67
|
return yield* Effect.die(
|
|
58
68
|
new Error(
|
|
@@ -61,7 +71,8 @@ export const reconcileMount = (
|
|
|
61
71
|
),
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
|
-
yield*
|
|
74
|
+
if (first !== undefined) yield* first(`Bao.Mount ${mountPath(news.path)}`);
|
|
75
|
+
if (!matches(live, news)) yield* tuneMount(news);
|
|
65
76
|
}
|
|
66
77
|
live = yield* readMount(news);
|
|
67
78
|
if (live === undefined) {
|
package/src/openbao/mount.ts
CHANGED
|
@@ -22,6 +22,9 @@ import { isResolved } from 'alchemy/Diff';
|
|
|
22
22
|
import * as Provider from 'alchemy/Provider';
|
|
23
23
|
import * as Effect from 'effect/Effect';
|
|
24
24
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
25
|
+
import { claimFor } from '../ownership/adopt.ts';
|
|
26
|
+
import { ownedRead } from '../ownership/probe.ts';
|
|
27
|
+
import { provingResumes } from '../ownership/resume.ts';
|
|
25
28
|
import { type BaoMountAttributes, type BaoMountProps, matches } from './mount-form.ts';
|
|
26
29
|
import { planMove } from './mount-move.ts';
|
|
27
30
|
import { readMount, reconcileMount } from './mount-reconcile.ts';
|
|
@@ -55,12 +58,17 @@ export const BaoMountProvider = () =>
|
|
|
55
58
|
*/
|
|
56
59
|
list: () => Effect.succeed([]),
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
/** ⛔ Stateless: `Unowned` unless our own interrupted create made it (ownership/probe.ts). */
|
|
62
|
+
read: Effect.fn(function* ({ fqn, instanceId, olds, output }) {
|
|
63
|
+
const found = yield* readMount(olds);
|
|
64
|
+
const ours = Effect.sync(() => found !== undefined && matches(found, olds));
|
|
65
|
+
return yield* ownedRead({ fqn, instanceId, output }, found, ours);
|
|
60
66
|
}),
|
|
61
67
|
|
|
62
68
|
diff: Effect.fn(function* ({ news, output }) {
|
|
63
|
-
|
|
69
|
+
// ★ No attributes: an unfinished generation, proven ours or not by provingResumes.
|
|
70
|
+
if (output === undefined) return undefined;
|
|
71
|
+
if (!isResolved(news)) return undefined;
|
|
64
72
|
/**
|
|
65
73
|
* ⛔ A CHANGED PATH IS DECIDED HERE, BEFORE ANY READ OF THE NEW PATH — reading it would
|
|
66
74
|
* find nothing and plan `update`, which is how an empty mount used to get enabled.
|
|
@@ -98,8 +106,9 @@ export const BaoMountProvider = () =>
|
|
|
98
106
|
* method and path — the job the per-command exit-code checks used to do by hand.
|
|
99
107
|
* The body, and the move, live in mount-reconcile.ts.
|
|
100
108
|
*/
|
|
101
|
-
reconcile: Effect.fn(function* ({ news, output }) {
|
|
102
|
-
|
|
109
|
+
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
110
|
+
const claim = claimFor({ fqn, instanceId, output });
|
|
111
|
+
return yield* reconcileMount(news, output?.path, undefined, claim);
|
|
103
112
|
}),
|
|
104
113
|
|
|
105
114
|
/**
|
|
@@ -111,5 +120,5 @@ export const BaoMountProvider = () =>
|
|
|
111
120
|
return undefined;
|
|
112
121
|
}),
|
|
113
122
|
}),
|
|
114
|
-
),
|
|
123
|
+
).pipe(Effect.map(provingResumes)),
|
|
115
124
|
);
|