@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,124 @@
1
+ /**
2
+ * Policies that are simply *there* (#1652, chant #1278).
3
+ *
4
+ * For most lexicons ambient discovery is housekeeping — an unattached security
5
+ * group, an orphaned volume. For an authorization store it is a security
6
+ * finding. A policy in the store that no chant entity declares is a grant
7
+ * nobody in the source tree can see: it was added in the console, or by a
8
+ * previous tool, or by a teammate's script, and it is being evaluated on every
9
+ * request. `describeResources` structurally cannot report it, because it
10
+ * resolves outward from what was declared and this is precisely what was not.
11
+ *
12
+ * The reader reports the policy and its statement. It does not decide that an
13
+ * ambient `permit` is dangerous — that is a conclusion, and putting conclusions
14
+ * in observations is the mistake chant #1271 undid. What it does carry is the
15
+ * `effect`, because "which ambient policies are permits" is the first question
16
+ * anyone asks and re-parsing the statement to answer it is work every consumer
17
+ * would repeat.
18
+ */
19
+
20
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
21
+ import { CEDAR_POLICY_TYPE } from "../serializer";
22
+ import { credentialsAvailable, type AvpClientOptions } from "./client";
23
+ import { ownershipFromDescription } from "./ownership";
24
+ import { loadLivePolicies, resolvePolicyStoreId } from "./store";
25
+ import { effectFromStatement } from "./statement";
26
+
27
+ /**
28
+ * The kinds this lexicon can enumerate beyond the declared estate.
29
+ *
30
+ * One, and it is the only kind cedar deploys. Declared separately from the
31
+ * reader so `chant search` can say `--ambient` is relevant to a policy query
32
+ * without paying for a scan to find out.
33
+ */
34
+ export const AVP_AMBIENT_KINDS: readonly string[] = [CEDAR_POLICY_TYPE];
35
+
36
+ export interface ObserveAvpAmbientOptions {
37
+ environment: string;
38
+ /** Entity types the project declares — the bound on what to enumerate. */
39
+ kinds: string[];
40
+ /** Already-observed managed resources, to exclude. */
41
+ observed: Record<string, ResourceMetadata>;
42
+ /** Cedar ids the project declares, so a declared-but-unobserved policy is not called ambient. */
43
+ declaredPolicyIds?: Iterable<string>;
44
+ policyStoreId?: string;
45
+ entities?: Map<string, { entityType: string; props: Record<string, unknown> }>;
46
+ client?: AvpClientOptions;
47
+ env?: Record<string, string | undefined>;
48
+ }
49
+
50
+ /**
51
+ * Policies in the store that nothing declares and nothing already observed.
52
+ *
53
+ * Best-effort by contract: ambient discovery is additive, and a failure here
54
+ * must not sink a managed observation that already succeeded, so the whole scan
55
+ * degrades to `{}` rather than throwing. The managed answer is complete without
56
+ * any of this.
57
+ */
58
+ export async function observeAvpAmbient(
59
+ options: ObserveAvpAmbientOptions,
60
+ ): Promise<Record<string, ResourceMetadata>> {
61
+ if (!options.kinds.includes(CEDAR_POLICY_TYPE)) return {};
62
+
63
+ const env = options.env ?? process.env;
64
+ const policyStoreId = resolvePolicyStoreId({
65
+ environment: options.environment,
66
+ ...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
67
+ ...(options.entities ? { entities: options.entities } : {}),
68
+ env,
69
+ });
70
+ if (!policyStoreId || !credentialsAvailable(env)) return {};
71
+
72
+ let policies;
73
+ try {
74
+ policies = await loadLivePolicies({
75
+ policyStoreId,
76
+ ...(options.client ? { client: options.client } : {}),
77
+ withStatements: true,
78
+ });
79
+ } catch {
80
+ return {};
81
+ }
82
+
83
+ // Two exclusions, because "managed" has two spellings here: the AVP policy id
84
+ // the observation recorded as a physicalId, and the Cedar id the source
85
+ // declares. A declared policy whose read failed has no physicalId, and
86
+ // calling it ambient would turn a hole into a security finding.
87
+ const observedIds = new Set(
88
+ Object.values(options.observed)
89
+ .map((meta) => meta.physicalId)
90
+ .filter((id): id is string => typeof id === "string" && id.length > 0),
91
+ );
92
+ const declaredIds = new Set(options.declaredPolicyIds ?? []);
93
+
94
+ const ambient: Record<string, ResourceMetadata> = {};
95
+ for (const policy of policies) {
96
+ if (observedIds.has(policy.policyId)) continue;
97
+ if (policy.cedarPolicyId !== undefined && declaredIds.has(policy.cedarPolicyId)) continue;
98
+
99
+ const effect = policy.statement ? effectFromStatement(policy.statement) : undefined;
100
+
101
+ ambient[`policy/${policy.policyId}`] = {
102
+ type: CEDAR_POLICY_TYPE,
103
+ status: policy.policyType || "STATIC",
104
+ physicalId: policy.policyId,
105
+ ...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
106
+ ownership: ownershipFromDescription(policy.description),
107
+ ambient: true,
108
+ attributes: {
109
+ policyStoreId: policy.policyStoreId,
110
+ policyId: policy.policyId,
111
+ policyType: policy.policyType,
112
+ ...(effect ? { effect } : {}),
113
+ ...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
114
+ ...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
115
+ // The statement itself, so the consumer can judge the grant rather than
116
+ // trusting a summary this reader invented.
117
+ ...(policy.statement ? { statement: policy.statement } : {}),
118
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
119
+ },
120
+ };
121
+ }
122
+
123
+ return ambient;
124
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * The AVP read transport (#1652).
3
+ *
4
+ * Amazon Verified Permissions speaks AWS JSON 1.0 — a POST of a JSON body with
5
+ * `x-amz-target: VerifiedPermissions.<Operation>` — so this is the same shape
6
+ * as `lexicons/aws/src/api/read-client.ts`'s Cloud Control half, pointed at a
7
+ * different service. That file is the precedent for the decision this module
8
+ * embodies: **no AWS SDK dependency**. The repo has none anywhere (the aws
9
+ * lexicon's runtime deps are `fflate` and `js-yaml`), and adding
10
+ * `@aws-sdk/client-verifiedpermissions` — 30-odd transitive packages — to the
11
+ * cedar lexicon so that one reader can issue three calls would put an AWS SDK
12
+ * in the dependency tree of a lexicon whose whole premise is that Cedar is
13
+ * vendor-neutral.
14
+ *
15
+ * ## Signing
16
+ *
17
+ * Requests are unsigned, exactly like the aws lexicon's read client. Real AWS
18
+ * rejects them; an emulator with an endpoint override does not. This is
19
+ * therefore an emulator-and-test transport today, and the honest consequence is
20
+ * encoded in {@link credentialsAvailable}: with no credentials and no endpoint
21
+ * override, the observation reports every entity NOT-OBSERVED with
22
+ * `no-credentials` rather than issuing a request that will fail. The signed
23
+ * path lands when SigV4 lands in `lexicons/aws/src/api/read-client.ts`, which
24
+ * is where it belongs — one implementation, not two.
25
+ */
26
+
27
+ const DEFAULT_REGION = "us-east-1";
28
+ const TARGET_PREFIX = "VerifiedPermissions";
29
+ const SERVICE = "verifiedpermissions";
30
+
31
+ /** Injectable HTTP, mirroring `AwsReadHttp` in the aws lexicon so tests avoid the network. */
32
+ export type AvpHttp = (
33
+ url: string,
34
+ init: { headers: Record<string, string>; body: string },
35
+ signal?: AbortSignal,
36
+ ) => Promise<{ status: number; text: string }>;
37
+
38
+ const defaultHttp: AvpHttp = async (url, init, signal) => {
39
+ const res = await fetch(url, { method: "POST", headers: init.headers, body: init.body, signal });
40
+ return { status: res.status, text: await res.text() };
41
+ };
42
+
43
+ /** A failed AVP read, carrying enough to classify it without matching on prose. */
44
+ export class AvpReadError extends Error {
45
+ constructor(
46
+ message: string,
47
+ readonly status: number,
48
+ /** The service's own error code (`ResourceNotFoundException`, `AccessDeniedException`, …). */
49
+ readonly code?: string,
50
+ ) {
51
+ super(message);
52
+ this.name = "AvpReadError";
53
+ }
54
+ }
55
+
56
+ export interface AvpClientOptions {
57
+ /** Endpoint override (an emulator, or a VPC endpoint). Omit for real AWS hosts. */
58
+ endpoint?: string;
59
+ /** Region for the real-AWS host and the credential scope. */
60
+ region?: string;
61
+ http?: AvpHttp;
62
+ signal?: AbortSignal;
63
+ /** Environment to read credentials from. Defaults to `process.env`; injectable for tests. */
64
+ env?: Record<string, string | undefined>;
65
+ }
66
+
67
+ /** Service host for AVP, honouring an endpoint override. */
68
+ export function avpUrl(endpoint?: string, region = DEFAULT_REGION): string {
69
+ return `${(endpoint ?? `https://${SERVICE}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
70
+ }
71
+
72
+ /**
73
+ * Whether this process has anything to authenticate with.
74
+ *
75
+ * An endpoint override counts: an emulator is reached without credentials, and
76
+ * refusing to look at one because `AWS_ACCESS_KEY_ID` is unset would make the
77
+ * local lanes unobservable. Everything else is the ordinary AWS credential
78
+ * surface, minus the instance-metadata path this transport cannot use anyway.
79
+ */
80
+ export function credentialsAvailable(env: Record<string, string | undefined> = process.env): boolean {
81
+ return Boolean(
82
+ env.AWS_ENDPOINT_URL ||
83
+ env.AWS_ACCESS_KEY_ID ||
84
+ env.AWS_PROFILE ||
85
+ env.AWS_SESSION_TOKEN ||
86
+ env.AWS_ROLE_ARN ||
87
+ env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||
88
+ env.AWS_WEB_IDENTITY_TOKEN_FILE,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * The credential scope header.
94
+ *
95
+ * This is NOT SigV4 — the signature is a placeholder, exactly as in the aws
96
+ * lexicon's read client, and it must not be mistaken for a signed read path. It
97
+ * carries the region so that an endpoint override (one host for every region)
98
+ * still reaches the right one.
99
+ */
100
+ function regionScope(region: string | undefined, env: Record<string, string | undefined>): Record<string, string> {
101
+ if (!region) return {};
102
+ const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
103
+ const key = env.AWS_ACCESS_KEY_ID || "chant";
104
+ return {
105
+ authorization:
106
+ `AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${SERVICE}/aws4_request, ` +
107
+ "SignedHeaders=host, Signature=unsigned",
108
+ };
109
+ }
110
+
111
+ function isRecord(value: unknown): value is Record<string, unknown> {
112
+ return typeof value === "object" && value !== null && !Array.isArray(value);
113
+ }
114
+
115
+ /** One AVP call. Throws {@link AvpReadError} carrying the service's `__type`. */
116
+ export async function avpCall(
117
+ operation: string,
118
+ payload: Record<string, unknown>,
119
+ options: AvpClientOptions = {},
120
+ ): Promise<Record<string, unknown>> {
121
+ const env = options.env ?? process.env;
122
+ const http = options.http ?? defaultHttp;
123
+ const url = avpUrl(options.endpoint ?? env.AWS_ENDPOINT_URL, options.region);
124
+ const res = await http(
125
+ url,
126
+ {
127
+ headers: {
128
+ "content-type": "application/x-amz-json-1.0",
129
+ "x-amz-target": `${TARGET_PREFIX}.${operation}`,
130
+ ...regionScope(options.region, env),
131
+ },
132
+ body: JSON.stringify(payload),
133
+ },
134
+ options.signal,
135
+ );
136
+
137
+ let parsed: unknown;
138
+ try {
139
+ parsed = JSON.parse(res.text);
140
+ } catch {
141
+ throw new AvpReadError(`unparseable ${operation} response`, res.status);
142
+ }
143
+ const body = isRecord(parsed) ? parsed : {};
144
+ const type = typeof body.__type === "string" ? body.__type.split("#").pop() : undefined;
145
+ if (type || res.status >= 400) {
146
+ const message = typeof body.message === "string" ? body.message : `${operation} failed with HTTP ${res.status}`;
147
+ throw new AvpReadError(message, res.status, type);
148
+ }
149
+ return body;
150
+ }
151
+
152
+ /* ── Shapes ───────────────────────────────────────────────────────────────── */
153
+
154
+ /** One policy, as `ListPolicies` reports it — no statement, only the description. */
155
+ export interface AvpPolicySummary {
156
+ policyStoreId: string;
157
+ policyId: string;
158
+ policyType: string;
159
+ /** `definition.static.description` — the channel chant's marker rides (see ./ownership.ts). */
160
+ description?: string;
161
+ createdDate?: string;
162
+ lastUpdatedDate?: string;
163
+ }
164
+
165
+ /** One policy, as `GetPolicy` reports it — the statement included. */
166
+ export interface AvpPolicyDetail extends AvpPolicySummary {
167
+ /** The Cedar policy text. */
168
+ statement: string;
169
+ }
170
+
171
+ function summaryOf(raw: Record<string, unknown>, fallbackStore: string): AvpPolicySummary {
172
+ const definition = isRecord(raw.definition) ? raw.definition : {};
173
+ const staticDef = isRecord(definition.static) ? definition.static : {};
174
+ const description = typeof staticDef.description === "string" ? staticDef.description : undefined;
175
+ return {
176
+ policyStoreId: typeof raw.policyStoreId === "string" ? raw.policyStoreId : fallbackStore,
177
+ policyId: typeof raw.policyId === "string" ? raw.policyId : "",
178
+ policyType: typeof raw.policyType === "string" ? raw.policyType : "STATIC",
179
+ ...(description !== undefined ? { description } : {}),
180
+ ...(typeof raw.createdDate === "string" ? { createdDate: raw.createdDate } : {}),
181
+ ...(typeof raw.lastUpdatedDate === "string" ? { lastUpdatedDate: raw.lastUpdatedDate } : {}),
182
+ };
183
+ }
184
+
185
+ /** `ListPolicies` for one store, paginated to exhaustion. */
186
+ export async function listPolicies(
187
+ policyStoreId: string,
188
+ options: AvpClientOptions = {},
189
+ ): Promise<AvpPolicySummary[]> {
190
+ const out: AvpPolicySummary[] = [];
191
+ let nextToken: string | undefined;
192
+ do {
193
+ const body = await avpCall(
194
+ "ListPolicies",
195
+ { policyStoreId, ...(nextToken ? { nextToken } : {}) },
196
+ options,
197
+ );
198
+ const rows = Array.isArray(body.policies) ? body.policies : [];
199
+ for (const row of rows) {
200
+ if (isRecord(row)) out.push(summaryOf(row, policyStoreId));
201
+ }
202
+ nextToken = typeof body.nextToken === "string" && body.nextToken.length > 0 ? body.nextToken : undefined;
203
+ } while (nextToken);
204
+ return out;
205
+ }
206
+
207
+ /**
208
+ * `GetPolicy` — the full record for one policy, statement included.
209
+ *
210
+ * A template-linked policy has no `definition.static`, so its statement comes
211
+ * back empty; callers treat an empty statement as "not a static policy this
212
+ * lexicon authored" rather than as a parse failure.
213
+ */
214
+ export async function getPolicy(
215
+ policyStoreId: string,
216
+ policyId: string,
217
+ options: AvpClientOptions = {},
218
+ ): Promise<AvpPolicyDetail> {
219
+ const body = await avpCall("GetPolicy", { policyStoreId, policyId }, options);
220
+ const definition = isRecord(body.definition) ? body.definition : {};
221
+ const staticDef = isRecord(definition.static) ? definition.static : {};
222
+ return {
223
+ ...summaryOf(body, policyStoreId),
224
+ policyId: typeof body.policyId === "string" ? body.policyId : policyId,
225
+ statement: typeof staticDef.statement === "string" ? staticDef.statement : "",
226
+ };
227
+ }
228
+
229
+ /** One policy store, as `GetPolicyStore` reports it. */
230
+ export interface AvpPolicyStore {
231
+ policyStoreId: string;
232
+ arn?: string;
233
+ description?: string;
234
+ createdDate?: string;
235
+ lastUpdatedDate?: string;
236
+ }
237
+
238
+ /** `GetPolicyStore` — used to resolve the store ARN the tag read needs. */
239
+ export async function getPolicyStore(
240
+ policyStoreId: string,
241
+ options: AvpClientOptions = {},
242
+ ): Promise<AvpPolicyStore> {
243
+ const body = await avpCall("GetPolicyStore", { policyStoreId }, options);
244
+ return {
245
+ policyStoreId: typeof body.policyStoreId === "string" ? body.policyStoreId : policyStoreId,
246
+ ...(typeof body.arn === "string" ? { arn: body.arn } : {}),
247
+ ...(typeof body.description === "string" ? { description: body.description } : {}),
248
+ ...(typeof body.createdDate === "string" ? { createdDate: body.createdDate } : {}),
249
+ ...(typeof body.lastUpdatedDate === "string" ? { lastUpdatedDate: body.lastUpdatedDate } : {}),
250
+ };
251
+ }
252
+
253
+ /** `ListTagsForResource` — the store-level half of the ownership channel. */
254
+ export async function listStoreTags(
255
+ resourceArn: string,
256
+ options: AvpClientOptions = {},
257
+ ): Promise<Record<string, string>> {
258
+ const body = await avpCall("ListTagsForResource", { resourceArn }, options);
259
+ const tags = isRecord(body.tags) ? body.tags : {};
260
+ const out: Record<string, string> = {};
261
+ for (const [key, value] of Object.entries(tags)) {
262
+ if (typeof value === "string") out[key] = value;
263
+ }
264
+ return out;
265
+ }
266
+
267
+ /* ── Classification ───────────────────────────────────────────────────────── */
268
+
269
+ /** True when the failure means the store itself is not there. */
270
+ export function storeDoesNotExist(err: unknown): boolean {
271
+ if (!(err instanceof AvpReadError)) return false;
272
+ return err.code === "ResourceNotFoundException" || /not\s*found|does not exist/i.test(err.message);
273
+ }
274
+
275
+ const CREDENTIAL_PATTERN =
276
+ /credential|token|expired|AccessDenied|not authorized|Unauthorized|UnrecognizedClient|InvalidSignature/i;
277
+
278
+ /**
279
+ * Map a transport failure onto the closed {@link UnobservedReason} set.
280
+ *
281
+ * Anything that is not demonstrably a credential problem is `read-failed`,
282
+ * because guessing wider would let a throttle or a DNS failure masquerade as a
283
+ * verdict about the estate.
284
+ */
285
+ export function classifyAvpFailure(err: unknown): { reason: "no-credentials" | "read-failed"; detail: string } {
286
+ const code = err instanceof AvpReadError && err.code ? `${err.code}: ` : "";
287
+ const message = err instanceof Error ? err.message : String(err);
288
+ const detail = `${code}${message}`;
289
+ return {
290
+ reason: CREDENTIAL_PATTERN.test(detail) ? "no-credentials" : "read-failed",
291
+ detail,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * The address a read was issued against, for the observation's `queried` map
297
+ * (chant #1620).
298
+ *
299
+ * An absence is only as trustworthy as the address behind it, and an AVP read
300
+ * has three ways to be pointed at the wrong place: the region, the store id,
301
+ * and the `@id` annotation the policy is matched on. All three are in here.
302
+ */
303
+ export function policyAddress(policyStoreId: string, cedarPolicyId: string, region?: string): string {
304
+ return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policy?@id=${encodeURIComponent(cedarPolicyId)}`;
305
+ }
306
+
307
+ /** The address of the store-wide enumeration itself. */
308
+ export function storeAddress(policyStoreId: string, region?: string): string {
309
+ return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policies`;
310
+ }