1import * as acmpca from "@distilled.cloud/aws/acm-pca";
2import * as Data from "effect/Data";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Schedule from "effect/Schedule";
6import * as Stream from "effect/Stream";
7import { Unowned } from "../../AdoptPolicy.ts";
8import { isResolved } from "../../Diff.ts";
9import * as Provider from "../../Provider.ts";
10import { Resource } from "../../Resource.ts";
11import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
12import { toWireDays } from "../../Util/Duration.ts";
13import type { Providers } from "../Providers.ts";
14
15/**
16 * X.500 distinguished name of the certificate authority. At least one
17 * field must be set (typically `commonName`).
18 */
19export interface CertificateAuthoritySubject {
20 /**
21 * Fully qualified domain name (FQDN) associated with the certificate
22 * subject, e.g. `corp.example.com`.
23 */
24 commonName?: string;
25 /**
26 * Legal name of the organization with which the certificate subject is
27 * affiliated.
28 */
29 organization?: string;
30 /**
31 * A subdivision or unit of the organization with which the certificate
32 * subject is affiliated.
33 */
34 organizationalUnit?: string;
35 /**
36 * Two-digit ISO 3166-1 country code, e.g. `US`.
37 */
38 country?: string;
39 /**
40 * State in which the subject of the certificate is located.
41 */
42 state?: string;
43 /**
44 * The locality (city) in which the certificate subject is located.
45 */
46 locality?: string;
47 /**
48 * The certificate serial number.
49 */
50 serialNumber?: string;
51 /**
52 * A title such as Mr. or Ms., assigned to the certificate subject.
53 */
54 title?: string;
55 /**
56 * Family name of the certificate subject.
57 */
58 surname?: string;
59 /**
60 * First name of the certificate subject.
61 */
62 givenName?: string;
63 /**
64 * Concatenation of first letters of the subject's first name, middle
65 * name(s) and last name.
66 */
67 initials?: string;
68 /**
69 * Shortened version of a longer given name.
70 */
71 pseudonym?: string;
72 /**
73 * Disambiguating information for the certificate subject.
74 */
75 distinguishedNameQualifier?: string;
76 /**
77 * A qualifier appended to the subject's name, e.g. `Jr.` or `III`.
78 */
79 generationQualifier?: string;
80}
81
82/**
83 * Certificate revocation list (CRL) configuration.
84 */
85export interface CrlConfigurationProps {
86 /**
87 * Whether Amazon Web Services Private CA maintains a CRL for the CA.
88 */
89 enabled: boolean;
90 /**
91 * Validity period of the CRL (e.g. `"7 days"` or `Duration.days(7)`).
92 * Rounded to whole days on the wire (`ExpirationInDays`).
93 * @default 7 days (service default when enabled)
94 */
95 expiration?: Duration.Input;
96 /**
97 * Alias to conceal the S3 bucket name inside issued certificates.
98 */
99 customCname?: string;
100 /**
101 * Name of the S3 bucket that receives the CRL. The bucket policy must
102 * grant write access to Amazon Web Services Private CA.
103 */
104 s3BucketName?: string;
105 /**
106 * ACL applied to the CRL objects written to S3.
107 */
108 s3ObjectAcl?: "PUBLIC_READ" | "BUCKET_OWNER_FULL_CONTROL";
109 /**
110 * Whether to generate a complete or partitioned CRL.
111 */
112 crlType?: "COMPLETE" | "PARTITIONED";
113 /**
114 * Custom path inside the S3 bucket under which the CRL is stored.
115 */
116 customPath?: string;
117}
118
119/**
120 * Online Certificate Status Protocol (OCSP) configuration.
121 */
122export interface OcspConfigurationProps {
123 /**
124 * Whether OCSP is enabled for the CA.
125 */
126 enabled: boolean;
127 /**
128 * Custom CNAME for the OCSP responder endpoint.
129 */
130 ocspCustomCname?: string;
131}
132
133export interface CertificateAuthorityProps {
134 /**
135 * The type of the certificate authority.
136 * Changing this replaces the CA.
137 * @default "ROOT"
138 */
139 type?: acmpca.CertificateAuthorityType;
140 /**
141 * The key algorithm used to generate the CA's private key.
142 * Changing this replaces the CA.
143 * @default "RSA_2048"
144 */
145 keyAlgorithm?: acmpca.KeyAlgorithm;
146 /**
147 * The signing algorithm the CA uses to sign certificate requests. Must
148 * match the key algorithm family (RSA vs ECDSA).
149 * Changing this replaces the CA.
150 * @default "SHA256WITHRSA"
151 */
152 signingAlgorithm?: acmpca.SigningAlgorithm;
153 /**
154 * X.500 distinguished name of the CA. Changing this replaces the CA.
155 */
156 subject: CertificateAuthoritySubject;
157 /**
158 * Whether the CA issues general-purpose or short-lived (7 days or less)
159 * certificates. Short-lived mode has a lower monthly price.
160 * Changing this replaces the CA.
161 * @default "GENERAL_PURPOSE"
162 */
163 usageMode?: acmpca.CertificateAuthorityUsageMode;
164 /**
165 * Security standard of the HSM that stores the CA key.
166 * Changing this replaces the CA.
167 * @default "FIPS_140_2_LEVEL_3_OR_HIGHER" (varies by region)
168 */
169 keyStorageSecurityStandard?: acmpca.KeyStorageSecurityStandard;
170 /**
171 * Certificate revocation configuration (CRL and/or OCSP). Applied at
172 * create time; subsequent changes are applied via
173 * `UpdateCertificateAuthority`, which AWS only permits while the CA is in
174 * the `ACTIVE` or `DISABLED` state. When omitted the existing revocation
175 * configuration is left unchanged.
176 */
177 revocationConfiguration?: {
178 /**
179 * Certificate revocation list (CRL) settings.
180 */
181 crlConfiguration?: CrlConfigurationProps;
182 /**
183 * Online Certificate Status Protocol (OCSP) settings.
184 */
185 ocspConfiguration?: OcspConfigurationProps;
186 };
187 /**
188 * How long (7-30 days, e.g. `"7 days"`) the CA remains restorable after
189 * deletion. Used when the CA is destroyed while in the
190 * `PENDING_CERTIFICATE` or `DISABLED` state. Rounded to whole days on the
191 * wire (`PermanentDeletionTimeInDays`).
192 * @default 7 days
193 */
194 permanentDeletionTime?: Duration.Input;
195 /**
196 * Tags to apply to the CA. Merged with internal Alchemy tags.
197 */
198 tags?: Record<string, string>;
199}
200
201export interface CertificateAuthority extends Resource<
202 "AWS.ACMPCA.CertificateAuthority",
203 CertificateAuthorityProps,
204 {
205 /** The ARN of the certificate authority. */
206 certificateAuthorityArn: string;
207 /** The status of the CA (e.g. `PENDING_CERTIFICATE`, `ACTIVE`). */
208 status: acmpca.CertificateAuthorityStatus;
209 },
210 never,
211 Providers
212> {}
213
214/**
215 * An Amazon Web Services Private CA certificate authority.
216 *
217 * A newly created CA starts in the `PENDING_CERTIFICATE` state — to
218 * activate it you must retrieve its CSR, sign it (self-sign for a root
219 * CA), and import the signed certificate. Private CAs bill a monthly fee
220 * for as long as they exist, so destroy test CAs promptly. Deletion
221 * places the CA in the `DELETED` state for a configurable 7-30 day
222 * restoration window.
223 * ### Creating a Certificate Authority
224 * **Example:** Root CA
225 * ```typescript
226 * import * as ACMPCA from "alchemy/AWS/ACMPCA";
227 *
228 * const ca = yield* ACMPCA.CertificateAuthority("RootCA", {
229 * subject: { commonName: "corp.example.com" },
230 * });
231 * ```
232 *
233 * **Example:** ECDSA Subordinate CA
234 * ```typescript
235 * const ca = yield* ACMPCA.CertificateAuthority("IssuingCA", {
236 * type: "SUBORDINATE",
237 * keyAlgorithm: "EC_prime256v1",
238 * signingAlgorithm: "SHA256WITHECDSA",
239 * subject: {
240 * commonName: "issuing.corp.example.com",
241 * organization: "Example Corp",
242 * country: "US",
243 * },
244 * });
245 * ```
246 *
247 * **Example:** Short-Lived Certificate Mode
248 * ```typescript
249 * const ca = yield* ACMPCA.CertificateAuthority("ShortLivedCA", {
250 * subject: { commonName: "ephemeral.example.com" },
251 * usageMode: "SHORT_LIVED_CERTIFICATE",
252 * });
253 * ```
254 *
255 * ### Revocation
256 * **Example:** CA with CRL published to S3
257 * ```typescript
258 * const ca = yield* ACMPCA.CertificateAuthority("RootCA", {
259 * subject: { commonName: "corp.example.com" },
260 * revocationConfiguration: {
261 * crlConfiguration: {
262 * enabled: true,
263 * expiration: "7 days",
264 * s3BucketName: bucket.bucketName,
265 * },
266 * },
267 * });
268 * ```
269 *
270 * ### Granting ACM Access
271 * **Example:** Allow ACM to auto-renew certificates issued by this CA
272 * ```typescript
273 * const permission = yield* ACMPCA.Permission("AcmRenewal", {
274 * certificateAuthorityArn: ca.certificateAuthorityArn,
275 * });
276 * ```
277 *
278 * ### Reacting to CA Events
279 * **Example:** Consume ACM PCA Events from EventBridge
280 * ```typescript
281 * // ACM PCA emits lifecycle events (certificate issuance, expiry, CRL and
282 * // audit-report generation) on the default EventBridge bus under the
283 * // `aws.acm-pca` source — consume them with the generic EventBridge
284 * // event source; there is no ACM PCA-specific notification config.
285 * yield* AWS.EventBridge.consumeBusEvents(
286 * {
287 * source: ["aws.acm-pca"],
288 * "detail-type": ["ACM Private CA Certificate Issuance"],
289 * },
290 * (events) =>
291 * Stream.runForEach(events, (event) => Effect.log(event.detail)),
292 * );
293 * ```
294 *
295 * @resource
296 */
297export const CertificateAuthority = Resource<CertificateAuthority>(
298 "AWS.ACMPCA.CertificateAuthority",
299);
300
301/**
302 * Internal signal used to poll a freshly created CA out of the
303 * `CREATING` state on a bounded schedule.
304 */
305class CertificateAuthorityStillCreating extends Data.TaggedError(
306 "CertificateAuthorityStillCreating",
307)<{ arn: string }> {}
308
309// Bounded retry helpers with explicit signatures. Inlining Effect.retry in
310// lifecycle ops leaks Retry.Return's conditional type into declaration emit
311// and widens the provider layer to `unknown` for every consumer.
312const retryWhileCreating = <A, E extends { readonly _tag: string }, R>(
313 self: Effect.Effect<A, E, R>,
314): Effect.Effect<A, E, R> =>
315 Effect.retry(self, {
316 while: (e) => e._tag === "CertificateAuthorityStillCreating",
317 schedule: Schedule.max([Schedule.fixed(2000), Schedule.recurs(30)]),
318 });
319
320const retryWhileConcurrentlyModified = <
321 A,
322 E extends { readonly _tag: string },
323 R,
324>(
325 self: Effect.Effect<A, E, R>,
326): Effect.Effect<A, E, R> =>
327 Effect.retry(self, {
328 while: (e) => e._tag === "ConcurrentModificationException",
329 schedule: Schedule.max([Schedule.fixed(2000), Schedule.recurs(10)]),
330 });
331
332const buildSubject = (
333 subject: CertificateAuthoritySubject,
334): acmpca.ASN1Subject => ({
335 CommonName: subject.commonName,
336 Organization: subject.organization,
337 OrganizationalUnit: subject.organizationalUnit,
338 Country: subject.country,
339 State: subject.state,
340 Locality: subject.locality,
341 SerialNumber: subject.serialNumber,
342 Title: subject.title,
343 Surname: subject.surname,
344 GivenName: subject.givenName,
345 Initials: subject.initials,
346 Pseudonym: subject.pseudonym,
347 DistinguishedNameQualifier: subject.distinguishedNameQualifier,
348 GenerationQualifier: subject.generationQualifier,
349});
350
351const buildRevocationConfiguration = (
352 props: CertificateAuthorityProps["revocationConfiguration"],
353): acmpca.RevocationConfiguration | undefined => {
354 if (props === undefined) return undefined;
355 const crl = props.crlConfiguration;
356 const ocsp = props.ocspConfiguration;
357 return {
358 CrlConfiguration: crl
359 ? {
360 Enabled: crl.enabled,
361 ExpirationInDays: toWireDays(crl.expiration),
362 CustomCname: crl.customCname,
363 S3BucketName: crl.s3BucketName,
364 S3ObjectAcl: crl.s3ObjectAcl,
365 CrlType: crl.crlType,
366 CustomPath: crl.customPath,
367 }
368 : undefined,
369 OcspConfiguration: ocsp
370 ? {
371 Enabled: ocsp.enabled,
372 OcspCustomCname: ocsp.ocspCustomCname,
373 }
374 : undefined,
375 };
376};
377
378export const CertificateAuthorityProvider = () =>
379 Provider.effect(
380 CertificateAuthority,
381 Effect.gen(function* () {
382 // Describe a CA by ARN; typed not-found → undefined. A CA in the
383 // DELETED state is inside its restoration window — for lifecycle
384 // purposes it is gone (we never restore automatically).
385 const describeCa = Effect.fn(function* (arn: string) {
386 return yield* acmpca
387 .describeCertificateAuthority({ CertificateAuthorityArn: arn })
388 .pipe(
389 Effect.map((r) => r.CertificateAuthority),
390 Effect.catchTag("ResourceNotFoundException", () =>
391 Effect.succeed(undefined),
392 ),
393 );
394 });
395
396 const listCaTags = Effect.fn(function* (arn: string) {
397 const tags = yield* acmpca.listTags
398 .items({ CertificateAuthorityArn: arn })
399 .pipe(
400 Stream.runCollect,
401 Effect.map((chunk) =>
402 Object.fromEntries(
403 Array.from(chunk)
404 .filter((t) => t.Value !== undefined)
405 .map((t) => [t.Key, t.Value!] as const),
406 ),
407 ),
408 Effect.catch(() => Effect.succeed({} as Record<string, string>)),
409 );
410 return tags;
411 });
412
413 const subjectFromAsn1 = (
414 s: acmpca.ASN1Subject | undefined,
415 ): CertificateAuthoritySubject => ({
416 commonName: s?.CommonName,
417 organization: s?.Organization,
418 organizationalUnit: s?.OrganizationalUnit,
419 country: s?.Country,
420 state: s?.State,
421 locality: s?.Locality,
422 serialNumber: s?.SerialNumber,
423 title: s?.Title,
424 surname: s?.Surname,
425 givenName: s?.GivenName,
426 initials: s?.Initials,
427 pseudonym: s?.Pseudonym,
428 distinguishedNameQualifier: s?.DistinguishedNameQualifier,
429 generationQualifier: s?.GenerationQualifier,
430 });
431
432 // Does the live CA's immutable configuration match the desired props?
433 // The tag-search fallback below must NOT match a CA whose immutable
434 // config differs — during a replacement the new instance reconciles
435 // with `output === undefined` and the same logical id, and matching
436 // the old CA by tags alone would silently re-adopt it instead of
437 // creating the replacement.
438 const matchesImmutableConfig = (
439 ca: acmpca.CertificateAuthority,
440 props: CertificateAuthorityProps,
441 ) => {
442 const config = ca.CertificateAuthorityConfiguration;
443 return (
444 (ca.Type ?? "ROOT") === (props.type ?? "ROOT") &&
445 config?.KeyAlgorithm === (props.keyAlgorithm ?? "RSA_2048") &&
446 config?.SigningAlgorithm ===
447 (props.signingAlgorithm ?? "SHA256WITHRSA") &&
448 (ca.UsageMode ?? "GENERAL_PURPOSE") ===
449 (props.usageMode ?? "GENERAL_PURPOSE") &&
450 (props.keyStorageSecurityStandard === undefined ||
451 ca.KeyStorageSecurityStandard ===
452 props.keyStorageSecurityStandard) &&
453 JSON.stringify(buildSubject(subjectFromAsn1(config?.Subject))) ===
454 JSON.stringify(buildSubject(props.subject ?? {}))
455 );
456 };
457
458 // ARNs are auto-assigned, so after a state persistence failure the
459 // only way back to an orphaned CA is its Alchemy ownership tags.
460 // Leaking a CA would keep billing, so reconcile searches by tags
461 // (plus matching immutable configuration) before creating.
462 const findManagedCa = Effect.fn(function* (
463 id: string,
464 props: CertificateAuthorityProps,
465 ) {
466 const cas = yield* acmpca.listCertificateAuthorities.items({}).pipe(
467 Stream.runCollect,
468 Effect.map((chunk) =>
469 Array.from(chunk).filter(
470 (ca) => ca.Arn !== undefined && ca.Status !== "DELETED",
471 ),
472 ),
473 );
474 for (const ca of cas) {
475 if (!matchesImmutableConfig(ca, props)) continue;
476 const tags = yield* listCaTags(ca.Arn!);
477 if (yield* hasAlchemyTags(id, tags)) {
478 return ca;
479 }
480 }
481 return undefined;
482 });
483
484 // Poll a freshly created CA out of CREATING (bounded ~60s; the
485 // transition to PENDING_CERTIFICATE is normally fast).
486 const waitUntilCreated = Effect.fn(function* (arn: string) {
487 return yield* describeCa(arn).pipe(
488 Effect.flatMap((ca) =>
489 ca?.Status === "CREATING"
490 ? Effect.fail(new CertificateAuthorityStillCreating({ arn }))
491 : Effect.succeed(ca),
492 ),
493 retryWhileCreating,
494 );
495 });
496
497 const toAttrs = (
498 arn: string,
499 status: acmpca.CertificateAuthorityStatus | undefined,
500 ): CertificateAuthority["Attributes"] => ({
501 certificateAuthorityArn: arn,
502 status: status ?? "PENDING_CERTIFICATE",
503 });
504
505 return {
506 stables: ["certificateAuthorityArn"],
507 list: () =>
508 acmpca.listCertificateAuthorities.items({}).pipe(
509 Stream.runCollect,
510 Effect.map((chunk) =>
511 Array.from(chunk)
512 // DELETED CAs are already inside their restoration window
513 // and cannot be deleted again.
514 .filter((ca) => ca.Arn !== undefined && ca.Status !== "DELETED")
515 .map((ca) => toAttrs(ca.Arn!, ca.Status)),
516 ),
517 ),
518 read: Effect.fn(function* ({ id, olds, output }) {
519 // The ARN is auto-assigned: prefer the cached output, fall back
520 // to searching by ownership tags (state persistence failure).
521 let ca =
522 output?.certificateAuthorityArn !== undefined
523 ? yield* describeCa(output.certificateAuthorityArn)
524 : undefined;
525 if ((ca === undefined || ca.Status === "DELETED") && olds) {
526 ca = yield* findManagedCa(id, olds);
527 }
528 if (ca?.Arn === undefined || ca.Status === "DELETED") {
529 return undefined;
530 }
531 const attrs = toAttrs(ca.Arn, ca.Status);
532 const tags = yield* listCaTags(ca.Arn);
533 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
534 }),
535 diff: Effect.fn(function* ({ news, olds }) {
536 if (!isResolved(news)) return;
537 const immutableChanged =
538 (news.type ?? "ROOT") !== (olds.type ?? "ROOT") ||
539 (news.keyAlgorithm ?? "RSA_2048") !==
540 (olds.keyAlgorithm ?? "RSA_2048") ||
541 (news.signingAlgorithm ?? "SHA256WITHRSA") !==
542 (olds.signingAlgorithm ?? "SHA256WITHRSA") ||
543 (news.usageMode ?? "GENERAL_PURPOSE") !==
544 (olds.usageMode ?? "GENERAL_PURPOSE") ||
545 news.keyStorageSecurityStandard !==
546 olds.keyStorageSecurityStandard ||
547 JSON.stringify(buildSubject(news.subject)) !==
548 JSON.stringify(buildSubject(olds.subject));
549 if (immutableChanged) {
550 return { action: "replace" } as const;
551 }
552 }),
553 reconcile: Effect.fn(function* ({ id, news, output, session }) {
554 const internalTags = yield* createInternalTags(id);
555 const desiredRevocation = buildRevocationConfiguration(
556 news.revocationConfiguration,
557 );
558
559 // 1. OBSERVE — output is only an ARN cache; cloud state is
560 // authoritative. Fall back to a tag search so a persistence
561 // failure never leaks (and double-bills) a CA.
562 let ca =
563 output?.certificateAuthorityArn !== undefined
564 ? yield* describeCa(output.certificateAuthorityArn)
565 : undefined;
566 if (ca === undefined || ca.Status === "DELETED") {
567 ca = yield* findManagedCa(id, news);
568 }
569
570 // 2. ENSURE — create if missing and wait out the CREATING state.
571 if (ca?.Arn === undefined || ca.Status === "DELETED") {
572 const created = yield* acmpca.createCertificateAuthority({
573 CertificateAuthorityConfiguration: {
574 KeyAlgorithm: news.keyAlgorithm ?? "RSA_2048",
575 SigningAlgorithm: news.signingAlgorithm ?? "SHA256WITHRSA",
576 Subject: buildSubject(news.subject),
577 },
578 CertificateAuthorityType: news.type ?? "ROOT",
579 RevocationConfiguration: desiredRevocation,
580 KeyStorageSecurityStandard: news.keyStorageSecurityStandard,
581 UsageMode: news.usageMode,
582 Tags: Object.entries({
583 ...news.tags,
584 ...internalTags,
585 }).map(([Key, Value]) => ({ Key, Value })),
586 });
587 const arn = created.CertificateAuthorityArn!;
588 yield* session.note(`Created certificate authority ${arn}`);
589 ca = (yield* waitUntilCreated(arn)) ?? { Arn: arn };
590 }
591 const arn = ca.Arn!;
592
593 // 3. SYNC revocation configuration — AWS only allows
594 // UpdateCertificateAuthority while the CA is ACTIVE or DISABLED,
595 // so a PENDING_CERTIFICATE CA keeps its create-time settings.
596 if (
597 desiredRevocation !== undefined &&
598 (ca.Status === "ACTIVE" || ca.Status === "DISABLED")
599 ) {
600 const observed = JSON.stringify(
601 buildRevocationConfiguration({
602 crlConfiguration: ca.RevocationConfiguration?.CrlConfiguration
603 ? {
604 enabled:
605 ca.RevocationConfiguration.CrlConfiguration.Enabled,
606 expiration:
607 ca.RevocationConfiguration.CrlConfiguration
608 .ExpirationInDays === undefined
609 ? undefined
610 : Duration.days(
611 ca.RevocationConfiguration.CrlConfiguration
612 .ExpirationInDays,
613 ),
614 customCname:
615 ca.RevocationConfiguration.CrlConfiguration.CustomCname,
616 s3BucketName:
617 ca.RevocationConfiguration.CrlConfiguration
618 .S3BucketName,
619 s3ObjectAcl: ca.RevocationConfiguration.CrlConfiguration
620 .S3ObjectAcl as "PUBLIC_READ" | undefined,
621 crlType: ca.RevocationConfiguration.CrlConfiguration
622 .CrlType as "COMPLETE" | undefined,
623 customPath:
624 ca.RevocationConfiguration.CrlConfiguration.CustomPath,
625 }
626 : undefined,
627 ocspConfiguration: ca.RevocationConfiguration?.OcspConfiguration
628 ? {
629 enabled:
630 ca.RevocationConfiguration.OcspConfiguration.Enabled,
631 ocspCustomCname:
632 ca.RevocationConfiguration.OcspConfiguration
633 .OcspCustomCname,
634 }
635 : undefined,
636 }),
637 );
638 if (observed !== JSON.stringify(desiredRevocation)) {
639 yield* acmpca
640 .updateCertificateAuthority({
641 CertificateAuthorityArn: arn,
642 RevocationConfiguration: desiredRevocation,
643 })
644 .pipe(retryWhileConcurrentlyModified);
645 }
646 }
647
648 // 3b. SYNC tags — diff against OBSERVED cloud tags so adoption
649 // converges.
650 const currentTags = yield* listCaTags(arn);
651 const desiredTags: Record<string, string> = {
652 ...news.tags,
653 ...internalTags,
654 };
655 const { upsert, removed } = diffTags(currentTags, desiredTags);
656 if (upsert.length > 0) {
657 yield* acmpca.tagCertificateAuthority({
658 CertificateAuthorityArn: arn,
659 Tags: upsert,
660 });
661 }
662 if (removed.length > 0) {
663 yield* acmpca.untagCertificateAuthority({
664 CertificateAuthorityArn: arn,
665 Tags: removed.map((Key) => ({ Key })),
666 });
667 }
668
669 // 4. RETURN fresh attributes.
670 const final = yield* describeCa(arn);
671 yield* session.note(arn);
672 return toAttrs(arn, final?.Status ?? ca.Status);
673 }),
674 delete: Effect.fn(function* ({ olds, output, session }) {
675 const arn = output.certificateAuthorityArn;
676 const ca = yield* describeCa(arn);
677 if (ca === undefined || ca.Status === "DELETED") {
678 return;
679 }
680 // An ACTIVE CA must be DISABLED before it can be deleted.
681 if (ca.Status === "ACTIVE") {
682 yield* acmpca
683 .updateCertificateAuthority({
684 CertificateAuthorityArn: arn,
685 Status: "DISABLED",
686 })
687 .pipe(
688 retryWhileConcurrentlyModified,
689 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
690 );
691 }
692 // The restoration window may only be set while the CA is in the
693 // PENDING_CERTIFICATE or DISABLED state (CREATING/FAILED CAs have
694 // no restoration period).
695 const canSetWindow =
696 ca.Status === "PENDING_CERTIFICATE" ||
697 ca.Status === "DISABLED" ||
698 ca.Status === "ACTIVE"; // just disabled above
699 yield* acmpca
700 .deleteCertificateAuthority({
701 CertificateAuthorityArn: arn,
702 PermanentDeletionTimeInDays: canSetWindow
703 ? (toWireDays(olds?.permanentDeletionTime) ?? 7)
704 : undefined,
705 })
706 .pipe(
707 retryWhileConcurrentlyModified,
708 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
709 );
710 yield* session.note(`Deleted certificate authority ${arn}`);
711 }),
712 };
713 }),
714 );