1import * as chromemanagement from "@distilled.cloud/gcp/chromemanagement_v1";
2import * as Config from "effect/Config";
3import * as Effect from "effect/Effect";
4import * as Option from "effect/Option";
5import * as Stream from "effect/Stream";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import {
8 alchemyLabelKeys,
9 createInternalLabels,
10 hasAlchemyLabels,
11} from "../Labels.ts";
12
13/** Optional deploy-time setting from the ConfigProvider (env by default). */
14const optionalSetting = (key: string) =>
15 Effect.gen(function* () {
16 const value = yield* Config.option(Config.String(key));
17 return Option.getOrUndefined(value)?.trim() || undefined;
18 });
19
20export const CUSTOMER_PREFIX = "customers/";
21export const MY_CUSTOMER = "customers/my_customer";
22export const MAX_DISPLAY_NAME_LENGTH = 256;
23export const MAX_CONNECTOR_CONFIG_ID_LENGTH = 63;
24export const PROBE_CUSTOMER = "customers/0";
25export const PROBE_CONNECTOR_CONFIG = `${PROBE_CUSTOMER}/connectorConfigs/alchemy-missing`;
26
27const INPUT_ONLY_KEYS = new Set(["apiKey", "hecToken"]);
28
29export const lastSegment = (value: string) => {
30 const trimmed = value.replace(/\/+$/, "");
31 const parts = trimmed.split("/");
32 return parts[parts.length - 1] || trimmed;
33};
34
35export const parentOf = (name: string) => {
36 const parts = name.split("/").filter((part) => part.length > 0);
37 return parts.slice(0, -2).join("/");
38};
39
40export const normalizeName = (value: string) =>
41 value.replace(/\/+$/, "").trim();
42
43export const toCustomerName = (value: string) => {
44 const trimmed = normalizeName(value);
45 if (trimmed.length === 0) return "";
46 const customersIdx = trimmed.lastIndexOf("/customers/");
47 if (customersIdx >= 0) {
48 const rest = trimmed.slice(customersIdx + "/customers/".length);
49 const id = rest.split("/")[0] ?? "";
50 return id.length > 0 ? `${CUSTOMER_PREFIX}${id}` : "";
51 }
52 if (trimmed.startsWith(CUSTOMER_PREFIX)) {
53 const id = trimmed.slice(CUSTOMER_PREFIX.length).split("/")[0] ?? "";
54 return id.length > 0 ? `${CUSTOMER_PREFIX}${id}` : trimmed;
55 }
56 return `${CUSTOMER_PREFIX}${lastSegment(trimmed)}`;
57};
58
59export const toConnectorConfigName = (
60 parent: string,
61 connectorConfigId?: string,
62) => {
63 if (
64 connectorConfigId !== undefined &&
65 connectorConfigId.includes("/connectorConfigs/")
66 ) {
67 return normalizeName(connectorConfigId);
68 }
69 const customer = toCustomerName(parent);
70 if (
71 connectorConfigId !== undefined &&
72 connectorConfigId.length > 0 &&
73 customer.length > 0
74 ) {
75 return `${customer}/connectorConfigs/${lastSegment(connectorConfigId)}`;
76 }
77 return "";
78};
79
80export const sameText = (left: string | undefined, right: string | undefined) =>
81 (left ?? "") === (right ?? "");
82
83export const jsonEqual = (left: unknown, right: unknown) =>
84 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
85
86export const updateMaskOf = (...fields: Array<string | undefined>) =>
87 fields.filter((field): field is string => field !== undefined).join(",");
88
89export const replaceOnIdentity = (input: {
90 previousParent?: string;
91 nextParent: string;
92 previousId?: string;
93 nextId?: string;
94 previousType?: string;
95 nextType?: string;
96}) => {
97 if (
98 input.previousParent !== undefined &&
99 input.previousParent.length > 0 &&
100 toCustomerName(input.previousParent) !== toCustomerName(input.nextParent)
101 ) {
102 return { action: "replace" as const, deleteFirst: false };
103 }
104 if (
105 input.previousId !== undefined &&
106 input.nextId !== undefined &&
107 lastSegment(input.previousId) !== lastSegment(input.nextId)
108 ) {
109 return { action: "replace" as const, deleteFirst: false };
110 }
111 if (
112 input.previousType !== undefined &&
113 input.nextType !== undefined &&
114 input.previousType !== input.nextType
115 ) {
116 return { action: "replace" as const, deleteFirst: false };
117 }
118 return undefined;
119};
120
121const stripInputOnly = (value: unknown): unknown => {
122 if (Array.isArray(value)) return value.map(stripInputOnly);
123 if (value === null || typeof value !== "object") return value;
124 const out: Record<string, unknown> = {};
125 for (const [key, child] of Object.entries(value)) {
126 if (INPUT_ONLY_KEYS.has(key)) continue;
127 out[key] = stripInputOnly(child);
128 }
129 return out;
130};
131
132export const visibleDetails = (
133 details:
134 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
135 | undefined,
136) =>
137 stripInputOnly(details) as
138 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
139 | undefined;
140
141const secretSlice = (
142 details:
143 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
144 | undefined,
145) => {
146 if (!details) return undefined;
147 return {
148 splunkHecToken: details.splunkConfig?.hecToken,
149 paloAltoApiKey: details.paloAltoNetworksConfig?.apiKey,
150 crowdStrikeApiKey: details.crowdStrikeConfig?.apiKey,
151 crowdStrikeFalconApiKey: details.crowdStrikeFalconNextGenConfig?.apiKey,
152 crowdStrikeXdrApiKey: details.crowdStrikeXdrConfig?.apiKey,
153 googleSecOpsApiKey: details.googleSecOpsConfig?.apiKey,
154 };
155};
156
157export const detailsNeedSync = (
158 observed:
159 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
160 | undefined,
161 desired:
162 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
163 | undefined,
164 previous:
165 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails
166 | undefined,
167) =>
168 !jsonEqual(visibleDetails(observed), visibleDetails(desired)) ||
169 (previous !== undefined &&
170 !jsonEqual(secretSlice(desired), secretSlice(previous)));
171
172const markerOf = (stack: string, stage: string, id: string) =>
173 `[alchemy ${alchemyLabelKeys.stack}=${stack} ${alchemyLabelKeys.stage}=${stage} ${alchemyLabelKeys.id}=${id}]`;
174
175const fitMarker = (labels: Record<string, string>, maxLength: number) => {
176 let stack = labels[alchemyLabelKeys.stack] ?? "x";
177 let stage = labels[alchemyLabelKeys.stage] ?? "x";
178 let id = labels[alchemyLabelKeys.id] ?? "x";
179 let marker = markerOf(stack, stage, id);
180 while (
181 marker.length > maxLength &&
182 (stack.length > 1 || stage.length > 1 || id.length > 1)
183 ) {
184 if (stack.length >= stage.length && stack.length >= id.length) {
185 stack = stack.slice(0, -1);
186 } else if (stage.length >= id.length) {
187 stage = stage.slice(0, -1);
188 } else {
189 id = id.slice(0, -1);
190 }
191 marker = markerOf(stack, stage, id);
192 }
193 return marker.slice(0, maxLength);
194};
195
196export const encodeOwnershipLine = (
197 labels: Record<string, string>,
198 text: string | undefined,
199 maxLength = MAX_DISPLAY_NAME_LENGTH,
200): string => {
201 const trimmed = text?.replace(/[\r\n]+/g, " ").trim();
202 if (!trimmed) return fitMarker(labels, maxLength);
203 const minMarker = 24;
204 const reserved = Math.min(
205 trimmed.length + 1,
206 Math.max(0, maxLength - minMarker),
207 );
208 const marker = fitMarker(labels, maxLength - reserved);
209 return `${marker} ${trimmed}`.slice(0, maxLength);
210};
211
212export const parseOwnership = (
213 text: string | undefined,
214): {
215 labels: Record<string, string>;
216 text: string | undefined;
217} => {
218 if (!text?.startsWith("[alchemy ")) {
219 return { labels: {}, text };
220 }
221 const end = text.indexOf("]");
222 if (end < 0) return { labels: {}, text };
223 const labels: Record<string, string> = {};
224 for (const part of text.slice("[alchemy ".length, end).split(/\s+/)) {
225 const eq = part.indexOf("=");
226 if (eq > 0) {
227 labels[part.slice(0, eq)] = part.slice(eq + 1);
228 }
229 }
230 const rest = text.slice(end + 1).replace(/^[\s\n]+/, "");
231 return { labels, text: rest.length > 0 ? rest : undefined };
232};
233
234export const hasOwnershipMarker = (text: string | undefined) =>
235 Object.keys(parseOwnership(text).labels).some((key) =>
236 key.startsWith("alchemy-"),
237 );
238
239const prefixMatch = (expected: string, observed: string) =>
240 expected === observed ||
241 expected.startsWith(observed) ||
242 observed.startsWith(expected);
243
244export const ownedByAlchemy = (id: string, text: string | undefined) =>
245 Effect.gen(function* () {
246 const expected = yield* createInternalLabels(id);
247 const { labels } = parseOwnership(text);
248 if (!hasOwnershipMarker(text)) return false;
249 const exact = yield* hasAlchemyLabels(id, labels);
250 if (exact) return true;
251 return (
252 prefixMatch(
253 expected[alchemyLabelKeys.stack] ?? "",
254 labels[alchemyLabelKeys.stack] ?? "",
255 ) &&
256 prefixMatch(
257 expected[alchemyLabelKeys.stage] ?? "",
258 labels[alchemyLabelKeys.stage] ?? "",
259 ) &&
260 prefixMatch(
261 expected[alchemyLabelKeys.id] ?? "",
262 labels[alchemyLabelKeys.id] ?? "",
263 )
264 );
265 });
266
267export const ownershipLabels = (id: string) => createInternalLabels(id);
268
269export const toDisplayName = (
270 id: string,
271 requested: string | undefined,
272 existing: string | undefined,
273 maxLength = MAX_DISPLAY_NAME_LENGTH,
274) =>
275 Effect.gen(function* () {
276 if (requested !== undefined && requested.length > 0) {
277 return requested.slice(0, maxLength);
278 }
279 if (existing !== undefined && existing.length > 0) {
280 return existing.slice(0, maxLength);
281 }
282 return yield* createPhysicalName({
283 id,
284 maxLength: Math.min(40, maxLength),
285 lowercase: true,
286 });
287 });
288
289export const toConnectorConfigId = (
290 id: string,
291 requested: string | undefined,
292 existing?: string,
293) =>
294 Effect.gen(function* () {
295 if (requested !== undefined && requested.length > 0) {
296 return lastSegment(requested);
297 }
298 if (existing !== undefined && existing.length > 0) {
299 return lastSegment(existing);
300 }
301 return yield* createPhysicalName({
302 id,
303 maxLength: MAX_CONNECTOR_CONFIG_ID_LENGTH,
304 lowercase: true,
305 });
306 });
307
308const emptyList = <A>() => Effect.succeed([] as A[]);
309
310export const collectPages = <Page, A, E, R>(
311 pages: Stream.Stream<Page, E, R>,
312 items: (page: Page) => readonly A[] | undefined,
313) =>
314 pages.pipe(
315 Stream.flatMap((page) => Stream.fromIterable(items(page) ?? [])),
316 Stream.runCollect,
317 Effect.map((chunk) => Array.from(chunk)),
318 );
319
320export const getConnectorConfig = (name: string) =>
321 name.length === 0
322 ? Effect.succeed(undefined)
323 : chromemanagement
324 .getCustomersConnectorConfigs({ name })
325 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
326
327export const listConnectorConfigsAt = (parent: string) =>
328 parent.length === 0
329 ? emptyList<chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfig>()
330 : collectPages(
331 chromemanagement.listCustomersConnectorConfigs.pages({
332 parent: toCustomerName(parent),
333 pageSize: 100,
334 }),
335 (page) => page.connectorConfigs,
336 ).pipe(
337 Effect.catchTag("NotFound", () =>
338 emptyList<chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfig>(),
339 ),
340 );
341
342/** `my_customer` plus any customers listed in the Chrome Management settings. */
343export const configuredCustomers = () =>
344 Effect.gen(function* () {
345 const names = new Set<string>();
346 names.add(MY_CUSTOMER);
347 for (const key of [
348 "GCP_CHROMEMANAGEMENT_CUSTOMER",
349 "GCP_CHROMEMANAGEMENT_CUSTOMER_ID",
350 "GCP_CHROMEMANAGEMENT_CUSTOMERS",
351 ]) {
352 const raw = yield* optionalSetting(key);
353 for (const part of (raw ?? "").split(/[,\s]+/)) {
354 const name = toCustomerName(part);
355 if (name.length > 0) names.add(name);
356 }
357 }
358 return [...names];
359 });
360
361export const findOwnedConnectorConfig = (id: string, parent?: string) =>
362 Effect.gen(function* () {
363 const parents =
364 parent !== undefined && parent.length > 0
365 ? [toCustomerName(parent)]
366 : yield* configuredCustomers();
367 for (const next of parents) {
368 const rows = yield* listConnectorConfigsAt(next);
369 for (const row of rows) {
370 if (yield* ownedByAlchemy(id, row.displayName)) {
371 return row;
372 }
373 }
374 }
375 return undefined;
376 });
377
378export const findConnectorConfigByName = (
379 displayName: string,
380 parent: string,
381) =>
382 Effect.gen(function* () {
383 const rows = yield* listConnectorConfigsAt(parent);
384 return rows.find((row) => row.displayName === displayName);
385 });
386
387export const listOwnedConnectorConfigs = () =>
388 Effect.gen(function* () {
389 const parents = yield* configuredCustomers();
390 const pages = yield* Effect.forEach(
391 parents,
392 (parent) => listConnectorConfigsAt(parent),
393 { concurrency: 4 },
394 );
395 return pages.flat().filter((row) => hasOwnershipMarker(row.displayName));
396 });