├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── post-tag.yaml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── build.sh
├── capabilities.json
├── e2e.sh
├── examples
├── deno
│ ├── .gitignore
│ ├── Makefile
│ ├── main.ts
│ └── test.rego
├── nodejs-app
│ ├── .gitignore
│ ├── README.md
│ ├── app.js
│ ├── example.rego
│ ├── package-lock.json
│ └── package.json
├── nodejs-ts-app-multi-entrypoint
│ ├── .gitignore
│ ├── README.md
│ ├── app.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── policies
│ │ ├── example-one.rego
│ │ └── example-two.rego
│ └── tsconfig.json
└── nodejs-ts-app
│ ├── .gitignore
│ ├── README.md
│ ├── app.ts
│ ├── example.rego
│ ├── package-lock.json
│ ├── package.json
│ └── tsconfig.json
├── package-lock.json
├── package.json
├── src
├── builtins
│ ├── index.js
│ ├── json.js
│ ├── regex.js
│ ├── strings.js
│ └── yaml.js
├── index.cjs
├── index.mjs
└── opa.js
└── test
├── browser-integration.test.js
├── fixtures
├── custom-builtins
│ ├── capabilities.json
│ └── custom-builtins-policy.rego
├── data-stress
│ ├── .gitignore
│ ├── base-data.json
│ └── example-one.rego
├── load-policy-sync-worker.js
├── memory
│ ├── .gitignore
│ └── policy.rego
├── multiple-entrypoints
│ ├── .gitignore
│ ├── example-one.rego
│ └── example-two.rego
├── stringified-support
│ ├── .gitignore
│ ├── stringified-support-data.json
│ └── stringified-support-policy.rego
└── yaml-support
│ ├── .gitignore
│ └── yaml-support-policy.rego
├── memory.test.js
├── multiple-entrypoints.test.js
├── opa-custom-builtins.test.js
├── opa-large-data.test.js
├── opa-node-cases.test.js
├── opa-stringified-support.test.js
├── opa-test-cases.test.js
└── opa-yaml-support.test.js
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
8 | - package-ecosystem: "npm"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | # When a new revision is pushed to a PR, cancel all in-progress CI runs for that
10 | # PR. See https://docs.github.com/en/actions/using-jobs/using-concurrency
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 | lint:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: denoland/setup-deno@v2
20 | with:
21 | deno-version: v1.39.2
22 | - uses: actions/checkout@v4
23 | - run: deno fmt --check
24 | - run: deno lint
25 |
26 | build:
27 | runs-on: ubuntu-latest
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | node-version: [18.x, 20.x, 21.x]
32 | opa-version:
33 | - 0.30.2 # last version with ABI 1.1, 0.31.0+ has ABI 1.2
34 | - 0.41.0 # 0.35.0 is the first release with https://github.com/open-policy-agent/opa/pull/4055
35 |
36 | steps:
37 | - uses: actions/checkout@v4
38 |
39 | - name: Checkout OPA v${{ matrix.opa-version }}
40 | uses: actions/checkout@v4
41 | with:
42 | repository: open-policy-agent/opa
43 | ref: v${{ matrix.opa-version }}
44 | path: opa
45 |
46 | - run: mkdir test/cases
47 |
48 | - name: Prep OPA cases
49 | working-directory: opa
50 | run: WASM_BUILD_ONLY=true make wasm-rego-test
51 |
52 | # NOTE(sr): we've got to get rid of the opa checkout because the test
53 | # runner would otherwise pick up any .js files it finds in there.
54 | - name: Unpack OPA cases
55 | run: >
56 | tar zxvf opa/.go/cache/testcases.tar.gz --exclude='*.js' -C test/cases &&
57 | mv opa/test/cases/testdata testdata &&
58 | rm -rf opa/
59 |
60 | - name: Use Node.js ${{ matrix.node-version }}
61 | uses: actions/setup-node@v4
62 | with:
63 | node-version: ${{ matrix.node-version }}
64 |
65 | - name: Install Open Policy Agent ${{ matrix.opa-version }}
66 | uses: open-policy-agent/setup-opa@v2
67 | with:
68 | version: v${{ matrix.opa-version }}
69 | - run: npm ci
70 | - run: npm run build
71 | - run: npm test
72 | env:
73 | OPA_CASES: test/cases/
74 | OPA_TEST_CASES: testdata
75 |
76 | examples-node:
77 | name: NodeJS examples
78 | runs-on: ubuntu-latest
79 | steps:
80 | - uses: actions/checkout@v4
81 | - uses: open-policy-agent/setup-opa@v2
82 | - uses: actions/setup-node@v4
83 | with:
84 | node-version: "20.x"
85 | - name: nodejs
86 | run: >
87 | npm ci
88 | npm run build
89 | ./e2e.sh
90 |
91 | examples-deno:
92 | name: Deno examples
93 | runs-on: ubuntu-latest
94 | steps:
95 | - uses: actions/checkout@v4
96 | - uses: open-policy-agent/setup-opa@v2
97 | - uses: denoland/setup-deno@v2
98 | with:
99 | deno-version: v1.39.2
100 | - run: make
101 | working-directory: examples/deno
102 |
--------------------------------------------------------------------------------
/.github/workflows/post-tag.yaml:
--------------------------------------------------------------------------------
1 | name: Publish Node.js Package
2 | on:
3 | push:
4 | tags:
5 | - '*'
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | environment: ci # contains the secret NPM_TOKEN used below
10 | steps:
11 | - uses: actions/checkout@v4
12 | # Setup .npmrc file to publish to npm
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: '14.x'
16 | registry-url: 'https://registry.npmjs.org'
17 | - run: npm install
18 | - run: npm run build
19 | - run: npm publish
20 | env:
21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | *.wasm
4 | node_modules
5 | *.lock
6 | types/
7 | dist/
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Work in Progress -- Contributions welcome!!**
2 |
3 | # Open Policy Agent WebAssemby NPM Module
4 |
5 | This is the source for the
6 | [@open-policy-agent/opa-wasm](https://www.npmjs.com/package/@open-policy-agent/opa-wasm)
7 | NPM module which is a small SDK for using WebAssembly (wasm) compiled
8 | [Open Policy Agent](https://www.openpolicyagent.org/) Rego policies.
9 |
10 | # Getting Started
11 |
12 | ## Install the module
13 |
14 | ```
15 | npm install @open-policy-agent/opa-wasm
16 | ```
17 |
18 | ## Usage
19 |
20 | There are only a couple of steps required to start evaluating the policy.
21 |
22 | ### Import the module
23 |
24 | ```javascript
25 | const { loadPolicy } = require("@open-policy-agent/opa-wasm");
26 | ```
27 |
28 | ### Load the policy
29 |
30 | ```javascript
31 | loadPolicy(policyWasm);
32 | ```
33 |
34 | The `loadPolicy` function returns a Promise with the loaded policy. Typically
35 | this means loading it in an `async` function like:
36 |
37 | ```javascript
38 | const policy = await loadPolicy(policyWasm);
39 | ```
40 |
41 | Or something like:
42 |
43 | ```javascript
44 | loadPolicy(policyWasm).then((policy) => {
45 | // evaluate or save the policy
46 | }, (error) => {
47 | console.error("Failed to load policy: " + error);
48 | });
49 | ```
50 |
51 | The `policyWasm` needs to be either the raw byte array of the compiled policy
52 | Wasm file, or a WebAssembly module.
53 |
54 | For example:
55 |
56 | ```javascript
57 | const fs = require("fs");
58 |
59 | const policyWasm = fs.readFileSync("policy.wasm");
60 | ```
61 |
62 | Alternatively the bytes can be pulled in remotely from a `fetch` or in some
63 | cases (like CloudFlare Workers) the Wasm binary can be loaded directly into the
64 | javascript context through external APIs.
65 |
66 | ### Evaluate the Policy
67 |
68 | The loaded policy object returned from `loadPolicy()` has a couple of important
69 | APIs for policy evaluation:
70 |
71 | `setData(data)` -- Provide an external `data` document for policy evaluation.
72 |
73 | - `data` MUST be a serializable object or `ArrayBuffer`, which assumed to be a
74 | well-formed stringified JSON
75 |
76 | `evaluate(input)` -- Evaluates the policy using any loaded data and the supplied
77 | `input` document.
78 |
79 | - `input` parameter MAY be an `object`, primitive literal or `ArrayBuffer`,
80 | which assumed to be a well-formed stringified JSON
81 |
82 | > `ArrayBuffer` supported in the APIs above as a performance optimisation
83 | > feature, given that either network or file system provided contents can easily
84 | > be represented as `ArrayBuffer` in a very performant way.
85 |
86 | Example:
87 |
88 | ```javascript
89 | input = '{"path": "/", "role": "admin"}';
90 |
91 | loadPolicy(policyWasm).then((policy) => {
92 | resultSet = policy.evaluate(input);
93 | if (resultSet == null) {
94 | console.error("evaluation error");
95 | } else if (resultSet.length == 0) {
96 | console.log("undefined");
97 | } else {
98 | console.log("allowed = " + resultSet[0].result);
99 | }
100 | }).catch((error) => {
101 | console.error("Failed to load policy: ", error);
102 | });
103 | ```
104 |
105 | > For any `opa build` created WASM binaries the result set, when defined, will
106 | > contain a `result` key with the value of the compiled entrypoint. See
107 | > [https://www.openpolicyagent.org/docs/latest/wasm/](https://www.openpolicyagent.org/docs/latest/wasm/)
108 | > for more details.
109 |
110 | ### Writing the policy
111 |
112 | See
113 | [https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/](https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/)
114 |
115 | ### Compiling the policy
116 |
117 | Either use the
118 | [Compile REST API](https://www.openpolicyagent.org/docs/latest/rest-api/#compile-api)
119 | or `opa build` CLI tool.
120 |
121 | For example, with OPA v0.20.5+:
122 |
123 | ```bash
124 | opa build -t wasm -e example/allow example.rego
125 | ```
126 |
127 | Which is compiling the `example.rego` policy file with the result set to
128 | `data.example.allow`. The result will be an OPA bundle with the `policy.wasm`
129 | binary included. See [./examples](./examples) for a more comprehensive example.
130 |
131 | See `opa build --help` for more details.
132 |
133 | ## Development
134 |
135 | ### Lint and Format checks
136 |
137 | This project is using Deno's
138 | [lint](https://deno.land/manual@v1.14.0/tools/linter) and
139 | [formatter](https://deno.land/manual@v1.14.0/tools/formatter) tools in CI. With
140 | `deno`
141 | [installed locally](https://deno.land/manual@v1.14.0/getting_started/installation),
142 | the same checks can be invoked using `npm`:
143 |
144 | - `npm run lint`
145 | - `npm run fmt` -- this will fix the formatting
146 | - `npm run fmt:check` -- this happens in CI
147 |
148 | All of these operate on git-tracked files, so make sure you've committed the
149 | code you'd like to see checked. Alternatively, you can invoke
150 | `deno lint my_new_file.js` directly, too.
151 |
152 | ### Build
153 |
154 | The published package provides four different entrypoints for consumption:
155 |
156 | 1. A CommonJS module for consumption with older versions of Node or those using
157 | `require()`:
158 | ```js
159 | const { loadPolicy } = require("@open-policy-agent/opa-wasm");
160 | ```
161 | 1. An ESM module for consumption with newer versions of Node:
162 | ```js
163 | import { loadPolicy } from "@open-policy-agent/opa-wasm";
164 | ```
165 | 1. An ESM module for consumption in modern browsers (this will contain all
166 | dependencies already bundled and can be used standalone).
167 | ```html
168 |
172 | ```
173 | 1. A script for consumption in all browsers (this will export an `opa` global
174 | variable).
175 | ```js
176 |
177 |
180 | ```
181 |
182 | The browser builds are generated in the `./build.sh` script and use
183 | [`esbuild`][esbuild]. All exports are defined in the `exports` field in the
184 | package.json file. More detials on how these work are described in the
185 | [Conditional Exports][conditional-exports] documentation.
186 |
187 | For TypeScript projects we also generate an opa.d.ts declaration file that will
188 | give correct typings and is also defined under the `types` field in the
189 | package.json.
190 |
191 | [esbuild]: https://esbuild.github.io/
192 | [conditional-exports]: https://nodejs.org/api/packages.html#conditional-exports
193 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Generates browser compatible versions of the package with dependencies
3 | # bundled as well as the type declaration.
4 | set -euo pipefail
5 |
6 | entrypoint=./src/opa.js
7 | outdir=./dist
8 | package=$(node -pe 'require("./package.json").name.split("/").pop()')
9 |
10 | if [[ ! -x $(npm bin)/esbuild || ! -x $(npm bin)/tsc ]]; then
11 | echo "Installing dependencies…"
12 | npm install
13 | fi
14 |
15 | echo "Generating default browser build…"
16 | npx esbuild $entrypoint \
17 | --outfile=$outdir/$package-browser.js \
18 | --bundle \
19 | --sourcemap \
20 | --minify \
21 | --format=iife \
22 | --platform=browser \
23 | --define:global=window \
24 | --global-name=opa \
25 | --external:util
26 |
27 | echo "Generating esm browser build…"
28 | npx esbuild $entrypoint \
29 | --outfile=$outdir/$package-browser.esm.js \
30 | --bundle \
31 | --sourcemap \
32 | --minify \
33 | --format=esm \
34 | --platform=browser \
35 | --define:global=window \
36 | --external:util
37 |
38 | echo "Generating TypeScript declaration file…"
39 | npx tsc ./src/index.mjs \
40 | --declaration \
41 | --allowJs \
42 | --emitDeclarationOnly \
43 | --outDir $outdir/types
44 |
45 | mv $outdir/types/opa.d.ts $outdir/types/opa.d.mts
46 | cp $outdir/types/opa.d.mts $outdir/types/opa.d.cts
47 |
--------------------------------------------------------------------------------
/capabilities.json:
--------------------------------------------------------------------------------
1 | {
2 | "builtins": [
3 | {
4 | "name": "abs",
5 | "decl": {
6 | "args": [
7 | {
8 | "type": "number"
9 | }
10 | ],
11 | "result": {
12 | "type": "number"
13 | },
14 | "type": "function"
15 | }
16 | },
17 | {
18 | "name": "all",
19 | "decl": {
20 | "args": [
21 | {
22 | "of": [
23 | {
24 | "of": {
25 | "type": "any"
26 | },
27 | "type": "set"
28 | },
29 | {
30 | "dynamic": {
31 | "type": "any"
32 | },
33 | "type": "array"
34 | }
35 | ],
36 | "type": "any"
37 | }
38 | ],
39 | "result": {
40 | "type": "boolean"
41 | },
42 | "type": "function"
43 | }
44 | },
45 | {
46 | "name": "and",
47 | "decl": {
48 | "args": [
49 | {
50 | "of": {
51 | "type": "any"
52 | },
53 | "type": "set"
54 | },
55 | {
56 | "of": {
57 | "type": "any"
58 | },
59 | "type": "set"
60 | }
61 | ],
62 | "result": {
63 | "of": {
64 | "type": "any"
65 | },
66 | "type": "set"
67 | },
68 | "type": "function"
69 | },
70 | "infix": "\u0026"
71 | },
72 | {
73 | "name": "any",
74 | "decl": {
75 | "args": [
76 | {
77 | "of": [
78 | {
79 | "of": {
80 | "type": "any"
81 | },
82 | "type": "set"
83 | },
84 | {
85 | "dynamic": {
86 | "type": "any"
87 | },
88 | "type": "array"
89 | }
90 | ],
91 | "type": "any"
92 | }
93 | ],
94 | "result": {
95 | "type": "boolean"
96 | },
97 | "type": "function"
98 | }
99 | },
100 | {
101 | "name": "array.concat",
102 | "decl": {
103 | "args": [
104 | {
105 | "dynamic": {
106 | "type": "any"
107 | },
108 | "type": "array"
109 | },
110 | {
111 | "dynamic": {
112 | "type": "any"
113 | },
114 | "type": "array"
115 | }
116 | ],
117 | "result": {
118 | "dynamic": {
119 | "type": "any"
120 | },
121 | "type": "array"
122 | },
123 | "type": "function"
124 | }
125 | },
126 | {
127 | "name": "array.slice",
128 | "decl": {
129 | "args": [
130 | {
131 | "dynamic": {
132 | "type": "any"
133 | },
134 | "type": "array"
135 | },
136 | {
137 | "type": "number"
138 | },
139 | {
140 | "type": "number"
141 | }
142 | ],
143 | "result": {
144 | "dynamic": {
145 | "type": "any"
146 | },
147 | "type": "array"
148 | },
149 | "type": "function"
150 | }
151 | },
152 | {
153 | "name": "assign",
154 | "decl": {
155 | "args": [
156 | {
157 | "type": "any"
158 | },
159 | {
160 | "type": "any"
161 | }
162 | ],
163 | "result": {
164 | "type": "boolean"
165 | },
166 | "type": "function"
167 | },
168 | "infix": ":="
169 | },
170 | {
171 | "name": "base64.decode",
172 | "decl": {
173 | "args": [
174 | {
175 | "type": "string"
176 | }
177 | ],
178 | "result": {
179 | "type": "string"
180 | },
181 | "type": "function"
182 | }
183 | },
184 | {
185 | "name": "base64.encode",
186 | "decl": {
187 | "args": [
188 | {
189 | "type": "string"
190 | }
191 | ],
192 | "result": {
193 | "type": "string"
194 | },
195 | "type": "function"
196 | }
197 | },
198 | {
199 | "name": "base64.is_valid",
200 | "decl": {
201 | "args": [
202 | {
203 | "type": "string"
204 | }
205 | ],
206 | "result": {
207 | "type": "boolean"
208 | },
209 | "type": "function"
210 | }
211 | },
212 | {
213 | "name": "base64url.decode",
214 | "decl": {
215 | "args": [
216 | {
217 | "type": "string"
218 | }
219 | ],
220 | "result": {
221 | "type": "string"
222 | },
223 | "type": "function"
224 | }
225 | },
226 | {
227 | "name": "base64url.encode",
228 | "decl": {
229 | "args": [
230 | {
231 | "type": "string"
232 | }
233 | ],
234 | "result": {
235 | "type": "string"
236 | },
237 | "type": "function"
238 | }
239 | },
240 | {
241 | "name": "bits.and",
242 | "decl": {
243 | "args": [
244 | {
245 | "type": "number"
246 | },
247 | {
248 | "type": "number"
249 | }
250 | ],
251 | "result": {
252 | "type": "number"
253 | },
254 | "type": "function"
255 | }
256 | },
257 | {
258 | "name": "bits.lsh",
259 | "decl": {
260 | "args": [
261 | {
262 | "type": "number"
263 | },
264 | {
265 | "type": "number"
266 | }
267 | ],
268 | "result": {
269 | "type": "number"
270 | },
271 | "type": "function"
272 | }
273 | },
274 | {
275 | "name": "bits.negate",
276 | "decl": {
277 | "args": [
278 | {
279 | "type": "number"
280 | }
281 | ],
282 | "result": {
283 | "type": "number"
284 | },
285 | "type": "function"
286 | }
287 | },
288 | {
289 | "name": "bits.or",
290 | "decl": {
291 | "args": [
292 | {
293 | "type": "number"
294 | },
295 | {
296 | "type": "number"
297 | }
298 | ],
299 | "result": {
300 | "type": "number"
301 | },
302 | "type": "function"
303 | }
304 | },
305 | {
306 | "name": "bits.rsh",
307 | "decl": {
308 | "args": [
309 | {
310 | "type": "number"
311 | },
312 | {
313 | "type": "number"
314 | }
315 | ],
316 | "result": {
317 | "type": "number"
318 | },
319 | "type": "function"
320 | }
321 | },
322 | {
323 | "name": "bits.xor",
324 | "decl": {
325 | "args": [
326 | {
327 | "type": "number"
328 | },
329 | {
330 | "type": "number"
331 | }
332 | ],
333 | "result": {
334 | "type": "number"
335 | },
336 | "type": "function"
337 | }
338 | },
339 | {
340 | "name": "ceil",
341 | "decl": {
342 | "args": [
343 | {
344 | "type": "number"
345 | }
346 | ],
347 | "result": {
348 | "type": "number"
349 | },
350 | "type": "function"
351 | }
352 | },
353 | {
354 | "name": "concat",
355 | "decl": {
356 | "args": [
357 | {
358 | "type": "string"
359 | },
360 | {
361 | "of": [
362 | {
363 | "of": {
364 | "type": "string"
365 | },
366 | "type": "set"
367 | },
368 | {
369 | "dynamic": {
370 | "type": "string"
371 | },
372 | "type": "array"
373 | }
374 | ],
375 | "type": "any"
376 | }
377 | ],
378 | "result": {
379 | "type": "string"
380 | },
381 | "type": "function"
382 | }
383 | },
384 | {
385 | "name": "contains",
386 | "decl": {
387 | "args": [
388 | {
389 | "type": "string"
390 | },
391 | {
392 | "type": "string"
393 | }
394 | ],
395 | "result": {
396 | "type": "boolean"
397 | },
398 | "type": "function"
399 | }
400 | },
401 | {
402 | "name": "count",
403 | "decl": {
404 | "args": [
405 | {
406 | "of": [
407 | {
408 | "of": {
409 | "type": "any"
410 | },
411 | "type": "set"
412 | },
413 | {
414 | "dynamic": {
415 | "type": "any"
416 | },
417 | "type": "array"
418 | },
419 | {
420 | "dynamic": {
421 | "key": {
422 | "type": "any"
423 | },
424 | "value": {
425 | "type": "any"
426 | }
427 | },
428 | "type": "object"
429 | },
430 | {
431 | "type": "string"
432 | }
433 | ],
434 | "type": "any"
435 | }
436 | ],
437 | "result": {
438 | "type": "number"
439 | },
440 | "type": "function"
441 | }
442 | },
443 | {
444 | "name": "div",
445 | "decl": {
446 | "args": [
447 | {
448 | "type": "number"
449 | },
450 | {
451 | "type": "number"
452 | }
453 | ],
454 | "result": {
455 | "type": "number"
456 | },
457 | "type": "function"
458 | },
459 | "infix": "/"
460 | },
461 | {
462 | "name": "endswith",
463 | "decl": {
464 | "args": [
465 | {
466 | "type": "string"
467 | },
468 | {
469 | "type": "string"
470 | }
471 | ],
472 | "result": {
473 | "type": "boolean"
474 | },
475 | "type": "function"
476 | }
477 | },
478 | {
479 | "name": "eq",
480 | "decl": {
481 | "args": [
482 | {
483 | "type": "any"
484 | },
485 | {
486 | "type": "any"
487 | }
488 | ],
489 | "result": {
490 | "type": "boolean"
491 | },
492 | "type": "function"
493 | },
494 | "infix": "="
495 | },
496 | {
497 | "name": "equal",
498 | "decl": {
499 | "args": [
500 | {
501 | "type": "any"
502 | },
503 | {
504 | "type": "any"
505 | }
506 | ],
507 | "result": {
508 | "type": "boolean"
509 | },
510 | "type": "function"
511 | },
512 | "infix": "=="
513 | },
514 | {
515 | "name": "floor",
516 | "decl": {
517 | "args": [
518 | {
519 | "type": "number"
520 | }
521 | ],
522 | "result": {
523 | "type": "number"
524 | },
525 | "type": "function"
526 | }
527 | },
528 | {
529 | "name": "format_int",
530 | "decl": {
531 | "args": [
532 | {
533 | "type": "number"
534 | },
535 | {
536 | "type": "number"
537 | }
538 | ],
539 | "result": {
540 | "type": "string"
541 | },
542 | "type": "function"
543 | }
544 | },
545 | {
546 | "name": "glob.match",
547 | "decl": {
548 | "args": [
549 | {
550 | "type": "string"
551 | },
552 | {
553 | "dynamic": {
554 | "type": "string"
555 | },
556 | "type": "array"
557 | },
558 | {
559 | "type": "string"
560 | }
561 | ],
562 | "result": {
563 | "type": "boolean"
564 | },
565 | "type": "function"
566 | }
567 | },
568 | {
569 | "name": "graph.reachable",
570 | "decl": {
571 | "args": [
572 | {
573 | "dynamic": {
574 | "key": {
575 | "type": "any"
576 | },
577 | "value": {
578 | "of": [
579 | {
580 | "of": {
581 | "type": "any"
582 | },
583 | "type": "set"
584 | },
585 | {
586 | "dynamic": {
587 | "type": "any"
588 | },
589 | "type": "array"
590 | }
591 | ],
592 | "type": "any"
593 | }
594 | },
595 | "type": "object"
596 | },
597 | {
598 | "of": [
599 | {
600 | "of": {
601 | "type": "any"
602 | },
603 | "type": "set"
604 | },
605 | {
606 | "dynamic": {
607 | "type": "any"
608 | },
609 | "type": "array"
610 | }
611 | ],
612 | "type": "any"
613 | }
614 | ],
615 | "result": {
616 | "of": {
617 | "type": "any"
618 | },
619 | "type": "set"
620 | },
621 | "type": "function"
622 | }
623 | },
624 | {
625 | "name": "gt",
626 | "decl": {
627 | "args": [
628 | {
629 | "type": "any"
630 | },
631 | {
632 | "type": "any"
633 | }
634 | ],
635 | "result": {
636 | "type": "boolean"
637 | },
638 | "type": "function"
639 | },
640 | "infix": "\u003e"
641 | },
642 | {
643 | "name": "gte",
644 | "decl": {
645 | "args": [
646 | {
647 | "type": "any"
648 | },
649 | {
650 | "type": "any"
651 | }
652 | ],
653 | "result": {
654 | "type": "boolean"
655 | },
656 | "type": "function"
657 | },
658 | "infix": "\u003e="
659 | },
660 | {
661 | "name": "indexof",
662 | "decl": {
663 | "args": [
664 | {
665 | "type": "string"
666 | },
667 | {
668 | "type": "string"
669 | }
670 | ],
671 | "result": {
672 | "type": "number"
673 | },
674 | "type": "function"
675 | }
676 | },
677 | {
678 | "name": "internal.member_2",
679 | "decl": {
680 | "args": [
681 | {
682 | "type": "any"
683 | },
684 | {
685 | "type": "any"
686 | }
687 | ],
688 | "result": {
689 | "type": "boolean"
690 | },
691 | "type": "function"
692 | },
693 | "infix": "in"
694 | },
695 | {
696 | "name": "internal.member_3",
697 | "decl": {
698 | "args": [
699 | {
700 | "type": "any"
701 | },
702 | {
703 | "type": "any"
704 | },
705 | {
706 | "type": "any"
707 | }
708 | ],
709 | "result": {
710 | "type": "boolean"
711 | },
712 | "type": "function"
713 | },
714 | "infix": "in"
715 | },
716 | {
717 | "name": "intersection",
718 | "decl": {
719 | "args": [
720 | {
721 | "of": {
722 | "of": {
723 | "type": "any"
724 | },
725 | "type": "set"
726 | },
727 | "type": "set"
728 | }
729 | ],
730 | "result": {
731 | "of": {
732 | "type": "any"
733 | },
734 | "type": "set"
735 | },
736 | "type": "function"
737 | }
738 | },
739 | {
740 | "name": "is_array",
741 | "decl": {
742 | "args": [
743 | {
744 | "type": "any"
745 | }
746 | ],
747 | "result": {
748 | "type": "boolean"
749 | },
750 | "type": "function"
751 | }
752 | },
753 | {
754 | "name": "is_boolean",
755 | "decl": {
756 | "args": [
757 | {
758 | "type": "any"
759 | }
760 | ],
761 | "result": {
762 | "type": "boolean"
763 | },
764 | "type": "function"
765 | }
766 | },
767 | {
768 | "name": "is_null",
769 | "decl": {
770 | "args": [
771 | {
772 | "type": "any"
773 | }
774 | ],
775 | "result": {
776 | "type": "boolean"
777 | },
778 | "type": "function"
779 | }
780 | },
781 | {
782 | "name": "is_number",
783 | "decl": {
784 | "args": [
785 | {
786 | "type": "any"
787 | }
788 | ],
789 | "result": {
790 | "type": "boolean"
791 | },
792 | "type": "function"
793 | }
794 | },
795 | {
796 | "name": "is_object",
797 | "decl": {
798 | "args": [
799 | {
800 | "type": "any"
801 | }
802 | ],
803 | "result": {
804 | "type": "boolean"
805 | },
806 | "type": "function"
807 | }
808 | },
809 | {
810 | "name": "is_set",
811 | "decl": {
812 | "args": [
813 | {
814 | "type": "any"
815 | }
816 | ],
817 | "result": {
818 | "type": "boolean"
819 | },
820 | "type": "function"
821 | }
822 | },
823 | {
824 | "name": "is_string",
825 | "decl": {
826 | "args": [
827 | {
828 | "type": "any"
829 | }
830 | ],
831 | "result": {
832 | "type": "boolean"
833 | },
834 | "type": "function"
835 | }
836 | },
837 | {
838 | "name": "json.filter",
839 | "decl": {
840 | "args": [
841 | {
842 | "dynamic": {
843 | "key": {
844 | "type": "any"
845 | },
846 | "value": {
847 | "type": "any"
848 | }
849 | },
850 | "type": "object"
851 | },
852 | {
853 | "of": [
854 | {
855 | "dynamic": {
856 | "of": [
857 | {
858 | "type": "string"
859 | },
860 | {
861 | "dynamic": {
862 | "type": "any"
863 | },
864 | "type": "array"
865 | }
866 | ],
867 | "type": "any"
868 | },
869 | "type": "array"
870 | },
871 | {
872 | "of": {
873 | "of": [
874 | {
875 | "type": "string"
876 | },
877 | {
878 | "dynamic": {
879 | "type": "any"
880 | },
881 | "type": "array"
882 | }
883 | ],
884 | "type": "any"
885 | },
886 | "type": "set"
887 | }
888 | ],
889 | "type": "any"
890 | }
891 | ],
892 | "result": {
893 | "type": "any"
894 | },
895 | "type": "function"
896 | }
897 | },
898 | {
899 | "name": "json.is_valid",
900 | "decl": {
901 | "args": [
902 | {
903 | "type": "string"
904 | }
905 | ],
906 | "result": {
907 | "type": "boolean"
908 | },
909 | "type": "function"
910 | }
911 | },
912 | {
913 | "name": "json.marshal",
914 | "decl": {
915 | "args": [
916 | {
917 | "type": "any"
918 | }
919 | ],
920 | "result": {
921 | "type": "string"
922 | },
923 | "type": "function"
924 | }
925 | },
926 | {
927 | "name": "json.remove",
928 | "decl": {
929 | "args": [
930 | {
931 | "dynamic": {
932 | "key": {
933 | "type": "any"
934 | },
935 | "value": {
936 | "type": "any"
937 | }
938 | },
939 | "type": "object"
940 | },
941 | {
942 | "of": [
943 | {
944 | "dynamic": {
945 | "of": [
946 | {
947 | "type": "string"
948 | },
949 | {
950 | "dynamic": {
951 | "type": "any"
952 | },
953 | "type": "array"
954 | }
955 | ],
956 | "type": "any"
957 | },
958 | "type": "array"
959 | },
960 | {
961 | "of": {
962 | "of": [
963 | {
964 | "type": "string"
965 | },
966 | {
967 | "dynamic": {
968 | "type": "any"
969 | },
970 | "type": "array"
971 | }
972 | ],
973 | "type": "any"
974 | },
975 | "type": "set"
976 | }
977 | ],
978 | "type": "any"
979 | }
980 | ],
981 | "result": {
982 | "type": "any"
983 | },
984 | "type": "function"
985 | }
986 | },
987 | {
988 | "name": "json.unmarshal",
989 | "decl": {
990 | "args": [
991 | {
992 | "type": "string"
993 | }
994 | ],
995 | "result": {
996 | "type": "any"
997 | },
998 | "type": "function"
999 | }
1000 | },
1001 | {
1002 | "name": "lower",
1003 | "decl": {
1004 | "args": [
1005 | {
1006 | "type": "string"
1007 | }
1008 | ],
1009 | "result": {
1010 | "type": "string"
1011 | },
1012 | "type": "function"
1013 | }
1014 | },
1015 | {
1016 | "name": "lt",
1017 | "decl": {
1018 | "args": [
1019 | {
1020 | "type": "any"
1021 | },
1022 | {
1023 | "type": "any"
1024 | }
1025 | ],
1026 | "result": {
1027 | "type": "boolean"
1028 | },
1029 | "type": "function"
1030 | },
1031 | "infix": "\u003c"
1032 | },
1033 | {
1034 | "name": "lte",
1035 | "decl": {
1036 | "args": [
1037 | {
1038 | "type": "any"
1039 | },
1040 | {
1041 | "type": "any"
1042 | }
1043 | ],
1044 | "result": {
1045 | "type": "boolean"
1046 | },
1047 | "type": "function"
1048 | },
1049 | "infix": "\u003c="
1050 | },
1051 | {
1052 | "name": "max",
1053 | "decl": {
1054 | "args": [
1055 | {
1056 | "of": [
1057 | {
1058 | "of": {
1059 | "type": "any"
1060 | },
1061 | "type": "set"
1062 | },
1063 | {
1064 | "dynamic": {
1065 | "type": "any"
1066 | },
1067 | "type": "array"
1068 | }
1069 | ],
1070 | "type": "any"
1071 | }
1072 | ],
1073 | "result": {
1074 | "type": "any"
1075 | },
1076 | "type": "function"
1077 | }
1078 | },
1079 | {
1080 | "name": "min",
1081 | "decl": {
1082 | "args": [
1083 | {
1084 | "of": [
1085 | {
1086 | "of": {
1087 | "type": "any"
1088 | },
1089 | "type": "set"
1090 | },
1091 | {
1092 | "dynamic": {
1093 | "type": "any"
1094 | },
1095 | "type": "array"
1096 | }
1097 | ],
1098 | "type": "any"
1099 | }
1100 | ],
1101 | "result": {
1102 | "type": "any"
1103 | },
1104 | "type": "function"
1105 | }
1106 | },
1107 | {
1108 | "name": "minus",
1109 | "decl": {
1110 | "args": [
1111 | {
1112 | "of": [
1113 | {
1114 | "type": "number"
1115 | },
1116 | {
1117 | "of": {
1118 | "type": "any"
1119 | },
1120 | "type": "set"
1121 | }
1122 | ],
1123 | "type": "any"
1124 | },
1125 | {
1126 | "of": [
1127 | {
1128 | "type": "number"
1129 | },
1130 | {
1131 | "of": {
1132 | "type": "any"
1133 | },
1134 | "type": "set"
1135 | }
1136 | ],
1137 | "type": "any"
1138 | }
1139 | ],
1140 | "result": {
1141 | "of": [
1142 | {
1143 | "type": "number"
1144 | },
1145 | {
1146 | "of": {
1147 | "type": "any"
1148 | },
1149 | "type": "set"
1150 | }
1151 | ],
1152 | "type": "any"
1153 | },
1154 | "type": "function"
1155 | },
1156 | "infix": "-"
1157 | },
1158 | {
1159 | "name": "mul",
1160 | "decl": {
1161 | "args": [
1162 | {
1163 | "type": "number"
1164 | },
1165 | {
1166 | "type": "number"
1167 | }
1168 | ],
1169 | "result": {
1170 | "type": "number"
1171 | },
1172 | "type": "function"
1173 | },
1174 | "infix": "*"
1175 | },
1176 | {
1177 | "name": "neq",
1178 | "decl": {
1179 | "args": [
1180 | {
1181 | "type": "any"
1182 | },
1183 | {
1184 | "type": "any"
1185 | }
1186 | ],
1187 | "result": {
1188 | "type": "boolean"
1189 | },
1190 | "type": "function"
1191 | },
1192 | "infix": "!="
1193 | },
1194 | {
1195 | "name": "net.cidr_contains",
1196 | "decl": {
1197 | "args": [
1198 | {
1199 | "type": "string"
1200 | },
1201 | {
1202 | "type": "string"
1203 | }
1204 | ],
1205 | "result": {
1206 | "type": "boolean"
1207 | },
1208 | "type": "function"
1209 | }
1210 | },
1211 | {
1212 | "name": "net.cidr_intersects",
1213 | "decl": {
1214 | "args": [
1215 | {
1216 | "type": "string"
1217 | },
1218 | {
1219 | "type": "string"
1220 | }
1221 | ],
1222 | "result": {
1223 | "type": "boolean"
1224 | },
1225 | "type": "function"
1226 | }
1227 | },
1228 | {
1229 | "name": "numbers.range",
1230 | "decl": {
1231 | "args": [
1232 | {
1233 | "type": "number"
1234 | },
1235 | {
1236 | "type": "number"
1237 | }
1238 | ],
1239 | "result": {
1240 | "dynamic": {
1241 | "type": "number"
1242 | },
1243 | "type": "array"
1244 | },
1245 | "type": "function"
1246 | }
1247 | },
1248 | {
1249 | "name": "object.filter",
1250 | "decl": {
1251 | "args": [
1252 | {
1253 | "dynamic": {
1254 | "key": {
1255 | "type": "any"
1256 | },
1257 | "value": {
1258 | "type": "any"
1259 | }
1260 | },
1261 | "type": "object"
1262 | },
1263 | {
1264 | "of": [
1265 | {
1266 | "dynamic": {
1267 | "type": "any"
1268 | },
1269 | "type": "array"
1270 | },
1271 | {
1272 | "of": {
1273 | "type": "any"
1274 | },
1275 | "type": "set"
1276 | },
1277 | {
1278 | "dynamic": {
1279 | "key": {
1280 | "type": "any"
1281 | },
1282 | "value": {
1283 | "type": "any"
1284 | }
1285 | },
1286 | "type": "object"
1287 | }
1288 | ],
1289 | "type": "any"
1290 | }
1291 | ],
1292 | "result": {
1293 | "type": "any"
1294 | },
1295 | "type": "function"
1296 | }
1297 | },
1298 | {
1299 | "name": "object.get",
1300 | "decl": {
1301 | "args": [
1302 | {
1303 | "dynamic": {
1304 | "key": {
1305 | "type": "any"
1306 | },
1307 | "value": {
1308 | "type": "any"
1309 | }
1310 | },
1311 | "type": "object"
1312 | },
1313 | {
1314 | "type": "any"
1315 | },
1316 | {
1317 | "type": "any"
1318 | }
1319 | ],
1320 | "result": {
1321 | "type": "any"
1322 | },
1323 | "type": "function"
1324 | }
1325 | },
1326 | {
1327 | "name": "object.remove",
1328 | "decl": {
1329 | "args": [
1330 | {
1331 | "dynamic": {
1332 | "key": {
1333 | "type": "any"
1334 | },
1335 | "value": {
1336 | "type": "any"
1337 | }
1338 | },
1339 | "type": "object"
1340 | },
1341 | {
1342 | "of": [
1343 | {
1344 | "dynamic": {
1345 | "type": "any"
1346 | },
1347 | "type": "array"
1348 | },
1349 | {
1350 | "of": {
1351 | "type": "any"
1352 | },
1353 | "type": "set"
1354 | },
1355 | {
1356 | "dynamic": {
1357 | "key": {
1358 | "type": "any"
1359 | },
1360 | "value": {
1361 | "type": "any"
1362 | }
1363 | },
1364 | "type": "object"
1365 | }
1366 | ],
1367 | "type": "any"
1368 | }
1369 | ],
1370 | "result": {
1371 | "type": "any"
1372 | },
1373 | "type": "function"
1374 | }
1375 | },
1376 | {
1377 | "name": "object.union",
1378 | "decl": {
1379 | "args": [
1380 | {
1381 | "dynamic": {
1382 | "key": {
1383 | "type": "any"
1384 | },
1385 | "value": {
1386 | "type": "any"
1387 | }
1388 | },
1389 | "type": "object"
1390 | },
1391 | {
1392 | "dynamic": {
1393 | "key": {
1394 | "type": "any"
1395 | },
1396 | "value": {
1397 | "type": "any"
1398 | }
1399 | },
1400 | "type": "object"
1401 | }
1402 | ],
1403 | "result": {
1404 | "type": "any"
1405 | },
1406 | "type": "function"
1407 | }
1408 | },
1409 | {
1410 | "name": "or",
1411 | "decl": {
1412 | "args": [
1413 | {
1414 | "of": {
1415 | "type": "any"
1416 | },
1417 | "type": "set"
1418 | },
1419 | {
1420 | "of": {
1421 | "type": "any"
1422 | },
1423 | "type": "set"
1424 | }
1425 | ],
1426 | "result": {
1427 | "of": {
1428 | "type": "any"
1429 | },
1430 | "type": "set"
1431 | },
1432 | "type": "function"
1433 | },
1434 | "infix": "|"
1435 | },
1436 | {
1437 | "name": "plus",
1438 | "decl": {
1439 | "args": [
1440 | {
1441 | "type": "number"
1442 | },
1443 | {
1444 | "type": "number"
1445 | }
1446 | ],
1447 | "result": {
1448 | "type": "number"
1449 | },
1450 | "type": "function"
1451 | },
1452 | "infix": "+"
1453 | },
1454 | {
1455 | "name": "product",
1456 | "decl": {
1457 | "args": [
1458 | {
1459 | "of": [
1460 | {
1461 | "of": {
1462 | "type": "number"
1463 | },
1464 | "type": "set"
1465 | },
1466 | {
1467 | "dynamic": {
1468 | "type": "number"
1469 | },
1470 | "type": "array"
1471 | }
1472 | ],
1473 | "type": "any"
1474 | }
1475 | ],
1476 | "result": {
1477 | "type": "number"
1478 | },
1479 | "type": "function"
1480 | }
1481 | },
1482 | {
1483 | "name": "rand.intn",
1484 | "decl": {
1485 | "args": [
1486 | {
1487 | "type": "string"
1488 | },
1489 | {
1490 | "type": "number"
1491 | }
1492 | ],
1493 | "result": {
1494 | "type": "number"
1495 | },
1496 | "type": "function"
1497 | }
1498 | },
1499 | {
1500 | "name": "re_match",
1501 | "decl": {
1502 | "args": [
1503 | {
1504 | "type": "string"
1505 | },
1506 | {
1507 | "type": "string"
1508 | }
1509 | ],
1510 | "result": {
1511 | "type": "boolean"
1512 | },
1513 | "type": "function"
1514 | }
1515 | },
1516 | {
1517 | "name": "regex.find_all_string_submatch_n",
1518 | "decl": {
1519 | "args": [
1520 | {
1521 | "type": "string"
1522 | },
1523 | {
1524 | "type": "string"
1525 | },
1526 | {
1527 | "type": "number"
1528 | }
1529 | ],
1530 | "result": {
1531 | "dynamic": {
1532 | "dynamic": {
1533 | "type": "string"
1534 | },
1535 | "type": "array"
1536 | },
1537 | "type": "array"
1538 | },
1539 | "type": "function"
1540 | }
1541 | },
1542 | {
1543 | "name": "regex.is_valid",
1544 | "decl": {
1545 | "args": [
1546 | {
1547 | "type": "string"
1548 | }
1549 | ],
1550 | "result": {
1551 | "type": "boolean"
1552 | },
1553 | "type": "function"
1554 | }
1555 | },
1556 | {
1557 | "name": "regex.match",
1558 | "decl": {
1559 | "args": [
1560 | {
1561 | "type": "string"
1562 | },
1563 | {
1564 | "type": "string"
1565 | }
1566 | ],
1567 | "result": {
1568 | "type": "boolean"
1569 | },
1570 | "type": "function"
1571 | }
1572 | },
1573 | {
1574 | "name": "rem",
1575 | "decl": {
1576 | "args": [
1577 | {
1578 | "type": "number"
1579 | },
1580 | {
1581 | "type": "number"
1582 | }
1583 | ],
1584 | "result": {
1585 | "type": "number"
1586 | },
1587 | "type": "function"
1588 | },
1589 | "infix": "%"
1590 | },
1591 | {
1592 | "name": "replace",
1593 | "decl": {
1594 | "args": [
1595 | {
1596 | "type": "string"
1597 | },
1598 | {
1599 | "type": "string"
1600 | },
1601 | {
1602 | "type": "string"
1603 | }
1604 | ],
1605 | "result": {
1606 | "type": "string"
1607 | },
1608 | "type": "function"
1609 | }
1610 | },
1611 | {
1612 | "name": "round",
1613 | "decl": {
1614 | "args": [
1615 | {
1616 | "type": "number"
1617 | }
1618 | ],
1619 | "result": {
1620 | "type": "number"
1621 | },
1622 | "type": "function"
1623 | }
1624 | },
1625 | {
1626 | "name": "set_diff",
1627 | "decl": {
1628 | "args": [
1629 | {
1630 | "of": {
1631 | "type": "any"
1632 | },
1633 | "type": "set"
1634 | },
1635 | {
1636 | "of": {
1637 | "type": "any"
1638 | },
1639 | "type": "set"
1640 | }
1641 | ],
1642 | "result": {
1643 | "of": {
1644 | "type": "any"
1645 | },
1646 | "type": "set"
1647 | },
1648 | "type": "function"
1649 | }
1650 | },
1651 | {
1652 | "name": "sort",
1653 | "decl": {
1654 | "args": [
1655 | {
1656 | "of": [
1657 | {
1658 | "dynamic": {
1659 | "type": "any"
1660 | },
1661 | "type": "array"
1662 | },
1663 | {
1664 | "of": {
1665 | "type": "any"
1666 | },
1667 | "type": "set"
1668 | }
1669 | ],
1670 | "type": "any"
1671 | }
1672 | ],
1673 | "result": {
1674 | "dynamic": {
1675 | "type": "any"
1676 | },
1677 | "type": "array"
1678 | },
1679 | "type": "function"
1680 | }
1681 | },
1682 | {
1683 | "name": "split",
1684 | "decl": {
1685 | "args": [
1686 | {
1687 | "type": "string"
1688 | },
1689 | {
1690 | "type": "string"
1691 | }
1692 | ],
1693 | "result": {
1694 | "dynamic": {
1695 | "type": "string"
1696 | },
1697 | "type": "array"
1698 | },
1699 | "type": "function"
1700 | }
1701 | },
1702 | {
1703 | "name": "sprintf",
1704 | "decl": {
1705 | "args": [
1706 | {
1707 | "type": "string"
1708 | },
1709 | {
1710 | "dynamic": {
1711 | "type": "any"
1712 | },
1713 | "type": "array"
1714 | }
1715 | ],
1716 | "result": {
1717 | "type": "string"
1718 | },
1719 | "type": "function"
1720 | }
1721 | },
1722 | {
1723 | "name": "startswith",
1724 | "decl": {
1725 | "args": [
1726 | {
1727 | "type": "string"
1728 | },
1729 | {
1730 | "type": "string"
1731 | }
1732 | ],
1733 | "result": {
1734 | "type": "boolean"
1735 | },
1736 | "type": "function"
1737 | }
1738 | },
1739 | {
1740 | "name": "strings.replace_n",
1741 | "decl": {
1742 | "args": [
1743 | {
1744 | "dynamic": {
1745 | "key": {
1746 | "type": "string"
1747 | },
1748 | "value": {
1749 | "type": "string"
1750 | }
1751 | },
1752 | "type": "object"
1753 | },
1754 | {
1755 | "type": "string"
1756 | }
1757 | ],
1758 | "result": {
1759 | "type": "string"
1760 | },
1761 | "type": "function"
1762 | }
1763 | },
1764 | {
1765 | "name": "substring",
1766 | "decl": {
1767 | "args": [
1768 | {
1769 | "type": "string"
1770 | },
1771 | {
1772 | "type": "number"
1773 | },
1774 | {
1775 | "type": "number"
1776 | }
1777 | ],
1778 | "result": {
1779 | "type": "string"
1780 | },
1781 | "type": "function"
1782 | }
1783 | },
1784 | {
1785 | "name": "sum",
1786 | "decl": {
1787 | "args": [
1788 | {
1789 | "of": [
1790 | {
1791 | "of": {
1792 | "type": "number"
1793 | },
1794 | "type": "set"
1795 | },
1796 | {
1797 | "dynamic": {
1798 | "type": "number"
1799 | },
1800 | "type": "array"
1801 | }
1802 | ],
1803 | "type": "any"
1804 | }
1805 | ],
1806 | "result": {
1807 | "type": "number"
1808 | },
1809 | "type": "function"
1810 | }
1811 | },
1812 | {
1813 | "name": "time.diff",
1814 | "decl": {
1815 | "args": [
1816 | {
1817 | "of": [
1818 | {
1819 | "type": "number"
1820 | },
1821 | {
1822 | "static": [
1823 | {
1824 | "type": "number"
1825 | },
1826 | {
1827 | "type": "string"
1828 | }
1829 | ],
1830 | "type": "array"
1831 | }
1832 | ],
1833 | "type": "any"
1834 | },
1835 | {
1836 | "of": [
1837 | {
1838 | "type": "number"
1839 | },
1840 | {
1841 | "static": [
1842 | {
1843 | "type": "number"
1844 | },
1845 | {
1846 | "type": "string"
1847 | }
1848 | ],
1849 | "type": "array"
1850 | }
1851 | ],
1852 | "type": "any"
1853 | }
1854 | ],
1855 | "result": {
1856 | "static": [
1857 | {
1858 | "type": "number"
1859 | },
1860 | {
1861 | "type": "number"
1862 | },
1863 | {
1864 | "type": "number"
1865 | },
1866 | {
1867 | "type": "number"
1868 | },
1869 | {
1870 | "type": "number"
1871 | },
1872 | {
1873 | "type": "number"
1874 | }
1875 | ],
1876 | "type": "array"
1877 | },
1878 | "type": "function"
1879 | }
1880 | },
1881 | {
1882 | "name": "to_number",
1883 | "decl": {
1884 | "args": [
1885 | {
1886 | "of": [
1887 | {
1888 | "type": "number"
1889 | },
1890 | {
1891 | "type": "string"
1892 | },
1893 | {
1894 | "type": "boolean"
1895 | },
1896 | {
1897 | "type": "null"
1898 | }
1899 | ],
1900 | "type": "any"
1901 | }
1902 | ],
1903 | "result": {
1904 | "type": "number"
1905 | },
1906 | "type": "function"
1907 | }
1908 | },
1909 | {
1910 | "name": "trim",
1911 | "decl": {
1912 | "args": [
1913 | {
1914 | "type": "string"
1915 | },
1916 | {
1917 | "type": "string"
1918 | }
1919 | ],
1920 | "result": {
1921 | "type": "string"
1922 | },
1923 | "type": "function"
1924 | }
1925 | },
1926 | {
1927 | "name": "trim_left",
1928 | "decl": {
1929 | "args": [
1930 | {
1931 | "type": "string"
1932 | },
1933 | {
1934 | "type": "string"
1935 | }
1936 | ],
1937 | "result": {
1938 | "type": "string"
1939 | },
1940 | "type": "function"
1941 | }
1942 | },
1943 | {
1944 | "name": "trim_prefix",
1945 | "decl": {
1946 | "args": [
1947 | {
1948 | "type": "string"
1949 | },
1950 | {
1951 | "type": "string"
1952 | }
1953 | ],
1954 | "result": {
1955 | "type": "string"
1956 | },
1957 | "type": "function"
1958 | }
1959 | },
1960 | {
1961 | "name": "trim_right",
1962 | "decl": {
1963 | "args": [
1964 | {
1965 | "type": "string"
1966 | },
1967 | {
1968 | "type": "string"
1969 | }
1970 | ],
1971 | "result": {
1972 | "type": "string"
1973 | },
1974 | "type": "function"
1975 | }
1976 | },
1977 | {
1978 | "name": "trim_space",
1979 | "decl": {
1980 | "args": [
1981 | {
1982 | "type": "string"
1983 | }
1984 | ],
1985 | "result": {
1986 | "type": "string"
1987 | },
1988 | "type": "function"
1989 | }
1990 | },
1991 | {
1992 | "name": "trim_suffix",
1993 | "decl": {
1994 | "args": [
1995 | {
1996 | "type": "string"
1997 | },
1998 | {
1999 | "type": "string"
2000 | }
2001 | ],
2002 | "result": {
2003 | "type": "string"
2004 | },
2005 | "type": "function"
2006 | }
2007 | },
2008 | {
2009 | "name": "type_name",
2010 | "decl": {
2011 | "args": [
2012 | {
2013 | "of": [
2014 | {
2015 | "type": "any"
2016 | }
2017 | ],
2018 | "type": "any"
2019 | }
2020 | ],
2021 | "result": {
2022 | "type": "string"
2023 | },
2024 | "type": "function"
2025 | }
2026 | },
2027 | {
2028 | "name": "union",
2029 | "decl": {
2030 | "args": [
2031 | {
2032 | "of": {
2033 | "of": {
2034 | "type": "any"
2035 | },
2036 | "type": "set"
2037 | },
2038 | "type": "set"
2039 | }
2040 | ],
2041 | "result": {
2042 | "of": {
2043 | "type": "any"
2044 | },
2045 | "type": "set"
2046 | },
2047 | "type": "function"
2048 | }
2049 | },
2050 | {
2051 | "name": "upper",
2052 | "decl": {
2053 | "args": [
2054 | {
2055 | "type": "string"
2056 | }
2057 | ],
2058 | "result": {
2059 | "type": "string"
2060 | },
2061 | "type": "function"
2062 | }
2063 | },
2064 | {
2065 | "name": "walk",
2066 | "decl": {
2067 | "args": [
2068 | {
2069 | "type": "any"
2070 | }
2071 | ],
2072 | "result": {
2073 | "static": [
2074 | {
2075 | "dynamic": {
2076 | "type": "any"
2077 | },
2078 | "type": "array"
2079 | },
2080 | {
2081 | "type": "any"
2082 | }
2083 | ],
2084 | "type": "array"
2085 | },
2086 | "type": "function"
2087 | },
2088 | "relation": true
2089 | },
2090 | {
2091 | "name": "yaml.is_valid",
2092 | "decl": {
2093 | "args": [
2094 | {
2095 | "type": "string"
2096 | }
2097 | ],
2098 | "result": {
2099 | "type": "boolean"
2100 | },
2101 | "type": "function"
2102 | }
2103 | },
2104 | {
2105 | "name": "yaml.marshal",
2106 | "decl": {
2107 | "args": [
2108 | {
2109 | "type": "any"
2110 | }
2111 | ],
2112 | "result": {
2113 | "type": "string"
2114 | },
2115 | "type": "function"
2116 | }
2117 | },
2118 | {
2119 | "name": "yaml.unmarshal",
2120 | "decl": {
2121 | "args": [
2122 | {
2123 | "type": "string"
2124 | }
2125 | ],
2126 | "result": {
2127 | "type": "any"
2128 | },
2129 | "type": "function"
2130 | }
2131 | }
2132 | ],
2133 | "wasm_abi_versions": [
2134 | {
2135 | "version": 1,
2136 | "minor_version": 1
2137 | },
2138 | {
2139 | "version": 1,
2140 | "minor_version": 2
2141 | }
2142 | ]
2143 | }
2144 |
--------------------------------------------------------------------------------
/e2e.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | cd examples/nodejs-ts-app
5 |
6 | echo "Installing dependencies..."
7 | npm ci
8 |
9 | echo "Building wasm bundle..."
10 | npm run build
11 |
12 | echo "Running tests..."
13 | echo -n "When input.message == world, return hello == true "
14 | if npm start --silent -- '{ "message": "world" }' | jq -e '.[0].result' >/dev/null; then
15 | echo "✔"
16 | else
17 | echo "✖"
18 | fail=1
19 | fi
20 |
21 | echo -n "When input.message != world, return hello == false "
22 | if npm start --silent -- '{ "message": "not-world" }' | jq -e '.[0].result | not' >/dev/null; then
23 | echo "✔"
24 | else
25 | echo "✖"
26 | fail=1
27 | fi
28 |
29 | exit $fail
30 |
--------------------------------------------------------------------------------
/examples/deno/.gitignore:
--------------------------------------------------------------------------------
1 | test.wasm
2 |
--------------------------------------------------------------------------------
/examples/deno/Makefile:
--------------------------------------------------------------------------------
1 | all: build run
2 |
3 | build: test.wasm
4 |
5 | test.wasm: test.rego
6 | opa build -t wasm -e test/p $<
7 | tar zxvf bundle.tar.gz /policy.wasm
8 | mv policy.wasm test.wasm
9 | touch test.wasm
10 | rm bundle.tar.gz
11 |
12 | run:
13 | deno run --allow-read=test.wasm main.ts
--------------------------------------------------------------------------------
/examples/deno/main.ts:
--------------------------------------------------------------------------------
1 | import opa from "https://unpkg.com/@open-policy-agent/opa-wasm@1.6.0/dist/opa-wasm-browser.esm.js";
2 |
3 | const file = await Deno.readFile("test.wasm");
4 | const policy = await opa.loadPolicy(file.buffer.slice(0, file.length));
5 | const input = { "foo": "bar" };
6 |
7 | const result = policy.evaluate(input);
8 |
9 | if (!result[0]?.result) {
10 | Deno.exit(1);
11 | }
12 |
--------------------------------------------------------------------------------
/examples/deno/test.rego:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | p {
4 | input.foo == "bar"
5 | }
--------------------------------------------------------------------------------
/examples/nodejs-app/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
3 | node_modules
4 |
--------------------------------------------------------------------------------
/examples/nodejs-app/README.md:
--------------------------------------------------------------------------------
1 | # Simple opa-wasm node application
2 |
3 | The application is in [app.js](./app.js) and shows loading a `*.wasm` file,
4 | initializing the policy, and evaluating it with input.
5 |
6 | ## Install dependencies
7 |
8 | This requires the `opa-wasm` package, see [package.json](./package.json) for
9 | details.
10 |
11 | ```bash
12 | npm install
13 | ```
14 |
15 | > The example uses a local path, in "real" use-cases use the standard NPM
16 | > module.
17 |
18 | ## Build the WebAssembly binary for the example policy:
19 |
20 | > The syntax shown below requires OPA v0.20.5+
21 |
22 | There is an example policy included with the example, see
23 | [example.rego](./example.rego)
24 |
25 | ```bash
26 | opa build -t wasm -e example/hello ./example.rego
27 | tar -xzf ./bundle.tar.gz /policy.wasm
28 | ```
29 |
30 | This will create a bundle tarball with the WASM binary included, and then unpack
31 | just the `policy.wasm` from the bundle.
32 |
33 | ## Run the example Node JS code that invokes the WASM binary:
34 |
35 | ```bash
36 | node app.js '{"message": "world"}'
37 | ```
38 |
39 | Produces:
40 |
41 | ```
42 | [
43 | {
44 | "result": true
45 | }
46 | ]
47 | ```
48 |
49 | ```bash
50 | node app.js '{"message": "not-world"}'
51 | ```
52 |
53 | Produces:
54 |
55 | ```
56 | [
57 | {
58 | "result": false
59 | }
60 | ]
61 | ```
62 |
--------------------------------------------------------------------------------
/examples/nodejs-app/app.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The OPA Authors. All rights reserved.
2 | // Use of this source code is governed by an Apache2
3 | // license that can be found in the LICENSE file.
4 |
5 | const fs = require("fs");
6 | const { loadPolicy } = require("@open-policy-agent/opa-wasm");
7 |
8 | // Read the policy wasm file
9 | const policyWasm = fs.readFileSync("policy.wasm");
10 |
11 | // Load the policy module asynchronously
12 | loadPolicy(policyWasm).then((policy) => {
13 | // Use console parameters for the input, do quick
14 | // validation by json parsing. Not efficient.. but
15 | // will raise an error
16 | const input = JSON.parse(process.argv[2]);
17 | // Provide a data document with a string value
18 | policy.setData({ world: "world" });
19 |
20 | // Evaluate the policy and log the result
21 | const result = policy.evaluate(input);
22 | console.log(JSON.stringify(result, null, 2));
23 | }).catch((err) => {
24 | console.log("ERROR: ", err);
25 | process.exit(1);
26 | });
27 |
--------------------------------------------------------------------------------
/examples/nodejs-app/example.rego:
--------------------------------------------------------------------------------
1 | package example
2 |
3 | default hello = false
4 |
5 | hello {
6 | x := input.message
7 | x == data.world
8 | }
9 |
--------------------------------------------------------------------------------
/examples/nodejs-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-app",
3 | "version": "1.0.0",
4 | "description": "demo app",
5 | "main": "app.js",
6 | "scripts": {},
7 | "dependencies": {
8 | "@open-policy-agent/opa-wasm": "file:../../"
9 | },
10 | "license": "Apache-2.0"
11 | }
12 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
3 | node_modules
4 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/README.md:
--------------------------------------------------------------------------------
1 | # Multi-entrypoint OPA-WASM node demo script
2 |
3 | This script demos loading a WASM OPA file and simulates 1,000,000 evaluations on
4 | a few different entrypoints to demonstrate how entrypoints can be used.
5 |
6 | ## Install dependencies
7 |
8 | ```bash
9 | npm install
10 | ```
11 |
12 | ## Build the WebAssembly binary for the example policies
13 |
14 | There are two example policies located in the ./policies directory, these are
15 | compiled into a WASM. Look in the package.json to see how the entrypoints are
16 | defined.
17 |
18 | > Tested with OPA v0.27.1
19 |
20 | ```bash
21 | npm run build
22 | ```
23 |
24 | ## Run the example Node JS code that invokes the Wasm binary:
25 |
26 | ```bash
27 | npm start
28 | ```
29 |
30 | Sample Output
31 |
32 | ```
33 | Running multi entrypoint demo suite
34 | Iterations: 100000 iterations of 10 inputs for 1000000 total evals per entrypoint
35 | default entrypoint: 7.988s
36 | example/one entrypoint (via string): 5.118s
37 | example/one entrypoint (via number "1"): 5.057s
38 | example/two/coolRule entrypoint (via string): 2.939s
39 | example/two/coolRule entrypoint (via number "3"): 2.895s
40 | Evaluate policy from default entrypoint
41 | [
42 | {
43 | result: {
44 | two: { ourRule: true, coolRule: true, theirRule: true },
45 | one: { myOtherRule: true, myRule: true, myCompositeRule: true }
46 | }
47 | }
48 | ]
49 | Evaluate policy from example/one entrypoint
50 | [ { result: { myOtherRule: true, myRule: false } } ]
51 | Evaluate policy from example/two/coolRule entrypoint
52 | [ { result: true } ]
53 | Evaluate policy from example/two entrypoint
54 | [ { result: { ourRule: true, theirRule: false } } ]
55 | ```
56 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/app.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import { loadPolicy } from "@open-policy-agent/opa-wasm";
3 |
4 | const iterations = 100000;
5 |
6 | const inputs = [
7 | {
8 | someProp: "thisValue",
9 | anotherProp: "thatValue",
10 | anyProp: "aValue",
11 | ourProp: "inTheMiddleOfTheStreet",
12 | },
13 | {
14 | someProp: "",
15 | anotherProp: "thatValue",
16 | anyProp: "aValue",
17 | ourProp: "inTheMiddleOfTheStreet",
18 | },
19 | {
20 | someProp: "thisValue",
21 | anotherProp: "",
22 | anyProp: "aValue",
23 | ourProp: "inTheMiddleOfTheStreet",
24 | },
25 | {
26 | someProp: "thisValue",
27 | anotherProp: "thatValue",
28 | anyProp: "",
29 | ourProp: "inTheMiddleOfTheStreet",
30 | },
31 | {
32 | someProp: "thisValue",
33 | anotherProp: "thatValue",
34 | anyProp: "aValue",
35 | ourProp: "",
36 | },
37 | { someProp: "thisValue", anotherProp: "thatValue" },
38 | { anyProp: "aValue", ourProp: "inTheMiddleOfTheStreet" },
39 | { someProp: "thisValue", ourProp: "inTheMiddleOfTheStreet" },
40 | { anotherProp: "thatValue", anyProp: "aValue" },
41 | {},
42 | ];
43 |
44 | (async function readPolicy() {
45 | const policy = await loadPolicy(fs.readFileSync("./policy.wasm"));
46 |
47 | console.log(`Running multi entrypoint demo suite`);
48 | console.log(
49 | `Iterations: ${iterations} iterations of ${inputs.length} inputs for ${
50 | iterations *
51 | inputs.length
52 | } total evals per entrypoint`,
53 | );
54 |
55 | // Run the default entrypoint first
56 | console.time(`default entrypoint`);
57 | for (let iteration = 0; iteration < iterations; iteration++) {
58 | for (const input of inputs) {
59 | policy.evaluate(input);
60 | }
61 | }
62 | console.timeEnd(`default entrypoint`);
63 |
64 | // Run the example one entrypoint, string access
65 | console.time(`example/one entrypoint (via string)`);
66 | for (let iteration = 0; iteration < iterations; iteration++) {
67 | for (const input of inputs) {
68 | policy.evaluate(input, "example/one");
69 | }
70 | }
71 | console.timeEnd(`example/one entrypoint (via string)`);
72 |
73 | // Run the example one entrypoint, number access
74 | const exampleOneEntrypoint = policy.entrypoints["example/one"];
75 | console.time(`example/one entrypoint (via number "${exampleOneEntrypoint}")`);
76 | for (let iteration = 0; iteration < iterations; iteration++) {
77 | for (const input of inputs) {
78 | policy.evaluate(input, exampleOneEntrypoint);
79 | }
80 | }
81 | console.timeEnd(
82 | `example/one entrypoint (via number "${exampleOneEntrypoint}")`,
83 | );
84 |
85 | // Run the example two coolRule entrypoint, number access
86 | console.time(`example/two/coolRule entrypoint (via string)`);
87 | for (let iteration = 0; iteration < iterations; iteration++) {
88 | for (const input of inputs) {
89 | policy.evaluate(input, "example/two/coolRule");
90 | }
91 | }
92 | console.timeEnd(`example/two/coolRule entrypoint (via string)`);
93 |
94 | // Run the example two coolRule entrypoint, number access
95 | const coolRuleEntrypoint = policy.entrypoints["example/two/coolRule"];
96 | console.time(
97 | `example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`,
98 | );
99 | for (let iteration = 0; iteration < iterations; iteration++) {
100 | for (const input of inputs) {
101 | policy.evaluate(input, coolRuleEntrypoint);
102 | }
103 | }
104 | console.timeEnd(
105 | `example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`,
106 | );
107 |
108 | console.log(`Evaluate policy from default entrypoint`);
109 | console.dir(policy.evaluate(inputs[0]), { depth: 3 });
110 |
111 | console.log(`Evaluate policy from example/one entrypoint`);
112 | console.dir(policy.evaluate(inputs[1], "example/one"));
113 |
114 | console.log(`Evaluate policy from example/two/coolRule entrypoint`);
115 | console.dir(policy.evaluate(inputs[2], "example/two/coolRule"));
116 |
117 | console.log(`Evaluate policy from example/two entrypoint`);
118 | console.dir(policy.evaluate(inputs[3], "example/two"));
119 | })().catch((err) => {
120 | console.log("ERROR: ", err);
121 | process.exit(1);
122 | });
123 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-ts-app-multi-entrypoint",
3 | "version": "1.0.0",
4 | "description": "demo app for a multi entrypoint WASM build",
5 | "main": "app.ts",
6 | "scripts": {
7 | "build": "opa build -t wasm -e example -e example/one -e example/two -e example/two/coolRule ./policies && tar -xzf ./bundle.tar.gz /policy.wasm",
8 | "start": "ts-node app.ts"
9 | },
10 | "dependencies": {
11 | "@open-policy-agent/opa-wasm": "../.."
12 | },
13 | "license": "Apache-2.0",
14 | "devDependencies": {
15 | "@types/node": "^14.14.39",
16 | "ts-node": "^8.10.1"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/policies/example-one.rego:
--------------------------------------------------------------------------------
1 | package example.one
2 |
3 | import input
4 |
5 | default myRule = false
6 | default myOtherRule = false
7 |
8 | myRule {
9 | input.someProp == "thisValue"
10 | }
11 |
12 | myOtherRule {
13 | input.anotherProp == "thatValue"
14 | }
15 |
16 | myCompositeRule {
17 | myRule
18 | myOtherRule
19 | }
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/policies/example-two.rego:
--------------------------------------------------------------------------------
1 | package example.two
2 |
3 | import input
4 |
5 | default theirRule = false
6 | default ourRule = false
7 |
8 | theirRule {
9 | input.anyProp == "aValue"
10 | }
11 |
12 | ourRule {
13 | input.ourProp == "inTheMiddleOfTheStreet"
14 | }
15 |
16 | coolRule {
17 | theirRule
18 | ourRule
19 | }
--------------------------------------------------------------------------------
/examples/nodejs-ts-app-multi-entrypoint/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts"],
3 | "compilerOptions": {
4 | "target": "ES2019",
5 | "moduleResolution": "node",
6 | "noEmit": true,
7 | "strict": true,
8 | "baseUrl": "./",
9 | "paths": {
10 | "@open-policy-agent/opa-wasm": ["../../"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
3 | node_modules
4 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/README.md:
--------------------------------------------------------------------------------
1 | # Simple opa-wasm node typescript application
2 |
3 | The application is in [app.ts](./app.ts) and shows loading a `*.wasm` file,
4 | initializing the policy, and evaluating it with input.
5 |
6 | ## Install dependencies
7 |
8 | ```bash
9 | npm install
10 | ```
11 |
12 | ## Build the WebAssembly binary for the example policy
13 |
14 | There is an example policy included with the example, see
15 | [example.rego](./example.rego)
16 |
17 | > Requires OPA v0.20.5+
18 |
19 | ```bash
20 | npm run build
21 | ```
22 |
23 | ## Run the example Node JS code that invokes the Wasm binary:
24 |
25 | ```bash
26 | npm start -- '{\"message\": \"world\"}'
27 | ```
28 |
29 | ```bash
30 | npm start -- '{\"message\": \"not-world\"}'
31 | ```
32 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/app.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The OPA Authors. All rights reserved.
2 | // Use of this source code is governed by an Apache2
3 | // license that can be found in the LICENSE file.
4 |
5 | import { promises as fs } from "fs";
6 | import { LoadedPolicy, loadPolicy } from "@open-policy-agent/opa-wasm";
7 |
8 | (async function readPolicy() {
9 | const policyWasm = await fs.readFile("policy.wasm");
10 | const policy: LoadedPolicy = await loadPolicy(policyWasm);
11 |
12 | // Use console parameters for the input, do quick
13 | // validation by json parsing. Not efficient.. but
14 | // will raise an error
15 | const input = JSON.parse(process.argv[2]);
16 | // Provide a data document with a string value
17 | policy.setData({ world: "world" });
18 |
19 | // Evaluate the policy and log the result
20 | const result = policy.evaluate(input);
21 | console.log(JSON.stringify(result, null, 2));
22 | })().catch((err) => {
23 | console.log("ERROR: ", err);
24 | process.exit(1);
25 | });
26 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/example.rego:
--------------------------------------------------------------------------------
1 | package example
2 |
3 | default hello = false
4 |
5 | hello {
6 | x := input.message
7 | x == data.world
8 | }
9 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-app",
3 | "version": "1.0.0",
4 | "description": "demo app",
5 | "main": "app.js",
6 | "scripts": {
7 | "build": "opa build -t wasm -e example/hello ./example.rego && tar xzf ./bundle.tar.gz /policy.wasm",
8 | "start": "ts-node app.ts"
9 | },
10 | "dependencies": {
11 | "@open-policy-agent/opa-wasm": "../.."
12 | },
13 | "license": "Apache-2.0",
14 | "devDependencies": {
15 | "@types/node": "^14.0.4",
16 | "ts-node": "^8.10.1"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/nodejs-ts-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts"],
3 | "compilerOptions": {
4 | "target": "ES2019",
5 | "moduleResolution": "node",
6 | "noEmit": true,
7 | "strict": true,
8 | "baseUrl": "./",
9 | "paths": {
10 | "@open-policy-agent/opa-wasm": ["../../"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@open-policy-agent/opa-wasm",
3 | "version": "1.10.0",
4 | "description": "Open Policy Agent WebAssembly SDK",
5 | "main": "./src/index.cjs",
6 | "types": "./dist/types/opa.d.cts",
7 | "exports": {
8 | ".": {
9 | "types": {
10 | "import": "./dist/types/opa.d.mts",
11 | "require": "./dist/types/opa.d.cts"
12 | },
13 | "import": "./src/index.mjs",
14 | "require": "./src/index.cjs"
15 | }
16 | },
17 | "files": [
18 | "capabilities.json",
19 | "src",
20 | "dist"
21 | ],
22 | "scripts": {
23 | "build": "./build.sh",
24 | "lint": "git ls-files | xargs deno lint",
25 | "fmt:check": "git ls-files | xargs deno fmt --check",
26 | "fmt": "git ls-files | xargs deno fmt",
27 | "test": "jest --verbose"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/open-policy-agent/npm-opa-wasm.git"
32 | },
33 | "keywords": [
34 | "opa",
35 | "wasm",
36 | "policy"
37 | ],
38 | "author": "patrick@styra.com",
39 | "license": "Apache-2.0",
40 | "bugs": {
41 | "url": "https://github.com/open-policy-agent/npm-opa-wasm/issues"
42 | },
43 | "homepage": "https://github.com/open-policy-agent/npm-opa-wasm#readme",
44 | "devDependencies": {
45 | "esbuild": "^0.24.0",
46 | "jest": "^29.0.0",
47 | "puppeteer": "^23.4.0",
48 | "semver": "^7.3.5",
49 | "smart-deep-sort": "^1.0.2",
50 | "tmp": "^0.2.1",
51 | "typescript": "^5.3.3",
52 | "@types/node": "^22.9.0"
53 | },
54 | "dependencies": {
55 | "sprintf-js": "^1.1.2",
56 | "yaml": "^1.10.2"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/builtins/index.js:
--------------------------------------------------------------------------------
1 | const json = require("./json");
2 | const strings = require("./strings");
3 | const regex = require("./regex");
4 | const yaml = require("./yaml");
5 |
6 | module.exports = {
7 | ...json,
8 | ...strings,
9 | ...regex,
10 | ...yaml,
11 | };
12 |
--------------------------------------------------------------------------------
/src/builtins/json.js:
--------------------------------------------------------------------------------
1 | function isValidJSON(str) {
2 | if (typeof str !== "string") {
3 | return;
4 | }
5 | try {
6 | JSON.parse(str);
7 | return true;
8 | } catch (err) {
9 | if (err instanceof SyntaxError) {
10 | return false;
11 | }
12 | throw err;
13 | }
14 | }
15 |
16 | module.exports = {
17 | "json.is_valid": isValidJSON,
18 | };
19 |
--------------------------------------------------------------------------------
/src/builtins/regex.js:
--------------------------------------------------------------------------------
1 | const regexSplit = (pattern, s) => s.split(RegExp(pattern));
2 |
3 | module.exports = { "regex.split": regexSplit };
4 |
--------------------------------------------------------------------------------
/src/builtins/strings.js:
--------------------------------------------------------------------------------
1 | const vsprintf = require("sprintf-js").vsprintf;
2 |
3 | const sprintf = (s, values) => vsprintf(s, values);
4 |
5 | module.exports = { sprintf };
6 |
--------------------------------------------------------------------------------
/src/builtins/yaml.js:
--------------------------------------------------------------------------------
1 | const yaml = require("yaml");
2 |
3 | // see: https://eemeli.org/yaml/v1/#errors
4 | const errors = new Set([
5 | "YAMLReferenceError",
6 | "YAMLSemanticError",
7 | "YAMLSyntaxError",
8 | "YAMLWarning",
9 | ]);
10 |
11 | function parse(str) {
12 | if (typeof str !== "string") {
13 | return { ok: false, result: undefined };
14 | }
15 |
16 | const YAML_SILENCE_WARNINGS_CACHED = global.YAML_SILENCE_WARNINGS;
17 | try {
18 | // see: https://eemeli.org/yaml/v1/#silencing-warnings
19 | global.YAML_SILENCE_WARNINGS = true;
20 | return { ok: true, result: yaml.parse(str) };
21 | } catch (err) {
22 | // Ignore parser errors.
23 | if (err && errors.has(err.name)) {
24 | return { ok: false, result: undefined };
25 | }
26 | throw err;
27 | } finally {
28 | global.YAML_SILENCE_WARNINGS = YAML_SILENCE_WARNINGS_CACHED;
29 | }
30 | }
31 |
32 | module.exports = {
33 | // is_valid is expected to return nothing if input is invalid otherwise
34 | // true/false for it being valid YAML.
35 | "yaml.is_valid": (str) => typeof str === "string" ? parse(str).ok : undefined,
36 | "yaml.marshal": (data) => yaml.stringify(data),
37 | "yaml.unmarshal": (str) => parse(str).result,
38 | };
39 |
--------------------------------------------------------------------------------
/src/index.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("./opa");
2 |
--------------------------------------------------------------------------------
/src/index.mjs:
--------------------------------------------------------------------------------
1 | import opa from "./opa.js";
2 | /**
3 | * @type {opa.loadPolicy}
4 | */
5 | export const loadPolicy = opa.loadPolicy;
6 | export default opa;
7 |
--------------------------------------------------------------------------------
/src/opa.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 The OPA Authors. All rights reserved.
2 | // Use of this source code is governed by an Apache2
3 | // license that can be found in the LICENSE file.
4 | const builtIns = require("./builtins/index");
5 |
6 | /**
7 | * @param {WebAssembly.Memory} mem
8 | */
9 | function stringDecoder(mem) {
10 | return function (addr) {
11 | const i8 = new Int8Array(mem.buffer);
12 | let s = "";
13 | while (i8[addr] !== 0) {
14 | s += String.fromCharCode(i8[addr++]);
15 | }
16 | return s;
17 | };
18 | }
19 |
20 | /**
21 | * Stringifies and loads an object into OPA's Memory
22 | * @param {WebAssembly.Instance} wasmInstance
23 | * @param {WebAssembly.Memory} memory
24 | * @param {any | ArrayBuffer} value data as `object`, literal primitive or ArrayBuffer (last is assumed to be a well-formed stringified JSON)
25 | * @returns {number}
26 | */
27 | function _loadJSON(wasmInstance, memory, value) {
28 | if (value === undefined) {
29 | return 0;
30 | }
31 |
32 | let valueBuf;
33 | if (value instanceof ArrayBuffer) {
34 | valueBuf = new Uint8Array(value);
35 | } else {
36 | const valueAsText = JSON.stringify(value);
37 | valueBuf = new TextEncoder().encode(valueAsText);
38 | }
39 |
40 | const valueBufLen = valueBuf.byteLength;
41 | const rawAddr = wasmInstance.exports.opa_malloc(valueBufLen);
42 | const memoryBuffer = new Uint8Array(memory.buffer);
43 | memoryBuffer.set(valueBuf, rawAddr);
44 |
45 | const parsedAddr = wasmInstance.exports.opa_json_parse(rawAddr, valueBufLen);
46 |
47 | if (parsedAddr === 0) {
48 | throw "failed to parse json value";
49 | }
50 | return parsedAddr;
51 | }
52 |
53 | /**
54 | * Dumps and parses a JSON object from OPA's Memory
55 | * @param {WebAssembly.Instance} wasmInstance
56 | * @param {WebAssembly.Memory} memory
57 | * @param {number} addr
58 | * @returns {object}
59 | */
60 | function _dumpJSON(wasmInstance, memory, addr) {
61 | const rawAddr = wasmInstance.exports.opa_json_dump(addr);
62 | return _dumpJSONRaw(memory, rawAddr);
63 | }
64 |
65 | /**
66 | * Parses a JSON object from wasm instance's memory
67 | * @param {WebAssembly.Memory} memory
68 | * @param {number} addr
69 | * @returns {object}
70 | */
71 | function _dumpJSONRaw(memory, addr) {
72 | const buf = new Uint8Array(memory.buffer);
73 |
74 | let idx = addr;
75 |
76 | while (buf[idx] !== 0) {
77 | idx++;
78 | }
79 |
80 | const utf8View = new Uint8Array(memory.buffer, addr, idx - addr);
81 | const jsonAsText = new TextDecoder().decode(utf8View);
82 |
83 | return JSON.parse(jsonAsText);
84 | }
85 |
86 | const builtinFuncs = builtIns;
87 |
88 | /**
89 | * _builtinCall dispatches the built-in function. The built-in function
90 | * arguments are loaded from Wasm and back in using JSON serialization.
91 | * @param {WebAssembly.Instance} wasmInstance
92 | * @param {WebAssembly.Memory} memory
93 | * @param {{ [builtinId: number]: string }} builtins
94 | * @param {{ [builtinName: string]: Function }} customBuiltins
95 | * @param {string} builtin_id
96 | */
97 | function _builtinCall(
98 | wasmInstance,
99 | memory,
100 | builtins,
101 | customBuiltins,
102 | builtinId,
103 | ) {
104 | const builtInName = builtins[builtinId];
105 | const impl = builtinFuncs[builtInName] || customBuiltins[builtInName];
106 |
107 | if (impl === undefined) {
108 | throw {
109 | message: "not implemented: built-in function " +
110 | builtinId +
111 | ": " +
112 | builtins[builtinId],
113 | };
114 | }
115 |
116 | const argArray = Array.prototype.slice.apply(arguments);
117 | const args = [];
118 |
119 | for (let i = 5; i < argArray.length; i++) {
120 | const jsArg = _dumpJSON(wasmInstance, memory, argArray[i]);
121 | args.push(jsArg);
122 | }
123 |
124 | const result = impl(...args);
125 |
126 | return _loadJSON(wasmInstance, memory, result);
127 | }
128 |
129 | /**
130 | * _importObject builds the WebAssembly.Imports
131 | * @param {Object} env
132 | * @param {WebAssembly.Memory} memory
133 | * @param {{ [builtinName: string]: Function }} customBuiltins
134 | * @returns {WebAssembly.Imports}
135 | */
136 | function _importObject(env, memory, customBuiltins) {
137 | const addr2string = stringDecoder(memory);
138 |
139 | return {
140 | env: {
141 | memory,
142 | opa_abort: function (addr) {
143 | throw addr2string(addr);
144 | },
145 | opa_println: function (addr) {
146 | console.log(addr2string(addr));
147 | },
148 | opa_builtin0: function (builtinId, _ctx) {
149 | return _builtinCall(
150 | env.instance,
151 | memory,
152 | env.builtins,
153 | customBuiltins,
154 | builtinId,
155 | );
156 | },
157 | opa_builtin1: function (builtinId, _ctx, arg1) {
158 | return _builtinCall(
159 | env.instance,
160 | memory,
161 | env.builtins,
162 | customBuiltins,
163 | builtinId,
164 | arg1,
165 | );
166 | },
167 | opa_builtin2: function (builtinId, _ctx, arg1, arg2) {
168 | return _builtinCall(
169 | env.instance,
170 | memory,
171 | env.builtins,
172 | customBuiltins,
173 | builtinId,
174 | arg1,
175 | arg2,
176 | );
177 | },
178 | opa_builtin3: function (builtinId, _ctx, arg1, arg2, arg3) {
179 | return _builtinCall(
180 | env.instance,
181 | memory,
182 | env.builtins,
183 | customBuiltins,
184 | builtinId,
185 | arg1,
186 | arg2,
187 | arg3,
188 | );
189 | },
190 | opa_builtin4: function (builtinId, _ctx, arg1, arg2, arg3, arg4) {
191 | return _builtinCall(
192 | env.instance,
193 | memory,
194 | env.builtins,
195 | customBuiltins,
196 | builtinId,
197 | arg1,
198 | arg2,
199 | arg3,
200 | arg4,
201 | );
202 | },
203 | },
204 | };
205 | }
206 |
207 | /**
208 | * _preparePolicy checks the ABI version and loads the built-in functions
209 | * @param {Object} env
210 | * @param {WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance} wasm
211 | * @param {WebAssembly.Memory} memory
212 | * @returns { policy: WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance, minorVersion: number }}
213 | */
214 | function _preparePolicy(env, wasm, memory) {
215 | env.instance = wasm.instance ? wasm.instance : wasm;
216 |
217 | // Note: On Node 10.x this value is a number on Node 12.x and up it is
218 | // an object with numberic `value` property.
219 | const abiVersionGlobal = env.instance.exports.opa_wasm_abi_version;
220 | if (abiVersionGlobal !== undefined) {
221 | const abiVersion = typeof abiVersionGlobal === "number"
222 | ? abiVersionGlobal
223 | : abiVersionGlobal.value;
224 | if (abiVersion !== 1) {
225 | throw `unsupported ABI version ${abiVersion}`;
226 | }
227 | } else {
228 | console.error("opa_wasm_abi_version undefined"); // logs to stderr
229 | }
230 |
231 | const abiMinorVersionGlobal = env.instance.exports.opa_wasm_abi_minor_version;
232 | let abiMinorVersion;
233 | if (abiMinorVersionGlobal !== undefined) {
234 | abiMinorVersion = typeof abiMinorVersionGlobal === "number"
235 | ? abiMinorVersionGlobal
236 | : abiMinorVersionGlobal.value;
237 | } else {
238 | console.error("opa_wasm_abi_minor_version undefined");
239 | }
240 |
241 | const builtins = _dumpJSON(
242 | env.instance,
243 | memory,
244 | env.instance.exports.builtins(),
245 | );
246 |
247 | /** @type {typeof builtIns} */
248 | env.builtins = {};
249 |
250 | for (const key of Object.keys(builtins)) {
251 | env.builtins[builtins[key]] = key;
252 | }
253 |
254 | return { policy: wasm, minorVersion: abiMinorVersion };
255 | }
256 |
257 | /**
258 | * _loadPolicy can take in either an ArrayBuffer or WebAssembly.Module
259 | * as its first argument, a WebAssembly.Memory for the second parameter,
260 | * and an object mapping string names to additional builtin functions for
261 | * the third parameter.
262 | * It will return a Promise, depending on the input type the promise
263 | * resolves to both a compiled WebAssembly.Module and its first WebAssembly.Instance
264 | * or to the WebAssemblyInstance.
265 | * @param {BufferSource | WebAssembly.Module | Response | Promise} policyWasm
266 | * @param {WebAssembly.Memory} memory
267 | * @param {{ [builtinName: string]: Function }} customBuiltins
268 | * @returns {Promise<{ policy: WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance, minorVersion: number }>}
269 | */
270 | async function _loadPolicy(policyWasm, memory, customBuiltins) {
271 | const env = {};
272 |
273 | const isStreaming = policyWasm instanceof Response ||
274 | policyWasm instanceof Promise;
275 |
276 | const importObject = _importObject(env, memory, customBuiltins);
277 |
278 | const wasm =
279 | await (isStreaming
280 | ? WebAssembly.instantiateStreaming(policyWasm, importObject)
281 | : WebAssembly.instantiate(policyWasm, importObject));
282 |
283 | return _preparePolicy(env, wasm, memory);
284 | }
285 |
286 | /**
287 | * _loadPolicySync can take in either an ArrayBuffer or WebAssembly.Module
288 | * as its first argument, a WebAssembly.Memory for the second parameter,
289 | * and an object mapping string names to additional builtin functions for
290 | * the third parameter.
291 | * It will return a compiled WebAssembly.Module and its first WebAssembly.Instance.
292 | * @param {BufferSource | WebAssembly.Module} policyWasm
293 | * @param {WebAssembly.Memory} memory
294 | * @param {{ [builtinName: string]: Function }} customBuiltins
295 | * @returns {Promise<{ policy: WebAssembly.Instance, minorVersion: number }>}
296 | */
297 | function _loadPolicySync(policyWasm, memory, customBuiltins) {
298 | const env = {};
299 |
300 | if (
301 | policyWasm instanceof ArrayBuffer ||
302 | policyWasm.buffer instanceof ArrayBuffer
303 | ) {
304 | policyWasm = new WebAssembly.Module(policyWasm);
305 | }
306 |
307 | const wasm = new WebAssembly.Instance(
308 | policyWasm,
309 | _importObject(env, memory, customBuiltins),
310 | );
311 |
312 | return _preparePolicy(env, wasm, memory);
313 | }
314 |
315 | /**
316 | * LoadedPolicy is a wrapper around a WebAssembly.Instance and WebAssembly.Memory
317 | * for a compiled Rego policy. There are helpers to run the wasm instance and
318 | * handle the output from the policy wasm.
319 | */
320 | class LoadedPolicy {
321 | /**
322 | * Loads and initializes a compiled Rego policy.
323 | * @param {WebAssembly.WebAssemblyInstantiatedSource} policy
324 | * @param {WebAssembly.Memory} memory
325 | */
326 | constructor(policy, memory, minorVersion) {
327 | this.minorVersion = minorVersion;
328 | this.mem = memory;
329 |
330 | // Depending on how the wasm was instantiated "policy" might be a
331 | // WebAssembly Instance or be a wrapper around the Module and
332 | // Instance. We only care about the Instance.
333 | this.wasmInstance = policy.instance ? policy.instance : policy;
334 |
335 | this.dataAddr = _loadJSON(this.wasmInstance, this.mem, {});
336 | this.baseHeapPtr = this.wasmInstance.exports.opa_heap_ptr_get();
337 | this.dataHeapPtr = this.baseHeapPtr;
338 | this.entrypoints = _dumpJSON(
339 | this.wasmInstance,
340 | this.mem,
341 | this.wasmInstance.exports.entrypoints(),
342 | );
343 | }
344 |
345 | /**
346 | * Evaluates the loaded policy with the given input and
347 | * return the result set. This should be re-used for multiple evaluations
348 | * of the same policy with different inputs.
349 | *
350 | * To call a non-default entrypoint in your WASM specify it as the second
351 | * param. A list of entrypoints can be accessed with the `this.entrypoints`
352 | * property.
353 | * @param {any | ArrayBuffer} input input to be evaluated in form of `object`, literal primitive or ArrayBuffer (last is assumed to be a well-formed stringified JSON)
354 | * @param {number | string} entrypoint ID or name of the entrypoint to call (optional)
355 | */
356 | evaluate(input, entrypoint = 0) {
357 | // determine entrypoint ID
358 | if (typeof entrypoint === "number") {
359 | // used as-is
360 | } else if (typeof entrypoint === "string") {
361 | if (Object.prototype.hasOwnProperty.call(this.entrypoints, entrypoint)) {
362 | entrypoint = this.entrypoints[entrypoint];
363 | } else {
364 | throw `entrypoint ${entrypoint} is not valid in this instance`;
365 | }
366 | } else {
367 | throw `entrypoint value is an invalid type, must be either string or number`;
368 | }
369 |
370 | // ABI 1.2 fastpath
371 | if (this.minorVersion >= 2) {
372 | // write input into memory, adjust heap pointer
373 | let inputBuf = null;
374 | let inputLen = 0;
375 | let inputAddr = 0;
376 | if (input) {
377 | if (input instanceof ArrayBuffer) {
378 | inputBuf = new Uint8Array(input);
379 | } else {
380 | const inputAsText = JSON.stringify(input);
381 | inputBuf = new TextEncoder().encode(inputAsText);
382 | }
383 |
384 | inputAddr = this.dataHeapPtr;
385 | inputLen = inputBuf.byteLength;
386 | const delta = inputAddr + inputLen - this.mem.buffer.byteLength;
387 | if (delta > 0) {
388 | const pages = roundup(delta);
389 | this.mem.grow(pages);
390 | }
391 | const buf = new Uint8Array(this.mem.buffer);
392 | buf.set(inputBuf, this.dataHeapPtr);
393 | }
394 |
395 | // opa_eval will update the Instance heap pointer to the value below
396 | const heapPtr = this.dataHeapPtr + inputLen;
397 |
398 | const ret = this.wasmInstance.exports.opa_eval(
399 | 0,
400 | entrypoint,
401 | this.dataAddr,
402 | inputAddr,
403 | inputLen,
404 | heapPtr,
405 | 0,
406 | );
407 | return _dumpJSONRaw(this.mem, ret);
408 | }
409 |
410 | // Reset the heap pointer before each evaluation
411 | this.wasmInstance.exports.opa_heap_ptr_set(this.dataHeapPtr);
412 |
413 | // Load the input data
414 | const inputAddr = _loadJSON(this.wasmInstance, this.mem, input);
415 |
416 | // Setup the evaluation context
417 | const ctxAddr = this.wasmInstance.exports.opa_eval_ctx_new();
418 | this.wasmInstance.exports.opa_eval_ctx_set_input(ctxAddr, inputAddr);
419 | this.wasmInstance.exports.opa_eval_ctx_set_data(ctxAddr, this.dataAddr);
420 | this.wasmInstance.exports.opa_eval_ctx_set_entrypoint(ctxAddr, entrypoint);
421 |
422 | // Actually evaluate the policy
423 | this.wasmInstance.exports.eval(ctxAddr);
424 |
425 | // Retrieve the result
426 | const resultAddr = this.wasmInstance.exports.opa_eval_ctx_get_result(
427 | ctxAddr,
428 | );
429 | return _dumpJSON(this.wasmInstance, this.mem, resultAddr);
430 | }
431 |
432 | /**
433 | * evalBool will evaluate the policy and return a boolean answer
434 | * depending on the return code from the policy evaluation.
435 | * @deprecated Use `evaluate` instead.
436 | * @param {object} input
437 | */
438 | evalBool(input) {
439 | const rs = this.evaluate(input);
440 | return rs && rs.length === 1 && rs[0] === true;
441 | }
442 |
443 | /**
444 | * Loads data for use in subsequent evaluations.
445 | * @param {object | ArrayBuffer} data data in form of `object` or ArrayBuffer (last is assumed to be a well-formed stringified JSON)
446 | */
447 | setData(data) {
448 | this.wasmInstance.exports.opa_heap_ptr_set(this.baseHeapPtr);
449 | this.dataAddr = _loadJSON(this.wasmInstance, this.mem, data);
450 | this.dataHeapPtr = this.wasmInstance.exports.opa_heap_ptr_get();
451 | }
452 | }
453 |
454 | function roundup(bytes) {
455 | const pageSize = 64 * 1024;
456 | return Math.ceil(bytes / pageSize);
457 | }
458 |
459 | module.exports = {
460 | /**
461 | * Takes in either an ArrayBuffer or WebAssembly.Module
462 | * and will return a Promise of a LoadedPolicy object which
463 | * can be used to evaluate the policy.
464 | *
465 | * To set custom memory size specify number of memory pages
466 | * as second param.
467 | * Defaults to 5 pages (320KB).
468 | * @param {BufferSource | WebAssembly.Module | Response | Promise} regoWasm
469 | * @param {number | WebAssembly.MemoryDescriptor} memoryDescriptor For backwards-compatibility, a 'number' argument is taken to be the initial memory size.
470 | * @param {{ [builtinName: string]: Function }} customBuiltins A map from string names to builtin functions
471 | * @returns {Promise}
472 | */
473 | async loadPolicy(regoWasm, memoryDescriptor = {}, customBuiltins = {}) {
474 | // back-compat, second arg used to be a number: 'memorySize', with default of 5
475 | if (typeof memoryDescriptor === "number") {
476 | memoryDescriptor = { initial: memoryDescriptor };
477 | }
478 | memoryDescriptor.initial = memoryDescriptor.initial || 5;
479 |
480 | const memory = new WebAssembly.Memory(memoryDescriptor);
481 | const { policy, minorVersion } = await _loadPolicy(
482 | regoWasm,
483 | memory,
484 | customBuiltins,
485 | );
486 | return new LoadedPolicy(policy, memory, minorVersion);
487 | },
488 |
489 | /**
490 | * Takes in either an ArrayBuffer or WebAssembly.Module
491 | * and will return a LoadedPolicy object which can be
492 | * used to evaluate the policy.
493 | *
494 | * This cannot be used from the main thread in a browser.
495 | * You must use the `loadPolicy` function instead, or call
496 | * from a worker thread.
497 | *
498 | * To set custom memory size specify number of memory pages
499 | * as second param.
500 | * Defaults to 5 pages (320KB).
501 | * @param {BufferSource | WebAssembly.Module} regoWasm
502 | * @param {number | WebAssembly.MemoryDescriptor} memoryDescriptor For backwards-compatibility, a 'number' argument is taken to be the initial memory size.
503 | * @param {{ [builtinName: string]: Function }} customBuiltins A map from string names to builtin functions
504 | * @returns {LoadedPolicy}
505 | */
506 | loadPolicySync(regoWasm, memoryDescriptor = {}, customBuiltins = {}) {
507 | // back-compat, second arg used to be a number: 'memorySize', with default of 5
508 | if (typeof memoryDescriptor === "number") {
509 | memoryDescriptor = { initial: memoryDescriptor };
510 | }
511 | memoryDescriptor.initial = memoryDescriptor.initial || 5;
512 |
513 | const memory = new WebAssembly.Memory(memoryDescriptor);
514 | const { policy, minorVersion } = _loadPolicySync(
515 | regoWasm,
516 | memory,
517 | customBuiltins,
518 | );
519 | return new LoadedPolicy(policy, memory, minorVersion);
520 | },
521 | LoadedPolicy,
522 | };
523 |
--------------------------------------------------------------------------------
/test/browser-integration.test.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require("puppeteer");
2 | const http = require("http");
3 | const fs = require("fs");
4 | const path = require("path");
5 | const { execFileSync } = require("child_process");
6 |
7 | let server;
8 | let browser;
9 | let page;
10 |
11 | beforeAll(async () => {
12 | generateFixtureBundle();
13 |
14 | server = await startStaticServer();
15 | const port = server.address().port;
16 |
17 | browser = await puppeteer.launch();
18 | page = await browser.newPage();
19 | await page.goto(`http://localhost:${port}/`);
20 | });
21 |
22 | afterAll(async () => {
23 | await browser.close();
24 | server.close();
25 | });
26 |
27 | test("esm script should expose working opa module", async () => {
28 | const result = await page.evaluate(async function () {
29 | // NOTE: Paths are evaluated relative to the project root.
30 | const { default: opa } = await import("/dist/opa-wasm-browser.esm.js");
31 | const wasm = await fetch("/test/fixtures/multiple-entrypoints/policy.wasm")
32 | .then((r) => r.blob())
33 | .then((b) => b.arrayBuffer());
34 | const policy = await opa.loadPolicy(wasm);
35 | return policy.evaluate({}, "example/one");
36 | });
37 | expect(result).toEqual([
38 | {
39 | result: { myOtherRule: false, myRule: false },
40 | },
41 | ]);
42 | });
43 |
44 | test("loadPolicy should allow for a response object that resolves to a fetched wasm module", async () => {
45 | const result = await page.evaluate(async function () {
46 | // NOTE: Paths are evaluated relative to the project root.
47 | const { default: opa } = await import("/dist/opa-wasm-browser.esm.js");
48 | const policy = await opa.loadPolicy(
49 | fetch("/test/fixtures/multiple-entrypoints/policy.wasm"),
50 | );
51 | return policy.evaluate({}, "example/one");
52 | });
53 | expect(result).toEqual([
54 | {
55 | result: { myOtherRule: false, myRule: false },
56 | },
57 | ]);
58 | });
59 |
60 | test("default script should expose working opa global", async () => {
61 | // Load module into global scope.
62 | const script = fs.readFileSync(
63 | path.join(__dirname, "../dist/opa-wasm-browser.js"),
64 | "utf-8",
65 | );
66 | await page.evaluate(script);
67 |
68 | const result = await page.evaluate(async function () {
69 | // NOTE: Paths are evaluated relative to the project root.
70 | const wasm = await fetch("/test/fixtures/multiple-entrypoints/policy.wasm")
71 | .then((r) => r.blob())
72 | .then((b) => b.arrayBuffer());
73 | const policy = await opa.loadPolicy(wasm);
74 | return policy.evaluate({}, "example/one");
75 | });
76 | expect(result).toEqual([
77 | {
78 | result: { myOtherRule: false, myRule: false },
79 | },
80 | ]);
81 | });
82 |
83 | test("loadPolicySync can be used inside a worker thread", async () => {
84 | const result = await page.evaluate(function () {
85 | return new Promise((resolve, _) => {
86 | const worker = new Worker("/test/fixtures/load-policy-sync-worker.js");
87 | worker.onmessage = function (e) {
88 | resolve(e.data);
89 | };
90 | });
91 | });
92 | expect(result).toEqual([
93 | {
94 | result: { myOtherRule: false, myRule: false },
95 | },
96 | ]);
97 | });
98 |
99 | async function startStaticServer() {
100 | // Basic webserver to serve the test suite relative to the root.
101 | const server = http.createServer(function (req, res) {
102 | // Serve an empty HTML page at the root.
103 | if (req.url === "/") {
104 | res.setHeader("Content-Type", "text/html");
105 | res.writeHead(200);
106 | res.end("");
107 | return;
108 | }
109 |
110 | fs.readFile(path.join(__dirname, "..", req.url), function (err, data) {
111 | if (err) {
112 | console.error(err);
113 | res.writeHead(404);
114 | res.end(JSON.stringify(err));
115 | return;
116 | }
117 | switch (path.extname(req.url)) {
118 | case ".js":
119 | res.setHeader("Content-Type", "text/javascript");
120 | break;
121 | case ".wasm":
122 | res.setHeader("Content-Type", "application/wasm");
123 | break;
124 | }
125 | res.writeHead(200);
126 | res.end(data);
127 | });
128 | });
129 | return await new Promise((resolve) => {
130 | server.listen(0, "localhost", function () {
131 | resolve(server);
132 | });
133 | });
134 | }
135 |
136 | function generateFixtureBundle() {
137 | try {
138 | execFileSync("opa", [
139 | "build",
140 | `${__dirname}/fixtures/multiple-entrypoints`,
141 | "-o",
142 | `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`,
143 | "-t",
144 | "wasm",
145 | "-e",
146 | "example",
147 | "-e",
148 | "example/one",
149 | "-e",
150 | "example/two",
151 | ]);
152 |
153 | execFileSync("tar", [
154 | "-xzf",
155 | `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`,
156 | "-C",
157 | `${__dirname}/fixtures/multiple-entrypoints/`,
158 | `/policy.wasm`,
159 | ]);
160 | } catch (err) {
161 | console.error("Error creating test binary, check that opa is in path");
162 | throw err;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/test/fixtures/custom-builtins/capabilities.json:
--------------------------------------------------------------------------------
1 | {
2 | "builtins": [
3 | {
4 | "name": "custom.zeroArgBuiltin",
5 | "decl": {
6 | "type": "function",
7 | "args": [],
8 | "result": {
9 | "type": "string"
10 | }
11 | }
12 | },
13 | {
14 | "name": "custom.oneArgBuiltin",
15 | "decl": {
16 | "type": "function",
17 | "args": [
18 | { "type": "string" }
19 | ],
20 | "result": {
21 | "type": "string"
22 | }
23 | }
24 | },
25 | {
26 | "name": "custom.twoArgBuiltin",
27 | "decl": {
28 | "type": "function",
29 | "args": [
30 | { "type": "string" },
31 | { "type": "string" }
32 | ],
33 | "result": {
34 | "type": "string"
35 | }
36 | }
37 | },
38 | {
39 | "name": "custom.threeArgBuiltin",
40 | "decl": {
41 | "type": "function",
42 | "args": [
43 | { "type": "string" },
44 | { "type": "string" },
45 | { "type": "string" }
46 | ],
47 | "result": {
48 | "type": "string"
49 | }
50 | }
51 | },
52 | {
53 | "name": "custom.fourArgBuiltin",
54 | "decl": {
55 | "type": "function",
56 | "args": [
57 | { "type": "string" },
58 | { "type": "string" },
59 | { "type": "string" },
60 | { "type": "string" }
61 | ],
62 | "result": {
63 | "type": "string"
64 | }
65 | }
66 | },
67 | {
68 | "name": "json.is_valid",
69 | "decl": {
70 | "type": "function",
71 | "args": [
72 | { "type": "string" }
73 | ],
74 | "result": {
75 | "type": "boolean"
76 | }
77 | }
78 | },
79 | {
80 | "name": "eq",
81 | "decl": {
82 | "args": [
83 | {
84 | "type": "any"
85 | },
86 | {
87 | "type": "any"
88 | }
89 | ],
90 | "result": {
91 | "type": "boolean"
92 | },
93 | "type": "function"
94 | },
95 | "infix": "="
96 | }
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/test/fixtures/custom-builtins/custom-builtins-policy.rego:
--------------------------------------------------------------------------------
1 | package custom_builtins
2 |
3 | zero_arg = x {
4 | x = custom.zeroArgBuiltin()
5 | }
6 |
7 | one_arg = x {
8 | x = custom.oneArgBuiltin(input.args[0])
9 | }
10 |
11 | two_arg = x {
12 | x = custom.twoArgBuiltin(input.args[0], input.args[1])
13 | }
14 |
15 | three_arg = x {
16 | x = custom.threeArgBuiltin(input.args[0], input.args[1], input.args[2])
17 | }
18 |
19 | four_arg = x {
20 | x = custom.fourArgBuiltin(input.args[0], input.args[1], input.args[2], input.args[3])
21 | }
22 |
23 | valid_json {
24 | json.is_valid("{}")
25 | }
26 |
--------------------------------------------------------------------------------
/test/fixtures/data-stress/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
--------------------------------------------------------------------------------
/test/fixtures/data-stress/base-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "static": {
3 | "roles": {
4 | "1": {
5 | "id": 1,
6 | "name": "superuser",
7 | "permissions": [
8 | {
9 | "id": "account_billing_information",
10 | "priv": "view",
11 | "type": "ui_element"
12 | },
13 | {
14 | "id": "account_billing_information_1",
15 | "priv": "execute",
16 | "type": "rest_api"
17 | },
18 | {
19 | "id": "account_billing_information_2",
20 | "priv": "manage",
21 | "type": "component"
22 | },
23 | {
24 | "id": "account_billing_information_3",
25 | "priv": "view",
26 | "type": "ui_element"
27 | },
28 | {
29 | "id": "account_billing_information_4",
30 | "priv": "execute",
31 | "type": "ui_element"
32 | },
33 | {
34 | "id": "account_billing_information_5",
35 | "priv": "execute",
36 | "type": "rest_api"
37 | },
38 | {
39 | "id": "account_billing_information_6",
40 | "priv": "execute",
41 | "type": "ui_element"
42 | },
43 | {
44 | "id": "account_billing_information_7",
45 | "priv": "view",
46 | "type": "ui_element"
47 | },
48 | {
49 | "id": "account_billing_information_8",
50 | "priv": "view",
51 | "type": "ui_element"
52 | },
53 | {
54 | "id": "account_billing_information_9",
55 | "priv": "execute",
56 | "type": "rest_api"
57 | },
58 | {
59 | "id": "account_billing_information_10",
60 | "priv": "exercise",
61 | "type": "rest_api"
62 | },
63 | {
64 | "id": "account_billing_information_11",
65 | "priv": "invoke",
66 | "type": "rest_api"
67 | },
68 | {
69 | "id": "account_billing_information_12",
70 | "priv": "view",
71 | "type": "ui_element"
72 | }
73 | ]
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/test/fixtures/data-stress/example-one.rego:
--------------------------------------------------------------------------------
1 | package example.one
2 |
3 | default myRule = false
4 | default myOtherRule = false
5 |
6 | myRule {
7 | input.someProp == "thisValue"
8 | }
9 |
10 | myOtherRule {
11 | input.anotherProp == "thatValue"
12 | }
13 |
14 | myCompositeRule {
15 | myRule
16 | myOtherRule
17 | }
--------------------------------------------------------------------------------
/test/fixtures/load-policy-sync-worker.js:
--------------------------------------------------------------------------------
1 | (async () => {
2 | importScripts("/dist/opa-wasm-browser.js");
3 |
4 | const wasm = await fetch("/test/fixtures/multiple-entrypoints/policy.wasm")
5 | .then((r) => r.blob())
6 | .then((b) => b.arrayBuffer());
7 |
8 | const policy = opa.loadPolicySync(wasm);
9 | this.postMessage(policy.evaluate({}, "example/one"));
10 | })();
11 |
--------------------------------------------------------------------------------
/test/fixtures/memory/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
--------------------------------------------------------------------------------
/test/fixtures/memory/policy.rego:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | default allow = false
4 |
5 | allow { input == "open sesame" }
--------------------------------------------------------------------------------
/test/fixtures/multiple-entrypoints/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
--------------------------------------------------------------------------------
/test/fixtures/multiple-entrypoints/example-one.rego:
--------------------------------------------------------------------------------
1 | package example.one
2 |
3 | default myRule = false
4 | default myOtherRule = false
5 |
6 | myRule {
7 | input.someProp == "thisValue"
8 | }
9 |
10 | myOtherRule {
11 | input.anotherProp == "thatValue"
12 | }
13 |
14 | myCompositeRule {
15 | myRule
16 | myOtherRule
17 | }
--------------------------------------------------------------------------------
/test/fixtures/multiple-entrypoints/example-two.rego:
--------------------------------------------------------------------------------
1 | package example.two
2 |
3 | default theirRule = false
4 | default ourRule = false
5 |
6 | theirRule {
7 | input.anyProp == "aValue"
8 | }
9 |
10 | ourRule {
11 | input.ourProp == "inTheMiddleOfTheStreet"
12 | }
13 |
14 | coolRule {
15 | theirRule
16 | ourRule
17 | }
--------------------------------------------------------------------------------
/test/fixtures/stringified-support/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
--------------------------------------------------------------------------------
/test/fixtures/stringified-support/stringified-support-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "roles": {
3 | "1": {
4 | "id": 1,
5 | "permissions": [
6 | {
7 | "id": "view:account-billing"
8 | },
9 | {
10 | "id": "edit:account-billing"
11 | }
12 | ]
13 | }
14 | },
15 | "secret": "secret"
16 | }
17 |
--------------------------------------------------------------------------------
/test/fixtures/stringified-support/stringified-support-policy.rego:
--------------------------------------------------------------------------------
1 | package stringified.support
2 |
3 | default hasPermission = false
4 | default plainInputBoolean = false
5 | default plainInputNumber = false
6 | default plainInputString = false
7 |
8 | hasPermission {
9 | input.secret == data.secret
10 | }
11 |
12 | hasPermission {
13 | input.permissions[_] == data.roles["1"].permissions[_].id
14 | }
15 |
16 | plainInputBoolean {
17 | input = true
18 | }
19 |
20 | plainInputNumber {
21 | input = 5
22 | }
23 |
24 | plainInputString {
25 | input = "test"
26 | }
--------------------------------------------------------------------------------
/test/fixtures/yaml-support/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.tar.gz
2 | policy.wasm
--------------------------------------------------------------------------------
/test/fixtures/yaml-support/yaml-support-policy.rego:
--------------------------------------------------------------------------------
1 | package yaml.support
2 |
3 | fixture := `
4 | ---
5 | openapi: "3.0.1"
6 | info:
7 | title: test
8 | paths:
9 | /path1:
10 | get:
11 | x-amazon-apigateway-integration:
12 | type: "mock"
13 | httpMethod: "GET"
14 | x-amazon-apigateway-policy:
15 | Version: "2012-10-17"
16 | Statement:
17 | - Effect: Allow
18 | Principal:
19 | AWS: "*"
20 | Action:
21 | - 'execute-api:Invoke'
22 | Resource: '*'
23 | `
24 |
25 | canParseYAML {
26 | resource := yaml.unmarshal(fixture)
27 | resource.info.title == "test"
28 | }
29 |
30 | hasSemanticError {
31 | # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L22
32 | yaml.unmarshal("a:\n\t1\nb:\n\t2\n")
33 | }
34 |
35 | hasSyntaxError {
36 | # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L49
37 | yaml.unmarshal("{ , }\n---\n{ 123,,, }\n")
38 | }
39 |
40 | hasReferenceError {
41 | # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L245
42 | yaml.unmarshal("{ , }\n---\n{ 123,,, }\n")
43 | }
44 |
45 | hasYAMLWarning {
46 | # see: https://github.com/eemeli/yaml/blob/395f892ec9a26b9038c8db388b675c3281ab8cd3/tests/doc/errors.js#L224
47 | yaml.unmarshal("%FOO\n---bar\n")
48 | }
49 |
50 | canMarshalYAML[x] {
51 | string := yaml.marshal(input)
52 | x := yaml.unmarshal(string)
53 | }
54 |
55 | isValidYAML {
56 | yaml.is_valid(fixture) == true
57 | yaml.is_valid("foo: {") == false
58 | yaml.is_valid("{\"foo\": \"bar\"}") == true
59 | }
60 |
--------------------------------------------------------------------------------
/test/memory.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require("fs");
2 | const { execFileSync } = require("child_process");
3 | const semver = require("semver");
4 | const { loadPolicy, loadPolicySync } = require("../src/opa.js");
5 |
6 | describe("growing memory", () => {
7 | const fixturesFolder = "test/fixtures/memory";
8 |
9 | // TODO(sr): split into helper function if we need this in other places
10 | const versionOutput = execFileSync("opa", ["version"], { encoding: "utf8" });
11 | const lines = versionOutput.split(/\r?\n/);
12 | const [_, version] = lines[0].split(" ");
13 | const sv = semver.coerce(version);
14 | if (!semver.satisfies(sv, ">=0.35.0")) {
15 | it.only("", () => {
16 | console.warn("memory tests unsupported for OPA < 0.35.0");
17 | });
18 | }
19 |
20 | let policyWasm;
21 |
22 | beforeAll(() => {
23 | const bundlePath = `${fixturesFolder}/bundle.tar.gz`;
24 |
25 | execFileSync("opa", [
26 | "build",
27 | fixturesFolder,
28 | "-o",
29 | bundlePath,
30 | "-t",
31 | "wasm",
32 | "-e",
33 | "test/allow",
34 | ]);
35 |
36 | execFileSync(
37 | "tar",
38 | ["-xzf", bundlePath, "-C", `${fixturesFolder}/`, "/policy.wasm"],
39 | { stdio: "ignore" },
40 | );
41 |
42 | policyWasm = readFileSync(`${fixturesFolder}/policy.wasm`);
43 | });
44 |
45 | it("input exceeds memory, host fails to grow it", async () => {
46 | const policy = await loadPolicy(policyWasm, { initial: 2, maximum: 3 });
47 | const input = "a".repeat(2 * 65536);
48 |
49 | // Note: In Node 10.x case is different
50 | expect(() => policy.evaluate(input)).toThrow(
51 | /WebAssembly\.Memory\.grow\(\): Maximum memory size exceeded/i,
52 | );
53 | });
54 |
55 | it("parsing input exceeds memory", async () => {
56 | const policy = await loadPolicy(policyWasm, { initial: 3, maximum: 4 });
57 | const input = "a".repeat(2 * 65536);
58 | expect(() => policy.evaluate(input)).toThrow("opa_malloc: failed");
59 | });
60 |
61 | it("large input, host and guest grow successfully", async () => {
62 | const policy = await loadPolicy(policyWasm, { initial: 2, maximum: 8 });
63 | const input = "a".repeat(2 * 65536);
64 | expect(() => policy.evaluate(input)).not.toThrow();
65 | });
66 |
67 | it("does not leak memory evaluating the same policy multiple times", async () => {
68 | const policy = await loadPolicy(policyWasm, { initial: 2, maximum: 8 });
69 | const input = "a".repeat(2 * 65536);
70 |
71 | for (const _ of new Array(16)) {
72 | expect(() => policy.evaluate(input)).not.toThrow();
73 | }
74 | });
75 |
76 | it("large input, host and guest grow successfully (synchronous load)", () => {
77 | const policy = loadPolicySync(policyWasm, { initial: 2, maximum: 8 });
78 | const input = "a".repeat(2 * 65536);
79 | expect(() => policy.evaluate(input)).not.toThrow();
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/multiple-entrypoints.test.js:
--------------------------------------------------------------------------------
1 | const { loadPolicy } = require("../src/opa.js");
2 | const { readFileSync } = require("fs");
3 | const { execFileSync } = require("child_process");
4 |
5 | describe("multiple entrypoints", () => {
6 | let policy = null;
7 |
8 | beforeAll(async () => {
9 | try {
10 | execFileSync("opa", [
11 | "build",
12 | `${__dirname}/fixtures/multiple-entrypoints`,
13 | "-o",
14 | `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`,
15 | "-t",
16 | "wasm",
17 | "-e",
18 | "example",
19 | "-e",
20 | "example/one",
21 | "-e",
22 | "example/two",
23 | ]);
24 |
25 | execFileSync("tar", [
26 | "-xzf",
27 | `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`,
28 | "-C",
29 | `${__dirname}/fixtures/multiple-entrypoints/`,
30 | `/policy.wasm`,
31 | ]);
32 | } catch (err) {
33 | console.error("Error creating test binary, check that opa is in path");
34 | throw err;
35 | }
36 |
37 | policy = await loadPolicy(
38 | readFileSync(`${__dirname}/fixtures/multiple-entrypoints/policy.wasm`),
39 | );
40 | });
41 |
42 | it("should run with default entrypoint", () => {
43 | const result = policy.evaluate();
44 |
45 | expect(result.length).not.toBe(0);
46 | expect(result[0]).toMatchObject({
47 | result: {
48 | one: expect.any(Object),
49 | two: expect.any(Object),
50 | },
51 | });
52 | });
53 |
54 | it("should run with numbered entrypoint specified", () => {
55 | const entrypointId = policy.entrypoints["example/one"];
56 | const result = policy.evaluate({}, entrypointId);
57 |
58 | expect(result.length).not.toBe(0);
59 | expect(result[0]).toMatchObject({
60 | result: {
61 | myRule: false,
62 | myOtherRule: false,
63 | },
64 | });
65 | });
66 |
67 | it("should run with named entrypoint specified", () => {
68 | const result = policy.evaluate({}, "example/one");
69 |
70 | expect(result.length).not.toBe(0);
71 | expect(result[0]).toMatchObject({
72 | result: {
73 | myRule: false,
74 | myOtherRule: false,
75 | },
76 | });
77 | });
78 |
79 | it("should run with second entrypoint specified", () => {
80 | const result = policy.evaluate({}, "example/two");
81 |
82 | expect(result.length).not.toBe(0);
83 | expect(result[0]).toMatchObject({
84 | result: {
85 | ourRule: false,
86 | theirRule: false,
87 | },
88 | });
89 | });
90 |
91 | it("should not run with entrypoint as object", () => {
92 | expect(() => {
93 | policy.evaluate({}, {});
94 | }).toThrow(
95 | "entrypoint value is an invalid type, must be either string or number",
96 | );
97 | });
98 |
99 | it("should not run if entrypoint string does not exist", () => {
100 | expect(() => {
101 | policy.evaluate({}, "not/a/real/entrypoint");
102 | }).toThrow(
103 | "entrypoint not/a/real/entrypoint is not valid in this instance",
104 | );
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/test/opa-custom-builtins.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require("fs");
2 | const { execFileSync } = require("child_process");
3 | const { loadPolicy } = require("../src/opa.js");
4 |
5 | describe("custom builtins", () => {
6 | const fixturesFolder = "test/fixtures/custom-builtins";
7 |
8 | let policy;
9 |
10 | beforeAll(async () => {
11 | const bundlePath = `${fixturesFolder}/bundle.tar.gz`;
12 |
13 | execFileSync("opa", [
14 | "build",
15 | fixturesFolder,
16 | "-o",
17 | bundlePath,
18 | "-t",
19 | "wasm",
20 | "--capabilities",
21 | `${fixturesFolder}/capabilities.json`,
22 | "-e",
23 | "custom_builtins/zero_arg",
24 | "-e",
25 | "custom_builtins/one_arg",
26 | "-e",
27 | "custom_builtins/two_arg",
28 | "-e",
29 | "custom_builtins/three_arg",
30 | "-e",
31 | "custom_builtins/four_arg",
32 | "-e",
33 | "custom_builtins/valid_json",
34 | ]);
35 |
36 | execFileSync("tar", [
37 | "-xzf",
38 | bundlePath,
39 | "-C",
40 | `${fixturesFolder}/`,
41 | "/policy.wasm",
42 | ]);
43 |
44 | const policyWasm = readFileSync(`${fixturesFolder}/policy.wasm`);
45 | const opts = { initial: 5, maximum: 10 };
46 | policy = await loadPolicy(policyWasm, opts, {
47 | "custom.zeroArgBuiltin": () => `hello`,
48 | "custom.oneArgBuiltin": (arg0) => `hello ${arg0}`,
49 | "custom.twoArgBuiltin": (arg0, arg1) => `hello ${arg0}, ${arg1}`,
50 | "custom.threeArgBuiltin": (arg0, arg1, arg2) => (
51 | `hello ${arg0}, ${arg1}, ${arg2}`
52 | ),
53 | "custom.fourArgBuiltin": (arg0, arg1, arg2, arg3) => (
54 | `hello ${arg0}, ${arg1}, ${arg2}, ${arg3}`
55 | ),
56 | "json.is_valid": () => {
57 | throw new Error("should never happen");
58 | },
59 | });
60 | });
61 |
62 | it("should call a custom zero-arg builtin", () => {
63 | const result = policy.evaluate({}, "custom_builtins/zero_arg");
64 | expect(result.length).not.toBe(0);
65 | expect(result[0]).toMatchObject({ result: "hello" });
66 | });
67 |
68 | it("should call a custom one-arg builtin", () => {
69 | const result = policy.evaluate(
70 | { args: ["arg0"] },
71 | "custom_builtins/one_arg",
72 | );
73 | expect(result.length).not.toBe(0);
74 | expect(result[0]).toMatchObject({ result: "hello arg0" });
75 | });
76 |
77 | it("should call a custom two-arg builtin", () => {
78 | const result = policy.evaluate(
79 | { args: ["arg0", "arg1"] },
80 | "custom_builtins/two_arg",
81 | );
82 | expect(result.length).not.toBe(0);
83 | expect(result[0]).toMatchObject({
84 | result: "hello arg0, arg1",
85 | });
86 | });
87 |
88 | it("should call a custom three-arg builtin", () => {
89 | const result = policy.evaluate(
90 | { args: ["arg0", "arg1", "arg2"] },
91 | "custom_builtins/three_arg",
92 | );
93 | expect(result.length).not.toBe(0);
94 | expect(result[0]).toMatchObject({
95 | result: "hello arg0, arg1, arg2",
96 | });
97 | });
98 |
99 | it("should call a custom four-arg builtin", () => {
100 | const result = policy.evaluate(
101 | { args: ["arg0", "arg1", "arg2", "arg3"] },
102 | "custom_builtins/four_arg",
103 | );
104 | expect(result.length).not.toBe(0);
105 | expect(result[0]).toMatchObject({
106 | result: "hello arg0, arg1, arg2, arg3",
107 | });
108 | });
109 |
110 | it("should call a provided builtin over a custom builtin", () => {
111 | const result = policy.evaluate({}, "custom_builtins/valid_json");
112 | expect(result.length).not.toBe(0);
113 | expect(result[0]).toMatchObject({ result: true });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/test/opa-large-data.test.js:
--------------------------------------------------------------------------------
1 | const { EOL } = require("os");
2 | const { readFileSync } = require("fs");
3 | const { execFileSync } = require("child_process");
4 | const { loadPolicy } = require("../src/opa.js");
5 | const util = require("util");
6 |
7 | describe("setData stress tests", () => {
8 | const baseDataRaw = readFileSync(
9 | "test/fixtures/data-stress/base-data.json",
10 | "utf-8",
11 | );
12 | const baseData = JSON.parse(baseDataRaw);
13 |
14 | let data;
15 | let dataBuf;
16 |
17 | const multiplyFactor = 50;
18 | data = multiplyData(baseData, multiplyFactor * 1000);
19 | dataBuf = new util.TextEncoder().encode(JSON.stringify(data)).buffer;
20 | data = null;
21 | const dataSize = dataBuf.byteLength / 1000000;
22 |
23 | beforeAll(() => {
24 | try {
25 | execFileSync("opa", [
26 | "build",
27 | `test/fixtures/data-stress`,
28 | "-o",
29 | `test/fixtures/data-stress/bundle.tar.gz`,
30 | "-t",
31 | "wasm",
32 | "-e",
33 | "example",
34 | "-e",
35 | "example/one",
36 | ]);
37 |
38 | execFileSync("tar", [
39 | "-xzf",
40 | `test/fixtures/data-stress/bundle.tar.gz`,
41 | "-C",
42 | `test/fixtures/data-stress/`,
43 | `/policy.wasm`,
44 | ]);
45 | } catch (err) {
46 | console.error("Error creating test binary, check that opa is in path");
47 | throw err;
48 | }
49 | });
50 |
51 | it(`setData of size ~${dataSize}Mb`, async () => {
52 | const policyWasm = readFileSync("test/fixtures/data-stress/policy.wasm");
53 | const policy = await loadPolicy(policyWasm, 32000);
54 |
55 | const start = Date.now();
56 |
57 | policy.setData(dataBuf);
58 | dataBuf = null;
59 |
60 | const end = Date.now();
61 | console.log(`setData of ~${dataSize}Mb took ${end - start}ms`);
62 |
63 | if (global.gc) {
64 | console.log("done");
65 | global.gc();
66 | }
67 | dumpMemoryUsage(`memory status AFTER setData ~${dataSize}Mb`);
68 | });
69 | });
70 |
71 | function multiplyData(input, count) {
72 | const result = {};
73 | for (let i = 0, l = count; i < l; i++) {
74 | result[`static${i}`] = input.static;
75 | }
76 | return result;
77 | }
78 |
79 | function dumpMemoryUsage(note) {
80 | process.stdout.write(`Memory dump: ${note}${EOL}`);
81 | for (const [key, value] of Object.entries(process.memoryUsage())) {
82 | process.stdout.write(`\t${key}: ${value / 1000000} MB${EOL}`);
83 | }
84 | process.stdout.write(EOL);
85 | }
86 |
--------------------------------------------------------------------------------
/test/opa-node-cases.test.js:
--------------------------------------------------------------------------------
1 | const { loadPolicy } = require("../src/opa.js");
2 | const { readFileSync, readdirSync } = require("fs");
3 |
4 | let files = [];
5 | const path = process.env.OPA_CASES;
6 | if (path === undefined) {
7 | describe("opa nodejs cases", () => {
8 | test.todo("not found, set OPA_CASES env var");
9 | });
10 | } else {
11 | files = readdirSync(path);
12 | }
13 | let numFiles = 0;
14 | const testCases = [];
15 |
16 | files.forEach((file) => {
17 | if (file.endsWith(".json")) {
18 | numFiles++;
19 | const testFile = JSON.parse(readFileSync(path + "/" + file));
20 | if (Array.isArray(testFile.cases)) {
21 | testFile.cases.forEach((testCase) => {
22 | testCase.note = `${file}: ${testCase.note}`;
23 | if (
24 | testCase.note === "018_builtins.json: custom built-in" ||
25 | testCase.note === "018_builtins.json: impure built-in" ||
26 | testCase.note === "019_call_indirect_optimization.json: memoization"
27 | ) {
28 | testCase.skip = "skipping tests with custom builtins";
29 | }
30 | testCases.push(testCase);
31 | });
32 | }
33 | }
34 | });
35 |
36 | testCases.forEach((tc) => {
37 | const {
38 | wasm,
39 | input,
40 | data,
41 | note,
42 | want_defined: wantDefined,
43 | want_result: wantResult,
44 | want_error: wantError,
45 | skip_reason: skipReason,
46 | skip,
47 | } = tc;
48 | describe(note, () => {
49 | if (skip) {
50 | test.skip(`skip ${note}: ${skipReason}`, () => {});
51 | return;
52 | }
53 |
54 | if (wantError) {
55 | it("errors", async () => {
56 | const policy = await loadPolicy(Buffer.from(wasm, "base64"));
57 | policy.setData(data);
58 | expect(() => policy.evaluate(input)).toThrow(wantError);
59 | });
60 | return;
61 | }
62 |
63 | it("has the desired result", async () => {
64 | const policy = await loadPolicy(Buffer.from(wasm, "base64"));
65 | policy.setData(data || {});
66 | const result = policy.evaluate(input);
67 | if (wantDefined !== undefined) {
68 | if (wantDefined) {
69 | expect(result.length).toBeGreaterThan(0);
70 | } else {
71 | expect(result.length).toBe(0);
72 | }
73 | }
74 | if (wantResult !== undefined) {
75 | expect(result.length).toEqual(wantResult.length);
76 | expect(result).toEqual(expect.arrayContaining(wantResult));
77 | }
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/test/opa-stringified-support.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require("fs");
2 | const { execFileSync } = require("child_process");
3 | const { loadPolicy } = require("../src/opa.js");
4 | const util = require("util");
5 |
6 | describe("stringified data/input support", () => {
7 | const fixturesFolder = "test/fixtures/stringified-support";
8 | const dataRaw = readFileSync(
9 | `${fixturesFolder}/stringified-support-data.json`,
10 | "utf-8",
11 | );
12 | const data = JSON.parse(dataRaw);
13 |
14 | let policy;
15 |
16 | beforeAll(async () => {
17 | const bundlePath = `${fixturesFolder}/bundle.tar.gz`;
18 |
19 | execFileSync("opa", [
20 | "build",
21 | fixturesFolder,
22 | "-o",
23 | bundlePath,
24 | "-t",
25 | "wasm",
26 | "-e",
27 | "stringified/support",
28 | "-e",
29 | "stringified/support/plainInputBoolean",
30 | "-e",
31 | "stringified/support/plainInputNumber",
32 | "-e",
33 | "stringified/support/plainInputString",
34 | ]);
35 |
36 | execFileSync("tar", [
37 | "-xzf",
38 | bundlePath,
39 | "-C",
40 | `${fixturesFolder}/`,
41 | "/policy.wasm",
42 | ]);
43 |
44 | const policyWasm = readFileSync(`${fixturesFolder}/policy.wasm`);
45 | const opts = { initial: 5, maximum: 10 };
46 | policy = await loadPolicy(policyWasm, opts);
47 | });
48 |
49 | it("should accept stringified data", () => {
50 | policy.setData(new util.TextEncoder().encode(dataRaw).buffer);
51 |
52 | // positive check
53 | let result = policy.evaluate({ secret: "secret" });
54 | expect(result.length).not.toBe(0);
55 | expect(result[0]).toMatchObject({ result: { hasPermission: true } });
56 |
57 | // negative check
58 | result = policy.evaluate({ secret: "wrong" });
59 | expect(result.length).not.toBe(0);
60 | expect(result[0]).toMatchObject({ result: { hasPermission: false } });
61 | });
62 |
63 | it("should accept stringified input - object", () => {
64 | policy.setData(data);
65 |
66 | // positive check
67 | let result = policy.evaluate(
68 | new util.TextEncoder().encode(
69 | JSON.stringify({ permissions: ["view:account-billing"] }),
70 | ).buffer,
71 | );
72 | expect(result.length).not.toBe(0);
73 | expect(result[0]).toMatchObject({ result: { hasPermission: true } });
74 |
75 | // negative check
76 | result = policy.evaluate(JSON.stringify({ secret: "wrong" }));
77 | expect(result.length).not.toBe(0);
78 | expect(result[0]).toMatchObject({ result: { hasPermission: false } });
79 | });
80 |
81 | it("should accept stringified input - plain boolean", () => {
82 | // positive check
83 | let result = policy.evaluate(true, "stringified/support/plainInputBoolean");
84 | expect(result.length).not.toBe(0);
85 | expect(result[0]).toMatchObject({ result: true });
86 |
87 | // negative check
88 | result = policy.evaluate(false, "stringified/support/plainInputBoolean");
89 | expect(result.length).not.toBe(0);
90 | expect(result[0]).toMatchObject({ result: false });
91 | });
92 |
93 | it("should accept stringified input - plain number", () => {
94 | // positive check
95 | let result = policy.evaluate(5, "stringified/support/plainInputNumber");
96 | expect(result.length).not.toBe(0);
97 | expect(result[0]).toMatchObject({ result: true });
98 |
99 | // negative check
100 | result = policy.evaluate(6, "stringified/support/plainInputNumber");
101 | expect(result.length).not.toBe(0);
102 | expect(result[0]).toMatchObject({ result: false });
103 | });
104 |
105 | it("should accept stringified input - plain string", () => {
106 | // positive check
107 | let result = policy.evaluate(
108 | "test",
109 | "stringified/support/plainInputString",
110 | );
111 | expect(result.length).not.toBe(0);
112 | expect(result[0]).toMatchObject({ result: true });
113 |
114 | // negative check
115 | result = policy.evaluate(
116 | "invalid",
117 | "stringified/support/plainInputString",
118 | );
119 | expect(result.length).not.toBe(0);
120 | expect(result[0]).toMatchObject({ result: false });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/test/opa-test-cases.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync, readdirSync, writeFileSync } = require("fs");
2 | const { execFileSync, spawnSync } = require("child_process");
3 | const { join } = require("path");
4 | const { loadPolicy } = require("../src/opa.js");
5 | const tmp = require("tmp");
6 | const sort = require("smart-deep-sort");
7 |
8 | // Known failures
9 | const exceptions = {
10 | "sprintf/big_int": "bit ints are loosing precision",
11 | "sprintf/big_int/max_cert_serial_number":
12 | "lost precision, scientific format displayed",
13 | "strings/sprintf: float too big": '2e308 displayed as "Infinity"',
14 | "strings/sprintf: composite": "array is concatenated",
15 | };
16 |
17 | function walk(dir) {
18 | let results = [];
19 | readdirSync(dir, { withFileTypes: true }).forEach((d) => {
20 | file = join(dir, d.name);
21 | if (d.isDirectory()) {
22 | results = results.concat(walk(file));
23 | } else {
24 | results.push(file);
25 | }
26 | });
27 | return results;
28 | }
29 |
30 | function modulesToTempFiles(modules) {
31 | const ret = [];
32 | for (const mod of modules) {
33 | const tmpFile = tmp.fileSync();
34 | writeFileSync(tmpFile.fd, mod);
35 | ret.push(tmpFile.name);
36 | }
37 | return ret;
38 | }
39 |
40 | function compileToWasm(modules, query) {
41 | if (modules && modules.length < 1) {
42 | return {
43 | skip: `empty modules cases are not supported (got ${
44 | modules && modules.length
45 | })`,
46 | };
47 | }
48 |
49 | // NOTE(sr) crude but effective
50 | let entrypoint;
51 | if (query === "data.generated.p = x") {
52 | entrypoint = "generated/p";
53 | } else if (query === "data.test.p = x") {
54 | entrypoint = "test/p";
55 | } else if (query === "data.decoded_object.p = x") {
56 | entrypoint = "decoded_object/p";
57 | } else {
58 | return { skip: `entrypoint ${query} not supported` };
59 | }
60 |
61 | const files = modulesToTempFiles(modules);
62 | const outFile = tmp.fileSync();
63 | const untarDir = tmp.dirSync();
64 |
65 | const res = spawnSync("opa", [
66 | "build",
67 | "-t",
68 | "wasm",
69 | "--capabilities",
70 | "capabilities.json",
71 | "-e",
72 | entrypoint,
73 | "-o",
74 | outFile.name,
75 | ...files,
76 | ]);
77 | if (res.error || res.status != 0) {
78 | return { skip: res.stdout };
79 | }
80 | execFileSync(
81 | "tar",
82 | ["xf", outFile.name, "-C", untarDir.name, "/policy.wasm"],
83 | { stdio: "ignore" },
84 | );
85 | return { wasm: join(untarDir.name, "policy.wasm") };
86 | }
87 |
88 | const path = process.env.OPA_TEST_CASES;
89 | if (path === undefined) {
90 | describe("opa external test cases", () => {
91 | test.todo("not found, set OPA_TEST_CASES env var");
92 | });
93 | } else {
94 | for (const file of walk(path)) {
95 | describe(file, () => {
96 | // the raw yaml often posed problems, have opa give us json
97 | const res = spawnSync("opa", [
98 | "eval",
99 | "--format",
100 | "raw",
101 | "--input",
102 | file,
103 | "input",
104 | ]);
105 | if (res.error || res.status != 0) {
106 | test.todo(
107 | `${file} can't convert to JSON: ${res.stdout} (status ${res.status})`,
108 | );
109 | return;
110 | }
111 | const doc = JSON.parse(res.stdout);
112 | cases:
113 | for (const tc of doc.cases) {
114 | const reason = exceptions[tc.note];
115 | if (reason) {
116 | test.todo(`${tc.note}: ${reason}`);
117 | continue cases;
118 | }
119 | if (tc.input_term) {
120 | let fail = false;
121 | try {
122 | JSON.parse(tc.input_term);
123 | } catch (_) {
124 | fail = true;
125 | }
126 | if (fail) {
127 | test.todo(`${tc.note}: input_term value format not supported`);
128 | continue cases;
129 | }
130 | }
131 | if (tc.want_result && tc.want_result.length > 1) {
132 | test.todo(
133 | `${tc.note}: more than one expected result not supported: ${
134 | tc.want_result && tc.want_result.length
135 | }`,
136 | );
137 | continue cases;
138 | }
139 | let expected = tc.want_result;
140 |
141 | const { wasm, skip } = compileToWasm(tc.modules, tc.query);
142 | if (skip) {
143 | test.todo(`${tc.note}: ${skip}`);
144 | continue cases;
145 | }
146 |
147 | it(tc.note, async () => {
148 | const buf = readFileSync(wasm);
149 | const policy = await loadPolicy(buf);
150 | if (tc.data) {
151 | policy.setData(tc.data);
152 | }
153 | let input = tc.input || tc.input_term;
154 | if (typeof input === "string") {
155 | input = JSON.parse(input);
156 | }
157 |
158 | if ((tc.want_error || tc.want_error_code) && !tc.strict_error) {
159 | expect(() => {
160 | policy.evaluate(input);
161 | }).toThrow();
162 | return;
163 | }
164 |
165 | let res;
166 | expect(() => {
167 | res = policy.evaluate(input);
168 | }).not.toThrow();
169 |
170 | if (expected) {
171 | expect(res).toHaveLength(expected.length);
172 | if (tc.sort_bindings) {
173 | res = { result: sort(res[0].result) };
174 | expected = { x: sort(expected[0].x) };
175 | }
176 | expect(res[0] && res[0].result).toEqual(
177 | expected[0] && expected[0].x,
178 | );
179 | } else {
180 | expect(res).toHaveLength(0);
181 | }
182 | });
183 | }
184 | });
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/test/opa-yaml-support.test.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require("fs");
2 | const { execFileSync } = require("child_process");
3 | const { loadPolicy } = require("../src/opa.js");
4 |
5 | describe("yaml support", () => {
6 | const fixturesFolder = "test/fixtures/yaml-support";
7 |
8 | let policy;
9 |
10 | beforeAll(async () => {
11 | const bundlePath = `${fixturesFolder}/bundle.tar.gz`;
12 |
13 | execFileSync("opa", [
14 | "build",
15 | fixturesFolder,
16 | "-o",
17 | bundlePath,
18 | "-t",
19 | "wasm",
20 | "-e",
21 | "yaml/support/canParseYAML",
22 | "-e",
23 | "yaml/support/hasSyntaxError",
24 | "-e",
25 | "yaml/support/hasSemanticError",
26 | "-e",
27 | "yaml/support/hasReferenceError",
28 | "-e",
29 | "yaml/support/hasYAMLWarning",
30 | "-e",
31 | "yaml/support/canMarshalYAML",
32 | "-e",
33 | "yaml/support/isValidYAML",
34 | ]);
35 |
36 | execFileSync("tar", [
37 | "-xzf",
38 | bundlePath,
39 | "-C",
40 | `${fixturesFolder}/`,
41 | "/policy.wasm",
42 | ]);
43 |
44 | const policyWasm = readFileSync(`${fixturesFolder}/policy.wasm`);
45 | const opts = { initial: 5, maximum: 10 };
46 | policy = await loadPolicy(policyWasm, opts);
47 | });
48 |
49 | it("should unmarshall YAML strings", () => {
50 | const result = policy.evaluate({}, "yaml/support/canParseYAML");
51 | expect(result.length).not.toBe(0);
52 | expect(result[0]).toMatchObject({ result: true });
53 | });
54 |
55 | it("should ignore YAML syntax errors", () => {
56 | expect(() => policy.evaluate({}, "yaml/support/hasSyntaxError")).not
57 | .toThrow();
58 | const result = policy.evaluate({}, "yaml/support/hasSyntaxError");
59 | expect(result.length).toBe(0);
60 | });
61 |
62 | it("should ignore YAML semantic errors", () => {
63 | expect(() => policy.evaluate({}, "yaml/support/hasSemanticError")).not
64 | .toThrow();
65 | const result = policy.evaluate({}, "yaml/support/hasSemanticError");
66 | expect(result.length).toBe(0);
67 | });
68 |
69 | it("should ignore YAML reference errors", () => {
70 | expect(() => policy.evaluate({}, "yaml/support/hasReferenceError")).not
71 | .toThrow();
72 | const result = policy.evaluate({}, "yaml/support/hasReferenceError");
73 | expect(result.length).toBe(0);
74 | });
75 |
76 | it("should ignore YAML warnings", () => {
77 | expect(() => policy.evaluate({}, "yaml/support/hasYAMLWarning")).not
78 | .toThrow();
79 | const result = policy.evaluate({}, "yaml/support/hasYAMLWarning");
80 | expect(result.length).toBe(0);
81 | });
82 |
83 | it("should marshal yaml", () => {
84 | const result = policy.evaluate(
85 | [{ foo: [1, 2, 3] }],
86 | "yaml/support/canMarshalYAML",
87 | );
88 | expect(result.length).toBe(1);
89 | expect(result[0]).toMatchObject({ result: [[{ foo: [1, 2, 3] }]] });
90 | });
91 |
92 | it("should validate yaml", () => {
93 | const result = policy.evaluate({}, "yaml/support/isValidYAML");
94 | expect(result.length).toBe(1);
95 | expect(result[0]).toMatchObject({ result: true });
96 | });
97 | });
98 |
--------------------------------------------------------------------------------