1import * as chromemanagement from "@distilled.cloud/gcp/chromemanagement_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 detailsNeedSync,
12 encodeOwnershipLine,
13 findConnectorConfigByName,
14 findOwnedConnectorConfig,
15 getConnectorConfig,
16 hasOwnershipMarker,
17 lastSegment,
18 listOwnedConnectorConfigs,
19 MAX_DISPLAY_NAME_LENGTH,
20 ownedByAlchemy,
21 ownershipLabels,
22 parentOf,
23 parseOwnership,
24 replaceOnIdentity,
25 sameText,
26 toConnectorConfigId,
27 toConnectorConfigName,
28 toCustomerName,
29 toDisplayName,
30 updateMaskOf,
31} from "./internal.ts";
32
33export type ConnectorConfigDetails =
34 chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigDetails;
35export type ConnectorConfigType =
36 | chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigTypeEnum
37 | (string & {});
38export type ConnectorConfigStatus =
39 chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfigStatus;
40
41export type CustomersConnectorConfigProps = {
42 /**
43 * Parent customer (`customers/{customer}` or `{customer}`). Immutable
44 * — changing it replaces the connector config.
45 */
46 parent: string;
47 /**
48 * Connector config id (last path segment). If omitted, a unique id is
49 * generated from the stack, stage, and logical id. Immutable —
50 * changing it replaces the connector config.
51 */
52 connectorConfigId?: string;
53 /**
54 * Admin-facing display name. Connector configs have no labels field,
55 * so Alchemy stores ownership in a `[alchemy …]` prefix and strips it
56 * from attributes.
57 */
58 displayName?: string;
59 /**
60 * Connector type (`REPORTING`, `DEVICE_TRUST`, `XDR`,
61 * `IDENTITY_BASED_ENROLLMENT`, `CERTIFICATE_AUTHORITY`, `ROOT_STORE`,
62 * `CONTENT_ANALYSIS`). Immutable — changing it replaces the connector
63 * config.
64 */
65 type: ConnectorConfigType;
66 /**
67 * Type-specific connector details (Pub/Sub, Splunk, CrowdStrike,
68 * device trust, MIP, …). `apiKey` and `hecToken` fields are input-only
69 * and are not returned by get.
70 */
71 details: ConnectorConfigDetails;
72};
73
74export type CustomersConnectorConfig = Resource<
75 "GCP.ChromeManagement.CustomersConnectorConfig",
76 CustomersConnectorConfigProps,
77 {
78 /** Resource name `customers/{customer}/connectorConfigs/{connector_config}`. */
79 name: string;
80 /** Connector config id (last path segment). */
81 connectorConfigId: string;
82 /** Parent customer name `customers/{customer}`. */
83 parent: string;
84 /** Project id used when the connector config was reconciled. */
85 project: string;
86 /** Display name with the Alchemy ownership prefix stripped. */
87 displayName: string | undefined;
88 /** Connector type. */
89 type: string | undefined;
90 /** Type-specific connector details. Input-only secrets are omitted. */
91 details: ConnectorConfigDetails | undefined;
92 /** Server-reported connector status. */
93 status: ConnectorConfigStatus | undefined;
94 },
95 never,
96 Providers
97>;
98
99/**
100 * A Chrome Management connector config
101 * (`customers/{customer}/connectorConfigs/{connector_config}`).
102 *
103 * Connector configs have no labels field, so Alchemy stamps ownership
104 * into `displayName` for `list` / nuke. Parent, connector config id, and
105 * type are identity — changing any of them replaces the config. Display
106 * name and details update in place. Create requires a Chrome Enterprise
107 * customer the caller can administer.
108 *
109 * ### Creating a Connector Config
110 * **Example:** Pub/Sub reporting connector
111 * ```typescript
112 * const topic = yield* GCP.PubSub.Topic("chrome-events", {});
113 * const config = yield* GCP.ChromeManagement.CustomersConnectorConfig(
114 * "Reporting",
115 * {
116 * parent: "customers/my_customer",
117 * type: "REPORTING",
118 * displayName: "Alchemy reporting",
119 * details: {
120 * pubSubConfig: {
121 * topicFullPath: topic.name,
122 * reportingSettings: {
123 * enabledDefaultEvents: ["ALL_DEFAULT_EVENTS"],
124 * },
125 * },
126 * },
127 * },
128 * );
129 * ```
130 *
131 * **Example:** Device trust connector
132 * ```typescript
133 * const config = yield* GCP.ChromeManagement.CustomersConnectorConfig(
134 * "Trust",
135 * {
136 * parent: "customers/C01234567",
137 * type: "DEVICE_TRUST",
138 * displayName: "Okta device trust",
139 * details: {
140 * deviceTrustConfig: {
141 * serviceProvider: "OKTA",
142 * urlMatchers: ["https://login.example.com"],
143 * serviceAccounts: ["trust@example.com"],
144 * scope: "BROWSERS_ONLY",
145 * },
146 * },
147 * },
148 * );
149 * ```
150 *
151 * ### Updating a Connector Config
152 * **Example:** Rename and change reporting events
153 * ```typescript
154 * const config = yield* GCP.ChromeManagement.CustomersConnectorConfig(
155 * "Reporting",
156 * {
157 * parent: "customers/my_customer",
158 * type: "REPORTING",
159 * displayName: "Alchemy reporting (prod)",
160 * details: {
161 * pubSubConfig: {
162 * topicFullPath: topic.name,
163 * reportingSettings: {
164 * enabledDefaultEvents: ["BROWSER_CRASH_EVENT"],
165 * enabledOptInEvents: ["ALL_OPT_IN_EVENTS"],
166 * },
167 * },
168 * },
169 * },
170 * );
171 * ```
172 *
173 * @resource
174 * @category ChromeManagement
175 */
176export const CustomersConnectorConfig = Resource<CustomersConnectorConfig>(
177 "GCP.ChromeManagement.CustomersConnectorConfig",
178);
179
180export class CustomersConnectorConfigNotResolved extends Data.TaggedError(
181 "GCP.ChromeManagement.CustomersConnectorConfigNotResolved",
182)<{
183 parent: string;
184 name: string;
185}> {}
186
187const lookupName = (
188 parent: string,
189 connectorConfigId: string | undefined,
190 existingName: string | undefined,
191) => {
192 if (existingName && existingName.length > 0) return existingName;
193 return toConnectorConfigName(parent, connectorConfigId);
194};
195
196const toAttrs = (
197 row: chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfig,
198 project: string,
199) => {
200 const name = row.name ?? "";
201 return {
202 name,
203 connectorConfigId: lastSegment(name),
204 parent: parentOf(name) || toCustomerName(name),
205 project,
206 displayName: parseOwnership(row.displayName).text,
207 type: row.type,
208 details: row.details,
209 status: row.status,
210 };
211};
212
213const desiredBody = (input: {
214 news: CustomersConnectorConfigProps;
215 displayName: string;
216}): chromemanagement.GoogleChromeManagementVersionsV1ConnectorConfig => ({
217 displayName: input.displayName,
218 type: input.news.type,
219 details: input.news.details,
220});
221
222export const CustomersConnectorConfigProvider = () =>
223 Provider.succeed(CustomersConnectorConfig, {
224 stables: ["name", "connectorConfigId", "parent", "project"],
225
226 diff: Effect.fn(function* ({ news, olds, output }) {
227 if (!isResolved(news)) return undefined;
228 return replaceOnIdentity({
229 previousParent: olds?.parent ?? output?.parent,
230 nextParent: news.parent,
231 previousId: olds?.connectorConfigId ?? output?.connectorConfigId,
232 nextId: news.connectorConfigId,
233 previousType: olds?.type ?? output?.type,
234 nextType: news.type,
235 });
236 }),
237
238 read: Effect.fn(function* ({ id, olds, output }) {
239 const env = yield* GcpEnvironment.current;
240 const parent = toCustomerName(olds?.parent ?? output?.parent ?? "");
241 const connectorConfigId = yield* toConnectorConfigId(
242 id,
243 olds?.connectorConfigId ?? output?.connectorConfigId,
244 output?.connectorConfigId,
245 );
246 const name = lookupName(parent, connectorConfigId, output?.name);
247 let existing = yield* getConnectorConfig(name);
248 if (existing === undefined) {
249 existing = yield* findOwnedConnectorConfig(id, parent);
250 }
251 if (existing === undefined) return undefined;
252 const attrs = toAttrs(existing, env.project);
253 return (yield* ownedByAlchemy(id, existing.displayName))
254 ? attrs
255 : Unowned(attrs);
256 }),
257
258 list: () =>
259 Effect.gen(function* () {
260 const env = yield* GcpEnvironment.current;
261 const rows = yield* listOwnedConnectorConfigs();
262 return rows
263 .filter((row) => hasOwnershipMarker(row.displayName))
264 .map((row) => toAttrs(row, env.project));
265 }),
266
267 reconcile: Effect.fn(function* ({ id, news, output, olds }) {
268 const env = yield* GcpEnvironment.current;
269 const parent = toCustomerName(news.parent);
270 const ownership = yield* ownershipLabels(id);
271 const rawName = yield* toDisplayName(
272 id,
273 news.displayName,
274 output?.displayName,
275 );
276 const displayName = encodeOwnershipLine(
277 ownership,
278 rawName,
279 MAX_DISPLAY_NAME_LENGTH,
280 );
281 const connectorConfigId = yield* toConnectorConfigId(
282 id,
283 news.connectorConfigId,
284 output?.connectorConfigId,
285 );
286 const name = lookupName(parent, connectorConfigId, output?.name);
287 const desired = desiredBody({ news, displayName });
288
289 let current = yield* getConnectorConfig(name);
290 if (current === undefined) {
291 current = yield* findOwnedConnectorConfig(id, parent);
292 }
293 if (current === undefined) {
294 current = yield* findConnectorConfigByName(displayName, parent);
295 }
296
297 if (current === undefined) {
298 const created = yield* chromemanagement
299 .createCustomersConnectorConfigs({
300 parent,
301 connectorConfigId,
302 body: desired,
303 })
304 .pipe(
305 Effect.catchTag("Conflict", () =>
306 getConnectorConfig(name).pipe(
307 Effect.flatMap((row) =>
308 row !== undefined
309 ? Effect.succeed(row)
310 : findConnectorConfigByName(displayName, parent),
311 ),
312 ),
313 ),
314 );
315 current = created ?? undefined;
316 }
317
318 if (current === undefined) {
319 return yield* new CustomersConnectorConfigNotResolved({
320 parent,
321 name: name || displayName,
322 });
323 }
324
325 const currentName = current.name ?? name;
326 const updateMask = updateMaskOf(
327 !sameText(current.displayName, desired.displayName)
328 ? "displayName"
329 : undefined,
330 detailsNeedSync(current.details, desired.details, olds?.details)
331 ? "details"
332 : undefined,
333 );
334
335 if (updateMask.length > 0 && currentName.length > 0) {
336 current = yield* chromemanagement.patchCustomersConnectorConfigs({
337 name: currentName,
338 updateMask,
339 body: desired,
340 });
341 }
342
343 const fresh =
344 (yield* getConnectorConfig(current.name ?? currentName)) ?? current;
345 return toAttrs(fresh, env.project);
346 }),
347
348 delete: Effect.fn(function* ({ output }) {
349 if (!output.name) return;
350 yield* chromemanagement
351 .deleteCustomersConnectorConfigs({
352 name: output.name,
353 })
354 .pipe(
355 Effect.catchTag("NotFound", () => Effect.void),
356 Effect.catchTag("BadRequest", () => Effect.void),
357 );
358 }),
359 });