@o3co/auth.policy-verifier.cedar-wasm 0.14.0 → 0.16.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,24 +10,28 @@ 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
16
- * request and re-parses nothing; the call is synchronous, deterministic, and a
17
- * few tens of microseconds, so the policy set answers as a plain `Rule`.
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`): the file's name for a file that holds one, numbered `#1`,
16
+ * `#2`… for one that holds several. Those are the ids Cedar answers with in
17
+ * `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 wasm memory
22
+ * under a per-load id. `isAuthorized` references that id per request and
23
+ * re-parses nothing; the call is synchronous, deterministic, and a few tens of
24
+ * microseconds, so the policy set answers as a plain `Rule`.
18
25
  *
19
- * What this engine costs is paid at import: the wasm module (about 12 MB on
20
- * disk) is instantiated when the bindings load. That is why it lives in its
21
- * own package — a deployment that evaluates out of process never carries it.
26
+ * The wasm module (about 12 MB on disk) is instantiated when the bindings
27
+ * load, which is why this engine lives in its own package: a deployment that
28
+ * evaluates out of process never carries it.
22
29
  *
23
- * It vouches for the revision it evaluated (`confirmsRevision`, #244), and can:
24
- * the set is compiled here, from the source `load` was handed, under an id
25
- * minted here and known to nothing else. An answer through that id cannot have
26
- * come from any other policies, so every answer names the revision of that
27
- * source — read once, at `load`, beside the compile. `PolicySource` is a plain
28
- * object the caller still holds; reading `source.revision` when answering would
29
- * be reading "the current revision", which is a claim about the object and not
30
- * about the policies that were compiled.
30
+ * It vouches for the revision it evaluated (`confirmsRevision`): the set is
31
+ * compiled here, under an id known to nothing else, so an answer through that
32
+ * id comes from the source `load` was handed and names that source's revision,
33
+ * read once at `load`. Reading `source.revision` when answering would describe
34
+ * the caller's object as it is now, not the policies that were compiled.
31
35
  */
32
36
  export declare const cedarWasmEngine: CedarWasmEngine;
33
37
  //# sourceMappingURL=wasmEngine.d.mts.map
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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,40 +15,41 @@ 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
21
- * request and re-parses nothing; the call is synchronous, deterministic, and a
22
- * few tens of microseconds, so the policy set answers as a plain `Rule`.
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`): the file's name for a file that holds one, numbered `#1`,
21
+ * `#2`… for one that holds several. Those are the ids Cedar answers with in
22
+ * `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 wasm memory
27
+ * under a per-load id. `isAuthorized` references that id per request and
28
+ * re-parses nothing; the call is synchronous, deterministic, and a few tens of
29
+ * microseconds, so the policy set answers as a plain `Rule`.
23
30
  *
24
- * What this engine costs is paid at import: the wasm module (about 12 MB on
25
- * disk) is instantiated when the bindings load. That is why it lives in its
26
- * own package — a deployment that evaluates out of process never carries it.
31
+ * The wasm module (about 12 MB on disk) is instantiated when the bindings
32
+ * load, which is why this engine lives in its own package: a deployment that
33
+ * evaluates out of process never carries it.
27
34
  *
28
- * It vouches for the revision it evaluated (`confirmsRevision`, #244), and can:
29
- * the set is compiled here, from the source `load` was handed, under an id
30
- * minted here and known to nothing else. An answer through that id cannot have
31
- * come from any other policies, so every answer names the revision of that
32
- * source — read once, at `load`, beside the compile. `PolicySource` is a plain
33
- * object the caller still holds; reading `source.revision` when answering would
34
- * be reading "the current revision", which is a claim about the object and not
35
- * about the policies that were compiled.
35
+ * It vouches for the revision it evaluated (`confirmsRevision`): the set is
36
+ * compiled here, under an id known to nothing else, so an answer through that
37
+ * id comes from the source `load` was handed and names that source's revision,
38
+ * read once at `load`. Reading `source.revision` when answering would describe
39
+ * the caller's object as it is now, not the policies that were compiled.
36
40
  */
37
41
  export const cedarWasmEngine = {
38
42
  name: CEDAR_WASM_ENGINE_NAME,
39
43
  async: false,
40
44
  confirmsRevision: true,
41
45
  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
- }
46
+ const policies = namePolicies(source.files, policiesOf);
48
47
  // Captured with the compile below, not read per answer — see the doc comment.
49
48
  const { revision } = source;
50
49
  const policySetId = `auth.policy-verifier.cedar-wasm:${policySetCounter++}`;
51
- const compiled = preparsePolicySet(policySetId, { staticPolicies: source.text });
50
+ const compiled = preparsePolicySet(policySetId, {
51
+ staticPolicies: Object.fromEntries(policies.map((policy) => [policy.id, policy.text])),
52
+ });
52
53
  if (compiled.type === "failure") {
53
54
  throw new CedarEngineError(`policy set from ${source.description} failed to compile: ${details(compiled.errors)}`);
54
55
  }
@@ -70,6 +71,30 @@ export const cedarWasmEngine = {
70
71
  };
71
72
  },
72
73
  };
74
+ /**
75
+ * The policies of one file, as text, in the file's order. Cedar splits a file
76
+ * under positional ids — `policy0`, `policy1`… — and hands the parts back
77
+ * sorted by those ids as strings, so `policy10` comes before `policy2`; the
78
+ * order is undone here by sorting the same ids the same way. That sort is the
79
+ * pinned `@cedar-policy/cedar-wasm`'s (see package.json); the test of a file
80
+ * holding a dozen policies fails on a release that sorts otherwise.
81
+ */
82
+ function policiesOf(file) {
83
+ const parts = policySetTextToParts(file.text);
84
+ if (parts.type === "failure") {
85
+ throw new CedarEngineError(`${file.source} failed to parse: ${details(parts.errors)}`);
86
+ }
87
+ if (parts.policy_templates.length > 0) {
88
+ // An unlinked template never applies, and this engine links none.
89
+ throw new CedarEngineError(`${file.source} holds a template — a policy with ?principal or ?resource never applies here, because nothing links it`);
90
+ }
91
+ const sorted = parts.policies.map((_, position) => `policy${position}`).sort();
92
+ const inFileOrder = new Array(parts.policies.length);
93
+ for (const [at, id] of sorted.entries()) {
94
+ inFileOrder[Number(id.slice("policy".length))] = parts.policies[at];
95
+ }
96
+ return inFileOrder;
97
+ }
73
98
  function details(errors) {
74
99
  return errors.map((error) => error.message).join("; ");
75
100
  }
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.16.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.16.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/node": "^26.6.2",
33
- "@vitest/coverage-v8": "^5.0.1",
34
- "vitest": "^5.0.1",
35
- "@o3co/auth.policy-verifier.core": "0.14.0"
32
+ "@types/node": "^26.6.3",
33
+ "@vitest/coverage-v8": "^5.0.2",
34
+ "vitest": "^5.0.2",
35
+ "@o3co/auth.policy-verifier.core": "0.16.0"
36
36
  },
37
37
  "scripts": {
38
38
  "prebuild": "rimraf dist",