@homeflare/alchemy 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts +36 -0
- package/dist/launchd/fake-runner.d.ts.map +1 -0
- package/dist/launchd/host-effect.d.ts +19 -0
- package/dist/launchd/host-effect.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +24 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -0
- package/dist/launchd/host-file-lifecycle.d.ts +29 -0
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
- package/dist/launchd/host-file.d.ts +25 -0
- package/dist/launchd/host-file.d.ts.map +1 -0
- package/dist/launchd/host-lookup.d.ts +27 -0
- package/dist/launchd/host-lookup.d.ts.map +1 -0
- package/dist/launchd/index.d.ts +21 -0
- package/dist/launchd/index.d.ts.map +1 -0
- package/dist/launchd/index.js +969 -0
- package/dist/launchd/index.js.map +25 -0
- package/dist/launchd/job-form.d.ts +90 -0
- package/dist/launchd/job-form.d.ts.map +1 -0
- package/dist/launchd/job-lifecycle.d.ts +34 -0
- package/dist/launchd/job-lifecycle.d.ts.map +1 -0
- package/dist/launchd/job-preflight.d.ts +50 -0
- package/dist/launchd/job-preflight.d.ts.map +1 -0
- package/dist/launchd/job-validate.d.ts +25 -0
- package/dist/launchd/job-validate.d.ts.map +1 -0
- package/dist/launchd/job.d.ts +34 -0
- package/dist/launchd/job.d.ts.map +1 -0
- package/dist/launchd/launchctl.d.ts +62 -0
- package/dist/launchd/launchctl.d.ts.map +1 -0
- package/dist/launchd/local-runner.d.ts +7 -0
- package/dist/launchd/local-runner.d.ts.map +1 -0
- package/dist/launchd/plist.d.ts +41 -0
- package/dist/launchd/plist.d.ts.map +1 -0
- package/dist/launchd/providers.d.ts +15 -0
- package/dist/launchd/providers.d.ts.map +1 -0
- package/dist/launchd/runner.d.ts +91 -0
- package/dist/launchd/runner.d.ts.map +1 -0
- package/dist/launchd/secret-tripwire.d.ts +23 -0
- package/dist/launchd/secret-tripwire.d.ts.map +1 -0
- package/dist/openbao/approle-login-form.d.ts +72 -0
- package/dist/openbao/approle-login-form.d.ts.map +1 -0
- package/dist/openbao/approle-login-result.d.ts +39 -0
- package/dist/openbao/approle-login-result.d.ts.map +1 -0
- package/dist/openbao/approle-login.d.ts +77 -0
- package/dist/openbao/approle-login.d.ts.map +1 -0
- package/dist/openbao/auth-method-form.d.ts +7 -0
- package/dist/openbao/auth-method-form.d.ts.map +1 -1
- package/dist/openbao/auth-method-reconcile.d.ts +15 -0
- package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
- package/dist/openbao/auth-method.d.ts +6 -0
- package/dist/openbao/auth-method.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role.d.ts.map +1 -1
- package/dist/openbao/auth-token-form.d.ts +48 -0
- package/dist/openbao/auth-token-form.d.ts.map +1 -0
- package/dist/openbao/bao-http.d.ts +2 -0
- package/dist/openbao/bao-http.d.ts.map +1 -1
- package/dist/openbao/bao-identity.d.ts +81 -0
- package/dist/openbao/bao-identity.d.ts.map +1 -0
- package/dist/openbao/bao-namespace.d.ts +20 -0
- package/dist/openbao/bao-namespace.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
- package/dist/openbao/host-approles.d.ts +62 -0
- package/dist/openbao/host-approles.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +26 -0
- package/dist/openbao/index.d.ts.map +1 -1
- package/dist/openbao/index.js +1889 -477
- package/dist/openbao/index.js.map +46 -17
- package/dist/openbao/jwt-config-form.d.ts +51 -0
- package/dist/openbao/jwt-config-form.d.ts.map +1 -0
- package/dist/openbao/jwt-config.d.ts +25 -0
- package/dist/openbao/jwt-config.d.ts.map +1 -0
- package/dist/openbao/jwt-role-form.d.ts +91 -0
- package/dist/openbao/jwt-role-form.d.ts.map +1 -0
- package/dist/openbao/jwt-role.d.ts +31 -0
- package/dist/openbao/jwt-role.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role-form.d.ts +68 -0
- package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
- package/dist/openbao/kubernetes-role.d.ts +25 -0
- package/dist/openbao/kubernetes-role.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
- package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
- package/dist/openbao/mfa-enforcement.d.ts +41 -0
- package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
- package/dist/openbao/mfa-totp-form.d.ts +52 -0
- package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
- package/dist/openbao/mfa-totp.d.ts +33 -0
- package/dist/openbao/mfa-totp.d.ts.map +1 -0
- package/dist/openbao/mfa-wire.d.ts +63 -0
- package/dist/openbao/mfa-wire.d.ts.map +1 -0
- package/dist/openbao/mount-form.d.ts +7 -0
- package/dist/openbao/mount-form.d.ts.map +1 -1
- package/dist/openbao/mount-move.d.ts +49 -0
- package/dist/openbao/mount-move.d.ts.map +1 -0
- package/dist/openbao/mount-path.d.ts +18 -0
- package/dist/openbao/mount-path.d.ts.map +1 -0
- package/dist/openbao/mount-reconcile.d.ts +16 -0
- package/dist/openbao/mount-reconcile.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +9 -0
- package/dist/openbao/mount.d.ts.map +1 -1
- package/dist/openbao/pki-role-canonical.d.ts +46 -0
- package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +38 -30
- package/dist/openbao/pki-role-form.d.ts.map +1 -1
- package/dist/openbao/pki-role.d.ts.map +1 -1
- package/dist/openbao/plugin-form.d.ts +60 -0
- package/dist/openbao/plugin-form.d.ts.map +1 -0
- package/dist/openbao/plugin-reconcile.d.ts +18 -0
- package/dist/openbao/plugin-reconcile.d.ts.map +1 -0
- package/dist/openbao/plugin-wire.d.ts +45 -0
- package/dist/openbao/plugin-wire.d.ts.map +1 -0
- package/dist/openbao/plugin.d.ts +31 -0
- package/dist/openbao/plugin.d.ts.map +1 -0
- package/dist/openbao/remount-wire.d.ts +44 -0
- package/dist/openbao/remount-wire.d.ts.map +1 -0
- package/dist/openbao/role-reconcile.d.ts +40 -0
- package/dist/openbao/role-reconcile.d.ts.map +1 -0
- package/dist/openbao/ssh-role-form.d.ts +6 -5
- package/dist/openbao/ssh-role-form.d.ts.map +1 -1
- package/dist/openbao/ssh-role.d.ts.map +1 -1
- package/docs/launchd.md +167 -0
- package/package.json +7 -2
- package/src/index.ts +1 -0
- package/src/launchd/fake-runner.ts +176 -0
- package/src/launchd/host-effect.ts +28 -0
- package/src/launchd/host-file-form.ts +71 -0
- package/src/launchd/host-file-lifecycle.test.ts +198 -0
- package/src/launchd/host-file-lifecycle.ts +164 -0
- package/src/launchd/host-file.ts +64 -0
- package/src/launchd/host-lookup.test.ts +92 -0
- package/src/launchd/host-lookup.ts +94 -0
- package/src/launchd/index.ts +26 -0
- package/src/launchd/job-form.test.ts +171 -0
- package/src/launchd/job-form.ts +180 -0
- package/src/launchd/job-identity.test.ts +147 -0
- package/src/launchd/job-lifecycle.test.ts +220 -0
- package/src/launchd/job-lifecycle.ts +194 -0
- package/src/launchd/job-preflight.ts +152 -0
- package/src/launchd/job-validate.ts +134 -0
- package/src/launchd/job.ts +96 -0
- package/src/launchd/launchctl.test.ts +181 -0
- package/src/launchd/launchctl.ts +188 -0
- package/src/launchd/local-runner.test.ts +125 -0
- package/src/launchd/local-runner.ts +143 -0
- package/src/launchd/plist.test.ts +186 -0
- package/src/launchd/plist.ts +156 -0
- package/src/launchd/providers.test.ts +222 -0
- package/src/launchd/providers.ts +21 -0
- package/src/launchd/runner.ts +95 -0
- package/src/launchd/secret-tripwire.test.ts +110 -0
- package/src/launchd/secret-tripwire.ts +81 -0
- package/src/openbao/README.md +181 -0
- package/src/openbao/REPLACE.md +87 -0
- package/src/openbao/approle-login-form.test.ts +64 -0
- package/src/openbao/approle-login-form.ts +153 -0
- package/src/openbao/approle-login-result.ts +96 -0
- package/src/openbao/approle-login.test.ts +200 -0
- package/src/openbao/approle-login.ts +156 -0
- package/src/openbao/auth-method-form.ts +7 -0
- package/src/openbao/auth-method-reconcile.ts +74 -0
- package/src/openbao/auth-method.ts +20 -41
- package/src/openbao/auth-role-form.test.ts +57 -0
- package/src/openbao/auth-role-form.ts +11 -0
- package/src/openbao/auth-role.ts +12 -0
- package/src/openbao/auth-token-form.ts +161 -0
- package/src/openbao/bao-http.ts +2 -0
- package/src/openbao/bao-identity.test.ts +131 -0
- package/src/openbao/bao-identity.ts +188 -0
- package/src/openbao/bao-namespace.ts +32 -0
- package/src/openbao/cloudflare-roles-expand.ts +2 -5
- package/src/openbao/host-approles.test.ts +121 -0
- package/src/openbao/host-approles.ts +126 -0
- package/src/openbao/index.ts +32 -0
- package/src/openbao/jwt-config-form.ts +161 -0
- package/src/openbao/jwt-config.ts +85 -0
- package/src/openbao/jwt-role-form.test.ts +130 -0
- package/src/openbao/jwt-role-form.ts +231 -0
- package/src/openbao/jwt-role.ts +102 -0
- package/src/openbao/kubernetes-role-form.test.ts +82 -0
- package/src/openbao/kubernetes-role-form.ts +149 -0
- package/src/openbao/kubernetes-role.ts +94 -0
- package/src/openbao/mfa-enforcement-form.ts +147 -0
- package/src/openbao/mfa-enforcement.ts +143 -0
- package/src/openbao/mfa-totp-form.ts +155 -0
- package/src/openbao/mfa-totp.ts +118 -0
- package/src/openbao/mfa-wire.ts +117 -0
- package/src/openbao/mfa.test.ts +200 -0
- package/src/openbao/mount-form.ts +9 -1
- package/src/openbao/mount-move.test.ts +164 -0
- package/src/openbao/mount-move.ts +113 -0
- package/src/openbao/mount-path.test.ts +33 -0
- package/src/openbao/mount-path.ts +31 -0
- package/src/openbao/mount-reconcile.ts +75 -0
- package/src/openbao/mount-wire.test.ts +13 -0
- package/src/openbao/mount.ts +26 -43
- package/src/openbao/pki-role-canonical.ts +163 -0
- package/src/openbao/pki-role-form.test.ts +112 -0
- package/src/openbao/pki-role-form.ts +76 -110
- package/src/openbao/pki-role.ts +12 -27
- package/src/openbao/plugin-form.test.ts +86 -0
- package/src/openbao/plugin-form.ts +164 -0
- package/src/openbao/plugin-reconcile.ts +92 -0
- package/src/openbao/plugin-wire.test.ts +146 -0
- package/src/openbao/plugin-wire.ts +65 -0
- package/src/openbao/plugin.ts +100 -0
- package/src/openbao/remount-wire.test.ts +87 -0
- package/src/openbao/remount-wire.ts +112 -0
- package/src/openbao/role-reconcile.test.ts +104 -0
- package/src/openbao/role-reconcile.ts +84 -0
- package/src/openbao/ssh-role-form.test.ts +105 -0
- package/src/openbao/ssh-role-form.ts +18 -16
- package/src/openbao/ssh-role.ts +3 -4
package/dist/openbao/index.js
CHANGED
|
@@ -1,15 +1,310 @@
|
|
|
1
|
+
// src/openbao/approle-login.ts
|
|
2
|
+
import * as Effect2 from "effect/Effect";
|
|
3
|
+
import * as FetchHttpClient2 from "effect/unstable/http/FetchHttpClient";
|
|
4
|
+
|
|
5
|
+
// src/openbao/bao-http.ts
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Schedule from "effect/Schedule";
|
|
10
|
+
import * as Semaphore from "effect/Semaphore";
|
|
11
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
12
|
+
import * as Headers from "effect/unstable/http/Headers";
|
|
13
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
14
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
15
|
+
|
|
16
|
+
// src/openbao/bao-address.ts
|
|
17
|
+
var DEFAULT_ADDRESS = "https://127.0.0.1:8200";
|
|
18
|
+
var UNIX = "unix://";
|
|
19
|
+
var readBaoVariable = (env, name) => env[name] ?? env[`VAULT_${name.slice("BAO_".length)}`] ?? "";
|
|
20
|
+
var resolveAddress = (env) => {
|
|
21
|
+
const agent = readBaoVariable(env, "BAO_AGENT_ADDR");
|
|
22
|
+
const direct = readBaoVariable(env, "BAO_ADDR");
|
|
23
|
+
const raw = agent !== "" ? agent : direct !== "" ? direct : DEFAULT_ADDRESS;
|
|
24
|
+
const namespace = readBaoVariable(env, "BAO_NAMESPACE");
|
|
25
|
+
const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
|
|
26
|
+
return {
|
|
27
|
+
base: socket === undefined ? raw.replace(/\/+$/, "") : "http://localhost",
|
|
28
|
+
namespace: namespace === "" ? undefined : namespace,
|
|
29
|
+
socket
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
var headersFor = (address, env) => {
|
|
33
|
+
const headers = { "X-Vault-Request": "true" };
|
|
34
|
+
if (address.namespace !== undefined)
|
|
35
|
+
headers["X-Vault-Namespace"] = address.namespace;
|
|
36
|
+
const token = readBaoVariable(env, "BAO_TOKEN");
|
|
37
|
+
if (token !== "")
|
|
38
|
+
headers["X-Vault-Token"] = token;
|
|
39
|
+
return headers;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/openbao/bao-status.ts
|
|
43
|
+
class BaoError extends Error {
|
|
44
|
+
status;
|
|
45
|
+
method;
|
|
46
|
+
path;
|
|
47
|
+
errors;
|
|
48
|
+
constructor(status, method, path, errors) {
|
|
49
|
+
const code = status === 0 ? "no response" : String(status);
|
|
50
|
+
super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join("; ") || "(no errors given)"}`);
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.method = method;
|
|
53
|
+
this.path = path;
|
|
54
|
+
this.errors = errors;
|
|
55
|
+
this.name = "BaoError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
var objectOf = (text) => {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(text);
|
|
61
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var errorsOf = (text) => {
|
|
67
|
+
const raw = objectOf(text)?.["errors"];
|
|
68
|
+
return Array.isArray(raw) && raw.every((entry) => typeof entry === "string") ? raw : undefined;
|
|
69
|
+
};
|
|
70
|
+
var settle = (intent, method, path, status, text) => {
|
|
71
|
+
const fail = (errors) => ({
|
|
72
|
+
error: new BaoError(status, method, path, errors)
|
|
73
|
+
});
|
|
74
|
+
if (status < 200 || status >= 300) {
|
|
75
|
+
const errors = errorsOf(text);
|
|
76
|
+
if (errors === undefined) {
|
|
77
|
+
return fail([`not an OpenBao error body (${String(text.length)} bytes)`]);
|
|
78
|
+
}
|
|
79
|
+
if (status === 404 && intent !== "write")
|
|
80
|
+
return { body: undefined };
|
|
81
|
+
return fail(errors);
|
|
82
|
+
}
|
|
83
|
+
if (text.trim() === "") {
|
|
84
|
+
return intent === "read" ? fail(["a successful read with an empty body"]) : { body: undefined };
|
|
85
|
+
}
|
|
86
|
+
const body = objectOf(text);
|
|
87
|
+
return body === undefined ? fail(["a success body that is not a JSON object"]) : { body };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/openbao/bao-http.ts
|
|
91
|
+
var BaoEnv = Context.Reference("homeflare/openbao/BaoEnv", {
|
|
92
|
+
defaultValue: () => process.env
|
|
93
|
+
});
|
|
94
|
+
var MAX_IN_FLIGHT = 8;
|
|
95
|
+
var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
|
|
96
|
+
defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
|
|
97
|
+
});
|
|
98
|
+
var TIMEOUT = "60 seconds";
|
|
99
|
+
var retryTransport = (effect) => Effect.retry(effect, {
|
|
100
|
+
schedule: Schedule.spaced("750 millis"),
|
|
101
|
+
times: 2,
|
|
102
|
+
while: (error) => error.status === 0
|
|
103
|
+
});
|
|
104
|
+
var redactingToken = (effect) => Effect.gen(function* () {
|
|
105
|
+
const names = yield* Headers.CurrentRedactedNames;
|
|
106
|
+
return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
|
|
107
|
+
...names,
|
|
108
|
+
"x-vault-token"
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
var overSocket = (socket, method, path) => (effect) => {
|
|
112
|
+
if (socket === undefined)
|
|
113
|
+
return effect;
|
|
114
|
+
if (typeof Bun === "undefined") {
|
|
115
|
+
return Effect.fail(new BaoError(0, method, path, ["a unix:// address needs Bun fetch; this is not Bun"]));
|
|
116
|
+
}
|
|
117
|
+
return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
|
|
118
|
+
const init = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
|
|
119
|
+
return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
var baoCall = (intent, method, path, body) => Effect.gen(function* () {
|
|
123
|
+
const env = yield* BaoEnv;
|
|
124
|
+
const address = resolveAddress(env);
|
|
125
|
+
const client = yield* HttpClient.HttpClient;
|
|
126
|
+
const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(HttpClientRequest.setHeaders(headersFor(address, env)), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
|
|
127
|
+
const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text2) => ({ status: response.status, text: text2 }))), Effect.timeout(TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
|
|
128
|
+
const gate = yield* BaoGate;
|
|
129
|
+
const { status, text } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
|
|
130
|
+
const outcome = settle(intent, method, path, status, text);
|
|
131
|
+
return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
|
|
132
|
+
});
|
|
133
|
+
var baoRead = (path) => Effect.map(baoCall("read", "GET", path), (body) => {
|
|
134
|
+
if (body === undefined)
|
|
135
|
+
return;
|
|
136
|
+
const data = body["data"];
|
|
137
|
+
return typeof data === "object" && data !== null ? data : body;
|
|
138
|
+
});
|
|
139
|
+
var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, path, body));
|
|
140
|
+
var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
|
|
141
|
+
|
|
142
|
+
// src/openbao/mount-path.ts
|
|
143
|
+
var trimTrailing = (text, char) => {
|
|
144
|
+
let end = text.length;
|
|
145
|
+
while (end > 0 && text[end - 1] === char)
|
|
146
|
+
end -= 1;
|
|
147
|
+
return text.slice(0, end);
|
|
148
|
+
};
|
|
149
|
+
var trimRuns = (text, char) => {
|
|
150
|
+
let start = 0;
|
|
151
|
+
while (start < text.length && text[start] === char)
|
|
152
|
+
start += 1;
|
|
153
|
+
return trimTrailing(text.slice(start), char);
|
|
154
|
+
};
|
|
155
|
+
var trimTrailingSlashes = (text) => trimTrailing(text, "/");
|
|
156
|
+
var trimSlashes = (text) => trimRuns(text, "/");
|
|
157
|
+
var sshMountOf = (mount) => trimTrailingSlashes(mount ?? "ssh");
|
|
158
|
+
|
|
159
|
+
// src/openbao/approle-login-form.ts
|
|
160
|
+
var failureOf = (status) => status === 0 ? "unreachable" : status >= 200 && status < 300 ? "response" : "refused";
|
|
161
|
+
|
|
162
|
+
class BaoLoginError extends Error {
|
|
163
|
+
reason;
|
|
164
|
+
status;
|
|
165
|
+
errors;
|
|
166
|
+
constructor(reason, status, errors, operation) {
|
|
167
|
+
const code = status === 0 ? reason : `${reason} ${String(status)}`;
|
|
168
|
+
super(`${operation}: ${code}: ${errors.join("; ") || "(no errors given)"}`);
|
|
169
|
+
this.reason = reason;
|
|
170
|
+
this.status = status;
|
|
171
|
+
this.errors = errors;
|
|
172
|
+
this.name = "BaoLoginError";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var refusal = (label, value) => {
|
|
176
|
+
if (typeof value !== "string" || value === "")
|
|
177
|
+
return `${label} is empty`;
|
|
178
|
+
if (value.trim() === "")
|
|
179
|
+
return `${label} is only whitespace`;
|
|
180
|
+
if (value.trim() !== value) {
|
|
181
|
+
return `${label} has leading or trailing whitespace (a file read keeps its newline; trim it)`;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
};
|
|
185
|
+
var loginMount = (mount) => trimSlashes(mount ?? "approle");
|
|
186
|
+
var mountProblem = (mount) => {
|
|
187
|
+
const path = loginMount(mount);
|
|
188
|
+
if (path === "")
|
|
189
|
+
return "mount is empty";
|
|
190
|
+
const segments = path.split("/");
|
|
191
|
+
if (/[\s?#%\\]/.test(path) || segments.some((part) => part === "" || part === "." || part === "..")) {
|
|
192
|
+
return `mount ${JSON.stringify(path)} is not a plain mount path`;
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
};
|
|
196
|
+
var loginPath = (mount) => `auth/${loginMount(mount)}/login`;
|
|
197
|
+
var GO_ESCAPES = {
|
|
198
|
+
7: "a",
|
|
199
|
+
8: "b",
|
|
200
|
+
9: "t",
|
|
201
|
+
10: "n",
|
|
202
|
+
11: "v",
|
|
203
|
+
12: "f",
|
|
204
|
+
13: "r"
|
|
205
|
+
};
|
|
206
|
+
var goQuoted = (value) => [...value].map((char) => {
|
|
207
|
+
const code = char.codePointAt(0) ?? 0;
|
|
208
|
+
const named = GO_ESCAPES[code];
|
|
209
|
+
if (char === '"' || char === "\\")
|
|
210
|
+
return `\\${char}`;
|
|
211
|
+
if (named !== undefined)
|
|
212
|
+
return `\\${named}`;
|
|
213
|
+
if (code < 32 || code === 127)
|
|
214
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
215
|
+
return char;
|
|
216
|
+
}).join("");
|
|
217
|
+
var redact = (errors, secrets) => {
|
|
218
|
+
const forms = [...new Set(secrets.flatMap((secret) => [secret, goQuoted(secret)]))].filter((form) => form !== "").sort((a, b) => b.length - a.length);
|
|
219
|
+
return errors.map((entry) => forms.reduce((text, form) => text.split(form).join("[redacted]"), entry));
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/openbao/approle-login-result.ts
|
|
223
|
+
class LoginResult {
|
|
224
|
+
#token;
|
|
225
|
+
accessor;
|
|
226
|
+
policies;
|
|
227
|
+
leaseDurationSeconds;
|
|
228
|
+
constructor(token, accessor, policies, lease) {
|
|
229
|
+
this.#token = token;
|
|
230
|
+
this.accessor = accessor;
|
|
231
|
+
this.policies = Object.freeze([...policies]);
|
|
232
|
+
this.leaseDurationSeconds = lease;
|
|
233
|
+
Object.freeze(this);
|
|
234
|
+
}
|
|
235
|
+
get clientToken() {
|
|
236
|
+
return this.#token;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
var strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
240
|
+
var loginOf = (body) => {
|
|
241
|
+
const auth = body?.["auth"];
|
|
242
|
+
if (typeof auth !== "object" || auth === null || Array.isArray(auth)) {
|
|
243
|
+
return { problem: "the answer carried no auth block" };
|
|
244
|
+
}
|
|
245
|
+
const fields = auth;
|
|
246
|
+
const token = fields["client_token"];
|
|
247
|
+
if (typeof token !== "string" || token === "") {
|
|
248
|
+
const mfa = fields["mfa_requirement"];
|
|
249
|
+
return {
|
|
250
|
+
problem: mfa !== undefined && mfa !== null ? "the login requires MFA, which this helper does not complete" : "the auth block carried no client_token"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const lease = fields["lease_duration"];
|
|
254
|
+
return {
|
|
255
|
+
login: new LoginResult(token, typeof fields["accessor"] === "string" ? fields["accessor"] : "", strings(fields["policies"]), typeof lease === "number" && Number.isFinite(lease) ? lease : 0)
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/openbao/approle-login.ts
|
|
260
|
+
var LOGIN = "OpenBao AppRole login";
|
|
261
|
+
var REVOKE = "OpenBao revoke-self";
|
|
262
|
+
var failed = (operation, secrets) => (error) => new BaoLoginError(failureOf(error.status), error.status, redact(error.errors, secrets), operation);
|
|
263
|
+
var appRoleLoginEffect = (input) => Effect2.gen(function* () {
|
|
264
|
+
const problems = [
|
|
265
|
+
refusal("roleId", input.roleId),
|
|
266
|
+
refusal("secretId", input.secretId),
|
|
267
|
+
mountProblem(input.mount)
|
|
268
|
+
].filter((problem) => problem !== undefined);
|
|
269
|
+
if (problems.length > 0)
|
|
270
|
+
return yield* Effect2.fail(new BaoLoginError("input", 0, problems, LOGIN));
|
|
271
|
+
const env = yield* BaoEnv;
|
|
272
|
+
const body = yield* baoCall("write", "PUT", loginPath(input.mount), {
|
|
273
|
+
role_id: input.roleId,
|
|
274
|
+
secret_id: input.secretId
|
|
275
|
+
}).pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: "" }), Effect2.mapError(failed(LOGIN, [input.roleId, input.secretId])));
|
|
276
|
+
const read = loginOf(body);
|
|
277
|
+
if ("problem" in read) {
|
|
278
|
+
return yield* Effect2.fail(new BaoLoginError("response", 0, [read.problem], LOGIN));
|
|
279
|
+
}
|
|
280
|
+
return read.login;
|
|
281
|
+
});
|
|
282
|
+
var revokeSelfEffect = (token) => Effect2.gen(function* () {
|
|
283
|
+
const problem = refusal("token", token);
|
|
284
|
+
if (problem !== undefined) {
|
|
285
|
+
return yield* Effect2.fail(new BaoLoginError("input", 0, [problem], REVOKE));
|
|
286
|
+
}
|
|
287
|
+
const env = yield* BaoEnv;
|
|
288
|
+
yield* baoCall("write", "PUT", "auth/token/revoke-self").pipe(Effect2.provideService(BaoEnv, { ...env, BAO_TOKEN: token }), Effect2.mapError(failed(REVOKE, [token])));
|
|
289
|
+
});
|
|
290
|
+
var runWith = (env, effect) => {
|
|
291
|
+
const scoped = env === undefined ? effect : Effect2.provideService(effect, BaoEnv, env);
|
|
292
|
+
return Effect2.runPromise(scoped.pipe(Effect2.provide(FetchHttpClient2.layer)));
|
|
293
|
+
};
|
|
294
|
+
var appRoleLogin = (input) => runWith(input.env, appRoleLoginEffect(input));
|
|
295
|
+
var revokeSelf = (token, env) => runWith(env, revokeSelfEffect(token));
|
|
1
296
|
// src/openbao/auth-method.ts
|
|
2
297
|
import { Resource } from "alchemy";
|
|
3
298
|
import { isResolved } from "alchemy/Diff";
|
|
4
299
|
import * as Provider from "alchemy/Provider";
|
|
5
|
-
import * as
|
|
300
|
+
import * as Effect7 from "effect/Effect";
|
|
6
301
|
|
|
7
302
|
// src/openbao/digest.ts
|
|
8
303
|
var canonical = (text) => text.trimEnd();
|
|
9
304
|
var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
|
|
10
305
|
|
|
11
306
|
// src/openbao/mount-form.ts
|
|
12
|
-
var mountPath = (path) => path
|
|
307
|
+
var mountPath = (path) => trimTrailingSlashes(path);
|
|
13
308
|
var DURATION = /^(\d+)([smhd])$/;
|
|
14
309
|
var parseDuration = (text) => {
|
|
15
310
|
const trimmed = text.trim();
|
|
@@ -142,174 +437,164 @@ var sameTtl2 = (want, have) => {
|
|
|
142
437
|
var matches2 = (attributes, props) => attributes.type === props.type && (props.description === undefined || attributes.description === props.description) && sameTtl2(props.defaultLeaseTtl, attributes.defaultLeaseTtl) && sameTtl2(props.maxLeaseTtl, attributes.maxLeaseTtl);
|
|
143
438
|
var wantsTune2 = (props) => props.description !== undefined || props.defaultLeaseTtl !== undefined || props.maxLeaseTtl !== undefined;
|
|
144
439
|
|
|
440
|
+
// src/openbao/auth-method-reconcile.ts
|
|
441
|
+
import * as Effect6 from "effect/Effect";
|
|
442
|
+
|
|
145
443
|
// src/openbao/auth-method-wire.ts
|
|
146
|
-
import * as
|
|
147
|
-
|
|
148
|
-
// src/openbao/bao-http.ts
|
|
149
|
-
import * as Context from "effect/Context";
|
|
150
|
-
import * as Effect from "effect/Effect";
|
|
151
|
-
import * as Option from "effect/Option";
|
|
152
|
-
import * as Schedule from "effect/Schedule";
|
|
153
|
-
import * as Semaphore from "effect/Semaphore";
|
|
154
|
-
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
155
|
-
import * as Headers from "effect/unstable/http/Headers";
|
|
156
|
-
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
157
|
-
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
158
|
-
|
|
159
|
-
// src/openbao/bao-address.ts
|
|
160
|
-
var DEFAULT_ADDRESS = "https://127.0.0.1:8200";
|
|
161
|
-
var UNIX = "unix://";
|
|
162
|
-
var readBaoVariable = (env, name) => env[name] ?? env[`VAULT_${name.slice("BAO_".length)}`] ?? "";
|
|
163
|
-
var resolveAddress = (env) => {
|
|
164
|
-
const agent = readBaoVariable(env, "BAO_AGENT_ADDR");
|
|
165
|
-
const direct = readBaoVariable(env, "BAO_ADDR");
|
|
166
|
-
const raw = agent !== "" ? agent : direct !== "" ? direct : DEFAULT_ADDRESS;
|
|
167
|
-
const namespace = readBaoVariable(env, "BAO_NAMESPACE");
|
|
168
|
-
const socket = raw.startsWith(UNIX) ? raw.slice(UNIX.length) : undefined;
|
|
169
|
-
return {
|
|
170
|
-
base: socket === undefined ? raw.replace(/\/+$/, "") : "http://localhost",
|
|
171
|
-
namespace: namespace === "" ? undefined : namespace,
|
|
172
|
-
socket
|
|
173
|
-
};
|
|
174
|
-
};
|
|
175
|
-
var headersFor = (address, env) => {
|
|
176
|
-
const headers = { "X-Vault-Request": "true" };
|
|
177
|
-
if (address.namespace !== undefined)
|
|
178
|
-
headers["X-Vault-Namespace"] = address.namespace;
|
|
179
|
-
const token = readBaoVariable(env, "BAO_TOKEN");
|
|
180
|
-
if (token !== "")
|
|
181
|
-
headers["X-Vault-Token"] = token;
|
|
182
|
-
return headers;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// src/openbao/bao-status.ts
|
|
186
|
-
class BaoError extends Error {
|
|
187
|
-
status;
|
|
188
|
-
method;
|
|
189
|
-
path;
|
|
190
|
-
errors;
|
|
191
|
-
constructor(status, method, path, errors) {
|
|
192
|
-
const code = status === 0 ? "no response" : String(status);
|
|
193
|
-
super(`OpenBao ${method} /v1/${path} -> ${code}: ${errors.join("; ") || "(no errors given)"}`);
|
|
194
|
-
this.status = status;
|
|
195
|
-
this.method = method;
|
|
196
|
-
this.path = path;
|
|
197
|
-
this.errors = errors;
|
|
198
|
-
this.name = "BaoError";
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
var objectOf = (text3) => {
|
|
202
|
-
try {
|
|
203
|
-
const parsed = JSON.parse(text3);
|
|
204
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : undefined;
|
|
205
|
-
} catch {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
var errorsOf = (text3) => {
|
|
210
|
-
const raw = objectOf(text3)?.["errors"];
|
|
211
|
-
return Array.isArray(raw) && raw.every((entry) => typeof entry === "string") ? raw : undefined;
|
|
212
|
-
};
|
|
213
|
-
var settle = (intent, method, path, status, text3) => {
|
|
214
|
-
const fail = (errors) => ({
|
|
215
|
-
error: new BaoError(status, method, path, errors)
|
|
216
|
-
});
|
|
217
|
-
if (status < 200 || status >= 300) {
|
|
218
|
-
const errors = errorsOf(text3);
|
|
219
|
-
if (errors === undefined) {
|
|
220
|
-
return fail([`not an OpenBao error body (${String(text3.length)} bytes)`]);
|
|
221
|
-
}
|
|
222
|
-
if (status === 404 && intent !== "write")
|
|
223
|
-
return { body: undefined };
|
|
224
|
-
return fail(errors);
|
|
225
|
-
}
|
|
226
|
-
if (text3.trim() === "") {
|
|
227
|
-
return intent === "read" ? fail(["a successful read with an empty body"]) : { body: undefined };
|
|
228
|
-
}
|
|
229
|
-
const body = objectOf(text3);
|
|
230
|
-
return body === undefined ? fail(["a success body that is not a JSON object"]) : { body };
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// src/openbao/bao-http.ts
|
|
234
|
-
var BaoEnv = Context.Reference("homeflare/openbao/BaoEnv", {
|
|
235
|
-
defaultValue: () => process.env
|
|
236
|
-
});
|
|
237
|
-
var MAX_IN_FLIGHT = 8;
|
|
238
|
-
var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
|
|
239
|
-
defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
|
|
240
|
-
});
|
|
241
|
-
var TIMEOUT = "60 seconds";
|
|
242
|
-
var retryTransport = (effect) => Effect.retry(effect, {
|
|
243
|
-
schedule: Schedule.spaced("750 millis"),
|
|
244
|
-
times: 2,
|
|
245
|
-
while: (error) => error.status === 0
|
|
246
|
-
});
|
|
247
|
-
var redactingToken = (effect) => Effect.gen(function* () {
|
|
248
|
-
const names = yield* Headers.CurrentRedactedNames;
|
|
249
|
-
return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
|
|
250
|
-
...names,
|
|
251
|
-
"x-vault-token"
|
|
252
|
-
]);
|
|
253
|
-
});
|
|
254
|
-
var overSocket = (socket, method, path) => (effect) => {
|
|
255
|
-
if (socket === undefined)
|
|
256
|
-
return effect;
|
|
257
|
-
if (typeof Bun === "undefined") {
|
|
258
|
-
return Effect.fail(new BaoError(0, method, path, ["a unix:// address needs Bun fetch; this is not Bun"]));
|
|
259
|
-
}
|
|
260
|
-
return Effect.flatMap(Effect.serviceOption(FetchHttpClient.RequestInit), (defaults) => {
|
|
261
|
-
const init = { ...Option.getOrElse(defaults, () => ({})), unix: socket };
|
|
262
|
-
return Effect.provideService(effect, FetchHttpClient.RequestInit, init);
|
|
263
|
-
});
|
|
264
|
-
};
|
|
265
|
-
var baoCall = (intent, method, path, body) => Effect.gen(function* () {
|
|
266
|
-
const env = yield* BaoEnv;
|
|
267
|
-
const address = resolveAddress(env);
|
|
268
|
-
const client = yield* HttpClient.HttpClient;
|
|
269
|
-
const request = HttpClientRequest.make(method)(`${address.base}/v1/${path}`).pipe(HttpClientRequest.setHeaders(headersFor(address, env)), body === undefined ? (self) => self : HttpClientRequest.bodyText(JSON.stringify(body), "application/json"));
|
|
270
|
-
const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text4) => ({ status: response.status, text: text4 }))), Effect.timeout(TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
|
|
271
|
-
const gate = yield* BaoGate;
|
|
272
|
-
const { status, text: text3 } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
|
|
273
|
-
const outcome = settle(intent, method, path, status, text3);
|
|
274
|
-
return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
|
|
275
|
-
});
|
|
276
|
-
var baoRead = (path) => Effect.map(baoCall("read", "GET", path), (body) => {
|
|
277
|
-
if (body === undefined)
|
|
278
|
-
return;
|
|
279
|
-
const data = body["data"];
|
|
280
|
-
return typeof data === "object" && data !== null ? data : body;
|
|
281
|
-
});
|
|
282
|
-
var baoWrite = (method, path, body) => Effect.asVoid(baoCall("write", method, path, body));
|
|
283
|
-
var baoDelete = (path) => Effect.asVoid(baoCall("delete", "DELETE", path));
|
|
284
|
-
|
|
285
|
-
// src/openbao/auth-method-wire.ts
|
|
286
|
-
var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect2.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
|
|
444
|
+
import * as Effect3 from "effect/Effect";
|
|
445
|
+
var absentFromTable = (path, asked) => baoRead("sys/auth").pipe(Effect3.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
|
|
287
446
|
...asked.errors,
|
|
288
447
|
`sys/auth could not confirm the method is absent: ${listing.message}`
|
|
289
|
-
])),
|
|
290
|
-
var readAuthMethodData = (path) => baoRead(readPath2(path)).pipe(
|
|
448
|
+
])), Effect3.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${authPath(path)}/`) ? Effect3.succeed(undefined) : Effect3.fail(asked)));
|
|
449
|
+
var readAuthMethodData = (path) => baoRead(readPath2(path)).pipe(Effect3.catchIf((error) => error.status === 400, (error) => absentFromTable(path, error)));
|
|
291
450
|
var enableAuthMethod = (props) => baoWrite("POST", readPath2(props.path), enableBody2(props));
|
|
292
451
|
var tuneAuthMethod = (props) => baoWrite("POST", tunePath2(props.path), tuneBody2(props));
|
|
293
452
|
var disableAuthMethod = (path) => baoDelete(readPath2(path));
|
|
294
453
|
|
|
295
|
-
// src/openbao/
|
|
296
|
-
|
|
297
|
-
|
|
454
|
+
// src/openbao/mount-move.ts
|
|
455
|
+
import * as Effect5 from "effect/Effect";
|
|
456
|
+
|
|
457
|
+
// src/openbao/remount-wire.ts
|
|
458
|
+
import * as Effect4 from "effect/Effect";
|
|
459
|
+
class RemountError extends Error {
|
|
460
|
+
constructor(message) {
|
|
461
|
+
super(message);
|
|
462
|
+
this.name = "RemountError";
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
466
|
+
var startRemount = (from, to) => Effect4.flatMap(baoCall("write", "POST", "sys/remount", { from, to }), (body) => {
|
|
467
|
+
const id = record(body?.["data"])?.["migration_id"];
|
|
468
|
+
return typeof id === "string" && id !== "" ? Effect4.succeed(id) : Effect4.fail(new RemountError(`sys/remount ${from} -> ${to} answered no migration_id.`));
|
|
469
|
+
});
|
|
470
|
+
var remountStatus = (id) => Effect4.map(baoRead(`sys/remount/status/${encodeURIComponent(id)}`), (data) => {
|
|
471
|
+
if (data === undefined)
|
|
472
|
+
return;
|
|
473
|
+
const status = record(data["migration_info"])?.["status"];
|
|
474
|
+
return typeof status === "string" ? status : "unknown";
|
|
475
|
+
});
|
|
476
|
+
var remountAndWait = (from, to, confirm, options = {}) => Effect4.gen(function* () {
|
|
477
|
+
const interval = options.intervalMillis ?? 1000;
|
|
478
|
+
const attempts = Math.max(1, options.attempts ?? 120);
|
|
479
|
+
const id = yield* startRemount(from, to);
|
|
480
|
+
const named = `sys/remount ${from} -> ${to} (migration ${id})`;
|
|
481
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
482
|
+
const status = yield* remountStatus(id);
|
|
483
|
+
if (status === "success")
|
|
484
|
+
return;
|
|
485
|
+
if (status === "failure") {
|
|
486
|
+
return yield* Effect4.fail(new RemountError(`${named} FAILED. The server log carries the reason, tagged with that migration_id. ` + "Read sys/mounts (or sys/auth) before retrying: the source may be tainted."));
|
|
487
|
+
}
|
|
488
|
+
if (status === undefined) {
|
|
489
|
+
if (yield* confirm)
|
|
490
|
+
return;
|
|
491
|
+
return yield* Effect4.fail(new RemountError(`${named}: the server no longer knows the migration (its tracker is in memory, lost ` + "on a restart or leader change), and the mount table does not show the move done."));
|
|
492
|
+
}
|
|
493
|
+
if (attempt < attempts)
|
|
494
|
+
yield* Effect4.sleep(interval);
|
|
495
|
+
}
|
|
496
|
+
return yield* Effect4.fail(new RemountError(`${named} is still in progress after ${String(attempts)} status reads. Do not re-run ` + `until sys/remount/status/${id} settles.`));
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// src/openbao/mount-move.ts
|
|
500
|
+
var planMove = (family, stated, path, remountFrom) => {
|
|
501
|
+
const to = mountPath(path);
|
|
502
|
+
const from = remountFrom === undefined ? undefined : mountPath(remountFrom);
|
|
503
|
+
if (stated === undefined) {
|
|
504
|
+
return from === undefined || from === to ? { kind: "stay" } : { kind: "move", from };
|
|
505
|
+
}
|
|
506
|
+
const was = mountPath(stated);
|
|
507
|
+
if (was === to)
|
|
508
|
+
return { kind: "stay" };
|
|
509
|
+
if (from === undefined) {
|
|
510
|
+
return {
|
|
511
|
+
kind: "refuse",
|
|
512
|
+
message: `${family}: path changed from ${was} to ${to}. That would enable an EMPTY mount at ${to} ` + `and strand everything under ${was}. To move it with its data, declare ` + `\`remountFrom: '${was}'\` (leases under it are revoked; policies are not rewritten). ` + "For a new, empty mount, give it a new resource id instead."
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (from !== was) {
|
|
516
|
+
return {
|
|
517
|
+
kind: "refuse",
|
|
518
|
+
message: `${family}: remountFrom is ${from}, but the mount in state is ${was}.`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { kind: "move", from };
|
|
522
|
+
};
|
|
523
|
+
var refuse = (message) => Effect5.die(new Error(message));
|
|
524
|
+
var performMove = (target, from, options) => Effect5.gen(function* () {
|
|
525
|
+
const to = mountPath(target.path);
|
|
526
|
+
const source = yield* target.read(from);
|
|
527
|
+
const landed = yield* target.read(to);
|
|
528
|
+
if (source !== undefined && landed !== undefined) {
|
|
529
|
+
return yield* refuse(`${target.family}: both ${from} and ${to} exist, so remountFrom cannot move one onto the ` + "other. Decide which holds the data, and retire the other by hand.");
|
|
530
|
+
}
|
|
531
|
+
if (source === undefined && landed === undefined) {
|
|
532
|
+
return yield* refuse(`${target.family}: remountFrom ${from} names no mount, and ${to} does not exist either. ` + "Refusing to enable an empty mount where data was expected.");
|
|
533
|
+
}
|
|
534
|
+
if (source === undefined)
|
|
535
|
+
return;
|
|
536
|
+
if (source.type !== target.type) {
|
|
537
|
+
return yield* refuse(`${target.family}: ${from} is type ${source.type}, not ${target.type}. A remount keeps the ` + "type; fix the declaration first.");
|
|
538
|
+
}
|
|
539
|
+
const confirm = Effect5.map(Effect5.all([target.read(from), target.read(to)]), ([gone, there]) => gone === undefined && there !== undefined);
|
|
540
|
+
yield* remountAndWait(target.wire(from), target.wire(to), confirm, options);
|
|
298
541
|
});
|
|
299
|
-
|
|
542
|
+
|
|
543
|
+
// src/openbao/auth-method-reconcile.ts
|
|
544
|
+
var readMethod = (props) => Effect6.gen(function* () {
|
|
300
545
|
const live = yield* readAuthMethodData(props.path);
|
|
301
546
|
if (live === undefined)
|
|
302
547
|
return;
|
|
303
548
|
return attributesOf2(props, live);
|
|
304
549
|
});
|
|
305
|
-
var
|
|
306
|
-
|
|
307
|
-
|
|
550
|
+
var moveTarget = (props) => ({
|
|
551
|
+
family: "Bao.AuthMethod",
|
|
552
|
+
path: props.path,
|
|
553
|
+
read: (path) => readMethod({ ...props, path }),
|
|
554
|
+
type: props.type,
|
|
555
|
+
wire: (path) => `auth/${authPath(path)}`
|
|
556
|
+
});
|
|
557
|
+
var reconcileAuthMethod = (news, stated, options) => Effect6.gen(function* () {
|
|
558
|
+
const move = planMove("Bao.AuthMethod", stated, news.path, news.remountFrom);
|
|
559
|
+
if (move.kind === "refuse")
|
|
560
|
+
return yield* Effect6.die(new Error(move.message));
|
|
561
|
+
if (move.kind === "move")
|
|
562
|
+
yield* performMove(moveTarget(news), move.from, options);
|
|
563
|
+
let live = yield* readMethod(news);
|
|
564
|
+
if (live === undefined) {
|
|
565
|
+
yield* enableAuthMethod(news);
|
|
566
|
+
if (wantsTune2(news))
|
|
567
|
+
yield* tuneAuthMethod(news);
|
|
568
|
+
} else if (!matches2(live, news)) {
|
|
569
|
+
if (live.type !== news.type) {
|
|
570
|
+
return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
|
|
571
|
+
}
|
|
572
|
+
yield* tuneAuthMethod(news);
|
|
573
|
+
}
|
|
574
|
+
live = yield* readMethod(news);
|
|
575
|
+
if (live === undefined) {
|
|
576
|
+
return yield* Effect6.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
|
|
577
|
+
}
|
|
578
|
+
return live;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// src/openbao/auth-method.ts
|
|
582
|
+
var BaoAuthMethod = Resource("Bao.AuthMethod", {
|
|
583
|
+
defaultRemovalPolicy: "retain"
|
|
584
|
+
});
|
|
585
|
+
var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect7.succeed(BaoAuthMethod.Provider.of({
|
|
586
|
+
list: () => Effect7.succeed([]),
|
|
587
|
+
read: Effect7.fn(function* ({ olds }) {
|
|
308
588
|
return yield* readMethod(olds);
|
|
309
589
|
}),
|
|
310
|
-
diff:
|
|
590
|
+
diff: Effect7.fn(function* ({ news, output }) {
|
|
311
591
|
if (output === undefined || !isResolved(news))
|
|
312
592
|
return;
|
|
593
|
+
const move = planMove("Bao.AuthMethod", output.path, news.path, news.remountFrom);
|
|
594
|
+
if (move.kind === "refuse")
|
|
595
|
+
return yield* Effect7.die(new Error(move.message));
|
|
596
|
+
if (move.kind === "move")
|
|
597
|
+
return { action: "update" };
|
|
313
598
|
const live = yield* readMethod(news);
|
|
314
599
|
if (live === undefined)
|
|
315
600
|
return { action: "update" };
|
|
@@ -319,25 +604,10 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
|
|
|
319
604
|
return { action: "noop" };
|
|
320
605
|
return { action: "update" };
|
|
321
606
|
}),
|
|
322
|
-
reconcile:
|
|
323
|
-
|
|
324
|
-
if (live === undefined) {
|
|
325
|
-
yield* enableAuthMethod(news);
|
|
326
|
-
if (wantsTune2(news))
|
|
327
|
-
yield* tuneAuthMethod(news);
|
|
328
|
-
} else if (!matches2(live, news)) {
|
|
329
|
-
if (live.type !== news.type) {
|
|
330
|
-
return yield* Effect3.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Auth method type is immutable — replace manually."));
|
|
331
|
-
}
|
|
332
|
-
yield* tuneAuthMethod(news);
|
|
333
|
-
}
|
|
334
|
-
live = yield* readMethod(news);
|
|
335
|
-
if (live === undefined) {
|
|
336
|
-
return yield* Effect3.die(new Error(`Bao.AuthMethod ${authPath(news.path)}: write returned no error but the method is still absent.`));
|
|
337
|
-
}
|
|
338
|
-
return live;
|
|
607
|
+
reconcile: Effect7.fn(function* ({ news, output }) {
|
|
608
|
+
return yield* reconcileAuthMethod(news, output?.path);
|
|
339
609
|
}),
|
|
340
|
-
delete:
|
|
610
|
+
delete: Effect7.fn(function* ({ output }) {
|
|
341
611
|
yield* disableAuthMethod(output.path);
|
|
342
612
|
return;
|
|
343
613
|
})
|
|
@@ -346,7 +616,7 @@ var BaoAuthMethodProvider = () => Provider.effect(BaoAuthMethod, Effect3.succeed
|
|
|
346
616
|
import { Resource as Resource2 } from "alchemy";
|
|
347
617
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
348
618
|
import * as Provider2 from "alchemy/Provider";
|
|
349
|
-
import * as
|
|
619
|
+
import * as Effect8 from "effect/Effect";
|
|
350
620
|
|
|
351
621
|
// src/openbao/auth-role-form.ts
|
|
352
622
|
var policiesOf = (live) => {
|
|
@@ -395,6 +665,7 @@ var writeBody = (props) => {
|
|
|
395
665
|
}
|
|
396
666
|
return body;
|
|
397
667
|
};
|
|
668
|
+
var isRename = (from, to) => from.toLowerCase() !== to.toLowerCase();
|
|
398
669
|
var samePolicies = (want, have) => want.length === have.length && want.every((policy, index) => policy === have[index]);
|
|
399
670
|
var sameTtl3 = (want, have) => {
|
|
400
671
|
const wantSeconds = parseDuration(want);
|
|
@@ -410,20 +681,22 @@ var matches3 = (attributes, props) => {
|
|
|
410
681
|
var BaoAuthRole = Resource2("Bao.AuthRole", {
|
|
411
682
|
defaultRemovalPolicy: "retain"
|
|
412
683
|
});
|
|
413
|
-
var readRole = (props) =>
|
|
684
|
+
var readRole = (props) => Effect8.gen(function* () {
|
|
414
685
|
const live = yield* baoRead(readPath3(props.name));
|
|
415
686
|
if (live === undefined)
|
|
416
687
|
return;
|
|
417
688
|
return attributesOf3(props, live);
|
|
418
689
|
});
|
|
419
|
-
var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole,
|
|
420
|
-
list: () =>
|
|
421
|
-
read:
|
|
690
|
+
var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect8.succeed(BaoAuthRole.Provider.of({
|
|
691
|
+
list: () => Effect8.succeed([]),
|
|
692
|
+
read: Effect8.fn(function* ({ olds }) {
|
|
422
693
|
return yield* readRole(olds);
|
|
423
694
|
}),
|
|
424
|
-
diff:
|
|
695
|
+
diff: Effect8.fn(function* ({ news, output }) {
|
|
425
696
|
if (output === undefined || !isResolved2(news))
|
|
426
697
|
return;
|
|
698
|
+
if (isRename(output.name, news.name))
|
|
699
|
+
return { action: "replace" };
|
|
427
700
|
const live = yield* readRole(news);
|
|
428
701
|
if (live === undefined)
|
|
429
702
|
return { action: "update" };
|
|
@@ -431,27 +704,107 @@ var BaoAuthRoleProvider = () => Provider2.effect(BaoAuthRole, Effect4.succeed(Ba
|
|
|
431
704
|
return { action: "noop" };
|
|
432
705
|
return { action: "update" };
|
|
433
706
|
}),
|
|
434
|
-
reconcile:
|
|
707
|
+
reconcile: Effect8.fn(function* ({ news }) {
|
|
435
708
|
const live = yield* readRole(news);
|
|
436
709
|
if (live === undefined || !matches3(live, news)) {
|
|
437
710
|
yield* baoWrite("PUT", readPath3(news.name), writeBody(news));
|
|
438
711
|
}
|
|
439
712
|
const after = yield* readRole(news);
|
|
440
713
|
if (after === undefined) {
|
|
441
|
-
return yield*
|
|
714
|
+
return yield* Effect8.die(new Error(`Bao.AuthRole ${news.name}: write returned no error but the role is still absent.`));
|
|
442
715
|
}
|
|
443
716
|
return after;
|
|
444
717
|
}),
|
|
445
|
-
delete:
|
|
718
|
+
delete: Effect8.fn(function* ({ output }) {
|
|
446
719
|
yield* baoDelete(readPath3(output.name));
|
|
447
720
|
return;
|
|
448
721
|
})
|
|
449
722
|
})));
|
|
723
|
+
// src/openbao/bao-identity.ts
|
|
724
|
+
import * as Effect9 from "effect/Effect";
|
|
725
|
+
import * as FetchHttpClient3 from "effect/unstable/http/FetchHttpClient";
|
|
726
|
+
|
|
727
|
+
// src/openbao/bao-namespace.ts
|
|
728
|
+
var canonicalNamespace = (namespace) => {
|
|
729
|
+
const bare = trimSlashes((namespace ?? "").trim());
|
|
730
|
+
return bare === "root" ? "" : bare;
|
|
731
|
+
};
|
|
732
|
+
var namespacePath = (namespace) => {
|
|
733
|
+
const bare = canonicalNamespace(namespace);
|
|
734
|
+
return bare === "" ? "" : `${bare}/`;
|
|
735
|
+
};
|
|
736
|
+
var envNamespace = (env) => canonicalNamespace(resolveAddress(env).namespace);
|
|
737
|
+
var namespaceLabel = (namespace) => namespace === "" ? "root" : namespace;
|
|
738
|
+
|
|
739
|
+
// src/openbao/bao-identity.ts
|
|
740
|
+
class BaoIdentityError extends Error {
|
|
741
|
+
reason;
|
|
742
|
+
constructor(reason, detail) {
|
|
743
|
+
super(`REFUSING: ${detail}`);
|
|
744
|
+
this.reason = reason;
|
|
745
|
+
this.name = "BaoIdentityError";
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
var HEALTH_PATH = "sys/health?standbyok=true&sealedcode=200&uninitcode=200";
|
|
749
|
+
var NAMESPACE_PROBE_PATH = "sys/internal/ui/mounts";
|
|
750
|
+
var addressOf = (env) => {
|
|
751
|
+
const address = resolveAddress(env);
|
|
752
|
+
return address.socket === undefined ? address.base : `unix://${address.socket}`;
|
|
753
|
+
};
|
|
754
|
+
var unreachable = (address, what) => (error) => new BaoIdentityError("unreachable", `${address} did not answer ${what}: ${error.message}`);
|
|
755
|
+
var text3 = (value) => typeof value === "string" ? value : "";
|
|
756
|
+
var checkHealth = (address, expectedCluster, body) => {
|
|
757
|
+
if (body === undefined || typeof body["sealed"] !== "boolean") {
|
|
758
|
+
return new BaoIdentityError("unconfirmed", `${address} answered sys/health, but not as OpenBao.`);
|
|
759
|
+
}
|
|
760
|
+
if (body["initialized"] === false) {
|
|
761
|
+
return new BaoIdentityError("unconfirmed", `${address} is not initialised.`);
|
|
762
|
+
}
|
|
763
|
+
if (body["sealed"] === true) {
|
|
764
|
+
return new BaoIdentityError("unconfirmed", `${address} is sealed, and a sealed vault does not report its cluster_name.`);
|
|
765
|
+
}
|
|
766
|
+
const clusterName = text3(body["cluster_name"]);
|
|
767
|
+
if (clusterName !== expectedCluster) {
|
|
768
|
+
return new BaoIdentityError("mismatch", `${address} answers as cluster \`${clusterName || "(none)"}\`, but this stack expects ` + `\`${expectedCluster}\`. Check BAO_ADDR and BAO_AGENT_ADDR (the agent address wins).`);
|
|
769
|
+
}
|
|
770
|
+
return { clusterId: text3(body["cluster_id"]), version: text3(body["version"]) };
|
|
771
|
+
};
|
|
772
|
+
var assertBaoIdentityEffect = (expected) => Effect9.gen(function* () {
|
|
773
|
+
const env = yield* BaoEnv;
|
|
774
|
+
const address = addressOf(env);
|
|
775
|
+
const clusterName = expected.clusterName.trim();
|
|
776
|
+
if (clusterName === "") {
|
|
777
|
+
return yield* Effect9.fail(new BaoIdentityError("input", "clusterName is empty, so there is nothing to compare."));
|
|
778
|
+
}
|
|
779
|
+
const namespace = canonicalNamespace(expected.namespace);
|
|
780
|
+
const sent = envNamespace(env);
|
|
781
|
+
if (sent !== namespace) {
|
|
782
|
+
return yield* Effect9.fail(new BaoIdentityError("mismatch", `BAO_NAMESPACE resolves to \`${namespaceLabel(sent)}\`, but this stack expects ` + `\`${namespaceLabel(namespace)}\`.`));
|
|
783
|
+
}
|
|
784
|
+
const anonymous = { ...env, BAO_NAMESPACE: "", BAO_TOKEN: "" };
|
|
785
|
+
const body = yield* baoCall("read", "GET", HEALTH_PATH).pipe(Effect9.provideService(BaoEnv, anonymous), Effect9.mapError(unreachable(address, "sys/health")));
|
|
786
|
+
const health = checkHealth(address, clusterName, body);
|
|
787
|
+
if (health instanceof BaoIdentityError)
|
|
788
|
+
return yield* Effect9.fail(health);
|
|
789
|
+
if (namespace !== "") {
|
|
790
|
+
const scoped = { ...env, BAO_NAMESPACE: namespace, BAO_TOKEN: "" };
|
|
791
|
+
const probe = yield* baoCall("read", "GET", NAMESPACE_PROBE_PATH).pipe(Effect9.provideService(BaoEnv, scoped), Effect9.mapError(unreachable(address, `the namespace probe for \`${namespace}\``)));
|
|
792
|
+
if (probe === undefined) {
|
|
793
|
+
return yield* Effect9.fail(new BaoIdentityError("mismatch", `cluster \`${clusterName}\` at ${address} has no namespace \`${namespace}\`.`));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return { address, clusterName, namespace, ...health };
|
|
797
|
+
});
|
|
798
|
+
var assertBaoIdentity = (input) => {
|
|
799
|
+
const effect3 = assertBaoIdentityEffect(input);
|
|
800
|
+
const scoped = input.env === undefined ? effect3 : Effect9.provideService(effect3, BaoEnv, input.env);
|
|
801
|
+
return Effect9.runPromise(scoped.pipe(Effect9.provide(FetchHttpClient3.layer)));
|
|
802
|
+
};
|
|
450
803
|
// src/openbao/cloudflare-role.ts
|
|
451
804
|
import { Resource as Resource3 } from "alchemy";
|
|
452
805
|
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
453
806
|
import * as Provider3 from "alchemy/Provider";
|
|
454
|
-
import * as
|
|
807
|
+
import * as Effect12 from "effect/Effect";
|
|
455
808
|
|
|
456
809
|
// src/openbao/cloudflare-group-scope.ts
|
|
457
810
|
var zoneResource = (zoneId) => `com.cloudflare.api.account.zone.${zoneId}`;
|
|
@@ -471,7 +824,7 @@ var groupKey = (name, scope) => `${scope} ${name}`;
|
|
|
471
824
|
|
|
472
825
|
// src/openbao/cloudflare-permission-groups.ts
|
|
473
826
|
import * as Context2 from "effect/Context";
|
|
474
|
-
import * as
|
|
827
|
+
import * as Effect11 from "effect/Effect";
|
|
475
828
|
|
|
476
829
|
// src/openbao/cloudflare-permission-groups-solve.ts
|
|
477
830
|
var observationsOf = (declared, live) => declared.flatMap((role) => {
|
|
@@ -641,7 +994,7 @@ var samePolicies2 = (want, have) => want.length === have.length && want.every((w
|
|
|
641
994
|
});
|
|
642
995
|
|
|
643
996
|
// src/openbao/cloudflare-role-wire.ts
|
|
644
|
-
import * as
|
|
997
|
+
import * as Effect10 from "effect/Effect";
|
|
645
998
|
|
|
646
999
|
// src/openbao/cloudflare-role-form.ts
|
|
647
1000
|
var rolePath = (mount, name) => `${mountPath(mount)}/roles/${name}`;
|
|
@@ -706,7 +1059,7 @@ var differences = (props, policies, live) => {
|
|
|
706
1059
|
};
|
|
707
1060
|
|
|
708
1061
|
// src/openbao/cloudflare-role-wire.ts
|
|
709
|
-
var readCloudflareRole = (mount, name) =>
|
|
1062
|
+
var readCloudflareRole = (mount, name) => Effect10.map(baoRead(rolePath(mount, name)), (data) => data === undefined ? undefined : liveRoleOf(data));
|
|
710
1063
|
var writeCloudflareRole = (mount, name, body) => baoWrite("PUT", rolePath(mount, name), body);
|
|
711
1064
|
var deleteCloudflareRole = (mount, name) => baoDelete(rolePath(mount, name));
|
|
712
1065
|
|
|
@@ -728,9 +1081,9 @@ var pick = (mount, groups, refs, hint) => {
|
|
|
728
1081
|
const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));
|
|
729
1082
|
if (unknown.length > 0) {
|
|
730
1083
|
const problems = unknown.map((ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`);
|
|
731
|
-
return
|
|
1084
|
+
return Effect11.fail(new CloudflareGroupsError(mount, problems));
|
|
732
1085
|
}
|
|
733
|
-
return
|
|
1086
|
+
return Effect11.succeed(new Map(refs.flatMap((ref) => {
|
|
734
1087
|
const key = groupKey(ref.name, ref.scope);
|
|
735
1088
|
const id = groups.get(key);
|
|
736
1089
|
return id === undefined ? [] : [[key, id]];
|
|
@@ -738,15 +1091,15 @@ var pick = (mount, groups, refs, hint) => {
|
|
|
738
1091
|
};
|
|
739
1092
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
740
1093
|
var deniedHint = (mount) => `GET ${mountPath(mount)}/permission-groups answered 403 — this token's policy carries no ` + `\`path "${mountPath(mount)}/permission-groups" { capabilities = ["read"] }\` grant. ` + "It is declared in ../platform/secrets/vault/policies/homeflare-admin/" + "20-cloudflare-permission-groups.hcl and applies with `alchemy deploy` of this stack";
|
|
741
|
-
var deniedNamed = (mount, read) =>
|
|
742
|
-
var engineGroups = (mount) =>
|
|
1094
|
+
var deniedNamed = (mount, read) => Effect11.catchIf(read, (error) => error.status === 403, () => Effect11.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
|
|
1095
|
+
var engineGroups = (mount) => Effect11.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
|
|
743
1096
|
const list = data?.["permission_groups"];
|
|
744
1097
|
if (data === undefined) {
|
|
745
1098
|
const why = "GET <mount>/permission-groups answered 404 — the endpoint is not deployed here";
|
|
746
|
-
return
|
|
1099
|
+
return Effect11.fail(new CloudflareGroupsError(mount, [why]));
|
|
747
1100
|
}
|
|
748
1101
|
if (!Array.isArray(list)) {
|
|
749
|
-
return
|
|
1102
|
+
return Effect11.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
|
|
750
1103
|
}
|
|
751
1104
|
const scoped = new Map;
|
|
752
1105
|
const idsOf = new Map;
|
|
@@ -773,24 +1126,24 @@ var engineGroups = (mount) => Effect6.flatMap(deniedNamed(mount, baoRead(`${moun
|
|
|
773
1126
|
const only = [...ids];
|
|
774
1127
|
return ids.size === 1 && only[0] !== undefined ? [[name, only[0]]] : [];
|
|
775
1128
|
}));
|
|
776
|
-
return problems.length > 0 ?
|
|
1129
|
+
return problems.length > 0 ? Effect11.fail(new CloudflareGroupsError(mount, problems)) : Effect11.succeed(new Map([...atEveryScope(sole), ...scoped]));
|
|
777
1130
|
});
|
|
778
|
-
var permissionGroupsFromEngine = () =>
|
|
1131
|
+
var permissionGroupsFromEngine = () => Effect11.sync(() => {
|
|
779
1132
|
const cache = new Map;
|
|
780
|
-
const cachedFor = (mount) =>
|
|
1133
|
+
const cachedFor = (mount) => Effect11.suspend(() => {
|
|
781
1134
|
const hit = cache.get(mountPath(mount));
|
|
782
1135
|
if (hit !== undefined)
|
|
783
1136
|
return hit;
|
|
784
|
-
return
|
|
1137
|
+
return Effect11.flatMap(Effect11.cached(engineGroups(mount)), (lookup) => {
|
|
785
1138
|
cache.set(mountPath(mount), lookup);
|
|
786
1139
|
return lookup;
|
|
787
1140
|
});
|
|
788
1141
|
});
|
|
789
1142
|
return CloudflarePermissionGroups.of({
|
|
790
|
-
resolve: (mount, refs) =>
|
|
1143
|
+
resolve: (mount, refs) => Effect11.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
|
|
791
1144
|
});
|
|
792
1145
|
});
|
|
793
|
-
var permissionGroupsFromLiveRoles = (declared) =>
|
|
1146
|
+
var permissionGroupsFromLiveRoles = (declared) => Effect11.gen(function* () {
|
|
794
1147
|
const accountOf = new Map;
|
|
795
1148
|
const byAccount = new Map;
|
|
796
1149
|
for (const [mount, roles] of declared) {
|
|
@@ -801,9 +1154,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
|
|
|
801
1154
|
}
|
|
802
1155
|
const solved = new Map;
|
|
803
1156
|
for (const [account, roles] of byAccount) {
|
|
804
|
-
const solve =
|
|
1157
|
+
const solve = Effect11.gen(function* () {
|
|
805
1158
|
const live = new Map;
|
|
806
|
-
yield*
|
|
1159
|
+
yield* Effect11.all(roles.map((role) => Effect11.map(readCloudflareRole(role.mount, role.name), (read) => {
|
|
807
1160
|
if (read?.policies !== undefined) {
|
|
808
1161
|
live.set(`${role.mount}/${role.name}`, read.policies);
|
|
809
1162
|
}
|
|
@@ -811,11 +1164,11 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
|
|
|
811
1164
|
const result = solveGroups(observationsOf(roles, live));
|
|
812
1165
|
const problems = describeProblems(result);
|
|
813
1166
|
if (problems.length > 0) {
|
|
814
|
-
return yield*
|
|
1167
|
+
return yield* Effect11.fail(new CloudflareGroupsError(`account ${account}`, problems));
|
|
815
1168
|
}
|
|
816
1169
|
return atEveryScope(result.groups);
|
|
817
1170
|
});
|
|
818
|
-
solved.set(account, yield*
|
|
1171
|
+
solved.set(account, yield* Effect11.cached(solve));
|
|
819
1172
|
}
|
|
820
1173
|
return CloudflarePermissionGroups.of({
|
|
821
1174
|
resolve: (mount, refs) => {
|
|
@@ -823,10 +1176,10 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
|
|
|
823
1176
|
const lookup = account === undefined ? undefined : solved.get(account);
|
|
824
1177
|
if (account === undefined || lookup === undefined) {
|
|
825
1178
|
const why = "no declared role on this mount, so no live role can vouch for its groups";
|
|
826
|
-
return
|
|
1179
|
+
return Effect11.fail(new CloudflareGroupsError(mount, [why]));
|
|
827
1180
|
}
|
|
828
1181
|
const hint = `no live role on account ${account} carries it; a NEW group needs the engine endpoint`;
|
|
829
|
-
return
|
|
1182
|
+
return Effect11.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
|
|
830
1183
|
}
|
|
831
1184
|
});
|
|
832
1185
|
});
|
|
@@ -835,9 +1188,9 @@ var permissionGroupsFromLiveRoles = (declared) => Effect6.gen(function* () {
|
|
|
835
1188
|
var BaoCloudflareRole = Resource3("Bao.CloudflareRole", {
|
|
836
1189
|
defaultRemovalPolicy: "retain"
|
|
837
1190
|
});
|
|
838
|
-
var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole,
|
|
1191
|
+
var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect12.gen(function* () {
|
|
839
1192
|
const groups = yield* CloudflarePermissionGroups;
|
|
840
|
-
const resolved = (props) =>
|
|
1193
|
+
const resolved = (props) => Effect12.gen(function* () {
|
|
841
1194
|
const refs = new Map;
|
|
842
1195
|
for (const policy of props.policies) {
|
|
843
1196
|
const scope = policy.scope ?? scopeOfResource(policy.resource);
|
|
@@ -847,17 +1200,17 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
847
1200
|
const ids = yield* groups.resolve(props.mount, [...refs.values()]);
|
|
848
1201
|
const { missing, policies } = resolvePolicies(props.policies, ids);
|
|
849
1202
|
if (missing.length > 0) {
|
|
850
|
-
return yield*
|
|
1203
|
+
return yield* Effect12.die(new Error(`Bao.CloudflareRole ${rolePath(props.mount, props.name)}: groups ` + `${missing.join(", ")} came back unresolved from a lookup that reported success.`));
|
|
851
1204
|
}
|
|
852
1205
|
return policies;
|
|
853
1206
|
});
|
|
854
1207
|
return BaoCloudflareRole.Provider.of({
|
|
855
|
-
list: () =>
|
|
856
|
-
read:
|
|
1208
|
+
list: () => Effect12.succeed([]),
|
|
1209
|
+
read: Effect12.fn(function* ({ olds }) {
|
|
857
1210
|
const live = yield* readCloudflareRole(olds.mount, olds.name);
|
|
858
1211
|
return live === undefined ? undefined : attributesOf4(olds, live);
|
|
859
1212
|
}),
|
|
860
|
-
diff:
|
|
1213
|
+
diff: Effect12.fn(function* ({ news, output }) {
|
|
861
1214
|
if (output === undefined || !isResolved3(news))
|
|
862
1215
|
return;
|
|
863
1216
|
if (refusalOf(news) !== undefined)
|
|
@@ -868,11 +1221,11 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
868
1221
|
return { action: "update" };
|
|
869
1222
|
return differences(news, policies, live).length === 0 ? { action: "noop" } : { action: "update" };
|
|
870
1223
|
}),
|
|
871
|
-
reconcile:
|
|
1224
|
+
reconcile: Effect12.fn(function* ({ news }) {
|
|
872
1225
|
const path = rolePath(news.mount, news.name);
|
|
873
|
-
const
|
|
874
|
-
if (
|
|
875
|
-
return yield*
|
|
1226
|
+
const refusal2 = refusalOf(news);
|
|
1227
|
+
if (refusal2 !== undefined) {
|
|
1228
|
+
return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: ${refusal2}.`));
|
|
876
1229
|
}
|
|
877
1230
|
const policies = yield* resolved(news);
|
|
878
1231
|
const live = yield* readCloudflareRole(news.mount, news.name);
|
|
@@ -881,22 +1234,22 @@ var BaoCloudflareRoleProvider = () => Provider3.effect(BaoCloudflareRole, Effect
|
|
|
881
1234
|
}
|
|
882
1235
|
const after = yield* readCloudflareRole(news.mount, news.name);
|
|
883
1236
|
if (after === undefined) {
|
|
884
|
-
return yield*
|
|
1237
|
+
return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: the write succeeded but the role is absent.`));
|
|
885
1238
|
}
|
|
886
1239
|
const left = differences(news, policies, after);
|
|
887
1240
|
if (left.length > 0) {
|
|
888
|
-
return yield*
|
|
1241
|
+
return yield* Effect12.die(new Error(`Bao.CloudflareRole ${path}: wrote cleanly but reads back different in ` + `${left.map((d) => d.field).join(", ")}. Inspect the role before retrying.`));
|
|
889
1242
|
}
|
|
890
1243
|
return attributesOf4(news, after);
|
|
891
1244
|
}),
|
|
892
|
-
delete:
|
|
1245
|
+
delete: Effect12.fn(function* ({ output }) {
|
|
893
1246
|
yield* deleteCloudflareRole(output.mount, output.name);
|
|
894
1247
|
return;
|
|
895
1248
|
})
|
|
896
1249
|
});
|
|
897
1250
|
}));
|
|
898
1251
|
// src/openbao/cloudflare-roles-expand.ts
|
|
899
|
-
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-")
|
|
1252
|
+
var slug = (name) => trimRuns(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"), "-");
|
|
900
1253
|
var zoneSlug = (zone) => `${slug(zone.name)}${zone.internal ? "-internal" : ""}`;
|
|
901
1254
|
var mountName = (account, surface) => `cloudflare-${account}-${surface}`;
|
|
902
1255
|
var policiesFor = (tpl, accountId, zone, accountGroups) => {
|
|
@@ -991,129 +1344,1014 @@ class RolesConfigError extends Error {
|
|
|
991
1344
|
}
|
|
992
1345
|
}
|
|
993
1346
|
var isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
994
|
-
var
|
|
1347
|
+
var record2 = (value, where) => {
|
|
995
1348
|
if (!isRaw(value))
|
|
996
1349
|
throw new RolesConfigError(where, "expected a mapping");
|
|
997
1350
|
return value;
|
|
998
1351
|
};
|
|
999
|
-
var
|
|
1000
|
-
const value = raw[key];
|
|
1001
|
-
if (typeof value === "string")
|
|
1002
|
-
return value;
|
|
1003
|
-
throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
|
|
1352
|
+
var text4 = (raw, key, where) => {
|
|
1353
|
+
const value = raw[key];
|
|
1354
|
+
if (typeof value === "string")
|
|
1355
|
+
return value;
|
|
1356
|
+
throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
|
|
1357
|
+
};
|
|
1358
|
+
var texts = (raw, key, where) => {
|
|
1359
|
+
const value = raw[key];
|
|
1360
|
+
if (value === undefined || value === null)
|
|
1361
|
+
return;
|
|
1362
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
1363
|
+
return value;
|
|
1364
|
+
}
|
|
1365
|
+
throw new RolesConfigError(`${where}.${key}`, "expected a list of strings");
|
|
1366
|
+
};
|
|
1367
|
+
var entries = (raw, key, where) => {
|
|
1368
|
+
const value = raw[key];
|
|
1369
|
+
if (value === undefined || value === null)
|
|
1370
|
+
return [];
|
|
1371
|
+
if (!Array.isArray(value))
|
|
1372
|
+
throw new RolesConfigError(`${where}.${key}`, "expected a list");
|
|
1373
|
+
return value;
|
|
1374
|
+
};
|
|
1375
|
+
var template = (value, where) => {
|
|
1376
|
+
const raw = record2(value, where);
|
|
1377
|
+
const groups = texts(raw, "groups", where);
|
|
1378
|
+
if (groups === undefined)
|
|
1379
|
+
throw new RolesConfigError(`${where}.groups`, "is required");
|
|
1380
|
+
const bucket = raw["bucket"] === undefined ? undefined : text4(raw, "bucket", where);
|
|
1381
|
+
if (bucket !== undefined && bucket.length === 0) {
|
|
1382
|
+
throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
|
|
1383
|
+
}
|
|
1384
|
+
const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text4(raw, "groups_scope", where));
|
|
1385
|
+
if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
|
|
1386
|
+
throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
accountGroups: texts(raw, "account_groups", where) ?? [],
|
|
1390
|
+
bucket,
|
|
1391
|
+
bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text4(raw, "bucket_jurisdiction", where),
|
|
1392
|
+
desc: raw["desc"] === undefined ? "" : text4(raw, "desc", where),
|
|
1393
|
+
groups,
|
|
1394
|
+
groupsScope,
|
|
1395
|
+
maxTtl: text4(raw, "max_ttl", where),
|
|
1396
|
+
name: text4(raw, "name", where),
|
|
1397
|
+
ttl: text4(raw, "ttl", where),
|
|
1398
|
+
zoneTypes: texts(raw, "zone_types", where) ?? [],
|
|
1399
|
+
zones: texts(raw, "zones", where)
|
|
1400
|
+
};
|
|
1401
|
+
};
|
|
1402
|
+
var surface = (name, value) => {
|
|
1403
|
+
const where = `surfaces.${name}`;
|
|
1404
|
+
const raw = record2(value, where);
|
|
1405
|
+
return {
|
|
1406
|
+
accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
|
|
1407
|
+
zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
|
|
1408
|
+
};
|
|
1409
|
+
};
|
|
1410
|
+
var account = (key, value, surfaces) => {
|
|
1411
|
+
const where = `accounts.${key}`;
|
|
1412
|
+
const raw = record2(value, where);
|
|
1413
|
+
const names = texts(raw, "surfaces", where) ?? [];
|
|
1414
|
+
for (const name of names) {
|
|
1415
|
+
if (surfaces[name] === undefined) {
|
|
1416
|
+
throw new RolesConfigError(`${where}.surfaces`, `names ${name}, which surfaces: never defines`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
|
|
1420
|
+
return { id: text4(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
|
|
1421
|
+
};
|
|
1422
|
+
var parseRolesConfig = (yaml) => {
|
|
1423
|
+
const root = record2(Bun.YAML.parse(yaml), "(document)");
|
|
1424
|
+
const surfaces = {};
|
|
1425
|
+
for (const [name, spec] of Object.entries(record2(root["surfaces"], "surfaces"))) {
|
|
1426
|
+
surfaces[name] = surface(name, spec);
|
|
1427
|
+
}
|
|
1428
|
+
const accounts = Object.entries(record2(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
|
|
1429
|
+
return {
|
|
1430
|
+
accounts,
|
|
1431
|
+
namespace: text4(record2(root["defaults"], "defaults"), "namespace", "defaults"),
|
|
1432
|
+
surfaces
|
|
1433
|
+
};
|
|
1434
|
+
};
|
|
1435
|
+
// src/openbao/host-approles.ts
|
|
1436
|
+
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1437
|
+
var HOST_ROLE_SEPARATOR = "--";
|
|
1438
|
+
var hostRoleName = (className, host) => `${className}${HOST_ROLE_SEPARATOR}${host}`;
|
|
1439
|
+
var classProblems = (name, spec) => {
|
|
1440
|
+
const found = [];
|
|
1441
|
+
if (!NAME.test(name))
|
|
1442
|
+
found.push(`class \`${name}\` is not lowercase letters, digits, single hyphens`);
|
|
1443
|
+
if (spec.policies.length === 0)
|
|
1444
|
+
found.push(`class \`${name}\` has no policies`);
|
|
1445
|
+
if (spec.policies.includes("root"))
|
|
1446
|
+
found.push(`class \`${name}\` grants \`root\``);
|
|
1447
|
+
const secretId = parseDuration(spec.secretIdTtl);
|
|
1448
|
+
if (secretId === undefined || secretId <= 0) {
|
|
1449
|
+
found.push(`class \`${name}\` secretIdTtl \`${spec.secretIdTtl}\` must be a duration above zero ` + "(0 never expires)");
|
|
1450
|
+
}
|
|
1451
|
+
const ttl = parseDuration(spec.tokenTtl);
|
|
1452
|
+
const max = parseDuration(spec.tokenMaxTtl);
|
|
1453
|
+
if (ttl === undefined || max === undefined) {
|
|
1454
|
+
found.push(`class \`${name}\` token TTLs must be durations like 15m or 1h`);
|
|
1455
|
+
} else if (max > 0 && ttl > max) {
|
|
1456
|
+
found.push(`class \`${name}\` tokenTtl is longer than tokenMaxTtl`);
|
|
1457
|
+
}
|
|
1458
|
+
return found;
|
|
1459
|
+
};
|
|
1460
|
+
var hostAppRoles = (input) => {
|
|
1461
|
+
const problems = Object.entries(input.classes).flatMap(([name, spec]) => classProblems(name, spec));
|
|
1462
|
+
const seen = new Set;
|
|
1463
|
+
const roles = [];
|
|
1464
|
+
for (const host of input.hosts) {
|
|
1465
|
+
if (!NAME.test(host.name)) {
|
|
1466
|
+
problems.push(`host \`${host.name}\` is not lowercase letters, digits, single hyphens`);
|
|
1467
|
+
}
|
|
1468
|
+
if (seen.has(host.name))
|
|
1469
|
+
problems.push(`host \`${host.name}\` is listed twice`);
|
|
1470
|
+
seen.add(host.name);
|
|
1471
|
+
const spec = input.classes[host.class];
|
|
1472
|
+
if (spec === undefined) {
|
|
1473
|
+
problems.push(`host \`${host.name}\` names unknown class \`${host.class}\``);
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
roles.push({
|
|
1477
|
+
bindSecretId: true,
|
|
1478
|
+
name: hostRoleName(host.class, host.name),
|
|
1479
|
+
secretIdNumUses: spec.secretIdNumUses ?? 0,
|
|
1480
|
+
secretIdTtl: spec.secretIdTtl,
|
|
1481
|
+
tokenMaxTtl: spec.tokenMaxTtl,
|
|
1482
|
+
tokenPolicies: [...new Set(spec.policies)].sort(),
|
|
1483
|
+
tokenTtl: spec.tokenTtl
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (problems.length > 0) {
|
|
1487
|
+
throw new Error(`hostAppRoles refused the input: ${problems.join("; ")}.`);
|
|
1488
|
+
}
|
|
1489
|
+
return roles.sort((a, b) => a.name.localeCompare(b.name));
|
|
1490
|
+
};
|
|
1491
|
+
// src/openbao/jwt-config.ts
|
|
1492
|
+
import { Resource as Resource4 } from "alchemy";
|
|
1493
|
+
import { isResolved as isResolved4 } from "alchemy/Diff";
|
|
1494
|
+
import * as Provider4 from "alchemy/Provider";
|
|
1495
|
+
import * as Effect14 from "effect/Effect";
|
|
1496
|
+
|
|
1497
|
+
// src/openbao/jwt-config-form.ts
|
|
1498
|
+
var text5 = (value) => typeof value === "string" ? value.trim() : "";
|
|
1499
|
+
var strings2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1500
|
+
var sortedSet = (values) => [...new Set(values.map((value) => value.trim()))].sort();
|
|
1501
|
+
var canonical2 = (input) => ({
|
|
1502
|
+
boundIssuer: input.boundIssuer.trim(),
|
|
1503
|
+
defaultRole: input.defaultRole.trim(),
|
|
1504
|
+
jwksCaPem: input.jwksCaPem.trim(),
|
|
1505
|
+
jwksUrl: input.jwksUrl.trim(),
|
|
1506
|
+
jwtSupportedAlgs: sortedSet(input.jwtSupportedAlgs),
|
|
1507
|
+
jwtValidationPubkeys: sortedSet(input.jwtValidationPubkeys),
|
|
1508
|
+
oidcDiscoveryCaPem: input.oidcDiscoveryCaPem.trim(),
|
|
1509
|
+
oidcDiscoveryUrl: input.oidcDiscoveryUrl.trim()
|
|
1510
|
+
});
|
|
1511
|
+
var canonicalFromProps = (props) => canonical2({
|
|
1512
|
+
boundIssuer: props.boundIssuer ?? "",
|
|
1513
|
+
defaultRole: props.defaultRole ?? "",
|
|
1514
|
+
jwksCaPem: props.jwksCaPem ?? "",
|
|
1515
|
+
jwksUrl: props.jwksUrl ?? "",
|
|
1516
|
+
jwtSupportedAlgs: props.jwtSupportedAlgs ?? [],
|
|
1517
|
+
jwtValidationPubkeys: props.jwtValidationPubkeys ?? [],
|
|
1518
|
+
oidcDiscoveryCaPem: props.oidcDiscoveryCaPem ?? "",
|
|
1519
|
+
oidcDiscoveryUrl: props.oidcDiscoveryUrl ?? ""
|
|
1520
|
+
});
|
|
1521
|
+
var canonicalFromLive = (live) => canonical2({
|
|
1522
|
+
boundIssuer: text5(live["bound_issuer"]),
|
|
1523
|
+
defaultRole: text5(live["default_role"]),
|
|
1524
|
+
jwksCaPem: text5(live["jwks_ca_pem"]),
|
|
1525
|
+
jwksUrl: text5(live["jwks_url"]),
|
|
1526
|
+
jwtSupportedAlgs: strings2(live["jwt_supported_algs"]),
|
|
1527
|
+
jwtValidationPubkeys: strings2(live["jwt_validation_pubkeys"]),
|
|
1528
|
+
oidcDiscoveryCaPem: text5(live["oidc_discovery_ca_pem"]),
|
|
1529
|
+
oidcDiscoveryUrl: text5(live["oidc_discovery_url"])
|
|
1530
|
+
});
|
|
1531
|
+
var mountOf = (props) => mountPath(props.mount ?? "jwt");
|
|
1532
|
+
var configPath = (props) => `auth/${mountOf(props)}/config`;
|
|
1533
|
+
var attributesOf5 = (props, live) => {
|
|
1534
|
+
const form = canonicalFromLive(live);
|
|
1535
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf(props) };
|
|
1536
|
+
};
|
|
1537
|
+
var matches4 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps(props)));
|
|
1538
|
+
var writeBody3 = (props) => ({
|
|
1539
|
+
bound_issuer: props.boundIssuer ?? "",
|
|
1540
|
+
default_role: props.defaultRole ?? "",
|
|
1541
|
+
jwks_ca_pem: props.jwksCaPem ?? "",
|
|
1542
|
+
jwks_url: props.jwksUrl ?? "",
|
|
1543
|
+
jwt_supported_algs: [...props.jwtSupportedAlgs ?? []],
|
|
1544
|
+
jwt_validation_pubkeys: [...props.jwtValidationPubkeys ?? []],
|
|
1545
|
+
oidc_discovery_ca_pem: props.oidcDiscoveryCaPem ?? "",
|
|
1546
|
+
oidc_discovery_url: props.oidcDiscoveryUrl ?? ""
|
|
1547
|
+
});
|
|
1548
|
+
var wouldErase = (live) => {
|
|
1549
|
+
const lost = [];
|
|
1550
|
+
if (text5(live["oidc_client_id"]) !== "")
|
|
1551
|
+
lost.push("oidc_client_id (and its unreadable secret)");
|
|
1552
|
+
const provider = live["provider_config"];
|
|
1553
|
+
if (typeof provider === "object" && provider !== null && Object.keys(provider).length > 0) {
|
|
1554
|
+
lost.push("provider_config");
|
|
1555
|
+
}
|
|
1556
|
+
if (text5(live["oidc_response_mode"]) !== "")
|
|
1557
|
+
lost.push("oidc_response_mode");
|
|
1558
|
+
if (strings2(live["oidc_response_types"]).length > 0)
|
|
1559
|
+
lost.push("oidc_response_types");
|
|
1560
|
+
if (strings2(live["override_allowed_server_names"]).length > 0) {
|
|
1561
|
+
lost.push("override_allowed_server_names");
|
|
1562
|
+
}
|
|
1563
|
+
return lost;
|
|
1564
|
+
};
|
|
1565
|
+
var problems = (props) => {
|
|
1566
|
+
const found = [];
|
|
1567
|
+
const sources = [
|
|
1568
|
+
(props.oidcDiscoveryUrl ?? "") !== "",
|
|
1569
|
+
(props.jwksUrl ?? "") !== "",
|
|
1570
|
+
(props.jwtValidationPubkeys?.length ?? 0) > 0
|
|
1571
|
+
].filter(Boolean).length;
|
|
1572
|
+
if (sources !== 1) {
|
|
1573
|
+
found.push("exactly one of oidcDiscoveryUrl, jwksUrl or jwtValidationPubkeys must be set");
|
|
1574
|
+
}
|
|
1575
|
+
if ((props.oidcDiscoveryUrl ?? "").includes(".well-known")) {
|
|
1576
|
+
found.push("oidcDiscoveryUrl must be the issuer, without /.well-known/openid-configuration");
|
|
1577
|
+
}
|
|
1578
|
+
const role = props.defaultRole ?? "";
|
|
1579
|
+
if (role !== role.toLowerCase())
|
|
1580
|
+
found.push(`defaultRole \`${role}\` must be lower case`);
|
|
1581
|
+
return found;
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
// src/openbao/role-reconcile.ts
|
|
1585
|
+
import * as Effect13 from "effect/Effect";
|
|
1586
|
+
var readRoleAt = (spec) => Effect13.map(baoRead(spec.path), (live) => live === undefined ? undefined : { attributes: spec.attributesOf(live), live });
|
|
1587
|
+
var planRole = (spec) => Effect13.gen(function* () {
|
|
1588
|
+
if (spec.problems.length > 0)
|
|
1589
|
+
return "update";
|
|
1590
|
+
const found = yield* readRoleAt(spec);
|
|
1591
|
+
return found !== undefined && spec.matches(found.attributes) ? "noop" : "update";
|
|
1592
|
+
});
|
|
1593
|
+
var refuse2 = (spec, message) => Effect13.die(new Error(`${spec.family} ${spec.path}: ${message}`));
|
|
1594
|
+
var reconcileRole = (spec) => Effect13.gen(function* () {
|
|
1595
|
+
if (spec.problems.length > 0)
|
|
1596
|
+
return yield* refuse2(spec, spec.problems.join("; "));
|
|
1597
|
+
const found = yield* readRoleAt(spec);
|
|
1598
|
+
if (found === undefined || !spec.matches(found.attributes)) {
|
|
1599
|
+
const lost = found === undefined ? [] : spec.wouldErase?.(found.live) ?? [];
|
|
1600
|
+
if (lost.length > 0) {
|
|
1601
|
+
return yield* refuse2(spec, `live carries ${lost.join(", ")}, which this resource does not declare and the write ` + "would reset. Fold it into the declaration, or clear it by hand first.");
|
|
1602
|
+
}
|
|
1603
|
+
yield* baoWrite("PUT", spec.path, spec.body);
|
|
1604
|
+
}
|
|
1605
|
+
const after = yield* readRoleAt(spec);
|
|
1606
|
+
if (after === undefined)
|
|
1607
|
+
return yield* refuse2(spec, "write reported success but it is absent.");
|
|
1608
|
+
if (!spec.matches(after.attributes)) {
|
|
1609
|
+
return yield* refuse2(spec, "wrote cleanly but read back different — OpenBao stored something other than what was " + "declared. Read it by hand before retrying.");
|
|
1610
|
+
}
|
|
1611
|
+
return after.attributes;
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// src/openbao/jwt-config.ts
|
|
1615
|
+
var BaoJwtAuthConfig = Resource4("Bao.JwtAuthConfig", {
|
|
1616
|
+
defaultRemovalPolicy: "retain"
|
|
1617
|
+
});
|
|
1618
|
+
var jwtConfigSpec = (props) => ({
|
|
1619
|
+
attributesOf: (live) => attributesOf5(props, live),
|
|
1620
|
+
body: writeBody3(props),
|
|
1621
|
+
family: "Bao.JwtAuthConfig",
|
|
1622
|
+
matches: (attributes) => matches4(attributes, props),
|
|
1623
|
+
path: configPath(props),
|
|
1624
|
+
problems: problems(props),
|
|
1625
|
+
wouldErase
|
|
1626
|
+
});
|
|
1627
|
+
var BaoJwtAuthConfigProvider = () => Provider4.effect(BaoJwtAuthConfig, Effect14.succeed(BaoJwtAuthConfig.Provider.of({
|
|
1628
|
+
list: () => Effect14.succeed([]),
|
|
1629
|
+
read: Effect14.fn(function* ({ olds }) {
|
|
1630
|
+
const found = yield* readRoleAt(jwtConfigSpec(olds));
|
|
1631
|
+
return found?.attributes;
|
|
1632
|
+
}),
|
|
1633
|
+
diff: Effect14.fn(function* ({ news, output }) {
|
|
1634
|
+
if (output === undefined || !isResolved4(news))
|
|
1635
|
+
return;
|
|
1636
|
+
if (mountOf(news) !== output.mount)
|
|
1637
|
+
return { action: "replace" };
|
|
1638
|
+
return { action: yield* planRole(jwtConfigSpec(news)) };
|
|
1639
|
+
}),
|
|
1640
|
+
reconcile: Effect14.fn(function* ({ news }) {
|
|
1641
|
+
return yield* reconcileRole(jwtConfigSpec(news));
|
|
1642
|
+
}),
|
|
1643
|
+
delete: () => Effect14.void
|
|
1644
|
+
})));
|
|
1645
|
+
// src/openbao/jwt-role.ts
|
|
1646
|
+
import { Resource as Resource5 } from "alchemy";
|
|
1647
|
+
import { isResolved as isResolved5 } from "alchemy/Diff";
|
|
1648
|
+
import * as Provider5 from "alchemy/Provider";
|
|
1649
|
+
import * as Effect15 from "effect/Effect";
|
|
1650
|
+
|
|
1651
|
+
// src/openbao/auth-token-form.ts
|
|
1652
|
+
var sortedSet2 = (values) => [...new Set(values)].sort();
|
|
1653
|
+
var cidr = (value) => value.trim().replace(/\/(32|128)$/, "");
|
|
1654
|
+
var strings3 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1655
|
+
var seconds2 = (text6) => parseDuration(text6 ?? "0") ?? -1;
|
|
1656
|
+
var tokenFields = (input) => ({
|
|
1657
|
+
tokenBoundCidrs: input.tokenBoundCidrs,
|
|
1658
|
+
tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
|
|
1659
|
+
tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
|
|
1660
|
+
tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
|
|
1661
|
+
tokenNumUses: input.tokenNumUses,
|
|
1662
|
+
tokenPeriodSeconds: input.tokenPeriodSeconds,
|
|
1663
|
+
tokenPolicies: input.tokenPolicies,
|
|
1664
|
+
tokenTtlSeconds: input.tokenTtlSeconds,
|
|
1665
|
+
tokenType: input.tokenType
|
|
1666
|
+
});
|
|
1667
|
+
var tokenForm = (input) => ({
|
|
1668
|
+
tokenBoundCidrs: sortedSet2(input.tokenBoundCidrs.map(cidr)),
|
|
1669
|
+
tokenExplicitMaxTtlSeconds: input.tokenExplicitMaxTtlSeconds,
|
|
1670
|
+
tokenMaxTtlSeconds: input.tokenMaxTtlSeconds,
|
|
1671
|
+
tokenNoDefaultPolicy: input.tokenNoDefaultPolicy,
|
|
1672
|
+
tokenNumUses: input.tokenNumUses,
|
|
1673
|
+
tokenPeriodSeconds: input.tokenPeriodSeconds,
|
|
1674
|
+
tokenPolicies: sortedSet2(input.tokenPolicies),
|
|
1675
|
+
tokenTtlSeconds: input.tokenTtlSeconds,
|
|
1676
|
+
tokenType: input.tokenType
|
|
1677
|
+
});
|
|
1678
|
+
var tokenFormOfProps = (props) => tokenForm({
|
|
1679
|
+
tokenBoundCidrs: props.tokenBoundCidrs ?? [],
|
|
1680
|
+
tokenExplicitMaxTtlSeconds: seconds2(props.tokenExplicitMaxTtl),
|
|
1681
|
+
tokenMaxTtlSeconds: seconds2(props.tokenMaxTtl),
|
|
1682
|
+
tokenNoDefaultPolicy: props.tokenNoDefaultPolicy ?? false,
|
|
1683
|
+
tokenNumUses: props.tokenNumUses ?? 0,
|
|
1684
|
+
tokenPeriodSeconds: seconds2(props.tokenPeriod),
|
|
1685
|
+
tokenPolicies: props.tokenPolicies,
|
|
1686
|
+
tokenTtlSeconds: seconds2(props.tokenTtl),
|
|
1687
|
+
tokenType: props.tokenType ?? "default"
|
|
1688
|
+
});
|
|
1689
|
+
var tokenFormOfLive = (live) => tokenForm({
|
|
1690
|
+
tokenBoundCidrs: strings3(live["token_bound_cidrs"]),
|
|
1691
|
+
tokenExplicitMaxTtlSeconds: ttlSeconds(live["token_explicit_max_ttl"]) ?? -1,
|
|
1692
|
+
tokenMaxTtlSeconds: ttlSeconds(live["token_max_ttl"]) ?? -1,
|
|
1693
|
+
tokenNoDefaultPolicy: live["token_no_default_policy"] === true,
|
|
1694
|
+
tokenNumUses: typeof live["token_num_uses"] === "number" ? live["token_num_uses"] : -1,
|
|
1695
|
+
tokenPeriodSeconds: ttlSeconds(live["token_period"]) ?? -1,
|
|
1696
|
+
tokenPolicies: strings3(live["token_policies"]),
|
|
1697
|
+
tokenTtlSeconds: ttlSeconds(live["token_ttl"]) ?? -1,
|
|
1698
|
+
tokenType: typeof live["token_type"] === "string" ? live["token_type"] : ""
|
|
1699
|
+
});
|
|
1700
|
+
var tokenBody = (props) => ({
|
|
1701
|
+
token_bound_cidrs: [...props.tokenBoundCidrs ?? []],
|
|
1702
|
+
token_explicit_max_ttl: props.tokenExplicitMaxTtl ?? "0",
|
|
1703
|
+
token_max_ttl: props.tokenMaxTtl ?? "0",
|
|
1704
|
+
token_no_default_policy: props.tokenNoDefaultPolicy ?? false,
|
|
1705
|
+
token_num_uses: props.tokenNumUses ?? 0,
|
|
1706
|
+
token_period: props.tokenPeriod ?? "0",
|
|
1707
|
+
token_policies: [...props.tokenPolicies],
|
|
1708
|
+
token_ttl: props.tokenTtl ?? "0",
|
|
1709
|
+
token_type: props.tokenType ?? "default"
|
|
1710
|
+
});
|
|
1711
|
+
var tokenProblems = (props) => {
|
|
1712
|
+
const found = [];
|
|
1713
|
+
const durations = {
|
|
1714
|
+
tokenExplicitMaxTtl: props.tokenExplicitMaxTtl,
|
|
1715
|
+
tokenMaxTtl: props.tokenMaxTtl,
|
|
1716
|
+
tokenPeriod: props.tokenPeriod,
|
|
1717
|
+
tokenTtl: props.tokenTtl
|
|
1718
|
+
};
|
|
1719
|
+
for (const [field, text6] of Object.entries(durations)) {
|
|
1720
|
+
if (seconds2(text6) < 0)
|
|
1721
|
+
found.push(`${field}=${text6 ?? ""} is not a duration (0 or <n>s|m|h|d)`);
|
|
1722
|
+
}
|
|
1723
|
+
const ttl = seconds2(props.tokenTtl);
|
|
1724
|
+
const max = seconds2(props.tokenMaxTtl);
|
|
1725
|
+
if (ttl > 0 && max > 0 && ttl > max)
|
|
1726
|
+
found.push("tokenTtl is longer than tokenMaxTtl");
|
|
1727
|
+
if (props.tokenType === "batch" && (seconds2(props.tokenPeriod) > 0 || (props.tokenNumUses ?? 0) > 0)) {
|
|
1728
|
+
found.push("a batch token cannot be periodic or use-limited (tokenutil.go:234-241)");
|
|
1729
|
+
}
|
|
1730
|
+
return found;
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
// src/openbao/jwt-role-form.ts
|
|
1734
|
+
var sortedSet3 = (values) => [...new Set(values)].sort();
|
|
1735
|
+
var text6 = (value) => typeof value === "string" ? value : "";
|
|
1736
|
+
var strings4 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1737
|
+
var record3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1738
|
+
var claimsOf = (value) => {
|
|
1739
|
+
const source = record3(value);
|
|
1740
|
+
const out = {};
|
|
1741
|
+
for (const key of Object.keys(source).sort()) {
|
|
1742
|
+
const raw = source[key];
|
|
1743
|
+
out[key] = sortedSet3(typeof raw === "string" ? [raw] : strings4(raw));
|
|
1744
|
+
}
|
|
1745
|
+
return out;
|
|
1746
|
+
};
|
|
1747
|
+
var mappingsOf = (value) => {
|
|
1748
|
+
const source = record3(value);
|
|
1749
|
+
const out = {};
|
|
1750
|
+
for (const key of Object.keys(source).sort())
|
|
1751
|
+
out[key] = text6(source[key]);
|
|
1752
|
+
return out;
|
|
1753
|
+
};
|
|
1754
|
+
var canonical3 = (input) => ({
|
|
1755
|
+
allowedRedirectUris: sortedSet3(input.allowedRedirectUris),
|
|
1756
|
+
boundAudiences: sortedSet3(input.boundAudiences),
|
|
1757
|
+
boundClaims: claimsOf(input.boundClaims),
|
|
1758
|
+
boundClaimsType: input.boundClaimsType,
|
|
1759
|
+
boundSubject: input.boundSubject,
|
|
1760
|
+
callbackMode: input.callbackMode,
|
|
1761
|
+
claimMappings: mappingsOf(input.claimMappings),
|
|
1762
|
+
groupsClaim: input.groupsClaim,
|
|
1763
|
+
oidcDisableConfirmation: input.oidcDisableConfirmation,
|
|
1764
|
+
oidcScopes: sortedSet3(input.oidcScopes),
|
|
1765
|
+
roleType: input.roleType,
|
|
1766
|
+
userClaim: input.userClaim,
|
|
1767
|
+
verboseOidcLogging: input.verboseOidcLogging,
|
|
1768
|
+
...tokenFields(input)
|
|
1769
|
+
});
|
|
1770
|
+
var canonicalFromProps2 = (props) => canonical3({
|
|
1771
|
+
...tokenFormOfProps(props),
|
|
1772
|
+
allowedRedirectUris: props.allowedRedirectUris ?? [],
|
|
1773
|
+
boundAudiences: props.boundAudiences ?? [],
|
|
1774
|
+
boundClaims: claimsOf(props.boundClaims ?? {}),
|
|
1775
|
+
boundClaimsType: props.boundClaimsType ?? "string",
|
|
1776
|
+
boundSubject: props.boundSubject ?? "",
|
|
1777
|
+
callbackMode: props.callbackMode ?? "client",
|
|
1778
|
+
claimMappings: props.claimMappings ?? {},
|
|
1779
|
+
groupsClaim: props.groupsClaim ?? "",
|
|
1780
|
+
oidcDisableConfirmation: false,
|
|
1781
|
+
oidcScopes: props.oidcScopes ?? [],
|
|
1782
|
+
roleType: props.roleType,
|
|
1783
|
+
userClaim: props.userClaim,
|
|
1784
|
+
verboseOidcLogging: false
|
|
1785
|
+
});
|
|
1786
|
+
var canonicalFromLive2 = (live) => canonical3({
|
|
1787
|
+
...tokenFormOfLive(live),
|
|
1788
|
+
allowedRedirectUris: strings4(live["allowed_redirect_uris"]),
|
|
1789
|
+
boundAudiences: strings4(live["bound_audiences"]),
|
|
1790
|
+
boundClaims: claimsOf(live["bound_claims"]),
|
|
1791
|
+
boundClaimsType: text6(live["bound_claims_type"]),
|
|
1792
|
+
boundSubject: text6(live["bound_subject"]),
|
|
1793
|
+
callbackMode: text6(live["callback_mode"]),
|
|
1794
|
+
claimMappings: mappingsOf(live["claim_mappings"]),
|
|
1795
|
+
groupsClaim: text6(live["groups_claim"]),
|
|
1796
|
+
oidcDisableConfirmation: live["oidc_disable_confirmation"] === true,
|
|
1797
|
+
oidcScopes: strings4(live["oidc_scopes"]),
|
|
1798
|
+
roleType: text6(live["role_type"]),
|
|
1799
|
+
userClaim: text6(live["user_claim"]),
|
|
1800
|
+
verboseOidcLogging: live["verbose_oidc_logging"] === true
|
|
1801
|
+
});
|
|
1802
|
+
var mountOf2 = (props) => mountPath(props.mount ?? "jwt");
|
|
1803
|
+
var rolePath2 = (props) => `auth/${mountOf2(props)}/role/${props.name}`;
|
|
1804
|
+
var attributesOf6 = (props, live) => {
|
|
1805
|
+
const form = canonicalFromLive2(live);
|
|
1806
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf2(props), name: props.name };
|
|
1807
|
+
};
|
|
1808
|
+
var matches5 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps2(props)));
|
|
1809
|
+
var writeBody4 = (props) => ({
|
|
1810
|
+
allowed_redirect_uris: [...props.allowedRedirectUris ?? []],
|
|
1811
|
+
bound_audiences: [...props.boundAudiences ?? []],
|
|
1812
|
+
bound_claims: { ...props.boundClaims },
|
|
1813
|
+
bound_claims_type: props.boundClaimsType ?? "string",
|
|
1814
|
+
bound_subject: props.boundSubject ?? "",
|
|
1815
|
+
callback_mode: props.callbackMode ?? "client",
|
|
1816
|
+
claim_mappings: { ...props.claimMappings },
|
|
1817
|
+
groups_claim: props.groupsClaim ?? "",
|
|
1818
|
+
oidc_disable_confirmation: false,
|
|
1819
|
+
oidc_scopes: [...props.oidcScopes ?? []],
|
|
1820
|
+
role_type: props.roleType,
|
|
1821
|
+
user_claim: props.userClaim,
|
|
1822
|
+
verbose_oidc_logging: false,
|
|
1823
|
+
...tokenBody(props)
|
|
1824
|
+
});
|
|
1825
|
+
var NAME2 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
|
|
1826
|
+
var problems2 = (props) => {
|
|
1827
|
+
const found = [...tokenProblems(props)];
|
|
1828
|
+
if (!NAME2.test(props.name))
|
|
1829
|
+
found.push(`name \`${props.name}\` must be lower case (the server folds it)`);
|
|
1830
|
+
if (props.userClaim.trim() === "")
|
|
1831
|
+
found.push("userClaim is empty; the server requires one");
|
|
1832
|
+
const bound = (props.boundAudiences?.length ?? 0) > 0 || (props.boundSubject ?? "") !== "" || Object.keys(props.boundClaims ?? {}).length > 0 || (props.tokenBoundCidrs?.length ?? 0) > 0;
|
|
1833
|
+
if (props.roleType === "jwt" && !bound) {
|
|
1834
|
+
found.push("a jwt role needs a bound constraint: boundAudiences, boundSubject, boundClaims or tokenBoundCidrs");
|
|
1835
|
+
}
|
|
1836
|
+
if (props.roleType === "oidc" && (props.allowedRedirectUris?.length ?? 0) === 0) {
|
|
1837
|
+
found.push("an oidc role needs allowedRedirectUris");
|
|
1838
|
+
}
|
|
1839
|
+
const targets = Object.values(props.claimMappings ?? {});
|
|
1840
|
+
if (targets.includes("role"))
|
|
1841
|
+
found.push("claimMappings may not map to the reserved key `role`");
|
|
1842
|
+
if (new Set(targets).size !== targets.length)
|
|
1843
|
+
found.push("claimMappings maps two claims to one key");
|
|
1844
|
+
return found;
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
// src/openbao/jwt-role.ts
|
|
1848
|
+
var BaoJwtRole = Resource5("Bao.JwtRole", {
|
|
1849
|
+
defaultRemovalPolicy: "retain"
|
|
1850
|
+
});
|
|
1851
|
+
var jwtRoleSpec = (props) => ({
|
|
1852
|
+
attributesOf: (live) => attributesOf6(props, live),
|
|
1853
|
+
body: writeBody4(props),
|
|
1854
|
+
family: "Bao.JwtRole",
|
|
1855
|
+
matches: (attributes) => matches5(attributes, props),
|
|
1856
|
+
path: rolePath2(props),
|
|
1857
|
+
problems: problems2(props)
|
|
1858
|
+
});
|
|
1859
|
+
var BaoJwtRoleProvider = () => Provider5.effect(BaoJwtRole, Effect15.succeed(BaoJwtRole.Provider.of({
|
|
1860
|
+
list: () => Effect15.succeed([]),
|
|
1861
|
+
read: Effect15.fn(function* ({ olds }) {
|
|
1862
|
+
const found = yield* readRoleAt(jwtRoleSpec(olds));
|
|
1863
|
+
return found?.attributes;
|
|
1864
|
+
}),
|
|
1865
|
+
diff: Effect15.fn(function* ({ news, output }) {
|
|
1866
|
+
if (output === undefined || !isResolved5(news))
|
|
1867
|
+
return;
|
|
1868
|
+
if (mountOf2(news) !== output.mount || news.name !== output.name) {
|
|
1869
|
+
return { action: "replace" };
|
|
1870
|
+
}
|
|
1871
|
+
return { action: yield* planRole(jwtRoleSpec(news)) };
|
|
1872
|
+
}),
|
|
1873
|
+
reconcile: Effect15.fn(function* ({ news }) {
|
|
1874
|
+
return yield* reconcileRole(jwtRoleSpec(news));
|
|
1875
|
+
}),
|
|
1876
|
+
delete: Effect15.fn(function* ({ output }) {
|
|
1877
|
+
yield* baoDelete(rolePath2(output));
|
|
1878
|
+
return;
|
|
1879
|
+
})
|
|
1880
|
+
})));
|
|
1881
|
+
// src/openbao/kubernetes-role.ts
|
|
1882
|
+
import { Resource as Resource6 } from "alchemy";
|
|
1883
|
+
import { isResolved as isResolved6 } from "alchemy/Diff";
|
|
1884
|
+
import * as Provider6 from "alchemy/Provider";
|
|
1885
|
+
import * as Effect16 from "effect/Effect";
|
|
1886
|
+
|
|
1887
|
+
// src/openbao/kubernetes-role-form.ts
|
|
1888
|
+
var sortedSet4 = (values) => [...new Set(values)].sort();
|
|
1889
|
+
var text7 = (value) => typeof value === "string" ? value : "";
|
|
1890
|
+
var strings5 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1891
|
+
var canonical4 = (input) => ({
|
|
1892
|
+
aliasNameSource: input.aliasNameSource,
|
|
1893
|
+
audience: input.audience,
|
|
1894
|
+
boundServiceAccountNames: sortedSet4(input.boundServiceAccountNames),
|
|
1895
|
+
boundServiceAccountNamespaceSelector: input.boundServiceAccountNamespaceSelector.trim(),
|
|
1896
|
+
boundServiceAccountNamespaces: sortedSet4(input.boundServiceAccountNamespaces),
|
|
1897
|
+
...tokenFields(input)
|
|
1898
|
+
});
|
|
1899
|
+
var canonicalFromProps3 = (props) => canonical4({
|
|
1900
|
+
...tokenFormOfProps(props),
|
|
1901
|
+
aliasNameSource: props.aliasNameSource,
|
|
1902
|
+
audience: props.audience ?? "",
|
|
1903
|
+
boundServiceAccountNames: props.boundServiceAccountNames,
|
|
1904
|
+
boundServiceAccountNamespaceSelector: props.boundServiceAccountNamespaceSelector ?? "",
|
|
1905
|
+
boundServiceAccountNamespaces: props.boundServiceAccountNamespaces ?? []
|
|
1906
|
+
});
|
|
1907
|
+
var canonicalFromLive3 = (live) => canonical4({
|
|
1908
|
+
...tokenFormOfLive(live),
|
|
1909
|
+
aliasNameSource: text7(live["alias_name_source"]),
|
|
1910
|
+
audience: text7(live["audience"]),
|
|
1911
|
+
boundServiceAccountNames: strings5(live["bound_service_account_names"]),
|
|
1912
|
+
boundServiceAccountNamespaceSelector: text7(live["bound_service_account_namespace_selector"]),
|
|
1913
|
+
boundServiceAccountNamespaces: strings5(live["bound_service_account_namespaces"])
|
|
1914
|
+
});
|
|
1915
|
+
var mountOf3 = (props) => mountPath(props.mount ?? "kubernetes");
|
|
1916
|
+
var rolePath3 = (props) => `auth/${mountOf3(props)}/role/${props.name}`;
|
|
1917
|
+
var attributesOf7 = (props, live) => {
|
|
1918
|
+
const form = canonicalFromLive3(live);
|
|
1919
|
+
return { ...form, digest: sha256(JSON.stringify(form)), mount: mountOf3(props), name: props.name };
|
|
1920
|
+
};
|
|
1921
|
+
var matches6 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps3(props)));
|
|
1922
|
+
var writeBody5 = (props) => ({
|
|
1923
|
+
alias_name_source: props.aliasNameSource,
|
|
1924
|
+
audience: props.audience ?? "",
|
|
1925
|
+
bound_service_account_names: [...props.boundServiceAccountNames],
|
|
1926
|
+
bound_service_account_namespace_selector: props.boundServiceAccountNamespaceSelector ?? "",
|
|
1927
|
+
bound_service_account_namespaces: [...props.boundServiceAccountNamespaces ?? []],
|
|
1928
|
+
...tokenBody(props)
|
|
1929
|
+
});
|
|
1930
|
+
var NAME3 = /^[a-z0-9_](?:[a-z0-9_.-]*[a-z0-9_])?$/;
|
|
1931
|
+
var starMixed = (values) => values.length > 1 && values.includes("*");
|
|
1932
|
+
var problems3 = (props) => {
|
|
1933
|
+
const found = [...tokenProblems(props)];
|
|
1934
|
+
const namespaces = props.boundServiceAccountNamespaces ?? [];
|
|
1935
|
+
const selector = (props.boundServiceAccountNamespaceSelector ?? "").trim();
|
|
1936
|
+
if (!NAME3.test(props.name))
|
|
1937
|
+
found.push(`name \`${props.name}\` must be lower case (stored folded)`);
|
|
1938
|
+
if (props.boundServiceAccountNames.length === 0)
|
|
1939
|
+
found.push("boundServiceAccountNames is empty");
|
|
1940
|
+
if (starMixed(props.boundServiceAccountNames))
|
|
1941
|
+
found.push("`*` mixed with other account names");
|
|
1942
|
+
if (namespaces.length === 0 && selector === "") {
|
|
1943
|
+
found.push("boundServiceAccountNamespaces is empty and no namespace selector is set");
|
|
1944
|
+
}
|
|
1945
|
+
if (starMixed(namespaces))
|
|
1946
|
+
found.push("`*` mixed with other namespaces");
|
|
1947
|
+
return found;
|
|
1004
1948
|
};
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1949
|
+
|
|
1950
|
+
// src/openbao/kubernetes-role.ts
|
|
1951
|
+
var BaoKubernetesRole = Resource6("Bao.KubernetesRole", {
|
|
1952
|
+
defaultRemovalPolicy: "retain"
|
|
1953
|
+
});
|
|
1954
|
+
var kubernetesRoleSpec = (props) => ({
|
|
1955
|
+
attributesOf: (live) => attributesOf7(props, live),
|
|
1956
|
+
body: writeBody5(props),
|
|
1957
|
+
family: "Bao.KubernetesRole",
|
|
1958
|
+
matches: (attributes) => matches6(attributes, props),
|
|
1959
|
+
path: rolePath3(props),
|
|
1960
|
+
problems: problems3(props)
|
|
1961
|
+
});
|
|
1962
|
+
var BaoKubernetesRoleProvider = () => Provider6.effect(BaoKubernetesRole, Effect16.succeed(BaoKubernetesRole.Provider.of({
|
|
1963
|
+
list: () => Effect16.succeed([]),
|
|
1964
|
+
read: Effect16.fn(function* ({ olds }) {
|
|
1965
|
+
const found = yield* readRoleAt(kubernetesRoleSpec(olds));
|
|
1966
|
+
return found?.attributes;
|
|
1967
|
+
}),
|
|
1968
|
+
diff: Effect16.fn(function* ({ news, output }) {
|
|
1969
|
+
if (output === undefined || !isResolved6(news))
|
|
1970
|
+
return;
|
|
1971
|
+
if (mountOf3(news) !== output.mount || news.name !== output.name) {
|
|
1972
|
+
return { action: "replace" };
|
|
1973
|
+
}
|
|
1974
|
+
return { action: yield* planRole(kubernetesRoleSpec(news)) };
|
|
1975
|
+
}),
|
|
1976
|
+
reconcile: Effect16.fn(function* ({ news }) {
|
|
1977
|
+
return yield* reconcileRole(kubernetesRoleSpec(news));
|
|
1978
|
+
}),
|
|
1979
|
+
delete: Effect16.fn(function* ({ output }) {
|
|
1980
|
+
yield* baoDelete(rolePath3(output));
|
|
1008
1981
|
return;
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1982
|
+
})
|
|
1983
|
+
})));
|
|
1984
|
+
// src/openbao/mfa-enforcement.ts
|
|
1985
|
+
import { Resource as Resource7 } from "alchemy";
|
|
1986
|
+
import { isResolved as isResolved7 } from "alchemy/Diff";
|
|
1987
|
+
import * as Provider7 from "alchemy/Provider";
|
|
1988
|
+
import * as Effect18 from "effect/Effect";
|
|
1989
|
+
|
|
1990
|
+
// src/openbao/mfa-enforcement-form.ts
|
|
1991
|
+
var sortedSet5 = (values) => [...new Set(values.map((value) => value.trim()))].filter((value) => value !== "").sort();
|
|
1992
|
+
var strings6 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1993
|
+
var text8 = (value) => typeof value === "string" ? value : "";
|
|
1994
|
+
var canonical5 = (input) => ({
|
|
1995
|
+
authMethodAccessors: sortedSet5(input.authMethodAccessors),
|
|
1996
|
+
authMethodTypes: sortedSet5(input.authMethodTypes),
|
|
1997
|
+
identityEntityIds: sortedSet5(input.identityEntityIds),
|
|
1998
|
+
identityGroupIds: sortedSet5(input.identityGroupIds),
|
|
1999
|
+
mfaMethodIds: sortedSet5(input.mfaMethodIds)
|
|
2000
|
+
});
|
|
2001
|
+
var canonicalFromProps4 = (props, resolved) => canonical5({
|
|
2002
|
+
authMethodAccessors: [...props.authMethodAccessors ?? [], ...resolved],
|
|
2003
|
+
authMethodTypes: props.authMethodTypes ?? [],
|
|
2004
|
+
identityEntityIds: props.identityEntityIds ?? [],
|
|
2005
|
+
identityGroupIds: props.identityGroupIds ?? [],
|
|
2006
|
+
mfaMethodIds: props.mfaMethodIds
|
|
2007
|
+
});
|
|
2008
|
+
var canonicalFromLive4 = (live) => canonical5({
|
|
2009
|
+
authMethodAccessors: strings6(live["auth_method_accessors"]),
|
|
2010
|
+
authMethodTypes: strings6(live["auth_method_types"]),
|
|
2011
|
+
identityEntityIds: strings6(live["identity_entity_ids"]),
|
|
2012
|
+
identityGroupIds: strings6(live["identity_group_ids"]),
|
|
2013
|
+
mfaMethodIds: strings6(live["mfa_method_ids"])
|
|
2014
|
+
});
|
|
2015
|
+
var attributesOf8 = (name, live) => {
|
|
2016
|
+
const form = canonicalFromLive4(live);
|
|
2017
|
+
return {
|
|
2018
|
+
...form,
|
|
2019
|
+
digest: sha256(JSON.stringify(form)),
|
|
2020
|
+
enforcementId: text8(live["id"]),
|
|
2021
|
+
name,
|
|
2022
|
+
namespacePath: text8(live["namespace_path"])
|
|
2023
|
+
};
|
|
1013
2024
|
};
|
|
1014
|
-
var
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
2025
|
+
var matches7 = (attributes, props, resolved) => attributes.digest === sha256(JSON.stringify(canonicalFromProps4(props, resolved)));
|
|
2026
|
+
var writeBody6 = (form) => ({
|
|
2027
|
+
auth_method_accessors: [...form.authMethodAccessors],
|
|
2028
|
+
auth_method_types: [...form.authMethodTypes],
|
|
2029
|
+
identity_entity_ids: [...form.identityEntityIds],
|
|
2030
|
+
identity_group_ids: [...form.identityGroupIds],
|
|
2031
|
+
mfa_method_ids: [...form.mfaMethodIds]
|
|
2032
|
+
});
|
|
2033
|
+
var NAME4 = /^\w(?:[\w.-]*\w)?$/;
|
|
2034
|
+
var problems4 = (props, missingPaths) => {
|
|
2035
|
+
const found = [];
|
|
2036
|
+
if (!NAME4.test(props.name))
|
|
2037
|
+
found.push(`name \`${props.name}\` is not a valid enforcement name`);
|
|
2038
|
+
if (sortedSet5(props.mfaMethodIds).length === 0)
|
|
2039
|
+
found.push("mfaMethodIds is empty");
|
|
2040
|
+
const targets = [
|
|
2041
|
+
props.authMethodPaths,
|
|
2042
|
+
props.authMethodAccessors,
|
|
2043
|
+
props.authMethodTypes,
|
|
2044
|
+
props.identityGroupIds,
|
|
2045
|
+
props.identityEntityIds
|
|
2046
|
+
].some((list) => sortedSet5(list ?? []).length > 0);
|
|
2047
|
+
if (!targets)
|
|
2048
|
+
found.push("no target: an enforcement with none is accepted and enforces NOTHING");
|
|
2049
|
+
if (missingPaths.length > 0) {
|
|
2050
|
+
found.push(`no auth mount at ${missingPaths.map((path) => mountPath(path)).join(", ")}`);
|
|
2051
|
+
}
|
|
2052
|
+
return found;
|
|
1021
2053
|
};
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
2054
|
+
|
|
2055
|
+
// src/openbao/mfa-wire.ts
|
|
2056
|
+
import * as Effect17 from "effect/Effect";
|
|
2057
|
+
var TOTP_PATH = "identity/mfa/method/totp";
|
|
2058
|
+
var METHODS_PATH = "identity/mfa/method";
|
|
2059
|
+
var enforcementPath = (name) => `identity/mfa/login-enforcement/${name}`;
|
|
2060
|
+
var record4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
2061
|
+
var ownNamespacePath = Effect17.gen(function* () {
|
|
2062
|
+
const env = yield* BaoEnv;
|
|
2063
|
+
return namespacePath(envNamespace(env));
|
|
2064
|
+
});
|
|
2065
|
+
var findTotp = (name) => Effect17.gen(function* () {
|
|
2066
|
+
const listing = yield* baoRead(`${METHODS_PATH}?list=true`);
|
|
2067
|
+
const info = record4(listing?.["key_info"]) ?? {};
|
|
2068
|
+
const own = yield* ownNamespacePath;
|
|
2069
|
+
for (const [id, raw] of Object.entries(info)) {
|
|
2070
|
+
const live = record4(raw);
|
|
2071
|
+
if (live === undefined || live["name"] !== name || live["namespace_path"] !== own)
|
|
2072
|
+
continue;
|
|
2073
|
+
if (live["type"] !== "totp") {
|
|
2074
|
+
return yield* Effect17.die(new Error(`Bao.MfaTotpMethod ${name}: that name belongs to a ${String(live["type"])} method ` + `(${id}) in this namespace, and a TOTP write would convert it. Pick another name.`));
|
|
2075
|
+
}
|
|
2076
|
+
return { id, live };
|
|
1030
2077
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
2078
|
+
return;
|
|
2079
|
+
});
|
|
2080
|
+
var writeTotp = (body) => Effect17.asVoid(baoCall("write", "POST", TOTP_PATH, body));
|
|
2081
|
+
var deleteTotp = (id) => baoDelete(`${TOTP_PATH}/${id}`);
|
|
2082
|
+
var readEnforcement = (name) => baoRead(enforcementPath(name));
|
|
2083
|
+
var writeEnforcement = (name, body) => Effect17.asVoid(baoCall("write", "POST", enforcementPath(name), body));
|
|
2084
|
+
var authAccessors = (paths) => Effect17.gen(function* () {
|
|
2085
|
+
if (paths.length === 0)
|
|
2086
|
+
return { accessors: [], missing: [] };
|
|
2087
|
+
const table = (yield* baoRead("sys/auth")) ?? {};
|
|
2088
|
+
const accessors = [];
|
|
2089
|
+
const missing = [];
|
|
2090
|
+
for (const path of paths) {
|
|
2091
|
+
const accessor = record4(table[`${mountPath(path)}/`])?.["accessor"];
|
|
2092
|
+
if (typeof accessor === "string" && accessor !== "")
|
|
2093
|
+
accessors.push(accessor);
|
|
2094
|
+
else
|
|
2095
|
+
missing.push(mountPath(path));
|
|
2096
|
+
}
|
|
2097
|
+
return { accessors, missing };
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
// src/openbao/mfa-enforcement.ts
|
|
2101
|
+
var BaoMfaLoginEnforcement = Resource7("Bao.MfaLoginEnforcement", {
|
|
2102
|
+
defaultRemovalPolicy: "retain"
|
|
2103
|
+
});
|
|
2104
|
+
var refuse3 = (name, message) => Effect18.die(new Error(`Bao.MfaLoginEnforcement ${name}: ${message}`));
|
|
2105
|
+
var resolveTargets = (props) => Effect18.map(authAccessors(props.authMethodPaths ?? []), ({ accessors, missing }) => ({
|
|
2106
|
+
bad: problems4(props, missing),
|
|
2107
|
+
resolved: accessors
|
|
2108
|
+
}));
|
|
2109
|
+
var readLive = (name) => Effect18.map(readEnforcement(name), (live) => live === undefined ? undefined : attributesOf8(name, live));
|
|
2110
|
+
var planEnforcement = (props) => Effect18.gen(function* () {
|
|
2111
|
+
const { bad, resolved } = yield* resolveTargets(props);
|
|
2112
|
+
if (bad.length > 0)
|
|
2113
|
+
return "update";
|
|
2114
|
+
const live = yield* readLive(props.name);
|
|
2115
|
+
return live !== undefined && matches7(live, props, resolved) ? "noop" : "update";
|
|
2116
|
+
});
|
|
2117
|
+
var reconcileEnforcement = (props) => Effect18.gen(function* () {
|
|
2118
|
+
const { bad, resolved } = yield* resolveTargets(props);
|
|
2119
|
+
if (bad.length > 0)
|
|
2120
|
+
return yield* refuse3(props.name, bad.join("; "));
|
|
2121
|
+
const live = yield* readLive(props.name);
|
|
2122
|
+
if (live === undefined || !matches7(live, props, resolved)) {
|
|
2123
|
+
yield* writeEnforcement(props.name, writeBody6(canonicalFromProps4(props, resolved)));
|
|
1034
2124
|
}
|
|
2125
|
+
const after = yield* readLive(props.name);
|
|
2126
|
+
if (after === undefined)
|
|
2127
|
+
return yield* refuse3(props.name, "the write succeeded but it is absent.");
|
|
2128
|
+
if (!matches7(after, props, resolved)) {
|
|
2129
|
+
return yield* refuse3(props.name, "the enforcement read back different.");
|
|
2130
|
+
}
|
|
2131
|
+
return after;
|
|
2132
|
+
});
|
|
2133
|
+
var BaoMfaLoginEnforcementProvider = () => Provider7.effect(BaoMfaLoginEnforcement, Effect18.succeed(BaoMfaLoginEnforcement.Provider.of({
|
|
2134
|
+
list: () => Effect18.succeed([]),
|
|
2135
|
+
read: Effect18.fn(function* ({ olds }) {
|
|
2136
|
+
return yield* readLive(olds.name);
|
|
2137
|
+
}),
|
|
2138
|
+
diff: Effect18.fn(function* ({ news, output }) {
|
|
2139
|
+
if (output === undefined || !isResolved7(news))
|
|
2140
|
+
return;
|
|
2141
|
+
if (news.name !== output.name)
|
|
2142
|
+
return { action: "replace" };
|
|
2143
|
+
return { action: yield* planEnforcement(news) };
|
|
2144
|
+
}),
|
|
2145
|
+
reconcile: Effect18.fn(function* ({ news }) {
|
|
2146
|
+
return yield* reconcileEnforcement(news);
|
|
2147
|
+
}),
|
|
2148
|
+
delete: Effect18.fn(function* ({ output }) {
|
|
2149
|
+
return yield* refuse3(output.name, "REFUSING to delete: OpenBao 2.6.2 drops a login enforcement from memory only, and it " + "returns on the next unseal (openbao/openbao#4030). Set this resource back to " + "retain to drop it from state; remove it for good only once the server is fixed.");
|
|
2150
|
+
})
|
|
2151
|
+
})));
|
|
2152
|
+
// src/openbao/mfa-totp.ts
|
|
2153
|
+
import { Resource as Resource8 } from "alchemy";
|
|
2154
|
+
import { isResolved as isResolved8 } from "alchemy/Diff";
|
|
2155
|
+
import * as Provider8 from "alchemy/Provider";
|
|
2156
|
+
import * as Effect19 from "effect/Effect";
|
|
2157
|
+
|
|
2158
|
+
// src/openbao/mfa-totp-form.ts
|
|
2159
|
+
var canonical6 = (input) => ({
|
|
2160
|
+
algorithm: input.algorithm,
|
|
2161
|
+
digits: input.digits,
|
|
2162
|
+
issuer: input.issuer,
|
|
2163
|
+
keySize: input.keySize,
|
|
2164
|
+
maxValidationAttempts: input.maxValidationAttempts,
|
|
2165
|
+
periodSeconds: input.periodSeconds,
|
|
2166
|
+
qrSize: input.qrSize,
|
|
2167
|
+
skew: input.skew
|
|
2168
|
+
});
|
|
2169
|
+
var canonicalFromProps5 = (props) => canonical6({
|
|
2170
|
+
algorithm: props.algorithm ?? "SHA1",
|
|
2171
|
+
digits: props.digits ?? 6,
|
|
2172
|
+
issuer: props.issuer,
|
|
2173
|
+
keySize: props.keySize ?? 20,
|
|
2174
|
+
maxValidationAttempts: props.maxValidationAttempts ?? 5,
|
|
2175
|
+
periodSeconds: parseDuration(props.period ?? "30s") ?? -1,
|
|
2176
|
+
qrSize: props.qrSize ?? 200,
|
|
2177
|
+
skew: props.skew ?? 1
|
|
2178
|
+
});
|
|
2179
|
+
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : -1;
|
|
2180
|
+
var text9 = (value) => typeof value === "string" ? value : "";
|
|
2181
|
+
var canonicalFromLive5 = (live) => canonical6({
|
|
2182
|
+
algorithm: text9(live["algorithm"]),
|
|
2183
|
+
digits: num(live["digits"]),
|
|
2184
|
+
issuer: text9(live["issuer"]),
|
|
2185
|
+
keySize: num(live["key_size"]),
|
|
2186
|
+
maxValidationAttempts: num(live["max_validation_attempts"]),
|
|
2187
|
+
periodSeconds: ttlSeconds(live["period"]) ?? -1,
|
|
2188
|
+
qrSize: num(live["qr_size"]),
|
|
2189
|
+
skew: num(live["skew"])
|
|
2190
|
+
});
|
|
2191
|
+
var attributesOf9 = (methodId, live) => {
|
|
2192
|
+
const form = canonicalFromLive5(live);
|
|
1035
2193
|
return {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
groupsScope,
|
|
1042
|
-
maxTtl: text3(raw, "max_ttl", where),
|
|
1043
|
-
name: text3(raw, "name", where),
|
|
1044
|
-
ttl: text3(raw, "ttl", where),
|
|
1045
|
-
zoneTypes: texts(raw, "zone_types", where) ?? [],
|
|
1046
|
-
zones: texts(raw, "zones", where)
|
|
2194
|
+
...form,
|
|
2195
|
+
digest: sha256(JSON.stringify(form)),
|
|
2196
|
+
methodId,
|
|
2197
|
+
name: text9(live["name"]),
|
|
2198
|
+
namespacePath: text9(live["namespace_path"])
|
|
1047
2199
|
};
|
|
1048
2200
|
};
|
|
1049
|
-
var
|
|
1050
|
-
|
|
1051
|
-
const
|
|
2201
|
+
var matches8 = (attributes, props) => attributes.digest === sha256(JSON.stringify(canonicalFromProps5(props)));
|
|
2202
|
+
var writeBody7 = (props) => {
|
|
2203
|
+
const form = canonicalFromProps5(props);
|
|
1052
2204
|
return {
|
|
1053
|
-
|
|
1054
|
-
|
|
2205
|
+
algorithm: form.algorithm,
|
|
2206
|
+
digits: form.digits,
|
|
2207
|
+
issuer: form.issuer,
|
|
2208
|
+
key_size: form.keySize,
|
|
2209
|
+
max_validation_attempts: form.maxValidationAttempts,
|
|
2210
|
+
method_name: props.name,
|
|
2211
|
+
period: props.period ?? "30s",
|
|
2212
|
+
qr_size: form.qrSize,
|
|
2213
|
+
skew: form.skew
|
|
1055
2214
|
};
|
|
1056
2215
|
};
|
|
1057
|
-
var
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
for (const [name, spec] of Object.entries(record(root["surfaces"], "surfaces"))) {
|
|
1073
|
-
surfaces[name] = surface(name, spec);
|
|
1074
|
-
}
|
|
1075
|
-
const accounts = Object.entries(record(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
|
|
1076
|
-
return {
|
|
1077
|
-
accounts,
|
|
1078
|
-
namespace: text3(record(root["defaults"], "defaults"), "namespace", "defaults"),
|
|
1079
|
-
surfaces
|
|
1080
|
-
};
|
|
2216
|
+
var renameProblem = (from, to) => from === to ? undefined : `renaming from \`${from}\` would strand every enrolled secret. Declare the new method as a ` + "second resource, list both ids on the enforcement, enrol, then remove the old one " + "(mfa-totp.ts).";
|
|
2217
|
+
var problems5 = (props) => {
|
|
2218
|
+
const found = [];
|
|
2219
|
+
const form = canonicalFromProps5(props);
|
|
2220
|
+
if (props.name.trim() === "")
|
|
2221
|
+
found.push("name is empty; it is the only key a declaration has");
|
|
2222
|
+
if (props.issuer.trim() === "")
|
|
2223
|
+
found.push("issuer is empty; the server requires one");
|
|
2224
|
+
if (form.periodSeconds <= 0)
|
|
2225
|
+
found.push(`period \`${props.period ?? ""}\` must be a duration above zero`);
|
|
2226
|
+
if (form.keySize <= 0)
|
|
2227
|
+
found.push("keySize must be above zero");
|
|
2228
|
+
if (form.maxValidationAttempts < 1)
|
|
2229
|
+
found.push("maxValidationAttempts must be at least 1 (0 reads back as 5)");
|
|
2230
|
+
return found;
|
|
1081
2231
|
};
|
|
2232
|
+
|
|
2233
|
+
// src/openbao/mfa-totp.ts
|
|
2234
|
+
var BaoMfaTotpMethod = Resource8("Bao.MfaTotpMethod", {
|
|
2235
|
+
defaultRemovalPolicy: "retain"
|
|
2236
|
+
});
|
|
2237
|
+
var refuse4 = (props, message) => Effect19.die(new Error(`Bao.MfaTotpMethod ${props.name}: ${message}`));
|
|
2238
|
+
var readTotp = (props) => Effect19.map(findTotp(props.name), (found) => found === undefined ? undefined : attributesOf9(found.id, found.live));
|
|
2239
|
+
var planTotp = (props) => Effect19.gen(function* () {
|
|
2240
|
+
if (problems5(props).length > 0)
|
|
2241
|
+
return "update";
|
|
2242
|
+
const live = yield* readTotp(props);
|
|
2243
|
+
return live !== undefined && matches8(live, props) ? "noop" : "update";
|
|
2244
|
+
});
|
|
2245
|
+
var reconcileTotp = (props) => Effect19.gen(function* () {
|
|
2246
|
+
const bad = problems5(props);
|
|
2247
|
+
if (bad.length > 0)
|
|
2248
|
+
return yield* refuse4(props, bad.join("; "));
|
|
2249
|
+
const live = yield* readTotp(props);
|
|
2250
|
+
if (live === undefined || !matches8(live, props))
|
|
2251
|
+
yield* writeTotp(writeBody7(props));
|
|
2252
|
+
const after = yield* readTotp(props);
|
|
2253
|
+
if (after === undefined)
|
|
2254
|
+
return yield* refuse4(props, "the write succeeded but no method has that name.");
|
|
2255
|
+
if (!matches8(after, props))
|
|
2256
|
+
return yield* refuse4(props, "the method read back different.");
|
|
2257
|
+
return after;
|
|
2258
|
+
});
|
|
2259
|
+
var BaoMfaTotpMethodProvider = () => Provider8.effect(BaoMfaTotpMethod, Effect19.succeed(BaoMfaTotpMethod.Provider.of({
|
|
2260
|
+
list: () => Effect19.succeed([]),
|
|
2261
|
+
read: Effect19.fn(function* ({ olds }) {
|
|
2262
|
+
return yield* readTotp(olds);
|
|
2263
|
+
}),
|
|
2264
|
+
diff: Effect19.fn(function* ({ news, output }) {
|
|
2265
|
+
if (output === undefined || !isResolved8(news))
|
|
2266
|
+
return;
|
|
2267
|
+
const rename = renameProblem(output.name, news.name);
|
|
2268
|
+
if (rename !== undefined)
|
|
2269
|
+
return yield* refuse4(news, rename);
|
|
2270
|
+
return { action: yield* planTotp(news) };
|
|
2271
|
+
}),
|
|
2272
|
+
reconcile: Effect19.fn(function* ({ news }) {
|
|
2273
|
+
return yield* reconcileTotp(news);
|
|
2274
|
+
}),
|
|
2275
|
+
delete: Effect19.fn(function* ({ output }) {
|
|
2276
|
+
yield* deleteTotp(output.methodId);
|
|
2277
|
+
return;
|
|
2278
|
+
})
|
|
2279
|
+
})));
|
|
1082
2280
|
// src/openbao/mount.ts
|
|
1083
|
-
import { Resource as
|
|
1084
|
-
import { isResolved as
|
|
1085
|
-
import * as
|
|
1086
|
-
import * as
|
|
2281
|
+
import { Resource as Resource9 } from "alchemy";
|
|
2282
|
+
import { isResolved as isResolved9 } from "alchemy/Diff";
|
|
2283
|
+
import * as Provider9 from "alchemy/Provider";
|
|
2284
|
+
import * as Effect22 from "effect/Effect";
|
|
2285
|
+
|
|
2286
|
+
// src/openbao/mount-reconcile.ts
|
|
2287
|
+
import * as Effect21 from "effect/Effect";
|
|
1087
2288
|
|
|
1088
2289
|
// src/openbao/mount-wire.ts
|
|
1089
|
-
import * as
|
|
1090
|
-
var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(
|
|
2290
|
+
import * as Effect20 from "effect/Effect";
|
|
2291
|
+
var absentFromTable2 = (path, asked) => baoRead("sys/mounts").pipe(Effect20.mapError((listing) => new BaoError(asked.status, asked.method, asked.path, [
|
|
1091
2292
|
...asked.errors,
|
|
1092
2293
|
`sys/mounts could not confirm the mount is absent: ${listing.message}`
|
|
1093
|
-
])),
|
|
1094
|
-
var readMountData = (path) => baoRead(readPath(path)).pipe(
|
|
2294
|
+
])), Effect20.flatMap((table) => table !== undefined && !Object.hasOwn(table, `${mountPath(path)}/`) ? Effect20.succeed(undefined) : Effect20.fail(asked)));
|
|
2295
|
+
var readMountData = (path) => baoRead(readPath(path)).pipe(Effect20.catchIf((error) => error.status === 400, (error) => absentFromTable2(path, error)));
|
|
1095
2296
|
var enableMount = (props) => baoWrite("POST", readPath(props.path), enableBody(props));
|
|
1096
2297
|
var tuneMount = (props) => baoWrite("POST", tunePath(props.path), tuneBody(props));
|
|
1097
2298
|
var disableMount = (path) => baoDelete(readPath(path));
|
|
1098
2299
|
|
|
1099
|
-
// src/openbao/mount.ts
|
|
1100
|
-
var
|
|
1101
|
-
defaultRemovalPolicy: "retain"
|
|
1102
|
-
});
|
|
1103
|
-
var readMount = (props) => Effect9.gen(function* () {
|
|
2300
|
+
// src/openbao/mount-reconcile.ts
|
|
2301
|
+
var readMount = (props) => Effect21.gen(function* () {
|
|
1104
2302
|
const live = yield* readMountData(props.path);
|
|
1105
2303
|
if (live === undefined)
|
|
1106
2304
|
return;
|
|
1107
2305
|
return attributesOf(props, live);
|
|
1108
2306
|
});
|
|
1109
|
-
var
|
|
1110
|
-
|
|
1111
|
-
|
|
2307
|
+
var moveTarget2 = (props) => ({
|
|
2308
|
+
family: "Bao.Mount",
|
|
2309
|
+
path: props.path,
|
|
2310
|
+
read: (path) => readMount({ ...props, path }),
|
|
2311
|
+
type: props.type,
|
|
2312
|
+
wire: mountPath
|
|
2313
|
+
});
|
|
2314
|
+
var reconcileMount = (news, stated, options) => Effect21.gen(function* () {
|
|
2315
|
+
const move = planMove("Bao.Mount", stated, news.path, news.remountFrom);
|
|
2316
|
+
if (move.kind === "refuse")
|
|
2317
|
+
return yield* Effect21.die(new Error(move.message));
|
|
2318
|
+
if (move.kind === "move")
|
|
2319
|
+
yield* performMove(moveTarget2(news), move.from, options);
|
|
2320
|
+
let live = yield* readMount(news);
|
|
2321
|
+
if (live === undefined) {
|
|
2322
|
+
yield* enableMount(news);
|
|
2323
|
+
if (wantsTune(news))
|
|
2324
|
+
yield* tuneMount(news);
|
|
2325
|
+
} else if (!matches(live, news)) {
|
|
2326
|
+
if (live.type !== news.type) {
|
|
2327
|
+
return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
|
|
2328
|
+
}
|
|
2329
|
+
yield* tuneMount(news);
|
|
2330
|
+
}
|
|
2331
|
+
live = yield* readMount(news);
|
|
2332
|
+
if (live === undefined) {
|
|
2333
|
+
return yield* Effect21.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
|
|
2334
|
+
}
|
|
2335
|
+
return live;
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
// src/openbao/mount.ts
|
|
2339
|
+
var BaoMount = Resource9("Bao.Mount", {
|
|
2340
|
+
defaultRemovalPolicy: "retain"
|
|
2341
|
+
});
|
|
2342
|
+
var BaoMountProvider = () => Provider9.effect(BaoMount, Effect22.succeed(BaoMount.Provider.of({
|
|
2343
|
+
list: () => Effect22.succeed([]),
|
|
2344
|
+
read: Effect22.fn(function* ({ olds }) {
|
|
1112
2345
|
return yield* readMount(olds);
|
|
1113
2346
|
}),
|
|
1114
|
-
diff:
|
|
1115
|
-
if (output === undefined || !
|
|
2347
|
+
diff: Effect22.fn(function* ({ news, output }) {
|
|
2348
|
+
if (output === undefined || !isResolved9(news))
|
|
1116
2349
|
return;
|
|
2350
|
+
const move = planMove("Bao.Mount", output.path, news.path, news.remountFrom);
|
|
2351
|
+
if (move.kind === "refuse")
|
|
2352
|
+
return yield* Effect22.die(new Error(move.message));
|
|
2353
|
+
if (move.kind === "move")
|
|
2354
|
+
return { action: "update" };
|
|
1117
2355
|
const live = yield* readMount(news);
|
|
1118
2356
|
if (live === undefined)
|
|
1119
2357
|
return { action: "update" };
|
|
@@ -1123,39 +2361,29 @@ var BaoMountProvider = () => Provider4.effect(BaoMount, Effect9.succeed(BaoMount
|
|
|
1123
2361
|
return { action: "noop" };
|
|
1124
2362
|
return { action: "update" };
|
|
1125
2363
|
}),
|
|
1126
|
-
reconcile:
|
|
1127
|
-
|
|
1128
|
-
if (live === undefined) {
|
|
1129
|
-
yield* enableMount(news);
|
|
1130
|
-
if (wantsTune(news))
|
|
1131
|
-
yield* tuneMount(news);
|
|
1132
|
-
} else if (!matches(live, news)) {
|
|
1133
|
-
if (live.type !== news.type) {
|
|
1134
|
-
return yield* Effect9.die(new Error(`Bao.Mount ${mountPath(news.path)}: live type ${live.type} != ${news.type}. ` + "Secrets engine type is immutable — replace manually."));
|
|
1135
|
-
}
|
|
1136
|
-
yield* tuneMount(news);
|
|
1137
|
-
}
|
|
1138
|
-
live = yield* readMount(news);
|
|
1139
|
-
if (live === undefined) {
|
|
1140
|
-
return yield* Effect9.die(new Error(`Bao.Mount ${mountPath(news.path)}: write returned no error but the mount is still absent.`));
|
|
1141
|
-
}
|
|
1142
|
-
return live;
|
|
2364
|
+
reconcile: Effect22.fn(function* ({ news, output }) {
|
|
2365
|
+
return yield* reconcileMount(news, output?.path);
|
|
1143
2366
|
}),
|
|
1144
|
-
delete:
|
|
2367
|
+
delete: Effect22.fn(function* ({ output }) {
|
|
1145
2368
|
yield* disableMount(output.path);
|
|
1146
2369
|
return;
|
|
1147
2370
|
})
|
|
1148
2371
|
})));
|
|
1149
2372
|
// src/openbao/pki-role.ts
|
|
1150
|
-
import { Resource as
|
|
1151
|
-
import { isResolved as
|
|
1152
|
-
import * as
|
|
1153
|
-
import * as
|
|
2373
|
+
import { Resource as Resource10 } from "alchemy";
|
|
2374
|
+
import { isResolved as isResolved10 } from "alchemy/Diff";
|
|
2375
|
+
import * as Provider10 from "alchemy/Provider";
|
|
2376
|
+
import * as Effect23 from "effect/Effect";
|
|
1154
2377
|
|
|
1155
|
-
// src/openbao/pki-role-
|
|
2378
|
+
// src/openbao/pki-role-canonical.ts
|
|
2379
|
+
var DEFAULT_KEY_USAGE = [
|
|
2380
|
+
"DigitalSignature",
|
|
2381
|
+
"KeyAgreement",
|
|
2382
|
+
"KeyEncipherment"
|
|
2383
|
+
];
|
|
1156
2384
|
var sorted = (values) => [...values].sort();
|
|
1157
2385
|
var foldedUsages = (values) => sorted(values.map((v) => v.toLowerCase()));
|
|
1158
|
-
var
|
|
2386
|
+
var canonical7 = (input) => ({
|
|
1159
2387
|
allowBareDomains: input.allowBareDomains,
|
|
1160
2388
|
allowGlobDomains: input.allowGlobDomains,
|
|
1161
2389
|
allowIpSans: input.allowIpSans,
|
|
@@ -1163,18 +2391,24 @@ var canonical2 = (input) => ({
|
|
|
1163
2391
|
allowSubdomains: input.allowSubdomains,
|
|
1164
2392
|
allowWildcardCertificates: input.allowWildcardCertificates,
|
|
1165
2393
|
allowedDomains: sorted(input.allowedDomains),
|
|
2394
|
+
allowedDomainsTemplate: input.allowedDomainsTemplate,
|
|
1166
2395
|
clientFlag: input.clientFlag,
|
|
1167
2396
|
cnValidations: sorted(input.cnValidations),
|
|
2397
|
+
enforceHostnames: input.enforceHostnames,
|
|
1168
2398
|
extKeyUsage: foldedUsages(input.extKeyUsage),
|
|
2399
|
+
generateLease: input.generateLease,
|
|
1169
2400
|
keyBits: input.keyBits,
|
|
1170
2401
|
keyType: input.keyType,
|
|
2402
|
+
keyUsage: foldedUsages(input.keyUsage),
|
|
1171
2403
|
maxTtlSeconds: input.maxTtlSeconds,
|
|
2404
|
+
noStore: input.noStore,
|
|
2405
|
+
requireCn: input.requireCn,
|
|
1172
2406
|
serverFlag: input.serverFlag,
|
|
1173
2407
|
signatureBits: input.signatureBits,
|
|
1174
2408
|
ttlSeconds: input.ttlSeconds
|
|
1175
2409
|
});
|
|
1176
|
-
var
|
|
1177
|
-
var
|
|
2410
|
+
var seconds3 = (text10) => parseDuration(text10) ?? -1;
|
|
2411
|
+
var canonicalFromProps6 = (props) => canonical7({
|
|
1178
2412
|
allowBareDomains: props.allowBareDomains ?? false,
|
|
1179
2413
|
allowGlobDomains: props.allowGlobDomains ?? false,
|
|
1180
2414
|
allowIpSans: props.allowIpSans ?? false,
|
|
@@ -1182,21 +2416,27 @@ var canonicalFromProps = (props) => canonical2({
|
|
|
1182
2416
|
allowSubdomains: props.allowSubdomains ?? false,
|
|
1183
2417
|
allowWildcardCertificates: props.allowWildcardCertificates ?? false,
|
|
1184
2418
|
allowedDomains: props.allowedDomains,
|
|
2419
|
+
allowedDomainsTemplate: props.allowedDomainsTemplate ?? false,
|
|
1185
2420
|
clientFlag: props.clientFlag ?? true,
|
|
1186
2421
|
cnValidations: props.cnValidations ?? ["hostname"],
|
|
2422
|
+
enforceHostnames: props.enforceHostnames ?? true,
|
|
1187
2423
|
extKeyUsage: props.extKeyUsage ?? [],
|
|
2424
|
+
generateLease: props.generateLease ?? false,
|
|
1188
2425
|
keyBits: props.keyBits ?? 0,
|
|
1189
2426
|
keyType: props.keyType ?? "rsa",
|
|
1190
|
-
|
|
2427
|
+
keyUsage: props.keyUsage ?? DEFAULT_KEY_USAGE,
|
|
2428
|
+
maxTtlSeconds: seconds3(props.maxTtl),
|
|
2429
|
+
noStore: props.noStore ?? false,
|
|
2430
|
+
requireCn: props.requireCn ?? true,
|
|
1191
2431
|
serverFlag: props.serverFlag ?? true,
|
|
1192
2432
|
signatureBits: props.signatureBits ?? 0,
|
|
1193
|
-
ttlSeconds:
|
|
2433
|
+
ttlSeconds: seconds3(props.ttl)
|
|
1194
2434
|
});
|
|
1195
2435
|
var bool2 = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
1196
2436
|
var int2 = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1197
2437
|
var list = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
1198
2438
|
var str = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
1199
|
-
var
|
|
2439
|
+
var canonicalFromLive6 = (live) => canonical7({
|
|
1200
2440
|
allowBareDomains: bool2(live["allow_bare_domains"], false),
|
|
1201
2441
|
allowGlobDomains: bool2(live["allow_glob_domains"], false),
|
|
1202
2442
|
allowIpSans: bool2(live["allow_ip_sans"], false),
|
|
@@ -1204,16 +2444,25 @@ var canonicalFromLive = (live) => canonical2({
|
|
|
1204
2444
|
allowSubdomains: bool2(live["allow_subdomains"], false),
|
|
1205
2445
|
allowWildcardCertificates: bool2(live["allow_wildcard_certificates"], false),
|
|
1206
2446
|
allowedDomains: list(live["allowed_domains"]),
|
|
2447
|
+
allowedDomainsTemplate: bool2(live["allowed_domains_template"], false),
|
|
1207
2448
|
clientFlag: bool2(live["client_flag"], true),
|
|
1208
2449
|
cnValidations: list(live["cn_validations"]),
|
|
2450
|
+
enforceHostnames: bool2(live["enforce_hostnames"], false),
|
|
1209
2451
|
extKeyUsage: list(live["ext_key_usage"]),
|
|
2452
|
+
generateLease: bool2(live["generate_lease"], false),
|
|
1210
2453
|
keyBits: int2(live["key_bits"], 0),
|
|
1211
2454
|
keyType: str(live["key_type"], "rsa"),
|
|
2455
|
+
keyUsage: list(live["key_usage"]),
|
|
1212
2456
|
maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? -1,
|
|
2457
|
+
noStore: bool2(live["no_store"], false),
|
|
2458
|
+
requireCn: bool2(live["require_cn"], false),
|
|
1213
2459
|
serverFlag: bool2(live["server_flag"], true),
|
|
1214
2460
|
signatureBits: int2(live["signature_bits"], 0),
|
|
1215
2461
|
ttlSeconds: ttlSeconds(live["ttl"]) ?? -1
|
|
1216
2462
|
});
|
|
2463
|
+
var digestOf = (form) => sha256(JSON.stringify(form));
|
|
2464
|
+
|
|
2465
|
+
// src/openbao/pki-role-form.ts
|
|
1217
2466
|
var ttlText2 = (value) => {
|
|
1218
2467
|
if (value < 0)
|
|
1219
2468
|
return "";
|
|
@@ -1225,10 +2474,9 @@ var ttlText2 = (value) => {
|
|
|
1225
2474
|
return `${String(value / 60)}m`;
|
|
1226
2475
|
return `${String(value)}s`;
|
|
1227
2476
|
};
|
|
1228
|
-
var
|
|
1229
|
-
var
|
|
1230
|
-
|
|
1231
|
-
const form = canonicalFromLive(live);
|
|
2477
|
+
var rolePath4 = (props) => `${mountPath(props.mount ?? "pki")}/roles/${props.name}`;
|
|
2478
|
+
var attributesOf10 = (props, live) => {
|
|
2479
|
+
const form = canonicalFromLive6(live);
|
|
1232
2480
|
return {
|
|
1233
2481
|
...form,
|
|
1234
2482
|
name: props.name,
|
|
@@ -1238,8 +2486,8 @@ var attributesOf5 = (props, live) => {
|
|
|
1238
2486
|
digest: digestOf(form)
|
|
1239
2487
|
};
|
|
1240
2488
|
};
|
|
1241
|
-
var
|
|
1242
|
-
const form =
|
|
2489
|
+
var writeBody8 = (props) => {
|
|
2490
|
+
const form = canonicalFromProps6(props);
|
|
1243
2491
|
const body = {
|
|
1244
2492
|
allowed_domains: form.allowedDomains.join(","),
|
|
1245
2493
|
allow_bare_domains: String(form.allowBareDomains),
|
|
@@ -1255,76 +2503,218 @@ var writeBody3 = (props) => {
|
|
|
1255
2503
|
key_bits: String(form.keyBits),
|
|
1256
2504
|
signature_bits: String(form.signatureBits),
|
|
1257
2505
|
ttl: props.ttl,
|
|
1258
|
-
max_ttl: props.maxTtl
|
|
2506
|
+
max_ttl: props.maxTtl,
|
|
2507
|
+
require_cn: String(form.requireCn),
|
|
2508
|
+
enforce_hostnames: String(form.enforceHostnames),
|
|
2509
|
+
key_usage: (props.keyUsage ?? DEFAULT_KEY_USAGE).join(","),
|
|
2510
|
+
allowed_domains_template: String(form.allowedDomainsTemplate),
|
|
2511
|
+
no_store: String(form.noStore),
|
|
2512
|
+
generate_lease: String(form.generateLease)
|
|
1259
2513
|
};
|
|
1260
2514
|
const usages = props.extKeyUsage ?? [];
|
|
1261
2515
|
if (usages.length > 0)
|
|
1262
2516
|
body["ext_key_usage"] = usages.join(",");
|
|
1263
2517
|
return body;
|
|
1264
2518
|
};
|
|
1265
|
-
var
|
|
2519
|
+
var matches9 = (attributes, props) => attributes.digest === digestOf(canonicalFromProps6(props));
|
|
2520
|
+
var problems6 = (props) => {
|
|
2521
|
+
const found = [];
|
|
2522
|
+
if (props.allowedDomains.length === 0) {
|
|
2523
|
+
found.push("allowedDomains is empty. With allow_any_name false that role can issue no certificate at all.");
|
|
2524
|
+
}
|
|
2525
|
+
const bad = [
|
|
2526
|
+
["ttl", props.ttl],
|
|
2527
|
+
["maxTtl", props.maxTtl]
|
|
2528
|
+
].filter(([, text10]) => parseDuration(text10 ?? "") === undefined);
|
|
2529
|
+
if (bad.length > 0) {
|
|
2530
|
+
found.push(`${bad.map(([k, v]) => `${k ?? ""}=${v ?? ""}`).join(", ")} is not a duration OpenBao ` + "parses (expect 30m, 720h, 8760h or 0).");
|
|
2531
|
+
}
|
|
2532
|
+
if (props.noStore === true && props.generateLease === true) {
|
|
2533
|
+
found.push("noStore and generateLease are both true; OpenBao keeps no_store and drops the lease.");
|
|
2534
|
+
}
|
|
2535
|
+
return found;
|
|
2536
|
+
};
|
|
1266
2537
|
|
|
1267
2538
|
// src/openbao/pki-role.ts
|
|
1268
|
-
var BaoPkiRole =
|
|
2539
|
+
var BaoPkiRole = Resource10("Bao.PkiRole", {
|
|
1269
2540
|
defaultRemovalPolicy: "retain"
|
|
1270
2541
|
});
|
|
1271
|
-
var readRole2 = (props) =>
|
|
1272
|
-
const live = yield* baoRead(
|
|
2542
|
+
var readRole2 = (props) => Effect23.gen(function* () {
|
|
2543
|
+
const live = yield* baoRead(rolePath4(props));
|
|
1273
2544
|
if (live === undefined)
|
|
1274
2545
|
return;
|
|
1275
|
-
return
|
|
2546
|
+
return attributesOf10(props, live);
|
|
1276
2547
|
});
|
|
1277
|
-
var
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
read: Effect10.fn(function* ({ olds }) {
|
|
2548
|
+
var BaoPkiRoleProvider = () => Provider10.effect(BaoPkiRole, Effect23.succeed(BaoPkiRole.Provider.of({
|
|
2549
|
+
list: () => Effect23.succeed([]),
|
|
2550
|
+
read: Effect23.fn(function* ({ olds }) {
|
|
1281
2551
|
return yield* readRole2(olds);
|
|
1282
2552
|
}),
|
|
1283
|
-
diff:
|
|
1284
|
-
if (output === undefined || !
|
|
2553
|
+
diff: Effect23.fn(function* ({ news, output }) {
|
|
2554
|
+
if (output === undefined || !isResolved10(news))
|
|
1285
2555
|
return;
|
|
1286
|
-
if (mountPath(news.mount ?? "pki") !== output.mount)
|
|
2556
|
+
if (mountPath(news.mount ?? "pki") !== output.mount || news.name !== output.name)
|
|
1287
2557
|
return { action: "replace" };
|
|
1288
|
-
if (
|
|
2558
|
+
if (problems6(news).length > 0)
|
|
1289
2559
|
return { action: "update" };
|
|
1290
|
-
}
|
|
1291
2560
|
const live = yield* readRole2(news);
|
|
1292
2561
|
if (live === undefined)
|
|
1293
2562
|
return { action: "update" };
|
|
1294
|
-
return
|
|
2563
|
+
return matches9(live, news) ? { action: "noop" } : { action: "update" };
|
|
1295
2564
|
}),
|
|
1296
|
-
reconcile:
|
|
1297
|
-
const path =
|
|
1298
|
-
|
|
1299
|
-
return yield* Effect10.die(new Error(`Bao.PkiRole ${path}: allowedDomains is empty. With allow_any_name false ` + "that role can issue no certificate at all."));
|
|
1300
|
-
}
|
|
1301
|
-
const bad = badDurations(news);
|
|
2565
|
+
reconcile: Effect23.fn(function* ({ news }) {
|
|
2566
|
+
const path = rolePath4(news);
|
|
2567
|
+
const bad = problems6(news);
|
|
1302
2568
|
if (bad.length > 0) {
|
|
1303
|
-
return yield*
|
|
2569
|
+
return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: ${bad.join(" ")}`));
|
|
1304
2570
|
}
|
|
1305
2571
|
const live = yield* readRole2(news);
|
|
1306
|
-
if (live === undefined || !
|
|
1307
|
-
yield* baoWrite("PUT", path,
|
|
2572
|
+
if (live === undefined || !matches9(live, news)) {
|
|
2573
|
+
yield* baoWrite("PUT", path, writeBody8(news));
|
|
1308
2574
|
}
|
|
1309
2575
|
const after = yield* readRole2(news);
|
|
1310
2576
|
if (after === undefined) {
|
|
1311
|
-
return yield*
|
|
2577
|
+
return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: write reported success but the role is absent.`));
|
|
1312
2578
|
}
|
|
1313
|
-
if (!
|
|
1314
|
-
return yield*
|
|
2579
|
+
if (!matches9(after, news)) {
|
|
2580
|
+
return yield* Effect23.die(new Error(`Bao.PkiRole ${path}: wrote cleanly but the role read back different. ` + "OpenBao stored something other than what was declared — inspect with " + `\`bao read ${path}\` before retrying.`));
|
|
1315
2581
|
}
|
|
1316
2582
|
return after;
|
|
1317
2583
|
}),
|
|
1318
|
-
delete:
|
|
1319
|
-
yield* baoDelete(
|
|
2584
|
+
delete: Effect23.fn(function* ({ output }) {
|
|
2585
|
+
yield* baoDelete(rolePath4(output));
|
|
2586
|
+
return;
|
|
2587
|
+
})
|
|
2588
|
+
})));
|
|
2589
|
+
// src/openbao/plugin.ts
|
|
2590
|
+
import { Resource as Resource11 } from "alchemy";
|
|
2591
|
+
import { isResolved as isResolved11 } from "alchemy/Diff";
|
|
2592
|
+
import * as Provider11 from "alchemy/Provider";
|
|
2593
|
+
import * as Effect25 from "effect/Effect";
|
|
2594
|
+
|
|
2595
|
+
// src/openbao/plugin-form.ts
|
|
2596
|
+
var resolve = (props) => ({
|
|
2597
|
+
args: props.args ?? [],
|
|
2598
|
+
command: props.command,
|
|
2599
|
+
name: props.name,
|
|
2600
|
+
sha256: props.sha256.toLowerCase(),
|
|
2601
|
+
type: props.type,
|
|
2602
|
+
version: props.version ?? ""
|
|
2603
|
+
});
|
|
2604
|
+
var CANONICAL_VERSION = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
|
|
2605
|
+
var problems7 = (form) => {
|
|
2606
|
+
const found = [];
|
|
2607
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(form.name) || form.name.includes("..")) {
|
|
2608
|
+
found.push(`name ${JSON.stringify(form.name)} is not a plain catalog name`);
|
|
2609
|
+
}
|
|
2610
|
+
if (!/^[0-9a-f]{64}$/.test(form.sha256))
|
|
2611
|
+
found.push("sha256 is not 64 hex characters");
|
|
2612
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(form.command) || form.command.includes("..")) {
|
|
2613
|
+
found.push(`command ${JSON.stringify(form.command)} is not a bare file name`);
|
|
2614
|
+
}
|
|
2615
|
+
if (form.version !== "" && !CANONICAL_VERSION.test(form.version)) {
|
|
2616
|
+
found.push(`version ${JSON.stringify(form.version)} is not canonical semver like v1.2.0`);
|
|
2617
|
+
}
|
|
2618
|
+
if (/\+builtin/.test(form.version))
|
|
2619
|
+
found.push("version metadata `builtin` is reserved");
|
|
2620
|
+
return found;
|
|
2621
|
+
};
|
|
2622
|
+
var versionQuery = (version) => version === "" ? "" : `?version=${encodeURIComponent(version)}`;
|
|
2623
|
+
var catalogPath = (type, name) => `sys/plugins/catalog/${type}/${name}`;
|
|
2624
|
+
var versionedPath = (type, name, version) => `${catalogPath(type, name)}${versionQuery(version)}`;
|
|
2625
|
+
var registerBody = (form) => ({
|
|
2626
|
+
command: form.command,
|
|
2627
|
+
sha256: form.sha256,
|
|
2628
|
+
...form.args.length > 0 ? { args: form.args } : {},
|
|
2629
|
+
...form.version === "" ? {} : { version: form.version }
|
|
2630
|
+
});
|
|
2631
|
+
var text10 = (value) => typeof value === "string" ? value : "";
|
|
2632
|
+
var list2 = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
2633
|
+
var attributesOf11 = (form, live) => {
|
|
2634
|
+
const attrs = {
|
|
2635
|
+
name: form.name,
|
|
2636
|
+
type: form.type,
|
|
2637
|
+
version: text10(live["version"]),
|
|
2638
|
+
sha256: text10(live["sha256"]).toLowerCase(),
|
|
2639
|
+
command: text10(live["command"]),
|
|
2640
|
+
args: list2(live["args"]),
|
|
2641
|
+
builtin: live["builtin"] === true
|
|
2642
|
+
};
|
|
2643
|
+
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
2644
|
+
};
|
|
2645
|
+
var sameArgs = (want, have) => want.length === have.length && want.every((entry, index) => entry === have[index]);
|
|
2646
|
+
var matches10 = (attributes, form) => !attributes.builtin && attributes.version === form.version && attributes.sha256 === form.sha256 && attributes.command === form.command && sameArgs(form.args, attributes.args);
|
|
2647
|
+
|
|
2648
|
+
// src/openbao/plugin-reconcile.ts
|
|
2649
|
+
import * as Effect24 from "effect/Effect";
|
|
2650
|
+
|
|
2651
|
+
// src/openbao/plugin-wire.ts
|
|
2652
|
+
var readPluginData = (form) => baoRead(versionedPath(form.type, form.name, form.version));
|
|
2653
|
+
var registerPlugin = (form) => baoWrite("PUT", catalogPath(form.type, form.name), registerBody(form));
|
|
2654
|
+
var deregisterPlugin = (type, name, version) => baoDelete(versionedPath(type, name, version));
|
|
2655
|
+
|
|
2656
|
+
// src/openbao/plugin-reconcile.ts
|
|
2657
|
+
var readEntry = (form) => Effect24.map(readPluginData(form), (live) => live === undefined ? undefined : { attributes: attributesOf11(form, live), live });
|
|
2658
|
+
var refuse5 = (form, message) => Effect24.die(new Error(`Bao.Plugin ${versionedPath(form.type, form.name, form.version)}: ${message}`));
|
|
2659
|
+
var SELF_REPORTED = "the write succeeded but the read-back does not match. If the binary reports its own version, " + "OpenBao filed the registration under it (plugin_catalog.go setInternal) — declare `version` " + "as exactly the version the binary reports; that adopts the entry this write just made.";
|
|
2660
|
+
var reconcilePlugin = (props) => Effect24.gen(function* () {
|
|
2661
|
+
const form = resolve(props);
|
|
2662
|
+
const bad = problems7(form);
|
|
2663
|
+
if (bad.length > 0)
|
|
2664
|
+
return yield* refuse5(form, bad.join("; "));
|
|
2665
|
+
const found = yield* readEntry(form);
|
|
2666
|
+
if (found !== undefined && !matches10(found.attributes, form)) {
|
|
2667
|
+
if (found.live["declarative"] === true || found.live["oci"] === true) {
|
|
2668
|
+
return yield* refuse5(form, "the live entry is declarative/OCI; manage it in server config.");
|
|
2669
|
+
}
|
|
2670
|
+
if (found.attributes.builtin) {
|
|
2671
|
+
return yield* refuse5(form, "this name is a builtin; an unversioned write would shadow it.");
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
if (found === undefined || !matches10(found.attributes, form))
|
|
2675
|
+
yield* registerPlugin(form);
|
|
2676
|
+
const after = yield* readEntry(form);
|
|
2677
|
+
if (after === undefined || !matches10(after.attributes, form)) {
|
|
2678
|
+
return yield* refuse5(form, SELF_REPORTED);
|
|
2679
|
+
}
|
|
2680
|
+
return after.attributes;
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
// src/openbao/plugin.ts
|
|
2684
|
+
var BaoPlugin = Resource11("Bao.Plugin", {
|
|
2685
|
+
defaultRemovalPolicy: "retain"
|
|
2686
|
+
});
|
|
2687
|
+
var BaoPluginProvider = () => Provider11.effect(BaoPlugin, Effect25.succeed(BaoPlugin.Provider.of({
|
|
2688
|
+
list: () => Effect25.succeed([]),
|
|
2689
|
+
read: Effect25.fn(function* ({ olds }) {
|
|
2690
|
+
const found = yield* readEntry(resolve(olds));
|
|
2691
|
+
return found?.attributes;
|
|
2692
|
+
}),
|
|
2693
|
+
diff: Effect25.fn(function* ({ news, output }) {
|
|
2694
|
+
if (output === undefined || !isResolved11(news))
|
|
2695
|
+
return;
|
|
2696
|
+
const form = resolve(news);
|
|
2697
|
+
if (output.name !== form.name || output.type !== form.type || output.version !== form.version) {
|
|
2698
|
+
return { action: "replace" };
|
|
2699
|
+
}
|
|
2700
|
+
const found = yield* readEntry(form);
|
|
2701
|
+
if (found === undefined)
|
|
2702
|
+
return { action: "update" };
|
|
2703
|
+
return matches10(found.attributes, form) ? { action: "noop" } : { action: "update" };
|
|
2704
|
+
}),
|
|
2705
|
+
reconcile: Effect25.fn(function* ({ news }) {
|
|
2706
|
+
return yield* reconcilePlugin(news);
|
|
2707
|
+
}),
|
|
2708
|
+
delete: Effect25.fn(function* ({ output }) {
|
|
2709
|
+
yield* deregisterPlugin(output.type, output.name, output.version);
|
|
1320
2710
|
return;
|
|
1321
2711
|
})
|
|
1322
2712
|
})));
|
|
1323
2713
|
// src/openbao/policy.ts
|
|
1324
|
-
import { Resource as
|
|
1325
|
-
import { isResolved as
|
|
1326
|
-
import * as
|
|
1327
|
-
import * as
|
|
2714
|
+
import { Resource as Resource12 } from "alchemy";
|
|
2715
|
+
import { isResolved as isResolved12 } from "alchemy/Diff";
|
|
2716
|
+
import * as Provider12 from "alchemy/Provider";
|
|
2717
|
+
import * as Effect27 from "effect/Effect";
|
|
1328
2718
|
import * as FileSystem from "effect/FileSystem";
|
|
1329
2719
|
import * as Path from "effect/Path";
|
|
1330
2720
|
|
|
@@ -1332,32 +2722,32 @@ import * as Path from "effect/Path";
|
|
|
1332
2722
|
var isEmptyAssembly = (parts, joined) => parts === 0 || !/^\s*path\s+"/m.test(joined);
|
|
1333
2723
|
|
|
1334
2724
|
// src/openbao/policy-wire.ts
|
|
1335
|
-
import * as
|
|
2725
|
+
import * as Effect26 from "effect/Effect";
|
|
1336
2726
|
var policyPath = (name) => `sys/policies/acl/${name}`;
|
|
1337
|
-
var readPolicy = (name) =>
|
|
2727
|
+
var readPolicy = (name) => Effect26.flatMap(baoRead(policyPath(name)), (data) => {
|
|
1338
2728
|
if (data === undefined)
|
|
1339
|
-
return
|
|
1340
|
-
const
|
|
1341
|
-
return typeof
|
|
2729
|
+
return Effect26.succeed("");
|
|
2730
|
+
const text11 = data["policy"];
|
|
2731
|
+
return typeof text11 === "string" ? Effect26.succeed(text11) : Effect26.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
|
|
1342
2732
|
});
|
|
1343
2733
|
var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
|
|
1344
2734
|
var deletePolicy = (name) => baoDelete(policyPath(name));
|
|
1345
2735
|
|
|
1346
2736
|
// src/openbao/policy.ts
|
|
1347
|
-
var BaoPolicy =
|
|
2737
|
+
var BaoPolicy = Resource12("Bao.Policy", {
|
|
1348
2738
|
defaultRemovalPolicy: "retain"
|
|
1349
2739
|
});
|
|
1350
2740
|
var grantsOf = (hcl) => (hcl.match(/^path /gm) ?? []).length;
|
|
1351
|
-
var
|
|
2741
|
+
var attributesOf12 = (name, hcl, parts) => ({
|
|
1352
2742
|
name,
|
|
1353
2743
|
digest: sha256(hcl),
|
|
1354
2744
|
grants: grantsOf(hcl),
|
|
1355
2745
|
parts
|
|
1356
2746
|
});
|
|
1357
|
-
var BaoPolicyProvider = () =>
|
|
2747
|
+
var BaoPolicyProvider = () => Provider12.effect(BaoPolicy, Effect27.gen(function* () {
|
|
1358
2748
|
const fs = yield* FileSystem.FileSystem;
|
|
1359
2749
|
const path = yield* Path.Path;
|
|
1360
|
-
const assemble = (dir) =>
|
|
2750
|
+
const assemble = (dir) => Effect27.gen(function* () {
|
|
1361
2751
|
const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
|
|
1362
2752
|
let joined = "";
|
|
1363
2753
|
for (const f of entries2)
|
|
@@ -1366,67 +2756,67 @@ var BaoPolicyProvider = () => Provider6.effect(BaoPolicy, Effect12.gen(function*
|
|
|
1366
2756
|
return { joined, parts: entries2.length };
|
|
1367
2757
|
});
|
|
1368
2758
|
return BaoPolicy.Provider.of({
|
|
1369
|
-
list: () =>
|
|
1370
|
-
read:
|
|
2759
|
+
list: () => Effect27.succeed([]),
|
|
2760
|
+
read: Effect27.fn(function* ({ olds }) {
|
|
1371
2761
|
const live = yield* readPolicy(olds.name);
|
|
1372
|
-
return live.length === 0 ? undefined :
|
|
2762
|
+
return live.length === 0 ? undefined : attributesOf12(olds.name, live, 0);
|
|
1373
2763
|
}),
|
|
1374
|
-
diff:
|
|
1375
|
-
if (output === undefined || !
|
|
2764
|
+
diff: Effect27.fn(function* ({ news, output }) {
|
|
2765
|
+
if (output === undefined || !isResolved12(news))
|
|
1376
2766
|
return;
|
|
1377
2767
|
const { joined } = yield* assemble(news.fragments);
|
|
1378
2768
|
const live = yield* readPolicy(news.name);
|
|
1379
2769
|
return live.length > 0 && sha256(live) === sha256(joined) ? { action: "noop" } : { action: "update" };
|
|
1380
2770
|
}),
|
|
1381
|
-
reconcile:
|
|
2771
|
+
reconcile: Effect27.fn(function* ({ news }) {
|
|
1382
2772
|
const name = news.name;
|
|
1383
2773
|
const { joined, parts } = yield* assemble(news.fragments);
|
|
1384
2774
|
if (isEmptyAssembly(parts, joined))
|
|
1385
|
-
return yield*
|
|
2775
|
+
return yield* Effect27.die(new Error(`Bao.Policy ${name}: the fragments under ${news.fragments} carry no path grant ` + `(${String(parts)} file(s)). Writing it would revoke every grant the policy carries.`));
|
|
1386
2776
|
const current = yield* readPolicy(name);
|
|
1387
2777
|
if (current.length > 0 && sha256(current) === sha256(joined)) {
|
|
1388
|
-
return
|
|
2778
|
+
return attributesOf12(name, joined, parts);
|
|
1389
2779
|
}
|
|
1390
2780
|
yield* writePolicy(name, joined);
|
|
1391
|
-
return
|
|
2781
|
+
return attributesOf12(name, joined, parts);
|
|
1392
2782
|
}),
|
|
1393
|
-
delete:
|
|
2783
|
+
delete: Effect27.fn(function* ({ output }) {
|
|
1394
2784
|
yield* deletePolicy(output.name);
|
|
1395
2785
|
return;
|
|
1396
2786
|
})
|
|
1397
2787
|
});
|
|
1398
2788
|
}));
|
|
1399
2789
|
// src/openbao/proxmox-role.ts
|
|
1400
|
-
import { Resource as
|
|
1401
|
-
import { isResolved as
|
|
1402
|
-
import * as
|
|
1403
|
-
import * as
|
|
2790
|
+
import { Resource as Resource13 } from "alchemy";
|
|
2791
|
+
import { isResolved as isResolved13 } from "alchemy/Diff";
|
|
2792
|
+
import * as Provider13 from "alchemy/Provider";
|
|
2793
|
+
import * as Effect28 from "effect/Effect";
|
|
1404
2794
|
|
|
1405
2795
|
// src/openbao/proxmox-role-form.ts
|
|
1406
|
-
var
|
|
1407
|
-
var ttlText3 = (
|
|
1408
|
-
if (
|
|
2796
|
+
var text11 = (value) => typeof value === "string" ? value : "";
|
|
2797
|
+
var ttlText3 = (seconds4, fallback = "0") => {
|
|
2798
|
+
if (seconds4 === undefined)
|
|
1409
2799
|
return fallback;
|
|
1410
|
-
if (
|
|
1411
|
-
return `${String(
|
|
1412
|
-
if (
|
|
1413
|
-
return `${String(
|
|
1414
|
-
if (
|
|
1415
|
-
return `${String(
|
|
1416
|
-
return `${String(
|
|
1417
|
-
};
|
|
1418
|
-
var
|
|
1419
|
-
var
|
|
2800
|
+
if (seconds4 % 86400 === 0)
|
|
2801
|
+
return `${String(seconds4 / 86400)}d`;
|
|
2802
|
+
if (seconds4 % 3600 === 0)
|
|
2803
|
+
return `${String(seconds4 / 3600)}h`;
|
|
2804
|
+
if (seconds4 % 60 === 0)
|
|
2805
|
+
return `${String(seconds4 / 60)}m`;
|
|
2806
|
+
return `${String(seconds4)}s`;
|
|
2807
|
+
};
|
|
2808
|
+
var rolePath5 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
|
|
2809
|
+
var attributesOf13 = (props, live) => {
|
|
1420
2810
|
const attrs = {
|
|
1421
2811
|
mount: mountPath(props.mount),
|
|
1422
|
-
name:
|
|
1423
|
-
mintUser:
|
|
2812
|
+
name: text11(live["name"]) || props.name,
|
|
2813
|
+
mintUser: text11(live["mint_user"]),
|
|
1424
2814
|
ttl: ttlText3(ttlSeconds(live["ttl"])),
|
|
1425
2815
|
maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
|
|
1426
2816
|
};
|
|
1427
2817
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
1428
2818
|
};
|
|
1429
|
-
var
|
|
2819
|
+
var writeBody9 = (props) => {
|
|
1430
2820
|
const ttl = parseDuration(props.ttl);
|
|
1431
2821
|
const maxTtl = parseDuration(props.maxTtl);
|
|
1432
2822
|
const bad = [];
|
|
@@ -1446,69 +2836,68 @@ var sameTtl5 = (want, have) => {
|
|
|
1446
2836
|
const haveSeconds = ttlSeconds(have);
|
|
1447
2837
|
return wantSeconds !== undefined && wantSeconds === haveSeconds;
|
|
1448
2838
|
};
|
|
1449
|
-
var
|
|
2839
|
+
var matches11 = (attributes, props) => attributes.mintUser === props.mintUser && sameTtl5(props.ttl, attributes.ttl) && sameTtl5(props.maxTtl, attributes.maxTtl);
|
|
1450
2840
|
|
|
1451
2841
|
// src/openbao/proxmox-role.ts
|
|
1452
|
-
var BaoProxmoxRole =
|
|
2842
|
+
var BaoProxmoxRole = Resource13("Bao.ProxmoxRole", {
|
|
1453
2843
|
defaultRemovalPolicy: "retain"
|
|
1454
2844
|
});
|
|
1455
|
-
var readRole3 = (props) =>
|
|
1456
|
-
const live = yield* baoRead(
|
|
2845
|
+
var readRole3 = (props) => Effect28.gen(function* () {
|
|
2846
|
+
const live = yield* baoRead(rolePath5(props.mount, props.name));
|
|
1457
2847
|
if (live === undefined)
|
|
1458
2848
|
return;
|
|
1459
|
-
return
|
|
2849
|
+
return attributesOf13(props, live);
|
|
1460
2850
|
});
|
|
1461
|
-
var BaoProxmoxRoleProvider = () =>
|
|
1462
|
-
list: () =>
|
|
1463
|
-
read:
|
|
2851
|
+
var BaoProxmoxRoleProvider = () => Provider13.effect(BaoProxmoxRole, Effect28.succeed(BaoProxmoxRole.Provider.of({
|
|
2852
|
+
list: () => Effect28.succeed([]),
|
|
2853
|
+
read: Effect28.fn(function* ({ olds }) {
|
|
1464
2854
|
return yield* readRole3(olds);
|
|
1465
2855
|
}),
|
|
1466
|
-
diff:
|
|
1467
|
-
if (output === undefined || !
|
|
2856
|
+
diff: Effect28.fn(function* ({ news, output }) {
|
|
2857
|
+
if (output === undefined || !isResolved13(news))
|
|
1468
2858
|
return;
|
|
1469
2859
|
const live = yield* readRole3(news);
|
|
1470
2860
|
if (live === undefined)
|
|
1471
2861
|
return { action: "update" };
|
|
1472
|
-
if (
|
|
2862
|
+
if (matches11(live, news))
|
|
1473
2863
|
return { action: "noop" };
|
|
1474
2864
|
return { action: "update" };
|
|
1475
2865
|
}),
|
|
1476
|
-
reconcile:
|
|
2866
|
+
reconcile: Effect28.fn(function* ({ news }) {
|
|
1477
2867
|
const live = yield* readRole3(news);
|
|
1478
2868
|
if (live !== undefined && live.mintUser !== news.mintUser && news.allowMintUserChange !== live.mintUser) {
|
|
1479
|
-
return yield*
|
|
2869
|
+
return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: live mint_user ` + `${live.mintUser} != declared ${news.mintUser}. Every token this role mints ` + "inherits the ACL of that PVE user. To re-scope deliberately, declare " + `allowMintUserChange: '${live.mintUser}'.`));
|
|
1480
2870
|
}
|
|
1481
|
-
if (live === undefined || !
|
|
1482
|
-
const write =
|
|
2871
|
+
if (live === undefined || !matches11(live, news)) {
|
|
2872
|
+
const write = writeBody9(news);
|
|
1483
2873
|
if (!write.ok) {
|
|
1484
|
-
return yield*
|
|
2874
|
+
return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: unparseable duration ` + `${write.bad.join(", ")}. Use 30s / 5m / 1h / 1d, or 0.`));
|
|
1485
2875
|
}
|
|
1486
|
-
yield* baoWrite("PUT",
|
|
2876
|
+
yield* baoWrite("PUT", rolePath5(news.mount, news.name), write.body);
|
|
1487
2877
|
}
|
|
1488
2878
|
const after = yield* readRole3(news);
|
|
1489
2879
|
if (after === undefined) {
|
|
1490
|
-
return yield*
|
|
2880
|
+
return yield* Effect28.die(new Error(`Bao.ProxmoxRole ${rolePath5(news.mount, news.name)}: write returned no error ` + "but the role is still absent."));
|
|
1491
2881
|
}
|
|
1492
2882
|
return after;
|
|
1493
2883
|
}),
|
|
1494
|
-
delete:
|
|
1495
|
-
yield* baoDelete(
|
|
2884
|
+
delete: Effect28.fn(function* ({ output }) {
|
|
2885
|
+
yield* baoDelete(rolePath5(output.mount, output.name));
|
|
1496
2886
|
return;
|
|
1497
2887
|
})
|
|
1498
2888
|
})));
|
|
1499
2889
|
// src/openbao/ssh-role.ts
|
|
1500
|
-
import { Resource as
|
|
1501
|
-
import { isResolved as
|
|
1502
|
-
import * as
|
|
1503
|
-
import * as
|
|
2890
|
+
import { Resource as Resource14 } from "alchemy";
|
|
2891
|
+
import { isResolved as isResolved14 } from "alchemy/Diff";
|
|
2892
|
+
import * as Provider14 from "alchemy/Provider";
|
|
2893
|
+
import * as Effect29 from "effect/Effect";
|
|
1504
2894
|
|
|
1505
2895
|
// src/openbao/ssh-role-form.ts
|
|
1506
|
-
var
|
|
1507
|
-
var
|
|
1508
|
-
var text5 = (value) => typeof value === "string" ? value : "";
|
|
2896
|
+
var readPath4 = (mount, name) => `${sshMountOf(mount)}/roles/${name}`;
|
|
2897
|
+
var text12 = (value) => typeof value === "string" ? value : "";
|
|
1509
2898
|
var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
1510
2899
|
var csvList = (value) => {
|
|
1511
|
-
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") :
|
|
2900
|
+
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text12(value).split(",");
|
|
1512
2901
|
return [
|
|
1513
2902
|
...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
|
|
1514
2903
|
].sort();
|
|
@@ -1519,12 +2908,12 @@ var extMap = (value) => {
|
|
|
1519
2908
|
return out;
|
|
1520
2909
|
const source = value;
|
|
1521
2910
|
for (const key of Object.keys(source).sort())
|
|
1522
|
-
out[key] =
|
|
2911
|
+
out[key] = text12(source[key]);
|
|
1523
2912
|
return out;
|
|
1524
2913
|
};
|
|
1525
|
-
var
|
|
2914
|
+
var resolve2 = (props) => ({
|
|
1526
2915
|
name: props.name,
|
|
1527
|
-
mount:
|
|
2916
|
+
mount: sshMountOf(props.mount),
|
|
1528
2917
|
allowedUsers: csvList(props.allowedUsers),
|
|
1529
2918
|
defaultUser: props.defaultUser ?? "",
|
|
1530
2919
|
allowUserCertificates: props.allowUserCertificates ?? false,
|
|
@@ -1540,12 +2929,12 @@ var resolve = (props) => ({
|
|
|
1540
2929
|
ttl: props.ttl,
|
|
1541
2930
|
maxTtl: props.maxTtl
|
|
1542
2931
|
});
|
|
1543
|
-
var
|
|
2932
|
+
var attributesOf14 = (form, live) => {
|
|
1544
2933
|
const attrs = {
|
|
1545
2934
|
name: form.name,
|
|
1546
2935
|
mount: form.mount,
|
|
1547
2936
|
allowedUsers: csvList(live["allowed_users"]),
|
|
1548
|
-
defaultUser:
|
|
2937
|
+
defaultUser: text12(live["default_user"]),
|
|
1549
2938
|
allowUserCertificates: bool3(live["allow_user_certificates"], false),
|
|
1550
2939
|
allowHostCertificates: bool3(live["allow_host_certificates"], false),
|
|
1551
2940
|
allowedDomains: csvList(live["allowed_domains"]),
|
|
@@ -1556,14 +2945,14 @@ var attributesOf8 = (form, live) => {
|
|
|
1556
2945
|
allowedCriticalOptions: csvList(live["allowed_critical_options"]),
|
|
1557
2946
|
defaultCriticalOptions: extMap(live["default_critical_options"]),
|
|
1558
2947
|
allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
|
|
1559
|
-
keyType:
|
|
2948
|
+
keyType: text12(live["key_type"]) || "ca",
|
|
1560
2949
|
ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
|
|
1561
2950
|
maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
|
|
1562
2951
|
};
|
|
1563
2952
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
1564
2953
|
};
|
|
1565
|
-
var mapValue = (
|
|
1566
|
-
var
|
|
2954
|
+
var mapValue = (map10) => extMap(map10);
|
|
2955
|
+
var writeBody10 = (form) => ({
|
|
1567
2956
|
key_type: "ca",
|
|
1568
2957
|
allowed_users: form.allowedUsers.join(","),
|
|
1569
2958
|
default_user: form.defaultUser,
|
|
@@ -1586,18 +2975,18 @@ var sameMap = (want, have) => {
|
|
|
1586
2975
|
return keys.length === Object.keys(have).length && keys.every((key) => have[key] === want[key]);
|
|
1587
2976
|
};
|
|
1588
2977
|
var sameTtl6 = (want, have) => parseDuration(want) === have;
|
|
1589
|
-
var
|
|
1590
|
-
var
|
|
2978
|
+
var badDurations = (form) => ["ttl", "maxTtl"].filter((field) => parseDuration(form[field]) === undefined);
|
|
2979
|
+
var matches12 = (attributes, form) => attributes.keyType === "ca" && sameList2(form.allowedUsers, attributes.allowedUsers) && attributes.defaultUser === form.defaultUser && attributes.allowUserCertificates === form.allowUserCertificates && attributes.allowHostCertificates === form.allowHostCertificates && sameList2(form.allowedDomains, attributes.allowedDomains) && attributes.allowSubdomains === form.allowSubdomains && attributes.allowBareDomains === form.allowBareDomains && sameList2(form.allowedExtensions, attributes.allowedExtensions) && sameMap(form.defaultExtensions, attributes.defaultExtensions) && sameList2(form.allowedCriticalOptions, attributes.allowedCriticalOptions) && sameMap(form.defaultCriticalOptions, attributes.defaultCriticalOptions) && attributes.allowEmptyPrincipals === form.allowEmptyPrincipals && sameTtl6(form.ttl, attributes.ttlSeconds) && sameTtl6(form.maxTtl, attributes.maxTtlSeconds);
|
|
1591
2980
|
|
|
1592
2981
|
// src/openbao/ssh-role.ts
|
|
1593
|
-
var BaoSshRole =
|
|
2982
|
+
var BaoSshRole = Resource14("Bao.SshRole", {
|
|
1594
2983
|
defaultRemovalPolicy: "retain"
|
|
1595
2984
|
});
|
|
1596
|
-
var readRole4 = (form) =>
|
|
2985
|
+
var readRole4 = (form) => Effect29.gen(function* () {
|
|
1597
2986
|
const live = yield* baoRead(readPath4(form.mount, form.name));
|
|
1598
2987
|
if (live === undefined)
|
|
1599
2988
|
return;
|
|
1600
|
-
return { attributes:
|
|
2989
|
+
return { attributes: attributesOf14(form, live), live };
|
|
1601
2990
|
});
|
|
1602
2991
|
var UNMANAGED_DEFAULTS = {
|
|
1603
2992
|
algorithm_signer: "default",
|
|
@@ -1612,59 +3001,59 @@ var UNMANAGED_DEFAULTS = {
|
|
|
1612
3001
|
not_before_duration: 30
|
|
1613
3002
|
};
|
|
1614
3003
|
var isDefault = (live, field) => live[field] === undefined || live[field] === "" || live[field] === UNMANAGED_DEFAULTS[field];
|
|
1615
|
-
var
|
|
3004
|
+
var wouldErase2 = (live) => {
|
|
1616
3005
|
const lost = Object.keys(UNMANAGED_DEFAULTS).filter((field) => !isDefault(live, field));
|
|
1617
3006
|
if (Object.keys(extMap(live["allowed_user_key_lengths"])).length > 0) {
|
|
1618
3007
|
lost.push("allowed_user_key_lengths");
|
|
1619
3008
|
}
|
|
1620
3009
|
return lost.sort();
|
|
1621
3010
|
};
|
|
1622
|
-
var
|
|
1623
|
-
var BaoSshRoleProvider = () =>
|
|
1624
|
-
list: () =>
|
|
1625
|
-
read:
|
|
1626
|
-
const found = yield* readRole4(
|
|
3011
|
+
var refuse6 = (message) => Effect29.die(new Error(message));
|
|
3012
|
+
var BaoSshRoleProvider = () => Provider14.effect(BaoSshRole, Effect29.succeed(BaoSshRole.Provider.of({
|
|
3013
|
+
list: () => Effect29.succeed([]),
|
|
3014
|
+
read: Effect29.fn(function* ({ olds }) {
|
|
3015
|
+
const found = yield* readRole4(resolve2(olds));
|
|
1627
3016
|
return found?.attributes;
|
|
1628
3017
|
}),
|
|
1629
|
-
diff:
|
|
1630
|
-
if (output === undefined || !
|
|
3018
|
+
diff: Effect29.fn(function* ({ news, output }) {
|
|
3019
|
+
if (output === undefined || !isResolved14(news))
|
|
1631
3020
|
return;
|
|
1632
|
-
const form =
|
|
3021
|
+
const form = resolve2(news);
|
|
1633
3022
|
if (output.mount !== form.mount || output.name !== form.name) {
|
|
1634
3023
|
return { action: "replace" };
|
|
1635
3024
|
}
|
|
1636
3025
|
const found = yield* readRole4(form);
|
|
1637
3026
|
if (found === undefined)
|
|
1638
3027
|
return { action: "update" };
|
|
1639
|
-
return
|
|
3028
|
+
return matches12(found.attributes, form) ? { action: "noop" } : { action: "update" };
|
|
1640
3029
|
}),
|
|
1641
|
-
reconcile:
|
|
1642
|
-
const form =
|
|
3030
|
+
reconcile: Effect29.fn(function* ({ news }) {
|
|
3031
|
+
const form = resolve2(news);
|
|
1643
3032
|
const path = readPath4(form.mount, form.name);
|
|
1644
|
-
const bad =
|
|
3033
|
+
const bad = badDurations(form);
|
|
1645
3034
|
if (bad.length > 0) {
|
|
1646
|
-
return yield*
|
|
3035
|
+
return yield* refuse6(`Bao.SshRole ${path}: ${bad.join(", ")} is not a duration parseDuration understands ` + "(`0` or `<n>s|m|h|d`). It would write, then report update on every plan forever.");
|
|
1647
3036
|
}
|
|
1648
3037
|
if (form.allowUserCertificates && form.allowedUsers.length === 0 && !form.allowEmptyPrincipals) {
|
|
1649
|
-
return yield*
|
|
3038
|
+
return yield* refuse6(`Bao.SshRole ${path}: allowUserCertificates with an empty allowedUsers and ` + "allowEmptyPrincipals false. Every sign request against it would be rejected.");
|
|
1650
3039
|
}
|
|
1651
3040
|
const found = yield* readRole4(form);
|
|
1652
|
-
if (found === undefined || !
|
|
3041
|
+
if (found === undefined || !matches12(found.attributes, form)) {
|
|
1653
3042
|
if (found !== undefined) {
|
|
1654
|
-
const lost =
|
|
3043
|
+
const lost = wouldErase2(found.live);
|
|
1655
3044
|
if (lost.length > 0) {
|
|
1656
|
-
return yield*
|
|
3045
|
+
return yield* refuse6(`Bao.SshRole ${path}: live role carries ${lost.join(", ")}, which this ` + "resource does not declare and the full-replace write would ERASE. " + "Fold those settings into the declaration, or clear them by hand first.");
|
|
1657
3046
|
}
|
|
1658
3047
|
}
|
|
1659
|
-
yield* baoWrite("PUT", path,
|
|
3048
|
+
yield* baoWrite("PUT", path, writeBody10(form));
|
|
1660
3049
|
}
|
|
1661
3050
|
const after = yield* readRole4(form);
|
|
1662
3051
|
if (after === undefined) {
|
|
1663
|
-
return yield*
|
|
3052
|
+
return yield* refuse6(`Bao.SshRole ${path}: write returned no error but the role is still absent.`);
|
|
1664
3053
|
}
|
|
1665
3054
|
return after.attributes;
|
|
1666
3055
|
}),
|
|
1667
|
-
delete:
|
|
3056
|
+
delete: Effect29.fn(function* ({ output }) {
|
|
1668
3057
|
yield* baoDelete(readPath4(output.mount, output.name));
|
|
1669
3058
|
return;
|
|
1670
3059
|
})
|
|
@@ -1676,10 +3065,24 @@ export {
|
|
|
1676
3065
|
BaoAuthRoleProvider,
|
|
1677
3066
|
BaoCloudflareRole,
|
|
1678
3067
|
BaoCloudflareRoleProvider,
|
|
3068
|
+
BaoIdentityError,
|
|
3069
|
+
BaoJwtAuthConfig,
|
|
3070
|
+
BaoJwtAuthConfigProvider,
|
|
3071
|
+
BaoJwtRole,
|
|
3072
|
+
BaoJwtRoleProvider,
|
|
3073
|
+
BaoKubernetesRole,
|
|
3074
|
+
BaoKubernetesRoleProvider,
|
|
3075
|
+
BaoLoginError,
|
|
3076
|
+
BaoMfaLoginEnforcement,
|
|
3077
|
+
BaoMfaLoginEnforcementProvider,
|
|
3078
|
+
BaoMfaTotpMethod,
|
|
3079
|
+
BaoMfaTotpMethodProvider,
|
|
1679
3080
|
BaoMount,
|
|
1680
3081
|
BaoMountProvider,
|
|
1681
3082
|
BaoPkiRole,
|
|
1682
3083
|
BaoPkiRoleProvider,
|
|
3084
|
+
BaoPlugin,
|
|
3085
|
+
BaoPluginProvider,
|
|
1683
3086
|
BaoPolicy,
|
|
1684
3087
|
BaoPolicyProvider,
|
|
1685
3088
|
BaoProxmoxRole,
|
|
@@ -1687,13 +3090,22 @@ export {
|
|
|
1687
3090
|
BaoSshRole,
|
|
1688
3091
|
BaoSshRoleProvider,
|
|
1689
3092
|
CloudflarePermissionGroups,
|
|
3093
|
+
HOST_ROLE_SEPARATOR,
|
|
3094
|
+
appRoleLogin,
|
|
3095
|
+
appRoleLoginEffect,
|
|
3096
|
+
assertBaoIdentity,
|
|
3097
|
+
assertBaoIdentityEffect,
|
|
1690
3098
|
expandAccount,
|
|
1691
3099
|
expandAll,
|
|
3100
|
+
hostAppRoles,
|
|
3101
|
+
hostRoleName,
|
|
1692
3102
|
mountName,
|
|
1693
3103
|
parseRolesConfig,
|
|
1694
3104
|
permissionGroupsFromEngine,
|
|
1695
|
-
permissionGroupsFromLiveRoles
|
|
3105
|
+
permissionGroupsFromLiveRoles,
|
|
3106
|
+
revokeSelf,
|
|
3107
|
+
revokeSelfEffect
|
|
1696
3108
|
};
|
|
1697
3109
|
|
|
1698
|
-
//# debugId=
|
|
3110
|
+
//# debugId=7E1DFE85CC398C5464756E2164756E21
|
|
1699
3111
|
//# sourceMappingURL=index.js.map
|