@intentius/chant-lexicon-cedar 0.44.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +190 -0
  2. package/dist/avp/ambient.d.ts +54 -0
  3. package/dist/avp/ambient.d.ts.map +1 -0
  4. package/dist/avp/client.d.ts +127 -0
  5. package/dist/avp/client.d.ts.map +1 -0
  6. package/dist/avp/describe-resources.d.ts +42 -0
  7. package/dist/avp/describe-resources.d.ts.map +1 -0
  8. package/dist/avp/embed.d.ts +120 -0
  9. package/dist/avp/embed.d.ts.map +1 -0
  10. package/dist/avp/live-export.d.ts +94 -0
  11. package/dist/avp/live-export.d.ts.map +1 -0
  12. package/dist/avp/ownership.d.ts +97 -0
  13. package/dist/avp/ownership.d.ts.map +1 -0
  14. package/dist/avp/statement.d.ts +22 -0
  15. package/dist/avp/statement.d.ts.map +1 -0
  16. package/dist/avp/store.d.ts +82 -0
  17. package/dist/avp/store.d.ts.map +1 -0
  18. package/dist/avp/testdata/mock-transport.d.ts +55 -0
  19. package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
  20. package/dist/codegen/docs-cli.d.ts +3 -0
  21. package/dist/codegen/docs-cli.d.ts.map +1 -0
  22. package/dist/codegen/docs.d.ts +26 -0
  23. package/dist/codegen/docs.d.ts.map +1 -0
  24. package/dist/codegen/emit.d.ts +85 -0
  25. package/dist/codegen/emit.d.ts.map +1 -0
  26. package/dist/codegen/generate-cli.d.ts +3 -0
  27. package/dist/codegen/generate-cli.d.ts.map +1 -0
  28. package/dist/codegen/generate.d.ts +37 -0
  29. package/dist/codegen/generate.d.ts.map +1 -0
  30. package/dist/codegen/naming.d.ts +48 -0
  31. package/dist/codegen/naming.d.ts.map +1 -0
  32. package/dist/codegen/package.d.ts +10 -0
  33. package/dist/codegen/package.d.ts.map +1 -0
  34. package/dist/composites/deny-by-default-set.d.ts +58 -0
  35. package/dist/composites/deny-by-default-set.d.ts.map +1 -0
  36. package/dist/composites/index.d.ts +12 -0
  37. package/dist/composites/index.d.ts.map +1 -0
  38. package/dist/composites/owner-can-manage.d.ts +48 -0
  39. package/dist/composites/owner-can-manage.d.ts.map +1 -0
  40. package/dist/config.d.ts +102 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/coverage.d.ts +59 -0
  43. package/dist/coverage.d.ts.map +1 -0
  44. package/dist/detect.d.ts +18 -0
  45. package/dist/detect.d.ts.map +1 -0
  46. package/dist/generated/index.d.ts +251 -0
  47. package/dist/generated/index.d.ts.map +1 -0
  48. package/dist/generated/runtime.d.ts +2 -0
  49. package/dist/generated/runtime.d.ts.map +1 -0
  50. package/dist/import/adapter.d.ts +21 -0
  51. package/dist/import/adapter.d.ts.map +1 -0
  52. package/dist/import/clause-text.d.ts +38 -0
  53. package/dist/import/clause-text.d.ts.map +1 -0
  54. package/dist/import/generator.d.ts +53 -0
  55. package/dist/import/generator.d.ts.map +1 -0
  56. package/dist/import/parser.d.ts +57 -0
  57. package/dist/import/parser.d.ts.map +1 -0
  58. package/dist/index.d.ts +34 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/init-templates.d.ts +32 -0
  61. package/dist/init-templates.d.ts.map +1 -0
  62. package/dist/integrity.json +22 -0
  63. package/dist/lint/audit-catalog.d.ts +29 -0
  64. package/dist/lint/audit-catalog.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
  66. package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cedc010.d.ts +17 -0
  68. package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cedc011.d.ts +17 -0
  70. package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cedc012.d.ts +21 -0
  72. package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cedc013.d.ts +20 -0
  74. package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cedc014.d.ts +19 -0
  76. package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cede010.d.ts +34 -0
  78. package/dist/lint/post-synth/cede010.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cede011.d.ts +22 -0
  80. package/dist/lint/post-synth/cede011.d.ts.map +1 -0
  81. package/dist/lint/post-synth/ceds010.d.ts +24 -0
  82. package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
  83. package/dist/lint/post-synth/ceds011.d.ts +19 -0
  84. package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
  85. package/dist/lint/post-synth/ceds012.d.ts +17 -0
  86. package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
  87. package/dist/lint/post-synth/index.d.ts +3 -0
  88. package/dist/lint/post-synth/index.d.ts.map +1 -0
  89. package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
  90. package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
  91. package/dist/lint/rules/index.d.ts +10 -0
  92. package/dist/lint/rules/index.d.ts.map +1 -0
  93. package/dist/lint/rules/policy-shape.d.ts +31 -0
  94. package/dist/lint/rules/policy-shape.d.ts.map +1 -0
  95. package/dist/lsp/completions.d.ts +24 -0
  96. package/dist/lsp/completions.d.ts.map +1 -0
  97. package/dist/lsp/hover.d.ts +11 -0
  98. package/dist/lsp/hover.d.ts.map +1 -0
  99. package/dist/lsp/registry.d.ts +45 -0
  100. package/dist/lsp/registry.d.ts.map +1 -0
  101. package/dist/manifest.json +8 -0
  102. package/dist/mcp/index.d.ts +34 -0
  103. package/dist/mcp/index.d.ts.map +1 -0
  104. package/dist/mcp/policy-coverage.d.ts +81 -0
  105. package/dist/mcp/policy-coverage.d.ts.map +1 -0
  106. package/dist/meta.json +699 -0
  107. package/dist/okf/index.md +37 -0
  108. package/dist/okf/rules/CEDC001.md +11 -0
  109. package/dist/okf/rules/CEDC010.md +11 -0
  110. package/dist/okf/rules/CEDC011.md +15 -0
  111. package/dist/okf/rules/CEDC012.md +11 -0
  112. package/dist/okf/rules/CEDC013.md +15 -0
  113. package/dist/okf/rules/CEDC014.md +15 -0
  114. package/dist/okf/rules/CEDE010.md +15 -0
  115. package/dist/okf/rules/CEDE011.md +15 -0
  116. package/dist/okf/rules/CEDS010.md +15 -0
  117. package/dist/okf/rules/CEDS011.md +11 -0
  118. package/dist/okf/rules/CEDS012.md +15 -0
  119. package/dist/okf/types/AdminAction.md +14 -0
  120. package/dist/okf/types/Application.md +14 -0
  121. package/dist/okf/types/ApproveAction.md +13 -0
  122. package/dist/okf/types/CommentAction.md +14 -0
  123. package/dist/okf/types/CreateAction.md +14 -0
  124. package/dist/okf/types/DeleteAction.md +14 -0
  125. package/dist/okf/types/Document.md +18 -0
  126. package/dist/okf/types/Folder.md +15 -0
  127. package/dist/okf/types/Group.md +14 -0
  128. package/dist/okf/types/ListAction.md +14 -0
  129. package/dist/okf/types/Policy.md +29 -0
  130. package/dist/okf/types/ReadAction.md +14 -0
  131. package/dist/okf/types/ServiceAccount.md +15 -0
  132. package/dist/okf/types/ShareAction.md +14 -0
  133. package/dist/okf/types/Team.md +14 -0
  134. package/dist/okf/types/User.md +18 -0
  135. package/dist/okf/types/WriteAction.md +14 -0
  136. package/dist/package-cli.d.ts +3 -0
  137. package/dist/package-cli.d.ts.map +1 -0
  138. package/dist/plugin.d.ts +8 -0
  139. package/dist/plugin.d.ts.map +1 -0
  140. package/dist/rules/cedar-helpers.ts +209 -0
  141. package/dist/rules/cedc010.ts +80 -0
  142. package/dist/rules/cedc011.ts +47 -0
  143. package/dist/rules/cedc012.ts +64 -0
  144. package/dist/rules/cedc013.ts +68 -0
  145. package/dist/rules/cedc014.ts +73 -0
  146. package/dist/rules/cede010.ts +89 -0
  147. package/dist/rules/cede011.ts +58 -0
  148. package/dist/rules/ceds010.ts +57 -0
  149. package/dist/rules/ceds011.ts +42 -0
  150. package/dist/rules/ceds012.ts +50 -0
  151. package/dist/rules/policy-shape.ts +148 -0
  152. package/dist/rules/wasm-helpers.ts +315 -0
  153. package/dist/serializer.d.ts +138 -0
  154. package/dist/serializer.d.ts.map +1 -0
  155. package/dist/spec/fetch.d.ts +71 -0
  156. package/dist/spec/fetch.d.ts.map +1 -0
  157. package/dist/spec/parse.d.ts +113 -0
  158. package/dist/spec/parse.d.ts.map +1 -0
  159. package/dist/spec/pin.d.ts +116 -0
  160. package/dist/spec/pin.d.ts.map +1 -0
  161. package/dist/spec/pinned-names.json +18 -0
  162. package/dist/spec/wasm.d.ts +143 -0
  163. package/dist/spec/wasm.d.ts.map +1 -0
  164. package/dist/types/index.d.ts +219 -0
  165. package/dist/validate-cli.d.ts +3 -0
  166. package/dist/validate-cli.d.ts.map +1 -0
  167. package/dist/validate.d.ts +25 -0
  168. package/dist/validate.d.ts.map +1 -0
  169. package/package.json +75 -0
  170. package/src/avp/OWNERSHIP.md +125 -0
  171. package/src/avp/ambient.test.ts +113 -0
  172. package/src/avp/ambient.ts +124 -0
  173. package/src/avp/client.ts +310 -0
  174. package/src/avp/describe-resources.test.ts +316 -0
  175. package/src/avp/describe-resources.ts +215 -0
  176. package/src/avp/embed.test.ts +114 -0
  177. package/src/avp/embed.ts +190 -0
  178. package/src/avp/live-export.test.ts +232 -0
  179. package/src/avp/live-export.ts +185 -0
  180. package/src/avp/ownership.test.ts +101 -0
  181. package/src/avp/ownership.ts +170 -0
  182. package/src/avp/statement.ts +50 -0
  183. package/src/avp/store.ts +152 -0
  184. package/src/avp/testdata/mock-transport.ts +147 -0
  185. package/src/codegen/docs-cli.ts +7 -0
  186. package/src/codegen/docs.ts +873 -0
  187. package/src/codegen/emit.ts +496 -0
  188. package/src/codegen/generate-cli.ts +18 -0
  189. package/src/codegen/generate.test.ts +128 -0
  190. package/src/codegen/generate.ts +123 -0
  191. package/src/codegen/naming.ts +101 -0
  192. package/src/codegen/package.ts +52 -0
  193. package/src/composites/composites.test.ts +206 -0
  194. package/src/composites/deny-by-default-set.ts +98 -0
  195. package/src/composites/index.ts +13 -0
  196. package/src/composites/owner-can-manage.ts +80 -0
  197. package/src/config-namespace.test.ts +44 -0
  198. package/src/config.test.ts +82 -0
  199. package/src/config.ts +119 -0
  200. package/src/coverage.test.ts +61 -0
  201. package/src/coverage.ts +166 -0
  202. package/src/detect.test.ts +64 -0
  203. package/src/detect.ts +59 -0
  204. package/src/generated/index.d.ts +219 -0
  205. package/src/generated/index.ts +279 -0
  206. package/src/generated/lexicon-cedar.json +699 -0
  207. package/src/generated/runtime.ts +2 -0
  208. package/src/import/adapter.ts +63 -0
  209. package/src/import/clause-text.ts +178 -0
  210. package/src/import/generator.test.ts +133 -0
  211. package/src/import/generator.ts +219 -0
  212. package/src/import/parser.test.ts +265 -0
  213. package/src/import/parser.ts +352 -0
  214. package/src/import/roundtrip.test.ts +127 -0
  215. package/src/import/testdata/full.cedar +36 -0
  216. package/src/import/testdata/full.cedar.json +238 -0
  217. package/src/import/testdata/realistic.cedar +35 -0
  218. package/src/import/testdata/simple.cedar +6 -0
  219. package/src/index.ts +94 -0
  220. package/src/init-templates.test.ts +158 -0
  221. package/src/init-templates.ts +358 -0
  222. package/src/lint/audit-catalog.ts +130 -0
  223. package/src/lint/post-synth/cedar-helpers.ts +209 -0
  224. package/src/lint/post-synth/cedc010.ts +80 -0
  225. package/src/lint/post-synth/cedc011.ts +47 -0
  226. package/src/lint/post-synth/cedc012.ts +64 -0
  227. package/src/lint/post-synth/cedc013.ts +68 -0
  228. package/src/lint/post-synth/cedc014.ts +73 -0
  229. package/src/lint/post-synth/cede010.ts +89 -0
  230. package/src/lint/post-synth/cede011.ts +58 -0
  231. package/src/lint/post-synth/ceds010.ts +57 -0
  232. package/src/lint/post-synth/ceds011.ts +42 -0
  233. package/src/lint/post-synth/ceds012.ts +50 -0
  234. package/src/lint/post-synth/index.ts +25 -0
  235. package/src/lint/post-synth/post-synth.test.ts +474 -0
  236. package/src/lint/post-synth/wasm-helpers.ts +315 -0
  237. package/src/lint/rules/index.ts +12 -0
  238. package/src/lint/rules/policy-shape.test.ts +90 -0
  239. package/src/lint/rules/policy-shape.ts +148 -0
  240. package/src/lsp/completions.test.ts +111 -0
  241. package/src/lsp/completions.ts +55 -0
  242. package/src/lsp/hover.test.ts +82 -0
  243. package/src/lsp/hover.ts +75 -0
  244. package/src/lsp/registry.ts +74 -0
  245. package/src/mcp/index.ts +103 -0
  246. package/src/mcp/policy-coverage.test.ts +176 -0
  247. package/src/mcp/policy-coverage.ts +205 -0
  248. package/src/package-cli.ts +22 -0
  249. package/src/plugin.test.ts +115 -0
  250. package/src/plugin.ts +312 -0
  251. package/src/serializer.test.ts +502 -0
  252. package/src/serializer.ts +335 -0
  253. package/src/skills/chant-cedar-authoring.md +180 -0
  254. package/src/skills/chant-cedar-avp-embedding.md +125 -0
  255. package/src/skills/chant-cedar-meta-policy.md +119 -0
  256. package/src/spec/default-schema.cedarschema +108 -0
  257. package/src/spec/fetch.ts +107 -0
  258. package/src/spec/parse.test.ts +123 -0
  259. package/src/spec/parse.ts +253 -0
  260. package/src/spec/pin.test.ts +88 -0
  261. package/src/spec/pin.ts +243 -0
  262. package/src/spec/pinned-names.json +18 -0
  263. package/src/spec/wasm.ts +283 -0
  264. package/src/validate-cli.ts +8 -0
  265. package/src/validate.ts +85 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Typed embedding of a cedar policy into `AWS::VerifiedPermissions::Policy`
