├── .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 | --------------------------------------------------------------------------------