1import * as androidmanagement from "@distilled.cloud/gcp/androidmanagement_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 findEnterpriseByDisplayName,
12 getEnterprise,
13 jsonEqual,
14 lastSegment,
15 MAX_DISPLAY_NAME_LENGTH,
16 sameStringList,
17 sameText,
18 toDisplayName,
19 toEnterpriseName,
20 updateMaskOf,
21} from "./internal.ts";
22
23export type EnterpriseProps = {
24 /**
25 * Cloud project that owns the enterprise. Defaults to the stack
26 * project. Immutable — changing it replaces the enterprise.
27 */
28 projectId?: string;
29 /**
30 * Signup URL name from `signupUrls.create`. Set with
31 * `enterpriseToken` for a customer-managed enterprise.
32 */
33 signupUrlName?: string;
34 /**
35 * Enterprise token appended to the signup callback URL. Set with
36 * `signupUrlName` for a customer-managed enterprise.
37 */
38 enterpriseToken?: string;
39 /**
40 * Whether the admin accepted the managed Google Play Agreement.
41 * Required for EMM-managed enterprises; must be omitted for
42 * customer-managed ones.
43 * @default true when `signupUrlName` is omitted
44 */
45 agreementAccepted?: boolean;
46 /**
47 * Display name shown to users (max 100 characters). Enterprises have
48 * no labels field, so without state Alchemy finds the enterprise by
49 * this name.
50 * @default a unique name generated from the stack, stage, and logical id
51 */
52 enterpriseDisplayName?: string;
53 /**
54 * Predominant UI color as `(red << 16) | (green << 8) | blue`.
55 */
56 primaryColor?: number;
57 /**
58 * Pub/Sub topic for enterprise notifications
59 * (`projects/{project}/topics/{topic}`).
60 */
61 pubsubTopic?: string;
62 /**
63 * Enabled Pub/Sub notification types (`ENROLLMENT`, `COMMAND`, …).
64 */
65 enabledNotificationTypes?: Array<
66 androidmanagement.EnterpriseEnabledNotificationTypesItemEnum | (string & {})
67 >;
68 /**
69 * Logo shown during device provisioning.
70 */
71 logo?: androidmanagement.ExternalData;
72 /**
73 * Terms and conditions pages shown during provisioning.
74 */
75 termsAndConditions?: androidmanagement.TermsAndConditionsList;
76 /**
77 * Sign-in details used for custom enrollment.
78 */
79 signinDetails?: androidmanagement.SigninDetailList;
80 /**
81 * Contact info for an EMM-managed enterprise.
82 */
83 contactInfo?: androidmanagement.ContactInfo;
84};
85
86export type Enterprise = Resource<
87 "GCP.AndroidManagement.Enterprise",
88 EnterpriseProps,
89 {
90 /** Resource name `enterprises/{enterprise}`. */
91 name: string;
92 /** Enterprise id (last path segment). */
93 enterpriseId: string;
94 /** Project id used when the enterprise was reconciled. */
95 project: string;
96 /** Display name. */
97 enterpriseDisplayName: string | undefined;
98 /** Predominant UI color. */
99 primaryColor: number | undefined;
100 /** Pub/Sub topic for notifications. */
101 pubsubTopic: string | undefined;
102 /** Enabled notification types. */
103 enabledNotificationTypes: string[] | undefined;
104 /** Provisioning logo. */
105 logo: androidmanagement.ExternalData | undefined;
106 /** Terms and conditions. */
107 termsAndConditions: androidmanagement.TermsAndConditionsList | undefined;
108 /** Sign-in details. */
109 signinDetails: androidmanagement.SigninDetailList | undefined;
110 /** Contact info. */
111 contactInfo: androidmanagement.ContactInfo | undefined;
112 /** Enterprise type. */
113 enterpriseType: string | undefined;
114 /** Managed Google Play Accounts enterprise type. */
115 managedGooglePlayAccountsEnterpriseType: string | undefined;
116 /** Managed Google domain type. */
117 managedGoogleDomainType: string | undefined;
118 /** Google authentication settings. */
119 googleAuthenticationSettings:
120 | androidmanagement.GoogleAuthenticationSettings
121 | undefined;
122 },
123 never,
124 Providers
125>;
126
127/**
128 * An Android Management API enterprise.
129 *
130 * Enterprises have no labels field, so Alchemy tracks the enterprise by
131 * its resource name; without state it is found by display name, and one
132 * found under an explicit (non-generated) display name is reported as
133 * unowned. Customer-managed
134 * enterprises need `signupUrlName` plus `enterpriseToken`; otherwise an
135 * EMM-managed enterprise is created with `agreementAccepted`. Display
136 * name, color, notifications, logo, terms, sign-in details, and contact
137 * info update in place. Changing `projectId` replaces the enterprise.
138 * `enterprises.delete` only works for EMM-managed enterprises.
139 *
140 * ### Creating an Enterprise
141 * **Example:** EMM-managed enterprise
142 * ```typescript
143 * const enterprise = yield* GCP.AndroidManagement.Enterprise("Work", {
144 * enterpriseDisplayName: "Alchemy Work",
145 * });
146 * ```
147 *
148 * **Example:** Customer-managed after signup
149 * ```typescript
150 * const enterprise = yield* GCP.AndroidManagement.Enterprise("Work", {
151 * signupUrlName: "signupUrls/abc",
152 * enterpriseToken: token,
153 * enterpriseDisplayName: "Acme",
154 * });
155 * ```
156 *
157 * ### Updating an Enterprise
158 * **Example:** Rename
159 * ```typescript
160 * const enterprise = yield* GCP.AndroidManagement.Enterprise("Work", {
161 * enterpriseDisplayName: "Alchemy Work 2026",
162 * });
163 * ```
164 *
165 * @resource
166 * @category AndroidManagement
167 */
168export const Enterprise = Resource<Enterprise>(
169 "GCP.AndroidManagement.Enterprise",
170);
171
172export class EnterpriseNotResolved extends Data.TaggedError(
173 "GCP.AndroidManagement.EnterpriseNotResolved",
174)<{
175 name: string;
176}> {}
177
178const toAttrs = (enterprise: androidmanagement.Enterprise, project: string) => {
179 const name = enterprise.name ?? "";
180 return {
181 name,
182 enterpriseId: lastSegment(name),
183 project,
184 enterpriseDisplayName: enterprise.enterpriseDisplayName,
185 primaryColor: enterprise.primaryColor,
186 pubsubTopic: enterprise.pubsubTopic,
187 enabledNotificationTypes: enterprise.enabledNotificationTypes,
188 logo: enterprise.logo,
189 termsAndConditions: enterprise.termsAndConditions,
190 signinDetails: enterprise.signinDetails,
191 contactInfo: enterprise.contactInfo,
192 enterpriseType: enterprise.enterpriseType,
193 managedGooglePlayAccountsEnterpriseType:
194 enterprise.managedGooglePlayAccountsEnterpriseType,
195 managedGoogleDomainType: enterprise.managedGoogleDomainType,
196 googleAuthenticationSettings: enterprise.googleAuthenticationSettings,
197 };
198};
199
200const desiredBody = (input: {
201 displayName: string;
202 news: EnterpriseProps;
203}): androidmanagement.Enterprise => ({
204 enterpriseDisplayName: input.displayName,
205 primaryColor: input.news.primaryColor,
206 pubsubTopic: input.news.pubsubTopic,
207 enabledNotificationTypes: input.news.enabledNotificationTypes,
208 logo: input.news.logo,
209 termsAndConditions: input.news.termsAndConditions,
210 signinDetails: input.news.signinDetails,
211 contactInfo: input.news.contactInfo,
212});
213
214export const EnterpriseProvider = () =>
215 Provider.succeed(Enterprise, {
216 stables: ["name", "enterpriseId", "project", "enterpriseType"],
217
218 diff: Effect.fn(function* ({ news, olds, output }) {
219 if (!isResolved(news)) return undefined;
220 const previousProject = olds?.projectId ?? output?.project;
221 if (
222 news.projectId !== undefined &&
223 previousProject !== undefined &&
224 news.projectId !== previousProject
225 ) {
226 return { action: "replace" as const, deleteFirst: false };
227 }
228 return undefined;
229 }),
230
231 read: Effect.fn(function* ({ id, olds, output }) {
232 const env = yield* GcpEnvironment.current;
233 const projectId = olds?.projectId ?? output?.project ?? env.project;
234 const byName = yield* getEnterprise(
235 output?.name ?? toEnterpriseName(output?.enterpriseId ?? ""),
236 );
237 if (byName !== undefined) return toAttrs(byName, projectId);
238 const generated = yield* toDisplayName(id, undefined, undefined);
239 const displayName = olds?.enterpriseDisplayName ?? generated;
240 const found = yield* findEnterpriseByDisplayName(projectId, displayName);
241 if (found === undefined) return undefined;
242 const attrs = toAttrs(found, projectId);
243 // A generated display name is unique to this stack, stage and id.
244 return displayName === generated ? attrs : Unowned(attrs);
245 }),
246
247 reconcile: Effect.fn(function* ({ id, news, output }) {
248 const env = yield* GcpEnvironment.current;
249 const projectId = news.projectId ?? output?.project ?? env.project;
250 const displayName = yield* toDisplayName(
251 id,
252 news.enterpriseDisplayName,
253 output?.enterpriseDisplayName,
254 MAX_DISPLAY_NAME_LENGTH,
255 );
256 const desired = desiredBody({ displayName, news });
257 const customerManaged =
258 (news.signupUrlName !== undefined && news.signupUrlName.length > 0) ||
259 (news.enterpriseToken !== undefined && news.enterpriseToken.length > 0);
260
261 let current = yield* getEnterprise(
262 output?.name ?? toEnterpriseName(output?.enterpriseId ?? ""),
263 );
264 if (current === undefined) {
265 current = yield* findEnterpriseByDisplayName(projectId, displayName);
266 }
267
268 if (current === undefined) {
269 const created = yield* androidmanagement
270 .createEnterprises({
271 projectId,
272 signupUrlName: news.signupUrlName,
273 enterpriseToken: news.enterpriseToken,
274 agreementAccepted: customerManaged
275 ? news.agreementAccepted
276 : (news.agreementAccepted ?? true),
277 body: desired,
278 })
279 .pipe(
280 Effect.catchTag("Conflict", () =>
281 findEnterpriseByDisplayName(projectId, displayName),
282 ),
283 );
284 current = created ?? undefined;
285 }
286
287 if (current === undefined) {
288 return yield* new EnterpriseNotResolved({
289 name: output?.name ?? displayName,
290 });
291 }
292
293 const name = current.name ?? output?.name ?? "";
294 const displayChanged = !sameText(
295 current.enterpriseDisplayName,
296 displayName,
297 );
298 const colorChanged =
299 news.primaryColor !== undefined &&
300 current.primaryColor !== news.primaryColor;
301 const topicChanged =
302 news.pubsubTopic !== undefined &&
303 !sameText(current.pubsubTopic, news.pubsubTopic);
304 const notificationsChanged =
305 news.enabledNotificationTypes !== undefined &&
306 !sameStringList(
307 current.enabledNotificationTypes,
308 news.enabledNotificationTypes,
309 );
310 const logoChanged =
311 news.logo !== undefined && !jsonEqual(current.logo, news.logo);
312 const termsChanged =
313 news.termsAndConditions !== undefined &&
314 !jsonEqual(current.termsAndConditions, news.termsAndConditions);
315 const signinChanged =
316 news.signinDetails !== undefined &&
317 !jsonEqual(current.signinDetails, news.signinDetails);
318 const contactChanged =
319 news.contactInfo !== undefined &&
320 !jsonEqual(current.contactInfo, news.contactInfo);
321
322 const updateMask = updateMaskOf(
323 displayChanged ? "enterpriseDisplayName" : undefined,
324 colorChanged ? "primaryColor" : undefined,
325 topicChanged ? "pubsubTopic" : undefined,
326 notificationsChanged ? "enabledNotificationTypes" : undefined,
327 logoChanged ? "logo" : undefined,
328 termsChanged ? "termsAndConditions" : undefined,
329 signinChanged ? "signinDetails" : undefined,
330 contactChanged ? "contactInfo" : undefined,
331 );
332
333 if (updateMask.length > 0 && name.length > 0) {
334 current = yield* androidmanagement.patchEnterprises({
335 name,
336 updateMask,
337 body: desired,
338 });
339 }
340
341 const fresh = (yield* getEnterprise(name)) ?? current;
342 return toAttrs(fresh, projectId);
343 }),
344
345 delete: Effect.fn(function* ({ output }) {
346 if (output.name.length === 0) return;
347 yield* androidmanagement.deleteEnterprises({ name: output.name }).pipe(
348 Effect.catchTag("NotFound", () => Effect.void),
349 // Customer-managed enterprises cannot be deleted through the API.
350 Effect.catchTag("BadRequest", () => Effect.void),
351 );
352 }),
353 });