1import * as ssoAdmin from "@distilled.cloud/aws/sso-admin";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7import {
8 listInstances,
9 resolveInstance,
10 retryIdentityCenter,
11 toInstanceAttributes,
12} from "./common.ts";
13
14export interface InstanceProps {
15 /**
16 * Explicit instance ARN to adopt.
17 */
18 instanceArn?: string;
19 /**
20 * Friendly instance name.
21 *
22 * When `mode` is `"account"`, this name is passed to `CreateInstance`.
23 * When `mode` is `"existing"`, it is used only for discovery.
24 */
25 name?: string;
26 /**
27 * How Alchemy should satisfy this resource.
28 *
29 * `existing` adopts a visible instance and fails if none is found.
30 * `account` creates an account instance when no visible instance matches.
31 *
32 * Organization instances cannot currently be enabled via public API, so
33 * organization-management-account deployments should use `existing`.
34 *
35 * @default "existing"
36 */
37 mode?: "existing" | "account";
38}
39
40export interface Instance extends Resource<
41 "AWS.IdentityCenter.Instance",
42 InstanceProps,
43 {
44 /** The ARN of the Identity Center instance. */
45 instanceArn: string;
46 /** The identity store backing the instance. */
47 identityStoreId: string;
48 /** The AWS account that owns the instance. */
49 ownerAccountId: string | undefined;
50 /** The friendly name of the instance, if set. */
51 name: string | undefined;
52 /** The current status of the instance (`ACTIVE`, ...). */
53 status: string | undefined;
54 /** Details when the instance is in a failed state. */
55 statusReason: string | undefined;
56 /** When the instance was created. */
57 createdDate: Date | undefined;
58 /** How the resource was satisfied: `existing` (adopted) or `account` (created). */
59 mode: "existing" | "account";
60 },
61 never,
62 Providers
63> {}
64
65/**
66 * An IAM Identity Center instance visible to the current account.
67 *
68 * Use `mode: "existing"` to adopt a pre-enabled organization instance. Use
69 * `mode: "account"` only for standalone or member-account account instances.
70 * ### Discovering Existing Instances
71 * **Example:** Adopt An Existing Instance
72 * ```typescript
73 * const instance = yield* Instance("IdentityCenter", {
74 * mode: "existing",
75 * });
76 * ```
77 *
78 * ### Creating Account Instances
79 * **Example:** Create A Member Account Instance
80 * ```typescript
81 * const instance = yield* Instance("IdentityCenter", {
82 * mode: "account",
83 * name: "customer-a",
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const Instance = Resource<Instance>("AWS.IdentityCenter.Instance");
90
91export const InstanceProvider = () =>
92 Provider.effect(
93 Instance,
94 Effect.gen(function* () {
95 return {
96 stables: ["instanceArn", "identityStoreId", "ownerAccountId", "mode"],
97 // Enumerate every Identity Center instance visible to the calling
98 // account (organization + account instances). `ListInstances` is
99 // paginated and fully hydrates each `InstanceMetadata`, so no
100 // per-item read is needed. Accounts with no SSO enabled return an
101 // empty list (not an error). Each item is mapped to the exact `read`
102 // Attributes shape; `mode` defaults to "existing" since enumeration
103 // only observes pre-existing instances.
104 list: () =>
105 Effect.gen(function* () {
106 const instances = yield* listInstances();
107 return instances
108 .filter(
109 (
110 instance,
111 ): instance is ssoAdmin.InstanceMetadata & {
112 InstanceArn: string;
113 IdentityStoreId: string;
114 } =>
115 instance.InstanceArn != null &&
116 instance.IdentityStoreId != null,
117 )
118 .map((instance) => ({
119 ...toInstanceAttributes(instance),
120 mode: "existing" as const,
121 }));
122 }),
123 diff: Effect.fn(function* ({ olds, news }) {
124 if (!isResolved(news)) return;
125 if (
126 olds?.instanceArn !== news.instanceArn ||
127 olds?.mode !== news.mode ||
128 olds?.name !== news.name
129 ) {
130 return { action: "replace" } as const;
131 }
132 }),
133 read: Effect.fn(function* ({ olds, output }) {
134 const mode = output?.mode ?? olds?.mode ?? "existing";
135 const instance = yield* readInstance({
136 instanceArn: output?.instanceArn ?? olds?.instanceArn,
137 name: olds?.name,
138 });
139 return instance ? { ...instance, mode } : undefined;
140 }),
141 reconcile: Effect.fn(function* ({ news, output, session }) {
142 const mode = output?.mode ?? news.mode ?? "existing";
143
144 // Observe — try to find a visible instance, preferring an
145 // explicit ARN (from output or props) and otherwise scanning
146 // the account.
147 const existing = yield* readInstance({
148 instanceArn: output?.instanceArn ?? news.instanceArn,
149 name: news.name,
150 });
151 if (existing) {
152 yield* session.note(existing.instanceArn);
153 return {
154 ...existing,
155 mode,
156 };
157 }
158
159 // Ensure — for `existing` mode we cannot create one; fail.
160 // Organization instances must be enabled manually in the
161 // management account.
162 if (mode !== "account") {
163 return yield* Effect.fail(
164 new Error(
165 "No visible Identity Center instance was found. Organization instances must be enabled manually in the management account before Alchemy can adopt them.",
166 ),
167 );
168 }
169
170 // Ensure — `account` mode creates an account instance.
171 const response = yield* retryIdentityCenter(
172 ssoAdmin.createInstance({
173 Name: news.name,
174 }),
175 );
176
177 const created = yield* readInstance({
178 instanceArn: response.InstanceArn,
179 name: news.name,
180 });
181 if (!created) {
182 return yield* Effect.fail(
183 new Error(
184 "failed to resolve Identity Center instance after create",
185 ),
186 );
187 }
188
189 yield* session.note(created.instanceArn);
190 return {
191 ...created,
192 mode,
193 };
194 }),
195 delete: Effect.fn(function* ({ output }) {
196 if (output.mode !== "account") {
197 return;
198 }
199
200 const existing = yield* readInstance({
201 instanceArn: output.instanceArn,
202 });
203 if (!existing) {
204 return;
205 }
206
207 yield* retryIdentityCenter(
208 ssoAdmin.deleteInstance({
209 InstanceArn: output.instanceArn,
210 }),
211 );
212 }),
213 };
214 }),
215 );
216
217const readInstance = Effect.fn(function* ({
218 instanceArn,
219 name,
220}: Pick<InstanceProps, "instanceArn" | "name">) {
221 if (instanceArn) {
222 const instance = yield* resolveInstance(instanceArn).pipe(
223 Effect.catch(() => Effect.succeed(undefined)),
224 );
225 return instance ? toInstanceAttributes(instance) : undefined;
226 }
227
228 const instances = yield* listInstances();
229 const match =
230 instances.find((instance) => instance.Name === name) ??
231 (instances.length === 1 ? instances[0] : undefined) ??
232 instances.find((instance) => instance.Status === "ACTIVE");
233
234 return match?.InstanceArn && match.IdentityStoreId
235 ? toInstanceAttributes(match)
236 : undefined;
237});