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,518 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
detectFormat,
|
|
4
|
+
normalizeEntities,
|
|
5
|
+
normalizePrincipalRef,
|
|
6
|
+
unwrapAvpAttributes,
|
|
7
|
+
} from "../../src/utils/format-detector.js";
|
|
8
|
+
|
|
9
|
+
// ─── Dataset A: Cedar/WASM format (baseline) ─────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const CEDAR_ENTITIES = [
|
|
12
|
+
{
|
|
13
|
+
uid: { type: "DocMgmt::User", id: "alice" },
|
|
14
|
+
attrs: { name: "Alice", email: "alice@example.com" },
|
|
15
|
+
parents: [{ type: "DocMgmt::Role", id: "admin" }],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
uid: { type: "DocMgmt::Role", id: "admin" },
|
|
19
|
+
attrs: {},
|
|
20
|
+
parents: [],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// ─── Dataset B: AVP entity format ────────────────────────────────────────────
|
|
25
|
+
// Spike proven: identifier key → hard failure. attributes key + typed values → silent wrong.
|
|
26
|
+
|
|
27
|
+
const AVP_ENTITIES = [
|
|
28
|
+
{
|
|
29
|
+
identifier: { entity_type: "DocMgmt::User", entity_id: "alice" },
|
|
30
|
+
attributes: {
|
|
31
|
+
name: { string: "Alice" },
|
|
32
|
+
email: { string: "alice@example.com" },
|
|
33
|
+
},
|
|
34
|
+
parents: [{ entity_type: "DocMgmt::Role", entity_id: "admin" }],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
identifier: { entity_type: "DocMgmt::Role", entity_id: "admin" },
|
|
38
|
+
attributes: {},
|
|
39
|
+
parents: [],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// ─── Dataset C: AVP entities with all three attribute types ──────────────────
|
|
44
|
+
|
|
45
|
+
const AVP_ENTITIES_ALL_TYPES = [
|
|
46
|
+
{
|
|
47
|
+
identifier: { entity_type: "SaaS::User", entity_id: "alice" },
|
|
48
|
+
attributes: {
|
|
49
|
+
name: { string: "Alice" },
|
|
50
|
+
age: { long: 30 },
|
|
51
|
+
active: { boolean: true },
|
|
52
|
+
},
|
|
53
|
+
parents: [],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// ─── Dataset D: Cedar CLI format (uid.__entity) — works natively, no conversion ───
|
|
58
|
+
|
|
59
|
+
const CEDAR_CLI_ENTITIES = [
|
|
60
|
+
{
|
|
61
|
+
uid: { __entity: { type: "DocMgmt::User", id: "alice" } },
|
|
62
|
+
attrs: { name: "Alice" },
|
|
63
|
+
parents: [{ type: "DocMgmt::Role", id: "admin" }],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// ─── Dataset E: AVP principal/action/resource ────────────────────────────────
|
|
68
|
+
// Spike proven: { entity_type, entity_id } → hard failure on isAuthorized.
|
|
69
|
+
|
|
70
|
+
const AVP_PRINCIPAL = { entity_type: "DocMgmt::User", entity_id: "alice" };
|
|
71
|
+
const AVP_ACTION = { action_type: "DocMgmt::Action", action_id: "READ" };
|
|
72
|
+
const AVP_RESOURCE = { entity_type: "DocMgmt::Document", entity_id: "doc-1" };
|
|
73
|
+
|
|
74
|
+
// ─── Dataset F: Cedar principal/action/resource (string) ─────────────────────
|
|
75
|
+
|
|
76
|
+
const CEDAR_PRINCIPAL = 'DocMgmt::User::"alice"';
|
|
77
|
+
const CEDAR_ACTION = 'DocMgmt::Action::"READ"';
|
|
78
|
+
const CEDAR_RESOURCE = 'DocMgmt::Document::"doc-1"';
|
|
79
|
+
|
|
80
|
+
// ─── Dataset G: Edge case — Cedar Record that LOOKS like AVP ─────────────────
|
|
81
|
+
// A real Cedar Record attribute with multiple fields should NOT be detected as AVP.
|
|
82
|
+
|
|
83
|
+
const CEDAR_RECORD_ENTITY = [
|
|
84
|
+
{
|
|
85
|
+
uid: { type: "MyApp::Config", id: "cfg-1" },
|
|
86
|
+
attrs: {
|
|
87
|
+
settings: { theme: "dark", lang: "en" }, // Record with 2+ fields → NOT AVP
|
|
88
|
+
meta: { string: "value", extra: "stuff" }, // Record with 2 fields (one named "string") → NOT AVP
|
|
89
|
+
},
|
|
90
|
+
parents: [],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// ─── Dataset H: AVP entities with integer and boolean attributes ──────────────
|
|
95
|
+
|
|
96
|
+
const AVP_ENTITY_MIXED_TYPES = {
|
|
97
|
+
identifier: { entity_type: "SaaS::User", entity_id: "bob" },
|
|
98
|
+
attributes: {
|
|
99
|
+
score: { long: 42 },
|
|
100
|
+
verified: { boolean: true },
|
|
101
|
+
plan: { string: "pro" },
|
|
102
|
+
},
|
|
103
|
+
parents: [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe("detectFormat", () => {
|
|
109
|
+
it("detects Cedar/WASM format from clean entities", () => {
|
|
110
|
+
const result = detectFormat(CEDAR_ENTITIES, CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
111
|
+
expect(result.format).toBe("cedar");
|
|
112
|
+
expect(result.confidence).toBe("high");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("detects AVP format from identifier key in entities", () => {
|
|
116
|
+
const result = detectFormat(AVP_ENTITIES, CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
117
|
+
expect(result.format).toBe("avp");
|
|
118
|
+
expect(result.confidence).toBe("high");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("detects AVP format from typed attribute values (string/long/boolean wrappers)", () => {
|
|
122
|
+
const result = detectFormat(AVP_ENTITIES_ALL_TYPES, CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
123
|
+
expect(result.format).toBe("avp");
|
|
124
|
+
expect(result.confidence).toBe("high");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("detects AVP format from structured principal object", () => {
|
|
128
|
+
const result = detectFormat(CEDAR_ENTITIES, AVP_PRINCIPAL, AVP_ACTION, AVP_RESOURCE);
|
|
129
|
+
expect(result.format).toBe("avp");
|
|
130
|
+
expect(result.confidence).toBe("high");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("detects cedar_cli format from __entity wrapper on uid", () => {
|
|
134
|
+
const result = detectFormat(CEDAR_CLI_ENTITIES, CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
135
|
+
expect(result.format).toBe("cedar_cli");
|
|
136
|
+
// Note: cedar_cli passes through without conversion — WASM accepts both uid forms
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does NOT misdetect Cedar Record attributes with 2+ fields as AVP", () => {
|
|
140
|
+
const result = detectFormat(CEDAR_RECORD_ENTITY, CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
141
|
+
expect(result.format).toBe("cedar");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("detects AVP when entity has both identifier key AND typed attrs", () => {
|
|
145
|
+
const result = detectFormat([AVP_ENTITY_MIXED_TYPES], CEDAR_PRINCIPAL, CEDAR_ACTION, CEDAR_RESOURCE);
|
|
146
|
+
expect(result.format).toBe("avp");
|
|
147
|
+
expect(result.confidence).toBe("high");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("normalizeEntities", () => {
|
|
152
|
+
it("passes Cedar entities through unchanged", () => {
|
|
153
|
+
const result = normalizeEntities(CEDAR_ENTITIES, "cedar");
|
|
154
|
+
expect(result[0]!.uid).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
155
|
+
expect(result[0]!.attrs).toEqual({ name: "Alice", email: "alice@example.com" });
|
|
156
|
+
expect(result[0]!.parents).toEqual([{ type: "DocMgmt::Role", id: "admin" }]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("converts AVP identifier → uid", () => {
|
|
160
|
+
const result = normalizeEntities(AVP_ENTITIES, "avp");
|
|
161
|
+
expect(result[0]!.uid).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("converts AVP attributes key → attrs", () => {
|
|
165
|
+
const result = normalizeEntities(AVP_ENTITIES, "avp");
|
|
166
|
+
expect(result[0]!.attrs).toBeDefined();
|
|
167
|
+
expect((result[0]!.attrs as Record<string, unknown>)["name"]).toBe("Alice");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("unwraps AVP typed string values", () => {
|
|
171
|
+
const result = normalizeEntities(AVP_ENTITIES, "avp");
|
|
172
|
+
expect((result[0]!.attrs as Record<string, unknown>)["name"]).toBe("Alice");
|
|
173
|
+
expect((result[0]!.attrs as Record<string, unknown>)["email"]).toBe("alice@example.com");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("unwraps AVP typed long values", () => {
|
|
177
|
+
const result = normalizeEntities(AVP_ENTITIES_ALL_TYPES, "avp");
|
|
178
|
+
expect((result[0]!.attrs as Record<string, unknown>)["age"]).toBe(30);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("unwraps AVP typed boolean values", () => {
|
|
182
|
+
const result = normalizeEntities(AVP_ENTITIES_ALL_TYPES, "avp");
|
|
183
|
+
expect((result[0]!.attrs as Record<string, unknown>)["active"]).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("converts AVP parent entity_type/entity_id → type/id", () => {
|
|
187
|
+
const result = normalizeEntities(AVP_ENTITIES, "avp");
|
|
188
|
+
expect(result[0]!.parents[0]).toEqual({ type: "DocMgmt::Role", id: "admin" });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("passes Cedar CLI entities through unchanged (WASM accepts __entity natively)", () => {
|
|
192
|
+
const result = normalizeEntities(CEDAR_CLI_ENTITIES, "cedar_cli");
|
|
193
|
+
expect((result[0]!.uid as Record<string, unknown>)["__entity"]).toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("does NOT unwrap Cedar Record attributes with multiple keys", () => {
|
|
197
|
+
const result = normalizeEntities(CEDAR_RECORD_ENTITY, "cedar");
|
|
198
|
+
expect((result[0]!.attrs as Record<string, unknown>)["settings"]).toEqual({ theme: "dark", lang: "en" });
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("normalizePrincipalRef", () => {
|
|
203
|
+
it("parses Cedar string literal to { type, id }", () => {
|
|
204
|
+
const result = normalizePrincipalRef('DocMgmt::User::"alice"');
|
|
205
|
+
expect(result).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("converts AVP entity_type/entity_id object to { type, id }", () => {
|
|
209
|
+
const result = normalizePrincipalRef({ entity_type: "DocMgmt::User", entity_id: "alice" });
|
|
210
|
+
expect(result).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("converts AVP action_type/action_id object to { type, id }", () => {
|
|
214
|
+
const result = normalizePrincipalRef({ action_type: "DocMgmt::Action", action_id: "READ" });
|
|
215
|
+
expect(result).toEqual({ type: "DocMgmt::Action", id: "READ" });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("passes { type, id } through unchanged", () => {
|
|
219
|
+
const result = normalizePrincipalRef({ type: "DocMgmt::User", id: "alice" });
|
|
220
|
+
expect(result).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("returns error object for unknown formats", () => {
|
|
224
|
+
const result = normalizePrincipalRef({ unknown_key: "value" });
|
|
225
|
+
expect("error" in result).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("unwrapAvpAttributes", () => {
|
|
230
|
+
it("unwraps string wrapper", () => {
|
|
231
|
+
const result = unwrapAvpAttributes({ name: { string: "Alice" } });
|
|
232
|
+
expect(result["name"]).toBe("Alice");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("unwraps long wrapper", () => {
|
|
236
|
+
const result = unwrapAvpAttributes({ score: { long: 42 } });
|
|
237
|
+
expect(result["score"]).toBe(42);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("unwraps boolean wrapper", () => {
|
|
241
|
+
const result = unwrapAvpAttributes({ active: { boolean: true } });
|
|
242
|
+
expect(result["active"]).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("leaves raw string values unchanged", () => {
|
|
246
|
+
const result = unwrapAvpAttributes({ name: "Alice" });
|
|
247
|
+
expect(result["name"]).toBe("Alice");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("leaves raw number values unchanged", () => {
|
|
251
|
+
const result = unwrapAvpAttributes({ count: 5 });
|
|
252
|
+
expect(result["count"]).toBe(5);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("does NOT unwrap Cedar Record with multiple keys", () => {
|
|
256
|
+
const result = unwrapAvpAttributes({ config: { theme: "dark", lang: "en" } });
|
|
257
|
+
expect(result["config"]).toEqual({ theme: "dark", lang: "en" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("does NOT unwrap object with 'string' key alongside other keys", () => {
|
|
261
|
+
const result = unwrapAvpAttributes({ meta: { string: "val", extra: "other" } });
|
|
262
|
+
expect(result["meta"]).toEqual({ string: "val", extra: "other" });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("handles empty attrs", () => {
|
|
266
|
+
const result = unwrapAvpAttributes({});
|
|
267
|
+
expect(result).toEqual({});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ─── New SDK variant datasets ─────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
// Dataset I: Python / JS SDK v3 format (camelCase structural keys)
|
|
274
|
+
const CAMEL_CASE_ENTITIES = [
|
|
275
|
+
{
|
|
276
|
+
identifier: { entityType: "DocMgmt::User", entityId: "alice" },
|
|
277
|
+
attributes: { name: { string: "Alice" } },
|
|
278
|
+
parents: [{ entityType: "DocMgmt::Role", entityId: "admin" }],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
identifier: { entityType: "DocMgmt::Role", entityId: "admin" },
|
|
282
|
+
attributes: {},
|
|
283
|
+
parents: [],
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const CAMEL_CASE_PRINCIPAL = { entityType: "DocMgmt::User", entityId: "alice" };
|
|
288
|
+
const CAMEL_CASE_ACTION = { actionType: "DocMgmt::Action", actionId: "READ" };
|
|
289
|
+
const CAMEL_CASE_RESOURCE = { entityType: "DocMgmt::Document", entityId: "doc-1" };
|
|
290
|
+
|
|
291
|
+
// Dataset J: Official API / AWS Console format (PascalCase everything)
|
|
292
|
+
const PASCAL_CASE_ENTITIES = [
|
|
293
|
+
{
|
|
294
|
+
Identifier: { EntityType: "DocMgmt::User", EntityId: "alice" },
|
|
295
|
+
Attributes: {
|
|
296
|
+
name: { String: "Alice" },
|
|
297
|
+
age: { Long: 30 },
|
|
298
|
+
active: { Boolean: true },
|
|
299
|
+
},
|
|
300
|
+
Parents: [{ EntityType: "DocMgmt::Role", EntityId: "admin" }],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
Identifier: { EntityType: "DocMgmt::Role", EntityId: "admin" },
|
|
304
|
+
Attributes: {},
|
|
305
|
+
Parents: [],
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const PASCAL_CASE_PRINCIPAL = { EntityType: "DocMgmt::User", EntityId: "alice" };
|
|
310
|
+
const PASCAL_CASE_ACTION = { ActionType: "DocMgmt::Action", ActionId: "READ" };
|
|
311
|
+
const PASCAL_CASE_RESOURCE = { EntityType: "DocMgmt::Document", EntityId: "doc-1" };
|
|
312
|
+
|
|
313
|
+
// Dataset K: entityIdentifier in attribute values (camelCase — JS/Python SDK)
|
|
314
|
+
const ENTITY_REF_IN_ATTRS = [
|
|
315
|
+
{
|
|
316
|
+
identifier: { entityType: "SaaS::Document", entityId: "doc-1" },
|
|
317
|
+
attributes: {
|
|
318
|
+
owner: {
|
|
319
|
+
entityIdentifier: { entityType: "SaaS::User", entityId: "alice" },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
parents: [],
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
// Dataset L: EntityIdentifier in attribute values (PascalCase — official API)
|
|
327
|
+
const ENTITY_REF_PASCAL = [
|
|
328
|
+
{
|
|
329
|
+
Identifier: { EntityType: "SaaS::Document", EntityId: "doc-1" },
|
|
330
|
+
Attributes: {
|
|
331
|
+
owner: {
|
|
332
|
+
EntityIdentifier: { EntityType: "SaaS::User", EntityId: "alice" },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
Parents: [],
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
// Dataset M: Set and Record attribute types
|
|
340
|
+
const SET_RECORD_ENTITIES = [
|
|
341
|
+
{
|
|
342
|
+
identifier: { entityType: "MyApp::User", entityId: "alice" },
|
|
343
|
+
attributes: {
|
|
344
|
+
tags: { set: [{ string: "admin" }, { string: "ops" }] },
|
|
345
|
+
config: { record: { theme: { string: "dark" }, lang: { string: "en" } } },
|
|
346
|
+
},
|
|
347
|
+
parents: [],
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
// Dataset N: Extension types (ipaddr, decimal, datetime, duration)
|
|
352
|
+
const EXTENSION_ATTR_ENTITIES = [
|
|
353
|
+
{
|
|
354
|
+
identifier: { entityType: "MyApp::Device", entityId: "device-1" },
|
|
355
|
+
attributes: {
|
|
356
|
+
ipAddress: { ipaddr: "192.168.1.1" },
|
|
357
|
+
balance: { decimal: "1.5" },
|
|
358
|
+
createdAt: { datetime: "2024-10-15T11:35:00Z" },
|
|
359
|
+
ttl: { duration: "1h30m" },
|
|
360
|
+
},
|
|
361
|
+
parents: [],
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
describe("detectFormat — camelCase (Python/JS SDK)", () => {
|
|
366
|
+
it("detects AVP format from camelCase identifier.entityType", () => {
|
|
367
|
+
const r = detectFormat(CAMEL_CASE_ENTITIES, CAMEL_CASE_PRINCIPAL, CAMEL_CASE_ACTION, CAMEL_CASE_RESOURCE);
|
|
368
|
+
expect(r.format).toBe("avp");
|
|
369
|
+
expect(r.confidence).toBe("high");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("detects AVP format from camelCase principal with no entities check", () => {
|
|
373
|
+
const r = detectFormat([], CAMEL_CASE_PRINCIPAL, CAMEL_CASE_ACTION, CAMEL_CASE_RESOURCE);
|
|
374
|
+
expect(r.format).toBe("avp");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("detectFormat — PascalCase (official API / AWS console)", () => {
|
|
379
|
+
it("detects AVP format from PascalCase Identifier.EntityType", () => {
|
|
380
|
+
const r = detectFormat(PASCAL_CASE_ENTITIES, PASCAL_CASE_PRINCIPAL, PASCAL_CASE_ACTION, PASCAL_CASE_RESOURCE);
|
|
381
|
+
expect(r.format).toBe("avp");
|
|
382
|
+
expect(r.confidence).toBe("high");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("detects AVP from PascalCase principal alone", () => {
|
|
386
|
+
const r = detectFormat([], PASCAL_CASE_PRINCIPAL, PASCAL_CASE_ACTION, PASCAL_CASE_RESOURCE);
|
|
387
|
+
expect(r.format).toBe("avp");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("normalizeEntities — camelCase AVP", () => {
|
|
392
|
+
it("converts camelCase identifier → uid with { type, id }", () => {
|
|
393
|
+
const result = normalizeEntities(CAMEL_CASE_ENTITIES, "avp");
|
|
394
|
+
expect(result[0]!.uid).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("converts camelCase parents entityType/entityId → type/id", () => {
|
|
398
|
+
const result = normalizeEntities(CAMEL_CASE_ENTITIES, "avp");
|
|
399
|
+
expect(result[0]!.parents[0]).toEqual({ type: "DocMgmt::Role", id: "admin" });
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("normalizeEntities — PascalCase AVP", () => {
|
|
404
|
+
it("converts PascalCase Identifier → uid with { type, id }", () => {
|
|
405
|
+
const result = normalizeEntities(PASCAL_CASE_ENTITIES, "avp");
|
|
406
|
+
expect(result[0]!.uid).toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("converts PascalCase String/Long/Boolean wrappers to raw values", () => {
|
|
410
|
+
const result = normalizeEntities(PASCAL_CASE_ENTITIES, "avp");
|
|
411
|
+
const attrs = result[0]!.attrs as Record<string, unknown>;
|
|
412
|
+
expect(attrs["name"]).toBe("Alice");
|
|
413
|
+
expect(attrs["age"]).toBe(30);
|
|
414
|
+
expect(attrs["active"]).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("converts PascalCase Parents EntityType/EntityId → type/id", () => {
|
|
418
|
+
const result = normalizeEntities(PASCAL_CASE_ENTITIES, "avp");
|
|
419
|
+
expect(result[0]!.parents[0]).toEqual({ type: "DocMgmt::Role", id: "admin" });
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("normalizePrincipalRef — camelCase and PascalCase", () => {
|
|
424
|
+
it("handles camelCase entityType/entityId", () => {
|
|
425
|
+
expect(normalizePrincipalRef({ entityType: "DocMgmt::User", entityId: "alice" }))
|
|
426
|
+
.toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("handles camelCase actionType/actionId", () => {
|
|
430
|
+
expect(normalizePrincipalRef({ actionType: "DocMgmt::Action", actionId: "READ" }))
|
|
431
|
+
.toEqual({ type: "DocMgmt::Action", id: "READ" });
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("handles PascalCase EntityType/EntityId", () => {
|
|
435
|
+
expect(normalizePrincipalRef({ EntityType: "DocMgmt::User", EntityId: "alice" }))
|
|
436
|
+
.toEqual({ type: "DocMgmt::User", id: "alice" });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("handles PascalCase ActionType/ActionId", () => {
|
|
440
|
+
expect(normalizePrincipalRef({ ActionType: "DocMgmt::Action", ActionId: "READ" }))
|
|
441
|
+
.toEqual({ type: "DocMgmt::Action", id: "READ" });
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("unwrapAvpAttributes — entityIdentifier", () => {
|
|
446
|
+
it("converts camelCase entityIdentifier to WASM __entity", () => {
|
|
447
|
+
const result = unwrapAvpAttributes({
|
|
448
|
+
owner: { entityIdentifier: { entityType: "SaaS::User", entityId: "alice" } },
|
|
449
|
+
});
|
|
450
|
+
expect(result["owner"]).toEqual({ __entity: { type: "SaaS::User", id: "alice" } });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("converts PascalCase EntityIdentifier to WASM __entity", () => {
|
|
454
|
+
const result = unwrapAvpAttributes({
|
|
455
|
+
owner: { EntityIdentifier: { EntityType: "SaaS::User", EntityId: "alice" } },
|
|
456
|
+
});
|
|
457
|
+
expect(result["owner"]).toEqual({ __entity: { type: "SaaS::User", id: "alice" } });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("converts snake_case entity_identifier to WASM __entity", () => {
|
|
461
|
+
const result = unwrapAvpAttributes({
|
|
462
|
+
owner: { entity_identifier: { entity_type: "SaaS::User", entity_id: "alice" } },
|
|
463
|
+
});
|
|
464
|
+
expect(result["owner"]).toEqual({ __entity: { type: "SaaS::User", id: "alice" } });
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("unwrapAvpAttributes — set and record types", () => {
|
|
469
|
+
it("unwraps set with nested typed values", () => {
|
|
470
|
+
const result = unwrapAvpAttributes({
|
|
471
|
+
tags: { set: [{ string: "admin" }, { string: "ops" }] },
|
|
472
|
+
});
|
|
473
|
+
expect(result["tags"]).toEqual(["admin", "ops"]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("unwraps PascalCase Set", () => {
|
|
477
|
+
const result = unwrapAvpAttributes({
|
|
478
|
+
tags: { Set: [{ String: "admin" }, { String: "ops" }] },
|
|
479
|
+
});
|
|
480
|
+
expect(result["tags"]).toEqual(["admin", "ops"]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("unwraps record with nested typed values", () => {
|
|
484
|
+
const result = unwrapAvpAttributes({
|
|
485
|
+
config: { record: { theme: { string: "dark" }, lang: { string: "en" } } },
|
|
486
|
+
});
|
|
487
|
+
expect(result["config"]).toEqual({ theme: "dark", lang: "en" });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("unwraps PascalCase Record", () => {
|
|
491
|
+
const result = unwrapAvpAttributes({
|
|
492
|
+
config: { Record: { theme: { String: "dark" } } },
|
|
493
|
+
});
|
|
494
|
+
expect(result["config"]).toEqual({ theme: "dark" });
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe("unwrapAvpAttributes — Cedar 4 extension types", () => {
|
|
499
|
+
it("converts ipaddr to WASM __extn format", () => {
|
|
500
|
+
const result = unwrapAvpAttributes({ ip: { ipaddr: "192.168.1.1" } });
|
|
501
|
+
expect(result["ip"]).toEqual({ __extn: { fn: "ip", arg: "192.168.1.1" } });
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("converts decimal to WASM __extn format", () => {
|
|
505
|
+
const result = unwrapAvpAttributes({ price: { decimal: "1.50" } });
|
|
506
|
+
expect(result["price"]).toEqual({ __extn: { fn: "decimal", arg: "1.50" } });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("converts datetime to WASM __extn format", () => {
|
|
510
|
+
const result = unwrapAvpAttributes({ at: { datetime: "2024-10-15T11:35:00Z" } });
|
|
511
|
+
expect(result["at"]).toEqual({ __extn: { fn: "datetime", arg: "2024-10-15T11:35:00Z" } });
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("converts duration to WASM __extn format", () => {
|
|
515
|
+
const result = unwrapAvpAttributes({ ttl: { duration: "1h30m" } });
|
|
516
|
+
expect(result["ttl"]).toEqual({ __extn: { fn: "duration", arg: "1h30m" } });
|
|
517
|
+
});
|
|
518
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
17
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
include: ["test/**/*.test.ts"],
|
|
8
|
+
coverage: {
|
|
9
|
+
reporter: ["text", "lcov"],
|
|
10
|
+
include: ["src/**/*.ts"],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|