@o3co/auth.policy-verifier.cedar-wasm 0.14.0 → 0.15.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @o3co/auth.policy-verifier.cedar-wasm
2
2
 
3
- Last updated: 2026-09-23
3
+ Last updated: 2026-09-26
4
4
 
5
5
  The in-process Cedar engine for
6
6
  [`@o3co/auth.policy-verifier.cedar`](../cedar/README.md): the official
@@ -37,8 +37,13 @@ deployment imports it.
37
37
 
38
38
  **Owns** ([`src/wasmEngine.mts`](src/wasmEngine.mts)):
39
39
 
40
- - Parse-checking each policy file at boot, so a syntax error names its file,
41
- and compiling the set once into wasm memory under an id minted per load.
40
+ - Parsing each policy file at boot, so a syntax error names its file;
41
+ naming each policy for its file (cedar's `namePolicies`, #199), so Cedar's
42
+ `diagnostics.reason` — and a decision's `determiningPolicies` — say
43
+ `30-forbid-contractors` or `20-rules#2` rather than a positional `policy7`;
44
+ refusing a set where two policies would share an id, a policy in a file
45
+ named only `.cedar`, or a template, which nothing here would link; and
46
+ compiling the set once into wasm memory under an id minted per load.
42
47
  - Evaluating each request against that compiled set, synchronously, and
43
48
  rendering the bindings' answer as a `CedarDecision`.
44
49
  - Naming the revision of the source it compiled on every answer.
@@ -77,3 +82,38 @@ a dependency change, not a config change.
77
82
  `@cedar-policy/cedar-wasm` is pinned exactly: Cedar minor releases can carry
78
83
  policy-language changes, so upgrades should be deliberate and re-validated,
79
84
  not fall out of a range resolution.
85
+
86
+ The re-validation is a suite (#198):
87
+ [`cedar-cli-equivalence.test.mts`](../../tests/integration/src/cedar-cli-equivalence.test.mts)
88
+ holds this engine, behind `CedarPolicyRuleCollector`, to the official `cedar`
89
+ CLI at the pinned version.
90
+ - **What it compares.** For every request of a fixture corpus, the CLI is
91
+ asked exactly the request the collector built, and both evaluators must agree
92
+ on the decision, on the policies that determined it, and on which policies
93
+ raised errors. The request itself must be the one the case records, so a
94
+ change to how requests are built shows as a diff. The corpus covers the cases
95
+ the entity synthesis decides:
96
+ - group membership, direct and transitive;
97
+ - entity references;
98
+ - a missing attribute;
99
+ - `forbid` over `permit`;
100
+ - the context allowlist;
101
+ - `resource.idWhenAbsent`.
102
+ - **Policy names.** Each fixture policy carries `@id` with the id its file
103
+ gives it. The CLI names a policy by `@id`, and otherwise by its position, so
104
+ this is how it answers in this engine's names.
105
+ - **How the versions stay together.** CI installs `cedar-policy-cli` at the
106
+ version read from this package's `package.json`, so bumping the pin moves the
107
+ CLI with it. A CLI at any other version fails the suite's version check.
108
+ - **Running it locally.** Install the CLI at the pinned version, then build
109
+ and run the suite. Without a CLI this engine's half is skipped, with a
110
+ notice. The suite's other half measures the `http` engine against a real
111
+ cedar-agent (#284, see the cedar package's README).
112
+
113
+ ```sh
114
+ cargo install cedar-policy-cli --locked --version \
115
+ "$(node -p 'require("./packages/cedar-wasm/package.json").dependencies["@cedar-policy/cedar-wasm"]')"
116
+ pnpm run build
117
+ CEDAR_CLI="$(command -v cedar)" pnpm --filter @o3co/auth.policy-verifier.integration-tests \
118
+ exec vitest run cedar-cli-
119
+ ```
@@ -10,9 +10,16 @@ export interface CedarWasmEngine extends CedarEngine {
10
10
  * The in-process Cedar engine: the official `@cedar-policy/cedar-wasm`
11
11
  * bindings behind the `CedarEngine` port.
12
12
  *
13
- * `load` parse-checks every file individually — so a syntax error names the
14
- * file that contains it — then compiles the concatenated set once, at boot,
15
- * into wasm memory under a per-load id. `isAuthorized` references that id per
13
+ * `load` parses every file on its own — so a syntax error names the file that
14
+ * contains it — splits it into its policies, and names each for its file
15
+ * (`namePolicies`, #199): the file's name for a file that holds one, numbered
16
+ * `#1`, `#2`… for one that holds several. Those are the ids Cedar answers with
17
+ * in `diagnostics.reason` and names in `errors`, and the ids the http engine
18
+ * gives cedar-agent, so a corpus laid out one policy per file reads the same
19
+ * under both. Refused at load: two policies that would share an id (`a.cedar`
20
+ * holding two, beside `a#1.cedar`), a policy in a file named only `.cedar`,
21
+ * and a template. The set is then compiled once, at boot, into
22
+ * wasm memory under a per-load id. `isAuthorized` references that id per
16
23
  * request and re-parses nothing; the call is synchronous, deterministic, and a
17
24
  * few tens of microseconds, so the policy set answers as a plain `Rule`.
18
25
  *
@@ -1 +1 @@
1
- {"version":3,"file":"wasmEngine.d.mts","sourceRoot":"","sources":["../src/wasmEngine.mts"],"names":[],"mappings":"AAeA,OAAO,EAEN,KAAK,WAAW,EAGhB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,MAAM,kCAAkC,CAAC;AAE1C,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,EAAG,MAAe,CAAC;AAEtD,oFAAoF;AACpF,MAAM,WAAW,eAAgB,SAAQ,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,OAAO,sBAAsB,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAAC;CAC/C;AAKD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,eAAe,EAAE,eA0C7B,CAAC"}
1
+ {"version":3,"file":"wasmEngine.d.mts","sourceRoot":"","sources":["../src/wasmEngine.mts"],"names":[],"mappings":"AAeA,OAAO,EAEN,KAAK,WAAW,EAKhB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,MAAM,kCAAkC,CAAC;AAE1C,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,EAAG,MAAe,CAAC;AAEtD,oFAAoF;AACpF,MAAM,WAAW,eAAgB,SAAQ,WAAW;IACnD,QAAQ,CAAC,IAAI,EAAE,OAAO,sBAAsB,CAAC;IAC7C,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAAC;CAC/C;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,eAAe,EAAE,eAuC7B,CAAC"}
@@ -5,8 +5,8 @@
5
5
  * `@cedar-policy/cedar-wasm` bindings, compiling it once at load and answering
6
6
  * synchronously. The package index registers it.
7
7
  */
8
- import { checkParsePolicySet, preparsePolicySet, statefulIsAuthorized, } from "@cedar-policy/cedar-wasm/nodejs";
9
- import { CedarEngineError, } from "@o3co/auth.policy-verifier.cedar";
8
+ import { policySetTextToParts, preparsePolicySet, statefulIsAuthorized, } from "@cedar-policy/cedar-wasm/nodejs";
9
+ import { CedarEngineError, namePolicies, } from "@o3co/auth.policy-verifier.cedar";
10
10
  /** The name this engine registers under, and the config value that selects it. */
11
11
  export const CEDAR_WASM_ENGINE_NAME = "wasm";
12
12
  /** Distinguishes concurrently-loaded policy sets inside the one wasm instance. */
@@ -15,9 +15,16 @@ let policySetCounter = 0;
15
15
  * The in-process Cedar engine: the official `@cedar-policy/cedar-wasm`
16
16
  * bindings behind the `CedarEngine` port.
17
17
  *
18
- * `load` parse-checks every file individually — so a syntax error names the
19
- * file that contains it — then compiles the concatenated set once, at boot,
20
- * into wasm memory under a per-load id. `isAuthorized` references that id per
18
+ * `load` parses every file on its own — so a syntax error names the file that
19
+ * contains it — splits it into its policies, and names each for its file
20
+ * (`namePolicies`, #199): the file's name for a file that holds one, numbered
21
+ * `#1`, `#2`… for one that holds several. Those are the ids Cedar answers with
22
+ * in `diagnostics.reason` and names in `errors`, and the ids the http engine
23
+ * gives cedar-agent, so a corpus laid out one policy per file reads the same
24
+ * under both. Refused at load: two policies that would share an id (`a.cedar`
25
+ * holding two, beside `a#1.cedar`), a policy in a file named only `.cedar`,
26
+ * and a template. The set is then compiled once, at boot, into
27
+ * wasm memory under a per-load id. `isAuthorized` references that id per
21
28
  * request and re-parses nothing; the call is synchronous, deterministic, and a
22
29
  * few tens of microseconds, so the policy set answers as a plain `Rule`.
23
30
  *
@@ -39,16 +46,13 @@ export const cedarWasmEngine = {
39
46
  async: false,
40
47
  confirmsRevision: true,
41
48
  load(source) {
42
- for (const file of source.files) {
43
- const answer = checkParsePolicySet({ staticPolicies: file.text });
44
- if (answer.type === "failure") {
45
- throw new CedarEngineError(`${file.source} failed to parse: ${details(answer.errors)}`);
46
- }
47
- }
49
+ const policies = namePolicies(source.files, policiesOf);
48
50
  // Captured with the compile below, not read per answer — see the doc comment.
49
51
  const { revision } = source;
50
52
  const policySetId = `auth.policy-verifier.cedar-wasm:${policySetCounter++}`;
51
- const compiled = preparsePolicySet(policySetId, { staticPolicies: source.text });
53
+ const compiled = preparsePolicySet(policySetId, {
54
+ staticPolicies: Object.fromEntries(policies.map((policy) => [policy.id, policy.text])),
55
+ });
52
56
  if (compiled.type === "failure") {
53
57
  throw new CedarEngineError(`policy set from ${source.description} failed to compile: ${details(compiled.errors)}`);
54
58
  }
@@ -70,6 +74,30 @@ export const cedarWasmEngine = {
70
74
  };
71
75
  },
72
76
  };
77
+ /**
78
+ * The policies of one file, as text, in the file's order. Cedar splits a file
79
+ * under positional ids — `policy0`, `policy1`… — and hands the parts back
80
+ * sorted by those ids as strings, so `policy10` comes before `policy2`; the
81
+ * order is undone here by sorting the same ids the same way. That sort is the
82
+ * pinned `@cedar-policy/cedar-wasm`'s (see package.json); the test of a file
83
+ * holding a dozen policies fails on a release that sorts otherwise.
84
+ */
85
+ function policiesOf(file) {
86
+ const parts = policySetTextToParts(file.text);
87
+ if (parts.type === "failure") {
88
+ throw new CedarEngineError(`${file.source} failed to parse: ${details(parts.errors)}`);
89
+ }
90
+ if (parts.policy_templates.length > 0) {
91
+ // An unlinked template never applies, and this engine links none.
92
+ throw new CedarEngineError(`${file.source} holds a template — a policy with ?principal or ?resource never applies here, because nothing links it`);
93
+ }
94
+ const sorted = parts.policies.map((_, position) => `policy${position}`).sort();
95
+ const inFileOrder = new Array(parts.policies.length);
96
+ for (const [at, id] of sorted.entries()) {
97
+ inFileOrder[Number(id.slice("policy".length))] = parts.policies[at];
98
+ }
99
+ return inFileOrder;
100
+ }
73
101
  function details(errors) {
74
102
  return errors.map((error) => error.message).join("; ");
75
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/auth.policy-verifier.cedar-wasm",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -26,13 +26,13 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@cedar-policy/cedar-wasm": "4.13.0",
29
- "@o3co/auth.policy-verifier.cedar": "0.14.0"
29
+ "@o3co/auth.policy-verifier.cedar": "0.15.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.6.2",
33
33
  "@vitest/coverage-v8": "^5.0.1",
34
34
  "vitest": "^5.0.1",
35
- "@o3co/auth.policy-verifier.core": "0.14.0"
35
+ "@o3co/auth.policy-verifier.core": "0.15.0"
36
36
  },
37
37
  "scripts": {
38
38
  "prebuild": "rimraf dist",