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.
- package/.editorconfig +12 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +42 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +241 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +182 -0
- package/README.md +1635 -0
- package/SECURITY.md +37 -0
- package/dist/http-server.d.ts +61 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +194 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/policy-ast.d.ts +49 -0
- package/dist/parser/policy-ast.d.ts.map +1 -0
- package/dist/parser/policy-ast.js +311 -0
- package/dist/parser/policy-ast.js.map +1 -0
- package/dist/prompts/index.d.ts +38 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +172 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/ref-resolver.d.ts +23 -0
- package/dist/resources/ref-resolver.d.ts.map +1 -0
- package/dist/resources/ref-resolver.js +128 -0
- package/dist/resources/ref-resolver.js.map +1 -0
- package/dist/resources/store-manager.d.ts +64 -0
- package/dist/resources/store-manager.d.ts.map +1 -0
- package/dist/resources/store-manager.js +221 -0
- package/dist/resources/store-manager.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +539 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/advise/avp-rules.d.ts +49 -0
- package/dist/tools/advise/avp-rules.d.ts.map +1 -0
- package/dist/tools/advise/avp-rules.js +59 -0
- package/dist/tools/advise/avp-rules.js.map +1 -0
- package/dist/tools/advise/cedar-patterns.d.ts +24 -0
- package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
- package/dist/tools/advise/cedar-patterns.js +57 -0
- package/dist/tools/advise/cedar-patterns.js.map +1 -0
- package/dist/tools/advise/context-builder.d.ts +28 -0
- package/dist/tools/advise/context-builder.d.ts.map +1 -0
- package/dist/tools/advise/context-builder.js +89 -0
- package/dist/tools/advise/context-builder.js.map +1 -0
- package/dist/tools/advise/gotchas.d.ts +15 -0
- package/dist/tools/advise/gotchas.d.ts.map +1 -0
- package/dist/tools/advise/gotchas.js +83 -0
- package/dist/tools/advise/gotchas.js.map +1 -0
- package/dist/tools/advise.d.ts +96 -0
- package/dist/tools/advise.d.ts.map +1 -0
- package/dist/tools/advise.js +258 -0
- package/dist/tools/advise.js.map +1 -0
- package/dist/tools/authorize-batch.d.ts +35 -0
- package/dist/tools/authorize-batch.d.ts.map +1 -0
- package/dist/tools/authorize-batch.js +262 -0
- package/dist/tools/authorize-batch.js.map +1 -0
- package/dist/tools/authorize.d.ts +115 -0
- package/dist/tools/authorize.d.ts.map +1 -0
- package/dist/tools/authorize.js +373 -0
- package/dist/tools/authorize.js.map +1 -0
- package/dist/tools/check-change.d.ts +19 -0
- package/dist/tools/check-change.d.ts.map +1 -0
- package/dist/tools/check-change.js +91 -0
- package/dist/tools/check-change.js.map +1 -0
- package/dist/tools/diff-schema.d.ts +103 -0
- package/dist/tools/diff-schema.d.ts.map +1 -0
- package/dist/tools/diff-schema.js +379 -0
- package/dist/tools/diff-schema.js.map +1 -0
- package/dist/tools/diff-stores.d.ts +45 -0
- package/dist/tools/diff-stores.d.ts.map +1 -0
- package/dist/tools/diff-stores.js +222 -0
- package/dist/tools/diff-stores.js.map +1 -0
- package/dist/tools/explain.d.ts +80 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +187 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/format.d.ts +11 -0
- package/dist/tools/format.d.ts.map +1 -0
- package/dist/tools/format.js +20 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/generate-sample.d.ts +28 -0
- package/dist/tools/generate-sample.d.ts.map +1 -0
- package/dist/tools/generate-sample.js +568 -0
- package/dist/tools/generate-sample.js.map +1 -0
- package/dist/tools/link-template.d.ts +17 -0
- package/dist/tools/link-template.d.ts.map +1 -0
- package/dist/tools/link-template.js +78 -0
- package/dist/tools/link-template.js.map +1 -0
- package/dist/tools/list-template-links.d.ts +16 -0
- package/dist/tools/list-template-links.d.ts.map +1 -0
- package/dist/tools/list-template-links.js +22 -0
- package/dist/tools/list-template-links.js.map +1 -0
- package/dist/tools/list-templates.d.ts +16 -0
- package/dist/tools/list-templates.d.ts.map +1 -0
- package/dist/tools/list-templates.js +36 -0
- package/dist/tools/list-templates.js.map +1 -0
- package/dist/tools/translate.d.ts +11 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +53 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/validate-entities.d.ts +19 -0
- package/dist/tools/validate-entities.d.ts.map +1 -0
- package/dist/tools/validate-entities.js +88 -0
- package/dist/tools/validate-entities.js.map +1 -0
- package/dist/tools/validate-schema.d.ts +22 -0
- package/dist/tools/validate-schema.d.ts.map +1 -0
- package/dist/tools/validate-schema.js +89 -0
- package/dist/tools/validate-schema.js.map +1 -0
- package/dist/tools/validate-template.d.ts +18 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +59 -0
- package/dist/tools/validate-template.js.map +1 -0
- package/dist/tools/validate.d.ts +90 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +351 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/utils/format-detector.d.ts +49 -0
- package/dist/utils/format-detector.d.ts.map +1 -0
- package/dist/utils/format-detector.js +298 -0
- package/dist/utils/format-detector.js.map +1 -0
- package/examples/README.md +36 -0
- package/examples/abac-multi-tenant/README.md +150 -0
- package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
- package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
- package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
- package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
- package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
- package/examples/abac-multi-tenant/run.ts +92 -0
- package/examples/abac-multi-tenant/schema.json +60 -0
- package/examples/api-gateway-path-routing/README.md +154 -0
- package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
- package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
- package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
- package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
- package/examples/api-gateway-path-routing/run.ts +108 -0
- package/examples/api-gateway-path-routing/schema.json +54 -0
- package/examples/rbac-document-management/README.md +167 -0
- package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
- package/examples/rbac-document-management/policies/admin.cedar +6 -0
- package/examples/rbac-document-management/policies/editor.cedar +6 -0
- package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
- package/examples/rbac-document-management/policies/viewer.cedar +6 -0
- package/examples/rbac-document-management/run.ts +87 -0
- package/examples/rbac-document-management/schema.json +57 -0
- package/package.json +50 -0
- package/src/http-server.ts +239 -0
- package/src/index.ts +294 -0
- package/src/parser/policy-ast.ts +345 -0
- package/src/prompts/README.md +3 -0
- package/src/prompts/index.ts +217 -0
- package/src/resources/ref-resolver.ts +134 -0
- package/src/resources/store-manager.ts +248 -0
- package/src/server.ts +711 -0
- package/src/tools/advise/avp-rules.ts +70 -0
- package/src/tools/advise/cedar-patterns.ts +73 -0
- package/src/tools/advise/context-builder.ts +109 -0
- package/src/tools/advise/gotchas.ts +92 -0
- package/src/tools/advise.ts +366 -0
- package/src/tools/authorize-batch.ts +345 -0
- package/src/tools/authorize.ts +464 -0
- package/src/tools/check-change.ts +119 -0
- package/src/tools/diff-schema.ts +510 -0
- package/src/tools/diff-stores.ts +298 -0
- package/src/tools/explain.ts +278 -0
- package/src/tools/format.ts +33 -0
- package/src/tools/generate-sample.ts +665 -0
- package/src/tools/link-template.ts +109 -0
- package/src/tools/list-template-links.ts +41 -0
- package/src/tools/list-templates.ts +55 -0
- package/src/tools/translate.ts +66 -0
- package/src/tools/validate-entities.ts +125 -0
- package/src/tools/validate-schema.ts +128 -0
- package/src/tools/validate-template.ts +72 -0
- package/src/tools/validate.ts +459 -0
- package/src/utils/format-detector.ts +356 -0
- package/test/fixtures/docmgmt.ts +121 -0
- package/test/fixtures/multitenant.ts +163 -0
- package/test/index.test.ts +96 -0
- package/test/integration/e2e/behavior.test.ts +359 -0
- package/test/integration/e2e/edge-cases.test.ts +365 -0
- package/test/integration/e2e/failure-modes.test.ts +266 -0
- package/test/integration/e2e/protocol.test.ts +252 -0
- package/test/integration/http-smoke.test.ts +588 -0
- package/test/integration/smoke.test.ts +475 -0
- package/test/prompts/prompts.test.ts +173 -0
- package/test/property/properties.test.ts +234 -0
- package/test/resources/ref-resolver.test.ts +186 -0
- package/test/resources/store-manager.test.ts +344 -0
- package/test/setup.test.ts +7 -0
- package/test/tools/advise/avp-rules.test.ts +76 -0
- package/test/tools/advise.test.ts +339 -0
- package/test/tools/authorize-batch.test.ts +459 -0
- package/test/tools/authorize.test.ts +682 -0
- package/test/tools/check-change.test.ts +104 -0
- package/test/tools/cross-fixture.test.ts +170 -0
- package/test/tools/diff-schema.test.ts +355 -0
- package/test/tools/diff-stores.test.ts +291 -0
- package/test/tools/explain.test.ts +221 -0
- package/test/tools/format.test.ts +33 -0
- package/test/tools/generate-sample.test.ts +480 -0
- package/test/tools/link-template.test.ts +90 -0
- package/test/tools/list-templates.test.ts +151 -0
- package/test/tools/translate.test.ts +89 -0
- package/test/tools/validate-entities.test.ts +178 -0
- package/test/tools/validate-schema.test.ts +86 -0
- package/test/tools/validate-template.test.ts +89 -0
- package/test/tools/validate.test.ts +331 -0
- package/test/utils/format-detector.test.ts +518 -0
- package/tsconfig.json +17 -0
- 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
|
+
}
|