From 163903dda456c2af1d2541787524807ae86e0d4b Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 16 May 2023 15:35:48 -0700 Subject: [PATCH 01/18] chore: sort dependencies --- ucan-key-support/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml index c456bd16..b5a3bf80 100644 --- a/ucan-key-support/Cargo.toml +++ b/ucan-key-support/Cargo.toml @@ -25,8 +25,8 @@ async-trait = "0.1" bs58 = "0.4" ed25519-zebra = "3.1" log = "0.4" -rsa = "0.8" p256 = "0.13" +rsa = "0.8" sha2 = { version = "0.10", features = ["oid"] } ucan = { path = "../ucan", version = "0.3.2" } From 9b7fa7a8c88fa70b36839d61f416ffdd8e764a80 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Mon, 22 May 2023 14:26:35 -0700 Subject: [PATCH 02/18] feat: add ucan-wasm and JS packaging boilerplate --- Cargo.toml | 1 + ucan-wasm/Cargo.toml | 50 ++++++++++ ucan-wasm/LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++ ucan-wasm/README.md | 11 +++ ucan-wasm/src/lib.rs | 54 +++++++++++ ucan-wasm/tests/web.rs | 12 +++ 6 files changed, 329 insertions(+) create mode 100644 ucan-wasm/Cargo.toml create mode 100644 ucan-wasm/LICENSE create mode 100644 ucan-wasm/README.md create mode 100644 ucan-wasm/src/lib.rs create mode 100644 ucan-wasm/tests/web.rs diff --git a/Cargo.toml b/Cargo.toml index bc2ef54b..1baa9d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "ucan", "ucan-key-support", + "ucan-wasm" ] # Speedup build on macOS diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml new file mode 100644 index 00000000..38910c8f --- /dev/null +++ b/ucan-wasm/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "ucan-wasm" +version = "0.2.0" +description = "A UCAN library built from rs-ucan" +keywords = ["authorization"] +categories = [] +include = ["/src", "README.md", "LICENSE"] +license = "Apache-2.0" +readme = "README.md" +edition = "2021" +rust-version = "1.64" +documentation = "https://docs.rs/rust-template-test-wasm" +repository = "https://github.com/bgins/template-test/tree/main/rust-template-test-wasm" + +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[dependencies] +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1", optional = true } +instant = { version = "0.1", features = ["wasm-bindgen"] } +js-sys = { version = "0.3", optional = true } +tracing = "0.1" +ucan = { path = "../ucan", version = "0.3" } +wasm-bindgen = { version = "0.2", optional = true, features = ["serde-serialize"] } +wasm-bindgen-futures = { version = "0.4", optional = true } +web-sys = { version = "0.3", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[features] +default = ["js"] +full = ["js", "web"] +js = [ + "console_error_panic_hook", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures" +] +web = ["web-sys"] + +[package.metadata.docs.rs] +all-features = true +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/ucan-wasm/LICENSE b/ucan-wasm/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/ucan-wasm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ucan-wasm/README.md b/ucan-wasm/README.md new file mode 100644 index 00000000..1f0a77c7 --- /dev/null +++ b/ucan-wasm/README.md @@ -0,0 +1,11 @@ +### Build + +``` +wasm-pack build --target web +``` + +### Test + +``` +wasm-pack test --headless --firefox +``` diff --git a/ucan-wasm/src/lib.rs b/ucan-wasm/src/lib.rs new file mode 100644 index 00000000..131d1028 --- /dev/null +++ b/ucan-wasm/src/lib.rs @@ -0,0 +1,54 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] +#![deny(unreachable_pub, private_in_public)] + +//! rust-template-test + +use wasm_bindgen::prelude::wasm_bindgen; + +/// Add two integers together. +#[wasm_bindgen] +pub fn add(a: i32, b: i32) -> i32 { + a + b +} + +//------------------------------------------------------------------------------ +// Utilities +//------------------------------------------------------------------------------ + +/// Panic hook lets us get better error messages if our Rust code ever panics. +/// +/// For more details see +/// +#[wasm_bindgen(js_name = "setPanicHook")] +pub fn set_panic_hook() { + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +extern "C" { + // For alerting + pub(crate) fn alert(s: &str); + // For logging in the console. + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} + +//------------------------------------------------------------------------------ +// Macros +//------------------------------------------------------------------------------ + +/// Return a representation of an object owned by JS. +#[macro_export] +macro_rules! value { + ($value:expr) => { + wasm_bindgen::JsValue::from($value) + }; +} + +/// Calls the wasm_bindgen console.log. +#[macro_export] +macro_rules! console_log { + ($($t:tt)*) => ($crate::log(&format_args!($($t)*).to_string())) +} diff --git a/ucan-wasm/tests/web.rs b/ucan-wasm/tests/web.rs new file mode 100644 index 00000000..f34c97ce --- /dev/null +++ b/ucan-wasm/tests/web.rs @@ -0,0 +1,12 @@ +#![cfg(target_arch = "wasm32")] + +//! Test suite for the Web and headless browsers. + +use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn test_add() { + assert_eq!(ucan_wasm::add(3, 2), 5); + ucan_wasm::console_log!("{}", "Test passes!"); +} From 4ca89b274b02a5206c5b8204ca3eb63463c58ff0 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 23 May 2023 14:44:27 -0700 Subject: [PATCH 03/18] feat: add vitest browser tests --- ucan-wasm/.gitignore | 3 +++ ucan-wasm/package.json | 30 ++++++++++++++++++++++++++++++ ucan-wasm/tests/verify.ts | 12 ++++++++++++ ucan-wasm/vitest.config.ts | 12 ++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 ucan-wasm/.gitignore create mode 100644 ucan-wasm/package.json create mode 100644 ucan-wasm/tests/verify.ts create mode 100644 ucan-wasm/vitest.config.ts diff --git a/ucan-wasm/.gitignore b/ucan-wasm/.gitignore new file mode 100644 index 00000000..03ad240e --- /dev/null +++ b/ucan-wasm/.gitignore @@ -0,0 +1,3 @@ +package-lock.json +node_modules/ +pkg/ diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json new file mode 100644 index 00000000..971fa4cf --- /dev/null +++ b/ucan-wasm/package.json @@ -0,0 +1,30 @@ +{ + "name": "ucan-wasm", + "version": "0.3.0", + "description": "A UCAN library built from rs-ucan", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "vitest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ucan-wg/rs-ucan.git" + }, + "keywords": [ + "authorization" + ], + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/ucan-wg/rs-ucan/issues" + }, + "homepage": "https://github.com/ucan-wg/rs-ucan#readme", + "devDependencies": { + "@vitest/browser": "^0.31.1", + "vite": "^4.3.8", + "vitest": "^0.31.1", + "webdriverio": "^8.10.5" + } +} diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts new file mode 100644 index 00000000..8a3d1552 --- /dev/null +++ b/ucan-wasm/tests/verify.ts @@ -0,0 +1,12 @@ +import { beforeAll, expect, test } from 'vitest' + +import init from '../pkg/ucan_wasm' + +beforeAll(async () => { + await init() +}) + +test("should work as expected", () => { + expect(1 + 1).toEqual(2) + +}) diff --git a/ucan-wasm/vitest.config.ts b/ucan-wasm/vitest.config.ts new file mode 100644 index 00000000..e8712c58 --- /dev/null +++ b/ucan-wasm/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: [ 'tests/*.ts' ], + browser: { + enabled: true, + name: 'chrome', + headless: true + }, + }, +}) From 7e2c7e2adc951b83437b60375e5016e9c09c7bae Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 23 May 2023 15:43:36 -0700 Subject: [PATCH 04/18] feat: add isExpired function and tests --- ucan-wasm/README.md | 2 +- ucan-wasm/src/lib.rs | 6 +---- ucan-wasm/src/ucan/mod.rs | 3 +++ ucan-wasm/src/ucan/verify.rs | 13 +++++++++ ucan-wasm/tests/fixtures/index.ts | 39 +++++++++++++++++++++++++++ ucan-wasm/tests/fixtures/invalid.json | 21 +++++++++++++++ ucan-wasm/tests/fixtures/valid.json | 21 +++++++++++++++ ucan-wasm/tests/verify.ts | 10 ++++--- ucan-wasm/tests/web.rs | 12 --------- 9 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 ucan-wasm/src/ucan/mod.rs create mode 100644 ucan-wasm/src/ucan/verify.rs create mode 100644 ucan-wasm/tests/fixtures/index.ts create mode 100644 ucan-wasm/tests/fixtures/invalid.json create mode 100644 ucan-wasm/tests/fixtures/valid.json delete mode 100644 ucan-wasm/tests/web.rs diff --git a/ucan-wasm/README.md b/ucan-wasm/README.md index 1f0a77c7..b42f99b9 100644 --- a/ucan-wasm/README.md +++ b/ucan-wasm/README.md @@ -7,5 +7,5 @@ wasm-pack build --target web ### Test ``` -wasm-pack test --headless --firefox +npm run test ``` diff --git a/ucan-wasm/src/lib.rs b/ucan-wasm/src/lib.rs index 131d1028..f6b7649d 100644 --- a/ucan-wasm/src/lib.rs +++ b/ucan-wasm/src/lib.rs @@ -6,11 +6,7 @@ use wasm_bindgen::prelude::wasm_bindgen; -/// Add two integers together. -#[wasm_bindgen] -pub fn add(a: i32, b: i32) -> i32 { - a + b -} +pub mod ucan; //------------------------------------------------------------------------------ // Utilities diff --git a/ucan-wasm/src/ucan/mod.rs b/ucan-wasm/src/ucan/mod.rs new file mode 100644 index 00000000..6bfa815b --- /dev/null +++ b/ucan-wasm/src/ucan/mod.rs @@ -0,0 +1,3 @@ +pub mod verify; + +pub type JsResult = Result; diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs new file mode 100644 index 00000000..b029dc8f --- /dev/null +++ b/ucan-wasm/src/ucan/verify.rs @@ -0,0 +1,13 @@ +use crate::ucan::JsResult; +use ::ucan::{time::now, Ucan}; + +use js_sys::Error; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name = "isExpired")] +pub fn is_expired(token: String) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + let now = now(); + + Ok(Ucan::is_expired(&ucan, Some(now))) +} diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts new file mode 100644 index 00000000..0e666638 --- /dev/null +++ b/ucan-wasm/tests/fixtures/index.ts @@ -0,0 +1,39 @@ +import invalid from './invalid.json' +import valid from './valid.json' + +type Expectation = 'valid' | 'invalid' + +type Fixture = { + comment: string + token: string + assertions: { + header: { + alg: "EdDSA" + typ: "JWT" + ucv: "0.9.0-canary" + }, + payload: { + iss: string + aud: string + exp: number | null + nbf?: number + nnc?: string + fct: Record[], + att: {with: string, can: string}[], + prf: string[] + } + } + +} + +export function getFixture(expectation: Expectation, comment: string): Fixture { + let fixture + + if (expectation === 'valid') { + fixture = valid.find(f => f.comment === comment) + } else if (expectation === 'invalid') { + fixture = invalid.find(f => f.comment === comment) + } + + return fixture +} diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json new file mode 100644 index 00000000..31e476ef --- /dev/null +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -0,0 +1,21 @@ +[ + { + "comment": "UCAN has expired", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6MSwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 1, + "fct": [], + "att": [], + "prf": [] + } + } + } +] diff --git a/ucan-wasm/tests/fixtures/valid.json b/ucan-wasm/tests/fixtures/valid.json new file mode 100644 index 00000000..71ae66d4 --- /dev/null +++ b/ucan-wasm/tests/fixtures/valid.json @@ -0,0 +1,21 @@ +[ + { + "comment": "UCAN has not expired", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + } + } + } +] diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts index 8a3d1552..011bc542 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/verify.ts @@ -1,12 +1,16 @@ import { beforeAll, expect, test } from 'vitest' -import init from '../pkg/ucan_wasm' +import { getFixture } from './fixtures' +import init, { isExpired } from '../pkg/ucan_wasm' beforeAll(async () => { await init() }) -test("should work as expected", () => { - expect(1 + 1).toEqual(2) +test('isExpired should report active and expired UCANs', async () => { + const valid = getFixture('valid', 'UCAN has not expired') + expect(isExpired(valid.token)).toBe(false) + const invalid = getFixture('invalid', 'UCAN has expired') + expect(isExpired(invalid.token)).toBe(true) }) diff --git a/ucan-wasm/tests/web.rs b/ucan-wasm/tests/web.rs deleted file mode 100644 index f34c97ce..00000000 --- a/ucan-wasm/tests/web.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -//! Test suite for the Web and headless browsers. - -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn test_add() { - assert_eq!(ucan_wasm::add(3, 2), 5); - ucan_wasm::console_log!("{}", "Test passes!"); -} From 60bcbfa2a3940a1645c37a61fa1e9bbe1e71cd0f Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Wed, 24 May 2023 10:36:41 -0700 Subject: [PATCH 05/18] feat: add wireit to run npm scripts --- ucan-wasm/.gitignore | 1 + ucan-wasm/package.json | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ucan-wasm/.gitignore b/ucan-wasm/.gitignore index 03ad240e..57eceb52 100644 --- a/ucan-wasm/.gitignore +++ b/ucan-wasm/.gitignore @@ -1,3 +1,4 @@ package-lock.json node_modules/ pkg/ +.wireit/ diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index 971fa4cf..d8c01c13 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -6,7 +6,26 @@ "test": "tests" }, "scripts": { - "test": "vitest" + "build": "wireit", + "clean": "wireit", + "test": "wireit" + }, + "wireit": { + "build": { + "command": "wasm-pack build --target web", + "output": [ + "pkg/**" + ] + }, + "clean": { + "command": "rm -rf ./pkg" + }, + "test": { + "dependencies": [ + "build" + ], + "command": "vitest run" + } }, "repository": { "type": "git", @@ -25,6 +44,7 @@ "@vitest/browser": "^0.31.1", "vite": "^4.3.8", "vitest": "^0.31.1", - "webdriverio": "^8.10.5" + "webdriverio": "^8.10.5", + "wireit": "^0.9.5" } } From 66d6ca365351b02e56e0a42f2ff4ccc3acc4423f Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Wed, 24 May 2023 11:07:00 -0700 Subject: [PATCH 06/18] feat: add isTooEarly function and tests --- ucan-wasm/src/ucan/verify.rs | 9 +++++++++ ucan-wasm/tests/fixtures/index.ts | 3 ++- ucan-wasm/tests/fixtures/invalid.json | 28 ++++++++++++++++++++++++++- ucan-wasm/tests/fixtures/valid.json | 20 +++++++++++++++++++ ucan-wasm/tests/verify.ts | 10 +++++++++- 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs index b029dc8f..bd4793f3 100644 --- a/ucan-wasm/src/ucan/verify.rs +++ b/ucan-wasm/src/ucan/verify.rs @@ -4,6 +4,7 @@ use ::ucan::{time::now, Ucan}; use js_sys::Error; use wasm_bindgen::prelude::wasm_bindgen; +/// Returns true if the UCAN has past its expiration date #[wasm_bindgen(js_name = "isExpired")] pub fn is_expired(token: String) -> JsResult { let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; @@ -11,3 +12,11 @@ pub fn is_expired(token: String) -> JsResult { Ok(Ucan::is_expired(&ucan, Some(now))) } + +/// Returns true if the not-before ("nbf") time is still in the future +#[wasm_bindgen(js_name = "isTooEarly")] +pub fn is_too_early(token: String) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + Ok(Ucan::is_too_early(&ucan)) +} diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts index 0e666638..709f5389 100644 --- a/ucan-wasm/tests/fixtures/index.ts +++ b/ucan-wasm/tests/fixtures/index.ts @@ -21,7 +21,8 @@ type Fixture = { fct: Record[], att: {with: string, can: string}[], prf: string[] - } + }, + validationErrors?: string[] } } diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json index 31e476ef..c5e2c295 100644 --- a/ucan-wasm/tests/fixtures/invalid.json +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -15,7 +15,33 @@ "fct": [], "att": [], "prf": [] - } + }, + "validationErrors": [ + "expExpired" + ] + } + }, + { + "comment": "UCAN is not ready to be used", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjkyNDYwMDAwMDAsInByZiI6W119.W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "nbf": 9246000000, + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + }, + "validationErrors": [ + "nbfNotReady" + ] } } ] diff --git a/ucan-wasm/tests/fixtures/valid.json b/ucan-wasm/tests/fixtures/valid.json index 71ae66d4..1d831d59 100644 --- a/ucan-wasm/tests/fixtures/valid.json +++ b/ucan-wasm/tests/fixtures/valid.json @@ -17,5 +17,25 @@ "prf": [] } } + }, + { + "comment": "UCAN is ready to be used", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInByZiI6W119.-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "nbf": 1, + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + } + } } ] diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts index 011bc542..0cc157b6 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/verify.ts @@ -1,7 +1,7 @@ import { beforeAll, expect, test } from 'vitest' import { getFixture } from './fixtures' -import init, { isExpired } from '../pkg/ucan_wasm' +import init, { isExpired, isTooEarly } from '../pkg/ucan_wasm' beforeAll(async () => { await init() @@ -14,3 +14,11 @@ test('isExpired should report active and expired UCANs', async () => { const invalid = getFixture('invalid', 'UCAN has expired') expect(isExpired(invalid.token)).toBe(true) }) + +test('isTooEarly should report active and early UCANs', async () => { + const valid = getFixture('valid', 'UCAN is ready to be used') + expect(isTooEarly(valid.token)).toBe(false) + + const invalid = getFixture('invalid', 'UCAN is not ready to be used') + expect(isTooEarly(invalid.token)).toBe(true) +}) From 472f504840a92949b49f34ee53ac2c66b06b8e6d Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 25 May 2023 10:47:36 -0600 Subject: [PATCH 07/18] Add checkSignature function and tests --- ucan-wasm/Cargo.toml | 1 + ucan-wasm/src/ucan/verify.rs | 30 ++++++++++++++++++++++++++- ucan-wasm/tests/fixtures/invalid.json | 19 +++++++++++++++++ ucan-wasm/tests/fixtures/valid.json | 19 +++++++++++++++++ ucan-wasm/tests/verify.ts | 16 ++++++++++++-- 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml index 38910c8f..b9585bb8 100644 --- a/ucan-wasm/Cargo.toml +++ b/ucan-wasm/Cargo.toml @@ -26,6 +26,7 @@ instant = { version = "0.1", features = ["wasm-bindgen"] } js-sys = { version = "0.3", optional = true } tracing = "0.1" ucan = { path = "../ucan", version = "0.3" } +ucan-key-support = { path = "../ucan-key-support", version = "0.1.5" } wasm-bindgen = { version = "0.2", optional = true, features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", optional = true } web-sys = { version = "0.3", optional = true } diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs index bd4793f3..0cbbec7b 100644 --- a/ucan-wasm/src/ucan/verify.rs +++ b/ucan-wasm/src/ucan/verify.rs @@ -1,9 +1,37 @@ use crate::ucan::JsResult; -use ::ucan::{time::now, Ucan}; +use ::ucan::{ + crypto::did::{ + DidParser, KeyConstructorSlice, ED25519_MAGIC_BYTES, P256_MAGIC_BYTES, RSA_MAGIC_BYTES, + }, + time::now, + Ucan, +}; +use ::ucan_key_support::{ + ed25519::bytes_to_ed25519_key, p256::bytes_to_p256_key, rsa::bytes_to_rsa_key, +}; use js_sys::Error; use wasm_bindgen::prelude::wasm_bindgen; +const SUPPORTED_KEYS: &KeyConstructorSlice = &[ + (ED25519_MAGIC_BYTES, bytes_to_ed25519_key), + (RSA_MAGIC_BYTES, bytes_to_rsa_key), + (P256_MAGIC_BYTES, bytes_to_p256_key), +]; + +/// Validate that the signed data was signed by the stated issuer +#[wasm_bindgen(js_name = "checkSignature")] +pub async fn check_signature(token: String) -> JsResult<()> { + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + ucan.check_signature(&mut did_parser) + .await + .map_err(|e| Error::new(&format!("{e}")))?; + + Ok(()) +} + /// Returns true if the UCAN has past its expiration date #[wasm_bindgen(js_name = "isExpired")] pub fn is_expired(token: String) -> JsResult { diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json index c5e2c295..247b14da 100644 --- a/ucan-wasm/tests/fixtures/invalid.json +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -43,5 +43,24 @@ "nbfNotReady" ] } + }, + { + "comment": "UCAN has an invalid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglC", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + } + } } ] diff --git a/ucan-wasm/tests/fixtures/valid.json b/ucan-wasm/tests/fixtures/valid.json index 1d831d59..022d964b 100644 --- a/ucan-wasm/tests/fixtures/valid.json +++ b/ucan-wasm/tests/fixtures/valid.json @@ -37,5 +37,24 @@ "prf": [] } } + }, + { + "comment": "UCAN has a valid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "fct": [], + "att": [], + "prf": [] + } + } } ] diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts index 0cc157b6..41350d80 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/verify.ts @@ -1,7 +1,7 @@ -import { beforeAll, expect, test } from 'vitest' +import { beforeAll, describe, expect, test } from 'vitest' import { getFixture } from './fixtures' -import init, { isExpired, isTooEarly } from '../pkg/ucan_wasm' +import init, { checkSignature, isExpired, isTooEarly } from '../pkg/ucan_wasm' beforeAll(async () => { await init() @@ -22,3 +22,15 @@ test('isTooEarly should report active and early UCANs', async () => { const invalid = getFixture('invalid', 'UCAN is not ready to be used') expect(isTooEarly(invalid.token)).toBe(true) }) + +describe('checkSignature', async () => { + test('should report a valid signature', async () => { + const valid = getFixture('valid', 'UCAN has a valid signature') + await expect(checkSignature(valid.token)).resolves.toBe(undefined); + }) + + test('should throw on an invalid signature', async () => { + const invalid = getFixture('invalid', 'UCAN has an invalid signature') + await expect(checkSignature(invalid.token)).rejects.toThrowError() + }) +}) From a6bd2f4daf4fd551ad8967764bcf3e5efdacb053 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 25 May 2023 14:56:51 -0600 Subject: [PATCH 08/18] refactor: arrange tests into describe blocks --- ucan-wasm/src/lib.rs | 4 ++-- ucan-wasm/tests/verify.ts | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/ucan-wasm/src/lib.rs b/ucan-wasm/src/lib.rs index f6b7649d..3fdb5225 100644 --- a/ucan-wasm/src/lib.rs +++ b/ucan-wasm/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -#![warn(missing_debug_implementations, missing_docs, rust_2018_idioms)] +#![warn(missing_debug_implementations, rust_2018_idioms)] #![deny(unreachable_pub, private_in_public)] -//! rust-template-test +//! ucan-wasm use wasm_bindgen::prelude::wasm_bindgen; diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts index 41350d80..738a553d 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/verify.ts @@ -7,20 +7,28 @@ beforeAll(async () => { await init() }) -test('isExpired should report active and expired UCANs', async () => { - const valid = getFixture('valid', 'UCAN has not expired') - expect(isExpired(valid.token)).toBe(false) +describe('isExpired', () => { + test('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN has not expired') + expect(isExpired(valid.token)).toBe(false) + }) - const invalid = getFixture('invalid', 'UCAN has expired') - expect(isExpired(invalid.token)).toBe(true) + test('should be true when a UCAN is expired', () => { + const invalid = getFixture('invalid', 'UCAN has expired') + expect(isExpired(invalid.token)).toBe(true) + }) }) -test('isTooEarly should report active and early UCANs', async () => { - const valid = getFixture('valid', 'UCAN is ready to be used') - expect(isTooEarly(valid.token)).toBe(false) +describe('isTooEarly', () => { + test('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN is ready to be used') + expect(isTooEarly(valid.token)).toBe(false) + }) - const invalid = getFixture('invalid', 'UCAN is not ready to be used') - expect(isTooEarly(invalid.token)).toBe(true) + test('should be true when a UCAN is early', () => { + const invalid = getFixture('invalid', 'UCAN is not ready to be used') + expect(isTooEarly(invalid.token)).toBe(true) + }) }) describe('checkSignature', async () => { From 3032871c907af38b722da100dc2ca0286b9ba0aa Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 25 May 2023 15:13:38 -0600 Subject: [PATCH 09/18] feat: add validate function and tests --- ucan-wasm/src/ucan/verify.rs | 14 +++++++++++ ucan-wasm/tests/verify.ts | 48 ++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs index 0cbbec7b..ad42a6e2 100644 --- a/ucan-wasm/src/ucan/verify.rs +++ b/ucan-wasm/src/ucan/verify.rs @@ -19,6 +19,20 @@ const SUPPORTED_KEYS: &KeyConstructorSlice = &[ (P256_MAGIC_BYTES, bytes_to_p256_key), ]; +/// Validate the UCAN's signature and timestamps +#[wasm_bindgen(js_name = "validate")] +pub async fn validate(token: String) -> JsResult<()> { + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + let now = now(); + + Ucan::validate(&ucan, Some(now), &mut did_parser) + .await + .map_err(|e| Error::new(&format!("{e}")))?; + + Ok(()) +} + /// Validate that the signed data was signed by the stated issuer #[wasm_bindgen(js_name = "checkSignature")] pub async fn check_signature(token: String) -> JsResult<()> { diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/verify.ts index 738a553d..fb9c7769 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/verify.ts @@ -1,12 +1,46 @@ import { beforeAll, describe, expect, test } from 'vitest' import { getFixture } from './fixtures' -import init, { checkSignature, isExpired, isTooEarly } from '../pkg/ucan_wasm' +import init, { checkSignature, isExpired, isTooEarly, validate } from '../pkg/ucan_wasm' beforeAll(async () => { await init() }) +describe('validate', async () => { + test('should resolve on a valid a UCAN', async () => { + const validSignature = getFixture('valid', 'UCAN has a valid signature') + const unexpired = getFixture('valid', 'UCAN has not expired') + const active = getFixture('valid', 'UCAN is ready to be used') + + await expect(validate(validSignature.token)).resolves.toBe(undefined); + await expect(validate(unexpired.token)).resolves.toBe(undefined); + await expect(validate(active.token)).resolves.toBe(undefined); + }) + + test('should be true when a UCAN is expired', async () => { + const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') + const expired = getFixture('invalid', 'UCAN has expired') + const early = getFixture('invalid', 'UCAN is not ready to be used') + + await expect(validate(invalidSignature.token)).rejects.toThrowError() + await expect(validate(expired.token)).rejects.toThrowError() + await expect(validate(early.token)).rejects.toThrowError() + }) +}) + +describe('checkSignature', async () => { + test('should resolve on a valid signature', async () => { + const valid = getFixture('valid', 'UCAN has a valid signature') + await expect(checkSignature(valid.token)).resolves.toBe(undefined); + }) + + test('should throw on an invalid signature', async () => { + const invalid = getFixture('invalid', 'UCAN has an invalid signature') + await expect(checkSignature(invalid.token)).rejects.toThrowError() + }) +}) + describe('isExpired', () => { test('should be false when a UCAN is active', () => { const valid = getFixture('valid', 'UCAN has not expired') @@ -30,15 +64,3 @@ describe('isTooEarly', () => { expect(isTooEarly(invalid.token)).toBe(true) }) }) - -describe('checkSignature', async () => { - test('should report a valid signature', async () => { - const valid = getFixture('valid', 'UCAN has a valid signature') - await expect(checkSignature(valid.token)).resolves.toBe(undefined); - }) - - test('should throw on an invalid signature', async () => { - const invalid = getFixture('invalid', 'UCAN has an invalid signature') - await expect(checkSignature(invalid.token)).rejects.toThrowError() - }) -}) From 2ad0a279278cbd5c63e5a8bd27c90635270fc3ad Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Fri, 26 May 2023 13:19:47 -0600 Subject: [PATCH 10/18] feat: add support for node, deno, and workerd Cloudflare workers use the workerd key. --- ucan-wasm/package.json | 124 +++++++++++++++---- ucan-wasm/src/loaders/export-workerd-wasm.js | 6 + 2 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 ucan-wasm/src/loaders/export-workerd-wasm.js diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index d8c01c13..653885f9 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -2,23 +2,117 @@ "name": "ucan-wasm", "version": "0.3.0", "description": "A UCAN library built from rs-ucan", - "directories": { - "test": "tests" + "repository": { + "type": "git", + "url": "git+https://github.com/ucan-wg/rs-ucan.git" + }, + "keywords": [ + "authorization" + ], + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/ucan-wg/rs-ucan/issues" + }, + "homepage": "https://github.com/ucan-wg/rs-ucan#readme", + "type": "module", + "exports": { + "deno": { + "types": "./lib/deno/ucan_wasm.d.ts", + "default": "./lib/deno/ucan_wasm.js" + }, + "node": { + "types": "./lib/node/ucan_wasm.d.ts", + "require": "./lib/node/ucan_wasm.js" + }, + "workerd": { + "types": "./lib/workerd/ucan_wasm.d.ts", + "default": "./lib/workerd/index.js" + }, + "types": "./lib/browser/ucan_wasm.d.ts", + "default": "./lib/browser/ucan_wasm.js" }, "scripts": { - "build": "wireit", + "build": "export PROFILE=dev && export TARGET_DIR=debug && npm run buildall", + "release": "export PROFILE=release && export TARGET_DIR=release && npm run buildall", + "buildall": "wireit", "clean": "wireit", "test": "wireit" }, "wireit": { - "build": { - "command": "wasm-pack build --target web", - "output": [ - "pkg/**" + "compile": { + "command": "cargo build --target wasm32-unknown-unknown --profile $PROFILE", + "env": { + "PROFILE": { + "external": true + } + } + }, + "opt": { + "command": "wasm-opt -O1 ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm -o ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "compile" + ] + }, + "bindgen:browser": { + "command": "wasm-bindgen --target web --out-dir lib/browser ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:node": { + "command": "wasm-bindgen --target nodejs --out-dir lib/node ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:deno": { + "command": "wasm-bindgen --target deno --out-dir lib/deno ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "bindgen:workerd": { + "command": "wasm-bindgen --target web --out-dir lib/workerd ../target/wasm32-unknown-unknown/$TARGET_DIR/ucan_wasm.wasm && shx cp src/loaders/export-workerd-wasm.js lib/workerd/index.js", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ] + }, + "buildall": { + "dependencies": [ + "bindgen:browser", + "bindgen:node", + "bindgen:deno", + "bindgen:workerd" ] }, "clean": { - "command": "rm -rf ./pkg" + "command": "shx rm -rf ./lib" }, "test": { "dependencies": [ @@ -27,21 +121,9 @@ "command": "vitest run" } }, - "repository": { - "type": "git", - "url": "git+https://github.com/ucan-wg/rs-ucan.git" - }, - "keywords": [ - "authorization" - ], - "author": "", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/ucan-wg/rs-ucan/issues" - }, - "homepage": "https://github.com/ucan-wg/rs-ucan#readme", "devDependencies": { "@vitest/browser": "^0.31.1", + "shx": "^0.3.4", "vite": "^4.3.8", "vitest": "^0.31.1", "webdriverio": "^8.10.5", diff --git a/ucan-wasm/src/loaders/export-workerd-wasm.js b/ucan-wasm/src/loaders/export-workerd-wasm.js new file mode 100644 index 00000000..29811960 --- /dev/null +++ b/ucan-wasm/src/loaders/export-workerd-wasm.js @@ -0,0 +1,6 @@ +// This entry point is inserted into ./lib/workerd to support Cloudflare workers + +import WASM from "./ucan_wasm_bg.wasm"; +import { initSync } from "./ucan_wasm.js"; +initSync(WASM); +export * from "./ucan_wasm.js"; From 71c23597be470897433e9bd26d426f1585bac161 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Sun, 28 May 2023 18:43:31 -0600 Subject: [PATCH 11/18] feat: add browser and node testing --- ucan-wasm/.gitignore | 1 + ucan-wasm/package.json | 43 ++++++++++++- .../{verify.ts => browser/verify.test.ts} | 4 +- ucan-wasm/tests/node/verify.test.ts | 62 +++++++++++++++++++ ucan-wasm/vitest.config.ts | 7 +-- 5 files changed, 106 insertions(+), 11 deletions(-) rename ucan-wasm/tests/{verify.ts => browser/verify.test.ts} (96%) create mode 100644 ucan-wasm/tests/node/verify.test.ts diff --git a/ucan-wasm/.gitignore b/ucan-wasm/.gitignore index 57eceb52..f6221e83 100644 --- a/ucan-wasm/.gitignore +++ b/ucan-wasm/.gitignore @@ -1,4 +1,5 @@ package-lock.json node_modules/ pkg/ +tests/report .wireit/ diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index 653885f9..2d728fd4 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -37,7 +37,9 @@ "release": "export PROFILE=release && export TARGET_DIR=release && npm run buildall", "buildall": "wireit", "clean": "wireit", - "test": "wireit" + "test": "wireit", + "test:browser": "wireit", + "test:node": "wireit" }, "wireit": { "compile": { @@ -114,11 +116,46 @@ "clean": { "command": "shx rm -rf ./lib" }, - "test": { + "test:chrome": { + "command": "vitest run --browser.name=chrome --browser.headless --dir tests/browser --outputFile tests/report/chrome.json", + "dependencies": [ + "build" + ], + "output": ["tests/report/chrome.json"] + }, + "test:firefox": { + "command": "vitest run --browser.name=firefox --browser.headless --dir tests/browser --outputFile tests/report/firefox.json", + "dependencies": [ + "build" + ], + "output": ["tests/report/firefox.json"] + }, + "test:edge": { + "command": "vitest run --browser.name=edge --browser.headless --dir tests/browser --outputFile tests/report/edge.json", + "dependencies": [ + "build" + ], + "output": ["tests/report/edge.json"] + }, + "test:node": { + "command": "vitest run --environment node --dir tests/node --outputFile tests/report/node.json", "dependencies": [ "build" ], - "command": "vitest run" + "output": ["tests/report/node.json"] + }, + "test:browser": { + "dependencies": [ + "test:chrome", + "test:firefox", + "test:edge" + ] + }, + "test": { + "dependencies": [ + "test:browser", + "test:node" + ] } }, "devDependencies": { diff --git a/ucan-wasm/tests/verify.ts b/ucan-wasm/tests/browser/verify.test.ts similarity index 96% rename from ucan-wasm/tests/verify.ts rename to ucan-wasm/tests/browser/verify.test.ts index fb9c7769..77128650 100644 --- a/ucan-wasm/tests/verify.ts +++ b/ucan-wasm/tests/browser/verify.test.ts @@ -1,7 +1,7 @@ import { beforeAll, describe, expect, test } from 'vitest' -import { getFixture } from './fixtures' -import init, { checkSignature, isExpired, isTooEarly, validate } from '../pkg/ucan_wasm' +import { getFixture } from '../fixtures' +import init, { checkSignature, isExpired, isTooEarly, validate } from '../../lib/browser/ucan_wasm.js' beforeAll(async () => { await init() diff --git a/ucan-wasm/tests/node/verify.test.ts b/ucan-wasm/tests/node/verify.test.ts new file mode 100644 index 00000000..2d0d2119 --- /dev/null +++ b/ucan-wasm/tests/node/verify.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from 'vitest' + +import { getFixture } from '../fixtures' +import { checkSignature, isExpired, isTooEarly, validate } from '../../lib/node/ucan_wasm.js' + +describe('validate', async () => { + test('should resolve on a valid a UCAN', async () => { + const validSignature = getFixture('valid', 'UCAN has a valid signature') + const unexpired = getFixture('valid', 'UCAN has not expired') + const active = getFixture('valid', 'UCAN is ready to be used') + + await expect(validate(validSignature.token)).resolves.toBe(undefined); + await expect(validate(unexpired.token)).resolves.toBe(undefined); + await expect(validate(active.token)).resolves.toBe(undefined); + }) + + test('should be true when a UCAN is expired', async () => { + const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') + const expired = getFixture('invalid', 'UCAN has expired') + const early = getFixture('invalid', 'UCAN is not ready to be used') + + await expect(validate(invalidSignature.token)).rejects.toThrowError() + await expect(validate(expired.token)).rejects.toThrowError() + await expect(validate(early.token)).rejects.toThrowError() + }) +}) + +describe('checkSignature', async () => { + test('should resolve on a valid signature', async () => { + const valid = getFixture('valid', 'UCAN has a valid signature') + await expect(checkSignature(valid.token)).resolves.toBe(undefined); + }) + + test('should throw on an invalid signature', async () => { + const invalid = getFixture('invalid', 'UCAN has an invalid signature') + await expect(checkSignature(invalid.token)).rejects.toThrowError() + }) +}) + +describe('isExpired', () => { + test('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN has not expired') + expect(isExpired(valid.token)).toBe(false) + }) + + test('should be true when a UCAN is expired', () => { + const invalid = getFixture('invalid', 'UCAN has expired') + expect(isExpired(invalid.token)).toBe(true) + }) +}) + +describe('isTooEarly', () => { + test('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN is ready to be used') + expect(isTooEarly(valid.token)).toBe(false) + }) + + test('should be true when a UCAN is early', () => { + const invalid = getFixture('invalid', 'UCAN is not ready to be used') + expect(isTooEarly(invalid.token)).toBe(true) + }) +}) diff --git a/ucan-wasm/vitest.config.ts b/ucan-wasm/vitest.config.ts index e8712c58..97ca8107 100644 --- a/ucan-wasm/vitest.config.ts +++ b/ucan-wasm/vitest.config.ts @@ -2,11 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - include: [ 'tests/*.ts' ], - browser: { - enabled: true, - name: 'chrome', - headless: true - }, + reporters: [ "json" ] }, }) From 2692f18741df72cdb803b9372785133b68ac02e8 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Wed, 31 May 2023 14:04:03 -0700 Subject: [PATCH 12/18] feat: improve node and browser tests Use playwright-tests for browser tests instead of vitest. It is currently more stable than vitest for browser testing. Convert tests from expect to assert for a purely JS implementation. Unify tests into a single module which is loaded by each environment after import and initialization (where required). Add coverage. Write coverage and test results for each environment to JSON files. --- ucan-wasm/.gitignore | 1 + ucan-wasm/package.json | 80 ++++++++++++++++++-------- ucan-wasm/tests/browser.test.ts | 15 +++++ ucan-wasm/tests/browser/verify.test.ts | 66 --------------------- ucan-wasm/tests/node.test.ts | 14 +++++ ucan-wasm/tests/node/verify.test.ts | 62 -------------------- ucan-wasm/tests/ucan/verify.test.ts | 77 +++++++++++++++++++++++++ ucan-wasm/tsconfig.json | 9 +++ 8 files changed, 172 insertions(+), 152 deletions(-) create mode 100644 ucan-wasm/tests/browser.test.ts delete mode 100644 ucan-wasm/tests/browser/verify.test.ts create mode 100644 ucan-wasm/tests/node.test.ts delete mode 100644 ucan-wasm/tests/node/verify.test.ts create mode 100644 ucan-wasm/tests/ucan/verify.test.ts create mode 100644 ucan-wasm/tsconfig.json diff --git a/ucan-wasm/.gitignore b/ucan-wasm/.gitignore index f6221e83..fa15686a 100644 --- a/ucan-wasm/.gitignore +++ b/ucan-wasm/.gitignore @@ -2,4 +2,5 @@ package-lock.json node_modules/ pkg/ tests/report +.nyc_output/ .wireit/ diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index 2d728fd4..32c46932 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -116,54 +116,86 @@ "clean": { "command": "shx rm -rf ./lib" }, - "test:chrome": { - "command": "vitest run --browser.name=chrome --browser.headless --dir tests/browser --outputFile tests/report/chrome.json", + "test:prepare": { + "command": "npx playwright install && shx mkdir tests/report", + "output": [ + "tests/report" + ] + }, + "test:chromium": { + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --cov > tests/report/chromium.json", "dependencies": [ - "build" - ], - "output": ["tests/report/chrome.json"] + "build", + "test:prepare" + ] }, "test:firefox": { - "command": "vitest run --browser.name=firefox --browser.headless --dir tests/browser --outputFile tests/report/firefox.json", + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --browser firefox > tests/report/firefox.json", "dependencies": [ - "build" + "build", + "test:prepare" ], - "output": ["tests/report/firefox.json"] + "output": [ + "tests/report/firefox.json" + ] }, - "test:edge": { - "command": "vitest run --browser.name=edge --browser.headless --dir tests/browser --outputFile tests/report/edge.json", + "test:webkit": { + "command": "pw-test tests/browser.test.ts --assets lib/browser --reporter json --browser webkit > tests/report/webkit.json", "dependencies": [ - "build" + "build", + "test:prepare" ], - "output": ["tests/report/edge.json"] + "output": [ + "tests/report/webkit.json" + ] + }, + "test:browser": { + "dependencies": [ + "test:chromium", + "test:firefox", + "test:webkit" + ] }, "test:node": { - "command": "vitest run --environment node --dir tests/node --outputFile tests/report/node.json", + "command": "vitest run node.test.ts --outputFile tests/report/node.json", "dependencies": [ - "build" + "build", + "test:prepare" ], - "output": ["tests/report/node.json"] + "output": [ + "tests/report/node.json" + ] }, - "test:browser": { + "test:report": { + "command": "nyc report --reporter=json-summary --report-dir tests/report", "dependencies": [ - "test:chrome", - "test:firefox", - "test:edge" + "test:chromium" + ], + "output": [ + "tests/report/coverage-summary.json" ] }, "test": { "dependencies": [ "test:browser", - "test:node" + "test:node", + "test:report" ] } }, "devDependencies": { - "@vitest/browser": "^0.31.1", + "@playwright/test": "^1.34.3", + "@types/expect": "^24.3.0", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", + "assert": "^2.0.0", + "expect": "^29.5.0", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "playwright-test": "^9.1.0", "shx": "^0.3.4", - "vite": "^4.3.8", - "vitest": "^0.31.1", - "webdriverio": "^8.10.5", + "ts-node": "^10.9.1", + "vitest": "^0.31.4", "wireit": "^0.9.5" } } diff --git a/ucan-wasm/tests/browser.test.ts b/ucan-wasm/tests/browser.test.ts new file mode 100644 index 00000000..0ea10e19 --- /dev/null +++ b/ucan-wasm/tests/browser.test.ts @@ -0,0 +1,15 @@ +import init, { checkSignature, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' +import { runVerifyTests } from "./ucan/verify.test.js" + +before(async () => { + await init() +}) + +runVerifyTests({ + ucan: { + isExpired, + isTooEarly, + checkSignature, + validate + } +}) diff --git a/ucan-wasm/tests/browser/verify.test.ts b/ucan-wasm/tests/browser/verify.test.ts deleted file mode 100644 index 77128650..00000000 --- a/ucan-wasm/tests/browser/verify.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { beforeAll, describe, expect, test } from 'vitest' - -import { getFixture } from '../fixtures' -import init, { checkSignature, isExpired, isTooEarly, validate } from '../../lib/browser/ucan_wasm.js' - -beforeAll(async () => { - await init() -}) - -describe('validate', async () => { - test('should resolve on a valid a UCAN', async () => { - const validSignature = getFixture('valid', 'UCAN has a valid signature') - const unexpired = getFixture('valid', 'UCAN has not expired') - const active = getFixture('valid', 'UCAN is ready to be used') - - await expect(validate(validSignature.token)).resolves.toBe(undefined); - await expect(validate(unexpired.token)).resolves.toBe(undefined); - await expect(validate(active.token)).resolves.toBe(undefined); - }) - - test('should be true when a UCAN is expired', async () => { - const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') - const expired = getFixture('invalid', 'UCAN has expired') - const early = getFixture('invalid', 'UCAN is not ready to be used') - - await expect(validate(invalidSignature.token)).rejects.toThrowError() - await expect(validate(expired.token)).rejects.toThrowError() - await expect(validate(early.token)).rejects.toThrowError() - }) -}) - -describe('checkSignature', async () => { - test('should resolve on a valid signature', async () => { - const valid = getFixture('valid', 'UCAN has a valid signature') - await expect(checkSignature(valid.token)).resolves.toBe(undefined); - }) - - test('should throw on an invalid signature', async () => { - const invalid = getFixture('invalid', 'UCAN has an invalid signature') - await expect(checkSignature(invalid.token)).rejects.toThrowError() - }) -}) - -describe('isExpired', () => { - test('should be false when a UCAN is active', () => { - const valid = getFixture('valid', 'UCAN has not expired') - expect(isExpired(valid.token)).toBe(false) - }) - - test('should be true when a UCAN is expired', () => { - const invalid = getFixture('invalid', 'UCAN has expired') - expect(isExpired(invalid.token)).toBe(true) - }) -}) - -describe('isTooEarly', () => { - test('should be false when a UCAN is active', () => { - const valid = getFixture('valid', 'UCAN is ready to be used') - expect(isTooEarly(valid.token)).toBe(false) - }) - - test('should be true when a UCAN is early', () => { - const invalid = getFixture('invalid', 'UCAN is not ready to be used') - expect(isTooEarly(invalid.token)).toBe(true) - }) -}) diff --git a/ucan-wasm/tests/node.test.ts b/ucan-wasm/tests/node.test.ts new file mode 100644 index 00000000..51c886c5 --- /dev/null +++ b/ucan-wasm/tests/node.test.ts @@ -0,0 +1,14 @@ +import { describe, it } from 'vitest' + +import { checkSignature, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { runVerifyTests } from "./ucan/verify.test.js" + +runVerifyTests({ + runner: { describe, it }, + ucan: { + isExpired, + isTooEarly, + checkSignature, + validate + } +}) diff --git a/ucan-wasm/tests/node/verify.test.ts b/ucan-wasm/tests/node/verify.test.ts deleted file mode 100644 index 2d0d2119..00000000 --- a/ucan-wasm/tests/node/verify.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { getFixture } from '../fixtures' -import { checkSignature, isExpired, isTooEarly, validate } from '../../lib/node/ucan_wasm.js' - -describe('validate', async () => { - test('should resolve on a valid a UCAN', async () => { - const validSignature = getFixture('valid', 'UCAN has a valid signature') - const unexpired = getFixture('valid', 'UCAN has not expired') - const active = getFixture('valid', 'UCAN is ready to be used') - - await expect(validate(validSignature.token)).resolves.toBe(undefined); - await expect(validate(unexpired.token)).resolves.toBe(undefined); - await expect(validate(active.token)).resolves.toBe(undefined); - }) - - test('should be true when a UCAN is expired', async () => { - const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') - const expired = getFixture('invalid', 'UCAN has expired') - const early = getFixture('invalid', 'UCAN is not ready to be used') - - await expect(validate(invalidSignature.token)).rejects.toThrowError() - await expect(validate(expired.token)).rejects.toThrowError() - await expect(validate(early.token)).rejects.toThrowError() - }) -}) - -describe('checkSignature', async () => { - test('should resolve on a valid signature', async () => { - const valid = getFixture('valid', 'UCAN has a valid signature') - await expect(checkSignature(valid.token)).resolves.toBe(undefined); - }) - - test('should throw on an invalid signature', async () => { - const invalid = getFixture('invalid', 'UCAN has an invalid signature') - await expect(checkSignature(invalid.token)).rejects.toThrowError() - }) -}) - -describe('isExpired', () => { - test('should be false when a UCAN is active', () => { - const valid = getFixture('valid', 'UCAN has not expired') - expect(isExpired(valid.token)).toBe(false) - }) - - test('should be true when a UCAN is expired', () => { - const invalid = getFixture('invalid', 'UCAN has expired') - expect(isExpired(invalid.token)).toBe(true) - }) -}) - -describe('isTooEarly', () => { - test('should be false when a UCAN is active', () => { - const valid = getFixture('valid', 'UCAN is ready to be used') - expect(isTooEarly(valid.token)).toBe(false) - }) - - test('should be true when a UCAN is early', () => { - const invalid = getFixture('invalid', 'UCAN is not ready to be used') - expect(isTooEarly(invalid.token)).toBe(true) - }) -}) diff --git a/ucan-wasm/tests/ucan/verify.test.ts b/ucan-wasm/tests/ucan/verify.test.ts new file mode 100644 index 00000000..32d9ca76 --- /dev/null +++ b/ucan-wasm/tests/ucan/verify.test.ts @@ -0,0 +1,77 @@ +import assert from 'assert' +import { getFixture } from '../fixtures/index.js' + +export function runVerifyTests( + impl: { + runner?: { describe, it }, + ucan: { + isExpired: (token: string) => boolean + isTooEarly: (token: string) => boolean + checkSignature: (token: string) => Promise + validate: (token: string) => Promise + } + }) { + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { checkSignature, isExpired, isTooEarly, validate } = impl.ucan + + describe('validate', async () => { + it('should resolve on a valid a UCAN', async () => { + const validSignature = getFixture('valid', 'UCAN has a valid signature') + const unexpired = getFixture('valid', 'UCAN has not expired') + const active = getFixture('valid', 'UCAN is ready to be used') + + await assert.doesNotReject(validate(validSignature.token)) + await assert.doesNotReject(validate(unexpired.token)) + await assert.doesNotReject(validate(active.token)) + }) + + it('should be true when a UCAN is expired', async () => { + const invalidSignature = getFixture('invalid', 'UCAN has an invalid signature') + const expired = getFixture('invalid', 'UCAN has expired') + const early = getFixture('invalid', 'UCAN is not ready to be used') + + await assert.rejects(validate(invalidSignature.token)) + await assert.rejects(validate(expired.token)) + await assert.rejects(validate(early.token)) + }) + }) + + describe('checkSignature', async () => { + it('should resolve on a valid signature', async () => { + const valid = getFixture('valid', 'UCAN has a valid signature') + await assert.doesNotReject(checkSignature(valid.token)) + }) + + it('should throw on an invalid signature', async () => { + const invalid = getFixture('invalid', 'UCAN has an invalid signature') + await assert.rejects(checkSignature(invalid.token)) + }) + }) + + describe('isExpired', () => { + it('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN has not expired') + assert.equal(isExpired(valid.token), false) + }) + + it('should be true when a UCAN is expired', () => { + const invalid = getFixture('invalid', 'UCAN has expired') + assert(isExpired(invalid.token)) + }) + }) + + describe('isTooEarly', () => { + it('should be false when a UCAN is active', () => { + const valid = getFixture('valid', 'UCAN is ready to be used') + assert.equal(isTooEarly(valid.token), false) + }) + + it('should be true when a UCAN is early', () => { + const invalid = getFixture('invalid', 'UCAN is not ready to be used') + assert(isTooEarly(invalid.token)) + }) + }) +} diff --git a/ucan-wasm/tsconfig.json b/ucan-wasm/tsconfig.json new file mode 100644 index 00000000..a8b91537 --- /dev/null +++ b/ucan-wasm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "nodenext", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": ["tests/**/*.test.ts"] +} From 83deb1e5b4a0c8da659dec0e99d9fa164cbe5105 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 6 Jun 2023 13:42:36 -0700 Subject: [PATCH 13/18] feat: add opt-level for ucan-wasm release --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1baa9d07..ffda8a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,6 @@ members = [ # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information [profile.dev] split-debuginfo = "unpacked" + +[profile.release.package.ucan-wasm] +opt-level = "z" From 18d9a41dc7aa9823bca315255f0aefb1ba2ceebf Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Tue, 6 Jun 2023 18:56:23 -0700 Subject: [PATCH 14/18] feat: add decode function and test --- ucan-wasm/Cargo.toml | 4 + ucan-wasm/src/ucan/mod.rs | 1 + ucan-wasm/src/ucan/token.rs | 101 ++++++++++++++++++++++++++ ucan-wasm/src/ucan/verify.rs | 1 - ucan-wasm/tests/browser.test.ts | 10 ++- ucan-wasm/tests/fixtures/index.ts | 8 +- ucan-wasm/tests/fixtures/invalid.json | 10 ++- ucan-wasm/tests/fixtures/valid.json | 41 ++++++++++- ucan-wasm/tests/node.test.ts | 10 ++- ucan-wasm/tests/ucan/token.test.ts | 46 ++++++++++++ ucan-wasm/tests/ucan/verify.test.ts | 1 + 11 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 ucan-wasm/src/ucan/token.rs create mode 100644 ucan-wasm/tests/ucan/token.test.ts diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml index b9585bb8..2f4792f9 100644 --- a/ucan-wasm/Cargo.toml +++ b/ucan-wasm/Cargo.toml @@ -21,9 +21,13 @@ path = "src/lib.rs" # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. +base64 = "0.21" console_error_panic_hook = { version = "0.1", optional = true } instant = { version = "0.1", features = ["wasm-bindgen"] } js-sys = { version = "0.3", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.5.0" +serde_json = "1.0" tracing = "0.1" ucan = { path = "../ucan", version = "0.3" } ucan-key-support = { path = "../ucan-key-support", version = "0.1.5" } diff --git a/ucan-wasm/src/ucan/mod.rs b/ucan-wasm/src/ucan/mod.rs index 6bfa815b..8770866f 100644 --- a/ucan-wasm/src/ucan/mod.rs +++ b/ucan-wasm/src/ucan/mod.rs @@ -1,3 +1,4 @@ +pub mod token; pub mod verify; pub type JsResult = Result; diff --git a/ucan-wasm/src/ucan/token.rs b/ucan-wasm/src/ucan/token.rs new file mode 100644 index 00000000..8761a4c2 --- /dev/null +++ b/ucan-wasm/src/ucan/token.rs @@ -0,0 +1,101 @@ +use crate::ucan::JsResult; +use ::ucan::{capability::CapabilityIpld, Ucan as RsUcan}; +use base64::Engine; +use js_sys::Error; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_wasm_bindgen::Serializer; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const UCAN: &'static str = r#" +interface Ucan { + header: { + alg: string, + typ: string, + ucv: string + }, + payload: { + iss: string, + aud: string, + exp: number, + nbf?: number, + nnc?: string, + att: unknown[], + fct?: Record[], + prf?: string[] + } + signature: string +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Ucan")] + pub type Ucan; +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct ResolvedUcan { + header: Header, + payload: Payload, + signature: String, +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct Header { + alg: String, + typ: String, + ucv: String, +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct Payload { + iss: String, + aud: String, + exp: u64, + nbf: Option, + nnc: Option, + att: Vec, + fct: Option>, + prf: Option>, +} + +/// Decode a UCAN +#[wasm_bindgen(js_name = "decode")] +pub async fn decode(token: String) -> JsResult { + let ucan = RsUcan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + let header = Header { + alg: ucan.algorithm().into(), + typ: "JWT".into(), + ucv: ucan.version().into(), + }; + + let payload = Payload { + iss: ucan.issuer().into(), + aud: ucan.audience().into(), + exp: *ucan.expires_at(), + nbf: *ucan.not_before(), + nnc: ucan.nonce().clone(), + att: ucan.attenuation().to_vec(), + fct: ucan.facts().clone(), + prf: ucan.proofs().clone(), + }; + + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(ucan.signature()); + + let resolved = ResolvedUcan { + header, + payload, + signature, + }; + + let serializer = Serializer::new().serialize_maps_as_objects(true); + let value = resolved.serialize(&serializer).unwrap(); + + Ok(Ucan { obj: value }) +} diff --git a/ucan-wasm/src/ucan/verify.rs b/ucan-wasm/src/ucan/verify.rs index ad42a6e2..27f262b0 100644 --- a/ucan-wasm/src/ucan/verify.rs +++ b/ucan-wasm/src/ucan/verify.rs @@ -9,7 +9,6 @@ use ::ucan::{ use ::ucan_key_support::{ ed25519::bytes_to_ed25519_key, p256::bytes_to_p256_key, rsa::bytes_to_rsa_key, }; - use js_sys::Error; use wasm_bindgen::prelude::wasm_bindgen; diff --git a/ucan-wasm/tests/browser.test.ts b/ucan-wasm/tests/browser.test.ts index 0ea10e19..11abd640 100644 --- a/ucan-wasm/tests/browser.test.ts +++ b/ucan-wasm/tests/browser.test.ts @@ -1,5 +1,6 @@ -import init, { checkSignature, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' +import init, { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' import { runVerifyTests } from "./ucan/verify.test.js" +import { runTokenTests } from "./ucan/token.test.js" before(async () => { await init() @@ -13,3 +14,10 @@ runVerifyTests({ validate } }) + + +runTokenTests({ + ucan: { + decode + } +}) diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts index 709f5389..f626e11c 100644 --- a/ucan-wasm/tests/fixtures/index.ts +++ b/ucan-wasm/tests/fixtures/index.ts @@ -18,13 +18,13 @@ type Fixture = { exp: number | null nbf?: number nnc?: string - fct: Record[], - att: {with: string, can: string}[], + fct: Record[], + att: { with: string, can: string }[], prf: string[] }, + signature: string, validationErrors?: string[] - } - + }, } export function getFixture(expectation: Expectation, comment: string): Fixture { diff --git a/ucan-wasm/tests/fixtures/invalid.json b/ucan-wasm/tests/fixtures/invalid.json index 247b14da..6b6a0a09 100644 --- a/ucan-wasm/tests/fixtures/invalid.json +++ b/ucan-wasm/tests/fixtures/invalid.json @@ -16,6 +16,7 @@ "att": [], "prf": [] }, + "signature": "berK6gshRnkODI6WKghxRRQIGzDNwiicJN2oEhKSKsKPhISK0SNbSRDtUGumYJXEEdR68KibI_zbc_EyTMqRDQ", "validationErrors": [ "expExpired" ] @@ -39,6 +40,7 @@ "att": [], "prf": [] }, + "signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", "validationErrors": [ "nbfNotReady" ] @@ -46,7 +48,7 @@ }, { "comment": "UCAN has an invalid signature", - "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglC", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", "assertions": { "header": { "alg": "EdDSA", @@ -60,7 +62,11 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "W86QoxmgiE5pyDhOxqIUH5YMK7nff2_uqN4s28SBglFZ0ZJSOO2FFj_qgGwf4uNoZ9WbosCcorQX-FBfSvj0Dg", + "validationErrors": [ + "invalidSignature" + ] } } ] diff --git a/ucan-wasm/tests/fixtures/valid.json b/ucan-wasm/tests/fixtures/valid.json index 022d964b..881b0ce1 100644 --- a/ucan-wasm/tests/fixtures/valid.json +++ b/ucan-wasm/tests/fixtures/valid.json @@ -1,4 +1,36 @@ [ + { + "comment": "UCAN is valid", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT", + "ucv": "0.9.0-canary" + }, + "payload": { + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": 9246211200, + "fct": [ + { + "challenge": "abcdef" + } + ], + "att": [ + { + "can": "email/send", + "nb": null, + "with": "mailto:alice@email.com" + } + ], + "prf": [ + "bafkr4igf3sz7kj5dhxrdjufxvavkkinpk2i73i5psup6cxuvdym2jecw2e" + ] + }, + "signature": "nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg" + } + }, { "comment": "UCAN has not expired", "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImV4cCI6OTI0NjIxMTIwMCwiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOltdfQ.l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ", @@ -15,7 +47,8 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ" } }, { @@ -35,7 +68,8 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "-8duL-fCdG-2hEbe4F6hqE-g2Tf6II-jBzb8qKAbp41snlHvPYpvoPAC4HobmtTodFDQXdmI7u_mbQhesGHTAw" } }, { @@ -54,7 +88,8 @@ "fct": [], "att": [], "prf": [] - } + }, + "signature": "l-OlVJ8sNv6dHcROAL1wkMZ3JMCGx-o4F9-sSycMtEikj1J1DTlVNep1J5zKR6sEniyFa__8zwrWydtHZyglCQ" } } ] diff --git a/ucan-wasm/tests/node.test.ts b/ucan-wasm/tests/node.test.ts index 51c886c5..0dcdc176 100644 --- a/ucan-wasm/tests/node.test.ts +++ b/ucan-wasm/tests/node.test.ts @@ -1,6 +1,7 @@ import { describe, it } from 'vitest' -import { checkSignature, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { runTokenTests } from "./ucan/token.test.js" import { runVerifyTests } from "./ucan/verify.test.js" runVerifyTests({ @@ -12,3 +13,10 @@ runVerifyTests({ validate } }) + +runTokenTests({ + runner: { describe, it }, + ucan: { + decode + } +}) diff --git a/ucan-wasm/tests/ucan/token.test.ts b/ucan-wasm/tests/ucan/token.test.ts new file mode 100644 index 00000000..839b41dc --- /dev/null +++ b/ucan-wasm/tests/ucan/token.test.ts @@ -0,0 +1,46 @@ +import assert from 'assert' +import { getFixture } from '../fixtures/index.js' + +// The Ucan type is the same across browser and node environments +import type { Ucan } from '../../lib/browser/ucan_wasm.js' + +export function runTokenTests( + impl: { + runner?: { describe, it }, + ucan: { + decode: (token: string) => Promise + } + }) { + + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { decode } = impl.ucan + + describe('decode', async () => { + it('should decode a token', async () => { + const valid = getFixture('valid', 'UCAN is valid') + const ucan = await decode(valid.token) + + // Check header + assert.equal(ucan.header.alg, valid.assertions.header.alg) + assert.equal(ucan.header.typ, valid.assertions.header.typ) + assert.equal(ucan.header.ucv, valid.assertions.header.ucv) + + // Check payload + assert.equal(ucan.payload.iss, valid.assertions.payload.iss) + assert.equal(ucan.payload.aud, valid.assertions.payload.aud) + assert.equal(ucan.payload.exp, valid.assertions.payload.exp) + assert.equal(ucan.payload.nbf, valid.assertions.payload.nbf) + assert.equal(ucan.payload.nnc, valid.assertions.payload.nnc) + assert.deepEqual(ucan.payload.att, valid.assertions.payload.att) + assert.deepEqual(ucan.payload.fct, valid.assertions.payload.fct) + assert.deepEqual(ucan.payload.prf, valid.assertions.payload.prf) + + // Check signature + assert.equal(ucan.signature, valid.assertions.signature) + }) + }) + +} diff --git a/ucan-wasm/tests/ucan/verify.test.ts b/ucan-wasm/tests/ucan/verify.test.ts index 32d9ca76..1037ce9e 100644 --- a/ucan-wasm/tests/ucan/verify.test.ts +++ b/ucan-wasm/tests/ucan/verify.test.ts @@ -11,6 +11,7 @@ export function runVerifyTests( validate: (token: string) => Promise } }) { + // Use runner or fallback to implicit mocha implementations const describe = impl.runner?.describe ?? globalThis.describe const it = impl.runner?.it ?? globalThis.it From 8520742bad4de0f9ba4d5f3004a7292d27cf363e Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Thu, 8 Jun 2023 09:24:01 -0700 Subject: [PATCH 15/18] feat: add toCID function and tests --- ucan-wasm/Cargo.toml | 1 + ucan-wasm/src/ucan/cid.rs | 43 +++++++++++++++++++++++++++++++ ucan-wasm/src/ucan/mod.rs | 1 + ucan-wasm/tests/browser.test.ts | 12 +++++++-- ucan-wasm/tests/fixtures/cid.json | 13 ++++++++++ ucan-wasm/tests/fixtures/index.ts | 18 +++++++++++++ ucan-wasm/tests/node.test.ts | 18 +++++++++---- ucan-wasm/tests/ucan/cid.test.ts | 37 ++++++++++++++++++++++++++ 8 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 ucan-wasm/src/ucan/cid.rs create mode 100644 ucan-wasm/tests/fixtures/cid.json create mode 100644 ucan-wasm/tests/ucan/cid.test.ts diff --git a/ucan-wasm/Cargo.toml b/ucan-wasm/Cargo.toml index 2f4792f9..08817624 100644 --- a/ucan-wasm/Cargo.toml +++ b/ucan-wasm/Cargo.toml @@ -22,6 +22,7 @@ path = "src/lib.rs" # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. base64 = "0.21" +cid = "0.10" console_error_panic_hook = { version = "0.1", optional = true } instant = { version = "0.1", features = ["wasm-bindgen"] } js-sys = { version = "0.3", optional = true } diff --git a/ucan-wasm/src/ucan/cid.rs b/ucan-wasm/src/ucan/cid.rs new file mode 100644 index 00000000..909bbe93 --- /dev/null +++ b/ucan-wasm/src/ucan/cid.rs @@ -0,0 +1,43 @@ +use crate::ucan::JsResult; +use ::ucan::Ucan; +use cid::multihash::Code; +use js_sys::Error; +use wasm_bindgen::prelude::wasm_bindgen; + +/// Compute CID for a UCAN +/// +/// Hashers include SHA2-256, SHA2-512, SHA3-224 +/// SHA3-256, SHA3-384, SHA3-512, Keccak-224, Keccak-256, Keccak-384 +/// Keccak-512, BLAKE2b-256, BLAKE2b-512, BLAKE2s-128, and BLAKE3-256. +/// +/// Defaults to BLAKE3-256 hash function. +/// +#[wasm_bindgen(js_name = "toCID")] +pub async fn to_cid(token: String, hasher: Option) -> JsResult { + let ucan = Ucan::try_from(token).map_err(|e| Error::new(&format!("{e}")))?; + + let hasher_code = get_hasher_code(&hasher.unwrap_or(String::from("BLAKE3-256"))); + let cid = Ucan::to_cid(&ucan, hasher_code).map_err(|e| Error::new(&format!("{e}")))?; + + Ok(cid.into()) +} + +fn get_hasher_code(hasher: &str) -> Code { + match hasher { + "SHA2-256" => Code::Sha2_256, + "SHA2-512" => Code::Sha2_512, + "SHA3-224" => Code::Sha3_224, + "SHA3-256" => Code::Sha3_256, + "SHA3-384" => Code::Sha3_384, + "SHA3-512" => Code::Sha3_512, + "Keccak-224" => Code::Keccak224, + "Keccak-256" => Code::Keccak256, + "Keccak-384" => Code::Keccak384, + "Keccak-512" => Code::Keccak512, + "BLAKE2b-256" => Code::Blake2b256, + "BLAKE2b-512" => Code::Blake2b512, + "BLAKE2s-128" => Code::Blake2s128, + "BLAKE3-256" => Code::Blake3_256, + _ => Code::Blake3_256, + } +} diff --git a/ucan-wasm/src/ucan/mod.rs b/ucan-wasm/src/ucan/mod.rs index 8770866f..e63e2c2a 100644 --- a/ucan-wasm/src/ucan/mod.rs +++ b/ucan-wasm/src/ucan/mod.rs @@ -1,3 +1,4 @@ +pub mod cid; pub mod token; pub mod verify; diff --git a/ucan-wasm/tests/browser.test.ts b/ucan-wasm/tests/browser.test.ts index 11abd640..941bc24f 100644 --- a/ucan-wasm/tests/browser.test.ts +++ b/ucan-wasm/tests/browser.test.ts @@ -1,6 +1,7 @@ -import init, { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/browser/ucan_wasm.js' -import { runVerifyTests } from "./ucan/verify.test.js" +import init, { checkSignature, decode, isExpired, isTooEarly, toCID, validate } from '../lib/browser/ucan_wasm.js' +import { runCIDTests } from "./ucan/cid.test.js" import { runTokenTests } from "./ucan/token.test.js" +import { runVerifyTests } from "./ucan/verify.test.js" before(async () => { await init() @@ -21,3 +22,10 @@ runTokenTests({ decode } }) + +runCIDTests({ + runner: { describe, it }, + ucan: { + toCID + } +}) diff --git a/ucan-wasm/tests/fixtures/cid.json b/ucan-wasm/tests/fixtures/cid.json new file mode 100644 index 00000000..8c8e0f0b --- /dev/null +++ b/ucan-wasm/tests/fixtures/cid.json @@ -0,0 +1,13 @@ + +[ + { + "hasher": "SHA2-256", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "cid": "bafkreigzlstpwdjhxgjexfm3njsidz3iusyys3d2way35re3gbcghjzsqa" + }, + { + "hasher": "BLAKE3-256", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOlt7ImNhbiI6ImVtYWlsL3NlbmQiLCJuYiI6bnVsbCwid2l0aCI6Im1haWx0bzphbGljZUBlbWFpbC5jb20ifV0sImF1ZCI6ImRpZDprZXk6ejZNa3RhZlpUUkVqSmt2VjVtZkp4Y0xwTkJvVlB3RExoVHVNZzluZzdkWTR6TUFMIiwiZXhwIjo5MjQ2MjExMjAwLCJmY3QiOlt7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9XSwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3I0aWdmM3N6N2tqNWRoeHJkanVmeHZhdmtraW5wazJpNzNpNXBzdXA2Y3h1dmR5bTJqZWN3MmUiXX0.nTJl6kKrEKYzp6D4tTc-xYgNxH4urv8tfGU7so6ZIf5s86yMnb6bLpSPMeRchbOafVIy9vil9vjjYACzY1GvBg", + "cid": "bafkr4igg7afrwmgzugre2zxd62t3ycs73hto3pe24bavabroxbjpfam57y" + } +] diff --git a/ucan-wasm/tests/fixtures/index.ts b/ucan-wasm/tests/fixtures/index.ts index f626e11c..c380609d 100644 --- a/ucan-wasm/tests/fixtures/index.ts +++ b/ucan-wasm/tests/fixtures/index.ts @@ -1,6 +1,10 @@ +import cid from './cid.json' import invalid from './invalid.json' import valid from './valid.json' + +// VERIFICATION + type Expectation = 'valid' | 'invalid' type Fixture = { @@ -27,6 +31,7 @@ type Fixture = { }, } + export function getFixture(expectation: Expectation, comment: string): Fixture { let fixture @@ -38,3 +43,16 @@ export function getFixture(expectation: Expectation, comment: string): Fixture { return fixture } + + +// CID + +type CIDFixture = { + hasher: string + token: string + cid: string +} + +export function getCIDFixture(hasher: string): CIDFixture { + return cid.find(f => f.hasher === hasher) +} diff --git a/ucan-wasm/tests/node.test.ts b/ucan-wasm/tests/node.test.ts index 0dcdc176..fab3d904 100644 --- a/ucan-wasm/tests/node.test.ts +++ b/ucan-wasm/tests/node.test.ts @@ -1,16 +1,17 @@ import { describe, it } from 'vitest' -import { checkSignature, decode, isExpired, isTooEarly, validate } from '../lib/node/ucan_wasm.js' +import { checkSignature, decode, isExpired, isTooEarly, toCID, validate } from '../lib/node/ucan_wasm.js' +import { runCIDTests } from "./ucan/cid.test.js" import { runTokenTests } from "./ucan/token.test.js" import { runVerifyTests } from "./ucan/verify.test.js" runVerifyTests({ runner: { describe, it }, ucan: { - isExpired, - isTooEarly, - checkSignature, - validate + isExpired, + isTooEarly, + checkSignature, + validate } }) @@ -20,3 +21,10 @@ runTokenTests({ decode } }) + +runCIDTests({ + runner: { describe, it }, + ucan: { + toCID + } +}) diff --git a/ucan-wasm/tests/ucan/cid.test.ts b/ucan-wasm/tests/ucan/cid.test.ts new file mode 100644 index 00000000..a0b5ab13 --- /dev/null +++ b/ucan-wasm/tests/ucan/cid.test.ts @@ -0,0 +1,37 @@ +import assert from 'assert' +import { getCIDFixture } from '../fixtures/index.js' + +export function runCIDTests( + impl: { + runner?: { describe, it }, + ucan: { + toCID: (token: string, hasher?: string) => Promise + } + }) { + + // Use runner or fallback to implicit mocha implementations + const describe = impl.runner?.describe ?? globalThis.describe + const it = impl.runner?.it ?? globalThis.it + + const { toCID } = impl.ucan + + describe('toCID', async () => { + it('should compute CID for a UCAN using a SHA2-256 hasher', async () => { + const fixture = getCIDFixture('SHA2-256') + const cid = await toCID(fixture.token, 'SHA2-256') + assert.equal(cid, fixture.cid) + }) + + it('should compute CID for a UCAN using a BLAKE3-256 hasher', async () => { + const fixture = getCIDFixture('BLAKE3-256') + const cid = await toCID(fixture.token, 'BLAKE3-256') + assert.equal(cid, fixture.cid) + }) + + it('should compute CID for a UCAN deafulting to the BLAKE3-256 hasher', async () => { + const fixture = getCIDFixture('BLAKE3-256') + const cid = await toCID(fixture.token) + assert.equal(cid, fixture.cid) + }) + }) +} From 2270947e62b3b93a0d857ea736061a47abc02953 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Fri, 16 Jun 2023 10:20:15 -0700 Subject: [PATCH 16/18] Update README --- ucan-wasm/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ucan-wasm/README.md b/ucan-wasm/README.md index b42f99b9..bf65a8b2 100644 --- a/ucan-wasm/README.md +++ b/ucan-wasm/README.md @@ -1,11 +1,28 @@ ### Build +The build command output for `browser`, `node`, `deno`, and `workerd` targets to `ucan-wasm/lib`. + ``` -wasm-pack build --target web +npm run build ``` ### Test +The test command tests `node` and browser environments including `chromium`, `firefox`, and `webkit` +using headless browsers. + ``` npm run test ``` + +Testing can also be run for `node` only. + +``` +npm run test:node +``` + +The test commands runs `build` to ensure all targets are available for testing. + +### Reporting + +Test runs output test results and coverage as JSON artifacts to `ucan-wasm/tests/report`. From a3c8aefa69e5c134390c7cf518e2123fb3e89135 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Wed, 5 Jul 2023 11:06:42 -0700 Subject: [PATCH 17/18] feat: add "." entry point --- ucan-wasm/package.json | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index 32c46932..fbfc716e 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -17,20 +17,22 @@ "homepage": "https://github.com/ucan-wg/rs-ucan#readme", "type": "module", "exports": { - "deno": { - "types": "./lib/deno/ucan_wasm.d.ts", - "default": "./lib/deno/ucan_wasm.js" - }, - "node": { - "types": "./lib/node/ucan_wasm.d.ts", - "require": "./lib/node/ucan_wasm.js" - }, - "workerd": { - "types": "./lib/workerd/ucan_wasm.d.ts", - "default": "./lib/workerd/index.js" - }, - "types": "./lib/browser/ucan_wasm.d.ts", - "default": "./lib/browser/ucan_wasm.js" + ".": { + "deno": { + "types": "./lib/deno/ucan_wasm.d.ts", + "default": "./lib/deno/ucan_wasm.js" + }, + "node": { + "types": "./lib/node/ucan_wasm.d.ts", + "require": "./lib/node/ucan_wasm.js" + }, + "workerd": { + "types": "./lib/workerd/ucan_wasm.d.ts", + "default": "./lib/workerd/index.js" + }, + "types": "./lib/browser/ucan_wasm.d.ts", + "default": "./lib/browser/ucan_wasm.js" + } }, "scripts": { "build": "export PROFILE=dev && export TARGET_DIR=debug && npm run buildall", From bc0dcf39c79c8ac722b7d458dbb0e5ca8dbc1f56 Mon Sep 17 00:00:00 2001 From: Brian Ginsburg Date: Wed, 12 Jul 2023 13:27:57 -0700 Subject: [PATCH 18/18] feat: update package exports --- ucan-wasm/package.json | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ucan-wasm/package.json b/ucan-wasm/package.json index fbfc716e..dce7b879 100644 --- a/ucan-wasm/package.json +++ b/ucan-wasm/package.json @@ -17,21 +17,27 @@ "homepage": "https://github.com/ucan-wg/rs-ucan#readme", "type": "module", "exports": { + "./package.json": "./package.json", ".": { - "deno": { - "types": "./lib/deno/ucan_wasm.d.ts", - "default": "./lib/deno/ucan_wasm.js" + "browser": { + "types": "./lib/browser/ucan_wasm.d.ts", + "import": "./lib/browser/ucan_wasm.js" }, "node": { "types": "./lib/node/ucan_wasm.d.ts", - "require": "./lib/node/ucan_wasm.js" + "require": "./lib/node/ucan_wasm.js", + "import": "./lib/browser/ucan_wasm.js" + }, + "deno": { + "types": "./lib/deno/ucan_wasm.d.ts", + "import": "./lib/deno/ucan_wasm.js" }, "workerd": { "types": "./lib/workerd/ucan_wasm.d.ts", - "default": "./lib/workerd/index.js" + "import": "./lib/workerd/index.js" }, - "types": "./lib/browser/ucan_wasm.d.ts", - "default": "./lib/browser/ucan_wasm.js" + "import": "./lib/browser/ucan_wasm.js", + "require": "./lib/node/ucan_wasm.js" } }, "scripts": {