@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,151 @@
1
+ /**
2
+ * `exportResources()` against an AVP policy store (#1652).
3
+ *
4
+ * `describeResources` returns scrubbed metadata for diffing — you cannot
5
+ * regenerate a policy from "STATIC, last updated Tuesday". This is the other
6
+ * half: the live statement, parsed back into the props a `Cedar::Policy`
7
+ * declaration carries, as `TemplateIR` so it feeds `templateGenerator()`
8
+ * unchanged. The scrubbing boundary stays single-purpose; nothing here is
9
+ * reachable from the lifecycle paths, which consume the lexicon through
10
+ * `ObservationLexicon` and cannot see this method at all.
11
+ *
12
+ * ## The parse is the import parser's
13
+ *
14
+ * The live-export guide's third step is "map to `TemplateIR` by reusing your
15
+ * import parser", and #1653 shipped one. So a live statement goes through
16
+ * `CedarTemplateParser` — the same code path `chant import policies.cedar`
17
+ * takes — rather than a second reader that would have to re-derive scope
18
+ * mapping, clause-text recovery and template detection, and would drift the
19
+ * first time either side changed. This module's job is the AVP half: bind the
20
+ * store, enumerate it, decide what is server-written, and apply the filters.
21
+ *
22
+ * ## What is server-written here
23
+ *
24
+ * The live-export guide says strip to the declared shape by default and keep
25
+ * the rest under `verbatim`. For AVP the split is unusually clean, because the
26
+ * only thing a user authors is the statement. Everything else on the record —
27
+ * `policyId` (service-assigned), `policyStoreId`, `policyType`, `createdDate`,
28
+ * `lastUpdatedDate`, and the derived `principal`/`resource` summaries AVP
29
+ * computes from the statement — is server-written and stripped by default.
30
+ * `Description` is stripped too: chant's marker lives in it, so exporting it
31
+ * verbatim into regenerated source would bake one environment's ownership stamp
32
+ * into the code that produces every environment.
33
+ *
34
+ * `verbatim: true` puts all of it back under an `avp` key, raw description
35
+ * included.
36
+ */
37
+ import { CedarTemplateParser } from "../import/adapter.js";
38
+ import { credentialsAvailable, storeDoesNotExist } from "./client.js";
39
+ import { descriptionIsOwned } from "./ownership.js";
40
+ import { loadLivePolicies, resolvePolicyStoreId } from "./store.js";
41
+ /**
42
+ * One live policy as import IR, or `undefined` when its statement could not be
43
+ * read or parsed.
44
+ *
45
+ * The parse is `CedarTemplateParser`'s — one policy is a policy set of one, so
46
+ * the same reader `chant import` uses answers here, including whether Cedar
47
+ * read it as a static policy or as a template (`Cedar::Policy` vs
48
+ * `Cedar::Template`, decided by whether it carries a slot). The `logicalId` it
49
+ * assigns is the Cedar policy id; turning that into a TypeScript identifier is
50
+ * `CedarGenerator`'s job downstream, and doing it here too would apply the
51
+ * transform twice.
52
+ *
53
+ * Dropping an unparseable policy is deliberate. A regenerated policy set that
54
+ * silently lost a `forbid` reads as a working export and is an authorization
55
+ * hole; a missing entry is visible.
56
+ */
57
+ export function policyToResourceIR(policy, options = {}) {
58
+ if (!policy.statement)
59
+ return undefined;
60
+ let parsed;
61
+ try {
62
+ parsed = new CedarTemplateParser().parse(policy.statement);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ const resource = parsed.resources[0];
68
+ if (!resource)
69
+ return undefined;
70
+ const properties = { ...resource.properties };
71
+ if (options.verbatim) {
72
+ const record = {
73
+ policyId: policy.policyId,
74
+ policyStoreId: policy.policyStoreId,
75
+ policyType: policy.policyType,
76
+ ...(policy.description !== undefined ? { description: policy.description } : {}),
77
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
78
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
79
+ statement: policy.statement,
80
+ };
81
+ properties.avp = record;
82
+ }
83
+ return {
84
+ ...resource,
85
+ properties,
86
+ metadata: { ...resource.metadata, avpPolicyId: policy.policyId },
87
+ };
88
+ }
89
+ /**
90
+ * Read a live policy store as full-fidelity import IR.
91
+ *
92
+ * Unlike the observation, this throws when it cannot read: `chant import` has
93
+ * no tri-state to degrade into, and an empty template returned from a failed
94
+ * read would generate a source tree that deletes every policy. The one
95
+ * exception is a store that does not exist, which is an honest empty estate.
96
+ */
97
+ export async function exportAvpResources(options) {
98
+ const env = options.env ?? process.env;
99
+ const policyStoreId = resolvePolicyStoreId({
100
+ environment: options.environment,
101
+ ...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
102
+ ...(options.entities ? { entities: options.entities } : {}),
103
+ env,
104
+ });
105
+ if (!policyStoreId) {
106
+ throw new Error(`no AVP policy store for environment "${options.environment}" — set CEDAR_AVP_POLICY_STORE_ID or pass policyStoreId`);
107
+ }
108
+ if (!credentialsAvailable(env)) {
109
+ throw new Error("no AWS credentials and no endpoint override — cannot export from AVP");
110
+ }
111
+ let policies;
112
+ try {
113
+ policies = await loadLivePolicies({
114
+ policyStoreId,
115
+ ...(options.client ? { client: options.client } : {}),
116
+ withStatements: true,
117
+ });
118
+ }
119
+ catch (err) {
120
+ if (storeDoesNotExist(err))
121
+ policies = [];
122
+ else
123
+ throw err;
124
+ }
125
+ const resources = [];
126
+ for (const policy of policies) {
127
+ // Ownership is read on this path, which is what lets it be declared in
128
+ // `ownershipChannel.reads` (chant #1348).
129
+ if (options.owned && !descriptionIsOwned(policy.description))
130
+ continue;
131
+ const ir = policyToResourceIR(policy, { ...(options.verbatim ? { verbatim: true } : {}) });
132
+ if (!ir)
133
+ continue;
134
+ // The type is compared against what the parser decided, not a constant:
135
+ // a policy carrying a `?principal`/`?resource` slot comes back as
136
+ // `Cedar::Template`, and `--type Cedar::Policy` should not sweep it up.
137
+ if (options.selector?.type !== undefined && options.selector.type !== ir.type)
138
+ continue;
139
+ if (options.selector?.name !== undefined && ir.logicalId !== options.selector.name)
140
+ continue;
141
+ resources.push(ir);
142
+ }
143
+ // Stable order: the store's enumeration order is not a contract, and an
144
+ // import that reshuffles the file on every run is unusable as a diff.
145
+ resources.sort((a, b) => a.logicalId.localeCompare(b.logicalId));
146
+ return {
147
+ resources,
148
+ parameters: [],
149
+ metadata: { lexicon: "cedar", policyStoreId },
150
+ };
151
+ }
@@ -0,0 +1,144 @@
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
+ import { classifyOwnership, hasOwnershipMarker, ownershipEntries } from "@intentius/chant/ownership";
24
+ /**
25
+ * The marker keys, shared by both AVP channels.
26
+ *
27
+ * The same three names serve the store's tag map and the per-policy description
28
+ * marker, because `ownershipChannel` declares one {@link ChannelKeys} and a
29
+ * lexicon whose two channels disagreed on key names would be two conventions
30
+ * wearing one declaration. AWS tag keys permit `:`, and the description
31
+ * encoding below quotes its values, so neither channel constrains the other.
32
+ */
33
+ export const AVP_OWNERSHIP_KEYS = {
34
+ managedBy: "chant:managed-by",
35
+ stack: "chant:stack",
36
+ env: "chant:env",
37
+ };
38
+ /**
39
+ * The Cedar policy id this AVP policy carries, recorded beside the marker.
40
+ *
41
+ * Deliberately *not* part of {@link AVP_OWNERSHIP_KEYS}: it is not an ownership
42
+ * claim and `hasOwnershipMarker` must not consult it. It is here because
43
+ * `ListPolicies` returns descriptions but not statements, so without it every
44
+ * live policy needs a second `GetPolicy` round trip just to learn which chant
45
+ * entity it belongs to.
46
+ */
47
+ export const AVP_POLICY_ID_KEY = "chant:policy-id";
48
+ /**
49
+ * AVP caps `Definition.Static.Description` at 150 characters.
50
+ *
51
+ * The marker is what makes a policy deletable, so when the author's own text
52
+ * plus the marker exceed the cap, the *text* is truncated and the marker is
53
+ * kept whole. Silently dropping the marker would turn an owned policy into a
54
+ * foreign one, and a foreign policy is never deleted — the estate would grow
55
+ * undeletable policies with no error anywhere.
56
+ */
57
+ export const AVP_DESCRIPTION_MAX = 150;
58
+ const MARKER_SEGMENT = /\s*\[(chant:[^\]]*)\]\s*$/;
59
+ /**
60
+ * Encode the marker into a description.
61
+ *
62
+ * Values are percent-encoded, so a stack named `my stack` or an id holding a
63
+ * `]` round-trips instead of corrupting the segment. Reversed by
64
+ * {@link decodeOwnershipDescription}.
65
+ */
66
+ export function encodeOwnershipDescription(text, marker, policyId) {
67
+ const entries = ownershipEntries(AVP_OWNERSHIP_KEYS, marker);
68
+ if (policyId)
69
+ entries[AVP_POLICY_ID_KEY] = policyId;
70
+ const segment = `[${Object.entries(entries)
71
+ .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
72
+ .join(" ")}]`;
73
+ const authored = (text ?? "").trim();
74
+ if (authored.length === 0)
75
+ return segment;
76
+ // The marker survives; the prose is what gives way (see AVP_DESCRIPTION_MAX).
77
+ const room = AVP_DESCRIPTION_MAX - segment.length - 1;
78
+ if (room <= 0)
79
+ return segment;
80
+ return `${authored.slice(0, room).trimEnd()} ${segment}`;
81
+ }
82
+ /** Recover the author's text and chant's marker from a live description. */
83
+ export function decodeOwnershipDescription(description) {
84
+ if (!description)
85
+ return { text: "", tags: {}, marked: false };
86
+ const match = MARKER_SEGMENT.exec(description);
87
+ if (!match)
88
+ return { text: description.trim(), tags: {}, marked: false };
89
+ const tags = {};
90
+ for (const pair of match[1].split(/\s+/)) {
91
+ if (pair.length === 0)
92
+ continue;
93
+ const eq = pair.indexOf("=");
94
+ if (eq <= 0)
95
+ continue;
96
+ try {
97
+ tags[pair.slice(0, eq)] = decodeURIComponent(pair.slice(eq + 1));
98
+ }
99
+ catch {
100
+ // A malformed escape is a corrupt marker, not a crash. The entry is
101
+ // dropped, which at worst downgrades the verdict to `foreign` — and a
102
+ // foreign policy is never deleted, so the failure direction is the safe
103
+ // one.
104
+ continue;
105
+ }
106
+ }
107
+ const policyId = tags[AVP_POLICY_ID_KEY];
108
+ return {
109
+ text: description.slice(0, match.index).trim(),
110
+ tags,
111
+ ...(policyId ? { policyId } : {}),
112
+ marked: true,
113
+ };
114
+ }
115
+ /**
116
+ * The per-policy verdict, read off a live description.
117
+ *
118
+ * Two-valued on purpose: this function is only ever called where the
119
+ * description was actually read, and `unknown` is reserved for the paths that
120
+ * could not read it at all (the contract in core's `ownership.ts`).
121
+ */
122
+ export function ownershipFromDescription(description) {
123
+ return classifyOwnership(decodeOwnershipDescription(description).tags, AVP_OWNERSHIP_KEYS);
124
+ }
125
+ /** True when a live description carries chant's marker. */
126
+ export function descriptionIsOwned(description) {
127
+ return hasOwnershipMarker(decodeOwnershipDescription(description).tags, AVP_OWNERSHIP_KEYS);
128
+ }
129
+ /**
130
+ * The store-level verdict, read off a policy store's tag map.
131
+ *
132
+ * The coarse channel: it answers "is this store chant's" and says nothing about
133
+ * any individual policy in it. Kept separate from
134
+ * {@link ownershipFromDescription} so a caller cannot accidentally treat a
135
+ * store's tags as a policy's marker — the mistake the whole design exists to
136
+ * avoid.
137
+ */
138
+ export function ownershipFromStoreTags(tags) {
139
+ return classifyOwnership(tags, AVP_OWNERSHIP_KEYS);
140
+ }
141
+ /** The tag entries to stamp onto a policy store. */
142
+ export function storeOwnershipTags(marker) {
143
+ return ownershipEntries(AVP_OWNERSHIP_KEYS, marker);
144
+ }
@@ -0,0 +1,45 @@
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
+ /** The `@id("…")` a chant-emitted statement always carries, or undefined. */
18
+ export function policyIdFromStatement(statement) {
19
+ const match = /@id\s*\(\s*"((?:[^"\\]|\\.)*)"\s*\)/.exec(statement);
20
+ if (!match)
21
+ return undefined;
22
+ return unescapeCedarString(match[1]);
23
+ }
24
+ /** `permit` or `forbid`, scanned past any annotations. */
25
+ export function effectFromStatement(statement) {
26
+ const match = /(?:^|\n)\s*(permit|forbid)\s*\(/.exec(stripAnnotations(statement));
27
+ return match ? match[1] : undefined;
28
+ }
29
+ function stripAnnotations(statement) {
30
+ return `\n${statement.replace(/@[A-Za-z_][A-Za-z0-9_]*\s*\(\s*"(?:[^"\\]|\\.)*"\s*\)/g, "\n")}`;
31
+ }
32
+ function unescapeCedarString(value) {
33
+ return value.replace(/\\(.)/g, (_, ch) => {
34
+ switch (ch) {
35
+ case "n":
36
+ return "\n";
37
+ case "r":
38
+ return "\r";
39
+ case "t":
40
+ return "\t";
41
+ default:
42
+ return ch;
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,105 @@
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
+ import { boundedConcurrently } from "@intentius/chant/observation";
10
+ import { getPolicy, listPolicies, } from "./client.js";
11
+ import { decodeOwnershipDescription } from "./ownership.js";
12
+ import { policyIdFromStatement } from "./statement.js";
13
+ /** The env var an environment's policy store is named in when config does not name it. */
14
+ export const AVP_POLICY_STORE_ENV = "CEDAR_AVP_POLICY_STORE_ID";
15
+ /** Per-environment override: `CEDAR_AVP_POLICY_STORE_ID_PROD`. */
16
+ export function policyStoreEnvKey(environment) {
17
+ return `${AVP_POLICY_STORE_ENV}_${environment.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
18
+ }
19
+ /**
20
+ * Which policy store this environment means.
21
+ *
22
+ * Resolution order, most specific first:
23
+ *
24
+ * 1. an explicit `policyStoreId` from the caller;
25
+ * 2. a `policyStoreId` prop on a declared policy — the entity-prop
26
+ * pass-through the observation guide recommends for finding the cloud-side
27
+ * identifier, and the form a project uses when it declares the store in the
28
+ * same build;
29
+ * 3. `CEDAR_AVP_POLICY_STORE_ID_<ENV>`, then `CEDAR_AVP_POLICY_STORE_ID`.
30
+ *
31
+ * `undefined` means no binding, and every caller turns that into `no-binding`
32
+ * rather than an empty read. An empty read would say the store holds nothing,
33
+ * which is a claim about an estate nobody looked at.
34
+ */
35
+ export function resolvePolicyStoreId(options) {
36
+ if (options.policyStoreId)
37
+ return options.policyStoreId;
38
+ for (const [, entity] of options.entities ?? new Map()) {
39
+ const declared = entity.props.policyStoreId;
40
+ if (typeof declared === "string" && declared.length > 0)
41
+ return declared;
42
+ }
43
+ const env = options.env ?? process.env;
44
+ const scoped = env[policyStoreEnvKey(options.environment)];
45
+ if (scoped)
46
+ return scoped;
47
+ return env[AVP_POLICY_STORE_ENV] || undefined;
48
+ }
49
+ /**
50
+ * Every policy in the store, each resolved to its Cedar id where possible.
51
+ *
52
+ * The marker is the cheap path: `ListPolicies` returns descriptions, so a
53
+ * chant-stamped policy is identified in the enumeration itself. Anything
54
+ * unmarked — a console edit, another tool's policy — costs one `GetPolicy` to
55
+ * read the `@id` out of its statement, run through the shared bounded pool so a
56
+ * store of two hundred policies is not two hundred serial round trips.
57
+ *
58
+ * A per-policy `GetPolicy` failure leaves that policy without a statement and
59
+ * without a Cedar id rather than sinking the enumeration: the caller then treats
60
+ * the entity it would have matched as not-observed, which is the honest verdict.
61
+ */
62
+ export async function loadLivePolicies(options) {
63
+ const summaries = await listPolicies(options.policyStoreId, options.client);
64
+ const live = summaries.map((summary) => {
65
+ const decoded = decodeOwnershipDescription(summary.description);
66
+ return {
67
+ ...summary,
68
+ ...(decoded.policyId ? { cedarPolicyId: decoded.policyId } : {}),
69
+ authoredDescription: decoded.text,
70
+ marked: decoded.marked,
71
+ };
72
+ });
73
+ const needsStatement = live.filter((policy) => options.withStatements || policy.cedarPolicyId === undefined);
74
+ await boundedConcurrently(needsStatement, async (policy) => {
75
+ try {
76
+ const detail = await getPolicy(options.policyStoreId, policy.policyId, options.client);
77
+ policy.statement = detail.statement;
78
+ if (policy.cedarPolicyId === undefined) {
79
+ const fromStatement = policyIdFromStatement(detail.statement);
80
+ if (fromStatement)
81
+ policy.cedarPolicyId = fromStatement;
82
+ }
83
+ }
84
+ catch {
85
+ // One unreadable policy is not a verdict about the store. It stays in the
86
+ // list without a statement, so anything that depended on reading it
87
+ // reports a hole instead of an absence.
88
+ }
89
+ });
90
+ return live;
91
+ }
92
+ /** Index live policies by the Cedar id a chant entity resolves to. */
93
+ export function indexByCedarId(policies) {
94
+ const index = new Map();
95
+ for (const policy of policies) {
96
+ if (policy.cedarPolicyId === undefined)
97
+ continue;
98
+ // First wins: two policies claiming one id is a live-side collision, and
99
+ // silently preferring the later one would make the observation depend on
100
+ // enumeration order.
101
+ if (!index.has(policy.cedarPolicyId))
102
+ index.set(policy.cedarPolicyId, policy);
103
+ }
104
+ return index;
105
+ }
@@ -0,0 +1,100 @@
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
+ import { encodeOwnershipDescription } from "../ownership.js";
11
+ /** A chant-stamped description for a policy the fake store should read as owned. */
12
+ export function markedDescription(policyId, text = "", marker = { stack: "authz", env: "prod" }) {
13
+ return encodeOwnershipDescription(text, marker, policyId);
14
+ }
15
+ /** A statement carrying the `@id` a chant entity resolves to. */
16
+ export function statementFor(policyId, body = "permit (\n principal,\n action,\n resource\n)") {
17
+ return `@id("${policyId}")\n${body};`;
18
+ }
19
+ /** Build an {@link AvpHttp} over one fake store. */
20
+ export function mockAvpTransport(store) {
21
+ const calls = [];
22
+ const pageSize = store.pageSize ?? 50;
23
+ const error = (type, message, status = 400) => ({
24
+ status,
25
+ text: JSON.stringify({ __type: `com.amazonaws.verifiedpermissions#${type}`, message }),
26
+ });
27
+ const definitionOf = (policy, withStatement) => ({
28
+ static: {
29
+ ...(policy.description !== undefined ? { description: policy.description } : {}),
30
+ ...(withStatement ? { statement: policy.statement } : {}),
31
+ },
32
+ });
33
+ const http = async (_url, init) => {
34
+ const operation = (init.headers["x-amz-target"] ?? "").split(".").pop() ?? "";
35
+ const payload = JSON.parse(init.body);
36
+ calls.push({ operation, payload });
37
+ if (payload.policyStoreId !== undefined && payload.policyStoreId !== store.policyStoreId) {
38
+ return error("ResourceNotFoundException", `No policy store: ${String(payload.policyStoreId)}`, 404);
39
+ }
40
+ switch (operation) {
41
+ case "ListPolicies": {
42
+ if (store.listFails) {
43
+ return error(store.listFails.code, store.listFails.message, store.listFails.status ?? 400);
44
+ }
45
+ const from = typeof payload.nextToken === "string" ? Number(payload.nextToken) : 0;
46
+ const page = store.policies.slice(from, from + pageSize);
47
+ const next = from + pageSize < store.policies.length ? String(from + pageSize) : undefined;
48
+ return {
49
+ status: 200,
50
+ text: JSON.stringify({
51
+ policies: page.map((policy) => ({
52
+ policyStoreId: store.policyStoreId,
53
+ policyId: policy.policyId,
54
+ policyType: policy.policyType ?? "STATIC",
55
+ definition: definitionOf(policy, false),
56
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
57
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
58
+ })),
59
+ ...(next ? { nextToken: next } : {}),
60
+ }),
61
+ };
62
+ }
63
+ case "GetPolicy": {
64
+ const policy = store.policies.find((p) => p.policyId === payload.policyId);
65
+ if (!policy)
66
+ return error("ResourceNotFoundException", "No such policy", 404);
67
+ if (policy.getFails)
68
+ return error("ThrottlingException", "Rate exceeded", 429);
69
+ return {
70
+ status: 200,
71
+ text: JSON.stringify({
72
+ policyStoreId: store.policyStoreId,
73
+ policyId: policy.policyId,
74
+ policyType: policy.policyType ?? "STATIC",
75
+ definition: definitionOf(policy, true),
76
+ ...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
77
+ ...(policy.lastUpdatedDate ? { lastUpdatedDate: policy.lastUpdatedDate } : {}),
78
+ }),
79
+ };
80
+ }
81
+ case "GetPolicyStore":
82
+ return {
83
+ status: 200,
84
+ text: JSON.stringify({
85
+ policyStoreId: store.policyStoreId,
86
+ arn: store.arn ?? `arn:aws:verifiedpermissions::111122223333:policy-store/${store.policyStoreId}`,
87
+ }),
88
+ };
89
+ case "ListTagsForResource":
90
+ return { status: 200, text: JSON.stringify({ tags: store.tags ?? {} }) };
91
+ default:
92
+ return error("UnknownOperationException", `no mock for ${operation}`, 400);
93
+ }
94
+ };
95
+ return { http, calls };
96
+ }
97
+ /** The env a reader needs to consider itself credentialed against a fake endpoint. */
98
+ export const MOCK_ENV = {
99
+ AWS_ENDPOINT_URL: "http://localhost:4566",
100
+ };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Thin entry point for `npm run docs` in lexicon-cedar.
4
+ */
5
+ import { generateDocs } from "./docs.js";
6
+ await generateDocs({ verbose: true });