3
+ * (#1652).
4
+ *
5
+ * The aws lexicon keeps the deployment vehicle — its generated
6
+ * `VerifiedPermissionsPolicy` takes a required `PolicyStoreId` and a required
7
+ * `Definition`, and the `Definition.Static.Statement` inside it is an opaque
8
+ * string. Opaque is the problem the whole cedar lexicon exists to remove: today
9
+ * a team writes that string with `${}` templating and finds out at deploy time
10
+ * whether the entity type exists.
11
+ *
12
+ * This module closes the loop without joining the two lexicons together. It
13
+ * emits the *string* and the *JSON envelope* AVP expects and stops there:
14
+ * nothing here imports `@intentius/chant-lexicon-aws`, and nothing here needs
15
+ * to. A project that has both installed writes
16
+ *
17
+ * ```ts
18
+ * new VerifiedPermissionsPolicy({
19
+ * PolicyStoreId: store.ref(),
20
+ * Definition: avpPolicyDefinition("ownerRead", ownerReadProps),
21
+ * });
22
+ * ```
23
+ *
24
+ * and the statement is the same text `chant build` writes to `.cedar`, rendered
25
+ * by the same renderer, with `@id` intact.
26
+ *
27
+ * ### Why no dependency, and why the example uses plain objects
28
+ *
29
+ * A cedar → aws dependency would invert the epic's decision that Cedar is
30
+ * vendor-neutral and AVP is one of several evaluators, and it would make the
31
+ * cedar lexicon unbuildable without the aws one. The seam is therefore the
32
+ * data shape, which is stable CloudFormation, and
33
+ * `examples/avp-embedding/` demonstrates the pairing with a plain-object stand-in
34
+ * for `VerifiedPermissionsPolicy` — the shipped examples build with the cedar
35
+ * serializer alone, so a real cross-lexicon example could not run in this
36
+ * repo's example harness at all. The stand-in carries the exact prop names the
37
+ * generated class declares (`PolicyStoreId`, `Definition`, `Name`), so the
38
+ * substitution is a one-line edit in a real project.
39
+ */
40
+
41
+ import type { Declarable } from "@intentius/chant/declarable";
42
+ import type { OwnershipMarker } from "@intentius/chant/ownership";
43
+ import {
44
+ cedarPolicyRecords,
45
+ renderPolicyText,
46
+ resolvePolicyId,
47
+ type CedarPolicyProps,
48
+ } from "../serializer";
49
+ import { policyToJson, type PolicyJson } from "../spec/wasm";
50
+ import { encodeOwnershipDescription } from "./ownership";
51
+
52
+ /** `Definition.Static` — the static-policy half of the AVP definition union. */
53
+ export interface AvpStaticDefinition {
54
+ Statement: string;
55
+ Description?: string;
56
+ }
57
+
58
+ /** The `Definition` property of `AWS::VerifiedPermissions::Policy`. */
59
+ export interface AvpPolicyDefinition {
60
+ Static: AvpStaticDefinition;
61
+ }
62
+
63
+ /** The props of `AWS::VerifiedPermissions::Policy` this module can fill. */
64
+ export interface AvpPolicyResource {
65
+ PolicyStoreId: string;
66
+ Definition: AvpPolicyDefinition;
67
+ }
68
+
69
+ export interface AvpEmbedOptions {
70
+ /**
71
+ * Ownership marker to stamp into the description — the per-policy channel
72
+ * (see ./ownership.ts). Supply it and `describeResources`/`exportResources`
73
+ * can tell this policy from one somebody added in the console.
74
+ */
75
+ ownership?: OwnershipMarker;
76
+ /** The author's own description, kept ahead of the marker. */
77
+ description?: string;
78
+ /** Override the Cedar id. Defaults to the serializer's own derivation. */
79
+ policyId?: string;
80
+ }
81
+
82
+ function propsRecord(props: CedarPolicyProps | Record<string, unknown>): Record<string, unknown> {
83
+ return props as Record<string, unknown>;
84
+ }
85
+
86
+ /**
87
+ * The Cedar policy text for one policy — the exact string
88
+ * `Definition.Static.Statement` wants.
89
+ *
90
+ * `name` is the chant entity name; the `@id` annotation the statement carries
91
+ * is derived from it the same way the serializer derives it, which is what lets
92
+ * the live observation match a policy in the store back to the entity that
93
+ * declared it.
94
+ */
95
+ export function avpStatement(
96
+ name: string,
97
+ props: CedarPolicyProps | Record<string, unknown>,
98
+ options: AvpEmbedOptions = {},
99
+ ): string {
100
+ const record = propsRecord(props);
101
+ return renderPolicyText(options.policyId ?? resolvePolicyId(name, record), record);
102
+ }
103
+
104
+ /**
105
+ * The Cedar JSON policy format for one policy — for an evaluator that takes
106
+ * JSON rather than text (`cedar-agent`, an embedded `cedar-wasm`).
107
+ *
108
+ * Not what AVP's `Definition` wants; AVP takes the text. Emitted here so the
109
+ * seam is documented in both directions rather than only the AWS one.
110
+ *
111
+ * Built the way the serializer's JSON leg is built (#1653): render the text,
112
+ * then hand it to `cedar-wasm`, so what comes back is Cedar's own reading
113
+ * rather than a second, worse encoder. Throws on text Cedar refuses — this is
114
+ * an authoring-time call, and a policy the module cannot read is a defect the
115
+ * caller wants to hear about rather than a `undefined` to thread through.
116
+ */
117
+ export function avpStatementJSON(
118
+ name: string,
119
+ props: CedarPolicyProps | Record<string, unknown>,
120
+ options: AvpEmbedOptions = {},
121
+ ): PolicyJson {
122
+ const statement = avpStatement(name, props, options);
123
+ const converted = policyToJson(statement);
124
+ if (!converted.ok) {
125
+ throw new Error(`cedar: ${name} did not parse, so no JSON form could be produced — ${converted.error}`);
126
+ }
127
+ return converted.value;
128
+ }
129
+
130
+ /**
131
+ * The `Definition` property of `AWS::VerifiedPermissions::Policy`.
132
+ *
133
+ * With `options.ownership` set, the description carries chant's marker — the
134
+ * only per-policy ownership channel AVP has, because policies are not taggable.
135
+ */
136
+ export function avpPolicyDefinition(
137
+ name: string,
138
+ props: CedarPolicyProps | Record<string, unknown>,
139
+ options: AvpEmbedOptions = {},
140
+ ): AvpPolicyDefinition {
141
+ const record = propsRecord(props);
142
+ const policyId = options.policyId ?? resolvePolicyId(name, record);
143
+ const description = options.ownership
144
+ ? encodeOwnershipDescription(options.description, options.ownership, policyId)
145
+ : options.description?.trim();
146
+
147
+ return {
148
+ Static: {
149
+ Statement: renderPolicyText(policyId, record),
150
+ ...(description ? { Description: description } : {}),
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Both required props of `AWS::VerifiedPermissions::Policy`, ready to spread
157
+ * into the generated class.
158
+ *
159
+ * `PolicyStoreId` is a string here rather than an AttrRef because this module
160
+ * does not know the aws lexicon's reference types. A project passes
161
+ * `store.ref()` in place of the literal and TypeScript is satisfied by the
162
+ * generated class's own prop type, not by this one.
163
+ */
164
+ export function avpPolicyResource(
165
+ name: string,
166
+ props: CedarPolicyProps | Record<string, unknown>,
167
+ policyStoreId: string,
168
+ options: AvpEmbedOptions = {},
169
+ ): AvpPolicyResource {
170
+ return { PolicyStoreId: policyStoreId, Definition: avpPolicyDefinition(name, props, options) };
171
+ }
172
+
173
+ /**
174
+ * Every `Cedar::Policy` in a build, rendered as AVP definitions and keyed by
175
+ * chant entity name.
176
+ *
177
+ * The whole-set form: one call turns a policy set into the definitions a stack
178
+ * of `VerifiedPermissionsPolicy` declarations needs, with references between
179
+ * declared entities already walked (that is what `cedarPolicyRecords` does).
180
+ */
181
+ export function avpPolicySet(
182
+ entities: Map<string, Declarable>,
183
+ options: AvpEmbedOptions = {},
184
+ ): Record<string, AvpPolicyDefinition> {
185
+ const out: Record<string, AvpPolicyDefinition> = {};
186
+ for (const { name, id, props } of cedarPolicyRecords(entities)) {
187
+ out[name] = avpPolicyDefinition(name, props, { ...options, policyId: id });
188
+ }
189
+ return out;
190
+ }
@@ -0,0 +1,232 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkParsePolicySet } from "@cedar-policy/cedar-wasm/nodejs";
3
+ import { CEDAR_POLICY_TYPE, type CedarPolicyProps } from "../serializer";
4
+ import { avpStatement } from "./embed";
5
+ import { exportAvpResources, policyToResourceIR } from "./live-export";
6
+ import {
7
+ markedDescription,
8
+ mockAvpTransport,
9
+ MOCK_ENV,
10
+ type MockStore,
11
+ } from "./testdata/mock-transport";
12
+
13
+ const STORE = "PS-abc123";
14
+
15
+ const OWNER_READ = `@id("owner-read")
16
+ @doc("Owners always read their own documents.")
17
+ permit (
18
+ principal is App::User,
19
+ action in [App::Action::"read", App::Action::"list"],
20
+ resource is App::Document
21
+ )
22
+ when { resource.owner == principal }
23
+ unless { context.mfa == false };`;
24
+
25
+ const RESTRICT_DELETE = `@id("restrict-delete")
26
+ forbid (
27
+ principal,
28
+ action == App::Action::"delete",
29
+ resource is App::Document
30
+ )
31
+ when { resource.classification == "confidential" };`;
32
+
33
+ const store: MockStore = {
34
+ policyStoreId: STORE,
35
+ policies: [
36
+ {
37
+ policyId: "p-2",
38
+ statement: RESTRICT_DELETE,
39
+ description: "added in the console",
40
+ createdDate: "2026-01-01T00:00:00Z",
41
+ },
42
+ {
43
+ policyId: "p-1",
44
+ statement: OWNER_READ,
45
+ description: markedDescription("owner-read", "Owners read their own."),
46
+ lastUpdatedDate: "2026-08-01T00:00:00Z",
47
+ },
48
+ ],
49
+ };
50
+
51
+ function run(options: Partial<Parameters<typeof exportAvpResources>[0]> = {}) {
52
+ const { http } = mockAvpTransport(store);
53
+ return exportAvpResources({
54
+ environment: "prod",
55
+ policyStoreId: STORE,
56
+ client: { http },
57
+ env: MOCK_ENV,
58
+ ...options,
59
+ });
60
+ }
61
+
62
+ /** A live policy through `policyToResourceIR`, which is the import parser's reading of it. */
63
+ function irFor(statement: string, verbatim = false) {
64
+ return policyToResourceIR(
65
+ {
66
+ policyId: "p-x",
67
+ policyStoreId: STORE,
68
+ policyType: "STATIC",
69
+ statement,
70
+ authoredDescription: "",
71
+ marked: false,
72
+ },
73
+ { verbatim },
74
+ );
75
+ }
76
+
77
+ describe("reading a live statement back", () => {
78
+ it("goes through the import parser, so the id is the Cedar policy id", () => {
79
+ // Not `ownerRead`: turning a policy id into a TypeScript identifier is
80
+ // CedarGenerator's job downstream, and doing it here too applies it twice.
81
+ expect(irFor(OWNER_READ)?.logicalId).toBe("owner-read");
82
+ expect(irFor(OWNER_READ)?.type).toBe(CEDAR_POLICY_TYPE);
83
+ });
84
+
85
+ it("recovers effect and all three scopes", () => {
86
+ const props = irFor(OWNER_READ)!.properties;
87
+ expect(props.effect).toBe("permit");
88
+ expect(props.principal).toEqual({ is: "App::User" });
89
+ expect(props.action).toEqual({ in: ['App::Action::"read"', 'App::Action::"list"'] });
90
+ expect(props.resource).toEqual({ is: "App::Document" });
91
+ expect(irFor(RESTRICT_DELETE)!.properties.action).toEqual({ eq: 'App::Action::"delete"' });
92
+ });
93
+
94
+ it("keeps the author's own condition text rather than a re-printed tree", () => {
95
+ const props = irFor(OWNER_READ)!.properties;
96
+ expect(props.when).toEqual(["resource.owner == principal"]);
97
+ expect(props.unless).toEqual(["context.mfa == false"]);
98
+ });
99
+
100
+ it("does not let a record literal or a quoted brace end a clause early", () => {
101
+ const statement = `@id("tricky")
102
+ permit (principal, action, resource)
103
+ when { context == { mfa: true, note: "}" } };`;
104
+ expect(irFor(statement)!.properties.when).toEqual(['context == { mfa: true, note: "}" }']);
105
+ });
106
+
107
+ it("files a slotted policy as a template, which is Cedar's call and not this reader's", () => {
108
+ const templated = `@id("linked")\npermit (principal == ?principal, action, resource);`;
109
+ expect(irFor(templated)?.type).toBe("Cedar::Template");
110
+ });
111
+
112
+ it("refuses text the parser rejects rather than half-exporting it", () => {
113
+ expect(irFor("this is not cedar")).toBeUndefined();
114
+ expect(irFor("")).toBeUndefined();
115
+ });
116
+
117
+ it("round-trips: live statement → props → statement the parser still accepts", () => {
118
+ const props = irFor(OWNER_READ)!.properties as CedarPolicyProps;
119
+ const rerendered = avpStatement("ownerRead", props);
120
+
121
+ expect(checkParsePolicySet({ staticPolicies: rerendered }).type).toBe("success");
122
+ expect(irFor(rerendered)!.properties).toEqual(props);
123
+ });
124
+ });
125
+
126
+ describe("exportAvpResources", () => {
127
+ it("returns import IR keyed by the Cedar policy id", async () => {
128
+ const ir = await run();
129
+ expect(ir.resources.map((r) => r.logicalId)).toEqual(["owner-read", "restrict-delete"]);
130
+ expect(ir.resources[0].type).toBe(CEDAR_POLICY_TYPE);
131
+ expect(ir.parameters).toEqual([]);
132
+ expect(ir.metadata?.policyStoreId).toBe(STORE);
133
+ });
134
+
135
+ it("strips the server-written record by default", async () => {
136
+ const ir = await run();
137
+ const owner = ir.resources.find((r) => r.logicalId === "owner-read")!;
138
+
139
+ expect(owner.properties.avp).toBeUndefined();
140
+ expect(Object.keys(owner.properties).sort()).toEqual([
141
+ "action",
142
+ "annotations",
143
+ "effect",
144
+ "principal",
145
+ "resource",
146
+ "unless",
147
+ "when",
148
+ ]);
149
+ // The marker in particular: baking one environment's ownership stamp into
150
+ // regenerated source would make every environment claim to be that one.
151
+ expect(JSON.stringify(owner.properties)).not.toContain("chant:managed-by");
152
+ });
153
+
154
+ it("keeps the whole server record under verbatim", async () => {
155
+ const ir = await run({ verbatim: true });
156
+ const owner = ir.resources.find((r) => r.logicalId === "owner-read")!;
157
+ const avp = owner.properties.avp as Record<string, unknown>;
158
+
159
+ expect(avp.policyId).toBe("p-1");
160
+ expect(avp.policyStoreId).toBe(STORE);
161
+ expect(avp.lastUpdatedDate).toBe("2026-08-01T00:00:00Z");
162
+ expect(String(avp.description)).toContain("chant:managed-by");
163
+ expect(avp.statement).toBe(OWNER_READ);
164
+ });
165
+
166
+ it("filters to chant-owned policies on owned:true", async () => {
167
+ const ir = await run({ owned: true });
168
+ expect(ir.resources.map((r) => r.logicalId)).toEqual(["owner-read"]);
169
+ });
170
+
171
+ it("applies a name selector", async () => {
172
+ expect((await run({ selector: { name: "restrict-delete" } })).resources.map((r) => r.logicalId)).toEqual([
173
+ "restrict-delete",
174
+ ]);
175
+ });
176
+
177
+ it("applies a type selector, and yields nothing for another lexicon's type", async () => {
178
+ expect((await run({ selector: { type: CEDAR_POLICY_TYPE } })).resources).toHaveLength(2);
179
+ expect((await run({ selector: { type: "AWS::S3::Bucket" } })).resources).toEqual([]);
180
+ });
181
+
182
+ it("orders resources deterministically, whatever order the store enumerates", async () => {
183
+ const ids = (await run()).resources.map((r) => r.logicalId);
184
+ expect(ids).toEqual([...ids].sort());
185
+ });
186
+
187
+ it("exports a store that does not exist yet as an empty template", async () => {
188
+ const ir = await run({ policyStoreId: "PS-other" });
189
+ expect(ir.resources).toEqual([]);
190
+ });
191
+
192
+ it("throws rather than reporting an empty estate when the read fails", async () => {
193
+ const { http } = mockAvpTransport({
194
+ ...store,
195
+ listFails: { code: "ThrottlingException", message: "Rate exceeded", status: 429 },
196
+ });
197
+ await expect(
198
+ exportAvpResources({ environment: "prod", policyStoreId: STORE, client: { http }, env: MOCK_ENV }),
199
+ ).rejects.toThrow(/Rate exceeded/);
200
+ });
201
+
202
+ it("throws when nothing binds the environment to a store", async () => {
203
+ const { http } = mockAvpTransport(store);
204
+ await expect(
205
+ exportAvpResources({ environment: "prod", client: { http }, env: MOCK_ENV }),
206
+ ).rejects.toThrow(/no AVP policy store/);
207
+ });
208
+
209
+ it("throws when there are no credentials", async () => {
210
+ const { http } = mockAvpTransport(store);
211
+ await expect(
212
+ exportAvpResources({ environment: "prod", policyStoreId: STORE, client: { http }, env: {} }),
213
+ ).rejects.toThrow(/no AWS credentials/);
214
+ });
215
+
216
+ it("drops a policy whose statement could not be read rather than exporting a hole", async () => {
217
+ const flaky = mockAvpTransport({
218
+ policyStoreId: STORE,
219
+ policies: [
220
+ { policyId: "p-1", statement: OWNER_READ, getFails: true },
221
+ { policyId: "p-2", statement: RESTRICT_DELETE },
222
+ ],
223
+ });
224
+ const ir = await exportAvpResources({
225
+ environment: "prod",
226
+ policyStoreId: STORE,
227
+ client: { http: flaky.http },
228
+ env: MOCK_ENV,
229
+ });
230
+ expect(ir.resources.map((r) => r.logicalId)).toEqual(["restrict-delete"]);
231
+ });
232
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `exportResources()` against an AVP policy store (#1652).
3
+ *
4
+ * `describeResources` returns scrubbed metadata for diffing — you cannot
5
+ * regenerate a policy from "STATIC, last updated Tuesday". This is the other
6
+ * half: the live statement, parsed back into the props a `Cedar::Policy`
7
+ * declaration carries, as `TemplateIR` so it feeds `templateGenerator()`
8
+ * unchanged. The scrubbing boundary stays single-purpose; nothing here is
9
+ * reachable from the lifecycle paths, which consume the lexicon through
10
+ * `ObservationLexicon` and cannot see this method at all.
11
+ *
12
+ * ## The parse is the import parser's
13
+ *
14
+ * The live-export guide's third step is "map to `TemplateIR` by reusing your
15
+ * import parser", and #1653 shipped one. So a live statement goes through
16
+ * `CedarTemplateParser` — the same code path `chant import policies.cedar`
17
+ * takes — rather than a second reader that would have to re-derive scope
18
+ * mapping, clause-text recovery and template detection, and would drift the
19
+ * first time either side changed. This module's job is the AVP half: bind the
20
+ * store, enumerate it, decide what is server-written, and apply the filters.
21
+ *
22
+ * ## What is server-written here
23
+ *
24
+ * The live-export guide says strip to the declared shape by default and keep
25
+ * the rest under `verbatim`. For AVP the split is unusually clean, because the
26
+ * only thing a user authors is the statement. Everything else on the record —
27
+ * `policyId` (service-assigned), `policyStoreId`, `policyType`, `createdDate`,
28
+ * `lastUpdatedDate`, and the derived `principal`/`resource` summaries AVP
29
+ * computes from the statement — is server-written and stripped by default.
30
+ * `Description` is stripped too: chant's marker lives in it, so exporting it
31
+ * verbatim into regenerated source would bake one environment's ownership stamp
32
+ * into the code that produces every environment.
33
+ *
34
+ * `verbatim: true` puts all of it back under an `avp` key, raw description
35
+ * included.
36
+ */
37
+
38
+ import type { ExportedTemplate, ResourceSelector } from "@intentius/chant/lexicon";
39
+ import type { ResourceIR } from "@intentius/chant/import/parser";
40
+ import { CedarTemplateParser } from "../import/adapter";
41
+ import { credentialsAvailable, storeDoesNotExist, type AvpClientOptions } from "./client";
42
+ import { descriptionIsOwned } from "./ownership";
43
+ import { loadLivePolicies, resolvePolicyStoreId, type AvpLivePolicy } from "./store";
44
+
45
+ export interface ExportAvpOptions {
46
+ environment: string;
47
+ selector?: ResourceSelector;
48
+ /** Restrict to policies carrying chant's description marker. */
49
+ owned?: boolean;
50
+ /** Keep the server-written AVP record alongside the authored props. */
51
+ verbatim?: boolean;
52
+ policyStoreId?: string;
53
+ entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
54
+ client?: AvpClientOptions;
55
+ env?: Record<string, string | undefined>;
56
+ }
57
+
58
+ /** The server-written AVP record, kept only under `verbatim`. */
59
+ export interface AvpRecord {
60
+ policyId: string;
61
+ policyStoreId: string;
62
+ policyType: string;
63
+ description?: string;
64
+ createdDate?: string;
65
+ lastUpdatedDate?: string;
66
+ statement: string;
67
+ }
68
+
69
+ /**
70
+ * One live policy as import IR, or `undefined` when its statement could not be
71
+ * read or parsed.
72
+ *
73
+ * The parse is `CedarTemplateParser`'s — one policy is a policy set of one, so
74
+ * the same reader `chant import` uses answers here, including whether Cedar
75
+ * read it as a static policy or as a template (`Cedar::Policy` vs
76
+ * `Cedar::Template`, decided by whether it carries a slot). The `logicalId` it
77
+ * assigns is the Cedar policy id; turning that into a TypeScript identifier is
78
+ * `CedarGenerator`'s job downstream, and doing it here too would apply the
79
+ * transform twice.
80
+ *
81
+ * Dropping an unparseable policy is deliberate. A regenerated policy set that
82
+ * silently lost a `forbid` reads as a working export and is an authorization
83
+ * hole; a missing entry is visible.
84
+ */
85
+ export function policyToResourceIR(
86
+ policy: AvpLivePolicy,
87
+ options: { verbatim?: boolean } = {},
88
+ ): ResourceIR | undefined {
89
+ if (!policy.statement) return undefined;
90
+
91
+ let parsed;
92
+ try {
93
+ parsed = new CedarTemplateParser().parse(policy.statement);
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ const resource = parsed.resources[0];
98
+ if (!resource) return undefined;
99
+
100
+ const properties: Record<string, unknown> = { ...resource.properties };
101
+
102
+ if (options.verbatim) {
103
+ const record: AvpRecord = {
104
+ policyId: policy.policyId,
105
+ policyStoreId: policy.policyStoreId,
106
+ policyType: policy.policyType,
107
+ ...(policy.description !== undefined ? { description: policy.description } : {}),
108
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
109
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
110
+ statement: policy.statement,
111
+ };
112
+ properties.avp = record;
113
+ }
114
+
115
+ return {
116
+ ...resource,
117
+ properties,
118
+ metadata: { ...resource.metadata, avpPolicyId: policy.policyId },
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Read a live policy store as full-fidelity import IR.
124
+ *
125
+ * Unlike the observation, this throws when it cannot read: `chant import` has
126
+ * no tri-state to degrade into, and an empty template returned from a failed
127
+ * read would generate a source tree that deletes every policy. The one
128
+ * exception is a store that does not exist, which is an honest empty estate.
129
+ */
130
+ export async function exportAvpResources(options: ExportAvpOptions): Promise<ExportedTemplate> {
131
+ const env = options.env ?? process.env;
132
+ const policyStoreId = resolvePolicyStoreId({
133
+ environment: options.environment,
134
+ ...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
135
+ ...(options.entities ? { entities: options.entities } : {}),
136
+ env,
137
+ });
138
+
139
+ if (!policyStoreId) {
140
+ throw new Error(
141
+ `no AVP policy store for environment "${options.environment}" — set CEDAR_AVP_POLICY_STORE_ID or pass policyStoreId`,
142
+ );
143
+ }
144
+ if (!credentialsAvailable(env)) {
145
+ throw new Error("no AWS credentials and no endpoint override — cannot export from AVP");
146
+ }
147
+
148
+ let policies: AvpLivePolicy[];
149
+ try {
150
+ policies = await loadLivePolicies({
151
+ policyStoreId,
152
+ ...(options.client ? { client: options.client } : {}),
153
+ withStatements: true,
154
+ });
155
+ } catch (err) {
156
+ if (storeDoesNotExist(err)) policies = [];
157
+ else throw err;
158
+ }
159
+
160
+ const resources: ResourceIR[] = [];
161
+ for (const policy of policies) {
162
+ // Ownership is read on this path, which is what lets it be declared in
163
+ // `ownershipChannel.reads` (chant #1348).
164
+ if (options.owned && !descriptionIsOwned(policy.description)) continue;
165
+
166
+ const ir = policyToResourceIR(policy, { ...(options.verbatim ? { verbatim: true } : {}) });
167
+ if (!ir) continue;
168
+ // The type is compared against what the parser decided, not a constant:
169
+ // a policy carrying a `?principal`/`?resource` slot comes back as
170
+ // `Cedar::Template`, and `--type Cedar::Policy` should not sweep it up.
171
+ if (options.selector?.type !== undefined && options.selector.type !== ir.type) continue;
172
+ if (options.selector?.name !== undefined && ir.logicalId !== options.selector.name) continue;
173
+ resources.push(ir);
174
+ }
175
+
176
+ // Stable order: the store's enumeration order is not a contract, and an
177
+ // import that reshuffles the file on every run is unusable as a diff.
178
+ resources.sort((a, b) => a.logicalId.localeCompare(b.logicalId));
179
+
180
+ return {
181
+ resources,
182
+ parameters: [],
183
+ metadata: { lexicon: "cedar", policyStoreId },
184
+ };
185
+ }