1import * as avp from "@distilled.cloud/aws/verifiedpermissions";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11
12/**
13 * Schema-validation strictness applied to Cedar policies and templates
14 * submitted to the store.
15 *
16 * - `OFF` — no schema validation.
17 * - `STRICT` — policies must reference entity types and actions declared in
18 * the store's schema (requires a schema to be present).
19 */
20export type ValidationMode = "OFF" | "STRICT";
21
22export interface PolicyStoreProps {
23 /**
24 * The schema-validation mode for the policy store.
25 * @default "OFF"
26 */
27 validationMode?: ValidationMode;
28 /**
29 * A human-readable description for the policy store.
30 */
31 description?: string;
32 /**
33 * When `ENABLED`, the store cannot be deleted until protection is disabled.
34 * Leave `DISABLED` for ephemeral / test stores.
35 * @default "DISABLED"
36 */
37 deletionProtection?: "ENABLED" | "DISABLED";
38 /**
39 * Tags to apply to the policy store. Merged with internal Alchemy tags.
40 */
41 tags?: Record<string, string>;
42}
43
44export interface PolicyStore extends Resource<
45 "AWS.VerifiedPermissions.PolicyStore",
46 PolicyStoreProps,
47 {
48 /**
49 * Service-assigned unique ID of the policy store — pass it to
50 * {@link Policy}, {@link Schema}, and `IsAuthorized`.
51 */
52 policyStoreId: string;
53 /**
54 * ARN of the policy store.
55 */
56 policyStoreArn: string;
57 },
58 {},
59 Providers
60> {}
61
62/**
63 * An Amazon Verified Permissions policy store — the container for Cedar
64 * policies, policy templates, and a schema. Authorization requests
65 * (`IsAuthorized`) are evaluated against all policies in a store.
66 * ### Creating a Policy Store
67 * **Example:** Basic Policy Store
68 * ```typescript
69 * import * as AWS from "alchemy/AWS";
70 *
71 * const store = yield* AWS.VerifiedPermissions.PolicyStore("Store", {
72 * validationMode: "OFF",
73 * });
74 * ```
75 *
76 * **Example:** Strict Validation with a Schema
77 * ```typescript
78 * const store = yield* AWS.VerifiedPermissions.PolicyStore("Store", {
79 * validationMode: "STRICT",
80 * description: "Photo app authorization",
81 * });
82 *
83 * yield* AWS.VerifiedPermissions.Schema("Schema", {
84 * policyStoreId: store.policyStoreId,
85 * cedarJson: JSON.stringify({
86 * PhotoApp: {
87 * entityTypes: { User: {}, Photo: {} },
88 * actions: { viewPhoto: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Photo"] } } },
89 * },
90 * }),
91 * });
92 * ```
93 *
94 * @resource
95 */
96export const PolicyStore = Resource<PolicyStore>(
97 "AWS.VerifiedPermissions.PolicyStore",
98);
99
100export const PolicyStoreProvider = () =>
101 Provider.effect(
102 PolicyStore,
103 Effect.gen(function* () {
104 const observe = Effect.fn(function* (policyStoreId: string) {
105 return yield* avp
106 .getPolicyStore({ policyStoreId, tags: true })
107 .pipe(
108 Effect.catchTag("ResourceNotFoundException", () =>
109 Effect.succeed(undefined),
110 ),
111 );
112 });
113
114 return PolicyStore.Provider.of({
115 stables: ["policyStoreId", "policyStoreArn"],
116
117 list: () =>
118 Effect.gen(function* () {
119 const pages = yield* avp.listPolicyStores
120 .pages({})
121 .pipe(Stream.runCollect);
122 const items = Array.from(pages).flatMap(
123 (page) => page.policyStores ?? [],
124 );
125 return items.map((item) => ({
126 policyStoreId: item.policyStoreId,
127 policyStoreArn: item.arn,
128 }));
129 }),
130
131 read: Effect.fn(function* ({ id, output }) {
132 if (output?.policyStoreId === undefined) return undefined;
133 const store = yield* observe(output.policyStoreId);
134 if (store === undefined) return undefined;
135 const attrs = {
136 policyStoreId: store.policyStoreId,
137 policyStoreArn: store.arn,
138 };
139 const tags = (store.tags ?? {}) as Record<string, string>;
140 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
141 }),
142
143 // no props trigger replacement — validationMode / description /
144 // deletionProtection are all mutable via updatePolicyStore
145 diff: Effect.fn(function* ({ news }) {
146 if (!isResolved(news)) return undefined;
147 return undefined;
148 }),
149
150 reconcile: Effect.fn(function* ({ id, news, output, session }) {
151 const internalTags = yield* createInternalTags(id);
152 const desiredTags = { ...news.tags, ...internalTags };
153 const validationMode = news.validationMode ?? "OFF";
154
155 // 1. OBSERVE — cloud state is authoritative
156 let store =
157 output?.policyStoreId !== undefined
158 ? yield* observe(output.policyStoreId)
159 : undefined;
160
161 // 2. ENSURE — createPolicyStore returns id + arn directly
162 if (store === undefined) {
163 const created = yield* avp.createPolicyStore({
164 validationSettings: { mode: validationMode },
165 description: news.description,
166 deletionProtection: news.deletionProtection,
167 tags: desiredTags,
168 });
169 store = yield* observe(created.policyStoreId);
170 if (store === undefined) {
171 return {
172 policyStoreId: created.policyStoreId,
173 policyStoreArn: created.arn,
174 };
175 }
176 }
177
178 // 3. SYNC — validation / description / deletion protection
179 const observedMode = store.validationSettings.mode;
180 // description decodes as a Redacted (SensitiveString) — unwrap to
181 // compare against the desired plain-string value
182 const observedDescription =
183 store.description === undefined
184 ? undefined
185 : Redacted.isRedacted(store.description)
186 ? Redacted.value(store.description)
187 : store.description;
188 const observedProtection = store.deletionProtection ?? "DISABLED";
189 const desiredProtection = news.deletionProtection ?? "DISABLED";
190 if (
191 observedMode !== validationMode ||
192 observedDescription !== (news.description ?? undefined) ||
193 observedProtection !== desiredProtection
194 ) {
195 yield* avp.updatePolicyStore({
196 policyStoreId: store.policyStoreId,
197 validationSettings: { mode: validationMode },
198 description: news.description,
199 deletionProtection: news.deletionProtection,
200 });
201 }
202
203 // 3b. SYNC TAGS — diff against OBSERVED cloud tags
204 const observedTags = (store.tags ?? {}) as Record<string, string>;
205 const { upsert, removed } = diffTags(observedTags, desiredTags);
206 if (upsert.length > 0) {
207 yield* avp.tagResource({
208 resourceArn: store.arn,
209 tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
210 });
211 }
212 if (removed.length > 0) {
213 yield* avp.untagResource({
214 resourceArn: store.arn,
215 tagKeys: removed,
216 });
217 }
218
219 yield* session.note(store.policyStoreId);
220 return {
221 policyStoreId: store.policyStoreId,
222 policyStoreArn: store.arn,
223 };
224 }),
225
226 delete: Effect.fn(function* ({ output }) {
227 // deletePolicyStore is idempotent — returns 200 even if the store
228 // does not exist
229 yield* avp.deletePolicyStore({
230 policyStoreId: output.policyStoreId,
231 });
232 }),
233 });
234 }),
235 );