@homeflare/alchemy 0.2.1 → 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.
@@ -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;AAExC,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;AAoDH,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,wFAkCtC,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"}
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"}
@@ -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 { BaoMountProvider } from './mount.ts';
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;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,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"}
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"}
@@ -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 = (text2) => parseDuration(text2) ?? -1;
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(([, text2]) => parseDuration(text2) === undefined);
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 text2 = data["policy"];
814
- return typeof text2 === "string" ? Effect9.succeed(text2) : Effect9.fail(new BaoError(200, "GET", policyPath(name), ["no data.policy in the response"]));
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 entries = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
1238
+ const entries2 = (yield* fs.readDirectory(dir)).filter((f) => f.endsWith(".hcl")).sort();
835
1239
  let joined = "";
836
- for (const f of entries)
1240
+ for (const f of entries2)
837
1241
  joined += `${yield* fs.readFileString(path.join(dir, f))}
838
1242
  `;
839
- return { joined, parts: entries.length };
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 text2 = (value) => typeof value === "string" ? value : "";
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: text2(live["name"]) || props.name,
896
- mintUser: text2(live["mint_user"]),
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 text3 = (value) => typeof value === "string" ? value : "";
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") : text3(value).split(",");
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] = text3(source[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: text3(live["default_user"]),
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: text3(live["key_type"]) || "ca",
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=72D46173613CE4CB64756E2164756E21
1571
+ //# debugId=4D8C0F4073136EF664756E2164756E21
1158
1572
  //# sourceMappingURL=index.js.map