cedar-mcp-server 1.0.0

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 (215) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +42 -0
  4. package/.nvmrc +1 -0
  5. package/CHANGELOG.md +241 -0
  6. package/CONTRIBUTING.md +83 -0
  7. package/LICENSE +182 -0
  8. package/README.md +1635 -0
  9. package/SECURITY.md +37 -0
  10. package/dist/http-server.d.ts +61 -0
  11. package/dist/http-server.d.ts.map +1 -0
  12. package/dist/http-server.js +194 -0
  13. package/dist/http-server.js.map +1 -0
  14. package/dist/index.d.ts +32 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +270 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/parser/policy-ast.d.ts +49 -0
  19. package/dist/parser/policy-ast.d.ts.map +1 -0
  20. package/dist/parser/policy-ast.js +311 -0
  21. package/dist/parser/policy-ast.js.map +1 -0
  22. package/dist/prompts/index.d.ts +38 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +172 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/resources/ref-resolver.d.ts +23 -0
  27. package/dist/resources/ref-resolver.d.ts.map +1 -0
  28. package/dist/resources/ref-resolver.js +128 -0
  29. package/dist/resources/ref-resolver.js.map +1 -0
  30. package/dist/resources/store-manager.d.ts +64 -0
  31. package/dist/resources/store-manager.d.ts.map +1 -0
  32. package/dist/resources/store-manager.js +221 -0
  33. package/dist/resources/store-manager.js.map +1 -0
  34. package/dist/server.d.ts +18 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +539 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/tools/advise/avp-rules.d.ts +49 -0
  39. package/dist/tools/advise/avp-rules.d.ts.map +1 -0
  40. package/dist/tools/advise/avp-rules.js +59 -0
  41. package/dist/tools/advise/avp-rules.js.map +1 -0
  42. package/dist/tools/advise/cedar-patterns.d.ts +24 -0
  43. package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
  44. package/dist/tools/advise/cedar-patterns.js +57 -0
  45. package/dist/tools/advise/cedar-patterns.js.map +1 -0
  46. package/dist/tools/advise/context-builder.d.ts +28 -0
  47. package/dist/tools/advise/context-builder.d.ts.map +1 -0
  48. package/dist/tools/advise/context-builder.js +89 -0
  49. package/dist/tools/advise/context-builder.js.map +1 -0
  50. package/dist/tools/advise/gotchas.d.ts +15 -0
  51. package/dist/tools/advise/gotchas.d.ts.map +1 -0
  52. package/dist/tools/advise/gotchas.js +83 -0
  53. package/dist/tools/advise/gotchas.js.map +1 -0
  54. package/dist/tools/advise.d.ts +96 -0
  55. package/dist/tools/advise.d.ts.map +1 -0
  56. package/dist/tools/advise.js +258 -0
  57. package/dist/tools/advise.js.map +1 -0
  58. package/dist/tools/authorize-batch.d.ts +35 -0
  59. package/dist/tools/authorize-batch.d.ts.map +1 -0
  60. package/dist/tools/authorize-batch.js +262 -0
  61. package/dist/tools/authorize-batch.js.map +1 -0
  62. package/dist/tools/authorize.d.ts +115 -0
  63. package/dist/tools/authorize.d.ts.map +1 -0
  64. package/dist/tools/authorize.js +373 -0
  65. package/dist/tools/authorize.js.map +1 -0
  66. package/dist/tools/check-change.d.ts +19 -0
  67. package/dist/tools/check-change.d.ts.map +1 -0
  68. package/dist/tools/check-change.js +91 -0
  69. package/dist/tools/check-change.js.map +1 -0
  70. package/dist/tools/diff-schema.d.ts +103 -0
  71. package/dist/tools/diff-schema.d.ts.map +1 -0
  72. package/dist/tools/diff-schema.js +379 -0
  73. package/dist/tools/diff-schema.js.map +1 -0
  74. package/dist/tools/diff-stores.d.ts +45 -0
  75. package/dist/tools/diff-stores.d.ts.map +1 -0
  76. package/dist/tools/diff-stores.js +222 -0
  77. package/dist/tools/diff-stores.js.map +1 -0
  78. package/dist/tools/explain.d.ts +80 -0
  79. package/dist/tools/explain.d.ts.map +1 -0
  80. package/dist/tools/explain.js +187 -0
  81. package/dist/tools/explain.js.map +1 -0
  82. package/dist/tools/format.d.ts +11 -0
  83. package/dist/tools/format.d.ts.map +1 -0
  84. package/dist/tools/format.js +20 -0
  85. package/dist/tools/format.js.map +1 -0
  86. package/dist/tools/generate-sample.d.ts +28 -0
  87. package/dist/tools/generate-sample.d.ts.map +1 -0
  88. package/dist/tools/generate-sample.js +568 -0
  89. package/dist/tools/generate-sample.js.map +1 -0
  90. package/dist/tools/link-template.d.ts +17 -0
  91. package/dist/tools/link-template.d.ts.map +1 -0
  92. package/dist/tools/link-template.js +78 -0
  93. package/dist/tools/link-template.js.map +1 -0
  94. package/dist/tools/list-template-links.d.ts +16 -0
  95. package/dist/tools/list-template-links.d.ts.map +1 -0
  96. package/dist/tools/list-template-links.js +22 -0
  97. package/dist/tools/list-template-links.js.map +1 -0
  98. package/dist/tools/list-templates.d.ts +16 -0
  99. package/dist/tools/list-templates.d.ts.map +1 -0
  100. package/dist/tools/list-templates.js +36 -0
  101. package/dist/tools/list-templates.js.map +1 -0
  102. package/dist/tools/translate.d.ts +11 -0
  103. package/dist/tools/translate.d.ts.map +1 -0
  104. package/dist/tools/translate.js +53 -0
  105. package/dist/tools/translate.js.map +1 -0
  106. package/dist/tools/validate-entities.d.ts +19 -0
  107. package/dist/tools/validate-entities.d.ts.map +1 -0
  108. package/dist/tools/validate-entities.js +88 -0
  109. package/dist/tools/validate-entities.js.map +1 -0
  110. package/dist/tools/validate-schema.d.ts +22 -0
  111. package/dist/tools/validate-schema.d.ts.map +1 -0
  112. package/dist/tools/validate-schema.js +89 -0
  113. package/dist/tools/validate-schema.js.map +1 -0
  114. package/dist/tools/validate-template.d.ts +18 -0
  115. package/dist/tools/validate-template.d.ts.map +1 -0
  116. package/dist/tools/validate-template.js +59 -0
  117. package/dist/tools/validate-template.js.map +1 -0
  118. package/dist/tools/validate.d.ts +90 -0
  119. package/dist/tools/validate.d.ts.map +1 -0
  120. package/dist/tools/validate.js +351 -0
  121. package/dist/tools/validate.js.map +1 -0
  122. package/dist/utils/format-detector.d.ts +49 -0
  123. package/dist/utils/format-detector.d.ts.map +1 -0
  124. package/dist/utils/format-detector.js +298 -0
  125. package/dist/utils/format-detector.js.map +1 -0
  126. package/examples/README.md +36 -0
  127. package/examples/abac-multi-tenant/README.md +150 -0
  128. package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
  129. package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
  130. package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
  131. package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
  132. package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
  133. package/examples/abac-multi-tenant/run.ts +92 -0
  134. package/examples/abac-multi-tenant/schema.json +60 -0
  135. package/examples/api-gateway-path-routing/README.md +154 -0
  136. package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
  137. package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
  138. package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
  139. package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
  140. package/examples/api-gateway-path-routing/run.ts +108 -0
  141. package/examples/api-gateway-path-routing/schema.json +54 -0
  142. package/examples/rbac-document-management/README.md +167 -0
  143. package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
  144. package/examples/rbac-document-management/policies/admin.cedar +6 -0
  145. package/examples/rbac-document-management/policies/editor.cedar +6 -0
  146. package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
  147. package/examples/rbac-document-management/policies/viewer.cedar +6 -0
  148. package/examples/rbac-document-management/run.ts +87 -0
  149. package/examples/rbac-document-management/schema.json +57 -0
  150. package/package.json +50 -0
  151. package/src/http-server.ts +239 -0
  152. package/src/index.ts +294 -0
  153. package/src/parser/policy-ast.ts +345 -0
  154. package/src/prompts/README.md +3 -0
  155. package/src/prompts/index.ts +217 -0
  156. package/src/resources/ref-resolver.ts +134 -0
  157. package/src/resources/store-manager.ts +248 -0
  158. package/src/server.ts +711 -0
  159. package/src/tools/advise/avp-rules.ts +70 -0
  160. package/src/tools/advise/cedar-patterns.ts +73 -0
  161. package/src/tools/advise/context-builder.ts +109 -0
  162. package/src/tools/advise/gotchas.ts +92 -0
  163. package/src/tools/advise.ts +366 -0
  164. package/src/tools/authorize-batch.ts +345 -0
  165. package/src/tools/authorize.ts +464 -0
  166. package/src/tools/check-change.ts +119 -0
  167. package/src/tools/diff-schema.ts +510 -0
  168. package/src/tools/diff-stores.ts +298 -0
  169. package/src/tools/explain.ts +278 -0
  170. package/src/tools/format.ts +33 -0
  171. package/src/tools/generate-sample.ts +665 -0
  172. package/src/tools/link-template.ts +109 -0
  173. package/src/tools/list-template-links.ts +41 -0
  174. package/src/tools/list-templates.ts +55 -0
  175. package/src/tools/translate.ts +66 -0
  176. package/src/tools/validate-entities.ts +125 -0
  177. package/src/tools/validate-schema.ts +128 -0
  178. package/src/tools/validate-template.ts +72 -0
  179. package/src/tools/validate.ts +459 -0
  180. package/src/utils/format-detector.ts +356 -0
  181. package/test/fixtures/docmgmt.ts +121 -0
  182. package/test/fixtures/multitenant.ts +163 -0
  183. package/test/index.test.ts +96 -0
  184. package/test/integration/e2e/behavior.test.ts +359 -0
  185. package/test/integration/e2e/edge-cases.test.ts +365 -0
  186. package/test/integration/e2e/failure-modes.test.ts +266 -0
  187. package/test/integration/e2e/protocol.test.ts +252 -0
  188. package/test/integration/http-smoke.test.ts +588 -0
  189. package/test/integration/smoke.test.ts +475 -0
  190. package/test/prompts/prompts.test.ts +173 -0
  191. package/test/property/properties.test.ts +234 -0
  192. package/test/resources/ref-resolver.test.ts +186 -0
  193. package/test/resources/store-manager.test.ts +344 -0
  194. package/test/setup.test.ts +7 -0
  195. package/test/tools/advise/avp-rules.test.ts +76 -0
  196. package/test/tools/advise.test.ts +339 -0
  197. package/test/tools/authorize-batch.test.ts +459 -0
  198. package/test/tools/authorize.test.ts +682 -0
  199. package/test/tools/check-change.test.ts +104 -0
  200. package/test/tools/cross-fixture.test.ts +170 -0
  201. package/test/tools/diff-schema.test.ts +355 -0
  202. package/test/tools/diff-stores.test.ts +291 -0
  203. package/test/tools/explain.test.ts +221 -0
  204. package/test/tools/format.test.ts +33 -0
  205. package/test/tools/generate-sample.test.ts +480 -0
  206. package/test/tools/link-template.test.ts +90 -0
  207. package/test/tools/list-templates.test.ts +151 -0
  208. package/test/tools/translate.test.ts +89 -0
  209. package/test/tools/validate-entities.test.ts +178 -0
  210. package/test/tools/validate-schema.test.ts +86 -0
  211. package/test/tools/validate-template.test.ts +89 -0
  212. package/test/tools/validate.test.ts +331 -0
  213. package/test/utils/format-detector.test.ts +518 -0
  214. package/tsconfig.json +17 -0
  215. package/vitest.config.ts +13 -0
