1import * as mi from "@distilled.cloud/aws/iot-managed-integrations";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 syncManagedIntegrationsTags,
13 toTagRecord,
14 unwrapSensitive,
15} from "./internal.ts";
16
17export interface CredentialLockerProps {
18 /**
19 * Name of the credential locker. If omitted, a unique name is generated
20 * from the app, stage, and logical ID. Changing the name replaces the
21 * locker (there is no update API).
22 */
23 name?: string;
24 /**
25 * User-defined tags to apply to the credential locker.
26 */
27 tags?: Record<string, string>;
28}
29
30export interface CredentialLocker extends Resource<
31 "AWS.IoTManagedIntegrations.CredentialLocker",
32 CredentialLockerProps,
33 {
34 /** Service-generated identifier of the credential locker. */
35 credentialLockerId: string;
36 /** ARN of the credential locker. */
37 credentialLockerArn: string;
38 /** Name of the credential locker. */
39 credentialLockerName: string;
40 /** Tags applied to the credential locker (user + internal). */
41 tags: Record<string, string>;
42 },
43 never,
44 Providers
45> {}
46
47/**
48 * An AWS IoT Managed Integrations credential locker — a secured store for the
49 * credentials that devices use to onboard and authenticate with Managed
50 * integrations.
51 *
52 * IoT Managed Integrations is a regional service available in a limited set
53 * of regions (e.g. `eu-west-1`, `ca-central-1`).
54 *
55 * ### Creating Credential Lockers
56 * **Example:** Basic Credential Locker
57 * ```typescript
58 * const locker = yield* CredentialLocker("DeviceCredentials", {});
59 * ```
60 *
61 * **Example:** Named Credential Locker with Tags
62 * ```typescript
63 * const locker = yield* CredentialLocker("DeviceCredentials", {
64 * name: "my-device-credentials",
65 * tags: { team: "iot" },
66 * });
67 * ```
68 *
69 * @resource
70 */
71export const CredentialLocker = Resource<CredentialLocker>(
72 "AWS.IoTManagedIntegrations.CredentialLocker",
73);
74
75export const CredentialLockerProvider = () =>
76 Provider.effect(
77 CredentialLocker,
78 Effect.gen(function* () {
79 const toName = (id: string, props: CredentialLockerProps = {}) =>
80 props.name
81 ? Effect.succeed(props.name)
82 : createPhysicalName({ id, maxLength: 64 });
83
84 // Observe by service-generated Id.
85 const observeById = (identifier: string) =>
86 mi
87 .getCredentialLocker({ Identifier: identifier })
88 .pipe(
89 Effect.catchTag("ResourceNotFoundException", () =>
90 Effect.succeed(undefined),
91 ),
92 );
93
94 // Recover an existing locker by name when no Id is cached (e.g. after a
95 // state persistence failure). Names are not unique server-side, so take
96 // the first match.
97 const findByName = Effect.fn(function* (name: string) {
98 const summary = yield* mi.listCredentialLockers.items({}).pipe(
99 Stream.filter((item) => unwrapSensitive(item.Name) === name),
100 Stream.take(1),
101 Stream.runCollect,
102 Effect.map((chunk) => Array.from(chunk)[0]),
103 );
104 if (summary?.Id === undefined) return undefined;
105 return yield* observeById(summary.Id);
106 });
107
108 const toAttributes = Effect.fn(function* (
109 locker: mi.GetCredentialLockerResponse,
110 ) {
111 const name = unwrapSensitive(locker.Name);
112 if (
113 locker.Id === undefined ||
114 locker.Arn === undefined ||
115 name === undefined
116 ) {
117 return yield* Effect.fail(
118 new Error("credential locker response is missing Id, Arn, or Name"),
119 );
120 }
121 return {
122 credentialLockerId: locker.Id,
123 credentialLockerArn: locker.Arn,
124 credentialLockerName: name,
125 tags: toTagRecord(locker.Tags),
126 };
127 });
128
129 return {
130 stables: [
131 "credentialLockerId",
132 "credentialLockerArn",
133 "credentialLockerName",
134 ],
135 diff: Effect.fn(function* ({ id, olds, news }) {
136 if (!isResolved(news)) return;
137 if (
138 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
139 ) {
140 return { action: "replace" } as const;
141 }
142 }),
143 read: Effect.fn(function* ({ id, olds, output }) {
144 const locker =
145 output?.credentialLockerId !== undefined
146 ? yield* observeById(output.credentialLockerId)
147 : yield* findByName(yield* toName(id, olds ?? {}));
148 if (locker === undefined) return undefined;
149 const attrs = yield* toAttributes(locker);
150 return (yield* hasAlchemyTags(id, attrs.tags))
151 ? attrs
152 : Unowned(attrs);
153 }),
154 reconcile: Effect.fn(function* ({ id, news, output, session }) {
155 const name = yield* toName(id, news);
156 const internalTags = yield* createInternalTags(id);
157 const desiredTags = { ...internalTags, ...news.tags };
158
159 // Observe — cloud state is authoritative; output is only an Id cache.
160 let locker =
161 output?.credentialLockerId !== undefined
162 ? yield* observeById(output.credentialLockerId)
163 : yield* findByName(name);
164
165 // Ensure — create if missing (there is no name-uniqueness conflict
166 // to race against; creation is idempotent via ClientToken).
167 if (locker === undefined) {
168 const created = yield* mi.createCredentialLocker({
169 Name: name,
170 Tags: desiredTags,
171 });
172 if (created.Id === undefined) {
173 return yield* Effect.fail(
174 new Error(`failed to create credential locker '${name}'`),
175 );
176 }
177 locker = yield* observeById(created.Id);
178 if (locker === undefined) {
179 return yield* Effect.fail(
180 new Error(
181 `credential locker '${created.Id}' vanished after create`,
182 ),
183 );
184 }
185 }
186
187 // Sync tags — diff against OBSERVED cloud tags.
188 if (locker.Arn !== undefined) {
189 yield* syncManagedIntegrationsTags(
190 locker.Arn,
191 toTagRecord(locker.Tags),
192 desiredTags,
193 );
194 }
195
196 const attrs = yield* toAttributes(locker);
197 yield* session.note(attrs.credentialLockerArn);
198 return { ...attrs, tags: desiredTags };
199 }),
200 // Enumerate every credential locker in the account/region. Fetch each
201 // locker to resolve its tags (summaries omit them).
202 list: () =>
203 Effect.gen(function* () {
204 const summaries = yield* mi.listCredentialLockers.items({}).pipe(
205 Stream.runCollect,
206 Effect.map((chunk) => Array.from(chunk)),
207 );
208 const lockers = yield* Effect.forEach(
209 summaries.filter(
210 (s): s is mi.CredentialLockerSummary & { Id: string } =>
211 s.Id !== undefined,
212 ),
213 (summary) => observeById(summary.Id),
214 { concurrency: 5 },
215 );
216 return yield* Effect.forEach(
217 lockers.filter(
218 (locker): locker is mi.GetCredentialLockerResponse =>
219 locker !== undefined,
220 ),
221 toAttributes,
222 );
223 }),
224 delete: Effect.fn(function* ({ output }) {
225 yield* mi
226 .deleteCredentialLocker({ Identifier: output.credentialLockerId })
227 .pipe(
228 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
229 );
230 }),
231 };
232 }),
233 );