@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,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { hasOwnershipMarker, readOwnership } from "@intentius/chant/ownership";
3
+ import {
4
+ AVP_DESCRIPTION_MAX,
5
+ AVP_OWNERSHIP_KEYS,
6
+ AVP_POLICY_ID_KEY,
7
+ decodeOwnershipDescription,
8
+ descriptionIsOwned,
9
+ encodeOwnershipDescription,
10
+ ownershipFromDescription,
11
+ ownershipFromStoreTags,
12
+ storeOwnershipTags,
13
+ } from "./ownership";
14
+
15
+ describe("the AVP description marker", () => {
16
+ it("keeps the author's text ahead of the marker", () => {
17
+ const encoded = encodeOwnershipDescription("Owners read their own documents.", {
18
+ stack: "authz",
19
+ env: "prod",
20
+ });
21
+ expect(encoded.startsWith("Owners read their own documents. [chant:")).toBe(true);
22
+
23
+ const decoded = decodeOwnershipDescription(encoded);
24
+ expect(decoded.text).toBe("Owners read their own documents.");
25
+ expect(decoded.marked).toBe(true);
26
+ });
27
+
28
+ it("round-trips the stack and env through core's own ownership helpers", () => {
29
+ const encoded = encodeOwnershipDescription(undefined, { stack: "authz", env: "prod" });
30
+ const { tags } = decodeOwnershipDescription(encoded);
31
+
32
+ expect(hasOwnershipMarker(tags, AVP_OWNERSHIP_KEYS)).toBe(true);
33
+ expect(readOwnership(tags, AVP_OWNERSHIP_KEYS)).toEqual({ stack: "authz", env: "prod" });
34
+ });
35
+
36
+ it("carries the cedar policy id, and does not let it count as an ownership claim", () => {
37
+ const encoded = encodeOwnershipDescription("", { stack: "authz" }, "owner-read");
38
+ expect(decodeOwnershipDescription(encoded).policyId).toBe("owner-read");
39
+
40
+ // A marker segment carrying only the id is not ownership.
41
+ const idOnly = decodeOwnershipDescription(`[${AVP_POLICY_ID_KEY}=owner-read]`);
42
+ expect(hasOwnershipMarker(idOnly.tags, AVP_OWNERSHIP_KEYS)).toBe(false);
43
+ expect(idOnly.policyId).toBe("owner-read");
44
+ });
45
+
46
+ it("survives values holding spaces and brackets", () => {
47
+ const encoded = encodeOwnershipDescription("desc", { stack: "my stack", env: "pr[7]" }, "a b");
48
+ const decoded = decodeOwnershipDescription(encoded);
49
+
50
+ expect(decoded.text).toBe("desc");
51
+ expect(readOwnership(decoded.tags, AVP_OWNERSHIP_KEYS)).toEqual({
52
+ stack: "my stack",
53
+ env: "pr[7]",
54
+ });
55
+ expect(decoded.policyId).toBe("a b");
56
+ });
57
+
58
+ it("truncates the prose rather than the marker at AVP's 150-character cap", () => {
59
+ const encoded = encodeOwnershipDescription("x".repeat(400), { stack: "authz", env: "prod" }, "p");
60
+
61
+ expect(encoded.length).toBeLessThanOrEqual(AVP_DESCRIPTION_MAX);
62
+ expect(descriptionIsOwned(encoded)).toBe(true);
63
+ expect(decodeOwnershipDescription(encoded).policyId).toBe("p");
64
+ });
65
+
66
+ it("keeps the marker whole when there is no room for prose at all", () => {
67
+ const encoded = encodeOwnershipDescription("x".repeat(400), {
68
+ stack: "s".repeat(60),
69
+ env: "e".repeat(60),
70
+ });
71
+ expect(descriptionIsOwned(encoded)).toBe(true);
72
+ });
73
+
74
+ it("reads an unmarked description as foreign, not as an error", () => {
75
+ expect(ownershipFromDescription("added by hand in the console")).toBe("foreign");
76
+ expect(ownershipFromDescription(undefined)).toBe("foreign");
77
+ expect(decodeOwnershipDescription("added by hand").text).toBe("added by hand");
78
+ });
79
+
80
+ it("degrades a corrupt marker entry to foreign rather than throwing", () => {
81
+ // A truncated percent-escape: the entry is dropped, and a foreign policy is
82
+ // never deleted, so the failure lands on the safe side.
83
+ const decoded = decodeOwnershipDescription("[chant:managed-by=%E0%A4 chant:stack=authz]");
84
+ expect(decoded.marked).toBe(true);
85
+ expect(hasOwnershipMarker(decoded.tags, AVP_OWNERSHIP_KEYS)).toBe(false);
86
+ });
87
+
88
+ it("classifies a marked description as owned", () => {
89
+ const encoded = encodeOwnershipDescription("d", { stack: "authz", env: "prod" }, "id");
90
+ expect(ownershipFromDescription(encoded)).toBe("owned");
91
+ });
92
+ });
93
+
94
+ describe("the store-level channel", () => {
95
+ it("stamps and reads the same keys as the per-policy channel", () => {
96
+ const tags = storeOwnershipTags({ stack: "authz", env: "prod" });
97
+ expect(tags[AVP_OWNERSHIP_KEYS.managedBy]).toBe("chant");
98
+ expect(ownershipFromStoreTags(tags)).toBe("owned");
99
+ expect(ownershipFromStoreTags({ Team: "platform" })).toBe("foreign");
100
+ });
101
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * The ownership channel for Amazon Verified Permissions (#1652).
3
+ *
4
+ * AVP is the one target in chant where the obvious channel does not exist. A
5
+ * policy *store* is taggable — `TagResource` takes the same key/value map every
6
+ * other AWS resource does. An individual policy is not: `CreatePolicy` accepts
7
+ * a policy store id, a definition, and a client token, and there is no tag
8
+ * surface on it at all. So the store-level tags can say "this store is chant's"
9
+ * and nothing native can say "this policy is chant's" — which is the granularity
10
+ * `delete` needs, because a store holds policies from more than one source the
11
+ * moment somebody uses the console.
12
+ *
13
+ * The one durable, writable, readable per-policy field AVP has is
14
+ * `Definition.Static.Description`. That is the channel: chant's marker is
15
+ * encoded into a trailing bracketed segment of the description, ahead of which
16
+ * the author's own text is preserved. It is read back on `describeResources`
17
+ * and on `exportResources`, which is exactly what `ownershipChannel.reads`
18
+ * declares (chant #1348) — no third path claims a verdict it cannot resolve.
19
+ *
20
+ * See ./OWNERSHIP.md for the design record, including what this channel costs
21
+ * and the two alternatives that were rejected.
22
+ */
23
+
24
+ import type { ChannelKeys, OwnershipMarker } from "@intentius/chant/ownership";
25
+ import { classifyOwnership, hasOwnershipMarker, ownershipEntries } from "@intentius/chant/ownership";
26
+
27
+ /**
28
+ * The marker keys, shared by both AVP channels.
29
+ *
30
+ * The same three names serve the store's tag map and the per-policy description
31
+ * marker, because `ownershipChannel` declares one {@link ChannelKeys} and a
32
+ * lexicon whose two channels disagreed on key names would be two conventions
33
+ * wearing one declaration. AWS tag keys permit `:`, and the description
34
+ * encoding below quotes its values, so neither channel constrains the other.
35
+ */
36
+ export const AVP_OWNERSHIP_KEYS: ChannelKeys = {
37
+ managedBy: "chant:managed-by",
38
+ stack: "chant:stack",
39
+ env: "chant:env",
40
+ };
41
+
42
+ /**
43
+ * The Cedar policy id this AVP policy carries, recorded beside the marker.
44
+ *
45
+ * Deliberately *not* part of {@link AVP_OWNERSHIP_KEYS}: it is not an ownership
46
+ * claim and `hasOwnershipMarker` must not consult it. It is here because
47
+ * `ListPolicies` returns descriptions but not statements, so without it every
48
+ * live policy needs a second `GetPolicy` round trip just to learn which chant
49
+ * entity it belongs to.
50
+ */
51
+ export const AVP_POLICY_ID_KEY = "chant:policy-id";
52
+
53
+ /**
54
+ * AVP caps `Definition.Static.Description` at 150 characters.
55
+ *
56
+ * The marker is what makes a policy deletable, so when the author's own text
57
+ * plus the marker exceed the cap, the *text* is truncated and the marker is
58
+ * kept whole. Silently dropping the marker would turn an owned policy into a
59
+ * foreign one, and a foreign policy is never deleted — the estate would grow
60
+ * undeletable policies with no error anywhere.
61
+ */
62
+ export const AVP_DESCRIPTION_MAX = 150;
63
+
64
+ /** What {@link decodeOwnershipDescription} recovers from a live description. */
65
+ export interface DecodedDescription {
66
+ /** The author's own description, marker segment removed. */
67
+ text: string;
68
+ /** Marker entries, keyed by {@link AVP_OWNERSHIP_KEYS} names, for the core ownership helpers. */
69
+ tags: Record<string, string>;
70
+ /** The Cedar policy id the marker recorded, when it carried one. */
71
+ policyId?: string;
72
+ /** Whether a marker segment was present at all. */
73
+ marked: boolean;
74
+ }
75
+
76
+ const MARKER_SEGMENT = /\s*\[(chant:[^\]]*)\]\s*$/;
77
+
78
+ /**
79
+ * Encode the marker into a description.
80
+ *
81
+ * Values are percent-encoded, so a stack named `my stack` or an id holding a
82
+ * `]` round-trips instead of corrupting the segment. Reversed by
83
+ * {@link decodeOwnershipDescription}.
84
+ */
85
+ export function encodeOwnershipDescription(
86
+ text: string | undefined,
87
+ marker: OwnershipMarker,
88
+ policyId?: string,
89
+ ): string {
90
+ const entries = ownershipEntries(AVP_OWNERSHIP_KEYS, marker);
91
+ if (policyId) entries[AVP_POLICY_ID_KEY] = policyId;
92
+
93
+ const segment = `[${Object.entries(entries)
94
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
95
+ .join(" ")}]`;
96
+
97
+ const authored = (text ?? "").trim();
98
+ if (authored.length === 0) return segment;
99
+
100
+ // The marker survives; the prose is what gives way (see AVP_DESCRIPTION_MAX).
101
+ const room = AVP_DESCRIPTION_MAX - segment.length - 1;
102
+ if (room <= 0) return segment;
103
+ return `${authored.slice(0, room).trimEnd()} ${segment}`;
104
+ }
105
+
106
+ /** Recover the author's text and chant's marker from a live description. */
107
+ export function decodeOwnershipDescription(description: string | undefined): DecodedDescription {
108
+ if (!description) return { text: "", tags: {}, marked: false };
109
+
110
+ const match = MARKER_SEGMENT.exec(description);
111
+ if (!match) return { text: description.trim(), tags: {}, marked: false };
112
+
113
+ const tags: Record<string, string> = {};
114
+ for (const pair of match[1].split(/\s+/)) {
115
+ if (pair.length === 0) continue;
116
+ const eq = pair.indexOf("=");
117
+ if (eq <= 0) continue;
118
+ try {
119
+ tags[pair.slice(0, eq)] = decodeURIComponent(pair.slice(eq + 1));
120
+ } catch {
121
+ // A malformed escape is a corrupt marker, not a crash. The entry is
122
+ // dropped, which at worst downgrades the verdict to `foreign` — and a
123
+ // foreign policy is never deleted, so the failure direction is the safe
124
+ // one.
125
+ continue;
126
+ }
127
+ }
128
+
129
+ const policyId = tags[AVP_POLICY_ID_KEY];
130
+ return {
131
+ text: description.slice(0, match.index).trim(),
132
+ tags,
133
+ ...(policyId ? { policyId } : {}),
134
+ marked: true,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * The per-policy verdict, read off a live description.
140
+ *
141
+ * Two-valued on purpose: this function is only ever called where the
142
+ * description was actually read, and `unknown` is reserved for the paths that
143
+ * could not read it at all (the contract in core's `ownership.ts`).
144
+ */
145
+ export function ownershipFromDescription(description: string | undefined): "owned" | "foreign" {
146
+ return classifyOwnership(decodeOwnershipDescription(description).tags, AVP_OWNERSHIP_KEYS);
147
+ }
148
+
149
+ /** True when a live description carries chant's marker. */
150
+ export function descriptionIsOwned(description: string | undefined): boolean {
151
+ return hasOwnershipMarker(decodeOwnershipDescription(description).tags, AVP_OWNERSHIP_KEYS);
152
+ }
153
+
154
+ /**
155
+ * The store-level verdict, read off a policy store's tag map.
156
+ *
157
+ * The coarse channel: it answers "is this store chant's" and says nothing about
158
+ * any individual policy in it. Kept separate from
159
+ * {@link ownershipFromDescription} so a caller cannot accidentally treat a
160
+ * store's tags as a policy's marker — the mistake the whole design exists to
161
+ * avoid.
162
+ */
163
+ export function ownershipFromStoreTags(tags: Record<string, unknown> | undefined): "owned" | "foreign" {
164
+ return classifyOwnership(tags, AVP_OWNERSHIP_KEYS);
165
+ }
166
+
167
+ /** The tag entries to stamp onto a policy store. */
168
+ export function storeOwnershipTags(marker: OwnershipMarker): Record<string, string> {
169
+ return ownershipEntries(AVP_OWNERSHIP_KEYS, marker);
170
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The two text-level facts the observation needs from a live Cedar statement
3
+ * (#1652): which `@id` it carries — the link back to a chant entity — and
4
+ * whether it permits or forbids.
5
+ *
6
+ * Both by regex, with no parser behind them, and that is the point. Reading a
7
+ * policy *properly* is `src/import/parser.ts`'s job and it goes through
8
+ * `cedar-wasm`, which is a 12 MB module; the observation path must not load it,
9
+ * because `plugin.ts` imports `ambientKinds()` eagerly and every `chant`
10
+ * invocation would pay for it. Live export, which is allowed to be slow and
11
+ * needs real fidelity, uses the import parser instead of anything here.
12
+ *
13
+ * Neither function is a judgement. `observeAmbient` reports the effect and
14
+ * stops; whether an ambient `permit` is a standing grant worth acting on is the
15
+ * consumer's call.
16
+ */
17
+
18
+ import type { CedarEffect } from "../serializer";
19
+
20
+ /** The `@id("…")` a chant-emitted statement always carries, or undefined. */
21
+ export function policyIdFromStatement(statement: string): string | undefined {
22
+ const match = /@id\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)/.exec(statement);
23
+ if (!match) return undefined;
24
+ return unescapeCedarString(match[1]);
25
+ }
26
+
27
+ /** `permit` or `forbid`, scanned past any annotations. */
28
+ export function effectFromStatement(statement: string): CedarEffect | undefined {
29
+ const match = /(?:^|\n)\s*(permit|forbid)\s*\(/.exec(stripAnnotations(statement));
30
+ return match ? (match[1] as CedarEffect) : undefined;
31
+ }
32
+
33
+ function stripAnnotations(statement: string): string {
34
+ return `\n${statement.replace(/@[A-Za-z_][A-Za-z0-9_]*\s*\(\s*"(?:[^"\\]|\\.)*"\s*\)/g, "\n")}`;
35
+ }
36
+
37
+ function unescapeCedarString(value: string): string {
38
+ return value.replace(/\\(.)/g, (_, ch: string) => {
39
+ switch (ch) {
40
+ case "n":
41
+ return "\n";
42
+ case "r":
43
+ return "\r";
44
+ case "t":
45
+ return "\t";
46
+ default:
47
+ return ch;
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Binding a chant environment to an AVP policy store, and reading it (#1652).
3
+ *
4
+ * Everything that talks to a store needs the same two things first: which store
5
+ * this environment means, and the store's policies indexed by the Cedar id a
6
+ * chant entity resolves to. Both live here so `describeResources`,
7
+ * `observeAmbient` and `exportResources` cannot disagree about either.
8
+ */
9
+
10
+ import { boundedConcurrently } from "@intentius/chant/observation";
11
+ import {
12
+ getPolicy,
13
+ listPolicies,
14
+ type AvpClientOptions,
15
+ type AvpPolicySummary,
16
+ } from "./client";
17
+ import { decodeOwnershipDescription } from "./ownership";
18
+ import { policyIdFromStatement } from "./statement";
19
+
20
+ /** The env var an environment's policy store is named in when config does not name it. */
21
+ export const AVP_POLICY_STORE_ENV = "CEDAR_AVP_POLICY_STORE_ID";
22
+
23
+ /** Per-environment override: `CEDAR_AVP_POLICY_STORE_ID_PROD`. */
24
+ export function policyStoreEnvKey(environment: string): string {
25
+ return `${AVP_POLICY_STORE_ENV}_${environment.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
26
+ }
27
+
28
+ export interface StoreBindingOptions {
29
+ environment: string;
30
+ /** Explicit store id from the caller — wins over everything. */
31
+ policyStoreId?: string;
32
+ /** Declared entities, read for a `policyStoreId` prop (the entity-prop pass-through). */
33
+ entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
34
+ env?: Record<string, string | undefined>;
35
+ }
36
+
37
+ /**
38
+ * Which policy store this environment means.
39
+ *
40
+ * Resolution order, most specific first:
41
+ *
42
+ * 1. an explicit `policyStoreId` from the caller;
43
+ * 2. a `policyStoreId` prop on a declared policy — the entity-prop
44
+ * pass-through the observation guide recommends for finding the cloud-side
45
+ * identifier, and the form a project uses when it declares the store in the
46
+ * same build;
47
+ * 3. `CEDAR_AVP_POLICY_STORE_ID_<ENV>`, then `CEDAR_AVP_POLICY_STORE_ID`.
48
+ *
49
+ * `undefined` means no binding, and every caller turns that into `no-binding`
50
+ * rather than an empty read. An empty read would say the store holds nothing,
51
+ * which is a claim about an estate nobody looked at.
52
+ */
53
+ export function resolvePolicyStoreId(options: StoreBindingOptions): string | undefined {
54
+ if (options.policyStoreId) return options.policyStoreId;
55
+
56
+ for (const [, entity] of options.entities ?? new Map()) {
57
+ const declared = (entity.props as { policyStoreId?: unknown }).policyStoreId;
58
+ if (typeof declared === "string" && declared.length > 0) return declared;
59
+ }
60
+
61
+ const env = options.env ?? process.env;
62
+ const scoped = env[policyStoreEnvKey(options.environment)];
63
+ if (scoped) return scoped;
64
+ return env[AVP_POLICY_STORE_ENV] || undefined;
65
+ }
66
+
67
+ /** One live policy, with whatever chant could learn about which entity it belongs to. */
68
+ export interface AvpLivePolicy extends AvpPolicySummary {
69
+ /** The Cedar `@id` — the link back to a chant entity. Undefined when unrecoverable. */
70
+ cedarPolicyId?: string;
71
+ /** The policy text, when it was fetched (see `withStatements`). */
72
+ statement?: string;
73
+ /** The author's description, marker segment removed. */
74
+ authoredDescription: string;
75
+ /** Whether the description carried chant's ownership marker. */
76
+ marked: boolean;
77
+ }
78
+
79
+ export interface LoadPoliciesOptions {
80
+ policyStoreId: string;
81
+ client?: AvpClientOptions;
82
+ /**
83
+ * Fetch each policy's statement.
84
+ *
85
+ * The thin observation does not need one: a marked policy's description
86
+ * already names its Cedar id, and `describeResources` returns scrubbed
87
+ * metadata rather than config. Ambient discovery and live export both do, so
88
+ * they ask for them.
89
+ */
90
+ withStatements?: boolean;
91
+ }
92
+
93
+ /**
94
+ * Every policy in the store, each resolved to its Cedar id where possible.
95
+ *
96
+ * The marker is the cheap path: `ListPolicies` returns descriptions, so a
97
+ * chant-stamped policy is identified in the enumeration itself. Anything
98
+ * unmarked — a console edit, another tool's policy — costs one `GetPolicy` to
99
+ * read the `@id` out of its statement, run through the shared bounded pool so a
100
+ * store of two hundred policies is not two hundred serial round trips.
101
+ *
102
+ * A per-policy `GetPolicy` failure leaves that policy without a statement and
103
+ * without a Cedar id rather than sinking the enumeration: the caller then treats
104
+ * the entity it would have matched as not-observed, which is the honest verdict.
105
+ */
106
+ export async function loadLivePolicies(options: LoadPoliciesOptions): Promise<AvpLivePolicy[]> {
107
+ const summaries = await listPolicies(options.policyStoreId, options.client);
108
+
109
+ const live: AvpLivePolicy[] = summaries.map((summary) => {
110
+ const decoded = decodeOwnershipDescription(summary.description);
111
+ return {
112
+ ...summary,
113
+ ...(decoded.policyId ? { cedarPolicyId: decoded.policyId } : {}),
114
+ authoredDescription: decoded.text,
115
+ marked: decoded.marked,
116
+ };
117
+ });
118
+
119
+ const needsStatement = live.filter(
120
+ (policy) => options.withStatements || policy.cedarPolicyId === undefined,
121
+ );
122
+
123
+ await boundedConcurrently(needsStatement, async (policy) => {
124
+ try {
125
+ const detail = await getPolicy(options.policyStoreId, policy.policyId, options.client);
126
+ policy.statement = detail.statement;
127
+ if (policy.cedarPolicyId === undefined) {
128
+ const fromStatement = policyIdFromStatement(detail.statement);
129
+ if (fromStatement) policy.cedarPolicyId = fromStatement;
130
+ }
131
+ } catch {
132
+ // One unreadable policy is not a verdict about the store. It stays in the
133
+ // list without a statement, so anything that depended on reading it
134
+ // reports a hole instead of an absence.
135
+ }
136
+ });
137
+
138
+ return live;
139
+ }
140
+
141
+ /** Index live policies by the Cedar id a chant entity resolves to. */
142
+ export function indexByCedarId(policies: readonly AvpLivePolicy[]): Map<string, AvpLivePolicy> {
143
+ const index = new Map<string, AvpLivePolicy>();
144
+ for (const policy of policies) {
145
+ if (policy.cedarPolicyId === undefined) continue;
146
+ // First wins: two policies claiming one id is a live-side collision, and
147
+ // silently preferring the later one would make the observation depend on
148
+ // enumeration order.
149
+ if (!index.has(policy.cedarPolicyId)) index.set(policy.cedarPolicyId, policy);
150
+ }
151
+ return index;
152
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A fake AVP wire, for the tests of every reader in ../ .
3
+ *
4
+ * Mocks the *client layer*, never the network: it implements {@link AvpHttp},
5
+ * the same injection point `lexicons/aws/src/api/read-client.ts` exposes, so a
6
+ * test drives real `avpCall` / `listPolicies` / `getPolicy` code — the
7
+ * pagination loop, the `__type` error mapping, the description decoding — with
8
+ * only the socket replaced.
9
+ */
10
+
11
+ import type { AvpHttp } from "../client";
12
+ import { encodeOwnershipDescription } from "../ownership";
13
+
14
+ /** One policy the fake store holds. */
15
+ export interface MockPolicy {
16
+ policyId: string;
17
+ statement: string;
18
+ description?: string;
19
+ policyType?: string;
20
+ createdDate?: string;
21
+ lastUpdatedDate?: string;
22
+ /** Make `GetPolicy` fail for this one, to exercise a per-policy hole. */
23
+ getFails?: boolean;
24
+ }
25
+
26
+ export interface MockStore {
27
+ policyStoreId: string;
28
+ policies: MockPolicy[];
29
+ /** Page size, so the pagination loop is actually exercised. */
30
+ pageSize?: number;
31
+ /** Fail `ListPolicies` with this AWS error code. */
32
+ listFails?: { code: string; message: string; status?: number };
33
+ tags?: Record<string, string>;
34
+ arn?: string;
35
+ }
36
+
37
+ /** A chant-stamped description for a policy the fake store should read as owned. */
38
+ export function markedDescription(
39
+ policyId: string,
40
+ text = "",
41
+ marker: { stack: string; env?: string } = { stack: "authz", env: "prod" },
42
+ ): string {
43
+ return encodeOwnershipDescription(text, marker, policyId);
44
+ }
45
+
46
+ /** A statement carrying the `@id` a chant entity resolves to. */
47
+ export function statementFor(policyId: string, body = "permit (\n principal,\n action,\n resource\n)"): string {
48
+ return `@id("${policyId}")\n${body};`;
49
+ }
50
+
51
+ export interface MockTransport {
52
+ http: AvpHttp;
53
+ /** Every operation issued, in order — for asserting round-trip counts. */
54
+ calls: Array<{ operation: string; payload: Record<string, unknown> }>;
55
+ }
56
+
57
+ /** Build an {@link AvpHttp} over one fake store. */
58
+ export function mockAvpTransport(store: MockStore): MockTransport {
59
+ const calls: Array<{ operation: string; payload: Record<string, unknown> }> = [];
60
+ const pageSize = store.pageSize ?? 50;
61
+
62
+ const error = (type: string, message: string, status = 400) => ({
63
+ status,
64
+ text: JSON.stringify({ __type: `com.amazonaws.verifiedpermissions#${type}`, message }),
65
+ });
66
+
67
+ const definitionOf = (policy: MockPolicy, withStatement: boolean) => ({
68
+ static: {
69
+ ...(policy.description !== undefined ? { description: policy.description } : {}),
70
+ ...(withStatement ? { statement: policy.statement } : {}),
71
+ },
72
+ });
73
+
74
+ const http: AvpHttp = async (_url, init) => {
75
+ const operation = (init.headers["x-amz-target"] ?? "").split(".").pop() ?? "";
76
+ const payload = JSON.parse(init.body) as Record<string, unknown>;
77
+ calls.push({ operation, payload });
78
+
79
+ if (payload.policyStoreId !== undefined && payload.policyStoreId !== store.policyStoreId) {
80
+ return error("ResourceNotFoundException", `No policy store: ${String(payload.policyStoreId)}`, 404);
81
+ }
82
+
83
+ switch (operation) {
84
+ case "ListPolicies": {
85
+ if (store.listFails) {
86
+ return error(store.listFails.code, store.listFails.message, store.listFails.status ?? 400);
87
+ }
88
+ const from = typeof payload.nextToken === "string" ? Number(payload.nextToken) : 0;
89
+ const page = store.policies.slice(from, from + pageSize);
90
+ const next = from + pageSize < store.policies.length ? String(from + pageSize) : undefined;
91
+ return {
92
+ status: 200,
93
+ text: JSON.stringify({
94
+ policies: page.map((policy) => ({
95
+ policyStoreId: store.policyStoreId,
96
+ policyId: policy.policyId,
97
+ policyType: policy.policyType ?? "STATIC",
98
+ definition: definitionOf(policy, false),
99
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
100
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
101
+ })),
102
+ ...(next ? { nextToken: next } : {}),
103
+ }),
104
+ };
105
+ }
106
+
107
+ case "GetPolicy": {
108
+ const policy = store.policies.find((p) => p.policyId === payload.policyId);
109
+ if (!policy) return error("ResourceNotFoundException", "No such policy", 404);
110
+ if (policy.getFails) return error("ThrottlingException", "Rate exceeded", 429);
111
+ return {
112
+ status: 200,
113
+ text: JSON.stringify({
114
+ policyStoreId: store.policyStoreId,
115
+ policyId: policy.policyId,
116
+ policyType: policy.policyType ?? "STATIC",
117
+ definition: definitionOf(policy, true),
118
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
119
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
120
+ }),
121
+ };
122
+ }
123
+
124
+ case "GetPolicyStore":
125
+ return {
126
+ status: 200,
127
+ text: JSON.stringify({
128
+ policyStoreId: store.policyStoreId,
129
+ arn: store.arn ?? `arn:aws:verifiedpermissions::111122223333:policy-store/${store.policyStoreId}`,
130
+ }),
131
+ };
132
+
133
+ case "ListTagsForResource":
134
+ return { status: 200, text: JSON.stringify({ tags: store.tags ?? {} }) };
135
+
136
+ default:
137
+ return error("UnknownOperationException", `no mock for ${operation}`, 400);
138
+ }
139
+ };
140
+
141
+ return { http, calls };
142
+ }
143
+
144
+ /** The env a reader needs to consider itself credentialed against a fake endpoint. */
145
+ export const MOCK_ENV: Record<string, string | undefined> = {
146
+ AWS_ENDPOINT_URL: "http://localhost:4566",
147
+ };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Thin entry point for `npm run docs` in lexicon-cedar.
4
+ */
5
+ import { generateDocs } from "./docs";
6
+
7
+ await generateDocs({ verbose: true });