@homeflare/alchemy 0.2.0 → 0.2.2
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 +3 -1
- package/dist/openbao/bao-http.d.ts.map +1 -1
- package/dist/openbao/index.d.ts +8 -3
- package/dist/openbao/index.d.ts.map +1 -1
- package/dist/openbao/index.js +432 -18
- package/dist/openbao/index.js.map +7 -4
- package/package.json +1 -1
- package/src/openbao/bao-http.ts +20 -4
- package/src/openbao/index.ts +12 -3
- package/src/proxmox/client.test.ts +7 -1
package/README.md
CHANGED
|
@@ -91,7 +91,8 @@ two copies load in one process.
|
|
|
91
91
|
"effect": "4.0.0-rc.112",
|
|
92
92
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
93
93
|
"@effect/platform-node-shared": "4.0.0-rc.112",
|
|
94
|
-
"@effect/platform-bun": "4.0.0-rc.112"
|
|
94
|
+
"@effect/platform-bun": "4.0.0-rc.112",
|
|
95
|
+
"rolldown": "1.2.8"
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
```
|
|
@@ -103,6 +104,7 @@ line is not semver-compatible with itself, and two separate skews bite:
|
|
|
103
104
|
| ---------------------------------------------------------------- | ------------------------------------------------------ |
|
|
104
105
|
| `effect` → rc.115 | `TypeError: Config.string is not a function` at import |
|
|
105
106
|
| `@effect/platform-node-shared` → rc.115 while `effect` is rc.112 | `Cannot find module 'effect/ByteSize'` |
|
|
107
|
+
| `rolldown` → 1.2.9 via vite's `~1.2.6` (measured 2026-09-16) | `GET …/rolldown-1.2.9.tgz - 404` at `bun add` |
|
|
106
108
|
|
|
107
109
|
The second is the nastier one: `@effect/platform-bun@rc.112` depends on
|
|
108
110
|
`platform-node-shared` at `^4.0.0-rc.112`, which resolves _up_ to rc.115 — whose own peer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bao-http.d.ts","sourceRoot":"","sources":["../../src/openbao/bao-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"bao-http.d.ts","sourceRoot":"","sources":["../../src/openbao/bao-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAC;AAG9C,OAAO,KAAK,UAAU,MAAM,iCAAiC,CAAC;AAG9D,OAAO,EAAE,QAAQ,EAAE,KAAK,SAAS,EAAU,MAAM,iBAAiB,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,MAAM,iEAEjB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,eAAO,MAAM,OAAO,wCAElB,CAAC;AAiEH,mGAAmG;AACnG,eAAO,MAAM,OAAO,WACV,SAAS,UACT,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,QACnC,MAAM,SACL,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,wFAoCtC,CAAC;AAEL;;;;;GAKG;AACH,eAAO,MAAM,OAAO,SAAU,MAAM,wFAOhC,CAAC;AAEL;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,WACX,MAAM,GAAG,KAAK,QAChB,MAAM,QACN,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,yDACe,CAAC;AAEzD,kGAAkG;AAClG,eAAO,MAAM,SAAS,SAAU,MAAM,yDAAqD,CAAC"}
|
package/dist/openbao/index.d.ts
CHANGED
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
|
|
9
9
|
* deliberate, visible act rather than an accident of barrelling.
|
|
10
10
|
*/
|
|
11
|
-
export { BaoAuthRoleProvider } from './auth-role.ts';
|
|
12
|
-
export { BaoCloudflareRoleProvider } from './cloudflare-role.ts';
|
|
13
|
-
export {
|
|
11
|
+
export { BaoAuthRole, BaoAuthRoleProvider } from './auth-role.ts';
|
|
12
|
+
export { BaoCloudflareRole, BaoCloudflareRoleProvider } from './cloudflare-role.ts';
|
|
13
|
+
export { CloudflarePermissionGroups, permissionGroupsFromEngine, permissionGroupsFromLiveRoles, } from './cloudflare-permission-groups.ts';
|
|
14
|
+
export type { CloudflareZone, ExpandedRole } from './cloudflare-roles-expand.ts';
|
|
15
|
+
export { expandAccount, expandAll, mountName } from './cloudflare-roles-expand.ts';
|
|
16
|
+
export type { RolesConfig, SurfaceSpec } from './cloudflare-roles-config.ts';
|
|
17
|
+
export { parseRolesConfig } from './cloudflare-roles-config.ts';
|
|
18
|
+
export { BaoMount, BaoMountProvider } from './mount.ts';
|
|
14
19
|
export { BaoPkiRoleProvider } from './pki-role.ts';
|
|
15
20
|
export { BaoPolicy, BaoPolicyProvider } from './policy.ts';
|
|
16
21
|
export { BaoProxmoxRole, BaoProxmoxRoleProvider } from './proxmox-role.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/openbao/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/openbao/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACpF,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACnF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/openbao/index.js
CHANGED
|
@@ -150,6 +150,7 @@ var matches2 = (attributes, props) => {
|
|
|
150
150
|
import * as Context from "effect/Context";
|
|
151
151
|
import * as Effect from "effect/Effect";
|
|
152
152
|
import * as Option from "effect/Option";
|
|
153
|
+
import * as Schedule from "effect/Schedule";
|
|
153
154
|
import * as Semaphore from "effect/Semaphore";
|
|
154
155
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
155
156
|
import * as Headers from "effect/unstable/http/Headers";
|
|
@@ -239,6 +240,11 @@ var BaoGate = Context.Reference("homeflare/openbao/BaoGate", {
|
|
|
239
240
|
defaultValue: () => Semaphore.makeUnsafe(MAX_IN_FLIGHT)
|
|
240
241
|
});
|
|
241
242
|
var TIMEOUT = "60 seconds";
|
|
243
|
+
var retryTransport = (effect) => Effect.retry(effect, {
|
|
244
|
+
schedule: Schedule.spaced("750 millis"),
|
|
245
|
+
times: 2,
|
|
246
|
+
while: (error) => error.status === 0
|
|
247
|
+
});
|
|
242
248
|
var redactingToken = (effect) => Effect.gen(function* () {
|
|
243
249
|
const names = yield* Headers.CurrentRedactedNames;
|
|
244
250
|
return yield* Effect.provideService(effect, Headers.CurrentRedactedNames, [
|
|
@@ -264,7 +270,7 @@ var baoCall = (intent, method, path, body) => Effect.gen(function* () {
|
|
|
264
270
|
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"));
|
|
265
271
|
const exchange = client.execute(request).pipe(Effect.flatMap((response) => Effect.map(response.text, (text3) => ({ status: response.status, text: text3 }))), Effect.timeout(TIMEOUT), Effect.mapError((cause) => new BaoError(0, method, path, [cause.message])));
|
|
266
272
|
const gate = yield* BaoGate;
|
|
267
|
-
const { status, text: text2 } = yield* exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1));
|
|
273
|
+
const { status, text: text2 } = yield* retryTransport(exchange.pipe(overSocket(address.socket, method, path), redactingToken, Semaphore.withPermits(gate, 1)));
|
|
268
274
|
const outcome = settle(intent, method, path, status, text2);
|
|
269
275
|
return "error" in outcome ? yield* Effect.fail(outcome.error) : outcome.body;
|
|
270
276
|
});
|
|
@@ -325,11 +331,15 @@ import * as Provider2 from "alchemy/Provider";
|
|
|
325
331
|
import * as Effect5 from "effect/Effect";
|
|
326
332
|
|
|
327
333
|
// src/openbao/cloudflare-group-scope.ts
|
|
334
|
+
var zoneResource = (zoneId) => `com.cloudflare.api.account.zone.${zoneId}`;
|
|
335
|
+
var accountResource = (accountId) => `com.cloudflare.api.account.${accountId}`;
|
|
336
|
+
var bucketResource = (accountId, jurisdiction, bucket) => `com.cloudflare.edge.r2.bucket.${accountId}_${jurisdiction}_${bucket}`;
|
|
328
337
|
var GROUP_SCOPES = {
|
|
329
338
|
account: "com.cloudflare.api.account",
|
|
330
339
|
bucket: "com.cloudflare.edge.r2.bucket",
|
|
331
340
|
zone: "com.cloudflare.api.account.zone"
|
|
332
341
|
};
|
|
342
|
+
var groupScopeNamed = (word) => Object.entries(GROUP_SCOPES).find(([name]) => name === word)?.[1];
|
|
333
343
|
var scopeOfResource = (resource) => {
|
|
334
344
|
const scopes = [GROUP_SCOPES.zone, GROUP_SCOPES.bucket, GROUP_SCOPES.account];
|
|
335
345
|
return scopes.find((scope) => resource.startsWith(`${scope}.`)) ?? resource;
|
|
@@ -340,7 +350,100 @@ var groupKey = (name, scope) => `${scope} ${name}`;
|
|
|
340
350
|
import * as Context2 from "effect/Context";
|
|
341
351
|
import * as Effect4 from "effect/Effect";
|
|
342
352
|
|
|
353
|
+
// src/openbao/cloudflare-permission-groups-solve.ts
|
|
354
|
+
var observationsOf = (declared, live) => declared.flatMap((role) => {
|
|
355
|
+
const entries = live.get(`${role.mount}/${role.name}`);
|
|
356
|
+
if (entries === undefined)
|
|
357
|
+
return [];
|
|
358
|
+
return role.policies.flatMap((policy, i) => {
|
|
359
|
+
const twins = role.policies.filter((other) => other.resource === policy.resource);
|
|
360
|
+
const matching = entries.filter((entry) => Object.keys(entry.resources).length === 1 && (policy.resource in entry.resources));
|
|
361
|
+
const [only] = matching;
|
|
362
|
+
if (twins.length !== 1 || matching.length !== 1 || only === undefined)
|
|
363
|
+
return [];
|
|
364
|
+
return [
|
|
365
|
+
{
|
|
366
|
+
ids: only.groupIds,
|
|
367
|
+
names: policy.groups,
|
|
368
|
+
where: `${role.mount}/${role.name}[${String(i)}]`
|
|
369
|
+
}
|
|
370
|
+
];
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
var unique = (items) => [...new Set(items)];
|
|
374
|
+
var solveGroups = (observations) => {
|
|
375
|
+
const idsOf = new Map;
|
|
376
|
+
const namesOf = new Map;
|
|
377
|
+
const bind = (name, id) => {
|
|
378
|
+
const ids = idsOf.get(name) ?? new Set;
|
|
379
|
+
const fresh = !ids.has(id);
|
|
380
|
+
idsOf.set(name, ids.add(id));
|
|
381
|
+
namesOf.set(id, (namesOf.get(id) ?? new Set).add(name));
|
|
382
|
+
return fresh;
|
|
383
|
+
};
|
|
384
|
+
const known = (name) => {
|
|
385
|
+
const ids = idsOf.get(name);
|
|
386
|
+
const [id] = ids ?? [];
|
|
387
|
+
return ids?.size === 1 && id !== undefined && namesOf.get(id)?.size === 1 ? id : undefined;
|
|
388
|
+
};
|
|
389
|
+
const shaped = observations.map((o) => ({ ids: unique(o.ids), names: unique(o.names) }));
|
|
390
|
+
for (const { ids, names } of shaped) {
|
|
391
|
+
const [name] = names;
|
|
392
|
+
const [id] = ids;
|
|
393
|
+
if (names.length === 1 && ids.length === 1 && name !== undefined && id !== undefined) {
|
|
394
|
+
bind(name, id);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (let changed = true;changed; ) {
|
|
398
|
+
changed = false;
|
|
399
|
+
for (const { ids, names } of shaped) {
|
|
400
|
+
if (names.length !== ids.length)
|
|
401
|
+
continue;
|
|
402
|
+
const knownIds = new Set(names.map(known));
|
|
403
|
+
const openNames = names.filter((name2) => known(name2) === undefined);
|
|
404
|
+
const openIds = ids.filter((id2) => !knownIds.has(id2));
|
|
405
|
+
const [name] = openNames;
|
|
406
|
+
const [id] = openIds;
|
|
407
|
+
if (openNames.length === 1 && openIds.length === 1 && name !== undefined && id !== undefined) {
|
|
408
|
+
changed = bind(name, id) || changed;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const contradicted = [];
|
|
413
|
+
for (const o of observations) {
|
|
414
|
+
for (const name of unique(o.names)) {
|
|
415
|
+
const id = known(name);
|
|
416
|
+
if (id !== undefined && !o.ids.includes(id))
|
|
417
|
+
contradicted.push({ id, name, where: o.where });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const groups = new Map;
|
|
421
|
+
const ambiguous = [];
|
|
422
|
+
const unresolved = [];
|
|
423
|
+
for (const name of unique(observations.flatMap((o) => o.names)).sort()) {
|
|
424
|
+
const ids = idsOf.get(name);
|
|
425
|
+
const id = known(name);
|
|
426
|
+
if (ids === undefined)
|
|
427
|
+
unresolved.push(name);
|
|
428
|
+
else if (ids.size > 1)
|
|
429
|
+
ambiguous.push({ bound: [...ids].sort(), kind: "name", value: name });
|
|
430
|
+
else if (id !== undefined)
|
|
431
|
+
groups.set(name, id);
|
|
432
|
+
}
|
|
433
|
+
for (const [id, names] of [...namesOf].sort(([a], [b]) => a < b ? -1 : 1)) {
|
|
434
|
+
if (names.size > 1)
|
|
435
|
+
ambiguous.push({ bound: [...names].sort(), kind: "id", value: id });
|
|
436
|
+
}
|
|
437
|
+
return { ambiguous, contradicted, groups, unresolved };
|
|
438
|
+
};
|
|
439
|
+
var describeProblems = (solved) => [
|
|
440
|
+
...solved.ambiguous.map((c) => c.kind === "name" ? `ambiguous: name ${JSON.stringify(c.value)} is bound to ids ${c.bound.join(", ")}` : `ambiguous: id ${c.value} is bound to names ${c.bound.map((n) => JSON.stringify(n)).join(", ")}`),
|
|
441
|
+
...solved.unresolved.map((name) => `unresolved: no live role isolates ${JSON.stringify(name)} to one id`),
|
|
442
|
+
...solved.contradicted.map((c) => `contradicted: ${c.where} declares ${JSON.stringify(c.name)} but lacks its id ${c.id}`)
|
|
443
|
+
];
|
|
444
|
+
|
|
343
445
|
// src/openbao/cloudflare-policy.ts
|
|
446
|
+
var atEveryScope = (byName) => new Map([...byName].flatMap(([name, id]) => Object.values(GROUP_SCOPES).map((scope) => [groupKey(name, scope), id])));
|
|
344
447
|
var resolvePolicies = (declared, ids) => {
|
|
345
448
|
const missing = new Set;
|
|
346
449
|
const policies = declared.map((policy) => {
|
|
@@ -485,8 +588,125 @@ var writeCloudflareRole = (mount, name, body) => baoWrite("PUT", rolePath(mount,
|
|
|
485
588
|
var deleteCloudflareRole = (mount, name) => baoDelete(rolePath(mount, name));
|
|
486
589
|
|
|
487
590
|
// src/openbao/cloudflare-permission-groups.ts
|
|
591
|
+
class CloudflareGroupsError extends Error {
|
|
592
|
+
scope;
|
|
593
|
+
problems;
|
|
594
|
+
constructor(scope, problems) {
|
|
595
|
+
super(`Cloudflare permission groups for ${scope}: ${problems.join("; ")}`);
|
|
596
|
+
this.scope = scope;
|
|
597
|
+
this.problems = problems;
|
|
598
|
+
this.name = "CloudflareGroupsError";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
488
602
|
class CloudflarePermissionGroups extends Context2.Service()("homeflare/openbao/CloudflarePermissionGroups") {
|
|
489
603
|
}
|
|
604
|
+
var pick = (mount, groups, refs, hint) => {
|
|
605
|
+
const unknown = refs.filter((ref) => !groups.has(groupKey(ref.name, ref.scope)));
|
|
606
|
+
if (unknown.length > 0) {
|
|
607
|
+
const problems = unknown.map((ref) => `unknown group ${JSON.stringify(ref.name)} at scope ${ref.scope} — ${hint}`);
|
|
608
|
+
return Effect4.fail(new CloudflareGroupsError(mount, problems));
|
|
609
|
+
}
|
|
610
|
+
return Effect4.succeed(new Map(refs.flatMap((ref) => {
|
|
611
|
+
const key = groupKey(ref.name, ref.scope);
|
|
612
|
+
const id = groups.get(key);
|
|
613
|
+
return id === undefined ? [] : [[key, id]];
|
|
614
|
+
})));
|
|
615
|
+
};
|
|
616
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
617
|
+
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";
|
|
618
|
+
var deniedNamed = (mount, read) => Effect4.catchIf(read, (error) => error.status === 403, () => Effect4.fail(new CloudflareGroupsError(mount, [deniedHint(mount)])));
|
|
619
|
+
var engineGroups = (mount) => Effect4.flatMap(deniedNamed(mount, baoRead(`${mountPath(mount)}/permission-groups`)), (data) => {
|
|
620
|
+
const list = data?.["permission_groups"];
|
|
621
|
+
if (data === undefined) {
|
|
622
|
+
const why = "GET <mount>/permission-groups answered 404 — the endpoint is not deployed here";
|
|
623
|
+
return Effect4.fail(new CloudflareGroupsError(mount, [why]));
|
|
624
|
+
}
|
|
625
|
+
if (!Array.isArray(list)) {
|
|
626
|
+
return Effect4.fail(new CloudflareGroupsError(mount, ["no permission_groups array in reply"]));
|
|
627
|
+
}
|
|
628
|
+
const scoped = new Map;
|
|
629
|
+
const idsOf = new Map;
|
|
630
|
+
const problems = [];
|
|
631
|
+
for (const item of list) {
|
|
632
|
+
const name = isRecord2(item) ? item["name"] : undefined;
|
|
633
|
+
const id = isRecord2(item) ? item["id"] : undefined;
|
|
634
|
+
const scopes = isRecord2(item) ? item["scopes"] : undefined;
|
|
635
|
+
if (typeof name !== "string" || typeof id !== "string" || !Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string")) {
|
|
636
|
+
problems.push("an entry without a string name, id and scopes");
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
idsOf.set(name, (idsOf.get(name) ?? new Set).add(id));
|
|
640
|
+
for (const scope of scopes) {
|
|
641
|
+
const key = groupKey(name, scope);
|
|
642
|
+
if (scoped.has(key) && scoped.get(key) !== id) {
|
|
643
|
+
problems.push(`ambiguous: ${JSON.stringify(name)} is listed with two ids at scope ${scope}`);
|
|
644
|
+
} else {
|
|
645
|
+
scoped.set(key, id);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const sole = new Map([...idsOf].flatMap(([name, ids]) => {
|
|
650
|
+
const only = [...ids];
|
|
651
|
+
return ids.size === 1 && only[0] !== undefined ? [[name, only[0]]] : [];
|
|
652
|
+
}));
|
|
653
|
+
return problems.length > 0 ? Effect4.fail(new CloudflareGroupsError(mount, problems)) : Effect4.succeed(new Map([...atEveryScope(sole), ...scoped]));
|
|
654
|
+
});
|
|
655
|
+
var permissionGroupsFromEngine = () => Effect4.sync(() => {
|
|
656
|
+
const cache = new Map;
|
|
657
|
+
const cachedFor = (mount) => Effect4.suspend(() => {
|
|
658
|
+
const hit = cache.get(mountPath(mount));
|
|
659
|
+
if (hit !== undefined)
|
|
660
|
+
return hit;
|
|
661
|
+
return Effect4.flatMap(Effect4.cached(engineGroups(mount)), (lookup) => {
|
|
662
|
+
cache.set(mountPath(mount), lookup);
|
|
663
|
+
return lookup;
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
return CloudflarePermissionGroups.of({
|
|
667
|
+
resolve: (mount, refs) => Effect4.flatMap(cachedFor(mount), (groups) => pick(mount, groups, refs, "the engine does not list it for this account at that scope"))
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
var permissionGroupsFromLiveRoles = (declared) => Effect4.gen(function* () {
|
|
671
|
+
const accountOf = new Map;
|
|
672
|
+
const byAccount = new Map;
|
|
673
|
+
for (const [mount, roles] of declared) {
|
|
674
|
+
for (const role of roles) {
|
|
675
|
+
accountOf.set(mount, role.account);
|
|
676
|
+
byAccount.set(role.account, [...byAccount.get(role.account) ?? [], role]);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const solved = new Map;
|
|
680
|
+
for (const [account, roles] of byAccount) {
|
|
681
|
+
const solve = Effect4.gen(function* () {
|
|
682
|
+
const live = new Map;
|
|
683
|
+
yield* Effect4.all(roles.map((role) => Effect4.map(readCloudflareRole(role.mount, role.name), (read) => {
|
|
684
|
+
if (read?.policies !== undefined) {
|
|
685
|
+
live.set(`${role.mount}/${role.name}`, read.policies);
|
|
686
|
+
}
|
|
687
|
+
})), { concurrency: 8, discard: true });
|
|
688
|
+
const result = solveGroups(observationsOf(roles, live));
|
|
689
|
+
const problems = describeProblems(result);
|
|
690
|
+
if (problems.length > 0) {
|
|
691
|
+
return yield* Effect4.fail(new CloudflareGroupsError(`account ${account}`, problems));
|
|
692
|
+
}
|
|
693
|
+
return atEveryScope(result.groups);
|
|
694
|
+
});
|
|
695
|
+
solved.set(account, yield* Effect4.cached(solve));
|
|
696
|
+
}
|
|
697
|
+
return CloudflarePermissionGroups.of({
|
|
698
|
+
resolve: (mount, refs) => {
|
|
699
|
+
const account = accountOf.get(mountPath(mount));
|
|
700
|
+
const lookup = account === undefined ? undefined : solved.get(account);
|
|
701
|
+
if (account === undefined || lookup === undefined) {
|
|
702
|
+
const why = "no declared role on this mount, so no live role can vouch for its groups";
|
|
703
|
+
return Effect4.fail(new CloudflareGroupsError(mount, [why]));
|
|
704
|
+
}
|
|
705
|
+
const hint = `no live role on account ${account} carries it; a NEW group needs the engine endpoint`;
|
|
706
|
+
return Effect4.flatMap(lookup, (groups) => pick(mount, groups, refs, hint));
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
});
|
|
490
710
|
|
|
491
711
|
// src/openbao/cloudflare-role.ts
|
|
492
712
|
var BaoCloudflareRole = Resource2("Bao.CloudflareRole", {
|
|
@@ -552,6 +772,190 @@ var BaoCloudflareRoleProvider = () => Provider2.effect(BaoCloudflareRole, Effect
|
|
|
552
772
|
})
|
|
553
773
|
});
|
|
554
774
|
}));
|
|
775
|
+
// src/openbao/cloudflare-roles-expand.ts
|
|
776
|
+
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
777
|
+
var zoneSlug = (zone) => `${slug(zone.name)}${zone.internal ? "-internal" : ""}`;
|
|
778
|
+
var mountName = (account, surface) => `cloudflare-${account}-${surface}`;
|
|
779
|
+
var policiesFor = (tpl, accountId, zone, accountGroups) => {
|
|
780
|
+
if (tpl.bucket !== undefined && zone !== null) {
|
|
781
|
+
throw new Error(`${tpl.name}: a zone role cannot be scoped to R2 bucket ${tpl.bucket}`);
|
|
782
|
+
}
|
|
783
|
+
const accountScoped = tpl.bucket === undefined ? accountResource(accountId) : bucketResource(accountId, tpl.bucketJurisdiction, tpl.bucket);
|
|
784
|
+
const resource = zone === null ? accountScoped : zoneResource(zone.id);
|
|
785
|
+
const first = {
|
|
786
|
+
effect: "allow",
|
|
787
|
+
groupOrder: "declared",
|
|
788
|
+
groups: tpl.groups,
|
|
789
|
+
resource,
|
|
790
|
+
...tpl.groupsScope === undefined ? {} : { scope: tpl.groupsScope }
|
|
791
|
+
};
|
|
792
|
+
if (accountGroups.length === 0)
|
|
793
|
+
return [first];
|
|
794
|
+
const second = {
|
|
795
|
+
effect: "allow",
|
|
796
|
+
groupOrder: "by-id",
|
|
797
|
+
groups: accountGroups,
|
|
798
|
+
resource: accountResource(accountId)
|
|
799
|
+
};
|
|
800
|
+
return [first, second];
|
|
801
|
+
};
|
|
802
|
+
var build = (account, surface, tpl, zone, accountGroups) => ({
|
|
803
|
+
account: account.key,
|
|
804
|
+
accountId: account.id,
|
|
805
|
+
catalog: {
|
|
806
|
+
description: tpl.desc,
|
|
807
|
+
maxTtl: tpl.maxTtl,
|
|
808
|
+
permissions: [...tpl.groups, ...accountGroups],
|
|
809
|
+
ttl: tpl.ttl,
|
|
810
|
+
zone: zone === null ? null : zone.name
|
|
811
|
+
},
|
|
812
|
+
description: zone === null ? tpl.desc : `${tpl.desc} (zone: ${zone.name})`,
|
|
813
|
+
maxTtl: tpl.maxTtl,
|
|
814
|
+
mount: mountName(account.key, surface),
|
|
815
|
+
name: zone === null ? tpl.name : `${zoneSlug(zone)}-${tpl.name}`,
|
|
816
|
+
policies: policiesFor(tpl, account.id, zone, accountGroups),
|
|
817
|
+
surface,
|
|
818
|
+
ttl: tpl.ttl
|
|
819
|
+
});
|
|
820
|
+
var expandAccount = (config, account, zones) => {
|
|
821
|
+
const roles = [];
|
|
822
|
+
for (const surface of account.surfaces) {
|
|
823
|
+
const spec = config.surfaces[surface];
|
|
824
|
+
if (spec === undefined)
|
|
825
|
+
throw new Error(`surface ${surface} is not defined`);
|
|
826
|
+
for (const tpl of spec.zoneRoles) {
|
|
827
|
+
const only = tpl.zones;
|
|
828
|
+
const internalOk = tpl.zoneTypes.includes("internal");
|
|
829
|
+
for (const zone of zones) {
|
|
830
|
+
if (account.zones !== "all" && !account.zones.includes(zone.name))
|
|
831
|
+
continue;
|
|
832
|
+
if (only !== undefined && only.length > 0 && !only.includes(zone.name))
|
|
833
|
+
continue;
|
|
834
|
+
if (zone.internal && !internalOk)
|
|
835
|
+
continue;
|
|
836
|
+
roles.push(build(account, surface, tpl, zone, tpl.accountGroups));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const tpl of spec.accountRoles)
|
|
840
|
+
roles.push(build(account, surface, tpl, null, []));
|
|
841
|
+
}
|
|
842
|
+
return roles;
|
|
843
|
+
};
|
|
844
|
+
var expandAll = (config, zonesByAccount) => {
|
|
845
|
+
const byMount = new Map;
|
|
846
|
+
const seen = new Set;
|
|
847
|
+
for (const account of config.accounts) {
|
|
848
|
+
const zones = zonesByAccount[account.key];
|
|
849
|
+
if (zones === undefined)
|
|
850
|
+
throw new Error(`no zone inventory for account ${account.key}`);
|
|
851
|
+
for (const role of expandAccount(config, account, zones)) {
|
|
852
|
+
const key = `${role.mount}/roles/${role.name}`;
|
|
853
|
+
if (seen.has(key))
|
|
854
|
+
throw new Error(`roles.yaml expands ${key} twice`);
|
|
855
|
+
seen.add(key);
|
|
856
|
+
const mount = byMount.get(role.mount) ?? [];
|
|
857
|
+
mount.push(role);
|
|
858
|
+
byMount.set(role.mount, mount);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return byMount;
|
|
862
|
+
};
|
|
863
|
+
// src/openbao/cloudflare-roles-config.ts
|
|
864
|
+
class RolesConfigError extends Error {
|
|
865
|
+
constructor(where, problem) {
|
|
866
|
+
super(`roles.yaml ${where}: ${problem}`);
|
|
867
|
+
this.name = "RolesConfigError";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
var isRaw = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
871
|
+
var record = (value, where) => {
|
|
872
|
+
if (!isRaw(value))
|
|
873
|
+
throw new RolesConfigError(where, "expected a mapping");
|
|
874
|
+
return value;
|
|
875
|
+
};
|
|
876
|
+
var text2 = (raw, key, where) => {
|
|
877
|
+
const value = raw[key];
|
|
878
|
+
if (typeof value === "string")
|
|
879
|
+
return value;
|
|
880
|
+
throw new RolesConfigError(`${where}.${key}`, `expected a string, got ${JSON.stringify(value)}`);
|
|
881
|
+
};
|
|
882
|
+
var texts = (raw, key, where) => {
|
|
883
|
+
const value = raw[key];
|
|
884
|
+
if (value === undefined || value === null)
|
|
885
|
+
return;
|
|
886
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
throw new RolesConfigError(`${where}.${key}`, "expected a list of strings");
|
|
890
|
+
};
|
|
891
|
+
var entries = (raw, key, where) => {
|
|
892
|
+
const value = raw[key];
|
|
893
|
+
if (value === undefined || value === null)
|
|
894
|
+
return [];
|
|
895
|
+
if (!Array.isArray(value))
|
|
896
|
+
throw new RolesConfigError(`${where}.${key}`, "expected a list");
|
|
897
|
+
return value;
|
|
898
|
+
};
|
|
899
|
+
var template = (value, where) => {
|
|
900
|
+
const raw = record(value, where);
|
|
901
|
+
const groups = texts(raw, "groups", where);
|
|
902
|
+
if (groups === undefined)
|
|
903
|
+
throw new RolesConfigError(`${where}.groups`, "is required");
|
|
904
|
+
const bucket = raw["bucket"] === undefined ? undefined : text2(raw, "bucket", where);
|
|
905
|
+
if (bucket !== undefined && bucket.length === 0) {
|
|
906
|
+
throw new RolesConfigError(`${where}.bucket`, "is empty; omit it to stay account-wide");
|
|
907
|
+
}
|
|
908
|
+
const groupsScope = raw["groups_scope"] === undefined ? undefined : groupScopeNamed(text2(raw, "groups_scope", where));
|
|
909
|
+
if (raw["groups_scope"] !== undefined && groupsScope === undefined) {
|
|
910
|
+
throw new RolesConfigError(`${where}.groups_scope`, `expected one of ${Object.keys(GROUP_SCOPES).join(", ")}`);
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
accountGroups: texts(raw, "account_groups", where) ?? [],
|
|
914
|
+
bucket,
|
|
915
|
+
bucketJurisdiction: raw["bucket_jurisdiction"] === undefined ? "default" : text2(raw, "bucket_jurisdiction", where),
|
|
916
|
+
desc: raw["desc"] === undefined ? "" : text2(raw, "desc", where),
|
|
917
|
+
groups,
|
|
918
|
+
groupsScope,
|
|
919
|
+
maxTtl: text2(raw, "max_ttl", where),
|
|
920
|
+
name: text2(raw, "name", where),
|
|
921
|
+
ttl: text2(raw, "ttl", where),
|
|
922
|
+
zoneTypes: texts(raw, "zone_types", where) ?? [],
|
|
923
|
+
zones: texts(raw, "zones", where)
|
|
924
|
+
};
|
|
925
|
+
};
|
|
926
|
+
var surface = (name, value) => {
|
|
927
|
+
const where = `surfaces.${name}`;
|
|
928
|
+
const raw = record(value, where);
|
|
929
|
+
return {
|
|
930
|
+
accountRoles: entries(raw, "account_roles", where).map((item, i) => template(item, `${where}.account_roles[${String(i)}]`)),
|
|
931
|
+
zoneRoles: entries(raw, "zone_roles", where).map((item, i) => template(item, `${where}.zone_roles[${String(i)}]`))
|
|
932
|
+
};
|
|
933
|
+
};
|
|
934
|
+
var account = (key, value, surfaces) => {
|
|
935
|
+
const where = `accounts.${key}`;
|
|
936
|
+
const raw = record(value, where);
|
|
937
|
+
const names = texts(raw, "surfaces", where) ?? [];
|
|
938
|
+
for (const name of names) {
|
|
939
|
+
if (surfaces[name] === undefined) {
|
|
940
|
+
throw new RolesConfigError(`${where}.surfaces`, `names ${name}, which surfaces: never defines`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const zones = raw["zones"] === undefined || raw["zones"] === "all" ? "all" : texts(raw, "zones", where);
|
|
944
|
+
return { id: text2(raw, "id", where), key, surfaces: names, zones: zones ?? "all" };
|
|
945
|
+
};
|
|
946
|
+
var parseRolesConfig = (yaml) => {
|
|
947
|
+
const root = record(Bun.YAML.parse(yaml), "(document)");
|
|
948
|
+
const surfaces = {};
|
|
949
|
+
for (const [name, spec] of Object.entries(record(root["surfaces"], "surfaces"))) {
|
|
950
|
+
surfaces[name] = surface(name, spec);
|
|
951
|
+
}
|
|
952
|
+
const accounts = Object.entries(record(root["accounts"], "accounts")).map(([key, value]) => account(key, value, surfaces));
|
|
953
|
+
return {
|
|
954
|
+
accounts,
|
|
955
|
+
namespace: text2(record(root["defaults"], "defaults"), "namespace", "defaults"),
|
|
956
|
+
surfaces
|
|
957
|
+
};
|
|
958
|
+
};
|
|
555
959
|
// src/openbao/mount.ts
|
|
556
960
|
import { Resource as Resource3 } from "alchemy";
|
|
557
961
|
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
@@ -646,7 +1050,7 @@ var canonical2 = (input) => ({
|
|
|
646
1050
|
signatureBits: input.signatureBits,
|
|
647
1051
|
ttlSeconds: input.ttlSeconds
|
|
648
1052
|
});
|
|
649
|
-
var seconds2 = (
|
|
1053
|
+
var seconds2 = (text3) => parseDuration(text3) ?? -1;
|
|
650
1054
|
var canonicalFromProps = (props) => canonical2({
|
|
651
1055
|
allowBareDomains: props.allowBareDomains ?? false,
|
|
652
1056
|
allowGlobDomains: props.allowGlobDomains ?? false,
|
|
@@ -747,7 +1151,7 @@ var readRole2 = (props) => Effect8.gen(function* () {
|
|
|
747
1151
|
return;
|
|
748
1152
|
return attributesOf4(props, live);
|
|
749
1153
|
});
|
|
750
|
-
var badDurations = (props) => [["ttl", props.ttl], ["maxTtl", props.maxTtl]].filter(([,
|
|
1154
|
+
var badDurations = (props) => [["ttl", props.ttl], ["maxTtl", props.maxTtl]].filter(([, text3]) => parseDuration(text3) === undefined);
|
|
751
1155
|
var BaoPkiRoleProvider = () => Provider4.effect(BaoPkiRole, Effect8.succeed(BaoPkiRole.Provider.of({
|
|
752
1156
|
list: () => Effect8.succeed([]),
|
|
753
1157
|
read: Effect8.fn(function* ({ olds }) {
|
|
@@ -810,8 +1214,8 @@ var policyPath = (name) => `sys/policies/acl/${name}`;
|
|
|
810
1214
|
var readPolicy = (name) => Effect9.flatMap(baoRead(policyPath(name)), (data) => {
|
|
811
1215
|
if (data === undefined)
|
|
812
1216
|
return Effect9.succeed("");
|
|
813
|
-
const
|
|
814
|
-
return typeof
|
|
1217
|
+
const text3 = data["policy"];
|
|
1218
|
+
return typeof text3 === "string" ? Effect9.succeed(text3) : Effect9.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
|
|
815
1219
|
});
|
|
816
1220
|
var writePolicy = (name, hcl) => baoWrite("PUT", policyPath(name), { policy: hcl });
|
|
817
1221
|
var deletePolicy = (name) => baoDelete(policyPath(name));
|
|
@@ -831,12 +1235,12 @@ var BaoPolicyProvider = () => Provider5.effect(BaoPolicy, Effect10.gen(function*
|
|
|
831
1235
|
const fs = yield* FileSystem.FileSystem;
|
|
832
1236
|
const path = yield* Path.Path;
|
|
833
1237
|
const assemble = (dir) => Effect10.gen(function* () {
|
|
834
|
-
const
|
|
1238
|
+
const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
|
|
835
1239
|
let joined = "";
|
|
836
|
-
for (const f of
|
|
1240
|
+
for (const f of entries2)
|
|
837
1241
|
joined += `${yield* fs.readFileString(path.join(dir, f))}
|
|
838
1242
|
`;
|
|
839
|
-
return { joined, parts:
|
|
1243
|
+
return { joined, parts: entries2.length };
|
|
840
1244
|
});
|
|
841
1245
|
return BaoPolicy.Provider.of({
|
|
842
1246
|
list: () => Effect10.succeed([]),
|
|
@@ -876,7 +1280,7 @@ import * as Provider6 from "alchemy/Provider";
|
|
|
876
1280
|
import * as Effect11 from "effect/Effect";
|
|
877
1281
|
|
|
878
1282
|
// src/openbao/proxmox-role-form.ts
|
|
879
|
-
var
|
|
1283
|
+
var text3 = (value) => typeof value === "string" ? value : "";
|
|
880
1284
|
var ttlText3 = (seconds3, fallback = "0") => {
|
|
881
1285
|
if (seconds3 === undefined)
|
|
882
1286
|
return fallback;
|
|
@@ -892,8 +1296,8 @@ var rolePath3 = (mount, name) => `${mountPath(mount)}/roles/${name}`;
|
|
|
892
1296
|
var attributesOf6 = (props, live) => {
|
|
893
1297
|
const attrs = {
|
|
894
1298
|
mount: mountPath(props.mount),
|
|
895
|
-
name:
|
|
896
|
-
mintUser:
|
|
1299
|
+
name: text3(live["name"]) || props.name,
|
|
1300
|
+
mintUser: text3(live["mint_user"]),
|
|
897
1301
|
ttl: ttlText3(ttlSeconds(live["ttl"])),
|
|
898
1302
|
maxTtl: ttlText3(ttlSeconds(live["max_ttl"]))
|
|
899
1303
|
};
|
|
@@ -978,10 +1382,10 @@ import * as Effect12 from "effect/Effect";
|
|
|
978
1382
|
// src/openbao/ssh-role-form.ts
|
|
979
1383
|
var mountOf = (mount) => (mount ?? "ssh").replace(/\/+$/, "");
|
|
980
1384
|
var readPath3 = (mount, name) => `${mountOf(mount)}/roles/${name}`;
|
|
981
|
-
var
|
|
1385
|
+
var text4 = (value) => typeof value === "string" ? value : "";
|
|
982
1386
|
var bool3 = (value, fallback) => typeof value === "boolean" ? value : fallback;
|
|
983
1387
|
var csvList = (value) => {
|
|
984
|
-
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") :
|
|
1388
|
+
const parts = Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : text4(value).split(",");
|
|
985
1389
|
return [
|
|
986
1390
|
...new Set(parts.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
|
|
987
1391
|
].sort();
|
|
@@ -992,7 +1396,7 @@ var extMap = (value) => {
|
|
|
992
1396
|
return out;
|
|
993
1397
|
const source = value;
|
|
994
1398
|
for (const key of Object.keys(source).sort())
|
|
995
|
-
out[key] =
|
|
1399
|
+
out[key] = text4(source[key]);
|
|
996
1400
|
return out;
|
|
997
1401
|
};
|
|
998
1402
|
var resolve = (props) => ({
|
|
@@ -1018,7 +1422,7 @@ var attributesOf7 = (form, live) => {
|
|
|
1018
1422
|
name: form.name,
|
|
1019
1423
|
mount: form.mount,
|
|
1020
1424
|
allowedUsers: csvList(live["allowed_users"]),
|
|
1021
|
-
defaultUser:
|
|
1425
|
+
defaultUser: text4(live["default_user"]),
|
|
1022
1426
|
allowUserCertificates: bool3(live["allow_user_certificates"], false),
|
|
1023
1427
|
allowHostCertificates: bool3(live["allow_host_certificates"], false),
|
|
1024
1428
|
allowedDomains: csvList(live["allowed_domains"]),
|
|
@@ -1029,7 +1433,7 @@ var attributesOf7 = (form, live) => {
|
|
|
1029
1433
|
allowedCriticalOptions: csvList(live["allowed_critical_options"]),
|
|
1030
1434
|
defaultCriticalOptions: extMap(live["default_critical_options"]),
|
|
1031
1435
|
allowEmptyPrincipals: bool3(live["allow_empty_principals"], false),
|
|
1032
|
-
keyType:
|
|
1436
|
+
keyType: text4(live["key_type"]) || "ca",
|
|
1033
1437
|
ttlSeconds: ttlSeconds(live["ttl"]) ?? 0,
|
|
1034
1438
|
maxTtlSeconds: ttlSeconds(live["max_ttl"]) ?? 0
|
|
1035
1439
|
};
|
|
@@ -1143,16 +1547,26 @@ var BaoSshRoleProvider = () => Provider7.effect(BaoSshRole, Effect12.succeed(Bao
|
|
|
1143
1547
|
})
|
|
1144
1548
|
})));
|
|
1145
1549
|
export {
|
|
1550
|
+
BaoAuthRole,
|
|
1146
1551
|
BaoAuthRoleProvider,
|
|
1552
|
+
BaoCloudflareRole,
|
|
1147
1553
|
BaoCloudflareRoleProvider,
|
|
1554
|
+
BaoMount,
|
|
1148
1555
|
BaoMountProvider,
|
|
1149
1556
|
BaoPkiRoleProvider,
|
|
1150
1557
|
BaoPolicy,
|
|
1151
1558
|
BaoPolicyProvider,
|
|
1152
1559
|
BaoProxmoxRole,
|
|
1153
1560
|
BaoProxmoxRoleProvider,
|
|
1154
|
-
BaoSshRoleProvider
|
|
1561
|
+
BaoSshRoleProvider,
|
|
1562
|
+
CloudflarePermissionGroups,
|
|
1563
|
+
expandAccount,
|
|
1564
|
+
expandAll,
|
|
1565
|
+
mountName,
|
|
1566
|
+
parseRolesConfig,
|
|
1567
|
+
permissionGroupsFromEngine,
|
|
1568
|
+
permissionGroupsFromLiveRoles
|
|
1155
1569
|
};
|
|
1156
1570
|
|
|
1157
|
-
//# debugId=
|
|
1571
|
+
//# debugId=4D8C0F4073136EF664756E2164756E21
|
|
1158
1572
|
//# sourceMappingURL=index.js.map
|