@intentius/chant-lexicon-cedar 0.124.30 → 0.124.32

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 (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,318 @@
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
+ * SigV4 lives once, in `lexicons/aws/src/api/sigv4.ts` (#1686). This module
18
+ * does not import it. A cedar → aws dependency edge would make the vendor-
19
+ * neutral lexicon unbuildable without the AWS one, which is the same rule that
20
+ * kept `src/avp/embed.ts` free of the aws lexicon: the seam is the data shape.
21
+ * So the signer arrives as a function on {@link AvpClientOptions}, typed here
22
+ * structurally against what the aws lexicon exports, and a project holding both
23
+ * lexicons wires them in one line:
24
+ *
25
+ * ```ts
26
+ * import { signRequest } from "@intentius/chant-lexicon-aws";
27
+ * import { describeAvpResources } from "@intentius/chant-lexicon-cedar";
28
+ *
29
+ * await describeAvpResources({
30
+ * environment,
31
+ * entityNames,
32
+ * entities,
33
+ * client: { region: "us-west-2", signer: signRequest },
34
+ * });
35
+ * ```
36
+ *
37
+ * Three cases go out unsigned, byte for byte as they did before signing
38
+ * existed — the credential scope of {@link regionScope} and nothing more:
39
+ *
40
+ * - **No signer.** The default, and the reason `credentialsAvailable` still
41
+ * gates the readers: a caller that wires nothing in is still an
42
+ * emulator-and-test transport, and real AWS still rejects it.
43
+ * - **No credentials.** A signer with nothing to sign with does not run.
44
+ * - **An endpoint override.** An emulator does not verify signatures, and
45
+ * signing against one would make every local lane need credentials to read
46
+ * what it just deployed. `signEndpointOverride` opts back in for an
47
+ * override that *is* real AWS — a VPC endpoint, a signing proxy.
48
+ *
49
+ * What counts as an override is {@link resolveEndpointOverride}: the
50
+ * `endpoint` option, else `AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS`, else
51
+ * `AWS_ENDPOINT_URL` — the AWS SDK's own precedence, and the same rule the aws
52
+ * lexicon's `api/read-client.ts` applies to every transport it owns (#1694).
53
+ * It is restated here rather than imported for the reason above: cedar does
54
+ * not depend on the aws lexicon, and a lexicon build compiles against the
55
+ * published core, so a helper hoisted into core would reach neither lexicon
56
+ * until the next core release.
57
+ */
58
+ const DEFAULT_REGION = "us-east-1";
59
+ const TARGET_PREFIX = "VerifiedPermissions";
60
+ const SERVICE = "verifiedpermissions";
61
+ const defaultHttp = async (url, init, signal) => {
62
+ const res = await fetch(url, { method: "POST", headers: init.headers, body: init.body, signal });
63
+ return { status: res.status, text: await res.text() };
64
+ };
65
+ /** A failed AVP read, carrying enough to classify it without matching on prose. */
66
+ export class AvpReadError extends Error {
67
+ constructor(message, status,
68
+ /** The service's own error code (`ResourceNotFoundException`, `AccessDeniedException`, …). */
69
+ code) {
70
+ super(message);
71
+ this.status = status;
72
+ this.code = code;
73
+ this.name = "AvpReadError";
74
+ }
75
+ }
76
+ /**
77
+ * Credentials for a request: explicit → environment → absent.
78
+ *
79
+ * The same rule the aws lexicon's `resolveCredentials` applies, and for the same
80
+ * reasons: an injected resolver is authoritative and its refusal is not
81
+ * second-guessed against the environment, and a half-set environment is absent
82
+ * rather than a signature that cannot verify. A caller with a profile file, IMDS
83
+ * or a container credential endpoint resolves those itself and passes the result
84
+ * in — that is what the resolver form is for.
85
+ */
86
+ export function resolveAvpCredentials(source, env = process.env) {
87
+ if (typeof source === "function")
88
+ return source();
89
+ if (source)
90
+ return source;
91
+ const accessKeyId = env.AWS_ACCESS_KEY_ID;
92
+ const secretAccessKey = env.AWS_SECRET_ACCESS_KEY;
93
+ if (!accessKeyId || !secretAccessKey)
94
+ return undefined;
95
+ return {
96
+ accessKeyId,
97
+ secretAccessKey,
98
+ ...(env.AWS_SESSION_TOKEN ? { sessionToken: env.AWS_SESSION_TOKEN } : {}),
99
+ };
100
+ }
101
+ /** Service host for AVP, honouring an endpoint override. */
102
+ export function avpUrl(endpoint, region = DEFAULT_REGION) {
103
+ return `${(endpoint ?? `https://${SERVICE}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
104
+ }
105
+ /** The service-specific endpoint variable the AWS SDK reads for Verified Permissions. */
106
+ const SERVICE_ENDPOINT_ENV_VAR = "AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS";
107
+ /**
108
+ * The one rule for what an endpoint override is (#1694): the `endpoint`
109
+ * option when given, else `AWS_ENDPOINT_URL_VERIFIEDPERMISSIONS`, else the
110
+ * ambient `AWS_ENDPOINT_URL`. The result is both the target and, through
111
+ * {@link requestHeaders}, the fact that decides whether the request is signed.
112
+ * Returns `undefined` for real AWS. Mirrors the aws lexicon's
113
+ * `resolveEndpointOverride` for this one service.
114
+ */
115
+ export function resolveEndpointOverride(endpoint, env = process.env) {
116
+ return endpoint || env[SERVICE_ENDPOINT_ENV_VAR] || env.AWS_ENDPOINT_URL || undefined;
117
+ }
118
+ /**
119
+ * Whether this process has anything to authenticate with.
120
+ *
121
+ * An endpoint override counts: an emulator is reached without credentials, and
122
+ * refusing to look at one because `AWS_ACCESS_KEY_ID` is unset would make the
123
+ * local lanes unobservable. Everything else is the ordinary AWS credential
124
+ * surface, minus the instance-metadata path this transport cannot use anyway.
125
+ */
126
+ export function credentialsAvailable(env = process.env) {
127
+ return Boolean(resolveEndpointOverride(undefined, env) ||
128
+ env.AWS_ACCESS_KEY_ID ||
129
+ env.AWS_PROFILE ||
130
+ env.AWS_SESSION_TOKEN ||
131
+ env.AWS_ROLE_ARN ||
132
+ env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ||
133
+ env.AWS_WEB_IDENTITY_TOKEN_FILE);
134
+ }
135
+ /**
136
+ * The credential scope header.
137
+ *
138
+ * This is NOT SigV4 — the signature is a placeholder, exactly as in the aws
139
+ * lexicon's read client, and it must not be mistaken for a signed read path. It
140
+ * carries the region so that an endpoint override (one host for every region)
141
+ * still reaches the right one, and it is only ever sent when no signature was
142
+ * produced; the moment one is, {@link requestHeaders} sends that instead.
143
+ */
144
+ function regionScope(region, env) {
145
+ if (!region)
146
+ return {};
147
+ const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
148
+ const key = env.AWS_ACCESS_KEY_ID || "chant";
149
+ return {
150
+ authorization: `AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${SERVICE}/aws4_request, ` +
151
+ "SignedHeaders=host, Signature=unsigned",
152
+ };
153
+ }
154
+ function isRecord(value) {
155
+ return typeof value === "object" && value !== null && !Array.isArray(value);
156
+ }
157
+ /**
158
+ * The headers one request goes out with — signed when a signer was injected,
159
+ * something resolved to sign with, and the target is real AWS; scope-only in
160
+ * every other case, which is the path this transport had before signing.
161
+ *
162
+ * Signing needs a region even when the caller named none, because the scope
163
+ * string has a slot for one; it borrows the same default {@link avpUrl} used to
164
+ * build the host, so the signature agrees with the endpoint it is sent to.
165
+ */
166
+ function requestHeaders(operation, url, body, endpoint, options, env) {
167
+ const base = {
168
+ "content-type": "application/x-amz-json-1.0",
169
+ "x-amz-target": `${TARGET_PREFIX}.${operation}`,
170
+ };
171
+ const signer = options.signer;
172
+ const credentials = signer ? resolveAvpCredentials(options.credentials, env) : undefined;
173
+ if (!signer || !credentials || (endpoint && options.signEndpointOverride !== true)) {
174
+ return { ...base, ...regionScope(options.region, env) };
175
+ }
176
+ return signer({
177
+ method: "POST",
178
+ url,
179
+ headers: base,
180
+ body,
181
+ service: SERVICE,
182
+ region: options.region ?? DEFAULT_REGION,
183
+ credentials,
184
+ ...(options.now ? { now: options.now } : {}),
185
+ });
186
+ }
187
+ /** One AVP call. Throws {@link AvpReadError} carrying the service's `__type`. */
188
+ export async function avpCall(operation, payload, options = {}) {
189
+ const env = options.env ?? process.env;
190
+ const http = options.http ?? defaultHttp;
191
+ const endpoint = resolveEndpointOverride(options.endpoint, env);
192
+ const url = avpUrl(endpoint, options.region);
193
+ const payloadJson = JSON.stringify(payload);
194
+ const res = await http(url, {
195
+ headers: requestHeaders(operation, url, payloadJson, endpoint, options, env),
196
+ body: payloadJson,
197
+ }, options.signal);
198
+ let parsed;
199
+ try {
200
+ parsed = JSON.parse(res.text);
201
+ }
202
+ catch {
203
+ throw new AvpReadError(`unparseable ${operation} response`, res.status);
204
+ }
205
+ const body = isRecord(parsed) ? parsed : {};
206
+ const type = typeof body.__type === "string" ? body.__type.split("#").pop() : undefined;
207
+ if (type || res.status >= 400) {
208
+ const message = typeof body.message === "string" ? body.message : `${operation} failed with HTTP ${res.status}`;
209
+ throw new AvpReadError(message, res.status, type);
210
+ }
211
+ return body;
212
+ }
213
+ function summaryOf(raw, fallbackStore) {
214
+ const definition = isRecord(raw.definition) ? raw.definition : {};
215
+ const staticDef = isRecord(definition.static) ? definition.static : {};
216
+ const description = typeof staticDef.description === "string" ? staticDef.description : undefined;
217
+ return {
218
+ policyStoreId: typeof raw.policyStoreId === "string" ? raw.policyStoreId : fallbackStore,
219
+ policyId: typeof raw.policyId === "string" ? raw.policyId : "",
220
+ policyType: typeof raw.policyType === "string" ? raw.policyType : "STATIC",
221
+ ...(description !== undefined ? { description } : {}),
222
+ ...(typeof raw.createdDate === "string" ? { createdDate: raw.createdDate } : {}),
223
+ ...(typeof raw.lastUpdatedDate === "string" ? { lastUpdatedDate: raw.lastUpdatedDate } : {}),
224
+ };
225
+ }
226
+ /** `ListPolicies` for one store, paginated to exhaustion. */
227
+ export async function listPolicies(policyStoreId, options = {}) {
228
+ const out = [];
229
+ let nextToken;
230
+ do {
231
+ const body = await avpCall("ListPolicies", { policyStoreId, ...(nextToken ? { nextToken } : {}) }, options);
232
+ const rows = Array.isArray(body.policies) ? body.policies : [];
233
+ for (const row of rows) {
234
+ if (isRecord(row))
235
+ out.push(summaryOf(row, policyStoreId));
236
+ }
237
+ nextToken = typeof body.nextToken === "string" && body.nextToken.length > 0 ? body.nextToken : undefined;
238
+ } while (nextToken);
239
+ return out;
240
+ }
241
+ /**
242
+ * `GetPolicy` — the full record for one policy, statement included.
243
+ *
244
+ * A template-linked policy has no `definition.static`, so its statement comes
245
+ * back empty; callers treat an empty statement as "not a static policy this
246
+ * lexicon authored" rather than as a parse failure.
247
+ */
248
+ export async function getPolicy(policyStoreId, policyId, options = {}) {
249
+ const body = await avpCall("GetPolicy", { policyStoreId, policyId }, options);
250
+ const definition = isRecord(body.definition) ? body.definition : {};
251
+ const staticDef = isRecord(definition.static) ? definition.static : {};
252
+ return {
253
+ ...summaryOf(body, policyStoreId),
254
+ policyId: typeof body.policyId === "string" ? body.policyId : policyId,
255
+ statement: typeof staticDef.statement === "string" ? staticDef.statement : "",
256
+ };
257
+ }
258
+ /** `GetPolicyStore` — used to resolve the store ARN the tag read needs. */
259
+ export async function getPolicyStore(policyStoreId, options = {}) {
260
+ const body = await avpCall("GetPolicyStore", { policyStoreId }, options);
261
+ return {
262
+ policyStoreId: typeof body.policyStoreId === "string" ? body.policyStoreId : policyStoreId,
263
+ ...(typeof body.arn === "string" ? { arn: body.arn } : {}),
264
+ ...(typeof body.description === "string" ? { description: body.description } : {}),
265
+ ...(typeof body.createdDate === "string" ? { createdDate: body.createdDate } : {}),
266
+ ...(typeof body.lastUpdatedDate === "string" ? { lastUpdatedDate: body.lastUpdatedDate } : {}),
267
+ };
268
+ }
269
+ /** `ListTagsForResource` — the store-level half of the ownership channel. */
270
+ export async function listStoreTags(resourceArn, options = {}) {
271
+ const body = await avpCall("ListTagsForResource", { resourceArn }, options);
272
+ const tags = isRecord(body.tags) ? body.tags : {};
273
+ const out = {};
274
+ for (const [key, value] of Object.entries(tags)) {
275
+ if (typeof value === "string")
276
+ out[key] = value;
277
+ }
278
+ return out;
279
+ }
280
+ /* ── Classification ───────────────────────────────────────────────────────── */
281
+ /** True when the failure means the store itself is not there. */
282
+ export function storeDoesNotExist(err) {
283
+ if (!(err instanceof AvpReadError))
284
+ return false;
285
+ return err.code === "ResourceNotFoundException" || /not\s*found|does not exist/i.test(err.message);
286
+ }
287
+ const CREDENTIAL_PATTERN = /credential|token|expired|AccessDenied|not authorized|Unauthorized|UnrecognizedClient|InvalidSignature/i;
288
+ /**
289
+ * Map a transport failure onto the closed {@link UnobservedReason} set.
290
+ *
291
+ * Anything that is not demonstrably a credential problem is `read-failed`,
292
+ * because guessing wider would let a throttle or a DNS failure masquerade as a
293
+ * verdict about the estate.
294
+ */
295
+ export function classifyAvpFailure(err) {
296
+ const code = err instanceof AvpReadError && err.code ? `${err.code}: ` : "";
297
+ const message = err instanceof Error ? err.message : String(err);
298
+ const detail = `${code}${message}`;
299
+ return {
300
+ reason: CREDENTIAL_PATTERN.test(detail) ? "no-credentials" : "read-failed",
301
+ detail,
302
+ };
303
+ }
304
+ /**
305
+ * The address a read was issued against, for the observation's `queried` map
306
+ * (chant #1620).
307
+ *
308
+ * An absence is only as trustworthy as the address behind it, and an AVP read
309
+ * has three ways to be pointed at the wrong place: the region, the store id,
310
+ * and the `@id` annotation the policy is matched on. All three are in here.
311
+ */
312
+ export function policyAddress(policyStoreId, cedarPolicyId, region) {
313
+ return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policy?@id=${encodeURIComponent(cedarPolicyId)}`;
314
+ }
315
+ /** The address of the store-wide enumeration itself. */
316
+ export function storeAddress(policyStoreId, region) {
317
+ return `avp://${region ?? DEFAULT_REGION}/policy-store/${policyStoreId}/policies`;
318
+ }
@@ -0,0 +1,144 @@
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
+ import { observation, observeEntities, unobservedAll, } from "@intentius/chant/observation";
17
+ import { CEDAR_POLICY_TYPE, resolvePolicyId } from "../serializer.js";
18
+ import { classifyAvpFailure, credentialsAvailable, policyAddress, storeAddress, storeDoesNotExist, } from "./client.js";
19
+ import { ownershipFromDescription } from "./ownership.js";
20
+ import { indexByCedarId, loadLivePolicies, resolvePolicyStoreId, } from "./store.js";
21
+ /** A bind failure that already knows its verdict. */
22
+ class AvpBindFailure extends Error {
23
+ constructor(reason, detail) {
24
+ super(detail);
25
+ this.reason = reason;
26
+ this.detail = detail;
27
+ this.name = "AvpBindFailure";
28
+ }
29
+ }
30
+ function metadataFor(policy, ownership) {
31
+ return {
32
+ type: CEDAR_POLICY_TYPE,
33
+ physicalId: policy.policyId,
34
+ // AVP has no lifecycle state on a policy: it exists or it does not. The
35
+ // policy type is the most useful thing to show, and a template-linked
36
+ // policy differing from a static one IS drift worth seeing.
37
+ status: policy.policyType || "STATIC",
38
+ ...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
39
+ ownership,
40
+ attributes: {
41
+ policyStoreId: policy.policyStoreId,
42
+ policyId: policy.policyId,
43
+ policyType: policy.policyType,
44
+ ...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
45
+ ...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
46
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
47
+ },
48
+ };
49
+ }
50
+ /**
51
+ * Observe the declared policy set against a live AVP store.
52
+ *
53
+ * Never throws for a transport or binding problem — a throw is the
54
+ * whole-lexicon failure and core would mark every entity `read-failed`, which
55
+ * is a worse-shaped version of what this returns directly with the right
56
+ * reason on it.
57
+ */
58
+ export async function describeAvpResources(options) {
59
+ const env = options.env ?? process.env;
60
+ const typesByName = {};
61
+ for (const name of options.entityNames) {
62
+ typesByName[name] = options.entities.get(name)?.entityType ?? CEDAR_POLICY_TYPE;
63
+ }
64
+ const policyStoreId = resolvePolicyStoreId({
65
+ environment: options.environment,
66
+ ...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
67
+ entities: options.entities,
68
+ env,
69
+ });
70
+ if (!policyStoreId) {
71
+ return observation({}, unobservedAll(options.entityNames, "no-binding", `no AVP policy store for environment "${options.environment}" — set ${"CEDAR_AVP_POLICY_STORE_ID"} or declare policyStoreId on a policy`, typesByName));
72
+ }
73
+ if (!credentialsAvailable(env)) {
74
+ return observation({}, unobservedAll(options.entityNames, "no-credentials", "no AWS credentials and no endpoint override — the AVP store was not queried", typesByName));
75
+ }
76
+ const region = options.client?.region;
77
+ const declared = options.entityNames.map((name) => {
78
+ const entity = options.entities.get(name);
79
+ return {
80
+ name,
81
+ type: entity?.entityType ?? CEDAR_POLICY_TYPE,
82
+ props: entity?.props ?? {},
83
+ };
84
+ });
85
+ const adapter = {
86
+ async bind() {
87
+ try {
88
+ const policies = await loadLivePolicies({
89
+ policyStoreId,
90
+ ...(options.client ? { client: options.client } : {}),
91
+ });
92
+ return { policyStoreId, byCedarId: indexByCedarId(policies), ...(region ? { region } : {}) };
93
+ }
94
+ catch (err) {
95
+ // A store that is not there yet is the pre-first-apply state: nothing
96
+ // is deployed, so every declared policy is genuinely absent and
97
+ // `create` is the right proposal.
98
+ if (storeDoesNotExist(err)) {
99
+ return { policyStoreId, byCedarId: new Map(), ...(region ? { region } : {}) };
100
+ }
101
+ const { reason, detail } = classifyAvpFailure(err);
102
+ throw new AvpBindFailure(reason, `ListPolicies failed for store ${policyStoreId}: ${detail}`);
103
+ }
104
+ },
105
+ classifyBindFailure(err) {
106
+ if (err instanceof AvpBindFailure)
107
+ return { reason: err.reason, detail: err.detail };
108
+ const { reason, detail } = classifyAvpFailure(err);
109
+ return { reason, detail: `${storeAddress(policyStoreId, region)}: ${detail}` };
110
+ },
111
+ async read(binding, entity) {
112
+ if (entity.type !== CEDAR_POLICY_TYPE) {
113
+ // Cedar declares one deployable kind. Anything else is a schema entity
114
+ // or action constant, which has no AVP counterpart at all — saying so
115
+ // per entity keeps it out of `lifecycle plan` as a create.
116
+ return {
117
+ unobserved: {
118
+ reason: "unsupported-kind",
119
+ detail: `no AVP reader for ${entity.type} — only ${CEDAR_POLICY_TYPE} is deployed to a policy store`,
120
+ },
121
+ };
122
+ }
123
+ const cedarPolicyId = resolvePolicyId(entity.name, entity.props);
124
+ const queried = policyAddress(binding.policyStoreId, cedarPolicyId, binding.region);
125
+ const policy = binding.byCedarId.get(cedarPolicyId);
126
+ // The store was enumerated and this `@id` is not in it. That is an
127
+ // absence, and the only shape allowed to become a create.
128
+ if (!policy)
129
+ return { absent: true, queried };
130
+ const ownership = ownershipFromDescription(policy.description);
131
+ if (options.owned && ownership !== "owned") {
132
+ return {
133
+ unobserved: {
134
+ reason: "filtered",
135
+ detail: "live policy carries no chant description marker",
136
+ },
137
+ queried,
138
+ };
139
+ }
140
+ return { present: metadataFor(policy, ownership), queried };
141
+ },
142
+ };
143
+ return observeEntities(declared, adapter);
144
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Typed embedding of a cedar policy into `AWS::VerifiedPermissions::Policy`
3
+ * (#1652).
4
+ *
5
+ * The aws lexicon keeps the deployment vehicle — its generated
6
+ * `VerifiedPermissionsPolicy` takes a required `PolicyStoreId` and a required
7
+ * `Definition`, and the `Definition.Static.Statement` inside it is an opaque
8
+ * string. Opaque is the problem the whole cedar lexicon exists to remove: today
9
+ * a team writes that string with `${}` templating and finds out at deploy time
10
+ * whether the entity type exists.
11
+ *
12
+ * This module closes the loop without joining the two lexicons together. It
13
+ * emits the *string* and the *JSON envelope* AVP expects and stops there:
14
+ * nothing here imports `@intentius/chant-lexicon-aws`, and nothing here needs
15
+ * to. A project that has both installed writes
16
+ *
17
+ * ```ts
18
+ * new VerifiedPermissionsPolicy({
19
+ * PolicyStoreId: store.ref(),
20
+ * Definition: avpPolicyDefinition("ownerRead", ownerReadProps),
21
+ * });
22
+ * ```
23
+ *
24
+ * and the statement is the same text `chant build` writes to `.cedar`, rendered
25
+ * by the same renderer, with `@id` intact.
26
+ *
27
+ * ### Why no dependency, and why the example uses plain objects
28
+ *
29
+ * A cedar → aws dependency would invert the epic's decision that Cedar is
30
+ * vendor-neutral and AVP is one of several evaluators, and it would make the
31
+ * cedar lexicon unbuildable without the aws one. The seam is therefore the
32
+ * data shape, which is stable CloudFormation, and
33
+ * `examples/avp-embedding/` demonstrates the pairing with a plain-object stand-in
34
+ * for `VerifiedPermissionsPolicy` — the shipped examples build with the cedar
35
+ * serializer alone, so a real cross-lexicon example could not run in this
36
+ * repo's example harness at all. The stand-in carries the exact prop names the
37
+ * generated class declares (`PolicyStoreId`, `Definition`, `Name`), so the
38
+ * substitution is a one-line edit in a real project.
39
+ */
40
+ import { cedarPolicyRecords, renderPolicyText, resolvePolicyId, } from "../serializer.js";
41
+ import { policyToJson } from "../spec/wasm.js";
42
+ import { encodeOwnershipDescription } from "./ownership.js";
43
+ function propsRecord(props) {
44
+ return props;
45
+ }
46
+ /**
47
+ * The Cedar policy text for one policy — the exact string
48
+ * `Definition.Static.Statement` wants.
49
+ *
50
+ * `name` is the chant entity name; the `@id` annotation the statement carries
51
+ * is derived from it the same way the serializer derives it, which is what lets
52
+ * the live observation match a policy in the store back to the entity that
53
+ * declared it.
54
+ */
55
+ export function avpStatement(name, props, options = {}) {
56
+ const record = propsRecord(props);
57
+ return renderPolicyText(options.policyId ?? resolvePolicyId(name, record), record);
58
+ }
59
+ /**
60
+ * The Cedar JSON policy format for one policy — for an evaluator that takes
61
+ * JSON rather than text (`cedar-agent`, an embedded `cedar-wasm`).
62
+ *
63
+ * Not what AVP's `Definition` wants; AVP takes the text. Emitted here so the
64
+ * seam is documented in both directions rather than only the AWS one.
65
+ *
66
+ * Built the way the serializer's JSON leg is built (#1653): render the text,
67
+ * then hand it to `cedar-wasm`, so what comes back is Cedar's own reading
68
+ * rather than a second, worse encoder. Throws on text Cedar refuses — this is
69
+ * an authoring-time call, and a policy the module cannot read is a defect the
70
+ * caller wants to hear about rather than a `undefined` to thread through.
71
+ */
72
+ export function avpStatementJSON(name, props, options = {}) {
73
+ const statement = avpStatement(name, props, options);
74
+ const converted = policyToJson(statement);
75
+ if (!converted.ok) {
76
+ throw new Error(`cedar: ${name} did not parse, so no JSON form could be produced — ${converted.error}`);
77
+ }
78
+ return converted.value;
79
+ }
80
+ /**
81
+ * The `Definition` property of `AWS::VerifiedPermissions::Policy`.
82
+ *
83
+ * With `options.ownership` set, the description carries chant's marker — the
84
+ * only per-policy ownership channel AVP has, because policies are not taggable.
85
+ */
86
+ export function avpPolicyDefinition(name, props, options = {}) {
87
+ const record = propsRecord(props);
88
+ const policyId = options.policyId ?? resolvePolicyId(name, record);
89
+ const description = options.ownership
90
+ ? encodeOwnershipDescription(options.description, options.ownership, policyId)
91
+ : options.description?.trim();
92
+ return {
93
+ Static: {
94
+ Statement: renderPolicyText(policyId, record),
95
+ ...(description ? { Description: description } : {}),
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * Both required props of `AWS::VerifiedPermissions::Policy`, ready to spread
101
+ * into the generated class.
102
+ *
103
+ * `PolicyStoreId` is a string here rather than an AttrRef because this module
104
+ * does not know the aws lexicon's reference types. A project passes
105
+ * `store.ref()` in place of the literal and TypeScript is satisfied by the
106
+ * generated class's own prop type, not by this one.
107
+ */
108
+ export function avpPolicyResource(name, props, policyStoreId, options = {}) {
109
+ return { PolicyStoreId: policyStoreId, Definition: avpPolicyDefinition(name, props, options) };
110
+ }
111
+ /**
112
+ * Every `Cedar::Policy` in a build, rendered as AVP definitions and keyed by
113
+ * chant entity name.
114
+ *
115
+ * The whole-set form: one call turns a policy set into the definitions a stack
116
+ * of `VerifiedPermissionsPolicy` declarations needs, with references between
117
+ * declared entities already walked (that is what `cedarPolicyRecords` does).
118
+ */
119
+ export function avpPolicySet(entities, options = {}) {
120
+ const out = {};
121
+ for (const { name, id, props } of cedarPolicyRecords(entities)) {
122
+ out[name] = avpPolicyDefinition(name, props, { ...options, policyId: id });
123
+ }
124
+ return out;
125
+ }