diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 437de666..00000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[target.'cfg(target_arch="wasm32")'] -runner = "wasm-bindgen-test-runner" diff --git a/.envrc b/.envrc index 3550a30f..c4b17d79 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake +use_flake diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 63541e1f..fcc837a7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default -* @cdata @ucan-wg/fission +* @ucan-wg diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8711a73a..e667a230 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,9 +30,10 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: -1. Use function '...' -1. Run command '...' -2. See error +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error **Expected behavior** @@ -42,12 +43,18 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. -**Environment (please fill in relevant information):** +**Desktop (please complete the following information):** - - OS: [e.g. macOS] - - Version [e.g. 13.3.1] - - Browser: [e.g. Chrome 113.0.5672.126, Firefox 113.01] - - Rust Version [1.68.2] + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] **Additional context** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b278a820..d4b1e629 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,26 +7,13 @@ version: 2 updates: - package-ecosystem: "cargo" - directory: "/ucan" - commit-message: - prefix: "chore" - include: "scope" - target-branch: "main" - schedule: - interval: "weekly" - labels: - - "chore" - - - package-ecosystem: "cargo" - directory: "/ucan-key-support" + directory: "/" commit-message: prefix: "chore" include: "scope" target-branch: "main" schedule: interval: "weekly" - labels: - - "chore" - package-ecosystem: "github-actions" directory: "/" @@ -36,5 +23,3 @@ updates: target-branch: "main" schedule: interval: "weekly" - labels: - - "chore" diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 00000000..24eeb6e0 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,17 @@ +name: 🛡 Audit-Check + +on: + schedule: + - cron: '0 0 * * *' + +jobs: + security-audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Run Audit-Check + uses: rustsec/audit-check@v1.3.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 00000000..f7b2cc74 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,58 @@ +name: 📈 Benchmark + +on: + push: + branches: [ main ] + + pull_request: + branches: [ '**' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + benchmark: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Rust Toolchain + uses: actions-rs/toolchain@v1 + with: + override: true + toolchain: stable + + - name: Cache Project + uses: Swatinem/rust-cache@v2 + + - name: Run Benchmark + run: cargo bench --features test_utils -- --output-format bencher | tee output.txt + + - name: Upload Benchmark Result Artifact + uses: actions/upload-artifact@v3 + with: + name: bench_result + path: output.txt + + - name: Create gh-pages Branch + uses: peterjgrainger/action-create-branch@v2.4.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + branch: gh-pages + + - name: Store Benchmark Result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: Rust Benchmark + tool: 'cargo' + output-file-path: output.txt + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: ${{ github.event_name == 'push' && github.repository == 'ucan-wg/ucan' && github.ref == 'refs/heads/main' }} + alert-threshold: '200%' + comment-on-alert: true + fail-on-alert: true + alert-comment-cc-users: '@ucan-wg' diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yml similarity index 77% rename from .github/workflows/coverage.yaml rename to .github/workflows/coverage.yml index a9ad95be..db763fcb 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yml @@ -5,7 +5,11 @@ on: branches: [ main ] pull_request: - branches: [ '*' ] + branches: [ '**' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: coverage: @@ -13,7 +17,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust Toolchain uses: actions-rs/toolchain@v1 @@ -29,10 +33,10 @@ jobs: - name: Generate Code coverage env: CARGO_INCREMENTAL: '0' - LLVM_PROFILE_FILE: "rs-ucan-%p-%m.profraw" - RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' - run: cargo test --all + LLVM_PROFILE_FILE: "ucan-%p-%m.profraw" + RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' + RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' + run: cargo test --all-features - name: Install grcov run: "curl -L https://github.com/mozilla/grcov/releases/download/v0.8.12/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf -" @@ -51,7 +55,8 @@ jobs: - name: Upload to codecov.io uses: codecov/codecov-action@v3 + continue-on-error: true with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + fail_ci_if_error: false files: lcov.info diff --git a/.github/workflows/nix_build.yml b/.github/workflows/nix_build.yml deleted file mode 100644 index 23f2cf5a..00000000 --- a/.github/workflows/nix_build.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: 📦 Nix Build - -on: - push: - branches: [ main ] - - pull_request: - branches: [ '**' ] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - run-checks: - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v4 - - - name: Cache Magic - uses: DeterminateSystems/magic-nix-cache-action@v2 - - - name: Check Nix flake inputs - uses: DeterminateSystems/flake-checker-action@v5 - with: - ignore-missing-flake-lock: false - fail-mode: true - - - name: Nix Build - run: | - nix develop --show-trace -c irust --version - nix develop --show-trace -c rustc --version diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yml similarity index 68% rename from .github/workflows/release.yaml rename to .github/workflows/release.yml index 63a74b08..58388fb1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' outputs: - releases_created: ${{ steps.release.outputs['ucan--release_created'] || steps.release.outputs['ucan-key-support--release_created'] }} + release_created: ${{ steps.release.outputs.release_created }} steps: - name: Run release-please @@ -45,13 +45,12 @@ jobs: permissions: contents: write - pull-requests: write - if: ${{ needs.release-please.outputs.releases_created || github.event.inputs.force-publish }} - steps: + if: ${{ needs.release-please.outputs.release_created || github.event.inputs.force-publish }} + steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Cache Project uses: Swatinem/rust-cache@v2 @@ -63,20 +62,12 @@ jobs: profile: minimal toolchain: stable - - name: Install Cargo Workspaces - env: - RUSTFLAGS: '-Copt-level=1' - uses: actions-rs/cargo@v1 - with: - args: --force cargo-workspaces - command: install - - name: Verify Publishing of crate - uses: katyo/publish-crates@v1 + uses: katyo/publish-crates@v2 with: dry-run: true - name: Cargo Publish to crates.io - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo workspaces publish --from-git --allow-dirty + uses: katyo/publish-crates@v2 + with: + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/run_test_suite.yaml b/.github/workflows/tests_and_checks.yml similarity index 52% rename from .github/workflows/run_test_suite.yaml rename to .github/workflows/tests_and_checks.yml index 16e20c77..05357ca4 100644 --- a/.github/workflows/run_test_suite.yaml +++ b/.github/workflows/tests_and_checks.yml @@ -5,7 +5,7 @@ on: branches: [ main ] pull_request: - branches: [ '*' ] + branches: [ '**' ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,38 +20,58 @@ jobs: rust-toolchain: - stable - nightly + # minimum version + - 1.67 steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Smarter caching action, speeds up build times compared to regular cache: # https://github.com/Swatinem/rust-cache - name: Cache Project uses: Swatinem/rust-cache@v2 + # Widely adopted suite of Rust-specific boilerplate actions, especially + # toolchain/cargo use: https://actions-rs.github.io/ - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@master + uses: actions-rs/toolchain@v1 with: + override: true + components: rustfmt, clippy toolchain: ${{ matrix.rust-toolchain }} - components: clippy, rustfmt - name: Check Format - run: cargo +${{ matrix.rust-toolchain }} fmt --all -- --check + uses: actions-rs/cargo@v1 + with: + args: --all -- --check + command: fmt + toolchain: ${{ matrix.rust-toolchain }} - name: Run Linter - run: cargo +${{ matrix.rust-toolchain }} clippy --all -- -D warnings + uses: actions-rs/cargo@v1 + with: + args: --all -- -D warnings + command: clippy + toolchain: ${{ matrix.rust-toolchain }} - - name: Install Cargo Audit + # Check for security advisories + - name: Check Advisories if: ${{ matrix.rust-toolchain == 'stable' }} - run: cargo install --force cargo-audit + uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check advisories + continue-on-error: true - - name: Run Audit on Deps + # Audit licenses, unreleased crates, and unexpected duplicate versions. + - name: Check Bans, Licenses, and Sources if: ${{ matrix.rust-toolchain == 'stable' }} - run: cargo-audit audit + uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check bans licenses sources # Only "test" release build on push event. - name: Test Release - if: ${{ matrix.rust-toolchain == 'stable' && github.event_name == 'push' }} + if: ${{ matrix.rust-toolchain == 'stable' && github.event_name == 'push' }} run: cargo build --release run-tests: @@ -64,7 +84,7 @@ jobs: - nightly steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Environment Packages run: | @@ -75,22 +95,10 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Install Rust Toolchain - uses: dtolnay/rust-toolchain@master + uses: actions-rs/toolchain@v1 with: + override: true toolchain: ${{ matrix.rust-toolchain }} - name: Run Tests - run: cargo test --all - - - name: Install Rust/WASM Test Dependencies - run: | - rustup target install wasm32-unknown-unknown - cargo install toml-cli - WASM_BINDGEN_VERSION=`toml get ./Cargo.lock . | jq '.package | map(select(.name == "wasm-bindgen"))[0].version' | xargs echo` - cargo install wasm-bindgen-cli --vers "$WASM_BINDGEN_VERSION" - - - name: Setup Chrome and Chromedriver - uses: nanasess/setup-chromedriver@v2 - - - name: Run Rust Headless Browser Tests - run: CHROMEDRIVER=/usr/local/bin/chromedriver cargo test --target wasm32-unknown-unknown + run: cargo test --all-features diff --git a/.gitignore b/.gitignore index c9131f0a..f408d360 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,22 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ Cargo.lock -**/target/ - -**/.DS_Store -README.html +# These are backup files generated by rustfmt +**/*.rs.bk +# Ignore local environment settings +.envrc.custom .direnv +# Other files + dirs +private +*.temp +*.tmp +.history +.DS_Store +.wireit +.nyc_output +tests/report +node_modules dist -bundle -lib diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14f2c3fb..5145061a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,17 @@ # See https://pre-commit.com for more information +# pre-commit install +# pre-commit install --hook-type commit-msg +exclude: ^(LICENSE|LICENSE*) repos: - repo: local hooks: - # allow for crate import granularity: - # https://github.com/rust-lang/rustfmt/issues/4991 - id: fmt name: fmt description: Format rust files. - entry: cargo +nightly fmt + entry: cargo fmt language: system types: [rust] - args: ["--all", "--", "--check"] + args: ["--", "--check"] - id: cargo-check name: cargo check description: Check the package for errors. @@ -23,15 +24,21 @@ repos: description: Lint via clippy entry: cargo clippy language: system - args: ["--workspace", "--", "-D", "warnings"] + args: ["--", "-D", "warnings"] types: [rust] pass_filenames: false + - id: alejandra + name: alejandra + description: Format nix files. + entry: alejandra + files: \.nix$ + language: system - repo: https://github.com/DevinR528/cargo-sort rev: v1.0.9 hooks: - id: cargo-sort - args: ["--workspace"] + args: [] - repo: https://github.com/compilerla/conventional-pre-commit rev: v2.1.1 @@ -49,11 +56,8 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - exclude: ^catalog-info.yaml - id: check-json - exclude: ^tests/data/ - id: check-added-large-files - id: detect-private-key - exclude: ^tests/data/ - id: check-executables-have-shebangs - id: check-toml diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0be28311..466df71c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,3 @@ { - "ucan": "0.4.0", - "ucan-key-support": "0.1.7" + ".": "0.1.0" } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..428e8687 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,108 @@ +# Code of Conduct + +**TL;DR Be kind, inclusive, and considerate.** + +In the interest of fostering an open, inclusive, and welcoming environment, all +members, contributors, and maintainers interacting within our online community +(including Discord, Discourse, etc.), on affiliated projects and repositories +(including issues, pull requests, and discussions on Github), and/or involved +with associated events pledge to accept and observe the following Code of +Conduct. + +As members, contributors, and maintainers, we pledge to make participation in +our projects and community a harassment-free experience, ensuring a safe +environment for all, regardless of background, gender, gender identity and +expression, age, sexual orientation, disability, physical appearance, body size, +race, ethnicity, religion (or lack thereof), or any other dimension of +diversity. + +Sexual language and imagery will not be accepted in any way. Be kind to others. +Do not insult or put down people within the community. Behave professionally. +Remember that harassment and sexist, racist, or exclusionary jokes are not +appropriate in any form. Participants violating these rules may be sanctioned or +expelled from the community and related projects. + +## Spelling it out. + +Harassment includes offensive verbal comments or actions related to or involving + +- background +- gender +- gender identity and expression +- age +- sexual orientation +- disability +- physical appearance +- body size +- race +- ethnicity +- religion (or lack thereof) +- economic status +- geographic location +- technology choices +- sexual imagery +- deliberate intimidation +- violence and threats of violence +- stalking +- doxing +- inappropriate or unwelcome physical contact in public spaces +- unwelcomed sexual attention +- influencing unacceptable behavior +- any other dimension of diversity + +## Our Responsibilities + +Maintainers of the community and associated projects are not only subject to the +anti-harassment policy, but also responsible for executing the policy, +moderating related forums, and for taking appropriate and fair corrective action +in response to any instances of unacceptable behavior that breach the policy. + +Maintainers have the right to remove and reject comments, threads, commits, +code, documentation, pull requests, issues, and contributions not aligned with +this Code of Conduct. + +## Scope + +This Code of Conduct applies within all project and community spaces, as well as +in any public spaces where an individual representing the community is involved. +This covers + +- Interactions on the Github repository, including discussions, issues, pull + requests, commits, and wikis +- Interactions on any affiliated Discord, Slack, IRC, or related online + communities and forums like Discourse, etc. +- Any official project emails and social media posts +- Individuals representing the community at public events like meetups, talks, + and presentations + +## Enforcement + +All instances of abusive, harassing, or otherwise unacceptable behavior should +be reported by contacting the project and community maintainers at +[quinn@fission.codes][support-email]. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. + +Maintainers of the community and associated projects are obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Anyone asked to stop abusive, harassing, or otherwise unacceptable behavior are +expected to comply immediately and accept the response decided on by the +maintainers of the community and associated projects. + +## Need help? + +If you are experiencing harassment, witness an incident or have concerns about +content please contact us at [quinn@fission.codes][support-email]. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant, v2.1][contributor-cov], +among other sources like [!!con’s Code of Conduct][!!con] and +[Mozilla’s Community Participation Guidelines][mozilla]. + +[!!con]: https://bangbangcon.com/conduct.html +[contributor-cov]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ +[mozilla]: https://www.mozilla.org/en-US/about/governance/policies/participation/ +[support-email]: mailto:quinn@fission.codes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..397b0e3f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,151 @@ +# Contributing to ucan + +We welcome everyone to contribute what and where they can. Whether you are brand +new, just want to contribute a little bit, or want to contribute a lot there is +probably something you can help out with. Check out our +[good first issues][good-first-issues] label for in the issues tab to see a list +of issue that good for those new to the project. + +## Where to Get Help + +The main way to get help is on our [discord server](https://discord.gg/4UdeQhw7fv). +Though, this guide should help you get started. It may be slightly lengthy, but it's +designed for those who are new so please don't let length intimidate you. + +## Code of Conduct + +Please be kind, inclusive, and considerate when interacting when interacting +with others and follow our [code of conduct](./CODE_OF_CONDUCT.md). + +## How to Contribute + +If the code adds a feature that is not already present in an issue, you can +create a new issue for the feature and add the pull request to it. If the code +adds a feature that is not already present in an issue, you can create a new +issue for the feature and add the pull request to it. + +### Contributing by Adding a Topic for Discussion + +#### Issues + +If you have found a bug and would like to report it or if you have a feature +that you feel we should add, then we'd love it if you opened an issue! ❤️ +Before you do, please search the other issues to avoid creating a duplicate +issue. + +To submit a new issue just hit the issue button and a choice between two +templates should appear. Then, follow along with the template you chose. If you +don't know how to fill in all parts of the template go ahead and skip those +parts. You can edit the issue later. + +#### Discussion + +If you have a new discussion you want to start but it isn't a bug or feature +add, then you can start a [GitHub discussion][gh-discussions]. Some examples of +what kinds of things that are good discussion topics can include, but are not +limited to the following: + +- Community announcements and/or asking the community for feedback +- Discussing a new release +- Asking questions, Q&A that isn't for sure a bug report + +### Contributing through Code + +In order to contribute through code follow the steps below. Note that you don't +need to be the best programmer to contribute. Our discord is open for questions. + + 1. **Pick a feature** you would like to add or a bug you would like to fix + - If you wish to contribute but what you want to fix/add is not already + covered in an existing issue, please open a new issue. + + 2. **Discuss** the issue with the rest of the community + - Before you write any code, it is recommended that you discuss your + intention to write the code on the issue you are attempting to edit. + - This helps to stop you from wasting your time duplicating the work of + others that maybe working on the same issue; at the same time. + - This step also allows you to get helpful pointers on the community on some + problems they may have encountered on similar issues. + + 3. **Fork** the repository + - A fork creates a copy of the code on your Github, so you can work on it + separately from everyone else. + - You can learn more about forking [here][forking]. + + 4. Ensure that you have **commit signing** enabled + - This ensures that the code you submit was committed by you and not someone + else who claims to be you. + - You can learn more about how to setup commit signing [here][commit-signing]. + - If you have already made some commits that you wish to put in a pull + request without signing them, then you can follow [this guide][post-signing] + on how to fix that. + + 5. **Clone** the repository to your local computer + - This puts a copy of your fork on your computer so you can edit it + - You can learn more about cloning repositories [here][git-clone]. + + 6. **Build** the project + - For a detailed look on how to build ucan look at our + [README file](./README.md). + + 7. **Start writing** your code + - Open up your favorite code editor and make the changes that you wanted to + make to the repository. + - Make sure to test your code with the test command(s) found in our + [README file](./README.md). + + 8. **Write tests** for your code + - If you are adding a new feature, you should write tests that ensure that + if someone make changes to the code it cannot break your new feature + without breaking the test. + - If your code adds a new feature, you should also write at least one + documentation test. The documentation test's purpose is to demonstrate and + document how to use the API feature. + - If your code fixes a bug, you should write tests that ensure that if + someone makes code changes in the future the bug does not re-emerge + without breaking test. + - Please create integration tests, if the addition is large enough to + warrant them, and unit tests. + * Unit tests are tests that ensure the functionality of a single + function or small section of code. + * Integration tests test large large sections of code. + * Read more about the differences [here][unit-and-integration]. + - For more information on test organization, take a look [here][test-org]. + + 9. Ensure that the code that you made follows our Rust **coding guidelines** + - You can find a list of some Rust guidelines [here][rust-style-guide]. This + is a courtesy to the programmers that come after you. The easier your code + is to read, the easier it will be for the next person to make modifications. + - If you find it difficult to follow the guidelines or if the guidelines or + unclear, please reach out to us through our discord linked above, or you + can just continue and leave a comment at the pull request stage. + + 10. **Commit and Push** your code + - This sends your changes to your repository branch. + - You can learn more about committing code [here][commiting-code] and + pushing it to a remote repository [here][push-remote]. + - We use conventional commits for the names and description of commits. + You can find out more about them [here][conventional-commits]. + + 11. The final step is to create **pull request** to our main branch 🎉 + - A pull request is how you merge the code you just worked so hard on with + the code everyone else has access to. + - Once you have submitted your pull request, we will review your code and + check to make sure the code implements the feature or fixes the bug. We + may leave some feedback and suggest edits. You can make the changes we + suggest by committing more code to your fork. + - You can learn more about pull requests [here][prs]. + + +[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[commiting-code]: https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project +[commit-signing]: https://www.freecodecamp.org/news/what-is-commit-signing-in-git/ +[forking]: https://docs.github.com/en/get-started/quickstart/fork-a-repo +[gh-discussions]: https://docs.github.com/en/discussions +[git-clone]: https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository +[good-first-issues]: [https://build.prestashop-project.org/news/a-definition-of-the-good-first-issue-label/] +[post-signing]: https://dev.to/jmarhee/signing-existing-commits-with-gpg-5b58 +[prs]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests +[push-remote]: https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository +[rust-style-guide]: https://rust-lang.github.io/api-guidelines/about.html +[test-org]: https://doc.rust-lang.org/book/ch11-03-test-organization.html +[unit-and-integration]: https://www.geeksforgeeks.org/difference-between-unit-testing-and-integration-testing/ diff --git a/Cargo.toml b/Cargo.toml index 6b73d443..648b98f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,151 @@ -[workspace] -members = [ - "ucan", - "ucan-key-support", +[package] +name = "ucan" +version = "0.2.0" +description = "Rust implementation of UCAN" +keywords = ["capabilities", "authorization", "ucan"] +categories = [] +include = ["/src", "/examples", "/benches", "README.md", "LICENSE"] +license = "Apache-2.0" +readme = "README.md" +edition = "2021" +rust-version = "1.75" +documentation = "https://docs.rs/ucan" +repository = "https://github.com/ucan-wg/rs-ucan" +authors = [ + "Quinn Wilton ", + "Brooklyn Zelenka " ] -resolver = "2" +[lib] +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" +bench = false +[[bench]] +name = "a_benchmark" # FIXME rename +harness = false +required-features = ["test_utils"] + +[[example]] +name = "counterparts" +path = "examples/counterparts.rs" + +[dependencies] + +# Docs +aquamarine = { version = "0.5", optional = true } + +# Encoding +multibase = "0.9" +base64 = "0.21" +nom = "7.1" +nom-unicode = "0.3" + +# Crypto +blst = { version = "0.3.11", optional = true, default-features = false } + +# Web Stack +did_url = "0.1" +ecdsa = { version = "0.16.8", features = ["alloc"], optional = true, default-features = false } +ed25519-dalek = { version = "2.0.0", features = ["rand_core"], optional = true } + +# Code Convenience +derive_builder = "0.20" +enum-as-inner = "0.6" +getrandom = { version = "0.2", features = ["js", "rdrand"] } +k256 = { version = "0.13.1", features = ["ecdsa"], optional = true, default-features = false } + +# Interplanetary Stack +libipld = { version = "0.16", optional = true } +libipld-cbor = "0.16" +libipld-core = { version = "0.16", features = ["serde-codec"] } +multihash = { version = "0.18" } +nonempty = { version = "0.9" } +p256 = { version = "0.13.2", features = ["alloc", "ecdsa"], optional = true, default-features = false } +p384 = { version = "0.13.0", features = ["alloc", "ecdsa"], optional = true, default-features = false } +p521 = { version = "0.13.3", features = ["alloc", "ecdsa", "getrandom"], optional = true, default-features = false } +proptest = { version = "1.1", optional = true } +proptest-derive = { version = "0.4", optional = true } +rsa = { version = "0.9.6", features = ["sha2", "std"], optional = true, default-features = false } +serde = { version = "1.0.188", features = ["derive"] } +serde_derive = "1.0" +signature = { version = "2.1.0", features = ["alloc"] } +thiserror = "1.0" +unsigned-varint = "0.7.2" +url = { version = "2.5", features = ["serde"] } +web-time = "0.2.3" +# FIXME actually use? async-signature = "0.4.0" + +# FIXME also have a wasi target +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = { version = "0.1" } +js-sys = { version = "0.3" } +serde-wasm-bindgen = "0.6" +wasm-bindgen = "0.2" +wasm-bindgen-derive = "0.2" +# wasm-bindgen-futures = { version = "0.4" } +web-sys = { version = "0.3", features = ["Crypto", "CryptoKey", "CryptoKeyPair", "SubtleCrypto"] } + +[dev-dependencies] +assert_matches = "1.5" +libipld = "0.16" +pretty_assertions = "1.4" +rand = "0.8" +testresult = "0.3" +test-log = "0.2" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +criterion = "0.4" +proptest = { version = "*" } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +criterion = { version = "0.4", default-features = false } +proptest = { version = "*", default-features = false, features = ["std"] } +wasm-bindgen-test = "0.2" + +[features] +default = [ + "es256", + "es256k", + "es384", + "es512", + "rs256", + "rs512", + "eddsa", + "bls", + + "ability-preset", + + # FIXME temp while developing + # "test_utils", +] + +test_utils = ["dep:proptest", "dep:proptest-derive", "dep:libipld"] + +eddsa = ["dep:ed25519-dalek"] +es256 = ["dep:p256"] +es256k = ["dep:k256"] +es384 = ["dep:p384"] +es512 = ["dep:ecdsa", "dep:p521"] +ps256 = ["dep:rsa"] +rs256 = ["dep:rsa"] +rs512 = ["dep:rsa"] +bls = ["dep:blst"] + +ability-preset = ["ability-crud", "ability-msg", "ability-wasm"] +ability-crud = [] +ability-msg = [] +ability-wasm = [] + +mermaid_docs = ["aquamarine"] + +[package.metadata.docs.rs] +all-features = true +# +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] +cargo-args = ["--features='mermaid_docs'"] +# # Speedup build on macOS # See https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#splitting-debug-information [profile.dev] diff --git a/LICENSE b/LICENSE index 261eeb9e..aaa61a98 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Ucan Wg Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index dd70d2c9..842d3fe6 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,108 @@
- - rs-ucan Logo + + ucan Logo -

rs-ucan

+

ucan

- UCAN - Crate Information + Crate - - Code Coverage + + Code Coverage - - Build Status + + Build Status - + License Docs - + Discord

-This is a Rust library to help the next generation of web applications make use -of UCANs in their authorization flows. To learn more about UCANs and how you -might use them in your application, visit [https://ucan.xyz][ucan website] or -read the [spec][spec]. +
:warning: Work in progress :warning:
-## Outline +## Usage -- [Crates](#crates) -- [Building the Project](#building-the-project) -- [Testing the Project](#testing-the-project) -- [Contributing](#contributing) -- [Getting Help](#getting-help) -- [License](#license) +Add the following to the `[dependencies]` section of your `Cargo.toml` file: -## Crates +```toml +ucan = "1.0.0-rc.1" +``` -- [ucan](https://github.com/ucan-wg/rs-ucan/tree/main/ucan) -- [ucan-key-support](https://github.com/ucan-wg/rs-ucan/tree/main/ucan-key-support) +## Testing the Project -## Building the Project +Run tests -- Clone the repository. +| Nix | Cargo | +|------------|--------------| +| `test:all` | `cargo test` | - ```bash - git clone https://github.com/ucan-wg/rs-ucan.git - ``` +## Benchmarking the Project -- Change directory +For benchmarking and measuring performance, this project leverages +[Criterion] and a `test_utils` feature flag +for integrating [proptest] within the the suite for working with +[strategies] and sampling from randomly generated values. - ```bash - cd rs-ucan - ``` +## Benchmarks -- Build the project +| Nix | Cargo | +|---------|-------------------------------------| +| `bench` | `cargo bench --features test_utils` | - ```bash - cargo build - ``` +## Contributing -## Testing the Project +:balloon: We're thankful for any feedback and help in improving our project! +We have a [contributing guide][CONTRIBUTING] to help you get involved. We +also adhere to our [Code of Conduct]. -- Run tests +### Nix - ```bash - cargo test - ``` +This repository contains a [Nix flake] that initiates both the Rust +toolchain set in [`rust-toolchain.toml`] and a [pre-commit hook]. It also +installs helpful cargo binaries for development. -## Contributing +Please install [Nix] to get started. We also recommend installing [direnv]. + +Run `nix develop` or `direnv allow` to load the `devShell` flake output, +according to your preference. + +The Nix shell also includes several helpful shortcut commands. +You can see a complete list of commands via the `menu` command. + +### Formatting + +For formatting Rust in particular, we automatically format on `nightly`, as it +uses specific nightly features we recommend by default. ### Pre-commit Hook -This library recommends using [pre-commit][pre-commit] for running pre-commit +This project recommends using [pre-commit] for running pre-commit hooks. Please run this before every commit and/or push. -- Once installed, Run `pre-commit install` to setup the pre-commit hooks - locally. This will reduce failed CI builds. - If you are doing interim commits locally, and for some reason if you _don't_ want pre-commit hooks to fire, you can run `git commit -a -m "Your message here" --no-verify`. +### Recommended Development Flow + +- We recommend leveraging [cargo-watch][cargo-watch], + [`cargo-expand`] and [IRust] for Rust development. +- We recommend using [cargo-udeps][cargo-udeps] for removing unused dependencies + before commits and pull-requests. + ### Conventional Commits -This library *lightly* follows the -[Conventional Commits convention][commit-spec-site] to help explain +This project *lightly* follows the [Conventional Commits +convention][commit-spec-site] to help explain commit history and tie in with our release process. The full specification can be found [here][commit-spec]. We recommend prefixing your commits with a type of `fix`, `feat`, `docs`, `ci`, `refactor`, etc..., structured like so: @@ -106,18 +117,51 @@ a type of `fix`, `feat`, `docs`, `ci`, `refactor`, etc..., structured like so: ## Getting Help -For usage questions, usecases, or issues reach out to us in our `rs-ucan` -[Discord channel](https://discord.gg/3EHEQ6M8BC). +For usage questions, usecases, or issues reach out to us in the [UCAN Discord]. + +We would be happy to try to answer your question or try opening a new issue on GitHub. -We would be happy to try to answer your question or try opening a new issue on -Github. +## External Resources + +These are references to specifications, talks and presentations, etc. ## License -This project is licensed under the [Apache License 2.0](https://github.com/ucan-wg/rs-ucan/blob/main/LICENSE). +This project is [licensed under the Apache License 2.0][LICENSE], or +[http://www.apache.org/licenses/LICENSE-2.0][Apache]. + + + +[Benchmarking the Project]: #benchmarking-the-project +[Contributing]: #contributing +[External Resources]: #external-resources +[Getting Help]: #getting-help +[License]: #license +[Testing the Project]: #testing-the-project +[Usage]: #usage +[pre-commit hook]: #pre-commit-hook + + + +[CONTRIBUTING]: ./CONTRIBUTING.md +[LICENSE]: ./LICENSE +[Code of Conduct]: ./CODE_OF_CONDUCT.md +[`rust-toolchain.toml`]: ./rust-toolchain.toml + + +[Apache]: https://www.apache.org/licenses/LICENSE-2.0 +[`cargo-expand`]: https://github.com/dtolnay/cargo-expand +[`cargo-udeps`]: https://github.com/est31/cargo-udeps +[`cargo-watch`]: https://github.com/watchexec/cargo-watch [commit-spec]: https://www.conventionalcommits.org/en/v1.0.0/#specification [commit-spec-site]: https://www.conventionalcommits.org/ +[Criterion]: https://github.com/bheisler/criterion.rs +[direnv]:https://direnv.net/ +[IRust]: https://github.com/sigmaSd/IRust +[Nix]:https://nixos.org/download.html +[Nix flake]: https://nixos.wiki/wiki/Flakes [pre-commit]: https://pre-commit.com/ -[spec]: https://github.com/ucan-wg/spec -[ucan website]: https://ucan.xyz +[proptest]: https://github.com/proptest-rs/proptest +[strategies]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html +[UCAN Discord]: https://discord.gg/4UdeQhw7fv diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a7a3276b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +## Report a security issue or vulnerability + +The `ucan` team welcomes security reports and is committed to +providing prompt attention to security issues. Security issues should be +reported privately via [quinn@fission.codes][support-email]. Security issues should +not be reported via the public GitHub Issue tracker. + +## Security advisories + +The project team is committed to transparency in the security issue disclosure +process. The ucan team announces security advisories through our +Github respository's [security portal][sec-advisories] and and the +[RustSec advisory database][rustsec-db]. + +[rustsec-db]: https://github.com/RustSec/advisory-db +[sec-advisories]: https://github.com/ucan-wg/ucan/security/advisories +[support-email]: mailto:quinn@fission.codes diff --git a/assets/a_logo.png b/assets/a_logo.png new file mode 100644 index 00000000..f86c0ffb Binary files /dev/null and b/assets/a_logo.png differ diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 834b13ce..00000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/benches/a_benchmark.rs b/benches/a_benchmark.rs new file mode 100644 index 00000000..fccc419e --- /dev/null +++ b/benches/a_benchmark.rs @@ -0,0 +1,16 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +pub fn add_benchmark(c: &mut Criterion) { + let mut rvg = ucan::test_utils::Rvg::deterministic(); + let int_val_1 = rvg.sample(&(0..100i32)); + let int_val_2 = rvg.sample(&(0..100i32)); + + c.bench_function("add", |b| { + b.iter(|| { + ucan::add(int_val_1, int_val_2); + }) + }); +} + +criterion_group!(benches, add_benchmark); +criterion_main!(benches); diff --git a/codecov.yaml b/codecov.yml similarity index 75% rename from codecov.yaml rename to codecov.yml index c80348aa..1227d5f8 100644 --- a/codecov.yaml +++ b/codecov.yml @@ -1,6 +1,7 @@ ignore: - - "ucan/src/tests/*" - - "ucan-key-support/src/fixtures/*" + - "tests" + - "benches" + - "examples" comment: layout: "reach, diff, flags, files" diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..c7499506 --- /dev/null +++ b/deny.toml @@ -0,0 +1,200 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "deny" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +#ignore = [ +#] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "warn" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "Apache-2.0", + "CC0-1.0", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib" +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # The Unicode-DFS-2016 license is necessary for unicode-ident because they + # use data from the unicode tables to generate the tables which are + # included in the application. We do not distribute those data files so + # this is not a problem for us. See https://github.com/dtolnay/unicode-ident/pull/9/files + { allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*"}, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "deny" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "deny" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +#[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +#github = [""] +# 1 or more gitlab.com organizations to allow git sources for +#gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +#bitbucket = [""] diff --git a/examples/counterparts.rs b/examples/counterparts.rs new file mode 100644 index 00000000..6ad86f4d --- /dev/null +++ b/examples/counterparts.rs @@ -0,0 +1,7 @@ +use std::error::Error; + +// FIXME use? +pub fn main() -> Result<(), Box> { + println!("Alien Shore!"); + Ok(()) +} diff --git a/flake.lock b/flake.lock index d3f4b552..a4fa5849 100644 --- a/flake.lock +++ b/flake.lock @@ -1,18 +1,40 @@ { "nodes": { - "flake-compat": { - "flake": false, + "command-utils": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "lastModified": 1709701816, + "narHash": "sha256-Kwv17invnVzrNrm5fK3Bt6ISJqfXguCx6vc3JDOQtCE=", + "owner": "expede", + "repo": "nix-command-utils", + "rev": "12056907b5194b82060fd8bc6ea11c9fdffb5f25", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "expede", + "repo": "nix-command-utils", + "type": "github" + } + }, + "devshell": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1708939976, + "narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=", + "owner": "numtide", + "repo": "devshell", + "rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", "type": "github" } }, @@ -21,26 +43,91 @@ "systems": "systems" }, "locked": { - "lastModified": 1689068808, - "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "inputs": { + "systems": "systems_3" + }, + "locked": { + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixos-unstable": { + "locked": { + "lastModified": 1709558755, + "narHash": "sha256-hx4FIbk4X4ve1oiHLOj+VE6dzO4CtXBR5RSU6kaq34M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "207107bbc7d6d19a8b2c36a088d3756d03490243", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable-small", + "type": "indirect" } }, "nixpkgs": { "locked": { - "lastModified": 1689321787, - "narHash": "sha256-ifk7hrfWnJaLlcjCf8YaWDR+9kQ0uT3x9eCz31D9qB0=", + "lastModified": 1709569716, + "narHash": "sha256-iOR44RU4jQ+YPGrn+uQeYAp7Xo7Z/+gT+wXJoGxxLTY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c11464c6625d9a71d91a3718a3567394638efc3e", + "rev": "617579a787259b9a6419492eaac670a5f7663917", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", "type": "github" }, "original": { @@ -50,11 +137,28 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1709569716, + "narHash": "sha256-iOR44RU4jQ+YPGrn+uQeYAp7Xo7Z/+gT+wXJoGxxLTY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "617579a787259b9a6419492eaac670a5f7663917", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, "root": { "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", + "command-utils": "command-utils", + "devshell": "devshell", + "flake-utils": "flake-utils_3", + "nixos-unstable": "nixos-unstable", + "nixpkgs": "nixpkgs_3", "rust-overlay": "rust-overlay" } }, @@ -68,11 +172,11 @@ ] }, "locked": { - "lastModified": 1689302058, - "narHash": "sha256-yD74lcHTrw4niXcE9goJLbzsgyce48rQQoy5jK5ZK40=", + "lastModified": 1709691047, + "narHash": "sha256-2Vwx1FLufoMEcOS8KAwP8H83IP3Hw6ZPrIDHkSXrFCY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "7b8dbbf4c67ed05a9bf3d9e658c12d4108bc24c8", + "rev": "d55139f3061cdf2c8f5f7bc8d49e884826e6a4ea", "type": "github" }, "original": { @@ -95,6 +199,36 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 0120ead9..925581d7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,15 +1,14 @@ { - description = "homestar"; + description = "ucan"; inputs = { - # we leverage unstable due to wasm-tools/wasm updates - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + nixpkgs.url = "nixpkgs/nixos-23.11"; + nixos-unstable.url = "nixpkgs/nixos-unstable-small"; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; + command-utils.url = "github:expede/nix-command-utils"; + + flake-utils.url = "github:numtide/flake-utils"; + devshell.url = "github:numtide/devshell"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -20,29 +19,64 @@ outputs = { self, - nixpkgs, - flake-compat, + devshell, flake-utils, + nixos-unstable, + nixpkgs, rust-overlay, + command-utils } @ inputs: flake-utils.lib.eachDefaultSystem ( system: let - overlays = [(import rust-overlay)]; - pkgs = import nixpkgs {inherit system overlays;}; + overlays = [ + devshell.overlays.default + (import rust-overlay) + ]; - rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { - extensions = ["cargo" "clippy" "rustfmt" "rust-src" "rust-std"]; - targets = ["wasm32-unknown-unknown" "wasm32-wasi"]; + pkgs = import nixpkgs { + inherit system overlays; }; - nightly-rustfmt = pkgs.rust-bin.nightly.latest.rustfmt; + unstable = import nixos-unstable { + inherit system overlays; + }; + + rust-toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { + extensions = [ + "cargo" + "clippy" + "llvm-tools-preview" + "rust-src" + "rust-std" + "rustfmt" + ]; + + targets = [ + "aarch64-apple-darwin" + "x86_64-apple-darwin" + + "x86_64-unknown-linux-musl" + "aarch64-unknown-linux-musl" + + "wasm32-unknown-unknown" + "wasm32-wasi" + ]; + }; format-pkgs = with pkgs; [ nixpkgs-fmt alejandra + taplo + ]; + + darwin-installs = with pkgs.darwin.apple_sdk.frameworks; [ + Security + CoreFoundation + Foundation ]; cargo-installs = with pkgs; [ + cargo-criterion cargo-deny cargo-expand cargo-nextest @@ -50,183 +84,183 @@ cargo-sort cargo-udeps cargo-watch + # llvmPackages.bintools twiggy + unstable.cargo-component + wasm-bindgen-cli wasm-tools ]; - ci = pkgs.writeScriptBin "ci" '' - cargo fmt --check - cargo clippy - cargo build --release - nx-test - nx-test-0 - ''; - - db = pkgs.writeScriptBin "db" '' - diesel setup - diesel migration run - ''; - - dbReset = pkgs.writeScriptBin "db-reset" '' - diesel database reset - diesel setup - diesel migration run - ''; - - doc = pkgs.writeScriptBin "doc" '' - cargo doc --no-deps --document-private-items --open - ''; - - compileWasm = pkgs.writeScriptBin "compile-wasm" '' - cargo build -p homestar-functions --target wasm32-unknown-unknown --release - ''; - - dockerBuild = arch: - pkgs.writeScriptBin "docker-${arch}" '' - docker buildx build --file docker/Dockerfile --platform=linux/${arch} -t homestar-runtime --progress=plain . - ''; + cargo = "${pkgs.cargo}/bin/cargo"; + node = "${unstable.nodejs_20}/bin/node"; + wasm-pack = "${pkgs.wasm-pack}/bin/wasm-pack"; + wasm-opt = "${pkgs.binaryen}/bin/wasm-opt"; - xFunc = cmd: - pkgs.writeScriptBin "x-${cmd}" '' - cargo watch -c -x ${cmd} - ''; + cmd = command-utils.cmd.${system}; - xFuncAll = cmd: - pkgs.writeScriptBin "x-${cmd}-all" '' - cargo watch -c -s "cargo ${cmd} --all-features" - ''; + release = { + "release:host" = cmd "Build release for ${system}" + "${cargo} build --release"; - xFuncNoDefault = cmd: - pkgs.writeScriptBin "x-${cmd}-0" '' - cargo watch -c -s "cargo ${cmd} --no-default-features" - ''; + "release:wasm:web" = cmd "Build release for wasm32-unknown-unknown with web bindings" + "${wasm-pack} build --release --target=web"; - xFuncPackage = cmd: crate: - pkgs.writeScriptBin "x-${cmd}-${crate}" '' - cargo watch -c -s "cargo ${cmd} -p homestar-${crate} --all-features" - ''; + "release:wasm:nodejs" = cmd "Build release for wasm32-unknown-unknown with Node.js bindgings" + "${wasm-pack} build --release --target=nodejs"; + }; - xFuncTest = pkgs.writeScriptBin "x-test" '' - cargo watch -c -s "cargo nextest run --nocapture && cargo test --doc" - ''; + build = { + "build:host" = cmd "Build for ${system}" + "${cargo} build"; - xFuncTestAll = pkgs.writeScriptBin "x-test-all" '' - cargo watch -c -s "cargo nextest run --all-features --nocapture \ - && cargo test --doc --all-features" - ''; + "build:wasm:web" = cmd "Build for wasm32-unknown-unknown with web bindings" + "${wasm-pack} build --dev --target=web"; - xFuncTestNoDefault = pkgs.writeScriptBin "x-test-0" '' - cargo watch -c -s "cargo nextest run --no-default-features --nocapture \ - && cargo test --doc --no-default-features" - ''; + "build:wasm:nodejs" = cmd "Build for wasm32-unknown-unknown with Node.js bindgings" + "${wasm-pack} build --dev --target=nodejs"; - xFuncTestPackage = crate: - pkgs.writeScriptBin "x-test-${crate}" '' - cargo watch -c -s "cargo nextest run -p homestar-${crate} --all-features \ - && cargo test --doc -p homestar-${crate} --all-features" - ''; + "build:node" = cmd "Build JS-wrapped Wasm library" + "${pkgs.nodePackages.pnpm}/bin/pnpm install && ${node} run build"; - nxTest = pkgs.writeScriptBin "nx-test" '' - cargo nextest run - cargo test --doc - ''; - - nxTestAll = pkgs.writeScriptBin "nx-test-all" '' - cargo nextest run --all-features --nocapture - cargo test --doc --all-features - ''; - - nxTestNoDefault = pkgs.writeScriptBin "nx-test-0" '' - cargo nextest run --no-default-features --nocapture - cargo test --doc --no-default-features - ''; - - wasmTest = pkgs.writeScriptBin "wasm-ex-test" '' - cargo build -p homestar-functions --features example-test --target wasm32-unknown-unknown --release - cp target/wasm32-unknown-unknown/release/homestar_functions.wasm homestar-wasm/fixtures/example_test.wasm - wasm-tools component new homestar-wasm/fixtures/example_test.wasm -o homestar-wasm/fixtures/example_test_component.wasm - ''; - - wasmAdd = pkgs.writeScriptBin "wasm-ex-add" '' - cargo build -p homestar-functions --features example-add --target wasm32-unknown-unknown --release - cp target/wasm32-unknown-unknown/release/homestar_functions.wasm homestar-wasm/fixtures/example_add.wasm - wasm-tools component new homestar-wasm/fixtures/example_add.wasm -o homestar-wasm/fixtures/example_add_component.wasm - wasm-tools print homestar-wasm/fixtures/example_add.wasm -o homestar-wasm/fixtures/example_add.wat - wasm-tools print homestar-wasm/fixtures/example_add_component.wasm -o homestar-wasm/fixtures/example_add_component.wat - ''; - - scripts = [ - ci - db - dbReset - doc - compileWasm - (builtins.map (arch: dockerBuild arch) ["amd64" "arm64"]) - (builtins.map (cmd: xFunc cmd) ["build" "check" "run" "clippy"]) - (builtins.map (cmd: xFuncAll cmd) ["build" "check" "run" "clippy"]) - (builtins.map (cmd: xFuncNoDefault cmd) ["build" "check" "run" "clippy"]) - (builtins.map (cmd: xFuncPackage cmd "core") ["build" "check" "run" "clippy"]) - (builtins.map (cmd: xFuncPackage cmd "wasm") ["build" "check" "run" "clippy"]) - (builtins.map (cmd: xFuncPackage cmd "runtime") ["build" "check" "run" "clippy"]) - xFuncTest - xFuncTestAll - xFuncTestNoDefault - (builtins.map (crate: xFuncTestPackage crate) ["core" "wasm" "guest-wasm" "runtime"]) - nxTest - nxTestAll - nxTestNoDefault - wasmTest - wasmAdd - ]; - in rec - { + "build:wasi" = cmd "Build for Wasm32-WASI" + "${cargo} build --target wasm32-wasi"; + }; + + bench = { + "bench" = cmd "Run benchmarks, including test utils" + "${cargo} bench --features test_utils"; + + # FIXME align with `bench`? + "bench:host" = cmd "Run host Criterion benchmarks" + "${cargo} criterion"; + + "bench:host:open" = cmd "Open host Criterion benchmarks in browser" + "${pkgs.xdg-utils}/bin/xdg-open ./target/criterion/report/index.html"; + }; + + lint = { + "lint" = cmd "Run Clippy" + "${cargo} clippy"; + + "lint:pedantic" = cmd "Run Clippy pedantically" + "${cargo} clippy -- -W clippy::pedantic"; + + "lint:fix" = cmd "Apply non-pendantic Clippy suggestions" + "${cargo} clippy --fix"; + }; + + watch = { + "watch:build:host" = cmd "Rebuild host target on save" + "${cargo} watch --clear"; + + "watch:build:wasm" = cmd "Rebuild Wasm target on save" + "${cargo} watch --clear --features=serde -- cargo build --target=wasm32-unknown-unknown"; + + "watch:lint" = cmd "Lint on save" + "${cargo} watch --clear --exec clippy"; + + "watch:lint:pedantic" = cmd "Pedantic lint on save" + "${cargo} watch --clear --exec 'clippy -- -W clippy::pedantic'"; + + "watch:test:host" = cmd "Run all host tests on save" + "${cargo} watch --clear --exec 'test --features=mermaid_docs,test_utils'"; + + "watch:test:wasm" = cmd "Run all Wasm tests on save" + "${cargo} watch --clear --exec 'test --target=wasm32-unknown-unknown'"; + }; + + test = { + "test:all" = cmd "Run Cargo tests" + "test:host && test:docs && test:wasm"; + + "test:host" = cmd "Run Cargo tests for host target" + "${cargo} test --features=test_utils"; + + "test:wasm" = cmd "Run wasm-pack tests on all targets" + "test:wasm:node && test:wasm:chrome"; + + "test:wasm:node" = cmd "Run wasm-pack tests in Node.js" + "${wasm-pack} test --node"; + + "test:wasm:chrome" = cmd "Run wasm-pack tests in headless Chrome" + "${wasm-pack} test --headless --chrome"; + + "test:docs" = cmd "Run Cargo doctests" + "${cargo} test --doc --features=mermaid_docs,test_utils"; + }; + + docs = { + "docs:build:host" = cmd "Refresh the docs" + "${cargo} doc --features=mermaid_docs"; + + "docs:build:wasm" = cmd "Refresh the docs with the wasm32-unknown-unknown target" + "${cargo} doc --features=mermaid_docs --target=wasm32-unknown-unknown"; + + "docs:open:host" = cmd "Open refreshed docs" + "${cargo} doc --features=mermaid_docs --open"; + + "docs:open:wasm" = cmd "Open refreshed docs" + "${cargo} doc --features=mermaid_docs --open --target=wasm32-unknown-unknown"; + }; + + command_menu = command-utils.commands.${system} + (release // build // bench // lint // watch // test // docs); + + in rec { devShells.default = pkgs.mkShell { - name = "homestar"; + name = "ucan"; + + # NOTE: blst requires --target=wasm32 support in Clang, which MacOS system clang doesn't provide + stdenv = pkgs.clangStdenv; + nativeBuildInputs = with pkgs; [ - # The ordering of these two items is important. For nightly rustfmt to be used instead of - # the rustfmt provided by `rust-toolchain`, it must appear first in the list. This is - # because native build inputs are added to $PATH in the order they're listed here. - nightly-rustfmt - rust-toolchain - rust-analyzer - pkg-config - pre-commit - diesel-cli direnv + rust-toolchain self.packages.${system}.irust + (pkgs.hiPrio pkgs.rust-bin.nightly.latest.rustfmt) + + pre-commit + pkgs.wasm-pack + chromedriver + protobuf + unstable.nodejs_20 + unstable.nodePackages.pnpm + + command_menu ] ++ format-pkgs ++ cargo-installs - ++ scripts - ++ lib.optionals stdenv.isDarwin [ - darwin.apple_sdk.frameworks.Security - darwin.apple_sdk.frameworks.CoreFoundation - darwin.apple_sdk.frameworks.Foundation - ]; - NIX_PATH = "nixpkgs=" + pkgs.path; - RUST_BACKTRACE = 1; + ++ lib.optionals stdenv.isDarwin darwin-installs; shellHook = '' [ -e .git/hooks/pre-commit ] || pre-commit install --install-hooks && pre-commit install --hook-type commit-msg + + export RUSTC_WRAPPER="${pkgs.sccache}/bin/sccache" + unset SOURCE_DATE_EPOCH + '' + + pkgs.lib.strings.optionalString pkgs.stdenv.isDarwin '' + # See https://github.com/nextest-rs/nextest/issues/267 + export DYLD_FALLBACK_LIBRARY_PATH="$(rustc --print sysroot)/lib" + export NIX_LDFLAGS="-F${pkgs.darwin.apple_sdk.frameworks.CoreFoundation}/Library/Frameworks -framework CoreFoundation $NIX_LDFLAGS"; ''; }; + formatter = pkgs.alejandra; + packages.irust = pkgs.rustPlatform.buildRustPackage rec { pname = "irust"; - version = "1.70.0"; + version = "1.71.19"; src = pkgs.fetchFromGitHub { owner = "sigmaSd"; repo = "IRust"; - rev = "v${version}"; - sha256 = "sha256-chZKesbmvGHXwhnJRZbXyX7B8OwJL9dJh0O1Axz/n2E="; + rev = "irust@${version}"; + sha256 = "sha256-R3EAovCI5xDCQ5R69nMeE6v0cGVcY00O3kV8qHf0akc="; }; doCheck = false; - cargoSha256 = "sha256-FmsD3ajMqpPrTkXCX2anC+cmm0a2xuP+3FHqzj56Ma4="; + cargoSha256 = "sha256-2aVCNz/Lw7364B5dgGaloVPcQHm2E+b/BOxF6Qlc8Hs="; }; - - formatter = pkgs.alejandra; } ); } diff --git a/package.json b/package.json new file mode 100644 index 00000000..93317fbb --- /dev/null +++ b/package.json @@ -0,0 +1,223 @@ +{ + "name": "ucan", + "version": "0.1.0", + "description": "A UCAN library built from ucan", + "repository": { + "type": "git", + "url": "git+https://github.com/fission-codes/ucan.git" + }, + "keywords": [ + "authorization" + ], + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/fission-codes/ucan/issues" + }, + "homepage": "https://github.com/fission-codes/ucan#readme", + "module": "dist/bundler/ucan.js", + "types": "dist/nodejs/ucan.d.ts", + "exports": { + ".": { + "workerd": "./dist/web/workerd.js", + "browser": "./dist/bundler/ucan.js", + "node": "./dist/nodejs/ucan.cjs", + "default": "./dist/bundler/ucan.js", + "types": "./dist/nodejs/ucan.d.ts" + }, + "./nodejs": { + "default": "./dist/nodejs/ucan.cjs", + "types": "./dist/nodejs/ucan.d.ts" + }, + "./web": { + "default": "./dist/web/ucan.js", + "types": "./dist/web/ucan.d.ts" + }, + "./workerd": { + "default": "./dist/web/workerd.js", + "types": "./dist/web/ucan.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "export PROFILE=dev && export TARGET_DIR=debug && pnpm run build:all", + "release": "export PROFILE=release && export TARGET_DIR=release && pnpm run build:all", + "build:all": "wireit", + "clean": "wireit", + "test": "wireit", + "test:node": "wireit" + }, + "wireit": { + "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 -o target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "compile" + ] + }, + "bindgen:bundler": { + "command": "wasm-bindgen --weak-refs --target bundler --out-dir dist/bundler target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ], + "output": [ + "dist/bundler" + ] + }, + "bindgen:nodejs": { + "command": "wasm-bindgen --weak-refs --target nodejs --out-dir dist/nodejs target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm && move-file dist/nodejs/ucan.js dist/nodejs/ucan.cjs", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ], + "output": [ + "dist/nodejs" + ] + }, + "bindgen:web": { + "command": "wasm-bindgen --weak-refs --target web --out-dir dist/web target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm && cpy --flat src/workerd.js dist/web", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ], + "output": [ + "dist/web" + ] + }, + "bindgen:deno": { + "command": "wasm-bindgen --weak-refs --target deno --out-dir dist/deno target/wasm32-unknown-unknown/$TARGET_DIR/ucan.wasm", + "env": { + "TARGET_DIR": { + "external": true + } + }, + "dependencies": [ + "opt" + ], + "output": [ + "dist/deno" + ] + }, + "build:all": { + "dependencies": [ + "bindgen:bundler", + "bindgen:nodejs", + "bindgen:web", + "bindgen:deno" + ] + }, + "clean": { + "command": "rimraf dist" + }, + "test:prepare": { + "command": "cross-env mkdir tests/report", + "output": [ + "tests/report" + ] + }, + "test:chromium": { + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --cov > tests/report/chromium.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/chromium.json" + ] + }, + "test:firefox": { + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --browser firefox > tests/report/firefox.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/firefox.json" + ] + }, + "test:webkit": { + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --browser webkit > tests/report/webkit.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/webkit.json" + ] + }, + "test:browser": { + "dependencies": [ + "test:chromium", + "test:firefox", + "test:webkit" + ] + }, + "test:node": { + "command": "pw-test tests/ucan.test.js -r mocha --reporter json --mode node > tests/report/node.json", + "dependencies": [ + "build", + "test:prepare" + ], + "output": [ + "tests/report/node.json" + ] + }, + "test:report": { + "command": "nyc report --reporter=json-summary --report-dir tests/report", + "dependencies": [ + "test:chromium" + ], + "output": [ + "tests/report/coverage-summary.json" + ] + }, + "test": { + "dependencies": [ + "test:browser", + "test:node", + "test:report" + ] + } + }, + "devDependencies": { + "@types/assert": "^1.5.6", + "@types/mocha": "^10.0.1", + "assert": "^2.0.0", + "cpy-cli": "^5.0.0", + "cross-env": "^7.0.3", + "mocha": "^10.2.0", + "move-file-cli": "^3.0.0", + "nyc": "^15.1.0", + "playwright-test": "^12.1.2", + "rimraf": "^5.0.1", + "typescript": "^5.1.6", + "wireit": "^0.10.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..fa2e6ed3 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3025 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + '@types/assert': + specifier: ^1.5.6 + version: 1.5.8 + '@types/mocha': + specifier: ^10.0.1 + version: 10.0.3 + assert: + specifier: ^2.0.0 + version: 2.1.0 + cpy-cli: + specifier: ^5.0.0 + version: 5.0.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + mocha: + specifier: ^10.2.0 + version: 10.2.0 + move-file-cli: + specifier: ^3.0.0 + version: 3.0.0 + nyc: + specifier: ^15.1.0 + version: 15.1.0 + playwright-test: + specifier: ^12.1.2 + version: 12.4.3 + rimraf: + specifier: ^5.0.1 + version: 5.0.5 + typescript: + specifier: ^5.1.6 + version: 5.2.2 + wireit: + specifier: ^0.10.0 + version: 0.10.0 + +packages: + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@arr/every@1.0.1: + resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} + engines: {node: '>=4'} + dev: true + + /@babel/code-frame@7.22.13: + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.20 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.23.2: + resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.2: + resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.23.2) + '@babel/helpers': 7.23.2 + '@babel/parser': 7.23.0 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.2 + '@babel/types': 7.23.0 + convert-source-map: 2.0.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.0: + resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.22.15: + resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/helper-validator-option': 7.22.15 + browserslist: 4.22.1 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-module-transforms@7.23.0(@babel/core@7.23.2): + resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.2 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.15: + resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.23.2: + resolution: {integrity: sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.2 + '@babel/types': 7.23.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.22.20: + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + dev: true + + /@babel/traverse@7.23.2: + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.13 + '@babel/generator': 7.23.0 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.0 + '@babel/types': 7.23.0 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@esbuild/android-arm64@0.19.5: + resolution: {integrity: sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.5: + resolution: {integrity: sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.5: + resolution: {integrity: sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.5: + resolution: {integrity: sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.5: + resolution: {integrity: sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.5: + resolution: {integrity: sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.5: + resolution: {integrity: sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.5: + resolution: {integrity: sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.5: + resolution: {integrity: sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.5: + resolution: {integrity: sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.5: + resolution: {integrity: sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.5: + resolution: {integrity: sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.5: + resolution: {integrity: sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.5: + resolution: {integrity: sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.5: + resolution: {integrity: sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.5: + resolution: {integrity: sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.5: + resolution: {integrity: sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.5: + resolution: {integrity: sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.5: + resolution: {integrity: sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.5: + resolution: {integrity: sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.5: + resolution: {integrity: sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.5: + resolution: {integrity: sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@polka/url@0.5.0: + resolution: {integrity: sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==} + dev: true + + /@polka/url@1.0.0-next.23: + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==} + dev: true + + /@types/assert@1.5.8: + resolution: {integrity: sha512-tL1NSDf4CF5hiVTnLd4KSth6bmRO3+tw8cJzEAUaN6fYQ26DIixX0lRFmtrA0jhxUS8SL6PDWtphMz3maxapjA==} + dev: true + + /@types/istanbul-lib-coverage@2.0.5: + resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} + dev: true + + /@types/minimist@1.2.4: + resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} + dev: true + + /@types/mocha@10.0.3: + resolution: {integrity: sha512-RsOPImTriV/OE4A9qKjMtk2MnXiuLLbcO3nCXK+kvq4nr0iMfFgpjaX3MPLb6f7+EL1FGSelYvuJMV6REH+ZPQ==} + dev: true + + /@types/normalize-package-data@2.4.3: + resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==} + dev: true + + /acorn-loose@8.4.0: + resolution: {integrity: sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 8.11.2 + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /aggregate-error@4.0.1: + resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} + engines: {node: '>=12'} + dependencies: + clean-stack: 4.2.0 + indent-string: 5.0.0 + dev: true + + /ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /append-transform@2.0.0: + resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} + engines: {node: '>=8'} + dependencies: + default-require-extensions: 3.0.1 + dev: true + + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + + /arrify@3.0.0: + resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} + engines: {node: '>=12'} + dev: true + + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.5 + is-nan: 1.3.2 + object-is: 1.1.5 + object.assign: 4.1.4 + util: 0.12.5 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001558 + electron-to-chromium: 1.4.570 + node-releases: 2.0.13 + update-browserslist-db: 1.0.13(browserslist@4.22.1) + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /c8@8.0.1: + resolution: {integrity: sha512-EINpopxZNH1mETuI0DzRA4MZpAUH+IFiRhnmFD3vFr3vdrgxqi3VfE3KL0AIL+zDq8rC9bZqwM/VDmmoe04y7w==} + engines: {node: '>=12'} + hasBin: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.1.6 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.1.3 + yargs: 17.7.2 + yargs-parser: 21.1.1 + dev: true + + /caching-transform@4.0.0: + resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} + engines: {node: '>=8'} + dependencies: + hasha: 5.2.2 + make-dir: 3.1.0 + package-hash: 4.0.0 + write-file-atomic: 3.0.3 + dev: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: true + + /camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + dev: true + + /caniuse-lite@1.0.30001558: + resolution: {integrity: sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /clean-stack@4.2.0: + resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} + engines: {node: '>=12'} + dependencies: + escape-string-regexp: 5.0.0 + dev: true + + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: true + + /cli-spinners@2.9.1: + resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} + engines: {node: '>=6'} + dev: true + + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cp-file@10.0.0: + resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==} + engines: {node: '>=14.16'} + dependencies: + graceful-fs: 4.2.11 + nested-error-stacks: 2.1.1 + p-event: 5.0.1 + dev: true + + /cpy-cli@5.0.0: + resolution: {integrity: sha512-fb+DZYbL9KHc0BC4NYqGRrDIJZPXUmjjtqdw4XRRg8iV8dIfghUX/WiL+q4/B/KFTy3sK6jsbUhBaz0/Hxg7IQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + cpy: 10.1.0 + meow: 12.1.1 + dev: true + + /cpy@10.1.0: + resolution: {integrity: sha512-VC2Gs20JcTyeQob6UViBLnyP0bYHkBh6EiKzot9vi2DmeGlFT9Wd7VG3NBrkNx/jYvFBeyDOMMHdHQhbtKLgHQ==} + engines: {node: '>=16'} + dependencies: + arrify: 3.0.0 + cp-file: 10.0.0 + globby: 13.2.2 + junk: 4.0.1 + micromatch: 4.0.5 + nested-error-stacks: 2.1.1 + p-filter: 3.0.0 + p-map: 6.0.0 + dev: true + + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: true + + /debug@4.3.4(supports-color@8.1.1): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + dev: true + + /default-require-extensions@3.0.1: + resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==} + engines: {node: '>=8'} + dependencies: + strip-bom: 4.0.0 + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + + /diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /electron-to-chromium@1.4.570: + resolution: {integrity: sha512-5GxH0PLSIfXKOUMMHMCT4M0olwj1WwAxsQHzVW5Vh3kbsvGw8b4k7LHQmTLC2aRhsgFzrF57XJomca4XLc/WHA==} + dev: true + + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + dev: true + + /esbuild-plugin-wasm@1.1.0: + resolution: {integrity: sha512-0bQ6+1tUbySSnxzn5jnXHMDvYnT0cN/Wd4Syk8g/sqAIJUg7buTIi22svS3Qz6ssx895NT+TgLPb33xi1OkZig==} + engines: {node: '>=0.10.0'} + dev: true + + /esbuild@0.19.5: + resolution: {integrity: sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.5 + '@esbuild/android-arm64': 0.19.5 + '@esbuild/android-x64': 0.19.5 + '@esbuild/darwin-arm64': 0.19.5 + '@esbuild/darwin-x64': 0.19.5 + '@esbuild/freebsd-arm64': 0.19.5 + '@esbuild/freebsd-x64': 0.19.5 + '@esbuild/linux-arm': 0.19.5 + '@esbuild/linux-arm64': 0.19.5 + '@esbuild/linux-ia32': 0.19.5 + '@esbuild/linux-loong64': 0.19.5 + '@esbuild/linux-mips64el': 0.19.5 + '@esbuild/linux-ppc64': 0.19.5 + '@esbuild/linux-riscv64': 0.19.5 + '@esbuild/linux-s390x': 0.19.5 + '@esbuild/linux-x64': 0.19.5 + '@esbuild/netbsd-x64': 0.19.5 + '@esbuild/openbsd-x64': 0.19.5 + '@esbuild/sunos-x64': 0.19.5 + '@esbuild/win32-arm64': 0.19.5 + '@esbuild/win32-ia32': 0.19.5 + '@esbuild/win32-x64': 0.19.5 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + dev: true + + /exit-hook@4.0.0: + resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==} + engines: {node: '>=18'} + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: true + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + + /glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 4.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + dev: true + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + dependencies: + lru-cache: 6.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: true + + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.13 + dev: true + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: true + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: true + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-hook@3.0.0: + resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} + engines: {node: '>=8'} + dependencies: + append-transform: 2.0.0 + dev: true + + /istanbul-lib-instrument@4.0.3: + resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.23.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-processinfo@2.0.3: + resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} + engines: {node: '>=8'} + dependencies: + archy: 1.0.0 + cross-spawn: 7.0.3 + istanbul-lib-coverage: 3.2.0 + p-map: 3.0.0 + rimraf: 3.0.2 + uuid: 8.3.2 + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /junk@4.0.1: + resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} + engines: {node: '>=12.20'} + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /log-symbols@5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: true + + /lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + + /matchit@1.1.0: + resolution: {integrity: sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==} + engines: {node: '>=6'} + dependencies: + '@arr/every': 1.0.1 + dev: true + + /meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + '@types/minimist': 1.2.4 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + dev: true + + /meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + dev: true + + /merge-options@3.0.4: + resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} + engines: {node: '>=10'} + dependencies: + is-plain-obj: 2.1.0 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mocha@10.2.0: + resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /move-file-cli@3.0.0: + resolution: {integrity: sha512-d9ef0fnyX6K/1sKvKG1F0cssJpIrzxWITjkiq3ufC/GQcMNsPMaNEmv+PnPdlBBzRAy4/EMkLkeQfuL946okuQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + meow: 10.1.5 + move-file: 3.1.0 + dev: true + + /move-file@3.1.0: + resolution: {integrity: sha512-4aE3U7CCBWgrQlQDMq8da4woBWDGHioJFiOZ8Ie6Yq2uwYQ9V2kGhTz4x3u6Wc+OU17nw0yc3rJ/lQ4jIiPe3A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-exists: 5.0.0 + dev: true + + /mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid@3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nanoid@5.0.2: + resolution: {integrity: sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==} + engines: {node: ^18 || >=20} + hasBin: true + dev: true + + /nested-error-stacks@2.1.1: + resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} + dev: true + + /node-preload@0.2.1: + resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} + engines: {node: '>=8'} + dependencies: + process-on-spawn: 1.0.0 + dev: true + + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: true + + /normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.13.1 + semver: 7.5.4 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + + /nyc@15.1.0: + resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==} + engines: {node: '>=8.9'} + hasBin: true + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + caching-transform: 4.0.0 + convert-source-map: 1.9.0 + decamelize: 1.2.0 + find-cache-dir: 3.3.2 + find-up: 4.1.0 + foreground-child: 2.0.0 + get-package-type: 0.1.0 + glob: 7.2.3 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-hook: 3.0.0 + istanbul-lib-instrument: 4.0.3 + istanbul-lib-processinfo: 2.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + make-dir: 3.1.0 + node-preload: 0.2.1 + p-map: 3.0.0 + process-on-spawn: 1.0.0 + resolve-from: 5.0.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + spawn-wrap: 2.0.0 + test-exclude: 6.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + dev: true + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + + /ora@7.0.1: + resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==} + engines: {node: '>=16'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.1 + is-interactive: 2.0.0 + is-unicode-supported: 1.3.0 + log-symbols: 5.1.0 + stdin-discarder: 0.1.0 + string-width: 6.1.0 + strip-ansi: 7.1.0 + dev: true + + /p-event@5.0.1: + resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-timeout: 5.1.0 + dev: true + + /p-filter@3.0.0: + resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-map: 5.5.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-map@5.5.0: + resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} + engines: {node: '>=12'} + dependencies: + aggregate-error: 4.0.1 + dev: true + + /p-map@6.0.0: + resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==} + engines: {node: '>=16'} + dev: true + + /p-timeout@5.1.0: + resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} + engines: {node: '>=12'} + dev: true + + /p-timeout@6.1.2: + resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} + engines: {node: '>=14.16'} + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /package-hash@4.0.0: + resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} + engines: {node: '>=8'} + dependencies: + graceful-fs: 4.2.11 + hasha: 5.2.2 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.22.13 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.1 + minipass: 7.0.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /playwright-core@1.39.0: + resolution: {integrity: sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright-test@12.4.3: + resolution: {integrity: sha512-51nFyab0RSOM23dTq34C61hAx0e13Scab6U5VJW7RjFhpEkQXBeq46R7WNeY0mHaYEUaLYdSch51ceibtQ4Tzg==} + engines: {node: '>=16.0.0'} + hasBin: true + dependencies: + acorn-loose: 8.4.0 + assert: 2.1.0 + buffer: 6.0.3 + c8: 8.0.1 + camelcase: 8.0.0 + chokidar: 3.5.3 + cpy: 10.1.0 + esbuild: 0.19.5 + esbuild-plugin-wasm: 1.1.0 + events: 3.3.0 + execa: 8.0.1 + exit-hook: 4.0.0 + globby: 13.2.2 + kleur: 4.1.5 + lilconfig: 2.1.0 + lodash: 4.17.21 + merge-options: 3.0.4 + nanoid: 5.0.2 + ora: 7.0.1 + p-timeout: 6.1.2 + path-browserify: 1.0.1 + playwright-core: 1.39.0 + polka: 0.5.2 + premove: 4.0.0 + process: 0.11.10 + sade: 1.8.1 + sirv: 2.0.3 + source-map: 0.6.1 + source-map-support: 0.5.21 + stream-browserify: 3.0.0 + tempy: 3.1.0 + test-exclude: 6.0.0 + util: 0.12.5 + v8-to-istanbul: 9.1.3 + dev: true + + /polka@0.5.2: + resolution: {integrity: sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==} + dependencies: + '@polka/url': 0.5.0 + trouter: 2.0.1 + dev: true + + /premove@4.0.0: + resolution: {integrity: sha512-zim/Hr4+FVdCIM7zL9b9Z0Wfd5Ya3mnKtiuDv7L5lzYzanSq6cOcVJ7EFcgK4I0pt28l8H0jX/x3nyog380XgQ==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /process-on-spawn@1.0.0: + resolution: {integrity: sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==} + engines: {node: '>=8'} + dependencies: + fromentries: 1.3.2 + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + dev: true + + /read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + dependencies: + '@types/normalize-package-data': 2.4.3 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + dependencies: + indent-string: 5.0.0 + strip-indent: 4.0.0 + dev: true + + /release-zalgo@1.0.0: + resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} + engines: {node: '>=4'} + dependencies: + es6-error: 4.1.1 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.23 + mrmime: 1.0.1 + totalist: 3.0.1 + dev: true + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /spawn-wrap@2.0.0: + resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} + engines: {node: '>=8'} + dependencies: + foreground-child: 2.0.0 + is-windows: 1.0.2 + make-dir: 3.1.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + which: 2.0.2 + dev: true + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.16 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.16 + dev: true + + /spdx-license-ids@3.0.16: + resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /stdin-discarder@0.1.0: + resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.1.0 + dev: true + + /stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string-width@6.1.0: + resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} + engines: {node: '>=16'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 10.3.0 + strip-ansi: 7.1.0 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + + /strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + dev: true + + /tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + + /trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} + dev: true + + /trouter@2.0.1: + resolution: {integrity: sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==} + engines: {node: '>=6'} + dependencies: + matchit: 1.1.0 + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: true + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: true + + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + dev: true + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + dependencies: + crypto-random-string: 4.0.0 + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.1): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.1 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.13 + dev: true + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: true + + /v8-to-istanbul@9.1.3: + resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + '@types/istanbul-lib-coverage': 2.0.5 + convert-source-map: 2.0.0 + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true + + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wireit@0.10.0: + resolution: {integrity: sha512-4TX6V9D/2iXUBzdqQaUG+cRePle0mDx1Q7x4Ka2cA8lgp1+ZBhrOTiLsXYRl2roQEldEFgQ2Ff1W8YgyNWAa6w==} + engines: {node: '>=14.14.0'} + hasBin: true + dependencies: + braces: 3.0.2 + chokidar: 3.5.3 + fast-glob: 3.3.1 + jsonc-parser: 3.2.0 + proper-lockfile: 4.1.2 + dev: true + + /workerpool@6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + dev: true + + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/proptest-regressions/delegation/payload.txt b/proptest-regressions/delegation/payload.txt new file mode 100644 index 00000000..03657047 --- /dev/null +++ b/proptest-regressions/delegation/payload.txt @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 2219b6f1cfd2b9c29cdae1174fd8633335cb25947b15ef61c3df44f2664175c3 # shrinks to payload = Payload { subject: None, issuer: Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) }))), audience: Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) }))), via: Some(Key(EdDsa(VerifyingKey(CompressedEdwardsY: [46, 111, 204, 227, 103, 1, 220, 121, 20, 136, 224, 208, 177, 116, 92, 193, 227, 58, 76, 28, 159, 204, 65, 198, 59, 211, 67, 219, 190, 9, 112, 230]), EdwardsPoint{ X: FieldElement51([1689611602193863, 490607132032821, 1343312146746774, 1090732682789050, 1815270510391065]), Y: FieldElement51([1127445621927726, 1752742079139643, 335263251657170, 455073811812238, 1802102173971517]), Z: FieldElement51([1, 0, 0, 0, 0]), T: FieldElement51([396516250482892, 1271770325328148, 2066179188049959, 970219954360817, 1259266248234093]) })))), command: "eâ'Ⱥ{Ⱥ𞹒/𖿠¦꧊=:Z꧉&ceG4ᅲQ&]%", policy: [And(LessThan(Select([Values]), Integer(-17977310508110653107821168443657725762)), GreaterThan(Select([ArrayIndex(-2063331637), Values, ArrayIndex(1729047197)]), Integer(-71863900340009931549720359637876494783))), LessThanOrEqual(Select([ArrayIndex(1500241979), ArrayIndex(-659046873), Field("`Ⴧ*ᧂ:𑛄&"), Values, Values, ArrayIndex(1750800327), Field("Ⱥd"), ArrayIndex(-1789308745)]), Integer(70158225305663682194208457939428168215)), LessThanOrEqual(Select([Values, Field("*🫁𝕄L𞸧/ຄ𑻤$u\"*𝼥m':ᨼ𑾰/\\"), ArrayIndex(-554915756)]), Float(9.08306675974206e-234))), Or(LessThanOrEqual(Select([Values]), Float(1.003428263794859e-307)), And(Equal(Select([ArrayIndex(-233587294), Field("𝒫𞻰& キ<&ஞ臨2m𞺣'𞋿&{\u{ac3}{D8𒑀𑰈5\u{1ac0}\""), Values, Values, Field("G.吸aj𑿤q^a9%.<\u{84}%.𝝹𔓶": true, "*E🕴5/.`\\x=\u{7a853}&\u{5fd16}𩟨\u{b}\u{b3012}\u{9e345}/A\u{1b}Dts&E{w}\u{d8473}": 51, ".Z\u{795c2}?*\u{bffa7}B¥-/\tb阂?": 50, ".\u{7f}": baguqegzax2jx7mre4zpv63vto6u3mtjy4ivcrpnsnrgfcb6bymbpbwz5ky2q, ".\u{80}%\u{b}$\u{c6cf9}=&&\u{4}↜$'N\u{7f}\u{b}2\u{2}?\u{65b02}Uð𫻓\u{b}\u{feff}": -1.3122304768215823e-86, ".𗨥Ø\u{7f}\u{fe136}g\u{880c8}0O\\è": false, "1={\u{4fc0c}/\u{37be6}+\u{73284}.{\r�腄/_%�?\r\u{b022e}H": false, "2𱟴.": null, "4'\u{70fad}\\�o{\u{ce77b}\u{7f}~\u{d4dac}\u{1b}V\u{b}𩱋�\t&\\\u{aee01}`🕴\u{ee7dd}": null, "5$w\u{df9bd}\\\u{6}\tѨ\u{1b}M\u{cd289}:U%\u{202e}I\u{ea2ae}\r\u{d0993}D\u{feff}Ñ": null, "5\u{db9ee}\u{feff}\"�Ⱥ🕴z\0v<<\"5": 0.0, ":": bafy2bzacecp4g47wlyry7zukgmgp6xoxsh5aqlhn2vtxbpk2j77pzmx5w5xci, "<\0\u{3fe95}\t=`Z𰲨pdËf\u{5}�\u{1b}30\u{a2f77}\t:|%\u{a65a3}": 25, "<\u{2}¥?\u{9ae9b}'?q\u{7f}\u{84177}/?d🕴\u{d0dee}_x\u{a73f9}=`/\"Ѩ¥+\u{1}\u{202e}+Ⱥ/": "%){\u{f34d0}\u{91bf2}{]¥?<\u{7e5a3}\\\u{7f}¥", "<\"k&🕴Q\u{a0cc1}\u{193c1}\u{6fef1}\u{57765}=\u{b04a9}\u{1ee38}*$i�趼\r:^�\t\u{2}/": null, "<'\u{202e}": [246, 133, 18, 125, 51, 172], "<*\r\u{feff}'\u{b}\u{feff},\t\u{202e}\u{b}": 1.9096030614441494e34, "\0#.\u{e50a7}`\u{6b781}\u{72c56}IѨTȺ:\u{202e}\u{c1d84}\u{d1ebf}F\u{bc6f1}P�\u{1b}}?d\u{feff}Ⱥ\u{96517}\u{7}": -21, ">\u{7f}h)\t&'\u{b}": "p$\u{ce2d9}T\t\u{6486c}\"\u{af416}", "?*=\u{eea8d}\u{a8f3d}\u{479bf}\\\u{577da}<Ѩ&\u{202e}\u{ceb29}\u{8}/\u{7f}\u{7f}𘂤*\u{91}\u{96d48}\u{202e}\u{60a35}6\u{7cd5b}\u{b}~7\u{feff}\u{fc2b3}\u{6ca08}": bafykbzacedzd5rmt4wtpaenao7lyjfstkk3babvem5zvxsx66esxsdwdmnuok, "?\u{f5061}\u{6}\u{adb94}\tz<\u{8e6f5}\t]\"\u{1b}\u{50c21}?=\u{feff}\"'\u{cdff0}{�\u{11b58}\u{7f}ñ¥'\"I\u{6efe5}\0": "M\u{d7240}\u{5fa2f}\"\t\\Ⱥ\u{8}\r𓂂E\u{615a1}$ :\0\u{4b6de}2�&::", "D\u{834c0}": bafybwictq6cimsvk5g36pcopei5vzwuqjgz6zab4qnlw6h6pexvenpnuz4, "G{\0P\u{202e}z?<*\u{e4d94}\u{caeb6}c\u{b}\"\u{d4fa0}\0\u{3d0e1}\u{c315a}": true, "O:{.:6\u{1}𒋵=": [215, 19, 250, 35, 145, 227, 89, 97, 226, 22, 100, 90, 212, 248, 231, 66, 1, 74, 222, 19, 252, 253, 50, 43, 95, 28, 217, 195, 43, 193, 37, 211, 223, 168, 162, 177, 117, 244, 235, 50, 129, 137, 255, 246], "P\u{7f}m[*^\u{d4b73}\u{a4603}\u{cae36}\u{202e}C\u{2}\u{99a81}/<": "\t=/\u{202e}{?=z𣿸&Ѩ\t\u{10fefb}:`\\\u{88684}ᇵ&<'IB9Ѩ", "Q=/\u{2}[W$🕴Ѩi`\u{6b1f0}g\0\u{202e}&äq\0\\\u{e7b6e}\u{7}2": 0.0, "S\u{feff}¥\u{363f5}\\�\"|/$\u{1b}🕴\u{d61d7}\u{9c5fc}\u{5}%": true, "\\": 1797410591.2173085, "\\'\\\u{72082}9\r\u{fd777}&'\u{8}-\t$Ѩ\u{85}&'\\$\u{202e}\u{4}\u{d59a8}𐋪": null, "\\*\t&>ó": [197, 70, 103, 161, 242, 103, 93, 85, 220, 20, 175, 146, 70, 248, 167, 201, 81, 105, 89, 137, 113, 31, 52, 197, 100, 44, 200, 169, 146, 8, 75, 185, 88, 27, 164, 65, 95, 19, 82, 178, 129, 49], "\\¥𧷋%\u{7f}/%": 5.3388979855107657e-222, "]\u{2},n{\t": 4.726007655014673e-268, "]\u{e5bc2}:\u{67d79}": [192, 72, 37, 153, 92, 243, 115, 45, 85, 213, 125, 24, 161, 32, 231, 155, 146, 251, 181, 211, 205, 114], "^": false, "`\"\rBbIU\u{b38e7}~{\u{1b}<": [166, 52, 57, 140, 164, 87, 17, 112, 52, 240, 111, 77, 237, 27, 236, 44, 43, 106, 52, 249, 5, 150, 136, 195, 238, 123], "`=": 37, "`=\0\u{ae1db}$'\u{b}<{\u{1}\u{2};": true, "`~HѨ\r=": null, "`\u{91304}8?=\u{456a5}?\u{ee878}`": [72, 35, 50, 245, 247, 42, 83, 78, 154, 184, 6, 235, 171, 159, 23, 112, 160, 167, 206, 73, 56, 6, 234, 198, 253, 149, 213, 217, 121, 184, 227, 205, 182, 89, 67, 116, 130, 153, 30, 111, 90, 28, 205, 170, 195, 105, 252, 13], "`\u{b62b4}l\rȺ\0䱮Ѩ\u{325fa}://%.H*\r4\u{67fc7}f\u{feff}\u{97c83}d_z𒋤𩞬<": "{\u{7}\u{1b}\u{1}\t\"Ѩr\u{5}", "`\u{db05f}\u{82068}%\u{e3259}\u{202e}&\u{8916b}r\u{b}\u{eda56}�7\u{7f}\u{800bc}": null, "a\u{7f}": -4.342818332244186e254, "e;='\u{66fb7}¦_.R\u{6}S\u{99a3c}1*'d�\u{aecf1}?¥:\u{cd4f6}🕴¥{\u{202e}V'": null, "i\u{7acc3}\0*\u{3a0a4};\"G\0x\u{202e}\u{202e}\u{a6ef1}\u{7}y\u{c4c0c}*=\u{202e}Ñ|]\u{b}`S�\u{63b03}&¦🕴|": "\u{9b}\u{feff}/\u{2}\u{32ba0}$5N\u{c18b1}\u{6e305}", "i\u{b88de}:%<:\u{b}i¥\\%&\u{e9081}\u{36b96}\\wa\u{53cd7}\u{7f}\u{441e2}": [254, 111, 111, 15, 235, 43, 5, 115, 184, 222, 104, 79, 29, 45, 167, 151, 205, 114, 64, 194, 79, 2, 66, 154, 153, 108, 151, 126, 250, 233, 134], "z": true, "{\u{7f}Ⱥ&��0a]\u{7f}\u{b3046}\u{2fa93}\u{7f}<`a\u{10b100}b\u{e36be}-:\u{b}}I\\\u{c99dc}(�\u{d4177}<": 5.69430214693307e-309, "{\u{90}�\r¥%`": null, "\u{7f}=n\r\u{b904f}m\\`&\u{8d}&\u{8}q\u{10fb4a}\u{202e}\u{feff}": 30, "\u{7f}\u{6677f}\"\t\t𥡏&\u{929fa}%\u{1b}": false, "¥\u{4b398}$\u{b}.\u{10edc3}.v\u{4}\u{c8f3b}`\u{1b}o\u{b}B�.\u{8a4e7}": true, "¦\u{567e2}\u{789fb}.\u{1}\u{bac03}FeLѨ": [46, 154, 121, 10, 113, 153, 177, 226, 108, 199, 94, 194, 182, 56, 189, 253, 144, 68, 220, 178, 227, 39, 5, 27, 46, 171, 2, 171, 100, 125, 136, 48, 185, 143, 175, 224, 38, 180, 89, 73, 253, 97, 152, 89, 126, 226, 69, 159, 39, 4, 5, 151, 166, 157, 129, 18, 74, 141, 30, 245, 18, 68, 59, 4, 84, 71, 127, 83, 157, 160, 70, 180, 19, 67, 237, 156, 119, 247, 69, 10, 220, 32, 51, 35], "Ê`/\u{e98d1}`8\u{5a946}¥\tѨ\\\u{71794}\u{1b}\u{5d467}\u{5f80a}\u{2}o\\\u{78e03}\u{1b}$V\0,Ⱥu<\t\u{4}": "l\u{80}/H", "Û\u{a246c}\0\u{cc1d1}\u{1b}": -2.3446004632097955e-204, "Ⱥ\u{48608}�\u{1}Ѩ\t\"\\\rRx\u{feff}𱋭🕴.>.\r?/Ѩe": bafybmibqonatpssw5xwj3ckqwghe3uv52fbojbwo6wevvgyxhhi6is34iy, "ЏѨ$N==jX\u{ef98d}?{Ⱥk\u{4c9b3}\u{a5c43}'\rx": true, "Ѩ?=Ⱥ\u{7f}\0🕴$�\u{39466}\u{1b}�\"`\0.\u{feff}*.x-\u{7f}EѨÔ\u{7}\u{10f98d}$P": false, "Ѩ뀳🕴\t\u{202e}<{\u{950c8}\0\u{6abe2}2¥G\0": bafkrmicucdpcfbvzkhxwm7wgcnbx7wbbsx7yf4xs3xnebcwqv4fzfnyeui, "\u{202e}\u{5}\u{f93a8}�\t\u{93751}DX.4\u{b}\u{47079}\u{cb546}Ê\u{3c9c6}('Q\u{df412}^(?%\u{1b}\u{d4087}\u{87657}\u{69291}": bafyr4igptyd3pys2jr5l7f6wly2ixknaqjhxa2goliyvnoxef4q5npn2wq, "\u{202e}//)H\u{489d0}\u{b}\"\u{7f}7\u{fd8c1}\u{52967}": -49, "퀹\u{1e584}Ⱥ\u{b5e48}?\u{d70a5}\u{202e}\u{feff}o&Ⱥ\t2&$TѨ\u{c7d9d}¥z9�#B,\u{7d2c6}\u{a9082}": -39, "�\u{b}\u{9b}:\u{afa9e}\u{8b}\"G%=\u{6}\u{b1e04}𫃏\u{202e}{:Ⱥ'\0[;\u{cbe4b}Ѩ¥V\r:e\r�\"\u{36274}{\"\u{feff}\t\u{1b},\\�鴇\\µ": "*\u{b2484}(j'0\\�\t\u{feff}~I*\u{1abf0}鲫!¥í\u{7f}\t\t\u{feff}", "\u{2}¥@@x\u{7}o<>\u{b}>�\"=\u{4a714}H\u{356a6}Y\u{feff}¥\0Ѩ`\u{8586b}�@\u{86433}8\u{b5eb0}U\t\u{928a7}?:3V\r\0\u{e963}\u{91606}\u{f5d9a}`&&\t\\{$&`": [203, 219, 129, 233, 167, 86, 133, 68, 30, 123, 72, 152, 245, 36, 18, 51, 184, 218, 78, 248, 74, 234, 85, 239, 225, 163, 68, 62, 10, 51, 41, 233, 241, 66, 225, 241, 213, 58, 199, 207, 196, 20, 9, 130, 162, 125, 157, 172, 66, 51, 176, 230, 83, 217, 243, 126, 106, 4, 227, 168, 130], "?\t#?\u{c8453}l<\u{ebb70}<\u{202e}\u{1}\u{4a0a6}\u{bb34d}𧆽{\"\u{202e}\u{a6e5e}<\u{2}$\u{1b}{\u{1b}\u{a7345}\u{bcba2}=\u{55f80}𠘓`": false, "@\0 \u{e2e8b}\u{feff}&{¸\u{1b}`Ã`<\u{c80a3}\"h%V\\:\r:{%\u{b}\u{91b96}\0": 16, "A\0\0%Ⱥ\u{7f}o6Ѩ\ra\t\u{fa6d1}": 0.0, "E5\u{3ef04}\\iѨ4==\u{ad632}\u{103a8b}\0\u{882ea}\u{3fb8f}\u{80a35}{\r\"\u{9afa0}\\<3*\u{e368f}\"{Ѩ\"\u{85b40}/": [62, 206, 252, 139, 165, 123, 83, 85], "K?\u{15ea9}%/Ⱥ\u{39c0e}Ѩ\tM/\u{806b5}𣎂+": "\u{7b58c}\u{eb27}", "Py/\u{eeae7}\t:z/u&\u{fdecd}\\`<": 2, "P🕴EU\t$\0V\"*": true, "S\u{6}?'{¦~:¥\"\u{797af}:Ⱥ": false, "W/\u{7d22d}\u{7f}k\u{6f5bc}\"/\u{7ac51}\u{feff}\r\u{1b}\u{b}`\r\u{16d8a}\"\u{feff}\u{4e68c}": null}, 3.7320199453131945e18, true, "\td", -2.1302548429054803e-66, [9, 130, 254, 29, 224, 58, 3, 226, 45, 154, 180, 58, 156, 107, 43, 160], [184, 58, 122, 21, 191, 153, 91, 193, 125, 41, 253, 9, 18, 17, 100, 75, 60, 215, 91, 98, 131, 82, 225, 220, 226, 154, 246, 54, 253, 108, 18, 104, 142, 238, 92, 249, 74, 194, 85, 60, 101, 114, 120, 130, 76, 198, 254, 54, 43, 192, 5], -3.035213207805024e-151, true, null, true, [144, 5, 247, 45, 158, 101, 252, 143, 38, 225, 226, 145, 14, 34, 204, 157, 201, 182, 152, 54, 29, 245, 248, 233, 203, 103, 50], 7.126162207025803e98, 0.0, 38, 44, false, 1.442945330979412e-308, -3.2821103465088214e185, false, -0.0])), GreaterThanOrEqual(Select([Field("\u{dca}?i%ѨѨ<\\{Ѩৡ=ᨁr\"T\u{1a7f}🛞{༴Ⱥ:ᛧ"), Values, Values, ArrayIndex(-206845918), ArrayIndex(284566073)]), Integer(-130442174005234510445148006169593931048)))), Every(Select([Values, ArrayIndex(-1235287252), ArrayIndex(1235974543), Field(":�𐔆Ѩ1ꬓ*%𝔹⑩%By�ࡤ{6&🃅%ᒜ!*t\u{cd5}ܔÈ🃌K-:f"), ArrayIndex(-74267187), Values, Field("𐝧¥ѨK𛄲ﹰ~/𞺀Ⱥ𐁑{A℈ꧻxኁ$X")]), Every(Select([ArrayIndex(-835767092), ArrayIndex(-90842294), Values, Field("&Z𛱺\u{1a6b}¥¡\u{2002}O�Eࡪ(/ѨѨ\u{fb3}ö2<=u,ⶣb"), Field("হ\u{1d242}]𐺱'<×\".᪗\u{cc6}%\u{9e2}/$`+ꚠ𞹲ѨRr'7\"\u{1921}"), ArrayIndex(-1657524013), ArrayIndex(-1358049927)]), Equal(Select([ArrayIndex(-2133215332), Field("�ѨA$=%0𑒌$𞴻{ῚL𒿎<*Hÿ)HಯmJ᧗ૉvকj¥𑶄ዅ"), ArrayIndex(1372867808), Field("9U𒐽ᅥטּ:^�ཬ{L\u{1a7f}🛞e{𝔊sை8%$-SޥJ=\u{1e136}&"), ArrayIndex(-1515605936), Values]), Newtype([[220, 112, 193, 144, 130, 2, 201, 3, 173, 97, 18, 34, 79, 189, 93, 185, 99, 8, 146, 72, 228, 21, 109, 226, 107, 211, 58, 9, 68, 90, 109, 0, 69, 37, 94, 48, 179, 143, 33, 56, 70, 63, 33, 221, 197, 203, 25, 245, 187, 130, 222, 127, 221, 178, 88, 248, 218, 158, 87, 24, 127, 138, 100, 104], bafkr4ighuk7gjom3gactib6iatiimrm4ytvtdk7y5k4pzutsg56i77lanm, -8.793256611809324e-208, [49, 12, 111, 228, 6, 196, 193, 26, 82, 212, 21, 13, 44, 254, 161, 77, 168, 161, 151, 85, 22, 53, 168, 74, 48, 15, 119, 171, 245], 3.675952639990706e158, false, null, null, null, null, -156.1567975459344, "\u{1070c5}\u{202e}m\u{7f}'#\t\":\u{dad72}ü4\u{202e}\u{7f}'涛\0$E{-/\u{202e}\u{7f}b", "\u{13e62}$e<\0&<\u{c5acc}ny\u{9a704}\"\u{202e}𔖚D", bafy6bzaceagsds7mbaud2m6qhdohvpgwuhfeedqwrntvioyma3kuo5v6vtyt6, null, null, "\r\tg:\u{46cc2}", [142, 185, 121, 120, 78, 34, 236, 132, 123, 49, 125, 116, 146, 139, 230, 17, 174, 47, 83, 146, 252, 30, 220, 161, 163, 207, 136, 121, 230, 68, 139, 61, 190, 89, 223, 74, 72, 219], [28, 249, 124, 123, 205, 232, 92, 8, 106, 47, 104, 13, 104, 201, 76, 228, 38, 128, 160, 168, 2, 34, 83, 71, 248, 91, 111, 250, 225, 49, 225, 194, 169, 133, 253, 173, 44, 47, 27, 4, 181, 140, 138, 211, 3, 136, 21, 139, 138, 196, 191, 191, 76, 163], true, [87, 3, 80, 153, 162, 125, 138, 138, 127, 8, 103, 89, 128, 18, 40, 41, 190, 156, 216, 106, 118, 162, 241, 107, 134, 144, 107, 52, 51, 165, 85, 124, 197, 155], "\u{2f39d}{¥\u{40daf}ѨS\u{a44e7}{\u{a7dc1}�\u{cee1b}'", [235, 139], false, "", "\u{a1d4d}\u{feff}\u{4}\u{feff}3\u{1494d}\u{e1172}.\u{1b}`\u{d2482}{\u{9e}\u{e1d5d}\"3/{\u{5}2].Ⱥ¤<\u{96fa3}Ⱥ\u{49ea4}*\"p\u{b83db}\u{6}\u{6}": null, "?/\u{bdd3c}\u{370b4}\0*\u{102d37}\u{cbc47}.\\Zâ\u{1ced4}KѨ🕴::\u{7f}/": 44, "?8?A:/'H\u{b}\tr\"¥=qu\u{b}¢\u{fde59}\"\u{2}ye'?\t\u{ea1b7}K`\t": -3.9299672377331247e-146, "?{\u{7f}%\u{7f}\r%\u{d3348}\u{b}\u{eb7b2}%¥\u{9406d}\rѨѨ],\u{202e}'\u{105dc6}<^úr\u{8015f}": "\u{9d}\u{891c2}𫩼\u{606aa}\u{45288}p!/&G:㝸ã\u{97}Ⱥ\u{b}", "?Ѩᆬq7/%\\\t\u{feff}\u{da5c3}\"\u{8509f}\"H`\u{1b}%\u{4d464}\u{479f4}\"\u{c147b}\u{1b}/𧳓*,|": "Y\\\u{7f}\u{b1fcf}\u{e99b9}7\u{9e18b}:?\u{ede3}$W", "?\u{5d644}\u{5a09a}\"�l\u{1b}/\06Ѩ.&{\u{10a396}": bafyrmihqvy27erjh5esd3x6wdgnr6z6lru7wfcmkuuvzp7h7am6z63mbia, "A\u{763e5}& \u{d7169}\u{e8031}{\u{b}*\"\t𰩧\u{548b4}?u`/\r🕴\u{7c626}$å🕴\u{10dd6}:🕴": null, "DѨ\u{6ebc8}\u{7f}<\\[?\u{7f}&ኵ:f=𫗿\0�\u{202e}": "s\\/;*\\\u{4150f}E", "F\u{1b}\u{10ea63}\r": -3.881911088191245e-265, "H.¥$": -21, "K\u{cbff5}\u{202e}\u{1260e}\u{feff}&:%\t$\"|&\u{342e6}*:\"&ju/c": true, "N\u{b}&\u{feff}\u{691d4}\u{6}\u{b}:&*Ѩ\u{6f0dd}w\u{e581c}¥z�\u{9cce5}\u{16d5d}\u{4f30a}**\u{93d8c}\u{202e}\u{7}{\u{1b}{±'{K": "%\u{feff}¥", "[𪲁@\u{1387b}\0]\u{6fca2}Bi.\u{89a95}\u{6a578}\0\u{4a64d}\u{b}\t\u{647d9}n\0^\u{8ff74}Yk\u{9c}\u{9c}(": [116, 173, 176, 186, 52, 23, 252, 79, 37, 2, 204, 64, 93, 133, 149, 198, 164, 255, 254, 251, 238, 69, 160, 225, 130, 239, 127, 62, 193, 179, 31, 95, 161, 114], "\\\u{6d15d}\u{b}&\\Ѩã\u{e6f74}<\u{a15a9}\tѨ,\u{b}\u{819b6}\u{3ef57}/\u{4ddf0}Y(\u{5}\u{100a6f}\u{b}/.:": -37, "]\u{3}\t\u{e5fb9}?{\u{202e}z\u{44ab3}\u{39431}\u{e8c44}¥\u{79b81}'\u{777e6}�:{%\u{3900c}{%\u{108b7b}<{`\u{7f}Ó\t🕴T.": 29, "`\u{6}.:🕴{\u{9c4a2}\\<*:": bafy6bzaced7s7j4l6dybvc55yypxhw5em5sg2ciws33rjhgce6hmfmpgf4fdu, "`o=\u{6}3\u{e81b2}\u{1b}G&\0tw\u{3}/\u{3d977}`㊈)=Y\u{2}\0": false, "`\u{44886}/\u{a1f9b}\u{5f978}:\r\u{95be2}/🕴": 0.0, "c\u{5}\u{4bbc4}b\u{c96f8}+%$\u{202e}'<\u{e078c}\u{ac7cc}x\"Ѩ": 53, "n": "\0?\u{efd30}\u{686e5}\"\r\u{5fe32}\u{b5b0c}\u{51f54}\t\u{89}=\u{33916}ZM¥\u{4}\u{7}m�}\u{91c01}\u{6839a}*\u{c59e9}", "oѨ`": null, "w\t\\`\u{da1d2}*&.$E;\u{62ec7})\"Y\r\"5\u{bd563}\u{326cb}{å?/\u{b}\u{98e3b}": "D::\u{202e}F\u{d9e2c}J'=\u{ec5db}\0'\"Ѩ\u{1b}\u{419e5}𮞿?E*mb\u{feff}_Z\u{ab92a}=\u{89c4e}逋\0\u{7f}J", "y6\t<": [179, 71, 5, 85, 100, 29, 222, 53, 97, 142, 194, 243, 96, 220, 13, 106, 64, 105, 167, 218, 123, 136, 220, 228, 82, 153, 8, 92, 185, 11, 112, 146, 197, 109, 163, 11, 117, 83, 66, 85, 178, 149, 17, 95, 212, 87, 96, 62, 216, 80, 38, 36, 236, 156, 22, 40, 44, 133, 95, 15], "{Ⱥ/&\u{7f}\u{5795a}_*\0*\u{a13ce}f?`/\u{1bd42}<𮧏<\\U(n\u{2ffdf}\u{bad0b}&]\u{feff}U🕴$": 9.93350022419413e112, "{\u{39ee3}TI\u{ea081}%Pï>\"Ⱥ\u{feff}{\\j": {"": "\u{feff}\u{1}\u{1}&=¥\u{68854}&/ö\\\u{33a28}\u{d8908}f4]Y\u{39e57}**Ѩ\\*Ѩ\u{5270a}{B¥+x\u{feff}", "\0${\u{8f006}:f<\u{3}\u{ae249}\u{a3dc8}\u{5eb29}\u{b67f9}%:\u{37105}\r\u{1b}'$\u{2faa8}$/A\tW'\u{909b7}\0\u{2}": 2.914324980421285e-195, "\0$\u{ac00f}\u{1b}9\r\u{3}\u{2fd7f}=:R.\u{e3705}\u{c4450}NG\u{73b7c}&$\u{202e}\u{b527c}GL": "\u{a3b69}6a*==?\u{feff}\u{1a1cf}\u{a82da}`/==�🕴\u{6b826}\rD?A\u{feff}\u{1}", "\0*{\u{202e}\t�\u{1b}/|Ѩ%©%": baguqefragwjjb3m3ljrschzrusb2unylas52kabmwfr34wqmvo6rkg2avvuq, "\u{3}\t\0\u{6695e}\"{»1\u{202e}\u{68a4a}N$": [233, 212, 54, 104, 52, 88, 23, 191, 247, 21, 223, 50, 174, 43, 59, 131, 37, 131, 62, 190, 142, 176, 67, 43, 184, 235, 120, 202, 175, 190, 189, 145, 211, 136, 59, 252, 222, 235, 131, 213, 187, 34, 118, 61, 42, 93, 166, 43, 180, 114, 34, 166, 57, 195, 172, 167, 175, 177, 81, 106, 26, 118, 209, 95, 105, 177, 234, 126, 58], "\t\u{1b}7🕴¥qb\u{8}\r\t\u{1062f0}%'\u{b}.g\u{411f2}𭕨B{Ⱥ\u{77264}/\u{b}D\u{3d766}<": null, "\u{b}/<\u{34eed}\0S&>�텐$$": ".\u{7ebf4}Z\u{d3912}\u{feff}?*H\"\u{202e}𦊇\u{91}Ⱥ\u{7f}\r?{)<\"\u{42b46}?T\\\rg\u{ec834}\\\u{7f}\u{7f}¥", "\u{b}?\t\u{2}�4¥𞸩*\"\u{578ba}\0G%��\\r%«=🕴=\\&*=": "\u{8237a}\u{8a}<\u{8ab59}.?<.tàô\u{945d6}\u{feff}`\\#\u{ce61d}c:)?.`a<\u{9d398}\u{962eb}\"&", "\u{b}ÞѨ¾y狦\0🕴\t\u{19d5e}\u{f0148}": bafykbzacebxelvnoczrdap55pukvsxpc6gicbmnqzgsouqceppvlhsicwet6e, "\u{b}ãx\"\\\u{4}`\r:\u{feff}ᦇ\r": 22, "\u{b}�`+\".?hC\u{88713}:\t\0N¥/Ⱥ\u{1d2b9}1&`Ѩ.": 0.0, "\rAf7\06`\u{ff186}\u{9f715}\t": "/$\u{eaf96}\rW.{{.l흄*\u{1b}\"🕴!?\u{a64ef}\\\u{7f}/\u{109cc5}\u{5}'\u{b}\u{fc0c5}𱇴\u{d1388}", "\rUKG/\u{3}\u{2}ꕻ\u{b}\t": -1.3995651248504633e262, "\r\u{72228}\u{3}\t'\t\u{c5bb7}\tv": [207, 209, 38, 232, 225, 181, 157, 174, 248, 85, 75, 104, 14, 234, 9, 86, 149, 189, 217, 176, 122, 32, 78, 186, 174, 19, 241, 185, 202, 135, 32, 184, 35, 47, 78, 189, 35, 153, 142, 128, 46, 7, 65, 55, 37, 215, 0, 109, 138, 244, 78, 202, 124, 93, 146, 143, 199, 70, 40, 153, 254, 139, 213, 103, 34, 98, 157, 146, 23, 126, 115, 247, 59, 186, 119, 63, 16, 179, 123], "\"%3\r\0\t*\u{74f05}\u{10ecdf}%\u{56b43}\u{e2041}\u{b}Ѩ🕴%\u{47ac1}\u{b4501}\u{10314f}\u{b}": 8.190682906409504e77, "\"üE8À%": -5.01070380812316e-309, "\"🕴¿%z4q\u{f5f81}\"\u{202e}\\": [99, 154, 31, 116, 21, 233, 127, 12, 159, 139, 166, 170, 113, 31, 35], "$&\u{81e34}�\u{feff}\u{3}🕴\u{b}\u{3e109}\u{8e8b1}A\u{1b}Ѩ)\r¥\u{c68f3}\u{10081f}\u{10dd8f}": bafyrwibh3uv7biwsbpe37hkwovhrpkm3l7s5icdgji6imq5fjykcl7p24e, "$'Ⱥ':\u{f6dca}{&\u{7d81d}GQѨ𧗿~.Ѩ\u{3f8b4}\u{feff}\u{2}\u{2}`\u{b}\u{202e}\u{7f}\u{8}\u{b45ad}": bafyrwiczadd6c3wkaraxq3lg5clhqyngn6fprrjscfzxfe2mp2kg4pvxba, "$}\u{4998f}}\u{7afe3}\\\0n?\"V\u{1}\"{!*y%\u{1b}\rꢢѨI\u{f13fb}X\u{1061f7}\u{7f}%'l\0=\0^)\u{5ac7e}<": [60, 118, 165, 67, 27, 119, 21, 252, 153, 133, 75, 209, 126, 27, 107, 9, 142, 133, 201, 242, 96, 56, 139, 85, 39, 185, 191, 172, 121, 16, 61, 101, 100, 93, 23, 114, 153, 201, 213, 220, 14, 1, 73, 194, 75, 71, 226, 125, 205, 187, 97, 24, 229, 247, 75, 87, 172, 90], ".`\u{bec17}Ç'\0/\u{d491b}𘅣🕴.\r": 1.2131909958473984e-266, "/\"": bafkr4ifg4p3nsgepq6lvarhhrt4lxydl5arndmqieaknvnqhyvw4l62eau, "/,/\u{b}\u{a9092}\u{3}.\u{7f}ñu?Ⱥ'&\u{e2082}\u{3d612}u": baguqfiheaiqibdayxvhi3ktwv4rfqhx7adreiuo2muqdzc72zj7wydxi2ircenq, "/.\t¬79\u{202e}<%Z\0\u{7}\u{b}&\\🕴\u{c07fd}j=(1\0𮟻mJ�.\u{fe739}ȺXB\u{feff}": [35, 209, 94, 4, 132, 12, 218, 13, 76, 94, 110, 176, 233, 104, 74, 8, 231, 103, 204, 48, 35, 1, 208, 81, 103, 39, 15, 209, 174, 232, 89, 234, 13, 222, 88, 45, 83, 226, 52, 81, 83, 59, 125, 241, 159, 38, 79, 5, 21, 197, 38, 169, 183, 154, 154, 183, 251, 209, 151, 212, 196, 132, 155, 189, 109, 45, 206, 253, 141, 141, 226, 210, 69, 80, 142, 13, 168, 40, 84, 52, 228, 159, 231, 76, 229, 248, 47, 21, 115, 15, 247, 164], "/.\u{b}\0gMg%\0\"n\u{1b}�\u{dc8ef}": null, "0\\\u{b2c71}:.\u{7f}.q\u{ee1a4}\u{3}.`:=\u{7f}shÕ\0'\u{ff5c2}\0<\u{48d2d}": null, "6\u{7f}z\u{fb43d}\0\u{e71c2}n\t\u{1b492}\u{be144}%m\0O£{\u{4}ß`S\u{cc20a}\t\r@\u{1b}\u{2f392}T\0�:4i": [184, 132, 8, 217, 132, 246, 183, 246, 210, 254, 92, 125, 142, 179, 49, 205, 173, 48, 36, 66, 57, 14, 184, 195, 88, 109, 101, 153, 91, 53, 120, 69, 198, 5, 81, 144, 203, 189, 119, 182, 143, 191, 14, 61, 34, 36], ":(`T\0\u{1b}\u{1b}\t$¥z\u{36613}\u{a5f2d}Ѩ\u{d1ec0}Ⱥ\u{7f}\"'u\u{b7fb7}ë` \u{feff}\u{5d6e0}f\rê🕴": [191, 71, 93, 227, 249, 253, 186, 190, 214, 13, 71, 250, 111, 182, 66, 219, 22, 139, 183, 203, 250, 58, 184, 20, 20, 213, 80, 32, 66, 40, 214, 69, 53, 203, 170, 113, 150, 95, 7, 238, 77, 250, 5, 90, 119, 215, 135, 80, 225, 91, 49, 36, 143, 153, 196, 238, 205, 224, 18, 127, 173, 184, 246, 90, 134, 17, 119], "=@\u{8f9f3}7?\\𫥂\u{1}": "i|*\u{c09ed}'\u{b871f}\u{4}*\u{202e}&\\\u{105cbd}bѨ\\*?{F\u{feff}uȺz\0`\u{feff}{+", "={": null, "=\u{a02a9}\\\":¥🕴\"\u{202e}P\u{7f}\u{b7f71}\0\u{3a7e9}Y<î\u{157d9}Å": [19, 138, 165, 245, 200, 2, 67, 217, 45, 240, 188, 107, 149, 30, 61, 57, 176, 165, 123, 163, 219, 5, 81, 31, 155, 252, 139, 204, 155, 201, 120, 3, 32, 18, 218, 98, 147, 233, 22, 106, 60, 86, 204, 202, 147, 32, 223, 159, 66, 107, 250, 251, 19, 167, 246, 252, 90, 205, 212, 54, 251, 5, 241, 43, 213, 120, 23, 1, 29, 163, 69, 234, 99], "?&🕴.*.\u{605a3}¥&j/=a\\\u{b}*Ѩ*'¾�ธ#Ⱥ\t": "\u{7f}\u{8}Y{5\u{60a62}jN=f\0'𩧦\u{feff}\u{7}//a¥kK\u{48989}&\u{f203b}\"\"`", "@Ѩ": "/*\u{95147}\u{b}{`\u{4e71c}", "C\u{3}<Ò?B¥@&": [130, 96, 105, 54, 223, 53, 178, 56, 46, 187, 18, 221, 159, 48, 247, 89, 117, 41, 108, 127, 91, 200, 247, 120, 240, 237, 155, 170, 65, 55, 58, 141, 247, 254, 60, 102, 29, 148, 211, 55, 4, 14, 68, 42, 90, 173, 222, 142, 20, 22, 68, 185, 132, 255, 132, 185, 189, 197], "D\u{a65a4}$\u{df148}\\F\rntѨK\u{1b}jd\u{8ea98}\u{b106e}�": 1.3784978135410346e-170, "Eò<.z\u{781d5}\u{2}\"�^": null, "H\r\u{361f5}{\"�\"\u{41b3d}\\'𦀳¥\u{80}Ⱥ\u{c26fd}�6-`N\u{dca3b}G\u{1de7a}=\\": true, "L\u{7f}:¥=t*{_AP\u{366cc}?wȺ\u{eb31}o.\u{881ae}\u{77df7}t\u{b}\0{\tѨ\u{d6c3a}": [97, 251, 86, 194, 220, 214, 153, 209, 190, 118, 25, 200, 75, 133, 240, 162, 84, 193, 128, 3, 238, 222, 6, 149, 222, 157, 239, 202, 16, 102, 50], "S=U\u{bff90}/?\u{da120}.\u{633c3}a|\u{1c959}\u{3be11}\u{feff}X\u{a9622}?\t.Å.🕴=\u{73c63}𰰅\u{61725}¥\u{a6664}<\u{895f4}i": [140, 113, 176, 106, 69, 9, 192, 221, 52, 44, 56, 187, 134, 114, 29, 65, 208, 39, 0, 95, 237, 224, 76, 195, 8, 225, 21, 98, 228, 60, 95, 240, 189, 156, 136, 235, 72, 132, 236, 170, 1, 250, 184, 134, 77, 48, 249, 199, 172, 3, 66, 201, 75, 15, 29, 254, 104, 111, 158, 53, 80, 85, 74, 36, 20, 15, 150, 52, 31, 50, 233, 6, 228, 244, 185, 202, 142, 56, 126, 47, 69, 35, 225, 162, 147, 42, 172, 213, 71], "\\\u{1b}Ç'(\u{76ac1}\r\u{202e}NÙ\u{87e35}`{\u{52666}Ѩ\u{7f}\u{e28bd}?窣\0\u{e4901}🕴\\": 1.307777027892035e-308, "\\V$\u{71773}\0.H𘑞\u{202e}`&)'\u{cc7d7}4_�📲j.\u{c5777}:\u{b}{\t?\u{abcf6}\u{2}\u{f114a}": null, "`\u{1}7\u{dc7f6}è\u{1b}\u{e6461}", "bF罩`/?{\u{6}m�🕴\u{a0dcf}\u{1b}\u{85683}\r*z\u{3}`\u{202e}\t\0¥\0ó�\u{fa710}": null, "f\u{71088}\u{eeba2}/\u{1b}\u{b}>\r:$H\"ï\\\u{7f}\0¥{+?": null, "k:\t<鏁\t^:u碗\tñr:": bafkrwidekl2tflko33qlm47p7adikj4q6bdju2kq5elnuipzykk565mhby, "s<Ⱥ*[L*<ѨÉ\u{88d19}": [88, 175, 1, 80, 241, 221], "u\r": -2.6904580729605092e122, "u¥\u{feff}\u{1b}": [1, 36, 101, 136, 222, 223], "x`\u{6d787}\u{e5350}\u{df1fb}(>qP🕴\u{ba6ac}𮠰🕴\\\u{4b5e0}J<": false, "{\0\u{3b4bd}=g¥\u{107402}\u{5c069}Ѩ=5k*$`<%p": [42, 216, 219, 136, 90, 214, 91, 194, 12, 10, 62, 81, 119, 54, 126, 138, 227, 206, 148, 138, 95, 191, 247, 89, 212, 142, 18, 121, 148, 163, 152, 177, 37, 69, 222, 22, 226, 117, 153, 5, 89, 234, 92, 155, 223, 10, 158, 200, 18, 37, 110, 103, 181, 109, 202, 35, 39, 117, 93, 5, 90], "\u{7f}\u{7e6e9}\u{8b}𥚝": baguqehrahkajpop4geeiwsi7gu2wdsuybkf6lbq6rfen4f7v3vrtqbyazd3a, "\u{7f}\u{b601e}'\rK/\t\u{feff}¤?\rp\u{af66a}": [73, 33, 204, 97, 67, 215, 167, 191, 121, 17, 85, 93, 75, 141, 150, 250, 223, 157, 229, 29, 48, 217, 58, 27, 191, 36, 145, 93, 14, 17, 12, 32, 61, 36, 83, 58, 181, 136, 104, 35, 237, 219, 146, 240, 223, 170, 203, 45, 27, 109, 190, 5, 96, 122, 180, 241, 211, 211, 147, 12, 136, 219, 25, 19, 108, 191, 73, 165, 95, 129, 7, 16, 40, 44, 123, 182, 100, 246, 148, 80, 99, 191, 137, 144, 177, 240, 82, 242, 163, 30, 210, 13, 45, 140, 6, 196, 122], "¥O\u{7f}Ⱥ𗢎e/\u{10697e}\u{202e}\u{7f}0�Ⱥ𤂹\u{e098b}\u{8383e}`G<<\u{b94d8}Q\u{e6032}\u{cf7bc}\0:\"?\u{1}\u{1a8a3},": null, "¥\u{202e}'\u{8d073}\u{78d6b}\u{6e610}\":E?\u{9bb82}Ѩ<�>\u{4c02a}//\u{a27e4}=\u{f456f}<3𧉟/\u{98}\u{f3520}": "x=`ᒼ\u{12af8}\"=", "Ò\u{51a90}�\u{1b}\"䩖𦄱\u{b3b7f}\u{aee50}<🕴{\\A\\\"X\u{f32ab}::\u{4}\u{c7c01}`\u{1b}\u{4}&\u{ff44f}": 0.0, "\u{86b18}Ⱥ<": null, "\u{a0feb}&¥\u{106aba}{B\u{be082}1\u{62718}\tS\u{aa928}\u{2}\u{3d1dc}\u{f5e89}": [16, 76, 209, 135, 206, 142, 16], "\u{afe7a}A`>\u{dafee}𡪩b\u{77678}%\r劗?\u{10819a}^.S\u{7f}\u{94083}$�\rA": 5, "\u{b2ccd}ë9('*\u{b}2\u{8}\u{c9f32}\u{5a462}\"?2\u{b}\0W�\u{3e6e8}th\\=\"F": null, "\u{e6ebb}r\u{202e}\u{80bd8}:🁚\t�\u{fd427}\u{b2fb5}\u{4badf}w/\"0\u{202e}<\u{15010}^v\u{53eb1}\0='": null, "\u{f31e7}41\r\u{68a9f}\u{202e}\t.p\u{4ea54}\u{860c8}\u{19987}l": -0.0, "\u{faf52}\u{6}¥\r\u{202e}䲖\u{202e}R&\r\u{973bd}\u{feff}l\\\"$\u{e5961}¥\u{d805a}\0+OB*tu\u{eb5a9}\u{109146}Ⱥ\u{759f5}`\0": null, "\u{fc398}\u{2}\\.\u{ee493}\u{d0de6};\u{ed8fa}🕴<": null, "\u{10ed00}?\u{7f}�`\u{b}L*\0®": -0.0}, "\u{7f}':e/Ѩ\u{36fde}`/\u{202e}\u{7f}=\u{feff}🕴\u{f28b6}\r": null, "¥": -0.0, "¥ :?\r\u{70234}": "�¥", "¥i🕴\0\u{93569}\u{71ace}\t:\u{75563}*+": false, "¥\u{be29b}\u{202e}\u{7ef68}5\u{1b}`\u{202e}\"\0\u{feff}\u{10e7da}�\u{7f123}|\u{8c8c5}n": 11, "±:𢽭🕴t�%r\u{8}?`^'r": false, "Ⱥ/\u{882e4}:\u{e0f71}$%/{\u{36b45}:,z\u{8}[1\u{7f})%\t\u{b}v\r\u{aaa37}\u{8}𫷝\u{7f}\u{46db0}¥\u{ad5a1}ke": -12, "🕴\u{feff}\\x\t\u{6}\t~\u{39f68}\u{202e};�\u{1b}6:/3\t\u{7f}": bafkrmiepyixjho2atqqcqgtd575iqsam6klfzdtkx5g4wummups24pdqcu, "🕴\u{5de4c}\\�𐝣\u{feff}𱴜\u{feff}": "\u{6}\r:\u{190a7}�%Ѩ𲇭:\u{6}_🕴Ò%\"'\u{e1b6d}F'\u{f7ad8}\u{57ad7}`\u{9c70b}$`\u{202e}\u{4b5bb}UK\u{90}", "𣉓��e\"$0R\u{a837d}\0<🕴?@,": true, "\u{2fa70}\\𪚅¯\u{108509}\u{4ab0e}\u{4ecc6}\u{bebaa}B\u{1}\\\u{f24f8}'%ð\u{202e}¥\u{3a476}/%Ⱥ\t$vyC]>+\u{87304}": [94, 245, 167, 7, 68, 250, 234, 108, 122, 64, 21, 238, 150, 215, 116, 48, 232, 184, 232, 43, 30, 213, 149, 183, 215, 1, 242, 95, 189, 228, 190, 9, 43, 222, 14, 254, 203, 238, 30, 51, 216, 40, 125, 87, 240, 71, 185, 206, 114, 87, 214, 85, 33, 163], "\u{373f7}": true, "\u{4f31f}*A?\\${\u{91276}\u{52eb1}#\u{b}\u{1b}": -47, "\u{5e975}🕴z${'\u{5d19a}b\u{aa904}'J}`Ѩ9": "\t=\u{1}±\u{84533}T*\u{4d0eb}\"A\u{8}<", "\u{69742}o\u{81}n<@\u{feff}L:\u{1041f7}🕴\u{8}𬾧\u{7f}\u{108007}?[\u{feff}\r": [31, 200, 160, 81, 110, 124, 249, 10, 85, 215, 192, 121, 17, 28, 185, 121], "\u{6cfa0}\u{6}𡽫{\u{feff}bJ&\u{5}`x\u{d2999}d&\u{badf8}\u{1b}\u{365dc}'�\u{5a37e}": null, "\u{a202e}~QE?\u{b}\u{7}/Ⱥ/\\&\u{fb894}\u{e0565}\u{2}�^?Ѩ'": [146, 41, 216, 116, 157, 252, 155, 192, 137, 113, 110, 231, 49, 142, 206, 2, 93, 201, 68, 48, 93, 58, 173, 197, 93, 40, 41, 112, 193, 244, 79, 228, 221, 71, 177, 129, 102, 97, 55, 137, 3, 148, 166, 101, 253, 166, 170, 139, 23, 120, 178, 140, 132, 104, 89, 46, 120, 30, 154, 194, 138, 39, 103, 152, 6, 136, 237, 241, 138, 193, 248, 33, 185, 56, 220, 118, 23, 17, 132, 220, 239, 90, 207, 237, 94, 75, 90, 222, 46, 180], "\u{b2ebe}*\u{84}.\u{a909e}7ѨjV𠷃Ⱥ?ü\u{6}u𝣒L\u{34303}:\u{bc294}?DZ\0\u{7f}/\u{103c3f}\t\\": bafkr4ihqz56mmzigglx6apxygvxcnreb7hjve6eftqfcooxtdzyzahyil4, "\u{c3588}*\"\u{8}\u{51cd1}躅\u{8f2d6}u🕴*\r\u{1}7🕴\u{71aa7}{\u{77c1d}%\u{cf015}\u{7a969}\u{8}\u{3cbf9}\u{92}\u{b}*\t\u{15db3}": "/{ªN?\u{8711e}=\0g{\u{202e}\u{202e}¥.\u{1b}H%\u{95}\0r%:", "\u{d7ffe}\u{44f52}\u{1b}\"%ỵ*m\u{5}$\r¥Ⱥ\u{1b}Uo':\r/%'<Ⱥ": -7, "\u{e0680}={\u{fd1e9}j<'CѨ�3\u{ce0a4}Ѩ\u{4b081}C\"`%9GGv\t\u{10322d};Ⱥ\\\u{e1a9c}\u{202e}\\": true, "\u{e5e60}\u{b}{\u{e3551}=\u{b5edf}<Ⱥ\"#?\u{7}\u{80}\\'5f=\":𪟤=\t\u{dc111}*<\u{346be}": true, "\u{ebf8a}Y\u{8}\u{f98c9}\\\u{7f}9🕴\u{4e9df}=\"t": [222, 226, 183, 11, 204, 93, 208, 118, 17, 243, 19, 124, 63, 137, 157, 195, 178, 3, 70, 223, 216, 164, 164, 66, 246, 151, 76, 100, 93, 11, 7, 234, 146, 38, 18, 169, 97, 221, 8, 133, 14, 197, 108, 213, 201, 214, 93, 202, 188, 165, 171, 86, 195, 223, 164, 6, 51, 234, 214, 26, 158, 32, 12, 35, 10, 245, 171], "\u{ffc11}#\u{b}¾": null, "\u{101fde}\u{b6522}'.&$": {"": 1.5658615101276272e160, "\u{2}=\u{5bd45}?o뗡p\u{2}\u{b}\\Ѩ¥\u{8b3a4}ib=": null, "\u{3}𤥺": null, "\u{6}¥\u{feff}%\u{202e}�Ѩ%\u{10df7a}$%\u{2}\u{b}\u{59972}𝝊\u{5107d}\u{202e}\0.k%V\u{55f7d}±": "`\u{7f}\u{4e0cf}\u{f7dab}\u{7f}zw[Ѩ.\u{b}Ⱥ\u{202e}\u{1b}#�\u{61e5a}\u{569c2}:\u{51a0f}\u{1b}*R\u{bb37d}:🕴{+Rf\u{ea9f}", "\u{7}Ѩ\u{1}\u{dca54}t": 17, "\t`": [25, 228, 9, 39, 85, 50, 121], "\t\u{2ef3e}\0": bafkrwibffxly7lxjvxuvp2ic3ao6f2icoflqqiadi5dvz4yvpdsyd27jne, "\r*\u{1b}\u{4}{`_\u{feff}\u{b}¥.K\t<\u{3}&\u{7}$🕴\u{7f}": -40, "\r\u{587e8}Ⱥ": [207, 53, 166, 39, 184, 202, 32, 200, 7, 155, 18, 5, 102, 226, 211, 93, 116, 183, 163, 131, 161, 249, 63, 112, 198, 231, 166, 86, 176, 31, 56, 43, 28, 142, 150, 56, 244, 64, 75, 60, 122, 214, 109, 138, 103, 217, 188, 127, 42, 7, 30, 47, 190, 51, 7, 110, 190, 106, 215, 102, 86, 94, 41, 76, 159, 221, 62, 236, 96, 83, 215, 26, 102, 233, 126, 117, 233, 43], "\"z=%`\r�ñ%:Ⱥ\u{ee132}.}Ue'\u{8}\u{a6c62}": bafyrwiefif6srseryrwhg7lx5ddm3cuzdwepr4rmvorftfhq2vj57d2gsq, "#\u{75c49}!": "\u{7f}\u{5a95d}L\u{32a81}\"{\u{7}¥²�\u{e3087}O'!\u{ab00c}\r./ \u{202e}h🕴'", "$\0\"\u{ca931}\u{2}\u{1}:\r\u{3}\u{63225}g¥Ñ": true, "$Ѩ\u{bd470}û54Ⱥ%¥\u{feff}": null, "$�0\u{a8b97}𱗶\\0\t$": 6.488444060884696e306, "&'\\w:<<%\u{feff}\u{5}\u{1b}`¥/?n𡚋&,4\u{5e1f3}\u{4b359}\u{fbe37}i\r\\\t:": true, "&?\u{6}r\u{c3665}\u{7f}i%": baguqefranz7i2sd2lryjkxvq7st4ujh3xjdttm6fldqgbci5rlbzwbet2dma, "&?%": bafykbzacea3napu4rwrzbqevwhiaptytsgo3gscof2glvjfje6h75nyc5npb6, "&\u{7f}\r}>`\"%\rY\")>\0\u{1b}X\u{b65b4}": [93, 89, 232, 156, 166, 193, 205, 122, 207, 235, 2, 186, 202, 177, 228, 245, 238, 222, 129, 191, 32, 176, 162, 238, 204, 162, 152, 39, 105, 236, 197, 76, 201, 44, 148, 170, 224, 150, 87, 94, 134, 189, 238, 51, 118, 124, 144, 5, 252, 27, 17, 111, 250, 236, 173, 159, 46, 45, 170, 32, 133, 60, 134, 50, 28, 47, 178, 197, 160, 126, 143, 31, 160, 25, 175], "'d'\u{4e27c}\0\u{b4b7c}`{?\u{3cff7}Â<\0": "h\u{feff}\u{15730}\u{c364f}]WV.TH\u{feff}.Z\0.\u{9d3ae}$\u{e5559}?\u{d403d}\u{7f}%(k𠢘", "'i🕴\tb{\u{7}\u{feff}&\u{49079}'S/\\\u{10600a}\u{af8aa}\u{3a6a6}\r": false, "'ѨH8'Ѩ{\u{f5f05}ѨQ\u{d5ba2}\u{3773e}::\u{eb168}:)CȺѨѨ": baguqehra3ml5lg6x5oaboqvr65pulvfntbvdrj6v5zyvuaf2j7bpx7wkhbpa, "*I¥w\u{b}\u{7b9a4}\u{3e747}\u{b2e81}/y<5'*S?$$O\"\u{7}\u{9f10a}\u{8}S\u{7a8bd}I\u{7f}U%": null, "*\u{202e}": [61, 242, 249, 132, 34, 222, 153], "*\u{861a1}//\u{10ca95}`'𓀒\\'=Q;\u{1073cb}M\u{5dbd9}": [34, 41, 251, 216, 124, 171, 190, 175, 12, 20, 65, 118, 149, 193, 33, 184, 96, 7, 181, 39, 92, 97, 36, 84, 72, 99, 69, 241, 55, 2, 31, 74, 55, 172, 146, 32, 230, 228, 234, 148, 164, 27, 55, 71, 222, 203, 70, 239, 15, 85, 157], ". \u{202e}v'*<\u{3c008}\u{ea8b}c¥?g:5$Y¥\u{5a3ec}": [60, 209, 45, 129, 120, 83, 199, 46, 198, 62, 131, 13, 210, 117, 41, 136, 9, 59, 142, 242, 198, 194, 181, 206, 19, 221, 114, 94, 216, 25, 58, 191, 30, 252, 131, 130, 133, 109, 116, 32, 231, 108, 160, 173, 195, 103, 78, 179, 165, 157, 227, 152, 245, 222, 164, 242, 208, 136, 66, 6, 54, 146], "/'%\u{3b4df}`𫨁e*\u{71a70}=\u{c5203}\rȺ궖JȺ/\u{4bb7c}\u{3}*#\u{d3bde}p\0qÙ\u{b}🕴\u{202e}\u{202e}r.": false, "/{H\u{10c9ea}\u{79be5}?\u{ca12d}": 8.6395723193706e-309, "4\u{54c08}'`?": -8.973748901720123e-29, "6\u{883eb}Ⱥ$\u{1b}": -1.0199828187670042e-159, ":<\u{1a04a}U�": -1.8179682492493788e-167, ":n\u{8a}\u{8}?𡜬\u{7f}F'ýr0\u{1edbd}%\r8oѨ\u{1b}": "", "<=🕴\u{dd6ce}\u{3}\u{65218}�\u{b3002}\u{81}<\\//z=\u{6106d}*Ѩ\u{8090a}hC\u{88ce1}\u{1b}\0🕴\u{ac262}\r/": null, "?*\u{a6066}aѨ\u{7f}\u{3}\u{1b}`'\u{aa717}\u{14b1c}o&\u{8d098}É?Sv\u{7f}¥`\0\0<]\u{15ce3}\u{1}=:$=": [66, 253, 182, 100, 225, 215, 132, 63, 183, 229, 8, 172, 23, 12, 49, 232, 47, 74, 175, 130, 83, 252, 88, 185, 110, 27, 188, 151, 251, 208, 54, 250, 230, 62, 210, 35, 23, 251, 21, 206, 161, 29, 185, 225, 190, 48, 125, 83, 49, 116, 98, 148, 46, 18, 204, 43, 23, 115, 8, 245, 113, 58, 152, 205, 222, 26, 169, 223, 244, 99, 43, 185, 3, 45, 50, 190, 66, 69, 188, 185, 150, 232, 128, 102, 195, 99, 78, 226, 251, 253, 84, 106, 110, 178, 192, 203], "?\u{7f}\u{b}\u{9f2c9}i\u{10bb8b}:{&8\r𠒫*t�\u{1}H\u{98f18}\" \u{c92b6}\u{8})\u{7d0c3}<Ï": false, "?\u{69bad}I&¥@🕴n\0??C\u{607cf}\u{7f}/&\u{b}🕴..qѨ\u{1b}": "𘉢`\0=\u{cc51f}\u{a4fb8}\0\u{47e89}/\u{b}\u{a0a22}±\\\u{feff}\u{feff}/.ck.\t\u{e3434}j", "A\u{e0fe6}'\u{202e}`%\u{202e}\0<\u{c18d9}/:\u{10b6bc}b\r*R:\u{feff}\u{3}\u{10ba3c}\u{84727}\u{c8ebd}^": -6, "C\u{c5e80}*g🖻\u{66e08}\u{9d567}{'Ѩ\u{58cb7}\u{9a1ca}¥Ⱥ\u{19181}\u{b1066}\u{5b8ee}\u{b9fcf}= =\u{dfbde}\u{18d8b}": "\u{5}\u{4d180}M\u{b}\\.Ⱥ*è=.e", "I*\u{96}\t=$\u{4747f}\u{5}\u{1b}n=a&rȺȺ\u{6cd13}": [230, 44], "R´\t\u{1}&\\\u{1b}kN\u{1}{´<\0\u{95a25}d": "k*`$U,\\\u{feff}\t\u{916a3}\u{849e2}<䨑\u{a2dd6}\u{9126c}", "Yq🕴\u{c5293}y:": 21, "Y\u{101e9f}JE{\t\u{7f}\0\u{4a162}aѨ.\u{7b388}댦\u{81d62}Ѩ\u{a6fb0}\\\u{dec90}\u{1}\r\u{b}/": null, "\\": -7.477551823968463e-129, "\\<\t\u{e9e4c}\u{b}:\u{a258b}¦\u{53415}5G\u{7}": true, "\\O\u{9e0a1}K.𠓎\u{fb34d}\u{7469e}\u{4c980}.:\u{bbb4f}8\"J:/\u{829d7}:\u{1}𣂇Ⱥ*J?\t\\\0\u{b}Ⱥ\u{ea85b}`": bafyrwihiw4vja2le5bbo63lbzgzdc7npl6b6ixw6nlheeirllwo5zpuzuy, "_]:\u{e11f7}¥On/<:\t=\u{1b}": "G\u{b}\u{71804}'🕴Ѩc\u{feff}<%Ѩo\u{1b}¥", "e\u{70c00}\0¥\u{202e}": [207, 96, 93, 148, 103, 198, 140, 36, 26, 221, 215, 6, 202, 231, 233, 247, 107, 178, 240, 144, 50, 206, 143, 13, 95, 15, 171, 239, 158, 75, 248, 192], "f\08<\u{9c37d}?\u{10f8f1}=`\u{95}Ⱥ{.\u{1b}\u{15cdd}\u{202e}Ⱥy<$\u{8}$\u{6c5d0}K\u{c4f04}\u{19f0a}": bafykbzacede6nne55uave2wj6epfrbud4xcozirijfysx2p3a4isvzez7oiza, "gÔ\u{b}繎.U": null, "i\u{889f5}r\u{e98e9}<Å\u{ad256}\u{b}R\r_%\u{d8e2e}/%r\u{b908c}%\rz~'\u{e04db}.\0\u{d9659}$\u{7cda4}`)": null, "u\u{a1658}0\u{f2355}}🕴\r": -7.035669443392254e-42, "v\u{10b659}�\r!o=\u{a24cd}]XÂf(\u{859d0}w\0.:": bafkrmihatl6qchcaitd2pcbxodwzlgbynl4rpzu6fnfcy3wgudnxpnabpm, "{%\u{61d33}=/EѨ/\"?�\u{5ca71}0\r": null, "{/:/JK\u{62214}Ⱥ¢\u{b}1o\u{5d859}\\\\\u{b65b6}*¥Ⱥ\u{1b}ѨѨ": bafk6bzacebf3fnemqzl52apgxmcok4gweclupgjrxwtee6ci75qsawcnyjuw6, "{:\u{f901d}\u{da30b}*:𱪙\rF\t\u{405de}\u{feff}%~\u{7f}*=\u{46e3e}\u{b}\u{202e}": true, "{f\\\u{b}\u{1}\u{7f}\tD\u{b}&Ⱥ𣡼\u{e84fe}J\u{d2ef7}E\u{1}'\0f\r\u{6}\u{4a79f}Dx\u{2}": -13, "{\u{d9a04}&ö%\u{7f}\u{d686e}&´": 7.588162654188097e-308, "\u{7f}\t\tj\tw\u{feff}Ѩ�練\u{3}:𧸨\u{a3acd}\u{101251}'\u{42a8f}E\u{3e47c}\u{7f}\u{7}&¥%<𩌯c": bafyobzaceco6clzsn57sjq2u5patyvukq3ksi4wyrztinj5srwspsi63qyfqc, "\u{7f}c\0\u{ffc41}<\r^Æ\u{9f}Ñ\u{b6f14}U\\\u{c45cd}\\\u{1b}g\u{ed9b9}🕴{\"R\u{60150}\u{b54c0}I:\u{10a33a}": [126, 247, 86, 174, 51, 26, 21, 148, 201, 8, 130, 49, 162, 197, 14, 242, 15, 142, 249, 226, 194, 131, 116, 158, 109, 70, 138, 10, 113, 85, 166, 21, 217, 49, 112], "\u{7f}\u{39cc5}'🕴\"\u{71cc2}&\u{7f}'": -4.297679451122769e-29, "Â\u{4}\u{feff}<'\u{c2f64}ѨȺ\\2\\\u{41534}*{<�\u{4a196}'8$\u{f88db}\u{feff}\u{feff}\u{9e1a6}*\u{cc465}\u{101f58}\u{dc98c}\u{b}ȺP\u{87281}", -29, bafkrmigi5ucblo6m6dbnw2y7po27ahssm6r6gwfog5w2eaxd7ze64u3vki, "(", -50, true, 4, null, null, false, bafyr4id3ardj34fsaufowtepdegrlkvrkv4jmyiva54gz4afht26prarau, [206, 72, 170, 13, 12, 162, 106, 248, 83, 67, 95, 57, 191, 163, 90, 243, 125, 164, 14, 6, 150, 168, 71, 206, 239, 41, 111, 27, 246, 79, 53, 193, 98, 168, 210, 107, 64, 164], -3.33502927060181e-309, -49, null, 3.5445664834722476e19, -1.563337693800014e-308, bafy2bzacedec3zioqwt4bdhiel5jmdrot2fhkwtx4u3fqug5x5djkkgop6rbm, -7, -18, [216, 49, 164, 168, 254, 77, 204, 85, 63, 36], baguqfyheaiqipcrhl2ltrwj5ykc3gnwp2i4i6m3wqqrmfi4ap7uchecatjrmp7y, -0.0, [64, 148, 93, 100, 165, 243, 24, 181, 67, 141, 104, 128, 194, 110, 216, 253, 234, 40, 83, 43, 194, 82, 152, 207, 225, 141, 174, 148, 236, 77, 110, 231, 110, 107, 120, 161, 188, 242, 70, 187, 10, 8], null, null, bafyrmigihlvyxai27ay3ws4ytyoickvlnpli3rxing2zlvcrwklqgvhp6m, null, 8.12819559637677e-221, 14, -2.3233856055803314e-132, true, [33, 203, 100, 207, 226, 57, 37, 141, 9, 44, 127, 89, 88, 208, 26, 54, 107, 192, 81, 78, 170, 47, 90, 250, 222, 53, 73, 136, 60, 141, 50, 179, 193, 1, 240, 61, 212, 102, 158, 81, 130, 103, 57, 0, 144, 186, 253, 79, 200, 167, 150, 245, 107, 237, 87, 24, 222, 200, 67, 180, 136, 164, 87, 137, 92, 199, 194, 10, 174, 65, 1, 168, 43, 25, 36, 186, 55, 153, 217, 27, 78, 197, 170, 47, 74, 87, 78, 123], "&t\u{51b31}\u{10b5b7}·Ѩ$\u{491b7} \u{e82db}S`\u{13ba2}$\u{feff}\u{3}🕴V¥\t{*\u{79d01}", true, 32, true, 9.168173353619497e259, null, 0, 41, "🕴\u{6ad7f}/V\u{8d90e}\u{7}\0$\\\u{1de07}ѨDw'\u{5f7ad}�$$🕴qa", [183, 153, 172, 129, 204, 68, 233, 142, 34, 132, 183, 94, 210, 53, 181, 118, 12, 36, 147, 178, 163, 147, 226, 164, 22, 84, 47, 52, 68, 11, 3, 202], true, -47, "'\u{7f}\u{9f}{\u{4bd1b}${{\u{9f533}\u{4bd4c}$\u{49671}`\u{5}🕴|�k\u{c46c0}\u{1}🕴*\u{feff}7\u{c7f8f}\ra:", 24, [124, 23, 6, 17, 136, 174, 8, 255, 36, 94, 92, 161, 178, 30, 64, 18, 140, 103, 208, 64], "\u{41adf}\u{b}\"\u{bb2f8}\u{7f}Ⱥl**", bafybmic7lxxicy772aqqwgxzod5qnr2q42gvznf7ybo3focmp5garzkofe, false, null, -1.453409372917667e275, -4, 37, -6, false, "H\0O\u{de3c0}I 욽;\u{e55dc}/qY*\u{9b}$\tHô^\u{c1add}.\rȺ:&_\0�\t\u{f05bb}\u{a5ba9}\u{fac7d}", null, true, "U\rȺ\rÖU\u{3e4ac}:\u{a58ed}/|\u{b}\u{74e95}$\\[x\u{b652f}'/", -48, -3.21819364036035e-56, 9.213513078060828e-91, "'\u{890b8}=\tx\u{d5bf7}?\u{a4dd2}\u{9811e}\u{a5c67}": 6.664526628123302e214, "&\u{54d6c}//\u{d211d}\u{7f}�": "¨\u{202e}\u{413db}T\u{1027f3}\u{b}O`#\\\u{a494a}\u{3b0c6}s.\t\u{103dc}:u/\u{a5e48}A\u{202e}\u{77c5c}🕴ȺmM'\u{6a3fe}K", "'\u{1}": "\t🕴\u{eab5e}\\\u{bab9b}\u{ee77b}\u{52d9f}\u{ab141}{\u{6}", "'\"9%?": null, "'#\u{6}\u{bdef5}¥\u{ca946}\u{7f}\u{202e}": 1.7292683522733168e-61, "'M\u{d3d34}\u{202e}$\u{3}\u{42800}𗸴\u{7f}\u{62be1}<\0¥<\u{202e}\u{feff}/Ñ4\u{f92f7}{´": 9.820150129594855e-167, ")C\u{ddeef}\"\u{c2e4f}": [219, 75, 110, 213, 246, 194, 60, 45, 146, 98, 88, 191, 123, 141, 55, 76, 57, 2, 29, 8, 227, 206, 96, 136, 115, 139, 137, 243, 222, 156, 251, 57, 28, 97, 118, 49, 218, 195, 184, 189, 188, 165, 205, 70, 190, 145, 58, 42, 104, 26, 29, 3, 218, 118, 2, 133, 228, 7, 235, 110, 91, 232, 215, 78, 99, 2, 64, 17, 72, 56, 70, 160, 255, 37, 168, 19, 29, 27], "*´\\yn<%\u{3cb12}\u{99}&\u{5}Ⱥ/\u{7}𭆼\u{4263d}\u{88dca}Q<\u{efab0}\u{f6ec2}": -1.1059496664576365e153, ".\"\\\u{3}{\r\r:�\u{81c10}\0Ⱥ\t>{\\:\u{e877b}\u{5e93f}": [65, 186, 167, 66, 51, 133, 164], ".e$Ⱥ6\u{63ca7}Ѩ`u\u{33bd2}'n\\\u{48590}\u{e646}Q\u{a08f7}.Uf": null, ".\u{9d849}\u{f94cd}=𖩡\u{bc869}¥+$5?\u{1016e0}`䎟\u{1043c3}\u{6c91d}\u{667af}🕴": false, "/\0\rѨ^\u{1a826}\u{202e}\u{7f}\u{fb3dc}\r_�\u{3}\r\u{7f}&ï\u{3}:8\u{9ed75}q𪥇\u{1b}\u{e8fa3}\u{4}𧽨\u{4efe1}\u{977d9}\u{3c6dd}\u{cfeed}X": [17, 67, 94, 98, 195, 0, 112, 199, 95, 233, 81, 197, 28, 69, 112, 207, 197, 72, 18, 50, 114, 108, 53, 31, 197, 3, 225, 75, 134, 216, 253, 203, 70, 71, 48, 33, 235, 32, 133, 245, 9, 224, 57, 91, 77, 233, 171, 9, 39, 186, 240, 49], "8:\u{c9e98}\"%1🕴<🕴\u{977c5}띦b4'": true, "9Ⱥ\u{84761}a\t\0": "î+%\u{202e}\"\u{c9bb9}*\u{2}\u{eb474}\u{4c05d}R{=\u{feff}\u{e7f1b}\u{1}\u{2}<Ⱥ-=", ":<'": bafy2bzacebdndpakl7grp6t2qzddcmrtpw4klywtvk77qimmd5fya7havzbyk, ";\u{8}�\u{1b}P'Ⱥ": null, "=$ȺN\u{e377a}\u{5a12b}`fW\u{7f}\u{2}:\".": -1.284865551557179e224, ">:O\u{3a925}ÿ\u{5}�{": -20, "A\u{7f}$'\u{b}¥\u{7}\"\0\\$=\u{b}\u{1b}�\u{feff}\u{b}\u{7e7a2}\u{b5184}\":\u{feff} D\0=\u{7f}耋Ⱥð`ࢁ": -41, "J\u{1008ed}\"\rLq\u{5}7": true, "[啡🕴mѨ®\"\u{100589}{\u{8}u\r'\u{b}\u{47a49}}\u{7}\u{1b}R\u{b4ad6}\u{b45f5}\\Ⱥ`'?": [234, 239, 156, 159], "].`'&\u{7f}^&.\u{202e}\0?%?¥\u{9929e}\u{7dd49}?a": 6.8070914168451965e-68, "_¦\u{ccda8}O\u{10bdee}fk$\u{feff}`픰>\r'�f➦\r|{\u{feff}laA=D\u{dc4db}\u{5f977}z": [142, 251, 14, 32, 228, 8, 15, 49, 217, 216, 50, 236, 66, 67, 25, 55, 193, 159, 240, 75, 84, 154, 198, 182, 176, 139, 245, 12, 189, 16, 217, 208, 9, 19, 164], "f\u{3f22c}\u{7f}\0.Y?'.*\rѨ)Q¥=�\u{6b46c}\u{42bbe}\u{ee66}": bafkr4idibino2zrwzvmr7jw2oqbramjdedkkhhexizf2kgnxongndjyvvm, "k\u{e0bf5}Ã\u{202e}<\u{202e}**\t\u{656ee}<&𘞄\0\u{b}*.b\u{1b}\u{b}\u{8}\u{c9220}&\u{36fa6}.?]G<ȺG": bafk2bzacec3uxd2irqv3kuoh6433iq7cdb2rkr4jes4ibxqlagobh7kcbz7b4, "pmOAB\u{b}\u{1094f0}\u{c3774}?\u{10918f}¥": true, "v\\\u{109f70}7`@\u{4}c'%!\u{7d32d}Ѩ\u{1f298}i\u{4d1e4}'�\"]�.\t𝦈": null, "v`\u{1b}\u{58c73}\0\u{101d7f}\0\u{d946b}\u{7f}\t\u{f2bf}\"\u{831aa}\u{60049}\u{51864}&?祕u\u{10403a}D]�\u{578ee}\u{ab4b8}:/ 𦁋R7Ѩ": -24, "{\"C썶\t\u{eec8b}<\u{ee555}\u{7574e}Ѩ\u{7f588}\rѨ*'\u{5eccf}%w{\r\r\u{b67d7}\0^kᗩ\u{1c3dd}G\u{8}`": 18, "{%🕴\0Ѩl%𤍭\u{feff}🕴\u{96}I\r=": "¥\u{93d84}y:w={", "¥~\u{566dc}9\u{ec076} \u{bd2c1};": false, "¥ȺD\u{1}\u{6b992}\u{434c4}\u{3}\t\u{1b}?\u{2}𥹩\u{9cb8c}\u{7f}𢲦N\u{e11b5}Q^{Ѩ\"w\u{505da}{": null, "Á-`^�\"\t\"`\"/<\u{1}=%�=\u{f8043}\t\u{3cb4c}\u{af74a}^\u{356ed}.:d?\u{65101}\t;\u{d0319}\u{b6b3a}$q\u{b}": [163, 128, 176, 15, 181, 109, 115, 80, 122, 244, 94, 32, 58, 251, 121, 38, 145, 131, 101, 44, 148, 129, 190, 123, 55, 19, 64, 213, 134, 70, 238, 108, 62, 157, 43, 176, 76, 240, 174, 14, 182, 98, 16, 190, 79, 42, 142, 212, 204, 192, 112, 130, 89, 121, 0], "�*??*e": [172, 83, 171, 22, 2], "�\u{9b09a}\u{15e59}\u{7}/\0E\u{6}\u{3cb2b}\u{4645e}\u{7f}\u{2}": true, "🕴": 41, "🕴\t\u{f0635}`Ⱥ\u{c865b}h\u{f39b}\u{fd001}\u{f197b}¥F/?\0\u{52376}%Ѩ/\t\u{b}\u{9a1af}\u{9b1fd}\u{feff}�\u{8f}/bt`k": [235, 24, 242, 136, 3, 90, 188, 21, 23, 10, 85, 3, 90, 163, 176, 209, 186, 96, 160, 240, 221, 232, 34, 228, 54, 35, 11, 200, 6, 93, 92, 140, 92, 36, 62, 178, 157, 50, 12, 66, 22, 208, 28, 217, 177, 9, 79, 248, 77, 198, 73, 78, 120, 28, 102, 200, 197, 67, 232, 133, 41, 38], "𥇖`": -3.1043921612514823e111, "\u{fdac9}\u{2}.𘠺🕴:'\r\t\u{3}嬊": bafk2bzacedwvgub3e2lthnyghripac4wx5ld6z3tavnb3iws5vdjukng5fwsk}, "$\u{890b7}&🕴=Ѩ\u{e0cc8}\u{7f}\u{8}\u{b}\\\u{ed02b}🕴°d\u{7}\u{202e}L\u{6d2ae}<\ta{*": {"\0&/\0&\u{8999c}.`%\u{feff}": baguqehraxkytankub52h3iajclairokepzb3skwifyzrg5qnfmbz4g4oodza, "\0🕴\r\u{2}`%O$%\u{de829}": bafkrmiht3e7seikbsbinibsxaqh4je5pzdcd4urxrwswdctilymrairlqy, "$\u{1e406}\u{1ea29}\u{7f}\u{c9cd8}𮖲X": [168, 224, 17, 123, 200, 96, 201, 98, 223, 37, 175, 231, 138, 28, 166, 190, 55, 123, 102, 20, 44, 208, 18, 184, 161, 233, 19, 136, 101, 164, 157, 248, 80, 214, 88, 79, 112, 146, 242], "=`C": -23, "\\8\u{d9453}($�[\u{539d4}\u{90f0d}\\u�\"$\u{bf277}\"\u{749e6}": 9, "wѨ&\u{f18dc}\u{8f209}\u{8}/\t\\": bafkr4ibmkkkbmtivel5pdx3jrmhp4wb5bd7rqzvoovp2j4b5hn2rtovype, "Ⱥ\u{b}\u{10433a}\\\u{1b}{`!\u{cd71b}¥<`ȺDȺ\u{d310e}\u{fee4b}𭨠0\u{b6c98}": true, "Ѩu鉏<}\u{8}Q\u{5ef3a}:Cm&\\\u{3}Q':@\u{f9a57}'\u{3f836}c<🕴P": false, "\u{202e}\u{6899a}<\u{b}/Ѩ&\u{2edba}\u{103edb}\u{9e328}`\u{4979f};\u{a6f16}!`\u{feff}w`": -32, "�Ѩ\u{8}�\"\u{fc9b3}\t": null}, "L_Ⱥ=,\u{4}\u{71158}'�\u{d01e8}/$\u{202e}\u{3b573}:`{w'": [-48, [93, 196, 141, 154, 134, 176, 139, 189, 180, 134, 124, 223, 175, 198, 167, 42, 110, 24, 222, 67, 196, 65, 202, 107, 120, 13, 17, 105, 196, 172, 101, 89, 218, 26, 140, 88, 96, 162, 89, 106, 160, 211, 109, 26, 157, 200, 132, 194, 38, 44, 145, 80, 249, 47, 202, 234, 243, 149, 231, 133, 231, 1, 121, 28, 157, 175, 187, 89, 100, 158, 213, 243, 162, 111, 87, 174], bafk2bzacecsuznauruwgtuwqc6nidbjw7qp5usnb5vr3w7lxuvebwffbqe3t4, -1.8324536891683285e-30, bafk2bzaceadjwnpm762btxzz5nfl24lfog3wm5ldrl7q7namae7ysv5647h76, -6.370155741536763e-309, null, null, 23, false, true, -41, 48, "`\t\u{cd15e}<{\u{ad7da}%W*腁=\t¯\u{7}-?,\\", 5, [238, 157, 140, 174, 18, 122, 121, 22, 84, 53, 144, 119, 53, 216, 130, 89, 6, 121, 229, 24, 153, 6, 11, 186, 92, 6, 52, 44, 74, 132, 139, 202, 28, 213, 100, 205, 127, 222, 130, 104, 210, 158, 235, 223, 136, 77, 58, 114, 135, 100, 151, 103, 91, 82, 150, 91, 149, 124, 36, 26, 78, 105, 207, 54, 253, 9, 201], [195, 193], -2.0736431262565832e-7, null, -7.471855593429185e234, [212, 22, 11, 89, 86, 227, 1, 226, 174, 110, 146, 235, 217, 246, 70, 133, 107, 165, 157, 151, 202, 116, 160, 243, 169, 65, 185, 193, 131, 184, 185, 157, 65, 108, 39]], "y#\u{49798}\u{202e}G<%r\u{1b}\u{c2763}\u{1b}\u{d7645}$M🕴\u{b}:": baguqegza3fmjvuxyw67cjennqkgi6ipcsxboanp4xjz4prjmp7h4r5muxaja, "¥\u{6}xS%\u{a21c0}Ѩ\0\u{b}\u{e52b5}\u{8cd0b}:\u{feff}\tS¨\u{dc97a}%$\u{8a51a}\u{8945b}\u{3c40a}🕴\u{feff}\u{feff}": "\u{b}\u{fbd6c}*", "🕴$�\u{1c50d}\t\u{2}": {"": 14, "\0�&\u{feff}\u{685bb}YVE9c\"|\"<[�K𤅰\u{cd669}}\"": bafy6bzaceak3lys2lo5m7woe2mfjyxxl3ore3aixosi5uajethhkjyze72xpm, "\u{1}\u{d0b74}\u{b6c77}y?[\u{202e}¥~{T\u{1b}\u{feff}\u{f8717}\u{a0f04}:\u{10f8b6}Ⱥ🕴\"Q\u{df178}\"\u{fe2ee}u": -1.5028963084100644e18, "\u{4}`\u{100473}\u{f434d}=�*/🦲Y\u{a0}W\u{a0e46}\u{5}": false, "\u{5}4\u{33c5d}\u{4}": 6, "\u{5}ZѨh\r&": true, "\t(/=:<\u{7}🕴.'Z": bafyb4ig3qyn3653hzem53fsfvx27lsgdkfmln4vgzjnu7bhb22rr6h5nvq, "\u{b}\u{5}\"\r𐴅\u{a3132}z$8\u{74e26}\0F\0\"*�J\u{7d9f5}\u{ad7c2}\u{f3faf}?\u{f35ed}": [158, 45, 189, 198, 136, 12, 91, 193, 206, 13, 142, 218, 17, 126, 95, 11, 139, 29, 191, 233, 236, 221, 187, 109, 193, 176, 213, 129, 96, 120, 253, 24, 3, 65, 228, 116, 138, 102, 127, 19, 54, 235, 37, 201, 238, 25, 41, 26, 248, 239, 191], "\u{b}Á\u{4}\0$\u{baa23}N\u{105c4d}\u{feff}g": 6.651792988712289e-76, "\r&\u{7f}𬛩\r*m\\𭘺\t": [7, 86, 209, 90, 4, 82, 142, 122, 123, 191, 41, 84, 37, 155, 147, 150], "\u{1b}=\u{ae8cb}🕴🕴YѨ": [15, 58, 171, 250, 163, 49, 118, 146, 209, 32, 161, 202, 1, 216, 170, 61, 6, 250, 37, 28, 65, 69, 84, 230, 70, 124, 99, 121, 212, 211, 35, 15, 202, 210, 87, 237, 13, 14, 12, 203], " n𦜹": false, "\"I¥": null, "'\u{202e}\u{1bfda}¥\r\u{e18c}{\u{4a199}Ⱥ\u{98ba2}{\u{202e}🕴f\\": false, "'🕴": [0, 125, 38, 17, 120, 195, 201, 150, 64, 36, 204, 245, 82, 31, 21, 85, 169, 69, 249, 10, 19, 33], ")�\r\"\r¥\u{202e}\u{cbc20}\u{5a381}\u{202e}𱙤{'": false, "*\rȺ\u{9af1d}g�.*🕴\u{1b}\u{feff}\u{6ecf1}`\rY{\u{88}\\*.\u{f8037}($\\¥.\r&ÑD`": null, "*'\u{fe9ce}\t𪡏h'Y\\{\0IWë\rU?$\u{7}IQi!%": bafkrwif2qr2egu3en3ttpfc277nx7wbogbs3exytgbmipe2ev4itu4abxi, "*{U\u{7f}3\u{feff}🕴%*``/ꁏw\u{88393}\u{1b}>": 16, "-&\u{588d4}J": -136947584516.8358, "/": bafybmigzjzmiin3bgn6qms6vavgzn6iyqjuo4onptcetzu657kebbwt5hu, "2\r\u{8c375}i\u{99}&🕴0J\t$Á\u{66ed4}/": "¾\t\t🕴$A\u{136fd}¥=l\u{8937a}\"\u{db049}`Ⱥ>@\u{b}\t\u{feff}%.\t\u{8ec09}\u{c5f5f}/$", "<\u{1b}/\\?z9\u{dbde9}\t?𦤐\u{1b}&H\u{1b}\u{125ec}\u{cf4c4}\t\u{b}\0.\t§\u{4a3f5}\\�": bafkr4id2n26zagqdmec7pq53l34j6st3x3xzqualxr3kl57litjvhiqjku, "=]\u{1b}\u{f411d}\t\"`\u{a4a2c}\u{6}": [238, 35, 78, 136, 226, 234, 214, 233, 77, 30, 183, 34, 221, 130, 60, 71, 179, 70, 33, 252, 49, 64, 114, 217, 11, 226, 152, 252, 143, 174, 237, 102, 78, 100, 138, 217, 6, 74, 250, 0, 234, 244, 152, 245, 69, 148, 151, 8, 51, 18, 105, 167, 200, 213, 240, 64, 138, 104], "=\u{8bf06}?x\u{1}𡊽@\u{6}.<%": "L\u{79877}\u{eccd7}*", ">=\u{feff}\u{8}à\u{1b}vL0\t\u{7f}I\u{feff}\u{7aba3}b$$Sv~i\t錦🕴&\u{709ee}.": null, ">`:𦎼\u{7f}º\u{be352}n$\u{eafbd}\u{cddd8}jw\u{1b}\u{feff}YT": true, "I韸\u{53960}\u{feff}?\u{7f}.\u{cf92c}Q\u{4}.\u{b}\u{8}🕴Ѩ%": -26, "M\u{2}𬗃\u{9082e}\u{e23ca}\u{4}\t\u{feff}\u{5bde3}*\u{e514b}x,\u{48624}\u{1b}\u{b}+\u{f8546}\u{8a2a6}\u{d4710}'🕴f;\u{46c31}\u{1004de}y{b:": true, "M&\u{e303}?&�\u{7f}j;\"\u{202e}픢\u{b}🕴": null, "W*/X\u{7f}^\u{7dd38}Þ7%.*3\t\u{ce607}:\u{d3d17}c\t\r\"\u{84fa3}\u{202e}=\u{92}": null, "Y*\u{c2e6a}?<~\u{feff}P'¥e¥\u{43a8e}\u{fa00a}`'": baguqehra4nft63u5d2aawrcgvhart2zja5ogq6c624azqqxgbgtoxekvlnpa, "Y\u{202e}'?": "\u{3}\u{cc1ea}\u{c7649}\u{a973e}\u{8}0\r^u\u{8eff2}l\r<]\u{5e264}&\u{f3e11}\t\r\06S", "\\\u{5}.\u{7}\u{b69af}*\u{19abc}]¥\u{202e}{🕴'\u{e46c7}`.xs\u{b23ec}\u{10beed}": -21, "\\*\r¢峭ìE\u{d96e1}\u{f8ff9}^\u{bc6ac}%:\u{50965}$6\u{8119f}¥\u{f67d0}y~\u{6c741}\u{3beb4}🕴": [126, 126, 7, 32, 17, 6, 234, 156, 163, 166, 193, 46, 81, 206, 32, 125, 166, 57, 195, 140, 39, 47, 171, 70, 117, 175], "\\\\/&Ѩ\\<": [92, 59, 122, 162, 91, 48, 33, 29, 29, 249, 254, 64, 21, 67, 100, 54, 206, 184, 116, 163, 175, 47, 42, 243, 205, 18, 136, 16, 249, 116, 141, 232, 122, 115, 123, 202, 72, 56, 116, 229, 177, 60, 49, 83, 199, 52, 23, 85, 114, 147, 155, 50, 31], "`¥?쉲G": "\t\u{4095b}\u{6c6a4}𮭀\u{2}\\\u{da8f7}8\u{90a53}.痗\\\u{202e}\u{56f97}{E]\u{492cd}&f\u{7f}¥D\\]Ⱥ?🕴", "`嬥\u{db443}.\u{ce5}\u{12eae}\u{108af8}\u{1b}\u{7f}\r\u{60df2}¥%&\u{5b0fe}-.&\u{3}\u{e2c1d}DR&:�\u{10c17b}Èc\u{8}U%": [49, 169, 171, 64, 201, 141, 228, 46, 7, 206, 13, 162, 170, 144, 168, 169, 198, 15, 58, 71, 112, 228, 149, 218, 144, 84, 70, 253, 39, 168, 223, 3, 232, 33, 162, 234, 136, 215], "`�`\u{7f}{\u{5c37e}\u{2}\u{202e}?": "/\"\u{3}𪜥N\u{57f77}h$\u{d1d92}𐊘�\u{7f}*¥<.\u{9e397}<\u{1}g\u{d0ad6}/\u{11ede}\u{7f}\u{ae007}\0$\u{3afa0}🕴", "c��W|`�*@.aѨN\u{feff}\u{f92a0}t$\u{f53cc}1\u{c8fbe}\r\u{19667}\t\r\r-\u{7c67c}": 2, "o\u{1}\u{1}\u{1}=\u{e8927}I\u{f584}&\u{a0}:9\0a𠿭/": true, "q$Ѩ\08{\"鉱[?ÀzY\u{9b8c9}%\u{3}{%\u{5}\0.\\\u{6c540}\u{7f}s\u{c05e8}`\u{47f61}=)🕴": -12, "s*¥\u{9a}\u{feff}": -1.6309188456998563e185, "v=<¥'<\\`\u{cc9eb}\u{798aa}\u{64542}:®3\u{202e}\tD&𰓏%�\u{7f}LF𒉠\t�\u{ee420}/'D": true, "v陈'\u{d832c}R??\u{7f}/\u{6}?\u{7d4d1}\u{dca1e}~🕴\u{aca77}:ç\t\rѨt?\u{10f5f3}\u{9db4d}": [35, 3, 217, 123, 77, 104, 154, 141, 118, 202, 130, 101, 62, 0, 182, 199, 130, 209, 149, 102, 212, 81, 90, 149, 217, 96, 127, 42, 251, 151, 46, 49, 33, 84, 28, 55, 109, 95, 208, 174, 193, 163, 28, 103, 91, 127, 159, 204, 177, 182, 31, 112, 61, 243, 10, 202, 83, 234, 176, 174, 61, 22], "v\u{43b04}6\u{f16c9}\u{2}\u{ad277}{Ã={/\u{202e}": [130, 234, 7, 201, 230, 244, 129, 23, 205, 123, 90, 239, 235, 197, 10, 88, 224, 76], "y/\u{1b}Ѩ\u{1b}": "?;*\u{156a5}🕴\u{1ad1f}:-\u{a1bea}r&\u{eb06}\u{a5275},:R\u{7f}&<\"®\"w\u{202e}:Ó.:\u{202e}\t.", "{B\u{7f}𲌜\u{9a6bf}.\0\u{4a3ca}🕴\u{7}\u{35765}/": -29, "{`=\t;\u{b}*EX\u{1b}鋪\u{61ce8}\u{b742a}/s🕴\u{5}\u{db6e7}'\u{10aa16}q?\u{8}\u{8d}\u{518fe}": "?\u{8d694}🕴", "{b\u{fe666}䀾\u{feff}]y\u{1b}[$\u{202e}\u{202e}🕴㞔\u{c132a}": true, "\u{7f}\"P=\u{555f9}\r\u{f034e}j\u{67c81}\t)Ø\t\u{a2116}\u{bcb49}<\u{108172}\u{489d6}3": [11, 195, 207, 131, 8, 71, 197, 12, 152, 172, 119, 96, 131, 191, 170, 2, 143, 112, 247, 189, 191, 156, 180, 76, 57, 86, 59, 6, 237, 106, 34, 175, 98, 78, 217, 192, 190, 63, 247, 202, 245, 190, 185, 58, 157, 147, 177, 129, 101, 197, 232, 22, 220, 131, 53, 79, 127, 137, 106, 0, 139, 8, 238, 179, 248, 40, 242, 178, 191, 193, 47, 56, 113], "\u{7f}_%\u{e1aa4}\u{c1f6f}/$s:\u{1010bd}*j\u{b}nêeȺ$\u{7f}\u{202e}\u{feff}\u{1b}\u{feff}%\u{9f08d}w\u{e8557}<$<\u{3e78b}": "Ⱥ\u{feff}$", "\u{7f}\u{9a}\0�\u{9c}'$=\u{4}&\u{94281}\t?\"Ѩ\u{654e1}": false, "\u{99}.\u{9d66b}z\u{db26c}\0\u{109c92}.W\u{ca505}\\¥\u{e0cf8}\0`4x\u{feff}×ꡔL\u{b16bc}\u{c1e8f}TYu\u{e7dae}\u{49a55}: ": 43, "\u{9e}%MWȺ`$\u{202e}:z?🕴\u{6}\u{a0}㶬{\u{5}<\u{6}*": bafk6bzaceaont65a5awr6tb55msdtvepr4xwa62mgsp4gyyfmbd62ey4ts4z6, "¥`?=%🕴`L": true, "¥\u{7aff2}Òb[?\u{8}\u{923a6}»\\»": false, "¦\u{6a307}": null, "Ѩ🕴T¥$*?\u{6}𪵅\u{98110}": -6.640335043931752e20, "\u{1bd53}Ѩd%@": null, "🕴3\u{feff}&\u{dd024}Ѩ=\u{c62d5}p\u{7f}�'AѨ|`g\u{635c5}h": 22, "𣅝\"\u{8b95b}\u{fbfeb}\u{cd3f3}A'\u{167f7}𘑦&🕴\u{94f3b}H⼣\u{95077}": "", "𮎾ýK\u{feff}qȺ\"\u{64e75}\u{70dd2}¥\u{41885}l": bafy6bzacedp25hwogiwuqmaa52iwzgx42gznm36zcpjwo2togshwzpsae6p26, "𲊳": [226, 239, 217, 38, 87, 223, 233, 194, 149, 219, 168, 60, 210, 19, 135, 178, 111, 92, 80, 108, 128, 121, 241, 63, 190, 148, 170, 24], "\u{3393b}\0¥\u{3b6e9}F\u{93d46}X=\u{d5dd8}:\u{feff}%'\t\u{95303}'*\u{aca4a}", "\0\\\"s\u{feff}M:b\u{8}H/\u{6cafc}\u{f1224}z\u{8}8P🕴\u{a2f8a}\u{8}\\": true, "\u{1}=\u{3134e}$\0v{K\"({Ѩ/�𪓆 \u{b3be1}�\"{$¥//🕴\0\u{604de}\u{a3afa}$\u{3}q": [43, 187, 50, 198], "\u{2}'Ⱥ\t\t\u{10b73d}\"\u{8}Ѩ:": [177, 214, 28, 238, 196, 162, 205, 149, 100, 179, 184, 109, 52, 32, 41, 6, 221, 134, 161, 95, 164, 2, 124, 18, 111, 203, 147, 153, 196, 122, 109, 242, 133, 28, 125, 73, 247, 14, 254, 57, 169, 190, 203], "\u{4}x1\u{b}\u{d46f0}k¥�'%\u{1007c4}": -41, "\u{6}/": 34, "\u{1b}2": -25, "\u{1b}\u{202e}\u{c9725}\u{80c81}?/\u{5}\u{e1f43}m\u{f878e}\u{feff}?M": bafyobzacecdottam3btcdm3lil44fdpksyl4ry22wyyufv5d3h6yuc2xic62a, "\u{1b}\u{4d55c}?|L¥ù\0\u{b}¥?[\u{f871d}k{.\u{9b}ć🕴$a\u{2}\u{db167}`\"": baguqeerag4wvi75skxsukegz4at6o2jjuwnsmujpjfkds3vdu4ybvdnqszfa, "\"[ .[": baguqefrayopq7znw46ljb72uinasfv3w2iuuvycues24ag6px6cwn7uhxgzq, "\"\u{eb96e}\u{3}7𩼱/\u{7f}?<\r�9\u{1b}'\u{b}<\u{56215}\u{612fc}*:'�EȺ/Ý\u{41397}-": -0.0, "$\u{3551b}$🕴\u{4051d}渴": -14, "%=�\t\u{cadf7}\"\0\"=z\u{f357}\u{a23f8}\":\u{f77ac}🕴\tJ\t$": [15, 242, 91, 76, 243, 187, 224, 9, 73, 10, 254, 67, 51, 52, 17, 243, 78, 108, 39, 226, 242, 59, 201, 164, 49, 64, 177, 101, 28, 139, 30, 14, 186, 53, 73, 148, 250, 157, 77, 84, 1, 36, 211, 17, 227, 29, 58, 144, 4, 192, 22, 128], "%\u{671d5}\u{5} =\r?'b.\u{1b}\u{7cbe2}𱛣\\\u{1}¹Sb\r/%\u{108071}ȺE$x\u{1b}": true, "&'\u{1398c}🕴\u{91} ": [35, 228, 163, 135, 36, 120, 223, 108, 37, 185, 72, 109, 236, 64, 172, 51, 97, 158, 76, 34, 242, 182, 40, 28, 179, 167, 49, 193, 42, 24, 222, 201, 26, 63, 190, 143, 222, 12, 8, 41, 47, 5, 168, 168, 222, 18, 54, 55, 98, 164, 82, 143, 124, 88, 254, 159, 177, 101, 205, 232, 17, 198, 34, 34, 105, 60, 25, 16, 173, 94, 188, 33, 68, 226, 245, 233, 154, 122, 243, 23, 198, 255, 31, 226, 251], "*]\u{8bb40}&.-Io\u{2ee60}%\u{b}\u{b4204}\u{b}\u{6fa9e}\u{1b}\t[\\\\uS=¥\u{7f}": -0.0, "*\u{1f84e}`𡣶\u{b}\"\u{df68f}A\u{33626}\u{202e}|\0\"\u{862b5}\u{7df5d}\u{1b}<\u{87a2b}\u{8dd82}~\tP\u{202e}Ѩ": null, "*\u{38df8}p\u{d2c21}\"\\\u{202e}\u{82032}㿶1&ѨU`H8�\"&": [38, 69, 84, 226, 186, 180, 254, 85, 31, 178, 18, 43, 46, 2, 151, 54, 154, 241, 244, 226, 93, 189, 139, 83, 133, 109], ".%\u{fd55f}?,/.`\r\u{dadcb}%\0\u{f3420}\"%#\\Ⱥ\u{95792}R¥}\\\r𡂂\u{69eab}\u{feff}\u{7f}{": [117, 59, 134, 197, 33, 33, 196, 45, 250, 203, 13, 97, 151, 48, 4, 203, 100, 245, 18, 88, 41, 62, 165, 72, 69, 218, 126, 161, 161, 184, 57], "/\u{5}\"I28qȺ|«㖃$c`\u{2}*?\u{ee2e9}\"\u{51724}": false, "/.\r¥": -1.2787723284947307e-308, "/J\u{10519c}%%/$.%\u{65527}\rW|Ⱥ{\\¥\u{8201f}\u{7f}\u{b}&%c=Ѩ\u{9bf3f}": 2.35302505714783e265, "/\u{52490}=|:%w\rl\u{90}": null, "5Q%\u{561e8}^r.\u{b8932}\u{92f2d}4\u{7f}+⩾\u{7f}/.¥\\\u{1}\rS1\0%": bafkreigrelqjps75gjoqf4ijk5muyqd3eo7nkw5cm5phlkiaaoy4yuiy6q, ":`\u{7112e}": baguqehraddx54dt5qiorol2h2cg5mnr7pjvahh3d43il2ernaikugjuvyymq, "?\u{75c14}\t龒Ⱥ\u{e5509}\u{534e4}<>\u{1b}\u{d3cef}\u{7f}\u{417fd}\u{7f}\u{6c856}%'\u{202e}>\u{43519}U[:\0\u{b}&\u{df77e}/&": -4.024585944763422e193, "K*\t\t\u{e140}Ø\r o\u{c3cad}2/{Ⱥ\u{7f}s\u{7f}¥\u{b71fe}&¥\u{e34cb}Ⱥ\u{135f0}\u{b}$\t2$$": -10, "OB\u{4}\u{7f}\rP5\u{80012}\u{16348}\\z\rÍ\u{1b}\u{3b61b}?": bafkreiaimlx7a6sb2ezufrqz5kqv3p2y4fadnzakimbrhq3ajzjg7ue75m, "P\u{93db9} &zG\u{7}?🕴\u{7f}'\u{10ec52}\u{feff}\t:\0\u{63df4}m\t\\": false, "U\u{2}\u{9ff8c}¥?\0\u{57a21}\u{9bb0e}ef\u{2}Ⱥp\u{f73a7}4\u{b}$\u{7f}\u{1b}9\u{7f}{\u{9e15c}\u{d804b}\u{705a1}\u{e750c}Ⱥ\u{7f}¾\u{7c515}": [182, 236, 56, 93, 36, 111, 103, 217, 53, 16, 250, 85, 194, 3, 183, 131, 156, 114, 43, 35, 38, 181, 143, 123, 141, 23, 109, 247, 10, 205, 121, 24, 25, 35, 178, 125, 3, 138, 141, 76, 31, 49, 144, 42, 23, 111, 9, 180, 199, 19, 28, 28, 36, 135, 72, 100, 199, 115], "Xa\u{b1ab2}$(\u{9e}\u{3812e}\u{fe6c}\u{6acee}\t8$=\u{7aa19}He\u{202e}|2*\u{6813f}Ⱥ:$%\u{70ca6}`\u{e4b58}o\u{a0}?": null, "Z\u{87bae}\0\u{f48d0}Æng": 49, "\\": [161, 24, 206, 12, 94, 242, 137, 40, 92, 187, 103, 166, 57, 108, 16, 46, 30, 140, 111, 96, 213, 196, 3], "\\\u{e7806}\u{7}%\r\u{7f}:<\u{95ec9}\u{10b262}\ry\u{d494a}g\u{5c182}'&.\u{2}\u{750dc}]Kq\t": -1.3364526424136176e63, "^\u{50332}\u{3}\u{7f}'$$Ѩ\u{7f}\"D?": -33, "`\"*\u{6989b}\u{a0552}Ⱥ&d\u{4}Ⱥ🕴Ѩ\u{f863f}Q\u{202e}\\=\u{202e}*}\u{6e906}\u{994d4}\t\u{10cc54}\u{3}_坢Ⱥ\u{ef59f}F\u{e82f1}": "'*𰀏Ѩ\\%\u{4}.*\u{202e}\u{96457}%|\u{100bbc}�\u{5046b}H\u{d9ba9}HL\u{5}*\r\u{8a1e4}\u{b}\":<\u{7f}\u{9f}Ѩ\u{7f}s{�\u{a46b4}\toh\u{7f}*", false, 46, 14, "�\u{102e58}'\u{f5c0e}\t\"E\\cM<'l", false, [135, 235, 189, 222, 190, 18, 166, 127, 220, 237, 86, 153, 31, 245, 85, 90, 198, 38, 141, 133, 190, 152, 45, 202, 134, 204, 48, 11, 139, 218, 166, 185, 151, 185, 14, 83, 198, 240, 223, 59, 82, 165, 180, 107, 145, 62, 145, 69, 120, 158, 111, 248, 129, 252, 131, 205, 198, 215, 133, 171, 133, 47, 12, 202, 128, 214, 166, 137, 1, 192, 251, 117, 7, 234, 236, 17, 204, 31, 74, 10, 168, 79, 30, 0, 231, 29, 135, 146, 72], "RvѨs$\u{75d03}Ѩ(<\t\t𤡧s`\0\u{1b}.\\\\$\u{1616b}\u{7f}\u{3cf54}", false, [235, 223, 146, 172, 43, 51, 24, 21, 97, 241, 49, 100, 109, 54, 82, 186, 134, 207, 194, 77, 228, 210, 37, 8, 203, 94, 183, 5, 197, 144, 138, 15, 165, 27, 102, 4, 88, 13, 236, 223, 176, 178, 233, 90, 97, 161, 185, 38, 223, 154, 255, 212, 108, 179, 214, 107, 224, 221, 198, 190, 85, 135, 36, 226, 102, 69, 206, 155, 37, 70, 205, 167, 176, 184, 47, 239, 155], -1.1621607219028973e-308, "\u{59b57}/x\"\u{7f}\u{7fb3b}\u{3}&2&\u{b}\u{2}*🕴l\u{9d87e}%\u{1b}\u{b}RA", -24, -18, null, [95, 22, 213, 216, 209, 22, 236, 29, 48, 196, 106, 1, 159, 4, 224, 154, 54, 81], true, bafyb4ieb5rs6wukn4nc3vkm5ej4qrmgt7ycs7lmkc65m3wcxmpgo2xms4u, bafkr4ieb5eefx4xbgebjbqtjvimlfqjwrqcrg7vlzoiqrnntc2avr3g5g4, bafyb4ibwxgkbwxtmkhdb5bwhyi7caxvo3ke6mk42emzae5iini52xuhlpy, -37, false, false, -28, "\u{8}1\u{79fd2}\0\u{ca356}\u{59494}O𫔓{\u{7f}Q\u{3269c}]\u{202e}\u{2}\u{97bce}\t\u{578dd}cP\0\r", null], [158, 31, 209, 14, 252, 175, 225, 4, 36, 138, 244, 197, 42, 56, 92], null, false, [52, 230, 174, 76, 132, 211, 203, 241, 115, 11, 214, 245, 111, 133, 43, 90, 154, 170, 151, 64, 144, 238, 229, 61, 189, 206, 192, 76, 209, 181, 128, 129, 149, 185, 71, 119, 213, 172, 36, 212, 47, 194, 112, 62, 251, 65, 101, 24, 90, 161, 131, 1, 164, 160, 99, 211, 197, 197, 201, 100, 159, 72, 192, 14, 231, 60, 84, 118, 134, 195, 67, 16, 246, 204, 36, 246, 158, 191, 131, 237, 231, 155, 25, 200, 124, 214], -1.6606481206991028e-178, bafybwif7fjc3n3ymfa23uyjq64sksrh45thlzmm76bb5bohapwgol2thvq, null, -1.346334315879978e109, "衴", [119, 113, 165, 145, 116, 9, 189, 140, 253, 252, 3, 148, 133, 255, 48, 161, 243], "\u{8}u/G\u{a6e8d}?\u{60a32}=ó\r\\?🕴\u{7}%a/\u{1b}¥'\u{7f}:\u{60bc8}\0i`\u{1037a2}A\u{c10fd}\u{b}>\u{202e}", "", {"\0\0¥": true, "\u{3}=Ѩ\u{5ce31}:/\u{97}\r\u{7f}/\\-\u{e2599}?\0\u{202e}`w0\u{7f}\u{b}\u{10ed54}5\u{84}%\"": "$\tE&\u{43d48}S\u{fb913}ÿzVt\u{e7bed}L&{²\u{6}\u{7f}", "\t\u{5}\u{a8a7f}\u{89589}sJ<'𫮹碗\"Ѩ1i/𦏲x🕴\u{a3b5e}\u{ee69}?": null, "\u{b}'\u{83bfd}\t/*㷰\t\u{97282}\u{5}I{": [223, 168, 69, 95, 72, 43, 203, 89, 110, 174, 115, 77, 84, 89, 26, 159, 57, 147, 74, 158, 40, 74, 218, 178, 183, 135, 65, 67, 200, 1, 1, 140, 39, 233, 54, 33, 28, 99, 87, 116, 176, 91, 93, 98, 3, 13, 238, 112, 18, 248, 83, 152, 221, 158, 134, 229, 80, 111, 168, 248, 209, 124, 19, 203, 55, 80, 89, 127, 31, 233, 114, 8, 236, 122, 44, 128], "\u{b}G;%&YP🕴t\u{4}E�.{\r\\/�K|\"\r": bafyrmifvkoneefkqfiseviqruma2fucp5bha2di7eg7jlyk3zrf46fw4na, "\u{b}d'S\u{4dc81}%\t)🕴\u{202e}.🂉\u{49dd2}𗹆\u{461bb}\\*c": true, "\r:*$㝩\u{98464}\u{6}𰂜.\u{1b}\u{b}=0\u{8d206}¥\u{95e7d}.\0\r:¥$\u{fd075}w{\u{202e}\u{7f}À{/\u{aee3d}": -54, "\u{1b}]Ѩr\u{46a43}Ⱥ\u{76dd1}\"@%6%�*L\u{10fe25}\u{cb0c9}::\u{82}{": baguqfiheaiqmnsckfvqrzdszfgxhnfdlbgv4tv2umb4ebrla4e4ovkqzr7qtl2q, "\u{1b}\u{202e}=\t%>\u{64bd7}=\t¥.?&\u{202e}\"=\u{feff}\0\u{7ed05}\u{7f}Ⱥ\u{3b248}": true, "\"ȺA\rZ": [182, 24, 119, 121, 203, 152, 112, 93, 47, 116, 134, 100], "$": bafyr4idvqlsptx6ct75oj6n343bqpaabhltktomqf74lrhnjwj5zwvrz5m, "$\u{2}\u{9f}¥{\0$\u{202e}c\u{1b}\u{736d1}\u{7e350}#K🕴´\u{44a44}.🕴Ⱥ\u{401cb}%\r//\u{44014}r\u{1b}é¥": bafyr4icg62pibjpimmwgopvait4hdkb3lkz4ry3duekmfx4expvn4rrwqm, "$)ѨÊ\u{69a2e}:»Ⱥ\u{99}\u{f3438}\u{b}\u{c0d9c}*": 17, "$/??{\t\0á<\u{6d90c}.\\\u{5ab70}W*D.`\u{81dac}`": "🕴j\u{202e}`\u{97293}Z<²\u{aabdb}¬\u{202e}.&=Ùh\u{5680a}\"𪻏", "*\"\0'`<ѨjȺ$\u{8d55d}Ⱥ\u{d1704}$\u{69be0}]\u{feff}%..": "Ⱥl'Ⱥ\r�<;\u{edd5d}<\0\u{a2820}\0\u{16271}\u{ccc45}\\:/\u{202e}\u{4}(%\u{7}", "*/$\u{3b185}/�:\u{c8c4f}CA&4\tc,>\r+": 0.0, ".{^\u{feff}\u{b31b8}\u{f0ff0}=\u{1b}\u{cf39b}&Ⱥ\u{cc0f0}ꟊj\u{7f}2*¥n.FѨ": bafkreifagebl3ayuazlaokrf45fopvx2mxh52amx5e7yh4w3pxhkken3vq, ".\u{202e}": true, "6\u{3}\u{febc8}P\u{6a6d5}~\u{3279b}p삣6\u{b}\u{b}K": baguqehrak3yvnpqo6ab5rdeccamokfdova7l5vpi4xq7skax4mmzw75slkeq, ":\\\u{1009ca}{'\u{7f}\u{9b51b}/ek%🕴=Ⱥ\u{7f}{\u{9ecd8}/Ⱥ`\u{aaa83}\u{feff}\u{1b}{\u{5ebd0}�\u{c68be}T": 9.620946678080735e-61, ":\u{feff}A*&\u{839f8}\u{86fd8}\r4{;�<`.<$\0\u{b1b8a}>": "𠑛%Ⱥ�\u{1}<\u{1b}\05$OѨ? =Io\u{3}Ⱥ\u{4a654}\u{95}\u{2}\\", ":\u{f12fc}\u{8f80d}\u{c4396}𣝛=\u{7}\u{92a55}🕴(&\u{91993}\u{e357e}\u{8ac6d}\u{7f}Ⱥ襥\"':�\u{5}": baguqegza2ch32i7qyewr7mke33b5bpzm7jep5ei5kpard5moafylxh46gpma, "<\"\u{fe42e}\r\u{103930}O3=\u{feff}𐽱Ѩ\u{c7c2a}<.譒\u{ec02e}/\u{520c0}FA㣂\u{b}\rù": -43, "<\u{b8112}}B\u{1}\u{e22e4}$[¥=\"\u{b}`$=": bafyobzacecdbrhr5ehtimriiz6mxvinsq5tuyh7dqu3pderxxdmgtepquklbe, "?¸H/%\u{feff}\\K\u{cc4ea}y:🕴\"@8:\0:𢭤¥<\u{c9d0a}\u{7f}\u{8}\u{7f}\u{5}": bafkrwidvq2ib5wbdwi6evstk4n4zuo2ptkqx62jlqzp65xevm54zh6caiy, "@\u{66e54}Ѩ$u": null, "C𫏦.\u{b}/'8\u{49182}ȺȺ&\u{c91e4}\t/": -46, "OS\u{b}\u{7}𩚡4?Ⱥ\u{feff}'\r¥\u{202e}\u{b}B🕴f\u{202e}.": null, "Q\u{d12fe}&`\t*\0g\u{1b}\u{8}2D/$)\r{?𠯚\u{fed96}x\\û": 5, "U^\"?d\u{f8edf}R\u{a3374}<🕴\\\u{202e}>%\u{feff}\u{a2d62}\u{ab6e8}\\\tP\u{81d20}W\u{998d7}\0\0\u{d2250}\u{74ab2}.:W*%": [212, 136, 153, 95, 249, 147, 220, 254, 102, 118, 32, 185, 203, 25, 119, 125, 56, 0, 225, 234, 220, 127, 234, 201, 81, 152, 213, 225, 133, 23, 85, 200, 158, 231, 88, 27, 12], "\\Ѩ&\u{ce722}\u{74bbb}i\u{9caf6}f}<\u{feff}Uo\u{f89a7}\u{2}$\0𰔭\u{63b24}𘥆&þ": null, "`\t\u{7abdf}Ð)&g&": "{Ѩ\u{e4a29}*nȺ\u{e491e}M\u{89c42}%a\u{8}\u{82}", "`r\r\u{8}=🕴\t\u{440b0}Ⱥ\u{4}�%*{\u{202e}\u{151ad}\u{55675}K": "\u{4}\u{5d2d2}:Úv\u{f704f}\0\u{7}\u{ad34a}¥A<\u{aebdd}\u{898f2}<\u{d3097}\u{8}W/`Ⱥ\t\rѨ\u{10361f}\u{6d1ed}", "e&#{`\u{34996}?'/\u{1b}\u{2}\u{19c0c}\u{6716f}\t$%\u{db2ea}[{": 22, "n?": 52, "o\u{106e1b}?\u{1b}{*/$<Ѩ\u{7f}\u{7f}.'\"`6\r🕴�\u{202e}\u{77895}�𩈷)�\u{f670f}\u{202e}`:\u{4fb5c}>`x\u{35613}(e\u{202e}µ\u{ae9bd}\u{83}Ѩ¥\u{cf5b4}¥~\u{1}\\\u{10bc4b}hl(:#z": "\u{b}<{\u{5}\u{3c7ce}\t\u{b0200}`Ⱥ\u{58ab1}2*\"\u{ff4a9}Dw🕴&", "葜\u{1b}`�:\u{10e218}<2\u{1b}`\u{10b2e2}C\u{7f}\u{202e}\"¥s\u{1b}\0*\u{b}%\u{9d41a}=\u{e573}": [true, [32, 206, 206, 97, 143, 10, 81, 82, 157, 249, 61, 22, 250, 48, 255, 194, 232, 184, 202, 22, 232, 158, 227, 32, 56, 8, 142, 212, 153, 240, 168, 65, 248, 231, 48, 108, 237, 135, 114, 246, 246, 204, 239, 24, 162, 41, 103, 211, 37, 81, 230, 208, 111, 248, 4, 9, 197, 153, 105, 20, 217, 206, 146], true, -5.25899467531148e-128, "M<0%\u{202e}\u{b}/\u{46e36}\u{108dc}\u{a2ef8}\u{7f}.]\u{b96d6}\u{b41c1}\u{4e68e}", "\u{5d121}\u{b}\u{cf745}\u{3}\u{5badc}\u{202e}\u{d984a}", bafybwic7uqdxsofaghtkiqawch55haewwlxbtyy7dzua22bjcif4y2jmri, false, null, 29, bafkr4idbn64n3h5bfrab6gnwlyggbaxq2lyvtcpvyzp5z4h3myyl7tkzui, "ѨSm¥L\u{7}?\u{7c63b}`x%qN\u{a501c}1.Ѩ\u{5}:\u{6120c}·{*7", false, [112, 107, 102, 23, 200, 228, 14, 201, 117, 98, 1, 76, 10, 156, 40, 126, 28, 147, 14], null, "%Ѩ\u{b}=\u{e06d3}\"\0\u{b}\u{105e48}\u{dd959}㮴a{N*¥\u{4}\u{6c856}", [1, 150, 20, 111, 201, 216, 245, 91, 220, 0, 194, 252, 33, 40, 135, 13, 44, 29, 164, 248, 22, 137, 89, 237, 222, 28, 4, 157, 221, 9, 100, 197, 235, 73, 41, 84, 55, 116, 103, 40, 48, 206, 112, 238, 68, 248, 66, 52, 206, 58, 27, 54, 24, 51, 14, 70, 8, 203, 39, 212, 210, 72, 151, 65, 50, 3, 131, 97, 121, 181, 227, 173, 63, 242, 250, 82, 244, 186, 53, 96, 222, 46, 102, 216, 26], -5.797387639150806e-228, true, [228, 221, 48, 49, 157, 129, 203, 175, 12, 252, 218, 247, 178, 191, 214, 73, 7, 40, 52, 53, 19, 147, 108, 202, 231, 162, 31, 28, 67, 99, 121, 1, 129, 25, 41, 204, 153, 97, 233, 32, 246, 185, 128], -2.3032544649066786e268, true, false, 29, -23, [119, 152, 15, 167, 240, 199, 14, 99, 170, 252, 95, 162, 82, 87, 28, 38, 176, 157, 185, 58, 156, 67, 186, 0, 230, 43, 167, 184, 246, 129, 63, 121, 129, 247, 200, 218], false, -0.0, true, true, 41, -1.9024538231552825e-308, null, 0, "%\u{5}\u{86d1a}ð\\\u{5}\u{1b}\u{3}n\u{feff}F\u{10d421}> From<&Object> for Named { + // FIXME probbaly needs to be a try_from + fn from(obj: &Object) -> Self { + let btree = Object::entries(obj) + .iter() + .map(|entry| { + let entry = Array::from(&entry); + let key = entry.get(0).as_string().unwrap(); // FIXME + let value = T::try_from(entry.get(1)).unwrap().0; // FIXME + (key, value) + }) + .collect::>(); + + Named(btree) + } +} + +#[cfg(target_arch = "wasm32")] +impl From> for JsValue { + fn from(arguments: Named) -> Self { + arguments + .0 + .iter() + .fold(Map::new(), |map, (ref k, v)| { + map.set(&JsValue::from_str(k), &JsValue::from(v.clone())); + map + }) + .into() + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Named { + type Error = TryFromJsValueError; + + fn try_from(js: JsValue) -> Result { + match T::try_from(js) { + Err(()) => Err(TryFromJsValueError::NotIpld), + Ok(Ipld::Map(map)) => Ok(Named(map)), + Ok(_wrong_ipld) => Err(TryFromJsValueError::NotAMap), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +pub enum TryFromJsValueError { + #[error("Not a map")] + NotAMap, + + #[error("Not Ipld")] + NotIpld, +} + +impl From> for Named> { + fn from(named: Named) -> Named> { + let btree: BTreeMap> = named + .into_iter() + .map(|(k, v)| (k, promise::Any::from_ipld(v))) + .collect(); + + Named(btree) + } +} + +impl From> for Named { + fn from(named: Named) -> Named { + let btree: BTreeMap = + named.into_iter().map(|(k, v)| (k, v.into())).collect(); + + Named(btree) + } +} + +impl TryFrom> for Named { + type Error = Pending; + + fn try_from(named: Named) -> Result { + named.iter().try_fold(Named::new(), |mut acc, (ref k, v)| { + let ipld = v.clone().try_into()?; + acc.insert(k.to_string(), ipld); + Ok(acc) + }) + } +} + +impl TryFrom>> for Named +where + Ipld: TryFrom, +{ + type Error = promise::Any>; + + fn try_from(resolves: promise::Any>) -> Result { + resolves + .clone() + .try_resolve()? + .into_iter() + .try_fold(Named::new(), |mut btree, (k, v)| { + let ipld = v.try_into().map_err(|_| ())?; + btree.insert(k, ipld); + Ok(btree) + }) + .map_err(|_: ()| resolves) // FIXME + } +} + +/// Errors for [`arguments::Named`][Named]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +pub enum NamedError { + /// A required field was missing. + #[error("Missing arguments::Named field {0}")] + FieldMissing(String), + + /// The value at the named field didn't match the expected value. + #[error("arguments::Named field {0}: value doesn't match")] + FieldValueMismatch(String), +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Named { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..20) + .prop_map(|newtype_map| { + newtype_map + .into_iter() + .fold(Named::new(), |mut named, (k, v)| { + named.insert(k, v.0); + named + }) + }) + .boxed() + } +} diff --git a/src/ability/command.rs b/src/ability/command.rs new file mode 100644 index 00000000..040ec2ea --- /dev/null +++ b/src/ability/command.rs @@ -0,0 +1,66 @@ +//! Ability command utilities +//! +//! Commands are the `cmd` field of a UCAN, and set the shape of the `args` field. +//! +//! ```js +//! // Here is a UCAN payload: +//! { +//! "iss": "did:example:123", +//! "aud": "did:example:456", +//! "cmd": "/msg/send", // <--- This is the command +//! "args": { // ┐ +//! "to": "mailto:alice@example.com", // ├─ The shape of the args is determined by the cmd +//! "message": "Hello, World!", // │ +//! } // ┘ +//! "exp": 1234567890 +//! } +//! ``` + +/// Attach a `cmd` field to a type +/// +/// Commands are the `cmd` field of a UCAN, and set the shape of the `args` field. +/// The `COMMAND` attaches this to types so that they can be serialized appropriately. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::ability::command::Command; +/// # +/// struct Upload { +/// pub gb_quota: u64, +/// pub mime_types: Vec, +/// } +/// +/// impl Command for Upload { +/// const COMMAND: &'static str = "/storage/upload"; +/// } +/// +/// assert_eq!(Upload::COMMAND, "/storage/upload"); +/// ``` +pub trait Command { + /// The value that will be placed in the UCAN's `cmd` field for the given type + /// + /// FIXME + /// This is a `const` because it *must not*[^dynamic] depend on the runtime values of a type + /// in order to ensure type safety. + /// + /// [^dynamic]: Note that if the `dynamic` feature is enabled, the exception is + /// a special ability called [`Dynamic`][super::dynamic::Dynamic] (for e.g. JS FFI) + /// that uses a non-exported code path separate from the [`Command`] trait. + const COMMAND: &'static str; +} + +// NOTE do not export; this is used to limit the Hierarchy +// interface to [Parentful] and [Parentless] while enabling [Dynamic] +// FIXME ^^^^ NOT ANYMORE? +// Either that needs to be re-locked down, or (because it's all abstract anyways) +// just note that you probably don;t want this one. +pub trait ToCommand { + fn to_command(&self) -> String; +} + +impl ToCommand for T { + fn to_command(&self) -> String { + T::COMMAND.to_string() + } +} diff --git a/src/ability/crud.rs b/src/ability/crud.rs new file mode 100644 index 00000000..7a47a611 --- /dev/null +++ b/src/ability/crud.rs @@ -0,0 +1,251 @@ +//! Abilties for [CRUD] (create, read, update, and destroy) interfaces +//! +//! An overview of the hierarchy can be found on [`crud::Any`][`Any`]. +//! +//! # Wrapping External Resources +//! +//! In most cases, the Subject _is_ the resource being acted +//! on with a CRUD interface. To model external resources directly +//! (i.e. without a URL), generate a unique [`Did`] that represents the +//! specific resource (i.e. the `sub`) directly. This makes the +//! UCAN self-certifying, and can give multiple names to a single +//! resource (which can be important if operating over an open network +//! such as a DHT or gossip). It also provides an abstraction if, +//! for example, the the domain name of a service changes. +//! +//! # `path` Field +//! +//! All variants of CRUD abilities include an *optional* `path` field. +//! +//! There are cases where a Subject acts as a gateway for *external* +//! resources, such as web services or hierarchical file systems. +//! Both of these contain sub-resources expressed via path. +//! If you are issued access to the root, and can attenuate that access to +//! any sub-path, or a single leaf resource. +//! +//! ```js +//! { +//! "sub: "did:example:1234", // <-- e.g. Wraps a web API +//! "cmd": "/crud/update", +//! "args": { +//! "path": "/some/path/to/a/resource", +//! }, +//! // ... +//! } +//! ``` +//! +//! [CRUD]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete +//! [`Did`]: crate::did::Did + +pub mod create; +pub mod destroy; +pub mod read; +pub mod update; + +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use create::{Create, PromisedCreate}; +use destroy::{Destroy, PromisedDestroy}; +use libipld_core::ipld::Ipld; +use read::{PromisedRead, Read}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use update::{PromisedUpdate, Update}; + +#[cfg(target_arch = "wasm32")] +pub mod js; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Crud { + Create(Create), + Read(Read), + Update(Update), + Destroy(Destroy), +} + +impl From for arguments::Named { + fn from(crud: Crud) -> Self { + match crud { + Crud::Create(create) => create.into(), + Crud::Read(read) => read.into(), + Crud::Update(update) => update.into(), + Crud::Destroy(destroy) => destroy.into(), + } + } +} + +impl From for Crud { + fn from(create: Create) -> Self { + Crud::Create(create) + } +} + +impl From for Crud { + fn from(read: Read) -> Self { + Crud::Read(read) + } +} + +impl From for Crud { + fn from(update: Update) -> Self { + Crud::Update(update) + } +} + +impl From for Crud { + fn from(destroy: Destroy) -> Self { + Crud::Destroy(destroy) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PromisedCrud { + Create(PromisedCreate), + Read(PromisedRead), + Update(PromisedUpdate), + Destroy(PromisedDestroy), +} + +impl ParsePromised for PromisedCrud { + type PromisedArgsError = InvalidArgs; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match PromisedCreate::try_parse_promised(cmd, args.clone()) { + Ok(create) => return Ok(PromisedCrud::Create(create)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Create(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedRead::try_parse_promised(cmd, args.clone()) { + Ok(read) => return Ok(PromisedCrud::Read(read)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Read(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedUpdate::try_parse_promised(cmd, args.clone()) { + Ok(update) => return Ok(PromisedCrud::Update(update)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Update(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match PromisedDestroy::try_parse_promised(cmd, args) { + Ok(destroy) => return Ok(PromisedCrud::Destroy(destroy)), + Err(ParseAbilityError::InvalidArgs(e)) => { + return Err(ParseAbilityError::InvalidArgs(InvalidArgs::Destroy(e))) + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + Err(ParseAbilityError::UnknownCommand(cmd.into())) + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum InvalidArgs { + #[error("Invalid args for create: {0}")] + Create(create::FromPromisedArgsError), + + #[error("Invalid args for read: {0}")] + Read(read::FromPromisedArgsError), + + #[error("Invalid args for update: {0}")] + Update(update::FromPromisedArgsError), + + #[error("Invalid args for destroy: {0}")] + Destroy(destroy::FromPromisedArgsError), +} + +impl ParseAbility for Crud { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match Create::try_parse(cmd, args.clone()) { + Ok(create) => return Ok(Crud::Create(create)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Read::try_parse(cmd, args.clone()) { + Ok(read) => return Ok(Crud::Read(read)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Update::try_parse(cmd, args.clone()) { + Ok(update) => return Ok(Crud::Update(update)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + match Destroy::try_parse(cmd, args) { + Ok(destroy) => return Ok(Crud::Destroy(destroy)), + Err(ParseAbilityError::InvalidArgs(_)) => { + return Err(ParseAbilityError::InvalidArgs(())); + } + Err(ParseAbilityError::UnknownCommand(_)) => (), + } + + Err(ParseAbilityError::UnknownCommand(cmd.into())) + } +} + +impl ToCommand for Crud { + fn to_command(&self) -> String { + match self { + Crud::Create(create) => create.to_command(), + Crud::Read(read) => read.to_command(), + Crud::Update(update) => update.to_command(), + Crud::Destroy(destroy) => destroy.to_command(), + } + } +} + +impl ToCommand for PromisedCrud { + fn to_command(&self) -> String { + match self { + PromisedCrud::Create(create) => create.to_command(), + PromisedCrud::Read(read) => read.to_command(), + PromisedCrud::Update(update) => update.to_command(), + PromisedCrud::Destroy(destroy) => destroy.to_command(), + } + } +} +impl Resolvable for Crud { + type Promised = PromisedCrud; +} + +impl From for arguments::Named { + fn from(promised: PromisedCrud) -> Self { + match promised { + PromisedCrud::Create(create) => create.into(), + PromisedCrud::Read(read) => read.into(), + PromisedCrud::Update(update) => update.into(), + PromisedCrud::Destroy(destroy) => destroy.into(), + } + } +} diff --git a/src/ability/crud/create.rs b/src/ability/crud/create.rs new file mode 100644 index 00000000..69acac8c --- /dev/null +++ b/src/ability/crud/create.rs @@ -0,0 +1,256 @@ +//! Create new resources. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/create` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// create("crud/create") +/// end +/// end +/// end +/// +/// createpromise("crud::create::PromisedCreate") +/// createready("crud::create::Create") +/// +/// top --> any --> mutate --> create +/// create -.->|invoke| createpromise -.->|resolve| createready -.-> exe{{execute}} +/// +/// style createready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Create { + /// An optional path to a sub-resource that is to be created. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Optional arguments for creation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>, +} + +impl From for Ipld { + fn from(create: Create) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = create.path { + map.insert("path".to_string(), path.display().to_string().into()); + } + + if let Some(args) = create.args { + map.insert("args".to_string(), args.into()); + } + + Ipld::Map(map) + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/create` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// create("crud/create") +/// end +/// end +/// end +/// +/// createpromise("crud::create::PromisedCreate") +/// createready("crud::create::Create") +/// +/// top --> any --> mutate --> create +/// create -.->|invoke| createpromise -.->|resolve| createready -.-> exe{{execute}} +/// +/// style createpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedCreate { + /// An optional path to a sub-resource that is to be created. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, + + /// Optional arguments for creation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>>, +} + +const COMMAND: &str = "/crud/create"; + +impl Command for Create { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedCreate { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for PromisedCreate { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + + "args" => { + args = match prom { + ipld::Promised::Map(map) => { + Some(promise::Any::Resolved(arguments::Named(map)).into()) + } + ipld::Promised::WaitOk(cid) => Some(promise::Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => Some(promise::Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => Some(promise::Any::PendingAny(cid)), + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + } + } + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedCreate { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl TryFrom> for Create { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, ipld) in arguments { + match k.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(()); + } + } + "args" => { + args = Some(ipld.try_into().map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Create { path, args }) + } +} + +impl From for PromisedCreate { + fn from(r: Create) -> PromisedCreate { + PromisedCreate { + path: r.path.map(|inner_path| promise::Any::Resolved(inner_path)), + + args: r + .args + .map(|inner_args| promise::Any::Resolved(inner_args.into())), + } + } +} + +impl promise::Resolvable for Create { + type Promised = PromisedCreate; +} + +impl From for arguments::Named { + fn from(create: Create) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = create.path { + named.insert("path".to_string(), path.display().to_string().into()); + } + + if let Some(args) = create.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl From for arguments::Named { + fn from(promised: PromisedCreate) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path_prom) = promised.path { + named.insert("path".to_string(), path_prom.to_promised_ipld()); + } + + if let Some(args_prom) = promised.args { + named.insert("args".to_string(), args_prom.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/destroy.rs b/src/ability/crud/destroy.rs new file mode 100644 index 00000000..3f52084a --- /dev/null +++ b/src/ability/crud/destroy.rs @@ -0,0 +1,244 @@ +//! Destroy a resource. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/destroy` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// destroy("crud/destroy") +/// end +/// end +/// end +/// +/// destroypromise("crud::destroy::Promised") +/// destroyready("crud::destroy::Destroy") +/// +/// top --> any --> mutate --> destroy +/// destroy -.->|invoke| destroypromise -.->|resolve| destroyready -.-> exe{{execute}} +/// +/// style destroyready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Destroy { + /// An optional path to a sub-resource that is to be destroyed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, +} + +impl From for Ipld { + fn from(destroy: Destroy) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = destroy.path { + map.insert("path".to_string(), path.display().to_string().into()); + } + + Ipld::Map(map) + } +} + +const COMMAND: &'static str = "/crud/destroy"; + +impl Command for Destroy { + const COMMAND: &'static str = COMMAND; +} + +impl From for arguments::Named { + fn from(ready: Destroy) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = ready.path { + named.insert( + "path".to_string(), + path.into_os_string() + .into_string() + .expect("PathBuf to generate valid paths") // FIXME reasonable assumption? + .into(), + ); + } + + named + } +} + +impl TryFrom> for Destroy { + type Error = TryFromArgsError; + + fn try_from(args: arguments::Named) -> Result { + let mut path = None; + + for (k, ipld) in args { + match k.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(TryFromArgsError::NotAPathBuf); + } + } + s => return Err(TryFromArgsError::InvalidField(s.into())), + } + } + + Ok(Destroy { path }) + } +} + +#[derive(Error, Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum TryFromArgsError { + #[error("Path value is not a PathBuf")] + NotAPathBuf, + + #[error("Invalid map key {0}")] + InvalidField(String), +} + +impl promise::Resolvable for Destroy { + type Promised = PromisedDestroy; +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/destroy` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// destroy("crud/destroy") +/// end +/// end +/// end +/// +/// destroypromise("crud::destroy::Promised") +/// destroyready("crud::destroy::Destroy") +/// +/// top --> any --> mutate --> destroy +/// destroy -.->|invoke| destroypromise -.->|resolve| destroyready -.-> exe{{execute}} +/// +/// style destroypromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedDestroy { + /// An optional path to a sub-resource that is to be destroyed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, +} + +impl TryFrom> for PromisedDestroy { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedDestroy { path }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl Command for PromisedDestroy { + const COMMAND: &'static str = COMMAND; +} + +// impl From for arguments::Named { +// fn from(promised: PromisedDestroy) -> Self { +// let mut named = arguments::Named::new(); +// +// if let Some(path_res) = promised.path { +// named.insert( +// "path".to_string(), +// path_res.map(|p| ipld::Newtype::from(p).0).into(), +// ); +// } +// +// named +// } +// } + +impl From for PromisedDestroy { + fn from(r: Destroy) -> PromisedDestroy { + PromisedDestroy { + path: r + .path + .map(|inner_path| promise::Any::Resolved(inner_path).into()), + } + } +} + +impl From for arguments::Named { + fn from(promised: PromisedDestroy) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = promised.path { + named.insert("path".to_string(), path.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/js.rs b/src/ability/crud/js.rs new file mode 100644 index 00000000..57a56bff --- /dev/null +++ b/src/ability/crud/js.rs @@ -0,0 +1,35 @@ +//! JavaScript bindings for the CRUD abilities. + +use super::read; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct CrudRead(#[wasm_bindgen(skip)] pub read::Ready); + +#[wasm_bindgen] +impl CrudRead { + pub fn to_jsvalue(self) -> JsValue { + ipld::Newtype(Ipld::from(self.0)).into() + } + + pub fn from_jsvalue(js_val: JsValue) -> Result { + ipld::Newtype::try_into_jsvalue(js_val).map(CrudRead) + } + + pub fn to_command(&self) -> String { + Read::to_command() + } + + pub fn check_same(&self, proof: &CrudRead) -> Result<(), JsError> { + self.0.check_same(&proof.0).map_err(Into::into) + } + + // FIXME more than any + pub fn check_parent(&self, proof: &CrudAny) -> Result<(), JsError> { + self.0.check_parent(&proof.0).map_err(Into::into) + } +} + +// FIXME needs bindings +#[wasm_bindgen] +pub struct CrudReadPromise(#[wasm_bindgen(skip)] pub read::Promised); diff --git a/src/ability/crud/read.rs b/src/ability/crud/read.rs new file mode 100644 index 00000000..873636e2 --- /dev/null +++ b/src/ability/crud/read.rs @@ -0,0 +1,252 @@ +//! Read from a resource. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::{error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// This ability is used to fetch messages from other actors. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("crud/*") +/// +/// subgraph Invokable +/// read("crud/read") +/// end +/// end +/// +/// readpromise("crud::read::Promised") +/// readready("crud::read::Read") +/// +/// top --> any --> read +/// read -.->|invoke| readpromise -.->|resolve| readready -.-> exe{{execute}} +/// +/// style readready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Read { + /// An optional path to a sub-resource that is to be read. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Optional arguments to modify the read request. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>, +} + +impl From for Ipld { + fn from(ready: Read) -> Self { + let mut map = BTreeMap::new(); + + if let Some(path) = ready.path { + map.insert("path".to_string(), Ipld::String(path.display().to_string())); + } + + if let Some(args) = ready.args { + map.insert("args".to_string(), args.into()); + } + + map.into() + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/read` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// subgraph Invokable +/// read("crud/read") +/// end +/// end +/// end +/// +/// readpromise("crud::read::Promised") +/// readready("crud::read::Read") +/// +/// top --> any --> read +/// read -.->|invoke| readpromise -.->|resolve| readready -.-> exe{{execute}} +/// +/// style readpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedRead { + /// An optional path to a sub-resource that is to be read. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub path: Option>, + + /// Optional arguments to modify the read request. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub args: Option>>, +} + +impl TryFrom> for PromisedRead { + type Error = FromPromisedArgsError; + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, prom) in arguments { + match k.as_str() { + "path" => match prom { + ipld::Promised::String(s) => { + path = Some(promise::Any::Resolved(PathBuf::from(s)).into()); + } + ipld::Promised::WaitOk(cid) => { + path = Some(promise::Any::PendingOk(cid).into()); + } + ipld::Promised::WaitErr(cid) => { + path = Some(promise::Any::PendingErr(cid).into()); + } + ipld::Promised::WaitAny(cid) => { + path = Some(promise::Any::PendingAny(cid).into()); + } + _ => return Err(FromPromisedArgsError::InvalidPath(k)), + }, + + "args" => { + args = match prom { + ipld::Promised::Map(map) => { + Some(promise::Any::Resolved(arguments::Named(map)).into()) + } + ipld::Promised::WaitOk(cid) => Some(promise::Any::PendingOk(cid).into()), + ipld::Promised::WaitErr(cid) => Some(promise::Any::PendingErr(cid).into()), + ipld::Promised::WaitAny(cid) => Some(promise::Any::PendingAny(cid).into()), + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + } + } + _ => return Err(FromPromisedArgsError::InvalidMapKey(k)), + } + } + + Ok(PromisedRead { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Invalid path {0}")] + InvalidPath(String), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +const COMMAND: &'static str = "/crud/read"; + +impl Command for Read { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedRead { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom for Read { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for arguments::Named { + fn from(ready: Read) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = ready.path { + named.insert( + "path".to_string(), + path.into_os_string() + .into_string() + .expect("PathBuf should make a valid path") + .into(), + ); + } + + if let Some(args) = ready.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl TryFrom> for Read { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (k, v) in arguments.into_iter() { + match k.as_str() { + "path" => { + if let Ipld::String(string) = v { + path = Some(PathBuf::from(string)); + } else { + return Err(()); + } + } + "args" => { + args = Some(arguments::Named::try_from(v).map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Read { path, args }) + } +} + +impl promise::Resolvable for Read { + type Promised = PromisedRead; +} + +impl From for arguments::Named { + fn from(promised: PromisedRead) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path_res) = promised.path { + named.insert("path".to_string(), path_res.to_promised_ipld()); + } + + if let Some(args_res) = promised.args { + named.insert("args".to_string(), args_res.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/crud/update.rs b/src/ability/crud/update.rs new file mode 100644 index 00000000..935ab40f --- /dev/null +++ b/src/ability/crud/update.rs @@ -0,0 +1,298 @@ +//! Update existing resources. + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, path::PathBuf}; +use thiserror::Error; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `crud/create` ability. +/// +/// # Lifecycle +/// +/// The relevant hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// update("crud/update") +/// end +/// end +/// end +/// +/// updatepromise("crud::update::Promised") +/// updateready("crud::update::Update") +/// +/// top --> any --> mutate --> update +/// update -.->|invoke| updatepromise -.->|resolve| updateready -.-> exe{{execute}} +/// +/// style updateready stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Update { + /// An optional path to a sub-resource that is to be updated. + #[serde(default, skip_serializing_if = "Option::is_none")] + path: Option, + + /// Optional arguments to be passed in the update. + #[serde(default, skip_serializing_if = "Option::is_none")] + args: Option>, +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// An invoked `crud/update` ability (but possibly awaiting another +/// [`Invocation`][crate::invocation::Invocation]). +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of CRUD abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// subgraph CRUD Abilities +/// any("crud/*") +/// +/// mutate("crud/mutate") +/// +/// subgraph Invokable +/// update("crud/update") +/// end +/// end +/// end +/// +/// updatepromise("crud::update::Promised") +/// updateready("crud::update::Update") +/// +/// top --> any --> mutate --> update +/// update -.->|invoke| updatepromise -.->|resolve| updateready -.-> exe{{execute}} +/// +/// style updatepromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedUpdate { + /// An optional path to a sub-resource that is to be updated. + #[serde(default, skip_serializing_if = "Option::is_none")] + path: Option>, + + /// Optional arguments to be passed in the update. + #[serde(default, skip_serializing_if = "Option::is_none")] + args: Option>>, +} + +const COMMAND: &'static str = "/crud/update"; + +impl Command for Update { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedUpdate { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for PromisedUpdate { + type Error = FromPromisedArgsError; + + fn try_from(named: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (key, prom) in named { + match key.as_str() { + "path" => match Ipld::try_from(prom) { + Err(pending) => { + path = Some(pending.into()); + } + Ok(ipld) => match ipld { + Ipld::String(s) => path = Some(promise::Any::Resolved(PathBuf::from(s))), + other => return Err(FromPromisedArgsError::PathBodyNotAString(other)), + }, + }, + + "args" => match prom { + ipld::Promised::Map(map) => { + args = Some(promise::Any::Resolved(arguments::Named(map))) + } + ipld::Promised::WaitOk(cid) => args = Some(promise::Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => args = Some(promise::Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => { + args = Some(promise::Any::PendingAny(cid)); + } + _ => return Err(FromPromisedArgsError::InvalidArgs(prom)), + }, + + _ => return Err(FromPromisedArgsError::InvalidMapKey(key)), + } + } + + Ok(PromisedUpdate { path, args }) + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedArgsError { + #[error("Path body is not a string")] + PathBodyNotAString(Ipld), + + #[error("Invalid args {0}")] + InvalidArgs(ipld::Promised), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl TryFrom> for Update { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut path = None; + let mut args = None; + + for (key, ipld) in named { + match key.as_str() { + "path" => { + if let Ipld::String(s) = ipld { + path = Some(PathBuf::from(s)); + } else { + return Err(()); + } + } + "args" => { + if let Ipld::Map(map) = ipld { + args = Some(arguments::Named(map)); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(Update { path, args }) + } +} + +impl From for arguments::Named { + fn from(create: Update) -> Self { + let mut named = arguments::Named::::new(); + + if let Some(path) = create.path { + named.insert("path".to_string(), Ipld::String(path.display().to_string())); + } + + if let Some(args) = create.args { + named.insert("args".to_string(), args.into()); + } + + named + } +} + +impl TryFrom for Update { + type Error = TryFromIpldError; + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Map(map) = ipld { + if map.len() > 2 { + return Err(TryFromIpldError::TooManyKeys); + } + + Ok(Update { + path: map + .get("path") + .map(|ipld| { + (ipld::Newtype(ipld.clone())) + .try_into() + .map_err(TryFromIpldError::InvalidPath) + }) + .transpose()?, + + args: map + .get("args") + .map(|ipld| { + arguments::Named::::try_from(ipld.clone()) + .map_err(|_| TryFromIpldError::InvalidArgs) + }) + .transpose()?, + }) + } else { + Err(TryFromIpldError::NotAMap) + } + } +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum TryFromIpldError { + #[error("Not a map")] + NotAMap, + + #[error("Too many keys")] + TooManyKeys, + + #[error("Invalid path: {0}")] + InvalidPath(ipld::newtype::NotAString), + + #[error("Invalid args: not a map")] + InvalidArgs, +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum FromPromisedUpdateError { + #[error("Unresolved args")] + UnresolvedArgs(promise::Any>), + + #[error("Args pending")] + ArgsPending(>::Error), + + #[error("Invalid map key {0}")] + InvalidMapKey(String), +} + +impl From for PromisedUpdate { + fn from(r: Update) -> PromisedUpdate { + PromisedUpdate { + path: r.path.map(|inner_path| promise::Any::Resolved(inner_path)), + + args: r + .args + .map(|inner_args| promise::Any::Resolved(inner_args.into())), + } + } +} + +impl promise::Resolvable for Update { + type Promised = PromisedUpdate; +} + +impl From for arguments::Named { + fn from(promised: PromisedUpdate) -> Self { + let mut named = arguments::Named::new(); + + if let Some(path) = promised.path { + named.insert("path".to_string(), path.to_promised_ipld()); + } + + if let Some(args) = promised.args { + named.insert("args".to_string(), args.to_promised_ipld()); + } + + named + } +} diff --git a/src/ability/dynamic.rs b/src/ability/dynamic.rs new file mode 100644 index 00000000..e5cbe65e --- /dev/null +++ b/src/ability/dynamic.rs @@ -0,0 +1,130 @@ +//! This module is for dynamic abilities, especially for FFI and Wasm support + +use super::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError}, +}; +use libipld_core::{error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use js_sys; + +// NOTE the lack of checking functions! + +/// A "dynamic" ability with the bare minimum of statics +/// +///
+/// This should be a last resort, and only for e.g. FFI. The Dynamic ability is +/// not recommended for typical Rust usage. +/// +/// This is instead meant to be embedded inside of structs that have e.g. FFI bindings to +/// a validation function, such as `js_sys::Function` for JS, `magnus::function!` for Ruby, +/// and so on. +///
+/// +/// [`Dynamic`] uses none of the typical ability traits directly. Rather, it must be wrapped +/// in [`Reader`][crate::reader::Reader], which wires up dynamic dispatch for the +/// relevant traits using a configuration struct. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] // FIXME serialize / deserilaize? +pub struct Dynamic { + /// The `cmd` field (hooks into a dynamic version of [`Command`][crate::ability::command::Command]) + pub cmd: String, + + /// Unstructured, named arguments + /// + /// The only requirement is that the keys are strings and the values are [`Ipld`] + pub args: arguments::Named, +} + +impl ParseAbility for Dynamic { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + Ok(Dynamic { + cmd: cmd.to_string(), + args, + }) + } +} + +impl ToCommand for Dynamic { + fn to_command(&self) -> String { + self.cmd.clone() + } +} + +impl From for arguments::Named { + fn from(dynamic: Dynamic) -> Self { + dynamic.args + } +} + +#[cfg(target_arch = "wasm32")] +impl From for js_sys::Map { + fn from(ability: Dynamic) -> Self { + let args = js_sys::Map::new(); + for (k, v) in ability.args.0 { + args.set(&k.into(), &ipld::Newtype(v).into()); + } + + let map = js_sys::Map::new(); + map.set(&"args".into(), &js_sys::Object::from(args).into()); + map.set(&"cmd".into(), &ability.cmd.into()); + map + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Dynamic { + type Error = JsValue; + + fn try_from(map: js_sys::Map) -> Result { + if let (Some(cmd), js_args) = ( + map.get(&("cmd".into())).as_string(), + &map.get(&("args".into())), + ) { + let obj_args = js_sys::Object::try_from(js_args).ok_or(wasm_bindgen::JsValue::NULL)?; + let keys = js_sys::Object::keys(obj_args); + let values = js_sys::Object::values(obj_args); + + let mut btree = BTreeMap::new(); + for (k, v) in keys.iter().zip(values) { + if let Some(k) = k.as_string() { + btree.insert(k, ipld::Newtype::try_from(v).expect("FIXME").0); + } else { + return Err(k); + } + } + + Ok(Dynamic { + cmd, + args: arguments::Named(btree), + }) + } else { + Err(JsValue::NULL) // FIXME + } + } +} + +impl From for Ipld { + fn from(dynamic: Dynamic) -> Self { + dynamic.into() + } +} + +impl TryFrom for Dynamic { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} diff --git a/src/ability/js.rs b/src/ability/js.rs new file mode 100644 index 00000000..e66d92f2 --- /dev/null +++ b/src/ability/js.rs @@ -0,0 +1,19 @@ +//! Bindings for the JavaScript via Wasm +//! +//! Note that these are all [`wasm_bindgen`]-specific, +//! and are not recommended elsewhere due to limited +//! type safety, poorer performance, and restrictions +//! on the API placed by [`wasm_bindgen`]. +//! +//! The overall pattern is roughly: "JS code hands the +//! Rust code a config object with handlers at runtime". +//! The Rust takes those handlers, and dispatches them +//! as part of the normal flow. +//! +//! When compiled for Wasm, the other abilities in this +//! crate export JS bindings. This allows them to be +//! plugged into e.g. ability hierarchies from the JS +//! side as an extension mechanism. + +pub mod parentful; +pub mod parentless; diff --git a/src/ability/js/config.rs b/src/ability/js/config.rs new file mode 100644 index 00000000..2f42905b --- /dev/null +++ b/src/ability/js/config.rs @@ -0,0 +1,120 @@ +//! JavaScript interface for abilities that *do* require a parent hierarchy + +use crate::{ + ability::{arguments, command::ToCommand, dynamic}, + reader::Reader, +}; +use js_sys::{Function, JsString, Map}; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; +use wasm_bindgen::{prelude::*, JsValue}; + +// FIXME rename +type WithParents = Reader>; + +// FIXME just make into a general config? + +/// The configuration object that expresses an ability (with parents) from JS +#[derive(Debug, Clone, PartialEq, Default)] +#[wasm_bindgen(getter_with_clone)] +pub struct ParentfulConfig { + pub command: String, + + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub is_nonce_meaningful: bool, + + #[wasm_bindgen(js_name = validateShape)] + pub validate_shape: Function, +} + +// NOTE if changed, please update this in the docs for `ParentfulArgs` below +#[wasm_bindgen(typescript_custom_section)] +const PARENTFUL_ARGS: &str = r#" +interface ParentfulArgs { + command: string, + isNonceMeaningful: boolean, + validateShape: Function, +} +"#; + +#[wasm_bindgen] +extern "C" { + /// Named constructor arguments for `ParentfulConfig` + /// + /// This forms the basis for configuring an ability. + /// These values will be used at runtime to perform + /// checks on the ability (e.g. during delegation), + /// for indexing, and storage (among others). + /// + /// ```typescript + /// // TypeScript + /// interface ParentfulArgs { + /// command: string, + /// isNonceMeaningful: boolean, + /// validateShape: Function, + /// } + /// ``` + #[wasm_bindgen(typescript_type = "ParentfulArgs")] + pub type ParentfulArgs; + + /// Get the [`Command`][crate::ability::command::Command] string + #[wasm_bindgen(js_name = command)] + pub fn command(this: &ParentfulArgs) -> String; + + /// Whether the nonce should factor into a receipt's global index ([`task::Id`]) + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub fn is_nonce_meaningful(this: &ParentfulArgs) -> bool; + + /// Parser validator + #[wasm_bindgen(js_name = validateShape)] + pub fn validate_shape(this: &ParentfulArgs) -> Function; +} + +#[wasm_bindgen] +impl ParentfulConfig { + /// Construct a new `ParentfulConfig` from JavaScript + /// + /// # Examples + /// + /// ```javascript + /// // JavaScript + /// const msgSendConfig = new ParentfulConfig({ + /// command: "msg/send", + /// isNonceMeaningful: true, + /// validateShape: (args) => { + /// if (args.to && args.message && args.length() === 2) { + /// return true; + /// } + /// return false; + /// } + /// } + /// ); + /// ``` + #[wasm_bindgen(constructor)] + pub fn new(js_obj: ParentfulArgs) -> Result { + Ok(ParentfulConfig { + command: command(&js_obj), + is_nonce_meaningful: is_nonce_meaningful(&js_obj), + validate_shape: validate_shape(&js_obj), + }) + } +} + +impl From for dynamic::Dynamic { + fn from(js: WithParents) -> Self { + dynamic::Dynamic { + cmd: js.env.command, + args: js.val, + } + } +} + +impl ToCommand for ParentfulConfig { + fn to_command(&self) -> String { + self.command.clone() + } +} + +impl Checkable for WithParents { + type Hierarchy = Parentful; +} diff --git a/src/ability/js/parentful.rs b/src/ability/js/parentful.rs new file mode 100644 index 00000000..2f42905b --- /dev/null +++ b/src/ability/js/parentful.rs @@ -0,0 +1,120 @@ +//! JavaScript interface for abilities that *do* require a parent hierarchy + +use crate::{ + ability::{arguments, command::ToCommand, dynamic}, + reader::Reader, +}; +use js_sys::{Function, JsString, Map}; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; +use wasm_bindgen::{prelude::*, JsValue}; + +// FIXME rename +type WithParents = Reader>; + +// FIXME just make into a general config? + +/// The configuration object that expresses an ability (with parents) from JS +#[derive(Debug, Clone, PartialEq, Default)] +#[wasm_bindgen(getter_with_clone)] +pub struct ParentfulConfig { + pub command: String, + + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub is_nonce_meaningful: bool, + + #[wasm_bindgen(js_name = validateShape)] + pub validate_shape: Function, +} + +// NOTE if changed, please update this in the docs for `ParentfulArgs` below +#[wasm_bindgen(typescript_custom_section)] +const PARENTFUL_ARGS: &str = r#" +interface ParentfulArgs { + command: string, + isNonceMeaningful: boolean, + validateShape: Function, +} +"#; + +#[wasm_bindgen] +extern "C" { + /// Named constructor arguments for `ParentfulConfig` + /// + /// This forms the basis for configuring an ability. + /// These values will be used at runtime to perform + /// checks on the ability (e.g. during delegation), + /// for indexing, and storage (among others). + /// + /// ```typescript + /// // TypeScript + /// interface ParentfulArgs { + /// command: string, + /// isNonceMeaningful: boolean, + /// validateShape: Function, + /// } + /// ``` + #[wasm_bindgen(typescript_type = "ParentfulArgs")] + pub type ParentfulArgs; + + /// Get the [`Command`][crate::ability::command::Command] string + #[wasm_bindgen(js_name = command)] + pub fn command(this: &ParentfulArgs) -> String; + + /// Whether the nonce should factor into a receipt's global index ([`task::Id`]) + #[wasm_bindgen(js_name = isNonceMeaningful)] + pub fn is_nonce_meaningful(this: &ParentfulArgs) -> bool; + + /// Parser validator + #[wasm_bindgen(js_name = validateShape)] + pub fn validate_shape(this: &ParentfulArgs) -> Function; +} + +#[wasm_bindgen] +impl ParentfulConfig { + /// Construct a new `ParentfulConfig` from JavaScript + /// + /// # Examples + /// + /// ```javascript + /// // JavaScript + /// const msgSendConfig = new ParentfulConfig({ + /// command: "msg/send", + /// isNonceMeaningful: true, + /// validateShape: (args) => { + /// if (args.to && args.message && args.length() === 2) { + /// return true; + /// } + /// return false; + /// } + /// } + /// ); + /// ``` + #[wasm_bindgen(constructor)] + pub fn new(js_obj: ParentfulArgs) -> Result { + Ok(ParentfulConfig { + command: command(&js_obj), + is_nonce_meaningful: is_nonce_meaningful(&js_obj), + validate_shape: validate_shape(&js_obj), + }) + } +} + +impl From for dynamic::Dynamic { + fn from(js: WithParents) -> Self { + dynamic::Dynamic { + cmd: js.env.command, + args: js.val, + } + } +} + +impl ToCommand for ParentfulConfig { + fn to_command(&self) -> String { + self.command.clone() + } +} + +impl Checkable for WithParents { + type Hierarchy = Parentful; +} diff --git a/src/ability/msg.rs b/src/ability/msg.rs new file mode 100644 index 00000000..9fd9230e --- /dev/null +++ b/src/ability/msg.rs @@ -0,0 +1,145 @@ +//! Message abilities + +pub mod receive; +pub mod send; + +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use libipld_core::ipld::Ipld; +use receive::{PromisedReceive, Receive}; +use send::{PromisedSend, Send}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +/// A family of abilities for sending and receiving messages. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +pub enum Msg { + /// The ability for sending messages. + Send(Send), + + /// The ability for receiving messages. + Receive(Receive), +} + +impl From for arguments::Named { + fn from(msg: Msg) -> Self { + match msg { + Msg::Send(send) => send.into(), + Msg::Receive(receive) => receive.into(), + } + } +} + +impl From for Ipld { + fn from(msg: Msg) -> Self { + match msg { + Msg::Send(send) => send.into(), + Msg::Receive(receive) => receive.into(), + } + } +} + +/// A promised version of the [`Msg`] ability. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PromisedMsg { + /// The promised ability for sending messages. + Send(PromisedSend), + + /// The promised ability for receiving messages. + Receive(PromisedReceive), +} + +impl ToCommand for Msg { + fn to_command(&self) -> String { + match self { + Msg::Send(send) => send.to_command(), + Msg::Receive(receive) => receive.to_command(), + } + } +} + +impl ToCommand for PromisedMsg { + fn to_command(&self) -> String { + match self { + PromisedMsg::Send(send) => send.to_command(), + PromisedMsg::Receive(receive) => receive.to_command(), + } + } +} + +impl ParsePromised for PromisedMsg { + type PromisedArgsError = (); + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if let Ok(send) = PromisedSend::try_parse_promised(cmd, args.clone()) { + return Ok(PromisedMsg::Send(send)); + } + + if let Ok(receive) = PromisedReceive::try_parse_promised(cmd, args) { + return Ok(PromisedMsg::Receive(receive)); + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +impl Resolvable for Msg { + type Promised = PromisedMsg; +} + +impl From for arguments::Named { + fn from(promised: PromisedMsg) -> Self { + match promised { + PromisedMsg::Send(send) => send.into(), + PromisedMsg::Receive(receive) => receive.into(), + } + } +} + +impl ParseAbility for Msg { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if let Ok(send) = Send::try_parse(cmd, args.clone()) { + return Ok(Msg::Send(send)); + } + + if let Ok(receive) = Receive::try_parse(cmd, args) { + return Ok(Msg::Receive(receive)); + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +// #[cfg(feature = "test_utils")] +// impl Arbitrary for Payload +// where +// T::Strategy: 'static, +// DID::Parameters: Clone, +// { +// type Parameters = (T::Parameters, DID::Parameters); +// type Strategy = BoxedStrategy; +// +// fn arbitrary_with((t_args, did_args): Self::Parameters) -> Self::Strategy { +// } +// } diff --git a/src/ability/msg/receive.rs b/src/ability/msg/receive.rs new file mode 100644 index 00000000..60abcd7f --- /dev/null +++ b/src/ability/msg/receive.rs @@ -0,0 +1,156 @@ +//! The ability to receive messages + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, url, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The ability to receive messages +/// +/// This ability is used to receive messages from other actors. +/// +/// # Delegation Hierarchy +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart TB +/// top("*") +/// +/// subgraph Message Abilities +/// any("msg/*") +/// +/// subgraph Invokable +/// rec("msg/receive") +/// end +/// end +/// +/// recrun{{"invoke"}} +/// +/// top --> any +/// any --> rec -.-> recrun +/// +/// style rec stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +#[serde(deny_unknown_fields)] +pub struct Receive { + /// An *optional* URL (e.g. email, DID, socket) to receive messages from. + /// This assumes that the `subject` has the authority to issue such a capability. + pub from: Option, +} + +const COMMAND: &'static str = "/msg/receive"; + +impl Command for Receive { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedReceive { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for Receive { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut from = None; + + for (key, ipld) in arguments { + match key.as_str() { + "from" => { + from = Some(url::Newtype::try_from(ipld).map_err(|_| ())?); + } + _ => return Err(()), + } + } + + Ok(Receive { from }) + } +} + +impl From for arguments::Named { + fn from(receive: Receive) -> Self { + let mut args = arguments::Named::new(); + + if let Some(from) = receive.from { + args.insert("from".into(), from.into()); + } + + args + } +} + +impl From for Ipld { + fn from(receive: Receive) -> Self { + arguments::Named::::from(receive).into() + } +} + +impl TryFrom for Receive { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Map(map) = ipld { + arguments::Named::(map).try_into().map_err(|_| ()) + } else { + Err(()) + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedReceive { + pub from: Option>, +} + +impl promise::Resolvable for Receive { + type Promised = PromisedReceive; +} + +impl TryFrom> for PromisedReceive { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut from = None; + + for (key, prom) in arguments { + match key.as_str() { + "from" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + from = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => from = Some(pending.into()), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(PromisedReceive { from }) + } +} + +impl From for arguments::Named { + fn from(promised: PromisedReceive) -> Self { + let mut args = arguments::Named::new(); + + if let Some(from) = promised.from { + let _ = from.to_promised_ipld().with_resolved(|ipld| { + args.insert("from".into(), ipld.into()); + }); + } + + args + } +} diff --git a/src/ability/msg/send.rs b/src/ability/msg/send.rs new file mode 100644 index 00000000..c3e47ab5 --- /dev/null +++ b/src/ability/msg/send.rs @@ -0,0 +1,260 @@ +//! The ability to send messages + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, url, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "test_utils")] +use proptest_derive::Arbitrary; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The executable/dispatchable variant of the `msg/send` ability. +/// +/// # Lifecycle +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("msg/*") +/// +/// subgraph Invokable +/// send("msg/send") +/// end +/// end +/// +/// sendpromise("msg::send::Promised") +/// sendrun("msg::send::Send") +/// +/// top --> any +/// any --> send -.->|invoke| sendpromise -.->|resolve| sendrun -.-> exe{{execute}} +/// +/// style sendrun stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "test_utils", derive(Arbitrary))] +#[serde(deny_unknown_fields)] +pub struct Send { + /// The recipient of the message + pub to: url::Newtype, + + /// The sender address of the message + /// + /// This *may* be a URL (such as an email address). + /// If provided, the `subject` must have the right to send from this address. + pub from: url::Newtype, + + /// The main body of the message + pub message: String, +} + +impl From for arguments::Named { + fn from(send: Send) -> Self { + arguments::Named::from_iter([ + ("to".to_string(), send.to.into()), + ("from".to_string(), send.from.into()), + ("message".to_string(), send.message.into()), + ]) + } +} + +impl From for Ipld { + fn from(send: Send) -> Self { + let args = arguments::Named::from(send); + Ipld::Map(args.0) + } +} + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// The invoked variant of the `msg/send` ability +/// +/// This variant may be linked to other invoked abilities by [`Promise`][crate::invocation::Promise]s. +/// +/// # Lifecycle +/// +/// The hierarchy of message abilities is as follows: +/// +/// ```mermaid +/// flowchart LR +/// subgraph Delegations +/// top("*") +/// +/// any("msg/*") +/// +/// subgraph Invokable +/// send("msg/send") +/// end +/// end +/// +/// sendpromise("msg::send::Promised") +/// sendrun("msg::send::Send") +/// +/// top --> any +/// any --> send -.->|invoke| sendpromise -.->|resolve| sendrun -.-> exe{{execute}} +/// +/// style sendpromise stroke:orange; +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PromisedSend { + /// The recipient of the message + pub to: promise::Any, + + /// The sender address of the message + /// + /// This *may* be a URL (such as an email address). + /// If provided, the `subject` must have the right to send from this address. + pub from: promise::Any, + + /// The main body of the message + pub message: promise::Any, +} + +impl promise::Resolvable for Send { + type Promised = PromisedSend; +} + +impl TryFrom> for Send { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut to = None; + let mut from = None; + let mut message = None; + + for (key, value) in named.0 { + match key.as_str() { + "to" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => { + to = Some(url::Newtype::parse(s.as_str()).map_err(|_| ())?) + } + _ => return Err(()), + }, + "from" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => { + from = Some(url::Newtype::parse(s.as_str()).map_err(|_| ())?) + } + _ => return Err(()), + }, + "message" => match Ipld::try_from(value) { + Ok(Ipld::String(s)) => message = Some(s), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(Send { + to: to.ok_or(())?, + from: from.ok_or(())?, + message: message.ok_or(())?, + }) + } +} + +impl TryFrom> for PromisedSend { + type Error = (); + + fn try_from(args: arguments::Named) -> Result { + let mut to = None; + let mut from = None; + let mut message = None; + + for (key, prom) in args.0 { + match key.as_str() { + "to" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + to = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => to = Some(pending.into()), + _ => return Err(()), + }, + "from" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => { + from = Some(promise::Any::Resolved( + url::Newtype::parse(s.as_str()).map_err(|_| ())?, + )); + } + Err(pending) => from = Some(pending.into()), + _ => return Err(()), + }, + "message" => match Ipld::try_from(prom) { + Ok(Ipld::String(s)) => message = Some(promise::Any::Resolved(s)), + Err(pending) => to = Some(pending.into()), + _ => return Err(()), + }, + _ => return Err(()), + } + } + + Ok(PromisedSend { + to: to.ok_or(())?, + from: from.ok_or(())?, + message: message.ok_or(())?, + }) + } +} + +impl From for arguments::Named { + fn from(p: PromisedSend) -> Self { + arguments::Named::from_iter([ + ("to".into(), p.to.into()), + ("from".into(), p.from.into()), + ("message".into(), p.message.into()), + ]) + } +} + +const COMMAND: &'static str = "/msg/send"; + +impl Command for Send { + const COMMAND: &'static str = COMMAND; +} + +impl Command for PromisedSend { + const COMMAND: &'static str = COMMAND; +} + +impl From for PromisedSend { + fn from(r: Send) -> Self { + PromisedSend { + to: promise::Any::Resolved(r.to), + from: promise::Any::Resolved(r.from), + message: promise::Any::Resolved(r.message), + } + } +} + +impl TryFrom for Send { + type Error = PromisedSend; + + fn try_from(p: PromisedSend) -> Result { + match p { + PromisedSend { + to: promise::Any::Resolved(to), + from: promise::Any::Resolved(from), + message: promise::Any::Resolved(message), + } => Ok(Send { to, from, message }), + _ => Err(p), + } + } +} + +impl From for arguments::Named { + fn from(p: PromisedSend) -> Self { + arguments::Named::from_iter([ + ("to".into(), p.to.to_promised_ipld()), + ("from".into(), p.from.to_promised_ipld()), + ("message".into(), p.message.to_promised_ipld()), + ]) + } +} diff --git a/src/ability/parse.rs b/src/ability/parse.rs new file mode 100644 index 00000000..73f9745a --- /dev/null +++ b/src/ability/parse.rs @@ -0,0 +1,68 @@ +use super::command::Command; +use crate::{ability::arguments, ipld}; +use libipld_core::ipld::Ipld; +use std::fmt; +use thiserror::Error; + +pub trait ParseAbility: Sized { + type ArgsErr: fmt::Debug; + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result>; +} + +#[derive(Debug, Clone, Error)] +pub enum ParseAbilityError { + #[error("Unknown command: {0}")] + UnknownCommand(String), + + #[error(transparent)] + InvalidArgs(#[from] E), +} + +impl>> ParseAbility for T +where + >>::Error: fmt::Debug, +{ + type ArgsErr = >>::Error; + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result>>::Error>> { + if cmd != T::COMMAND { + return Err(ParseAbilityError::UnknownCommand(cmd.to_string())); + } + + Self::try_from(args).map_err(ParseAbilityError::InvalidArgs) + } +} + +pub trait ParsePromised: Sized { + type PromisedArgsError; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result>; +} + +impl>> ParsePromised for T +where + >>::Error: fmt::Debug, +{ + type PromisedArgsError = >>::Error; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + if cmd != T::COMMAND { + return Err(ParseAbilityError::UnknownCommand(cmd.to_string())); + } + + Self::try_from(args).map_err(ParseAbilityError::InvalidArgs) + } +} diff --git a/src/ability/pipe.rs b/src/ability/pipe.rs new file mode 100644 index 00000000..536fd55a --- /dev/null +++ b/src/ability/pipe.rs @@ -0,0 +1,22 @@ +use crate::{crypto::varsig, delegation, did::Did, ipld}; +use libipld_core::{codec::Codec, ipld::Ipld}; + +pub struct Pipe, C: Codec + TryFrom + Into> { + pub source: Cap, + pub sink: Cap, +} + +pub enum Cap, C: Codec + TryFrom + Into> { + Proof(delegation::Proof), + Literal(Ipld), +} + +pub struct PromisedPipe, C: Codec + TryFrom + Into> { + pub source: PromisedCap, + pub sink: PromisedCap, +} + +pub enum PromisedCap, C: Codec + TryFrom + Into> { + Proof(delegation::Proof), + Promised(ipld::Promised), +} diff --git a/src/ability/preset.rs b/src/ability/preset.rs new file mode 100644 index 00000000..8adec291 --- /dev/null +++ b/src/ability/preset.rs @@ -0,0 +1,180 @@ +use super::{ + crud::{self, Crud, PromisedCrud}, + msg::{Msg, PromisedMsg}, + ucan::revoke::{PromisedRevoke, Revoke}, + wasm::run as wasm, +}; +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParseAbilityError, ParsePromised}, + }, + invocation::promise::Resolvable, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Preset { + Crud(Crud), + Msg(Msg), + Ucan(Revoke), + Wasm(wasm::Run), +} + +impl From for Preset +where + Crud: From, +{ + fn from(t: T) -> Self { + Preset::Crud(Crud::from(t)) + } +} + +impl ToCommand for Preset { + fn to_command(&self) -> String { + match self { + Preset::Crud(crud) => crud.to_command(), + Preset::Msg(msg) => msg.to_command(), + Preset::Ucan(ucan) => ucan.to_command(), + Preset::Wasm(wasm) => wasm.to_command(), + } + } +} + +impl From for arguments::Named { + fn from(preset: Preset) -> Self { + match preset { + Preset::Crud(crud) => crud.into(), + Preset::Msg(msg) => msg.into(), + Preset::Ucan(ucan) => ucan.into(), + Preset::Wasm(wasm) => wasm.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] //, Serialize, Deserialize)] +pub enum PromisedPreset { + Crud(PromisedCrud), + Msg(PromisedMsg), + Ucan(PromisedRevoke), + Wasm(wasm::PromisedRun), +} + +impl Resolvable for Preset { + type Promised = PromisedPreset; +} + +impl ToCommand for PromisedPreset { + fn to_command(&self) -> String { + match self { + PromisedPreset::Crud(promised) => promised.to_command(), + PromisedPreset::Msg(promised) => promised.to_command(), + PromisedPreset::Ucan(promised) => promised.to_command(), + PromisedPreset::Wasm(promised) => promised.to_command(), + } + } +} + +impl ParsePromised for PromisedPreset { + type PromisedArgsError = ParsePromisedError; + + fn try_parse_promised( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match PromisedCrud::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Crud(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => { + return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Crud( + err, + ))) + } + } + + match PromisedMsg::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Msg(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Msg)), + } + + match wasm::PromisedRun::try_parse_promised(cmd, args.clone()) { + Ok(promised) => return Ok(PromisedPreset::Wasm(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Wasm)), + } + + match PromisedRevoke::try_parse_promised(cmd, args) { + Ok(promised) => return Ok(PromisedPreset::Ucan(promised)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(_err) => return Err(ParseAbilityError::InvalidArgs(ParsePromisedError::Ucan)), + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +#[derive(Debug, Clone, Error)] +pub enum ParsePromisedError { + #[error("Crud error: {0}")] + Crud(ParseAbilityError), + + #[error("Msg error")] + Msg, // FIXME + + #[error("Wasm error")] + Wasm, // FIXME + + #[error("Ucan error")] + Ucan, // FIXME +} + +impl ParseAbility for Preset { + type ArgsErr = (); + + fn try_parse( + cmd: &str, + args: arguments::Named, + ) -> Result> { + match Msg::try_parse(cmd, args.clone()) { + Ok(msg) => return Ok(Preset::Msg(msg)), + Err(ParseAbilityError::UnknownCommand(_)) => (), // FIXME + Err(err) => return Err(err), + } + + match Crud::try_parse(cmd, args.clone()) { + Ok(crud) => return Ok(Preset::Crud(crud)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + match wasm::Run::try_parse(cmd, args.clone()) { + Ok(wasm) => return Ok(Preset::Wasm(wasm)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + match Revoke::try_parse(cmd, args) { + Ok(ucan) => return Ok(Preset::Ucan(ucan)), + Err(ParseAbilityError::UnknownCommand(_)) => (), + Err(err) => return Err(err), + } + + Err(ParseAbilityError::UnknownCommand(cmd.to_string())) + } +} + +impl From for arguments::Named { + fn from(promised: PromisedPreset) -> Self { + match promised { + PromisedPreset::Crud(promised) => promised.into(), + PromisedPreset::Msg(promised) => promised.into(), + PromisedPreset::Ucan(promised) => promised.into(), + PromisedPreset::Wasm(promised) => promised.into(), + } + } +} diff --git a/src/ability/ucan.rs b/src/ability/ucan.rs new file mode 100644 index 00000000..220034b6 --- /dev/null +++ b/src/ability/ucan.rs @@ -0,0 +1,5 @@ +//! Abilities for and about UCANs themselves + +pub mod assert; +pub mod batch; +pub mod revoke; diff --git a/src/ability/ucan/assert.rs b/src/ability/ucan/assert.rs new file mode 100644 index 00000000..62946c23 --- /dev/null +++ b/src/ability/ucan/assert.rs @@ -0,0 +1,30 @@ +use crate::ability::command::Command; +use crate::task::Task; +use libipld_core::{cid::Cid, ipld::Ipld}; + +// Things that you can assert include content and receipts + +#[derive(Debug, PartialEq)] +pub struct Ran { + ran: Cid, + out: Box>, + fx: Vec, // FIXME may be more than "just" a task +} + +impl Command for Ran { + const COMMAND: &'static str = "/ucan/assert/ran"; + // const COMMAND: &'static str = "/ucan/ran";???? +} + +/////////////// +/////////////// +/////////////// + +#[derive(Debug, PartialEq)] +pub struct Claim { + claim: T, +} // Where Ipld: From + +impl Command for Claim { + const COMMAND: &'static str = "/ucan/assert/claim"; +} diff --git a/src/ability/ucan/batch.rs b/src/ability/ucan/batch.rs new file mode 100644 index 00000000..9712fd93 --- /dev/null +++ b/src/ability/ucan/batch.rs @@ -0,0 +1,18 @@ +// use crate::{crypto::varsig, delegation::Delegation, did::Did}; +// use libipld_core::{cid::Cid, codec::Codec, ipld::Ipld}; +// use std::collections::BTreeMap; +// +// #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +// pub struct Batch, Enc: Codec + TryFrom + Into> { +// pub batch: Vec>, // FIXME not quite right; would be nice to include meta etc +// } +// +// pub struct Step, Enc: Codec + TryFrom + Into> { +// pub subject: DID, +// pub audience: Option, +// pub ability: A, // FIXME promise version instead? Promised version shoudl be able to promise any field +// pub cause: Option, +// pub metadata: BTreeMap, +// +// pub cap: Vec>, +// } diff --git a/src/ability/ucan/revoke.rs b/src/ability/ucan/revoke.rs new file mode 100644 index 00000000..b7ee1313 --- /dev/null +++ b/src/ability/ucan/revoke.rs @@ -0,0 +1,112 @@ +//! This is an ability for revoking [`Delegation`][crate::delegation::Delegation]s by their [`Cid`]. +//! +//! For more, see the [UCAN Revocation spec](https://github.com/ucan-wg/revocation). + +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt::Debug; + +/// The fully resolved variant: ready to execute. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Revoke { + /// The UCAN to revoke + pub ucan: Cid, + // FIXME pub witness +} + +impl From for arguments::Named { + fn from(revoke: Revoke) -> Self { + arguments::Named::from_iter([("ucan".to_string(), Ipld::Link(revoke.ucan).into())]) + } +} + +const COMMAND: &'static str = "/ucan/revoke"; + +impl Command for Revoke { + const COMMAND: &'static str = COMMAND; +} +impl Command for PromisedRevoke { + const COMMAND: &'static str = COMMAND; +} + +impl TryFrom> for Revoke { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let ipld: Ipld = arguments.get("ucan").ok_or(())?.clone(); + let nt: ipld::cid::Newtype = ipld.try_into().map_err(|_| ())?; + + Ok(Revoke { ucan: nt.cid }) + } +} + +impl promise::Resolvable for Revoke { + type Promised = PromisedRevoke; +} + +impl From for arguments::Named { + fn from(promised: PromisedRevoke) -> Self { + arguments::Named::from_iter([("ucan".into(), Ipld::from(promised.ucan).into())]) + } +} + +/// A variant where arguments may be [`Promise`][crate::invocation::promise]s. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedRevoke { + pub ucan: promise::Any, +} + +impl TryFrom> for PromisedRevoke { + type Error = (); + + fn try_from(arguments: arguments::Named) -> Result { + let mut ucan = None; + + for (k, prom) in arguments { + match k.as_str() { + "ucan" => match Ipld::try_from(prom) { + Ok(Ipld::Link(cid)) => { + ucan = Some(promise::Any::Resolved(cid)); + } + Err(pending) => ucan = Some(pending.into()), + _ => return Err(()), + }, + _ => (), + } + } + + Ok(PromisedRevoke { + ucan: ucan.ok_or(())?, + }) + } +} + +impl From for PromisedRevoke { + fn from(r: Revoke) -> PromisedRevoke { + PromisedRevoke { + ucan: promise::Any::Resolved(r.ucan), + } + } +} + +impl From for arguments::Named { + fn from(p: PromisedRevoke) -> arguments::Named { + arguments::Named::from_iter([("ucan".into(), p.ucan.into())]) + } +} + +impl TryFrom for Revoke { + type Error = (); + + fn try_from(p: PromisedRevoke) -> Result { + Ok(Revoke { + ucan: p.ucan.try_resolve().map_err(|_| ())?, + }) + } +} diff --git a/src/ability/wasm.rs b/src/ability/wasm.rs new file mode 100644 index 00000000..3aa1e67b --- /dev/null +++ b/src/ability/wasm.rs @@ -0,0 +1,4 @@ +//! [WebAssembly](https://webassembly.org/) abilities + +pub mod module; +pub mod run; diff --git a/src/ability/wasm/module.rs b/src/ability/wasm/module.rs new file mode 100644 index 00000000..c17726d7 --- /dev/null +++ b/src/ability/wasm/module.rs @@ -0,0 +1,89 @@ +//! Wasm module representations + +use crate::ipld; +use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine as _}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, link::Link, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; + +/// Ways to represent a Wasm module in a `wasm/run` payload. +#[derive(Debug, Clone, PartialEq)] +pub enum Module { + /// The raw bytes of the Wasm module + /// + /// Encodes as a `data:` URL + Inline(Vec), + + /// A [`Cid`] link to the Wasm module + Remote(Link>), +} + +impl From for Ipld { + fn from(module: Module) -> Self { + match module { + Module::Inline(bytes) => Ipld::Bytes(bytes), + Module::Remote(cid) => Ipld::Link(*cid), + } + } +} + +impl TryFrom for Module { + type Error = SerdeError; + + fn try_from(nt: ipld::Newtype) -> Result { + ipld_serde::from_ipld(nt.0) + } +} + +impl TryFrom for Module { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl Serialize for Module { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + match self { + Module::Remote(link) => link.cid().serialize(serializer), + Module::Inline(bytes) => format!( + "data:application/wasm;base64,{}", + Base64Display::new(bytes.as_ref(), &STANDARD) + ) + .serialize(serializer), + } + } +} + +impl<'de> Deserialize<'de> for Module { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.starts_with("data:") { + let data = s + .split(',') + .nth(1) + .ok_or_else(|| serde::de::Error::custom("missing base64 data"))?; + + let bytes = STANDARD + .decode(data) + .map_err(|err| serde::de::Error::custom(err))?; + + Ok(Module::Inline(bytes)) + } else { + let cid = Cid::try_from(s).map_err(serde::de::Error::custom)?; + Ok(Module::Remote(Link::new(cid))) + } + } +} + +impl From for ipld::Promised { + fn from(module: Module) -> Self { + module.into() + } +} diff --git a/src/ability/wasm/run.rs b/src/ability/wasm/run.rs new file mode 100644 index 00000000..77f95abc --- /dev/null +++ b/src/ability/wasm/run.rs @@ -0,0 +1,146 @@ +//! Ability to run a Wasm module + +use super::module::Module; +use crate::{ + ability::{arguments, command::Command}, + invocation::promise, + ipld, +}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; + +const COMMAND: &'static str = "/wasm/run"; + +impl Command for Run { + const COMMAND: &'static str = COMMAND; +} + +// FIXME autogenerate for resolvable? +impl Command for PromisedRun { + const COMMAND: &'static str = COMMAND; +} + +/// The ability to run a Wasm module on the subject's machine +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Run { + /// The Wasm module to run + pub module: Module, + + /// The function from the module to run + pub function: String, + + /// Arguments to pass to the function + pub args: Vec, +} + +impl From for arguments::Named { + fn from(run: Run) -> Self { + arguments::Named::from_iter([ + ("mod".into(), Ipld::from(run.module)), + ("fun".into(), run.function.into()), + ("args".into(), run.args.into()), + ]) + } +} + +impl TryFrom> for Run { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut module = None; + let mut function = None; + let mut args = None; + + for (key, ipld) in named { + match key.as_str() { + "mod" => { + module = Some(ipld.try_into().map_err(|_| ())?); + } + "fun" => { + if let Ipld::String(s) = ipld { + function = Some(s); + } else { + return Err(()); + } + } + "args" => { + if let Ipld::List(list) = ipld { + args = Some(list); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(Run { + module: module.ok_or(())?, + function: function.ok_or(())?, + args: args.ok_or(())?, + }) + } +} + +impl promise::Resolvable for Run { + type Promised = PromisedRun; +} + +/// A variant meant for linking together invocations with promises +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PromisedRun { + pub module: promise::Any, + pub function: promise::Any, + pub args: promise::Any>, +} + +impl TryFrom> for PromisedRun { + type Error = (); + + fn try_from(named: arguments::Named) -> Result { + let mut module = None; + let mut function = None; + let mut args = None; + + for (key, prom) in named { + match key.as_str() { + "module" => module = Some(prom.to_promise_any().map_err(|_| ())?), + "function" => function = Some(prom.to_promise_any_string()?), + "args" => { + if let ipld::Promised::List(list) = prom.into() { + args = Some(promise::Any::Resolved(list)); + } else { + return Err(()); + } + } + _ => return Err(()), + } + } + + Ok(PromisedRun { + module: module.ok_or(())?, + function: function.ok_or(())?, + args: args.ok_or(())?, + }) + } +} + +impl From for PromisedRun { + fn from(run: Run) -> Self { + PromisedRun { + module: promise::Any::Resolved(run.module), + function: promise::Any::Resolved(run.function), + args: promise::Any::Resolved(run.args.iter().map(|ipld| ipld.clone().into()).collect()), + } + } +} + +impl From for arguments::Named { + fn from(promised: PromisedRun) -> Self { + arguments::Named::from_iter([ + ("module".into(), promised.module.to_promised_ipld()), + ("function".into(), promised.function.to_promised_ipld()), + ("args".into(), promised.args.to_promised_ipld()), + ]) + } +} diff --git a/src/capsule.rs b/src/capsule.rs new file mode 100644 index 00000000..af2f1b2e --- /dev/null +++ b/src/capsule.rs @@ -0,0 +1,55 @@ +//! Capsule type utilities. +//! +//! Capsule types are a pattern where you associate a string to type, +//! and use the tag as a key and the payload as a value in a map. +//! This helps disambiguate types when serializing and deserializing. +//! +//! Unlike a `type` field, the fact that it's on the outside of the payload +//! is often helpful in improving serializaion and deserialization performance. +//! It also avoids needing fields on nested structures where the inner types are known. +//! +//! Some simple examples include: +//! +//! ```javascript +//! {"u32": 42} +//! {"i64": 99} +//! {"coord": {"x": 1, "y": 2}} +//! { +//! "boundary": [ +//! {"x": 1, "y": 2}, // ─┐ +//! {"x": 3, "y": 4}, // ├─ Untagged coords inside "boundary" capsule +//! {"x": 5, "y": 6}, // │ +//! {"x": 7, "y": 8} // ─┘ +//! ] +//! } +//! ``` +//! +//! UCAN uses these in payload wrappers, such as [`Delegation`][crate::delegation::Delegation]. + +/// The primary capsule trait +/// +/// # Examples +/// +/// ```rust +/// # use ucan::capsule::Capsule; +/// # use std::collections::BTreeMap; +/// # +/// # #[derive(Debug, PartialEq)] +/// struct Coord { +/// x: i32, +/// y: i32 +/// } +/// +/// impl Capsule for Coord { +/// const TAG: &'static str = "coordinate"; +/// } +/// +/// let coord = Coord { x: 1, y: 2 }; +/// let capsuled = BTreeMap::from_iter([(Coord::TAG.to_string(), coord)]); +/// +/// assert_eq!(capsuled.get("coordinate"), Some(&Coord { x: 1, y: 2 })); +/// ```` +pub trait Capsule { + /// The tag to use when constructing or matching on the capsule + const TAG: &'static str; +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 00000000..4bc82072 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,22 @@ +//! Cryptographic signature utilities + +mod domain_separator; +mod nonce; + +pub mod signature; +pub mod varsig; + +pub use domain_separator::DomainSeparator; +pub use nonce::*; + +#[cfg(feature = "bls")] +pub mod bls12381; + +#[cfg(feature = "es512")] +pub mod es512; + +#[cfg(feature = "rs256")] +pub mod rs256; + +#[cfg(feature = "rs512")] +pub mod rs512; diff --git a/src/crypto/bls12381.rs b/src/crypto/bls12381.rs new file mode 100644 index 00000000..2ad56f6f --- /dev/null +++ b/src/crypto/bls12381.rs @@ -0,0 +1,5 @@ +//! BLS12-381 signature support + +pub mod error; +pub mod min_pk; +pub mod min_sig; diff --git a/src/crypto/bls12381/error.rs b/src/crypto/bls12381/error.rs new file mode 100644 index 00000000..8475a875 --- /dev/null +++ b/src/crypto/bls12381/error.rs @@ -0,0 +1,52 @@ +use blst::BLST_ERROR; +use enum_as_inner::EnumAsInner; +use thiserror::Error; + +/// Errors that can occur during BLS verification. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error, EnumAsInner)] +pub enum VerificationError { + /// Signature mismatch. + #[error("signature mismatch")] + VerifyMsgFail, + + /// Bad encoding. + #[error("bad encoding")] + BadEncoding, + + /// Point not on curve. + #[error("point not on curve")] + PointNotOnCurve, + + /// Point not in group. + #[error("bad point not in group")] + PointNotInGroup, + + /// Aggregate type mismatch. + #[error("aggregate type mismatch")] + AggrTypeMismatch, + + /// Public key is infinity. + #[error("public key is infinity")] + PkIsInfinity, + + /// Bad scalar. + #[error("bad scalar")] + BadScalar, +} + +impl TryFrom for VerificationError { + type Error = (); + + fn try_from(err: BLST_ERROR) -> Result { + match err { + BLST_ERROR::BLST_SUCCESS => Err(()), + BLST_ERROR::BLST_VERIFY_FAIL => Ok(VerificationError::VerifyMsgFail), + BLST_ERROR::BLST_BAD_ENCODING => Ok(VerificationError::BadEncoding), + BLST_ERROR::BLST_POINT_NOT_ON_CURVE => Ok(VerificationError::PointNotOnCurve), + BLST_ERROR::BLST_POINT_NOT_IN_GROUP => Ok(VerificationError::PointNotInGroup), + BLST_ERROR::BLST_AGGR_TYPE_MISMATCH => Ok(VerificationError::AggrTypeMismatch), + BLST_ERROR::BLST_PK_IS_INFINITY => Ok(VerificationError::PkIsInfinity), + BLST_ERROR::BLST_BAD_SCALAR => Ok(VerificationError::BadScalar), + } + } +} diff --git a/src/crypto/bls12381/min_pk.rs b/src/crypto/bls12381/min_pk.rs new file mode 100644 index 00000000..87a58f38 --- /dev/null +++ b/src/crypto/bls12381/min_pk.rs @@ -0,0 +1,53 @@ +use super::error::VerificationError; +use crate::crypto::domain_separator::DomainSeparator; +use blst::BLST_ERROR; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// A BLS12-381 MinPubKey signature +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub blst::min_pk::Signature); + +impl DomainSeparator for Signature { + /// From the [IETF BLS Signature Spec](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#section-4.2.1) + const DST: &'static [u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = BLST_ERROR; + + fn try_from(bytes: &'a [u8]) -> Result { + Ok(Self(blst::min_pk::Signature::uncompress(bytes)?)) + } +} + +impl From for [u8; 96] { + fn from(sig: Signature) -> Self { + sig.0.compress() + } +} + +impl SignatureEncoding for Signature { + type Repr = [u8; 96]; +} + +impl Signer for blst::min_pk::SecretKey { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(Signature(self.sign(msg, Signature::DST, &[]))) + } +} + +impl Verifier for blst::min_pk::PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match VerificationError::try_from(signature.0.verify( + true, + msg, + Signature::DST, + &[], + &self, + true, + )) { + Ok(err) => Err(signature::Error::from_source(err)), + Err(_) => Ok(()), + } + } +} diff --git a/src/crypto/bls12381/min_sig.rs b/src/crypto/bls12381/min_sig.rs new file mode 100644 index 00000000..1298f098 --- /dev/null +++ b/src/crypto/bls12381/min_sig.rs @@ -0,0 +1,53 @@ +use super::error::VerificationError; +use crate::crypto::domain_separator::DomainSeparator; +use blst::BLST_ERROR; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// A BLS12-381 MinSig signature +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub blst::min_sig::Signature); + +impl DomainSeparator for Signature { + /// From the [IETF BLS Signature Spec](https://www.ietf.org/archive/id/draft-irtf-cfrg-bls-signature-05.html#section-4.2.1) + const DST: &'static [u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = BLST_ERROR; + + fn try_from(bytes: &'a [u8]) -> Result { + Ok(Self(blst::min_sig::Signature::uncompress(bytes)?)) + } +} + +impl From for [u8; 48] { + fn from(sig: Signature) -> Self { + sig.0.compress() + } +} + +impl SignatureEncoding for Signature { + type Repr = [u8; 48]; +} + +impl Signer for blst::min_sig::SecretKey { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(Signature(self.sign(msg, Signature::DST, &[]))) + } +} + +impl Verifier for blst::min_sig::PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match VerificationError::try_from(signature.0.verify( + true, + msg, + Signature::DST, + &[], + &self, + true, + )) { + Ok(err) => Err(signature::Error::from_source(err)), + Err(_) => Ok(()), + } + } +} diff --git a/src/crypto/domain_separator.rs b/src/crypto/domain_separator.rs new file mode 100644 index 00000000..fbe325f3 --- /dev/null +++ b/src/crypto/domain_separator.rs @@ -0,0 +1,7 @@ +//! Domain separation utilities. + +/// Static domain separator for the DID method. +pub trait DomainSeparator { + /// The domain separator bytes; + const DST: &'static [u8]; +} diff --git a/src/crypto/es512.rs b/src/crypto/es512.rs new file mode 100644 index 00000000..18a31e25 --- /dev/null +++ b/src/crypto/es512.rs @@ -0,0 +1,33 @@ +//! ES512 signature support (P-512) + +use p521; +use signature::Verifier; +use std::fmt; + +/// The verifying/public key for ES512. +#[derive(Clone)] // FIXME , Serialize, Deserialize)] +pub struct VerifyingKey(pub p521::ecdsa::VerifyingKey); + +impl fmt::Debug for VerifyingKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VerifyingKey").finish() + } +} + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.to_encoded_point(true) == other.0.to_encoded_point(true) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify( + &self, + msg: &[u8], + signature: &p521::ecdsa::Signature, + ) -> Result<(), signature::Error> { + self.0.verify(msg, &signature) + } +} diff --git a/src/crypto/nonce.rs b/src/crypto/nonce.rs new file mode 100644 index 00000000..0e46d2dc --- /dev/null +++ b/src/crypto/nonce.rs @@ -0,0 +1,360 @@ +//! [Nonce]s & utilities. +//! +//! [Nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + +use enum_as_inner::EnumAsInner; +use getrandom::getrandom; +use libipld_core::{ + ipld::Ipld, + multibase::Base::Base32HexLower, + multihash::{Hasher, Sha2_256}, +}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// Known [`Nonce`] types +#[derive(Clone, Debug, EnumAsInner, Serialize, Deserialize)] +pub enum Nonce { + /// 96-bit, 12-byte nonce + Nonce12([u8; 12]), + + /// 128-bit, 16-byte nonce + Nonce16([u8; 16]), + + /// Dynamic sized nonce + Custom(Vec), +} + +impl PartialEq for Nonce { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Nonce::Nonce12(a), Nonce::Nonce12(b)) => a == b, + (Nonce::Nonce16(a), Nonce::Nonce16(b)) => a == b, + (Nonce::Custom(a), Nonce::Custom(b)) => a == b, + (Nonce::Custom(a), Nonce::Nonce12(b)) => { + if a.len() == 12 { + a.as_slice() == b + } else { + false + } + } + (Nonce::Custom(a), Nonce::Nonce16(b)) => { + if a.len() == 16 { + a.as_slice() == b + } else { + false + } + } + (Nonce::Nonce12(a), Nonce::Custom(b)) => { + if b.len() == 12 { + a == b.as_slice() + } else { + false + } + } + (Nonce::Nonce16(a), Nonce::Custom(b)) => { + if b.len() == 16 { + a == b.as_slice() + } else { + false + } + } + _ => false, + } + } +} + +impl From<[u8; 12]> for Nonce { + fn from(s: [u8; 12]) -> Self { + Nonce::Nonce12(s) + } +} + +impl From<[u8; 16]> for Nonce { + fn from(s: [u8; 16]) -> Self { + Nonce::Nonce16(s) + } +} + +impl From for Vec { + fn from(nonce: Nonce) -> Self { + match nonce { + Nonce::Nonce12(nonce) => nonce.to_vec(), + Nonce::Nonce16(nonce) => nonce.to_vec(), + Nonce::Custom(nonce) => nonce, + } + } +} + +impl From> for Nonce { + fn from(nonce: Vec) -> Self { + if let Ok(twelve) = <[u8; 12]>::try_from(nonce.clone()) { + return twelve.into(); + } + + if let Ok(sixteen) = <[u8; 16]>::try_from(nonce.clone()) { + return sixteen.into(); + } + + Nonce::Custom(nonce) + } +} + +impl Nonce { + /// Generate a 96-bit, 12-byte nonce. + /// This is the minimum nonce size typically recommended. + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + /// + /// # Example + /// + /// ```rust + /// # use ucan::crypto::Nonce; + /// # use ucan::did::Did; + /// # + /// let mut salt = "did:example:123".as_bytes().to_vec(); + /// let nonce = Nonce::generate_12(&mut salt); + /// + /// assert_eq!(Vec::from(nonce).len(), 12); + /// ``` + pub fn generate_12(salt: &mut Vec) -> Nonce { + salt.append(&mut [0].repeat(12)); + + let buf = salt.as_mut_slice(); + getrandom(buf).expect("irrecoverable getrandom failure"); + + let mut hasher = Sha2_256::default(); + hasher.update(buf); + + let bytes = hasher + .finalize() + .chunks(12) + .next() + .expect("SHA2_256 is 32 bytes") + .try_into() + .expect("we set the length to 12 earlier"); + + Nonce::Nonce12(bytes) + } + + /// Generate a 128-bit, 16-byte nonce + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + /// + /// # Example + /// + /// ```rust + /// # use ucan::crypto::Nonce; + /// # use ucan::did::Did; + /// # + /// let mut salt = "did:example:123".as_bytes().to_vec(); + /// let nonce = Nonce::generate_16(&mut salt); + /// + /// assert_eq!(Vec::from(nonce).len(), 16); + /// ``` + pub fn generate_16(salt: &mut Vec) -> Nonce { + salt.append(&mut [0].repeat(16)); + + let buf = salt.as_mut_slice(); + getrandom(buf).expect("irrecoverable getrandom failure"); + + let mut hasher = Sha2_256::default(); + hasher.update(buf); + + let bytes = hasher + .finalize() + .chunks(16) + .next() + .expect("SHA2_256 is 32 bytes") + .try_into() + .expect("we set the length to 16 earlier"); + + Nonce::Nonce16(bytes) + } +} + +impl fmt::Display for Nonce { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Nonce::Nonce12(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + Nonce::Nonce16(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + Nonce::Custom(nonce) => { + write!(f, "{}", Base32HexLower.encode(nonce.as_slice())) + } + } + } +} + +impl From for Ipld { + fn from(nonce: Nonce) -> Self { + match nonce { + Nonce::Nonce12(nonce) => Ipld::Bytes(nonce.to_vec()), + Nonce::Nonce16(nonce) => Ipld::Bytes(nonce.to_vec()), + Nonce::Custom(nonce) => Ipld::Bytes(nonce), + } + } +} + +impl TryFrom for Nonce { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::Bytes(v) = ipld { + match v.len() { + 12 => Ok(Nonce::Nonce12( + v.try_into() + .expect("12 bytes because we checked in the match"), + )), + 16 => Ok(Nonce::Nonce16( + v.try_into() + .expect("16 bytes because we checked in the match"), + )), + _ => Ok(Nonce::Custom(v)), + } + } else { + Err(()) + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Nonce { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::<[u8; 12]>().prop_map(Nonce::Nonce12), + any::<[u8; 16]>().prop_map(Nonce::Nonce16), + any::>().prop_map(Nonce::Custom) + ] + .boxed() + } +} + +// FIXME move module? +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[wasm_bindgen] +/// A JavaScript-compatible wrapper for [`Nonce`] +pub struct JsNonce(#[wasm_bindgen(skip)] pub Nonce); + +#[cfg(target_arch = "wasm32")] +impl From for Nonce { + fn from(newtype: JsNonce) -> Self { + newtype.0 + } +} + +#[cfg(target_arch = "wasm32")] +impl From for JsNonce { + fn from(nonce: Nonce) -> Self { + JsNonce(nonce) + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl JsNonce { + /// Generate a 96-bit, 12-byte nonce. + /// This is the minimum nonce size typically recommended. + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + pub fn generate_12(mut salt: Vec) -> JsNonce { + Nonce::generate_12(&mut salt).into() + } + + /// Generate a 128-bit, 16-byte nonce + /// + /// # Arguments + /// + /// * `salt` - A salt. This may be left empty, but is recommended to avoid collision. + pub fn generate_16(mut salt: Vec) -> JsNonce { + Nonce::generate_16(&mut salt).into() + } + + /// Directly lift a 12-byte `Uint8Array` into a [`JsNonce`] + /// + /// # Arguments + /// + /// * `nonce` - The exact nonce to convert to a [`JsNonce`] + pub fn from_uint8_array(arr: Box<[u8]>) -> JsNonce { + Nonce::from(arr.to_vec()).into() + } + + /// Expose the underlying bytes of a [`JsNonce`] as a 12-byte `Uint8Array` + /// + /// # Arguments + /// + /// * `self` - The [`JsNonce`] to convert to a `Uint8Array` + pub fn to_uint8_array(&self) -> Box<[u8]> { + match &self.0 { + Nonce::Nonce12(nonce) => nonce.to_vec().into_boxed_slice(), + Nonce::Nonce16(nonce) => nonce.to_vec().into_boxed_slice(), + Nonce::Custom(nonce) => nonce.clone().into_boxed_slice(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + // FIXME prop test with lots of inputs + #[test] + fn ipld_roundtrip_12() { + let gen = Nonce::generate_12(&mut vec![]); + let ipld = Ipld::from(gen.clone()); + + let inner = if let Nonce::Nonce12(nonce) = gen { + Ipld::Bytes(nonce.to_vec()) + } else { + panic!("No conversion!") + }; + + assert_eq!(ipld, inner); + assert_eq!(gen, ipld.try_into().unwrap()); + } + + // FIXME prop test with lots of inputs + #[test] + fn ipld_roundtrip_16() { + let gen = Nonce::generate_16(&mut vec![]); + let ipld = Ipld::from(gen.clone()); + + let inner = if let Nonce::Nonce16(nonce) = gen { + Ipld::Bytes(nonce.to_vec()) + } else { + panic!("No conversion!") + }; + + assert_eq!(ipld, inner); + assert_eq!(gen, ipld.try_into().unwrap()); + } + + // FIXME prop test with lots of inputs + // #[test] + // fn ser_de() { + // let gen = Nonce::generate_16(&mut vec![]); + // let ser = serde_json::to_string(&gen).unwrap(); + // let de = serde_json::from_str(&ser).unwrap(); + + // assert_eq!(gen, de); + // } +} diff --git a/src/crypto/p521.rs b/src/crypto/p521.rs new file mode 100644 index 00000000..e5b13a37 --- /dev/null +++ b/src/crypto/p521.rs @@ -0,0 +1,32 @@ +use p521; +use serde::{Deserialize, Serialize}; +use signature::Verifier; +use std::fmt; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +pub struct VerifyingKey(pub p521::ecdsa::VerifyingKey); + +impl fmt::Debug for VerifyingKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VerifyingKey").finish() + } +} + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.to_encoded_point(true) == other.0.to_encoded_point(true) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify( + &self, + msg: &[u8], + signature: &p521::ecdsa::Signature, + ) -> Result<(), signature::Error> { + self.0.verify(msg, &signature) + } +} diff --git a/src/crypto/rs256.rs b/src/crypto/rs256.rs new file mode 100644 index 00000000..de6205cd --- /dev/null +++ b/src/crypto/rs256.rs @@ -0,0 +1,67 @@ +//! RS256 signature support (2048-bit RSA PKCS #1 v1.5). + +use rsa; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// The verifying/public key for RS256. +#[derive(Debug, Clone)] +pub struct VerifyingKey(pub rsa::pkcs1v15::VerifyingKey); + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + self.0.as_ref() == other.0.as_ref() + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(msg, &signature.0) + } +} + +/// The signing/secret key for RS256. +#[derive(Debug, Clone)] +pub struct SigningKey(pub rsa::pkcs1v15::SigningKey); + +impl Signer for SigningKey { + fn try_sign(&self, msg: &[u8]) -> Result { + self.0.try_sign(msg).map(Signature) + } +} + +/// The signature for RS256. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(pub rsa::pkcs1v15::Signature); + +impl SignatureEncoding for Signature { + type Repr = [u8; 256]; +} + +impl From<[u8; 256]> for Signature { + fn from(bytes: [u8; 256]) -> Self { + Signature( + rsa::pkcs1v15::Signature::try_from(bytes.as_ref()) + .expect("passed in [u8; 256], so should succeed"), + ) + } +} + +impl From for [u8; 256] { + fn from(sig: Signature) -> [u8; 256] { + sig.0 + .to_bytes() + .as_ref() + .try_into() + .expect("Signature should be exactly 256 bytes") + } +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = signature::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + rsa::pkcs1v15::Signature::try_from(bytes).map(Signature) + } +} diff --git a/src/crypto/rs512.rs b/src/crypto/rs512.rs new file mode 100644 index 00000000..32a739a7 --- /dev/null +++ b/src/crypto/rs512.rs @@ -0,0 +1,67 @@ +//! RS512 signature support (4096-bit RSA PKCS #1 v1.5). + +use rsa; +use signature::{SignatureEncoding, Signer, Verifier}; + +/// The verifying/public key for RS512. +#[derive(Debug, Clone)] // FIXME , Serialize, Deserialize)] +pub struct VerifyingKey(pub rsa::pkcs1v15::VerifyingKey); + +impl PartialEq for VerifyingKey { + fn eq(&self, other: &Self) -> bool { + rsa::RsaPublicKey::from(self.0.clone()) == rsa::RsaPublicKey::from(other.0.clone()) + } +} + +impl Eq for VerifyingKey {} + +impl Verifier for VerifyingKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0.verify(msg, &signature.0) + } +} + +/// The signing/secret key for RS512. +#[derive(Debug, Clone)] // FIXME , Serialize, Deserialize)] +pub struct SigningKey(pub rsa::pkcs1v15::SigningKey); + +impl Signer for SigningKey { + fn try_sign(&self, msg: &[u8]) -> Result { + self.0.try_sign(msg).map(Signature) + } +} + +/// The signature for RS512. +#[derive(Debug, Clone, PartialEq, Eq)] // FIXME , Serialize, Deserialize)] +pub struct Signature(pub rsa::pkcs1v15::Signature); + +impl SignatureEncoding for Signature { + type Repr = [u8; 512]; +} + +impl From<[u8; 512]> for Signature { + fn from(bytes: [u8; 512]) -> Self { + Signature( + rsa::pkcs1v15::Signature::try_from(bytes.as_ref()) + .expect("passed in [u8; 512], so should succeed"), + ) + } +} + +impl From for [u8; 512] { + fn from(sig: Signature) -> [u8; 512] { + sig.0 + .to_bytes() + .as_ref() + .try_into() + .expect("Signature should be exactly 512 bytes") + } +} + +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = signature::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + rsa::pkcs1v15::Signature::try_from(bytes).map(Signature) + } +} diff --git a/src/crypto/signature.rs b/src/crypto/signature.rs new file mode 100644 index 00000000..41e90739 --- /dev/null +++ b/src/crypto/signature.rs @@ -0,0 +1,5 @@ +//! Signatures and cryptographic envelopes. + +mod envelope; + +pub use envelope::*; diff --git a/src/crypto/signature/envelope.rs b/src/crypto/signature/envelope.rs new file mode 100644 index 00000000..170b719d --- /dev/null +++ b/src/crypto/signature/envelope.rs @@ -0,0 +1,248 @@ +use crate::ability::arguments::Named; +use crate::{capsule::Capsule, crypto::varsig, did::Did}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + error::Result, + ipld::Ipld, + multihash::{Code, MultihashDigest}, +}; +use signature::SignatureEncoding; +use signature::Verifier; +use std::collections::BTreeMap; +use thiserror::Error; + +pub trait Envelope: Sized { + type DID: Did; + type Payload: Clone + Capsule + TryFrom> + Into>; + type VarsigHeader: varsig::Header + Clone; + type Encoder: Codec + TryFrom + Into; + + fn varsig_header(&self) -> &Self::VarsigHeader; + fn signature(&self) -> &::Signature; + fn payload(&self) -> &Self::Payload; + fn verifier(&self) -> &Self::DID; + + fn construct( + varsig_header: Self::VarsigHeader, + signature: ::Signature, + payload: Self::Payload, + ) -> Self; + + fn to_ipld_envelope(&self) -> Ipld { + let inner_args: Named = self.payload().clone().into(); + let inner_ipld: Ipld = inner_args.into(); + + let wrapped_payload: Ipld = + BTreeMap::from_iter([(Self::Payload::TAG.into(), inner_ipld)]).into(); + + let header_bytes: Vec = (*self.varsig_header()).clone().into(); + let header: Ipld = vec![header_bytes.into(), wrapped_payload].into(); + let sig_bytes: Ipld = self.signature().to_vec().into(); + + vec![sig_bytes.into(), header].into() + } + + fn try_from_ipld_envelope( + ipld: Ipld, + ) -> Result>>::Error>> { + if let Ipld::List(list) = ipld { + if let [Ipld::Bytes(sig), Ipld::List(inner)] = list.as_slice() { + if let [Ipld::Bytes(varsig_header), Ipld::Map(btree)] = inner.as_slice() { + if let (1, Some(Ipld::Map(inner))) = ( + btree.len(), + btree.get(::TAG.into()), + ) { + let payload = Self::Payload::try_from(Named(inner.clone())) + .map_err(FromIpldError::CannotParsePayload)?; + + let varsig_header = Self::VarsigHeader::try_from(varsig_header.as_slice()) + .map_err(|_| FromIpldError::CannotParseVarsigHeader)?; + + let signature = ::Signature::try_from(sig.as_slice()) + .map_err(|_| FromIpldError::CannotParseSignature)?; + + Ok(Self::construct(varsig_header, signature, payload)) + } else { + Err(FromIpldError::InvalidPayloadCapsule) + } + } else { + Err(FromIpldError::InvalidVarsigContainer) + } + } else { + Err(FromIpldError::InvalidSignatureContainer) + } + } else { + Err(FromIpldError::InvalidSignatureContainer) + } + } + + fn varsig_encode(self, w: &mut Vec) -> Result<(), libipld_core::error::Error> + where + Ipld: Encode + From, + { + let codec = varsig::header::Header::codec(self.varsig_header()).clone(); + let ipld = Ipld::from(self); + ipld.encode(codec, w) + } + + /// Attempt to sign some payload with a given signer. + /// + /// # Arguments + /// + /// * `signer` - The signer to use to sign the payload. + /// * `payload` - The payload to sign. + /// + /// # Errors + /// + /// * [`SignError`] - the payload can't be encoded or the signature fails. + // FIXME ported + fn try_sign( + signer: &::Signer, + varsig_header: Self::VarsigHeader, + payload: Self::Payload, + ) -> Result + where + Ipld: Encode, + Named: From, + { + Self::try_sign_generic(signer, varsig_header, payload) + } + + /// Attempt to sign some payload with a given signer and specific codec. + /// + /// # Arguments + /// + /// * `signer` - The signer to use to sign the payload. + /// * `codec` - The codec to use to encode the payload. + /// * `payload` - The payload to sign. + /// + /// # Errors + /// + /// * [`SignError`] - the payload can't be encoded or the signature fails. + /// + /// # Example + /// + fn try_sign_generic( + signer: &::Signer, + varsig_header: Self::VarsigHeader, + payload: Self::Payload, + ) -> Result + where + Ipld: Encode, + Named: From, + { + let ipld: Ipld = BTreeMap::from_iter([( + Self::Payload::TAG.into(), + Named::::from(payload.clone()).into(), + )]) + .into(); + + let mut buffer = vec![]; + ipld.encode(*varsig::header::Header::codec(&varsig_header), &mut buffer) + .map_err(SignError::PayloadEncodingError)?; + + let signature = + signature::Signer::try_sign(signer, &buffer).map_err(SignError::SignatureError)?; + + Ok(Self::construct(varsig_header, signature, payload)) + } + + /// Attempt to validate a signature. + /// + /// # Arguments + /// + /// * `self` - The envelope to validate. + /// + /// # Errors + /// + /// * [`ValidateError`] - the payload can't be encoded or the signature fails. + /// + /// # Exmaples + /// + /// FIXME + fn validate_signature(&self) -> Result<(), ValidateError> + where + Ipld: Encode, + Named: From, + { + let mut encoded = vec![]; + let ipld: Ipld = BTreeMap::from_iter([( + Self::Payload::TAG.to_string(), + Named::::from(self.payload().clone()).into(), + )]) + .into(); + + ipld.encode( + *varsig::header::Header::codec(self.varsig_header()), + &mut encoded, + ) + .map_err(ValidateError::PayloadEncodingError)?; + + self.verifier() + .verify(&encoded, &self.signature()) + .map_err(ValidateError::VerifyError) + } + + fn cid(&self) -> Result + where + Ipld: Encode, + { + let codec = varsig::header::Header::codec(self.varsig_header()).clone(); + let mut ipld_buffer = vec![]; + self.to_ipld_envelope().encode(codec, &mut ipld_buffer)?; + + let multihash = Code::Sha2_256.digest(&ipld_buffer); + Ok(Cid::new_v1( + varsig::header::Header::codec(self.varsig_header()) + .clone() + .into(), + multihash, + )) + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +pub enum FromIpldError { + #[error("Invalid signature container")] + InvalidSignatureContainer, + + #[error("Invalid varsig container")] + InvalidVarsigContainer, + + #[error("Cannot parse payload: {0}")] + CannotParsePayload(#[from] E), + + #[error("Cannot parse varsig header")] + CannotParseVarsigHeader, + + #[error("Cannot parse signature")] + CannotParseSignature, + + #[error("Invalid payload capsule")] + InvalidPayloadCapsule, +} + +/// Errors that can occur when signing a [`siganture::Envelope`][Envelope]. +#[derive(Debug, Error)] +pub enum SignError { + /// Unable to encode the payload. + #[error("Unable to encode payload")] + PayloadEncodingError(#[from] libipld_core::error::Error), + + /// Error while signing. + #[error("Signature error: {0}")] + SignatureError(#[from] signature::Error), +} + +/// Errors that can occur when validating a [`signature::Envelope`][Envelope]. +#[derive(Debug, Error)] +pub enum ValidateError { + /// Unable to encode the payload. + #[error("Unable to encode payload")] + PayloadEncodingError(#[from] libipld_core::error::Error), + + /// Error while verifying the signature. + #[error("Signature verification failed: {0}")] + VerifyError(#[from] signature::Error), +} diff --git a/src/crypto/varsig.rs b/src/crypto/varsig.rs new file mode 100644 index 00000000..9308f13e --- /dev/null +++ b/src/crypto/varsig.rs @@ -0,0 +1,4 @@ +pub mod encoding; +pub mod header; + +pub use header::Header; diff --git a/src/crypto/varsig/encoding.rs b/src/crypto/varsig/encoding.rs new file mode 100644 index 00000000..237cb0ef --- /dev/null +++ b/src/crypto/varsig/encoding.rs @@ -0,0 +1,3 @@ +mod preset; + +pub use preset::Preset; diff --git a/src/crypto/varsig/encoding/preset.rs b/src/crypto/varsig/encoding/preset.rs new file mode 100644 index 00000000..e69a5a87 --- /dev/null +++ b/src/crypto/varsig/encoding/preset.rs @@ -0,0 +1,147 @@ +use crate::crypto::signature::Envelope; +use crate::delegation::Delegation; +use libipld_core::codec::Codec; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Preset { + Identity = 0x5f, + DagPb = 0x70, + DagCbor = 0x71, + DagJson = 0x0129, + Jwt = 0x6a77, // FIXME break out Jwt & EIP-191? + Eip191 = 0xe191, +} + +impl Encode for Ipld { + fn encode( + &self, + c: Preset, + w: &mut W, + ) -> Result<(), libipld_core::error::Error> { + match c { + Preset::Identity => todo!(), + Preset::DagPb => todo!(), + Preset::DagCbor => self.encode(libipld_cbor::DagCborCodec, w), + Preset::DagJson => todo!(), + Preset::Jwt => todo!(), + Preset::Eip191 => todo!(), + } + } +} + +impl Encode for Delegation { + fn encode( + &self, + c: Preset, + w: &mut W, + ) -> Result<(), libipld_core::error::Error> { + self.clone().to_ipld_envelope().encode(c, w) + } +} + +impl TryFrom for Preset { + type Error = libipld_core::error::UnsupportedCodec; + + fn try_from(value: u64) -> Result { + match value { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => Ok(Preset::MerkleBatchSig), + _ => Err(libipld_core::error::UnsupportedCodec(value)), + } + } +} + +impl From for u64 { + fn from(encoding: Preset) -> u64 { + encoding as u64 + } +} + +impl Codec for Preset {} + +// FIXME pub struct MerkleSig + +impl<'a> TryFrom<&'a [u8]> for Preset { + type Error = (); + + fn try_from(bytes: &'a [u8]) -> Result { + if let (encoding_info, &[]) = unsigned_varint::decode::u64(&bytes).map_err(|_| ())? { + return match encoding_info { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => { + // let merkle_proof = Vec::new(); + // Ok(Preset::MerkleBatchSig(merkle_proof)) + // } + _ => Err(()), + }; + }; + + Err(()) + } +} + +impl AsRef<[u8]> for Preset { + fn as_ref(&self) -> &[u8] { + match self { + Preset::Identity => &[0x5f], + Preset::DagPb => &[0x70], + Preset::DagCbor => &[0x71], + Preset::DagJson => &[0x01, 0x29], + Preset::Jwt => &[0x6a, 0x77], + Preset::Eip191 => &[0xe1, 0x91], + // Preset::Eip191(inner) => { + // let mut buffer = vec![0xe191]; + // buffer.extend(inner.as_ref()); + // buffer.as_ref() + // } // Preset::MerkleBatchSig(merkle_proof) => { + // let mut buffer = vec![0xe1]; + // buffer.extend(merkle_proof.as_ref()); + // buffer.as_ref() + // } + } + } +} + +impl From for u32 { + fn from(encoding: Preset) -> u32 { + match encoding { + Preset::Identity => 0x5f, + Preset::DagPb => 0x70, + Preset::DagCbor => 0x71, + Preset::DagJson => 0x0129, + Preset::Jwt => 0x6a77, + Preset::Eip191 => 0xe191, + // Preset::MerkleBatchSig(_) => 0xe1, + } + } +} + +impl TryFrom for Preset { + type Error = libipld_core::error::UnsupportedCodec; + + fn try_from(value: u32) -> Result { + match value { + 0x5f => Ok(Preset::Identity), + 0x70 => Ok(Preset::DagPb), + 0x71 => Ok(Preset::DagCbor), + 0x0129 => Ok(Preset::DagJson), + 0x6a77 => Ok(Preset::Jwt), + 0xe191 => Ok(Preset::Eip191), + // 0xe1 => Ok(Preset::MerkleBatchSig), + _ => Err(libipld_core::error::UnsupportedCodec(value as u64)), + } + } +} diff --git a/src/crypto/varsig/header.rs b/src/crypto/varsig/header.rs new file mode 100644 index 00000000..17005ac6 --- /dev/null +++ b/src/crypto/varsig/header.rs @@ -0,0 +1,17 @@ +mod eddsa; +mod es256; +mod es256k; +mod es512; +mod preset; +mod rs256; +mod rs512; +mod traits; + +pub use eddsa::EdDsaHeader; +pub use es256::Es256Header; +pub use es256k::Es256kHeader; +pub use es512::Es512Header; +pub use preset::Preset; +pub use rs256::Rs256Header; +pub use rs512::Rs512Header; +pub use traits::Header; diff --git a/src/crypto/varsig/header/eddsa.rs b/src/crypto/varsig/header/eddsa.rs new file mode 100644 index 00000000..5dd2009e --- /dev/null +++ b/src/crypto/varsig/header/eddsa.rs @@ -0,0 +1,43 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct EdDsaHeader { + pub codec: C, +} + +impl> TryFrom<&[u8]> for EdDsaHeader { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0xed, inner)) = unsigned_varint::decode::u8(&bytes) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&inner) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(EdDsaHeader { codec }); + } + } + + return Err(()); + } +} + +impl + Clone> From> for Vec { + fn from(ed: EdDsaHeader) -> Vec { + let mut tag_buf: [u8; 2] = Default::default(); + let tag: &[u8] = unsigned_varint::encode::u8(0xed, &mut tag_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc: &[u8] = unsigned_varint::encode::u64(ed.codec.into(), &mut enc_buf); + + [tag, enc].concat().into() + } +} + +impl + TryFrom> Header for EdDsaHeader { + type Signature = ed25519_dalek::Signature; + type Verifier = ed25519_dalek::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es256.rs b/src/crypto/varsig/header/es256.rs new file mode 100644 index 00000000..4e53a460 --- /dev/null +++ b/src/crypto/varsig/header/es256.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es256Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es256Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1200, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es256Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es256Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1200, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl + TryFrom> Header for Es256Header { + type Signature = p256::ecdsa::Signature; + type Verifier = p256::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es256k.rs b/src/crypto/varsig/header/es256k.rs new file mode 100644 index 00000000..465c4338 --- /dev/null +++ b/src/crypto/varsig/header/es256k.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es256kHeader { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es256kHeader { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0xe7, inner)) = unsigned_varint::decode::u8(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner).map_err(|_| ()) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es256kHeader { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es256kHeader) -> Vec { + let mut tag_buf: [u8; 2] = Default::default(); + let tag = unsigned_varint::encode::u8(0xe7, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl + TryFrom> Header for Es256kHeader { + type Signature = k256::ecdsa::Signature; + type Verifier = k256::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/es512.rs b/src/crypto/varsig/header/es512.rs new file mode 100644 index 00000000..07089fac --- /dev/null +++ b/src/crypto/varsig/header/es512.rs @@ -0,0 +1,48 @@ +use super::Header; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Es512Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Es512Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1202, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x13, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Es512Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(es: Es512Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1202, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x13, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(es.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl + TryFrom> Header for Es512Header { + type Signature = p521::ecdsa::Signature; + type Verifier = p521::ecdsa::VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/preset.rs b/src/crypto/varsig/header/preset.rs new file mode 100644 index 00000000..80664d5e --- /dev/null +++ b/src/crypto/varsig/header/preset.rs @@ -0,0 +1,106 @@ +use super::{eddsa, es256, es256k, es512, rs256, rs512, Header}; +use crate::{crypto::varsig::encoding, did::key}; + +#[derive(Clone, Debug, PartialEq)] +pub enum Preset { + EdDsa(eddsa::EdDsaHeader), + Es256(es256::Es256Header), + Es256k(es256k::Es256kHeader), + Es512(es512::Es512Header), + Rs256(rs256::Rs256Header), + Rs512(rs512::Rs512Header), + // FIXME BLS? needs varsig specs + // FIXME Es384 needs varsig specs +} + +impl From> for Preset { + fn from(ed: eddsa::EdDsaHeader) -> Self { + Preset::EdDsa(ed) + } +} + +impl From> for Preset { + fn from(rs256: rs256::Rs256Header) -> Self { + Preset::Rs256(rs256) + } +} + +impl From> for Preset { + fn from(rs512: rs512::Rs512Header) -> Self { + Preset::Rs512(rs512) + } +} + +impl From> for Preset { + fn from(es256: es256::Es256Header) -> Self { + Preset::Es256(es256) + } +} + +impl From> for Preset { + fn from(es256k: es256k::Es256kHeader) -> Self { + Preset::Es256k(es256k) + } +} + +impl From for Vec { + fn from(preset: Preset) -> Vec { + match preset { + Preset::EdDsa(ed) => ed.into(), + Preset::Rs256(rs256) => rs256.into(), + Preset::Rs512(rs512) => rs512.into(), + Preset::Es256(es256) => es256.into(), + Preset::Es256k(es256k) => es256k.into(), + Preset::Es512(es512) => es512.into(), + } + } +} + +impl<'a> TryFrom<&'a [u8]> for Preset { + type Error = (); + + fn try_from(bytes: &'a [u8]) -> Result { + if let Ok(ed) = eddsa::EdDsaHeader::try_from(bytes) { + return Ok(Preset::EdDsa(ed)); + } + + if let Ok(rs256) = rs256::Rs256Header::::try_from(bytes) { + return Ok(Preset::Rs256(rs256)); + } + + if let Ok(rs512) = rs512::Rs512Header::::try_from(bytes) { + return Ok(Preset::Rs512(rs512)); + } + + if let Ok(es256) = es256::Es256Header::::try_from(bytes) { + return Ok(Preset::Es256(es256)); + } + + if let Ok(es256k) = es256k::Es256kHeader::::try_from(bytes) { + return Ok(Preset::Es256k(es256k)); + } + + if let Ok(es512) = es512::Es512Header::::try_from(bytes) { + return Ok(Preset::Es512(es512)); + } + + Err(()) + } +} + +impl Header for Preset { + type Signature = key::Signature; + type Verifier = key::Verifier; + + fn codec(&self) -> &encoding::Preset { + match self { + Preset::EdDsa(ed) => ed.codec(), + Preset::Rs256(rs256) => rs256.codec(), + Preset::Rs512(rs512) => rs512.codec(), + Preset::Es256(es256) => es256.codec(), + Preset::Es256k(es256k) => es256k.codec(), + Preset::Es512(es512) => es512.codec(), + // Preset::Bls + } + } +} diff --git a/src/crypto/varsig/header/rs256.rs b/src/crypto/varsig/header/rs256.rs new file mode 100644 index 00000000..bc137ecb --- /dev/null +++ b/src/crypto/varsig/header/rs256.rs @@ -0,0 +1,61 @@ +use super::Header; +use crate::crypto::rs256::{Signature, VerifyingKey}; +use libipld_core::codec::Codec; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq)] +pub struct Rs256Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Rs256Header { + type Error = ParseFromBytesError<>::Error>; + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1205, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x12, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = + C::try_from(codec_info).map_err(ParseFromBytesError::CodecPrefixError)?; + + return Ok(Rs256Header { codec }); + } + } + } + + Err(ParseFromBytesError::InvalidHeader) + } +} + +#[derive(Debug, PartialEq, Clone, Error)] +pub enum ParseFromBytesError { + #[error("Invalid header")] + InvalidHeader, + + #[error("Codec prefix error: {0}")] + CodecPrefixError(#[from] C), +} + +impl> From> for Vec { + fn from(rs: Rs256Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1205, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x12, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(rs.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl + TryFrom> Header for Rs256Header { + type Signature = Signature; + type Verifier = VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/rs512.rs b/src/crypto/varsig/header/rs512.rs new file mode 100644 index 00000000..7b9d161d --- /dev/null +++ b/src/crypto/varsig/header/rs512.rs @@ -0,0 +1,49 @@ +use super::Header; +use crate::crypto::rs512::{Signature, VerifyingKey}; +use libipld_core::codec::Codec; + +#[derive(Clone, Debug, PartialEq)] +pub struct Rs512Header { + pub codec: C, +} + +impl> TryFrom<&[u8]> for Rs512Header { + type Error = (); // FIXME + + fn try_from(bytes: &[u8]) -> Result { + if let Ok((0x1205, inner)) = unsigned_varint::decode::u16(&bytes) { + if let Ok((0x13, more)) = unsigned_varint::decode::u8(&inner) { + if let Ok((codec_info, &[])) = unsigned_varint::decode::u64(&more) { + let codec = C::try_from(codec_info).map_err(|_| ())?; + return Ok(Rs512Header { codec }); + } + } + } + + Err(()) + } +} + +impl> From> for Vec { + fn from(rs: Rs512Header) -> Vec { + let mut tag_buf: [u8; 3] = Default::default(); + let tag = unsigned_varint::encode::u16(0x1205, &mut tag_buf); + + let mut hash_buf: [u8; 2] = Default::default(); + let hash = unsigned_varint::encode::u8(0x13, &mut hash_buf); + + let mut enc_buf: [u8; 10] = Default::default(); + let enc = unsigned_varint::encode::u64(rs.codec.into(), &mut enc_buf); + + [tag, hash, enc].concat().into() + } +} + +impl + TryFrom> Header for Rs512Header { + type Signature = Signature; + type Verifier = VerifyingKey; + + fn codec(&self) -> &C { + &self.codec + } +} diff --git a/src/crypto/varsig/header/traits.rs b/src/crypto/varsig/header/traits.rs new file mode 100644 index 00000000..a91bfe29 --- /dev/null +++ b/src/crypto/varsig/header/traits.rs @@ -0,0 +1,44 @@ +use libipld_core::codec::{Codec, Encode}; +use signature::Verifier; +use thiserror::Error; + +pub trait Header + Into>: + for<'a> TryFrom<&'a [u8]> + Into> +{ + type Signature: signature::SignatureEncoding; + type Verifier: signature::Verifier; + + fn codec(&self) -> &Enc; + + fn encode_payload, Buf: std::io::Write>( + &self, + payload: T, + buffer: &mut Buf, + ) -> Result<(), libipld_core::error::Error> { + payload.encode(Self::codec(self).clone(), buffer) + } + + fn try_verify<'a, T: Encode>( + &self, + verifier: &'a Self::Verifier, + signature: &'a Self::Signature, + payload: T, + ) -> Result<(), VerifyError> { + let mut buffer = vec![]; + self.encode_payload(payload, &mut buffer) + .map_err(VerifyError::CodecError)?; + + verifier + .verify(&buffer, signature) + .map_err(VerifyError::SignatureError) + } +} + +#[derive(Debug, Error)] +pub enum VerifyError { + #[error("Varsig codec error: {0}")] + CodecError(libipld_core::error::Error), + + #[error("varsig signature error: {0}")] + SignatureError(signature::Error), +} diff --git a/src/delegation.rs b/src/delegation.rs new file mode 100644 index 00000000..e25aa5fe --- /dev/null +++ b/src/delegation.rs @@ -0,0 +1,194 @@ +//! A [`Delegation`] is the way to grant someone else the use of [`Ability`][crate::ability]. +//! +//! ## Data +//! +//! - [`Delegation`] is the top-level, signed data struture. +//! - [`Payload`] is the fields unique to an invocation. +//! - [`Preset`] is an [`Delegation`] preloaded with this library's [preset abilities](crate::ability::preset::Ready). +//! - [`Predicate`]s are syntactically-driven validation rules for [`Delegation`]s. +//! +//! ## Stateful Helpers +//! +//! - [`Agent`] is a high-level interface for sessions that will involve more than one invoctaion. +//! - [`store`] is an interface for caching [`Delegation`]s. + +pub mod policy; +pub mod store; + +mod agent; +mod payload; + +pub use agent::Agent; +pub use payload::*; + +use crate::ability::arguments::Named; +use crate::{ + capsule::Capsule, + crypto::{signature::Envelope, varsig, Nonce}, + did::{self, Did}, + time::{TimeBoundError, Timestamp}, +}; +use libipld_core::link::Link; +use libipld_core::{codec::Codec, ipld::Ipld}; +use policy::Predicate; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use web_time::SystemTime; + +/// A [`Delegation`] is a signed delegation [`Payload`] +/// +/// A [`Payload`] on its own is not a valid [`Delegation`], as it must be signed by the issuer. +#[derive(Clone, Debug, PartialEq)] +pub struct Delegation< + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub payload: Payload, + pub signature: DID::Signature, + _marker: std::marker::PhantomData, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Proof< + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + pub prf: Vec>>, +} + +impl, C: Codec + TryFrom + Into> Capsule + for Proof +{ + const TAG: &'static str = "ucan/prf"; +} + +impl, C: Codec + Into + TryFrom> Delegation { + pub fn new( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Delegation { + Delegation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + /// Retrive the `issuer` of a [`Delegation`] + pub fn issuer(&self) -> &DID { + &self.payload.issuer + } + + /// Retrive the `subject` of a [`Delegation`] + pub fn subject(&self) -> &Option { + &self.payload.subject + } + + /// Retrive the `audience` of a [`Delegation`] + pub fn audience(&self) -> &DID { + &self.payload.audience + } + + /// Retrive the `policy` of a [`Delegation`] + pub fn policy(&self) -> &Vec { + &self.payload.policy + } + + /// Retrive the `metadata` of a [`Delegation`] + pub fn metadata(&self) -> &BTreeMap { + &self.payload.metadata + } + + /// Retrive the `nonce` of a [`Delegation`] + pub fn nonce(&self) -> &Nonce { + &self.payload.nonce + } + + /// Retrive the `not_before` of a [`Delegation`] + pub fn not_before(&self) -> Option<&Timestamp> { + self.payload.not_before.as_ref() + } + + /// Retrive the `expiration` of a [`Delegation`] + pub fn expiration(&self) -> &Timestamp { + &self.payload.expiration + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), TimeBoundError> { + self.payload.check_time(now) + } +} + +impl + Clone, C: Codec + TryFrom + Into> Envelope + for Delegation +where + Payload: TryFrom>, + Named: From>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Delegation { + Delegation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl + Clone, C: Codec + TryFrom + Into> Serialize + for Delegation +where + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl<'de, DID: Did + Clone, V: varsig::Header + Clone, C: Codec + TryFrom + Into> + Deserialize<'de> for Delegation +where + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/delegation/agent.rs b/src/delegation/agent.rs new file mode 100644 index 00000000..16482c14 --- /dev/null +++ b/src/delegation/agent.rs @@ -0,0 +1,165 @@ +use super::{payload::Payload, policy::Predicate, store::Store, Delegation}; +use crate::ability::arguments::Named; +use crate::did; +use crate::{ + crypto::{signature::Envelope, varsig, Nonce}, + did::Did, + time::Timestamp, +}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use std::{collections::BTreeMap, marker::PhantomData}; +use thiserror::Error; +use web_time::SystemTime; + +/// A stateful agent capable of delegating to others, and being delegated to. +/// +/// This is helpful for sessions where more than one delegation will be made. +#[derive(Debug)] +pub struct Agent< + S: Store, + DID: Did + Clone = did::preset::Verifier, + V: varsig::Header + Clone = varsig::header::Preset, + C: Codec + Into + TryFrom = varsig::encoding::Preset, +> where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + /// The [`Did`][Did] of the agent. + pub did: DID, + + /// The attached [`deleagtion::Store`][super::store::Store]. + pub store: S, + + signer: ::Signer, + _marker: PhantomData<(V, C)>, +} + +impl< + S: Store + Clone, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Agent +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + pub fn new(did: DID, signer: ::Signer, store: S) -> Self { + Self { + did, + store, + signer, + _marker: PhantomData, + } + } + + pub fn delegate( + &self, + audience: DID, + subject: Option, + via: Option, + command: String, + new_policy: Vec, + metadata: BTreeMap, + expiration: Timestamp, + not_before: Option, + now: SystemTime, + varsig_header: V, + ) -> Result, DelegateError> { + let mut salt = self.did.clone().to_string().into_bytes(); + let nonce = Nonce::generate_12(&mut salt); + + if let Some(ref sub) = subject { + if sub == &self.did { + let payload: Payload = Payload { + issuer: self.did.clone(), + audience, + subject, + via, + command, + metadata, + nonce, + expiration: expiration.into(), + not_before: not_before.map(Into::into), + policy: new_policy, + }; + + return Ok( + Delegation::try_sign(&self.signer, varsig_header, payload).expect("FIXME") + ); + } + } + + let proofs = &self + .store + .get_chain(&self.did, &subject, "/".into(), vec![], now) + .map_err(DelegateError::StoreError)? + .ok_or(DelegateError::ProofsNotFound)?; + let to_delegate = proofs.first().1.payload(); + + let mut policy = to_delegate.policy.clone(); + policy.append(&mut new_policy.clone()); + + let payload: Payload = Payload { + issuer: self.did.clone(), + audience, + subject, + via, + command, + policy, + metadata, + nonce, + expiration: expiration.into(), + not_before: not_before.map(Into::into), + }; + + Ok(Delegation::try_sign(&self.signer, varsig_header, payload).expect("FIXME")) + } + + pub fn receive( + &self, + cid: Cid, // FIXME remove and generate from the capsule header? + delegation: Delegation, + ) -> Result<(), ReceiveError> { + if self.store.get(&cid).is_ok() { + return Ok(()); + } + + if delegation.audience() != &self.did { + return Err(ReceiveError::WrongAudience(delegation.audience().clone())); + } + + delegation + .validate_signature() + .map_err(|_| ReceiveError::InvalidSignature(cid))?; + + self.store.insert_keyed(cid, delegation).map_err(Into::into) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum DelegateError { + #[error("The current agent does not have the necessary proofs to delegate.")] + ProofsNotFound, + + #[error(transparent)] + StoreError(#[from] StoreErr), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum ReceiveError { + #[error("The current agent ({0}) is not the intended audience of the delegation.")] + WrongAudience(DID), + + #[error("Signature for UCAN with CID {0} is invalid.")] + InvalidSignature(Cid), + + #[error(transparent)] + StoreError(#[from] StoreErr), +} diff --git a/src/delegation/payload.rs b/src/delegation/payload.rs new file mode 100644 index 00000000..c54438ec --- /dev/null +++ b/src/delegation/payload.rs @@ -0,0 +1,488 @@ +use super::policy::{predicate, Predicate}; +use crate::ability::arguments::Named; +use crate::time; +use crate::{ + capsule::Capsule, + crypto::{varsig, Nonce}, + did::{Did, Verifiable}, + time::{TimeBoundError, Timestamp}, +}; +use core::str::FromStr; +use derive_builder::Builder; +use did_url::DID; +use libipld_core::{codec::Codec, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Debug}; +use thiserror::Error; +use web_time::SystemTime; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +/// The payload portion of a [`Delegation`][super::Delegation]. +/// +/// This contains the semantic information about the delegation, including the +/// issuer, subject, audience, the delegated ability, time bounds, and so on. +#[derive(Debug, Clone, PartialEq, Builder)] // FIXME Serialize, Deserialize, Builder)] +pub struct Payload { + /// The subject of the [`Delegation`]. + /// + /// This role *must* have issued the earlier (root) + /// delegation in the chain. This makes the chains + /// self-certifying. + /// + /// The semantics of the delegation are established + /// by the subject. + /// + /// [`Delegation`]: super::Delegation + pub subject: Option, + + /// The issuer of the [`Delegation`]. + /// + /// This [`Did`] *must* match the signature on + /// the outer layer of [`Delegation`]. + /// + /// [`Delegation`]: super::Delegation + pub issuer: DID, + + /// The agent being delegated to. + pub audience: DID, + + /// A [`Did`] that must be in the delegation chain at invocation time. + #[builder(default)] + pub via: Option, + + /// The command being delegated. + pub command: String, + + /// Any [`Predicate`] policies that constrain the `args` on an [`Invocation`][crate::invocation::Invocation]. + #[builder(default)] + pub policy: Vec, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptograpgic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + /// [`Cid`]: libipld_core::cid::Cid ; + #[builder(default = "Nonce::generate_16(&mut vec![])")] + pub nonce: Nonce, + + /// The latest wall-clock time that the UCAN is valid until, + /// given as a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + pub expiration: Timestamp, + + /// An optional earliest wall-clock time that the UCAN is valid from, + /// given as a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + #[builder(default)] + pub not_before: Option, +} + +impl Payload { + pub fn check_time(&self, now: SystemTime) -> Result<(), TimeBoundError> { + let ts_now = &Timestamp::postel(now); + + if &self.expiration < ts_now { + return Err(TimeBoundError::Expired); + } + + if let Some(ref nbf) = self.not_before { + if nbf > ts_now { + return Err(TimeBoundError::NotYetValid); + } + } + + Ok(()) + } +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/d@1.0.0-rc.1"; +} + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +impl TryFrom> for Payload +where + ::Err: Debug, +{ + type Error = ParseError; + + fn try_from(args: Named) -> Result { + let mut subject = None; + let mut issuer = None; + let mut audience = None; + let mut via = None; + let mut command = None; + let mut policy = None; + let mut metadata = None; + let mut nonce = None; + let mut expiration = None; + let mut not_before = None; + + for (k, ipld) in args { + match k.as_str() { + "sub" => { + subject = Some(match ipld { + Ipld::Null => None, + Ipld::String(s) => { + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("sub".to_string(), bad)), + }) + } + "iss" => match ipld { + Ipld::String(s) => { + issuer = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("iss".to_string(), bad)), + }, + "aud" => match ipld { + Ipld::String(s) => { + audience = + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("aud".to_string(), bad)), + }, + "via" => match ipld { + Ipld::String(s) => { + via = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + bad => return Err(ParseError::WrongTypeForField("via".to_string(), bad)), + }, + "cmd" => match ipld { + Ipld::String(s) => command = Some(s), + bad => return Err(ParseError::WrongTypeForField("cmd".to_string(), bad)), + }, + "pol" => match ipld { + Ipld::List(xs) => { + let result: Result, ParseError> = + xs.iter().try_fold(vec![], |mut acc, ipld| { + let pred = Predicate::try_from(ipld.clone())?; + acc.push(pred); + Ok(acc) + }); + + policy = Some(result?); + } + bad => return Err(ParseError::WrongTypeForField("pol".to_string(), bad)), + }, + "meta" => match ipld { + Ipld::Map(m) => metadata = Some(m), + bad => return Err(ParseError::WrongTypeForField("meta".to_string(), bad)), + }, + "nonce" => match ipld { + Ipld::Bytes(b) => nonce = Some(Nonce::from(b).into()), + bad => return Err(ParseError::WrongTypeForField("nonce".to_string(), bad)), + }, + "exp" => match ipld { + Ipld::Integer(i) => { + expiration = Some(Timestamp::try_from(i).map_err(ParseError::BadTimestamp)?) + } + bad => return Err(ParseError::WrongTypeForField("exp".to_string(), bad)), + }, + "nbf" => match ipld { + Ipld::Integer(i) => { + not_before = Some(Timestamp::try_from(i).map_err(ParseError::BadTimestamp)?) + } + bad => return Err(ParseError::WrongTypeForField("nbf".to_string(), bad)), + }, + other => return Err(ParseError::UnknownField(other.to_string())), + } + } + + Ok(Payload { + subject: subject.ok_or(ParseError::MissingSub)?, + issuer: issuer.ok_or(ParseError::MissingIss)?, + audience: audience.ok_or(ParseError::MissingAud)?, + via, + command: command.ok_or(ParseError::MissingCmd)?, + policy: policy.ok_or(ParseError::MissingPol)?, + metadata: metadata.unwrap_or_default(), + nonce: nonce.ok_or(ParseError::MissingNonce)?, + expiration: expiration.ok_or(ParseError::MissingExp)?, + not_before, + }) + } +} + +#[derive(Debug, Error)] +pub enum ParseError +where + ::Err: Debug, +{ + #[error("Unknown field: {0}")] + UnknownField(String), + + #[error("Missing sub field")] + MissingSub, + + #[error("Missing iss field")] + MissingIss, + + #[error("Missing aud field")] + MissingAud, + + #[error("Missing cmd field")] + MissingCmd, + + #[error("Missing pol field")] + MissingPol, + + #[error("Missing nonce field")] + MissingNonce, + + #[error("Missing exp field")] + MissingExp, + + #[error("Wrong type for field {0}: {1:?}")] + WrongTypeForField(String, Ipld), + + #[error("Cannot parse DID")] + DidParseError(::Err), + + #[error("Cannot parse timestamp: {0}")] + BadTimestamp(#[from] time::OutOfRangeError), + + #[error("Cannot parse policy predicate: {0}")] + InvalidPolicy(#[from] predicate::FromIpldError), +} + +impl From> for Ipld { + fn from(payload: Payload) -> Self { + let named: Named = payload.into(); + Ipld::Map(named.0) + } +} + +impl TryFrom for Payload +where + DID: Did + FromStr, + ::Err: Debug, +{ + type Error = TryFromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Map(map) => { + let named = Named::(map); + Payload::try_from(named).map_err(TryFromIpldError::MapParseError) + } + _ => Err(TryFromIpldError::NotAMap), + } + } +} + +#[derive(Debug, Error)] +pub enum TryFromIpldError +where + ::Err: Debug, +{ + NotAMap, + MapParseError(ParseError), +} + +impl From> for Named { + fn from(payload: Payload) -> Self { + let mut args = Named::::from_iter([ + ("iss".to_string(), Ipld::String(payload.issuer.to_string())), + ( + "aud".to_string(), + Ipld::String(payload.audience.to_string()), + ), + ("cmd".to_string(), Ipld::String(payload.command)), + ("pol".to_string(), { + Ipld::List(payload.policy.into_iter().map(|p| p.into()).collect()) + }), + ("nonce".to_string(), payload.nonce.into()), + ("exp".to_string(), payload.expiration.into()), + ]); + + if let Some(subject) = payload.subject { + args.insert("sub".to_string(), Ipld::String(subject.to_string())); + } else { + args.insert("sub".to_string(), Ipld::Null); + } + + if let Some(via) = payload.via { + args.insert("via".to_string(), Ipld::String(via.to_string())); + } + + if let Some(not_before) = payload.not_before { + args.insert("nbf".to_string(), Ipld::from(not_before)); + } + + if !payload.metadata.is_empty() { + args.insert("meta".to_string(), Ipld::Map(payload.metadata)); + } + + args + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + DID::Parameters: Clone, +{ + type Parameters = (DID::Parameters, ::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((did_args, pred_args): Self::Parameters) -> Self::Strategy { + ( + Option::::arbitrary(), + DID::arbitrary_with(did_args.clone()), + DID::arbitrary_with(did_args), + String::arbitrary(), + Nonce::arbitrary(), + Timestamp::arbitrary(), + Option::::arbitrary(), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..5).prop_map(|m| { + m.into_iter() + .map(|(k, v)| (k, v.0)) + .collect::>() + }), + prop::collection::vec(Predicate::arbitrary_with(pred_args), 0..10), + Option::::arbitrary(), + ) + .prop_map( + |( + subject, + issuer, + audience, + command, + nonce, + expiration, + not_before, + metadata, + policy, + via, + )| { + Payload { + issuer, + subject, + audience, + command, + policy, + metadata, + nonce, + expiration, + not_before, + via, + } + }, + ) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test_log::test] + fn test_ipld_round_trip(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + let parsed = Payload::::try_from(observed); + + prop_assert!(parsed.is_ok()); + prop_assert_eq!(parsed.unwrap(), payload); + } + + #[test_log::test] + fn test_ipld_has_correct_fields(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + + if let Ipld::Map(named) = observed { + prop_assert!(named.len() >= 6); + prop_assert!(named.len() <= 10); + + for key in named.keys() { + prop_assert!(matches!(key.as_str(), "sub" | "iss" | "aud" | "via" | "cmd" | "pol" | "meta" | "nonce" | "exp" | "nbf")); + } + } else { + prop_assert!(false, "ipld map"); + } + } + + #[test_log::test] + fn test_ipld_field_types(payload in Payload::::arbitrary()) { + let named: Named = payload.clone().into(); + + let iss = named.get("iss".into()); + let aud = named.get("aud".into()); + let cmd = named.get("cmd".into()); + let pol = named.get("pol".into()); + let nonce = named.get("nonce".into()); + let exp = named.get("exp".into()); + + // Required Fields + prop_assert_eq!(iss.unwrap(), &Ipld::String(payload.issuer.to_string())); + prop_assert_eq!(aud.unwrap(), &Ipld::String(payload.audience.to_string())); + prop_assert_eq!(cmd.unwrap(), &Ipld::String(payload.command.clone())); + prop_assert_eq!(pol.unwrap(), &Ipld::List(payload.policy.clone().into_iter().map(|p| p.into()).collect())); + prop_assert_eq!(nonce.unwrap(), &payload.nonce.into()); + prop_assert_eq!(exp.unwrap(), &payload.expiration.into()); + + // Optional Fields + match (payload.subject, named.get("sub")) { + (Some(sub), Some(Ipld::String(s))) => { + prop_assert_eq!(&sub.to_string(), s); + } + (None, Some(Ipld::Null)) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.via, named.get("via")) { + (Some(via), Some(Ipld::String(s))) => { + prop_assert_eq!(&via.to_string(), s); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.metadata.is_empty(), named.get("meta")) { + (false, Some(Ipld::Map(btree))) => { + prop_assert_eq!(&payload.metadata, btree); + } + (true, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.not_before, named.get("nbf")) { + (Some(nbf), Some(Ipld::Integer(i))) => { + prop_assert_eq!(&i128::from(nbf), i); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + } + + #[test_log::test] + fn test_non_payload(ipld in ipld::Newtype::arbitrary()) { + // Just ensuring that a negative test shows up + let parsed = Payload::::try_from(ipld.0); + prop_assert!(parsed.is_err()) + } + } +} diff --git a/src/delegation/policy.rs b/src/delegation/policy.rs new file mode 100644 index 00000000..2bbc2abe --- /dev/null +++ b/src/delegation/policy.rs @@ -0,0 +1,10 @@ +//! Policy language. +//! +//! The policy language is a simple predicate language extended with [`jq`]-style selectors. +//! +//! [`jq`]: https://stedolan.github.io/jq/ + +pub mod selector; + +pub mod predicate; +pub use predicate::*; diff --git a/src/delegation/policy/predicate.rs b/src/delegation/policy/predicate.rs new file mode 100644 index 00000000..30c9c0dd --- /dev/null +++ b/src/delegation/policy/predicate.rs @@ -0,0 +1,1758 @@ +use super::selector::filter::Filter; +use super::selector::{Select, SelectorError}; +use crate::ipld; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use std::{fmt, str::FromStr}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq)] +pub enum Predicate { + // Comparison + Equal(Select, ipld::Newtype), + + GreaterThan(Select, ipld::Number), + GreaterThanOrEqual(Select, ipld::Number), + + LessThan(Select, ipld::Number), + LessThanOrEqual(Select, ipld::Number), + + Like(Select, String), + + // Connectives + Not(Box), + And(Box, Box), + Or(Box, Box), + + // Collection iteration + Every(Select, Box), // ∀x ∈ xs + Some(Select, Box), // ∃x ∈ xs +} + +#[derive(Debug, Clone, PartialEq, EnumAsInner)] +pub enum Harmonization { + Equal, // e.g. x > 10 vs x > 10 + Conflict, // e.g. x == 1 vs x == 2 + LhsWeaker, // e.g. x > 10 vs x > 100 (AKA compatible but rhs narrower than lhs) + LhsStronger, // e.g. x > 10 vs x > 1 (AKA compatible lhs narrower than rhs) + StrongerTogether, // e.g. x > 10 vs x < 100 (AKA both narrow each other) + IncomparablePath, // e.g. .foo and .bar +} + +impl Harmonization { + pub fn complement(self) -> Self { + match self { + Harmonization::Equal => Harmonization::Conflict, + Harmonization::Conflict => Harmonization::Equal, // FIXME Correct? + Harmonization::LhsWeaker => Harmonization::LhsStronger, + Harmonization::LhsStronger => Harmonization::LhsWeaker, + Harmonization::StrongerTogether => Harmonization::StrongerTogether, + Harmonization::IncomparablePath => Harmonization::IncomparablePath, + } + } + + pub fn flip(self) -> Self { + match self { + Harmonization::Equal => Harmonization::Equal, + Harmonization::Conflict => Harmonization::Conflict, + Harmonization::LhsWeaker => Harmonization::LhsStronger, + Harmonization::LhsStronger => Harmonization::LhsWeaker, + Harmonization::StrongerTogether => Harmonization::StrongerTogether, + Harmonization::IncomparablePath => Harmonization::IncomparablePath, + } + } +} + +impl Predicate { + // FIXME make &self? + pub fn run(self, data: &Ipld) -> Result { + Ok(match self { + Predicate::Equal(lhs, rhs_data) => lhs.get(data)? == rhs_data, + Predicate::GreaterThan(lhs, rhs_data) => lhs.get(data)? > rhs_data, + Predicate::GreaterThanOrEqual(lhs, rhs_data) => lhs.get(data)? >= rhs_data, + Predicate::LessThan(lhs, rhs_data) => lhs.get(data)? < rhs_data, + Predicate::LessThanOrEqual(lhs, rhs_data) => lhs.get(data)? <= rhs_data, + Predicate::Like(lhs, rhs_data) => glob(&lhs.get(data)?, &rhs_data), + Predicate::Not(inner) => !inner.run(data)?, + Predicate::And(lhs, rhs) => lhs.run(data)? && rhs.run(data)?, + Predicate::Or(lhs, rhs) => lhs.run(data)? || rhs.run(data)?, + Predicate::Every(xs, p) => xs + .get(data)? + .to_vec() + .iter() + .try_fold(true, |acc, each_datum| { + Ok(acc && p.clone().run(&each_datum.0)?) + })?, + Predicate::Some(xs, p) => xs + .get(data)? + .to_vec() + .iter() + .try_fold(false, |acc, each_datum| { + Ok(acc || p.clone().run(&each_datum.0)?) + })?, + }) + } + + // FIXME check paths are subsets, becase that changes some of these + pub fn harmonize( + &self, + other: &Self, + lhs_ctx: Vec, + rhs_ctx: Vec, + ) -> Harmonization { + match (self, other) { + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::Equal(rhs_selector, rhs_ipld), + ) => { + // FIXME include ctx in path? + if lhs_selector.is_related(rhs_selector) { + if lhs_ipld == rhs_ipld { + Harmonization::Equal + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + // FIXME lhs + rhs selector must be exact + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num > *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num >= *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num < *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::Equal(lhs_selector, lhs_ipld), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if let Ok(lhs_num) = ipld::Number::try_from(lhs_ipld.0.clone()) { + if lhs_num <= *rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + /*********** + * Strings * + ***********/ + (Predicate::Like(lhs_selector, lhs_str), Predicate::Like(rhs_selector, rhs_str)) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_str == rhs_str { + Harmonization::Equal + } else { + // FIXME actually not accurate; need to walk both in case of inner patterns + match (glob(lhs_str, rhs_str), glob(rhs_str, lhs_str)) { + (true, true) => Harmonization::StrongerTogether, + _ => Harmonization::Conflict, + } + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + (Predicate::Equal(lhs_selector, lhs_ipld), Predicate::Like(rhs_selector, rhs_str)) => { + if lhs_selector.is_related(rhs_selector) { + if let Ipld::String(lhs_str) = &lhs_ipld.0 { + if glob(&lhs_str, rhs_str) { + // FIXME? + Harmonization::LhsStronger + } else { + Harmonization::Conflict + } + } else { + // NOTE Predicate::Like forces this to unify as a string, so anything else fails + // ...so this is not *not* a type checker + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + (lhs @ Predicate::Like(_, _), rhs @ Predicate::Equal(_, _)) => { + rhs.harmonize(lhs, rhs_ctx, lhs_ctx).complement() + } + + /**************** + * Greater Than * + ***************/ + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num > rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::LhsWeaker + } else { + Harmonization::LhsStronger + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThan(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /************************* + * Greater Than Or Equal * + *************************/ + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num > rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num <= rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::GreaterThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num < rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /********************** + * Less Than Or Equal * + **********************/ + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::LhsWeaker + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThanOrEqual(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num >= rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /************* + * Less Than * + *************/ + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::LessThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::Equal + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::LessThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num == rhs_num { + Harmonization::LhsStronger + } else if lhs_num < rhs_num { + Harmonization::LhsStronger + } else { + Harmonization::LhsWeaker + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::GreaterThan(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + ( + Predicate::LessThan(lhs_selector, lhs_num), + Predicate::GreaterThanOrEqual(rhs_selector, rhs_num), + ) => { + if lhs_selector.is_related(rhs_selector) { + if lhs_selector == rhs_selector { + if lhs_num > rhs_num { + Harmonization::StrongerTogether + } else { + Harmonization::Conflict + } + } else { + Harmonization::Conflict + } + } else { + Harmonization::IncomparablePath + } + } + + /*************** + * Connectives * + ***************/ + (_self, Predicate::Not(rhs_inner)) => { + self.harmonize(rhs_inner, lhs_ctx, rhs_ctx).complement() + } + (Predicate::Not(lhs_inner), rhs) => { + lhs_inner.harmonize(rhs, lhs_ctx, rhs_ctx).complement() + } + (_self, Predicate::And(and_left, and_right)) => { + let rhs_raw_pred1: Predicate = *and_left.clone(); + let rhs_raw_pred2: Predicate = *and_right.clone(); + + match ( + self.harmonize(&rhs_raw_pred1, lhs_ctx.clone(), rhs_ctx.clone()), + self.harmonize(&rhs_raw_pred2, lhs_ctx, rhs_ctx), + ) { + (Harmonization::Conflict, _) => Harmonization::Conflict, + (_, Harmonization::Conflict) => Harmonization::Conflict, + (Harmonization::IncomparablePath, right) => right, + (left, Harmonization::IncomparablePath) => left, + (Harmonization::Equal, rhs) => rhs, + (lhs, Harmonization::Equal) => lhs, + (Harmonization::LhsWeaker, Harmonization::LhsWeaker) => { + Harmonization::LhsWeaker + } + (Harmonization::LhsStronger, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (Harmonization::LhsStronger, Harmonization::LhsWeaker) => { + Harmonization::StrongerTogether + } + (Harmonization::LhsWeaker, Harmonization::LhsStronger) => { + Harmonization::StrongerTogether + } + (Harmonization::StrongerTogether, _) => Harmonization::StrongerTogether, + (_, Harmonization::StrongerTogether) => Harmonization::StrongerTogether, + } + } + (lhs @ Predicate::And(_, _), rhs) => lhs.harmonize(rhs, lhs_ctx, rhs_ctx).flip(), + (_self, Predicate::Or(or_left, or_right)) => { + let rhs_raw_pred1: Predicate = *or_left.clone(); + let rhs_raw_pred2: Predicate = *or_right.clone(); + + match ( + self.harmonize(&rhs_raw_pred1, lhs_ctx.clone(), rhs_ctx.clone()), + self.harmonize(&rhs_raw_pred2, lhs_ctx, rhs_ctx), + ) { + (Harmonization::Conflict, Harmonization::Conflict) => Harmonization::Conflict, + (lhs, Harmonization::Conflict) => lhs, + (Harmonization::Conflict, rhs) => rhs, + (Harmonization::IncomparablePath, right) => right, + (left, Harmonization::IncomparablePath) => left, + (Harmonization::Equal, rhs) => rhs, + (lhs, Harmonization::Equal) => lhs, + (Harmonization::LhsWeaker, Harmonization::LhsWeaker) => { + Harmonization::LhsWeaker + } + (Harmonization::LhsStronger, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (_, Harmonization::LhsWeaker) => Harmonization::LhsWeaker, + (Harmonization::LhsWeaker, _) => Harmonization::LhsWeaker, + (Harmonization::LhsStronger, Harmonization::StrongerTogether) => { + Harmonization::LhsStronger + } + (Harmonization::StrongerTogether, Harmonization::LhsStronger) => { + Harmonization::LhsStronger + } + (Harmonization::StrongerTogether, Harmonization::StrongerTogether) => { + Harmonization::StrongerTogether + } + } + } + (lhs @ Predicate::Or(_, _), rhs) => lhs.harmonize(rhs, lhs_ctx, rhs_ctx).flip(), + // /****************** + // * Quantification * + // ******************/ + // Predicate::Every(rhs_selector, rhs_inner) => { + // let rhs_raw_pred: Predicate = *rhs_inner.clone(); + // // TODO FIXME exact path + // todo!() + // // match self.harmonize(&rhs_raw_pred, lhs_ctx, rhs_ctx) { + // // Harmonization::LhsPassed => Harmonization::LhsPassed, + // // Harmonization::LhsWeaker => Harmonization::LhsWeaker, + // // Harmonization::IncomparablePath => Harmonization::IncomparablePath, + // // Harmonization::Conflict => { + // // Harmonization::Conflict + // // } + // // } + // } + // Predicate::Some(rhs_selector, rhs_inner) => { + // let rhs_raw_pred: Predicate = *rhs_inner.clone(); + // // TODO FIXME As long as the lhs path doens't terminate earlier, then pass + // todo!() + // // match self.harmonize(&rhs_raw_pred, lhs_ctx, rhs_ctx) { + // // Harmonization::LhsPassed => Harmonization::LhsPassed, + // // Harmonization::LhsWeaker => Harmonization::LhsWeaker, + // // Harmonization::IncomparablePath => Harmonization::IncomparablePath, + // // Harmonization::Conflict => { + // // Harmonization::Conflict + // // } + // // } + // } + // }, + _ => todo!(), + } + } +} + +pub fn glob(input: &str, pattern: &str) -> bool { + if pattern.is_empty() { + return input == ""; + } + + // Parsing pattern + let (saw_escape, mut patterns, mut working) = pattern.chars().fold( + (false, vec![], "".to_string()), + |(saw_escape, mut acc, mut working), c| { + match c { + '*' => { + if saw_escape { + working.push('*'); + (false, acc, working) + } else { + acc.push(working); + working = "".to_string(); + (false, acc, working) + } + } + '\\' => { + if saw_escape { + // Push prev escape + working.push('\\'); + } + (true, acc, working) + } + _ => { + if saw_escape { + working.push('\\'); + } + + working.push(c); + (false, acc, working) + } + } + }, + ); + + if saw_escape { + working.push('\\'); + } + + patterns.push(working); + + // Test input against the pattern + patterns + .iter() + .enumerate() + .try_fold(input, |acc, (idx, pattern_frag)| { + if let Some((pre, post)) = acc.split_once(pattern_frag) { + if idx == 0 && !pattern.starts_with("*") && !pre.is_empty() { + Err(()) + } else if idx == patterns.len() - 1 && !pattern.ends_with("*") && !post.is_empty() { + Err(()) + } else { + Ok(post) + } + } else { + Err(()) + } + }) + .is_ok() +} + +impl TryFrom for Predicate { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::List(v) => match v.as_slice() { + [Ipld::String(s), inner] if s == "not" => { + let inner = Box::new(Predicate::try_from(inner.clone())?); + Ok(Predicate::Not(inner)) + } + [Ipld::String(op_str), Ipld::String(sel_str), val] => match op_str.as_str() { + "==" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidIpldSelector)?; + + Ok(Predicate::Equal(sel, ipld::Newtype(val.clone()))) + } + ">" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::GreaterThan(sel, num)) + } + ">=" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + Ok(Predicate::GreaterThanOrEqual(sel, num)) + } + "<" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::LessThan(sel, num)) + } + "<=" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidNumberSelector)?; + + let num = ipld::Number::try_from(val.clone()) + .map_err(FromIpldError::CannotParseIpldNumber)?; + + Ok(Predicate::LessThanOrEqual(sel, num)) + } + "like" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidStringSelector)?; + + if let Ipld::String(s) = val { + Ok(Predicate::Like(sel, s.to_string())) + } else { + Err(FromIpldError::NotAString(val.clone())) + } + } + "every" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidCollectionSelector)?; + + let p = Box::new(Predicate::try_from(val.clone())?); + Ok(Predicate::Every(sel, p)) + } + "some" => { + let sel = Select::::from_str(sel_str.as_str()) + .map_err(FromIpldError::InvalidCollectionSelector)?; + + let p = Box::new(Predicate::try_from(val.clone())?); + Ok(Predicate::Some(sel, p)) + } + _ => Err(FromIpldError::UnrecognizedTripleTag(op_str.to_string())), + }, + [Ipld::String(op_str), lhs, rhs] => match op_str.as_str() { + "and" => { + let lhs = Box::new(Predicate::try_from(lhs.clone())?); + let rhs = Box::new(Predicate::try_from(rhs.clone())?); + Ok(Predicate::And(lhs, rhs)) + } + "or" => { + let lhs = Box::new(Predicate::try_from(lhs.clone())?); + let rhs = Box::new(Predicate::try_from(rhs.clone())?); + Ok(Predicate::Or(lhs, rhs)) + } + _ => Err(FromIpldError::UnrecognizedTripleTag(op_str.to_string())), + }, + _ => Err(FromIpldError::UnrecognizedShape), + }, + _ => Err(FromIpldError::NotATuple(ipld)), + } + } +} + +#[derive(Debug, PartialEq, Error)] +pub enum FromIpldError { + #[error("Invalid Ipld selector {0:?}")] + InvalidIpldSelector( as FromStr>::Err), + + #[error("Invalid ipld::Number selector {0:?}")] + InvalidNumberSelector( as FromStr>::Err), + + #[error("Invalid ipld::Collection selector {0:?}")] + InvalidCollectionSelector( as FromStr>::Err), + + #[error("Invalid String selector {0:?}")] + InvalidStringSelector( as FromStr>::Err), + + #[error("Cannot parse ipld::Number {0:?}")] + CannotParseIpldNumber(>::Error), + + #[error("Not a string: {0:?}")] + NotAString(Ipld), + + #[error("Unrecognized triple tag {0}")] + UnrecognizedTripleTag(String), + + #[error("Unrecognized shape")] + UnrecognizedShape, + + #[error("Not a predicate tuple {0:?}")] + NotATuple(Ipld), +} + +impl From for Ipld { + fn from(p: Predicate) -> Self { + match p { + Predicate::Equal(lhs, rhs) => { + Ipld::List(vec![Ipld::String("==".to_string()), lhs.into(), rhs.into()]) + } + Predicate::GreaterThan(lhs, rhs) => { + Ipld::List(vec![Ipld::String(">".to_string()), lhs.into(), rhs.into()]) + } + Predicate::GreaterThanOrEqual(lhs, rhs) => { + Ipld::List(vec![Ipld::String(">=".to_string()), lhs.into(), rhs.into()]) + } + Predicate::LessThan(lhs, rhs) => { + Ipld::List(vec![Ipld::String("<".to_string()), lhs.into(), rhs.into()]) + } + Predicate::LessThanOrEqual(lhs, rhs) => { + Ipld::List(vec![Ipld::String("<=".to_string()), lhs.into(), rhs.into()]) + } + Predicate::Like(lhs, rhs) => Ipld::List(vec![ + Ipld::String("like".to_string()), + lhs.into(), + rhs.into(), + ]), + Predicate::Not(inner) => { + let unboxed = *inner; + Ipld::List(vec![Ipld::String("not".to_string()), unboxed.into()]) + } + Predicate::And(lhs, rhs) => Ipld::List(vec![ + Ipld::String("and".to_string()), + (*lhs).into(), + (*rhs).into(), + ]), + Predicate::Or(lhs, rhs) => Ipld::List(vec![ + Ipld::String("or".to_string()), + (*lhs).into(), + (*rhs).into(), + ]), + Predicate::Every(xs, p) => Ipld::List(vec![ + Ipld::String("every".to_string()), + xs.into(), + (*p).into(), + ]), + Predicate::Some(xs, p) => Ipld::List(vec![ + Ipld::String("some".to_string()), + xs.into(), + (*p).into(), + ]), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Predicate { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy { + let leaf = prop_oneof![ + (Select::arbitrary(), ipld::Newtype::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::Equal(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::GreaterThan(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::GreaterThanOrEqual(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::LessThan(lhs, rhs) }), + (Select::arbitrary(), ipld::Number::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::LessThanOrEqual(lhs, rhs) }), + (Select::arbitrary(), String::arbitrary()) + .prop_map(|(lhs, rhs)| { Predicate::Like(lhs, rhs) }) + ]; + + let connective = leaf.clone().prop_recursive(8, 16, 4, |inner| { + prop_oneof![ + (inner.clone(), inner.clone()) + .prop_map(|(lhs, rhs)| { Predicate::And(Box::new(lhs), Box::new(rhs)) }), + (inner.clone(), inner.clone()) + .prop_map(|(lhs, rhs)| { Predicate::Or(Box::new(lhs), Box::new(rhs)) }), + ] + }); + + let quantified = leaf.clone().prop_recursive(8, 16, 4, |inner| { + prop_oneof![ + (Select::arbitrary(), inner.clone()) + .prop_map(|(xs, p)| { Predicate::Every(xs, Box::new(p)) }), + (Select::arbitrary(), inner.clone()) + .prop_map(|(xs, p)| { Predicate::Some(xs, Box::new(p)) }), + ] + }); + + prop_oneof![leaf, connective, quantified].boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod glob { + use super::*; + + #[test_log::test] + fn test_concrete() -> TestResult { + let got = glob(&"hello world", &"hello world"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_concrete_fail() -> TestResult { + let got = glob(&"hello world", &"NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_empty_pattern_fail() -> TestResult { + let got = glob(&"hello world", &""); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_escaped_star() -> TestResult { + let got = glob(&"*", &r#"\*"#); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_inner_escaped_star() -> TestResult { + let got = glob(&"hello, * world*", &r#"hello*\**\*"#); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_empty_string_fail() -> TestResult { + let got = glob(&"", &"NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_left_star() -> TestResult { + let got = glob(&"hello world", &"*world"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_left_star_failure() -> TestResult { + let got = glob(&"hello world", &"*NOPE"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_right_star() -> TestResult { + let got = glob(&"hello world", &"hello*"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_right_star_failure() -> TestResult { + let got = glob(&"hello world", &"NOPE*"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_only_star() -> TestResult { + let got = glob(&"hello world", &"*"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_two_stars() -> TestResult { + let got = glob(&"hello world", &"* *"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_two_stars_fail() -> TestResult { + let got = glob(&"hello world", &"*@*"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_multiple_inner_stars() -> TestResult { + let got = glob(&"hello world", &"h*l*o*w*r*d"); + assert!(got); + Ok(()) + } + + #[test_log::test] + fn test_multiple_inner_stars_fail() -> TestResult { + let got = glob(&"hello world", &"a*b*c*d*e*f"); + assert!(!got); + Ok(()) + } + + #[test_log::test] + fn test_concrete_with_multiple_inner_stars() -> TestResult { + let got = glob(&"hello world", &"hello* *world"); + assert!(got); + Ok(()) + } + } + + mod run { + use super::*; + use libipld::ipld; + + fn simple() -> Ipld { + ipld!({ + "foo": 42, + "bar": "baz".to_string(), + "qux": true + }) + } + + fn email() -> Ipld { + ipld!({ + "from": "alice@example.com", + "to": ["bob@example.com", "fraud@example.com"], + "cc": ["carol@example.com"], + "subject": "Quarterly Reports", + "body": "Here's Q2 the reports ..." + }) + } + + fn wasm() -> Ipld { + ipld!({ + "mod": "data:application/wasm;base64,SOMEBASE64GOESHERE", + "fun": "test", + "input": [0, 1, 2 ,3] + }) + } + + #[test_log::test] + fn test_eq() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".not_from?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_dot_field_ending_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from.not?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_dot_field_inner_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".nope?.not").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_root_try_not_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".?").unwrap(), Ipld::Null.into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_try_not_null() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".from?").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_nested_try_null() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from?.not?").unwrap(), Ipld::Null.into()); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_fail_same_type() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from").unwrap(), "NOPE".into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_eq_bad_selector() -> TestResult { + let p = Predicate::Equal( + Select::from_str(".NOPE").unwrap(), + "alice@example.com".into(), + ); + + assert!(p.run(&email()).is_err()); + Ok(()) + } + + #[test_log::test] + fn test_eq_fail_different_type() -> TestResult { + let p = Predicate::Equal(Select::from_str(".from").unwrap(), 42.into()); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_gt() -> TestResult { + let p = Predicate::GreaterThan(Select::from_str(".foo").unwrap(), (41.9).into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gt_fail() -> TestResult { + let p = Predicate::GreaterThan(Select::from_str(".foo").unwrap(), 42.into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gte() -> TestResult { + let p = Predicate::GreaterThanOrEqual(Select::from_str(".foo").unwrap(), 42.into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_gte_fail() -> TestResult { + let p = Predicate::GreaterThanOrEqual(Select::from_str(".foo").unwrap(), (42.1).into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lt() -> TestResult { + let p = Predicate::LessThan(Select::from_str(".foo").unwrap(), (42.1).into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lt_fail() -> TestResult { + let p = Predicate::LessThan(Select::from_str(".foo").unwrap(), 42.into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lte() -> TestResult { + let p = Predicate::LessThanOrEqual(Select::from_str(".foo").unwrap(), 42.into()); + assert!(p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_lte_fail() -> TestResult { + let p = Predicate::LessThanOrEqual(Select::from_str(".foo").unwrap(), (41.9).into()); + assert!(!p.run(&simple())?); + Ok(()) + } + + #[test_log::test] + fn test_like() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "alice@*".into()); + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_concrete() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "NOPE".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_left_star() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "*NOPE".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_right_star() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "NOPE*".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_like_fail_both_stars() -> TestResult { + let p = Predicate::Like(Select::from_str(".from").unwrap(), "*NOPE*".into()); + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_not() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + ))); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_double_negative() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ))))); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_not_fail() -> TestResult { + let p = Predicate::Not(Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + ))); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_both_succeed() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_left_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_right_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_and_both_fail() -> TestResult { + let p = Predicate::And( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_both_succeed() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_left_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "Quarterly Reports".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_right_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "alice@example.com".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(p.run(&email())?); + Ok(()) + } + + #[test_log::test] + fn test_or_both_fail() -> TestResult { + let p = Predicate::Or( + Box::new(Predicate::Equal( + Select::from_str(".from").unwrap(), + "NOPE".into(), + )), + Box::new(Predicate::Equal( + Select::from_str(".subject").unwrap(), + "NOPE".into(), + )), + ); + + assert!(!p.run(&email())?); + Ok(()) + } + + // FIXME nested, too + #[test_log::test] + fn test_every() -> TestResult { + let p = Predicate::Every( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 100.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_every_failure() -> TestResult { + let p = Predicate::Every( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 1.into(), + )), + ); + + assert!(!p.run(&wasm())?); + Ok(()) + } + + // FIXME nested, too + #[test_log::test] + fn test_some_all_succeed() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 100.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_some_not_all() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 1.into(), + )), + ); + + assert!(p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_some_all_fail() -> TestResult { + let p = Predicate::Some( + Select::from_str(".input[]").unwrap(), + Box::new(Predicate::LessThan( + Select::from_str(".").unwrap(), + 0.into(), + )), + ); + + assert!(!p.run(&wasm())?); + Ok(()) + } + + #[test_log::test] + fn test_alternate_every_and_some() -> TestResult { + // ["every", ".a", ["some", ".b[]", ["==", ".", 0]]] + let p = Predicate::Every( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, but ok because "some" + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 0, 1] + } + ] + } + ); + + assert!(p.run(&nested_data)?); + Ok(()) + } + + #[test_log::test] + fn test_alternate_fail_every_and_some() -> TestResult { + // ["every", ".a", ["some", ".b[]", ["==", ".", 0]]] + let p = Predicate::Every( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, but ok because "some" + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 42, 1] // No 0, so fail "every" + } + ] + } + ); + + assert!(!p.run(&nested_data)?); + Ok(()) + } + + // FIXME + #[test_log::test] + fn test_alternate_some_and_every() -> TestResult { + // ["some", ".a", ["every", ".b[]", ["==", ".", 0]]] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope, so fail this every, but... + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [0, 0, 0] // This every succeeds, so the outer "some" succeeds + } + ] + } + ); + + assert!(p.run(&nested_data)?); + Ok(()) + } + + // FIXME + #[test_log::test] + fn test_alternate_fail_some_and_every() -> TestResult { + // ["some", ".a", ["every", ".b[]", ["==", ".", 0]]] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b[]").unwrap(), + Box::new(Predicate::Equal(Select::from_str(".").unwrap(), 0.into())), + )), + ); + + let nested_data = ipld!( + { + "a": [ + { + "b": { + "c": 0, // Yep + "d": 0, // Yep + "e": 1 // Nope + }, + "not-b": "ignore" + }, + { + "also-not-b": "ignore", + "b": [-1, 42, 1] // Also nope, so fail + } + ] + } + ); + + assert!(!p.run(&nested_data)?); + Ok(()) + } + + #[test_log::test] + fn test_deeply_alternate_some_and_every() -> TestResult { + // ["some", ".a", + // ["every", ".b.c[]", + // ["some", ".d", + // ["every", ".e[]", + // ["==", ".f.g", 0] + // ] + // ] + // ] + // ] + let p = Predicate::Some( + Select::from_str(".a").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".b.c[]").unwrap(), + Box::new(Predicate::Some( + Select::from_str(".d").unwrap(), + Box::new(Predicate::Every( + Select::from_str(".e[]").unwrap(), + Box::new(Predicate::Equal( + Select::from_str(".f.g").unwrap(), + 0.into(), + )), + )), + )), + )), + ); + + let deeply_nested_data = ipld!( + { + // Some + "a": [ + { + "b": { + "c": { + // Every + "c1": { + // Some + "d": [ + { + // Every + "e": { + "e1": { + "f": { + "g": 0 + }, + "nope": -10 + }, + "e2": { + "_": "not selected", + "f": { + "g": 0 + }, + } + } + } + ] + }, + "c2": { + // Some + "*": "avoid", + "d": [ + { + // Every + "e": { + "e1": { + "f": { + "g": 0 + }, + "nope": -10 + }, + "e2": { + "_": "not selected", + "f": { + "g": 0 + }, + } + } + } + ] + } + } + } + } + ], + "z": "doesn't read this" + } + ); + + assert!(p.run(&deeply_nested_data)?); + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector.rs b/src/delegation/policy/selector.rs new file mode 100644 index 00000000..acba9a2c --- /dev/null +++ b/src/delegation/policy/selector.rs @@ -0,0 +1,333 @@ +pub mod filter; + +mod error; +mod select; +mod selectable; + +pub use error::{ParseError, SelectorErrorReason}; +pub use select::Select; +pub use selectable::Selectable; + +use filter::Filter; +use nom::{ + self, + bytes::complete::tag, + character::complete::char, + combinator::map_res, + error::context, + multi::{many0, many1}, + sequence::preceded, + IResult, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::cmp::Ordering; +use std::{fmt, str::FromStr}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Selector(pub Vec); + +impl Selector { + pub fn new() -> Self { + Selector(vec![]) + } + + pub fn is_related(&self, other: &Selector) -> bool { + self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b) + } +} + +impl fmt::Display for Selector { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ops = self.0.iter(); + + if let Some(field) = ops.next() { + if !field.is_dot_field() { + write!(f, ".")?; + } + + write!(f, "{}", field)?; + } else { + write!(f, ".")?; + } + + for op in ops { + op.fmt(f)?; + } + + Ok(()) + } +} + +impl FromStr for Selector { + type Err = nom::Err; + + fn from_str(s: &str) -> Result { + if !s.starts_with(".") { + return Err(nom::Err::Error(ParseError::MissingStartingDot( + s.to_string(), + ))); + } + + if s.starts_with("..") { + return Err(nom::Err::Error(ParseError::StartsWithDoubleDot( + s.to_string(), + ))); + } + + let working; + let mut acc = vec![]; + + if let Ok((more, found)) = + nom::branch::alt((filter::parse_try_dot_field, filter::parse_dot_field))(s) + { + working = more; + acc.push(found); + } else { + working = &s[1..]; + } + + match preceded(many0(char('?')), many0(filter::parse))(working) { + Ok(("", ops)) => { + let mut mut_ops = ops.clone(); + acc.append(&mut mut_ops); + Ok(Selector(acc)) + } + Ok((more, _ops)) => Err(nom::Err::Error(ParseError::TrailingInput(more.to_string()))), + Err(err) => Err(err.map(|input| ParseError::UnknownPattern(input.to_string()))), + } + } +} +impl Serialize for Selector { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Selector { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Selector::from_str(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +#[error("Selector {selector} encountered runtime error: {reason}")] +pub struct SelectorError { + pub selector: Selector, + pub reason: SelectorErrorReason, +} + +impl SelectorError { + pub fn from_refs(path_refs: &Vec<&Filter>, reason: SelectorErrorReason) -> SelectorError { + SelectorError { + selector: Selector(path_refs.iter().map(|op| (*op).clone()).collect()), + reason, + } + } +} + +impl PartialOrd for Selector { + fn partial_cmp(&self, other: &Self) -> Option { + if self == other { + return Some(Ordering::Equal); + } + + if self.0.starts_with(&other.0) { + return Some(Ordering::Greater); + } + + if other.0.starts_with(&self.0) { + return Some(Ordering::Less); + } + + None + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Selector { + type Parameters = ::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(Filter::arbitrary_with(args), 0..12) + .prop_map(|ops| Selector(ops)) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod serialization { + use super::*; + + proptest! { + #[test] + fn test_selector_round_trip(sel: Selector) { + let serialized = sel.to_string(); + let deserialized = serialized.parse(); + prop_assert_eq!(Ok(sel), deserialized); + } + } + + #[test_log::test] + fn test_bare_dot() -> TestResult { + pretty::assert_eq!(Selector::from_str("."), Ok(Selector(vec![]))); + Ok(()) + } + + #[test_log::test] + fn test_dot_try() -> TestResult { + pretty::assert_eq!(Selector::from_str(".?"), Ok(Selector(vec![]))); + Ok(()) + } + + #[test_log::test] + fn test_dot_many_tries() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".?????????????????????"), + Ok(Selector(vec![])) + ); + Ok(()) + } + + #[test_log::test] + fn test_inner_try_is_null() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".nope?.not"), + Ok(Selector(vec![ + Filter::Try(Box::new(Filter::Field("nope".into()))), + Filter::Field("not".into()) + ])) + ); + Ok(()) + } + + #[test_log::test] + fn test_dot_many_tries_and_dot_field() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".?????????????????????.foo"), + Ok(Selector(vec![Filter::Field("foo".to_string())])) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_question_marks() -> TestResult { + pretty::assert_eq!( + Selector::from_str(".foo??????????????"), + Ok(Selector(vec![Filter::Try(Box::new(Filter::Field( + "foo".to_string() + )))])) + ); + Ok(()) + } + + #[test_log::test] + fn test_fails_trailing_dot() -> TestResult { + let got = Selector::from_str(".foo."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_leading_double_dot() -> TestResult { + let got = Selector::from_str("..foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_inner_double_dot() -> TestResult { + let got = Selector::from_str(".foo..bar"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_multiple_leading_dots() -> TestResult { + let got = Selector::from_str(".."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fail_missing_leading_dot() -> TestResult { + let got = Selector::from_str("[22]"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_dot_field() -> TestResult { + let got = Selector::from_str(".foo"); + pretty::assert_eq!(got, Ok(Selector(vec![Filter::Field("foo".to_string())]))); + Ok(()) + } + + #[test_log::test] + fn test_multiple_dot_fields() -> TestResult { + let got = Selector::from_str(".foo.bar.baz"); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Field("baz".to_string()) + ])) + ); + Ok(()) + } + + #[test_log::test] + fn test_fairly_complex() -> TestResult { + let got = Selector::from_str(r#".foo.bar[].baz[0][]["42"]._quux?[8]"#); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Values, + Filter::Field("baz".to_string()), + Filter::ArrayIndex(0), + Filter::Values, + Filter::Field("42".to_string()), + Filter::Try(Box::new(Filter::Field("_quux".to_string()))), + Filter::ArrayIndex(8) + ])) + ); + + Ok(()) + } + + #[test_log::test] + fn test_very_complex() -> TestResult { + let got = Selector::from_str(r#".???.foo.bar[].baz[0][]["42"]._quux??[8]"#); + pretty::assert_eq!( + got, + Ok(Selector(vec![ + Filter::Field("foo".to_string()), + Filter::Field("bar".to_string()), + Filter::Values, + Filter::Field("baz".to_string()), + Filter::ArrayIndex(0), + Filter::Values, + Filter::Field("42".to_string()), + Filter::Try(Box::new(Filter::Field("_quux".to_string()))), + Filter::ArrayIndex(8) + ])) + ); + + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector/error.rs b/src/delegation/policy/selector/error.rs new file mode 100644 index 00000000..37f663d6 --- /dev/null +++ b/src/delegation/policy/selector/error.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq, Serialize, Deserialize)] +pub enum ParseError { + #[error("unmatched trailing input")] + TrailingInput(String), + + #[error("unknown pattern: {0}")] + UnknownPattern(String), + + #[error("missing starting dot: {0}")] + MissingStartingDot(String), + + #[error("starts with double dot: {0}")] + StartsWithDoubleDot(String), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Error)] +pub enum SelectorErrorReason { + #[error("Index out of bounds")] + IndexOutOfBounds, + + #[error("Key not found")] + KeyNotFound, + + #[error("Not a list")] + NotAList, + + #[error("Not a map")] + NotAMap, + + #[error("Not a collection")] + NotACollection, + + #[error("Not a number")] + NotANumber, + + #[error("Not a string")] + NotAString, +} diff --git a/src/delegation/policy/selector/filter.rs b/src/delegation/policy/selector/filter.rs new file mode 100644 index 00000000..96343a5a --- /dev/null +++ b/src/delegation/policy/selector/filter.rs @@ -0,0 +1,600 @@ +use super::error::ParseError; +use enum_as_inner::EnumAsInner; +use nom::{ + self, + branch::alt, + bytes::complete::tag, + character::complete::{alphanumeric1, anychar, char, digit1}, + combinator::{map_opt, map_res}, + error::context, + multi::many1, + sequence::{delimited, preceded, terminated}, + IResult, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::{fmt, str::FromStr}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, EnumAsInner)] +pub enum Filter { + ArrayIndex(i32), // [2] + Field(String), // ["key"] (or .key) + Values, // .[] + Try(Box), // ? +} + +impl Filter { + pub fn is_in(&self, other: &Self) -> bool { + match (self, other) { + (Filter::ArrayIndex(a), Filter::ArrayIndex(b)) => a == b, + (Filter::Field(a), Filter::Field(b)) => a == b, + (Filter::Values, Filter::Values) => true, + (Filter::ArrayIndex(_a), Filter::Values) => true, + (Filter::Field(_k), Filter::Values) => true, + (Filter::Try(a), Filter::Try(b)) => a.is_in(b), // FIXME Try is basically == null? + _ => false, + } + } + + pub fn is_dot_field(&self) -> bool { + match self { + Filter::Field(k) => { + if let Some(first) = k.chars().next() { + (first.is_alphabetic() || first == '_') + && k.chars().all(|c| char::is_alphanumeric(c) || c == '_') + } else { + false + } + } + _ => false, + } + } +} + +impl fmt::Display for Filter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Filter::ArrayIndex(i) => write!(f, "[{}]", i), + Filter::Field(k) => { + if self.is_dot_field() { + write!(f, ".{}", k) + } else { + write!(f, "[\"{}\"]", k) + } + } + Filter::Values => write!(f, "[]"), + Filter::Try(inner) => write!(f, "{}?", inner), + } + } +} + +pub fn parse(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_try, parse_non_try)); + context("selector_op", p)(input) +} + +pub fn parse_try(input: &str) -> IResult<&str, Filter> { + let p = map_res( + terminated(parse_non_try, many1(tag("?"))), + |found: Filter| Ok::(Filter::Try(Box::new(found))), + ); + + context("try", p)(input) +} + +pub fn parse_try_dot_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + terminated(parse_dot_field, many1(tag("?"))), + |found: Filter| Ok::(Filter::Try(Box::new(found))), + ); + + context("try", p)(input) +} + +pub fn parse_non_try(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_values, parse_field, parse_array_index)); + context("non_try", p)(input) +} + +pub fn parse_array_index(input: &str) -> IResult<&str, Filter> { + let num = nom::combinator::recognize(preceded(nom::combinator::opt(tag("-")), digit1)); + + let array_index = map_res(delimited(char('['), num, char(']')), |found| { + let idx = i32::from_str(found).map_err(|_| ())?; + Ok::(Filter::ArrayIndex(idx)) + }); + + context("array_index", array_index)(input) +} + +pub fn parse_values(input: &str) -> IResult<&str, Filter> { + context("values", tag("[]"))(input).map(|(rest, _)| (rest, Filter::Values)) +} + +pub fn parse_field(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_delim_field, parse_dot_field)); + + context("map_field", p)(input) +} + +pub fn parse_dot_field(input: &str) -> IResult<&str, Filter> { + let p = alt((parse_dot_alpha_field, parse_dot_underscore_field)); + context("dot_field", p)(input) +} + +fn dot_starter(input: &str) -> IResult<&str, &str> { + if input.len() < 2 { + return Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))); + } + + let bytes = input.as_bytes(); + + if bytes[0] == b'.' { + if char::from(bytes[1]).is_alphabetic() || bytes[1] == b'_' { + return Ok((&input[2..], &input[..2])); + } + } + + Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::Tag, + ))) +} + +fn is_allowed_in_dot_field(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +pub fn parse_dot_alpha_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + preceded( + dot_starter, + nom::multi::many0(nom::character::complete::satisfy(is_allowed_in_dot_field)), + ), + |found: Vec| { + let inner = [input.as_bytes()[1] as char] + .iter() + .chain(found.iter()) + .collect::(); + Ok::(Filter::Field(inner)) + }, + ); + + context("dot_field", p)(input) +} + +pub fn parse_dot_underscore_field(input: &str) -> IResult<&str, Filter> { + let p = map_res(preceded(tag("._"), alphanumeric1), |found: &str| { + let key = format!("{}{}", '_', found); + Ok::(Filter::Field(key)) + }); + + context("dot_field", p)(input) +} + +pub fn parse_empty_quotes_field(input: &str) -> IResult<&str, Filter> { + let p = map_res(tag("[\"\"]"), |_: &str| { + Ok::(Filter::Field("".to_string())) + }); + + context("empty_quotes_field", p)(input) +} + +pub fn unicode_or_space(input: &str) -> IResult<&str, &str> { + #[derive(Copy, Clone, PartialEq, Debug)] + enum Status { + Looking, + FoundQuote, + Done, + Failed, + } + + let (status, len) = + input + .as_bytes() + .iter() + .fold((Status::Looking, 0), |(status, len), byte| { + if status == Status::Failed { + return (status, len); + } + + if status == Status::Done { + return (status, len); + } + + let c = char::from(*byte); + + if status == Status::FoundQuote { + if c == ']' { + return (Status::Done, len + 1); + } else { + return (Status::Looking, len + 1); + } + } + + if c == '"' { + return (Status::FoundQuote, len + 1); + } + + if c == ' ' || (!nom_unicode::is_whitespace(c) && !nom_unicode::is_control(c)) { + return (Status::Looking, len + 1); + } + + (Status::Failed, 0) + }); + + match (status, len) { + (Status::Done, len) => Ok((&input[len - 2..], &input[..len - 2])), + _ => Err(nom::Err::Error(nom::error::Error::new( + input, + nom::error::ErrorKind::TakeWhile1, + ))), + } +} + +pub fn parse_delim_field(input: &str) -> IResult<&str, Filter> { + let p = map_res( + delimited(tag(r#"[""#), unicode_or_space, tag(r#""]"#)), + |found: &str| Ok::(Filter::Field(found.to_string())), + ); + + context("delimited_field", alt((p, parse_empty_quotes_field)))(input) +} + +impl FromStr for Filter { + type Err = nom::Err; + + fn from_str(s: &str) -> Result { + match parse(s).map_err(|e| nom::Err::Failure(ParseError::UnknownPattern(e.to_string())))? { + ("", found) => Ok(found), + (rest, _) => Err(nom::Err::Failure(ParseError::TrailingInput(rest.into()))), + } + } +} +impl Serialize for Filter { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Filter { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Filter::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string())) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Filter { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_params: Self::Parameters) -> Self::Strategy { + prop_oneof![ + i32::arbitrary().prop_map(|i| Filter::ArrayIndex(i)), + "[a-zA-Z_ ]*".prop_map(Filter::Field), + "[a-zA-Z_][a-zA-Z0-9_]*".prop_map(Filter::Field), + Just(Filter::Values), + // FIXME prop_recursive::lazy(|_| { Filter::arbitrary_with(()).prop_map(Filter::Try) }), + ] + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod serialization { + use super::*; + + proptest! { + #[test] + fn test_filter_round_trip(filter: Filter) { + let serialized = filter.to_string(); + let deserialized = serialized.parse(); + prop_assert_eq!(Ok(filter), deserialized); + } + } + + #[test_log::test] + fn test_fails_on_empty() -> TestResult { + let got = Filter::from_str(""); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_bare_dot() -> TestResult { + // NOTE this passes as a Selector, but not a Filter + let got = Filter::from_str("."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_multiple_bare_dots() -> TestResult { + let got = Filter::from_str(".."); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_leading_dots() -> TestResult { + let got = Filter::from_str("..foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_on_empty_whitespace() -> TestResult { + let got = Filter::from_str(" "); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_leading_whitespace() -> TestResult { + let got = Filter::from_str(" .foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_trailing_whitespace() -> TestResult { + let got = Filter::from_str(".foo "); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_values() -> TestResult { + let got = Filter::from_str("[]"); + pretty::assert_eq!(got, Ok(Filter::Values)); + Ok(()) + } + + #[test_log::test] + fn test_values_fails_inner_whitespace() -> TestResult { + let got = Filter::from_str("[ ]"); + pretty::assert_eq!(got.is_err(), true); + Ok(()) + } + + #[test_log::test] + fn test_array_index_zero() -> TestResult { + let got = Filter::from_str("[0]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(0))); + Ok(()) + } + + #[test_log::test] + fn test_array_index_small() -> TestResult { + let got = Filter::from_str("[2]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(2))); + Ok(()) + } + + #[test_log::test] + fn test_array_index_large() -> TestResult { + let got = Filter::from_str("[1234567890]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(1234567890))); + Ok(()) + } + + #[test_log::test] + fn test_array_from_end() -> TestResult { + let got = Filter::from_str("[-42]"); + pretty::assert_eq!(got, Ok(Filter::ArrayIndex(-42))); + Ok(()) + } + + #[test_log::test] + fn test_array_fails_spaces() -> TestResult { + let got = Filter::from_str("[ 42]"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_dot_field() -> TestResult { + let got = Filter::from_str(".F0o"); + pretty::assert_eq!(got, Ok(Filter::Field("F0o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_dot_field_starting_underscore() -> TestResult { + let got = Filter::from_str("._foo"); + pretty::assert_eq!(got, Ok(Filter::Field("_foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_dot_field_trailing_underscore() -> TestResult { + let got = Filter::from_str(".fO0_"); + pretty::assert_eq!(got, Ok(Filter::Field("fO0_".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_field_with_leading_number() -> TestResult { + let got = Filter::from_str(".1foo"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_field_with_inner_symbol() -> TestResult { + let got = Filter::from_str(".fo%o"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field() -> TestResult { + let got = Filter::from_str(r#"["F0o"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("F0o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_fails_without_quotes() -> TestResult { + let got = Filter::from_str(r#"[F0o]"#); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_fails_if_missing_right_brace() -> TestResult { + let got = Filter::from_str(r#"["F0o""#); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_starting_underscore() -> TestResult { + let got = Filter::from_str(r#"["_foo"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("_foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_trailing_underscore() -> TestResult { + let got = Filter::from_str(r#"["fO0_"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("fO0_".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_with_leading_number() -> TestResult { + let got = Filter::from_str(r#"["1foo"]"#); + pretty::assert_eq!(got, Ok(Filter::Field("1foo".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_delim_field_with_inner_symbol() -> TestResult { + let got = Filter::from_str(r#"[".fo%o"]"#); + pretty::assert_eq!(got, Ok(Filter::Field(".fo%o".to_string()))); + Ok(()) + } + + #[test_log::test] + fn test_try() -> TestResult { + let got = Filter::from_str(".foo?"); + pretty::assert_eq!( + got, + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_try() -> TestResult { + let got = parse(".foo?"); + pretty::assert_eq!( + got, + Ok(("", Filter::Try(Box::new(Filter::Field("foo".to_string()))))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + Filter::from_str(".foo???????????????????"), + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + parse(".foo???????????????????"), + Ok(("", Filter::Try(Box::new(Filter::Field("foo".to_string()))))) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_multiple_tries_after_dot_field_trailing() -> TestResult { + pretty::assert_eq!( + parse(".foo???????????????????abc"), + Ok(( + "abc", + Filter::Try(Box::new(Filter::Field("foo".to_string()))) + )) + ); + Ok(()) + } + + #[test_log::test] + fn test_parse_many0_multiple_tries_after_dot_field() -> TestResult { + pretty::assert_eq!( + nom::multi::many0(parse)(".foo???????????????????abc"), + Ok(( + "abc", + vec![Filter::Try(Box::new(Filter::Field("foo".to_string())))] + )) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_delim_field() -> TestResult { + pretty::assert_eq!( + Filter::from_str(r#"["foo"]???????"#), + Ok(Filter::Try(Box::new(Filter::Field("foo".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_delim_field_inner_questionmarks() -> TestResult { + let got = Filter::from_str(r#"["f?o"]???????"#); + pretty::assert_eq!( + got, + Ok(Filter::Try(Box::new(Filter::Field("f?o".to_string())))) + ); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_values() -> TestResult { + let got = Filter::from_str("[]???????"); + pretty::assert_eq!(got, Ok(Filter::Try(Box::new(Filter::Values)))); + Ok(()) + } + + #[test_log::test] + fn test_multiple_tries_after_index() -> TestResult { + let got = Filter::from_str("[42]???????"); + pretty::assert_eq!(got, Ok(Filter::Try(Box::new(Filter::ArrayIndex(42))))); + Ok(()) + } + + #[test_log::test] + fn test_fails_bare_try() -> TestResult { + let got = Filter::from_str("?"); + assert!(got.is_err()); + Ok(()) + } + + #[test_log::test] + fn test_fails_dot_try() -> TestResult { + let got = Filter::from_str(".?"); + assert!(got.is_err()); + Ok(()) + } + } +} diff --git a/src/delegation/policy/selector/select.rs b/src/delegation/policy/selector/select.rs new file mode 100644 index 00000000..7506cc63 --- /dev/null +++ b/src/delegation/policy/selector/select.rs @@ -0,0 +1,260 @@ +use super::Selector; // FIXME cycle? +use super::{error::SelectorErrorReason, filter::Filter, Selectable, SelectorError}; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::fmt; +use std::str::FromStr; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Clone)] +pub struct Select { + filters: Vec, + _marker: std::marker::PhantomData, +} + +impl fmt::Debug for Select { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Select({:?})", self.filters) + } +} + +impl PartialEq for Select { + fn eq(&self, other: &Self) -> bool { + Selector(self.filters.clone()) == Selector(other.filters.clone()) + } +} + +impl Select { + pub fn new(filters: Vec) -> Self { + Self { + filters, + _marker: std::marker::PhantomData, + } + } + + pub fn is_related(&self, other: &Select) -> bool + where + Ipld: From + From, + { + Selector(self.filters.clone()).is_related(&Selector(other.filters.clone())) + } +} + +impl Select { + pub fn get(self, ctx: &Ipld) -> Result { + let got = self.filters.iter().try_fold( + (ctx.clone(), vec![], false), + |(ipld, mut seen_ops, is_try), op| { + seen_ops.push(op); + + match op { + Filter::Try(inner) => { + let op: Filter = *inner.clone(); + let ipld: Ipld = + Select::::new(vec![op]).get(ctx).unwrap_or(Ipld::Null); + + Ok((ipld, seen_ops.clone(), true)) + } + Filter::ArrayIndex(i) => { + let result = { + match ipld { + Ipld::List(xs) => { + if i.abs() as usize > xs.len() { + return Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )); + }; + + xs.get((xs.len() as i32 + *i) as usize) + .ok_or(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::IndexOutOfBounds, + ), + )) + .cloned() + } + _ => Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::NotAList, + ), + )), + } + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + Filter::Field(k) => { + let result = match ipld { + Ipld::Map(xs) => xs + .get(k) + .ok_or(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::KeyNotFound, + ), + )) + .cloned(), + _ => Err(( + is_try, + SelectorError::from_refs(&seen_ops, SelectorErrorReason::NotAMap), + )), + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + Filter::Values => { + let result = match ipld { + Ipld::List(xs) => Ok(Ipld::List(xs)), + Ipld::Map(xs) => Ok(Ipld::List(xs.values().cloned().collect())), + _ => Err(( + is_try, + SelectorError::from_refs( + &seen_ops, + SelectorErrorReason::NotACollection, + ), + )), + }; + + Ok((result?, seen_ops.clone(), is_try)) + } + } + }, + ); + + let (ipld, path) = match got { + Ok((ipld, seen_ops, _)) => Ok((ipld, seen_ops)), + Err((is_try, ref e @ SelectorError { ref selector, .. })) => { + if is_try { + Ok((Ipld::Null, selector.0.iter().map(|x| x).collect::>())) + } else { + Err(e.clone()) + } + } + }?; + + T::try_select(ipld).map_err(|e| SelectorError::from_refs(&path, e)) + } +} + +impl From> for Ipld +where + Ipld: From, +{ + fn from(s: Select) -> Self { + Selector(s.filters).to_string().into() + } +} + +impl FromStr for Select { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let selector = Selector::from_str(s).map_err(ParseError)?; + Ok(Select { + filters: selector.0, + _marker: std::marker::PhantomData, + }) + } +} + +#[derive(Debug, PartialEq, Error)] +#[error("Failed to parse selector: {0}")] +pub struct ParseError(#[from] nom::Err); + +impl PartialOrd for Select { + fn partial_cmp(&self, other: &Self) -> Option { + Selector(self.filters.clone()).partial_cmp(&Selector(other.filters.clone())) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Select { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + prop::collection::vec(Filter::arbitrary(), 1..10) + .prop_map(Select::new) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ipld; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + mod get { + use super::*; + + fn nested_data() -> Ipld { + Ipld::Map( + vec![ + ("name".to_string(), Ipld::String("Alice".to_string())), + ("age".to_string(), Ipld::Integer(42)), + ( + "friends".to_string(), + Ipld::List(vec![ + Ipld::String("Bob".to_string()), + Ipld::String("Charlie".to_string()), + ]), + ), + ] + .into_iter() + .collect(), + ) + } + + proptest! { + #[test_log::test] + fn test_identity(data: ipld::Newtype) { + let selector = Select::::from_str(".")?; + prop_assert_eq!(selector.get(&data.0)?, data); + } + + #[test_log::test] + fn test_try_missing_is_null(data: ipld::Newtype) { + let selector = Select::::from_str(".foo?")?; + let cleaned_data = match data.0.clone() { + Ipld::Map(mut m) => { + m.remove("foo").map_or(Ipld::Null, |v| v) + } + ipld => ipld + }; + prop_assert_eq!(selector.get(&cleaned_data)?, Ipld::Null); + } + + #[test_log::test] + fn test_try_missing_plus_trailing_is_null(data: ipld::Newtype, more: Vec) { + let mut filters = vec![Filter::Try(Box::new(Filter::Field("foo".into())))]; + filters.append(&mut more.clone()); + + let selector: Select = Select::new(filters); + + let cleaned_data = match data.0.clone() { + Ipld::Map(mut m) => { + m.remove("foo").map_or(Ipld::Null, |v| v) + } + ipld => ipld + }; + prop_assert_eq!(selector.get(&cleaned_data)?, Ipld::Null); + } + } + } +} diff --git a/src/delegation/policy/selector/selectable.rs b/src/delegation/policy/selector/selectable.rs new file mode 100644 index 00000000..9fa22186 --- /dev/null +++ b/src/delegation/policy/selector/selectable.rs @@ -0,0 +1,62 @@ +use super::error::SelectorErrorReason; +use crate::ipld; +use libipld_core::ipld::Ipld; +use std::collections::BTreeMap; + +pub trait Selectable: Sized { + fn try_select(ipld: Ipld) -> Result; +} + +impl Selectable for Ipld { + fn try_select(ipld: Ipld) -> Result { + Ok(ipld) + } +} + +impl Selectable for ipld::Newtype { + fn try_select(ipld: Ipld) -> Result { + Ok(ipld::Newtype(ipld)) + } +} + +impl Selectable for ipld::Number { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::Integer(i) => Ok(ipld::Number::Integer(i)), + Ipld::Float(f) => Ok(ipld::Number::Float(f)), + _ => Err(SelectorErrorReason::NotANumber), + } + } +} + +impl Selectable for String { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::String(s) => Ok(s), + _ => Err(SelectorErrorReason::NotAString), + } + } +} + +impl Selectable for ipld::Collection { + fn try_select(ipld: Ipld) -> Result { + match ipld { + Ipld::List(xs) => Ok(ipld::Collection::Array(xs.into_iter().try_fold( + vec![], + |mut acc, v| { + acc.push(Selectable::try_select(v)?); + Ok(acc) + }, + )?)), + Ipld::Map(xs) => Ok(ipld::Collection::Map(xs.into_iter().try_fold( + BTreeMap::new(), + |mut map, (k, v)| { + let value = Selectable::try_select(v)?; + map.insert(k, value); + Ok(map) + }, + )?)), + _ => Err(SelectorErrorReason::NotACollection), + } + } +} diff --git a/src/delegation/store.rs b/src/delegation/store.rs new file mode 100644 index 00000000..680d5f99 --- /dev/null +++ b/src/delegation/store.rs @@ -0,0 +1,7 @@ +//! Storage interface for [`Delegation`][super::Delegation]s. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::Store; diff --git a/src/delegation/store/memory.rs b/src/delegation/store/memory.rs new file mode 100644 index 00000000..a0cd4ec1 --- /dev/null +++ b/src/delegation/store/memory.rs @@ -0,0 +1,864 @@ +use super::Store; +use crate::ability::arguments::Named; +use crate::delegation; +use crate::{ + crypto::varsig, + delegation::{policy::Predicate, Delegation}, + did::{self, Did}, +}; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; +use libipld_core::{cid::Cid, codec::Codec}; +use nonempty::NonEmpty; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::Infallible, +}; +use web_time::SystemTime; + +#[cfg_attr(doc, aquamarine::aquamarine)] +/// A simple in-memory store for delegations. +/// +/// The store is laid out as follows: +/// +/// `{Subject => {Audience => {Cid => Delegation}}}` +/// +/// ```mermaid +/// flowchart LR +/// subgraph Subjects +/// direction TB +/// +/// Akiko +/// Boris +/// Carol +/// +/// subgraph aud[Boris's Audiences] +/// direction TB +/// +/// Denzel +/// Erin +/// Frida +/// Georgia +/// Hugo +/// +/// subgraph cid[Frida's CIDs] +/// direction LR +/// +/// CID1 --> Delegation1 +/// CID2 --> Delegation2 +/// CID3 --> Delegation3 +/// end +/// end +/// end +/// +/// Akiko ~~~ Hugo +/// Carol ~~~ Hugo +/// Boris --> Frida --> CID2 +/// +/// Boris -.-> Denzel +/// Boris -.-> Erin +/// Boris -.-> Georgia +/// Boris -.-> Hugo +/// +/// Frida -.-> CID1 +/// Frida -.-> CID3 +/// +/// style Boris stroke:orange; +/// style Frida stroke:orange; +/// style CID2 stroke:orange; +/// style Delegation2 stroke:orange; +/// +/// linkStyle 5 stroke:orange; +/// linkStyle 6 stroke:orange; +/// linkStyle 1 stroke:orange; +/// ``` +#[derive(Debug, Clone)] +pub struct MemoryStore< + DID: did::Did + Ord = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + inner: Arc>>, +} + +#[derive(Debug, Clone, PartialEq)] +struct MemoryStoreInner< + DID: did::Did + Ord = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + ucans: BTreeMap>>, + index: BTreeMap, BTreeMap>>, + revocations: BTreeSet, +} + +impl, C: Codec + TryFrom + Into> + MemoryStore +{ + pub fn new() -> Self { + Self::default() + } + + pub fn len(&self) -> usize { + self.read().ucans.len() + } + + pub fn is_empty(&self) -> bool { + self.read().ucans.is_empty() // FIXME account for revocations? + } + + fn read(&self) -> RwLockReadGuard<'_, MemoryStoreInner> { + match self.inner.read() { + Ok(guard) => guard, + Err(poison) => { + // We ignore lock poisoning for simplicity + poison.into_inner() + } + } + } + + fn write(&self) -> RwLockWriteGuard<'_, MemoryStoreInner> { + match self.inner.write() { + Ok(guard) => guard, + Err(poison) => { + // We ignore lock poisoning for simplicity + poison.into_inner() + } + } + } +} + +impl, C: Codec + TryFrom + Into> Default + for MemoryStore +{ + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl, C: Codec + TryFrom + Into> Default + for MemoryStoreInner +{ + fn default() -> Self { + MemoryStoreInner { + ucans: BTreeMap::new(), + index: BTreeMap::new(), + revocations: BTreeSet::new(), + } + } +} + +// FIXME check that UCAN is valid +impl< + DID: Did + Ord + Clone, + V: varsig::Header + Clone, + Enc: Codec + TryFrom + Into, + > Store for MemoryStore +where + Named: From>, + delegation::Payload: TryFrom>, + Ipld: Encode, +{ + type DelegationStoreError = Infallible; + + fn get( + &self, + cid: &Cid, + ) -> Result>>, Self::DelegationStoreError> { + // cheap Arc clone + Ok(self.read().ucans.get(cid).cloned()) + // FIXME + } + + fn insert_keyed( + &self, + cid: Cid, + delegation: Delegation, + ) -> Result<(), Self::DelegationStoreError> { + let mut write_tx = self.write(); + + write_tx + .index + .entry(delegation.subject().clone()) + .or_default() + .entry(delegation.audience().clone()) + .or_default() + .insert(cid); + + write_tx.ucans.insert(cid.clone(), Arc::new(delegation)); + + Ok(()) + } + + fn revoke(&self, cid: Cid) -> Result<(), Self::DelegationStoreError> { + self.write().revocations.insert(cid); + Ok(()) + } + + // FIXME take a PayloadBuilder + fn get_chain( + &self, + aud: &DID, + subject: &Option, + command: String, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::DelegationStoreError> + { + let blank_set = BTreeSet::new(); + let blank_map = BTreeMap::new(); + let read_tx = self.read(); + + let all_powerlines = read_tx.index.get(&None).unwrap_or(&blank_map); + let all_aud_for_subject = read_tx.index.get(subject).unwrap_or(&blank_map); + let powerline_candidates = all_powerlines.get(aud).unwrap_or(&blank_set); + let sub_candidates = all_aud_for_subject.get(aud).unwrap_or(&blank_set); + + let mut parent_candidate_stack = + vec![sub_candidates.iter().chain(powerline_candidates.iter())]; + let mut hypothesis_chain = vec![]; + + let corrected_target_command = if command.ends_with('/') { + command + } else { + format!("{}/", command) + }; + + 'outer: loop { + if let Some(parent_cid_candidates) = parent_candidate_stack.last_mut() { + if parent_cid_candidates.clone().collect::>().is_empty() { + parent_candidate_stack.pop(); + continue; + } + + 'inner: for cid in parent_cid_candidates { + // CHECKS + if read_tx.revocations.contains(cid) { + continue; + } + + if let Some(delegation) = read_tx.ucans.get(cid) { + if delegation.check_time(now).is_err() { + continue; + } + + // FIXME extract + let corrected_delegation_command = + if delegation.payload.command.ends_with('/') { + delegation.payload.command.clone() + } else { + format!("{}/", delegation.payload.command) + }; + + if !corrected_target_command.starts_with(&corrected_delegation_command) { + continue; + } + + // FIXME + // for target_pred in policy.iter() { + // for delegate_pred in delegation.payload.policy.iter() { + // let comparison = + // target_pred.harmonize(delegate_pred, vec![], vec![]); + + // if comparison.is_conflict() || comparison.is_lhs_weaker() { + // continue 'inner; + // } + // } + // } + + // PASSED CHECKS, so processing + hypothesis_chain.push((cid.clone(), Arc::clone(delegation))); + + let issuer = delegation.issuer().clone(); + + // Hit a root delegation, AKA base case + if &Some(issuer.clone()) == delegation.subject() { + break 'outer; + } + + let new_aud_candidates = + all_aud_for_subject.get(&issuer).unwrap_or(&blank_set); + + if !new_aud_candidates.is_empty() || !all_powerlines.get(&issuer).is_none() + { + parent_candidate_stack.push( + new_aud_candidates.iter().chain( + all_powerlines.get(&issuer).unwrap_or(&blank_set).iter(), + ), + ); + + break 'inner; + } + } + } + } else { + parent_candidate_stack.pop(); + break 'outer; + } + } + + Ok(NonEmpty::from_vec(hypothesis_chain)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::varsig::encoding; + use crate::crypto::varsig::header; + use crate::{crypto::signature::Envelope, delegation::store::Store}; + + use libipld_core::cid::Cid; + use nonempty::nonempty; + use pretty_assertions as pretty; + use rand::thread_rng; + use std::time::SystemTime; + use testresult::TestResult; + + fn gen_did() -> (crate::did::preset::Verifier, crate::did::preset::Signer) { + let sk = ed25519_dalek::SigningKey::generate(&mut thread_rng()); + let verifier = + crate::did::preset::Verifier::Key(crate::did::key::Verifier::EdDsa(sk.verifying_key())); + let signer = crate::did::preset::Signer::Key(crate::did::key::Signer::EdDsa(sk)); + + (verifier, signer) + } + + #[test_log::test] + fn test_get_fail() -> TestResult { + let store = MemoryStore::< + did::preset::Verifier, + varsig::header::Preset, + varsig::encoding::Preset, + >::default(); + store.get(&Cid::default())?; + pretty::assert_eq!(store.get(&Cid::default()), Ok(None)); + Ok(()) + } + + #[test_log::test] + fn test_insert_get_roundtrip() -> TestResult { + let (did, signer) = gen_did(); + + let store = MemoryStore::default(); + let varsig_header = header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }); + + let deleg = Delegation::try_sign( + &signer, + varsig_header, + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(did.clone()) + .audience(did.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg.clone())?; + let retrieved = store.get(&deleg.cid()?)?.ok_or("failed to retrieve")?; + + pretty::assert_eq!(deleg, *retrieved); + + Ok(()) + } + + #[test_log::test] + fn test_insert_is_idempotent() -> TestResult { + let (did, signer) = gen_did(); + + let store = MemoryStore::default(); + let varsig_header = header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }); + + let deleg = Delegation::try_sign( + &signer, + varsig_header, + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(did.clone()) + .audience(did.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + store.insert(deleg.clone())?; + + let retrieved = store.get(&deleg.cid()?)?.ok_or("failed to retrieve")?; + + pretty::assert_eq!(deleg, *retrieved); + pretty::assert_eq!(store.len(), 1); + + Ok(()) + } + + mod get_chain { + use super::*; + + #[test_log::test] + fn test_simple_fail() -> TestResult { + let (server, _server_signer) = gen_did(); + + let store = MemoryStore::< + did::preset::Verifier, + varsig::header::Preset, + varsig::encoding::Preset, + >::default(); + let got = store.get_chain(&server, &None, "/".into(), vec![], SystemTime::now())?; + + pretty::assert_eq!(got, None); + Ok(()) + } + + #[test_log::test] + fn test_with_one() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, _bob_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg.clone())?; + + let got = store.get_chain(&bob, &Some(alice), "/".into(), vec![], SystemTime::now())?; + pretty::assert_eq!(got, Some(nonempty![(deleg.cid()?, Arc::new(deleg))].into())); + Ok(()) + } + + #[test_log::test] + fn test_with_one_with_others_in_store() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let noise = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/example".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(noise.clone())?; + + let deleg = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg.clone())?; + + let more_noise = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(carol.clone()) + .command("/test".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(more_noise.clone())?; + + let got = store.get_chain(&bob, &Some(alice), "/".into(), vec![], SystemTime::now())?; + pretty::assert_eq!(got, Some(nonempty![(deleg.cid()?, Arc::new(deleg))].into())); + Ok(()) + } + + #[test_log::test] + fn test_with_two() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg_1 = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg_1.clone())?; + + let deleg_2 = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg_2.clone())?; + + let got = + store.get_chain(&carol, &Some(alice), "/".into(), vec![], SystemTime::now())?; + + pretty::assert_eq!( + got, + Some( + nonempty![ + (deleg_2.cid()?, Arc::new(deleg_2)), + (deleg_1.cid()?, Arc::new(deleg_1)), + ] + .into() + ) + ); + Ok(()) + } + + #[test_log::test] + fn test_looking_for_narrower_command() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, _carol_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let deleg_1 = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/test".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg_1.clone())?; + + let deleg_2 = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/test/me".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(deleg_2.clone())?; + + let got = store.get_chain( + &carol, + &Some(alice), + "/test/me/now".into(), + vec![], + SystemTime::now(), + )?; + + pretty::assert_eq!( + got, + Some( + nonempty![ + (deleg_2.cid()?, Arc::new(deleg_2)), + (deleg_1.cid()?, Arc::new(deleg_1)), + ] + .into() + ) + ); + Ok(()) + } + + #[test_log::test] + fn test_broken_chain() -> TestResult { + let (alice, alice_signer) = gen_did(); + let (bob, _bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dan, _dan_signer) = gen_did(); + + let store = crate::delegation::store::MemoryStore::default(); + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/test".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(alice_to_bob.clone())?; + + let carol_to_dan = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(carol.clone()) + .audience(dan.clone()) + .command("/test/me".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(carol_to_dan.clone())?; + + let got = store.get_chain( + &carol, + &Some(alice), + "/test/me/now".into(), + vec![], + SystemTime::now(), + )?; + + pretty::assert_eq!(got, None); + Ok(()) + } + + #[test_log::test] + fn test_long_chain() -> TestResult { + // Scenario + // ======== + // 1. bob -*-> carol + // 2. carol -a-> dave + // 3. alice -d-> bob + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dave, _) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let store = crate::delegation::store::MemoryStore::default(); + + // 1. bob -*-> carol + let bob_to_carol = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + // 2. carol -a-> dave + let carol_to_dave = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(carol.clone()) + .audience(dave.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, // I don't love this is now failable + )?; + + // 3. alice -d-> bob + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(bob_to_carol.clone())?; + store.insert(carol_to_dave.clone())?; + store.insert(alice_to_bob.clone())?; + + let got: Vec = store + .get_chain(&dave, &Some(alice), "/".into(), vec![], SystemTime::now()) + .map_err(|e| e.to_string())? + .ok_or("failed during proof lookup")? + .iter() + .map(|(cid, _)| cid) + .cloned() + .collect(); + + pretty::assert_eq!( + got, + vec![ + carol_to_dave.cid()?, + bob_to_carol.cid()?, + alice_to_bob.cid()? + ] + ); + + Ok(()) + } + + #[test_log::test] + fn test_long_powerline() -> TestResult { + // Scenario + // ======== + // 1. bob -*-> carol + // 2. carol -a-> dave + // 3. alice -d-> bob + let (alice, alice_signer) = gen_did(); + let (bob, bob_signer) = gen_did(); + let (carol, carol_signer) = gen_did(); + let (dave, _) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let store = crate::delegation::store::MemoryStore::default(); + + // 1. bob -*-> carol + let bob_to_carol = crate::Delegation::try_sign( + &bob_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(bob.clone()) + .audience(carol.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + // 2. carol -a-> dave + let carol_to_dave = crate::Delegation::try_sign( + &carol_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(carol.clone()) + .audience(dave.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, // I don't love this is now failable + )?; + + // 3. alice -d-> bob + let alice_to_bob = crate::Delegation::try_sign( + &alice_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(alice.clone())) + .issuer(alice.clone()) + .audience(bob.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + store.insert(bob_to_carol.clone())?; + store.insert(carol_to_dave.clone())?; + store.insert(alice_to_bob.clone())?; + + let got: Vec = store + .get_chain( + &dave, + &Some(alice.clone()), + "/".into(), + vec![], + SystemTime::now(), + ) + .map_err(|e| e.to_string())? + .ok_or("failed during proof lookup")? + .iter() + .map(|(cid, _)| cid) + .cloned() + .collect(); + + pretty::assert_eq!( + got, + vec![ + carol_to_dave.cid()?, + bob_to_carol.cid()?, + alice_to_bob.cid()? + ] + ); + + Ok(()) + } + } +} diff --git a/src/delegation/store/traits.rs b/src/delegation/store/traits.rs new file mode 100644 index 00000000..c917dc20 --- /dev/null +++ b/src/delegation/store/traits.rs @@ -0,0 +1,130 @@ +use crate::{ + ability::arguments::Named, + crypto::signature::Envelope, + crypto::varsig, + delegation::payload::Payload, + delegation::{policy::Predicate, Delegation}, + did::Did, +}; +use libipld_core::codec::Encode; +use libipld_core::ipld::Ipld; +use libipld_core::{cid::Cid, codec::Codec}; +use nonempty::NonEmpty; +use std::{fmt::Debug, sync::Arc}; +use web_time::SystemTime; + +pub trait Store + Clone, C: Codec + TryFrom + Into> +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + type DelegationStoreError: Debug; + + fn get( + &self, + cid: &Cid, + ) -> Result>>, Self::DelegationStoreError>; + + fn insert(&self, delegation: Delegation) -> Result<(), Self::DelegationStoreError> { + self.insert_keyed(delegation.cid().expect("FIXME"), delegation) + } + + fn insert_keyed( + &self, + cid: Cid, + delegation: Delegation, + ) -> Result<(), Self::DelegationStoreError>; + + // FIXME validate invocation + // store invocation + // just... move to invocation + fn revoke(&self, cid: Cid) -> Result<(), Self::DelegationStoreError>; + + fn get_chain( + &self, + audience: &DID, + subject: &Option, + command: String, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::DelegationStoreError>; + + fn get_chain_cids( + &self, + audience: &DID, + subject: &Option, + command: String, + policy: Vec, + now: SystemTime, + ) -> Result>, Self::DelegationStoreError> { + self.get_chain(audience, subject, command, policy, now) + .map(|chain| chain.map(|chain| chain.map(|(cid, _)| cid))) + } + + fn can_delegate( + &self, + issuer: DID, + audience: &DID, + command: String, + policy: Vec, + now: SystemTime, + ) -> Result { + self.get_chain(audience, &Some(issuer), command, policy, now) + .map(|chain| chain.is_some()) + } + + fn get_many( + &self, + cids: &[Cid], + ) -> Result>>>, Self::DelegationStoreError> { + cids.iter() + .map(|cid| self.get(cid)) + .collect::>() + } +} + +impl< + T: Store, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Store for &T +where + Ipld: Encode, + Payload: TryFrom>, + Named: From>, +{ + type DelegationStoreError = >::DelegationStoreError; + + fn get( + &self, + cid: &Cid, + ) -> Result>>, Self::DelegationStoreError> { + (**self).get(cid) + } + + fn insert_keyed( + &self, + cid: Cid, + delegation: Delegation, + ) -> Result<(), Self::DelegationStoreError> { + (**self).insert_keyed(cid, delegation) + } + + fn revoke(&self, cid: Cid) -> Result<(), Self::DelegationStoreError> { + (**self).revoke(cid) + } + + fn get_chain( + &self, + audience: &DID, + subject: &Option, + command: String, + policy: Vec, + now: SystemTime, + ) -> Result>)>>, Self::DelegationStoreError> + { + (**self).get_chain(audience, subject, command, policy, now) + } +} diff --git a/src/did.rs b/src/did.rs new file mode 100644 index 00000000..65a00d29 --- /dev/null +++ b/src/did.rs @@ -0,0 +1,12 @@ +//! Decentralized Identifier ([DID][wiki]) utilities. +//! +//! [wiki]: https://en.wikipedia.org/wiki/Decentralized_identifier + +mod newtype; +mod traits; + +pub mod key; +pub mod preset; + +pub use newtype::{FromIpldError, Newtype}; +pub use traits::{Did, Verifiable}; diff --git a/src/did/dns.rs b/src/did/dns.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/did/dns.rs @@ -0,0 +1 @@ + diff --git a/src/did/key.rs b/src/did/key.rs new file mode 100644 index 00000000..6be3a7bb --- /dev/null +++ b/src/did/key.rs @@ -0,0 +1,11 @@ +//! Support for the [`did:key`](https://w3c-ccg.github.io/did-method-key/) DID method. + +mod signature; +mod verifier; +mod signer; + +pub mod traits; + +pub use signature::Signature; +pub use verifier::*; +pub use signer::*; diff --git a/src/did/key/signature.rs b/src/did/key/signature.rs new file mode 100644 index 00000000..1392eec6 --- /dev/null +++ b/src/did/key/signature.rs @@ -0,0 +1,116 @@ +use enum_as_inner::EnumAsInner; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use ::p521 as ext_p521; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +#[cfg(feature = "bls")] +use crate::crypto::bls12381; + +/// Signature types that are verifiable by `did:key` [`Verifier`]s. +#[derive(Debug, Clone, PartialEq, Eq, EnumAsInner)] +pub enum Signature { + /// `EdDSA` signature. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::Signature), + + /// `ES256K` (`secp256k1`) signature. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::Signature), + + /// `P-256` signature. + #[cfg(feature = "es256")] + P256(p256::ecdsa::Signature), + + /// `P-384` signature. + #[cfg(feature = "es384")] + P384(p384::ecdsa::Signature), + + /// `P-521` signature. + #[cfg(feature = "es512")] + P521(ext_p521::ecdsa::Signature), + + /// `RS256` signature. + #[cfg(feature = "rs256")] + Rs256(rs256::Signature), + + /// `RS512` signature. + #[cfg(feature = "rs512")] + Rs512(rs512::Signature), + + /// `BLS 12-381` signature for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(bls12381::min_pk::Signature), + + /// `BLS 12-381` signature for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(bls12381::min_sig::Signature), + + /// An unknown signature type. + /// + /// This is primarily for parsing, where reification is delayed + /// until the DID method is known. + Unknown(Vec), +} + +impl signature::SignatureEncoding for Signature { + type Repr = Vec; +} + +impl From for Vec { + fn from(sig: Signature) -> Vec { + match sig { + #[cfg(feature = "eddsa")] + Signature::EdDsa(sig) => sig.to_vec(), + + #[cfg(feature = "es256k")] + Signature::Es256k(sig) => sig.to_vec(), + + #[cfg(feature = "es256")] + Signature::P256(sig) => sig.to_vec(), + + #[cfg(feature = "es384")] + Signature::P384(sig) => sig.to_vec(), + + #[cfg(feature = "es512")] + Signature::P521(sig) => sig.to_vec(), + + #[cfg(feature = "rs256")] + Signature::Rs256(sig) => <[u8; 256]>::from(sig).into(), + + #[cfg(feature = "rs512")] + Signature::Rs512(sig) => <[u8; 512]>::from(sig).into(), + + #[cfg(feature = "bls")] + Signature::BlsMinPk(sig) => <[u8; 96]>::from(sig).into(), + + #[cfg(feature = "bls")] + Signature::BlsMinSig(sig) => <[u8; 48]>::from(sig).into(), + + Signature::Unknown(vec) => vec, + } + } +} + +impl From<&[u8]> for Signature { + fn from(arr: &[u8]) -> Signature { + Signature::Unknown(arr.to_vec()) + } +} diff --git a/src/did/key/signer.rs b/src/did/key/signer.rs new file mode 100644 index 00000000..b7f95e40 --- /dev/null +++ b/src/did/key/signer.rs @@ -0,0 +1,131 @@ +use super::Signature; +use enum_as_inner::EnumAsInner; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use ::p521 as ext_p521; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +#[cfg(feature = "bls")] +use crate::crypto::bls12381; + +/// Signer types that are verifiable by `did:key` [`Verifier`]s. +#[derive(Clone, EnumAsInner)] +pub enum Signer { + /// `EdDSA` signer. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::SigningKey), + + /// `ES256K` (`secp256k1`) signer. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::SigningKey), + + /// `P-256` signer. + #[cfg(feature = "es256")] + P256(p256::ecdsa::SigningKey), + + /// `P-384` signer. + #[cfg(feature = "es384")] + P384(p384::ecdsa::SigningKey), + + /// `P-521` signer. + #[cfg(feature = "es512")] + P521(ext_p521::ecdsa::SigningKey), + + /// `RS256` signer. + #[cfg(feature = "rs256")] + Rs256(rs256::SigningKey), + + /// `RS512` signer. + #[cfg(feature = "rs512")] + Rs512(rs512::SigningKey), + + /// `BLS 12-381` signer for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(blst::min_pk::SecretKey), + + /// `BLS 12-381` signer for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(blst::min_sig::SecretKey), + // /// An unknown signer type. + // /// + // /// This is primarily for parsing, where reification is delayed + // /// until the DID method is known. + // FIXME rmeove Unknown(Vec), +} + +impl signature::Signer for Signer { + fn try_sign(&self, msg: &[u8]) -> Result { + match self { + #[cfg(feature = "eddsa")] + Signer::EdDsa(signer) => { + let sig = signer.sign(msg); + Ok(Signature::EdDsa(sig)) + } + + #[cfg(feature = "es256k")] + Signer::Es256k(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Es256k(sig)) + } + + #[cfg(feature = "es256")] + Signer::P256(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P256(sig)) + } + + #[cfg(feature = "es384")] + Signer::P384(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P384(sig)) + } + + #[cfg(feature = "es512")] + Signer::P521(signer) => { + let sig = signer.sign(msg); + Ok(Signature::P521(sig)) + } + + #[cfg(feature = "rs256")] + Signer::Rs256(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Rs256(sig)) + } + + #[cfg(feature = "rs512")] + Signer::Rs512(signer) => { + let sig = signer.sign(msg); + Ok(Signature::Rs512(sig)) + } + + #[cfg(feature = "bls")] + Signer::BlsMinPk(signer) => { + let sig = signer.try_sign(msg)?; + Ok(Signature::BlsMinPk(sig)) + } + + #[cfg(feature = "bls")] + Signer::BlsMinSig(signer) => { + let sig = signer.try_sign(msg)?; + Ok(Signature::BlsMinSig(sig)) + } + } + } +} diff --git a/src/did/key/traits.rs b/src/did/key/traits.rs new file mode 100644 index 00000000..7a1eb9f1 --- /dev/null +++ b/src/did/key/traits.rs @@ -0,0 +1,81 @@ +/// A trait aligning signatures with keys. + +use crate::crypto::{bls12381, es512, rs256, rs512}; +use ::p521 as ext_p521; +use ed25519_dalek; +use k256; +use p256; +use p384; + +// FIXME +// also: e.g. HSM? + +pub trait DidKey: signature::Verifier { + const BASE58_PREFIX: &'static str; + + type Signer: signature::Signer; + type Signature: signature::SignatureEncoding; +} + +impl DidKey for ed25519_dalek::VerifyingKey { + const BASE58_PREFIX: &'static str = "6Mk"; + + type Signer = ed25519_dalek::SigningKey; + type Signature = ed25519_dalek::Signature; +} + +impl DidKey for p256::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "Dn"; + + type Signer = p256::ecdsa::SigningKey; + type Signature = p256::ecdsa::Signature; +} + +impl DidKey for k256::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "Q3s"; + + type Signer = k256::ecdsa::SigningKey; + type Signature = k256::ecdsa::Signature; +} + +impl DidKey for p384::ecdsa::VerifyingKey { + const BASE58_PREFIX: &'static str = "82"; + + type Signer = p384::ecdsa::SigningKey; + type Signature = p384::ecdsa::Signature; +} + +impl DidKey for es512::VerifyingKey { + const BASE58_PREFIX: &'static str = "2J9"; + + type Signer = ext_p521::ecdsa::SigningKey; + type Signature = ext_p521::ecdsa::Signature; +} + +impl DidKey for rs256::VerifyingKey { + const BASE58_PREFIX: &'static str = "4MX"; + + type Signer = rs256::SigningKey; + type Signature = rs256::Signature; +} + +impl DidKey for rs512::VerifyingKey { + const BASE58_PREFIX: &'static str = "zgg"; + + type Signer = rs512::SigningKey; + type Signature = rs512::Signature; +} + +impl DidKey for blst::min_sig::PublicKey { + const BASE58_PREFIX: &'static str = "UC7"; + + type Signer = blst::min_sig::SecretKey; + type Signature = bls12381::min_sig::Signature; +} + +impl DidKey for blst::min_pk::PublicKey { + const BASE58_PREFIX: &'static str = "UC7"; + + type Signer = blst::min_pk::SecretKey; + type Signature = bls12381::min_pk::Signature; +} diff --git a/src/did/key/verifier.rs b/src/did/key/verifier.rs new file mode 100644 index 00000000..4dcced0e --- /dev/null +++ b/src/did/key/verifier.rs @@ -0,0 +1,593 @@ +use super::Signature; +use blst::BLST_ERROR; +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use multibase; +use multibase::Base; +use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; +use serde::{Deserialize, Serialize}; +use signature as sig; +use std::{fmt::Display, str::FromStr}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "eddsa")] +use ed25519_dalek; + +#[cfg(feature = "es256")] +use p256; + +#[cfg(feature = "es256k")] +use k256; + +#[cfg(feature = "es384")] +use p384; + +#[cfg(feature = "es512")] +use crate::crypto::es512; + +#[cfg(feature = "rs256")] +use crate::crypto::rs256; + +#[cfg(feature = "rs512")] +use crate::crypto::rs512; + +#[cfg(feature = "bls")] +use blst; + +/// Verifiers (public/verifying keys) for `did:key`. +#[derive(Debug, Clone, PartialEq, Eq, EnumAsInner)] +pub enum Verifier { + /// `EdDSA` verifying key. + #[cfg(feature = "eddsa")] + EdDsa(ed25519_dalek::VerifyingKey), + + /// `ES256K` (`secp256k1`) verifying key. + #[cfg(feature = "es256k")] + Es256k(k256::ecdsa::VerifyingKey), + + /// `P-256` verifying key. + #[cfg(feature = "es256")] + P256(p256::ecdsa::VerifyingKey), + + /// `P-384` verifying key. + #[cfg(feature = "es384")] + P384(p384::ecdsa::VerifyingKey), + + /// `P-521` verifying key. + #[cfg(feature = "es512")] + P521(es512::VerifyingKey), + + /// `RS256` verifying key. + #[cfg(feature = "rs256")] + Rs256(rs256::VerifyingKey), + + /// `RS512` verifying key. + #[cfg(feature = "rs512")] + Rs512(rs512::VerifyingKey), + + /// `BLS 12-381` verifying key for the "min pub key" variant. + #[cfg(feature = "bls")] + BlsMinPk(blst::min_pk::PublicKey), + + /// `BLS 12-381` verifying key for the "min sig" variant. + #[cfg(feature = "bls")] + BlsMinSig(blst::min_sig::PublicKey), +} + +impl PartialOrd for Verifier { + fn partial_cmp(&self, other: &Self) -> Option { + self.to_string().partial_cmp(&other.to_string()) + } +} + +impl Ord for Verifier { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.to_string().cmp(&other.to_string()) + } +} + +impl signature::Verifier for Verifier { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + match (self, signature) { + (Verifier::EdDsa(vk), Signature::EdDsa(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Es256k(vk), Signature::Es256k(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P256(vk), Signature::P256(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P384(vk), Signature::P384(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::P521(vk), Signature::P521(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Rs256(vk), Signature::Rs256(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::Rs512(vk), Signature::Rs512(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::BlsMinPk(vk), Signature::BlsMinPk(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (Verifier::BlsMinSig(vk), Signature::BlsMinSig(sig)) => { + vk.verify(msg, sig).map_err(signature::Error::from_source) + } + (_, _) => Err(signature::Error::from_source( + "invalid signature type for verifier", + )), + } + } +} + +impl Display for Verifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = match self { + Verifier::EdDsa(ed25519_pk) => { + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xed, &mut buf); + + let mut payload: Vec = tag.to_vec(); + let bytes = ed25519_pk.to_bytes(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Es256k(secp256k1_pk) => { + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xe7, &mut buf); + + let mut payload = tag.to_vec(); + let bytes = secp256k1_pk.to_sec1_bytes(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P256(p256_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1200, &mut buf); + + let mut payload = tag.to_vec(); + let point = p256_key.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P384(p384_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1201, &mut buf); + + let mut payload = tag.to_vec(); + let point = p384_key.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::P521(p521_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1202, &mut buf); + + let mut payload = tag.to_vec(); + let point = p521_key.0.to_encoded_point(true); + payload.extend_from_slice(point.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Rs256(rsa2048_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1205, &mut buf); + + let mut payload = tag.to_vec(); + let raw = rsa2048_key.0.to_pkcs1_der().map_err(|_| std::fmt::Error)?; // NOTE: technically should never fail + payload.extend_from_slice(raw.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::Rs512(rsa4096_key) => { + let mut buf = [0u8; 3]; + let tag = unsigned_varint::encode::u16(0x1205, &mut buf); + + let mut payload = tag.to_vec(); + let raw = rsa4096_key.0.to_pkcs1_der().map_err(|_| std::fmt::Error)?; // NOTE: technically should never fail + payload.extend_from_slice(raw.as_bytes()); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::BlsMinPk(bls_minpk_pk) => { + let bytes = bls_minpk_pk.compress(); + + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xeb, &mut buf); + + let mut payload = tag.to_vec(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + Verifier::BlsMinSig(bls_minsig_pk) => { + let bytes = bls_minsig_pk.compress(); + + let mut buf = [0u8; 2]; + let tag = unsigned_varint::encode::u8(0xeb, &mut buf); + + let mut payload = tag.to_vec(); + payload.extend_from_slice(&bytes); + + multibase::encode(Base::Base58Btc, payload) + } + }; + + write!(f, "did:key:{}", inner) + } +} + +impl FromStr for Verifier { + type Err = FromStrError; + + fn from_str(s: &str) -> Result { + if s.len() < 32 { + // Smallest key size + return Err(FromStrError::TooShort); + } + + match s.split_at(8) { + ("did:key:", more) => { + let (_base, varint_bytes): (multibase::Base, Vec) = multibase::decode(more)?; + let (tag, rest) = unsigned_varint::decode::u16(&varint_bytes)?; + + // FIXME also check max length on bytes + match tag { + 0xed => { + let arr: [u8; 32] = rest.try_into().map_err(|_| FromStrError::TooShort)?; + + let vk = ed25519_dalek::VerifyingKey::from_bytes(&arr) + .map_err(FromStrError::CannotParseEdDsa)?; + + Ok(Verifier::EdDsa(vk)) + } + 0xe7 => { + let vk = k256::ecdsa::VerifyingKey::from_sec1_bytes(&rest) + .map_err(FromStrError::CannotParseEs256k)?; + + Ok(Verifier::Es256k(vk)) + } + 0x1200 => { + let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP256)?; + + Ok(Verifier::P256(vk)) + } + 0x1201 => { + let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP384)?; + + Ok(Verifier::P384(vk)) + } + 0x1202 => { + let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(rest) + .map_err(FromStrError::CannotParseP521)?; + + Ok(Verifier::P521(es512::VerifyingKey(vk))) + } + 0x1205 => match rest.len() { + // 256-bytes plus params + 270 => { + let vk = rsa::pkcs1v15::VerifyingKey::from_pkcs1_der(rest) + .map_err(FromStrError::CannotParseRs256)?; + + Ok(Verifier::Rs256(rs256::VerifyingKey(vk))) + } + // 512-bytes plus params + 526 => { + let vk = rsa::pkcs1v15::VerifyingKey::from_pkcs1_der(rest) + .map_err(FromStrError::CannotParseRs512)?; + + Ok(Verifier::Rs512(rs512::VerifyingKey(vk))) + } + len => Err(FromStrError::InvalidRsaLength(len)), + }, + 0xeb => match rest.len() { + 48 => { + let pk = blst::min_pk::PublicKey::deserialize(rest) + .map_err(FromStrError::CannotParseBlsMinPk)?; + + Ok(Verifier::BlsMinPk(pk)) + } + 96 => { + let pk = blst::min_sig::PublicKey::deserialize(rest) + .map_err(FromStrError::CannotParseBlsMinSig)?; + + Ok(Verifier::BlsMinSig(pk)) + } + len => Err(FromStrError::InvalidBlsLength(len)), + }, + word => Err(FromStrError::UnexpectedPrefix(word)), + } + } + + (s, _) => Err(FromStrError::UnexpectedHeader(s.to_string())), + } + } +} + +impl From for Ipld { + fn from(v: Verifier) -> Self { + v.to_string().into() + } +} + +impl TryFrom for Verifier { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + if let Ipld::String(s) = ipld { + Verifier::from_str(&s).map_err(|_| ()) + } else { + Err(()) + } + } +} + +#[derive(Debug, Error)] +pub enum FromStrError { + #[error("not a did:key prefix: {0}")] + NotADidKey(usize), + + #[error("unexpected prefix: {0:?}")] + UnexpectedPrefix(u16), + + #[error("unexpected header: {0}")] + UnexpectedHeader(String), + + #[error("unexpected BLS length: {0}")] + InvalidBlsLength(usize), + + #[error("Invalid RSA length: {0}")] + InvalidRsaLength(usize), + + #[error("key too short")] + TooShort, + + #[error("cannot parse EdDSA key: {0}")] + CannotParseEdDsa(sig::Error), + + #[error("cannot parse ES256K key: {0}")] + CannotParseEs256k(sig::Error), + + #[error("cannot parse P-256 key: {0}")] + CannotParseP256(sig::Error), + + #[error("cannot parse P-384 key: {0}")] + CannotParseP384(sig::Error), + + #[error("cannot parse P-521 key: {0}")] + CannotParseP521(sig::Error), + + #[error("cannot parse RS256 key: {0}")] + CannotParseRs256(rsa::pkcs1::Error), + + #[error("cannot parse RS512 key: {0}")] + CannotParseRs512(rsa::pkcs1::Error), + + #[error("cannot parse BLS min pk key: {0:?}")] + CannotParseBlsMinPk(BLST_ERROR), + + #[error("cannot parse BLS min sig key: {0:?}")] + CannotParseBlsMinSig(BLST_ERROR), + + #[error("cannot decode multibase: {0}")] + CannotDecodeMultibase(#[from] multibase::Error), + + #[error("cannot parse tag: {0}")] + CannotParseTag(#[from] unsigned_varint::decode::Error), +} + +impl PartialEq for FromStrError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (FromStrError::NotADidKey(a), FromStrError::NotADidKey(b)) => a == b, + (FromStrError::UnexpectedPrefix(a), FromStrError::UnexpectedPrefix(b)) => a == b, + (FromStrError::TooShort, FromStrError::TooShort) => true, + (FromStrError::CannotParseEdDsa(a), FromStrError::CannotParseEdDsa(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseEs256k(a), FromStrError::CannotParseEs256k(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP256(a), FromStrError::CannotParseP256(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP384(a), FromStrError::CannotParseP384(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseP521(a), FromStrError::CannotParseP521(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseRs256(a), FromStrError::CannotParseRs256(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseRs512(a), FromStrError::CannotParseRs512(b)) => { + a.to_string() == b.to_string() + } + (FromStrError::CannotParseBlsMinPk(a), FromStrError::CannotParseBlsMinPk(b)) => a == b, + (FromStrError::CannotParseBlsMinSig(a), FromStrError::CannotParseBlsMinSig(b)) => { + a == b + } + ( + FromStrError::CannotDecodeMultibase(lhs), + FromStrError::CannotDecodeMultibase(rhs), + ) => lhs == rhs, + (FromStrError::CannotParseTag(lhs), FromStrError::CannotParseTag(rhs)) => lhs == rhs, + _ => false, + } + } +} + +impl Serialize for Verifier { + fn serialize(&self, serializer: S) -> Result { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Verifier { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Verifier::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl From for DID { + fn from(v: Verifier) -> Self { + DID::parse(&v.to_string()).expect("verifier to be a valid DID") + } +} + +impl TryFrom for Verifier { + type Error = FromStrError; + + fn try_from(did: DID) -> Result { + Verifier::from_str(&did.to_string()) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Verifier { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // NOTE these are just the test vectors from `did:key` v0.7 + prop_oneof![ + // ed25519 + Just("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), + + // secp256k1 + Just("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"), + Just("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"), + Just("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"), + + // BLS + Just("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"), + Just("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"), + + // P-256 + Just("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"), + Just("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"), + + // P-384 + Just("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"), + Just("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"), + + // P-521 + Just("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"), + Just("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"), + + // RSA-2048 + Just("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"), + + // RSA-4096 + Just("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + ] + .prop_map(|s: &str| Verifier::from_str(s).expect("did:key spec test vectors to work")) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions as pretty; + use testresult::TestResult; + + mod serialization { + use super::*; + + fn roundtrip(s: &str) -> TestResult { + let v = Verifier::from_str(s)?; + let serialized = v.to_string(); + pretty::assert_eq!(s, serialized); + Ok(()) + } + + #[test_log::test] + fn test_ed25519_parse() -> TestResult { + roundtrip("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") + } + + #[test_log::test] + fn test_secp256k_1_parse() -> TestResult { + roundtrip("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme") + } + + #[test_log::test] + fn test_secp256k_2_parse() -> TestResult { + roundtrip("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2") + } + + #[test_log::test] + fn test_secp256k_3_parse() -> TestResult { + roundtrip("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N") + } + + #[test_log::test] + fn test_bls_1_parse() -> TestResult { + roundtrip("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY") + } + + #[test_log::test] + fn test_bls_2_parse() -> TestResult { + roundtrip("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW") + } + + #[test_log::test] + fn test_p256_1_parse() -> TestResult { + roundtrip("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169") + } + + #[test_log::test] + fn test_p256_2_parse() -> TestResult { + roundtrip("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv") + } + + #[test_log::test] + fn test_p384_1_parse() -> TestResult { + roundtrip( + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + ) + } + + #[test_log::test] + fn test_p384_2_parse() -> TestResult { + roundtrip( + "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54", + ) + } + + #[test_log::test] + fn test_p521_1_parse() -> TestResult { + roundtrip("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7") + } + + #[test_log::test] + fn test_p521_2_parse() -> TestResult { + roundtrip("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f") + } + + #[test_log::test] + fn test_rs256_parse() -> TestResult { + roundtrip("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i") + } + + #[test_log::test] + fn test_rs512_parse() -> TestResult { + roundtrip("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + } + } +} diff --git a/src/did/newtype.rs b/src/did/newtype.rs new file mode 100644 index 00000000..1057247b --- /dev/null +++ b/src/did/newtype.rs @@ -0,0 +1,156 @@ +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::{fmt, string::ToString}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// A [Decentralized Identifier (DID)][wiki] +/// +/// This is a newtype wrapper around the [`DID`] type from the [`did_url`] crate. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::did; +/// # +/// let did = did::Newtype::try_from("did:example:123".to_string()).unwrap(); +/// assert_eq!(did.0.method(), "example"); +/// ``` +/// +/// [wiki]: https://en.wikipedia.org/wiki/Decentralized_identifier +pub struct Newtype(pub DID); + +impl Serialize for Newtype { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + String::from(self.clone()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Newtype { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + Newtype::try_from(string).map_err(serde::de::Error::custom) + } +} + +impl From for String { + fn from(did: Newtype) -> Self { + did.0.to_string() + } +} + +impl TryFrom for Newtype { + type Error = >::Error; + + fn try_from(string: String) -> Result { + DID::parse(&string).map(Newtype) + } +} + +impl fmt::Display for Newtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } +} + +impl From for Ipld { + fn from(did: Newtype) -> Self { + did.into() + } +} + +impl TryFrom for Newtype { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::String(string) => { + Newtype::try_from(string).map_err(FromIpldError::StructuralError) + } + other => Err(FromIpldError::NotAnIpldString(other)), + } + } +} + +/// Errors that can occur when converting to or from a [`Newtype`] +#[derive(Debug, Clone, EnumAsInner, PartialEq, Error)] +pub enum FromIpldError { + /// Strutural errors in the [`Newtype`] + #[error(transparent)] + StructuralError(#[from] did_url::Error), + + /// The [`Ipld`] was not a string + #[error("Not an IPLD String")] + NotAnIpldString(Ipld), +} + +impl Serialize for FromIpldError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for FromIpldError { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Ok(FromIpldError::NotAnIpldString(ipld)) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // NOTE these are just the test vectors from `did:key` v0.7 + prop_oneof![ + // did:key + Just("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"), + + // secp256k1 + Just("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"), + Just("did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"), + Just("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"), + + // BLS + Just("did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"), + Just("did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"), + + // P-256 + Just("did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"), + Just("did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"), + + // P-384 + Just("did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"), + Just("did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"), + + // P-521 + Just("did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"), + Just("did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"), + + // RSA-2048 + Just("did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"), + + // RSA-4096 + Just("did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2") + ].prop_map(|s: &str| Newtype(did_url::DID::parse(s).expect("did:key spec test vectors to work"))).boxed() + } +} diff --git a/src/did/preset.rs b/src/did/preset.rs new file mode 100644 index 00000000..a4616525 --- /dev/null +++ b/src/did/preset.rs @@ -0,0 +1,115 @@ +use super::key; +use super::Did; +use did_url::DID; +use enum_as_inner::EnumAsInner; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, str::FromStr}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The set of [`Did`] types that ship with this library ("presets"). +#[derive(Debug, Clone, EnumAsInner, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Verifier { + /// `did:key` DIDs. + Key(key::Verifier), + // + // FIXME Dns(did_url::DID), +} + +impl From for Ipld { + fn from(verifier: Verifier) -> Self { + match verifier { + Verifier::Key(verifier) => verifier.into(), + } + } +} + +impl TryFrom for Verifier { + type Error = (); // FIXME + + fn try_from(ipld: Ipld) -> Result { + key::Verifier::try_from(ipld) + .map(Verifier::Key) + .map_err(|_| ()) + } +} + +impl From for DID { + fn from(verifier: Verifier) -> Self { + match verifier { + Verifier::Key(verifier) => verifier.into(), + } + } +} + +#[derive(Clone, EnumAsInner)] +pub enum Signer { + Key(key::Signer), + // FIXME Dns(did_url::DID), +} + +impl std::fmt::Debug for Signer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Signer::Key(signer) => write!(f, "Signer::Key(HIDDEN)"), + } + } +} + +impl Did for Verifier { + type Signature = key::Signature; + type Signer = self::Signer; +} + +impl TryFrom for Verifier { + type Error = key::FromStrError; + + fn try_from(did: DID) -> Result { + key::Verifier::try_from(did).map(Verifier::Key) + } +} + +impl signature::Signer for Signer { + fn try_sign(&self, message: &[u8]) -> Result { + match self { + Signer::Key(signer) => signer.try_sign(message), + } + } +} + +impl signature::Verifier for Verifier { + fn verify(&self, message: &[u8], signature: &key::Signature) -> Result<(), signature::Error> { + match self { + Verifier::Key(verifier) => verifier.verify(message, signature), + } + } +} + +impl Display for Verifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Verifier::Key(verifier) => verifier.fmt(f), + } + } +} + +impl FromStr for Verifier { + type Err = key::FromStrError; + + fn from_str(s: &str) -> Result { + key::Verifier::from_str(s).map(Verifier::Key) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Verifier { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + key::Verifier::arbitrary().prop_map(Verifier::Key).boxed() + } +} diff --git a/src/did/traits.rs b/src/did/traits.rs new file mode 100644 index 00000000..76e9220f --- /dev/null +++ b/src/did/traits.rs @@ -0,0 +1,12 @@ +use did_url::DID; +use std::fmt; +use std::str::FromStr; + +pub trait Did: PartialEq + ToString + FromStr + signature::Verifier + Ord { + type Signature: signature::SignatureEncoding + PartialEq + fmt::Debug; + type Signer: signature::Signer + fmt::Debug; +} + +pub trait Verifiable { + fn verifier<'a>(&'a self) -> &'a DID; +} diff --git a/src/invocation.rs b/src/invocation.rs new file mode 100644 index 00000000..4c486070 --- /dev/null +++ b/src/invocation.rs @@ -0,0 +1,258 @@ +//! An [`Invocation`] is a request to use an [`Ability`][crate::ability]. +//! +//! ## Data +//! +//! - [`Invocation`] is the top-level, signed data struture. +//! - [`Payload`] is the fields unique to an invocation. +//! - [`Preset`] is an [`Invocation`] preloaded with this library's [preset abilities](crate::ability::preset::Ready). +//! - [`promise`]s are a mechanism to chain invocations together. +//! +//! ## Stateful Helpers +//! +//! - [`Agent`] is a high-level interface for sessions that will involve more than one invoctaion. +//! - [`store`] is an interface for caching [`Invocation`]s. + +pub mod agent; +pub mod payload; + +pub mod promise; +pub mod store; + +pub use agent::Agent; +pub use payload::*; + +use crate::ability::arguments::Named; +use crate::ability::command::ToCommand; +use crate::ability::parse::ParseAbility; +use crate::{ + crypto::{signature::Envelope, varsig}, + did::{self, Did}, + time::{Expired, Timestamp}, +}; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use serde::{Deserialize, Serialize}; +use web_time::SystemTime; + +/// The complete, signed [`invocation::Payload`][Payload]. +/// +/// Invocations are the actual "doing" in the UCAN lifecycle. +/// Unlike [`Delegation`][crate::Delegation]s, which live for some period of time and +/// can be used multiple times, [`Invocation`]s are unique and single-use. +/// +/// # Expiration +/// +/// `Invocations` include an optional expiration field which behaves like a timeout: +/// "if this isn't run by a the expiration time, I'm going to assume that it didn't happen." +/// This is a best practice in message-passing distributed systems because the network is +/// [unreliable](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing). +#[derive(Debug, Clone, PartialEq)] +pub struct Invocation< + A, + DID: did::Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub payload: Payload, + pub signature: DID::Signature, + _marker: std::marker::PhantomData, +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Clone + did::Did, + V: Clone + varsig::Header, + C: Codec + TryFrom + Into, + > Encode for Invocation +where + Ipld: Encode, + Named: From + From>, + Payload: TryFrom>, + // >>::Error: std::fmt::Debug, +{ + fn encode(&self, c: C, w: &mut W) -> Result<(), libipld_core::error::Error> { + self.to_ipld_envelope().encode(c, w) + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header, + C: Codec + TryFrom + Into, + > Invocation +where + Ipld: Encode, +{ + pub fn new(varsig_header: V, signature: DID::Signature, payload: Payload) -> Self { + Invocation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + pub fn audience(&self) -> &Option { + &self.payload.audience + } + + pub fn normalized_audience(&self) -> &DID { + if let Some(audience) = &self.payload.audience { + audience + } else { + &self.payload.subject + } + } + + pub fn issuer(&self) -> &DID { + &self.payload.issuer + } + + pub fn subject(&self) -> &DID { + &self.payload.subject + } + + pub fn ability(&self) -> &A { + &self.payload.ability + } + + pub fn map_ability(self, f: F) -> Invocation + where + F: FnOnce(A) -> Z, + Z: ParseAbility + ToCommand, + { + Invocation::new( + self.varsig_header, + self.signature, + self.payload.map_ability(f), + ) + } + + pub fn proofs(&self) -> &Vec { + &self.payload.proofs + } + + pub fn issued_at(&self) -> &Option { + &self.payload.issued_at + } + + pub fn expiration(&self) -> &Option { + &self.payload.expiration + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), Expired> { + self.payload.check_time(now) + } +} + +impl, C: Codec + TryFrom + Into> did::Verifiable + for Invocation +{ + fn verifier(&self) -> &DID { + &self.payload.verifier() + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > From> for Ipld +where + Named: From, + Payload: TryFrom>, +{ + fn from(invocation: Invocation) -> Self { + invocation.to_ipld_envelope() + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Envelope for Invocation +where + Named: From, + Payload: TryFrom>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Invocation { + Invocation { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl< + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Serialize for Invocation +where + Named: From, + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl< + 'de, + A: Clone + ToCommand + ParseAbility, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Deserialize<'de> for Invocation +where + Named: From, + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/invocation/agent.rs b/src/invocation/agent.rs new file mode 100644 index 00000000..158f49c2 --- /dev/null +++ b/src/invocation/agent.rs @@ -0,0 +1,786 @@ +use super::{ + payload::{Payload, ValidationError}, + store::Store, + Invocation, +}; +use crate::ability::arguments::Named; +use crate::ability::command::ToCommand; +use crate::ability::parse::ParseAbility; +use crate::delegation::Delegation; +use crate::invocation::payload::PayloadBuilder; +use crate::{ + ability::{self, arguments, parse::ParseAbilityError, ucan::revoke::Revoke}, + crypto::{ + signature::{self, Envelope}, + varsig, Nonce, + }, + delegation, + did::{self, Did}, + time::Timestamp, +}; +use enum_as_inner::EnumAsInner; +use libipld_core::{ + cid::Cid, + codec::{Codec, Encode}, + ipld::Ipld, +}; +use std::{collections::BTreeMap, fmt, marker::PhantomData}; +use thiserror::Error; +use web_time::SystemTime; + +#[derive(Debug)] +pub struct Agent< + S: Store, + D: delegation::store::Store, + T: ToCommand = ability::preset::Preset, + DID: Did + Clone = did::preset::Verifier, + V: varsig::Header + Clone = varsig::header::Preset, + C: Codec + Into + TryFrom = varsig::encoding::Preset, +> where + Ipld: Encode, + delegation::Payload: TryFrom>, + Named: From>, +{ + /// The agent's [`DID`]. + pub did: DID, + + /// A [`delegation::Store`][delegation::store::Store]. + pub delegation_store: D, + + /// A [`Store`][Store] for the agent's [`Invocation`]s. + pub invocation_store: S, + + signer: ::Signer, + marker: PhantomData<(T, V, C)>, +} + +impl Agent +where + Ipld: Encode, + T: ToCommand + Clone + ParseAbility, + Named: From, + Payload: TryFrom>, + delegation::Payload: Clone, + DID: Did + Clone, + S: Store, + D: delegation::store::Store, + V: varsig::Header + Clone, + C: Codec + Into + TryFrom, + >::InvocationStoreError: fmt::Debug, + >::DelegationStoreError: fmt::Debug, + delegation::Payload: TryFrom>, + Named: From>, +{ + pub fn new( + did: DID, + signer: ::Signer, + invocation_store: S, + delegation_store: D, + ) -> Self { + Self { + did, + invocation_store, + delegation_store, + signer, + marker: PhantomData, + } + } + + pub fn invoke( + &self, + audience: Option, + subject: DID, + ability: T, + metadata: BTreeMap, + cause: Option, + expiration: Option, + issued_at: Option, + now: SystemTime, + varsig_header: V, + ) -> Result, InvokeError> { + let proofs = if subject == self.did { + vec![] + } else { + self.delegation_store + .get_chain( + &self.did, + &Some(subject.clone()), + ability.to_command(), + vec![], + now, + ) + .map_err(InvokeError::DelegationStoreError)? + .map(|chain| chain.map(|(cid, _)| cid).into()) + .unwrap_or(vec![]) // FIXME + }; + + let payload = Payload { + issuer: self.did.clone(), + subject, + audience, + ability, + proofs, + metadata, + nonce: Nonce::generate_12(&mut vec![]), + cause, + expiration, + issued_at, + }; + + Ok(Invocation::try_sign(&self.signer, varsig_header, payload) + .map_err(InvokeError::SignError)?) + } + + // pub fn invoke_promise( + // &mut self, + // audience: Option<&DID>, + // subject: DID, + // ability: T::Promised, + // metadata: BTreeMap, + // cause: Option, + // expiration: Option, + // issued_at: Option, + // now: SystemTime, + // varsig_header: V, + // ) -> Result< + // Invocation, + // InvokeError< + // D::DelegationStoreError, + // ParseAbilityError<()>, // FIXME errs + // >, + // > + // where + // Named: From, + // Payload: TryFrom>, + // { + // let proofs = self + // .delegation_store + // .get_chain(self.did, &Some(subject.clone()), "/".into(), vec![], now) + // .map_err(InvokeError::DelegationStoreError)? + // .map(|chain| chain.map(|(cid, _)| cid).into()) + // .unwrap_or(vec![]); + + // let mut seed = vec![]; + + // let payload = Payload { + // issuer: self.did.clone(), + // subject, + // audience: audience.cloned(), + // ability, + // proofs, + // metadata, + // nonce: Nonce::generate_12(&mut seed), + // cause, + // expiration, + // issued_at, + // }; + + // Ok(Invocation::try_sign(self.signer, varsig_header, payload) + // .map_err(InvokeError::SignError)?) + // } + + pub fn receive( + &self, + invocation: Invocation, + ) -> Result>, ReceiveError> + where + arguments::Named: From, + Payload: TryFrom>, + Invocation: Clone + Encode, + { + self.generic_receive(invocation, SystemTime::now()) + } + + pub fn generic_receive( + &self, + invocation: Invocation, + now: SystemTime, + ) -> Result>, ReceiveError> + where + arguments::Named: From, + Payload: TryFrom>, + Invocation: Clone + Encode, + { + let cid: Cid = invocation.cid().map_err(ReceiveError::EncodingError)?; + + invocation + .validate_signature() + .map_err(ReceiveError::SigVerifyError)?; + + // FIXME validate signature directly in inv store + + self.invocation_store + .put(cid.clone(), invocation.clone()) + .map_err(ReceiveError::InvocationStoreError)?; + + let proofs = &self + .delegation_store + .get_many(&invocation.proofs()) + .map_err(ReceiveError::DelegationStoreError)?; + let proof_payloads: Vec<&delegation::Payload> = proofs + .iter() + .zip(invocation.proofs().iter()) + .map(|(d, cid)| { + Ok(&d + .as_ref() + .ok_or(ReceiveError::MissingDelegation(*cid))? + .payload) + }) + .collect::>>()?; + + let _ = &invocation + .payload + .check(proof_payloads, now) + .map_err(ReceiveError::ValidationError)?; + + Ok(if invocation.normalized_audience() != &self.did { + Recipient::Other(invocation.payload) + } else { + Recipient::You(invocation.payload) + }) + } + + // pub fn revoke( + // &self, + // subject: DID, + // cause: Option, + // cid: Cid, + // now: Timestamp, + // varsig_header: V, + // // FIXME return type + // ) -> Result, ()> + // where + // Named: From, + // T: From, + // Payload: TryFrom>, + // { + // let ability: T = Revoke { ucan: cid.clone() }.into(); + // let proofs = if &subject == self.did { + // vec![] + // } else { + // todo!("update to latest trait interface"); // FIXME + // // self.delegation_store + // // .get_chain(&subject, &Some(self.did.clone()), vec![], now.into()) + // // .map_err(|_| ())? + // // .map(|chain| chain.map(|(index_cid, _)| index_cid).into()) + // // .unwrap_or(vec![]) + // }; + + // let payload = Payload { + // issuer: self.did.clone(), + // subject: self.did.clone(), + // audience: Some(self.did.clone()), + // ability, + // proofs, + // cause, + // metadata: BTreeMap::new(), + // nonce: Nonce::generate_12(&mut vec![]), + // expiration: None, + // issued_at: None, + // }; + + // let invocation = + // Invocation::try_sign(self.signer, varsig_header, payload).map_err(|_| ())?; + + // self.delegation_store.revoke(cid).map_err(|_| ())?; + // Ok(invocation) + // } +} + +#[derive(Debug, PartialEq, Clone, EnumAsInner)] +pub enum Recipient { + // FIXME change to status? + You(T), + Other(T), + Unresolved(Cid), +} + +#[derive(Debug, Error, EnumAsInner)] +pub enum ReceiveError< + T, + DID: Did, + D, + S: Store, + V: varsig::Header, + C: Codec + TryFrom + Into, +> where + >::InvocationStoreError: fmt::Debug, +{ + #[error("missing delegation: {0}")] + MissingDelegation(Cid), + + #[error("encoding error: {0}")] + EncodingError(#[from] libipld_core::error::Error), + + #[error("signature verification error: {0}")] + SigVerifyError(#[from] signature::ValidateError), + + #[error("invocation store error: {0}")] + InvocationStoreError(#[source] >::InvocationStoreError), + + #[error("delegation store error: {0}")] + DelegationStoreError(#[source] D), + + #[error("delegation validation error: {0}")] + ValidationError(#[source] ValidationError), +} + +#[derive(Debug, Error)] +pub enum InvokeError { + #[error("delegation store error: {0}")] + DelegationStoreError(#[source] D), + + #[error("store error: {0}")] + SignError(#[source] signature::SignError), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ability::crud::read::Read; + use crate::crypto::varsig; + use crate::crypto::varsig::encoding; + use crate::crypto::varsig::header; + use crate::invocation::{payload::ValidationError, Agent}; + use crate::{ + ability::{arguments::Named, command::Command}, + crypto::signature::Envelope, + delegation::store::Store, + invocation::promise::{CantResolve, Resolvable}, + ipld, + }; + use libipld_core::{cid::Cid, ipld::Ipld}; + use pretty_assertions as pretty; + use rand::thread_rng; + use std::ops::{Add, Sub}; + use std::time::{Duration, SystemTime}; + use testresult::TestResult; + + #[derive(Debug, Clone, PartialEq)] + pub struct AccountManage; + + impl Command for AccountManage { + const COMMAND: &'static str = "/account/info"; + } + + impl From for Named { + fn from(_: AccountManage) -> Self { + Default::default() + } + } + + impl TryFrom> for AccountManage { + type Error = (); + + fn try_from(args: Named) -> Result { + if args == Default::default() { + Ok(AccountManage) + } else { + Err(()) + } + } + } + + impl From for Named { + fn from(_: AccountManage) -> Self { + Default::default() + } + } + + impl TryFrom> for AccountManage { + type Error = (); + + fn try_from(args: Named) -> Result { + if args == Default::default() { + Ok(AccountManage) + } else { + Err(()) + } + } + } + + impl Resolvable for AccountManage { + type Promised = AccountManage; + + fn try_resolve(promised: Self::Promised) -> Result> { + Ok(promised) + } + } + + fn gen_did() -> (crate::did::preset::Verifier, crate::did::preset::Signer) { + let sk = ed25519_dalek::SigningKey::generate(&mut thread_rng()); + let verifier = + crate::did::preset::Verifier::Key(crate::did::key::Verifier::EdDsa(sk.verifying_key())); + let signer = crate::did::preset::Signer::Key(crate::did::key::Signer::EdDsa(sk)); + + (verifier, signer) + } + + fn setup_agent( + ) -> Agent { + let (did, signer) = gen_did(); + let inv_store = crate::invocation::store::MemoryStore::default(); + let del_store = crate::delegation::store::MemoryStore::default(); + + crate::invocation::agent::Agent::new(did, signer, inv_store, del_store) + } + + fn setup_valid_time() -> (Timestamp, Timestamp, Timestamp) { + let now = SystemTime::UNIX_EPOCH.add(Duration::from_secs(60 * 60 * 24 * 30)); + let exp = now.add(std::time::Duration::from_secs(60)); + let nbf = now.sub(std::time::Duration::from_secs(60)); + + ( + nbf.try_into().expect("valid nbf time"), + now.try_into().expect("valid now time"), + exp.try_into().expect("valid exp time"), + ) + } + + mod receive { + use super::*; + use assert_matches::assert_matches; + + #[test_log::test] + fn test_invoker_is_sub_implicit_aud() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + varsig::header::Preset::EdDsa(varsig::header::EdDsaHeader { + codec: varsig::encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::You(invocation.payload)); + Ok(()) + } + + #[test_log::test] + fn test_invoker_is_sub_and_aud() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + Some(agent.did.clone()), + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + header::Preset::EdDsa(header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::You(invocation.payload)); + + Ok(()) + } + + #[test_log::test] + fn test_other_recipient() -> TestResult { + let (_nbf, now, exp) = setup_valid_time(); + let agent = setup_agent(); + + let (not_server, _) = gen_did(); + + let invocation = agent.invoke( + Some(not_server), + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(exp.try_into()?), + Some(now.try_into()?), + now.into(), + varsig::header::Preset::EdDsa(varsig::header::EdDsaHeader { + codec: varsig::encoding::Preset::DagCbor, + }), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into())?; + pretty::assert_eq!(observed, Recipient::Other(invocation.payload)); + Ok(()) + } + + #[test_log::test] + fn test_expired() -> TestResult { + let (past, now, _exp) = setup_valid_time(); + let agent = setup_agent(); + + let invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + Some(past.try_into()?), + Some(now.try_into()?), + now.into(), + header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + } + .into(), + )?; + + let observed = agent.generic_receive(invocation.clone(), now.into()); + pretty::assert_eq!( + observed + .unwrap_err() + .as_validation_error() + .ok_or("not a validation error")?, + &ValidationError::Expired + ); + Ok(()) + } + + #[test_log::test] + fn test_invalid_sig() -> TestResult { + let (_past, now, _exp) = setup_valid_time(); + let agent = setup_agent(); + let server = &agent.did; + + let mut invocation = agent.invoke( + None, + agent.did.clone(), + Read { + path: None, + args: None, + } + .into(), + BTreeMap::new(), + None, + None, + Some(now.try_into()?), + now.into(), + header::EdDsaHeader { + codec: encoding::Preset::DagCbor, + } + .into(), + )?; + + let (not_server, _) = gen_did(); + + invocation.payload.issuer = not_server.clone(); + invocation.payload.audience = Some(server.clone()); + invocation.payload.subject = not_server; + + let observed = agent.generic_receive(invocation, now.into()); + + assert_matches!( + observed, + Err(ReceiveError::SigVerifyError( + crate::crypto::signature::ValidateError::VerifyError(_) + )) + ); + + Ok(()) + } + } + + mod chain { + use super::*; + use assert_matches::assert_matches; + + struct Ctx { + varsig_header: crate::crypto::varsig::header::Preset, + dnslink_len: usize, + inv_store: crate::invocation::store::MemoryStore, + del_store: crate::delegation::store::MemoryStore, + account_invocation: Invocation, + server: crate::did::preset::Verifier, + server_signer: crate::did::preset::Signer, + device: crate::did::preset::Verifier, + dnslink: crate::did::preset::Verifier, + } + + fn setup_test_chain() -> Result> { + let (server, server_signer) = gen_did(); + let (account, account_signer) = gen_did(); + let (device, device_signer) = gen_did(); + let (dnslink, dnslink_signer) = gen_did(); + + let varsig_header = crate::crypto::varsig::header::Preset::EdDsa( + crate::crypto::varsig::header::EdDsaHeader { + codec: crate::crypto::varsig::encoding::Preset::DagCbor, + }, + ); + + let inv_store = crate::invocation::store::MemoryStore::default(); + let del_store = crate::delegation::store::MemoryStore::default(); + + // Scenario + // ======== + // + // Delegations + // 1. account -*-> server + // 2. server -a-> device + // 3. dnslink -d-> account + // + // Invocation + // 4. [dnslink -d-> account -*-> server -a-> device] + + // 1. account -*-> server + let account_to_server = crate::Delegation::try_sign( + &account_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(None) + .issuer(account.clone()) + .audience(server.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + // 2. server -a-> device + let server_to_device = crate::Delegation::try_sign( + &server_signer, + varsig_header.clone(), // FIXME can also put this on a builder + crate::delegation::PayloadBuilder::default() + .subject(None) // FIXME needs a sibject when we figure out powerbox + .issuer(server.clone()) + .audience(device.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, // I don't love this is now failable + )?; + + // 3. dnslink -d-> account + let dnslink_to_account = crate::Delegation::try_sign( + &dnslink_signer, + varsig_header.clone(), + crate::delegation::PayloadBuilder::default() + .subject(Some(dnslink.clone())) + .issuer(dnslink.clone()) + .audience(account.clone()) + .command("/".into()) + .expiration(crate::time::Timestamp::five_years_from_now()) + .build()?, + )?; + + del_store.insert(account_to_server.clone())?; + del_store.insert(server_to_device.clone())?; + del_store.insert(dnslink_to_account.clone())?; + + let chain_for_dnslink: Vec = del_store + .get_chain( + &device, + &Some(dnslink.clone()), + "/".into(), + vec![], + SystemTime::now(), + )? + .ok_or("failed during proof lookup")? + .iter() + .map(|x| x.0.clone()) + .collect(); + + // 4. [dnslink -d-> account -*-> server -a-> device] + let account_invocation = crate::Invocation::try_sign( + &device_signer, + varsig_header.clone(), + crate::invocation::PayloadBuilder::default() + .subject(dnslink.clone()) + .issuer(device.clone()) + .audience(Some(server.clone())) + .ability(AccountManage) + .proofs(chain_for_dnslink.clone()) + .build()?, + )?; + + let dnslink_len = chain_for_dnslink.len(); + + Ok(Ctx { + varsig_header, + dnslink_len, + inv_store, + del_store, + account_invocation, + server, + server_signer, + device, + dnslink, + }) + } + + #[test_log::test] + fn test_chain_ok() -> TestResult { + let ctx = setup_test_chain()?; + + let agent = Agent::new( + ctx.server.clone(), + ctx.server_signer.clone(), + &ctx.inv_store, + &ctx.del_store, + ); + + let observed = agent.receive(ctx.account_invocation.clone()); + assert_matches!(observed, Ok(Recipient::You(_))); + Ok(()) + } + + #[test_log::test] + fn test_chain_wrong_sub() -> TestResult { + let ctx = setup_test_chain()?; + + let agent = Agent::new( + ctx.server.clone(), + ctx.server_signer.clone(), + &ctx.inv_store, + &ctx.del_store, + ); + + let not_account_invocation = crate::Invocation::try_sign( + &ctx.server_signer, + ctx.varsig_header, + crate::invocation::PayloadBuilder::default() + .subject(ctx.dnslink.clone()) + .issuer(ctx.server.clone()) + .audience(Some(ctx.device.clone())) + .ability(AccountManage) + .proofs(vec![]) // FIXME + .build()?, + )?; + + let observed_other = agent.receive(not_account_invocation); + assert_matches!( + observed_other, + Err(ReceiveError::ValidationError( + ValidationError::DidNotTerminateInSubject + )) + ); + + Ok(()) + } + } +} diff --git a/src/invocation/payload.rs b/src/invocation/payload.rs new file mode 100644 index 00000000..6e92ae64 --- /dev/null +++ b/src/invocation/payload.rs @@ -0,0 +1,837 @@ +use super::promise::Resolvable; +use crate::ability::command::Command; +use crate::ability::parse::ParseAbilityError; +use crate::delegation::policy::selector; +use crate::invocation::Named; +use crate::time; +use crate::{ + ability::{arguments, command::ToCommand, parse::ParseAbility}, + capsule::Capsule, + crypto::{varsig, Nonce}, + delegation::{ + self, + policy::{selector::SelectorError, Predicate}, + }, + did::{Did, Verifiable}, + time::{Expired, Timestamp}, +}; +use derive_builder::Builder; +use libipld_core::{cid::Cid, codec::Codec, ipld::Ipld}; +use serde::{ + de::{self, MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Serialize, Serializer, +}; +use std::collections::BTreeSet; +use std::str::FromStr; +use std::{collections::BTreeMap, fmt}; +use thiserror::Error; +use web_time::SystemTime; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +#[derive(Debug, Clone, PartialEq, Builder)] +pub struct Payload { + /// The subject of the [`Invocation`]. + /// + /// This is typically also the `audience`, hence the [`audence`] + /// field is optional. + /// + /// This role *must* have issued the earlier (root) + /// delegation in the chain. This makes the chains + /// self-certifying. + /// + /// The semantics of the delegation are established + /// by the subject. + /// + /// [`Invocation`]: super::Invocation + pub subject: DID, + + /// The issuer of the [`Invocation`]. + /// + /// This [`Did`] *must* match the signature on + /// the outer layer of [`Invocation`]. + /// + /// [`Invocation`]: super::Invocation + pub issuer: DID, + + /// The agent being delegated to. + /// + /// Note that if this is the same as the [`subject`], + /// this field may be omitted. + #[builder(default)] + pub audience: Option, + + /// The [Ability] being invoked. + /// + /// The specific shape and semantics of this ability + /// are established by the [`subject`] and the `A` type. + /// + /// [Ability]: crate::ability + pub ability: A, + + /// [`Cid`] links to the proofs that authorize this [`Invocation`]. + /// + /// These must be given in order starting from one where the [`issuer`] + /// of this invocation matches the [`audience`] of that [`Delegation`] proof. + /// + /// [`Invocation`]: super::Invocation + /// [`Delegation`]: crate::delegation::Delegation + #[builder(default)] + pub proofs: Vec, + + /// An optional [`Cid`] of the [`Receipt`] that requested this be invoked. + /// + /// This is helpful for provenance of calls. + /// + /// [`Receipt`]: crate::receipt::Receipt + #[builder(default)] + pub cause: Option, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptographic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + #[builder(default = "Nonce::generate_16(&mut vec![])")] + pub nonce: Nonce, + + /// An optional [Unix timestamp] (wall-clock time) at which this [`Invocation`] + /// was created. + #[builder(default)] + pub issued_at: Option, + + /// An optional [Unix timestamp] (wall-clock time) at which this [`Invocation`] + /// should no longer be executed. + /// + /// One way of thinking about this is as a `timeout`. It also guards against + /// certain types of denial-of-service attacks. + #[builder(default = "Some(Timestamp::five_minutes_from_now())")] + pub expiration: Option, +} + +impl Payload { + pub fn map_ability(self, f: F) -> Payload + where + F: FnOnce(A) -> Z, + { + Payload { + issuer: self.issuer, + subject: self.subject, + audience: self.audience, + ability: f(self.ability), + proofs: self.proofs, + cause: self.cause, + metadata: self.metadata, + nonce: self.nonce, + issued_at: self.issued_at, + expiration: self.expiration, + } + } + + pub fn check_time(&self, now: SystemTime) -> Result<(), Expired> { + let ts_now = &Timestamp::postel(now); + + if let Some(ref exp) = self.expiration { + if exp < ts_now { + return Err(Expired); + } + } + + Ok(()) + } + + pub fn check( + &self, + proofs: Vec<&delegation::Payload>, + now: SystemTime, + ) -> Result<(), ValidationError> + where + A: ToCommand + Clone, + DID: Clone, + arguments::Named: From, + { + let now_ts = Timestamp::postel(now); + + if let Some(exp) = self.expiration { + if exp < now_ts { + return Err(ValidationError::Expired); + } + } + + let args: arguments::Named = self.ability.clone().into(); + + let mut cmd = self.ability.to_command(); + if !cmd.ends_with('/') { + cmd.push('/'); + } + + let (final_iss, vias) = proofs.into_iter().try_fold( + (&self.issuer, BTreeSet::new()), + |(iss, mut vias), proof| { + if *iss != proof.audience { + return Err(ValidationError::MisalignedIssAud.into()); + } + + if let Some(proof_subject) = &proof.subject { + if self.subject != *proof_subject { + return Err(ValidationError::InvalidSubject.into()); + } + } + + if proof.expiration < now_ts { + return Err(ValidationError::Expired.into()); + } + + if let Some(nbf) = proof.not_before.clone() { + if nbf > now_ts { + return Err(ValidationError::NotYetValid.into()); + } + } + + vias.remove(&iss); + if let Some(via_did) = &proof.via { + vias.insert(via_did); + } + + if !cmd.starts_with(&proof.command) { + return Err(ValidationError::CommandMismatch(proof.command.clone())); + } + + let ipld_args = Ipld::from(args.clone()); + + for predicate in proof.policy.iter() { + if !predicate + .clone() + .run(&ipld_args) + .map_err(ValidationError::SelectorError)? + { + return Err(ValidationError::FailedPolicy(predicate.clone())); + } + } + + Ok((&proof.issuer, vias)) + }, + )?; + + if self.subject != *final_iss { + return Err(ValidationError::DidNotTerminateInSubject); + } + + if !vias.is_empty() { + return Err(ValidationError::UnfulfilledViaConstraint( + vias.into_iter().cloned().collect(), + )); + } + + Ok(()) + } +} + +/// Delegation validation errors. +#[derive(Debug, Clone, PartialEq, Error)] +pub enum ValidationError { + #[error("The subject of the delegation is invalid")] + InvalidSubject, + + #[error("The issuer and audience of the delegation are misaligned")] + MisalignedIssAud, + + #[error("The delegation has expired")] + Expired, + + #[error("The delegation is not yet valid")] + NotYetValid, + + #[error("The command of the delegation does not match the proof: {0:?}")] + CommandMismatch(String), + + #[error("The delegation failed a policy predicate: {0:?}")] + FailedPolicy(Predicate), + + #[error(transparent)] + SelectorError(#[from] SelectorError), + + #[error("via field constraint was unfulfilled: {0:?}")] + UnfulfilledViaConstraint(BTreeSet), + + #[error("The chain did not terminate in the expected subject")] + DidNotTerminateInSubject, +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/i@1.0.0-rc.1"; +} + +impl From> for arguments::Named +where + arguments::Named: From, +{ + fn from(payload: Payload) -> Self { + let mut args = arguments::Named::from_iter([ + ("iss".into(), { payload.issuer.to_string().into() }), + ("sub".into(), { payload.subject.to_string().into() }), + ("cmd".into(), { payload.ability.to_command().into() }), + ("args".into(), { + Ipld::Map(arguments::Named::::from(payload.ability).0) + }), + ("prf".into(), { + Ipld::List(payload.proofs.iter().map(Into::into).collect()) + }), + ("nonce".into(), payload.nonce.into()), + ]); + + if let Some(aud) = payload.audience { + args.insert("aud".into(), aud.to_string().into()); + } + + if let Some(cause) = payload.cause { + args.insert("cause".into(), cause.into()); + } + + if !payload.metadata.is_empty() { + args.insert("meta".into(), payload.metadata.into()); + } + + if let Some(iat) = payload.issued_at { + args.insert("iat".into(), iat.into()); + } + + if let Some(exp) = payload.expiration { + args.insert("exp".into(), exp.into()); + } + + args + } +} + +impl From> for Ipld +where + arguments::Named: From>, +{ + fn from(payload: Payload) -> Self { + arguments::Named::from(payload).into() + } +} + +impl Serialize for Payload +where + A: ToCommand + Into + Serialize, + DID: Did + Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let field_count = 9 + + self.audience.is_some() as usize + + self.issued_at.is_some() as usize + + self.expiration.is_some() as usize; + + let mut state = serializer.serialize_struct("invocation::Payload", field_count)?; + + state.serialize_field("iss", &self.issuer)?; + state.serialize_field("sub", &self.subject)?; + + state.serialize_field("cmd", &self.ability.to_command())?; + state.serialize_field("args", &self.ability)?; + + state.serialize_field("prf", &self.proofs)?; + state.serialize_field("nonce", &self.nonce)?; + state.serialize_field("cause", &self.cause)?; + state.serialize_field("meta", &self.metadata)?; + + if let Some(aud) = &self.audience { + state.serialize_field("aud", aud)?; + } + + if let Some(iat) = &self.issued_at { + state.serialize_field("iat", iat)?; + } + + if let Some(exp) = &self.expiration { + state.serialize_field("exp", &exp)?; + } + + state.end() + } +} + +impl<'de, A: ParseAbility + Deserialize<'de>, DID: Did + Deserialize<'de>> Deserialize<'de> + for Payload +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + struct InvocationPayloadVisitor(std::marker::PhantomData<(A, DID)>); + + const FIELDS: &'static [&'static str] = &[ + "iss", "sub", "aud", "cmd", "args", "prf", "nonce", "cause", "meta", "iat", "exp", + ]; + + impl<'de, T: ParseAbility + Deserialize<'de>, DID: Did + Deserialize<'de>> Visitor<'de> + for InvocationPayloadVisitor + { + type Value = Payload; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct invocation::Payload") + } + + fn visit_map>(self, mut map: M) -> Result { + let mut issuer = None; + let mut subject = None; + let mut audience = None; + let mut command = None; + let mut arguments = None; + let mut proofs = None; + let mut nonce = None; + let mut cause = None; + let mut metadata = None; + let mut issued_at = None; + let mut expiration = None; + + while let Some(key) = map.next_key()? { + match key { + "iss" => { + if issuer.is_some() { + return Err(de::Error::duplicate_field("iss")); + } + issuer = Some(map.next_value()?); + } + "sub" => { + if subject.is_some() { + return Err(de::Error::duplicate_field("sub")); + } + subject = Some(map.next_value()?); + } + "aud" => { + if audience.is_some() { + return Err(de::Error::duplicate_field("aud")); + } + audience = map.next_value()?; + } + "cmd" => { + if command.is_some() { + return Err(de::Error::duplicate_field("cmd")); + } + command = Some(map.next_value()?); + } + "args" => { + if arguments.is_some() { + return Err(de::Error::duplicate_field("args")); + } + arguments = Some(map.next_value()?); + } + "prf" => { + if proofs.is_some() { + return Err(de::Error::duplicate_field("prf")); + } + proofs = Some(map.next_value()?); + } + "nonce" => { + if nonce.is_some() { + return Err(de::Error::duplicate_field("nonce")); + } + nonce = Some(map.next_value()?); + } + "cause" => { + if cause.is_some() { + return Err(de::Error::duplicate_field("cause")); + } + cause = map.next_value()?; + } + "meta" => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("meta")); + } + metadata = Some(map.next_value()?); + } + "issued_at" => { + if issued_at.is_some() { + return Err(de::Error::duplicate_field("iat")); + } + issued_at = map.next_value()?; + } + "exp" => { + if expiration.is_some() { + return Err(de::Error::duplicate_field("exp")); + } + expiration = map.next_value()?; + } + other => { + return Err(de::Error::unknown_field(other, FIELDS)); + } + } + } + + let cmd: String = command.ok_or(de::Error::missing_field("cmd"))?; + let args = arguments.ok_or(de::Error::missing_field("args"))?; + + let ability = ::try_parse(cmd.as_str(), args).map_err(|e| { + de::Error::custom(format!( + "Unable to parse ability field for {:?} becuase {:?}", + cmd, e + )) + })?; + + Ok(Payload { + issuer: issuer.ok_or(de::Error::missing_field("iss"))?, + subject: subject.ok_or(de::Error::missing_field("sub"))?, + proofs: proofs.ok_or(de::Error::missing_field("prf"))?, + metadata: metadata.ok_or(de::Error::missing_field("meta"))?, + nonce: nonce.ok_or(de::Error::missing_field("nonce"))?, + audience, + ability, + cause, + issued_at, + expiration, + }) + } + } + + deserializer.deserialize_struct( + "invocation::Payload", + FIELDS, + InvocationPayloadVisitor(Default::default()), + ) + } +} + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +impl TryFrom> for Payload +where + ::ArgsErr: fmt::Debug, + ::Err: fmt::Debug, +{ + type Error = ParseError; + + fn try_from(named: arguments::Named) -> Result { + let mut subject = None; + let mut issuer = None; + let mut audience = None; + let mut command = None; + let mut args = None; + let mut cause = None; + let mut metadata = None; + let mut nonce = None; + let mut expiration = None; + let mut proofs = None; + let mut issued_at = None; + + for (k, v) in named { + match k.as_str() { + "sub" => { + subject = Some(match v { + Ipld::String(s) => { + DID::from_str(s.as_str()).map_err(ParseError::DidParseError)? + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }) + } + "iss" => match v { + Ipld::String(s) => { + issuer = Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "aud" => match v { + Ipld::String(s) => { + audience = + Some(DID::from_str(s.as_str()).map_err(ParseError::DidParseError)?) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "cmd" => match v { + Ipld::String(s) => command = Some(s), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "args" => match v.try_into() { + Ok(a) => args = Some(a), + _ => return Err(ParseError::ArgsNotAMap), + }, + "meta" => match v { + Ipld::Map(m) => metadata = Some(m), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "nonce" => match v { + Ipld::Bytes(b) => nonce = Some(Nonce::from(b)), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "cause" => match v { + Ipld::Link(c) => cause = Some(c), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "exp" => match v { + Ipld::Integer(i) => expiration = Some(i.try_into()?), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "iat" => match v { + Ipld::Integer(i) => issued_at = Some(i.try_into()?), + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + "prf" => match &v { + Ipld::List(xs) => { + proofs = Some( + xs.iter() + .map(|x| match x { + Ipld::Link(cid) => Ok(*cid), + _ => Err(ParseError::WrongTypeForField(k.clone(), v.clone())), + }) + .collect::, ParseError>>()?, + ) + } + _ => return Err(ParseError::WrongTypeForField(k, v)), + }, + _ => return Err(ParseError::UnknownField(k.to_string())), + } + } + + let cmd = command.ok_or(ParseError::MissingCmd)?; + let some_args = args.ok_or(ParseError::MissingArgs)?; + let ability = ::try_parse(cmd.as_str(), some_args) + .map_err(|e| ParseError::AbilityError(e))?; + + Ok(Payload { + issuer: issuer.ok_or(ParseError::MissingIss)?, + subject: subject.ok_or(ParseError::MissingSub)?, + audience, + ability, + proofs: proofs.ok_or(ParseError::MissingProofsField)?, + cause, + metadata: metadata.unwrap_or_default(), + nonce: nonce.ok_or(ParseError::MissingNonce)?, + issued_at, + expiration, + }) + } +} + +#[derive(Debug, Error)] +pub enum ParseError +where + ::ArgsErr: fmt::Debug, + ::Err: fmt::Debug, +{ + #[error("Unknown field: {0}")] + UnknownField(String), + + #[error("Missing sub field")] + MissingSub, + + #[error("Missing iss field")] + MissingIss, + + #[error("Missing cmd field")] + MissingCmd, + + #[error("Missing args field")] + MissingArgs, + + #[error("Unable to parse ability: {0:?}")] + AbilityError(ParseAbilityError<::ArgsErr>), + + #[error("Missing nonce field")] + MissingNonce, + + #[error("Wrong type for field {0}: {1:?}")] + WrongTypeForField(String, Ipld), + + #[error("Cannot parse DID")] + DidParseError(::Err), + + // FIXME + #[error("Cannot parse timestamp: {0}")] + BadTimestamp(#[from] time::OutOfRangeError), + + #[error("Args are not a map")] + ArgsNotAMap, + + #[error("Misisng proofs field")] + MissingProofsField, +} + +/// A variant that accepts [`Promise`]s. +/// +/// [`Promise`]: crate::invocation::promise::Promise +pub type Promised = Payload<::Promised, DID>; + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + T::Strategy: 'static, + DID::Parameters: Clone, +{ + type Parameters = (T::Parameters, DID::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((t_args, did_args): Self::Parameters) -> Self::Strategy { + ( + T::arbitrary_with(t_args), + DID::arbitrary_with(did_args.clone()), + DID::arbitrary_with(did_args.clone()), + Option::::arbitrary_with((0.5.into(), did_args)), + Nonce::arbitrary(), + prop::collection::vec(cid::Newtype::arbitrary().prop_map(|nt| nt.cid), 0..12), + Option::::arbitrary().prop_map(|opt_nt| opt_nt.map(|nt| nt.cid)), + Option::::arbitrary(), + Option::::arbitrary(), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..12).prop_map(|m| { + m.into_iter() + .map(|(k, v)| (k, v.0)) + .collect::>() + }), + ) + .prop_map( + |( + ability, + issuer, + subject, + audience, + nonce, + proofs, + cause, + expiration, + issued_at, + metadata, + )| { + Payload { + issuer, + subject, + audience, + ability, + proofs, + cause, + nonce, + metadata, + issued_at, + expiration, + } + }, + ) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ability::msg::Msg; + use crate::ipld; + use assert_matches::assert_matches; + use pretty_assertions as pretty; + use proptest::prelude::*; + use testresult::TestResult; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test_log::test] + fn test_ipld_round_trip(payload in Payload::::arbitrary()) { + let observed: Named = payload.clone().into(); + let parsed = Payload::::try_from(observed.clone()); + + prop_assert!(parsed.is_ok()); + prop_assert_eq!(parsed.unwrap(), payload); + } + + #[test_log::test] + fn test_ipld_only_has_correct_fields(payload in Payload::::arbitrary()) { + let observed: Ipld = payload.clone().into(); + + if let Ipld::Map(named) = observed { + prop_assert!(named.len() >= 6); + prop_assert!(named.len() <= 11); + + for key in named.keys() { + prop_assert!(matches!(key.as_str(), "sub" | "iss" | "aud" | "cmd" | "args" | "prf" | "cause" | "meta" | "nonce" | "exp" | "iat")); + } + } else { + prop_assert!(false, "ipld map"); + } + } + + #[test_log::test] + fn test_ipld_field_types(payload in Payload::::arbitrary()) { + let named: Named = payload.clone().into(); + + let sub = named.get("sub".into()); + let iss = named.get("iss".into()); + let cmd = named.get("cmd".into()); + let args = named.get("args".into()); + let prf = named.get("prf".into()); + let nonce = named.get("nonce".into()); + + // Required Fields + prop_assert_eq!(sub.unwrap(), &Ipld::String(payload.subject.to_string())); + prop_assert_eq!(iss.unwrap(), &Ipld::String(payload.issuer.to_string())); + prop_assert_eq!(cmd.unwrap(), &Ipld::String(payload.ability.to_command())); + + prop_assert_eq!(args.unwrap(), &payload.ability.into()); + prop_assert!(matches!(args, Some(Ipld::Map(_)))); + + prop_assert!(matches!(prf.unwrap(), &Ipld::List(_))); + if let Some(Ipld::List(ipld_proofs)) = prf { + prop_assert_eq!(ipld_proofs.len(), payload.proofs.len()); + + for entry in ipld_proofs { + prop_assert!(matches!(entry, Ipld::Link(_))); + } + } else { + prop_assert!(false); + } + + prop_assert_eq!(nonce.unwrap(), &payload.nonce.into()); + + // Optional Fields + prop_assert_eq!(payload.audience.map(|did| did.into()), named.get("aud").cloned()); + prop_assert_eq!(payload.cause.map(Ipld::Link), named.get("cause").cloned()); + + match (payload.metadata.is_empty(), named.get("meta")) { + (false, Some(Ipld::Map(btree))) => { + prop_assert_eq!(&payload.metadata, btree); + } + (true, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.expiration, named.get("exp")) { + (Some(exp), Some(Ipld::Integer(i))) => { + prop_assert_eq!(i128::from(exp), i.clone()); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + + match (payload.issued_at, named.get("iat")) { + (Some(iat), Some(Ipld::Integer(i))) => { + prop_assert_eq!(i128::from(iat), i.clone()); + } + (None, None) => prop_assert!(true), + _ => prop_assert!(false) + } + } + + #[test_log::test] + fn test_non_payload(named in arguments::Named::::arbitrary()) { + // Just ensuring that a negative test shows up + let parsed = Payload::::try_from(named); + prop_assert!(parsed.is_err()) + } + } +} diff --git a/src/invocation/promise.rs b/src/invocation/promise.rs new file mode 100644 index 00000000..baacb12b --- /dev/null +++ b/src/invocation/promise.rs @@ -0,0 +1,43 @@ +//! [UCAN Promise](https://github.com/ucan-wg/promise)s: selectors, wrappers, and traits. + +mod any; +mod err; +mod ok; +mod pending; +mod resolvable; + +pub mod store; +// FIXME pub mod js; + +pub use any::Any; +pub use err::PromiseErr; +pub use ok::PromiseOk; +pub use pending::Pending; +pub use resolvable::*; +pub use store::Store; + +use enum_as_inner::EnumAsInner; +use libipld_core::cid::Cid; +use serde::{Deserialize, Serialize}; + +/// Top-level union of all UCAN Promise options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Promise { + /// The `ucan/await/ok` promise + Ok(T), + + /// The `ucan/await/err` promise + Err(E), + + /// The `ucan/await/ok` promise + PendingOk(Cid), + + /// The `ucan/await/err` promise + PendingErr(Cid), + + /// The `ucan/await/*` promise + PendingAny(Cid), + + /// The `ucan/await` promise + PendingTagged(Cid), +} diff --git a/src/invocation/promise/any.rs b/src/invocation/promise/any.rs new file mode 100644 index 00000000..657c1366 --- /dev/null +++ b/src/invocation/promise/any.rs @@ -0,0 +1,111 @@ +use crate::ipld; +use super::pending::Pending; +use enum_as_inner::EnumAsInner; +use libipld_core::cid::Cid; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// FIXME +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Any { + /// The `ucan/await/ok` promise + Resolved(T), + + /// The `ucan/await/ok` promise + PendingOk(Cid), + + /// The `ucan/await/err` promise + PendingErr(Cid), + + /// The `ucan/await/*` promise + PendingAny(Cid), +} + +impl Any { + pub fn try_resolve(self) -> Result> { + match self { + Any::Resolved(value) => Ok(value), + _ => Err(self), + } + } + + pub fn from_ipld(ipld: Ipld) -> Self + where + T: From, + { + match ipld { + Ipld::Map(ref map) => { + if let Some(Ipld::Link(cid)) = map.get("ucan/await/ok") { + return Any::PendingOk(cid.clone()); + } + + if let Some(Ipld::Link(cid)) = map.get("ucan/await/err") { + return Any::PendingErr(cid.clone()); + } + + if let Some(Ipld::Link(cid)) = map.get("ucan/await/*") { + return Any::PendingAny(cid.clone()); + } + + Any::Resolved(ipld.into()) + } + other => Any::Resolved(other.into()), + } + } + + pub fn to_promised_ipld(self) -> ipld::Promised + where + T: Into, + { + match self { + Any::Resolved(value) => value.into(), + Any::PendingOk(cid) => ipld::Promised::WaitOk(cid), + Any::PendingErr(cid) => ipld::Promised::WaitErr(cid), + Any::PendingAny(cid) => ipld::Promised::WaitAny(cid), + } + } +} + +impl From for Any { + fn from(pending: Pending) -> Any { + match pending { + Pending::Ok(cid) => Any::PendingOk(cid), + Pending::Err(cid) => Any::PendingErr(cid), + Pending::Any(cid) => Any::PendingAny(cid), + } + } +} + +impl> From> for Ipld { + fn from(promise: Any) -> Ipld { + match promise { + Any::Resolved(val) => val.into(), + Any::PendingOk(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/ok".to_string(), + cid.into(), + )])), + Any::PendingErr(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/err".to_string(), + cid.into(), + )])), + Any::PendingAny(cid) => Ipld::Map(BTreeMap::from_iter([( + "ucan/await/*".to_string(), + cid.into(), + )])), + } + } +} + +impl> TryFrom for Any { + type Error = >::Error; + + fn try_from(promised: ipld::Promised) -> Result, Self::Error> { + match promised { + ipld::Promised::WaitOk(cid) => Ok(Any::PendingOk(cid)), + ipld::Promised::WaitErr(cid) => Ok(Any::PendingErr(cid)), + ipld::Promised::WaitAny(cid) => Ok(Any::PendingAny(cid)), + other => Ok(Any::Resolved(T::try_from(other)?)), + } + } +} diff --git a/src/invocation/promise/err.rs b/src/invocation/promise/err.rs new file mode 100644 index 00000000..01c47403 --- /dev/null +++ b/src/invocation/promise/err.rs @@ -0,0 +1,113 @@ +use crate::{ability::arguments, ipld::cid}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A promise that only selects the `{"err": error}` branch of a result. +/// +/// On resolution, the value is unwrapped from the `{"err": error}`, +/// leaving just the `error` (much like [`Result::unwrap_err`]). +/// +/// FIXME exmaple +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PromiseErr { + /// The failure state of a promise. + Rejected(E), + + /// The [`Cid`] that is being waited on to return an `{"err": value}` + Pending(#[serde(rename = "await/err")] Cid), +} + +impl PromiseErr { + pub fn try_resolve(self) -> Result> { + match self { + PromiseErr::Rejected(err) => Ok(err), + PromiseErr::Pending(_cid) => Err(self), + } + } + + pub fn map(self, f: F) -> PromiseErr + where + F: FnOnce(E) -> X, + { + match self { + PromiseErr::Rejected(err) => PromiseErr::Rejected(f(err)), + PromiseErr::Pending(cid) => PromiseErr::Pending(cid), + } + } +} + +impl From> for Option { + fn from(p: PromiseErr) -> Option { + match p { + PromiseErr::Rejected(err) => Some(err), + PromiseErr::Pending(_) => None, + } + } +} + +impl From> for Ipld { + fn from(p: PromiseErr) -> Ipld { + p.into() + } +} + +impl TryFrom for PromiseErr { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result, Self::Error> { + ipld_serde::from_ipld(ipld) + } +} + +impl>> From> for arguments::Named +where + Ipld: From, +{ + fn from(p: PromiseErr) -> arguments::Named { + match p { + PromiseErr::Rejected(err) => err.into(), + PromiseErr::Pending(cid) => { + arguments::Named::from_iter([("await/err".into(), Ipld::Link(cid))]) + } + } + } +} + +impl> TryFrom> for PromiseErr { + type Error = >::Error; + + fn try_from(args: arguments::Named) -> Result, Self::Error> { + if let Some(ipld) = args.get("ucan/err") { + if args.len() == 1 { + if let Ok(cid::Newtype { cid }) = cid::Newtype::try_from(ipld) { + return Ok(PromiseErr::Pending(cid)); + } + } + } + + E::try_from(Ipld::from(args)).map(PromiseErr::Rejected) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for PromiseErr +where + T::Strategy: 'static, + T::Parameters: 'static, +{ + type Parameters = T::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(t_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + T::arbitrary_with(t_args).prop_map(PromiseErr::Rejected), + cid::Newtype::arbitrary().prop_map(|nt| PromiseErr::Pending(nt.cid)), + ] + .boxed() + } +} diff --git a/src/invocation/promise/js.rs b/src/invocation/promise/js.rs new file mode 100644 index 00000000..9f8b9069 --- /dev/null +++ b/src/invocation/promise/js.rs @@ -0,0 +1,123 @@ +use crate::ability::arguments; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde_derive::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt::Debug}; + +// FIXME +// #[cfg(target_arch = "wasm32")] +// use wasm_bindgen::prelude::*; +// +// #[cfg(target_arch = "wasm32")] +// #[derive(Clone, Debug, PartialEq, Eq)] +// #[wasm_bindgen] +// pub enum UcanPromiseStatus { +// Fulfilled, +// Pending, +// } +// +// // FIXME no way to make this consistent, because of C enums ruining Rust convetions, right? +// // FIXME consider wrapping in a trait +// #[cfg(target_arch = "wasm32")] +// #[derive(Clone, Debug, PartialEq)] +// #[wasm_bindgen] +// pub struct UcanPromise { +// status: UcanPromiseStatus, +// selector: Option, +// value: Option, +// } +// +// #[cfg(target_arch = "wasm32")] +// #[wasm_bindgen(getter_with_clone)] +// pub struct IncoherentPromise(pub UcanPromise); +// +// #[cfg(target_arch = "wasm32")] +// impl TryFrom for Promise { +// type Error = IncoherentPromise; +// +// fn try_from(js: UcanPromise) -> Result { +// match js.status { +// UcanPromiseStatus::Fulfilled => { +// if let Some(val) = &js.value { +// return Ok(Promise::Fulfilled(val.clone())); +// } +// } +// UcanPromiseStatus::Pending => { +// if let Some(selector) = &js.selector { +// return Ok(Promise::Pending(selector.clone())); +// } +// } +// } +// +// Err(IncoherentPromise(js)) +// } +// } +// +// #[cfg(target_arch = "wasm32")] +// impl> From> for UcanPromise { +// fn from(promise: Promise) -> Self { +// match promise { +// Promise::Fulfilled(val) => UcanPromise { +// status: UcanPromiseStatus::Fulfilled, +// selector: None, +// value: Some(val.into()), +// }, +// Promise::Pending(cid) => UcanPromise { +// status: UcanPromiseStatus::Pending, +// selector: Some(cid), +// value: None, +// }, +// } +// } +// } +// +// /// A [`Promise`] is a way to defer the presence of a value to the result of some [`Invocation`]. +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// #[serde(untagged, deny_unknown_fields)] // FIXME check that this is right, also +// pub enum Selector { +// Any { +// #[serde(rename = "ucan/*")] // FIXME test to make sure that this is right? +// any: Cid, +// }, +// Ok { +// #[serde(rename = "await/ok")] +// ok: Cid, +// }, +// Err { +// #[serde(rename = "await/err")] +// err: Cid, +// }, +// } +// +// impl From for Ipld { +// fn from(selector: Selector) -> Self { +// selector.into() +// } +// } +// +// impl TryFrom for Selector { +// type Error = (); +// +// fn try_from(ipld: Ipld) -> Result { +// ipld_serde::from_ipld(ipld).map_err(|_| ()) +// } +// } +// +// impl From for arguments::Named { +// fn from(selector: Selector) -> Self { +// let mut btree = BTreeMap::new(); +// +// match selector { +// Selector::Any { any } => { +// btree.insert("ucan/*".into(), any.into()); +// } +// Selector::Ok { ok } => { +// btree.insert("await/ok".into(), ok.into()); +// } +// Selector::Err { err } => { +// btree.insert("await/err".into(), err.into()); +// } +// } +// +// arguments::Named(btree) +// } +// } diff --git a/src/invocation/promise/ok.rs b/src/invocation/promise/ok.rs new file mode 100644 index 00000000..4cc406a9 --- /dev/null +++ b/src/invocation/promise/ok.rs @@ -0,0 +1,114 @@ +use crate::{ability::arguments, ipld::cid}; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A promise that only selects the `{"ok": value}` branch of a result. +/// +/// On resolution, the value is unwrapped from the `{"ok": value}`, +/// leaving just the `value` (much like [`Result::unwrap`]). +/// +/// FIXME exmaple +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub enum PromiseOk { + /// The fulfilled (resolved) value. + Fulfilled(T), + + /// The [`Cid`] that is being waited on to return an `{"ok": value}` + Pending(#[serde(rename = "await/ok")] Cid), +} + +// FIXME move try_resolve to a trait, give a blanket impl for prims, and tag them +impl PromiseOk { + pub fn try_resolve(self) -> Result> { + match self { + PromiseOk::Fulfilled(value) => Ok(value), + PromiseOk::Pending(_cid) => Err(self), + } + } + + pub fn map(self, f: F) -> PromiseOk + where + F: FnOnce(T) -> U, + { + match self { + PromiseOk::Fulfilled(val) => PromiseOk::Fulfilled(f(val)), + PromiseOk::Pending(cid) => PromiseOk::Pending(cid), + } + } +} + +impl From> for Option { + fn from(p: PromiseOk) -> Option { + match p { + PromiseOk::Fulfilled(value) => Some(value), + PromiseOk::Pending(_) => None, + } + } +} + +impl From> for Ipld { + fn from(p: PromiseOk) -> Ipld { + p.into() + } +} + +impl TryFrom for PromiseOk { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result, Self::Error> { + ipld_serde::from_ipld(ipld) + } +} + +impl>> From> for arguments::Named +where + Ipld: From, +{ + fn from(p: PromiseOk) -> arguments::Named { + match p { + PromiseOk::Fulfilled(val) => val.into(), + PromiseOk::Pending(cid) => { + arguments::Named::from_iter([("await/ok".into(), Ipld::Link(cid))]) + } + } + } +} + +impl> TryFrom> for PromiseOk { + type Error = >::Error; + + fn try_from(args: arguments::Named) -> Result, Self::Error> { + if let Some(ipld) = args.get("ucan/ok") { + if args.len() == 1 { + if let Ok(cid::Newtype { cid }) = cid::Newtype::try_from(ipld) { + return Ok(PromiseOk::Pending(cid)); + } + } + } + + T::try_from(Ipld::from(args)).map(PromiseOk::Fulfilled) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for PromiseOk +where + T::Strategy: 'static, + T::Parameters: 'static, +{ + type Parameters = T::Parameters; + type Strategy = BoxedStrategy; + + fn arbitrary_with(t_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + T::arbitrary_with(t_args).prop_map(PromiseOk::Fulfilled), + cid::Newtype::arbitrary().prop_map(|nt| PromiseOk::Pending(nt.cid)), + ] + .boxed() + } +} diff --git a/src/invocation/promise/pending.rs b/src/invocation/promise/pending.rs new file mode 100644 index 00000000..1c56e48c --- /dev/null +++ b/src/invocation/promise/pending.rs @@ -0,0 +1,9 @@ +use libipld_core::cid::Cid; + +// AKA Selector +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Pending { + Ok(Cid), + Err(Cid), + Any(Cid), +} diff --git a/src/invocation/promise/resolvable.rs b/src/invocation/promise/resolvable.rs new file mode 100644 index 00000000..c65c70da --- /dev/null +++ b/src/invocation/promise/resolvable.rs @@ -0,0 +1,92 @@ +use crate::{ + ability::{ + arguments, + command::ToCommand, + parse::{ParseAbility, ParsePromised}, + }, + invocation::promise::Pending, + ipld, +}; +use libipld_core::{cid::Cid, ipld::Ipld}; +use std::{collections::BTreeSet, fmt}; +use thiserror::Error; + +/// A trait for [`Delegable`]s that can be deferred (by promises). +/// +/// FIXME exmaples +pub trait Resolvable: Sized + ParseAbility + ToCommand { + /// The promise type that resolves to `Self`. + /// + /// Note that this may be a more complex type than the promise selector + /// variants. One example is [letting any leaf][PromiseIpld] of an [`Ipld`] graph + /// be a promise. + /// + /// [PromiseIpld]: crate::ipld::Promised + type Promised: ToCommand + + ParsePromised // TryFrom> + + Into>; + + /// Attempt to resolve the [`Self::Promised`]. + fn try_resolve(promised: Self::Promised) -> Result> + where + Self::Promised: Clone, + { + let ipld_promise: arguments::Named = promised.clone().into(); + match arguments::Named::::try_from(ipld_promise) { + Err(pending) => Err(CantResolve { + promised, + reason: ResolveError::StillWaiting(pending), + }), + Ok(named) => { + ParseAbility::try_parse(&promised.to_command(), named).map_err(|_reason| { + CantResolve { + promised, + reason: ResolveError::ConversionError, + } + }) + } + } + } + + fn get_all_pending(promised: Self::Promised) -> BTreeSet { + let promise_map: arguments::Named = promised.into(); + + promise_map + .values() + .fold(BTreeSet::new(), |mut set, promised| { + if let ipld::Promised::Link(cid) = promised { + set.insert(*cid); + } + + set + }) + } +} + +#[derive(Error, Clone)] +pub struct CantResolve { + pub promised: S::Promised, + pub reason: ResolveError, +} + +impl fmt::Debug for CantResolve +where + S::Promised: fmt::Debug, + Pending: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CantResolve") + .field("promised", &self.promised) + .field("reason", &self.reason) + .finish() + } +} + +#[derive(Error, PartialEq, Eq, Clone, Debug)] +pub enum ResolveError { + #[error("The promise is still has arguments waiting to be resolved")] + StillWaiting(Pending), + + #[error("The resolved promise was unable to reify an ability from IPLD")] + ConversionError, +} diff --git a/src/invocation/promise/store.rs b/src/invocation/promise/store.rs new file mode 100644 index 00000000..2cd345c2 --- /dev/null +++ b/src/invocation/promise/store.rs @@ -0,0 +1,7 @@ +//! Storage of resolved and unresolved promises. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::Store; diff --git a/src/invocation/promise/store/memory.rs b/src/invocation/promise/store/memory.rs new file mode 100644 index 00000000..6f587f7a --- /dev/null +++ b/src/invocation/promise/store/memory.rs @@ -0,0 +1,49 @@ +use super::Store; +use crate::{did::Did, invocation::promise::Resolvable}; +use libipld_core::cid::Cid; +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::Infallible, +}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct MemoryStore { + pub index: BTreeMap>, +} + +impl Store for MemoryStore { + type PromiseStoreError = Infallible; + + fn put_waiting( + &mut self, + invocation: Cid, + waiting_on: Vec, + ) -> Result<(), Self::PromiseStoreError> { + self.index + .insert(invocation, BTreeSet::from_iter(waiting_on)); + + Ok(()) + } + + fn get_waiting( + &self, + waiting_on: &mut Vec, + ) -> Result, Self::PromiseStoreError> { + Ok(match waiting_on.pop() { + None => BTreeSet::new(), + Some(first) => waiting_on + .iter() + .try_fold(BTreeSet::from_iter([first]), |acc, cid| { + let next = self.index.get(cid).ok_or(())?; + + let reduced: BTreeSet = acc.intersection(&next).cloned().collect(); + if reduced.is_empty() { + return Err(()); + } + + Ok(reduced) + }) + .unwrap_or_default(), + }) + } +} diff --git a/src/invocation/promise/store/traits.rs b/src/invocation/promise/store/traits.rs new file mode 100644 index 00000000..0e894c84 --- /dev/null +++ b/src/invocation/promise/store/traits.rs @@ -0,0 +1,18 @@ +use crate::{did::Did, invocation::promise::Resolvable}; +use libipld_core::cid::Cid; +use std::collections::BTreeSet; + +pub trait Store { + type PromiseStoreError; + + fn put_waiting( + &mut self, + invocation: Cid, + waiting_on: Vec, + ) -> Result<(), Self::PromiseStoreError>; + + fn get_waiting( + &self, + waiting_on: &mut Vec, + ) -> Result, Self::PromiseStoreError>; +} diff --git a/src/invocation/store.rs b/src/invocation/store.rs new file mode 100644 index 00000000..224e0c77 --- /dev/null +++ b/src/invocation/store.rs @@ -0,0 +1,7 @@ +//! Storage for [`Invocation`]s. + +mod memory; +mod traits; + +pub use memory::{MemoryStore, MemoryStoreInner}; +pub use traits::Store; diff --git a/src/invocation/store/memory.rs b/src/invocation/store/memory.rs new file mode 100644 index 00000000..2ae9a360 --- /dev/null +++ b/src/invocation/store/memory.rs @@ -0,0 +1,83 @@ +use crate::{crypto::varsig, did::Did, invocation::Invocation}; +use super::Store; +use libipld_core::{cid::Cid, codec::Codec}; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::{collections::BTreeMap, convert::Infallible}; + +#[derive(Debug, Clone)] +pub struct MemoryStore< + T = crate::ability::preset::Preset, + DID: crate::did::Did = crate::did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + inner: Arc>>, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MemoryStoreInner< + T = crate::ability::preset::Preset, + DID: crate::did::Did = crate::did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + TryFrom + Into = varsig::encoding::Preset, +> { + store: BTreeMap>>, +} + +impl, Enc: Codec + Into + TryFrom> + MemoryStore +{ + fn read(&self) -> RwLockReadGuard<'_, MemoryStoreInner> { + match self.inner.read() { + Ok(guard) => guard, + Err(poison) => { + // There's no logic errors through lock poisoning in our case + poison.into_inner() + } + } + } + + fn write(&self) -> RwLockWriteGuard<'_, MemoryStoreInner> { + match self.inner.write() { + Ok(guard) => guard, + Err(poison) => { + // There's no logic errors through lock poisoning in our case + poison.into_inner() + } + } + } +} + +impl, Enc: Codec + Into + TryFrom> Default + for MemoryStore +{ + fn default() -> Self { + Self { + inner: Arc::new(RwLock::new(MemoryStoreInner { + store: BTreeMap::new(), + })), + } + } +} + +impl, Enc: Codec + Into + TryFrom> + Store for MemoryStore +{ + type InvocationStoreError = Infallible; + + fn get( + &self, + cid: Cid, + ) -> Result>>, Self::InvocationStoreError> { + Ok(self.read().store.get(&cid).cloned()) + } + + fn put( + &self, + cid: Cid, + invocation: Invocation, + ) -> Result<(), Self::InvocationStoreError> { + self.write().store.insert(cid, Arc::new(invocation)); + Ok(()) + } +} diff --git a/src/invocation/store/traits.rs b/src/invocation/store/traits.rs new file mode 100644 index 00000000..6d3fc723 --- /dev/null +++ b/src/invocation/store/traits.rs @@ -0,0 +1,51 @@ +use crate::{crypto::varsig, did::Did, invocation::Invocation}; +use libipld_core::{cid::Cid, codec::Codec}; +use std::sync::Arc; + +pub trait Store, C: Codec + Into + TryFrom> { + type InvocationStoreError; + + fn get( + &self, + cid: Cid, + ) -> Result>>, Self::InvocationStoreError>; + + fn put( + &self, + cid: Cid, + invocation: Invocation, + ) -> Result<(), Self::InvocationStoreError>; + + fn has(&self, cid: Cid) -> Result { + Ok(self.get(cid).is_ok()) + } +} + +impl< + S: Store, + T, + DID: Did, + V: varsig::Header, + C: Codec + Into + TryFrom, + > Store for &S +{ + type InvocationStoreError = >::InvocationStoreError; + + fn get( + &self, + cid: Cid, + ) -> Result< + Option>>, + >::InvocationStoreError, + > { + (**self).get(cid) + } + + fn put( + &self, + cid: Cid, + invocation: Invocation, + ) -> Result<(), >::InvocationStoreError> { + (**self).put(cid, invocation) + } +} diff --git a/src/ipld.rs b/src/ipld.rs new file mode 100644 index 00000000..61b2e2bb --- /dev/null +++ b/src/ipld.rs @@ -0,0 +1,19 @@ +//! Helpers for working with [`Ipld`][libipld_core::ipld::Ipld]. +//! +//! [`Ipld`] is a fully concrete data type, and only has a few trait implementations. +//! This module provides a few newtype wrappers that allow you to add trait implementations, +//! and generalized forms to embed non-IPLD into IPLD structure. +//! +//! [`Ipld`]: libipld_core::ipld::Ipld + +mod collection; +mod number; +mod promised; + +pub mod cid; +pub mod newtype; + +pub use collection::Collection; +pub use newtype::Newtype; +pub use number::Number; +pub use promised::*; diff --git a/src/ipld/cid.rs b/src/ipld/cid.rs new file mode 100644 index 00000000..5715b5c8 --- /dev/null +++ b/src/ipld/cid.rs @@ -0,0 +1,141 @@ +//! Utilities for [`Cid`]s + +use crate::ipld; +use libipld_core::{cid::Cid, ipld::Ipld, multihash::MultihashGeneric}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_derive::TryFromJsValue; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::test_utils::SomeCodec; + +#[cfg(feature = "test_utils")] +use crate::test_utils::SomeMultihash; + +/// A newtype wrapper around a [`Cid`] +/// +/// This is largely to attach traits to [`Cid`]s, such as [`wasm_bindgen`] conversions. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct Newtype { + pub cid: Cid, +} + +/// A newtype wrapper around a [`Cid`] +/// +/// This is largely to attach traits to [`Cid`]s, such as [`wasm_bindgen`] conversions. +#[cfg(target_arch = "wasm32")] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct Newtype { + #[wasm_bindgen(skip)] + pub cid: Cid, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + /// This is here because the TryFromJsValue derivation macro + /// doesn't automatically support `Option`. + /// + /// [https://docs.rs/wasm-bindgen-derive/0.2.1/wasm_bindgen_derive/#optional-arguments] + #[wasm_bindgen(typescript_type = "Newtype | undefined")] + pub type OptionNewtype; +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl Newtype { + /// Parse a [`Newtype`] from a string + pub fn from_string(cid_string: String) -> Result { + Newtype::try_from(cid_string).map_err(|e| JsError::new(&format!("{}", e))) + } + + pub fn try_from_js_value(js: &JsValue) -> Result { + match &js.as_string() { + Some(s) => Newtype::from_string(s.clone()), + None => Err(JsError::new("Expected a string")), + } + } +} + +impl Newtype { + /// Convert the [`Cid`] to a string + pub fn to_string(&self) -> String { + self.cid.to_string() + } +} + +impl TryFrom for Newtype { + type Error = >::Error; + + fn try_from(cid_string: String) -> Result { + Cid::try_from(cid_string).map(Into::into) + } +} + +impl From for Cid { + fn from(wrapper: Newtype) -> Self { + wrapper.cid + } +} + +impl From for Newtype { + fn from(cid: Cid) -> Self { + Self { cid } + } +} + +impl TryFrom for Newtype { + type Error = NotACid; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Link(cid) => Ok(Newtype { cid }), + other => Err(NotACid(other.into())), + } + } +} + +impl TryFrom<&Ipld> for Newtype { + type Error = NotACid; + + fn try_from(ipld: &Ipld) -> Result { + match ipld { + Ipld::Link(cid) => Ok(Newtype { cid: *cid }), + other => Err(NotACid(other.clone().into())), + } + } +} + +// #[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, PartialEq, Clone, Error, Serialize, Deserialize)] +#[error("Not a CID: {0:?}")] +pub struct NotACid(pub ipld::Newtype); + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // Very much faking it + any::<([u8; 32], SomeMultihash, SomeCodec)>() + .prop_map(|(hash_bytes, hasher, codec)| { + let multihash = MultihashGeneric::wrap(hasher.0.into(), &hash_bytes.as_slice()) + .expect("Sha2_256 should always successfully encode a hash"); + + let cid = Cid::new_v1(codec.0.into(), multihash); + Newtype { cid } + }) + .boxed() + } +} diff --git a/src/ipld/collection.rs b/src/ipld/collection.rs new file mode 100644 index 00000000..fb7bc4b0 --- /dev/null +++ b/src/ipld/collection.rs @@ -0,0 +1,50 @@ +use crate::ipld; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Collection { + Array(Vec), + Map(BTreeMap), +} + +impl From for Ipld { + fn from(collection: Collection) -> Self { + match collection { + Collection::Array(xs) => Ipld::List(xs.into_iter().map(Into::into).collect()), + Collection::Map(xs) => Ipld::Map( + xs.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ), + } + } +} + +impl Collection { + pub fn to_vec(self) -> Vec { + match self { + Collection::Array(xs) => xs, + Collection::Map(xs) => xs.into_values().collect(), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Collection { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + prop::collection::vec(ipld::Newtype::arbitrary(), 0..10).prop_map(Collection::Array), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..10) + .prop_map(Collection::Map), + ] + .boxed() + } +} diff --git a/src/ipld/enriched.rs b/src/ipld/enriched.rs new file mode 100644 index 00000000..bea703fc --- /dev/null +++ b/src/ipld/enriched.rs @@ -0,0 +1,201 @@ +//! A generalized version of [`Ipld`][libipld_core::ipld::Ipld] +//! that can contain non-IPLD leaves. + +use enum_as_inner::EnumAsInner; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A generalized version of [`Ipld`][libipld_core::ipld::Ipld] +/// that can contain non-IPLD leaves. +/// +/// This is helpful especially when building (mutually) recursive +/// data strutcures that are reducable to [`Ipld`], such as +/// [`ipld::Promised`][crate::ipld::Promised]. +#[derive(Clone, Debug, PartialEq, EnumAsInner, Serialize, Deserialize)] +pub enum Enriched { + /// Lifted [`Ipld::Null`] + Null, + + /// Lifted [`Ipld::Bool`] + Bool(bool), + + /// Lifted [`Ipld::Integer`] + Integer(i128), + + /// Lifted [`Ipld::Float`] + Float(f64), + + /// Lifted [`Ipld::String`] + String(String), + + /// Lifted [`Ipld::Bytes`] (byte array) + Bytes(Vec), + + /// Lifted [`Ipld::Link`] + Link(Cid), + + /// [`Ipld::List`], but where the values are the provided [`T`]. + List(Vec), + + /// [`Ipld::Map`], but where the values are the provided [`T`]. + Map(BTreeMap), +} + +impl<'a, T: Clone> IntoIterator for &'a Enriched { + type Item = Item<'a, T>; + type IntoIter = PostOrderIpldIter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + PostOrderIpldIter::new(&self) + } +} + +impl<'a, T: Clone> FromIterator> for Enriched { + fn from_iter>>(it: I) -> Self { + it.into_iter().fold(Enriched::Null, |acc, x| match x { + Item::Node(Enriched::Null) => Enriched::Null, + Item::Node(Enriched::Bool(b)) => Enriched::Bool(*b), + Item::Node(Enriched::Integer(i)) => Enriched::Integer(*i), + Item::Node(Enriched::Float(f)) => Enriched::Float(*f), + Item::Node(Enriched::String(s)) => Enriched::String(s.clone()), + Item::Node(Enriched::Bytes(b)) => Enriched::Bytes(b.clone()), + Item::Node(Enriched::Link(c)) => Enriched::Link(c.clone()), + Item::Node(Enriched::List(vec)) => { + let mut list = vec![]; + for item in vec { + list.push(item); + } + Enriched::List(list.iter().map(|a| (*a).clone()).collect()) + } + Item::Node(Enriched::Map(btree)) => { + let mut map = BTreeMap::new(); + for (k, v) in btree { + map.insert(k.clone(), (*v).clone()); + } + Enriched::Map(map) + } + Item::Inner(_) => acc, + }) + } +} + +impl<'a, T: Clone> From<&'a Enriched> for PostOrderIpldIter<'a, T> { + fn from(enriched: &'a Enriched) -> Self { + PostOrderIpldIter::new(enriched) + } +} +impl> From for Enriched { + fn from(ipld: Ipld) -> Self { + match ipld { + Ipld::Null => Enriched::Null, + Ipld::Bool(b) => Enriched::Bool(b), + Ipld::Integer(i) => Enriched::Integer(i), + Ipld::Float(f) => Enriched::Float(f), + Ipld::String(s) => Enriched::String(s), + Ipld::Bytes(b) => Enriched::Bytes(b), + Ipld::List(l) => Enriched::List(l.into_iter().map(From::from).collect()), + Ipld::Map(m) => Enriched::Map(m.into_iter().map(|(k, v)| (k, From::from(v))).collect()), + Ipld::Link(c) => Enriched::Link(c), + } + } +} + +impl> TryFrom> for Ipld { + type Error = Enriched; + + fn try_from(enriched: Enriched) -> Result { + match enriched { + Enriched::List(ref vec) => { + let result: Result, ()> = vec.iter().try_fold(vec![], |mut acc, x| { + let resolved = x.clone().try_into().map_err(|_| ())?; + acc.push(resolved); + Ok(acc) + }); + + match result { + Ok(vec) => Ok(vec.into()), + Err(()) => Err(enriched), + } + } + Enriched::Map(ref btree) => { + let result: Result, ()> = + btree.iter().try_fold(BTreeMap::new(), |mut acc, (k, v)| { + let resolved = v.clone().try_into().map_err(|_| ())?; + acc.insert(k.clone(), resolved); + Ok(acc) + }); + + match result { + Ok(vec) => Ok(vec.into()), + Err(()) => Err(enriched), + } + } + Enriched::Null => Ok(Ipld::Null), + Enriched::Bool(b) => Ok(b.into()), + Enriched::Integer(i) => Ok(i.into()), + Enriched::Float(f) => Ok(f.into()), + Enriched::String(s) => Ok(s.into()), + Enriched::Bytes(b) => Ok(b.into()), + Enriched::Link(l) => Ok(l.into()), + } + } +} + +/*************************** +| POST ORDER IPLD ITERATOR | +***************************/ + +/// A post-order [`Ipld`] iterator +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde-codec", derive(serde::Serialize))] +#[allow(clippy::module_name_repetitions)] +pub struct PostOrderIpldIter<'a, T> { + inbound: Vec>, + outbound: Vec>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Item<'a, T> { + Node(&'a Enriched), + Inner(&'a T), +} + +impl<'a, T> PostOrderIpldIter<'a, T> { + /// Initialize a new [`PostOrderIpldIter`] + #[must_use] + pub fn new(enriched: &'a Enriched) -> Self { + PostOrderIpldIter { + inbound: vec![Item::Node(enriched)], + outbound: vec![], + } + } +} + +impl<'a, T: Clone> Iterator for PostOrderIpldIter<'a, T> { + type Item = Item<'a, T>; + + fn next(&mut self) -> Option { + loop { + match self.inbound.pop() { + None => return self.outbound.pop(), + Some(ref map @ Item::Node(Enriched::Map(ref btree))) => { + self.outbound.push(map.clone()); + + for node in btree.values() { + self.inbound.push(Item::Inner(node)); + } + } + + Some(ref list @ Item::Node(Enriched::List(ref vector))) => { + self.outbound.push(list.clone()); + + for node in vector { + self.inbound.push(Item::Inner(node)); + } + } + Some(node) => self.outbound.push(node), + } + } + } +} diff --git a/src/ipld/newtype.rs b/src/ipld/newtype.rs new file mode 100644 index 00000000..31451de1 --- /dev/null +++ b/src/ipld/newtype.rs @@ -0,0 +1,311 @@ +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::path::PathBuf; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use js_sys::{Array, Map, Object, Uint8Array}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use super::cid; + +#[cfg(target_arch = "wasm32")] +use super::cid; + +/// A newtype wrapper around [`Ipld`] that has additional trait implementations. +/// +/// Usage is very simple: wrap a [`Newtype`] to gain access to additional traits and methods. +/// +/// ```rust +/// # use libipld_core::ipld::Ipld; +/// # use ucan::ipld; +/// # +/// let ipld = Ipld::String("hello".into()); +/// let wrapped = ipld::Newtype(ipld.clone()); +/// // wrapped.some_trait_method(); +/// ``` +/// +/// Unwrap a [`Newtype`] to use any interfaces that expect plain [`Ipld`]. +/// +/// ``` +/// # use libipld_core::ipld::Ipld; +/// # use ucan::ipld; +/// # +/// # let ipld = Ipld::String("hello".into()); +/// # let wrapped = ipld::Newtype(ipld.clone()); +/// # +/// assert_eq!(wrapped.0, ipld); +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Newtype(pub Ipld); + +impl From for Newtype { + fn from(ipld: Ipld) -> Self { + Self(ipld) + } +} + +impl From for Newtype { + fn from(i: i128) -> Self { + Newtype(Ipld::Integer(i)) + } +} + +impl From for Newtype { + fn from(f: f64) -> Self { + Newtype(Ipld::Float(f)) + } +} + +impl From<&str> for Newtype { + fn from(s: &str) -> Self { + Newtype(Ipld::String(s.to_string())) + } +} + +impl From for Newtype { + fn from(s: String) -> Self { + Newtype(Ipld::String(s)) + } +} + +impl TryFrom for String { + type Error = (); + + fn try_from(nt: Newtype) -> Result { + match nt.0 { + Ipld::String(s) => Ok(s), + _ => Err(()), + } + } +} + +impl TryFrom for i128 { + type Error = (); + + fn try_from(nt: Newtype) -> Result { + match nt.0 { + Ipld::Integer(i) => Ok(i), + _ => Err(()), + } + } +} + +impl From for Ipld { + fn from(wrapped: Newtype) -> Self { + wrapped.0 + } +} + +impl From for Newtype { + fn from(path: PathBuf) -> Self { + Newtype(Ipld::String(path.to_string_lossy().to_string())) + } +} + +impl TryFrom for PathBuf { + type Error = NotAString; + + fn try_from(wrapped: Newtype) -> Result { + match wrapped.0 { + Ipld::String(s) => Ok(PathBuf::from(s)), + ipld => Err(NotAString(ipld)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Error)] +#[error("Ipld variant is not a string")] +pub struct NotAString(pub Ipld); + +#[cfg(target_arch = "wasm32")] +impl Newtype { + pub fn try_from_js>(js_val: JsValue) -> Result + where + JsError: From<>::Error>, + { + match Newtype::try_from(js_val) { + Err(_err) => Err(JsError::new("can't convert")), + Ok(nt) => nt.0.try_into().map_err(JsError::from), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl From for JsValue { + fn from(wrapped: Newtype) -> Self { + match wrapped.0 { + Ipld::Null => JsValue::NULL, + Ipld::Bool(b) => JsValue::from(b), + Ipld::Integer(i) => JsValue::from(i), + Ipld::Float(f) => JsValue::from_f64(f), + Ipld::String(s) => JsValue::from_str(&s), + Ipld::Bytes(bs) => { + let u8arr = Uint8Array::new(&bs.len().into()); + for (i, b) in bs.iter().enumerate() { + u8arr.set_index(i as u32, *b); + } + JsValue::from(u8arr) + } + Ipld::List(ls) => { + let arr = Array::new(); + for ipld in ls { + arr.push(&JsValue::from(Newtype(ipld))); + } + JsValue::from(arr) + } + Ipld::Map(m) => { + let map = Map::new(); + for (k, v) in m { + map.set(&JsValue::from(k), &JsValue::from(Newtype(v))); + } + JsValue::from(map) + } + Ipld::Link(cid) => cid::Newtype::from(cid).into(), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl TryFrom for Newtype { + type Error = (); // FIXME + + fn try_from(js_val: JsValue) -> Result { + if js_val.is_null() { + return Ok(Newtype(Ipld::Null)); + } + + if let Some(b) = js_val.as_bool() { + return Ok(Newtype(Ipld::Bool(b))); + } + + if let Some(f) = js_val.as_f64() { + return Ok(Newtype(Ipld::Float(f))); + } + + if let Some(s) = js_val.as_string() { + return Ok(Newtype(Ipld::String(s))); + } + + if let Some(arr) = js_val.dyn_ref::() { + let mut list = vec![]; + for x in arr.to_vec().iter() { + let ipld = Newtype::try_from(x.clone())?.into(); + list.push(ipld); + } + + return Ok(Newtype(Ipld::List(list))); + } + + if let Some(arr) = js_val.dyn_ref::() { + let mut v = vec![]; + for item in arr.to_vec().iter() { + v.push(item.clone()); + } + + return Ok(Newtype(Ipld::Bytes(v))); + } + + if let Some(map) = js_val.dyn_ref::() { + let mut m = std::collections::BTreeMap::new(); + let mut acc = Ok(()); + + // Weird order, but correct per the docs + // vvvvvvvvvv + map.for_each(&mut |value, key| { + if acc.is_err() { + return; + } + + match (key.as_string(), Newtype::try_from(value.clone())) { + (Some(k), Ok(v)) => { + m.insert(k, v.0); + } + _ => { + acc = Err(()); + } + } + }); + + return acc.map(|_| Newtype(Ipld::Map(m))); + } + + // NOTE *must* come before `is_object` (which is hopefully below) + if let Ok(nt) = cid::Newtype::try_from_js_value(&js_val) { + return Ok(Newtype(Ipld::Link(nt.into()))); + } + + if js_val.is_object() { + let obj = Object::from(js_val); + let mut m = std::collections::BTreeMap::new(); + let mut acc = Ok(()); + + Object::entries(&obj).for_each(&mut |js_val, _, _| { + if acc.is_err() { + return; + } + + // By definition this must be the array [value, key], in that order + let arr = Array::from(&js_val); + + match (arr.get(0).as_string(), Newtype::try_from(arr.get(1))) { + (Some(k), Ok(v)) => { + m.insert(k, v.0); + } + // FIXME more specific errors + _ => { + acc = Err(()); + } + } + }); + + return acc.map(|_| Newtype(Ipld::Map(m))); + } + + // NOTE fails on `undefined` and `function` + + Err(()) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let leaf = prop_oneof![ + Just(Ipld::Null), + any::().prop_map(Ipld::Bool), + any::>().prop_map(Ipld::Bytes), + any::().prop_map(move |i| { + Ipld::Integer((i % (2 ^ 53)).into()) // NOTE Because DAG-JSON + }), + any::().prop_map(Ipld::Float), + ".*".prop_map(Ipld::String), + any::().prop_map(|newtype_cid| { Ipld::Link(newtype_cid.cid) }) + ]; + + let coll = leaf.clone().prop_recursive(16, 1024, 128, |inner| { + prop_oneof![ + prop::collection::vec(inner.clone(), 0..128).prop_map(Ipld::List), + prop::collection::btree_map(".*", inner, 0..128).prop_map(Ipld::Map), + ] + }); + + prop_oneof![ + 1 => leaf, + 9 => coll + ] + .prop_map(Newtype) + .boxed() + } +} diff --git a/src/ipld/number.rs b/src/ipld/number.rs new file mode 100644 index 00000000..e7c3c611 --- /dev/null +++ b/src/ipld/number.rs @@ -0,0 +1,87 @@ +//! Helpers for working with [`Ipld`] numerics. + +use enum_as_inner::EnumAsInner; +use libipld_core::{error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The union of [`Ipld`] numeric types +/// +/// This is helpful when comparing different numeric types, such as +/// bounds checking in [`Predicate`]s. +/// +/// [`Predicate`]: crate::delegation::policy::predicate::Predicate +#[derive(Debug, Clone, PartialEq, EnumAsInner, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Number { + /// Designate a floating point number + Float(f64), + + /// Designate an integer + Integer(i128), +} + +impl PartialOrd for Number { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Number::Float(a), Number::Float(b)) => a.partial_cmp(b), + (Number::Integer(a), Number::Integer(b)) => a.partial_cmp(b), + (Number::Float(a), Number::Integer(b)) => a.partial_cmp(&(*b as f64)), + (Number::Integer(a), Number::Float(b)) => (*a as f64).partial_cmp(b), + } + } +} + +impl From for Ipld { + fn from(number: Number) -> Self { + match number { + Number::Float(f) => Ipld::Float(f), + Number::Integer(i) => Ipld::Integer(i), + } + } +} + +impl TryFrom for Number { + type Error = NotANumber; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::Integer(i) => Ok(Number::Integer(i)), + Ipld::Float(f) => Ok(Number::Float(f)), + _ => Err(NotANumber(ipld)), + } + } +} + +#[derive(Debug, Clone, PartialEq, Error)] +#[error("Expected Ipld numeric, got: {0:?}")] +pub struct NotANumber(Ipld); + +impl From for Number { + fn from(i: i128) -> Number { + Number::Integer(i) + } +} + +impl From for Number { + fn from(f: f64) -> Number { + Number::Float(f) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Number { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + any::().prop_map(Number::Float), + any::().prop_map(Number::Integer), + ] + .boxed() + } +} diff --git a/src/ipld/promised.rs b/src/ipld/promised.rs new file mode 100644 index 00000000..0416fde2 --- /dev/null +++ b/src/ipld/promised.rs @@ -0,0 +1,405 @@ +use crate::{ + ability::arguments, + invocation::promise::{self, Pending, PromiseErr, PromiseOk}, + ipld, url, +}; +use enum_as_inner::EnumAsInner; +use libipld_core::{cid::Cid, ipld::Ipld}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fmt, path::PathBuf}; + +/// A recursive data structure whose leaves may be [`Ipld`] or promises. +/// +/// [`Promised`] resolves to regular [`Ipld`]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner)] +pub enum Promised { + /// Resolved null. + Null, + + /// Resolved Boolean. + Bool(bool), + + /// Resolved integer. + Integer(i128), + + /// Resolved float. + Float(f64), + + /// Resolved string. + String(String), + + /// Resolved bytes. + Bytes(Vec), + + /// Resolved link. + Link(Cid), + + /// Promise pending the `ok` branch. + WaitOk(Cid), + + /// Promise pending the `err` branch. + WaitErr(Cid), + + /// Promise pending either branch. + WaitAny(Cid), + + /// Recursively promised list. + List(Vec), + + /// Recursively promised map. + Map(BTreeMap), +} + +impl Promised { + pub fn try_resolve(self) -> Result { + match self { + Promised::WaitOk(cid) => Err(Pending::Ok(cid)), + Promised::WaitErr(cid) => Err(Pending::Err(cid)), + Promised::WaitAny(cid) => Err(Pending::Any(cid)), + other => other.try_into().map_err(Into::into), + } + } + + pub fn with_resolved(self, f: F) -> Result + where + F: FnOnce(Ipld) -> T, + { + match self.try_into() { + Ok(ipld) => Ok(f(ipld)), + Err(pending) => Err(pending), + } + } + + pub fn with_pending(self, f: F) -> Result + where + F: FnOnce(Pending) -> E, + { + match self.try_into() { + Ok(ipld) => Err(ipld), + Err(promised) => Ok(f(promised)), + } + } + + pub fn to_promise_any>( + self, + ) -> Result, >::Error> { + Ok(match Ipld::try_from(self) { + Ok(ipld) => promise::Any::Resolved(ipld.try_into()?), + Err(pending) => match pending { + Pending::Ok(cid) => promise::Any::PendingOk(cid), + Pending::Err(cid) => promise::Any::PendingErr(cid), + Pending::Any(cid) => promise::Any::PendingAny(cid), + }, + }) + } + + // FIXME return type + pub fn to_promise_any_string(self) -> Result, ()> { + match self { + Promised::String(s) => Ok(promise::Any::Resolved(s)), + Promised::WaitOk(cid) => Ok(promise::Any::PendingOk(cid)), + Promised::WaitErr(cid) => Ok(promise::Any::PendingErr(cid)), + Promised::WaitAny(cid) => Ok(promise::Any::PendingAny(cid)), + _ => Err(()), + } + } +} + +impl fmt::Display for Promised { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Promised::Null => write!(f, "null"), + Promised::Bool(b) => write!(f, "{}", b), + Promised::Integer(i) => write!(f, "{}", i), + Promised::Float(fl) => write!(f, "{}", fl), + Promised::String(s) => write!(f, "{}", s), + Promised::Bytes(b) => write!(f, "{:?}", b), + Promised::Link(cid) => write!(f, "{}", cid), + Promised::WaitOk(cid) => write!(f, "await/ok: {}", cid), + Promised::WaitErr(cid) => write!(f, "await/err: {}", cid), + Promised::WaitAny(cid) => write!(f, "await/*: {}", cid), + Promised::List(list) => { + write!(f, "[")?; + for (i, promised) in list.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", promised)?; + } + write!(f, "]") + } + Promised::Map(map) => { + write!(f, "{{")?; + for (i, (k, v)) in map.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", k, v)?; + } + write!(f, "}}") + } + } + } +} + +impl From for Promised { + fn from(ipld: Ipld) -> Promised { + match ipld { + Ipld::Null => Promised::Null, + Ipld::Bool(b) => Promised::Bool(b), + Ipld::Integer(i) => Promised::Integer(i), + Ipld::Float(f) => Promised::Float(f), + Ipld::String(s) => Promised::String(s), + Ipld::Bytes(b) => Promised::Bytes(b), + Ipld::Link(cid) => Promised::Link(cid), + Ipld::List(list) => Promised::List(list.into_iter().map(Into::into).collect()), + Ipld::Map(map) => { + if map.len() == 1 { + if let Some((k, Ipld::Link(cid))) = map.first_key_value() { + return match k.as_str() { + "await/ok" => Promised::WaitOk(*cid), + "await/err" => Promised::WaitErr(*cid), + "await/*" => Promised::WaitAny(*cid), + _ => Promised::Map(BTreeMap::from_iter([( + k.to_string(), + Promised::Link(*cid), + )])), + }; + } + } + + let map = map.into_iter().fold(BTreeMap::new(), |mut acc, (k, v)| { + acc.insert(k, v.into()); + acc + }); + + Promised::Map(map) + } + } + } +} + +impl TryFrom for Ipld { + type Error = Pending; + + fn try_from(promised: Promised) -> Result { + match promised { + Promised::Null => Ok(Ipld::Null), + Promised::Bool(b) => Ok(Ipld::Bool(b)), + Promised::Integer(i) => Ok(Ipld::Integer(i)), + Promised::Float(f) => Ok(Ipld::Float(f)), + Promised::String(s) => Ok(Ipld::String(s)), + Promised::Bytes(b) => Ok(Ipld::Bytes(b)), + Promised::Link(cid) => Ok(Ipld::Link(cid)), + Promised::List(list) => list + .into_iter() + .try_fold(Vec::new(), |mut acc, promised| { + acc.push(promised.try_into()?); + Ok(acc) + }) + .map(Ipld::List), + Promised::Map(map) => map + .into_iter() + .try_fold(BTreeMap::new(), |mut acc, (k, v)| { + acc.insert(k, v.try_into()?); + Ok(acc) + }) + .map(Ipld::Map), + Promised::WaitOk(cid) => Err(Pending::Ok(cid).into()), + Promised::WaitErr(cid) => Err(Pending::Err(cid).into()), + Promised::WaitAny(cid) => Err(Pending::Any(cid).into()), + } + } +} + +impl From> for Promised { + fn from(p_ok: PromiseOk) -> Promised { + match p_ok { + PromiseOk::Fulfilled(ipld) => ipld.into(), + PromiseOk::Pending(cid) => Promised::WaitOk(cid), + } + } +} + +impl From> for Promised { + fn from(p_err: PromiseErr) -> Promised { + match p_err { + PromiseErr::Rejected(ipld) => ipld.into(), + PromiseErr::Pending(cid) => Promised::WaitErr(cid), + } + } +} + +impl From> for Promised { + fn from(p_any: promise::Any) -> Promised { + match p_any { + promise::Any::Resolved(ipld) => ipld.into(), + promise::Any::PendingOk(cid) => Promised::WaitOk(cid), + promise::Any::PendingErr(cid) => Promised::WaitErr(cid), + promise::Any::PendingAny(cid) => Promised::WaitAny(cid), + } + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(args: arguments::Named) -> Promised { + Promised::Map( + args.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ) + } +} + +impl From for Promised { + fn from(path: PathBuf) -> Promised { + Promised::String(path.to_string_lossy().to_string()) + } +} + +impl From for Promised { + fn from(cid: Cid) -> Promised { + Promised::Link(cid) + } +} + +impl From<::url::Url> for Promised { + fn from(url: ::url::Url) -> Promised { + Promised::String(url.to_string()) + } +} + +impl TryFrom for url::Newtype { + type Error = (); + + fn try_from(promised: Promised) -> Result { + match promised { + Promised::String(s) => Ok(url::Newtype(::url::Url::parse(&s).map_err(|_| ())?)), + _ => Err(()), + } + } +} + +impl From for Promised { + fn from(nt: url::Newtype) -> Promised { + nt.0.into() + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(opt: Option) -> Promised { + match opt { + Some(val) => val.into(), + None => Promised::Null, + } + } +} + +impl From for Promised { + fn from(s: String) -> Promised { + Promised::String(s) + } +} + +impl From for Promised { + fn from(f: f64) -> Promised { + Promised::Float(f) + } +} + +impl From for Promised { + fn from(i: i128) -> Promised { + Promised::Integer(i) + } +} + +impl From for Promised { + fn from(b: bool) -> Promised { + Promised::Bool(b) + } +} + +impl From> for Promised { + fn from(b: Vec) -> Promised { + Promised::Bytes(b) + } +} + +impl From> for Promised +where + Promised: From, +{ + fn from(map: BTreeMap) -> Promised { + Promised::Map( + map.into_iter() + .map(|(k, v)| (k, v.into())) + .collect::>(), + ) + } +} +impl From> for Promised +where + Promised: From, +{ + fn from(list: Vec) -> Promised { + Promised::List(list.into_iter().map(Into::into).collect()) + } +} + +/*************************** +| POST ORDER IPLD ITERATOR | +***************************/ + +/// A post-order [`Ipld`] iterator +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde-codec", derive(serde::Serialize))] +#[allow(clippy::module_name_repetitions)] +pub struct PostOrderIpldIter<'a> { + inbound: Vec<&'a Promised>, + outbound: Vec<&'a Promised>, +} + +impl<'a> PostOrderIpldIter<'a> { + /// Initialize a new [`PostOrderIpldIter`] + #[must_use] + pub fn new(promised: &'a Promised) -> Self { + PostOrderIpldIter { + inbound: vec![promised], + outbound: vec![], + } + } +} + +impl<'a> Iterator for PostOrderIpldIter<'a> { + type Item = &'a Promised; + + fn next(&mut self) -> Option { + loop { + match self.inbound.pop() { + None => return self.outbound.pop(), + Some(ref map @ Promised::Map(ref btree)) => { + self.outbound.push(map); + + for node in btree.values() { + self.inbound.push(node); + } + } + + Some(ref list @ Promised::List(ref vector)) => { + self.outbound.push(list); + + for node in vector { + self.inbound.push(node); + } + } + Some(node) => self.outbound.push(node), + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..a793a1ab --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,35 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn( + // FIXME missing_debug_implementations, + future_incompatible, + let_underscore, + // FIXME missing_docs, + rust_2021_compatibility, + nonstandard_style +)] +#![deny(unreachable_pub)] + +//! ucan + +#[cfg(target_arch = "wasm32")] +extern crate alloc; + +pub mod ability; +pub mod capsule; +pub mod crypto; +pub mod delegation; +pub mod did; +pub mod invocation; +pub mod ipld; +pub mod reader; +pub mod receipt; +pub mod task; +pub mod time; +pub mod url; + +#[cfg(feature = "test_utils")] +pub mod test_utils; + +pub use delegation::Delegation; +pub use invocation::Invocation; +pub use receipt::Receipt; diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 00000000..2a4e12c5 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,9 @@ +//! Configure & attach an ambient environment to a value. +//! +//! See the [`Reader`] struct for more information. + +mod generic; +mod promised; + +pub use generic::Reader; +pub use promised::Promised; diff --git a/src/reader/generic.rs b/src/reader/generic.rs new file mode 100644 index 00000000..680ead6c --- /dev/null +++ b/src/reader/generic.rs @@ -0,0 +1,175 @@ +use crate::ability::{arguments, command::ToCommand, parse::ParseAbilityError}; +use libipld_core::ipld::Ipld; + +/// A struct that attaches an ambient environment to a value. +/// +/// This is a simple way to perform runtime [dependency injection][DI] in a way +/// that plumbs through traits. +/// +/// This is helpful for dependency injection and/or passing around values that +/// would otherwise need to be threaded through next to the value. +/// +/// This is loosely based on the [functional `Reader`][SO] type, +/// but is not implemented with forced purity. Many of the "ambient" features +/// and guarantees of the [functional `Reader`][SO] monad are not present here. +/// +/// # Examples +/// +/// ```rust +/// # use ucan::reader::Reader; +/// # use std::string::ToString; +/// # +/// struct Config { +/// name: String, +/// formatter: Box String>, +/// trimmer: Box String>, +/// } +/// +/// fn run(r: Reader) -> String { +/// let formatted = (r.env.formatter)(r.val.to_string()); +/// (r.env.trimmer)(formatted) +/// } +/// +/// let cfg1 = Config { +/// name: "cfg1".into(), +/// formatter: Box::new(|s| s.to_uppercase()), +/// trimmer: Box::new(|mut s| s.trim().into()) +/// }; +/// +/// let cfg2 = Config { +/// name: "cfg2".into(), +/// formatter: Box::new(|s| s.to_lowercase()), +/// trimmer: Box::new(|mut s| s.split_off(5).into()) +/// }; +/// +/// +/// let reader1 = Reader { +/// env: cfg1, +/// val: " value", +/// }; +/// +/// let reader2 = Reader { +/// env: cfg2, +/// val: " value", +/// }; +/// +/// assert_eq!(run(reader1), "VALUE"); +/// assert_eq!(run(reader2), "e"); +/// ``` +/// +/// [SO]: https://stackoverflow.com/questions/14178889/what-is-the-purpose-of-the-reader-monad +/// [DI]: https://en.wikipedia.org/wiki/Dependency_injection +#[derive(Clone, PartialEq, Debug)] +pub struct Reader { + /// The environment (or configuration) being passed with the value + pub env: Env, + + /// The raw value + pub val: T, +} + +impl Reader { + /// Map a function over the `val` of the [`Reader`] + pub fn map(self, func: F) -> Reader + where + F: FnOnce(T) -> U, + { + Reader { + env: self.env, + val: func(self.val), + } + } + + /// Modify the `env` field of the [`Reader`] + pub fn map_env(self, func: F) -> Reader + where + F: FnOnce(Env) -> NewEnv, + { + Reader { + env: func(self.env), + val: self.val, + } + } + + /// Temporarily modify the environment + /// + /// # Examples + /// + /// ```rust + /// # use ucan::reader::Reader; + /// # use std::string::ToString; + /// # + /// # #[derive(Clone)] + /// struct Config<'a> { + /// name: String, + /// formatter: &'a dyn Fn(String) -> String, + /// trimmer: &'a dyn Fn(String) -> String, + /// } + /// + /// fn run(r: Reader) -> String { + /// let formatted = (r.env.formatter)(r.val.to_string()); + /// (r.env.trimmer)(formatted) + /// } + /// + /// let cfg = Config { + /// name: "cfg1".into(), + /// formatter: &|s| s.to_uppercase(), + /// trimmer: &|mut s| s.trim().into() + /// }; + /// + /// let my_reader = Reader { + /// env: cfg, + /// val: " value", + /// }; + /// + /// assert_eq!(run(my_reader.clone()), "VALUE"); + /// + /// // Modify the env locally + /// let observed = my_reader.clone().local(|mut env| { + /// // Modifying env + /// env.trimmer = &|mut s: String| s.split_off(5).into(); + /// env + /// }, |r| run(r)); // Running + /// assert_eq!(observed, "E"); + /// + /// // Back to normal (the above was in fact "local") + /// assert_eq!(run(my_reader.clone()), "VALUE"); + /// ``` + pub fn local(&self, modify_env: F, closure: G) -> U + where + T: Clone, + Env: Clone, + F: Fn(Env) -> Env, + G: Fn(Reader) -> U, + { + closure(Reader { + val: self.val.clone(), + env: modify_env(self.env.clone()), + }) + } +} + +impl>> From> for arguments::Named { + fn from(reader: Reader) -> Self { + reader.val.into() + } +} + +impl ToCommand for Reader { + fn to_command(&self) -> String { + self.env.to_command() + } +} + +impl>> TryFrom> + for Reader +{ + type Error = ParseAbilityError<>>::Error>; + + fn try_from(args: arguments::Named) -> Result { + Ok(Reader { + env: Default::default(), + val: T::try_from(args).map_err(ParseAbilityError::InvalidArgs)?, + }) + } +} diff --git a/src/reader/promised.rs b/src/reader/promised.rs new file mode 100644 index 00000000..bea7d705 --- /dev/null +++ b/src/reader/promised.rs @@ -0,0 +1,41 @@ +use super::Reader; +use crate::ability::{arguments, command::ToCommand}; +use serde::{Deserialize, Serialize}; + +/// A helper newtype that marks a value as being a [`Resolvable::Promised`][crate::invocation::Resolvable::Promised]. +/// +/// Despite this being the intention, due to constraits, the consuming type needs to +/// implement the [`Resolvable`][crate::invocation::Resolvable] trait. +/// For example, there is a `wasm_bindgen` implementation in this crate if +/// compiled for `wasm32`. +/// +/// The is often used as: +/// +/// ```rust +/// # use ucan::reader::{Reader, Promised}; +/// # type Env = (); +/// # let env = (); +/// let example: Reader> = Reader { +/// env: env, +/// val: Promised(42), +/// }; +/// ``` +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct Promised(pub T); + +impl>> From> for arguments::Named { + fn from(promised: Promised) -> Self { + promised.0.into() + } +} +impl From> for Reader> { + fn from(reader: Reader) -> Self { + reader.map(Promised) + } +} + +impl From>> for Reader { + fn from(reader: Reader>) -> Self { + reader.map(|p| p.0) + } +} diff --git a/src/receipt.rs b/src/receipt.rs new file mode 100644 index 00000000..537d5335 --- /dev/null +++ b/src/receipt.rs @@ -0,0 +1,145 @@ +//! A [`Receipt`] is the (optional) response from an [`Invocation`][`crate::invocation::Invocation`]. +//! +//! - [`Receipt`]s are the result of an [`Invocation`][`crate::invocation::Invocation`]. +//! - [`Payload`] contains the pimary semantic information for a [`Receipt`]. +//! - [`Store`] is the storage interface for [`Receipt`]s. +//! - [`Responds`] associates the response success type to an [Ability][crate::ability]. + +mod payload; +mod responds; + +pub mod store; + +pub use payload::*; +pub use responds::Responds; +pub use store::Store; + +use crate::ability::arguments; +use crate::{ + crypto::{signature::Envelope, varsig}, + did::{self, Did}, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; +use serde::{Deserialize, Serialize}; + +/// The complete, signed receipt of an [`Invocation`][`crate::invocation::Invocation`]. +#[derive(Clone, Debug, PartialEq)] +pub struct Receipt< + T: Responds, + DID: Did = did::preset::Verifier, + V: varsig::Header = varsig::header::Preset, + C: Codec + Into + TryFrom = varsig::encoding::Preset, +> { + pub varsig_header: V, + pub signature: DID::Signature, + pub payload: Payload, + + _marker: std::marker::PhantomData, +} + +impl, C: Codec + TryFrom + Into> + did::Verifiable for Receipt +{ + fn verifier(&self) -> &DID { + &self.payload.verifier() + } +} + +impl< + T: Responds + Clone, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > From> for Ipld +where + Ipld: From, + Payload: TryFrom>, +{ + fn from(rec: Receipt) -> Self { + rec.to_ipld_envelope() + } +} + +impl< + T: Responds + Clone, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Envelope for Receipt +where + Ipld: From, + Payload: TryFrom>, +{ + type DID = DID; + type Payload = Payload; + type VarsigHeader = V; + type Encoder = C; + + fn construct( + varsig_header: V, + signature: DID::Signature, + payload: Payload, + ) -> Receipt { + Receipt { + varsig_header, + payload, + signature, + _marker: std::marker::PhantomData, + } + } + + fn varsig_header(&self) -> &V { + &self.varsig_header + } + + fn payload(&self) -> &Payload { + &self.payload + } + + fn signature(&self) -> &DID::Signature { + &self.signature + } + + fn verifier(&self) -> &DID { + &self.payload.issuer + } +} + +impl< + T: Responds + Clone, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Serialize for Receipt +where + Ipld: From, + Payload: TryFrom>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_ipld_envelope().serialize(serializer) + } +} + +impl< + 'de, + T: Responds + Clone, + DID: Did + Clone, + V: varsig::Header + Clone, + C: Codec + TryFrom + Into, + > Deserialize<'de> for Receipt +where + Ipld: From, + Payload: TryFrom>, + as TryFrom>>::Error: std::fmt::Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ipld = Ipld::deserialize(deserializer)?; + Self::try_from_ipld_envelope(ipld).map_err(serde::de::Error::custom) + } +} diff --git a/src/receipt/payload.rs b/src/receipt/payload.rs new file mode 100644 index 00000000..cd3d22ce --- /dev/null +++ b/src/receipt/payload.rs @@ -0,0 +1,334 @@ +//! The payload (non-signature) portion of a response from an [`Invocation`]. +//! +//! [`Invocation`]: crate::invocation::Invocation + +use super::responds::Responds; +use crate::{ + ability::arguments, + capsule::Capsule, + crypto::Nonce, + did::{Did, Verifiable}, + time::Timestamp, +}; +use derive_builder::Builder; +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde::{ + de::{self, MapAccess, Visitor}, + ser::SerializeStruct, + Deserialize, Serialize, Serializer, +}; +use std::{collections::BTreeMap, fmt, fmt::Debug}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +impl Verifiable for Payload { + fn verifier(&self) -> &DID { + &self.issuer + } +} + +/// The payload (non-signature) portion of a response from an [`Invocation`]. +/// +/// [`Invocation`]: crate::invocation::Invocation +#[derive(Debug, Clone, PartialEq, Builder)] +pub struct Payload { + /// The issuer of the [`Receipt`]. This [`Did`] *must* match the signature on + /// the outer layer of [`Receipt`]. + /// + /// [`Receipt`]: super::Receipt + pub issuer: DID, + + /// The [`Cid`] of the [`Invocation`] that was run. + /// + /// [`Invocation`]: crate::invocation::Invocation + pub ran: Cid, + + /// The output of the [`Invocation`]. This is always of + /// the form `{"ok": ...}` or `{"err": ...}`. + /// + /// [`Invocation`]: crate::invocation::Invocation + pub out: Result>, + + /// Any further [`Invocation`]s that the `ran` [`Invocation`] + /// requested to be queued next. + /// + /// [`Invocation`]: crate::invocation::Invocation + #[builder(default)] + pub next: Vec, + + /// An optional proof chain authorizing a different [`Did`] to + /// be the receipt `iss` than the audience (or subject) of the + /// [`Invocation`] that was run. + /// + /// [`Invocation`]: crate::invocation::Invocation + #[builder(default)] + pub proofs: Vec, + + /// Extensible, free-form fields. + #[builder(default)] + pub metadata: BTreeMap, + + /// A [cryptographic nonce] to ensure that the UCAN's [`Cid`] is unique. + /// + /// [cryptographic nonce]: https://en.wikipedia.org/wiki/Cryptographic_nonce + /// [`Cid`]: libipld_core::cid::Cid + #[builder(default = "Nonce::generate_16(&mut vec![])")] + pub nonce: Nonce, + + /// An optional [Unix timestamp] (wall-clock time) at which the + /// receipt claims to have been issued at. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + #[builder(default)] + pub issued_at: Option, +} + +impl Capsule for Payload { + const TAG: &'static str = "ucan/r@1.0.0-rc.1"; +} + +impl From> for arguments::Named +where + Ipld: From, +{ + fn from(payload: Payload) -> Self { + let out_ipld = match payload.out { + Ok(ok) => BTreeMap::from_iter([("ok".to_string(), Ipld::from(ok))]).into(), + Err(err) => BTreeMap::from_iter([("err".to_string(), err.0.into())]).into(), + }; + + let mut args = arguments::Named::::from_iter([ + ("iss".to_string(), Ipld::String(payload.issuer.to_string())), + ("ran".to_string(), payload.ran.into()), + ("out".to_string(), out_ipld), + ( + "next".to_string(), + Ipld::List( + payload + .next + .clone() + .into_iter() + .map(|x| Ipld::Link(x)) + .collect(), + ), + ), + ( + "prf".to_string(), + Ipld::List(payload.next.into_iter().map(|x| Ipld::Link(x)).collect()), + ), + ("meta".to_string(), payload.metadata.into()), + ("nonce".to_string(), payload.nonce.into()), + ]); + + if let Some(issued_at) = payload.issued_at { + args.insert("iat".to_string(), issued_at.into()); + } + + args + } +} + +impl Serialize for Payload +where + T::Success: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let field_count = 7 + self.issued_at.is_some() as usize; + + let mut state = serializer.serialize_struct("receipt::Payload", field_count)?; + + state.serialize_field("iss", &self.issuer.to_string().as_str())?; + state.serialize_field("ran", &self.ran)?; + state.serialize_field("out", &self.out)?; + state.serialize_field("next", &self.next)?; + state.serialize_field("prf", &self.proofs)?; + state.serialize_field("meta", &self.metadata)?; + state.serialize_field("nonce", &self.nonce)?; + state.serialize_field("iat", &self.issued_at)?; + + state.end() + } +} + +impl<'de, T: Responds, DID: Did + Deserialize<'de>> Deserialize<'de> for Payload +where + T::Success: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ReceiptPayloadVisitor(std::marker::PhantomData<(T, DID)>); + + const FIELDS: &'static [&'static str] = + &["iss", "ran", "out", "next", "prf", "meta", "nonce", "iat"]; + + impl<'de, T: Responds, DID: Did + Deserialize<'de>> Visitor<'de> for ReceiptPayloadVisitor + where + T::Success: Deserialize<'de>, + { + type Value = Payload; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("struct delegation::Payload") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut issuer = None; + let mut ran = None; + let mut out = None; + let mut next = None; + let mut proofs = None; + let mut metadata = None; + let mut nonce = None; + let mut issued_at = None; + + while let Some(key) = map.next_key()? { + match key { + "iss" => { + if issuer.is_some() { + return Err(de::Error::duplicate_field("iss")); + } + issuer = Some(map.next_value()?); + } + "ran" => { + if ran.is_some() { + return Err(de::Error::duplicate_field("ran")); + } + ran = Some(map.next_value()?); + } + "out" => { + if out.is_some() { + return Err(de::Error::duplicate_field("out")); + } + out = Some(map.next_value()?); + } + "next" => { + if next.is_some() { + return Err(de::Error::duplicate_field("next")); + } + next = Some(map.next_value()?); + } + "prf" => { + if proofs.is_some() { + return Err(de::Error::duplicate_field("prf")); + } + proofs = Some(map.next_value()?); + } + "meta" => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("meta")); + } + metadata = Some(map.next_value()?); + } + "nonce" => { + if nonce.is_some() { + return Err(de::Error::duplicate_field("nonce")); + } + nonce = Some(map.next_value()?); + } + "iat" => { + if issued_at.is_some() { + return Err(de::Error::duplicate_field("iat")); + } + issued_at = map.next_value()?; + } + other => { + return Err(de::Error::unknown_field(other, FIELDS)); + } + } + } + + Ok(Payload { + issuer: issuer.ok_or(de::Error::missing_field("iss"))?, + ran: ran.ok_or(de::Error::missing_field("ran"))?, + out: out.ok_or(de::Error::missing_field("out"))?, + next: next.ok_or(de::Error::missing_field("next"))?, + proofs: proofs.ok_or(de::Error::missing_field("prf"))?, + metadata: metadata.ok_or(de::Error::missing_field("meta"))?, + nonce: nonce.ok_or(de::Error::missing_field("nonce"))?, + issued_at, + }) + } + } + + deserializer.deserialize_struct( + "ReceiptPayload", + FIELDS, + ReceiptPayloadVisitor(Default::default()), + ) + } +} + +impl From> for Ipld { + fn from(payload: Payload) -> Self { + payload.into() + } +} + +impl TryFrom for Payload +where + Payload: for<'de> Deserialize<'de>, +{ + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Payload +where + T::Success: Arbitrary + 'static, + DID::Parameters: Clone, + DID::Strategy: 'static, +{ + type Parameters = (::Parameters, DID::Parameters); + type Strategy = BoxedStrategy; + + fn arbitrary_with((t_params, did_params): Self::Parameters) -> Self::Strategy { + ( + DID::arbitrary_with(did_params), + cid::Newtype::arbitrary(), + prop_oneof![ + T::Success::arbitrary_with(t_params).prop_map(Result::Ok), + arguments::Named::arbitrary().prop_map(Result::Err), + ], + prop::collection::vec(cid::Newtype::arbitrary(), 0..25), + prop::collection::vec(cid::Newtype::arbitrary(), 0..25), + prop::collection::btree_map(".*", ipld::Newtype::arbitrary(), 0..50), + Nonce::arbitrary(), + prop::option::of(Timestamp::arbitrary()), + ) + .prop_map( + |(issuer, ran, out, next, proofs, newtype_metadata, nonce, issued_at)| Payload { + issuer, + ran: ran.cid, + out, + next: next.into_iter().map(|nt| nt.cid).collect(), + proofs: proofs.into_iter().map(|nt| nt.cid).collect(), + metadata: newtype_metadata + .into_iter() + .map(|(k, v)| (k, v.0)) + .collect(), + nonce, + issued_at, + }, + ) + .boxed() + } +} diff --git a/src/receipt/responds.rs b/src/receipt/responds.rs new file mode 100644 index 00000000..24db9f43 --- /dev/null +++ b/src/receipt/responds.rs @@ -0,0 +1,25 @@ +use crate::{crypto::Nonce, task, task::Task}; +use std::fmt; + +/// Describe the relationship between an ability and the [`Receipt`]s. +/// +/// This is used for constucting [`Receipt`]s, and indexing them for +/// reverse lookup. +/// +/// [`Receipt`]: crate::receipt::Receipt +pub trait Responds { + /// The successful return type for running `Self`. + type Success: Clone + fmt::Debug + PartialEq; + + /// Convert an Ability (`Self`) into a [`Task`]. + /// + /// This is used to index receipts by a minimal [`Id`]. + fn to_task(&self, subject: did_url::DID, nonce: Nonce) -> Task; + + /// Convert an Ability (`Self`) directly into a [`Task`]'s [`Id`]. + fn to_task_id(&self, subject: did_url::DID, nonce: Nonce) -> task::Id { + task::Id { + cid: self.to_task(subject, nonce).into(), + } + } +} diff --git a/src/receipt/store.rs b/src/receipt/store.rs new file mode 100644 index 00000000..ff1ecad8 --- /dev/null +++ b/src/receipt/store.rs @@ -0,0 +1,7 @@ +//! Store trait and MemoryStore implementation. + +mod memory; +mod traits; + +pub use memory::MemoryStore; +pub use traits::Store; diff --git a/src/receipt/store/memory.rs b/src/receipt/store/memory.rs new file mode 100644 index 00000000..6be098fa --- /dev/null +++ b/src/receipt/store/memory.rs @@ -0,0 +1,39 @@ +use super::Store; +use crate::{ + crypto::varsig, + did::Did, + receipt::{Receipt, Responds}, + task, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; +use std::{collections::BTreeMap, convert::Infallible, fmt}; + +/// An in-memory [`receipt::Store`][crate::receipt::Store]. +#[derive(Debug, Clone, PartialEq)] +pub struct MemoryStore< + T: Responds, + DID: Did, + V: varsig::Header, + Enc: Codec + Into + TryFrom, +> where + T::Success: fmt::Debug + Clone + PartialEq, +{ + store: BTreeMap>, +} + +impl, Enc: Codec + Into + TryFrom> + Store for MemoryStore +where + ::Success: TryFrom + Into + Clone + fmt::Debug + PartialEq, +{ + type Error = Infallible; + + fn get(&self, id: &task::Id) -> Result>, Self::Error> { + Ok(self.store.get(id)) + } + + fn put(&mut self, id: task::Id, receipt: Receipt) -> Result<(), Self::Error> { + self.store.insert(id, receipt); + Ok(()) + } +} diff --git a/src/receipt/store/traits.rs b/src/receipt/store/traits.rs new file mode 100644 index 00000000..8d53a1f0 --- /dev/null +++ b/src/receipt/store/traits.rs @@ -0,0 +1,26 @@ +use crate::{ + crypto::varsig, + did::Did, + receipt::{Receipt, Responds}, + task, +}; +use libipld_core::{codec::Codec, ipld::Ipld}; + +/// A store for [`Receipt`]s indexed by their [`task::Id`]s. +pub trait Store, C: Codec + Into + TryFrom> { + /// The error type representing all the ways a store operation can fail. + type Error; + + /// Retrieve a [`Receipt`] by its [`task::Id`]. + /// + /// If the store itself did not experience an error, but the value + /// was not found, the result will be `Ok(None)`. + fn get<'a>(&self, id: &task::Id) -> Result>, Self::Error> + where + ::Success: TryFrom; + + /// Store a [`Receipt`] by its [`task::Id`]. + fn put(&mut self, id: task::Id, receipt: Receipt) -> Result<(), Self::Error> + where + ::Success: Into; +} diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 00000000..6b931c59 --- /dev/null +++ b/src/task.rs @@ -0,0 +1,102 @@ +//! Task indices for [`Receipt`][crate::receipt::Receipt] reverse lookup. + +mod id; +pub use id::Id; + +use crate::{ability::arguments, crypto::Nonce, did}; +use libipld_cbor::DagCborCodec; +use libipld_core::{ + cid::{Cid, CidGeneric}, + codec::Encode, + error::SerdeError, + ipld::Ipld, + multihash::{Code, MultihashGeneric}, + serde as ipld_serde, +}; +use serde_derive::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// The fields required to uniquely identify a [`Task`], potentially across multiple executors. +/// +/// This struct should not be used directly, but rather through a [`From`] instance +/// on the type. In particular, the `nonce` field should be constant for all of the same type. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Task { + /// The `subject`: root issuer, and arbiter of the semantics/namespace. + pub sub: did::Newtype, + + /// A unique identifier for the particular task run. + /// + /// This is an [`Option`] because not all task types require a nonce. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nonce: Option, + + /// The command identifier. + pub cmd: String, + + /// The arguments to the command. + pub args: arguments::Named, +} + +impl TryFrom for Task { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for Ipld { + fn from(task: Task) -> Self { + task.into() + } +} + +impl From for Cid { + fn from(task: Task) -> Cid { + let mut buffer = vec![]; + let ipld: Ipld = task.into(); + + ipld.encode(DagCborCodec, &mut buffer) + .expect("DagCborCodec to encode any arbitrary `Ipld`"); + + CidGeneric::new_v1( + DagCborCodec.into(), + MultihashGeneric::wrap(Code::Sha2_256.into(), buffer.as_slice()) + .expect("DagCborCodec + Sha2_256 should always successfully encode Ipld to a Cid"), + ) + } +} + +impl From for Id { + fn from(task: Task) -> Id { + Id { + cid: Cid::from(task), + } + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Task { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::>(), + any::(), + any::>(), + ) + .prop_map(|(sub, nonce, cmd, args)| Task { + sub, + nonce, + cmd, + args, + }) + .boxed() + } +} diff --git a/src/task/id.rs b/src/task/id.rs new file mode 100644 index 00000000..de799110 --- /dev/null +++ b/src/task/id.rs @@ -0,0 +1,49 @@ +//! A newtype wrapper around [`Cid`]s to tag them as able to identify a particular invocation. + +use libipld_core::{cid::Cid, error::SerdeError, ipld::Ipld, serde as ipld_serde}; +use serde_derive::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +#[cfg(feature = "test_utils")] +use crate::ipld::cid; + +/// The unique identifier for a [`Task`][super::Task]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Id { + /// The CID of the [`Task`][super::Task]. + /// + /// This acts as a unique identifier for the task. + pub cid: Cid, +} + +impl TryFrom for Id { + type Error = SerdeError; + + fn try_from(ipld: Ipld) -> Result { + ipld_serde::from_ipld(ipld) + } +} + +impl From for Ipld { + fn from(id: Id) -> Self { + id.cid.into() + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Id { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + any::() + .prop_map(|cid_newtype| Id { + cid: cid_newtype.cid, + }) + .boxed() + } +} diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 00000000..e3817b91 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,95 @@ +use libipld::{ + cid::multihash::{Code, MultihashDigest, MultihashGeneric}, + codec_impl::IpldCodec, +}; +use proptest::prelude::*; + +#[derive(Clone, Debug, PartialEq)] +pub struct SomeCodec(pub IpldCodec); + +impl Arbitrary for SomeCodec { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + Just(IpldCodec::Raw), + Just(IpldCodec::DagCbor), + Just(IpldCodec::DagJson), + Just(IpldCodec::DagPb), + ] + .prop_map(SomeCodec) + .boxed() + } +} + +#[derive(Eq, Copy, Clone, Debug, PartialEq)] +pub struct SomeMultihash(pub Code); + +impl Default for SomeMultihash { + fn default() -> Self { + SomeMultihash(Code::Sha2_256) + } +} + +impl SomeMultihash { + pub fn new(multihash: Code) -> Self { + SomeMultihash(multihash) + } +} + +impl From for SomeMultihash { + fn from(multihash: Code) -> Self { + SomeMultihash(multihash) + } +} + +impl From for Code { + fn from(wrapper: SomeMultihash) -> Self { + wrapper.0 + } +} + +impl From for u64 { + fn from(wrapper: SomeMultihash) -> Self { + wrapper.0.into() + } +} + +impl TryFrom for SomeMultihash { + type Error = >::Error; + + fn try_from(code: u64) -> Result { + let inner = code.try_into()?; + Ok(SomeMultihash(inner)) + } +} + +impl MultihashDigest<64> for SomeMultihash { + fn digest(&self, input: &[u8]) -> MultihashGeneric<64> { + self.0.digest(input) + } + + fn wrap(&self, digest: &[u8]) -> Result, Self::Error> { + self.0.wrap(digest) + } +} + +impl Arbitrary for SomeMultihash { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // Only the 256-bit variants for now + prop_oneof![ + Just(Code::Sha2_256), + Just(Code::Sha3_256), + Just(Code::Keccak256), + Just(Code::Blake2s256), + Just(Code::Blake2b256), + Just(Code::Blake3_256), + ] + .prop_map(SomeMultihash) + .boxed() + } +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 00000000..3c37cb1d --- /dev/null +++ b/src/time.rs @@ -0,0 +1,9 @@ +//! Time utilities. +//! +//! The [`Timestamp`] struct is the main type for representing time in a UCAN token. + +mod error; +mod timestamp; + +pub use error::*; +pub use timestamp::Timestamp; diff --git a/src/time/error.rs b/src/time/error.rs new file mode 100644 index 00000000..e9c3149e --- /dev/null +++ b/src/time/error.rs @@ -0,0 +1,29 @@ +//! Temporal errors. + +use thiserror::Error; +use web_time::SystemTime; + +/// An error expressing when a time is larger than 2⁵³ seconds past the Unix epoch +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("Time out of JsTime (2⁵³) range: {:?}", tried)] +pub struct OutOfRangeError { + /// The [`SystemTime`] that is outside of the [`JsTime`] range (2⁵³). + pub tried: SystemTime, +} + +/// An error expressing when a time is not within the bounds of a UCAN. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Error)] +pub enum TimeBoundError { + /// The UCAN has expired. + #[error("Expired")] + Expired, + + /// The UCAN is not yet valid, but will be in the future. + #[error("Not yet valid")] + NotYetValid, +} + +/// The UCAN has expired. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)] +#[error("Expired")] +pub struct Expired; diff --git a/src/time/timestamp.rs b/src/time/timestamp.rs new file mode 100644 index 00000000..30f62f29 --- /dev/null +++ b/src/time/timestamp.rs @@ -0,0 +1,191 @@ +//! A JavaScript-wrapper for [`Timestamp`][crate::time::Timestamp]. + +use super::OutOfRangeError; +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use web_time::{Duration, SystemTime, UNIX_EPOCH}; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A [`Timestamp`][super::Timestamp] with safe JavaScript interop. +/// +/// Per the UCAN spec, timestamps MUST respect [IEEE-754] +/// (64-bit double precision = 53-bit truncated integer) for +/// JavaScript interoperability. +/// +/// This range can represent millions of years into the future, +/// and is thus sufficient for "nearly" all auth use cases. +/// +/// This type internally deserializes permissively from any [`SystemTime`], +/// but checks that any time created is in the 53-bit bound when created via +/// the public API. +/// +/// [IEEE-754]: https://en.wikipedia.org/wiki/IEEE_754 +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub struct Timestamp { + time: SystemTime, +} + +impl Timestamp { + /// Create a [`Timestamp`] from a [`SystemTime`]. + /// + /// # Arguments + /// + /// * `time` — The time to convert + /// + /// # Errors + /// + /// * [`OutOfRangeError`] — If the time is more than 2⁵³ seconds since the Unix epoch + pub fn new(time: SystemTime) -> Result { + if time + .duration_since(UNIX_EPOCH) + .map_err(|_| OutOfRangeError { tried: time })? + .as_secs() + > 0x1FFFFFFFFFFFFF + { + Err(OutOfRangeError { tried: time }) + } else { + Ok(Timestamp { time }) + } + } + + /// Get the current time in seconds since [`UNIX_EPOCH`] as a [`Timestamp`]. + pub fn now() -> Timestamp { + Self::new(SystemTime::now()) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + pub fn five_minutes_from_now() -> Timestamp { + Self::new(SystemTime::now() + Duration::from_secs(5 * 60)) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + pub fn five_years_from_now() -> Timestamp { + Self::new(SystemTime::now() + Duration::from_secs(5 * 365 * 24 * 60 * 60)) + .expect("the current time to be somtime in the 3rd millenium CE") + } + + /// Convert a [`Timestamp`] to a [Unix timestamp]. + /// + /// [Unix timestamp]: https://en.wikipedia.org/wiki/Unix_time + pub fn to_unix(&self) -> u64 { + self.time + .duration_since(UNIX_EPOCH) + .expect("System time to be after the Unix epoch") + .as_secs() + } + + /// An intentionally permissive variant of `new` for + /// deseriazation. See the note on the struct. + pub(crate) fn postel(time: SystemTime) -> Self { + Timestamp { time } + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl Timestamp { + /// Lift a [`js_sys::Date`] into a Rust [`Timestamp`] + pub fn from_date(date_time: js_sys::Date) -> Result { + let millis = date_time.get_time() as u64; + let secs: u64 = (millis / 1000) as u64; + let duration = Duration::new(secs, 0); // Just round off the nanos + Timestamp::new(UNIX_EPOCH + duration).map_err(Into::into) + } + + /// Lower the [`Timestamp`] to a [`js_sys::Date`] + pub fn to_date(&self) -> js_sys::Date { + js_sys::Date::new(&JsValue::from( + self.time + .duration_since(UNIX_EPOCH) + .expect("time should be in range since it's getting a JS Date") + .as_millis(), + )) + } +} + +impl TryFrom for Timestamp { + type Error = OutOfRangeError; + + fn try_from(sys_time: SystemTime) -> Result { + Timestamp::new(sys_time) + } +} + +impl From for SystemTime { + fn from(js_time: Timestamp) -> Self { + js_time.time + } +} + +impl From for Ipld { + fn from(timestamp: Timestamp) -> Self { + timestamp.to_unix().into() + } +} + +impl TryFrom for Timestamp { + type Error = (); + + fn try_from(ipld: Ipld) -> Result { + match ipld { + // FIXME do bounds checking + Ipld::Integer(secs) => Ok(Timestamp::new( + UNIX_EPOCH + Duration::from_secs(secs as u64), + ) + .map_err(|_| ())?), + _ => Err(()), + } + } +} + +impl From for i128 { + fn from(timestamp: Timestamp) -> i128 { + timestamp.to_unix() as i128 + } +} + +impl TryFrom for Timestamp { + type Error = OutOfRangeError; + + fn try_from(secs: i128) -> Result { + // FIXME do bounds checking + Timestamp::new(UNIX_EPOCH + Duration::from_secs(secs as u64)) + } +} + +impl Serialize for Timestamp { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_unix().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let seconds = u64::deserialize(deserializer)?; + Ok(Timestamp::postel(UNIX_EPOCH + Duration::from_secs(seconds))) + } +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Timestamp { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0..(u64::pow(2, 53) - 1)) + .prop_map(|secs| { + Timestamp::new(UNIX_EPOCH + Duration::from_secs(secs)) + .expect("the current time to be somtime in the 3rd millenium CE") + }) + .boxed() + } +} diff --git a/src/url.rs b/src/url.rs new file mode 100644 index 00000000..dc8bd9c6 --- /dev/null +++ b/src/url.rs @@ -0,0 +1,96 @@ +//! URL utilities. + +use libipld_core::ipld::Ipld; +use serde::{Deserialize, Serialize}; +use std::fmt; +use thiserror::Error; +use url::Url; + +#[cfg(feature = "test_utils")] +use proptest::prelude::*; + +/// A wrapper around [`Url`] that has additional trait implementations. +/// +/// Usage is very simple: wrap a [`Newtype`] to gain access to additional traits and methods. +/// +/// ```rust +/// # use ::url::Url; +/// # use ucan::url; +/// # +/// let url = Url::parse("https://example.com").unwrap(); +/// let wrapped = url::Newtype(url.clone()); +/// // wrapped.some_trait_method(); +/// ``` +/// +/// Unwrap a [`Newtype`] to use any interfaces that expect plain [`Ipld`]. +/// +/// ``` +/// # use ::url::Url; +/// # use ucan::url; +/// # +/// # let url = Url::parse("https://example.com").unwrap(); +/// # let wrapped = url::Newtype(url.clone()); +/// # +/// assert_eq!(wrapped.0, url); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Newtype(pub Url); + +impl Newtype { + pub fn parse(s: &str) -> Result { + Ok(Newtype(Url::parse(s)?)) + } +} + +impl fmt::Display for Newtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for Ipld { + fn from(newtype: Newtype) -> Self { + Ipld::String(newtype.to_string()) + } +} + +impl TryFrom for Newtype { + type Error = FromIpldError; + + fn try_from(ipld: Ipld) -> Result { + match ipld { + Ipld::String(s) => Url::parse(&s) + .map(Newtype) + .map_err(FromIpldError::UrlParseError), + _ => Err(FromIpldError::NotAString), + } + } +} + +/// Possible errors when trying to convert from [`Ipld`]. +#[derive(Debug, Error)] +pub enum FromIpldError { + /// Not an IPLD string. + #[error("Not an IPLD string")] + NotAString, + + /// Failed to parse the URL. + #[error(transparent)] + UrlParseError(#[from] url::ParseError), +} + +#[cfg(feature = "test_utils")] +impl Arbitrary for Newtype { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let url_regex: &str = &r#"[a-zA-Z]+[a-zA-Z0-9]*:(//)?[a-zA-Z0-9._]+(#)?[a-zA-Z0-9_]"#; + url_regex + .prop_map(|s| { + Newtype(Url::parse(&s).expect("the regex generator to create valid URLs")) + }) + .boxed() + } +} diff --git a/src/workerd.js b/src/workerd.js new file mode 100644 index 00000000..fad5ae1d --- /dev/null +++ b/src/workerd.js @@ -0,0 +1,6 @@ +// This entry point is inserted into ./lib/workerd to support Cloudflare workers + +import WASM from "./ucan_bg.wasm"; +import { initSync } from "./ucan.js"; +initSync(WASM); +export * from "./ucan.js"; diff --git a/tests/fixtures/0.10.0/all.json b/tests/fixtures/0.10.0/all.json new file mode 100644 index 00000000..e4904778 --- /dev/null +++ b/tests/fixtures/0.10.0/all.json @@ -0,0 +1,1418 @@ +[ + { + "name": "UCAN has not expired", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 9246211200, + "cap": {} + }, + "signature": "pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg" + } + }, + { + "name": "UCAN is ready to be used", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInVjdiI6IjAuMTAuMCJ9.-yaM1x8v4jIvi5ldLsjN3unAJiaFx2D1gl4z_Ct8OCcS_afEW-q8phwyOVu3DKFP8dGoEvlMQMhTfPsiUOCsAQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "nbf": 1, + "cap": {} + }, + "signature": "-yaM1x8v4jIvi5ldLsjN3unAJiaFx2D1gl4z_Ct8OCcS_afEW-q8phwyOVu3DKFP8dGoEvlMQMhTfPsiUOCsAQ" + } + }, + { + "name": "UCAN has same time bounds as proof", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJuYmYiOjEsInByZiI6WyJiYWZrcmVpZTNiZndyMnB4dHpwaHpwc3BiM3BsdmRyaDNvYnE1eWl5aWJmdXVpcWoybTNrY3JyZXhwdSJdLCJ1Y3YiOiIwLjEwLjAifQ.C7ceqIwzJYqC5TQf8PRXjMCYri1JxpioZFU0LIYpM1fP_Xn7Eij9qcRd5WUXvKmUAGmn_gmv8rolXbe4n3UAAA", + "proofs": { + "bafkreie3bfwr2pxtzphzpspb3plvdrh3obq5yiyibfuuiqj2m3kcrrexpu": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInVjdiI6IjAuMTAuMCJ9.qvC6-4agkAxd72ZKNardHy8YHpKGAhz9sbNlWMys0LBoccifGCl-9Yz3bpy4SuosAbWy-W2tc5MGzFCNRmLpAw" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": 9246211200, + "nbf": 1, + "cap": {}, + "prf": [ + "bafkreie3bfwr2pxtzphzpspb3plvdrh3obq5yiyibfuuiqj2m3kcrrexpu" + ] + }, + "signature": "C7ceqIwzJYqC5TQf8PRXjMCYri1JxpioZFU0LIYpM1fP_Xn7Eij9qcRd5WUXvKmUAGmn_gmv8rolXbe4n3UAAA" + } + }, + { + "name": "UCAN expires before proof", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3JlaWN6Nnl2czRlcHplbXprNmMyb3E0d3BvamVvNWh1c3diaDd1cGtzNDVpa25kYnllcmdsbmEiXSwidWN2IjoiMC4xMC4wIn0.iWzUN38aE9Kid_f3P8ahMPg7oKHymAVqdx0Lr1XfZqdBPB33T0uBBuGQiMpMPmx_55ReWAulyxZzFgTqgBDKDw", + "proofs": { + "bafkreicz6yvs4epzemzk6c2oq4wpojeo5huswbh7upks45ikndbyerglna": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6MTQwNjkxNDIwMDAsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.qtp60plAo81TRm56PMdlkOwUZT2uPFWzWLtjZmti6_KLULOZXQYN6h9ihXz9MNX3HflUIZoBsWJnPN_8--y4AA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": 9246211200, + "cap": {}, + "prf": [ + "bafkreicz6yvs4epzemzk6c2oq4wpojeo5huswbh7upks45ikndbyerglna" + ] + }, + "signature": "iWzUN38aE9Kid_f3P8ahMPg7oKHymAVqdx0Lr1XfZqdBPB33T0uBBuGQiMpMPmx_55ReWAulyxZzFgTqgBDKDw" + } + }, + { + "name": "UCAN active after proof", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJuYmYiOjIsInByZiI6WyJiYWZrcmVpZ3hpeTdwdndtZmxhaDZjN3picW12a3JpaW02amlodnFwaDIzb216M3V3ZGphN3E0ZnEyaSJdLCJ1Y3YiOiIwLjEwLjAifQ.X9vegej9T07LaA5wPtCj4WcV_vjy2KgkvKYTIT4IXoFvtZwrcUj6ABOG54LpWlXVto-Y09zIi2W3Miwzu10CAw", + "proofs": { + "bafkreigxiy7pvwmflah6c7zbqmvkriim6jihvqph23omz3uwdja7q4fq2i": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInVjdiI6IjAuMTAuMCJ9.-yaM1x8v4jIvi5ldLsjN3unAJiaFx2D1gl4z_Ct8OCcS_afEW-q8phwyOVu3DKFP8dGoEvlMQMhTfPsiUOCsAQ" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "nbf": 2, + "cap": {}, + "prf": [ + "bafkreigxiy7pvwmflah6c7zbqmvkriim6jihvqph23omz3uwdja7q4fq2i" + ] + }, + "signature": "X9vegej9T07LaA5wPtCj4WcV_vjy2KgkvKYTIT4IXoFvtZwrcUj6ABOG54LpWlXVto-Y09zIi2W3Miwzu10CAw" + } + }, + { + "name": "UCAN has a well-formed capability", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + {} + ] + } + } + }, + "signature": "mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + } + }, + { + "name": "UCAN has a well-formed capability with a caveat", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJtYXJrZXRpbmciXX1dfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.NWWHxBFHSW68IvbeB2utg5G67tSvEN9uHGxHOC5nzzoIdjpz39q5qI7CNuXlPLQDGvuUkZSjIAUzKtU3-HvaCg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + { + "templates": [ + "marketing" + ] + } + ] + } + } + }, + "signature": "NWWHxBFHSW68IvbeB2utg5G67tSvEN9uHGxHOC5nzzoIdjpz39q5qI7CNuXlPLQDGvuUkZSjIAUzKtU3-HvaCg" + } + }, + { + "name": "UCAN has multiple well-formed capabilities", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19LCJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbeyJ0ZW1wbGF0ZXMiOlsibWFya2V0aW5nIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f-xu84oUX45_2ncrkTjVX1zmJSBdsqrE21DxOUf-9eV3SjxRPmVpshE1bcTGqyjdxsaST0hdr3CXkBGKLD5wBw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + {} + ] + }, + "mailto:marketing@email.com": { + "email/send": [ + { + "templates": [ + "marketing" + ] + } + ] + } + } + }, + "signature": "f-xu84oUX45_2ncrkTjVX1zmJSBdsqrE21DxOUf-9eV3SjxRPmVpshE1bcTGqyjdxsaST0hdr3CXkBGKLD5wBw" + } + }, + { + "name": "UCAN issuer matches proof audience", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3JlaWJ2djQzenQ1ZGZ6d3I1ZWptczNvZWI1cmthenVmemt2cXM2djNpend3bGo2NGptcmNoY2kiXSwidWN2IjoiMC4xMC4wIn0.0BPu7MCETzLUwNqJdmw-D0CTQcXOrXaxJrRr-ONV0LG7e_P5ZkH6K8Et6k6lRp5JL7VhrnD2W1bT6lD2PbC_Cw", + "proofs": { + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": {}, + "prf": [ + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci" + ] + }, + "signature": "0BPu7MCETzLUwNqJdmw-D0CTQcXOrXaxJrRr-ONV0LG7e_P5ZkH6K8Et6k6lRp5JL7VhrnD2W1bT6lD2PbC_Cw" + } + }, + { + "name": "UCAN has a delegated capability", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInByZiI6WyJiYWZrcmVpYnZtaTc2NGNtdGFvNGsybWUybGtzYmY3NGRqcXBscWp3cWFsYWhkNHhmbnR5MzNycnBnbSJdLCJ1Y3YiOiIwLjEwLjAifQ.fwWnOgRSYryzvkvLyqYQZozrzKLIBfW4uGHKG6hR8Dygj1OOrDrcVXY88N7UQmj6O4ETXsrF99om5NK3QBB7Cw", + "proofs": { + "bafkreibvmi764cmtao4k2me2lksbf74djqplqjwqalahd4xfnty33rrpgm": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + {} + ] + } + }, + "prf": [ + "bafkreibvmi764cmtao4k2me2lksbf74djqplqjwqalahd4xfnty33rrpgm" + ] + }, + "signature": "fwWnOgRSYryzvkvLyqYQZozrzKLIBfW4uGHKG6hR8Dygj1OOrDrcVXY88N7UQmj6O4ETXsrF99om5NK3QBB7Cw" + } + }, + { + "name": "UCAN merges delegated capabilities", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19LCJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbe31dfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3JlaWJ2bWk3NjRjbXRhbzRrMm1lMmxrc2JmNzRkanFwbHFqd3FhbGFoZDR4Zm50eTMzcnJwZ20iLCJiYWZrcmVpYnNjaGNsbGRvdGVlbWM2enVkNHA1aXhoM2p5cWN0ZG9kNmJnMmdnZW8zb2drd3lyNHFubSJdLCJ1Y3YiOiIwLjEwLjAifQ.2C9kEs6nJmfabHn4iarDAfAbFQ70jwMlM_S76ky7O5ia8s9SYBpCDd9xEWu_9aHpLg34PnpTxOx8GqcWdm6CAA", + "proofs": { + "bafkreibvmi764cmtao4k2me2lksbf74djqplqjwqalahd4xfnty33rrpgm": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA", + "bafkreibschclldoteemc6zud4p5ixh3jyqctdod6bg2ggeo3ogkwyr4qnm": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbe31dfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.7KyzLK-9VeArJJaVwy3UMlR0I_u0J_Wpq0dQmVm45KWMEW8_pxFzLSUSKWIU-nFvcS4ehGLOOTEhuq4S8eTCDg" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + {} + ] + }, + "mailto:marketing@email.com": { + "email/send": [ + {} + ] + } + }, + "prf": [ + "bafkreibvmi764cmtao4k2me2lksbf74djqplqjwqalahd4xfnty33rrpgm", + "bafkreibschclldoteemc6zud4p5ixh3jyqctdod6bg2ggeo3ogkwyr4qnm" + ] + }, + "signature": "2C9kEs6nJmfabHn4iarDAfAbFQ70jwMlM_S76ky7O5ia8s9SYBpCDd9xEWu_9aHpLg34PnpTxOx8GqcWdm6CAA" + } + }, + { + "name": "UCAN capability caveats equal to proof caveats", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2ZmRFpDa0NUV3JlZzg4NjhmRzFGR0ZvZ2NKajVYNlBZOTNwUGNXRG45Ym9iIiwicHJmIjpbImJhZmtyZWloY2ZhcGE2bDMyd256dWthemxpb2FzcHR5MzY1dWh2ZXgzNm9zdGN3amNqN2JyZHZ1eWZxIl0sInVjdiI6IjAuMTAuMCJ9._kh7_uU71DHQBksna_eak-hOPjXfiKQsQgs7Uuv00VNe81qZj9bOcqHSlfVbnH3Gd7K7E86Kftvl-VYEn7NTDw", + "proofs": { + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f4QEc5oN43eUKxUfYuQ9zrjz3A6jiW6XPQCALv9RQ4QWnt4LvNy53gX3Z53lHc_-Ei8ykn4YUSGM3qL5AtdSBA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": { + "mailto:alice@email.com": { + "email/send": [ + { + "templates": [ + "newsletter" + ] + } + ] + } + }, + "prf": [ + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq" + ] + }, + "signature": "_kh7_uU71DHQBksna_eak-hOPjXfiKQsQgs7Uuv00VNe81qZj9bOcqHSlfVbnH3Gd7K7E86Kftvl-VYEn7NTDw" + } + }, + { + "name": "UCAN capability attenuates existing caveats", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbeyJ0ZW1wbGF0ZXMiOlsibmV3c2xldHRlciJdfV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInByZiI6WyJiYWZrcmVpZGJ6emRucGwyNHI3dHRsZnRrdGFwcGR2YTVubW5hNXdseHg1aXZxY2NvcmN5am82eGFxdSJdLCJ1Y3YiOiIwLjEwLjAifQ.l3qeyfVGZRpDCZRMVU9MT2NZxo-4f6sTmEjbGqsg5t8H57olEbMx5nAYFa1x5XBL1Mfo-fj_Ase5r7LppIUGCw", + "proofs": { + "bafkreidbzzdnpl24r7ttlftktappdva5nmna5wlxx5ivqccorcyjo6xaqu": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbeyJ0ZW1wbGF0ZXMiOlsibmV3c2xldHRlciIsIm1hcmtldGluZyJdfV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.I2bU54PhYvKbctQkBrE0YFi1M9bLacUT_Zz7w6QgJSaZ7I2O7F3I3EBr8T9J3BwqTyrVjJwe05mHmBg0GR-QAQ" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": { + "mailto:marketing@email.com": { + "email/send": [ + { + "templates": [ + "newsletter" + ] + } + ] + } + }, + "prf": [ + "bafkreidbzzdnpl24r7ttlftktappdva5nmna5wlxx5ivqccorcyjo6xaqu" + ] + }, + "signature": "l3qeyfVGZRpDCZRMVU9MT2NZxo-4f6sTmEjbGqsg5t8H57olEbMx5nAYFa1x5XBL1Mfo-fj_Ase5r7LppIUGCw" + } + }, + { + "name": "UCAN capability attenuates from no caveats", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbeyJ0ZW1wbGF0ZXMiOlsibmV3c2xldHRlciJdfV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInByZiI6WyJiYWZrcmVpYnNjaGNsbGRvdGVlbWM2enVkNHA1aXhoM2p5cWN0ZG9kNmJnMmdnZW8zb2drd3lyNHFubSJdLCJ1Y3YiOiIwLjEwLjAifQ.qEip9gJLndvsRXIhi0zx4cn73DxteX5J3cpTAX5-ufZHgcHQF76nPZzRUCtGEZ34xQHNcJVfUv4kWWuikwyNAg", + "proofs": { + "bafkreibschclldoteemc6zud4p5ixh3jyqctdod6bg2ggeo3ogkwyr4qnm": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86bWFya2V0aW5nQGVtYWlsLmNvbSI6eyJlbWFpbC9zZW5kIjpbe31dfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.7KyzLK-9VeArJJaVwy3UMlR0I_u0J_Wpq0dQmVm45KWMEW8_pxFzLSUSKWIU-nFvcS4ehGLOOTEhuq4S8eTCDg" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": { + "mailto:marketing@email.com": { + "email/send": [ + { + "templates": [ + "newsletter" + ] + } + ] + } + }, + "prf": [ + "bafkreibschclldoteemc6zud4p5ixh3jyqctdod6bg2ggeo3ogkwyr4qnm" + ] + }, + "signature": "qEip9gJLndvsRXIhi0zx4cn73DxteX5J3cpTAX5-ufZHgcHQF76nPZzRUCtGEZ34xQHNcJVfUv4kWWuikwyNAg" + } + }, + { + "name": "UCAN has a fact", + "task": "verify", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiZmN0Ijp7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9LCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.uhk0MI1CyB3nEu4RxZqoOq3-BucWT86UXtP_th9ffa_uosmj6Aln3AUELkqJDsgr710UguNKQQzJVzmIdPoLCg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {}, + "fct": { + "challenge": "abcdef" + } + }, + "signature": "uhk0MI1CyB3nEu4RxZqoOq3-BucWT86UXtP_th9ffa_uosmj6Aln3AUELkqJDsgr710UguNKQQzJVzmIdPoLCg" + } + }, + { + "name": "UCAN has expired", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6MSwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.uVbv1qjgo20f5z4xsQkvxLFx4pEx60K4Ud-fyjfReE-NJNLwijhCMJiDgLHWc28zK9ml3Ooc4-naOmuipWXLBg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 1, + "cap": {} + }, + "signature": "uVbv1qjgo20f5z4xsQkvxLFx4pEx60K4Ud-fyjfReE-NJNLwijhCMJiDgLHWc28zK9ml3Ooc4-naOmuipWXLBg" + }, + "errors": [ + "expired" + ] + }, + { + "name": "UCAN is not ready to be used", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjkyNDYyMTEyMDAsInVjdiI6IjAuMTAuMCJ9.KziqvLp9cWEJkRanhjVb2q-d1C-YdphKEd5TkAz3eO-XuisLD_PAvRnXplNFkh04uFaR4uwTY-G5fzYeXphsBQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "nbf": 9246211200, + "cap": {} + }, + "signature": "KziqvLp9cWEJkRanhjVb2q-d1C-YdphKEd5TkAz3eO-XuisLD_PAvRnXplNFkh04uFaR4uwTY-G5fzYeXphsBQ" + }, + "errors": [ + "notReady" + ] + }, + { + "name": "UCAN expires after proofs", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6MTQwNjkxNDIwMDAsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwicHJmIjpbImJhZmtyZWlmbXdzN3U1dzZubHVwcnh1NXpjc3VuMndjcDdyaW94ankycWVtNnBqajV6MzY3ZHA2NGxpIl0sInVjdiI6IjAuMTAuMCJ9.yZkK6eGFgZ9LiKkLb70BeVo0EW3_iLqB6sSER-fgKOu5lVBIoqUL21cENaiZDrfBT0Qwura0rJjkCNEjfnD9Bg", + "proofs": { + "bafkreifmws7u5w6nluprxu5zcsun2wcp7rioxjy2qem6pjj5z367dp64li": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": 14069142000, + "cap": {}, + "prf": [ + "bafkreifmws7u5w6nluprxu5zcsun2wcp7rioxjy2qem6pjj5z367dp64li" + ] + }, + "signature": "yZkK6eGFgZ9LiKkLb70BeVo0EW3_iLqB6sSER-fgKOu5lVBIoqUL21cENaiZDrfBT0Qwura0rJjkCNEjfnD9Bg" + }, + "errors": [ + "timeBoundsViolation" + ] + }, + { + "name": "UCAN ready before proofs", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInByZiI6WyJiYWZrcmVpYWZtZTRxaDc2Nnl5azNtZ3Y0c3hwNjVlM2NkM29vNmVpeGI0dzd1enVsM3BlbGthaTJjNCJdLCJ1Y3YiOiIwLjEwLjAifQ.YOKAbtClCQJziz4Y0L_VuFa6WtnvQaNn4Ft3MmfF-PE1Asph1UgyMf8VKODZl9P-bN85J1ZQmEc9TZhN1qTTCg", + "proofs": { + "bafkreiafme4qh766yyk3mgv4sxp65e3cd3oo6eixb4w7uzul3pelkai2c4": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjIsInVjdiI6IjAuMTAuMCJ9.ejt__REc7NcLFre9mouOn6kszMjLgXvP2RFDObXpHyjtmFAKbQqwVPK3XYzMUPtTBLhw6XQPzazEGucXgBWEBg" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "nbf": 1, + "cap": {}, + "prf": [ + "bafkreiafme4qh766yyk3mgv4sxp65e3cd3oo6eixb4w7uzul3pelkai2c4" + ] + }, + "signature": "YOKAbtClCQJziz4Y0L_VuFa6WtnvQaNn4Ft3MmfF-PE1Asph1UgyMf8VKODZl9P-bN85J1ZQmEc9TZhN1qTTCg" + }, + "errors": [ + "timeBoundsViolation" + ] + }, + { + "name": "UCAN header is missing typ field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.mFFFVP-hfpI16xLV657cFPbmHHCy-LRuXaLaCr0c07o5gi9DLMs0RS54ZOWwNcCVLPwp1howg_aa4tUk9_DuBw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN header is missing alg field", + "task": "refute", + "inputs": { + "token": "eyJ0eXAiOiJKV1QifQ.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.yqNQQoNTHCixB8bjivcgnjBEnO14ILoH_H2lQdzt8sSYNnvMikdhS0unT1oBwY7-n2SAAxpVxIpDd1rFXh-zAg", + "proofs": {} + }, + "assertions": { + "header": { + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN payload is missing ucv field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkifQ.laZ9fqz-DhR_sbVS3S6hxRCgHU9NeWsXmv4ytxqyAgy86nmSy058q45seKfNF1FpXMt-0BsJ59GD5Uo9hLthBw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN payload is missing iss field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwidWN2IjoiMC4xMC4wIn0.b-g9tAwf78671zutr1CQgXlP3aP-2E2HjVbcJeYxAlp0V0qUWUCYErhhvH62NBfBKBO8NHHz1aml6T7ATFiyCQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN payload is missing aud field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ1Y3YiOiIwLjEwLjAiLCJjYXAiOnt9LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIn0.mvBd9TGdEB3p-EIcaXSSqCCCzAg7_8TL5axffLQn2VoAcn0nlHjkT-VwQzdwh5lp5Pt47BplQNTMkhzPw5aZBQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN payload is missing exp field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sInVjdiI6IjAuMTAuMCIsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIn0.YwUzs6T1Rir3bc6MmNlve6yGPWaXZZ4lntabiNmtAN6uY0reiSakxvqQEozFzpbKpECZvIOYagjVCaMwKQATDw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN payload is missing cap field", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInVjdiI6IjAuMTAuMCIsImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkifQ.22qwsTJvEqHCG1X1Gw30piphYmQhYYkih6gxh3M2qzvAXItGdymAsbBY7YY3I8lld7Tx6ZzTRh4shj63Y9LDCQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "missingField" + ] + }, + { + "name": "UCAN header alg field is not a string", + "task": "refute", + "inputs": { + "token": "eyJhbGciOjEsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.bDkVEY5PgTeC_6AY6i64EWHFin-NHxR4eynaUxpo9ThUZmf47G5yhruDB6XLTY389WjM39oDS5Bkh1_oFWlzAg", + "proofs": {} + }, + "assertions": { + "header": { + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN header typ field is not a string", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6MX0.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.1AobwbdMFtPhaqspFW-i7LirzgGrU4su9WZ4EBx-Hy7MuwZmjrpIj1O3fVj4zpJRXpFnOYwsZVu9lqLIeyDYAw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN type is not JWT", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6Ik5PVF9KV1QifQ.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.pk-UHwHy89YbLWtgSveyDAY8GNP519F8oRR3s-GuW1cFNgMOYClTwP-7Olq09daUFmQ09myAO4cLLAvcJcvEBw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload ucv field is not a string", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOjF9.i83bMf3ZTiEntyQgJzlBU0kiAkTHT3VR6uRSY45UITM6VrMz5H94jYKxlolGM53iL4WdfZ0dThdFUvOyepzpBQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload ucv field is not semantically versioned", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwIn0.aDj_GdR1OtwXoeWqt-5n0kmABvk7vqUZJR3qhT9IPljjaEmNATQcoDLxzTH2fe-oiQcFAxq8mX5XtpZ4OlpPDw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload iss field is not a DID", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.aZ8qyiXbEMX4GLsTgtJ8RBJHTGAmMz3elIg48SAVY4r48OZJmtW3JS9LE8boY2azWrksCVs32EehaVQBoIw3DQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload aud field is not a DID", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJjYXAiOnt9LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.jq4S2p5NEwxdB6eHYoVVAeSzcduZip20m8A8M3qKORFZPRSXT2RDxo6SuzTktm_gBMxqpG3_RzOOzTZzywQlAg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "exp": null, + "cap": {} + }, + "signature": "MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload nbf field is not a number", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOiIxIiwidWN2IjoiMC4xMC4wIn0.wNsH1CpmiZjJhfIlFZyxHoH8Xobf-_e0CRr3jzr2kECmICn8sWClr_zu5j2iR0ILj--Bj3k4GsLQTmQtQOx5Ag", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "-yaM1x8v4jIvi5ldLsjN3unAJiaFx2D1gl4z_Ct8OCcS_afEW-q8phwyOVu3DKFP8dGoEvlMQMhTfPsiUOCsAQ" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload exp field is not a number", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6IjkyNDYyMTEyMDAiLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.4_TiWe594ggaM3PDNADAZsfScA7z9ZpwnEDJ4a-x20JZ6qg8gabsVqK7d1O9Zje9TgQuqA4By0o3jwaTpgLEBA", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "cap": {} + }, + "signature": "pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload nnc field is not a string", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJubmMiOjEsInVjdiI6IjAuMTAuMCJ9.7sEB7EzEDslqFjufVeNih0oBquhC_BvYNrelnk1bfSfZ6Qrg4KvP4MiwsDzghMejwvXIm-ujXMFfvKWUoC3NCw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "52Y5NhPBJLzFcqncGX5pFDsfe2yG1PPnE-tvtF795JGTkvxgQnwI9Sec1z9sk71OND6CP-HIfYnVDmhe6uaJAw" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload fct field is not a JSON object", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiZmN0IjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.xHfMIVdVRnJfw5dePQgPLEb8uWAo8QrKufd_JsPsYD0RwXV4HEuC-x46NfZxNo9sQwFhuKBT-6xmNgV2CZODBA", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "uhk0MI1CyB3nEu4RxZqoOq3-BucWT86UXtP_th9ffa_uosmj6Aln3AUELkqJDsgr710UguNKQQzJVzmIdPoLCg" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload cap field is not a JSON object", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6bnVsbCwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.YRrWLF6guHy1BgFTAgTWNA1naDGdsbpbP3Y9RwuC1nHOPephImKtObLDqxg5cSJZindU0YsgYviszuJ7H-TODQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null + }, + "signature": "mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload cap field ability for resource is not a JSON object", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjpudWxsfSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.Q4zXZ0HHWCBFa5CpgQLYK42FI5-aSdjiGNSGAmy9t4Tsbnbs3yv4mj4u9TJJR6ZrCOEmhq3Z-th6aVN_GOvADg", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null + }, + "signature": "mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload cap field caveat is not an array", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOm51bGx9fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.YQrHUn5KXeZd5TcI7n1xjMueErLV5M3CIOLVYybgKlCrgHWOanQclif63cLgFCUSKws7XpmzZfdqSQtUDphSDA", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null + }, + "signature": "mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload cap field caveat is an empty array", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOltdfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.kld4oDIgz6sSf3H7iP3Yi_xKfdzjx6MFBpW4IWEge2_1hXCac2hsfJ094kNkZD8FZ7TuF_LPETpb-dgSlqowBw", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null + }, + "signature": "mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload prf field is not an array", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOnt9LCJ1Y3YiOiIwLjEwLjAifQ.Jeqh3JbtKDug0GgHyc7B6BvpQK4uu96V-FCbO549c3_RDqlK-V44xC4SJ0KLjiRmIDwBu7nLVFWdwItrmQObCA", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "uG2KDLAkshoiEV_vd1k4XTiI3j3xlMF0KWh6Upsxyve5SCLNnSk7AeVYcgjoKqI1TQoBUVRugVBEjhW1eIHVCw" + }, + "errors": [ + "incorrectType" + ] + }, + { + "name": "UCAN payload prf field is not an array of CIDs", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJwcmYiOlsid2UiLCJwcm92ZSIsIm5vdGhpbmciXSwidWN2IjoiMC4xMC4wIn0.rmsYqCZqa4ugeJz0pYDfI1ZqHIvRYHygL-kj-4SZUyPxMffAcU_WK4txhrEPgXnrtsGJsDYH83qoxN1Zs2bAAQ", + "proofs": {} + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi", + "aud": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "exp": null, + "cap": {} + }, + "signature": "uG2KDLAkshoiEV_vd1k4XTiI3j3xlMF0KWh6Upsxyve5SCLNnSk7AeVYcgjoKqI1TQoBUVRugVBEjhW1eIHVCw" + }, + "errors": [ + "incorrectProofs" + ] + }, + { + "name": "UCAN issuer does not match proof audience", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1rdGFmWlRSRWpKa3ZWNW1mSnhjTHBOQm9WUHdETGhUdU1nOW5nN2RZNHpNQUwiLCJwcmYiOlsiYmFma3JlaWJ2djQzenQ1ZGZ6d3I1ZWptczNvZWI1cmthenVmemt2cXM2djNpend3bGo2NGptcmNoY2kiXSwidWN2IjoiMC4xMC4wIn0.x4AuOHBAlXipWtkYdjwdp_u6uOUlBc_sQHYN76bwXqfOFxc3XiKQQDvk-Gi9GsqZbAo86u6NAXJUrDWHuIkeCw", + "proofs": { + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "cap": {}, + "prf": [ + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci" + ] + }, + "signature": "0BPu7MCETzLUwNqJdmw-D0CTQcXOrXaxJrRr-ONV0LG7e_P5ZkH6K8Et6k6lRp5JL7VhrnD2W1bT6lD2PbC_Cw" + }, + "errors": [ + "invalidDelegation" + ] + }, + { + "name": "UCAN claims a capability that has not been delegated", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInByZiI6WyJiYWZrcmVpYnZ2NDN6dDVkZnp3cjVlam1zM29lYjVya2F6dWZ6a3ZxczZ2M2l6d3dsajY0am1yY2hjaSJdLCJ1Y3YiOiIwLjEwLjAifQ.EdZXPSt8GxcmQu2_5IUVi9XZ5x2-bT_7AaCbGJTZ2q_X_5_9jjE_vd8MhaxnL7RfMIoHUgzVb6JYEZvlow8JDw", + "proofs": { + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "prf": [ + "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci" + ] + }, + "signature": "EdZXPSt8GxcmQu2_5IUVi9XZ5x2-bT_7AaCbGJTZ2q_X_5_9jjE_vd8MhaxnL7RfMIoHUgzVb6JYEZvlow8JDw" + }, + "errors": [ + "invalidDelegation" + ] + }, + { + "name": "UCAN escalates by adding a new caveat", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIiwibWFya2V0aW5nIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2ZmRFpDa0NUV3JlZzg4NjhmRzFGR0ZvZ2NKajVYNlBZOTNwUGNXRG45Ym9iIiwicHJmIjpbImJhZmtyZWloY2ZhcGE2bDMyd256dWthemxpb2FzcHR5MzY1dWh2ZXgzNm9zdGN3amNqN2JyZHZ1eWZxIl0sInVjdiI6IjAuMTAuMCJ9.AQmbtPT0n4SGlRaSY7QFH1kIDl3qglPbcSCtb5AaWpDYqvIhHtIzSvwxKfbrVR2pPs-I5oD2iuhcHivD6OpxCw", + "proofs": { + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f4QEc5oN43eUKxUfYuQ9zrjz3A6jiW6XPQCALv9RQ4QWnt4LvNy53gX3Z53lHc_-Ei8ykn4YUSGM3qL5AtdSBA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "prf": [ + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq" + ] + }, + "signature": "AQmbtPT0n4SGlRaSY7QFH1kIDl3qglPbcSCtb5AaWpDYqvIhHtIzSvwxKfbrVR2pPs-I5oD2iuhcHivD6OpxCw" + }, + "errors": [ + "invalidDelegation" + ] + }, + { + "name": "UCAN escalates to no caveats", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsInByZiI6WyJiYWZrcmVpaGNmYXBhNmwzMnduenVrYXpsaW9hc3B0eTM2NXVodmV4MzZvc3Rjd2pjajdicmR2dXlmcSJdLCJ1Y3YiOiIwLjEwLjAifQ.WwdZ21MV5RW2h_-ROJUVAM2EyeEgtc1KSLNkFUS9Vi2ieeDSImt3TuQ920rsHoE4k7FTiWJ7xoLlXAPSLs2SCQ", + "proofs": { + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f4QEc5oN43eUKxUfYuQ9zrjz3A6jiW6XPQCALv9RQ4QWnt4LvNy53gX3Z53lHc_-Ei8ykn4YUSGM3qL5AtdSBA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "prf": [ + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq" + ] + }, + "signature": "WwdZ21MV5RW2h_-ROJUVAM2EyeEgtc1KSLNkFUS9Vi2ieeDSImt3TuQ920rsHoE4k7FTiWJ7xoLlXAPSLs2SCQ" + }, + "errors": [ + "invalidDelegation" + ] + }, + { + "name": "UCAN escalates by adding a different caveat", + "task": "refute", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWt0YWZaVFJFakprdlY1bWZKeGNMcE5Cb1ZQd0RMaFR1TWc5bmc3ZFk0ek1BTCIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJtYXJrZXRpbmciXX1dfX0sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJwcmYiOlsiYmFma3JlaWhjZmFwYTZsMzJ3bnp1a2F6bGlvYXNwdHkzNjV1aHZleDM2b3N0Y3dqY2o3YnJkdnV5ZnEiXSwidWN2IjoiMC4xMC4wIn0.b1xIr3VnJFcEqljPB4mTG5poRLR6JiPiY_h_Lk-nxzyaEk_JBGmZpj7_imeCHbfyXrnlDBXjRBZ3zKT4y3VlDQ", + "proofs": { + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f4QEc5oN43eUKxUfYuQ9zrjz3A6jiW6XPQCALv9RQ4QWnt4LvNy53gX3Z53lHc_-Ei8ykn4YUSGM3qL5AtdSBA" + } + }, + "assertions": { + "header": { + "alg": "EdDSA", + "typ": "JWT" + }, + "payload": { + "ucv": "0.10.0", + "iss": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "aud": "did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL", + "exp": null, + "prf": [ + "bafkreihcfapa6l32wnzukazlioaspty365uhvex36ostcwjcj7brdvuyfq" + ] + }, + "signature": "b1xIr3VnJFcEqljPB4mTG5poRLR6JiPiY_h_Lk-nxzyaEk_JBGmZpj7_imeCHbfyXrnlDBXjRBZ3zKT4y3VlDQ" + }, + "errors": [ + "invalidDelegation" + ] + }, + { + "name": "UCAN has an expiration", + "task": "build", + "inputs": { + "version": "0.10.0", + "issuer_base64_key": "U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==", + "signature_scheme": "Ed25519", + "audience": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "expiration": 9246211200, + "capabilities": {} + }, + "outputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg" + } + }, + { + "name": "UCAN has a not before", + "task": "build", + "inputs": { + "version": "0.10.0", + "issuer_base64_key": "U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==", + "signature_scheme": "Ed25519", + "audience": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "not_before": 1, + "expiration": null, + "capabilities": {} + }, + "outputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJuYmYiOjEsInVjdiI6IjAuMTAuMCJ9.-yaM1x8v4jIvi5ldLsjN3unAJiaFx2D1gl4z_Ct8OCcS_afEW-q8phwyOVu3DKFP8dGoEvlMQMhTfPsiUOCsAQ" + } + }, + { + "name": "UCAN delegates send email capability", + "task": "build", + "inputs": { + "version": "0.10.0", + "issuer_base64_key": "U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==", + "signature_scheme": "Ed25519", + "audience": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "expiration": null, + "capabilities": { + "mailto:alice@email.com": { + "email/send": [ + {} + ] + } + } + }, + "outputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7fV19fSwiZXhwIjpudWxsLCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.mWmgl4OyAl_OKCB0tYBaxbw_0MkR2jM0W_G6eH8OW39IuB9y9ArbBcCSnG7r0WdeZaJBh6Qf4MxLiuSKM3ZFCA" + } + }, + { + "name": "UCAN delegates send email capability with newsletter template caveat", + "task": "build", + "inputs": { + "version": "0.10.0", + "issuer_base64_key": "U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==", + "signature_scheme": "Ed25519", + "audience": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "expiration": null, + "capabilities": { + "mailto:alice@email.com": { + "email/send": [ + { + "templates": [ + "newsletter" + ] + } + ] + } + } + }, + "outputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6eyJtYWlsdG86YWxpY2VAZW1haWwuY29tIjp7ImVtYWlsL3NlbmQiOlt7InRlbXBsYXRlcyI6WyJuZXdzbGV0dGVyIl19XX19LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwidWN2IjoiMC4xMC4wIn0.f4QEc5oN43eUKxUfYuQ9zrjz3A6jiW6XPQCALv9RQ4QWnt4LvNy53gX3Z53lHc_-Ei8ykn4YUSGM3qL5AtdSBA" + } + }, + { + "name": "UCAN has a fact with a challenge", + "task": "build", + "inputs": { + "version": "0.10.0", + "issuer_base64_key": "U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==", + "signature_scheme": "Ed25519", + "audience": "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob", + "expiration": null, + "facts": { + "challenge": "abcdef" + }, + "capabilities": {} + }, + "outputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiZmN0Ijp7ImNoYWxsZW5nZSI6ImFiY2RlZiJ9LCJpc3MiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSIsInVjdiI6IjAuMTAuMCJ9.uhk0MI1CyB3nEu4RxZqoOq3-BucWT86UXtP_th9ffa_uosmj6Aln3AUELkqJDsgr710UguNKQQzJVzmIdPoLCg" + } + }, + { + "name": "Compute CID for token using SHA2-256 hasher", + "task": "toCID", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA", + "hasher": "SHA2-256" + }, + "outputs": { + "cid": "bafkreibvv43zt5dfzwr5ejms3oeb5rkazufzkvqs6v3izwwlj64jmrchci" + } + }, + { + "name": "Compute CID for token using BLAKE3-256 hasher", + "task": "toCID", + "inputs": { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6bnVsbCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.MkBYb77b19pn8fCODCMYpqNTs5_neWBsNHKL73U68S1w3sj0RCllCoHq-Ih-rrFsNvNWSSyOQN3ZC_nN966BAA", + "hasher": "BLAKE3-256" + }, + "outputs": { + "cid": "bafkr4icyq2ikdjxba7bytuxjjgxfhouzdbwutrqio77cb5logrg7osnsli" + } + } +] diff --git a/tests/rs_ucan.test.js b/tests/rs_ucan.test.js new file mode 100644 index 00000000..f8385e26 --- /dev/null +++ b/tests/rs_ucan.test.js @@ -0,0 +1,116 @@ +import assert from "assert"; +import { build, decode } from "../dist/bundler/ucan.js"; + +describe("decode", async function () { + let ucan = await decode( + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6a2V5Ono2TWtmZkRaQ2tDVFdyZWc4ODY4ZkcxRkdGb2djSmo1WDZQWTkzcFBjV0RuOWJvYiIsImNhcCI6e30sImV4cCI6OTI0NjIxMTIwMCwiaXNzIjoiZGlkOmtleTp6Nk1razg5YkMzSnJWcUtpZTcxWUVjYzVNMVNNVnh1Q2dOeDZ6TFo4U1lKc3hBTGkiLCJ1Y3YiOiIwLjEwLjAifQ.pkJxQke-FDVB1Eg_7Jh2socNBKgo6_0OF1XXRfRMazmpXBG37tScYGAzJKB2Z4RFvSBpbBu29Sozrv4GQLFrDg", + ); + + it("decodes the signature", async function () { + let actual = ucan.signature; + let expected = new Uint8Array([ + 166, 66, 113, 66, 71, 190, 20, 53, 65, 212, 72, 63, 236, 152, 118, 178, + 135, 13, 4, 168, 40, 235, 253, 14, 23, 85, 215, 69, 244, 76, 107, 57, 169, + 92, 17, 183, 238, 212, 156, 96, 96, 51, 36, 160, 118, 103, 132, 69, 189, + 32, 105, 108, 27, 182, 245, 42, 51, 174, 254, 6, 64, 177, 107, 14, + ]); + + assert.equal(actual.byteLength, expected.byteLength); + assert.ok(actual.every((v, i) => v === expected[i])); + }); + + it("decodes the typ", async function () { + let actual = ucan.typ; + let expected = "JWT"; + + assert.equal(actual, expected); + }); + + it("decodes the alg", async function () { + let actual = ucan.algorithm; + let expected = "EdDSA"; + + assert.equal(actual, expected); + }); + + it("decodes the iss", async function () { + let actual = ucan.issuer; + let expected = "did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi"; + + assert.equal(actual, expected); + }); + + it("decodes the aud", async function () { + let actual = ucan.audience; + let expected = "did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob"; + + assert.equal(actual, expected); + }); + + it("decodes the exp", async function () { + let actual = ucan.expiresAt.getTime(); + let expected = new Date(9246211200 * 1000).getTime(); + + assert.equal(actual, expected); + }); + + it("decodes the nbf", async function () { + let actual = ucan.notBefore; + let expected = null; + + assert.equal(actual, expected); + }); + + it("decodes the nnc", async function () { + let actual = ucan.nonce; + let expected = null; + + assert.equal(actual, expected); + }); + + it("decodes the facts", async function () { + let actual = ucan.facts; + let expected = null; + + assert.equal(actual, expected); + }); + + it("decodes the ucn", async function () { + let actual = ucan.version; + let expected = "0.10.0"; + + assert.equal(actual, expected); + }); + + it("preserves the CID", async function () { + let actual = ucan.cid(); + let expected = + "bafkreifmws7u5w6nluprxu5zcsun2wcp7rioxjy2qem6pjj5z367dp64li"; + + assert.equal(actual, expected); + }); +}); + +describe("build", async function () { + const RSA_ALG = "RSASSA-PKCS1-v1_5"; + const DEFAULT_KEY_SIZE = 2048; + const DEFAULT_HASH_ALG = "SHA-256"; + const SALT_LEGNTH = 128; + + it("builds the ucan", async function () { + let keypair = await crypto.subtle.generateKey( + { + name: RSA_ALG, + modulusLength: DEFAULT_KEY_SIZE, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: DEFAULT_HASH_ALG }, + }, + false, + ["sign", "verify"], + ); + + let ucan = await build(keypair, "did:key:test", {}); + + assert.equal(ucan.signature, null); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..b6828dc2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "module": "es2015", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2015" + ] + } +} diff --git a/ucan-key-support/CHANGELOG.md b/ucan-key-support/CHANGELOG.md deleted file mode 100644 index 43566890..00000000 --- a/ucan-key-support/CHANGELOG.md +++ /dev/null @@ -1,67 +0,0 @@ -# Changelog - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.1.2 to 0.2.0 - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.2.0 to 0.3.0 - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.3.0 to 0.3.1 - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.3.1 to 0.3.2 - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.3.2 to 0.4.0 - -## [0.1.2](https://github.com/ucan-wg/rs-ucan/compare/ucan-key-support-v0.1.1...ucan-key-support-v0.1.2) (2023-04-22) - - -### Features - -* Upgrade deps: `cid`, `libipld`, `base64`, `p256`, `rsa` ([#78](https://github.com/ucan-wg/rs-ucan/issues/78)) ([cfeed69](https://github.com/ucan-wg/rs-ucan/commit/cfeed6903d9a53d3728f35914d670e3b7920d88d)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.1.1 to 0.1.2 - -## [0.1.1](https://github.com/ucan-wg/rs-ucan/compare/ucan-key-support-v0.1.0...ucan-key-support-v0.1.1) (2023-03-13) - - -### Features - -* ucan-key-support: add P-256 key support (aka ESRSA, aka secp256r1) ([#46](https://github.com/ucan-wg/rs-ucan/issues/46)) ([36fe961](https://github.com/ucan-wg/rs-ucan/commit/36fe9617513a25c7815772204a9426e0ca75ef7e)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.1.0 to 0.1.1 - -## [0.1.0](https://github.com/ucan-wg/rs-ucan/compare/ucan-key-support-v0.1.0...ucan-key-support-v0.1.0) (2022-11-29) - - -### ⚠ BREAKING CHANGES - -* New version requirements include rsa@0.7 - -### Miscellaneous Chores - -* rsa dep changes ([#58](https://github.com/ucan-wg/rs-ucan/issues/58)) ([ecde6ff](https://github.com/ucan-wg/rs-ucan/commit/ecde6ffce6ad07c1ccb1c9d2257a3f7650189afc)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * ucan bumped from 0.7.0-alpha.1 to 0.1.0 diff --git a/ucan-key-support/Cargo.toml b/ucan-key-support/Cargo.toml deleted file mode 100644 index e8cbe332..00000000 --- a/ucan-key-support/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "ucan-key-support" -description = "Ready to use SigningKey implementations for the ucan crate" -edition = "2021" -keywords = ["ucan", "authz", "jwt", "pki"] -categories = [ - "authorization", - "cryptography", - "encoding", - "web-programming" -] -documentation = "https://docs.rs/ucan" -repository = "https://github.com/cdata/rs-ucan/" -homepage = "https://github.com/cdata/rs-ucan" -license = "Apache-2.0" -readme = "README.md" -version = "0.1.7" - -[features] -default = [] - -[dependencies] -anyhow = "1.0" -async-trait = "0.1" -bs58 = "0.5" -ed25519-zebra = "3.1" -log = "0.4" -rsa = "0.9" -p256 = "0.13" -sha2 = { version = "0.10", features = ["oid"] } -ucan = { path = "../ucan", version = "0.4.0" } - -[build-dependencies] -npm_rs = "1.0" - -[dev-dependencies] -# NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "0.2", features = ["js"] } -rand = "0.8" -wasm-bindgen-test = "0.3" - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { version = "1.21", features = ["macros", "rt"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = { version = "0.2" } -wasm-bindgen-futures = { version = "0.4" } -js-sys = { version = "0.3" } - -[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] -version = "0.3" -features = [ - 'Window', - 'SubtleCrypto', - 'Crypto', - 'CryptoKey', - 'CryptoKeyPair', - 'DedicatedWorkerGlobalScope' -] - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -pollster = "0.3.0" diff --git a/ucan-key-support/README.md b/ucan-key-support/README.md deleted file mode 100644 index 4f09b28b..00000000 --- a/ucan-key-support/README.md +++ /dev/null @@ -1,36 +0,0 @@ - - - -## - -This is an auxilliary crate containing ready-to-use `SigningKey` implementations -for the [Rust UCAN implementation][rs-ucan]. - -[rs-ucan]: https://docs.rs/ucan diff --git a/ucan-key-support/src/ed25519.rs b/ucan-key-support/src/ed25519.rs deleted file mode 100644 index 4de05858..00000000 --- a/ucan-key-support/src/ed25519.rs +++ /dev/null @@ -1,92 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; - -use ed25519_zebra::{ - Signature, SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey, -}; - -use ucan::crypto::KeyMaterial; - -pub use ucan::crypto::{did::ED25519_MAGIC_BYTES, JwtSignatureAlgorithm}; - -pub fn bytes_to_ed25519_key(bytes: Vec) -> Result> { - let public_key = Ed25519PublicKey::try_from(bytes.as_slice())?; - Ok(Box::new(Ed25519KeyMaterial(public_key, None))) -} - -#[derive(Clone)] -pub struct Ed25519KeyMaterial(pub Ed25519PublicKey, pub Option); - -#[cfg_attr(target_arch="wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl KeyMaterial for Ed25519KeyMaterial { - fn get_jwt_algorithm_name(&self) -> String { - JwtSignatureAlgorithm::EdDSA.to_string() - } - - async fn get_did(&self) -> Result { - let bytes = [ED25519_MAGIC_BYTES, self.0.as_ref()].concat(); - Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) - } - - async fn sign(&self, payload: &[u8]) -> Result> { - match self.1 { - Some(private_key) => { - let signature = private_key.sign(payload); - let bytes: [u8; 64] = signature.into(); - Ok(bytes.to_vec()) - } - None => Err(anyhow!("No private key; cannot sign data")), - } - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - let signature = Signature::try_from(signature)?; - self.0 - .verify(&signature, payload) - .map_err(|error| anyhow!("Could not verify signature: {:?}", error)) - } -} - -#[cfg(test)] -mod tests { - use super::{bytes_to_ed25519_key, Ed25519KeyMaterial, ED25519_MAGIC_BYTES}; - use ed25519_zebra::{SigningKey as Ed25519PrivateKey, VerificationKey as Ed25519PublicKey}; - use ucan::{ - builder::UcanBuilder, - crypto::{did::DidParser, KeyMaterial}, - ucan::Ucan, - }; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_sign_and_verify_a_ucan() { - let rng = rand::thread_rng(); - let private_key = Ed25519PrivateKey::new(rng); - let public_key = Ed25519PublicKey::from(&private_key); - - let key_material = Ed25519KeyMaterial(public_key, Some(private_key)); - let token_string = UcanBuilder::default() - .issued_by(&key_material) - .for_audience(key_material.get_did().await.unwrap().as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut did_parser = DidParser::new(&[(ED25519_MAGIC_BYTES, bytes_to_ed25519_key)]); - - let ucan = Ucan::try_from(token_string).unwrap(); - ucan.check_signature(&mut did_parser).await.unwrap(); - } -} diff --git a/ucan-key-support/src/fixtures/rsa_key.pk8 b/ucan-key-support/src/fixtures/rsa_key.pk8 deleted file mode 100644 index 1af064cc..00000000 Binary files a/ucan-key-support/src/fixtures/rsa_key.pk8 and /dev/null differ diff --git a/ucan-key-support/src/lib.rs b/ucan-key-support/src/lib.rs deleted file mode 100644 index e890a1b5..00000000 --- a/ucan-key-support/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[macro_use] -extern crate log; - -#[cfg(target_arch = "wasm32")] -pub mod web_crypto; - -pub mod ed25519; -pub mod p256; -pub mod rsa; diff --git a/ucan-key-support/src/p256.rs b/ucan-key-support/src/p256.rs deleted file mode 100644 index 10d2905f..00000000 --- a/ucan-key-support/src/p256.rs +++ /dev/null @@ -1,86 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; - -use p256::ecdsa::{ - self, - signature::{Signer, Verifier}, - Signature, SigningKey as P256PrivateKey, VerifyingKey as P256PublicKey, -}; - -use ucan::crypto::KeyMaterial; - -pub use ucan::crypto::{did::P256_MAGIC_BYTES, JwtSignatureAlgorithm}; - -pub fn bytes_to_p256_key(bytes: Vec) -> Result> { - let public_key = P256PublicKey::try_from(bytes.as_slice())?; - Ok(Box::new(P256KeyMaterial(public_key, None))) -} - -/// Support for NIST P-256 keys, aka secp256r1, aka ES256 -#[derive(Clone)] -pub struct P256KeyMaterial(pub P256PublicKey, pub Option); - -#[cfg_attr(target_arch="wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl KeyMaterial for P256KeyMaterial { - fn get_jwt_algorithm_name(&self) -> String { - JwtSignatureAlgorithm::ES256.to_string() - } - - async fn get_did(&self) -> Result { - let bytes = [P256_MAGIC_BYTES, &self.0.to_encoded_point(true).to_bytes()].concat(); - Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) - } - - async fn sign(&self, payload: &[u8]) -> Result> { - match self.1 { - Some(ref private_key) => { - let signature: ecdsa::Signature = private_key.sign(payload); - Ok(signature.to_vec()) - } - None => Err(anyhow!("No private key; cannot sign data")), - } - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - let signature = Signature::try_from(signature)?; - self.0 - .verify(payload, &signature) - .map_err(|error| anyhow!("Could not verify signature: {:?}", error)) - } -} - -#[cfg(test)] -mod tests { - use super::{bytes_to_p256_key, P256KeyMaterial, P256_MAGIC_BYTES}; - use p256::ecdsa::{SigningKey as P256PrivateKey, VerifyingKey as P256PublicKey}; - use ucan::{ - builder::UcanBuilder, - crypto::{did::DidParser, KeyMaterial}, - ucan::Ucan, - }; - - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_sign_and_verify_a_ucan() { - let private_key = P256PrivateKey::random(&mut p256::elliptic_curve::rand_core::OsRng); - let public_key = P256PublicKey::from(&private_key); - - let key_material = P256KeyMaterial(public_key, Some(private_key)); - let token_string = UcanBuilder::default() - .issued_by(&key_material) - .for_audience(key_material.get_did().await.unwrap().as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut did_parser = DidParser::new(&[(P256_MAGIC_BYTES, bytes_to_p256_key)]); - - let ucan = Ucan::try_from(token_string).unwrap(); - ucan.check_signature(&mut did_parser).await.unwrap(); - } -} diff --git a/ucan-key-support/src/rsa.rs b/ucan-key-support/src/rsa.rs deleted file mode 100644 index 29219115..00000000 --- a/ucan-key-support/src/rsa.rs +++ /dev/null @@ -1,114 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; - -use rsa::{ - pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}, - Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, -}; - -use sha2::{Digest, Sha256}; -use ucan::crypto::{JwtSignatureAlgorithm, KeyMaterial}; - -pub use ucan::crypto::did::RSA_MAGIC_BYTES; - -pub fn bytes_to_rsa_key(bytes: Vec) -> Result> { - println!("Trying to parse RSA key..."); - // NOTE: DID bytes are PKCS1, but we store RSA keys as PKCS8 - let public_key = RsaPublicKey::from_pkcs1_der(&bytes)?; - - Ok(Box::new(RsaKeyMaterial(public_key, None))) -} - -#[derive(Clone)] -pub struct RsaKeyMaterial(pub RsaPublicKey, pub Option); - -#[cfg_attr(target_arch="wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl KeyMaterial for RsaKeyMaterial { - fn get_jwt_algorithm_name(&self) -> String { - JwtSignatureAlgorithm::RS256.to_string() - } - - async fn get_did(&self) -> Result { - let bytes = match self.0.to_pkcs1_der() { - Ok(document) => [RSA_MAGIC_BYTES, document.as_bytes()].concat(), - Err(error) => { - // TODO: Probably shouldn't swallow this error... - warn!("Could not get RSA public key bytes for DID: {:?}", error); - Vec::new() - } - }; - Ok(format!("did:key:z{}", bs58::encode(bytes).into_string())) - } - - async fn sign(&self, payload: &[u8]) -> Result> { - let mut hasher = Sha256::new(); - hasher.update(payload); - let hashed = hasher.finalize(); - - match &self.1 { - Some(private_key) => { - let padding = Pkcs1v15Sign::new::(); - let signature = private_key.sign(padding, hashed.as_ref())?; - info!("SIGNED!"); - Ok(signature) - } - None => Err(anyhow!("No private key; cannot sign data")), - } - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - let mut hasher = Sha256::new(); - hasher.update(payload); - let hashed = hasher.finalize(); - let padding = Pkcs1v15Sign::new::(); - - self.0 - .verify(padding, hashed.as_ref(), signature) - .map_err(|error| anyhow!(error)) - } -} - -#[cfg(test)] -mod tests { - use super::{bytes_to_rsa_key, RsaKeyMaterial, RSA_MAGIC_BYTES}; - - use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey, RsaPublicKey}; - use ucan::{ - builder::UcanBuilder, - crypto::{did::DidParser, KeyMaterial}, - ucan::Ucan, - }; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_sign_and_verify_a_ucan() { - let private_key = - RsaPrivateKey::from_pkcs8_der(include_bytes!("./fixtures/rsa_key.pk8")).unwrap(); - let public_key = RsaPublicKey::from(&private_key); - - let key_material = RsaKeyMaterial(public_key, Some(private_key)); - let token_string = UcanBuilder::default() - .issued_by(&key_material) - .for_audience(key_material.get_did().await.unwrap().as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); - - let ucan = Ucan::try_from(token_string).unwrap(); - ucan.check_signature(&mut did_parser).await.unwrap(); - } -} diff --git a/ucan-key-support/src/web_crypto.rs b/ucan-key-support/src/web_crypto.rs deleted file mode 100644 index 4d1ea625..00000000 --- a/ucan-key-support/src/web_crypto.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::rsa::RsaKeyMaterial; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use js_sys::{Array, ArrayBuffer, Boolean, Object, Reflect, Uint8Array}; -use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey}; -use ucan::crypto::{JwtSignatureAlgorithm, KeyMaterial}; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Crypto, CryptoKey, CryptoKeyPair, SubtleCrypto}; - -pub fn convert_spki_to_rsa_public_key(spki_bytes: &[u8]) -> Result> { - // TODO: This is maybe a not-good, hacky solution; verifying the first - // 24 bytes would be more wholesome - // SEE: https://github.com/ucan-wg/ts-ucan/issues/30#issuecomment-1007333500 - Ok(Vec::from(&spki_bytes[24..])) -} - -pub const WEB_CRYPTO_RSA_ALGORITHM: &str = "RSASSA-PKCS1-v1_5"; - -#[derive(Debug)] -pub struct WebCryptoRsaKeyMaterial(pub CryptoKey, pub Option); - -impl WebCryptoRsaKeyMaterial { - fn get_subtle_crypto() -> Result { - // NOTE: Accessing either `Window` or `DedicatedWorkerGlobalScope` in - // a context where they are not defined will cause a JS error, so we - // do a sneaky workaround here: - let global = js_sys::global(); - match Reflect::get(&global, &JsValue::from("crypto")) { - Ok(value) => Ok(value.dyn_into::().expect("Unexpected API").subtle()), - _ => Err(anyhow!("Could not access WebCrypto API")), - } - } - - fn private_key(&self) -> Result<&CryptoKey> { - match &self.1 { - Some(key) => Ok(key), - None => Err(anyhow!("No private key configured")), - } - } - - pub async fn generate(key_size: Option) -> Result { - let subtle_crypto = Self::get_subtle_crypto()?; - let algorithm = Object::new(); - - Reflect::set( - &algorithm, - &JsValue::from("name"), - &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - Reflect::set( - &algorithm, - &JsValue::from("modulusLength"), - &JsValue::from(key_size.unwrap_or(2048)), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - let public_exponent = Uint8Array::new(&JsValue::from(3u8)); - public_exponent.copy_from(&[0x01u8, 0x00, 0x01]); - - Reflect::set( - &algorithm, - &JsValue::from("publicExponent"), - &JsValue::from(public_exponent), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - let hash = Object::new(); - - Reflect::set(&hash, &JsValue::from("name"), &JsValue::from("SHA-256")) - .map_err(|error| anyhow!("{:?}", error))?; - - Reflect::set(&algorithm, &JsValue::from("hash"), &JsValue::from(hash)) - .map_err(|error| anyhow!("{:?}", error))?; - - let uses = Array::new(); - - uses.push(&JsValue::from("sign")); - uses.push(&JsValue::from("verify")); - - let crypto_key_pair_generates = subtle_crypto - .generate_key_with_object(&algorithm, false, &uses) - .map_err(|error| anyhow!("{:?}", error))?; - let crypto_key_pair = CryptoKeyPair::from( - JsFuture::from(crypto_key_pair_generates) - .await - .map_err(|error| anyhow!("{:?}", error))?, - ); - - let public_key = CryptoKey::from( - Reflect::get(&crypto_key_pair, &JsValue::from("publicKey")) - .map_err(|error| anyhow!("{:?}", error))?, - ); - let private_key = CryptoKey::from( - Reflect::get(&crypto_key_pair, &JsValue::from("privateKey")) - .map_err(|error| anyhow!("{:?}", error))?, - ); - - Ok(WebCryptoRsaKeyMaterial(public_key, Some(private_key))) - } -} - -#[async_trait(?Send)] -impl KeyMaterial for WebCryptoRsaKeyMaterial { - fn get_jwt_algorithm_name(&self) -> String { - JwtSignatureAlgorithm::RS256.to_string() - } - - async fn get_did(&self) -> Result { - let public_key = &self.0; - let subtle_crypto = Self::get_subtle_crypto()?; - - let public_key_bytes = Uint8Array::new( - &JsFuture::from( - subtle_crypto - .export_key("spki", public_key) - .expect("Could not access key extraction API"), - ) - .await - .expect("Failed to extract public key bytes") - .dyn_into::() - .expect("Bytes were not an ArrayBuffer"), - ); - - let public_key_bytes = public_key_bytes.to_vec(); - let public_key_bytes = convert_spki_to_rsa_public_key(public_key_bytes.as_slice())?; - let public_key = RsaPublicKey::from_pkcs1_der(&public_key_bytes)?; - - Ok(RsaKeyMaterial(public_key, None).get_did().await?) - } - - async fn sign(&self, payload: &[u8]) -> Result> { - let key = self.private_key()?; - let subtle_crypto = Self::get_subtle_crypto()?; - let algorithm = Object::new(); - - Reflect::set( - &algorithm, - &JsValue::from("name"), - &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - Reflect::set( - &algorithm, - &JsValue::from("saltLength"), - &JsValue::from(128u8), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - let data = unsafe { Uint8Array::view(payload) }; - - let result = Uint8Array::new( - &JsFuture::from( - subtle_crypto - .sign_with_object_and_buffer_source(&algorithm, key, &data) - .map_err(|error| anyhow!("{:?}", error))?, - ) - .await - .map_err(|error| anyhow!("{:?}", error))?, - ); - - Ok(result.to_vec()) - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - let key = &self.0; - let subtle_crypto = Self::get_subtle_crypto()?; - let algorithm = Object::new(); - - Reflect::set( - &algorithm, - &JsValue::from("name"), - &JsValue::from(WEB_CRYPTO_RSA_ALGORITHM), - ) - .map_err(|error| anyhow!("{:?}", error))?; - Reflect::set( - &algorithm, - &JsValue::from("saltLength"), - &JsValue::from(128u8), - ) - .map_err(|error| anyhow!("{:?}", error))?; - - let signature = unsafe { Uint8Array::view(signature) }; - let data = unsafe { Uint8Array::view(payload) }; - - let valid = JsFuture::from( - subtle_crypto - .verify_with_object_and_buffer_source_and_buffer_source( - &algorithm, key, &signature, &data, - ) - .map_err(|error| anyhow!("{:?}", error))?, - ) - .await - .map_err(|error| anyhow!("{:?}", error))? - .dyn_into::() - .map_err(|error| anyhow!("{:?}", error))?; - - match valid.is_truthy() { - true => Ok(()), - false => Err(anyhow!("Could not verify signature")), - } - } -} - -#[cfg(test)] -mod tests { - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - use super::WebCryptoRsaKeyMaterial; - use crate::rsa::{bytes_to_rsa_key, RSA_MAGIC_BYTES}; - use ucan::{ - builder::UcanBuilder, - crypto::{did::DidParser, KeyMaterial}, - ucan::Ucan, - }; - - #[wasm_bindgen_test] - async fn it_can_sign_and_verify_data() { - let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); - let data = &[0xdeu8, 0xad, 0xbe, 0xef]; - let signature = key_material.sign(data).await.unwrap(); - - key_material.verify(data, signature.as_ref()).await.unwrap(); - } - - #[wasm_bindgen_test] - async fn it_produces_a_legible_rsa_did() { - let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); - let did = key_material.get_did().await.unwrap(); - let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); - - did_parser.parse(&did).unwrap(); - } - - #[wasm_bindgen_test] - async fn it_signs_ucans_that_can_be_verified_elsewhere() { - let key_material = WebCryptoRsaKeyMaterial::generate(None).await.unwrap(); - - let token = UcanBuilder::default() - .issued_by(&key_material) - .for_audience(key_material.get_did().await.unwrap().as_str()) - .with_lifetime(300) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut did_parser = DidParser::new(&[(RSA_MAGIC_BYTES, bytes_to_rsa_key)]); - let ucan = Ucan::try_from(token.as_str()).unwrap(); - - ucan.check_signature(&mut did_parser).await.unwrap(); - } -} diff --git a/ucan-key-support/webdriver.json b/ucan-key-support/webdriver.json deleted file mode 100644 index 26fea6ca..00000000 --- a/ucan-key-support/webdriver.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "os": "Windows", - "os_version": "11", - "browser": "chrome", - "browser_version": "latest" -} diff --git a/ucan/CHANGELOG.md b/ucan/CHANGELOG.md deleted file mode 100644 index 6385fa94..00000000 --- a/ucan/CHANGELOG.md +++ /dev/null @@ -1,84 +0,0 @@ -# Changelog - -## [0.4.0](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.3.2...ucan-v0.4.0) (2023-06-27) - - -### ⚠ BREAKING CHANGES - -* Update capabilites in line with UCAN 0.9/0.10 specs ([#105](https://github.com/ucan-wg/rs-ucan/issues/105)) -* Update `fct`/`ucv` layout for 0.10.0 spec ([#108](https://github.com/ucan-wg/rs-ucan/issues/108)) -* Support generic hashers in `UcanBuilder` and `ProofChain`. ([#89](https://github.com/ucan-wg/rs-ucan/issues/89)) - -### Features - -* Allow nullable expiry, per 0.9.0 spec. Fixes [#23](https://github.com/ucan-wg/rs-ucan/issues/23) ([#95](https://github.com/ucan-wg/rs-ucan/issues/95)) ([12d4756](https://github.com/ucan-wg/rs-ucan/commit/12d475606da940b64654f17807adf592551982d0)) -* Support generic hashers in `UcanBuilder` and `ProofChain`. ([#89](https://github.com/ucan-wg/rs-ucan/issues/89)) ([e057f87](https://github.com/ucan-wg/rs-ucan/commit/e057f87c7b278d18e77b1d3d213656d18b1a2fee)) -* Update `fct`/`ucv` layout for 0.10.0 spec ([#108](https://github.com/ucan-wg/rs-ucan/issues/108)) ([ae19741](https://github.com/ucan-wg/rs-ucan/commit/ae197415048da201f7d75bf08cdb010b4f657895)) -* Update capabilites in line with UCAN 0.9/0.10 specs ([#105](https://github.com/ucan-wg/rs-ucan/issues/105)) ([0bdf98f](https://github.com/ucan-wg/rs-ucan/commit/0bdf98f9043e753026711fb19449ab0bc6d87fc7)) - -## [0.3.2](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.3.1...ucan-v0.3.2) (2023-05-25) - - -### Features - -* `fct` and `prf` are now optional fields. Fixes [#98](https://github.com/ucan-wg/rs-ucan/issues/98) ([#99](https://github.com/ucan-wg/rs-ucan/issues/99)) ([6802b5c](https://github.com/ucan-wg/rs-ucan/commit/6802b5c85ce2b16680baa86342e6154896712041)) - -## [0.3.1](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.3.0...ucan-v0.3.1) (2023-05-23) - - -### Features - -* add PartialEq, Eq traits to Ucan. Fixes [#90](https://github.com/ucan-wg/rs-ucan/issues/90) ([#91](https://github.com/ucan-wg/rs-ucan/issues/91)) ([27c3628](https://github.com/ucan-wg/rs-ucan/commit/27c36288fc47bd53ab6e8f4c3e8a596714dcc6ff)) - -## [0.3.0](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.2.0...ucan-v0.3.0) (2023-05-22) - - -### ⚠ BREAKING CHANGES - -* Migrate default hashing from blake2b to blake3. ([#85](https://github.com/ucan-wg/rs-ucan/issues/85)) -* Remove `stdweb` feature from instant crate to circumvent downstream issues with `stdweb/wasm-bindgen` ([#86](https://github.com/ucan-wg/rs-ucan/issues/86)) - -### Features - -* Migrate default hashing from blake2b to blake3. ([#85](https://github.com/ucan-wg/rs-ucan/issues/85)) ([205cb96](https://github.com/ucan-wg/rs-ucan/commit/205cb962fcc99814caac8e1b9d4f8ffd956eb184)) - - -### Bug Fixes - -* Remove `stdweb` feature from instant crate to circumvent downstream issues with `stdweb/wasm-bindgen` ([#86](https://github.com/ucan-wg/rs-ucan/issues/86)) ([67ec64d](https://github.com/ucan-wg/rs-ucan/commit/67ec64db527b8bfadc4a219a65b580bdbc459640)) - -## [0.2.0](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.1.2...ucan-v0.2.0) (2023-05-04) - - -### ⚠ BREAKING CHANGES - -* Custom 'now' for proof chain validation ([#83](https://github.com/ucan-wg/rs-ucan/issues/83)) - -### Features - -* Custom 'now' for proof chain validation ([#83](https://github.com/ucan-wg/rs-ucan/issues/83)) ([1732a89](https://github.com/ucan-wg/rs-ucan/commit/1732a8911b67546f446126e4d469126f61769b44)) - -## [0.1.2](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.1.1...ucan-v0.1.2) (2023-04-22) - - -### Features - -* Upgrade deps: `cid`, `libipld`, `base64`, `p256`, `rsa` ([#78](https://github.com/ucan-wg/rs-ucan/issues/78)) ([cfeed69](https://github.com/ucan-wg/rs-ucan/commit/cfeed6903d9a53d3728f35914d670e3b7920d88d)) - -## [0.1.1](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.1.0...ucan-v0.1.1) (2023-03-13) - - -### Features - -* More derives for use in other libs ([#75](https://github.com/ucan-wg/rs-ucan/issues/75)) ([e60715f](https://github.com/ucan-wg/rs-ucan/commit/e60715f94f3b15b27ae7c1443cd4abae983d93ae)) - -## [0.1.0](https://github.com/ucan-wg/rs-ucan/compare/ucan-v0.1.0...ucan-v0.1.0) (2022-11-29) - - -### ⚠ BREAKING CHANGES - -* New version requirements include `cid@0.9`, `libipld-core@0.15` and `libipld-json@0.15` - -### Miscellaneous Chores - -* Update IPLD-adjacent crates ([#55](https://github.com/ucan-wg/rs-ucan/issues/55)) ([bf55a3f](https://github.com/ucan-wg/rs-ucan/commit/bf55a3ffad0095d88c6b33b0cd6504e66918064a)) diff --git a/ucan/Cargo.toml b/ucan/Cargo.toml deleted file mode 100644 index 79967795..00000000 --- a/ucan/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "ucan" -description = "Implement UCAN-based authorization with conciseness and ease!" -keywords = ["ucan", "authz", "jwt", "pki"] -categories = [ - "authentication", - "cryptography", - "encoding", - "web-programming" -] -documentation = "https://docs.rs/ucan" -repository = "https://github.com/cdata/rs-ucan/" -homepage = "https://github.com/cdata/rs-ucan" -license = "Apache-2.0" -readme = "README.md" -version = "0.4.0" -edition = "2021" - -[features] -default = [] - -[dependencies] -anyhow = "1.0" -async-recursion = "1.0" -async-trait = "0.1" -base64 = "0.21" -bs58 = "0.5" -cid = "0.10" -futures = "0.3" -instant = { version = "0.1", features = ["wasm-bindgen"] } -libipld-core = { version = "0.16", features = ["serde-codec", "serde"] } -libipld-json = "0.16" -log = "0.4" -rand = "0.8" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -strum = "0.24" -strum_macros = "0.25" -unsigned-varint = "0.7" -url = "2.0" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -# NOTE: This is needed so that rand can be included in WASM builds -getrandom = { version = "~0.2", features = ["js"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { version = "^1", features = ["macros", "test-util"] } - -[dev-dependencies] -did-key = "0.2" -serde_ipld_dagcbor = "0.3" -wasm-bindgen-test = "0.3" diff --git a/ucan/LICENSE b/ucan/LICENSE deleted file mode 100644 index c61b6639..00000000 --- a/ucan/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/README.md b/ucan/README.md deleted file mode 100644 index 11fefd37..00000000 --- a/ucan/README.md +++ /dev/null @@ -1,39 +0,0 @@ -
- - rs-ucan Logo - - -

ucan

- -

- - Crate Information - - - Code Coverage - - - Build Status - - - License - - - Docs - - - Discord - -

-
- - -## - -This is a Rust library to help the next generation of web applications make use -of UCANs in their authorization flows. To learn more about UCANs and how you -might use them in your application, visit [https://ucan.xyz][ucan website] or -read the [spec][spec]. - -[spec]: https://github.com/ucan-wg/spec -[ucan website]: https://ucan.xyz diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs deleted file mode 100644 index 44fb9cb9..00000000 --- a/ucan/src/builder.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::collections::BTreeMap; - -use crate::{ - capability::{proof::ProofDelegationSemantics, Capability, CapabilitySemantics}, - crypto::KeyMaterial, - serde::Base64Encode, - time::now, - ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, -}; -use anyhow::{anyhow, Result}; -use base64::Engine; -use cid::multihash::Code; -use log::warn; -use rand::Rng; -use serde::{de::DeserializeOwned, Serialize}; - -/// A signable is a UCAN that has all the state it needs in order to be signed, -/// but has not yet been signed. -/// NOTE: This may be useful for bespoke signing flows down the road. It is -/// meant to approximate the way that ts-ucan produces an unsigned intermediate -/// artifact (e.g., ) -pub struct Signable<'a, K> -where - K: KeyMaterial, -{ - pub issuer: &'a K, - pub audience: String, - - pub capabilities: Vec, - - pub expiration: Option, - pub not_before: Option, - - pub facts: FactsMap, - pub proofs: Vec, - pub add_nonce: bool, -} - -impl<'a, K> Signable<'a, K> -where - K: KeyMaterial, -{ - /// The header field components of the UCAN JWT - pub fn ucan_header(&self) -> UcanHeader { - UcanHeader { - alg: self.issuer.get_jwt_algorithm_name(), - typ: "JWT".into(), - } - } - - /// The payload field components of the UCAN JWT - pub async fn ucan_payload(&self) -> Result { - let nonce = match self.add_nonce { - true => Some( - base64::engine::general_purpose::URL_SAFE_NO_PAD - .encode(rand::thread_rng().gen::<[u8; 32]>()), - ), - false => None, - }; - - let facts = if self.facts.is_empty() { - None - } else { - Some(self.facts.clone()) - }; - - let proofs = if self.proofs.is_empty() { - None - } else { - Some(self.proofs.clone()) - }; - - Ok(UcanPayload { - ucv: UCAN_VERSION.into(), - aud: self.audience.clone(), - iss: self.issuer.get_did().await?, - exp: self.expiration, - nbf: self.not_before, - nnc: nonce, - cap: self.capabilities.clone().try_into()?, - fct: facts, - prf: proofs, - }) - } - - /// Produces a Ucan, which contains finalized UCAN fields along with signed - /// data suitable for encoding as a JWT token string - pub async fn sign(&self) -> Result { - let header = self.ucan_header(); - let payload = self - .ucan_payload() - .await - .expect("Unable to generate UCAN payload"); - - let header_base64 = header.jwt_base64_encode()?; - let payload_base64 = payload.jwt_base64_encode()?; - - let data_to_sign = format!("{header_base64}.{payload_base64}") - .as_bytes() - .to_vec(); - let signature = self.issuer.sign(data_to_sign.as_slice()).await?; - - Ok(Ucan::new(header, payload, data_to_sign, signature)) - } -} - -/// A builder API for UCAN tokens -#[derive(Clone)] -pub struct UcanBuilder<'a, K> -where - K: KeyMaterial, -{ - issuer: Option<&'a K>, - audience: Option, - - capabilities: Vec, - - lifetime: Option, - expiration: Option, - not_before: Option, - - facts: FactsMap, - proofs: Vec, - add_nonce: bool, -} - -impl<'a, K> Default for UcanBuilder<'a, K> -where - K: KeyMaterial, -{ - /// Create an empty builder. - /// Before finalising the builder, you need to at least call: - /// - /// - `issued_by` - /// - `to_audience` and one of - /// - `with_lifetime` or `with_expiration`. - /// - /// To finalise the builder, call its `build` or `build_parts` method. - fn default() -> Self { - UcanBuilder { - issuer: None, - audience: None, - - capabilities: Vec::new(), - - lifetime: None, - expiration: None, - not_before: None, - - facts: BTreeMap::new(), - proofs: Vec::new(), - add_nonce: false, - } - } -} - -impl<'a, K> UcanBuilder<'a, K> -where - K: KeyMaterial, -{ - /// The UCAN must be signed with the private key of the issuer to be valid. - pub fn issued_by(mut self, issuer: &'a K) -> Self { - self.issuer = Some(issuer); - self - } - - /// This is the identity this UCAN transfers rights to. - /// - /// It could e.g. be the DID of a service you're posting this UCAN as a JWT to, - /// or it could be the DID of something that'll use this UCAN as a proof to - /// continue the UCAN chain as an issuer. - pub fn for_audience(mut self, audience: &str) -> Self { - self.audience = Some(String::from(audience)); - self - } - - /// The number of seconds into the future (relative to when build() is - /// invoked) to set the expiration. This is ignored if an explicit expiration - /// is set. - pub fn with_lifetime(mut self, seconds: u64) -> Self { - self.lifetime = Some(seconds); - self - } - - /// Set the POSIX timestamp (in seconds) for when the UCAN should expire. - /// Setting this value overrides a configured lifetime value. - pub fn with_expiration(mut self, timestamp: u64) -> Self { - self.expiration = Some(timestamp); - self - } - - /// Set the POSIX timestamp (in seconds) of when the UCAN becomes active. - pub fn not_before(mut self, timestamp: u64) -> Self { - self.not_before = Some(timestamp); - self - } - - /// Add a fact or proof of knowledge to this UCAN. - pub fn with_fact(mut self, key: &str, fact: T) -> Self { - match serde_json::to_value(fact) { - Ok(value) => { - self.facts.insert(key.to_owned(), value); - } - Err(error) => warn!("Could not add fact to UCAN: {}", error), - } - self - } - - /// Will ensure that the built UCAN includes a number used once. - pub fn with_nonce(mut self) -> Self { - self.add_nonce = true; - self - } - - /// Includes a UCAN in the list of proofs for the UCAN to be built. - /// Note that the proof's audience must match this UCAN's issuer - /// or else the proof chain will be invalidated! - /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] - /// algorithm, unless one is provided. - pub fn witnessed_by(mut self, authority: &Ucan, hasher: Option) -> Self { - match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { - Ok(proof) => self.proofs.push(proof.to_string()), - Err(error) => warn!("Failed to add authority to proofs: {}", error), - } - - self - } - - /// Claim a capability by inheritance (from an authorizing proof) or - /// implicitly by ownership of the resource by this UCAN's issuer - pub fn claiming_capability(mut self, capability: C) -> Self - where - C: Into, - { - self.capabilities.push(capability.into()); - self - } - - /// Claim capabilities by inheritance (from an authorizing proof) or - /// implicitly by ownership of the resource by this UCAN's issuer - pub fn claiming_capabilities(mut self, capabilities: &[C]) -> Self - where - C: Into + Clone, - { - let caps: Vec = capabilities - .iter() - .map(|c| >::into(c.to_owned())) - .collect(); - self.capabilities.extend(caps); - self - } - - /// Delegate all capabilities from a given proof to the audience of the UCAN - /// you're building. - /// The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()] - /// algorithm, unless one is provided. - pub fn delegating_from(mut self, authority: &Ucan, hasher: Option) -> Self { - match authority.to_cid(hasher.unwrap_or_else(|| UcanBuilder::::default_hasher())) { - Ok(proof) => { - self.proofs.push(proof.to_string()); - let proof_index = self.proofs.len() - 1; - let proof_delegation = ProofDelegationSemantics {}; - let capability = - proof_delegation.parse(&format!("prf:{proof_index}"), "ucan/DELEGATE", None); - - match capability { - Some(capability) => { - self.capabilities.push(Capability::from(&capability)); - } - None => warn!("Could not produce delegation capability"), - } - } - Err(error) => warn!("Could not encode authoritative UCAN: {:?}", error), - }; - - self - } - - /// Returns the default hasher ([Code::Blake3_256]) used for [Cid] encodings. - pub fn default_hasher() -> Code { - Code::Blake3_256 - } - - fn implied_expiration(&self) -> Option { - if self.expiration.is_some() { - self.expiration - } else { - self.lifetime.map(|lifetime| now() + lifetime) - } - } - - pub fn build(self) -> Result> { - match &self.issuer { - Some(issuer) => match &self.audience { - Some(audience) => Ok(Signable { - issuer, - audience: audience.clone(), - not_before: self.not_before, - expiration: self.implied_expiration(), - facts: self.facts.clone(), - capabilities: self.capabilities.clone(), - proofs: self.proofs.clone(), - add_nonce: self.add_nonce, - }), - None => Err(anyhow!("Missing audience")), - }, - None => Err(anyhow!("Missing issuer")), - } - } -} diff --git a/ucan/src/capability/caveats.rs b/ucan/src/capability/caveats.rs deleted file mode 100644 index e7d508a3..00000000 --- a/ucan/src/capability/caveats.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::ops::Deref; - -use anyhow::{anyhow, Error, Result}; -use serde_json::{Map, Value}; - -#[derive(Clone)] -pub struct Caveat(Map); - -impl Caveat { - /// Determines if this [Caveat] enables/allows the provided caveat. - /// - /// ``` - /// use ucan::capability::{Caveat}; - /// use serde_json::json; - /// - /// let no_caveat = Caveat::try_from(json!({})).unwrap(); - /// let x_caveat = Caveat::try_from(json!({ "x": true })).unwrap(); - /// let x_diff_caveat = Caveat::try_from(json!({ "x": false })).unwrap(); - /// let y_caveat = Caveat::try_from(json!({ "y": true })).unwrap(); - /// let xz_caveat = Caveat::try_from(json!({ "x": true, "z": true })).unwrap(); - /// - /// assert!(no_caveat.enables(&no_caveat)); - /// assert!(x_caveat.enables(&x_caveat)); - /// assert!(no_caveat.enables(&x_caveat)); - /// assert!(x_caveat.enables(&xz_caveat)); - /// - /// assert!(!x_caveat.enables(&x_diff_caveat)); - /// assert!(!x_caveat.enables(&no_caveat)); - /// assert!(!x_caveat.enables(&y_caveat)); - /// ``` - pub fn enables(&self, other: &Caveat) -> bool { - if self.is_empty() { - return true; - } - - if other.is_empty() { - return false; - } - - if self == other { - return true; - } - - for (key, value) in self.iter() { - if let Some(other_value) = other.get(key) { - if value != other_value { - return false; - } - } else { - return false; - } - } - - true - } -} - -impl Deref for Caveat { - type Target = Map; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PartialEq for Caveat { - fn eq(&self, other: &Caveat) -> bool { - self.0 == other.0 - } -} - -impl TryFrom for Caveat { - type Error = Error; - fn try_from(value: Value) -> Result { - Ok(Caveat(match value { - Value::Object(obj) => obj, - _ => return Err(anyhow!("Caveat must be an object")), - })) - } -} - -impl TryFrom<&Value> for Caveat { - type Error = Error; - fn try_from(value: &Value) -> Result { - Caveat::try_from(value.to_owned()) - } -} diff --git a/ucan/src/capability/data.rs b/ucan/src/capability/data.rs deleted file mode 100644 index 307fe718..00000000 --- a/ucan/src/capability/data.rs +++ /dev/null @@ -1,229 +0,0 @@ -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{ - collections::{btree_map::Iter as BTreeMapIter, BTreeMap}, - fmt::Debug, - iter::FlatMap, - ops::Deref, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -/// Represents a single, flattened capability containing a resource, ability, and caveat. -pub struct Capability { - pub resource: String, - pub ability: String, - pub caveat: Value, -} - -impl Capability { - pub fn new(resource: String, ability: String, caveat: Value) -> Self { - Capability { - resource, - ability, - caveat, - } - } -} - -impl From<&Capability> for Capability { - fn from(value: &Capability) -> Self { - value.to_owned() - } -} - -impl From<(String, String, Value)> for Capability { - fn from(value: (String, String, Value)) -> Self { - Capability::new(value.0, value.1, value.2) - } -} - -impl From<(&str, &str, &Value)> for Capability { - fn from(value: (&str, &str, &Value)) -> Self { - Capability::new(value.0.to_owned(), value.1.to_owned(), value.2.to_owned()) - } -} - -impl From for (String, String, Value) { - fn from(value: Capability) -> Self { - (value.resource, value.ability, value.caveat) - } -} - -type MapImpl = BTreeMap; -type MapIter<'a, K, V> = BTreeMapIter<'a, K, V>; -type AbilitiesImpl = MapImpl>; -type CapabilitiesImpl = MapImpl; -type AbilitiesMapClosure<'a> = Box)) -> Vec + 'a>; -type AbilitiesMap<'a> = - FlatMap>, Vec, AbilitiesMapClosure<'a>>; -type CapabilitiesIterator<'a> = FlatMap< - MapIter<'a, String, AbilitiesImpl>, - AbilitiesMap<'a>, - fn((&'a String, &'a AbilitiesImpl)) -> AbilitiesMap<'a>, ->; - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -/// The [Capabilities] struct contains capability data as a map-of-maps, matching the -/// [spec](https://github.com/ucan-wg/spec#326-capabilities--attenuation). -/// See `iter()` to deconstruct this map into a sequence of [Capability] datas. -/// -/// ``` -/// use ucan::capability::Capabilities; -/// use serde_json::json; -/// -/// let capabilities = Capabilities::try_from(&json!({ -/// "mailto:username@example.com": { -/// "msg/receive": [{}], -/// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] -/// } -/// })).unwrap(); -/// -/// let resource = capabilities.get("mailto:username@example.com").unwrap(); -/// assert_eq!(resource.get("msg/receive").unwrap(), &vec![json!({})]); -/// assert_eq!(resource.get("msg/send").unwrap(), &vec![json!({ "draft": true }), json!({ "publish": true, "topic": ["foo"] })]) -/// ``` -pub struct Capabilities(CapabilitiesImpl); - -impl Capabilities { - /// Using a [FlatMap] implementation, iterate over a [Capabilities] map-of-map - /// as a sequence of [Capability] datas. - /// - /// ``` - /// use ucan::capability::{Capabilities, Capability}; - /// use serde_json::json; - /// - /// let capabilities = Capabilities::try_from(&json!({ - /// "example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr": { - /// "wnfs/append": [{}] - /// }, - /// "mailto:username@example.com": { - /// "msg/receive": [{}], - /// "msg/send": [{ "draft": true }, { "publish": true, "topic": ["foo"]}] - /// } - /// })).unwrap(); - /// - /// assert_eq!(capabilities.iter().collect::>(), vec![ - /// Capability::from(("example://example.com/private/84MZ7aqwKn7sNiMGsSbaxsEa6EPnQLoKYbXByxNBrCEr", "wnfs/append", &json!({}))), - /// Capability::from(("mailto:username@example.com", "msg/receive", &json!({}))), - /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "draft": true }))), - /// Capability::from(("mailto:username@example.com", "msg/send", &json!({ "publish": true, "topic": ["foo"] }))), - /// ]); - /// ``` - pub fn iter(&self) -> CapabilitiesIterator { - self.0 - .iter() - .flat_map(|(resource, abilities): (&String, &AbilitiesImpl)| { - abilities - .iter() - .flat_map(Box::new( - |(ability, caveats): (&String, &Vec)| match caveats.len() { - 0 => vec![], // An empty caveats list is the same as no capability at all - _ => caveats - .iter() - .map(|caveat| { - Capability::from(( - resource.to_owned(), - ability.to_owned(), - caveat.to_owned(), - )) - }) - .collect(), - }, - )) - }) - } -} - -impl Deref for Capabilities { - type Target = CapabilitiesImpl; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl TryFrom> for Capabilities { - type Error = anyhow::Error; - fn try_from(value: Vec) -> Result { - let mut resources: CapabilitiesImpl = BTreeMap::new(); - for capability in value.into_iter() { - let (resource_name, ability, caveat) = <(String, String, Value)>::from(capability); - - let resource = if let Some(resource) = resources.get_mut(&resource_name) { - resource - } else { - let resource: AbilitiesImpl = BTreeMap::new(); - resources.insert(resource_name.clone(), resource); - resources.get_mut(&resource_name).unwrap() - }; - - if !caveat.is_object() { - return Err(anyhow!("Caveat must be an object: {}", caveat)); - } - - if let Some(ability_vec) = resource.get_mut(&ability) { - ability_vec.push(caveat); - } else { - resource.insert(ability, vec![caveat]); - } - } - Capabilities::try_from(resources) - } -} - -impl TryFrom for Capabilities { - type Error = anyhow::Error; - - fn try_from(value: CapabilitiesImpl) -> Result { - for (resource, abilities) in value.iter() { - if abilities.is_empty() { - // [0.10.0/3.2.6.2](https://github.com/ucan-wg/spec#3262-abilities): - // One or more abilities MUST be given for each resource. - return Err(anyhow!("No abilities given for resource: {}", resource)); - } - } - Ok(Capabilities(value)) - } -} - -impl TryFrom<&Value> for Capabilities { - type Error = anyhow::Error; - - fn try_from(value: &Value) -> Result { - let map = value - .as_object() - .ok_or_else(|| anyhow!("Capabilities must be an object."))?; - let mut resources: CapabilitiesImpl = BTreeMap::new(); - - for (key, value) in map.iter() { - let resource = key.to_owned(); - let abilities_object = value - .as_object() - .ok_or_else(|| anyhow!("Abilities must be an object."))?; - - let abilities = { - let mut abilities: AbilitiesImpl = BTreeMap::new(); - for (key, value) in abilities_object.iter() { - let ability = key.to_owned(); - let mut caveats: Vec = vec![]; - - let array = value - .as_array() - .ok_or_else(|| anyhow!("Caveats must be defined as an array."))?; - for value in array.iter() { - if !value.is_object() { - return Err(anyhow!("Caveat must be an object: {}", value)); - } - caveats.push(value.to_owned()); - } - abilities.insert(ability, caveats); - } - abilities - }; - - resources.insert(resource, abilities); - } - - Capabilities::try_from(resources) - } -} diff --git a/ucan/src/capability/mod.rs b/ucan/src/capability/mod.rs deleted file mode 100644 index cde4fe33..00000000 --- a/ucan/src/capability/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod proof; - -mod caveats; -mod data; -mod semantics; - -pub use caveats::*; -pub use data::*; -pub use semantics::*; diff --git a/ucan/src/capability/proof.rs b/ucan/src/capability/proof.rs deleted file mode 100644 index 986bb578..00000000 --- a/ucan/src/capability/proof.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::{Ability, CapabilitySemantics, Scope}; -use anyhow::{anyhow, Result}; -use url::Url; - -#[derive(Ord, Eq, PartialEq, PartialOrd, Clone)] -pub enum ProofAction { - Delegate, -} - -impl Ability for ProofAction {} - -impl TryFrom for ProofAction { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "ucan/DELEGATE" => Ok(ProofAction::Delegate), - unsupported => Err(anyhow!( - "Unsupported action for proof resource ({})", - unsupported - )), - } - } -} - -impl ToString for ProofAction { - fn to_string(&self) -> String { - match self { - ProofAction::Delegate => "ucan/DELEGATE".into(), - } - } -} - -#[derive(Eq, PartialEq, Clone)] -pub enum ProofSelection { - Index(usize), - All, -} - -impl Scope for ProofSelection { - fn contains(&self, other: &Self) -> bool { - self == other || *self == ProofSelection::All - } -} - -impl TryFrom for ProofSelection { - type Error = anyhow::Error; - - fn try_from(value: Url) -> Result { - match value.scheme() { - "prf" => String::from(value.path()).try_into(), - _ => Err(anyhow!("Unrecognized URI scheme")), - } - } -} - -impl TryFrom for ProofSelection { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "*" => Ok(ProofSelection::All), - selection => Ok(ProofSelection::Index(selection.parse::()?)), - } - } -} - -impl ToString for ProofSelection { - fn to_string(&self) -> String { - match self { - ProofSelection::Index(usize) => format!("prf:{usize}"), - ProofSelection::All => "prf:*".to_string(), - } - } -} - -pub struct ProofDelegationSemantics {} - -impl CapabilitySemantics for ProofDelegationSemantics {} diff --git a/ucan/src/capability/semantics.rs b/ucan/src/capability/semantics.rs deleted file mode 100644 index 65966fb4..00000000 --- a/ucan/src/capability/semantics.rs +++ /dev/null @@ -1,301 +0,0 @@ -use super::{Capability, Caveat}; -use serde_json::{json, Value}; -use std::fmt::Debug; -use url::Url; - -pub trait Scope: ToString + TryFrom + PartialEq + Clone { - fn contains(&self, other: &Self) -> bool; -} - -pub trait Ability: Ord + TryFrom + ToString + Clone {} - -#[derive(Clone, Eq, PartialEq)] -pub enum ResourceUri -where - S: Scope, -{ - Scoped(S), - Unscoped, -} - -impl ResourceUri -where - S: Scope, -{ - pub fn contains(&self, other: &Self) -> bool { - match self { - ResourceUri::Unscoped => true, - ResourceUri::Scoped(scope) => match other { - ResourceUri::Scoped(other_scope) => scope.contains(other_scope), - _ => false, - }, - } - } -} - -impl ToString for ResourceUri -where - S: Scope, -{ - fn to_string(&self) -> String { - match self { - ResourceUri::Unscoped => "*".into(), - ResourceUri::Scoped(value) => value.to_string(), - } - } -} - -#[derive(Clone, Eq, PartialEq)] -pub enum Resource -where - S: Scope, -{ - Resource { kind: ResourceUri }, - My { kind: ResourceUri }, - As { did: String, kind: ResourceUri }, -} - -impl Resource -where - S: Scope, -{ - pub fn contains(&self, other: &Self) -> bool { - match (self, other) { - ( - Resource::Resource { kind: resource }, - Resource::Resource { - kind: other_resource, - }, - ) => resource.contains(other_resource), - ( - Resource::My { kind: resource }, - Resource::My { - kind: other_resource, - }, - ) => resource.contains(other_resource), - ( - Resource::As { - did, - kind: resource, - }, - Resource::As { - did: other_did, - kind: other_resource, - }, - ) if did == other_did => resource.contains(other_resource), - _ => false, - } - } -} - -impl ToString for Resource -where - S: Scope, -{ - fn to_string(&self) -> String { - match self { - Resource::Resource { kind } => kind.to_string(), - Resource::My { kind } => format!("my:{}", kind.to_string()), - Resource::As { did, kind } => format!("as:{did}:{}", kind.to_string()), - } - } -} - -pub trait CapabilitySemantics -where - S: Scope, - A: Ability, -{ - fn parse_scope(&self, scope: &Url) -> Option { - S::try_from(scope.clone()).ok() - } - fn parse_action(&self, ability: &str) -> Option { - A::try_from(String::from(ability)).ok() - } - - fn extract_did(&self, path: &str) -> Option<(String, String)> { - let mut path_parts = path.split(':'); - - match path_parts.next() { - Some("did") => (), - _ => return None, - }; - - match path_parts.next() { - Some("key") => (), - _ => return None, - }; - - let value = match path_parts.next() { - Some(value) => value, - _ => return None, - }; - - Some((format!("did:key:{value}"), path_parts.collect())) - } - - fn parse_resource(&self, resource: &Url) -> Option> { - Some(match resource.path() { - "*" => ResourceUri::Unscoped, - _ => ResourceUri::Scoped(self.parse_scope(resource)?), - }) - } - - fn parse_caveat(&self, caveat: Option<&Value>) -> Value { - if let Some(caveat) = caveat { - caveat.to_owned() - } else { - json!({}) - } - } - - /// Parse a resource and abilities string and a caveats object. - /// The default "no caveats" (`[{}]`) is implied if `None` caveats given. - fn parse( - &self, - resource: &str, - ability: &str, - caveat: Option<&Value>, - ) -> Option> { - let uri = Url::parse(resource).ok()?; - - let cap_resource = match uri.scheme() { - "my" => Resource::My { - kind: self.parse_resource(&uri)?, - }, - "as" => { - let (did, resource) = self.extract_did(uri.path())?; - Resource::As { - did, - kind: self.parse_resource(&Url::parse(resource.as_str()).ok()?)?, - } - } - _ => Resource::Resource { - kind: self.parse_resource(&uri)?, - }, - }; - - let cap_ability = match self.parse_action(ability) { - Some(ability) => ability, - None => return None, - }; - - let cap_caveat = self.parse_caveat(caveat); - - Some(CapabilityView::new_with_caveat( - cap_resource, - cap_ability, - cap_caveat, - )) - } - - fn parse_capability(&self, value: &Capability) -> Option> { - self.parse(&value.resource, &value.ability, Some(&value.caveat)) - } -} - -#[derive(Clone, Eq, PartialEq)] -pub struct CapabilityView -where - S: Scope, - A: Ability, -{ - pub resource: Resource, - pub ability: A, - pub caveat: Value, -} - -impl Debug for CapabilityView -where - S: Scope, - A: Ability, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Capability") - .field("resource", &self.resource.to_string()) - .field("ability", &self.ability.to_string()) - .field("caveats", &serde_json::to_string(&self.caveat)) - .finish() - } -} - -impl CapabilityView -where - S: Scope, - A: Ability, -{ - /// Creates a new [CapabilityView] semantics view over a capability - /// without caveats. - pub fn new(resource: Resource, ability: A) -> Self { - CapabilityView { - resource, - ability, - caveat: json!({}), - } - } - - /// Creates a new [CapabilityView] semantics view over a capability - /// with caveats. Note that an empty caveats array will imply NO - /// capabilities, rendering this capability meaningless. - pub fn new_with_caveat(resource: Resource, ability: A, caveat: Value) -> Self { - CapabilityView { - resource, - ability, - caveat, - } - } - - pub fn enables(&self, other: &CapabilityView) -> bool { - match ( - Caveat::try_from(self.caveat()), - Caveat::try_from(other.caveat()), - ) { - (Ok(self_caveat), Ok(other_caveat)) => { - self.resource.contains(&other.resource) - && self.ability >= other.ability - && self_caveat.enables(&other_caveat) - } - _ => false, - } - } - - pub fn resource(&self) -> &Resource { - &self.resource - } - - pub fn ability(&self) -> &A { - &self.ability - } - - pub fn caveat(&self) -> &Value { - &self.caveat - } -} - -impl From<&CapabilityView> for Capability -where - S: Scope, - A: Ability, -{ - fn from(value: &CapabilityView) -> Self { - Capability::new( - value.resource.to_string(), - value.ability.to_string(), - value.caveat.to_owned(), - ) - } -} - -impl From> for Capability -where - S: Scope, - A: Ability, -{ - fn from(value: CapabilityView) -> Self { - Capability::new( - value.resource.to_string(), - value.ability.to_string(), - value.caveat, - ) - } -} diff --git a/ucan/src/chain.rs b/ucan/src/chain.rs deleted file mode 100644 index 285efbba..00000000 --- a/ucan/src/chain.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::{ - capability::{ - proof::{ProofDelegationSemantics, ProofSelection}, - Ability, CapabilitySemantics, CapabilityView, Resource, ResourceUri, Scope, - }, - crypto::did::DidParser, - store::UcanJwtStore, - ucan::Ucan, -}; -use anyhow::{anyhow, Result}; -use async_recursion::async_recursion; -use cid::Cid; -use std::{collections::BTreeSet, fmt::Debug}; - -const PROOF_DELEGATION_SEMANTICS: ProofDelegationSemantics = ProofDelegationSemantics {}; - -#[derive(Eq, PartialEq)] -pub struct CapabilityInfo { - pub originators: BTreeSet, - pub not_before: Option, - pub expires_at: Option, - pub capability: CapabilityView, -} - -impl Debug for CapabilityInfo -where - S: Scope, - A: Ability, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CapabilityInfo") - .field("originators", &self.originators) - .field("not_before", &self.not_before) - .field("expires_at", &self.expires_at) - .field("capability", &self.capability) - .finish() - } -} - -/// A deserialized chain of ancestral proofs that are linked to a UCAN -#[derive(Debug)] -pub struct ProofChain { - ucan: Ucan, - proofs: Vec, - redelegations: BTreeSet, -} - -impl ProofChain { - /// Instantiate a [ProofChain] from a [Ucan], given a [UcanJwtStore] and [DidParser] - #[cfg_attr(target_arch = "wasm32", async_recursion(?Send))] - #[cfg_attr(not(target_arch = "wasm32"), async_recursion)] - pub async fn from_ucan( - ucan: Ucan, - now_time: Option, - did_parser: &mut DidParser, - store: &S, - ) -> Result - where - S: UcanJwtStore, - { - ucan.validate(now_time, did_parser).await?; - - let mut proofs: Vec = Vec::new(); - - if let Some(ucan_proofs) = ucan.proofs() { - for cid_string in ucan_proofs.iter() { - let cid = Cid::try_from(cid_string.as_str())?; - let ucan_token = store.require_token(&cid).await?; - let proof_chain = - Self::try_from_token_string(&ucan_token, now_time, did_parser, store).await?; - proof_chain.validate_link_to(&ucan)?; - proofs.push(proof_chain); - } - } - - let mut redelegations = BTreeSet::::new(); - - for capability in ucan - .capabilities() - .iter() - .filter_map(|cap| PROOF_DELEGATION_SEMANTICS.parse_capability(&cap)) - { - match capability.resource() { - Resource::Resource { - kind: ResourceUri::Scoped(ProofSelection::All), - } => { - for index in 0..proofs.len() { - redelegations.insert(index); - } - } - Resource::Resource { - kind: ResourceUri::Scoped(ProofSelection::Index(index)), - } => { - if *index < proofs.len() { - redelegations.insert(*index); - } else { - return Err(anyhow!( - "Unable to redelegate proof; no proof at zero-based index {}", - index - )); - } - } - _ => continue, - } - } - - Ok(ProofChain { - ucan, - proofs, - redelegations, - }) - } - - /// Instantiate a [ProofChain] from a [Cid], given a [UcanJwtStore] and [DidParser] - /// The [Cid] must resolve to a JWT token string - pub async fn from_cid( - cid: &Cid, - now_time: Option, - did_parser: &mut DidParser, - store: &S, - ) -> Result - where - S: UcanJwtStore, - { - Self::try_from_token_string( - &store.require_token(cid).await?, - now_time, - did_parser, - store, - ) - .await - } - - /// Instantiate a [ProofChain] from a JWT token string, given a [UcanJwtStore] and [DidParser] - pub async fn try_from_token_string<'a, S>( - ucan_token_string: &str, - now_time: Option, - did_parser: &mut DidParser, - store: &S, - ) -> Result - where - S: UcanJwtStore, - { - let ucan = Ucan::try_from(ucan_token_string)?; - Self::from_ucan(ucan, now_time, did_parser, store).await - } - - fn validate_link_to(&self, ucan: &Ucan) -> Result<()> { - let audience = self.ucan.audience(); - let issuer = ucan.issuer(); - - match audience == issuer { - true => match self.ucan.lifetime_encompasses(ucan) { - true => Ok(()), - false => Err(anyhow!("Invalid UCAN link: lifetime exceeds attenuation")), - }, - false => Err(anyhow!( - "Invalid UCAN link: audience {} does not match issuer {}", - audience, - issuer - )), - } - } - - pub fn ucan(&self) -> &Ucan { - &self.ucan - } - - pub fn proofs(&self) -> &Vec { - &self.proofs - } - - pub fn reduce_capabilities( - &self, - semantics: &Semantics, - ) -> Vec> - where - Semantics: CapabilitySemantics, - S: Scope, - A: Ability, - { - // Get the set of inherited attenuations (excluding redelegations) - // before further attenuating by own lifetime and capabilities: - let ancestral_capability_infos: Vec> = self - .proofs - .iter() - .enumerate() - .flat_map(|(index, ancestor_chain)| { - if self.redelegations.contains(&index) { - Vec::new() - } else { - ancestor_chain.reduce_capabilities(semantics) - } - }) - .collect(); - - // Get the set of capabilities that are blanket redelegated from - // ancestor proofs (via the prf: resource): - let mut redelegated_capability_infos: Vec> = self - .redelegations - .iter() - .flat_map(|index| { - self.proofs - .get(*index) - .unwrap() - .reduce_capabilities(semantics) - .into_iter() - .map(|mut info| { - // Redelegated capabilities should be attenuated by - // this UCAN's lifetime - info.not_before = *self.ucan.not_before(); - info.expires_at = *self.ucan.expires_at(); - info - }) - }) - .collect(); - - let self_capabilities_iter = self - .ucan - .capabilities() - .iter() - .map_while(|data| semantics.parse_capability(&data)); - - // Get the claimed attenuations of this ucan, cross-checking ancestral - // attenuations to discover the originating authority - let mut self_capability_infos: Vec> = match self.proofs.len() { - 0 => self_capabilities_iter - .map(|capability| CapabilityInfo { - originators: BTreeSet::from_iter(vec![self.ucan.issuer().to_string()]), - capability, - not_before: *self.ucan.not_before(), - expires_at: *self.ucan.expires_at(), - }) - .collect(), - _ => self_capabilities_iter - .map(|capability| { - let mut originators = BTreeSet::::new(); - - for ancestral_capability_info in ancestral_capability_infos.iter() { - match ancestral_capability_info.capability.enables(&capability) { - true => { - originators.extend(ancestral_capability_info.originators.clone()) - } - // true => return Some(capability), - false => continue, - } - } - - // If there are no related ancestral capability, then this - // link in the chain is considered the first originator - if originators.is_empty() { - originators.insert(self.ucan.issuer().to_string()); - } - - CapabilityInfo { - capability, - originators, - not_before: *self.ucan.not_before(), - expires_at: *self.ucan.expires_at(), - } - }) - .collect(), - }; - - self_capability_infos.append(&mut redelegated_capability_infos); - - let mut merged_capability_infos = Vec::>::new(); - - // Merge redundant capabilities (accounting for redelegation), ensuring - // that discrete originators are aggregated as we go - 'merge: while let Some(capability_info) = self_capability_infos.pop() { - for remaining_capability_info in &mut self_capability_infos { - if remaining_capability_info - .capability - .enables(&capability_info.capability) - { - remaining_capability_info - .originators - .extend(capability_info.originators); - continue 'merge; - } - } - - merged_capability_infos.push(capability_info); - } - - merged_capability_infos - } -} diff --git a/ucan/src/crypto/did.rs b/ucan/src/crypto/did.rs deleted file mode 100644 index 3322f2c9..00000000 --- a/ucan/src/crypto/did.rs +++ /dev/null @@ -1,67 +0,0 @@ -use super::KeyMaterial; -use anyhow::{anyhow, Result}; -use std::{collections::BTreeMap, sync::Arc}; - -pub type DidPrefix = &'static [u8]; -pub type BytesToKey = fn(Vec) -> Result>; -pub type KeyConstructors = BTreeMap; -pub type KeyConstructorSlice = [(DidPrefix, BytesToKey)]; -pub type KeyCache = BTreeMap>>; - -pub const DID_PREFIX: &str = "did:"; -pub const DID_KEY_PREFIX: &str = "did:key:z"; - -pub const ED25519_MAGIC_BYTES: &[u8] = &[0xed, 0x01]; -pub const RSA_MAGIC_BYTES: &[u8] = &[0x85, 0x24]; -pub const BLS12381G1_MAGIC_BYTES: &[u8] = &[0xea, 0x01]; -pub const BLS12381G2_MAGIC_BYTES: &[u8] = &[0xeb, 0x01]; -pub const P256_MAGIC_BYTES: &[u8] = &[0x80, 0x24]; -pub const SECP256K1_MAGIC_BYTES: &[u8] = &[0xe7, 0x1]; - -/// A parser that is able to convert from a DID string into a corresponding -/// [`KeyMaterial`] implementation. The parser extracts the signature -/// magic bytes from a given DID and tries to match them to a corresponding -/// constructor function that produces a `SigningKey`. -pub struct DidParser { - key_constructors: KeyConstructors, - key_cache: KeyCache, -} - -impl DidParser { - pub fn new(key_constructor_slice: &KeyConstructorSlice) -> Self { - let mut key_constructors = BTreeMap::new(); - for pair in key_constructor_slice { - key_constructors.insert(pair.0, pair.1); - } - DidParser { - key_constructors, - key_cache: BTreeMap::new(), - } - } - - pub fn parse(&mut self, did: &str) -> Result>> { - if !did.starts_with(DID_KEY_PREFIX) { - return Err(anyhow!("Expected valid did:key, got: {}", did)); - } - - let did = did.to_owned(); - if let Some(key) = self.key_cache.get(&did) { - return Ok(key.clone()); - } - - let did_bytes = bs58::decode(&did[DID_KEY_PREFIX.len()..]).into_vec()?; - let magic_bytes = &did_bytes[0..2]; - match self.key_constructors.get(magic_bytes) { - Some(ctor) => { - let key = ctor(Vec::from(&did_bytes[2..]))?; - self.key_cache.insert(did.clone(), Arc::new(key)); - - self.key_cache - .get(&did) - .ok_or_else(|| anyhow!("Couldn't find cached key")) - .map(|key| key.clone()) - } - None => Err(anyhow!("Unrecognized magic bytes: {:?}", magic_bytes)), - } - } -} diff --git a/ucan/src/crypto/key.rs b/ucan/src/crypto/key.rs deleted file mode 100644 index 022176ac..00000000 --- a/ucan/src/crypto/key.rs +++ /dev/null @@ -1,79 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use std::sync::Arc; - -#[cfg(not(target_arch = "wasm32"))] -pub trait KeyMaterialConditionalSendSync: Send + Sync {} - -#[cfg(not(target_arch = "wasm32"))] -impl KeyMaterialConditionalSendSync for K where K: KeyMaterial + Send + Sync {} - -#[cfg(target_arch = "wasm32")] -pub trait KeyMaterialConditionalSendSync {} - -#[cfg(target_arch = "wasm32")] -impl KeyMaterialConditionalSendSync for K where K: KeyMaterial {} - -/// This trait must be implemented by a struct that encapsulates cryptographic -/// keypair data. The trait represent the minimum required API capability for -/// producing a signed UCAN from a cryptographic keypair, and verifying such -/// signatures. -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait KeyMaterial: KeyMaterialConditionalSendSync { - /// The algorithm that will be used to produce the signature returned by the - /// sign method in this implementation - fn get_jwt_algorithm_name(&self) -> String; - - /// Provides a valid DID that can be used to solve the key - async fn get_did(&self) -> Result; - - /// Sign some data with this key - async fn sign(&self, payload: &[u8]) -> Result>; - - /// Verify the alleged signature of some data against this key - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl KeyMaterial for Box { - fn get_jwt_algorithm_name(&self) -> String { - self.as_ref().get_jwt_algorithm_name() - } - - async fn get_did(&self) -> Result { - self.as_ref().get_did().await - } - - async fn sign(&self, payload: &[u8]) -> Result> { - self.as_ref().sign(payload).await - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - self.as_ref().verify(payload, signature).await - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl KeyMaterial for Arc -where - K: KeyMaterial, -{ - fn get_jwt_algorithm_name(&self) -> String { - (**self).get_jwt_algorithm_name() - } - - async fn get_did(&self) -> Result { - (**self).get_did().await - } - - async fn sign(&self, payload: &[u8]) -> Result> { - (**self).sign(payload).await - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - (**self).verify(payload, signature).await - } -} diff --git a/ucan/src/crypto/mod.rs b/ucan/src/crypto/mod.rs deleted file mode 100644 index 75ac72f0..00000000 --- a/ucan/src/crypto/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod did; -mod key; -mod signature; - -pub use key::*; -pub use signature::*; diff --git a/ucan/src/crypto/signature.rs b/ucan/src/crypto/signature.rs deleted file mode 100644 index bc319d66..00000000 --- a/ucan/src/crypto/signature.rs +++ /dev/null @@ -1,12 +0,0 @@ -use strum_macros::{Display, EnumString}; - -// See: https://www.rfc-editor.org/rfc/rfc7518 -// See: https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.4 -#[derive(Debug, Display, EnumString, Eq, PartialEq)] -pub enum JwtSignatureAlgorithm { - EdDSA, - RS256, - ES256, - ES384, - ES512, -} diff --git a/ucan/src/ipld/mod.rs b/ucan/src/ipld/mod.rs deleted file mode 100644 index d055d81f..00000000 --- a/ucan/src/ipld/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod principle; -mod signature; -mod ucan; - -pub use self::ucan::*; -pub use principle::*; -pub use signature::*; diff --git a/ucan/src/ipld/principle.rs b/ucan/src/ipld/principle.rs deleted file mode 100644 index 9980f40f..00000000 --- a/ucan/src/ipld/principle.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::crypto::did::{DID_KEY_PREFIX, DID_PREFIX}; -use anyhow::anyhow; -use serde::{Deserialize, Serialize}; -use std::{fmt::Display, str::FromStr}; - -// Note: varint encoding of 0x0d1d -pub const DID_IPLD_PREFIX: &[u8] = &[0x9d, 0x1a]; - -#[repr(transparent)] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Principle(Vec); - -impl FromStr for Principle { - type Err = anyhow::Error; - - fn from_str(input: &str) -> Result { - if let Some(stripped) = input.strip_prefix(DID_KEY_PREFIX) { - Ok(Principle(bs58::decode(stripped).into_vec()?)) - } else if let Some(stripped) = input.strip_prefix(DID_PREFIX) { - Ok(Principle([DID_IPLD_PREFIX, stripped.as_bytes()].concat())) - } else { - Err(anyhow!("This is not a DID: {}", input)) - } - } -} - -impl Display for Principle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes = &self.0; - let did_content = match &bytes[0..2] { - DID_IPLD_PREFIX => [ - DID_PREFIX, - std::str::from_utf8(&bytes[2..]).map_err(|_| std::fmt::Error)?, - ] - .concat(), - _ => [DID_KEY_PREFIX, &bs58::encode(bytes).into_string()].concat(), - }; - - write!(f, "{did_content}") - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use crate::{ipld::Principle, tests::helpers::dag_cbor_roundtrip}; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn it_round_trips_a_principle_did() { - let did_string = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; - let principle = dag_cbor_roundtrip(&Principle::from_str(&did_string).unwrap()).unwrap(); - assert_eq!(did_string, principle.to_string()); - - let did_string = "did:web:example.com"; - let principle = dag_cbor_roundtrip(&Principle::from_str(&did_string).unwrap()).unwrap(); - assert_eq!(did_string, principle.to_string()); - } -} diff --git a/ucan/src/ipld/signature.rs b/ucan/src/ipld/signature.rs deleted file mode 100644 index ed520cbd..00000000 --- a/ucan/src/ipld/signature.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::crypto::JwtSignatureAlgorithm; -use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, str::FromStr}; - -// See -// See -const NONSTANDARD_VARSIG_PREFIX: u64 = 0xd000; -const ES256K_VARSIG_PREFIX: u64 = 0xd0e7; -const BLS12381G1_VARSIG_PREFIX: u64 = 0xd0ea; -const BLS12381G2_VARSIG_PREFIX: u64 = 0xd0eb; -const EDDSA_VARSIG_PREFIX: u64 = 0xd0ed; -const ES256_VARSIG_PREFIX: u64 = 0xd01200; -const ES384_VARSIG_PREFIX: u64 = 0xd01201; -const ES512_VARSIG_PREFIX: u64 = 0xd01202; -const RS256_VARSIG_PREFIX: u64 = 0xd01205; -const EIP191_VARSIG_PREFIX: u64 = 0xd191; - -/// A helper for transforming signatures used in JWTs to their UCAN-IPLD -/// counterpart representation and vice-versa -/// Note, not all valid JWT signature algorithms are represented by this -/// library, nor are all valid varsig prefixes -/// See -#[derive(Debug, Eq, PartialEq)] -pub enum VarsigPrefix { - NonStandard, - ES256K, - BLS12381G1, - BLS12381G2, - EdDSA, - ES256, - ES384, - ES512, - RS256, - EIP191, -} - -impl TryFrom for VarsigPrefix { - type Error = anyhow::Error; - - fn try_from(value: JwtSignatureAlgorithm) -> Result { - Ok(match value { - JwtSignatureAlgorithm::EdDSA => VarsigPrefix::EdDSA, - JwtSignatureAlgorithm::RS256 => VarsigPrefix::RS256, - JwtSignatureAlgorithm::ES256 => VarsigPrefix::ES256, - JwtSignatureAlgorithm::ES384 => VarsigPrefix::ES384, - JwtSignatureAlgorithm::ES512 => VarsigPrefix::ES512, - }) - } -} - -impl TryFrom for JwtSignatureAlgorithm { - type Error = anyhow::Error; - - fn try_from(value: VarsigPrefix) -> Result { - Ok(match value { - VarsigPrefix::EdDSA => JwtSignatureAlgorithm::EdDSA, - VarsigPrefix::RS256 => JwtSignatureAlgorithm::RS256, - VarsigPrefix::ES256 => JwtSignatureAlgorithm::ES256, - VarsigPrefix::ES384 => JwtSignatureAlgorithm::ES384, - VarsigPrefix::ES512 => JwtSignatureAlgorithm::ES512, - _ => { - return Err(anyhow!( - "JWT signature algorithm name for {:?} is not known", - value - )) - } - }) - } -} - -impl FromStr for VarsigPrefix { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - VarsigPrefix::try_from(JwtSignatureAlgorithm::from_str(s)?) - } -} - -impl From for u64 { - fn from(value: VarsigPrefix) -> Self { - match value { - VarsigPrefix::NonStandard { .. } => NONSTANDARD_VARSIG_PREFIX, - VarsigPrefix::ES256K => ES256K_VARSIG_PREFIX, - VarsigPrefix::BLS12381G1 => BLS12381G1_VARSIG_PREFIX, - VarsigPrefix::BLS12381G2 => BLS12381G2_VARSIG_PREFIX, - VarsigPrefix::EdDSA => EDDSA_VARSIG_PREFIX, - VarsigPrefix::ES256 => ES256_VARSIG_PREFIX, - VarsigPrefix::ES384 => ES384_VARSIG_PREFIX, - VarsigPrefix::ES512 => ES512_VARSIG_PREFIX, - VarsigPrefix::RS256 => RS256_VARSIG_PREFIX, - VarsigPrefix::EIP191 => EIP191_VARSIG_PREFIX, - } - } -} - -impl TryFrom for VarsigPrefix { - type Error = anyhow::Error; - - fn try_from(value: u64) -> Result { - Ok(match value { - EDDSA_VARSIG_PREFIX => VarsigPrefix::EdDSA, - RS256_VARSIG_PREFIX => VarsigPrefix::RS256, - ES256K_VARSIG_PREFIX => VarsigPrefix::ES256K, - BLS12381G1_VARSIG_PREFIX => VarsigPrefix::BLS12381G1, - BLS12381G2_VARSIG_PREFIX => VarsigPrefix::BLS12381G2, - EIP191_VARSIG_PREFIX => VarsigPrefix::EIP191, - ES256_VARSIG_PREFIX => VarsigPrefix::ES256, - ES384_VARSIG_PREFIX => VarsigPrefix::ES384, - ES512_VARSIG_PREFIX => VarsigPrefix::ES512, - NONSTANDARD_VARSIG_PREFIX => VarsigPrefix::NonStandard, - _ => return Err(anyhow!("Signature does not have a recognized prefix")), - }) - } -} - -/// An envelope for the UCAN-IPLD-equivalent of a UCAN's JWT signature, which -/// is a specified prefix in front of the raw signature bytes -/// See: -#[repr(transparent)] -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Signature(pub Vec); - -impl Signature { - pub fn decode(&self) -> Result<(JwtSignatureAlgorithm, Vec)> { - let buffer = self.0.as_slice(); - let (prefix, buffer) = unsigned_varint::decode::u64(buffer)?; - let (signature_length, buffer) = unsigned_varint::decode::usize(buffer)?; - - // TODO: Non-standard algorithm support here... - - let algorithm = JwtSignatureAlgorithm::try_from(VarsigPrefix::try_from(prefix)?)?; - let signature = buffer[..signature_length].to_vec(); - - Ok((algorithm, signature)) - } -} - -// TODO: Support non-standard signature algorithms for experimental purposes -// Note that non-standard signatures should additionally have the signature name -// appended after the signature bytes in the varsig representation -impl> TryFrom<(JwtSignatureAlgorithm, T)> for Signature { - type Error = anyhow::Error; - - fn try_from((algorithm, signature): (JwtSignatureAlgorithm, T)) -> Result { - // TODO: Non-standard JWT algorithm support here - let signature_bytes = signature.as_ref(); - let prefix = VarsigPrefix::try_from(algorithm)?; - let mut prefix_buffer = unsigned_varint::encode::u64_buffer(); - let prefix_bytes = unsigned_varint::encode::u64(prefix.into(), &mut prefix_buffer); - let mut size_buffer = unsigned_varint::encode::usize_buffer(); - - let size_bytes = unsigned_varint::encode::usize(signature_bytes.len(), &mut size_buffer); - - Ok(Signature( - [prefix_bytes, size_bytes, signature_bytes].concat(), - )) - } -} - -#[cfg(test)] -mod tests { - use crate::{crypto::JwtSignatureAlgorithm, ipld::Signature}; - - use base64::Engine; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - fn it_can_convert_between_jwt_and_bytesprefix_form() { - let token_signature = "Ab-xfYRoqYEHuo-252MKXDSiOZkLD-h1gHt8gKBP0AVdJZ6Jruv49TLZOvgWy9QkCpiwKUeGVbHodKcVx-azCQ"; - let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(token_signature) - .unwrap(); - - let bytesprefix_signature = - Signature::try_from((JwtSignatureAlgorithm::EdDSA, &signature_bytes)).unwrap(); - - let (decoded_algorithm, decoded_signature_bytes) = bytesprefix_signature.decode().unwrap(); - - assert_eq!(decoded_algorithm, JwtSignatureAlgorithm::EdDSA); - assert_eq!(decoded_signature_bytes, signature_bytes); - } - - #[allow(dead_code)] - // #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), test)] - #[ignore = "Support non-standard signature algorithms"] - fn it_can_convert_between_jwt_and_bytesprefix_for_nonstandard_signatures() {} -} diff --git a/ucan/src/ipld/ucan.rs b/ucan/src/ipld/ucan.rs deleted file mode 100644 index c98970d5..00000000 --- a/ucan/src/ipld/ucan.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::{ - capability::Capabilities, - crypto::JwtSignatureAlgorithm, - ipld::{Principle, Signature}, - serde::Base64Encode, - ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, -}; -use cid::Cid; -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, str::FromStr}; - -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct UcanIpld { - pub v: String, - - pub iss: Principle, - pub aud: Principle, - pub s: Signature, - - pub cap: Capabilities, - pub prf: Option>, - pub exp: Option, - pub fct: Option, - - pub nnc: Option, - pub nbf: Option, -} - -impl TryFrom<&Ucan> for UcanIpld { - type Error = anyhow::Error; - - fn try_from(ucan: &Ucan) -> Result { - let prf = if let Some(proofs) = ucan.proofs() { - let mut prf = Vec::new(); - for cid_string in proofs { - prf.push(Cid::try_from(cid_string.as_str())?); - } - if prf.is_empty() { - None - } else { - Some(prf) - } - } else { - None - }; - - Ok(UcanIpld { - v: ucan.version().to_string(), - iss: Principle::from_str(ucan.issuer())?, - aud: Principle::from_str(ucan.audience())?, - s: Signature::try_from(( - JwtSignatureAlgorithm::from_str(ucan.algorithm())?, - ucan.signature(), - ))?, - cap: ucan.capabilities().clone(), - prf, - exp: *ucan.expires_at(), - fct: ucan.facts().clone(), - nnc: ucan.nonce().as_ref().cloned(), - nbf: *ucan.not_before(), - }) - } -} - -impl TryFrom<&UcanIpld> for Ucan { - type Error = anyhow::Error; - - fn try_from(value: &UcanIpld) -> Result { - let (algorithm, signature) = value.s.decode()?; - - let header = UcanHeader { - alg: algorithm.to_string(), - typ: "JWT".into(), - }; - - let payload = UcanPayload { - ucv: UCAN_VERSION.into(), - iss: value.iss.to_string(), - aud: value.aud.to_string(), - exp: value.exp, - nbf: value.nbf, - nnc: value.nnc.clone(), - cap: value.cap.clone(), - fct: value.fct.clone(), - prf: value - .prf - .clone() - .map(|prf| prf.iter().map(|cid| cid.to_string()).collect()), - }; - - let signed_data = format!( - "{}.{}", - header.jwt_base64_encode()?, - payload.jwt_base64_encode()? - ) - .as_bytes() - .to_vec(); - - Ok(Ucan::new(header, payload, signed_data, signature)) - } -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use serde_json::json; - - use crate::{ - tests::{ - fixtures::Identities, - helpers::{dag_cbor_roundtrip, scaffold_ucan_builder}, - }, - Ucan, - }; - - use super::UcanIpld; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_produces_canonical_jwt_despite_json_ambiguity() { - let identities = Identities::new().await; - let canon_builder = scaffold_ucan_builder(&identities).await.unwrap(); - let other_builder = scaffold_ucan_builder(&identities).await.unwrap(); - - let canon_jwt = canon_builder - .with_fact( - "abc/challenge", - json!({ - "baz": true, - "foo": "bar" - }), - ) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let other_jwt = other_builder - .with_fact( - "abc/challenge", - json!({ - "foo": "bar", - "baz": true - }), - ) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - assert_eq!(canon_jwt, other_jwt); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_stays_canonical_when_converting_between_jwt_and_ipld() { - let identities = Identities::new().await; - let builder = scaffold_ucan_builder(&identities).await.unwrap(); - - let jwt = builder - .with_fact( - "abc/challenge", - json!({ - "baz": true, - "foo": "bar" - }), - ) - .with_nonce() - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let ucan = Ucan::try_from(jwt.as_str()).unwrap(); - let ucan_ipld = UcanIpld::try_from(&ucan).unwrap(); - - let decoded_ucan_ipld = dag_cbor_roundtrip(&ucan_ipld).unwrap(); - - let decoded_ucan = Ucan::try_from(&decoded_ucan_ipld).unwrap(); - - let decoded_jwt = decoded_ucan.encode().unwrap(); - - assert_eq!(jwt, decoded_jwt); - } -} diff --git a/ucan/src/lib.rs b/ucan/src/lib.rs deleted file mode 100644 index 04c5c8db..00000000 --- a/ucan/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Implement UCAN-based authorization with conciseness and ease! -//! -//! [UCANs][UCAN docs] are an emerging pattern based on -//! [JSON Web Tokens][JWT docs] (aka JWTs) that facilitate distributed and/or -//! decentralized authorization flows in web applications. Visit -//! [https://ucan.xyz][UCAN docs] for an introduction to UCANs and ideas for -//! how you can use them in your application. -//! -//! # Examples -//! -//! This crate offers the [`builder::UcanBuilder`] abstraction to generate -//! signed UCAN tokens. -//! -//! To generate a signed token, you need to provide a [`crypto::KeyMaterial`] -//! implementation. For more information on providing a signing key, see the -//! [`crypto`] module documentation. -//! -//! ```rust -//! use ucan::{ -//! builder::UcanBuilder, -//! crypto::KeyMaterial, -//! }; -//! -//! async fn generate_token<'a, K: KeyMaterial>(issuer_key: &'a K, audience_did: &'a str) -> Result { -//! UcanBuilder::default() -//! .issued_by(issuer_key) -//! .for_audience(audience_did) -//! .with_lifetime(60) -//! .build()? -//! .sign().await? -//! .encode() -//! } -//! ``` -//! -//! The crate also offers a validating parser to interpret UCAN tokens and -//! the capabilities they grant via their issuer and/or witnessing proofs: -//! the [`chain::ProofChain`]. -//! -//! Most capabilities are closely tied to a specific application domain. See the -//! [`capability`] module documentation to read more about defining your own -//! domain-specific semantics. -//! -//! ```rust -//! use ucan::{ -//! chain::{ProofChain, CapabilityInfo}, -//! capability::{CapabilitySemantics, Scope, Ability}, -//! crypto::did::{DidParser, KeyConstructorSlice}, -//! store::UcanJwtStore -//! }; -//! -//! const SUPPORTED_KEY_TYPES: &KeyConstructorSlice = &[ -//! // You must bring your own key support -//! ]; -//! -//! async fn get_capabilities<'a, Semantics, S, A, Store>(ucan_token: &'a str, semantics: &'a Semantics, store: &'a Store) -> Result>, anyhow::Error> -//! where -//! Semantics: CapabilitySemantics, -//! S: Scope, -//! A: Ability, -//! Store: UcanJwtStore -//! { -//! let mut did_parser = DidParser::new(SUPPORTED_KEY_TYPES); -//! -//! Ok(ProofChain::try_from_token_string(ucan_token, None, &mut did_parser, store).await? -//! .reduce_capabilities(semantics)) -//! } -//! ``` -//! -//! Note that you must bring your own key support in order to build a -//! `ProofChain`, via a [`crypto::did::DidParser`]. This is so that the core -//! library can remain agnostic of backing implementations for specific key -//! types. -//! -//! [JWT docs]: https://jwt.io/ -//! [UCAN docs]: https://ucan.xyz/ -//! [DID spec]: https://www.w3.org/TR/did-core/ -//! [DID Key spec]: https://w3c-ccg.github.io/did-method-key/ - -pub mod crypto; -pub mod time; - -pub mod builder; -pub mod capability; -pub mod chain; -pub mod ipld; -pub mod serde; -pub mod store; -pub mod ucan; -pub use self::ucan::Ucan; - -#[cfg(test)] -mod tests; diff --git a/ucan/src/serde.rs b/ucan/src/serde.rs deleted file mode 100644 index a2ffebc4..00000000 --- a/ucan/src/serde.rs +++ /dev/null @@ -1,46 +0,0 @@ -use anyhow::Result; -use base64::Engine; -use libipld_core::{ - codec::{Decode, Encode}, - ipld::Ipld, - serde::{from_ipld, to_ipld}, -}; -use libipld_json::DagJsonCodec; -use serde::{de::DeserializeOwned, Serialize, Serializer}; -use std::io::Cursor; - -/// Utility function to enforce lower-case string values when serializing -pub fn ser_to_lower_case(string: &str, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&string.to_lowercase()) -} - -/// Helper trait to ser/de any serde-implementing value to/from DAG-JSON -pub trait DagJson: Serialize + DeserializeOwned { - fn to_dag_json(&self) -> Result> { - let ipld = to_ipld(self)?; - let mut json_bytes = Vec::new(); - - ipld.encode(DagJsonCodec, &mut json_bytes)?; - - Ok(json_bytes) - } - - fn from_dag_json(json_bytes: &[u8]) -> Result { - let ipld = Ipld::decode(DagJsonCodec, &mut Cursor::new(json_bytes))?; - Ok(from_ipld(ipld)?) - } -} - -impl DagJson for T where T: Serialize + DeserializeOwned {} - -/// Helper trait to encode structs as base64 as part of creating a JWT -pub trait Base64Encode: DagJson { - fn jwt_base64_encode(&self) -> Result { - Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.to_dag_json()?)) - } -} - -impl Base64Encode for T where T: DagJson {} diff --git a/ucan/src/store.rs b/ucan/src/store.rs deleted file mode 100644 index 034a4c32..00000000 --- a/ucan/src/store.rs +++ /dev/null @@ -1,130 +0,0 @@ -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use cid::{ - multihash::{Code, MultihashDigest}, - Cid, -}; -use libipld_core::{ - codec::{Codec, Decode, Encode}, - ipld::Ipld, - raw::RawCodec, -}; -use std::{ - collections::HashMap, - io::Cursor, - sync::{Arc, Mutex}, -}; - -#[cfg(not(target_arch = "wasm32"))] -pub trait UcanStoreConditionalSend: Send {} - -#[cfg(not(target_arch = "wasm32"))] -impl UcanStoreConditionalSend for U where U: Send {} - -#[cfg(target_arch = "wasm32")] -pub trait UcanStoreConditionalSend {} - -#[cfg(target_arch = "wasm32")] -impl UcanStoreConditionalSend for U {} - -#[cfg(not(target_arch = "wasm32"))] -pub trait UcanStoreConditionalSendSync: Send + Sync {} - -#[cfg(not(target_arch = "wasm32"))] -impl UcanStoreConditionalSendSync for U where U: Send + Sync {} - -#[cfg(target_arch = "wasm32")] -pub trait UcanStoreConditionalSendSync {} - -#[cfg(target_arch = "wasm32")] -impl UcanStoreConditionalSendSync for U {} - -/// This trait is meant to be implemented by a storage backend suitable for -/// persisting UCAN tokens that may be referenced as proofs by other UCANs -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait UcanStore: UcanStoreConditionalSendSync { - /// Read a value from the store by CID, returning a Result> that unwraps - /// to None if no value is found, otherwise Some - async fn read>(&self, cid: &Cid) -> Result>; - - /// Write a value to the store, receiving a Result that wraps the values CID if the - /// write was successful - async fn write + UcanStoreConditionalSend + core::fmt::Debug>( - &mut self, - token: T, - ) -> Result; -} - -/// This trait is sugar over the UcanStore trait to add convenience methods -/// for the case of storing JWT-encoded UCAN strings using the 'raw' codec -/// which is the only combination strictly required by the UCAN spec -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -pub trait UcanJwtStore: UcanStore { - async fn require_token(&self, cid: &Cid) -> Result { - match self.read_token(cid).await? { - Some(token) => Ok(token), - None => Err(anyhow!("No token found for CID {}", cid.to_string())), - } - } - - async fn read_token(&self, cid: &Cid) -> Result> { - let codec = RawCodec; - - if cid.codec() != u64::from(codec) { - return Err(anyhow!( - "Only 'raw' codec supported, but CID refers to {:#x}", - cid.codec() - )); - } - - match self.read::(cid).await? { - Some(Ipld::Bytes(bytes)) => Ok(Some(std::str::from_utf8(&bytes)?.to_string())), - _ => Err(anyhow!("No UCAN was found for CID {:?}", cid)), - } - } - - async fn write_token(&mut self, token: &str) -> Result { - self.write(Ipld::Bytes(token.as_bytes().to_vec())).await - } -} - -impl UcanJwtStore for U where U: UcanStore {} - -/// A basic in-memory store that implements UcanStore for the 'raw' -/// codec. This will serve for basic use cases and tests, but it is -/// recommended that a store that persists to disk be used in most -/// practical use cases. -#[derive(Clone, Default, Debug)] -pub struct MemoryStore { - dags: Arc>>>, -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl UcanStore for MemoryStore { - async fn read>(&self, cid: &Cid) -> Result> { - let codec = RawCodec; - let dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; - - Ok(match dags.get(cid) { - Some(bytes) => Some(T::decode(codec, &mut Cursor::new(bytes))?), - None => None, - }) - } - - async fn write + UcanStoreConditionalSend + core::fmt::Debug>( - &mut self, - token: T, - ) -> Result { - let codec = RawCodec; - let block = codec.encode(&token)?; - let cid = Cid::new_v1(codec.into(), Code::Blake3_256.digest(&block)); - - let mut dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; - dags.insert(cid, block); - - Ok(cid) - } -} diff --git a/ucan/src/tests/attenuation.rs b/ucan/src/tests/attenuation.rs deleted file mode 100644 index 2ef9679f..00000000 --- a/ucan/src/tests/attenuation.rs +++ /dev/null @@ -1,447 +0,0 @@ -use super::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}; -use crate::{ - builder::UcanBuilder, - capability::{Capability, CapabilitySemantics}, - chain::{CapabilityInfo, ProofChain}, - crypto::did::DidParser, - store::{MemoryStore, UcanJwtStore}, -}; -use std::collections::BTreeSet; - -use serde_json::json; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - -#[cfg(target_arch = "wasm32")] -wasm_bindgen_test_configure!(run_in_browser); - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_works_with_a_simple_example() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let email_semantics = EmailSemantics {}; - let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com", "email/send", None) - .unwrap(); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let attenuated_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let chain = - ProofChain::try_from_token_string(attenuated_token.as_str(), None, &mut did_parser, &store) - .await - .unwrap(); - - let capability_infos = chain.reduce_capabilities(&email_semantics); - - assert_eq!(capability_infos.len(), 1); - - let info = capability_infos.get(0).unwrap(); - - assert_eq!( - info.capability.resource().to_string().as_str(), - "mailto:alice@email.com", - ); - assert_eq!(info.capability.ability().to_string().as_str(), "email/send"); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_reports_the_first_issuer_in_the_chain_as_originator() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let email_semantics = EmailSemantics {}; - let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into(), None) - .unwrap(); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .claiming_capability(&send_email_as_bob) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let capability_infos = - ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) - .await - .unwrap() - .reduce_capabilities(&email_semantics); - - assert_eq!(capability_infos.len(), 1); - - let info = capability_infos.get(0).unwrap(); - - assert_eq!( - info.originators.iter().collect::>(), - vec![&identities.bob_did] - ); - assert_eq!(info.capability, send_email_as_bob); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_finds_the_right_proof_chain_for_the_originator() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let email_semantics = EmailSemantics {}; - let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into(), None) - .unwrap(); - let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into(), None) - .unwrap(); - - let leaf_ucan_alice = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(60) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let leaf_ucan_bob = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(60) - .claiming_capability(&send_email_as_bob) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan = UcanBuilder::default() - .issued_by(&identities.mallory_key) - .for_audience(identities.alice_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan_alice, None) - .witnessed_by(&leaf_ucan_bob, None) - .claiming_capability(&send_email_as_alice) - .claiming_capability(&send_email_as_bob) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan_token = ucan.encode().unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan_alice.encode().unwrap()) - .await - .unwrap(); - store - .write_token(&leaf_ucan_bob.encode().unwrap()) - .await - .unwrap(); - - let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) - .await - .unwrap(); - let capability_infos = proof_chain.reduce_capabilities(&email_semantics); - - assert_eq!(capability_infos.len(), 2); - - let send_email_as_bob_info = capability_infos.get(0).unwrap(); - let send_email_as_alice_info = capability_infos.get(1).unwrap(); - - assert_eq!( - send_email_as_alice_info, - &CapabilityInfo { - originators: BTreeSet::from_iter(vec![identities.alice_did]), - capability: send_email_as_alice, - not_before: ucan.not_before().clone(), - expires_at: ucan.expires_at().clone() - } - ); - - assert_eq!( - send_email_as_bob_info, - &CapabilityInfo { - originators: BTreeSet::from_iter(vec![identities.bob_did]), - capability: send_email_as_bob, - not_before: ucan.not_before().clone(), - expires_at: ucan.expires_at().clone() - } - ); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_reports_all_chain_options() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let email_semantics = EmailSemantics {}; - let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into(), None) - .unwrap(); - - let leaf_ucan_alice = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(60) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let leaf_ucan_bob = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(60) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan = UcanBuilder::default() - .issued_by(&identities.mallory_key) - .for_audience(identities.alice_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan_alice, None) - .witnessed_by(&leaf_ucan_bob, None) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan_token = ucan.encode().unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan_alice.encode().unwrap()) - .await - .unwrap(); - store - .write_token(&leaf_ucan_bob.encode().unwrap()) - .await - .unwrap(); - - let proof_chain = ProofChain::try_from_token_string(&ucan_token, None, &mut did_parser, &store) - .await - .unwrap(); - let capability_infos = proof_chain.reduce_capabilities(&email_semantics); - - assert_eq!(capability_infos.len(), 1); - - let send_email_as_alice_info = capability_infos.get(0).unwrap(); - - assert_eq!( - send_email_as_alice_info, - &CapabilityInfo { - originators: BTreeSet::from_iter(vec![identities.alice_did, identities.bob_did]), - capability: send_email_as_alice, - not_before: ucan.not_before().clone(), - expires_at: ucan.expires_at().clone() - } - ); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_validates_caveats() -> anyhow::Result<()> { - let resource = "mailto:alice@email.com"; - let ability = "email/send"; - - let no_caveat = Capability::from((resource, ability, &json!({}))); - let x_caveat = Capability::from((resource, ability, &json!({ "x": true }))); - let y_caveat = Capability::from((resource, ability, &json!({ "y": true }))); - let z_caveat = Capability::from((resource, ability, &json!({ "z": true }))); - let yz_caveat = Capability::from((resource, ability, &json!({ "y": true, "z": true }))); - - let valid = [ - (vec![&no_caveat], vec![&no_caveat]), - (vec![&x_caveat], vec![&x_caveat]), - (vec![&no_caveat], vec![&x_caveat]), - (vec![&x_caveat, &y_caveat], vec![&x_caveat]), - (vec![&x_caveat, &y_caveat], vec![&x_caveat, &yz_caveat]), - ]; - - let invalid = [ - (vec![&x_caveat], vec![&no_caveat]), - (vec![&x_caveat], vec![&y_caveat]), - ( - vec![&x_caveat, &y_caveat], - vec![&x_caveat, &y_caveat, &z_caveat], - ), - ]; - - for (proof_capabilities, delegated_capabilities) in valid { - let is_successful = - test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; - assert!( - is_successful, - "{} enables {}", - render_caveats(&proof_capabilities), - render_caveats(&delegated_capabilities) - ); - } - - for (proof_capabilities, delegated_capabilities) in invalid { - let is_successful = - test_capabilities_delegation(&proof_capabilities, &delegated_capabilities).await?; - assert!( - !is_successful, - "{} disallows {}", - render_caveats(&proof_capabilities), - render_caveats(&delegated_capabilities) - ); - } - - fn render_caveats(capabilities: &Vec<&Capability>) -> String { - format!( - "{:?}", - capabilities - .iter() - .map(|cap| cap.caveat.to_string()) - .collect::>() - ) - } - - async fn test_capabilities_delegation( - proof_capabilities: &Vec<&Capability>, - delegated_capabilities: &Vec<&Capability>, - ) -> anyhow::Result { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - let email_semantics = EmailSemantics {}; - let mut store = MemoryStore::default(); - let proof_capabilities = proof_capabilities - .to_owned() - .into_iter() - .map(|cap| cap.to_owned()) - .collect::>(); - let delegated_capabilities = delegated_capabilities - .to_owned() - .into_iter() - .map(|cap| cap.to_owned()) - .collect::>(); - - let proof_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(60) - .claiming_capabilities(&proof_capabilities) - .build()? - .sign() - .await?; - - let ucan = UcanBuilder::default() - .issued_by(&identities.mallory_key) - .for_audience(identities.alice_did.as_str()) - .with_lifetime(50) - .witnessed_by(&proof_ucan, None) - .claiming_capabilities(&delegated_capabilities) - .build()? - .sign() - .await?; - store.write_token(&proof_ucan.encode().unwrap()).await?; - store.write_token(&ucan.encode().unwrap()).await?; - - let proof_chain = ProofChain::from_ucan(ucan, None, &mut did_parser, &store).await?; - - Ok(enables_capabilities( - &proof_chain, - &email_semantics, - &identities.alice_did, - &delegated_capabilities, - )) - } - - /// Checks proof chain returning true if all desired capabilities are enabled. - fn enables_capabilities( - proof_chain: &ProofChain, - semantics: &EmailSemantics, - originator: &String, - desired_capabilities: &Vec, - ) -> bool { - let capability_infos = proof_chain.reduce_capabilities(semantics); - - for desired_capability in desired_capabilities { - let mut has_capability = false; - for info in &capability_infos { - if info.originators.contains(originator) - && info - .capability - .enables(&semantics.parse_capability(desired_capability).unwrap()) - { - has_capability = true; - break; - } - } - if !has_capability { - return false; - } - } - true - } - - Ok(()) -} diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs deleted file mode 100644 index cb1fdeaf..00000000 --- a/ucan/src/tests/builder.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::collections::BTreeMap; - -use crate::{ - builder::UcanBuilder, - capability::{Capabilities, Capability, CapabilitySemantics}, - chain::ProofChain, - crypto::did::DidParser, - store::UcanJwtStore, - tests::fixtures::{ - Blake2bMemoryStore, EmailSemantics, Identities, WNFSSemantics, SUPPORTED_KEYS, - }, - time::now, -}; -use cid::multihash::Code; -use did_key::PatchedKeyPair; -use serde_json::json; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - -#[cfg(target_arch = "wasm32")] -wasm_bindgen_test_configure!(run_in_browser); - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -async fn it_builds_with_a_simple_example() { - let identities = Identities::new().await; - - let fact_1 = json!({ - "test": true - }); - - let fact_2 = json!({ - "preimage": "abc", - "hash": "sth" - }); - - let email_semantics = EmailSemantics {}; - let wnfs_semantics = WNFSSemantics {}; - - let cap_1 = email_semantics - .parse("mailto:alice@gmail.com", "email/send", None) - .unwrap(); - - let cap_2 = wnfs_semantics - .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) - .unwrap(); - - let expiration = now() + 30; - let not_before = now() - 30; - - let token = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_expiration(expiration) - .not_before(not_before) - .with_fact("abc/challenge", fact_1.clone()) - .with_fact("def/challenge", fact_2.clone()) - .claiming_capability(&cap_1) - .claiming_capability(&cap_2) - .with_nonce() - .build() - .unwrap(); - - let ucan = token.sign().await.unwrap(); - - assert_eq!(ucan.issuer(), identities.alice_did); - assert_eq!(ucan.audience(), identities.bob_did); - assert!(ucan.expires_at().is_some()); - assert_eq!(ucan.expires_at().unwrap(), expiration); - assert!(ucan.not_before().is_some()); - assert_eq!(ucan.not_before().unwrap(), not_before); - assert_eq!( - ucan.facts(), - &Some(BTreeMap::from([ - (String::from("abc/challenge"), fact_1), - (String::from("def/challenge"), fact_2), - ])) - ); - - let expected_attenuations = - Capabilities::try_from(vec![Capability::from(&cap_1), Capability::from(&cap_2)]).unwrap(); - - assert_eq!(ucan.capabilities(), &expected_attenuations); - assert!(ucan.nonce().is_some()); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -async fn it_builds_with_lifetime_in_seconds() { - let identities = Identities::new().await; - - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(300) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - assert!(ucan.expires_at().unwrap() > (now() + 290)); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -async fn it_prevents_duplicate_proofs() { - let wnfs_semantics = WNFSSemantics {}; - - let parent_cap = wnfs_semantics - .parse("wnfs://alice.fission.name/public", "wnfs/super_user", None) - .unwrap(); - - let identities = Identities::new().await; - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(30) - .claiming_capability(&parent_cap) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let attenuated_cap_1 = wnfs_semantics - .parse("wnfs://alice.fission.name/public/Apps", "wnfs/create", None) - .unwrap(); - - let attenuated_cap_2 = wnfs_semantics - .parse( - "wnfs://alice.fission.name/public/Domains", - "wnfs/create", - None, - ) - .unwrap(); - - let next_ucan = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(30) - .witnessed_by(&ucan, None) - .claiming_capability(&attenuated_cap_1) - .claiming_capability(&attenuated_cap_2) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - assert_eq!( - next_ucan.proofs(), - &Some(vec![ucan - .to_cid(UcanBuilder::::default_hasher()) - .unwrap() - .to_string()]) - ) -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_can_use_custom_hasher() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.alice_key) - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, Some(Code::Blake2b256)) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let mut store = Blake2bMemoryStore::default(); - - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let _ = store - .write_token(&delegated_token.encode().unwrap()) - .await - .unwrap(); - - let valid_chain = - ProofChain::from_ucan(delegated_token, Some(now()), &mut did_parser, &store).await; - - assert!(valid_chain.is_ok()); -} diff --git a/ucan/src/tests/capability.rs b/ucan/src/tests/capability.rs deleted file mode 100644 index 11e9bd2d..00000000 --- a/ucan/src/tests/capability.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::capability::{Capabilities, Capability}; -use serde_json::json; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - -#[cfg(target_arch = "wasm32")] -wasm_bindgen_test_configure!(run_in_browser); - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn it_can_cast_between_map_and_sequence() { - let cap_foo = Capability::from(("example://foo", "ability/foo", &json!({}))); - let cap_bar_1 = Capability::from(("example://bar", "ability/bar", &json!({ "beep": 1 }))); - let cap_bar_2 = Capability::from(("example://bar", "ability/bar", &json!({ "boop": 1 }))); - - let cap_sequence = vec![cap_bar_1.clone(), cap_bar_2.clone(), cap_foo]; - let cap_map = Capabilities::try_from(&json!({ - "example://bar": { - "ability/bar": [{ "beep": 1 }, { "boop": 1 }] - }, - "example://foo": { "ability/foo": [{}] }, - })) - .unwrap(); - - assert_eq!( - &cap_map.iter().collect::>(), - &cap_sequence, - "Capabilities map to sequence." - ); - assert_eq!( - &Capabilities::try_from(cap_sequence).unwrap(), - &cap_map, - "Capabilities sequence to map." - ); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn it_rejects_non_compliant_json() { - let failure_cases = [ - (json!([]), "resources must be map"), - ( - json!({ - "resource:foo": [] - }), - "abilities must be map", - ), - ( - json!({"resource:foo": {}}), - "resource must have at least one ability", - ), - ( - json!({"resource:foo": { "ability/read": {} }}), - "caveats must be array", - ), - ( - json!({"resource:foo": { "ability/read": [1] }}), - "caveat must be object", - ), - ]; - - for (json_data, message) in failure_cases { - assert!(Capabilities::try_from(&json_data).is_err(), "{message}"); - } - - assert!(Capabilities::try_from(&json!({ - "resource:foo": { "ability/read": [{}] } - })) - .is_ok()); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), test)] -fn it_filters_out_empty_caveats_when_iterating() { - let cap_map = Capabilities::try_from(&json!({ - "example://bar": { "ability/bar": [{}] }, - "example://foo": { "ability/foo": [] } - })) - .unwrap(); - - assert_eq!( - cap_map.iter().collect::>(), - vec![Capability::from(( - "example://bar", - "ability/bar", - &json!({}) - ))], - "iter() filters out capabilities with empty caveats" - ); -} diff --git a/ucan/src/tests/chain.rs b/ucan/src/tests/chain.rs deleted file mode 100644 index d671b590..00000000 --- a/ucan/src/tests/chain.rs +++ /dev/null @@ -1,254 +0,0 @@ -use super::fixtures::{Identities, SUPPORTED_KEYS}; -use crate::{ - builder::UcanBuilder, - chain::ProofChain, - crypto::did::DidParser, - store::{MemoryStore, UcanJwtStore}, - time::now, -}; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - -#[cfg(target_arch = "wasm32")] -wasm_bindgen_test_configure!(run_in_browser); - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_decodes_deep_ucan_chains() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let chain = - ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) - .await - .unwrap(); - - assert_eq!(chain.ucan().audience(), &identities.mallory_did); - assert_eq!( - chain.proofs().get(0).unwrap().ucan().issuer(), - &identities.alice_did - ); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_fails_with_incorrect_chaining() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let parse_token_result = - ProofChain::try_from_token_string(delegated_token.as_str(), None, &mut did_parser, &store) - .await; - - assert!(parse_token_result.is_err()); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_can_be_instantiated_by_cid() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let cid = store.write_token(&delegated_token).await.unwrap(); - - let chain = ProofChain::from_cid(&cid, None, &mut did_parser, &store) - .await - .unwrap(); - - assert_eq!(chain.ucan().audience(), &identities.mallory_did); - assert_eq!( - chain.proofs().get(0).unwrap().ucan().issuer(), - &identities.alice_did - ); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_can_handle_multiple_leaves() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan_1 = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let leaf_ucan_2 = UcanBuilder::default() - .issued_by(&identities.mallory_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.alice_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan_1, None) - .witnessed_by(&leaf_ucan_2, None) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - store - .write_token(&leaf_ucan_1.encode().unwrap()) - .await - .unwrap(); - store - .write_token(&leaf_ucan_2.encode().unwrap()) - .await - .unwrap(); - - ProofChain::try_from_token_string(&delegated_token, None, &mut did_parser, &store) - .await - .unwrap(); -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] -pub async fn it_can_use_a_custom_timestamp_to_validate_a_ucan() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let leaf_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(60) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let delegated_token = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_lifetime(50) - .witnessed_by(&leaf_ucan, None) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap(); - - let mut store = MemoryStore::default(); - - store - .write_token(&leaf_ucan.encode().unwrap()) - .await - .unwrap(); - - let cid = store.write_token(&delegated_token).await.unwrap(); - - let valid_chain = ProofChain::from_cid(&cid, Some(now()), &mut did_parser, &store).await; - - assert!(valid_chain.is_ok()); - - let invalid_chain = ProofChain::from_cid(&cid, Some(now() + 61), &mut did_parser, &store).await; - - assert!(invalid_chain.is_err()); -} diff --git a/ucan/src/tests/crypto.rs b/ucan/src/tests/crypto.rs deleted file mode 100644 index 0b40b155..00000000 --- a/ucan/src/tests/crypto.rs +++ /dev/null @@ -1,27 +0,0 @@ -mod did_from_keypair { - use base64::Engine; - use did_key::{from_existing_key, Ed25519KeyPair, KeyMaterial as _KeyMaterial}; - - use crate::crypto::KeyMaterial; - - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_handles_ed25519_keys() { - let pub_key = base64::engine::general_purpose::STANDARD - .decode("Hv+AVRD2WUjUFOsSNbsmrp9fokuwrUnjBcr92f0kxw4=") - .unwrap(); - let key = Ed25519KeyPair::from_public_key(&pub_key); - let keypair = from_existing_key::(&key.public_key_bytes(), None); - - let expected_did = "did:key:z6MkgYGF3thn8k1Fv4p4dWXKtsXCnLH7q9yw4QgNPULDmDKB"; - let result_did = keypair.get_did().await.unwrap(); - - assert_eq!(expected_did, result_did.as_str()); - } -} diff --git a/ucan/src/tests/fixtures/capabilities/email.rs b/ucan/src/tests/fixtures/capabilities/email.rs deleted file mode 100644 index d8e44524..00000000 --- a/ucan/src/tests/fixtures/capabilities/email.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::capability::{Ability, CapabilitySemantics, Scope}; -use anyhow::{anyhow, Result}; -use url::Url; - -#[derive(Clone, PartialEq)] -pub struct EmailAddress(String); - -impl Scope for EmailAddress { - fn contains(&self, other: &Self) -> bool { - return self.0 == other.0; - } -} - -impl ToString for EmailAddress { - fn to_string(&self) -> String { - format!("mailto:{}", self.0.clone()) - } -} - -impl TryFrom for EmailAddress { - type Error = anyhow::Error; - - fn try_from(value: Url) -> Result { - match value.scheme() { - "mailto" => Ok(EmailAddress(String::from(value.path()))), - _ => Err(anyhow!( - "Could not interpret URI as an email address: {}", - value - )), - } - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] -pub enum EmailAction { - Send, -} - -impl Ability for EmailAction {} - -impl ToString for EmailAction { - fn to_string(&self) -> String { - match self { - EmailAction::Send => "email/send", - } - .into() - } -} - -impl TryFrom for EmailAction { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "email/send" => Ok(EmailAction::Send), - _ => Err(anyhow!("Unrecognized action: {}", value)), - } - } -} - -pub struct EmailSemantics {} - -impl CapabilitySemantics for EmailSemantics {} diff --git a/ucan/src/tests/fixtures/capabilities/mod.rs b/ucan/src/tests/fixtures/capabilities/mod.rs deleted file mode 100644 index ee70eec2..00000000 --- a/ucan/src/tests/fixtures/capabilities/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod email; -mod wnfs; - -pub use email::*; -pub use wnfs::*; diff --git a/ucan/src/tests/fixtures/capabilities/wnfs.rs b/ucan/src/tests/fixtures/capabilities/wnfs.rs deleted file mode 100644 index 4b18a3c6..00000000 --- a/ucan/src/tests/fixtures/capabilities/wnfs.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::capability::{Ability, CapabilitySemantics, Scope}; -use anyhow::{anyhow, Result}; -use url::Url; - -#[derive(Ord, Eq, PartialOrd, PartialEq, Clone)] -pub enum WNFSCapLevel { - Create, - Revise, - SoftDelete, - Overwrite, - SuperUser, -} - -impl Ability for WNFSCapLevel {} - -impl TryFrom for WNFSCapLevel { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - Ok(match value.as_str() { - "wnfs/create" => WNFSCapLevel::Create, - "wnfs/revise" => WNFSCapLevel::Revise, - "wnfs/soft_delete" => WNFSCapLevel::SoftDelete, - "wnfs/overwrite" => WNFSCapLevel::Overwrite, - "wnfs/super_user" => WNFSCapLevel::SuperUser, - _ => return Err(anyhow!("No such WNFS capability level: {}", value)), - }) - } -} - -impl ToString for WNFSCapLevel { - fn to_string(&self) -> String { - match self { - WNFSCapLevel::Create => "wnfs/create", - WNFSCapLevel::Revise => "wnfs/revise", - WNFSCapLevel::SoftDelete => "wnfs/soft_delete", - WNFSCapLevel::Overwrite => "wnfs/overwrite", - WNFSCapLevel::SuperUser => "wnfs/super_user", - } - .into() - } -} - -#[derive(Clone, PartialEq)] -pub struct WNFSScope { - origin: String, - path: String, -} - -impl Scope for WNFSScope { - fn contains(&self, other: &Self) -> bool { - if self.origin != other.origin { - return false; - } - - let self_path_parts = self.path.split('/'); - let mut other_path_parts = other.path.split('/'); - - for part in self_path_parts { - match other_path_parts.nth(0) { - Some(other_part) => { - if part != other_part { - return false; - } - } - None => return false, - } - } - - true - } -} - -impl TryFrom for WNFSScope { - type Error = anyhow::Error; - - fn try_from(value: Url) -> Result { - match (value.scheme(), value.host_str(), value.path()) { - ("wnfs", Some(host), path) => Ok(WNFSScope { - origin: String::from(host), - path: String::from(path), - }), - _ => Err(anyhow!("Cannot interpret URI as WNFS scope: {}", value)), - } - } -} - -impl ToString for WNFSScope { - fn to_string(&self) -> String { - format!("wnfs://{}{}", self.origin, self.path) - } -} - -pub struct WNFSSemantics {} - -impl CapabilitySemantics for WNFSSemantics {} diff --git a/ucan/src/tests/fixtures/crypto.rs b/ucan/src/tests/fixtures/crypto.rs deleted file mode 100644 index c2fcfc98..00000000 --- a/ucan/src/tests/fixtures/crypto.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::crypto::{ - did::{KeyConstructorSlice, ED25519_MAGIC_BYTES}, - KeyMaterial, -}; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use did_key::{from_existing_key, CoreSign, Ed25519KeyPair, Fingerprint, PatchedKeyPair}; - -pub const SUPPORTED_KEYS: &KeyConstructorSlice = &[ - // https://github.com/multiformats/multicodec/blob/e9ecf587558964715054a0afcc01f7ace220952c/table.csv#L94 - (ED25519_MAGIC_BYTES, bytes_to_ed25519_key), -]; - -pub fn bytes_to_ed25519_key(bytes: Vec) -> Result> { - Ok(Box::new(from_existing_key::( - bytes.as_slice(), - None, - ))) -} - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl KeyMaterial for PatchedKeyPair { - fn get_jwt_algorithm_name(&self) -> String { - "EdDSA".into() - } - - async fn get_did(&self) -> Result { - Ok(format!("did:key:{}", self.fingerprint())) - } - - async fn sign(&self, payload: &[u8]) -> Result> { - Ok(CoreSign::sign(self, payload)) - } - - async fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { - CoreSign::verify(self, payload, signature).map_err(|error| anyhow!("{:?}", error)) - } -} diff --git a/ucan/src/tests/fixtures/identities.rs b/ucan/src/tests/fixtures/identities.rs deleted file mode 100644 index df199d6a..00000000 --- a/ucan/src/tests/fixtures/identities.rs +++ /dev/null @@ -1,60 +0,0 @@ -use base64::Engine; -use did_key::{ - from_existing_key, Ed25519KeyPair, Generate, KeyMaterial as _KeyMaterial, PatchedKeyPair, -}; - -use crate::crypto::KeyMaterial; - -pub struct Identities { - pub alice_key: PatchedKeyPair, - pub bob_key: PatchedKeyPair, - pub mallory_key: PatchedKeyPair, - - pub alice_did: String, - pub bob_did: String, - pub mallory_did: String, -} - -/// An adaptation of the fixtures used in the canonical ts-ucan repo -/// See: https://github.com/ucan-wg/ts-ucan/blob/main/tests/fixtures.ts -impl Identities { - pub async fn new() -> Self { - // NOTE: tweetnacl secret keys concat the public keys, so we only care - // about the first 32 bytes - let alice_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("U+bzp2GaFQHso587iSFWPSeCzbSfn/CbNHEz7ilKRZ1UQMmMS7qq4UhTzKn3X9Nj/4xgrwa+UqhMOeo4Ki8JUw==".as_bytes()).unwrap().as_slice()[0..32]); - let alice_keypair = from_existing_key::( - &alice_key.public_key_bytes(), - Some(&alice_key.private_key_bytes()), - ); - let bob_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("G4+QCX1b3a45IzQsQd4gFMMe0UB1UOx9bCsh8uOiKLER69eAvVXvc8P2yc4Iig42Bv7JD2zJxhyFALyTKBHipg==".as_bytes()).unwrap().as_slice()[0..32]); - let bob_keypair = from_existing_key::( - &bob_key.public_key_bytes(), - Some(&bob_key.private_key_bytes()), - ); - let mallory_key = Ed25519KeyPair::from_secret_key(&base64::engine::general_purpose::STANDARD.decode("LR9AL2MYkMARuvmV3MJV8sKvbSOdBtpggFCW8K62oZDR6UViSXdSV/dDcD8S9xVjS61vh62JITx7qmLgfQUSZQ==".as_bytes()).unwrap().as_slice()[0..32]); - let mallory_keypair = from_existing_key::( - &mallory_key.public_key_bytes(), - Some(&mallory_key.private_key_bytes()), - ); - - Identities { - alice_did: alice_keypair.get_did().await.unwrap(), - bob_did: bob_keypair.get_did().await.unwrap(), - mallory_did: mallory_keypair.get_did().await.unwrap(), - - alice_key: alice_keypair, - bob_key: bob_keypair, - mallory_key: mallory_keypair, - } - } - - #[allow(dead_code)] - pub fn name_for(&self, did: String) -> String { - match did { - _ if did == self.alice_did => "alice".into(), - _ if did == self.bob_did => "bob".into(), - _ if did == self.mallory_did => "mallory".into(), - _ => did, - } - } -} diff --git a/ucan/src/tests/fixtures/mod.rs b/ucan/src/tests/fixtures/mod.rs deleted file mode 100644 index 82669dbd..00000000 --- a/ucan/src/tests/fixtures/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod capabilities; -mod crypto; -mod identities; -mod store; - -pub use capabilities::*; -pub use crypto::*; -pub use identities::*; -pub use store::*; diff --git a/ucan/src/tests/fixtures/store.rs b/ucan/src/tests/fixtures/store.rs deleted file mode 100644 index bd158760..00000000 --- a/ucan/src/tests/fixtures/store.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::store::{UcanStore, UcanStoreConditionalSend}; -use anyhow::{anyhow, Result}; -use async_trait::async_trait; -use cid::{ - multihash::{Code, MultihashDigest}, - Cid, -}; -use libipld_core::{ - codec::{Codec, Decode, Encode}, - raw::RawCodec, -}; -use std::{ - collections::HashMap, - io::Cursor, - sync::{Arc, Mutex}, -}; - -#[derive(Clone, Default, Debug)] -pub struct Blake2bMemoryStore { - dags: Arc>>>, -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl UcanStore for Blake2bMemoryStore { - async fn read>(&self, cid: &Cid) -> Result> { - let dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; - - Ok(match dags.get(cid) { - Some(bytes) => Some(T::decode(RawCodec, &mut Cursor::new(bytes))?), - None => None, - }) - } - - async fn write + UcanStoreConditionalSend + core::fmt::Debug>( - &mut self, - token: T, - ) -> Result { - let codec = RawCodec; - let block = codec.encode(&token)?; - let cid = Cid::new_v1(codec.into(), Code::Blake2b256.digest(&block)); - - let mut dags = self.dags.lock().map_err(|_| anyhow!("poisoned mutex!"))?; - dags.insert(cid, block); - - Ok(cid) - } -} diff --git a/ucan/src/tests/helpers.rs b/ucan/src/tests/helpers.rs deleted file mode 100644 index c178a4c0..00000000 --- a/ucan/src/tests/helpers.rs +++ /dev/null @@ -1,56 +0,0 @@ -use super::fixtures::{EmailSemantics, Identities}; -use crate::{builder::UcanBuilder, capability::CapabilitySemantics}; -use anyhow::Result; -use did_key::PatchedKeyPair; -use serde::{de::DeserializeOwned, Serialize}; -use serde_ipld_dagcbor::{from_slice, to_vec}; - -pub fn dag_cbor_roundtrip(data: &T) -> Result -where - T: Serialize + DeserializeOwned, -{ - Ok(from_slice(&to_vec(data)?)?) -} - -pub async fn scaffold_ucan_builder(identities: &Identities) -> Result> { - let email_semantics = EmailSemantics {}; - let send_email_as_bob = email_semantics - .parse("mailto:bob@email.com".into(), "email/send".into(), None) - .unwrap(); - let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into(), None) - .unwrap(); - - let leaf_ucan_alice = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.mallory_did.as_str()) - .with_expiration(1664232146010) - .claiming_capability(&send_email_as_alice) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let leaf_ucan_bob = UcanBuilder::default() - .issued_by(&identities.bob_key) - .for_audience(identities.mallory_did.as_str()) - .with_expiration(1664232146010) - .claiming_capability(&send_email_as_bob) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let builder = UcanBuilder::default() - .issued_by(&identities.mallory_key) - .for_audience(identities.alice_did.as_str()) - .with_expiration(1664232146010) - .witnessed_by(&leaf_ucan_alice, None) - .witnessed_by(&leaf_ucan_bob, None) - .claiming_capability(&send_email_as_alice) - .claiming_capability(&send_email_as_bob); - - Ok(builder) -} diff --git a/ucan/src/tests/mod.rs b/ucan/src/tests/mod.rs deleted file mode 100644 index 1a8c2355..00000000 --- a/ucan/src/tests/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod attenuation; -mod builder; -mod capability; -mod chain; -mod crypto; -pub mod fixtures; -pub mod helpers; -mod ucan; diff --git a/ucan/src/tests/ucan.rs b/ucan/src/tests/ucan.rs deleted file mode 100644 index 5f2b4b12..00000000 --- a/ucan/src/tests/ucan.rs +++ /dev/null @@ -1,235 +0,0 @@ -mod validate { - use crate::{ - builder::UcanBuilder, - capability::CapabilitySemantics, - crypto::did::DidParser, - tests::fixtures::{EmailSemantics, Identities, SUPPORTED_KEYS}, - time::now, - ucan::Ucan, - }; - use anyhow::Result; - - use serde_json::json; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test_configure!(run_in_browser); - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_round_trips_with_encode() { - let identities = Identities::new().await; - let mut did_parser = DidParser::new(SUPPORTED_KEYS); - - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(30) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let encoded_ucan = ucan.encode().unwrap(); - let decoded_ucan = Ucan::try_from(encoded_ucan.as_str()).unwrap(); - - decoded_ucan.validate(None, &mut did_parser).await.unwrap(); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_identifies_a_ucan_that_is_not_active_yet() { - let identities = Identities::new().await; - - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .not_before(now() + 30) - .with_lifetime(30) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - assert!(ucan.is_too_early()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_identifies_a_ucan_that_has_become_active() { - let identities = Identities::new().await; - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .not_before(now() / 1000) - .with_lifetime(30) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - assert!(!ucan.is_too_early()); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_be_serialized_as_json() -> Result<()> { - let identities = Identities::new().await; - - let email_semantics = EmailSemantics {}; - let send_email_as_alice = email_semantics - .parse("mailto:alice@email.com".into(), "email/send".into(), None) - .unwrap(); - - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .not_before(now() / 1000) - .with_lifetime(30) - .with_fact("abc/challenge", json!({ "foo": "bar" })) - .claiming_capability(&send_email_as_alice) - .build()? - .sign() - .await?; - - let ucan_json = serde_json::to_value(ucan.clone())?; - - assert_eq!( - ucan_json, - serde_json::json!({ - "header": { - "alg": "EdDSA", - "typ": "JWT" - }, - "payload": { - "ucv": crate::ucan::UCAN_VERSION, - "iss": ucan.issuer(), - "aud": ucan.audience(), - "exp": ucan.expires_at(), - "nbf": ucan.not_before(), - "cap": { - "mailto:alice@email.com": { - "email/send": [{}] - } - }, - "fct": { - "abc/challenge": { "foo": "bar" } - } - }, - "signed_data": ucan.signed_data(), - "signature": ucan.signature() - }) - ); - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_can_be_serialized_as_json_without_optionals() -> Result<()> { - let identities = Identities::new().await; - let ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .build()? - .sign() - .await?; - - let ucan_json = serde_json::to_value(ucan.clone())?; - - assert_eq!( - ucan_json, - serde_json::json!({ - "header": { - "alg": "EdDSA", - "typ": "JWT" - }, - "payload": { - "ucv": crate::ucan::UCAN_VERSION, - "iss": ucan.issuer(), - "aud": ucan.audience(), - "exp": serde_json::Value::Null, - "cap": {} - }, - "signed_data": ucan.signed_data(), - "signature": ucan.signature() - }) - ); - - Ok(()) - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn it_implements_partial_eq() { - let identities = Identities::new().await; - let ucan_a = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_expiration(10000000) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan_b = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_expiration(10000000) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - let ucan_c = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_expiration(20000000) - .build() - .unwrap() - .sign() - .await - .unwrap(); - - assert!(ucan_a == ucan_b); - assert!(ucan_a != ucan_c); - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] - async fn test_lifetime_ends_after() -> Result<()> { - let identities = Identities::new().await; - let forever_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .build()? - .sign() - .await?; - let early_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(2000) - .build()? - .sign() - .await?; - let later_ucan = UcanBuilder::default() - .issued_by(&identities.alice_key) - .for_audience(identities.bob_did.as_str()) - .with_lifetime(4000) - .build()? - .sign() - .await?; - - assert_eq!(*forever_ucan.expires_at(), None); - assert!(forever_ucan.lifetime_ends_after(&early_ucan)); - assert!(!early_ucan.lifetime_ends_after(&forever_ucan)); - assert!(later_ucan.lifetime_ends_after(&early_ucan)); - - Ok(()) - } -} diff --git a/ucan/src/time.rs b/ucan/src/time.rs deleted file mode 100644 index af8b5b0b..00000000 --- a/ucan/src/time.rs +++ /dev/null @@ -1,8 +0,0 @@ -use instant::SystemTime; - -pub fn now() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() -} diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs deleted file mode 100644 index beaf8d23..00000000 --- a/ucan/src/ucan.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::{ - capability::Capabilities, - crypto::did::DidParser, - serde::{Base64Encode, DagJson}, - time::now, -}; -use anyhow::{anyhow, Result}; -use base64::Engine; -use cid::{ - multihash::{Code, MultihashDigest}, - Cid, -}; -use libipld_core::{codec::Codec, raw::RawCodec}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{collections::BTreeMap, convert::TryFrom, str::FromStr}; - -pub const UCAN_VERSION: &str = "0.10.0-canary"; - -pub type FactsMap = BTreeMap; - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct UcanHeader { - pub alg: String, - pub typ: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct UcanPayload { - pub ucv: String, - pub iss: String, - pub aud: String, - pub exp: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub nbf: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub nnc: Option, - pub cap: Capabilities, - #[serde(skip_serializing_if = "Option::is_none")] - pub fct: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub prf: Option>, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] -pub struct Ucan { - header: UcanHeader, - payload: UcanPayload, - signed_data: Vec, - signature: Vec, -} - -impl Ucan { - pub fn new( - header: UcanHeader, - payload: UcanPayload, - signed_data: Vec, - signature: Vec, - ) -> Self { - Ucan { - signed_data, - header, - payload, - signature, - } - } - - /// Validate the UCAN's signature and timestamps - pub async fn validate<'a>( - &self, - now_time: Option, - did_parser: &mut DidParser, - ) -> Result<()> { - if self.is_expired(now_time) { - return Err(anyhow!("Expired")); - } - - if self.is_too_early() { - return Err(anyhow!("Not active yet (too early)")); - } - - self.check_signature(did_parser).await - } - - /// Validate that the signed data was signed by the stated issuer - pub async fn check_signature<'a>(&self, did_parser: &mut DidParser) -> Result<()> { - let key = did_parser.parse(&self.payload.iss)?; - key.verify(&self.signed_data, &self.signature).await - } - - /// Produce a base64-encoded serialization of the UCAN suitable for - /// transferring in a header field - pub fn encode(&self) -> Result { - let header = self.header.jwt_base64_encode()?; - let payload = self.payload.jwt_base64_encode()?; - let signature = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.signature.as_slice()); - - Ok(format!("{header}.{payload}.{signature}")) - } - - /// Returns true if the UCAN has past its expiration date - pub fn is_expired(&self, now_time: Option) -> bool { - if let Some(exp) = self.payload.exp { - exp < now_time.unwrap_or_else(now) - } else { - false - } - } - - /// Raw bytes of signed data for this UCAN - pub fn signed_data(&self) -> &[u8] { - &self.signed_data - } - - pub fn signature(&self) -> &[u8] { - &self.signature - } - - /// Returns true if the not-before ("nbf") time is still in the future - pub fn is_too_early(&self) -> bool { - match self.payload.nbf { - Some(nbf) => nbf > now(), - None => false, - } - } - - /// Returns true if this UCAN's lifetime begins no later than the other - /// Note that if a UCAN specifies an NBF but the other does not, the - /// other has an unbounded start time and this function will return - /// false. - pub fn lifetime_begins_before(&self, other: &Ucan) -> bool { - match (self.payload.nbf, other.payload.nbf) { - (Some(nbf), Some(other_nbf)) => nbf <= other_nbf, - (Some(_), None) => false, - _ => true, - } - } - - /// Returns true if this UCAN expires no earlier than the other - pub fn lifetime_ends_after(&self, other: &Ucan) -> bool { - match (self.payload.exp, other.payload.exp) { - (Some(exp), Some(other_exp)) => exp >= other_exp, - (Some(_), None) => false, - (None, _) => true, - } - } - - /// Returns true if this UCAN's lifetime fully encompasses the other - pub fn lifetime_encompasses(&self, other: &Ucan) -> bool { - self.lifetime_begins_before(other) && self.lifetime_ends_after(other) - } - - pub fn algorithm(&self) -> &str { - &self.header.alg - } - - pub fn issuer(&self) -> &str { - &self.payload.iss - } - - pub fn audience(&self) -> &str { - &self.payload.aud - } - - pub fn proofs(&self) -> &Option> { - &self.payload.prf - } - - pub fn expires_at(&self) -> &Option { - &self.payload.exp - } - - pub fn not_before(&self) -> &Option { - &self.payload.nbf - } - - pub fn nonce(&self) -> &Option { - &self.payload.nnc - } - - #[deprecated(since = "0.4.0", note = "use `capabilities()`")] - pub fn attenuation(&self) -> &Capabilities { - self.capabilities() - } - - pub fn capabilities(&self) -> &Capabilities { - &self.payload.cap - } - - pub fn facts(&self) -> &Option { - &self.payload.fct - } - - pub fn version(&self) -> &str { - &self.payload.ucv - } - - pub fn to_cid(&self, hasher: Code) -> Result { - let codec = RawCodec; - let token = self.encode()?; - let encoded = codec.encode(token.as_bytes())?; - Ok(Cid::new_v1(codec.into(), hasher.digest(&encoded))) - } -} - -/// Deserialize an encoded UCAN token string reference into a UCAN -impl<'a> TryFrom<&'a str> for Ucan { - type Error = anyhow::Error; - - fn try_from(ucan_token: &str) -> Result { - Ucan::from_str(ucan_token) - } -} - -/// Deserialize an encoded UCAN token string into a UCAN -impl TryFrom for Ucan { - type Error = anyhow::Error; - - fn try_from(ucan_token: String) -> Result { - Ucan::from_str(ucan_token.as_str()) - } -} - -/// Deserialize an encoded UCAN token string reference into a UCAN -impl FromStr for Ucan { - type Err = anyhow::Error; - - fn from_str(ucan_token: &str) -> Result { - // better to create multiple iterators than collect, or clone. - let signed_data = ucan_token - .split('.') - .take(2) - .map(String::from) - .reduce(|l, r| format!("{l}.{r}")) - .ok_or_else(|| anyhow!("Could not parse signed data from token string"))?; - - let mut parts = ucan_token.split('.').map(|str| { - base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(str) - .map_err(|error| anyhow!(error)) - }); - - let header = parts - .next() - .ok_or_else(|| anyhow!("Missing UCAN header in token part"))? - .map(|decoded| UcanHeader::from_dag_json(&decoded)) - .map_err(|e| e.context("Could not decode UCAN header base64"))? - .map_err(|e| e.context("Could not parse UCAN header JSON"))?; - - let payload = parts - .next() - .ok_or_else(|| anyhow!("Missing UCAN payload in token part"))? - .map(|decoded| UcanPayload::from_dag_json(&decoded)) - .map_err(|e| e.context("Could not decode UCAN payload base64"))? - .map_err(|e| e.context("Could not parse UCAN payload JSON"))?; - - let signature = parts - .next() - .ok_or_else(|| anyhow!("Missing UCAN signature in token part"))? - .map_err(|e| e.context("Could not parse UCAN signature base64"))?; - - Ok(Ucan::new( - header, - payload, - signed_data.as_bytes().into(), - signature, - )) - } -}
- - rs-ucan Logo - - -

ucan-key-support

- -

- - Crate Information - - - Code Coverage - - - Build Status - - - License - - - Docs - - - Discord - -

-