1import * as kms from "@distilled.cloud/aws/kms";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { AWSEnvironment, type AccountID } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import type { RegionID } from "../Region.ts";
13import type { KeyArn, KeyId } from "./Key.ts";
14
15export type AliasName = `alias/${string}`;
16export type AliasArn = `arn:aws:kms:${RegionID}:${AccountID}:${AliasName}`;
17
18export interface AliasProps {
19 /**
20 * Alias name. Must begin with `alias/`. If omitted, Alchemy generates one.
21 */
22 aliasName?: AliasName;
23 /**
24 * Target KMS key ID or ARN for this alias.
25 */
26 targetKeyId: KeyId | KeyArn;
27}
28
29export interface Alias extends Resource<
30 "AWS.KMS.Alias",
31 AliasProps,
32 {
33 aliasName: AliasName;
34 aliasArn: AliasArn;
35 targetKeyId: KeyId;
36 },
37 never,
38 Providers
39> {}
40
41/**
42 * An AWS KMS alias that points to a customer managed key.
43 *
44 * ### Creating Aliases
45 * **Example:** Alias for a Key
46 * ```typescript
47 * import * as KMS from "alchemy/AWS/KMS";
48 *
49 * const key = yield* KMS.Key("AppKey");
50 * const alias = yield* KMS.Alias("AppAlias", {
51 * aliasName: "alias/app",
52 * targetKeyId: key.keyId,
53 * });
54 * ```
55 */
56export const Alias = Resource<Alias>("AWS.KMS.Alias");
57
58export const AliasProvider = () =>
59 Provider.succeed(Alias, {
60 stables: ["aliasName", "aliasArn"],
61 list: () =>
62 Effect.gen(function* () {
63 const aliases = yield* kms.listAliases.pages({}).pipe(
64 Stream.runCollect,
65 Effect.map((chunk) =>
66 Array.from(chunk).flatMap((page) => page.Aliases ?? []),
67 ),
68 );
69
70 return aliases
71 .filter(
72 (
73 alias,
74 ): alias is kms.AliasListEntry & {
75 AliasName: AliasName;
76 AliasArn: AliasArn;
77 TargetKeyId: KeyId;
78 } =>
79 isCustomerAlias(alias.AliasName) &&
80 alias.AliasArn !== undefined &&
81 alias.TargetKeyId !== undefined,
82 )
83 .map((alias) => ({
84 aliasName: alias.AliasName,
85 aliasArn: alias.AliasArn,
86 targetKeyId: alias.TargetKeyId,
87 }));
88 }),
89 read: Effect.fn(function* ({ id, olds, output }) {
90 const aliasName =
91 output?.aliasName ?? (yield* toAliasName(id, olds ?? {}));
92 const state = yield* readAlias(aliasName);
93 if (!state) return undefined;
94 return output ? state : Unowned(state);
95 }),
96 diff: Effect.fn(function* ({ id, news, olds = {} }) {
97 if (!isResolved(news)) return;
98 if ((yield* toAliasName(id, news)) !== (yield* toAliasName(id, olds))) {
99 return { action: "replace" } as const;
100 }
101 }),
102 reconcile: Effect.fn(function* ({ id, news, output, session }) {
103 const aliasName = output?.aliasName ?? (yield* toAliasName(id, news));
104 const targetKeyId = yield* resolveTargetKeyId(news.targetKeyId);
105 let state = yield* readAlias(aliasName);
106
107 if (!state) {
108 yield* kms
109 .createAlias({
110 AliasName: aliasName,
111 TargetKeyId: targetKeyId,
112 })
113 .pipe(
114 Effect.retry({
115 while: isKmsEventuallyConsistent,
116 schedule: kmsRetrySchedule,
117 }),
118 Effect.catchTag("AlreadyExistsException", () => Effect.void),
119 );
120 state = yield* readAlias(aliasName);
121 }
122
123 if (state?.targetKeyId !== targetKeyId) {
124 yield* kms
125 .updateAlias({
126 AliasName: aliasName,
127 TargetKeyId: targetKeyId,
128 })
129 .pipe(
130 Effect.retry({
131 while: isKmsEventuallyConsistent,
132 schedule: kmsRetrySchedule,
133 }),
134 );
135 state = yield* readAlias(aliasName);
136 }
137
138 if (!state) {
139 return yield* Effect.die(
140 new Error(`failed to read KMS alias ${aliasName}`),
141 );
142 }
143
144 yield* session.note(`KMS alias ${aliasName}`);
145 return state;
146 }),
147 delete: Effect.fn(function* ({ output, session }) {
148 yield* kms
149 .deleteAlias({
150 AliasName: output.aliasName,
151 })
152 .pipe(Effect.catchTag("NotFoundException", () => Effect.void));
153
154 const remaining = yield* Effect.repeat(readAlias(output.aliasName), {
155 schedule: Schedule.fixed("250 millis"),
156 until: (alias) => alias === undefined,
157 times: 20,
158 });
159 if (remaining !== undefined) {
160 yield* Effect.die(
161 new Error(
162 `KMS alias ${output.aliasName} remained observable after delete`,
163 ),
164 );
165 }
166 yield* session.note(`Deleted KMS alias ${output.aliasName}`);
167 }),
168 });
169
170const toAliasName = Effect.fn(function* (
171 id: string,
172 props: { aliasName?: AliasName },
173) {
174 if (props.aliasName) {
175 return props.aliasName;
176 }
177
178 return `alias/${yield* createPhysicalName({
179 id,
180 maxLength: 256 - "alias/".length,
181 })}` as AliasName;
182});
183
184const readAlias = Effect.fn(function* (aliasName: AliasName) {
185 const alias = yield* findAlias(aliasName);
186 if (!alias?.AliasName || !alias.TargetKeyId) return undefined;
187
188 return {
189 aliasName: alias.AliasName as AliasName,
190 aliasArn: (alias.AliasArn ?? (yield* aliasArn(aliasName))) as AliasArn,
191 targetKeyId: alias.TargetKeyId,
192 };
193});
194
195const findAlias = Effect.fn(function* (aliasName: AliasName) {
196 const aliases = yield* kms.listAliases.pages({}).pipe(
197 Stream.runCollect,
198 Effect.map((chunk) =>
199 Array.from(chunk).flatMap((page) => page.Aliases ?? []),
200 ),
201 );
202
203 return aliases.find((alias) => alias.AliasName === aliasName);
204});
205
206const aliasArn = Effect.fn(function* (aliasName: AliasName) {
207 const { accountId, region } = yield* AWSEnvironment.current;
208 return `arn:aws:kms:${region}:${accountId}:${aliasName}` as AliasArn;
209});
210
211const resolveTargetKeyId = Effect.fn(function* (targetKeyId: string) {
212 const described = yield* kms.describeKey({ KeyId: targetKeyId });
213 return described.KeyMetadata?.KeyId!;
214});
215
216const isCustomerAlias = (
217 aliasName: string | undefined,
218): aliasName is AliasName =>
219 aliasName !== undefined &&
220 aliasName.startsWith("alias/") &&
221 !aliasName.startsWith("alias/aws/");
222
223const isKmsEventuallyConsistent = (error: { _tag: string }) =>
224 error._tag === "DependencyTimeoutException" ||
225 error._tag === "KMSInternalException" ||
226 error._tag === "KMSInvalidStateException" ||
227 error._tag === "NotFoundException";
228
229const kmsRetrySchedule = Schedule.max([
230 Schedule.exponential(250),
231 Schedule.recurs(7),
232]);