@@ -0,0 +1,109 @@
1
+ import { templateToJson, policyToText, policyToJson, validate } from "@cedar-policy/cedar-wasm/nodejs";
2
+ import type { PolicyJson, DetailedError } from "@cedar-policy/cedar-wasm/nodejs";
3
+
4
+ export interface LinkTemplateInput {
5
+ template: string;
6
+ principal?: string;
7
+ resource?: string;
8
+ schema?: string;
9
+ }
10
+
11
+ export interface LinkTemplateResult {
12
+ linked_policy?: string;
13
+ slots_bound: Record<string, string>;
14
+ valid?: boolean;
15
+ errors?: Array<{ message: string }>;
16
+ error?: string;
17
+ }
18
+
19
+ interface EntityRef {
20
+ type: string;
21
+ id: string;
22
+ }
23
+
24
+ function parseEntityRef(ref: string): EntityRef | null {
25
+ // Expects: "Namespace::Type::\"id\"" or "Type::\"id\""
26
+ const match = ref.match(/^(.+)::"(.+)"$/);
27
+ if (!match) return null;
28
+ return { type: match[1]!, id: match[2]! };
29
+ }
30
+
31
+ export async function handleLinkTemplate(input: LinkTemplateInput): Promise<LinkTemplateResult> {
32
+ // Parse the template
33
+ const parseResult = templateToJson(input.template);
34
+ if (parseResult.type === "failure") {
35
+ const msg = parseResult.errors.map(e => e.message).join("; ");
36
+ return { slots_bound: {}, error: `Failed to parse template: ${msg}` };
37
+ }
38
+
39
+ const json = parseResult.json as unknown as Record<string, unknown>;
40
+
41
+ // Determine which slots are present
42
+ const principalSlot = (json.principal as Record<string, unknown>)?.slot === "?principal";
43
+ const resourceSlot = (json.resource as Record<string, unknown>)?.slot === "?resource";
44
+
45
+ const slots_bound: Record<string, string> = {};
46
+
47
+ // Validate that required slots are provided
48
+ if (principalSlot && !input.principal) {
49
+ return { slots_bound: {}, error: "Template has a ?principal slot but no principal value was provided." };
50
+ }
51
+ if (resourceSlot && !input.resource) {
52
+ return { slots_bound: {}, error: "Template has a ?resource slot but no resource value was provided." };
53
+ }
54
+
55
+ // Parse and substitute slots
56
+ const linked = { ...json };
57
+
58
+ if (principalSlot && input.principal) {
59
+ const entity = parseEntityRef(input.principal);
60
+ if (!entity) {
61
+ return { slots_bound: {}, error: `Invalid principal entity reference format: "${input.principal}". Expected format: Namespace::Type::"id"` };
62
+ }
63
+ linked.principal = { op: "==", entity };
64
+ slots_bound["?principal"] = input.principal;
65
+ }
66
+
67
+ if (resourceSlot && input.resource) {
68
+ const entity = parseEntityRef(input.resource);
69
+ if (!entity) {
70
+ return { slots_bound: {}, error: `Invalid resource entity reference format: "${input.resource}". Expected format: Namespace::Type::"id"` };
71
+ }
72
+ linked.resource = { op: "==", entity };
73
+ slots_bound["?resource"] = input.resource;
74
+ }
75
+
76
+ // Convert linked JSON to Cedar text
77
+ const textResult = policyToText(linked as unknown as PolicyJson);
78
+ if (textResult.type === "failure") {
79
+ const msg = (textResult.errors as DetailedError[]).map(e => e.message).join("; ");
80
+ return { slots_bound, error: `Failed to render linked policy: ${msg}` };
81
+ }
82
+
83
+ const linked_policy = textResult.text;
84
+
85
+ // Optionally validate the linked policy (now a regular policy, not a template) against schema
86
+ if (input.schema) {
87
+ const parsed = policyToJson(linked_policy);
88
+ if (parsed.type === "failure") {
89
+ return { linked_policy, slots_bound, valid: false, errors: parsed.errors.map(e => ({ message: e.message })) };
90
+ }
91
+ let validateResult: ReturnType<typeof validate>;
92
+ try {
93
+ validateResult = validate({ schema: input.schema, policies: { staticPolicies: { p0: parsed.json }, templates: {} } });
94
+ } catch (e) {
95
+ return { linked_policy, slots_bound, valid: false, errors: [{ message: e instanceof Error ? e.message : String(e) }] };
96
+ }
97
+ if (validateResult.type === "failure") {
98
+ return { linked_policy, slots_bound, valid: false, errors: validateResult.errors.map(e => ({ message: e.message })) };
99
+ }
100
+ return {
101
+ linked_policy,
102
+ slots_bound,
103
+ valid: validateResult.validationErrors.length === 0,
104
+ errors: validateResult.validationErrors.map(e => ({ message: e.error.message })),
105
+ };
106
+ }
107
+
108
+ return { linked_policy, slots_bound };
109
+ }
@@ -0,0 +1,41 @@
1
+ import { storeManager, StoreManager } from "../resources/store-manager.js";
2
+
3
+ export interface ListTemplateLinksInput {
4
+ store: string;
5
+ }
6
+
7
+ export interface TemplateLinkEntry {
8
+ id: string;
9
+ template_id: string;
10
+ slot_values: Record<string, string>;
11
+ }
12
+
13
+ export interface ListTemplateLinksResult {
14
+ store: string;
15
+ links: TemplateLinkEntry[];
16
+ error?: string;
17
+ }
18
+
19
+ export async function handleListTemplateLinks(
20
+ input: ListTemplateLinksInput,
21
+ manager: StoreManager = storeManager
22
+ ): Promise<ListTemplateLinksResult> {
23
+ let ids: string[];
24
+ try {
25
+ ids = manager.listTemplateLinks(input.store);
26
+ } catch (e) {
27
+ return { store: input.store, links: [], error: e instanceof Error ? e.message : String(e) };
28
+ }
29
+
30
+ const links: TemplateLinkEntry[] = [];
31
+ for (const id of ids) {
32
+ try {
33
+ const data = manager.readTemplateLink(input.store, id);
34
+ links.push({ id, template_id: data.template_id, slot_values: data.slot_values });
35
+ } catch (e) {
36
+ return { store: input.store, links, error: `Failed to read link "${id}": ${e instanceof Error ? e.message : String(e)}` };
37
+ }
38
+ }
39
+
40
+ return { store: input.store, links };
41
+ }
@@ -0,0 +1,55 @@
1
+ import { templateToJson } from "@cedar-policy/cedar-wasm/nodejs";
2
+ import type { PolicyJson } from "@cedar-policy/cedar-wasm/nodejs";
3
+ import { storeManager, StoreManager } from "../resources/store-manager.js";
4
+
5
+ export interface ListTemplatesInput {
6
+ store: string;
7
+ }
8
+
9
+ export interface TemplateEntry {
10
+ id: string;
11
+ content: string;
12
+ slots: string[];
13
+ }
14
+
15
+ export interface ListTemplatesResult {
16
+ store: string;
17
+ templates: TemplateEntry[];
18
+ error?: string;
19
+ }
20
+
21
+ function detectSlots(json: PolicyJson): string[] {
22
+ const slots: string[] = [];
23
+ const p = json.principal as Record<string, unknown>;
24
+ const r = json.resource as Record<string, unknown>;
25
+ if (p?.slot === "?principal") slots.push("?principal");
26
+ if (r?.slot === "?resource") slots.push("?resource");
27
+ return slots;
28
+ }
29
+
30
+ export async function handleListTemplates(
31
+ input: ListTemplatesInput,
32
+ manager: StoreManager = storeManager
33
+ ): Promise<ListTemplatesResult> {
34
+ let ids: string[];
35
+ try {
36
+ ids = manager.listTemplates(input.store);
37
+ } catch (e) {
38
+ return { store: input.store, templates: [], error: e instanceof Error ? e.message : String(e) };
39
+ }
40
+
41
+ const templates: TemplateEntry[] = [];
42
+ for (const id of ids) {
43
+ let content: string;
44
+ try {
45
+ content = manager.readTemplate(input.store, id);
46
+ } catch (e) {
47
+ return { store: input.store, templates, error: `Failed to read template "${id}": ${e instanceof Error ? e.message : String(e)}` };
48
+ }
49
+ const parsed = templateToJson(content);
50
+ const slots = parsed.type === "success" ? detectSlots(parsed.json as PolicyJson) : [];
51
+ templates.push({ id, content, slots });
52
+ }
53
+
54
+ return { store: input.store, templates };
55
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ policyToJson,
3
+ policyToText,
4
+ schemaToJson,
5
+ schemaToText,
6
+ } from "@cedar-policy/cedar-wasm/nodejs";
7
+ import type { Schema } from "@cedar-policy/cedar-wasm/nodejs";
8
+
9
+ export interface TranslateInput {
10
+ input: string;
11
+ type: "policy" | "schema";
12
+ direction: "to_json" | "to_cedar";
13
+ }
14
+
15
+ export interface TranslateResult {
16
+ output: string | null;
17
+ error: string | null;
18
+ }
19
+
20
+ function parseSchemaInput(input: string): Schema {
21
+ try {
22
+ return JSON.parse(input);
23
+ } catch {
24
+ return input;
25
+ }
26
+ }
27
+
28
+ export async function handleTranslate(input: TranslateInput): Promise<TranslateResult> {
29
+ // per spike-report-wasm-api.md §5-6: function names are policyToJson/policyToText/schemaToJson/schemaToText,
30
+ // not translate_policy/translate_schema as the design doc assumed
31
+ if (input.type === "policy") {
32
+ if (input.direction === "to_json") {
33
+ const answer = policyToJson(input.input);
34
+ if (answer.type === "failure") {
35
+ return { output: null, error: answer.errors.map((e) => e.message).join("; ") };
36
+ }
37
+ return { output: JSON.stringify(answer.json, null, 2), error: null };
38
+ } else {
39
+ let parsed: unknown;
40
+ try {
41
+ parsed = JSON.parse(input.input);
42
+ } catch {
43
+ return { output: null, error: "Input must be a valid JSON policy object for to_cedar direction" };
44
+ }
45
+ const answer = policyToText(parsed as Parameters<typeof policyToText>[0]);
46
+ if (answer.type === "failure") {
47
+ return { output: null, error: answer.errors.map((e) => e.message).join("; ") };
48
+ }
49
+ return { output: answer.text, error: null };
50
+ }
51
+ } else {
52
+ if (input.direction === "to_json") {
53
+ const answer = schemaToJson(parseSchemaInput(input.input));
54
+ if (answer.type === "failure") {
55
+ return { output: null, error: answer.errors.map((e) => e.message).join("; ") };
56
+ }
57
+ return { output: JSON.stringify(answer.json, null, 2), error: null };
58
+ } else {
59
+ const answer = schemaToText(parseSchemaInput(input.input));
60
+ if (answer.type === "failure") {
61
+ return { output: null, error: answer.errors.map((e) => e.message).join("; ") };
62
+ }
63
+ return { output: answer.text, error: null };
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,125 @@
1
+ import { checkParseEntities } from "@cedar-policy/cedar-wasm/nodejs";
2
+ import type { Schema, Entities } from "@cedar-policy/cedar-wasm/nodejs";
3
+
4
+ export interface ValidateEntitiesInput {
5
+ entities: string;
6
+ schema?: string;
7
+ }
8
+
9
+ export type EntityErrorKind =
10
+ | "unknown_type"
11
+ | "missing_required_attribute"
12
+ | "type_mismatch"
13
+ | "unknown_attribute"
14
+ | "disallowed_parent_type"
15
+ | "parse_error"
16
+ | "other";
17
+
18
+ export interface EntityError {
19
+ entity_uid: string;
20
+ error_kind: EntityErrorKind;
21
+ message: string;
22
+ attribute?: string;
23
+ }
24
+
25
+ export interface ValidateEntitiesResult {
26
+ valid: boolean;
27
+ entity_count: number;
28
+ errors: EntityError[];
29
+ }
30
+
31
+ function parseSchema(schemaStr: string | undefined): Schema | undefined {
32
+ if (!schemaStr) return undefined;
33
+ try {
34
+ return JSON.parse(schemaStr);
35
+ } catch {
36
+ return schemaStr;
37
+ }
38
+ }
39
+
40
+ // Each regex captures: 1) entity_uid (everything between backticks), 2) attribute name when present.
41
+ const RE_TYPE_MISMATCH = /in attribute `([^`]+)` on `([^`]+)`, type mismatch/;
42
+ const RE_MISSING_REQUIRED = /expected entity `([^`]+)` to have attribute `([^`]+)`, but it does not/;
43
+ const RE_UNKNOWN_TYPE = /entity `([^`]+)` has type `[^`]+` which is not declared in the schema/;
44
+ const RE_UNKNOWN_ATTR = /attribute `([^`]+)` on `([^`]+)` should not exist according to the schema/;
45
+ const RE_DISALLOWED_PARENT = /`([^`]+)` is not allowed to have an ancestor of type `[^`]+` according to the schema/;
46
+
47
+ export function classifyError(message: string): EntityError {
48
+ let m: RegExpMatchArray | null;
49
+
50
+ if ((m = message.match(RE_TYPE_MISMATCH))) {
51
+ return { entity_uid: m[2], error_kind: "type_mismatch", attribute: m[1], message };
52
+ }
53
+ if ((m = message.match(RE_MISSING_REQUIRED))) {
54
+ return {
55
+ entity_uid: m[1],
56
+ error_kind: "missing_required_attribute",
57
+ attribute: m[2],
58
+ message,
59
+ };
60
+ }
61
+ if ((m = message.match(RE_UNKNOWN_TYPE))) {
62
+ return { entity_uid: m[1], error_kind: "unknown_type", message };
63
+ }
64
+ if ((m = message.match(RE_UNKNOWN_ATTR))) {
65
+ return { entity_uid: m[2], error_kind: "unknown_attribute", attribute: m[1], message };
66
+ }
67
+ if ((m = message.match(RE_DISALLOWED_PARENT))) {
68
+ return { entity_uid: m[1], error_kind: "disallowed_parent_type", message };
69
+ }
70
+
71
+ return {
72
+ entity_uid: "",
73
+ error_kind: "other",
74
+ message: `[unrecognized error pattern; the regex classifier did not match this message, so error_kind defaulted to "other"] ${message}`,
75
+ };
76
+ }
77
+
78
+ export async function handleValidateEntities(
79
+ input: ValidateEntitiesInput
80
+ ): Promise<ValidateEntitiesResult> {
81
+ // 1. Parse entities JSON
82
+ let entities: unknown;
83
+ try {
84
+ entities = JSON.parse(input.entities);
85
+ } catch (e) {
86
+ return {
87
+ valid: false,
88
+ entity_count: 0,
89
+ errors: [
90
+ {
91
+ entity_uid: "",
92
+ error_kind: "parse_error",
93
+ message: `Entities JSON failed to parse: ${e instanceof Error ? e.message : String(e)}`,
94
+ },
95
+ ],
96
+ };
97
+ }
98
+
99
+ if (!Array.isArray(entities)) {
100
+ return {
101
+ valid: false,
102
+ entity_count: 0,
103
+ errors: [
104
+ {
105
+ entity_uid: "",
106
+ error_kind: "parse_error",
107
+ message: "Entities must be a JSON array of entity objects",
108
+ },
109
+ ],
110
+ };
111
+ }
112
+
113
+ const entity_count = entities.length;
114
+ const schema = parseSchema(input.schema);
115
+
116
+ const call = schema ? { entities: entities as Entities, schema } : { entities: entities as Entities };
117
+ const answer = checkParseEntities(call);
118
+
119
+ if (answer.type === "success") {
120
+ return { valid: true, entity_count, errors: [] };
121
+ }
122
+
123
+ const errors = answer.errors.map((e) => classifyError(e.message));
124
+ return { valid: false, entity_count, errors };
125
+ }
@@ -0,0 +1,128 @@
1
+ import { checkParseSchema } from "@cedar-policy/cedar-wasm/nodejs";
2
+ import type { Schema } from "@cedar-policy/cedar-wasm/nodejs";
3
+
4
+ export interface ValidateSchemaInput {
5
+ schema: string;
6
+ }
7
+
8
+ export interface SchemaParseError {
9
+ message: string;
10
+ source_location?: { start: number; end: number; label?: string | null };
11
+ }
12
+
13
+ export interface ValidateSchemaResult {
14
+ valid: boolean;
15
+ format: "json" | "cedarschema";
16
+ namespaces: string[];
17
+ entity_type_count: number;
18
+ action_count: number;
19
+ common_type_count: number;
20
+ errors: SchemaParseError[];
21
+ }
22
+
23
+ function parseSchemaInput(schemaStr: string): { schema: Schema; format: "json" | "cedarschema" } {
24
+ try {
25
+ return { schema: JSON.parse(schemaStr), format: "json" };
26
+ } catch {
27
+ return { schema: schemaStr, format: "cedarschema" };
28
+ }
29
+ }
30
+
31
+ interface JsonSchemaShape {
32
+ [namespace: string]: {
33
+ entityTypes?: Record<string, unknown>;
34
+ actions?: Record<string, unknown>;
35
+ commonTypes?: Record<string, unknown>;
36
+ };
37
+ }
38
+
39
+ function summarizeJsonSchema(json: unknown): {
40
+ namespaces: string[];
41
+ entity_type_count: number;
42
+ action_count: number;
43
+ common_type_count: number;
44
+ } {
45
+ const empty = { namespaces: [], entity_type_count: 0, action_count: 0, common_type_count: 0 };
46
+ if (!json || typeof json !== "object") return empty;
47
+ const shape = json as JsonSchemaShape;
48
+
49
+ const namespaces = Object.keys(shape);
50
+ let entity_type_count = 0;
51
+ let action_count = 0;
52
+ let common_type_count = 0;
53
+
54
+ for (const ns of namespaces) {
55
+ const block = shape[ns];
56
+ if (block.entityTypes) entity_type_count += Object.keys(block.entityTypes).length;
57
+ if (block.actions) action_count += Object.keys(block.actions).length;
58
+ if (block.commonTypes) common_type_count += Object.keys(block.commonTypes).length;
59
+ }
60
+
61
+ return { namespaces, entity_type_count, action_count, common_type_count };
62
+ }
63
+
64
+ export async function handleValidateSchema(
65
+ input: ValidateSchemaInput
66
+ ): Promise<ValidateSchemaResult> {
67
+ if (!input.schema || input.schema.trim() === "") {
68
+ return {
69
+ valid: false,
70
+ format: "cedarschema",
71
+ namespaces: [],
72
+ entity_type_count: 0,
73
+ action_count: 0,
74
+ common_type_count: 0,
75
+ errors: [{ message: "Schema input is empty" }],
76
+ };
77
+ }
78
+
79
+ const { schema, format } = parseSchemaInput(input.schema);
80
+ const answer = checkParseSchema(schema);
81
+
82
+ if (answer.type === "failure") {
83
+ return {
84
+ valid: false,
85
+ format,
86
+ namespaces: [],
87
+ entity_type_count: 0,
88
+ action_count: 0,
89
+ common_type_count: 0,
90
+ errors: answer.errors.map((e) => ({
91
+ message: e.message,
92
+ ...(e.sourceLocations && e.sourceLocations.length > 0
93
+ ? { source_location: { start: e.sourceLocations[0].start, end: e.sourceLocations[0].end, label: e.sourceLocations[0].label } }
94
+ : {}),
95
+ })),
96
+ };
97
+ }
98
+
99
+ if (format === "json") {
100
+ const summary = summarizeJsonSchema(schema);
101
+ return { valid: true, format, ...summary, errors: [] };
102
+ }
103
+
104
+ // For cedarschema text, derive summary by translating to JSON form.
105
+ // schemaToJsonWithResolvedTypes only accepts string input (per spike-report §"Schema standalone ops spike").
106
+ if (typeof schema === "string") {
107
+ try {
108
+ const { schemaToJsonWithResolvedTypes } = await import("@cedar-policy/cedar-wasm/nodejs");
109
+ const jsonAnswer = schemaToJsonWithResolvedTypes(schema);
110
+ if (jsonAnswer.type === "success") {
111
+ const summary = summarizeJsonSchema(jsonAnswer.json);
112
+ return { valid: true, format, ...summary, errors: [] };
113
+ }
114
+ } catch {
115
+ // fall through to summary-less success
116
+ }
117
+ }
118
+
119
+ return {
120
+ valid: true,
121
+ format,
122
+ namespaces: [],
123
+ entity_type_count: 0,
124
+ action_count: 0,
125
+ common_type_count: 0,
126
+ errors: [],
127
+ };
128
+ }
@@ -0,0 +1,72 @@
1
+ import { templateToJson, validate } from "@cedar-policy/cedar-wasm/nodejs";
2
+ import type { PolicyJson, Schema } from "@cedar-policy/cedar-wasm/nodejs";
3
+
4
+ function parseSchema(schemaStr: string): Schema {
5
+ try { return JSON.parse(schemaStr); } catch { return schemaStr; }
6
+ }
7
+
8
+ export interface ValidateTemplateInput {
9
+ template: string;
10
+ schema: string;
11
+ }
12
+
13
+ export interface ValidateTemplateResult {
14
+ valid: boolean;
15
+ errors: Array<{ message: string; help?: string }>;
16
+ warnings: Array<{ message: string }>;
17
+ slots_detected: string[];
18
+ error?: string;
19
+ }
20
+
21
+ function detectSlots(json: PolicyJson): string[] {
22
+ const slots: string[] = [];
23
+ const p = json.principal as Record<string, unknown>;
24
+ const r = json.resource as Record<string, unknown>;
25
+ if (p?.slot === "?principal") slots.push("?principal");
26
+ if (r?.slot === "?resource") slots.push("?resource");
27
+ return slots;
28
+ }
29
+
30
+ export async function handleValidateTemplate(input: ValidateTemplateInput): Promise<ValidateTemplateResult> {
31
+ if (!input.schema?.trim()) {
32
+ return { valid: false, errors: [], warnings: [], slots_detected: [], error: "schema is required" };
33
+ }
34
+
35
+ // Parse the template
36
+ const parseResult = templateToJson(input.template);
37
+ if (parseResult.type === "failure") {
38
+ return {
39
+ valid: false,
40
+ errors: parseResult.errors.map(e => ({ message: e.message })),
41
+ warnings: [],
42
+ slots_detected: [],
43
+ };
44
+ }
45
+
46
+ const slots_detected = detectSlots(parseResult.json as PolicyJson);
47
+
48
+ // Validate against schema using the JSON policy struct format with templates key
49
+ const templateId = "t0";
50
+ let validateResult: ReturnType<typeof validate>;
51
+ try {
52
+ validateResult = validate({ schema: parseSchema(input.schema), policies: { staticPolicies: {}, templates: { [templateId]: parseResult.json } } });
53
+ } catch (e) {
54
+ return { valid: false, errors: [{ message: e instanceof Error ? e.message : String(e) }], warnings: [], slots_detected };
55
+ }
56
+
57
+ if (validateResult.type === "failure") {
58
+ return {
59
+ valid: false,
60
+ errors: validateResult.errors.map(e => ({ message: e.message, help: e.help ?? undefined })),
61
+ warnings: [],
62
+ slots_detected,
63
+ };
64
+ }
65
+
66
+ return {
67
+ valid: validateResult.validationErrors.length === 0,
68
+ errors: validateResult.validationErrors.map(e => ({ message: e.error.message, help: e.error.help ?? undefined })),
69
+ warnings: validateResult.validationWarnings.map(w => ({ message: w.error.message })),
70
+ slots_detected,
71
+ };
72
+ }