@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,316 @@
1
+ /**
2
+ * The AVP observation, against a mocked client layer.
3
+ *
4
+ * Every scenario drives the real `listPolicies`/`getPolicy` code through an
5
+ * injected {@link AvpHttp}; nothing here touches the network, and nothing stubs
6
+ * the readers themselves. The conformance suite at the bottom is the part that
7
+ * matters most — it runs these same results through core's own
8
+ * `buildChangeSet`, so a well-shaped result that still proposes a spurious
9
+ * `create` fails here rather than in somebody's account.
10
+ */
11
+
12
+ import { describe, expect, it } from "vitest";
13
+ import { describeObservationConformance } from "@intentius/chant-test-utils";
14
+ import { normalizeObservation } from "@intentius/chant/observation";
15
+ import { CEDAR_POLICY_TYPE } from "../serializer";
16
+ import { cedarPlugin } from "../plugin";
17
+ import { describeAvpResources } from "./describe-resources";
18
+ import {
19
+ markedDescription,
20
+ mockAvpTransport,
21
+ statementFor,
22
+ MOCK_ENV,
23
+ type MockStore,
24
+ } from "./testdata/mock-transport";
25
+
26
+ const STORE = "PS-abc123";
27
+
28
+ function entities(names: Record<string, Record<string, unknown>>) {
29
+ return new Map(
30
+ Object.entries(names).map(([name, props]) => [name, { entityType: CEDAR_POLICY_TYPE, props }]),
31
+ );
32
+ }
33
+
34
+ function run(store: MockStore, options: Partial<Parameters<typeof describeAvpResources>[0]> = {}) {
35
+ const { http } = mockAvpTransport(store);
36
+ return describeAvpResources({
37
+ environment: "prod",
38
+ entityNames: ["ownerRead", "requireMfa"],
39
+ entities: entities({ ownerRead: {}, requireMfa: {} }),
40
+ policyStoreId: STORE,
41
+ client: { http, region: "us-west-2" },
42
+ env: MOCK_ENV,
43
+ ...options,
44
+ });
45
+ }
46
+
47
+ /** A store holding both declared policies, both chant-stamped. */
48
+ const healthy: MockStore = {
49
+ policyStoreId: STORE,
50
+ policies: [
51
+ {
52
+ policyId: "p-1",
53
+ statement: statementFor("owner-read"),
54
+ description: markedDescription("owner-read", "Owners read their own documents."),
55
+ lastUpdatedDate: "2026-08-01T00:00:00Z",
56
+ },
57
+ {
58
+ policyId: "p-2",
59
+ statement: statementFor("require-mfa", "forbid (\n principal,\n action,\n resource\n)"),
60
+ description: markedDescription("require-mfa"),
61
+ },
62
+ ],
63
+ };
64
+
65
+ describe("describeAvpResources", () => {
66
+ it("keys live policies by chant entity name, through the @id the serializer assigns", async () => {
67
+ const { resources } = normalizeObservation(await run(healthy));
68
+
69
+ expect(Object.keys(resources).sort()).toEqual(["ownerRead", "requireMfa"]);
70
+ expect(resources.ownerRead.physicalId).toBe("p-1");
71
+ expect(resources.ownerRead.type).toBe(CEDAR_POLICY_TYPE);
72
+ expect(resources.ownerRead.lastUpdated).toBe("2026-08-01T00:00:00Z");
73
+ expect(resources.ownerRead.attributes?.policyStoreId).toBe(STORE);
74
+ });
75
+
76
+ it("honours an explicit annotations.id over the derived one", async () => {
77
+ const store: MockStore = {
78
+ policyStoreId: STORE,
79
+ policies: [{ policyId: "p-9", statement: statementFor("legacy-name"), description: markedDescription("legacy-name") }],
80
+ };
81
+ const { resources } = normalizeObservation(
82
+ await run(store, {
83
+ entityNames: ["ownerRead"],
84
+ entities: entities({ ownerRead: { annotations: { id: "legacy-name" } } }),
85
+ }),
86
+ );
87
+ expect(resources.ownerRead?.physicalId).toBe("p-9");
88
+ });
89
+
90
+ it("says where it looked, including the region and the store", async () => {
91
+ const { queried } = normalizeObservation(await run(healthy));
92
+ expect(queried.ownerRead).toBe(
93
+ `avp://us-west-2/policy-store/${STORE}/policy?@id=owner-read`,
94
+ );
95
+ });
96
+
97
+ it("reports a policy the store does not hold as absent, not as a hole", async () => {
98
+ const partial: MockStore = { ...healthy, policies: [healthy.policies[0]] };
99
+ const { resources, unobserved, queried } = normalizeObservation(await run(partial));
100
+
101
+ expect(Object.keys(resources)).toEqual(["ownerRead"]);
102
+ expect(unobserved.requireMfa).toBeUndefined();
103
+ // Absence is spelled "in neither map", so `queried` is the only record it gets.
104
+ expect(queried.requireMfa).toContain("@id=require-mfa");
105
+ });
106
+
107
+ it("resolves an unmarked policy by reading its statement", async () => {
108
+ const consoleEdited: MockStore = {
109
+ policyStoreId: STORE,
110
+ policies: [
111
+ { policyId: "p-1", statement: statementFor("owner-read") },
112
+ { policyId: "p-2", statement: statementFor("require-mfa") },
113
+ ],
114
+ };
115
+ const { resources } = normalizeObservation(await run(consoleEdited));
116
+
117
+ expect(Object.keys(resources).sort()).toEqual(["ownerRead", "requireMfa"]);
118
+ expect(resources.ownerRead.ownership).toBe("foreign");
119
+ });
120
+
121
+ it("does not pay for a GetPolicy on a policy whose marker already names it", async () => {
122
+ const { http, calls } = mockAvpTransport(healthy);
123
+ await describeAvpResources({
124
+ environment: "prod",
125
+ entityNames: ["ownerRead", "requireMfa"],
126
+ entities: entities({ ownerRead: {}, requireMfa: {} }),
127
+ policyStoreId: STORE,
128
+ client: { http },
129
+ env: MOCK_ENV,
130
+ });
131
+ expect(calls.filter((c) => c.operation === "GetPolicy")).toHaveLength(0);
132
+ });
133
+
134
+ it("paginates the enumeration to exhaustion", async () => {
135
+ const many: MockStore = {
136
+ policyStoreId: STORE,
137
+ pageSize: 1,
138
+ policies: healthy.policies,
139
+ };
140
+ const { resources } = normalizeObservation(await run(many));
141
+ expect(Object.keys(resources).sort()).toEqual(["ownerRead", "requireMfa"]);
142
+ });
143
+
144
+ it("reports a store that does not exist yet as an empty estate", async () => {
145
+ const missing = await run({ ...healthy, policyStoreId: "PS-other" });
146
+ const { resources, unobserved } = normalizeObservation(missing);
147
+ expect(resources).toEqual({});
148
+ expect(unobserved).toEqual({});
149
+ });
150
+
151
+ it("marks every entity not-observed when the enumeration fails on credentials", async () => {
152
+ const denied: MockStore = {
153
+ ...healthy,
154
+ listFails: { code: "AccessDeniedException", message: "not authorized to perform ListPolicies", status: 403 },
155
+ };
156
+ const { unobserved } = normalizeObservation(await run(denied));
157
+
158
+ expect(Object.keys(unobserved).sort()).toEqual(["ownerRead", "requireMfa"]);
159
+ expect(unobserved.ownerRead.reason).toBe("no-credentials");
160
+ });
161
+
162
+ it("distinguishes a throttle from a credential problem", async () => {
163
+ const throttled: MockStore = {
164
+ ...healthy,
165
+ listFails: { code: "ThrottlingException", message: "Rate exceeded", status: 429 },
166
+ };
167
+ const { unobserved } = normalizeObservation(await run(throttled));
168
+ expect(unobserved.ownerRead.reason).toBe("read-failed");
169
+ });
170
+
171
+ it("never throws when there are no credentials at all", async () => {
172
+ const { http } = mockAvpTransport(healthy);
173
+ const result = await describeAvpResources({
174
+ environment: "prod",
175
+ entityNames: ["ownerRead", "requireMfa"],
176
+ entities: entities({ ownerRead: {}, requireMfa: {} }),
177
+ policyStoreId: STORE,
178
+ client: { http },
179
+ env: {},
180
+ });
181
+ const { resources, unobserved } = normalizeObservation(result);
182
+ expect(resources).toEqual({});
183
+ expect(unobserved.ownerRead.reason).toBe("no-credentials");
184
+ expect(unobserved.requireMfa.reason).toBe("no-credentials");
185
+ });
186
+
187
+ it("reports no-binding when nothing names a policy store", async () => {
188
+ const { http } = mockAvpTransport(healthy);
189
+ const result = await describeAvpResources({
190
+ environment: "prod",
191
+ entityNames: ["ownerRead"],
192
+ entities: entities({ ownerRead: {} }),
193
+ client: { http },
194
+ env: MOCK_ENV,
195
+ });
196
+ expect(normalizeObservation(result).unobserved.ownerRead.reason).toBe("no-binding");
197
+ });
198
+
199
+ it("takes the store from an entity prop when the caller names none", async () => {
200
+ const { http } = mockAvpTransport(healthy);
201
+ const result = await describeAvpResources({
202
+ environment: "prod",
203
+ entityNames: ["ownerRead"],
204
+ entities: entities({ ownerRead: { policyStoreId: STORE } }),
205
+ client: { http },
206
+ env: MOCK_ENV,
207
+ });
208
+ expect(Object.keys(normalizeObservation(result).resources)).toEqual(["ownerRead"]);
209
+ });
210
+
211
+ it("takes the store from a per-environment env var", async () => {
212
+ const { http } = mockAvpTransport(healthy);
213
+ const result = await describeAvpResources({
214
+ environment: "prod",
215
+ entityNames: ["ownerRead"],
216
+ entities: entities({ ownerRead: {} }),
217
+ client: { http },
218
+ env: { ...MOCK_ENV, CEDAR_AVP_POLICY_STORE_ID_PROD: STORE, CEDAR_AVP_POLICY_STORE_ID: "PS-wrong" },
219
+ });
220
+ expect(Object.keys(normalizeObservation(result).resources)).toEqual(["ownerRead"]);
221
+ });
222
+
223
+ it("calls a non-policy entity type unsupported, never absent", async () => {
224
+ const { http } = mockAvpTransport(healthy);
225
+ const result = await describeAvpResources({
226
+ environment: "prod",
227
+ entityNames: ["appUser"],
228
+ entities: new Map([["appUser", { entityType: "App::User", props: {} }]]),
229
+ policyStoreId: STORE,
230
+ client: { http },
231
+ env: MOCK_ENV,
232
+ });
233
+ expect(normalizeObservation(result).unobserved.appUser.reason).toBe("unsupported-kind");
234
+ });
235
+
236
+ it("withholds a foreign policy under owned:true rather than calling it absent", async () => {
237
+ const mixed: MockStore = {
238
+ policyStoreId: STORE,
239
+ policies: [
240
+ { policyId: "p-1", statement: statementFor("owner-read"), description: markedDescription("owner-read") },
241
+ { policyId: "p-2", statement: statementFor("require-mfa"), description: "added in the console" },
242
+ ],
243
+ };
244
+ const { resources, unobserved } = normalizeObservation(await run(mixed, { owned: true }));
245
+
246
+ expect(Object.keys(resources)).toEqual(["ownerRead"]);
247
+ expect(resources.ownerRead.ownership).toBe("owned");
248
+ expect(unobserved.requireMfa.reason).toBe("filtered");
249
+ });
250
+
251
+ it("leaves a policy whose GetPolicy failed unmatched, not misattributed", async () => {
252
+ const flaky: MockStore = {
253
+ policyStoreId: STORE,
254
+ policies: [
255
+ { policyId: "p-1", statement: statementFor("owner-read"), getFails: true },
256
+ { policyId: "p-2", statement: statementFor("require-mfa"), description: markedDescription("require-mfa") },
257
+ ],
258
+ };
259
+ const { resources } = normalizeObservation(await run(flaky));
260
+ // p-1's `@id` was unreadable, so ownerRead is absent rather than being
261
+ // matched onto whichever policy happened to be next in the list.
262
+ expect(Object.keys(resources)).toEqual(["requireMfa"]);
263
+ });
264
+ });
265
+
266
+ describeObservationConformance({
267
+ lexicon: "cedar",
268
+ ownershipChannel: cedarPlugin.ownershipChannel,
269
+ scenarios: [
270
+ {
271
+ name: "a healthy policy store",
272
+ declared: ["ownerRead", "requireMfa"],
273
+ expectPresent: ["ownerRead", "requireMfa"],
274
+ run: () => run(healthy),
275
+ },
276
+ {
277
+ name: "a store missing one declared policy",
278
+ declared: ["ownerRead", "requireMfa"],
279
+ expectPresent: ["ownerRead"],
280
+ expectAbsent: ["requireMfa"],
281
+ run: () => run({ ...healthy, policies: [healthy.policies[0]] }),
282
+ },
283
+ {
284
+ name: "an enumeration that fails on credentials",
285
+ declared: ["ownerRead", "requireMfa"],
286
+ expectUnobserved: ["ownerRead", "requireMfa"],
287
+ run: () =>
288
+ run({
289
+ ...healthy,
290
+ listFails: { code: "AccessDeniedException", message: "not authorized", status: 403 },
291
+ }),
292
+ },
293
+ {
294
+ name: "an environment bound to no policy store",
295
+ declared: ["ownerRead", "requireMfa"],
296
+ expectUnobserved: ["ownerRead", "requireMfa"],
297
+ run: () => {
298
+ const { http } = mockAvpTransport(healthy);
299
+ return describeAvpResources({
300
+ environment: "prod",
301
+ entityNames: ["ownerRead", "requireMfa"],
302
+ entities: entities({ ownerRead: {}, requireMfa: {} }),
303
+ client: { http },
304
+ env: MOCK_ENV,
305
+ });
306
+ },
307
+ },
308
+ {
309
+ name: "an owned read on the path that declares a marker channel",
310
+ declared: ["ownerRead", "requireMfa"],
311
+ expectPresent: ["ownerRead", "requireMfa"],
312
+ owned: true,
313
+ run: () => run(healthy, { owned: true }),
314
+ },
315
+ ],
316
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `describeResources()` against an AVP policy store (#1652).
3
+ *
4
+ * The tri-state matters more here than it does for most substrates. A Cedar
5
+ * policy that chant declares and the store does not have is a *missing
6
+ * authorization rule*: reporting it absent proposes a create, which is right.
7
+ * Reporting it absent because the store id was wrong, or because the read
8
+ * failed, proposes creating a permit that already exists — duplicating a grant.
9
+ * So every path that did not actually look says so.
10
+ *
11
+ * Built on core's observer harness (`observeEntities`), which owns the shape:
12
+ * bind-or-not-observe-all with a typed reason, bounded concurrency, a per-entity
13
+ * throw degrading to `read-failed` rather than a silent absence, and collection
14
+ * of the `queried` addresses.
15
+ */
16
+
17
+ import type { ObservationResult, ResourceMetadata } from "@intentius/chant/lexicon";
18
+ import {
19
+ observation,
20
+ observeEntities,
21
+ unobservedAll,
22
+ type DeclaredEntity,
23
+ type EntityObservation,
24
+ type ObserverAdapter,
25
+ type UnobservedReason,
26
+ } from "@intentius/chant/observation";
27
+ import { CEDAR_POLICY_TYPE, resolvePolicyId } from "../serializer";
28
+ import {
29
+ classifyAvpFailure,
30
+ credentialsAvailable,
31
+ policyAddress,
32
+ storeAddress,
33
+ storeDoesNotExist,
34
+ type AvpClientOptions,
35
+ } from "./client";
36
+ import { ownershipFromDescription } from "./ownership";
37
+ import {
38
+ indexByCedarId,
39
+ loadLivePolicies,
40
+ resolvePolicyStoreId,
41
+ type AvpLivePolicy,
42
+ } from "./store";
43
+
44
+ export interface DescribeAvpOptions {
45
+ environment: string;
46
+ entityNames: string[];
47
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
48
+ /** Restrict to policies carrying chant's description marker. */
49
+ owned?: boolean;
50
+ /** Explicit store id; otherwise resolved per {@link resolvePolicyStoreId}. */
51
+ policyStoreId?: string;
52
+ client?: AvpClientOptions;
53
+ /** Environment to read the binding and credentials from. Injectable for tests. */
54
+ env?: Record<string, string | undefined>;
55
+ }
56
+
57
+ /** What `bind()` hands each read: the store, its policies, and the address prefix. */
58
+ interface AvpBinding {
59
+ policyStoreId: string;
60
+ byCedarId: Map<string, AvpLivePolicy>;
61
+ region?: string;
62
+ }
63
+
64
+ /** A bind failure that already knows its verdict. */
65
+ class AvpBindFailure extends Error {
66
+ constructor(
67
+ readonly reason: UnobservedReason,
68
+ readonly detail: string,
69
+ ) {
70
+ super(detail);
71
+ this.name = "AvpBindFailure";
72
+ }
73
+ }
74
+
75
+ function metadataFor(policy: AvpLivePolicy, ownership: "owned" | "foreign"): ResourceMetadata {
76
+ return {
77
+ type: CEDAR_POLICY_TYPE,
78
+ physicalId: policy.policyId,
79
+ // AVP has no lifecycle state on a policy: it exists or it does not. The
80
+ // policy type is the most useful thing to show, and a template-linked
81
+ // policy differing from a static one IS drift worth seeing.
82
+ status: policy.policyType || "STATIC",
83
+ ...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
84
+ ownership,
85
+ attributes: {
86
+ policyStoreId: policy.policyStoreId,
87
+ policyId: policy.policyId,
88
+ policyType: policy.policyType,
89
+ ...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
90
+ ...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
91
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Observe the declared policy set against a live AVP store.
98
+ *
99
+ * Never throws for a transport or binding problem — a throw is the
100
+ * whole-lexicon failure and core would mark every entity `read-failed`, which
101
+ * is a worse-shaped version of what this returns directly with the right
102
+ * reason on it.
103
+ */
104
+ export async function describeAvpResources(options: DescribeAvpOptions): Promise<ObservationResult> {
105
+ const env = options.env ?? process.env;
106
+ const typesByName: Record<string, string> = {};
107
+ for (const name of options.entityNames) {
108
+ typesByName[name] = options.entities.get(name)?.entityType ?? CEDAR_POLICY_TYPE;
109
+ }
110
+
111
+ const policyStoreId = resolvePolicyStoreId({
112
+ environment: options.environment,
113
+ ...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
114
+ entities: options.entities,
115
+ env,
116
+ });
117
+
118
+ if (!policyStoreId) {
119
+ return observation(
120
+ {},
121
+ unobservedAll(
122
+ options.entityNames,
123
+ "no-binding",
124
+ `no AVP policy store for environment "${options.environment}" — set ${"CEDAR_AVP_POLICY_STORE_ID"} or declare policyStoreId on a policy`,
125
+ typesByName,
126
+ ),
127
+ );
128
+ }
129
+
130
+ if (!credentialsAvailable(env)) {
131
+ return observation(
132
+ {},
133
+ unobservedAll(
134
+ options.entityNames,
135
+ "no-credentials",
136
+ "no AWS credentials and no endpoint override — the AVP store was not queried",
137
+ typesByName,
138
+ ),
139
+ );
140
+ }
141
+
142
+ const region = options.client?.region;
143
+ const declared: DeclaredEntity[] = options.entityNames.map((name) => {
144
+ const entity = options.entities.get(name);
145
+ return {
146
+ name,
147
+ type: entity?.entityType ?? CEDAR_POLICY_TYPE,
148
+ props: entity?.props ?? {},
149
+ };
150
+ });
151
+
152
+ const adapter: ObserverAdapter<AvpBinding> = {
153
+ async bind(): Promise<AvpBinding> {
154
+ try {
155
+ const policies = await loadLivePolicies({
156
+ policyStoreId,
157
+ ...(options.client ? { client: options.client } : {}),
158
+ });
159
+ return { policyStoreId, byCedarId: indexByCedarId(policies), ...(region ? { region } : {}) };
160
+ } catch (err) {
161
+ // A store that is not there yet is the pre-first-apply state: nothing
162
+ // is deployed, so every declared policy is genuinely absent and
163
+ // `create` is the right proposal.
164
+ if (storeDoesNotExist(err)) {
165
+ return { policyStoreId, byCedarId: new Map(), ...(region ? { region } : {}) };
166
+ }
167
+ const { reason, detail } = classifyAvpFailure(err);
168
+ throw new AvpBindFailure(reason, `ListPolicies failed for store ${policyStoreId}: ${detail}`);
169
+ }
170
+ },
171
+
172
+ classifyBindFailure(err: unknown) {
173
+ if (err instanceof AvpBindFailure) return { reason: err.reason, detail: err.detail };
174
+ const { reason, detail } = classifyAvpFailure(err);
175
+ return { reason, detail: `${storeAddress(policyStoreId, region)}: ${detail}` };
176
+ },
177
+
178
+ async read(binding: AvpBinding, entity: DeclaredEntity): Promise<EntityObservation> {
179
+ if (entity.type !== CEDAR_POLICY_TYPE) {
180
+ // Cedar declares one deployable kind. Anything else is a schema entity
181
+ // or action constant, which has no AVP counterpart at all — saying so
182
+ // per entity keeps it out of `lifecycle plan` as a create.
183
+ return {
184
+ unobserved: {
185
+ reason: "unsupported-kind",
186
+ detail: `no AVP reader for ${entity.type} — only ${CEDAR_POLICY_TYPE} is deployed to a policy store`,
187
+ },
188
+ };
189
+ }
190
+
191
+ const cedarPolicyId = resolvePolicyId(entity.name, entity.props);
192
+ const queried = policyAddress(binding.policyStoreId, cedarPolicyId, binding.region);
193
+ const policy = binding.byCedarId.get(cedarPolicyId);
194
+
195
+ // The store was enumerated and this `@id` is not in it. That is an
196
+ // absence, and the only shape allowed to become a create.
197
+ if (!policy) return { absent: true, queried };
198
+
199
+ const ownership = ownershipFromDescription(policy.description);
200
+ if (options.owned && ownership !== "owned") {
201
+ return {
202
+ unobserved: {
203
+ reason: "filtered",
204
+ detail: "live policy carries no chant description marker",
205
+ },
206
+ queried,
207
+ };
208
+ }
209
+
210
+ return { present: metadataFor(policy, ownership), queried };
211
+ },
212
+ };
213
+
214
+ return observeEntities(declared, adapter);
215
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkParsePolicySet } from "@cedar-policy/cedar-wasm/nodejs";
3
+ import { build } from "@intentius/chant/build";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ avpPolicyDefinition,
8
+ avpPolicyResource,
9
+ avpPolicySet,
10
+ avpStatement,
11
+ avpStatementJSON,
12
+ } from "./embed";
13
+ import { decodeOwnershipDescription, descriptionIsOwned } from "./ownership";
14
+ import { cedarSerializer, type CedarPolicyProps } from "../serializer";
15
+
16
+ const ownerRead: CedarPolicyProps = {
17
+ effect: "permit",
18
+ principal: { is: "App::User" },
19
+ action: { eq: 'App::Action::"read"' },
20
+ resource: { is: "App::Document" },
21
+ when: ["resource.owner == principal"],
22
+ };
23
+
24
+ describe("the AVP statement", () => {
25
+ it("is Cedar the real parser accepts", () => {
26
+ const statement = avpStatement("ownerRead", ownerRead);
27
+ expect(checkParsePolicySet({ staticPolicies: statement }).type).toBe("success");
28
+ });
29
+
30
+ it("carries the @id a live policy is matched back on", () => {
31
+ expect(avpStatement("ownerRead", ownerRead)).toContain('@id("owner-read")');
32
+ expect(avpStatement("ownerRead", { ...ownerRead, annotations: { id: "explicit" } })).toContain(
33
+ '@id("explicit")',
34
+ );
35
+ expect(avpStatement("ownerRead", ownerRead, { policyId: "override" })).toContain('@id("override")');
36
+ });
37
+
38
+ it("is byte-identical to what the serializer writes to .cedar", async () => {
39
+ const dir = join(dirname(fileURLToPath(import.meta.url)), "../../examples/getting-started/src");
40
+ const result = await build(dir, [cedarSerializer]);
41
+ const output = result.outputs.get("cedar");
42
+ const text = typeof output === "string" ? output : (output?.primary ?? "");
43
+
44
+ // One renderer, not two: every statement the embedding emits appears
45
+ // verbatim in the .cedar file the same build produced.
46
+ for (const statement of Object.values(avpPolicySet(result.entities))) {
47
+ expect(text).toContain(statement.Static.Statement);
48
+ }
49
+ });
50
+
51
+ it("also emits the Cedar JSON form, for evaluators that take JSON", () => {
52
+ const json = avpStatementJSON("ownerRead", ownerRead);
53
+ expect(json.effect).toBe("permit");
54
+ expect(json.annotations?.id).toBe("owner-read");
55
+ // Cedar's own reading, so a condition is an expression tree — not the
56
+ // `{ __expr: "…" }` escape the model-derived encoder used to write.
57
+ expect(JSON.stringify(json)).not.toContain("__expr");
58
+ expect(json.conditions?.[0]?.kind).toBe("when");
59
+ });
60
+
61
+ it("refuses to hand back JSON for a policy Cedar cannot read", () => {
62
+ expect(() => avpStatementJSON("broken", { when: ["this is not an expression"] })).toThrow(
63
+ /did not parse/,
64
+ );
65
+ });
66
+ });
67
+
68
+ describe("the AVP Definition envelope", () => {
69
+ it("is the Static shape AWS::VerifiedPermissions::Policy declares", () => {
70
+ const definition = avpPolicyDefinition("ownerRead", ownerRead);
71
+ expect(Object.keys(definition)).toEqual(["Static"]);
72
+ expect(typeof definition.Static.Statement).toBe("string");
73
+ expect(definition.Static.Description).toBeUndefined();
74
+ });
75
+
76
+ it("stamps the ownership marker into the description when asked", () => {
77
+ const definition = avpPolicyDefinition("ownerRead", ownerRead, {
78
+ description: "Owners read their own documents.",
79
+ ownership: { stack: "authz", env: "prod" },
80
+ });
81
+
82
+ expect(descriptionIsOwned(definition.Static.Description)).toBe(true);
83
+ const decoded = decodeOwnershipDescription(definition.Static.Description);
84
+ expect(decoded.text).toBe("Owners read their own documents.");
85
+ expect(decoded.policyId).toBe("owner-read");
86
+ });
87
+
88
+ it("keeps a plain description untouched when no marker is requested", () => {
89
+ const definition = avpPolicyDefinition("ownerRead", ownerRead, { description: "just prose" });
90
+ expect(definition.Static.Description).toBe("just prose");
91
+ });
92
+
93
+ it("fills both required props of the generated class", () => {
94
+ const resource = avpPolicyResource("ownerRead", ownerRead, "store-123");
95
+ expect(resource.PolicyStoreId).toBe("store-123");
96
+ expect(resource.Definition.Static.Statement).toContain("permit");
97
+ });
98
+ });
99
+
100
+ describe("the whole-set form", () => {
101
+ it("keys definitions by chant entity name", async () => {
102
+ const dir = join(dirname(fileURLToPath(import.meta.url)), "../../examples/getting-started/src");
103
+ const result = await build(dir, [cedarSerializer]);
104
+
105
+ const set = avpPolicySet(result.entities, {
106
+ ownership: { stack: "authz", env: "prod" },
107
+ });
108
+ expect(Object.keys(set).sort()).toEqual(["ownerRead", "requireMfa"]);
109
+ for (const definition of Object.values(set)) {
110
+ expect(descriptionIsOwned(definition.Static.Description)).toBe(true);
111
+ expect(checkParsePolicySet({ staticPolicies: definition.Static.Statement }).type).toBe("success");
112
+ }
113
+ });
114
+ });