├── .eslintrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── examples └── simple │ ├── README.md │ ├── esbuild.js │ ├── example-webpack.config.js │ ├── index.html │ └── main.js ├── package-lock.json ├── package.json ├── src ├── actor.ts ├── events.ts ├── fetch.ts ├── host.ts ├── index.ts ├── types.ts ├── util.ts └── wasmbus.ts ├── test ├── index.test.ts └── infra │ ├── docker-compose.yml │ ├── docker-registry.yml │ └── nats.conf ├── tsconfig.json ├── wasmcloud-rs-js ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── utils.rs └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "ignorePatterns": [ 7 | "dist/*", 8 | "node_modules/*", 9 | "tsconfig.json", 10 | "*.d.ts" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "sourceType": "module" 15 | }, 16 | "env": { 17 | "browser": true, 18 | "amd": true, 19 | "es6": true, 20 | "node": true, 21 | "mocha": true 22 | }, 23 | "rules": { 24 | "comma-dangle": 1, 25 | "quotes": [ 26 | 1, 27 | "single" 28 | ], 29 | "no-undef": 1, 30 | "global-strict": 0, 31 | "no-extra-semi": 1, 32 | "no-underscore-dangle": 0, 33 | "no-console": 1, 34 | "no-unused-vars": 1, 35 | "no-trailing-spaces": [ 36 | 1, 37 | { 38 | "skipBlankLines": true 39 | } 40 | ], 41 | "no-unreachable": 1, 42 | "no-alert": 0 43 | } 44 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | tags: 9 | - v* 10 | pull_request: 11 | branches: 12 | - main 13 | - release-* 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: 12 23 | registry-url: https://registry.npmjs.org/ 24 | - name: Install wasm-pack 25 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 26 | - run: npm install 27 | # - run: npm run test 28 | - run: npm run lint 29 | - run: npm run build 30 | 31 | - name: Is Release? 32 | if: startswith(github.ref, 'refs/tags/v') 33 | run: echo "DEPLOY_PACKAGE=true" >> $GITHUB_ENV 34 | 35 | - name: Publish to npm 36 | if: env.DEPLOY_PACKAGE == 'true' 37 | run: npm pack && npm publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | index.html 4 | !examples/**/*index.html* 5 | *.sh* 6 | wasmcloud-rs-js/pkg/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "trailingComma": "none", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "printWidth": 120, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } -------------------------------------------------------------------------------- /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 | > [!IMPORTANT] 2 | > This host is **experimental** and does not implement all features or security settings. As a result, this host should not be used in production. For deploying wasmCloud to production, use the [primary host runtime](https://github.com/wasmCloud/wasmCloud/tree/main/crates/host). 3 | 4 | # wasmCloud Host in JavaScript/Browser 5 | 6 | This is the JavaScript implementation of the wasmCloud host for the browser (NodeJS support in progress). The library runs a host inside a web browser/application that connects to a remote lattice via NATS and can run wasmCloud actors in the browser. The host will automatically listen for actor start/stop from NATS and will initialize the actor using `wapcJS`. Any invocations will be handled by the browser actor and returned to the requesting host via NATS. Users can pass callbacks to handle invocation and event data. 7 | 8 | ## Demonstration Video 9 | 10 | In this demonstration video we will demonstration the following: 11 | 12 | * Load an HTTP Server capability into a wasmCloud Host running on a machine 13 | * Load an the wasmcloud-js host into a web browser 14 | * Load an 'echo' actor into the web browser 15 | * **seamlessly** bind the actor to the capability provider through Lattice 16 | * Access the webserver, which in turn delivers the request to the actor, processes it, and returns it to the requestion client via the capability 17 | * Unload the actor 18 | 19 | https://user-images.githubusercontent.com/1530656/130013412-b9a9daa6-fc71-424b-814c-2ca400926794.mp4 20 | 21 | 22 | 23 | ## Prerequisities 24 | 25 | * NATS with WebSockets enabled 26 | 27 | * wasmCloud lattice (OTP Version) 28 | 29 | * (OPTIONAL) Docker Registry with CORS configured 30 | 31 | * If launching actors from remote registries in the browser host, CORS must be configured on the registry server 32 | 33 | ## Development Prerequisities 34 | 35 | * NodeJS, npm 36 | 37 | * rust, cargo, wasm-pack 38 | 39 | * Used to port the rust versions of wascap, nkeys to JS 40 | 41 | 42 | ## Installation 43 | 44 | ```sh 45 | $ npm install @wasmcloud/wasmcloud-js 46 | ``` 47 | 48 | ## Usage 49 | 50 | More examples can be found in the [examples](examples/) directory, including sample `webpack` and `esbuild` configurations 51 | 52 | **Browser** 53 | 54 | ```html 55 | 56 | 104 | ``` 105 | 106 | **With a bundler** 107 | 108 | There are some caveats to using with a bundler: 109 | 110 | * The module contains `.wasm` files that need to be present alongside the final build output. Using `webpack-copy-plugin` (or `fs.copyFile` with other bundlers) can solve this issue. 111 | 112 | * If using with `create-react-app`, the webpack config will need to be ejected via `npm run eject` OR an npm library like `react-app-rewired` can handle the config injection. 113 | 114 | ```javascript 115 | // as esm -- this will grant you access to the types/params 116 | import { startHost } from '@wasmcloud/wasmcloud-js'; 117 | // as cjs 118 | // const wasmcloudjs = require('@wasmcloud/wasmcloud-js); 119 | 120 | async function cjsHost() { 121 | const host = await wasmcloudjs.startHost('default', false, ['ws://localhost:4222']) 122 | console.log(host); 123 | } 124 | 125 | async function esmHost() { 126 | const host = await startHost('default', false, ['ws://localhost:4222']) 127 | console.log(host); 128 | } 129 | 130 | cjsHost(); 131 | esmHost(); 132 | ``` 133 | 134 | ```javascript 135 | // webpack config, add this to the plugin section 136 | plugins: [ 137 | new CopyPlugin({ 138 | patterns: [ 139 | { 140 | from: 'node_modules/@wasmcloud/wasmcloud-js/dist/wasmcloud-rs-js/pkg/*.wasm', 141 | to: '[name].wasm' 142 | } 143 | ] 144 | }), 145 | ] 146 | ``` 147 | 148 | **Node** 149 | 150 | *IN PROGRESS* - NodeJS does not support WebSockets natively (required by nats.ws) 151 | 152 | ## Contributing 153 | 154 | ### Running tests 155 | 156 | ```sh 157 | $ npm run test 158 | ``` 159 | 160 | ### Building 161 | 162 | ```sh 163 | $ npm run build 164 | ``` 165 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # wasmcloud-js Examples 2 | 3 | This directory contains examples of using the `wasmcloud-js` library with sample `webpack` and `esbuild` configurations. 4 | 5 | ## Prerequisities 6 | 7 | * NATS with WebSockets enabled 8 | 9 | * There is sample infra via docker in the `test/infra` directory of this repo, `cd test/infra && docker-compose up` 10 | 11 | * wasmCloud lattice (OTP Version) 12 | 13 | * (OPTIONAL) Docker Registry with CORS configured 14 | 15 | * If launching actors from remote registries in the browser host, CORS must be configured on the registry server 16 | 17 | * NodeJS, NPM, npx 18 | 19 | ## Build 20 | 21 | ```sh 22 | $ npm install # this will run and build the rust deps 23 | $ npm install webpack esbuild copy-webpack-plugin fs 24 | $ #### if you want to use esbuild, follow these next 2 steps #### 25 | $ node esbuild.js # this produces the esbuild version 26 | $ mv out-esbuild.js out.js # rename the esbuild version 27 | $ #### if you want to use webpack, follow the steps below #### 28 | $ npx webpack --config=example-webpack.config.js # this produces the webpack output 29 | $ mv out-webpack.js out.js #rename the webpack version to out.js 30 | ``` 31 | 32 | ## Usage 33 | 34 | 1. Build the code with esbuild or webpack 35 | 36 | 2. Rename the output file to `out.js` 37 | 38 | 3. Start a web server inside this directory (e.g `python3 -m http.server` or `npx serve`) 39 | 40 | 3. Navigate to a browser `localhost:` 41 | 42 | 4. Open the developer console to view the host output 43 | 44 | 5. In the dev tools run `host` and you will get the full host object and methods 45 | 46 | 6. Start an actor with `host.launchActor('registry:5000/image', (data) => console.log(data))`. Echo actor is recommended (the 2nd parameter here is an invocation callback to handle the data from an invocation) 47 | 48 | 7. Link the actor with a provider running in Wasmcloud (eg `httpserver`) 49 | 50 | 8. Run a `curl localhost:port/echo` to see the response in the console (based off the invocation callback). -------------------------------------------------------------------------------- /examples/simple/esbuild.js: -------------------------------------------------------------------------------- 1 | // Use this path in projects using the node import 2 | let defaultWasmFileLocation = './node_modules/@wasmcloud/wasmcloud-js/dist/wasmcloud-rs-js/pkgwasmcloud.wasm' 3 | let wasmFileLocationForLocal = '../../dist/wasmcloud-rs-js/pkg/wasmcloud.wasm' 4 | 5 | let copyPlugin = { 6 | name: 'copy', 7 | setup(build) { 8 | require('fs').copyFile(wasmFileLocationForLocal, `${process.cwd()}/wasmcloud.wasm`, (err) => { 9 | if (err) throw err; 10 | }); 11 | } 12 | } 13 | require('esbuild').build({ 14 | entryPoints: ['main.js'], 15 | bundle: true, 16 | outfile: 'out-esbuild.js', 17 | plugins: [copyPlugin] 18 | }).catch(() => process.exit(1)) -------------------------------------------------------------------------------- /examples/simple/example-webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin') 3 | 4 | module.exports = { 5 | stats: { assets: false, modules: false }, 6 | entry: './main.js', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/ 13 | } 14 | ] 15 | }, 16 | // this is needed to copy the wasm file used by the js code to initiate a host key/extract the token from a signed actor 17 | // this SHOULD go away once the upstream webpack build issues are resolved (webpack will automatically pick up the webpack file without needing a copy) 18 | plugins: [ 19 | new CopyPlugin({ 20 | patterns: [ 21 | { 22 | // the node_module path should be referenced in projects using the node import 23 | // from: 'node_modules/@wasmcloud/wasmcloud-js/dist/*.wasm', 24 | from: '../../dist/wasmcloud-rs-js/pkg/*.wasm', 25 | to: '[name].wasm' 26 | } 27 | ] 28 | }) 29 | ], 30 | mode: 'production', 31 | resolve: { 32 | extensions: ['.tsx', '.ts', '.js', '.wasm'] 33 | }, 34 | experiments: { 35 | asyncWebAssembly: true 36 | }, 37 | output: { 38 | filename: 'out-webpack.js', 39 | path: path.resolve(__dirname, '') 40 | } 41 | } -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World - wasmcloudjs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/simple/main.js: -------------------------------------------------------------------------------- 1 | import { startHost } from '../../dist/src' 2 | 3 | // Importing inside of a project 4 | // import { startHost } from '@wasmcloud/wasmcloud-js'; 5 | // const { startHost } = require('@wasmcloud/wasmcloud-js'); 6 | 7 | (async () => { 8 | console.log('USING A JS BUNDLER') 9 | const host = await startHost('default', false, ['ws://localhost:6222']) 10 | window.host = host; 11 | console.log(host); 12 | })() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wasmcloud/wasmcloud-js", 3 | "version": "1.0.6", 4 | "description": "wasmcloud host in JavaScript/Browser", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src", 10 | "README.md", 11 | "wasmcloud-rs-js" 12 | ], 13 | "scripts": { 14 | "build": "npm run clean && npm run build:browser && npm run build:cjs", 15 | "build:browser": "webpack", 16 | "build:cjs": "tsc --declaration && webpack --env target=cjs", 17 | "build:wasm": "cd wasmcloud-rs-js && wasm-pack build", 18 | "clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/", 19 | "lint": "eslint --ext .ts src test", 20 | "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", 21 | "test": "mocha --require ts-node/register", 22 | "watch": "npm run clean && tsc -w --declrataion", 23 | "prepare": "npm run build" 24 | }, 25 | "prettier": "./.prettierrc.json", 26 | "keywords": [ 27 | "wasmcloud", 28 | "wasmcloud-host", 29 | "wasmcloud-js", 30 | "wasm" 31 | ], 32 | "eslintConfig": { 33 | "$schema": "http://json.schemastore.org/eslintrc", 34 | "root": true, 35 | "parser": "@typescript-eslint/parser", 36 | "plugins": [ 37 | "@typescript-eslint" 38 | ], 39 | "extends": [ 40 | "eslint:recommended", 41 | "plugin:@typescript-eslint/recommended" 42 | ] 43 | }, 44 | "mocha": { 45 | "$schema": "https://json.schemastore.org/mocharc", 46 | "extension": [ 47 | "ts" 48 | ], 49 | "spec": "test/**/*.test.ts" 50 | }, 51 | "author": "ks2211 ", 52 | "license": "ISC", 53 | "devDependencies": { 54 | "@babel/core": "^7.15.0", 55 | "@babel/preset-env": "^7.15.0", 56 | "@types/chai": "^4.2.21", 57 | "@types/chai-as-promised": "^7.1.4", 58 | "@types/mocha": "^9.0.0", 59 | "@typescript-eslint/eslint-plugin": "^4.22.0", 60 | "@typescript-eslint/parser": "^4.29.2", 61 | "@wasm-tool/wasm-pack-plugin": "^1.5.0", 62 | "babel-loader": "^8.2.2", 63 | "chai": "^4.3.4", 64 | "chai-as-promised": "^7.1.1", 65 | "eslint": "^7.32.0", 66 | "mocha": "^9.0.3", 67 | "path": "^0.12.7", 68 | "prettier": "^2.3.2", 69 | "ts-loader": "^9.2.5", 70 | "ts-node": "^10.2.1", 71 | "typescript": "^4.3.5", 72 | "webpack": "^5.50.0", 73 | "webpack-cli": "^4.8.0" 74 | }, 75 | "dependencies": { 76 | "@msgpack/msgpack": "^2.7.1", 77 | "@wapc/host": "0.0.2", 78 | "axios": "^0.24.0", 79 | "nats.ws": "^1.2.0" 80 | } 81 | } -------------------------------------------------------------------------------- /src/actor.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode } from '@msgpack/msgpack'; 2 | import { NatsConnection, Subscription } from 'nats.ws'; 3 | 4 | import { instantiate, Wasmbus } from './wasmbus'; 5 | import { createEventMessage, EventType } from './events'; 6 | import { 7 | ActorClaims, 8 | ActorClaimsMessage, 9 | ActorStartedMessage, 10 | ActorHealthCheckPassMessage, 11 | InvocationMessage, 12 | StopActorMessage, 13 | HostCall, 14 | Writer 15 | } from './types'; 16 | import { jsonEncode, parseJwt, uuidv4 } from './util'; 17 | 18 | /** 19 | * Actor holds the actor wasm module 20 | */ 21 | export class Actor { 22 | claims: ActorClaims; 23 | key: string; 24 | module!: Wasmbus; 25 | hostKey: string; 26 | hostName: string; 27 | wasm: any; 28 | invocationCallback?: Function; 29 | hostCall?: HostCall; 30 | writer?: Writer; 31 | 32 | constructor( 33 | hostName: string = 'default', 34 | hostKey: string, 35 | wasm: any, 36 | invocationCallback?: Function, 37 | hostCall?: HostCall, 38 | writer?: Writer 39 | ) { 40 | this.key = ''; 41 | this.hostName = hostName; 42 | this.hostKey = hostKey; 43 | this.claims = { 44 | jti: '', 45 | iat: 0, 46 | iss: '', 47 | sub: '', 48 | wascap: { 49 | name: '', 50 | hash: '', 51 | tags: [], 52 | caps: [], 53 | ver: '', 54 | prov: false 55 | } 56 | }; 57 | this.wasm = wasm; 58 | this.invocationCallback = invocationCallback; 59 | this.hostCall = hostCall; 60 | this.writer = writer; 61 | } 62 | 63 | /** 64 | * startActor takes an actor wasm uint8array, extracts the jwt, validates the jwt, and uses wapcJS to instantiate the module 65 | * 66 | * @param {Uint8Array} actorBuffer - the wasm actor module as uint8array 67 | */ 68 | async startActor(actorBuffer: Uint8Array) { 69 | const token: string = await this.wasm.extract_jwt(actorBuffer); 70 | const valid: boolean = await this.wasm.validate_jwt(token); 71 | if (!valid) { 72 | throw new Error('invalid token'); 73 | } 74 | this.claims = parseJwt(token); 75 | this.key = this.claims.sub; 76 | this.module = await instantiate(actorBuffer, this.hostCall, this.writer); 77 | } 78 | 79 | /** 80 | * stopActor publishes the stop_actor message 81 | * 82 | * @param {NatsConnection} natsConn - the nats connection object 83 | */ 84 | async stopActor(natsConn: NatsConnection) { 85 | const actorToStop: StopActorMessage = { 86 | host_id: this.hostKey, 87 | actor_ref: this.key 88 | }; 89 | natsConn.publish(`wasmbus.ctl.${this.hostName}.cmd.${this.hostKey}.sa`, jsonEncode(actorToStop)); 90 | } 91 | 92 | /** 93 | * publishActorStarted publishes the claims, the actor_started, and health_check_pass messages 94 | * 95 | * @param {NatsConnection} natsConn - the natsConnection object 96 | */ 97 | async publishActorStarted(natsConn: NatsConnection) { 98 | // publish claims 99 | const claims: ActorClaimsMessage = { 100 | call_alias: '', 101 | caps: this.claims.wascap.caps[0], 102 | iss: this.claims.iss, 103 | name: this.claims.wascap.name, 104 | rev: '1', 105 | sub: this.claims.sub, 106 | tags: '', 107 | version: this.claims.wascap.ver 108 | }; 109 | natsConn.publish(`lc.${this.hostName}.claims.${this.key}`, jsonEncode(claims)); 110 | 111 | // publish actor_started 112 | const actorStarted: ActorStartedMessage = { 113 | api_version: 0, 114 | instance_id: uuidv4(), 115 | public_key: this.key 116 | }; 117 | natsConn.publish( 118 | `wasmbus.evt.${this.hostName}`, 119 | jsonEncode(createEventMessage(this.hostKey, EventType.ActorStarted, actorStarted)) 120 | ); 121 | 122 | // publish actor health_check 123 | const actorHealthCheck: ActorHealthCheckPassMessage = { 124 | instance_id: uuidv4(), 125 | public_key: this.key 126 | }; 127 | natsConn.publish( 128 | `wasmbus.evt.${this.hostName}`, 129 | jsonEncode(createEventMessage(this.hostKey, EventType.HealthCheckPass, actorHealthCheck)) 130 | ); 131 | } 132 | 133 | /** 134 | * subscribeInvocations does a subscribe on nats for invocations 135 | * 136 | * @param {NatsConnection} natsConn the nats connection object 137 | */ 138 | async subscribeInvocations(natsConn: NatsConnection) { 139 | // subscribe to topic, wait for invokes, invoke the host, if callback set, send message 140 | const invocationsTopic: Subscription = natsConn.subscribe(`wasmbus.rpc.${this.hostName}.${this.key}`); 141 | for await (const invocationMessage of invocationsTopic) { 142 | const invocationData = decode(invocationMessage.data); 143 | const invocation: InvocationMessage = invocationData as InvocationMessage; 144 | const invocationResult: Uint8Array = await this.module.invoke(invocation.operation, invocation.msg); 145 | invocationMessage.respond( 146 | encode({ 147 | invocation_id: (invocationData as any).id, 148 | instance_id: uuidv4(), 149 | msg: invocationResult 150 | }) 151 | ); 152 | if (this.invocationCallback) { 153 | this.invocationCallback(invocationResult); 154 | } 155 | } 156 | throw new Error('actor.inovcation subscription closed'); 157 | } 158 | } 159 | /** 160 | * startActor initializes an actor and listens for invocation messages 161 | * 162 | * @param {string} hostName - the name of the host 163 | * @param {string} hostKey - the publickey of the host 164 | * @param {Uint8Array} actorModule - the wasm module of the actor 165 | * @param {NatsConnection} natsConn - the nats connection object 166 | * @param {any} wasm - the rust wasm module 167 | * @param {Function} invocationCallback - an optional function to call when the invocation is successful 168 | * @param {HostCall} hostCall - the hostCallback 169 | * @param {Writer} writer - the hostCallback writer 170 | * @returns {Actor} 171 | */ 172 | export async function startActor( 173 | hostName: string, 174 | hostKey: string, 175 | actorModule: Uint8Array, 176 | natsConn: NatsConnection, 177 | wasm: any, 178 | invocationCallback?: Function, 179 | hostCall?: HostCall, 180 | writer?: Writer 181 | ): Promise { 182 | const actor: Actor = new Actor(hostName, hostKey, wasm, invocationCallback, hostCall, writer); 183 | await actor.startActor(actorModule); 184 | await actor.publishActorStarted(natsConn); 185 | Promise.all([actor.subscribeInvocations(natsConn)]).catch(err => { 186 | throw err; 187 | }); 188 | return actor; 189 | } 190 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | import { uuidv4 } from './util'; 2 | 3 | export enum EventType { 4 | HeartBeat = 'com.wasmcloud.lattice.host_heartbeat', 5 | ActorStarted = 'com.wasmcloud.lattice.actor_started', 6 | ActorStopped = 'com.wasmcloud.lattice.actor_stopped', 7 | HealthCheckPass = 'com.wasmcloud.lattice.health_check_passed' 8 | } 9 | 10 | export type EventData = { 11 | datacontenttype: string; 12 | id: string; 13 | source: string; 14 | specversion: string; 15 | time: string; 16 | type: EventType; 17 | data: any; 18 | }; 19 | 20 | /** 21 | * createEventMessage is a helper function to create a message for "wasmbus.evt.{host}" 22 | * 23 | * @param {string} hostKey - the host public key 24 | * @param {EventType} eventType - the event type using the EventType enum 25 | * @param {any} data - the json data object 26 | * @returns {EventData} 27 | */ 28 | export function createEventMessage(hostKey: string, eventType: EventType, data: any): EventData { 29 | return { 30 | data: data, 31 | datacontenttype: 'application/json', 32 | id: uuidv4(), 33 | source: hostKey, 34 | specversion: '1.0', 35 | time: new Date().toISOString(), 36 | type: eventType 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | 3 | export type ImageDigest = { 4 | name: string; 5 | digest: string; 6 | registry: string; 7 | }; 8 | 9 | type FetchActorDigestResponse = { 10 | schemaVersion: number; 11 | mediaType: any; 12 | config: { 13 | annotations: any; 14 | digest: string; 15 | mediaType: string; 16 | size: 2; 17 | urls: any; 18 | }; 19 | layers: Array<{ 20 | annotations: { 21 | ['org.opencontainers.image.title']: string; 22 | }; 23 | digest: string; 24 | mediaType: string; 25 | size: number; 26 | urls: any; 27 | }>; 28 | annotations: any; 29 | }; 30 | 31 | /** 32 | * fetchActorDigest fetches the actor digest from a registry (sha) 33 | * 34 | * @param {string} actorRef - the actor url e.g host:port/image:version 35 | * @param {boolean} withTLS - whether or not the registry uses tls 36 | * @returns {ImageDigest} 37 | */ 38 | export async function fetchActorDigest(actorRef: string, withTLS?: boolean): Promise { 39 | const image: Array = actorRef.split('/'); 40 | const registry: string = image[0]; 41 | const [name, version] = image[1].split(':'); 42 | 43 | const response: AxiosResponse = await axios 44 | .get(`${withTLS ? 'https://' : 'http://'}${registry}/v2/${name}/manifests/${version}`, { 45 | headers: { 46 | Accept: 'application/vnd.oci.image.manifest.v1+json' 47 | } 48 | }) 49 | .catch(err => { 50 | throw err; 51 | }); 52 | const layers: FetchActorDigestResponse = response.data; 53 | 54 | if (layers.layers.length === 0) { 55 | throw new Error('no layers'); 56 | } 57 | 58 | return { 59 | name, 60 | digest: layers.layers[0].digest, 61 | registry 62 | }; 63 | } 64 | 65 | /** 66 | * fetchActor fetches an actor from either the local disk or a registry and returns it as uint8array 67 | * 68 | * @param {string} url - the url of the actor module 69 | * @returns {Uint8Array} 70 | */ 71 | export async function fetchActor(url: string): Promise { 72 | const response: AxiosResponse = await axios 73 | .get(url, { 74 | responseType: 'arraybuffer' 75 | }) 76 | .catch(err => { 77 | throw err; 78 | }); 79 | 80 | return new Uint8Array(response.data); 81 | } 82 | -------------------------------------------------------------------------------- /src/host.ts: -------------------------------------------------------------------------------- 1 | import { encode } from '@msgpack/msgpack'; 2 | import { connect, ConnectionOptions, NatsConnection, Subscription } from 'nats.ws'; 3 | 4 | import { Actor, startActor } from './actor'; 5 | import { createEventMessage, EventType } from './events'; 6 | import { fetchActor, fetchActorDigest, ImageDigest } from './fetch'; 7 | import { 8 | ActorStoppedMessage, 9 | CreateLinkDefMessage, 10 | HeartbeatMessage, 11 | InvocationCallbacks, 12 | LaunchActorMessage, 13 | StopActorMessage, 14 | HostCall, 15 | Writer 16 | } from './types'; 17 | import { jsonDecode, jsonEncode, uuidv4 } from './util'; 18 | 19 | const HOST_HEARTBEAT_INTERVAL: number = 30000; 20 | 21 | /** 22 | * Host holds the js/browser host 23 | */ 24 | export class Host { 25 | name: string; 26 | key: string; 27 | seed: string; 28 | heartbeatInterval: number; 29 | heartbeatIntervalId: any; 30 | withRegistryTLS: boolean; 31 | actors: { 32 | [key: string]: { 33 | actor: Actor; 34 | count: number; 35 | }; 36 | }; 37 | natsConnOpts: Array | ConnectionOptions; 38 | natsConn!: NatsConnection; 39 | eventsSubscription!: Subscription | null; 40 | wasm: any; 41 | invocationCallbacks?: InvocationCallbacks; 42 | hostCalls?: { 43 | [key: string]: HostCall; 44 | }; 45 | writers?: { 46 | [key: string]: Writer; 47 | }; 48 | 49 | constructor( 50 | name: string = 'default', 51 | withRegistryTLS: boolean, 52 | heartbeatInterval: number, 53 | natsConnOpts: Array | ConnectionOptions, 54 | wasm: any 55 | ) { 56 | const hostKey = new wasm.HostKey(); 57 | this.name = name; 58 | this.key = hostKey.pk; 59 | this.seed = hostKey.seed; 60 | this.withRegistryTLS = withRegistryTLS; 61 | this.actors = {}; 62 | this.wasm = wasm; 63 | this.heartbeatInterval = heartbeatInterval; 64 | this.natsConnOpts = natsConnOpts; 65 | this.invocationCallbacks = {}; 66 | this.hostCalls = {}; 67 | this.writers = {}; 68 | } 69 | 70 | /** 71 | * connectNATS connects to nats using either the array of servers or the connection options object 72 | */ 73 | async connectNATS() { 74 | const opts: ConnectionOptions = Array.isArray(this.natsConnOpts) 75 | ? { 76 | servers: this.natsConnOpts 77 | } 78 | : this.natsConnOpts; 79 | this.natsConn = await connect(opts); 80 | } 81 | 82 | /** 83 | * disconnectNATS disconnects from nats 84 | */ 85 | async disconnectNATS() { 86 | this.natsConn.close(); 87 | } 88 | 89 | /** 90 | * startHeartbeat starts a heartbeat publish message every X seconds based on the interval 91 | */ 92 | async startHeartbeat() { 93 | this.heartbeatIntervalId; 94 | const heartbeat: HeartbeatMessage = { 95 | actors: [], 96 | providers: [] 97 | }; 98 | for (const actor in this.actors) { 99 | heartbeat.actors.push({ 100 | actor: actor, 101 | instances: 1 102 | }); 103 | } 104 | this.heartbeatIntervalId = await setInterval(() => { 105 | this.natsConn.publish( 106 | `wasmbus.evt.${this.name}`, 107 | jsonEncode(createEventMessage(this.key, EventType.HeartBeat, heartbeat)) 108 | ); 109 | }, this.heartbeatInterval); 110 | } 111 | 112 | /** 113 | * stopHeartbeat clears the heartbeat interval 114 | */ 115 | async stopHeartbeat() { 116 | clearInterval(this.heartbeatIntervalId); 117 | this.heartbeatIntervalId = null; 118 | } 119 | 120 | /** 121 | * subscribeToEvents subscribes to the events on the host 122 | * 123 | * @param eventCallback - an optional callback(data) to handle the event message 124 | */ 125 | async subscribeToEvents(eventCallback?: Function) { 126 | this.eventsSubscription = this.natsConn.subscribe(`wasmbus.evt.${this.name}`); 127 | for await (const event of this.eventsSubscription) { 128 | const eventData = jsonDecode(event.data); 129 | if (eventCallback) { 130 | eventCallback(eventData); 131 | } 132 | } 133 | throw new Error('evt subscription was closed'); 134 | } 135 | 136 | /** 137 | * unsubscribeEvents unsubscribes and removes the events subscription 138 | */ 139 | async unsubscribeEvents() { 140 | this.eventsSubscription?.unsubscribe(); 141 | this.eventsSubscription = null; 142 | } 143 | 144 | /** 145 | * launchActor launches an actor via the launch actor message 146 | * 147 | * @param actorRef - the actor to start 148 | * @param invocationCallback - an optional callback(data) to handle the invocation 149 | * @param hostCall - the hostCallback 150 | * @param writer - writer for the hostCallback, can be undefined 151 | */ 152 | async launchActor(actorRef: string, invocationCallback?: Function, hostCall?: HostCall, writer?: Writer) { 153 | const actor: LaunchActorMessage = { 154 | actor_ref: actorRef, 155 | host_id: this.key 156 | }; 157 | this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.la`, jsonEncode(actor)); 158 | if (invocationCallback) { 159 | this.invocationCallbacks![actorRef] = invocationCallback; 160 | } 161 | if (hostCall) { 162 | this.hostCalls![actorRef] = hostCall; 163 | } 164 | if (writer) { 165 | this.writers![actorRef] = writer; 166 | } 167 | } 168 | 169 | /** 170 | * stopActor stops an actor by publishing the sa message 171 | * 172 | * @param {string} actorRef - the actor to stop 173 | */ 174 | async stopActor(actorRef: string) { 175 | const actorToStop: StopActorMessage = { 176 | host_id: this.key, 177 | actor_ref: actorRef 178 | }; 179 | this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`, jsonEncode(actorToStop)); 180 | } 181 | 182 | /** 183 | * listenLaunchActor listens for start actor message and will fetch the actor (either from disk or registry) and initialize the actor 184 | */ 185 | async listenLaunchActor() { 186 | // subscribe to the .la topic `wasmbus.ctl.${this.name}.cmd.${this.key}.la` 187 | // decode the data 188 | // fetch the actor from registry or local 189 | // start the actor class 190 | // listen for invocation events 191 | const actorsTopic: Subscription = this.natsConn.subscribe(`wasmbus.ctl.${this.name}.cmd.${this.key}.la`); 192 | for await (const actorMessage of actorsTopic) { 193 | const actorData = jsonDecode(actorMessage.data); 194 | const actorRef: string = (actorData as LaunchActorMessage).actor_ref; 195 | const usingRegistry: boolean = !actorRef.endsWith('.wasm'); 196 | try { 197 | let url: string; 198 | if (usingRegistry) { 199 | const actorDigest: ImageDigest = await fetchActorDigest(actorRef); 200 | url = `${this.withRegistryTLS ? 'https://' : 'http://'}${actorDigest.registry}/v2/${actorDigest.name}/blobs/${ 201 | actorDigest.digest 202 | }`; 203 | } else { 204 | url = actorRef; 205 | } 206 | const actorModule: Uint8Array = await fetchActor(url); 207 | const actor: Actor = await startActor( 208 | this.name, 209 | this.key, 210 | actorModule, 211 | this.natsConn, 212 | this.wasm, 213 | this.invocationCallbacks?.[actorRef], 214 | this.hostCalls?.[actorRef], 215 | this.writers?.[actorRef] 216 | ); 217 | 218 | if (this.actors[actorRef]) { 219 | this.actors[actorRef].count++; 220 | } else { 221 | this.actors[actorRef] = { 222 | count: 1, 223 | actor: actor 224 | }; 225 | } 226 | } catch (err) { 227 | // TODO: error handling 228 | console.log('error', err); 229 | } 230 | } 231 | throw new Error('la.subscription was closed'); 232 | } 233 | 234 | /** 235 | * listenStopActor listens for the actor stopped message and will tear down the actor on message receive 236 | */ 237 | async listenStopActor() { 238 | // listen for stop actor message, decode the data 239 | // publish actor_stopped to the lattice 240 | // delete the actor from the host and remove the invocation callback 241 | const actorsTopic: Subscription = this.natsConn.subscribe(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`); 242 | for await (const actorMessage of actorsTopic) { 243 | const actorData = jsonDecode(actorMessage.data); 244 | const actorStop: ActorStoppedMessage = { 245 | instance_id: uuidv4(), 246 | public_key: this.actors[(actorData as StopActorMessage).actor_ref].actor.key 247 | }; 248 | this.natsConn.publish( 249 | `wasmbus.evt.${this.name}`, 250 | jsonEncode(createEventMessage(this.key, EventType.ActorStopped, actorStop)) 251 | ); 252 | delete this.actors[(actorData as StopActorMessage).actor_ref]; 253 | delete this.invocationCallbacks![(actorData as StopActorMessage).actor_ref]; 254 | } 255 | throw new Error('sa.subscription was closed'); 256 | } 257 | 258 | /** 259 | * createLinkDefinition creates a link definition between an actor and a provider (unused) 260 | * 261 | * @param {string} actorKey - the actor key 262 | * @param {string} providerKey - the provider public key 263 | * @param {string} linkName - the name of the link 264 | * @param {string} contractId - the contract id of the linkdef 265 | * @param {any} values - list of key/value pairs to pass for the linkdef 266 | */ 267 | async createLinkDefinition(actorKey: string, providerKey: string, linkName: string, contractId: string, values: any) { 268 | const linkDefinition: CreateLinkDefMessage = { 269 | actor_id: actorKey, 270 | provider_id: providerKey, 271 | link_name: linkName, 272 | contract_id: contractId, 273 | values: values 274 | }; 275 | this.natsConn.publish(`wasmbus.rpc.${this.name}.${providerKey}.${linkName}.linkdefs.put`, encode(linkDefinition)); 276 | } 277 | 278 | /** 279 | * startHost connects to nats, starts the heartbeat, listens for actors start/stop 280 | */ 281 | async startHost() { 282 | await this.connectNATS(); 283 | Promise.all([this.startHeartbeat(), this.listenLaunchActor(), this.listenStopActor()]).catch((err: Error) => { 284 | throw err; 285 | }); 286 | } 287 | 288 | /** 289 | * stopHost stops the heartbeat, stops all actors, drains the nats connections and disconnects from nats 290 | */ 291 | async stopHost() { 292 | // stop the heartbeat 293 | await this.stopHeartbeat(); 294 | // stop all actors 295 | for (const actor in this.actors) { 296 | await this.stopActor(actor); 297 | } 298 | // TODO: we need to wait to drain and disconnect to wait for the stop_actors to process 299 | // drain all subscriptions 300 | await this.natsConn.drain(); 301 | // disconnect nats 302 | await this.disconnectNATS(); 303 | // return or throw? 304 | } 305 | } 306 | 307 | /** 308 | * startHost is the main function to start the js/browser host 309 | * 310 | * @param {string} name - the name of the host (defaults to 'default') 311 | * @param {boolean} withRegistryTLS - whether or not remote registries use tls 312 | * @param {Array|ConnectionOptions} natsConnection - an array of nats websocket servers OR a full nats connection object 313 | * @param {number} heartbeatInterval - used to determine the heartbeat to the lattice (defaults to 30000 or 30 seconds) 314 | * @returns {Host} 315 | */ 316 | export async function startHost( 317 | name: string, 318 | withRegistryTLS: boolean = true, 319 | natsConnection: Array | ConnectionOptions, 320 | heartbeatInterval?: number 321 | ) { 322 | const wasmModule: any = await import('../wasmcloud-rs-js/pkg/'); 323 | const wasm: any = await wasmModule.default; 324 | const host: Host = new Host( 325 | name, 326 | withRegistryTLS, 327 | heartbeatInterval ? heartbeatInterval : HOST_HEARTBEAT_INTERVAL, 328 | natsConnection, 329 | wasm 330 | ); 331 | await host.startHost(); 332 | return host; 333 | } 334 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { startHost } from './host'; 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type HeartbeatMessage = { 2 | actors: Array<{ 3 | actor: string; 4 | instances: number; 5 | }>; 6 | providers: []; 7 | }; 8 | 9 | export type CreateLinkDefMessage = { 10 | actor_id: string; 11 | provider_id: string; 12 | link_name: string; 13 | contract_id: string; 14 | values: any; 15 | }; 16 | 17 | export type ActorClaims = { 18 | jti: string; 19 | iat: number; 20 | iss: string; 21 | sub: string; 22 | wascap: { 23 | name: string; 24 | hash: string; 25 | tags: Array; 26 | caps: Array; 27 | ver: string; 28 | prov: boolean; 29 | }; 30 | }; 31 | 32 | export type ActorClaimsMessage = { 33 | call_alias: string; 34 | caps: any; 35 | iss: string; 36 | name: string; 37 | rev: string; 38 | sub: string; 39 | tags: string; 40 | version: string; 41 | }; 42 | 43 | export type LaunchActorMessage = { 44 | actor_ref: string; 45 | host_id: string; 46 | }; 47 | 48 | export type ActorStartedMessage = { 49 | api_version: number; 50 | instance_id: string; 51 | public_key: string; 52 | }; 53 | 54 | export type ActorHealthCheckPassMessage = { 55 | instance_id: string; 56 | public_key: string; 57 | }; 58 | 59 | export type StopActorMessage = { 60 | host_id: string; 61 | actor_ref: string; 62 | }; 63 | 64 | export type ActorStoppedMessage = { 65 | public_key: string; 66 | instance_id: string; 67 | }; 68 | 69 | export type InvocationMessage = { 70 | encoded_claims: string; 71 | host_id: string; 72 | id: string; 73 | msg: Uint8Array; 74 | operation: string; 75 | origin: { 76 | public_key: string; 77 | link_name: string; 78 | contract_id: string; 79 | }; 80 | target: { 81 | public_key: string; 82 | link_name: string; 83 | contract_id: string; 84 | }; 85 | }; 86 | 87 | export type InvocationCallbacks = { 88 | [key: string]: Function; 89 | }; 90 | 91 | /* eslint-disable no-unused-vars */ 92 | export type HostCall = (binding: string, namespace: string, operation: string, payload: Uint8Array) => Uint8Array; 93 | /* eslint-disable no-unused-vars */ 94 | export type Writer = (message: string) => void; 95 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { JSONCodec } from 'nats.ws'; 2 | 3 | const jc = JSONCodec(); 4 | 5 | /** 6 | * uuidv4 returns a uuid string 7 | * 8 | * @returns {string} 9 | */ 10 | export function uuidv4(): string { 11 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 12 | const r = (Math.random() * 16) | 0, 13 | v = c == 'x' ? r : (r & 0x3) | 0x8; 14 | return v.toString(16); 15 | }); 16 | } 17 | 18 | /** 19 | * parseJwt takes a jwt token and parses it into a json object 20 | * 21 | * @param token - the jwt token 22 | * @returns {any} - the parsed jwt token with claims 23 | */ 24 | export function parseJwt(token: string) { 25 | var base64Url = token.split('.')[1]; 26 | var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 27 | var jsonPayload = decodeURIComponent( 28 | atob(base64) 29 | .split('') 30 | .map(function (c) { 31 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 32 | }) 33 | .join('') 34 | ); 35 | 36 | return JSON.parse(jsonPayload); 37 | } 38 | 39 | /** 40 | * jsonEncode taks a json object and encodes it into uint8array for nats 41 | * 42 | * @param data - the data to encode 43 | * @returns {Uint8Array} 44 | */ 45 | export function jsonEncode(data: any): Uint8Array { 46 | return jc.encode(data); 47 | } 48 | 49 | /** 50 | * jsonDecode decodes nats messages into json 51 | * 52 | * @param {Uint8Array} data - the nats encoded data 53 | * @returns {any} 54 | */ 55 | export function jsonDecode(data: Uint8Array) { 56 | return jc.decode(data); 57 | } 58 | -------------------------------------------------------------------------------- /src/wasmbus.ts: -------------------------------------------------------------------------------- 1 | import { WapcHost } from '@wapc/host'; 2 | 3 | import { HostCall, Writer } from './types'; 4 | 5 | export async function instantiate(source: Uint8Array, hostCall?: HostCall, writer?: Writer): Promise { 6 | const host = new Wasmbus(hostCall, writer); 7 | return host.instantiate(source); 8 | } 9 | 10 | export class Wasmbus extends WapcHost { 11 | constructor(hostCall?: HostCall, writer?: Writer) { 12 | super(hostCall, writer); 13 | } 14 | 15 | async instantiate(source: Uint8Array): Promise { 16 | const imports = super.getImports(); 17 | const result = await WebAssembly.instantiate(source, { 18 | wasmbus: imports.wapc, 19 | wasi: imports.wasi, 20 | wasi_unstable: imports.wasi_unstable 21 | }).catch(e => { 22 | throw new Error(`Invalid wasm binary: ${e.message}`); 23 | }); 24 | super.initialize(result.instance); 25 | 26 | return this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import { describe } from 'mocha'; 4 | import { startHost } from '../src'; 5 | // import fs from 'fs'; 6 | // import path from 'path'; 7 | // import { encode, decode } from '@msgpack/msgpack'; 8 | 9 | chai.use(chaiAsPromised); 10 | const expect = chai.expect; 11 | 12 | describe('wasmcloudjs', function () { 13 | it('should initialize a host with the name and key set', async () => { 14 | const host = await startHost('default', false, ['ws://localhost:4222']); 15 | expect(host.name).to.equal('default'); 16 | expect(host.key) 17 | .to.be.a('string') 18 | .and.satisfy((key: string) => key.startsWith('N')); 19 | expect(host.actors).to.equal({}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/infra/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | registry: 4 | image: registry:2 5 | ports: 6 | - "5001:5001" 7 | volumes: 8 | - ./docker-registry.yml:/etc/docker/registry/config.yml 9 | nats: 10 | image: nats:latest 11 | ports: 12 | - "4222:4222" 13 | - "6222:6222" 14 | volumes: 15 | - ./nats.conf:/etc/nats.conf 16 | command: "-c=/etc/nats.conf -js" 17 | -------------------------------------------------------------------------------- /test/infra/docker-registry.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | fields: 4 | service: registry 5 | storage: 6 | cache: 7 | blobdescriptor: inmemory 8 | filesystem: 9 | rootdirectory: /var/lib/registry 10 | http: 11 | addr: :5000 12 | headers: 13 | X-Content-Type-Options: [nosniff] 14 | Access-Control-Allow-Origin: ["*"] 15 | health: 16 | storagedriver: 17 | enabled: true 18 | interval: 10s 19 | threshold: 3 20 | cors: 21 | origins: ["*"] 22 | methods: ["GET", "PUT", "POST", "DELETE"] 23 | headers: ["Access-Control-Allow-Origin", "Content-Type"] -------------------------------------------------------------------------------- /test/infra/nats.conf: -------------------------------------------------------------------------------- 1 | listen: localhost:4222 2 | websocket { 3 | # host: "hostname" 4 | port: 6222 5 | no_tls: true 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.ts", 4 | ], 5 | "exclude": [ 6 | "node_modules" 7 | ], 8 | "compilerOptions": { 9 | "rootDir": "./", 10 | "baseUrl": ".", 11 | "target": "ES2018", 12 | "module": "commonjs", 13 | "lib": ["es2018", "DOM"], 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | "declaration": true, 17 | "sourceMap": true, 18 | "outDir": "./dist", 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "typeRoots": [ 22 | "./types", 23 | "./node_modules/@types", 24 | "./wasmcloud-rs-js/pkg/" 25 | ], 26 | "esModuleInterop": true, 27 | "allowSyntheticDefaultImports": true, 28 | "forceConsistentCasingInFileNames": true 29 | } 30 | } -------------------------------------------------------------------------------- /wasmcloud-rs-js/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /tests 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | wasm-pack.log -------------------------------------------------------------------------------- /wasmcloud-rs-js/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmcloud-rs-js" 3 | version = "0.1.0" 4 | authors = ["ks2211 "] 5 | edition = "2018" 6 | description = "wasmcloud/wascap/nkeys rust crates ported to JavaScript via wasm-bindgen and wasm-pack" 7 | repository = "github.com/wasmCloud/wasmcloudjs" 8 | license = "Apache 2.0" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [features] 14 | default = ["console_error_panic_hook"] 15 | 16 | [dependencies] 17 | wasm-bindgen = "0.2.76" 18 | wascap = "0.6.0" 19 | getrandom = { version = "0.2", features = ["js"] } 20 | rand = { version = "0.7.3", features = ["wasm-bindgen"] } 21 | js-sys = "0.3.52" 22 | nkeys = "0.1.0" 23 | console_error_panic_hook = { version = "0.1.6", optional = true } 24 | 25 | [profile.release] 26 | # Tell `rustc` to optimize for small code size. 27 | opt-level = "s" 28 | -------------------------------------------------------------------------------- /wasmcloud-rs-js/README.md: -------------------------------------------------------------------------------- 1 | # wasmCloud Rust in JavaScript 2 | 3 | This contains a set of wasmCloud Rust related functionailty (`wascap`, `nkeys`) ported to JavaScript/wasm via `wasm-bindgen` and `wasm-pack`. The code is compiled to a library that is then imported in the JavaScript library. 4 | 5 | ## Prerequisities 6 | 7 | * rust 8 | 9 | * cargo 10 | 11 | * wasm-pack 12 | 13 | ## Build 14 | 15 | ```sh 16 | $ wasm-pack build 17 | ``` 18 | 19 | ## Usage 20 | 21 | **JavaScript** 22 | 23 | ```html 24 | 40 | ``` 41 | 42 | ## Contributing -------------------------------------------------------------------------------- /wasmcloud-rs-js/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use js_sys::{Uint8Array}; 3 | 4 | use wascap::wasm::{extract_claims}; 5 | use wascap::jwt::{validate_token, Actor}; 6 | 7 | use nkeys::KeyPair; 8 | 9 | // HostKey holds pk and seed for server key 10 | #[wasm_bindgen] 11 | pub struct HostKey { 12 | pk: String, 13 | seed: String, 14 | } 15 | 16 | // HostKey nkeys implementation 17 | #[wasm_bindgen] 18 | impl HostKey { 19 | #[wasm_bindgen(constructor)] 20 | // new creates a new nkeys server key 21 | pub fn new() -> HostKey { 22 | let kp = KeyPair::new_server(); 23 | let seed = kp.seed().unwrap(); 24 | HostKey { 25 | pk: kp.public_key(), 26 | seed: seed, 27 | } 28 | } 29 | 30 | // pk returns the HostKey pk 31 | #[wasm_bindgen(getter)] 32 | pub fn pk(&self) -> String { 33 | return String::from(&self.pk); 34 | } 35 | 36 | // seed returns the HostKey seed 37 | #[wasm_bindgen(getter)] 38 | pub fn seed(&self) -> String { 39 | return String::from(&self.seed); 40 | } 41 | } 42 | 43 | // extract_jwt extracts the jwt token from a wasm module as js_sys::Uint8Array 44 | #[wasm_bindgen] 45 | pub fn extract_jwt(contents: &Uint8Array) -> Result { 46 | let claims = extract_claims(contents.to_vec()); 47 | let out = match claims { 48 | Ok(token) => Ok(String::from(token.unwrap().jwt)), 49 | Err(err) => Err(JsValue::from_str(&format!("{}", err))), 50 | }; 51 | return out; 52 | } 53 | 54 | // validate_jwt validates the jwt token 55 | #[wasm_bindgen] 56 | pub fn validate_jwt(jwt: &str) -> bool { 57 | let validate = validate_token::(jwt); 58 | let token = validate.unwrap(); 59 | if token.cannot_use_yet || token.expired { 60 | return false; 61 | } 62 | return true; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /wasmcloud-rs-js/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); 3 | 4 | const sharedConfig = { 5 | stats: { assets: false, modules: false, errors: true }, 6 | mode: 'production', 7 | resolve: { 8 | extensions: ['.tsx', '.ts', '.js', '.wasm'] 9 | }, 10 | experiments: { 11 | asyncWebAssembly: true 12 | } 13 | } 14 | 15 | // this is specifically to use in a script tag 16 | const browserConfig = { 17 | output: { 18 | webassemblyModuleFilename: 'wasmcloud.wasm', 19 | filename: 'wasmcloud.js', 20 | path: path.resolve(__dirname, 'dist'), 21 | library: 'wasmcloudjs' 22 | }, 23 | entry: './src/index.ts', 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | use: 'ts-loader', 29 | exclude: /node_modules/ 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new WasmPackPlugin({ 35 | crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'), 36 | extraArgs: '--target bundler', 37 | outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg') 38 | }) 39 | ], 40 | ...sharedConfig 41 | } 42 | 43 | // this is used to bundle the rust wasm code in order to properly import into the compiled typescript code in the dist/src dir 44 | // the tsc compiler handles the src code to cjs 45 | const commonJSConfig = { 46 | entry: './wasmcloud-rs-js/pkg/index.js', 47 | output: { 48 | webassemblyModuleFilename: 'wasmcloud.wasm', 49 | filename: 'index.js', 50 | libraryTarget: 'commonjs2', 51 | path: path.resolve(__dirname, 'dist', 'wasmcloud-rs-js', 'pkg') 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.js$/, 57 | exclude: /(node_modules)/, 58 | use: { 59 | loader: 'babel-loader', 60 | options: { 61 | presets: ['@babel/preset-env'] 62 | } 63 | } 64 | } 65 | ] 66 | }, 67 | ...sharedConfig 68 | } 69 | 70 | module.exports = (env) => { 71 | switch (env.target) { 72 | case 'cjs': 73 | return commonJSConfig 74 | default: 75 | return browserConfig 76 | } 77 | }; --------------------------------------------------------------------------------