@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,90 @@
1
+ import { NamingStrategy } from "@intentius/chant/codegen/naming";
2
+ /**
3
+ * The name of the policy resource every generated bundle carries.
4
+ *
5
+ * It is not schema-derived — a Cedar schema declares entity types and actions,
6
+ * never policies — so it is claimed as a priority name before any schema type
7
+ * gets to contest a short name. A project whose schema declares its own
8
+ * `Policy` entity type therefore gets `AppPolicy` (or whatever its namespace
9
+ * is), and the authoring class keeps the name users import.
10
+ */
11
+ export const POLICY_TS_NAME = "Policy";
12
+ /** The `entityType` of the policy resource, matching the serializer. */
13
+ export const POLICY_TYPE = "Cedar::Policy";
14
+ /** `read` → `Read`; `service-account` → `ServiceAccount`. */
15
+ export function pascalCase(value) {
16
+ return value
17
+ .split(/[^a-zA-Z0-9]+/)
18
+ .filter(Boolean)
19
+ .map((part) => part[0].toUpperCase() + part.slice(1))
20
+ .join("");
21
+ }
22
+ /** Split `App::Action::"read"` into its action id, or null for a non-action. */
23
+ export function actionIdOf(typeName) {
24
+ const match = /(?:^|::)Action::"(.*)"$/.exec(typeName);
25
+ return match ? match[1] : null;
26
+ }
27
+ /**
28
+ * Naming configuration for the cedar lexicon.
29
+ *
30
+ * Two shapes go through this rather than one: `App::User` (an entity type) and
31
+ * `App::Action::"read"` (an action). Both are entities in Cedar's model, so
32
+ * both are contested in the same pool — a schema with an entity type called
33
+ * `Read` and an action called `read` collides, and phase 4 qualifies the loser
34
+ * with its namespace rather than one silently overwriting the other.
35
+ */
36
+ export const namingConfig = {
37
+ priorityNames: {
38
+ [POLICY_TYPE]: POLICY_TS_NAME,
39
+ },
40
+ priorityAliases: {},
41
+ priorityPropertyAliases: {},
42
+ serviceAbbreviations: {},
43
+ /**
44
+ * Actions get an `Action` suffix. Without it `read`/`write`/`delete` become
45
+ * `Read`/`Write`/`Delete`, which read as entity types at the import site and
46
+ * collide with them in schemas that have both.
47
+ */
48
+ shortName: (typeName) => {
49
+ const actionId = actionIdOf(typeName);
50
+ if (actionId !== null)
51
+ return `${pascalCase(actionId)}Action`;
52
+ return pascalCase(typeName.split("::").pop() ?? typeName);
53
+ },
54
+ /** The namespace — `App::User` → `App`, and the empty namespace → `Cedar`. */
55
+ serviceName: (typeName) => {
56
+ const segments = typeName.split("::");
57
+ return segments.length > 1 ? segments[0] : "Cedar";
58
+ },
59
+ };
60
+ /**
61
+ * Create a NamingStrategy instance from parsed results.
62
+ *
63
+ * `Cedar::Policy` is injected as an input because it has to be *in* the contest
64
+ * to win it: `priorityNames` is applied over the input type names, so a policy
65
+ * type that never appears there cannot claim `Policy` before a schema's own
66
+ * `Policy` entity type takes it in the short-name phase.
67
+ */
68
+ export function createNaming(inputs) {
69
+ return new NamingStrategy([{ typeName: POLICY_TYPE, propertyTypes: [] }, ...inputs], namingConfig);
70
+ }
71
+ /**
72
+ * The names derived from a primary name — the entity's attribute record and
73
+ * the action's context record.
74
+ *
75
+ * These are not contested by `NamingStrategy` (it only names the primary
76
+ * declarations), so they are de-duplicated against everything already taken.
77
+ * The suffix loop is deliberately boring: a schema that manages to collide
78
+ * `UserAttributes` twice gets `UserAttributes2`, which is ugly and correct,
79
+ * rather than one entry silently overwriting the other in the registry.
80
+ */
81
+ export function deriveName(base, suffix, taken) {
82
+ let candidate = `${base}${suffix}`;
83
+ let n = 2;
84
+ while (taken.has(candidate)) {
85
+ candidate = `${base}${suffix}${n}`;
86
+ n++;
87
+ }
88
+ taken.add(candidate);
89
+ return candidate;
90
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cedar lexicon packaging — delegates to core's packagePipeline.
3
+ */
4
+ import { cedarPlugin } from "../plugin.js";
5
+ import { readFileSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ import { moduleSourceDir } from "@intentius/chant/module-source";
8
+ import { collectSkills, packagePipeline, } from "@intentius/chant/codegen/package";
9
+ import { generate } from "./generate.js";
10
+ const pkgDir = dirname(moduleSourceDir(import.meta.url));
11
+ /**
12
+ * Package the Cedar lexicon into a distributable BundleSpec.
13
+ */
14
+ export async function packageLexicon(opts = {}) {
15
+ const pkgJson = JSON.parse(readFileSync(join(pkgDir, "..", "package.json"), "utf-8"));
16
+ return packagePipeline({
17
+ // Codegen reads this package's own directory as the project root, so
18
+ // bundling produces the default-schema surface no matter where the
19
+ // command is run from (#1650).
20
+ generate: (genOpts) => generate({ ...genOpts, projectRoot: pkgDir }),
21
+ buildManifest: () => ({
22
+ name: "cedar",
23
+ version: pkgJson.version ?? "0.0.0",
24
+ chantVersion: ">=0.1.0",
25
+ namespace: "Cedar",
26
+ intrinsics: [],
27
+ pseudoParameters: {},
28
+ }),
29
+ srcDir: pkgDir,
30
+ collectSkills: () => collectSkills(cedarPlugin.skills?.() ?? []),
31
+ version: pkgJson.version ?? "0.0.0",
32
+ }, opts);
33
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `chant cedar <verb>` — the consumer-facing surface over this lexicon's
3
+ * codegen (#1696), mounted through the command-group seam (#1078).
4
+ *
5
+ * `chant dev generate` exists, but it is lexicon development: it runs every
6
+ * configured lexicon's generate, validate and coverage in turn, and for a
7
+ * project that also lists `aws` that means fetching the CloudFormation spec.
8
+ * A consumer regenerating its own Cedar classes after a schema edit wants one
9
+ * lexicon, one step, and an output path it can see.
10
+ */
11
+ import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
12
+ function parseFlags(verb, raw, accept) {
13
+ const args = splitJoinedFlags(raw, new Set(["--verbose", "-v"]));
14
+ const flags = { verbose: false };
15
+ for (let i = 0; i < args.length; i++) {
16
+ const a = args[i];
17
+ if ((a === "--verbose" || a === "-v") && accept.has("--verbose")) {
18
+ flags.verbose = true;
19
+ }
20
+ else if (a === "--out-dir" && accept.has("--out-dir")) {
21
+ const value = args[++i];
22
+ if (!value)
23
+ throw new Error(`--out-dir needs a directory.`);
24
+ flags.outDir = value;
25
+ }
26
+ else if (a === "--min-overall" && accept.has("--min-overall")) {
27
+ const value = Number(args[++i]);
28
+ if (!Number.isFinite(value))
29
+ throw new Error(`--min-overall needs a number.`);
30
+ flags.minOverall = value;
31
+ }
32
+ else {
33
+ const hint = `"chant cedar ${verb}" accepts ${[...accept].join(", ")}.`;
34
+ throw unknownFlagError(a, hint);
35
+ }
36
+ }
37
+ return flags;
38
+ }
39
+ async function generateHandler(ctx) {
40
+ const flags = parseFlags("generate", ctx.rawArgs, new Set(["--out-dir", "--verbose"]));
41
+ const { generate, resolveGeneratedDir, writeGeneratedFiles } = await import("./codegen/generate.js");
42
+ const { loadCedarProject } = await import("./config.js");
43
+ const project = await loadCedarProject(process.cwd());
44
+ // A flag beats the config, and both beat the default — the same order
45
+ // `cedar.schema` follows one level down.
46
+ const config = flags.outDir ? { ...project.config, outDir: flags.outDir } : project.config;
47
+ const result = await generate({ verbose: flags.verbose, projectRoot: project.projectRoot, config });
48
+ const outDir = resolveGeneratedDir({ projectRoot: project.projectRoot, config });
49
+ writeGeneratedFiles(result, outDir);
50
+ console.error(`cedar: generated ${result.resources} declaration(s) into ${outDir}`);
51
+ return 0;
52
+ }
53
+ async function coverageHandler(ctx) {
54
+ const flags = parseFlags("coverage", ctx.rawArgs, new Set(["--min-overall", "--verbose"]));
55
+ const { analyzeCedarCoverage } = await import("./coverage.js");
56
+ const { resolveGeneratedDir } = await import("./codegen/generate.js");
57
+ const { loadCedarProject } = await import("./config.js");
58
+ const { projectRoot, config } = await loadCedarProject(process.cwd());
59
+ analyzeCedarCoverage({
60
+ projectRoot,
61
+ config,
62
+ generatedDir: resolveGeneratedDir({ projectRoot, config }),
63
+ verbose: flags.verbose,
64
+ minOverall: flags.minOverall,
65
+ });
66
+ return 0;
67
+ }
68
+ /** The `chant cedar` verb group. */
69
+ export function cedarCommandGroup() {
70
+ return {
71
+ name: "cedar",
72
+ description: "Schema-driven codegen for a project's Cedar policies: generate typed classes into the project, check schema coverage",
73
+ commands: [
74
+ {
75
+ name: "generate",
76
+ description: "Read the project's .cedarschema and write typed classes into cedar.outDir (default src/generated/cedar)",
77
+ handler: generateHandler,
78
+ },
79
+ {
80
+ name: "coverage",
81
+ description: "Report which schema declarations the generated classes cover",
82
+ handler: coverageHandler,
83
+ },
84
+ ],
85
+ };
86
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * DenyByDefaultSet — a forbid floor under a group of permits.
3
+ *
4
+ * Cedar is already default-deny, so a floor is not about the absence of a
5
+ * permit; it is about the presence of a bad one. A `forbid` beats every
6
+ * `permit` in the same set unconditionally, which makes a forbid the only
7
+ * construct that survives a later, wider grant. The pattern teams write by hand
8
+ * is a guarded forbid at the top of a file and a pile of permits under it, with
9
+ * nothing tying the two together — delete the forbid and the permits keep
10
+ * working, wider than anyone intended.
11
+ *
12
+ * This composite returns both halves from one call, so the floor cannot be
13
+ * dropped without dropping the members with it.
14
+ */
15
+ import { Policy } from "../generated/index.js";
16
+ function scope(value) {
17
+ if (value === undefined)
18
+ return {};
19
+ return typeof value === "string" ? { is: value } : value;
20
+ }
21
+ function actionScope(actions) {
22
+ if (actions === undefined)
23
+ return {};
24
+ if (Array.isArray(actions)) {
25
+ return actions.length === 1 ? { eq: actions[0] } : { in: actions };
26
+ }
27
+ return { eq: actions };
28
+ }
29
+ /**
30
+ * A guarded `forbid` plus the permits it governs.
31
+ */
32
+ export function DenyByDefaultSet(opts) {
33
+ if (opts.when.length === 0) {
34
+ throw new Error("DenyByDefaultSet: `when` must carry at least one condition. An unguarded forbid " +
35
+ "overrides every permit in the set, so the result would authorize nothing.");
36
+ }
37
+ const floor = new Policy({
38
+ effect: "forbid",
39
+ principal: scope(opts.principal),
40
+ action: actionScope(opts.actions),
41
+ resource: scope(opts.entityType),
42
+ when: opts.when,
43
+ ...(opts.unless && opts.unless.length > 0 ? { unless: opts.unless } : {}),
44
+ annotations: {
45
+ composite: "DenyByDefaultSet",
46
+ members: String(opts.policies.length),
47
+ ...(opts.annotations ?? {}),
48
+ },
49
+ });
50
+ return { floor, members: [...opts.policies], all: [floor, ...opts.policies] };
51
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Cedar lexicon composites.
3
+ *
4
+ * Cedar has no functions, no modules, no loops, and templates with exactly two
5
+ * slots — so every repeated policy shape is copy-paste in `.cedar` text. These
6
+ * factories are where that repetition goes instead.
7
+ */
8
+ export { OwnerCanManage } from "./owner-can-manage.js";
9
+ export { DenyByDefaultSet } from "./deny-by-default-set.js";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * OwnerCanManage — the ownership grant, written once.
3
+ *
4
+ * "The owner of a thing may act on it" is the most-repeated shape in every
5
+ * Cedar policy set, and hand-writing it per entity type is where the two
6
+ * classic mistakes get made: the `when` guard names an attribute the schema
7
+ * spells differently, or the grant is written wide and the scoping clause is
8
+ * forgotten. Cedar itself cannot help — it has no functions, no modules, and
9
+ * templates carry exactly two slots. A TypeScript factory is the only place
10
+ * this abstraction can live.
11
+ *
12
+ * The composite emits a permit whose resource scope is pinned with `is` and
13
+ * whose ownership test is a `when` clause, so both halves arrive together or
14
+ * neither does.
15
+ */
16
+ import { Policy } from "../generated/index.js";
17
+ function actionScope(actions) {
18
+ if (actions === undefined)
19
+ return {};
20
+ if (Array.isArray(actions)) {
21
+ return actions.length === 1 ? { eq: actions[0] } : { in: actions };
22
+ }
23
+ return { eq: actions };
24
+ }
25
+ function principalScope(principal) {
26
+ if (principal === undefined)
27
+ return {};
28
+ return typeof principal === "string" ? { is: principal } : principal;
29
+ }
30
+ /**
31
+ * One permit: the owner of an `entityType` may take `actions` on it.
32
+ */
33
+ export function OwnerCanManage(opts) {
34
+ const ownerAttribute = opts.ownerAttribute ?? "owner";
35
+ return new Policy({
36
+ effect: "permit",
37
+ principal: principalScope(opts.principal),
38
+ action: actionScope(opts.actions),
39
+ resource: { is: opts.entityType },
40
+ when: [`resource.${ownerAttribute} == principal`, ...(opts.when ?? [])],
41
+ ...(opts.unless && opts.unless.length > 0 ? { unless: opts.unless } : {}),
42
+ annotations: {
43
+ composite: "OwnerCanManage",
44
+ scopedTo: opts.entityType,
45
+ ...(opts.annotations ?? {}),
46
+ },
47
+ });
48
+ }
package/dist/config.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The `cedar` namespace in `chant.config.ts` (#1344, #1650).
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-cedar"; // brings the `cedar` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["cedar"],
10
+ * cedar: {
11
+ * schema: "authz/app.cedarschema",
12
+ * validation: { mode: "strict", warnings: "warn" },
13
+ * },
14
+ * } satisfies ChantConfig;
15
+ * ```
16
+ *
17
+ * Cedar is the one lexicon whose codegen input is the *project's* file rather
18
+ * than a global upstream (epic #1645), so this namespace is not a convenience —
19
+ * it is where `generate()` is told what to read. A typo in the path is the
20
+ * difference between "your entity types" and "the bundled default schema",
21
+ * which is exactly the kind of silent fallback `z.strictObject` exists to stop
22
+ * one level down.
23
+ */
24
+ import { z } from "zod";
25
+ /**
26
+ * Where `generate()` looks when `cedar.schema` is unset.
27
+ *
28
+ * Relative to the project root (the directory holding `chant.config.ts`).
29
+ */
30
+ export const CEDAR_DEFAULT_SCHEMA_PATH = "schema.cedarschema";
31
+ /**
32
+ * Where `generate()` writes when `cedar.outDir` is unset and the project is a
33
+ * consumer of the published package (#1696).
34
+ *
35
+ * Relative to the project root. Inside this lexicon's own checkout the
36
+ * package directory *is* the project root, and the output stays at
37
+ * `src/generated/` so `src/index.ts` can re-export it; see
38
+ * `resolveGeneratedDir` in `codegen/generate.ts`.
39
+ */
40
+ export const CEDAR_DEFAULT_OUT_DIR = "src/generated/cedar";
41
+ /**
42
+ * `strictObject`, not `object`. Core applies `.strict()` to the top level of a
43
+ * declared namespace itself; `validation` is nested, and is the lexicon's own
44
+ * to close.
45
+ */
46
+ export const cedarConfigSchema = z.strictObject({
47
+ /**
48
+ * Path to the project's human-readable Cedar schema, relative to the project
49
+ * root. Defaults to {@link CEDAR_DEFAULT_SCHEMA_PATH}; when neither the
50
+ * configured path nor the default exists, generation falls back to the
51
+ * schema bundled at `src/spec/default-schema.cedarschema` so a fresh
52
+ * checkout still produces artifacts.
53
+ */
54
+ schema: z.string().optional(),
55
+ /**
56
+ * Directory `generate()` writes the typed classes into, relative to the
57
+ * project root. Defaults to {@link CEDAR_DEFAULT_OUT_DIR}.
58
+ *
59
+ * The output used to land in the installed package's own `src/generated/`,
60
+ * which `npm ci` wipes (#1696). It is the project's artifact — the classes
61
+ * describe the project's schema, not the package's — so it lives in the
62
+ * project tree, and `src/policies.ts` imports from it rather than from
63
+ * `@intentius/chant-lexicon-cedar`. Commit it or regenerate it in CI; either
64
+ * way it is never under `node_modules`.
65
+ */
66
+ outDir: z.string().optional(),
67
+ /**
68
+ * Knobs for the `cedar-wasm` validator. `mode` is a single-variant enum on
69
+ * purpose: `ValidationMode` in cedar-wasm 4.12.0 accepts `"strict"` and
70
+ * nothing else, and passing anything further throws (#1648 §5.7).
71
+ */
72
+ validation: z
73
+ .strictObject({
74
+ /** Validator mode. Only `"strict"` exists in this build of cedar-wasm. */
75
+ mode: z.literal("strict").optional(),
76
+ /**
77
+ * What to do with `validationWarnings` — the validator reports things
78
+ * like "policy is impossible" here, separately from errors.
79
+ */
80
+ warnings: z.enum(["ignore", "warn", "error"]).optional(),
81
+ /**
82
+ * Refuse to build when no project schema is found rather than falling
83
+ * back to the bundled default. Off by default so `chant init` works;
84
+ * worth turning on once a project has its own schema, because
85
+ * `checkParseSchema("")` succeeds and an empty schema validates
86
+ * everything clean (#1648 §1).
87
+ */
88
+ requireProjectSchema: z.boolean().optional(),
89
+ })
90
+ .optional(),
91
+ /**
92
+ * The dogwood dialect's CLI-gated leg (#1659).
93
+ *
94
+ * `strictObject` again, for the reason the outer one is: a typo here is the
95
+ * difference between "full .dw validation ran" and "DWDE010 said it could
96
+ * not run", and the second is easy to skim past.
97
+ */
98
+ dogwood: z
99
+ .strictObject({
100
+ /**
101
+ * Path to the `dogwood` binary, absolute or relative to the config file.
102
+ *
103
+ * Optional, and its absence is not an error: DWDE010 warns-and-skips
104
+ * with an issue-linked advisory when no binary is found, which is the
105
+ * epic's explicit exception. Set this when the binary is built somewhere
106
+ * `PATH` does not reach — the on-demand harness shape the epic borrows
107
+ * from forgejo-runtime-e2e.
108
+ *
109
+ * A `chant.config.ts` cannot be read from inside a post-synth check —
110
+ * `check()` is synchronous and evaluating project code is not — so this
111
+ * key is honoured from `chant.config.json` and the `CHANT_DOGWOOD_BINARY`
112
+ * environment variable covers the TypeScript-config case. See
113
+ * `src/dogwood/cli.ts`.
114
+ */
115
+ binary: z.string().optional(),
116
+ })
117
+ .optional(),
118
+ });
119
+ /**
120
+ * Read the `cedar` namespace out of the project's config.
121
+ *
122
+ * Searches upward from `startDir`, so `chant cedar generate` finds the config from a
123
+ * subdirectory the same way `chant build` does. A project with no config, or
124
+ * one with no `cedar` key, gets `{}` — the schema resolution order in
125
+ * `spec/fetch.ts` handles that case without needing to know why.
126
+ *
127
+ * Core has already validated the namespace against {@link cedarConfigSchema}
128
+ * by the time this reads it, so the parse here is a type narrowing rather than
129
+ * a second gate; it stays because a caller can point this at a directory core
130
+ * never loaded.
131
+ */
132
+ export async function loadCedarConfig(startDir) {
133
+ return (await loadCedarProject(startDir)).config;
134
+ }
135
+ /**
136
+ * {@link loadCedarConfig}, plus the directory the config was found in.
137
+ *
138
+ * `cedar.schema` and `cedar.outDir` are relative to the config file, not to
139
+ * wherever the command was run (#1696) — `chant cedar generate` from a subdirectory
140
+ * has to write to the same place as the same command from the root.
141
+ */
142
+ export async function loadCedarProject(startDir) {
143
+ const { loadChantConfigUpward } = await import("@intentius/chant/config");
144
+ const { dirname } = await import("path");
145
+ try {
146
+ const { config, configPath } = await loadChantConfigUpward(startDir);
147
+ const parsed = cedarConfigSchema.safeParse(config.cedar ?? {});
148
+ return {
149
+ projectRoot: configPath ? dirname(configPath) : startDir,
150
+ config: parsed.success ? parsed.data : {},
151
+ };
152
+ }
153
+ catch {
154
+ // A project whose config does not load is not this function's problem to
155
+ // report — every other command reports it first, and generation falling
156
+ // back to the bundled schema is the same behaviour as having no config.
157
+ return { projectRoot: startDir, config: {} };
158
+ }
159
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Schema coverage for the cedar lexicon.
3
+ *
4
+ * Core's `computeCoverage` measures CloudFormation-shaped dimensions —
5
+ * property constraints, create-only/write-only lifecycle, return attributes —
6
+ * none of which a Cedar schema has. Reporting 0% across four axes that cannot
7
+ * apply would be worse than reporting nothing, so this asks the question that
8
+ * does mean something here: **is every entity type and every action in the
9
+ * schema reachable from the generated artifacts?**
10
+ *
11
+ * A gap is a real defect, not a nice-to-have. An action with no generated
12
+ * constant is an action a policy can only name as a hand-typed string, which
13
+ * is the exact failure mode the lexicon exists to remove.
14
+ */
15
+ import { existsSync, readFileSync } from "fs";
16
+ import { join } from "path";
17
+ import { moduleSourceDir } from "@intentius/chant/module-source";
18
+ import { LEXICON_JSON_FILENAME } from "./codegen/generate.js";
19
+ import { attributesTypeName, contextTypeName } from "./codegen/emit.js";
20
+ import { readSchemaSource } from "./spec/fetch.js";
21
+ import { parseCedarSchema } from "./spec/parse.js";
22
+ function defaultGeneratedDir() {
23
+ return join(moduleSourceDir(import.meta.url), "generated");
24
+ }
25
+ function pct(covered, total) {
26
+ return total === 0 ? 100 : Math.round((covered / total) * 100);
27
+ }
28
+ /** Compute schema coverage against the artifacts already on disk. */
29
+ export function computeCedarCoverage(options = {}) {
30
+ const generatedDir = options.generatedDir ?? defaultGeneratedDir();
31
+ const registryPath = join(generatedDir, LEXICON_JSON_FILENAME);
32
+ const covered = new Set();
33
+ if (existsSync(registryPath)) {
34
+ const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
35
+ for (const entry of Object.values(registry)) {
36
+ if (entry.resourceType)
37
+ covered.add(entry.resourceType);
38
+ }
39
+ }
40
+ const source = readSchemaSource(options);
41
+ const { decls } = parseCedarSchema(source.text);
42
+ const items = decls
43
+ .map((decl) => {
44
+ const recordName = decl.kind === "entity" ? attributesTypeName(decl.typeName) : contextTypeName(decl.typeName);
45
+ return {
46
+ name: decl.typeName,
47
+ kind: decl.kind,
48
+ covered: covered.has(decl.typeName),
49
+ recordCovered: covered.has(recordName),
50
+ attributes: decl.kind === "entity" ? decl.attributes.length : decl.context.length,
51
+ };
52
+ })
53
+ .sort((a, b) => a.name.localeCompare(b.name));
54
+ const entities = items.filter((i) => i.kind === "entity");
55
+ const actions = items.filter((i) => i.kind === "action");
56
+ const entityTypesCovered = entities.filter((i) => i.covered).length;
57
+ const actionsCovered = actions.filter((i) => i.covered).length;
58
+ const recordTypesCovered = items.filter((i) => i.recordCovered).length;
59
+ const entityPct = pct(entityTypesCovered, entities.length);
60
+ const actionPct = pct(actionsCovered, actions.length);
61
+ const recordPct = pct(recordTypesCovered, items.length);
62
+ return {
63
+ schemaPath: source.path,
64
+ isDefaultSchema: source.isDefault,
65
+ entityTypes: entities.length,
66
+ entityTypesCovered,
67
+ actions: actions.length,
68
+ actionsCovered,
69
+ recordTypes: items.length,
70
+ recordTypesCovered,
71
+ entityPct,
72
+ actionPct,
73
+ recordPct,
74
+ overallPct: Math.round((entityPct + actionPct + recordPct) / 3),
75
+ items,
76
+ };
77
+ }
78
+ /** Human-readable summary. */
79
+ export function formatCedarCoverage(report, verbose = false) {
80
+ const lines = [
81
+ `Cedar schema coverage — ${report.isDefaultSchema ? "bundled default schema" : report.schemaPath}`,
82
+ ` Entity types: ${report.entityTypesCovered}/${report.entityTypes} (${report.entityPct}%)`,
83
+ ` Actions: ${report.actionsCovered}/${report.actions} (${report.actionPct}%)`,
84
+ ` Record types: ${report.recordTypesCovered}/${report.recordTypes} (${report.recordPct}%)`,
85
+ ` Overall: ${report.overallPct}%`,
86
+ ];
87
+ if (verbose) {
88
+ lines.push("");
89
+ for (const item of report.items) {
90
+ const gaps = [];
91
+ if (!item.covered)
92
+ gaps.push("no generated declaration");
93
+ if (!item.recordCovered)
94
+ gaps.push(item.kind === "entity" ? "no attributes type" : "no context type");
95
+ lines.push(gaps.length === 0
96
+ ? ` ${item.name}: covered (${item.attributes} attribute(s))`
97
+ : ` ${item.name}: ${gaps.join(", ")}`);
98
+ }
99
+ }
100
+ return lines.join("\n");
101
+ }
102
+ /**
103
+ * Print the report, and throw when it falls below `minOverall`.
104
+ */
105
+ export function analyzeCedarCoverage(options = {}) {
106
+ const report = computeCedarCoverage(options);
107
+ console.error(formatCedarCoverage(report, options.verbose));
108
+ if (options.minOverall !== undefined && report.overallPct < options.minOverall) {
109
+ throw new Error(`Cedar schema coverage ${report.overallPct}% is below the ${options.minOverall}% minimum`);
110
+ }
111
+ return report;
112
+ }
package/dist/detect.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Edge-safe template detection for cedar (#426, #1653).
3
+ *
4
+ * Two documents belong to this lexicon: the JSON policy-set envelope the
5
+ * serializer writes, and `.cedar` policy text. The first is recognized by its
6
+ * shape and then confirmed by `checkParsePolicySet`; the second only by asking
7
+ * the module to split it.
8
+ *
9
+ * The shape test is strict on purpose. `chant import` walks every loaded
10
+ * plugin's `detectTemplate` in turn, so a lexicon that answers `true` for
11
+ * someone else's JSON steals the file. `staticPolicies`/`templates`/
12
+ * `templateLinks` and nothing else is a document no other lexicon writes, and
13
+ * a policy set with no policies in it is not claimed at all — `{}` parses
14
+ * perfectly well as an empty Cedar policy set, and matching it would swallow
15
+ * every empty JSON object in the repo.
16
+ */
17
+ import { isPolicySetEnvelope } from "./import/parser.js";
18
+ import { parsesAsPolicySet, splitPolicySet } from "./spec/wasm.js";
19
+ /** Does the envelope actually carry a policy or a template? */
20
+ function hasContent(envelope) {
21
+ const count = (value) => {
22
+ if (Array.isArray(value))
23
+ return value.length;
24
+ if (value && typeof value === "object")
25
+ return Object.keys(value).length;
26
+ if (typeof value === "string")
27
+ return value.trim().length > 0 ? 1 : 0;
28
+ return 0;
29
+ };
30
+ return count(envelope.staticPolicies) + count(envelope.templates) > 0;
31
+ }
32
+ /** Is this text a Cedar policy document with at least one policy in it? */
33
+ function isPolicyText(content) {
34
+ if (!content.trim())
35
+ return false;
36
+ const parts = splitPolicySet(content);
37
+ return parts.ok && parts.value.policies.length + parts.value.templates.length > 0;
38
+ }
39
+ export function detectTemplate(data) {
40
+ if (typeof data === "string") {
41
+ const trimmed = data.trim();
42
+ if (!trimmed)
43
+ return false;
44
+ // A string may still be the envelope — `chant import` hands the parsed
45
+ // object around, but the audit discovery path passes raw file contents.
46
+ if (trimmed.startsWith("{")) {
47
+ try {
48
+ return detectTemplate(JSON.parse(trimmed));
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
54
+ return isPolicyText(trimmed);
55
+ }
56
+ if (!isPolicySetEnvelope(data))
57
+ return false;
58
+ if (!hasContent(data))
59
+ return false;
60
+ return parsesAsPolicySet(data);
61
+ }