1import * as storage from "@distilled.cloud/gcp/storage_v1";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import { GcpEnvironment } from "../Environment.ts";
6import { ALCHEMY_LABEL_PREFIX } from "../Labels.ts";
7
8export const normalizeRole = (role: string) => role.trim().toUpperCase();
9
10export const normalizeEntity = (entity: string) => entity.trim();
11
12/** Default project-team ACL entries GCS stamps on fine-grained buckets. */
13export const isProjectAclEntity = (entity: string | undefined) =>
14 (entity ?? "").toLowerCase().startsWith("project-");
15
16/**
17 * ACL entities Alchemy may have created. Skips project-team defaults and
18 * numeric `user-{id}` object-owner entries so `list` / nuke do not try to
19 * delete GCS-managed ACLs.
20 */
21export const isUserManagedAclEntity = (entity: string | undefined) => {
22 const value = (entity ?? "").toLowerCase();
23 if (value.length === 0 || isProjectAclEntity(value)) return false;
24 if (value === "allusers" || value === "allauthenticatedusers") return true;
25 if (value.startsWith("domain-") || value.startsWith("group-")) return true;
26 return value.startsWith("user-") && value.includes("@");
27};
28
29export const withTrailingSlash = (name: string) =>
30 name.endsWith("/") ? name : `${name}/`;
31
32export const toFolderName = (
33 id: string,
34 name: string | undefined,
35 existing?: string,
36) =>
37 Effect.gen(function* () {
38 const raw =
39 name ??
40 existing ??
41 (yield* createPhysicalName({
42 id,
43 maxLength: 200,
44 lowercase: true,
45 }));
46 return withTrailingSlash(raw.replace(/^\/+/, ""));
47 });
48
49export const listAlchemyBuckets = () =>
50 Effect.gen(function* () {
51 const env = yield* GcpEnvironment.current;
52 return yield* storage.listBuckets
53 .items({
54 project: env.project,
55 projection: "full",
56 maxResults: 1000,
57 })
58 .pipe(
59 Stream.filter((bucket) =>
60 Object.keys(bucket.labels ?? {}).some((key) =>
61 key.startsWith(ALCHEMY_LABEL_PREFIX),
62 ),
63 ),
64 Stream.runCollect,
65 Effect.map((chunk) => Array.from(chunk)),
66 );
67 });