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