1import * as recaptchaenterprise from "@distilled.cloud/gcp/recaptchaenterprise_v1";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import { tagRecord } from "../../Tags.ts";
6import { hasAlchemyLabels } from "../Labels.ts";
7
8export const MAX_DESCRIPTION_LENGTH = 256;
9export const MAX_DISPLAY_NAME_LENGTH = 63;
10export const MAX_PATH_LENGTH = 200;
11
12export const lastSegment = (value: string) => {
13 const trimmed = value.replace(/\/+$/, "");
14 const parts = trimmed.split("/");
15 return parts[parts.length - 1] || trimmed;
16};
17
18export const projectParent = (project: string) => `projects/${project}`;
19
20export const keyNameOf = (project: string, keyId: string) =>
21 `projects/${project}/keys/${keyId}`;
22
23export const firewallNameOf = (project: string, firewallpolicyId: string) =>
24 `projects/${project}/firewallpolicies/${firewallpolicyId}`;
25
26export const sameText = (left: string | undefined, right: string | undefined) =>
27 (left ?? "") === (right ?? "");
28
29export const sameJson = (left: unknown, right: unknown) =>
30 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
31
32export const sortedStrings = (values: readonly string[] | undefined) =>
33 [...(values ?? [])].slice().sort();
34
35export const stringList = (
36 values: readonly (string | undefined)[] | null | undefined,
37): string[] =>
38 (values ?? []).filter((value): value is string => typeof value === "string");
39
40export const updateMaskOf = (...fields: Array<string | undefined>) =>
41 fields.filter((field): field is string => field !== undefined).join(",");
42
43export const toDisplayName = (
44 id: string,
45 requested: string | undefined,
46 existing: string | undefined,
47 maxLength = MAX_DISPLAY_NAME_LENGTH,
48) =>
49 Effect.gen(function* () {
50 if (requested !== undefined && requested.length > 0) return requested;
51 if (existing !== undefined && existing.length > 0) return existing;
52 return yield* createPhysicalName({
53 id,
54 maxLength,
55 lowercase: true,
56 });
57 });
58
59export const toGeneratedPath = (id: string, requested: string | undefined) =>
60 Effect.gen(function* () {
61 if (requested !== undefined) return requested;
62 const generated = yield* createPhysicalName({
63 id,
64 maxLength: 40,
65 lowercase: true,
66 });
67 const path = `/alc/${generated}`.slice(0, MAX_PATH_LENGTH);
68 return path.length > 0 ? path : "/alc";
69 });
70
71export const getKey = (name: string) =>
72 name.length === 0
73 ? Effect.succeed(undefined)
74 : recaptchaenterprise
75 .getProjectsKeys({ name })
76 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
77
78export const getFirewallPolicy = (name: string) =>
79 name.length === 0
80 ? Effect.succeed(undefined)
81 : recaptchaenterprise
82 .getProjectsFirewallpolicies({ name })
83 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
84
85export const listKeys = (project: string) =>
86 recaptchaenterprise.listProjectsKeys
87 .pages({
88 parent: projectParent(project),
89 pageSize: 1000,
90 })
91 .pipe(
92 Stream.flatMap((page) => Stream.fromIterable(page.keys ?? [])),
93 Stream.runCollect,
94 Effect.map((chunk) => Array.from(chunk)),
95 );
96
97export const listFirewallPolicies = (project: string) =>
98 recaptchaenterprise.listProjectsFirewallpolicies
99 .pages({
100 parent: projectParent(project),
101 pageSize: 1000,
102 })
103 .pipe(
104 Stream.flatMap((page) =>
105 Stream.fromIterable(page.firewallPolicies ?? []),
106 ),
107 Stream.runCollect,
108 Effect.map((chunk) => Array.from(chunk)),
109 );
110
111export const findOwnedKey = (project: string, id: string, name?: string) =>
112 Effect.gen(function* () {
113 if (name !== undefined && name.length > 0) {
114 const existing = yield* getKey(name);
115 if (existing !== undefined) return existing;
116 }
117 const keys = yield* listKeys(project);
118 for (const key of keys) {
119 if (yield* hasAlchemyLabels(id, tagRecord(key.labels))) {
120 return key;
121 }
122 }
123 return undefined;
124 });